gitbay/Views/Orgs/TeamView.swift
97 lines · 4033 bytes
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
11 init(client: GitbayClient, org: String, team: String) {
12 _model = State(initialValue: TeamViewModel(client: client, orgName: org, teamName: team))
13 }
14
15 var body: some View {
16 List {
17 if let loaded = model.state.value {
18 if let error = model.actionError {
19 Section {
20 GBNotice(error, .gbWarn)
21 }
22 }
23
24 Section("Members") {
25 ForEach(loaded.members ?? [], id: \.self) { user in
26 Text(user)
27 .font(.gbSans(.subheadline))
28 .swipeActions {
29 Button("Remove", role: .destructive) {
30 Task { await model.removeMember(user) }
31 }
32 }
33 }
34 HStack {
35 TextField("Add org member", text: $newMember)
36 .autocorrectionDisabled()
37 .textInputAutocapitalization(.never)
38 Button {
39 let user = newMember.trimmingCharacters(in: .whitespaces)
40 newMember = ""
41 Task { await model.addMember(user) }
42 } label: {
43 Image(systemName: "plus.circle.fill")
44 }
45 .disabled(newMember.trimmingCharacters(in: .whitespaces).isEmpty
46 || model.working)
47 }
48 }
49
50 Section {
51 ForEach(loaded.grants ?? []) { grant in
52 HStack {
53 Text(grant.repo)
54 .font(.gbSans(.subheadline))
55 .lineLimit(1)
56 Spacer()
57 GBChip(grant.role, .secondary)
58 }
59 .swipeActions {
60 Button("Revoke", role: .destructive) {
61 Task { await model.revoke(grant) }
62 }
63 }
64 }
65 VStack(spacing: 8) {
66 TextField("org/repo", text: $grantRepo)
67 .autocorrectionDisabled()
68 .textInputAutocapitalization(.never)
69 HStack {
70 Picker("Role", selection: $grantRole) {
71 Text("read").tag("read")
72 Text("write").tag("write")
73 Text("admin").tag("admin")
74 }
75 .pickerStyle(.segmented)
76 Button("Grant") {
77 let repo = grantRepo.trimmingCharacters(in: .whitespaces)
78 grantRepo = ""
79 Task { await model.grant(repo: repo, role: grantRole) }
80 }
81 .disabled(!grantRepo.contains("/") || model.working)
82 }
83 }
84 } header: {
85 Text("Repository grants")
86 } footer: {
87 Text("A grant gives every team member the role on that org repository.")
88 }
89 }
90 }
91 .overlay { LoadStateOverlay(state: model.state) }
92 .navigationTitle(model.teamName)
93 .navigationBarTitleDisplayMode(.inline)
94 .task { await model.load() }
95 .refreshable { await model.load() }
96 }
97}