Commit ff141c0e34
Unsigned
Layout: unified · split
Hutch/Views/Repositories/ArtifactsView.swift +37 −10
| @@ -1,5 +1,11 @@ | |||
| 1 | import SwiftUI | 1 | import SwiftUI |
| 2 | import UniformTypeIdentifiers | 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 | struct ArtifactsView: View { | 10 | struct ArtifactsView: View { |
| 5 | let viewModel: RepositoryDetailViewModel | 11 | let viewModel: RepositoryDetailViewModel |
| @@ -9,6 +15,7 @@ struct ArtifactsView: View { | |||
| 9 | @Environment(\.openURL) private var openURL | 15 | @Environment(\.openURL) private var openURL |
| 10 | 16 | ||
| 11 | @State private var uploadTargetRef: String? | 17 | @State private var uploadTargetRef: String? |
| 18 | @State private var isImporting = false | ||
| 12 | @State private var pendingDeletion: ArtifactInfo? | 19 | @State private var pendingDeletion: ArtifactInfo? |
| 13 | 20 | ||
| 14 | private var isOwnedByCurrentUser: Bool { canManage } | 21 | private var isOwnedByCurrentUser: Bool { canManage } |
| @@ -25,17 +32,23 @@ struct ArtifactsView: View { | |||
| 25 | ForEach(viewModel.tags.prefix(12), id: \.name) { tag in | 32 | ForEach(viewModel.tags.prefix(12), id: \.name) { tag in |
| 26 | Button(RepositorySummary.displayBranchName(for: tag.name)) { | 33 | Button(RepositorySummary.displayBranchName(for: tag.name)) { |
| 27 | uploadTargetRef = tag.name | 34 | uploadTargetRef = tag.name |
| 35 | isImporting = true | ||
| 28 | } | 36 | } |
| 29 | } | 37 | } |
| 30 | } | 38 | } |
| 31 | } label: { | 39 | } label: { |
| 32 | SwiftUI.Label("Upload Artifact…", systemImage: "square.and.arrow.up") | 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 | var body: some View { | 48 | var body: some View { |
| 38 | List { | 49 | @Bindable var vm = viewModel |
| 50 | |||
| 51 | return List { | ||
| 39 | // In the list rather than the toolbar: this view is a segment inside | 52 | // In the list rather than the toolbar: this view is a segment inside |
| 40 | // RepositoryDetailView's tab switch, not its own navigation | 53 | // RepositoryDetailView's tab switch, not its own navigation |
| 41 | // destination, and a toolbar declared from there does not reliably | 54 | // destination, and a toolbar declared from there does not reliably |
| @@ -75,6 +88,7 @@ struct ArtifactsView: View { | |||
| 75 | // on the tag rather than in the toolbar. | 88 | // on the tag rather than in the toolbar. |
| 76 | Button { | 89 | Button { |
| 77 | uploadTargetRef = refArtifacts.name | 90 | uploadTargetRef = refArtifacts.name |
| 91 | isImporting = true | ||
| 78 | } label: { | 92 | } label: { |
| 79 | SwiftUI.Label("Upload", systemImage: "plus.circle") | 93 | SwiftUI.Label("Upload", systemImage: "plus.circle") |
| 80 | .font(.caption) | 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 | .fileImporter( | 106 | .fileImporter( |
| 89 | isPresented: .init( | 107 | isPresented: $isImporting, |
| 90 | get: { uploadTargetRef != nil }, | ||
| 91 | set: { if !$0 { uploadTargetRef = nil } } | ||
| 92 | ), | ||
| 93 | allowedContentTypes: [.data] | 108 | allowedContentTypes: [.data] |
| 94 | ) { result in | 109 | ) { result in |
| 95 | guard let revspec = uploadTargetRef else { return } | 110 | let revspec = uploadTargetRef |
| 96 | uploadTargetRef = nil | 111 | uploadTargetRef = nil |
| 97 | if case .success(let fileURL) = result { | 112 | guard let revspec, case .success(let fileURL) = result else { return } |
| 98 | Task { await viewModel.uploadArtifact(revspec: revspec, fileURL: fileURL) } | 113 | Task { await viewModel.uploadArtifact(revspec: revspec, fileURL: fileURL) } |
| 99 | } | ||
| 100 | } | 114 | } |
| 101 | .confirmationDialog( | 115 | .confirmationDialog( |
| 102 | pendingDeletion.map { "Delete \($0.filename)?" } ?? "", | 116 | pendingDeletion.map { "Delete \($0.filename)?" } ?? "", |
| @@ -116,11 +130,24 @@ struct ArtifactsView: View { | |||
| 116 | } | 130 | } |
| 117 | .themedList() | 131 | .themedList() |
| 118 | .listStyle(.insetGrouped) | 132 | .listStyle(.insetGrouped) |
| 133 | .srhtErrorBanner(error: $vm.error) | ||
| 119 | .task { | 134 | .task { |
| 120 | // Tags drive the picker above and are not otherwise needed by this tab. | 135 | // Tags drive the picker above and are not otherwise needed by this tab. |
| 121 | if isOwnedByCurrentUser, viewModel.tags.isEmpty { | 136 | if isOwnedByCurrentUser, viewModel.tags.isEmpty { |
| 122 | await viewModel.loadReferences() | 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 | .overlay { | 152 | .overlay { |
| 126 | if viewModel.isLoadingArtifacts, viewModel.referenceArtifacts.isEmpty { | 153 | if viewModel.isLoadingArtifacts, viewModel.referenceArtifacts.isEmpty { |