gitbay/Views/Orgs/TeamView.swift

36835f99e6d463f3e18c5c3fc3562bb92513f52b
gitbay-ios/gitbay/Views/Orgs/TeamView.swift history · blame · raw

130 lines · 5295 bytes

1 symbol in this file
  1import SwiftUI
  2
  3/// One team: its members and its repo grants.
  4struct TeamView: View {
  5
  6    @State private var model: TeamViewModel
  7    @State private var newMember = ""
  8    @State private var grantRepo = ""
  9    @State private var grantRole = "read"
 10    @State private var removingMember: String?
 11    @State private var revokingGrant: TeamViewModel.Grant?
 12
 13    init(client: GitbayClient, org: String, team: String) {
 14        _model = State(initialValue: TeamViewModel(client: client, orgName: org, teamName: team))
 15    }
 16
 17    var body: some View {
 18        List {
 19            if let loaded = model.state.value {
 20                if let error = model.actionError {
 21                    Section {
 22                        GBNotice(error, .gbWarn)
 23                    }
 24                }
 25
 26                Section("Members") {
 27                    ForEach(loaded.members ?? [], id: \.self) { user in
 28                        Text(user)
 29                            .font(.gbSans(.subheadline))
 30                            .swipeActions {
 31                                Button("Remove", role: .destructive) {
 32                                    removingMember = user
 33                                }
 34                            }
 35                    }
 36                    HStack {
 37                        TextField("Add org member", text: $newMember)
 38                            .autocorrectionDisabled()
 39                            .textInputAutocapitalization(.never)
 40                        Button {
 41                            let user = newMember.trimmingCharacters(in: .whitespaces)
 42                            newMember = ""
 43                            Task { await model.addMember(user) }
 44                        } label: {
 45                            Image(systemName: "plus.circle.fill")
 46                        }
 47                        .accessibilityLabel("Add member")
 48                        .disabled(newMember.trimmingCharacters(in: .whitespaces).isEmpty
 49                            || model.working)
 50                    }
 51                }
 52
 53                Section {
 54                    ForEach(loaded.grants ?? []) { grant in
 55                        HStack {
 56                            Text(grant.repo)
 57                                .font(.gbSans(.subheadline))
 58                                .lineLimit(1)
 59                            Spacer()
 60                            GBChip(grant.role, .secondary)
 61                        }
 62                        .swipeActions {
 63                            Button("Revoke", role: .destructive) {
 64                                revokingGrant = grant
 65                            }
 66                        }
 67                    }
 68                    VStack(spacing: 8) {
 69                        TextField("org/repo", text: $grantRepo)
 70                            .autocorrectionDisabled()
 71                            .textInputAutocapitalization(.never)
 72                        HStack {
 73                            Picker("Role", selection: $grantRole) {
 74                                Text("read").tag("read")
 75                                Text("write").tag("write")
 76                                Text("admin").tag("admin")
 77                            }
 78                            .pickerStyle(.segmented)
 79                            Button("Grant") {
 80                                let repo = grantRepo.trimmingCharacters(in: .whitespaces)
 81                                grantRepo = ""
 82                                Task { await model.grant(repo: repo, role: grantRole) }
 83                            }
 84                            .disabled(!grantRepo.contains("/") || model.working)
 85                        }
 86                    }
 87                } header: {
 88                    Text("Repository grants")
 89                } footer: {
 90                    Text("A grant gives every team member the role on that org repository.")
 91                }
 92            }
 93        }
 94        .overlay { LoadStateOverlay(state: model.state) }
 95        .navigationTitle(model.teamName)
 96        .navigationBarTitleDisplayMode(.inline)
 97        .task { await model.load() }
 98        .confirmationDialog(
 99            "Remove \(removingMember ?? "") from \(model.teamName)?",
100            isPresented: Binding(
101                get: { removingMember != nil },
102                set: { if !$0 { removingMember = nil } }
103            )
104        ) {
105            Button("Remove", role: .destructive) {
106                if let user = removingMember {
107                    Task { await model.removeMember(user) }
108                }
109                removingMember = nil
110            }
111            Button("Cancel", role: .cancel) {}
112        }
113        .confirmationDialog(
114            "Revoke \(model.teamName)'s \(revokingGrant?.role ?? "") access to \(revokingGrant?.repo ?? "")?",
115            isPresented: Binding(
116                get: { revokingGrant != nil },
117                set: { if !$0 { revokingGrant = nil } }
118            )
119        ) {
120            Button("Revoke", role: .destructive) {
121                if let grant = revokingGrant {
122                    Task { await model.revoke(grant) }
123                }
124                revokingGrant = nil
125            }
126            Button("Cancel", role: .cancel) {}
127        }
128        .refreshable { await model.load() }
129    }
130}