import Foundation import Observation /// One membership row of `org list`. nonisolated struct OrgMembership: Decodable, Sendable, Hashable, Identifiable { let org: String let role: String var id: String { org } } /// One member of `org show` / `org members list`. nonisolated struct OrgMember: Decodable, Sendable, Hashable, Identifiable { let user: String let role: String var id: String { user } } /// An organization: members and teams, plus the membership writes. /// Reads require membership; the refusal names the rule for outsiders. @Observable @MainActor final class OrgViewModel { nonisolated struct Loaded: Sendable, Hashable { let members: [OrgMember] let teams: [String] } private(set) var state: LoadState = .loading private(set) var actionError: String? private(set) var working = false private let client: GitbayClient let orgName: String init(client: GitbayClient, orgName: String) { self.client = client self.orgName = orgName } nonisolated private struct OrgShow: Decodable, Sendable { let org: String let members: [OrgMember] } func load() async { do { async let show = client.read(["org", "show", orgName], as: OrgShow.self) // Team list is bare names. async let teams = client.readList(["org", "team", "list", orgName], of: String.self) state = .loaded(Loaded(members: try await show.members, teams: try await teams)) } catch { state = .from(error) } } func addMember(_ user: String, role: String) async { await perform(["org", "members", "add", orgName, user, "--role", role]) } func removeMember(_ member: OrgMember) async { await perform(["org", "members", "remove", orgName, member.user]) } /// Role changes reuse `members add` — it updates an existing member. func setRole(_ member: OrgMember, role: String) async { await perform(["org", "members", "add", orgName, member.user, "--role", role]) } func createTeam(_ team: String) async { await perform(["org", "team", "create", orgName, team]) } func deleteTeam(_ team: String) async { await perform(["org", "team", "delete", orgName, team]) } private func perform(_ argv: [String]) async { working = true actionError = nil defer { working = false } do { try await client.run(argv) await load() } catch let error as GitbayError { actionError = error.userFacingMessage } catch { actionError = GitbayError.transport(error).userFacingMessage } } } /// One team: members and repo grants. @Observable @MainActor final class TeamViewModel { nonisolated struct Grant: Decodable, Sendable, Hashable, Identifiable { let repo: String let role: String var id: String { repo } } nonisolated struct Loaded: Decodable, Sendable, Hashable { let team: String let members: [String]? let grants: [Grant]? } private(set) var state: LoadState = .loading private(set) var actionError: String? private(set) var working = false private let client: GitbayClient let orgName: String let teamName: String init(client: GitbayClient, orgName: String, teamName: String) { self.client = client self.orgName = orgName self.teamName = teamName } func load() async { do { state = .loaded(try await client.read( ["org", "team", "show", orgName, teamName], as: Loaded.self)) } catch { state = .from(error) } } func addMember(_ user: String) async { await perform(["org", "team", "add", orgName, teamName, user]) } func removeMember(_ user: String) async { await perform(["org", "team", "remove", orgName, teamName, user]) } func grant(repo: String, role: String) async { await perform(["org", "team", "grant", orgName, teamName, repo, role]) } func revoke(_ grant: Grant) async { await perform(["org", "team", "revoke", orgName, teamName, grant.repo]) } private func perform(_ argv: [String]) async { working = true actionError = nil defer { working = false } do { try await client.run(argv) await load() } catch let error as GitbayError { actionError = error.userFacingMessage } catch { actionError = GitbayError.transport(error).userFacingMessage } } }