gitbay/Orgs/OrgViewModels.swift
162 lines · 4712 bytes
1import Foundation
2import Observation
3
4/// One membership row of `org list`.
5nonisolated struct OrgMembership: Decodable, Sendable, Hashable, Identifiable {
6 let org: String
7 let role: String
8 var id: String { org }
9}
10
11/// One member of `org show` / `org members list`.
12nonisolated struct OrgMember: Decodable, Sendable, Hashable, Identifiable {
13 let user: String
14 let role: String
15 var id: String { user }
16}
17
18/// An organization: members and teams, plus the membership writes.
19/// Reads require membership; the refusal names the rule for outsiders.
20@Observable
21@MainActor
22final class OrgViewModel {
23
24 nonisolated struct Loaded: Sendable, Hashable {
25 let members: [OrgMember]
26 let teams: [String]
27 }
28
29 private(set) var state: LoadState<Loaded> = .loading
30 private(set) var actionError: String?
31 private(set) var working = false
32
33 private let client: GitbayClient
34 let orgName: String
35
36 init(client: GitbayClient, orgName: String) {
37 self.client = client
38 self.orgName = orgName
39 }
40
41 nonisolated private struct OrgShow: Decodable, Sendable {
42 let org: String
43 let members: [OrgMember]
44 }
45
46 func load() async {
47 do {
48 async let show = client.read(["org", "show", orgName], as: OrgShow.self)
49 // Team list is bare names.
50 async let teams = client.readList(["org", "team", "list", orgName], of: String.self)
51 state = .loaded(Loaded(members: try await show.members, teams: try await teams))
52 } catch {
53 state = .from(error)
54 }
55 }
56
57 func addMember(_ user: String, role: String) async {
58 await perform(["org", "members", "add", orgName, user, "--role", role])
59 }
60
61 func removeMember(_ member: OrgMember) async {
62 await perform(["org", "members", "remove", orgName, member.user])
63 }
64
65 /// Role changes reuse `members add` — it updates an existing member.
66 func setRole(_ member: OrgMember, role: String) async {
67 await perform(["org", "members", "add", orgName, member.user, "--role", role])
68 }
69
70 func createTeam(_ team: String) async {
71 await perform(["org", "team", "create", orgName, team])
72 }
73
74 func deleteTeam(_ team: String) async {
75 await perform(["org", "team", "delete", orgName, team])
76 }
77
78 private func perform(_ argv: [String]) async {
79 working = true
80 actionError = nil
81 defer { working = false }
82 do {
83 try await client.run(argv)
84 await load()
85 } catch let error as GitbayError {
86 actionError = error.userFacingMessage
87 } catch {
88 actionError = GitbayError.transport(error).userFacingMessage
89 }
90 }
91}
92
93/// One team: members and repo grants.
94@Observable
95@MainActor
96final class TeamViewModel {
97
98 nonisolated struct Grant: Decodable, Sendable, Hashable, Identifiable {
99 let repo: String
100 let role: String
101 var id: String { repo }
102 }
103
104 nonisolated struct Loaded: Decodable, Sendable, Hashable {
105 let team: String
106 let members: [String]?
107 let grants: [Grant]?
108 }
109
110 private(set) var state: LoadState<Loaded> = .loading
111 private(set) var actionError: String?
112 private(set) var working = false
113
114 private let client: GitbayClient
115 let orgName: String
116 let teamName: String
117
118 init(client: GitbayClient, orgName: String, teamName: String) {
119 self.client = client
120 self.orgName = orgName
121 self.teamName = teamName
122 }
123
124 func load() async {
125 do {
126 state = .loaded(try await client.read(
127 ["org", "team", "show", orgName, teamName], as: Loaded.self))
128 } catch {
129 state = .from(error)
130 }
131 }
132
133 func addMember(_ user: String) async {
134 await perform(["org", "team", "add", orgName, teamName, user])
135 }
136
137 func removeMember(_ user: String) async {
138 await perform(["org", "team", "remove", orgName, teamName, user])
139 }
140
141 func grant(repo: String, role: String) async {
142 await perform(["org", "team", "grant", orgName, teamName, repo, role])
143 }
144
145 func revoke(_ grant: Grant) async {
146 await perform(["org", "team", "revoke", orgName, teamName, grant.repo])
147 }
148
149 private func perform(_ argv: [String]) async {
150 working = true
151 actionError = nil
152 defer { working = false }
153 do {
154 try await client.run(argv)
155 await load()
156 } catch let error as GitbayError {
157 actionError = error.userFacingMessage
158 } catch {
159 actionError = GitbayError.transport(error).userFacingMessage
160 }
161 }
162}