diff options
| author | Christian Cleberg <[email protected]> | 2026-04-12 22:06:16 -0500 |
|---|---|---|
| committer | Christian Cleberg <[email protected]> | 2026-04-12 22:06:16 -0500 |
| commit | 86bcd9452d3107f68aca085e03e69b02667c0dbb (patch) | |
| tree | 8b0adece5cb0f8ffb178eaab2cd12d025c9ebf0a /Hutch/Views/Tickets | |
| parent | 31cbca674957eea7ae2529012e20a9d95158e561 (diff) | |
| download | hutch-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.swift | 209 |
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 { |
