gitbay/Views/Dashboard/DashboardView.swift
166 lines · 5758 bytes
1import SwiftUI
2
3/// The native representation of `gitbay dashboard`, ordered and phrased
4/// like the logged-in web dashboard.
5struct DashboardView: View {
6
7 @Environment(SessionStore.self) private var session
8 @State private var model: DashboardViewModel
9 @State private var createModel: RepoCreateViewModel
10 @State private var composing = false
11
12 init(client: GitbayClient) {
13 _model = State(initialValue: DashboardViewModel(client: client))
14 _createModel = State(initialValue: RepoCreateViewModel(client: client))
15 }
16
17 var body: some View {
18 List {
19 if let data = model.state.value {
20 itemSection(
21 "Waiting on your review", items: data.reviewQueue,
22 empty: "Nothing waiting on you", marker: "!"
23 ) { MRRoute.mr(repo: $0.repo, number: $0.number) }
24
25 itemSection(
26 "Assigned to you", items: data.assignedIssues,
27 empty: "Nothing assigned to you", marker: "#"
28 ) { IssueRoute.issue(repo: $0.repo, number: $0.number) }
29
30 itemSection(
31 "Open merge requests", items: data.openMRs,
32 empty: "No open merge requests", marker: "!"
33 ) { MRRoute.mr(repo: $0.repo, number: $0.number) }
34
35 itemSection(
36 "Open issues", items: data.openIssues,
37 empty: "No open issues", marker: "#"
38 ) { IssueRoute.issue(repo: $0.repo, number: $0.number) }
39
40 pinnedSection(data.pinned)
41 activitySection(data.recentActivity)
42 }
43 }
44 .overlay { LoadStateOverlay(state: model.state) }
45 .navigationTitle("Dashboard")
46 .toolbar {
47 ToolbarItem(placement: .topBarTrailing) {
48 Button {
49 composing = true
50 } label: {
51 Image(systemName: "plus")
52 }
53 .accessibilityIdentifier("dashboard-create-button")
54 }
55 }
56 .sheet(isPresented: $composing) {
57 RepoCreateSheet(
58 model: createModel,
59 ownerPrefix: session.current?.username ?? ""
60 ) {
61 composing = false
62 Task { await model.load() }
63 }
64 }
65 .task { await model.load() }
66 .refreshable { await model.load() }
67 }
68
69 private func itemSection(
70 _ title: String,
71 items: [DashboardItem],
72 empty: String,
73 marker: String,
74 route: @escaping (DashboardItem) -> some Hashable
75 ) -> some View {
76 Section {
77 if items.isEmpty {
78 Text(empty)
79 .font(.gbSans(.subheadline))
80 .foregroundStyle(.secondary)
81 } else {
82 ForEach(items) { item in
83 NavigationLink(value: route(item)) {
84 dashboardItem(item, marker: marker)
85 }
86 }
87 }
88 } header: {
89 sectionHeader(title, count: items.count)
90 }
91 }
92
93 private func dashboardItem(_ item: DashboardItem, marker: String) -> some View {
94 VStack(alignment: .leading, spacing: 3) {
95 Text(item.title)
96 .font(.gbSans(.subheadline).weight(.semibold))
97 .lineLimit(2)
98 HStack(spacing: 4) {
99 Text(item.repo + marker + String(item.number))
100 .font(.gbSans(.caption))
101 Text("·")
102 Text(item.author)
103 .foregroundStyle(Color.gbAccent)
104 Text("·")
105 Text(item.updatedAt, format: .relative(presentation: .named))
106 if item.state == "source_gone" {
107 Text("·")
108 GBChip("source gone", .secondary)
109 }
110 }
111 .font(.gbSans(.caption))
112 .foregroundStyle(.secondary)
113 .lineLimit(1)
114 }
115 .padding(.vertical, 2)
116 }
117
118 private func pinnedSection(_ pinned: [RepoSummary]) -> some View {
119 Section {
120 if pinned.isEmpty {
121 Text("Pin a repository to keep it here")
122 .font(.gbSans(.subheadline))
123 .foregroundStyle(.secondary)
124 } else {
125 ForEach(pinned) { repo in
126 NavigationLink(value: RepoRoute.repo(repo.path)) {
127 HStack(spacing: 6) {
128 Text(repo.path)
129 .font(.gbSans(.subheadline).weight(.medium))
130 .foregroundStyle(Color.gbAccent)
131 if repo.isPrivate {
132 GBChip("Private", .secondary)
133 }
134 }
135 }
136 }
137 }
138 } header: {
139 Text("Pinned")
140 }
141 }
142
143 private func activitySection(_ events: [FeedEvent]) -> some View {
144 Section {
145 if events.isEmpty {
146 Text("No activity yet")
147 .font(.gbSans(.subheadline))
148 .foregroundStyle(.secondary)
149 } else {
150 ForEach(events) { event in
151 FeedLink(event: event)
152 }
153 }
154 } header: {
155 Text("Recent activity")
156 }
157 }
158
159 private func sectionHeader(_ title: String, count: Int) -> some View {
160 HStack(alignment: .firstTextBaseline, spacing: 4) {
161 Text(title)
162 Text(String(count))
163 .foregroundStyle(.secondary)
164 }
165 }
166}