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 | 91 | @ViewBuilder |
| 92 | 92 | private func triageSection(_ issue: IssueDetail) -> some View { |
| 93 | 93 | Section("Labels") { |
| 94 | | labelFlow(issue.labels ?? [], color: { model.colors.color($0) }, remove: { label in |
| 95 | | Task { await model.removeLabel(label) } |
| 96 | | }) |
| 94 | RemovableChips(items: issue.labels ?? [], idPrefix: "issue-label", |
| 95 | disabled: model.working, color: { model.colors.color($0) }, |
| 96 | remove: { label in Task { await model.removeLabel(label) } }) |
| 97 | 97 | HStack { |
| 98 | 98 | TextField("Add label", text: $editingLabel) |
| 99 | 99 | .autocorrectionDisabled() |
| @@ -109,9 +109,9 @@ struct IssueView: View { |
| 109 | 109 | } |
| 110 | 110 | } |
| 111 | 111 | Section("Assignees") { |
| 112 | | labelFlow(issue.assignees ?? [], remove: { user in |
| 113 | | Task { await model.removeAssignee(user) } |
| 114 | | }) |
| 112 | RemovableChips(items: issue.assignees ?? [], idPrefix: "issue-assignee", |
| 113 | disabled: model.working, |
| 114 | remove: { user in Task { await model.removeAssignee(user) } }) |
| 115 | 115 | HStack { |
| 116 | 116 | TextField("Assign user", text: $editingAssignee) |
| 117 | 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 | 155 | private func commentsSection(_ comments: [IssueDetail.Comment]) -> some View { |
| 187 | 156 | Section("Comments") { |
| 188 | 157 | ForEach(comments) { comment in |
gitbay/Views/Shared/FilterSheet.swift
+19 −6
| @@ -2,8 +2,8 @@ import SwiftUI |
| 2 | 2 | |
| 3 | 3 | /// The issue and MR lists' filter sheet. A `Form` with the fields both |
| 4 | 4 | /// lists share — author and milestone — plus an `extra` slot for the |
| 5 | | /// rows only issues have, since `mr list` takes neither `--label` nor |
| 6 | | /// `--assignee`. |
| 5 | /// rows each list adds: the label picker on both, and an assignee field |
| 6 | /// on issues only, since `mr list` takes no `--assignee`. |
| 7 | 7 | struct FilterSheet<Extra: View>: View { |
| 8 | 8 | |
| 9 | 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 |
| 62 | | /// `FilterSheet`'s `extra` slot. |
| 63 | | struct IssueFilterExtra: View { |
| 61 | /// The label picker both lists' sheets add through `FilterSheet`'s |
| 62 | /// `extra` slot. |
| 63 | struct LabelFilterSection: View { |
| 64 | 64 | |
| 65 | 65 | @Binding var label: String? |
| 66 | | @Binding var assignee: String? |
| 67 | 66 | let labels: [IssueLabel] |
| 68 | 67 | let colors: LabelColors |
| 69 | 68 | |
| @@ -93,6 +92,20 @@ struct IssueFilterExtra: View { |
| 93 | 92 | } |
| 94 | 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 | 109 | Section("Assignee") { |
| 97 | 110 | TextField("Username", text: $assignee.orEmpty) |
| 98 | 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 | } |