gitbay/Orgs/OrgViewModels.swift

v1.0.6
gitbay-ios/gitbay/Orgs/OrgViewModels.swift history · blame · raw

189 lines · 5805 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    private(set) var 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    /// `org rename <old> <new>` — both positional, old first. Swapping
 71    /// them renames the wrong way with no error at all.
 72    ///
 73    /// Doesn't go through `perform`: its automatic reload would still
 74    /// query the old, now-renamed org. `orgName` only moves to the new
 75    /// name once the rename itself has actually succeeded, and the
 76    /// reload that follows targets that new name.
 77    func rename(to newName: String) async -> Bool {
 78        let newName = newName.trimmingCharacters(in: .whitespacesAndNewlines)
 79        guard !newName.isEmpty else { return false }
 80        working = true
 81        actionError = nil
 82        defer { working = false }
 83        do {
 84            try await client.run(["org", "rename", orgName, newName])
 85            orgName = newName
 86            await load()
 87            return true
 88        } catch let error as GitbayError {
 89            actionError = error.userFacingMessage
 90            return false
 91        } catch {
 92            actionError = GitbayError.transport(error).userFacingMessage
 93            return false
 94        }
 95    }
 96
 97    func createTeam(_ team: String) async {
 98        await perform(["org", "team", "create", orgName, team])
 99    }
100
101    func deleteTeam(_ team: String) async {
102        await perform(["org", "team", "delete", orgName, team])
103    }
104
105    private func perform(_ argv: [String]) async {
106        working = true
107        actionError = nil
108        defer { working = false }
109        do {
110            try await client.run(argv)
111            await load()
112        } catch let error as GitbayError {
113            actionError = error.userFacingMessage
114        } catch {
115            actionError = GitbayError.transport(error).userFacingMessage
116        }
117    }
118}
119
120/// One team: members and repo grants.
121@Observable
122@MainActor
123final class TeamViewModel {
124
125    nonisolated struct Grant: Decodable, Sendable, Hashable, Identifiable {
126        let repo: String
127        let role: String
128        var id: String { repo }
129    }
130
131    nonisolated struct Loaded: Decodable, Sendable, Hashable {
132        let team: String
133        let members: [String]?
134        let grants: [Grant]?
135    }
136
137    private(set) var state: LoadState<Loaded> = .loading
138    private(set) var actionError: String?
139    private(set) var working = false
140
141    private let client: GitbayClient
142    let orgName: String
143    let teamName: String
144
145    init(client: GitbayClient, orgName: String, teamName: String) {
146        self.client = client
147        self.orgName = orgName
148        self.teamName = teamName
149    }
150
151    func load() async {
152        do {
153            state = .loaded(try await client.read(
154                ["org", "team", "show", orgName, teamName], as: Loaded.self))
155        } catch {
156            state = .from(error)
157        }
158    }
159
160    func addMember(_ user: String) async {
161        await perform(["org", "team", "add", orgName, teamName, user])
162    }
163
164    func removeMember(_ user: String) async {
165        await perform(["org", "team", "remove", orgName, teamName, user])
166    }
167
168    func grant(repo: String, role: String) async {
169        await perform(["org", "team", "grant", orgName, teamName, repo, role])
170    }
171
172    func revoke(_ grant: Grant) async {
173        await perform(["org", "team", "revoke", orgName, teamName, grant.repo])
174    }
175
176    private func perform(_ argv: [String]) async {
177        working = true
178        actionError = nil
179        defer { working = false }
180        do {
181            try await client.run(argv)
182            await load()
183        } catch let error as GitbayError {
184            actionError = error.userFacingMessage
185        } catch {
186            actionError = GitbayError.transport(error).userFacingMessage
187        }
188    }
189}