Commit 34e06cf562

34e06cf562bee6386038cf685fb1de980cb34603

parent: 80d5f2e5cb

Verified · cmc

cmc <hello@cleberg.net> · 2026-09-06 18:19 UTC

Label management screen

Layout: unified · split

gitbay/ContentView.swift +2
@@ -98,6 +98,8 @@ private struct RouteDestinations: ViewModifier {
9898 RefsView(client: client, repo: repo)
9999 case .milestones(let repo):
100100 MilestoneListView(client: client, repo: repo)
101 case .labels(let repo):
102 LabelListView(client: client, repo: repo)
101103 case .wiki(let repo):
102104 WikiView(client: client, repo: repo)
103105 case .wikiPage(let repo, let page):
gitbay/Views/Issues/LabelListView.swift added +102
@@ -0,0 +1,102 @@
1import SwiftUI
2
3/// A repository's labels: their colour, how many issues carry each, and
4/// the controls to create, recolour and remove them.
5struct LabelListView: View {
6
7 @State private var model: LabelListViewModel
8 @State private var newLabel = ""
9 @State private var pendingRemoval: IssueLabel?
10
11 init(client: GitbayClient, repo: String) {
12 _model = State(initialValue: LabelListViewModel(client: client, repoPath: repo))
13 }
14
15 private var colors: LabelColors { LabelColors(model.state.value ?? []) }
16
17 var body: some View {
18 List {
19 if let actionError = model.actionError {
20 Section { GBNotice(actionError) }
21 }
22
23 Section("New label") {
24 HStack {
25 TextField("Label", text: $newLabel)
26 .autocorrectionDisabled()
27 .textInputAutocapitalization(.never)
28 Button {
29 let name = newLabel.trimmingCharacters(in: .whitespaces)
30 newLabel = ""
31 Task { await model.setLabel(name, color: nil) }
32 } label: {
33 Image(systemName: "plus.circle.fill")
34 }
35 .disabled(
36 newLabel.trimmingCharacters(in: .whitespaces).isEmpty || model.working
37 )
38 }
39 }
40
41 ForEach(model.state.value ?? []) { label in
42 LabelRow(label: label, color: colors.color(label.name), working: model.working) {
43 hex in
44 Task { await model.setLabel(label.name, color: hex) }
45 } remove: {
46 pendingRemoval = label
47 }
48 }
49 }
50 .overlay { LoadStateOverlay(state: model.state) }
51 .navigationTitle("Labels")
52 .navigationBarTitleDisplayMode(.inline)
53 .task { await model.load() }
54 .refreshable { await model.load() }
55 .confirmationDialog(
56 "Remove \(pendingRemoval?.name ?? "")?",
57 isPresented: .init(
58 get: { pendingRemoval != nil },
59 set: { if !$0 { pendingRemoval = nil } }
60 ),
61 titleVisibility: .visible
62 ) {
63 Button("Remove", role: .destructive) {
64 guard let label = pendingRemoval else { return }
65 pendingRemoval = nil
66 Task { await model.remove(label.name) }
67 }
68 Button("Cancel", role: .cancel) { pendingRemoval = nil }
69 } message: {
70 // The command takes the label off every issue, not just the set.
71 Text("This takes the label off every issue that carries it.")
72 }
73 }
74}
75
76private struct LabelRow: View {
77 let label: IssueLabel
78 let color: Color
79 let working: Bool
80 let recolour: (String) -> Void
81 let remove: () -> Void
82
83 var body: some View {
84 HStack(spacing: 8) {
85 GBChip(label.name, color)
86 Text(label.issues == 1 ? "1 issue" : "\(label.issues) issues")
87 .font(.gbSans(.caption))
88 .foregroundStyle(.secondary)
89 Spacer()
90 Menu {
91 ForEach(LabelPalette.palette, id: \.self) { hex in
92 Button(hex) { recolour(String(hex.dropFirst())) }
93 }
94 Button("Clear colour") { recolour("") }
95 Button("Remove", role: .destructive) { remove() }
96 } label: {
97 Image(systemName: "ellipsis.circle")
98 }
99 .disabled(working)
100 }
101 }
102}
gitbay/Views/Repos/RepoRoute.swift +1
@@ -12,6 +12,7 @@ nonisolated enum RepoRoute: Hashable {
1212 case blame(repo: String, path: String, ref: String?)
1313 case refs(repo: String)
1414 case milestones(repo: String)
15 case labels(repo: String)
1516 case wiki(repo: String)
1617 case wikiPage(repo: String, page: String)
1718 case commit(repo: String, sha: String)
gitbay/Views/Repos/RepoView.swift +3
@@ -43,6 +43,9 @@ struct RepoView: View {
4343 NavigationLink(value: RepoRoute.milestones(repo: path)) {
4444 Label("Milestones", systemImage: "flag")
4545 }
46 NavigationLink(value: RepoRoute.labels(repo: path)) {
47 Label("Labels", systemImage: "tag")
48 }
4649 NavigationLink(value: BuildRoute.list(repo: path)) {
4750 Label("Builds", systemImage: "hammer")
4851 }