gitbay/Views/Issues/IssueListView.swift
114 lines · 4086 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(.caption.monospaced())
79 .foregroundStyle(.secondary)
80 Text(issue.title)
81 .font(.subheadline.weight(.medium))
82 .lineLimit(2)
83 }
84 HStack(spacing: 6) {
85 Image(systemName: issue.isOpen ? "circle" : "checkmark.circle.fill")
86 .font(.caption2)
87 .foregroundStyle(issue.isOpen ? .green : .purple)
88 ForEach(issue.labels ?? [], id: \.self) { label in
89 Text(label)
90 .font(.caption2)
91 .padding(.horizontal, 5)
92 .padding(.vertical, 1)
93 .background(.quaternary, in: Capsule())
94 }
95 Spacer()
96 if let assignees = issue.assignees, !assignees.isEmpty {
97 Text(assignees.joined(separator: ", "))
98 .font(.caption)
99 .foregroundStyle(.secondary)
100 .lineLimit(1)
101 }
102 Text(issue.createdAt, format: .relative(presentation: .named))
103 .font(.caption)
104 .foregroundStyle(.tertiary)
105 }
106 }
107 .padding(.vertical, 2)
108 }
109}
110
111nonisolated enum IssueRoute: Hashable {
112 case list(repo: String)
113 case issue(repo: String, number: Int64)
114}