summaryrefslogtreecommitdiff
path: root/Hutch/Views/Projects/ProjectDetailView.swift
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-08-07 04:12:50 -0500
committerChristian Cleberg <[email protected]>2026-08-07 04:12:50 -0500
commit1f8f651a9c2ac2d6be231ff9f71ee3bcce9936d8 (patch)
tree25cf810fa0e9a0ab343b60b59c26d8cc096c9a98 /Hutch/Views/Projects/ProjectDetailView.swift
parentdb40d346c28800a1eec384a5e4f3d7d090198e4d (diff)
downloadhutch-1f8f651a9c2ac2d6be231ff9f71ee3bcce9936d8.tar.gz
hutch-1f8f651a9c2ac2d6be231ff9f71ee3bcce9936d8.tar.bz2
hutch-1f8f651a9c2ac2d6be231ff9f71ee3bcce9936d8.zip
Projects: discovery, create, edit, manage linked resources (#12, #13, #14, #15)
hub.sr.ht's GraphQL now exposes the project write API (create/update/ link/unlink) and a public `projects` discovery query, so these previously-blocked issues can be built. ProjectService gains: fetchPublicProjects (#12); createProject (#13); updateProject with ProjectInput (#14); link/unlink for sources, trackers, and mailing lists plus linkable-resource candidate fetches (#15); and hub cache invalidation after every mutation. UI: a Discover browser and a create form reached from the projects list; an Edit form and a Manage Resources sheet on project detail, gated to projects the user owns. Mutations degrade to a visible error if a field isn't live yet. Built against the master schema; live deployment on sr.ht/query is not yet confirmed (introspection there requires a token).
Diffstat (limited to 'Hutch/Views/Projects/ProjectDetailView.swift')
-rw-r--r--Hutch/Views/Projects/ProjectDetailView.swift90
1 files changed, 90 insertions, 0 deletions
diff --git a/Hutch/Views/Projects/ProjectDetailView.swift b/Hutch/Views/Projects/ProjectDetailView.swift
index c30ec88..0685a83 100644
--- a/Hutch/Views/Projects/ProjectDetailView.swift
+++ b/Hutch/Views/Projects/ProjectDetailView.swift
@@ -2,6 +2,7 @@ import SwiftUI
struct ProjectDetailView: View {
let project: Project
+ var canManage: Bool = false
@Environment(AppState.self) private var appState
@Environment(\.dismiss) private var dismiss
@@ -10,6 +11,14 @@ struct ProjectDetailView: View {
@State private var isLoading = false
@State private var error: String?
@State private var pinChangeCount = 0
+ @State private var isPresentingEdit = false
+ @State private var isPresentingManage = false
+ @State private var isSavingEdit = false
+ @State private var editError: String?
+
+ private var projectService: ProjectService {
+ ProjectService(client: appState.client)
+ }
private var displayedProject: Project {
detailProject ?? project
@@ -59,6 +68,46 @@ struct ProjectDetailView: View {
.accessibilityLabel(isPinnedToHome ? "Unpin from Home" : "Pin to Home")
}
}
+ if canManage {
+ ToolbarItem(placement: .topBarTrailing) {
+ Menu {
+ Button {
+ editError = nil
+ isPresentingEdit = true
+ } label: {
+ Label("Edit Project", systemImage: "pencil")
+ }
+ Button {
+ isPresentingManage = true
+ } label: {
+ Label("Manage Resources", systemImage: "link")
+ }
+ } label: {
+ Image(systemName: "ellipsis.circle")
+ }
+ .accessibilityLabel("Manage project")
+ }
+ }
+ }
+ .sheet(isPresented: $isPresentingEdit) {
+ ProjectFormSheet(
+ title: "Edit Project",
+ confirmationTitle: "Save",
+ isSaving: isSavingEdit,
+ error: editError,
+ includeWebsite: true,
+ initialName: displayedProject.name,
+ initialDescription: displayedProject.description ?? "",
+ initialWebsite: displayedProject.website ?? "",
+ initialTags: displayedProject.tags,
+ initialVisibility: displayedProject.visibility,
+ onSave: { await saveEdits($0) }
+ )
+ }
+ .sheet(isPresented: $isPresentingManage) {
+ ManageProjectResourcesView(project: displayedProject) {
+ await loadProjectIfNeeded(forceRefresh: true)
+ }
}
.task {
await loadProjectIfNeeded()
@@ -69,6 +118,47 @@ struct ProjectDetailView: View {
.srhtErrorBanner(error: $error)
}
+ private func saveEdits(_ values: ProjectFormValues) async -> Bool {
+ guard !isSavingEdit else { return false }
+ isSavingEdit = true
+ editError = nil
+ defer { isSavingEdit = false }
+
+ do {
+ let updated = try await projectService.updateProject(
+ rid: displayedProject.id,
+ name: values.name,
+ description: values.description,
+ website: values.website,
+ visibility: values.visibility,
+ tags: values.tags
+ )
+ // Preserve already-loaded linked resources; the mutation returns metadata only.
+ detailProject = Project(
+ metadata: .init(
+ id: updated.id,
+ name: updated.name,
+ description: updated.description,
+ website: updated.website,
+ visibility: updated.visibility,
+ tags: updated.tags,
+ updated: updated.updated
+ ),
+ resources: .init(
+ mailingLists: displayedProject.mailingLists,
+ sources: displayedProject.sources,
+ trackers: displayedProject.trackers,
+ isFullyLoaded: displayedProject.isFullyLoaded
+ )
+ )
+ await loadProjectIfNeeded(forceRefresh: true)
+ return true
+ } catch {
+ editError = error.userFacingMessage
+ return false
+ }
+ }
+
@ViewBuilder
private var headerSection: some View {
Section {