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`.
7struct FilterSheet<Extra: View>: View { 7struct 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.
63struct IssueFilterExtra: View { 63struct 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.
100struct 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 @@
1import 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>`.
6struct 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}