diff --git a/Sources/Screens/File/FileToolbar.swift b/Sources/Screens/File/FileToolbar.swift index 5525172..07186c0 100644 --- a/Sources/Screens/File/FileToolbar.swift +++ b/Sources/Screens/File/FileToolbar.swift @@ -37,8 +37,10 @@ struct FileToolbar: ViewModifier { ToolbarItem(placement: .topBarTrailing) { tasksButton } - ToolbarItem(placement: .topBarLeading) { - uploadMenu + if let file = fileStore.file, file.permission.ge(.editor) { + ToolbarItem(placement: .topBarLeading) { + uploadMenu + } } ToolbarItem(placement: .topBarLeading) { if fileStore.entitiesIsLoading { diff --git a/Sources/Screens/Group/GroupMemberList.swift b/Sources/Screens/Group/GroupMemberList.swift index e5443de..94640ff 100644 --- a/Sources/Screens/Group/GroupMemberList.swift +++ b/Sources/Screens/Group/GroupMemberList.swift @@ -65,11 +65,13 @@ struct GroupMemberList: View, ViewDataProvider, LoadStateProvider, TimerLifecycl .navigationBarTitleDisplayMode(.inline) .navigationTitle("Members") .toolbar { - ToolbarItem(placement: .topBarLeading) { - Button { - addMemberIsPresentable = true - } label: { - Image(systemName: "plus") + if let group = groupStore.current, group.permission.ge(.owner) { + ToolbarItem(placement: .topBarLeading) { + Button { + addMemberIsPresentable = true + } label: { + Image(systemName: "plus") + } } } } diff --git a/Sources/Screens/Group/GroupOverview.swift b/Sources/Screens/Group/GroupOverview.swift index 803dbae..72ce96a 100644 --- a/Sources/Screens/Group/GroupOverview.swift +++ b/Sources/Screens/Group/GroupOverview.swift @@ -24,23 +24,21 @@ struct GroupOverview: View { var body: some View { VStack { - if let current = groupStore.current { - VStack { - VOAvatar(name: current.name, size: 100) - .padding() - Form { - NavigationLink { - GroupMemberList(groupStore: groupStore) - } label: { - Label("Members", systemImage: "person.2") - } - NavigationLink { - GroupSettings(groupStore: groupStore) { - dismiss() - } - } label: { - Label("Settings", systemImage: "gear") + VStack { + VOAvatar(name: group.name, size: 100) + .padding() + Form { + NavigationLink { + GroupMemberList(groupStore: groupStore) + } label: { + Label("Members", systemImage: "person.2") + } + NavigationLink { + GroupSettings(groupStore: groupStore) { + dismiss() } + } label: { + Label("Settings", systemImage: "gear") } } } diff --git a/Sources/Screens/Group/GroupSettings.swift b/Sources/Screens/Group/GroupSettings.swift index 3bb33f6..ad20fa6 100644 --- a/Sources/Screens/Group/GroupSettings.swift +++ b/Sources/Screens/Group/GroupSettings.swift @@ -26,7 +26,7 @@ struct GroupSettings: View, ErrorPresentable { var body: some View { Group { - if let group = groupStore.current { + if let current = groupStore.current { Form { Section(header: VOSectionHeader("Basics")) { NavigationLink { @@ -40,27 +40,29 @@ struct GroupSettings: View, ErrorPresentable { HStack { Text("Name") Spacer() - Text(group.name) + Text(current.name) .lineLimit(1) .truncationMode(.tail) .foregroundStyle(.secondary) } } - .disabled(isDeleting) + .disabled(isDeleting || current.permission.lt(.editor)) } - Section(header: VOSectionHeader("Advanced")) { - Button(role: .destructive) { - deleteConfirmationIsPresented = true - } label: { - VOFormButtonLabel("Delete Group", isLoading: isDeleting) - } - .disabled(isDeleting) - .confirmationDialog("Delete Group", isPresented: $deleteConfirmationIsPresented) { - Button("Delete Permanently", role: .destructive) { - performDelete() + if current.permission.ge(.owner) { + Section(header: VOSectionHeader("Advanced")) { + Button(role: .destructive) { + deleteConfirmationIsPresented = true + } label: { + VOFormButtonLabel("Delete Group", isLoading: isDeleting) + } + .disabled(isDeleting) + .confirmationDialog("Delete Group", isPresented: $deleteConfirmationIsPresented) { + Button("Delete Permanently", role: .destructive) { + performDelete() + } + } message: { + Text("Are you sure you want to delete this group?") } - } message: { - Text("Are you sure you want to delete this group?") } } } diff --git a/Sources/Screens/Invitation/InvitationStore.swift b/Sources/Screens/Invitation/InvitationStore.swift index 6914f8e..941c17e 100644 --- a/Sources/Screens/Invitation/InvitationStore.swift +++ b/Sources/Screens/Invitation/InvitationStore.swift @@ -48,9 +48,11 @@ class InvitationStore: ObservableObject { private func fetchList(page: Int = 1, size: Int = Constants.pageSize) async throws -> VOInvitation.List? { if let organizationID { - try await invitationClient?.fetchOutgoingList(.init(organizationID: organizationID, page: page, size: size)) + try await invitationClient?.fetchOutgoingList( + .init(organizationID: organizationID, page: page, size: size, sortBy: .dateCreated, sortOrder: .desc)) } else { - try await invitationClient?.fetchIncomingList(.init(page: page, size: size)) + try await invitationClient?.fetchIncomingList( + .init(page: page, size: size, sortBy: .dateCreated, sortOrder: .desc)) } } diff --git a/Sources/Screens/Organization/OrganizationOverview.swift b/Sources/Screens/Organization/OrganizationOverview.swift index 7fa7a29..2bdcc09 100644 --- a/Sources/Screens/Organization/OrganizationOverview.swift +++ b/Sources/Screens/Organization/OrganizationOverview.swift @@ -24,28 +24,28 @@ struct OrganizationOverview: View { var body: some View { VStack { - if let current = organizationStore.current { - VStack { - VOAvatar(name: current.name, size: 100) - .padding() - Form { - NavigationLink { - OrganizationMemberList(organizationStore: organizationStore) - } label: { - Label("Members", systemImage: "person.2") - } + VStack { + VOAvatar(name: organization.name, size: 100) + .padding() + Form { + NavigationLink { + OrganizationMemberList(organizationStore: organizationStore) + } label: { + Label("Members", systemImage: "person.2") + } + if organization.permission.ge(.owner) { NavigationLink { InvitationOutgoingList(organization.id) } label: { Label("Invitations", systemImage: "paperplane") } - NavigationLink { - OrganizationSettings(organizationStore: organizationStore) { - dismiss() - } - } label: { - Label("Settings", systemImage: "gear") + } + NavigationLink { + OrganizationSettings(organizationStore: organizationStore) { + dismiss() } + } label: { + Label("Settings", systemImage: "gear") } } } diff --git a/Sources/Screens/Organization/OrganizationSettings.swift b/Sources/Screens/Organization/OrganizationSettings.swift index 86fce84..48b338d 100644 --- a/Sources/Screens/Organization/OrganizationSettings.swift +++ b/Sources/Screens/Organization/OrganizationSettings.swift @@ -15,7 +15,9 @@ struct OrganizationSettings: View, ErrorPresentable { @EnvironmentObject private var tokenStore: TokenStore @ObservedObject private var organizationStore: OrganizationStore @Environment(\.dismiss) private var dismiss + @State private var leaveConfirmationIsPresented = false @State private var deleteConfirmationIsPresented = false + @State private var isLeaving = false @State private var isDeleting = false private var onCompletion: (() -> Void)? @@ -26,7 +28,7 @@ struct OrganizationSettings: View, ErrorPresentable { var body: some View { Group { - if let organization = organizationStore.current { + if let current = organizationStore.current { Form { Section(header: VOSectionHeader("Basics")) { NavigationLink { @@ -42,27 +44,44 @@ struct OrganizationSettings: View, ErrorPresentable { HStack { Text("Name") Spacer() - Text(organization.name) + Text(current.name) .lineLimit(1) .truncationMode(.tail) .foregroundStyle(.secondary) } } - .disabled(isDeleting) + .disabled(isDeleting || current.permission.lt(.editor)) } - Section(header: VOSectionHeader("Advanced")) { + Section(header: VOSectionHeader("Membership")) { Button(role: .destructive) { - deleteConfirmationIsPresented = true + leaveConfirmationIsPresented = true } label: { - VOFormButtonLabel("Delete Organization", isLoading: isDeleting) + VOFormButtonLabel("Leave Organization", isLoading: isLeaving) } - .disabled(isDeleting) - .confirmationDialog("Delete Organization", isPresented: $deleteConfirmationIsPresented) { - Button("Delete Permanently", role: .destructive) { - performDelete() + .disabled(isLeaving) + .confirmationDialog("Leave Organization", isPresented: $leaveConfirmationIsPresented) { + Button("Leave", role: .destructive) { + performLeave() } } message: { - Text("Are you sure you want to delete this organization?") + Text("Are you sure you want to leave this organization?") + } + } + if current.permission.ge(.owner) { + Section(header: VOSectionHeader("Advanced")) { + Button(role: .destructive) { + deleteConfirmationIsPresented = true + } label: { + VOFormButtonLabel("Delete Organization", isLoading: isDeleting) + } + .disabled(isDeleting) + .confirmationDialog("Delete Organization", isPresented: $deleteConfirmationIsPresented) { + Button("Delete Permanently", role: .destructive) { + performDelete() + } + } message: { + Text("Are you sure you want to delete this organization?") + } } } } @@ -73,8 +92,27 @@ struct OrganizationSettings: View, ErrorPresentable { .voErrorSheet(isPresented: $errorIsPresented, message: errorMessage) } + private func performLeave() { + withErrorHandling { + try await organizationStore.leave() + return true + } before: { + isLeaving = true + } success: { + dismiss() + if let current = organizationStore.current { + reflectLeaveInStore(current.id) + } + onCompletion?() + } failure: { message in + errorMessage = message + errorIsPresented = true + } anyways: { + isLeaving = false + } + } + private func performDelete() { - let current = organizationStore.current withErrorHandling { try await organizationStore.delete() return true @@ -82,7 +120,7 @@ struct OrganizationSettings: View, ErrorPresentable { isDeleting = true } success: { dismiss() - if let current { + if let current = organizationStore.current { reflectDeleteInStore(current.id) } onCompletion?() @@ -94,6 +132,10 @@ struct OrganizationSettings: View, ErrorPresentable { } } + private func reflectLeaveInStore(_ id: String) { + organizationStore.entities?.removeAll(where: { $0.id == id }) + } + private func reflectDeleteInStore(_ id: String) { organizationStore.entities?.removeAll(where: { $0.id == id }) } diff --git a/Sources/Screens/Organization/OrganizationStore.swift b/Sources/Screens/Organization/OrganizationStore.swift index ff746d4..8835338 100644 --- a/Sources/Screens/Organization/OrganizationStore.swift +++ b/Sources/Screens/Organization/OrganizationStore.swift @@ -111,6 +111,11 @@ class OrganizationStore: ObservableObject { try await organizationClient?.patchName(id, options: .init(name: name)) } + func leave() async throws { + guard let current else { return } + try await organizationClient?.leave(current.id) + } + func delete() async throws { guard let current else { return } try await organizationClient?.delete(current.id) diff --git a/Sources/Screens/Workspace/WorkspaceOverview.swift b/Sources/Screens/Workspace/WorkspaceOverview.swift index ad0276e..30a811c 100644 --- a/Sources/Screens/Workspace/WorkspaceOverview.swift +++ b/Sources/Screens/Workspace/WorkspaceOverview.swift @@ -29,26 +29,24 @@ struct WorkspaceOverview: View, ViewDataProvider, LoadStateProvider { } else if let error { VOErrorMessage(error) } else { - if let current = workspaceStore.current { - VStack { - VOAvatar(name: workspace.name, size: 100) - .padding() - Form { - NavigationLink { - if let root = workspaceStore.root { - FileOverview(root, workspaceStore: workspaceStore) - .navigationTitle(current.name) - } - } label: { - Label("Files", systemImage: "folder") + VStack { + VOAvatar(name: workspace.name, size: 100) + .padding() + Form { + NavigationLink { + if let root = workspaceStore.root { + FileOverview(root, workspaceStore: workspaceStore) + .navigationTitle(workspace.name) } - NavigationLink { - WorkspaceSettings(workspaceStore: workspaceStore) { - dismiss() - } - } label: { - Label("Settings", systemImage: "gear") + } label: { + Label("Files", systemImage: "folder") + } + NavigationLink { + WorkspaceSettings(workspaceStore: workspaceStore) { + dismiss() } + } label: { + Label("Settings", systemImage: "gear") } } } diff --git a/Sources/Screens/Workspace/WorkspaceSettings.swift b/Sources/Screens/Workspace/WorkspaceSettings.swift index af04ad9..b20882e 100644 --- a/Sources/Screens/Workspace/WorkspaceSettings.swift +++ b/Sources/Screens/Workspace/WorkspaceSettings.swift @@ -53,7 +53,7 @@ struct WorkspaceSettings: View, ViewDataProvider, LoadStateProvider, ErrorPresen .foregroundStyle(.secondary) } } - .disabled(isDeleting) + .disabled(isDeleting || current.permission.lt(.owner)) } Section(header: VOSectionHeader("Basics")) { NavigationLink { @@ -75,21 +75,23 @@ struct WorkspaceSettings: View, ViewDataProvider, LoadStateProvider, ErrorPresen .foregroundStyle(.secondary) } } - .disabled(isDeleting) + .disabled(isDeleting || current.permission.lt(.editor)) } - Section(header: VOSectionHeader("Advanced")) { - Button(role: .destructive) { - deleteConfirmationIsPresentable = true - } label: { - VOFormButtonLabel("Delete Workspace", isLoading: isDeleting) - } - .disabled(isDeleting) - .confirmationDialog("Delete Workspace", isPresented: $deleteConfirmationIsPresentable) { - Button("Delete Permanently", role: .destructive) { - performDelete() + if current.permission.ge(.owner) { + Section(header: VOSectionHeader("Advanced")) { + Button(role: .destructive) { + deleteConfirmationIsPresentable = true + } label: { + VOFormButtonLabel("Delete Workspace", isLoading: isDeleting) + } + .disabled(isDeleting) + .confirmationDialog("Delete Workspace", isPresented: $deleteConfirmationIsPresentable) { + Button("Delete Permanently", role: .destructive) { + performDelete() + } + } message: { + Text("Are you sure you want to delete this workspace?") } - } message: { - Text("Are you sure you want to delete this workspace?") } } } diff --git a/Voltaserve.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Voltaserve.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 39fe173..a9545ac 100644 --- a/Voltaserve.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Voltaserve.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -16,7 +16,7 @@ "location" : "https://github.com/kouprlabs/voltaserve-swift.git", "state" : { "branch" : "main", - "revision" : "8e7eca9e8cdc04c42cff9cd05ac41f1bfd61f881" + "revision" : "c3e98454a3ee6a71f1d4021926f4623460027c55" } } ],