Commit 52faf7192c
52faf7192c3afff5d52a11e264a5be9a799cb585
parent: 045cba839a
Verified · cmc
cmc <hello@cleberg.net> · 2026-09-22 21:10 UTC
views: share the chip row and the label picker
Merge requests are about to carry labels too; both screens use one
copy.
Layout: unified · split
gitbay/Views/Issues/IssueView.swift
+6 −37
| @@ -91,9 +91,9 @@ struct IssueView: View { |
| 91 | @ViewBuilder |
91 | @ViewBuilder |
| 92 | private func triageSection(_ issue: IssueDetail) -> some View { |
92 | private func triageSection(_ issue: IssueDetail) -> some View { |
| 93 | Section("Labels") { |
93 | Section("Labels") { |
| 94 | labelFlow(issue.labels ?? [], color: { model.colors.color($0) }, remove: { label in |
94 | RemovableChips(items: issue.labels ?? [], idPrefix: "issue-label", |
| 95 | Task { await model.removeLabel(label) } |
95 | disabled: model.working, color: { model.colors.color($0) }, |
| 96 | }) |
96 | remove: { label in Task { await model.removeLabel(label) } }) |
| 97 | HStack { |
97 | HStack { |
| 98 | TextField("Add label", text: $editingLabel) |
98 | TextField("Add label", text: $editingLabel) |
| 99 | .autocorrectionDisabled() |
99 | .autocorrectionDisabled() |
| @@ -109,9 +109,9 @@ struct IssueView: View { |
| 109 | } |
109 | } |
| 110 | } |
110 | } |
| 111 | Section("Assignees") { |
111 | Section("Assignees") { |
| 112 | labelFlow(issue.assignees ?? [], remove: { user in |
112 | RemovableChips(items: issue.assignees ?? [], idPrefix: "issue-assignee", |
| 113 | Task { await model.removeAssignee(user) } |
113 | disabled: model.working, |
| 114 | }) |
114 | remove: { user in Task { await model.removeAssignee(user) } }) |
| 115 | HStack { |
115 | HStack { |
| 116 | TextField("Assign user", text: $editingAssignee) |
116 | TextField("Assign user", text: $editingAssignee) |
| 117 | .autocorrectionDisabled() |
117 | .autocorrectionDisabled() |
| @@ -152,37 +152,6 @@ struct IssueView: View { |
| 152 | } |
152 | } |
| 153 | } |
153 | } |
| 154 | |
154 | |
| 155 | @ViewBuilder |
| |
| 156 | private func labelFlow( |
| |
| 157 | _ items: [String], |
| |
| 158 | color: @escaping (String) -> Color = { _ in .secondary }, |
| |
| 159 | remove: @escaping (String) -> Void |
| |
| 160 | ) -> some View { |
| |
| 161 | if !items.isEmpty { |
| |
| 162 | ScrollView(.horizontal, showsIndicators: false) { |
| |
| 163 | HStack(spacing: 6) { |
| |
| 164 | ForEach(items, id: \.self) { item in |
| |
| 165 | HStack(spacing: 3) { |
| |
| 166 | Text(item) |
| |
| 167 | Button { |
| |
| 168 | remove(item) |
| |
| 169 | } label: { |
| |
| 170 | Image(systemName: "xmark.circle.fill") |
| |
| 171 | .foregroundStyle(.tertiary) |
| |
| 172 | } |
| |
| 173 | .disabled(model.working) |
| |
| 174 | } |
| |
| 175 | .font(.gbSans(.caption)) |
| |
| 176 | .padding(.horizontal, 8) |
| |
| 177 | .padding(.vertical, 3) |
| |
| 178 | .background(color(item).opacity(0.07), in: gbChipShape) |
| |
| 179 | .overlay(gbChipShape.stroke(color(item).opacity(0.35), lineWidth: 1)) |
| |
| 180 | } |
| |
| 181 | } |
| |
| 182 | } |
| |
| 183 | } |
| |
| 184 | } |
| |
| 185 | |
| |
| 186 | private func commentsSection(_ comments: [IssueDetail.Comment]) -> some View { |
155 | private func commentsSection(_ comments: [IssueDetail.Comment]) -> some View { |
| 187 | Section("Comments") { |
156 | Section("Comments") { |
| 188 | ForEach(comments) { comment in |
157 | ForEach(comments) { comment in |
gitbay/Views/Shared/FilterSheet.swift
+19 −6
| @@ -2,8 +2,8 @@ import SwiftUI |
| 2 | |
2 | |
| 3 | /// The issue and MR lists' filter sheet. A `Form` with the fields both |
3 | /// The issue and MR lists' filter sheet. A `Form` with the fields both |
| 4 | /// lists share — author and milestone — plus an `extra` slot for the |
4 | /// lists share — author and milestone — plus an `extra` slot for the |
| 5 | /// rows only issues have, since `mr list` takes neither `--label` nor |
5 | /// rows each list adds: the label picker on both, and an assignee field |
| 6 | /// `--assignee`. |
6 | /// on issues only, since `mr list` takes no `--assignee`. |
| 7 | struct FilterSheet<Extra: View>: View { |
7 | struct FilterSheet<Extra: View>: View { |
| 8 | |
8 | |
| 9 | let title: String |
9 | let title: String |
| @@ -58,12 +58,11 @@ struct FilterSheet<Extra: View>: View { |
| 58 | } |
58 | } |
| 59 | } |
59 | } |
| 60 | |
60 | |
| 61 | /// The label and assignee rows the issue list's sheet adds through |
61 | /// The label picker both lists' sheets add through `FilterSheet`'s |
| 62 | /// `FilterSheet`'s `extra` slot. |
62 | /// `extra` slot. |
| 63 | struct IssueFilterExtra: View { |
63 | struct LabelFilterSection: View { |
| 64 | |
64 | |
| 65 | @Binding var label: String? |
65 | @Binding var label: String? |
| 66 | @Binding var assignee: String? |
| |
| 67 | let labels: [IssueLabel] |
66 | let labels: [IssueLabel] |
| 68 | let colors: LabelColors |
67 | let colors: LabelColors |
| 69 | |
68 | |
| @@ -93,6 +92,20 @@ struct IssueFilterExtra: View { |
| 93 | } |
92 | } |
| 94 | .accessibilityIdentifier("filter-label-menu") |
93 | .accessibilityIdentifier("filter-label-menu") |
| 95 | } |
94 | } |
| |
95 | } |
| |
96 | } |
| |
97 | |
| |
98 | /// The label and assignee rows the issue list's sheet adds through |
| |
99 | /// `FilterSheet`'s `extra` slot. |
| |
100 | struct IssueFilterExtra: View { |
| |
101 | |
| |
102 | @Binding var label: String? |
| |
103 | @Binding var assignee: String? |
| |
104 | let labels: [IssueLabel] |
| |
105 | let colors: LabelColors |
| |
106 | |
| |
107 | var body: some View { |
| |
108 | LabelFilterSection(label: $label, labels: labels, colors: colors) |
| 96 | Section("Assignee") { |
109 | Section("Assignee") { |
| 97 | TextField("Username", text: $assignee.orEmpty) |
110 | TextField("Username", text: $assignee.orEmpty) |
| 98 | .autocorrectionDisabled() |
111 | .autocorrectionDisabled() |
gitbay/Views/Shared/RemovableChips.swift
added
+40
| @@ -0,0 +1,40 @@ |
| |
1 | import SwiftUI |
| |
2 | |
| |
3 | /// A horizontal row of chips, each with a remove button: an issue's |
| |
4 | /// labels and assignees, a merge request's labels. The remove button is |
| |
5 | /// identified as `<idPrefix>-remove-<item>`. |
| |
6 | struct RemovableChips: View { |
| |
7 | |
| |
8 | let items: [String] |
| |
9 | let idPrefix: String |
| |
10 | let disabled: Bool |
| |
11 | var color: (String) -> Color = { _ in .secondary } |
| |
12 | let remove: (String) -> Void |
| |
13 | |
| |
14 | var body: some View { |
| |
15 | if !items.isEmpty { |
| |
16 | ScrollView(.horizontal, showsIndicators: false) { |
| |
17 | HStack(spacing: 6) { |
| |
18 | ForEach(items, id: \.self) { item in |
| |
19 | HStack(spacing: 3) { |
| |
20 | Text(item) |
| |
21 | Button { |
| |
22 | remove(item) |
| |
23 | } label: { |
| |
24 | Image(systemName: "xmark.circle.fill") |
| |
25 | .foregroundStyle(.tertiary) |
| |
26 | } |
| |
27 | .disabled(disabled) |
| |
28 | .accessibilityIdentifier("\(idPrefix)-remove-\(item)") |
| |
29 | } |
| |
30 | .font(.gbSans(.caption)) |
| |
31 | .padding(.horizontal, 8) |
| |
32 | .padding(.vertical, 3) |
| |
33 | .background(color(item).opacity(0.07), in: gbChipShape) |
| |
34 | .overlay(gbChipShape.stroke(color(item).opacity(0.35), lineWidth: 1)) |
| |
35 | } |
| |
36 | } |
| |
37 | } |
| |
38 | } |
| |
39 | } |
| |
40 | } |