diff options
| author | Christian Cleberg <[email protected]> | 2026-07-16 00:29:34 -0500 |
|---|---|---|
| committer | Christian Cleberg <[email protected]> | 2026-07-16 00:29:34 -0500 |
| commit | 6898bc3fc00decee7224895ea75908daf0f97f59 (patch) | |
| tree | ee4cdf2760d45586d20c106212a6cfb1b7d0d9aa /Hutch | |
| parent | 2514b58a96ccb73b4feea8f74a2367c72ba824c8 (diff) | |
| download | hutch-6898bc3fc00decee7224895ea75908daf0f97f59.tar.gz hutch-6898bc3fc00decee7224895ea75908daf0f97f59.tar.bz2 hutch-6898bc3fc00decee7224895ea75908daf0f97f59.zip | |
fix: blank mailing list from Projects, swipe flicker, hidden upload
Three problems from manual testing.
Opening a mailing list from More → Projects showed a blank screen, while the
same tap on a project pinned to Home worked. handleTabNavigation reset the
target path and appended to it two Task.yields later. When the target tab is
already on screen — Projects lives under More — the reset starts an animated pop
of the view the user is standing on and the appends land mid-animation. From
Home the tab actually changes, so the More stack is quiescent and the appends
land cleanly. Each case now builds its path and assigns it once, so SwiftUI gets
a single diff with nothing to race.
Destructive swipe actions made the row vanish and spring back while the
confirmation was still up. role: .destructive makes SwiftUI perform the row
removal on activation, which allowsFullSwipe: false does not prevent — the
report was a tap, not a full swipe. These buttons only record pending state and
wait for an answer, so they are plain buttons tinted red instead. Six sites: the
two added here, plus trackers, pastes, and tracker ACLs and labels, which had
the same flicker already.
The artifacts upload control was invisible. It was declared as a toolbar item
from a view that is a segment inside RepositoryDetailView's tab switch rather
than its own navigation destination, so it never reached the navigation bar. It
is a row in the list now, and also an action on the empty state — the overlay
covers the list, and a repository with no artifacts is precisely the one that
needs uploading.
Diffstat (limited to 'Hutch')
| -rw-r--r-- | Hutch/App/RootView.swift | 48 | ||||
| -rw-r--r-- | Hutch/Views/Lists/MailingListListView.swift | 3 | ||||
| -rw-r--r-- | Hutch/Views/Pastes/PasteListView.swift | 3 | ||||
| -rw-r--r-- | Hutch/Views/Repositories/ArtifactsView.swift | 52 | ||||
| -rw-r--r-- | Hutch/Views/Tickets/TrackerListView.swift | 3 | ||||
| -rw-r--r-- | Hutch/Views/Tickets/TrackerManagementView.swift | 6 |
6 files changed, 69 insertions, 46 deletions
diff --git a/Hutch/App/RootView.swift b/Hutch/App/RootView.swift index 32d1624..0194d63 100644 --- a/Hutch/App/RootView.swift +++ b/Hutch/App/RootView.swift @@ -291,39 +291,45 @@ struct RootView: View { } } + /// Replaces the target tab's path in one assignment. + /// + /// Resetting the path and appending to it afterwards races when the target tab + /// is already the one on screen: the reset starts an animated pop of the view + /// the user is standing on, and the appends land mid-animation, leaving a blank + /// screen. That is why opening a mailing list from a pinned project on Home + /// worked while the same tap under More → Projects did not — one changes tabs + /// and the other does not. + /// + /// Building the whole path first and assigning once gives SwiftUI a single + /// diff, with nothing to race. private func handleTabNavigation(_ target: AppState.TabNavigationTarget) { switch target { case .repository(let repository): - repoPath = NavigationPath() + var path = NavigationPath() + path.append(repository) + repoPath = path appState.selectedTab = .repositories - Task { - await settleNavigationTransition() - repoPath.append(repository) - } case .tracker(let tracker): - ticketsPath = NavigationPath() + var path = NavigationPath() + path.append(tracker) + ticketsPath = path appState.selectedTab = .tickets - Task { - await settleNavigationTransition() - ticketsPath.append(tracker) - } case .mailingList(let mailingList): - morePath = NavigationPath() + // .lists first so back lands on Mailing Lists rather than dead-ending. + var path = NavigationPath() + path.append(MoreRoute.lists) + path.append(MoreRoute.mailingList(mailingList)) + morePath = path appState.selectedTab = .more - Task { - await settleNavigationTransition() - morePath.append(MoreRoute.lists) - morePath.append(MoreRoute.mailingList(mailingList)) - } + case .systemStatus: - morePath = NavigationPath() + var path = NavigationPath() + path.append(MoreRoute.systemStatus) + morePath = path appState.selectedTab = .more - Task { - await settleNavigationTransition() - morePath.append(MoreRoute.systemStatus) - } + case .builds: buildsPath = NavigationPath() appState.selectedTab = .builds diff --git a/Hutch/Views/Lists/MailingListListView.swift b/Hutch/Views/Lists/MailingListListView.swift index 77df89b..1b159bf 100644 --- a/Hutch/Views/Lists/MailingListListView.swift +++ b/Hutch/Views/Lists/MailingListListView.swift @@ -373,11 +373,12 @@ struct MailingListListView: View { // the data has not actually changed. .swipeActions(edge: .trailing, allowsFullSwipe: false) { if isOwned(mailingList) { - Button(role: .destructive) { + Button { pendingDeletion = mailingList } label: { SwiftUI.Label("Delete", systemImage: "trash") } + .tint(.red) Button { editingList = mailingList } label: { diff --git a/Hutch/Views/Pastes/PasteListView.swift b/Hutch/Views/Pastes/PasteListView.swift index b325153..b2c7838 100644 --- a/Hutch/Views/Pastes/PasteListView.swift +++ b/Hutch/Views/Pastes/PasteListView.swift @@ -90,11 +90,12 @@ struct PasteListView: View { } .swipeActions(edge: .trailing, allowsFullSwipe: false) { if swipeActionsEnabled { - Button(role: .destructive) { + Button { pasteToDelete = paste } label: { Label("Delete", systemImage: "trash") } + .tint(.red) } } .task { diff --git a/Hutch/Views/Repositories/ArtifactsView.swift b/Hutch/Views/Repositories/ArtifactsView.swift index 037c092..752c7c5 100644 --- a/Hutch/Views/Repositories/ArtifactsView.swift +++ b/Hutch/Views/Repositories/ArtifactsView.swift @@ -16,6 +16,21 @@ struct ArtifactsView: View { var body: some View { 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 + // reach the navigation bar. It also has to be reachable when there are + // no artifacts at all, which is the state a new tag is in. + if isOwnedByCurrentUser { + Button { + showTagPicker = true + } label: { + SwiftUI.Label("Upload Artifact…", systemImage: "square.and.arrow.up") + } + .disabled(viewModel.isMutatingArtifact || viewModel.tags.isEmpty) + .themedRow() + } + ForEach(viewModel.referenceArtifacts) { refArtifacts in Section { ForEach(refArtifacts.artifacts) { artifact in @@ -26,11 +41,12 @@ struct ArtifactsView: View { // action animates the row out before the confirmation. .swipeActions(edge: .trailing, allowsFullSwipe: false) { if isOwnedByCurrentUser { - Button(role: .destructive) { + Button { pendingDeletion = artifact } label: { SwiftUI.Label("Delete", systemImage: "trash") } + .tint(.red) } } } @@ -83,20 +99,6 @@ struct ArtifactsView: View { } message: { _ in Text("This permanently removes the artifact from the tag. This cannot be undone.") } - // The sections above only list tags that already have an artifact, so - // without this there would be no way to attach the first one to a tag. - .toolbar { - if isOwnedByCurrentUser { - ToolbarItem(placement: .topBarTrailing) { - Button { - showTagPicker = true - } label: { - SwiftUI.Label("Upload Artifact", systemImage: "square.and.arrow.up") - } - .disabled(viewModel.isMutatingArtifact || viewModel.tags.isEmpty) - } - } - } .confirmationDialog("Upload to Tag", isPresented: $showTagPicker, titleVisibility: .visible) { ForEach(viewModel.tags.prefix(12), id: \.name) { tag in Button(RepositorySummary.displayBranchName(for: tag.name)) { @@ -125,11 +127,21 @@ struct ArtifactsView: View { retryAction: { await viewModel.loadArtifacts() } ) } else if viewModel.referenceArtifacts.isEmpty { - ContentUnavailableView( - "No Artifacts", - systemImage: "archivebox", - description: Text("This repository has no release artifacts.") - ) + // The overlay covers the whole list, so the upload row above is + // hidden underneath it — and a repository with no artifacts is + // exactly the one that needs uploading. Offer it here too. + ContentUnavailableView { + SwiftUI.Label("No Artifacts", systemImage: "archivebox") + } description: { + Text("This repository has no release artifacts.") + } actions: { + if isOwnedByCurrentUser { + Button("Upload Artifact…") { + showTagPicker = true + } + .disabled(viewModel.isMutatingArtifact || viewModel.tags.isEmpty) + } + } } } .task { diff --git a/Hutch/Views/Tickets/TrackerListView.swift b/Hutch/Views/Tickets/TrackerListView.swift index 5deb513..cb4a59c 100644 --- a/Hutch/Views/Tickets/TrackerListView.swift +++ b/Hutch/Views/Tickets/TrackerListView.swift @@ -145,11 +145,12 @@ struct TrackerListView: View { TrackerRowView(tracker: tracker) } .swipeActions(edge: .trailing, allowsFullSwipe: false) { - Button(role: .destructive) { + Button { pendingDeletion = tracker } label: { Label("Delete", systemImage: "trash") } + .tint(.red) Button { editingTracker = tracker diff --git a/Hutch/Views/Tickets/TrackerManagementView.swift b/Hutch/Views/Tickets/TrackerManagementView.swift index fad1ae8..73b2a08 100644 --- a/Hutch/Views/Tickets/TrackerManagementView.swift +++ b/Hutch/Views/Tickets/TrackerManagementView.swift @@ -751,11 +751,12 @@ struct TrackerACLManagementSheet: View { TrackerPermissionSummary(permissions: entry.permissions) } .swipeActions(edge: .trailing, allowsFullSwipe: false) { - Button(role: .destructive) { + Button { pendingDeletion = entry } label: { Label("Delete", systemImage: "trash") } + .tint(.red) Button { editingACL = entry @@ -1132,11 +1133,12 @@ struct TrackerLabelManagementSheet: View { } .tint(.blue) - Button(role: .destructive) { + Button { pendingDeletion = label } label: { Label("Delete", systemImage: "trash") } + .tint(.red) } } .themedRow() |
