summaryrefslogtreecommitdiff
path: root/Hutch/Views/Tickets
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-04-12 22:06:16 -0500
committerChristian Cleberg <[email protected]>2026-04-12 22:06:16 -0500
commit86bcd9452d3107f68aca085e03e69b02667c0dbb (patch)
tree8b0adece5cb0f8ffb178eaab2cd12d025c9ebf0a /Hutch/Views/Tickets
parent31cbca674957eea7ae2529012e20a9d95158e561 (diff)
downloadhutch-86bcd9452d3107f68aca085e03e69b02667c0dbb.tar.gz
hutch-86bcd9452d3107f68aca085e03e69b02667c0dbb.tar.bz2
hutch-86bcd9452d3107f68aca085e03e69b02667c0dbb.zip
feat: polish read-only project views and home integration
Refs: https://todo.sr.ht/~ccleberg/hutch/36 Refs: https://todo.sr.ht/~ccleberg/hutch/37 Refs: https://todo.sr.ht/~ccleberg/hutch/38
Diffstat (limited to 'Hutch/Views/Tickets')
-rw-r--r--Hutch/Views/Tickets/TicketListView.swift209
1 files changed, 133 insertions, 76 deletions
diff --git a/Hutch/Views/Tickets/TicketListView.swift b/Hutch/Views/Tickets/TicketListView.swift
index fffaba4..3e0c656 100644
--- a/Hutch/Views/Tickets/TicketListView.swift
+++ b/Hutch/Views/Tickets/TicketListView.swift
@@ -194,75 +194,67 @@ struct TicketListView: View {
List {
Section {
- VStack(alignment: .leading, spacing: 12) {
- Picker("Filter", selection: $vm.filter) {
- ForEach(TicketFilter.allCases, id: \.self) { filter in
- Text(filter.rawValue).tag(filter)
- }
- }
- .pickerStyle(.segmented)
-
- TicketQuickFilterBar(
- selectedLabels: viewModel.selectedLabels,
- savedFilters: viewModel.savedFilters,
- activeSavedFilterID: viewModel.activeSavedFilterID,
- canSaveCurrentFilter: viewModel.hasCustomFilterSelection
- ) {
- showLabelFilterSheet = true
- } onSaveFilter: {
- showSaveFilterSheet = true
- } onResetFilters: {
- vm.resetFilters()
- } onApplySavedFilter: { savedFilter in
- vm.applySavedFilter(savedFilter)
- } onDeleteSavedFilter: { savedFilter in
- vm.deleteSavedFilter(savedFilter)
- }
+ TicketListFilterHeader(
+ selectedLabels: viewModel.selectedLabels,
+ savedFilters: viewModel.savedFilters,
+ activeSavedFilterID: viewModel.activeSavedFilterID,
+ canSaveCurrentFilter: viewModel.hasCustomFilterSelection,
+ filter: $vm.filter
+ ) {
+ showLabelFilterSheet = true
+ } onSaveFilter: {
+ showSaveFilterSheet = true
+ } onResetFilters: {
+ vm.resetFilters()
+ } onApplySavedFilter: { savedFilter in
+ vm.applySavedFilter(savedFilter)
+ } onDeleteSavedFilter: { savedFilter in
+ vm.deleteSavedFilter(savedFilter)
}
- .padding(.vertical, 4)
- .listRowBackground(Color.clear)
.listRowInsets(EdgeInsets())
- }
+ .listRowBackground(Color.clear)
+ .listRowSeparator(.hidden)
- // Tickets
- ForEach(viewModel.filteredTickets) { ticket in
- NavigationLink {
- TicketDetailView(
- ownerUsername: String(tracker.owner.canonicalName.dropFirst()),
- trackerName: tracker.name,
- trackerId: tracker.id,
- trackerRid: tracker.rid,
- ticketId: ticket.id
- )
- } label: {
- TicketRowView(ticket: ticket)
- }
- .swipeActions(edge: .leading, allowsFullSwipe: true) {
- if swipeActionsEnabled {
- ticketAssignSwipeAction(ticket, viewModel: viewModel)
+ ForEach(viewModel.filteredTickets) { ticket in
+ NavigationLink {
+ TicketDetailView(
+ ownerUsername: String(tracker.owner.canonicalName.dropFirst()),
+ trackerName: tracker.name,
+ trackerId: tracker.id,
+ trackerRid: tracker.rid,
+ ticketId: ticket.id
+ )
+ } label: {
+ TicketRowView(ticket: ticket)
}
- }
- .swipeActions(edge: .trailing, allowsFullSwipe: false) {
- if swipeActionsEnabled {
- ticketStatusSwipeAction(ticket, viewModel: viewModel)
- ticketLabelSwipeAction(ticket, viewModel: viewModel)
+ .swipeActions(edge: .leading, allowsFullSwipe: true) {
+ if swipeActionsEnabled {
+ ticketAssignSwipeAction(ticket, viewModel: viewModel)
+ }
+ }
+ .swipeActions(edge: .trailing, allowsFullSwipe: false) {
+ if swipeActionsEnabled {
+ ticketStatusSwipeAction(ticket, viewModel: viewModel)
+ ticketLabelSwipeAction(ticket, viewModel: viewModel)
+ }
+ }
+ .task {
+ await viewModel.loadMoreIfNeeded(currentItem: ticket)
}
}
- .task {
- await viewModel.loadMoreIfNeeded(currentItem: ticket)
- }
- }
- if viewModel.isLoadingMore {
- HStack {
- Spacer()
- ProgressView()
- Spacer()
+ if viewModel.isLoadingMore {
+ HStack {
+ Spacer()
+ ProgressView()
+ Spacer()
+ }
+ .listRowSeparator(.hidden)
}
- .listRowSeparator(.hidden)
}
}
.listStyle(.plain)
+ .listSectionSpacing(.compact)
.searchable(
text: $vm.searchText,
placement: .navigationBarDrawer(displayMode: .always),
@@ -538,11 +530,12 @@ private struct TicketLabelsSheet: View {
}
}
-private struct TicketQuickFilterBar: View {
+private struct TicketListFilterHeader: View {
let selectedLabels: [TicketLabel]
let savedFilters: [SavedTicketFilter]
let activeSavedFilterID: SavedTicketFilter.ID?
let canSaveCurrentFilter: Bool
+ @Binding var filter: TicketFilter
let onShowLabels: () -> Void
let onSaveFilter: () -> Void
let onResetFilters: () -> Void
@@ -551,26 +544,45 @@ private struct TicketQuickFilterBar: View {
var body: some View {
VStack(alignment: .leading, spacing: 10) {
- HStack(spacing: 8) {
- Button(action: onShowLabels) {
- Label(labelButtonTitle, systemImage: "tag")
- .font(.caption.weight(.medium))
+ Picker("Filter", selection: $filter) {
+ ForEach(TicketFilter.allCases, id: \.self) { filter in
+ Text(filter.rawValue).tag(filter)
}
- .buttonStyle(.bordered)
+ }
+ .pickerStyle(.segmented)
+
+ TicketQuickFilterBar(
+ selectedLabels: selectedLabels,
+ savedFilters: savedFilters,
+ activeSavedFilterID: activeSavedFilterID,
+ canSaveCurrentFilter: canSaveCurrentFilter,
+ onShowLabels: onShowLabels,
+ onSaveFilter: onSaveFilter,
+ onResetFilters: onResetFilters,
+ onApplySavedFilter: onApplySavedFilter,
+ onDeleteSavedFilter: onDeleteSavedFilter
+ )
+ }
+ .padding(.horizontal, 16)
+ .padding(.top, 6)
+ .padding(.bottom, 10)
+ }
+}
- Button(action: onSaveFilter) {
- Label("Save Filter", systemImage: "square.and.arrow.down")
- .font(.caption.weight(.medium))
- }
- .buttonStyle(.bordered)
- .disabled(!canSaveCurrentFilter)
+private struct TicketQuickFilterBar: View {
+ let selectedLabels: [TicketLabel]
+ let savedFilters: [SavedTicketFilter]
+ let activeSavedFilterID: SavedTicketFilter.ID?
+ let canSaveCurrentFilter: Bool
+ let onShowLabels: () -> Void
+ let onSaveFilter: () -> Void
+ let onResetFilters: () -> Void
+ let onApplySavedFilter: (SavedTicketFilter) -> Void
+ let onDeleteSavedFilter: (SavedTicketFilter) -> Void
- if canSaveCurrentFilter {
- Button("Reset", action: onResetFilters)
- .font(.caption.weight(.medium))
- .buttonStyle(.bordered)
- }
- }
+ var body: some View {
+ VStack(alignment: .leading, spacing: 8) {
+ actionButtons
if !selectedLabels.isEmpty {
ScrollView(.horizontal, showsIndicators: false) {
@@ -623,6 +635,51 @@ private struct TicketQuickFilterBar: View {
private var labelButtonTitle: String {
selectedLabels.isEmpty ? "Labels" : "Labels (\(selectedLabels.count))"
}
+
+ @ViewBuilder
+ private var actionButtons: some View {
+ ViewThatFits(in: .horizontal) {
+ HStack(spacing: 8) {
+ labelsButton
+ saveFilterButton
+ if canSaveCurrentFilter {
+ resetButton
+ }
+ Spacer(minLength: 0)
+ }
+
+ FlowLayout(spacing: 8) {
+ labelsButton
+ saveFilterButton
+ if canSaveCurrentFilter {
+ resetButton
+ }
+ }
+ }
+ }
+
+ private var labelsButton: some View {
+ Button(action: onShowLabels) {
+ Label(labelButtonTitle, systemImage: "tag")
+ .font(.caption.weight(.medium))
+ }
+ .buttonStyle(.bordered)
+ }
+
+ private var saveFilterButton: some View {
+ Button(action: onSaveFilter) {
+ Label("Save Filter", systemImage: "square.and.arrow.down")
+ .font(.caption.weight(.medium))
+ }
+ .buttonStyle(.bordered)
+ .disabled(!canSaveCurrentFilter)
+ }
+
+ private var resetButton: some View {
+ Button("Reset", action: onResetFilters)
+ .font(.caption.weight(.medium))
+ .buttonStyle(.bordered)
+ }
}
private struct TicketFilterLabelsSheet: View {