gitbay/Views/Orgs/OrgView.swift

5411753023a5e2958f17b523903abce380941107
gitbay-ios/gitbay/Views/Orgs/OrgView.swift history · blame · raw

158 lines · 5998 bytes

  1import SwiftUI
  2
  3/// An organization's members and teams. Reads need membership — the
  4/// server's refusal explains itself to outsiders.
  5struct OrgView: View {
  6
  7    @State private var model: OrgViewModel
  8    @State private var newMember = ""
  9    @State private var newMemberRole = "member"
 10    @State private var newTeam = ""
 11    @State private var removingMember: OrgMember?
 12    @State private var deletingTeam: String?
 13
 14    init(client: GitbayClient, org: String) {
 15        _model = State(initialValue: OrgViewModel(client: client, orgName: org))
 16    }
 17
 18    var body: some View {
 19        List {
 20            if let loaded = model.state.value {
 21                if let error = model.actionError {
 22                    Section {
 23                        Label(error, systemImage: "hand.raised")
 24                            .foregroundStyle(.orange)
 25                            .font(.subheadline)
 26                    }
 27                }
 28                membersSection(loaded.members)
 29                teamsSection(loaded.teams)
 30            }
 31        }
 32        .overlay { LoadStateOverlay(state: model.state) }
 33        .navigationTitle(model.orgName)
 34        .navigationBarTitleDisplayMode(.inline)
 35        .task { await model.load() }
 36        .refreshable { await model.load() }
 37        .confirmationDialog(
 38            "Remove \(removingMember?.user ?? "") from \(model.orgName)?",
 39            isPresented: Binding(
 40                get: { removingMember != nil },
 41                set: { if !$0 { removingMember = nil } }
 42            )
 43        ) {
 44            Button("Remove", role: .destructive) {
 45                if let member = removingMember {
 46                    Task { await model.removeMember(member) }
 47                }
 48                removingMember = nil
 49            }
 50            Button("Cancel", role: .cancel) {}
 51        }
 52        .confirmationDialog(
 53            "Delete team \(deletingTeam ?? "")? Its repo grants go with it.",
 54            isPresented: Binding(
 55                get: { deletingTeam != nil },
 56                set: { if !$0 { deletingTeam = nil } }
 57            )
 58        ) {
 59            Button("Delete", role: .destructive) {
 60                if let team = deletingTeam {
 61                    Task { await model.deleteTeam(team) }
 62                }
 63                deletingTeam = nil
 64            }
 65            Button("Cancel", role: .cancel) {}
 66        }
 67    }
 68
 69    private func membersSection(_ members: [OrgMember]) -> some View {
 70        Section("Members") {
 71            ForEach(members) { member in
 72                HStack {
 73                    NavigationLink(value: RepoRoute.profile(member.user)) {
 74                        Text(member.user)
 75                            .font(.subheadline)
 76                    }
 77                    Spacer()
 78                    Menu {
 79                        Button("Admin") { Task { await model.setRole(member, role: "admin") } }
 80                        Button("Member") { Task { await model.setRole(member, role: "member") } }
 81                    } label: {
 82                        Text(member.role)
 83                            .font(.caption)
 84                            .padding(.horizontal, 6)
 85                            .padding(.vertical, 2)
 86                            .background(.quaternary, in: Capsule())
 87                    }
 88                    .disabled(model.working)
 89                }
 90                .swipeActions {
 91                    Button("Remove", role: .destructive) {
 92                        removingMember = member
 93                    }
 94                }
 95            }
 96            HStack {
 97                TextField("Add user", text: $newMember)
 98                    .autocorrectionDisabled()
 99                    .textInputAutocapitalization(.never)
100                    .accessibilityIdentifier("org-add-member")
101                Picker("", selection: $newMemberRole) {
102                    Text("member").tag("member")
103                    Text("admin").tag("admin")
104                }
105                .labelsHidden()
106                .fixedSize()
107                Button {
108                    let user = newMember.trimmingCharacters(in: .whitespaces)
109                    newMember = ""
110                    Task { await model.addMember(user, role: newMemberRole) }
111                } label: {
112                    Image(systemName: "plus.circle.fill")
113                }
114                .disabled(newMember.trimmingCharacters(in: .whitespaces).isEmpty || model.working)
115            }
116        }
117    }
118
119    private func teamsSection(_ teams: [String]) -> some View {
120        Section {
121            ForEach(teams, id: \.self) { team in
122                NavigationLink(value: OrgRoute.team(org: model.orgName, team: team)) {
123                    Label(team, systemImage: "person.3")
124                        .font(.subheadline)
125                }
126                .swipeActions {
127                    Button("Delete", role: .destructive) {
128                        deletingTeam = team
129                    }
130                }
131            }
132            HStack {
133                TextField("New team", text: $newTeam)
134                    .autocorrectionDisabled()
135                    .textInputAutocapitalization(.never)
136                    .accessibilityIdentifier("org-new-team")
137                Button {
138                    let team = newTeam.trimmingCharacters(in: .whitespaces)
139                    newTeam = ""
140                    Task { await model.createTeam(team) }
141                } label: {
142                    Image(systemName: "plus.circle.fill")
143                }
144                .disabled(newTeam.trimmingCharacters(in: .whitespaces).isEmpty || model.working)
145                .accessibilityIdentifier("org-new-team-submit")
146            }
147        } header: {
148            Text("Teams")
149        } footer: {
150            Text("Plain membership already implies a role on every org repo; teams grant more.")
151        }
152    }
153}
154
155nonisolated enum OrgRoute: Hashable {
156    case org(String)
157    case team(org: String, team: String)
158}