a native ios client for gitbay

client ios swift

https://gitbay.org

gitbay/Views/Orgs/OrgView.swift

152 lines · 5696 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                        GBNotice(error, .gbWarn)
 24                    }
 25                }
 26                membersSection(loaded.members)
 27                teamsSection(loaded.teams)
 28            }
 29        }
 30        .overlay { LoadStateOverlay(state: model.state) }
 31        .navigationTitle(model.orgName)
 32        .navigationBarTitleDisplayMode(.inline)
 33        .task { await model.load() }
 34        .refreshable { await model.load() }
 35        .confirmationDialog(
 36            "Remove \(removingMember?.user ?? "") from \(model.orgName)?",
 37            isPresented: Binding(
 38                get: { removingMember != nil },
 39                set: { if !$0 { removingMember = nil } }
 40            )
 41        ) {
 42            Button("Remove", role: .destructive) {
 43                if let member = removingMember {
 44                    Task { await model.removeMember(member) }
 45                }
 46                removingMember = nil
 47            }
 48            Button("Cancel", role: .cancel) {}
 49        }
 50        .confirmationDialog(
 51            "Delete team \(deletingTeam ?? "")? Its repo grants go with it.",
 52            isPresented: Binding(
 53                get: { deletingTeam != nil },
 54                set: { if !$0 { deletingTeam = nil } }
 55            )
 56        ) {
 57            Button("Delete", role: .destructive) {
 58                if let team = deletingTeam {
 59                    Task { await model.deleteTeam(team) }
 60                }
 61                deletingTeam = nil
 62            }
 63            Button("Cancel", role: .cancel) {}
 64        }
 65    }
 66
 67    private func membersSection(_ members: [OrgMember]) -> some View {
 68        Section("Members") {
 69            ForEach(members) { member in
 70                HStack {
 71                    NavigationLink(value: RepoRoute.profile(member.user)) {
 72                        Text(member.user)
 73                            .font(.gbSans(.subheadline))
 74                    }
 75                    Spacer()
 76                    Menu {
 77                        Button("Admin") { Task { await model.setRole(member, role: "admin") } }
 78                        Button("Member") { Task { await model.setRole(member, role: "member") } }
 79                    } label: {
 80                        GBChip(member.role, .secondary)
 81                    }
 82                    .disabled(model.working)
 83                }
 84                .swipeActions {
 85                    Button("Remove", role: .destructive) {
 86                        removingMember = member
 87                    }
 88                }
 89            }
 90            HStack {
 91                TextField("Add user", text: $newMember)
 92                    .autocorrectionDisabled()
 93                    .textInputAutocapitalization(.never)
 94                    .accessibilityIdentifier("org-add-member")
 95                Picker("", selection: $newMemberRole) {
 96                    Text("member").tag("member")
 97                    Text("admin").tag("admin")
 98                }
 99                .labelsHidden()
100                .fixedSize()
101                Button {
102                    let user = newMember.trimmingCharacters(in: .whitespaces)
103                    newMember = ""
104                    Task { await model.addMember(user, role: newMemberRole) }
105                } label: {
106                    Image(systemName: "plus.circle.fill")
107                }
108                .disabled(newMember.trimmingCharacters(in: .whitespaces).isEmpty || model.working)
109            }
110        }
111    }
112
113    private func teamsSection(_ teams: [String]) -> some View {
114        Section {
115            ForEach(teams, id: \.self) { team in
116                NavigationLink(value: OrgRoute.team(org: model.orgName, team: team)) {
117                    Label(team, systemImage: "person.3")
118                        .font(.gbSans(.subheadline))
119                }
120                .swipeActions {
121                    Button("Delete", role: .destructive) {
122                        deletingTeam = team
123                    }
124                }
125            }
126            HStack {
127                TextField("New team", text: $newTeam)
128                    .autocorrectionDisabled()
129                    .textInputAutocapitalization(.never)
130                    .accessibilityIdentifier("org-new-team")
131                Button {
132                    let team = newTeam.trimmingCharacters(in: .whitespaces)
133                    newTeam = ""
134                    Task { await model.createTeam(team) }
135                } label: {
136                    Image(systemName: "plus.circle.fill")
137                }
138                .disabled(newTeam.trimmingCharacters(in: .whitespaces).isEmpty || model.working)
139                .accessibilityIdentifier("org-new-team-submit")
140            }
141        } header: {
142            Text("Teams")
143        } footer: {
144            Text("Plain membership already implies a role on every org repo; teams grant more.")
145        }
146    }
147}
148
149nonisolated enum OrgRoute: Hashable {
150    case org(String)
151    case team(org: String, team: String)
152}