gitbay/Views/Orgs/TeamView.swift
103 lines · 4351 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 Label(error, systemImage: "hand.raised")
21 .foregroundStyle(.orange)
22 .font(.subheadline)
23 }
24 }
25
26 Section("Members") {
27 ForEach(loaded.members ?? [], id: \.self) { user in
28 Text(user)
29 .font(.subheadline)
30 .swipeActions {
31 Button("Remove", role: .destructive) {
32 Task { await model.removeMember(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 .disabled(newMember.trimmingCharacters(in: .whitespaces).isEmpty
48 || model.working)
49 }
50 }
51
52 Section {
53 ForEach(loaded.grants ?? []) { grant in
54 HStack {
55 Text(grant.repo)
56 .font(.subheadline)
57 .lineLimit(1)
58 Spacer()
59 Text(grant.role)
60 .font(.caption)
61 .padding(.horizontal, 6)
62 .padding(.vertical, 2)
63 .background(.quaternary, in: Capsule())
64 }
65 .swipeActions {
66 Button("Revoke", role: .destructive) {
67 Task { await model.revoke(grant) }
68 }
69 }
70 }
71 VStack(spacing: 8) {
72 TextField("org/repo", text: $grantRepo)
73 .autocorrectionDisabled()
74 .textInputAutocapitalization(.never)
75 HStack {
76 Picker("Role", selection: $grantRole) {
77 Text("read").tag("read")
78 Text("write").tag("write")
79 Text("admin").tag("admin")
80 }
81 .pickerStyle(.segmented)
82 Button("Grant") {
83 let repo = grantRepo.trimmingCharacters(in: .whitespaces)
84 grantRepo = ""
85 Task { await model.grant(repo: repo, role: grantRole) }
86 }
87 .disabled(!grantRepo.contains("/") || model.working)
88 }
89 }
90 } header: {
91 Text("Repository grants")
92 } footer: {
93 Text("A grant gives every team member the role on that org repository.")
94 }
95 }
96 }
97 .overlay { LoadStateOverlay(state: model.state) }
98 .navigationTitle(model.teamName)
99 .navigationBarTitleDisplayMode(.inline)
100 .task { await model.load() }
101 .refreshable { await model.load() }
102 }
103}