Commit ff141c0e34
Unsigned
Layout: unified · split
Hutch/Views/Repositories/ArtifactsView.swift +37 −10
| @@ -1,5 +1,11 @@ | ||
| 1 | 1 | import SwiftUI |
| 2 | 2 | import UniformTypeIdentifiers |
| 3 | import os | |
| 4 | ||
| 5 | #if DEBUG | |
| 6 | /// Temporary: diagnosing why the upload menu swallows taps. | |
| 7 | private let artifactsLogger = Logger(subsystem: "net.cleberg.Hutch", category: "Artifacts") | |
| 8 | #endif | |
| 3 | 9 | |
| 4 | 10 | struct ArtifactsView: View { |
| 5 | 11 | let viewModel: RepositoryDetailViewModel |
| @@ -9,6 +15,7 @@ struct ArtifactsView: View { | ||
| 9 | 15 | @Environment(\.openURL) private var openURL |
| 10 | 16 | |
| 11 | 17 | @State private var uploadTargetRef: String? |
| 18 | @State private var isImporting = false | |
| 12 | 19 | @State private var pendingDeletion: ArtifactInfo? |
| 13 | 20 | |
| 14 | 21 | private var isOwnedByCurrentUser: Bool { canManage } |
| @@ -25,17 +32,23 @@ struct ArtifactsView: View { | ||
| 25 | 32 | ForEach(viewModel.tags.prefix(12), id: \.name) { tag in |
| 26 | 33 | Button(RepositorySummary.displayBranchName(for: tag.name)) { |
| 27 | 34 | uploadTargetRef = tag.name |
| 35 | isImporting = true | |
| 28 | 36 | } |
| 29 | 37 | } |
| 30 | 38 | } |
| 31 | 39 | } label: { |
| 32 | 40 | SwiftUI.Label("Upload Artifact…", systemImage: "square.and.arrow.up") |
| 33 | 41 | } |
| 34 | .disabled(viewModel.isMutatingArtifact || viewModel.tags.isEmpty) | |
| 42 | // Deliberately not disabled when there are no tags. The explanation for | |
| 43 | // that state lives inside the menu, and disabling the control makes the | |
| 44 | // explanation unreachable — the tap just dies with no reason given. | |
| 45 | .disabled(viewModel.isMutatingArtifact) | |
| 35 | 46 | } |
| 36 | 47 | |
| 37 | 48 | var body: some View { |
| 38 | List { | |
| 49 | @Bindable var vm = viewModel | |
| 50 | ||
| 51 | return List { | |
| 39 | 52 | // In the list rather than the toolbar: this view is a segment inside |
| 40 | 53 | // RepositoryDetailView's tab switch, not its own navigation |
| 41 | 54 | // destination, and a toolbar declared from there does not reliably |
| @@ -75,6 +88,7 @@ struct ArtifactsView: View { | ||
| 75 | 88 | // on the tag rather than in the toolbar. |
| 76 | 89 | Button { |
| 77 | 90 | uploadTargetRef = refArtifacts.name |
| 91 | isImporting = true | |
| 78 | 92 | } label: { |
| 79 | 93 | SwiftUI.Label("Upload", systemImage: "plus.circle") |
| 80 | 94 | .font(.caption) |
| @@ -85,18 +99,18 @@ struct ArtifactsView: View { | ||
| 85 | 99 | } |
| 86 | 100 | } |
| 87 | 101 | } |
| 102 | // isImporting drives presentation; uploadTargetRef carries the tag. They | |
| 103 | // have to be separate: a binding derived from uploadTargetRef clears it on | |
| 104 | // dismissal, and dismissal happens before the completion runs — so the | |
| 105 | // completion read nil and returned without uploading anything. | |
| 88 | 106 | .fileImporter( |
| 89 | isPresented: .init( | |
| 90 | get: { uploadTargetRef != nil }, | |
| 91 | set: { if !$0 { uploadTargetRef = nil } } | |
| 92 | ), | |
| 107 | isPresented: $isImporting, | |
| 93 | 108 | allowedContentTypes: [.data] |
| 94 | 109 | ) { result in |
| 95 | guard let revspec = uploadTargetRef else { return } | |
| 110 | let revspec = uploadTargetRef | |
| 96 | 111 | uploadTargetRef = nil |
| 97 | if case .success(let fileURL) = result { | |
| 98 | Task { await viewModel.uploadArtifact(revspec: revspec, fileURL: fileURL) } | |
| 99 | } | |
| 112 | guard let revspec, case .success(let fileURL) = result else { return } | |
| 113 | Task { await viewModel.uploadArtifact(revspec: revspec, fileURL: fileURL) } | |
| 100 | 114 | } |
| 101 | 115 | .confirmationDialog( |
| 102 | 116 | pendingDeletion.map { "Delete \($0.filename)?" } ?? "", |
| @@ -116,11 +130,24 @@ struct ArtifactsView: View { | ||
| 116 | 130 | } |
| 117 | 131 | .themedList() |
| 118 | 132 | .listStyle(.insetGrouped) |
| 133 | .srhtErrorBanner(error: $vm.error) | |
| 119 | 134 | .task { |
| 120 | 135 | // Tags drive the picker above and are not otherwise needed by this tab. |
| 121 | 136 | if isOwnedByCurrentUser, viewModel.tags.isEmpty { |
| 122 | 137 | await viewModel.loadReferences() |
| 123 | 138 | } |
| 139 | #if DEBUG | |
| 140 | // Temporary: diagnosing why the upload menu swallows taps. | |
| 141 | artifactsLogger.debug( | |
| 142 | """ | |
| 143 | canManage=\(canManage, privacy: .public) \ | |
| 144 | tags=\(viewModel.tags.count, privacy: .public) \ | |
| 145 | isMutating=\(viewModel.isMutatingArtifact, privacy: .public) \ | |
| 146 | menuDisabled=\(viewModel.isMutatingArtifact || viewModel.tags.isEmpty, privacy: .public) \ | |
| 147 | error=\(viewModel.error ?? "nil", privacy: .public) | |
| 148 | """ | |
| 149 | ) | |
| 150 | #endif | |
| 124 | 151 | } |
| 125 | 152 | .overlay { |
| 126 | 153 | if viewModel.isLoadingArtifacts, viewModel.referenceArtifacts.isEmpty { |