gitbay/Views/Orgs/TeamView.swift
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}