diff options
| author | Christian Cleberg <[email protected]> | 2026-07-16 00:54:59 -0500 |
|---|---|---|
| committer | Christian Cleberg <[email protected]> | 2026-07-16 00:54:59 -0500 |
| commit | d32ad837ac9eb153598c10c3cf47fbdb4e51e3ba (patch) | |
| tree | 4eac5967f649efac0df53c9e94d96274ead3cd2f /Hutch/Views | |
| parent | 77cd5b5e56ec054b451a6165162fef655873a5d6 (diff) | |
| download | hutch-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')
| -rw-r--r-- | Hutch/Views/Repositories/ArtifactsView.swift | 47 |
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 { |
