gitbay/Views/Issues/IssueListView.swift
110 lines · 3940 bytes
1import SwiftUI
2
3struct IssueListView: View {
4
5 @State private var model: IssueListViewModel
6 @State private var createModel: IssueCreateViewModel
7 @State private var composing = false
8 @State private var draftTitle = ""
9 @State private var draftBody = ""
10
11 init(client: GitbayClient, repo: String) {
12 _model = State(initialValue: IssueListViewModel(client: client, repoPath: repo))
13 _createModel = State(initialValue: IssueCreateViewModel(client: client, repoPath: repo))
14 }
15
16 var body: some View {
17 List {
18 Picker("State", selection: Bindable(model).filter) {
19 ForEach(IssueListViewModel.StateFilter.allCases) { filter in
20 Text(filter.rawValue.capitalized).tag(filter)
21 }
22 }
23 .pickerStyle(.segmented)
24 .listRowBackground(Color.clear)
25 .listRowInsets(EdgeInsets())
26
27 ForEach(model.state.value ?? []) { issue in
28 NavigationLink(value: IssueRoute.issue(repo: model.repoPath, number: issue.number)) {
29 IssueRow(issue: issue)
30 }
31 }
32 PageFooter(list: model.list)
33 }
34 .overlay { LoadStateOverlay(state: model.state) }
35 .navigationTitle("Issues")
36 .navigationBarTitleDisplayMode(.inline)
37 .toolbar {
38 ToolbarItem(placement: .topBarTrailing) {
39 Button {
40 composing = true
41 } label: {
42 Image(systemName: "plus")
43 }
44 .accessibilityIdentifier("issue-create-button")
45 }
46 }
47 .sheet(isPresented: $composing) {
48 ComposeSheet(
49 heading: "New Issue",
50 submitLabel: "Create",
51 working: createModel.working,
52 errorMessage: createModel.errorMessage,
53 title: $draftTitle,
54 bodyText: $draftBody
55 ) {
56 Task {
57 if await createModel.create(title: draftTitle, body: draftBody) != nil {
58 draftTitle = ""
59 draftBody = ""
60 composing = false
61 await model.load()
62 }
63 }
64 }
65 }
66 .task { await model.load() }
67 .refreshable { await model.load() }
68 }
69}
70
71private struct IssueRow: View {
72 let issue: Issue
73
74 var body: some View {
75 VStack(alignment: .leading, spacing: 4) {
76 HStack(alignment: .firstTextBaseline, spacing: 6) {
77 Text("#\(issue.number)")
78 .font(.gbMono(.caption))
79 .foregroundStyle(.secondary)
80 Text(issue.title)
81 .font(.gbSans(.subheadline).weight(.medium))
82 .lineLimit(2)
83 }
84 HStack(spacing: 6) {
85 Image(systemName: issue.isOpen ? "circle" : "checkmark.circle.fill")
86 .font(.gbSans(.caption2))
87 .foregroundStyle(issue.isOpen ? Color.gbOK : Color.gbBad)
88 ForEach(issue.labels ?? [], id: \.self) { label in
89 GBChip(label, .secondary)
90 }
91 Spacer()
92 if let assignees = issue.assignees, !assignees.isEmpty {
93 Text(assignees.joined(separator: ", "))
94 .font(.gbSans(.caption))
95 .foregroundStyle(.secondary)
96 .lineLimit(1)
97 }
98 Text(issue.createdAt, format: .relative(presentation: .named))
99 .font(.gbSans(.caption))
100 .foregroundStyle(.secondary)
101 }
102 }
103 .padding(.vertical, 2)
104 }
105}
106
107nonisolated enum IssueRoute: Hashable {
108 case list(repo: String)
109 case issue(repo: String, number: Int64)
110}