Commit 2790ec0678

2790ec06788e63b96e0bb6b1db504a026ffd22e8

parent: 2a56dd7f00

Unsigned

cmc <hello@cleberg.net> · 2026-04-13 00:16 UTC

feat: improve ticket list actions and metadata visibility

Implements: https://todo.sr.ht/~ccleberg/hutch/20
Implements: https://todo.sr.ht/~ccleberg/hutch/21

Layout: unified · split

Hutch/Views/Tickets/TicketListView.swift +71 −48
@@ -205,12 +205,13 @@ struct TicketListView: View {
205205 }
206206 .swipeActions(edge: .leading, allowsFullSwipe: true) {
207207 if swipeActionsEnabled {
208 ticketLeadingSwipeAction(ticket, viewModel: viewModel)
208 ticketAssignSwipeAction(ticket, viewModel: viewModel)
209209 }
210210 }
211211 .swipeActions(edge: .trailing, allowsFullSwipe: false) {
212212 if swipeActionsEnabled {
213 ticketTrailingSwipeActions(ticket, viewModel: viewModel)
213 ticketStatusSwipeAction(ticket, viewModel: viewModel)
214 ticketLabelSwipeAction(ticket, viewModel: viewModel)
214215 }
215216 }
216217 .task {
@@ -294,52 +295,10 @@ struct TicketListView: View {
294295 }
295296
296297 @ViewBuilder
297 private func ticketLeadingSwipeAction(
298 private func ticketAssignSwipeAction(
298299 _ ticket: TicketSummary,
299300 viewModel: TicketListViewModel
300301 ) -> some View {
301 if ticket.status.isOpen {
302 Button {
303 Task {
304 await resolveTicket(ticket, viewModel: viewModel)
305 }
306 } label: {
307 Label("Resolve", systemImage: "checkmark.circle")
308 }
309 .tint(.green)
310 } else {
311 Button {
312 Task {
313 await reopenTicket(ticket, viewModel: viewModel)
314 }
315 } label: {
316 Label("Reopen", systemImage: "arrow.uturn.backward")
317 }
318 .tint(.blue)
319 }
320 }
321
322 private func resolveTicket(_ ticket: TicketSummary, viewModel: TicketListViewModel) async {
323 await viewModel.resolveTicket(ticket)
324 }
325
326 private func reopenTicket(_ ticket: TicketSummary, viewModel: TicketListViewModel) async {
327 await viewModel.reopenTicket(ticket)
328 }
329
330 @ViewBuilder
331 private func ticketTrailingSwipeActions(
332 _ ticket: TicketSummary,
333 viewModel: TicketListViewModel
334 ) -> some View {
335 Button {
336 labelEditorTicket = LabelEditorTicket(id: ticket.id)
337 Task { await viewModel.loadTrackerLabels() }
338 } label: {
339 Label("Edit Labels", systemImage: "tag")
340 }
341 .tint(.purple)
342
343302 if let currentUser = appState.currentUser {
344303 let isAssigned = ticket.assignees.contains { assignee in
345304 matchesAssignee(assignee, user: currentUser)
@@ -367,6 +326,42 @@ struct TicketListView: View {
367326 }
368327 }
369328
329 @ViewBuilder
330 private func ticketStatusSwipeAction(
331 _ ticket: TicketSummary,
332 viewModel: TicketListViewModel
333 ) -> some View {
334 if ticket.status.isOpen {
335 Button {
336 Task { await viewModel.resolveTicket(ticket) }
337 } label: {
338 Label("Close", systemImage: "xmark.circle")
339 }
340 .tint(.red)
341 } else {
342 Button {
343 Task { await viewModel.reopenTicket(ticket) }
344 } label: {
345 Label("Reopen", systemImage: "arrow.uturn.backward")
346 }
347 .tint(.blue)
348 }
349 }
350
351 @ViewBuilder
352 private func ticketLabelSwipeAction(
353 _ ticket: TicketSummary,
354 viewModel: TicketListViewModel
355 ) -> some View {
356 Button {
357 labelEditorTicket = LabelEditorTicket(id: ticket.id)
358 Task { await viewModel.loadTrackerLabels() }
359 } label: {
360 Label("Labels", systemImage: "tag")
361 }
362 .tint(.purple)
363 }
364
370365 private func matchesAssignee(_ entity: Entity, user: User) -> Bool {
371366 let assigneeCanonical = normalizedCanonicalName(entity.canonicalName)
372367 let userCanonical = normalizedCanonicalName(user.canonicalName)
@@ -546,6 +541,8 @@ private struct TicketRowView: View {
546541 }
547542
548543 Spacer()
544
545 TicketStatusBadge(status: ticket.status)
549546 }
550547
551548 HStack(spacing: 8) {
@@ -584,6 +581,32 @@ private struct TicketRowView: View {
584581 }
585582}
586583
584// MARK: - Ticket Status Badge
585
586private struct TicketStatusBadge: View {
587 let status: TicketStatus
588
589 var body: some View {
590 Text(status.displayName)
591 .font(.caption2.weight(.semibold))
592 .padding(.horizontal, 6)
593 .padding(.vertical, 2)
594 .foregroundStyle(color)
595 .background(color.opacity(0.12))
596 .clipShape(Capsule())
597 }
598
599 private var color: Color {
600 switch status {
601 case .reported: .gray
602 case .confirmed: .blue
603 case .inProgress: .yellow
604 case .pending: .orange
605 case .resolved: .green
606 }
607 }
608}
609
587610// MARK: - Ticket Status Icon
588611
589612struct TicketStatusIcon: View {
@@ -622,9 +645,9 @@ struct LabelPill: View {
622645
623646 var body: some View {
624647 Text(label.name)
625 .font(.caption2.weight(.medium))
626 .padding(.horizontal, 6)
627 .padding(.vertical, 2)
648 .font(.caption.weight(.medium))
649 .padding(.horizontal, 8)
650 .padding(.vertical, 3)
628651 .background(backgroundColor)
629652 .foregroundStyle(foregroundColor)
630653 .clipShape(Capsule())