summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Hutch/App/RootView.swift48
-rw-r--r--Hutch/Views/Lists/MailingListListView.swift3
-rw-r--r--Hutch/Views/Pastes/PasteListView.swift3
-rw-r--r--Hutch/Views/Repositories/ArtifactsView.swift52
-rw-r--r--Hutch/Views/Tickets/TrackerListView.swift3
-rw-r--r--Hutch/Views/Tickets/TrackerManagementView.swift6
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()