summaryrefslogtreecommitdiff
path: root/Hutch/Views/Repositories/ArtifactsView.swift
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-07-16 00:54:59 -0500
committerChristian Cleberg <[email protected]>2026-07-16 00:54:59 -0500
commitd32ad837ac9eb153598c10c3cf47fbdb4e51e3ba (patch)
tree4eac5967f649efac0df53c9e94d96274ead3cd2f /Hutch/Views/Repositories/ArtifactsView.swift
parent77cd5b5e56ec054b451a6165162fef655873a5d6 (diff)
downloadhutch-d32ad837ac9eb153598c10c3cf47fbdb4e51e3ba.tar.gz
hutch-d32ad837ac9eb153598c10c3cf47fbdb4e51e3ba.tar.bz2
hutch-d32ad837ac9eb153598c10c3cf47fbdb4e51e3ba.zip
fix: make artifact upload actually fire, and show it when it fails
Picking a file did nothing. The fileImporter's isPresented binding was derived from uploadTargetRef and nilled it on dismissal, but dismissal happens before the completion runs — so the completion read nil and returned without uploading. Presentation state and payload state cannot be the same state. A plain isImporting bool drives presentation now; the tag survives in uploadTargetRef until the completion consumes it. The upload menu was also disabled when the repository has no tags, while the explanation for that state lived inside the menu — unreachable exactly when it applies, so the tap died with no reason given. sr.ht requires revspec to match a tag, so having none is a real state worth explaining rather than hiding. Failures were invisible too. uploadArtifact and deleteArtifact set error, but the overlay only renders it when the list is empty, so a rejection on a repository that already has artifacts — a duplicate filename is the likely one, since sr.ht requires filenames to be unique per repository — set an error nobody saw. The tab carries an error banner now.
Diffstat (limited to 'Hutch/Views/Repositories/ArtifactsView.swift')
-rw-r--r--Hutch/Views/Repositories/ArtifactsView.swift47
1 files changed, 37 insertions, 10 deletions
diff --git a/Hutch/Views/Repositories/ArtifactsView.swift b/Hutch/Views/Repositories/ArtifactsView.swift
index 30ab0b1..8843c6f 100644
--- a/Hutch/Views/Repositories/ArtifactsView.swift
+++ b/Hutch/Views/Repositories/ArtifactsView.swift
@@ -1,5 +1,11 @@
import SwiftUI
import UniformTypeIdentifiers
+import os
+
+#if DEBUG
+/// Temporary: diagnosing why the upload menu swallows taps.
+private let artifactsLogger = Logger(subsystem: "net.cleberg.Hutch", category: "Artifacts")
+#endif
struct ArtifactsView: View {
let viewModel: RepositoryDetailViewModel
@@ -9,6 +15,7 @@ struct ArtifactsView: View {
@Environment(\.openURL) private var openURL
@State private var uploadTargetRef: String?
+ @State private var isImporting = false
@State private var pendingDeletion: ArtifactInfo?
private var isOwnedByCurrentUser: Bool { canManage }
@@ -25,17 +32,23 @@ struct ArtifactsView: View {
ForEach(viewModel.tags.prefix(12), id: \.name) { tag in
Button(RepositorySummary.displayBranchName(for: tag.name)) {
uploadTargetRef = tag.name
+ isImporting = true
}
}
}
} label: {
SwiftUI.Label("Upload Artifact…", systemImage: "square.and.arrow.up")
}
- .disabled(viewModel.isMutatingArtifact || viewModel.tags.isEmpty)
+ // Deliberately not disabled when there are no tags. The explanation for
+ // that state lives inside the menu, and disabling the control makes the
+ // explanation unreachable — the tap just dies with no reason given.
+ .disabled(viewModel.isMutatingArtifact)
}
var body: some View {
- List {
+ @Bindable var vm = viewModel
+
+ return List {
// In the list rather than the toolbar: this view is a segment inside
// RepositoryDetailView's tab switch, not its own navigation
// destination, and a toolbar declared from there does not reliably
@@ -75,6 +88,7 @@ struct ArtifactsView: View {
// on the tag rather than in the toolbar.
Button {
uploadTargetRef = refArtifacts.name
+ isImporting = true
} label: {
SwiftUI.Label("Upload", systemImage: "plus.circle")
.font(.caption)
@@ -85,18 +99,18 @@ struct ArtifactsView: View {
}
}
}
+ // isImporting drives presentation; uploadTargetRef carries the tag. They
+ // have to be separate: a binding derived from uploadTargetRef clears it on
+ // dismissal, and dismissal happens before the completion runs — so the
+ // completion read nil and returned without uploading anything.
.fileImporter(
- isPresented: .init(
- get: { uploadTargetRef != nil },
- set: { if !$0 { uploadTargetRef = nil } }
- ),
+ isPresented: $isImporting,
allowedContentTypes: [.data]
) { result in
- guard let revspec = uploadTargetRef else { return }
+ let revspec = uploadTargetRef
uploadTargetRef = nil
- if case .success(let fileURL) = result {
- Task { await viewModel.uploadArtifact(revspec: revspec, fileURL: fileURL) }
- }
+ guard let revspec, case .success(let fileURL) = result else { return }
+ Task { await viewModel.uploadArtifact(revspec: revspec, fileURL: fileURL) }
}
.confirmationDialog(
pendingDeletion.map { "Delete \($0.filename)?" } ?? "",
@@ -116,11 +130,24 @@ struct ArtifactsView: View {
}
.themedList()
.listStyle(.insetGrouped)
+ .srhtErrorBanner(error: $vm.error)
.task {
// Tags drive the picker above and are not otherwise needed by this tab.
if isOwnedByCurrentUser, viewModel.tags.isEmpty {
await viewModel.loadReferences()
}
+ #if DEBUG
+ // Temporary: diagnosing why the upload menu swallows taps.
+ artifactsLogger.debug(
+ """
+ canManage=\(canManage, privacy: .public) \
+ tags=\(viewModel.tags.count, privacy: .public) \
+ isMutating=\(viewModel.isMutatingArtifact, privacy: .public) \
+ menuDisabled=\(viewModel.isMutatingArtifact || viewModel.tags.isEmpty, privacy: .public) \
+ error=\(viewModel.error ?? "nil", privacy: .public)
+ """
+ )
+ #endif
}
.overlay {
if viewModel.isLoadingArtifacts, viewModel.referenceArtifacts.isEmpty {