diff options
| author | Christian Cleberg <[email protected]> | 2026-03-17 23:19:22 -0500 |
|---|---|---|
| committer | Christian Cleberg <[email protected]> | 2026-03-17 23:19:22 -0500 |
| commit | 8f2057c53e9009c2529c9c4849c914666c0e4b40 (patch) | |
| tree | 77b5a93625fe9ea98baf4ab0d137055524705dad /Hutch/Views/Repositories/RepositorySettingsViewModel.swift | |
| parent | d27726d8755a5631fbd2105f735811a95379c4ab (diff) | |
| download | hutch-8f2057c53e9009c2529c9c4849c914666c0e4b40.tar.gz hutch-8f2057c53e9009c2529c9c4849c914666c0e4b40.tar.bz2 hutch-8f2057c53e9009c2529c9c4849c914666c0e4b40.zip | |
create privacy policy
Diffstat (limited to 'Hutch/Views/Repositories/RepositorySettingsViewModel.swift')
| -rw-r--r-- | Hutch/Views/Repositories/RepositorySettingsViewModel.swift | 301 |
1 files changed, 301 insertions, 0 deletions
diff --git a/Hutch/Views/Repositories/RepositorySettingsViewModel.swift b/Hutch/Views/Repositories/RepositorySettingsViewModel.swift new file mode 100644 index 0000000..bdae87f --- /dev/null +++ b/Hutch/Views/Repositories/RepositorySettingsViewModel.swift @@ -0,0 +1,301 @@ +import Foundation + +// MARK: - Response types + +private struct UpdateRepoResponse: Decodable, Sendable { + let updateRepository: UpdatedRepo +} + +private struct UpdatedRepo: Decodable, Sendable { + let id: Int + let rid: String + let name: String + let description: String? + let visibility: Visibility +} + +private struct ACLResponse: Decodable, Sendable { + let repository: ACLRepository? +} + +private struct ACLRepository: Decodable, Sendable { + let acls: ACLPage +} + +private struct ACLPage: Decodable, Sendable { + let results: [ACLEntry] + let cursor: String? +} + +private struct UpdateACLResponse: Decodable, Sendable { + let updateACL: ACLEntry +} + +private struct DeleteACLResponse: Decodable, Sendable { + let deleteACL: DeletedACL +} + +private struct DeletedACL: Decodable, Sendable { + let id: Int +} + +private struct DeleteRepoResponse: Decodable, Sendable { + let deleteRepository: DeletedRepo +} + +private struct DeletedRepo: Decodable, Sendable { + let id: Int +} + +// MARK: - ACL Model + +struct ACLEntry: Decodable, Sendable, Identifiable { + let id: Int + let mode: String + let entity: Entity +} + +// MARK: - View Model + +@Observable +@MainActor +final class RepositorySettingsViewModel { + + let repositoryId: Int + let repositoryRid: String + private let client: SRHTClient + + // MARK: - Info fields + + var editedDescription: String + var editedVisibility: Visibility + var editedHead: String + var isSavingInfo = false + + // MARK: - Rename fields + + var editedName: String + var isRenaming = false + + // MARK: - ACL state + + private(set) var acls: [ACLEntry] = [] + private(set) var isLoadingACLs = false + var newACLEntity = "" + var newACLMode = "RO" + var isAddingACL = false + var isDeletingACL = false + + // MARK: - Delete state + + var isDeleting = false + + // MARK: - Branches (for HEAD picker) + + var branches: [Reference] + + // MARK: - Results + + var error: String? + var updatedName: String? + var didDelete = false + + init( + repository: RepositorySummary, + branches: [Reference], + client: SRHTClient + ) { + self.repositoryId = repository.id + self.repositoryRid = repository.rid + self.client = client + self.editedDescription = repository.description ?? "" + self.editedVisibility = repository.visibility + self.editedName = repository.name + self.branches = branches + + // Extract branch name from HEAD reference + if let head = repository.head?.name { + self.editedHead = head.replacingOccurrences(of: "refs/heads/", with: "") + } else { + self.editedHead = "main" + } + } + + // MARK: - Update Repository Info + + private static let updateRepoMutation = """ + mutation updateRepository($id: Int!, $input: RepoInput!) { + updateRepository(id: $id, input: $input) { + id rid name description visibility + } + } + """ + + func saveInfo() async { + isSavingInfo = true + defer { isSavingInfo = false } + error = nil + + do { + let input: [String: any Sendable] = [ + "description": editedDescription, + "visibility": editedVisibility.rawValue, + "HEAD": editedHead + ] + _ = try await client.execute( + service: .git, + query: Self.updateRepoMutation, + variables: ["id": repositoryId, "input": input], + responseType: UpdateRepoResponse.self + ) + } catch { + self.error = error.localizedDescription + } + } + + // MARK: - Rename + + func rename() async { + isRenaming = true + defer { isRenaming = false } + error = nil + + do { + let input: [String: any Sendable] = [ + "name": editedName + ] + let result = try await client.execute( + service: .git, + query: Self.updateRepoMutation, + variables: ["id": repositoryId, "input": input], + responseType: UpdateRepoResponse.self + ) + updatedName = result.updateRepository.name + } catch { + self.error = error.localizedDescription + } + } + + // MARK: - ACLs + + private static let aclsQuery = """ + query acls($rid: ID!) { + repository(rid: $rid) { + acls { + results { + id + mode + entity { canonicalName } + } + cursor + } + } + } + """ + + private static let updateACLMutation = """ + mutation updateACL($repoId: Int!, $mode: AccessMode!, $entity: String!) { + updateACL(repoId: $repoId, mode: $mode, entity: $entity) { + id mode entity { canonicalName } + } + } + """ + + private static let deleteACLMutation = """ + mutation deleteACL($id: Int!) { + deleteACL(id: $id) { id } + } + """ + + func loadACLs() async { + guard !isLoadingACLs else { return } + isLoadingACLs = true + defer { isLoadingACLs = false } + + do { + let result = try await client.execute( + service: .git, + query: Self.aclsQuery, + variables: ["rid": repositoryRid], + responseType: ACLResponse.self + ) + acls = result.repository?.acls.results ?? [] + } catch { + self.error = error.localizedDescription + } + } + + func addACL() async { + let entity = newACLEntity.trimmingCharacters(in: .whitespacesAndNewlines) + guard !entity.isEmpty else { return } + isAddingACL = true + defer { isAddingACL = false } + error = nil + + do { + let result = try await client.execute( + service: .git, + query: Self.updateACLMutation, + variables: [ + "repoId": repositoryId, + "mode": newACLMode, + "entity": entity + ], + responseType: UpdateACLResponse.self + ) + // Replace existing entry or append + if let index = acls.firstIndex(where: { $0.id == result.updateACL.id }) { + acls[index] = result.updateACL + } else { + acls.append(result.updateACL) + } + newACLEntity = "" + } catch { + self.error = error.localizedDescription + } + } + + func deleteACL(_ entry: ACLEntry) async { + isDeletingACL = true + defer { isDeletingACL = false } + error = nil + + do { + _ = try await client.execute( + service: .git, + query: Self.deleteACLMutation, + variables: ["id": entry.id], + responseType: DeleteACLResponse.self + ) + acls.removeAll { $0.id == entry.id } + } catch { + self.error = error.localizedDescription + } + } + + // MARK: - Delete Repository + + private static let deleteRepoMutation = """ + mutation deleteRepository($id: Int!) { + deleteRepository(id: $id) { id } + } + """ + + func deleteRepository() async { + isDeleting = true + defer { isDeleting = false } + error = nil + + do { + _ = try await client.execute( + service: .git, + query: Self.deleteRepoMutation, + variables: ["id": repositoryId], + responseType: DeleteRepoResponse.self + ) + didDelete = true + } catch { + self.error = error.localizedDescription + } + } +} |
