Commit 5bf7b91cd7

5bf7b91cd757507e1fba8fea710933430baae9fa

parent: 3b4a8cfe33

Verified · cmc

cmc <hello@cleberg.net> · 2026-09-30 02:10 UTC

Sentence case for states and merge requests

Issue and MR states read Open, Merged, Closed, Source gone in filters,
chips and headers. The MR filter says Source gone instead of Gone.

Layout: unified · split

gitbay/Issues/IssueFilter.swift +1 −1
@@ -8,7 +8,7 @@ nonisolated struct IssueFilter: Equatable, Sendable {
8 enum State: String, CaseIterable, Identifiable, Sendable { 8 enum State: String, CaseIterable, Identifiable, Sendable {
9 case open, closed, all 9 case open, closed, all
10 var id: String { rawValue } 10 var id: String { rawValue }
11 var label: String { rawValue.capitalized } 11 var label: String { stateLabel(rawValue) }
12 } 12 }
13 13
14 var state: State = .open 14 var state: State = .open
gitbay/MRs/MRFilter.swift +8 −6
@@ -10,12 +10,7 @@ nonisolated struct MRFilter: Equatable, Sendable {
10 case all 10 case all
11 11
12 var id: String { rawValue } 12 var id: String { rawValue }
13 /// Segment text. Five segments share a phone's width, so the 13 var label: String { stateLabel(rawValue) }
14 /// source-gone state is one word; the rows and chips still say
15 /// "source gone" in full.
16 var label: String {
17 self == .sourceGone ? "Gone" : rawValue.capitalized
18 }
19 } 14 }
20 15
21 var state: State = .open 16 var state: State = .open
@@ -45,3 +40,10 @@ nonisolated struct MRFilter: Equatable, Sendable {
45 return state == .all ? "No \(noun)." : "No \(state.label.lowercased()) \(noun)." 40 return state == .all ? "No \(noun)." : "No \(state.label.lowercased()) \(noun)."
46 } 41 }
47} 42}
43
44/// A wire state (`open`, `source_gone`) as the app shows it: sentence
45/// case, underscores as spaces.
46nonisolated func stateLabel(_ state: String) -> String {
47 let words = state.replacingOccurrences(of: "_", with: " ")
48 return words.prefix(1).uppercased() + words.dropFirst()
49}
gitbay/Views/Dashboard/DashboardView.swift +1 −1
@@ -124,7 +124,7 @@ struct DashboardView: View {
124 Text(item.updatedAt, format: .relative(presentation: .named)) 124 Text(item.updatedAt, format: .relative(presentation: .named))
125 if item.state == "source_gone" { 125 if item.state == "source_gone" {
126 Text("·") 126 Text("·")
127 GBChip("source gone", .secondary) 127 GBChip(stateLabel(item.state), .secondary)
128 } 128 }
129 } 129 }
130 .font(.gbSans(.caption)) 130 .font(.gbSans(.caption))
gitbay/Views/Discovery/ExploreView.swift +1 −1
@@ -159,7 +159,7 @@ private struct SearchEntryRow: View {
159 } 159 }
160 HStack(spacing: 6) { 160 HStack(spacing: 6) {
161 if let state = result.state { 161 if let state = result.state {
162 GBChip(state, color(for: state)) 162 GBChip(stateLabel(state), color(for: state))
163 } 163 }
164 Spacer() 164 Spacer()
165 if let updatedAt = result.updatedAt { 165 if let updatedAt = result.updatedAt {
gitbay/Views/Issues/IssueListView.swift +1 −1
@@ -65,7 +65,7 @@ struct IssueListView: View {
65 } 65 }
66 .sheet(isPresented: $filtering) { 66 .sheet(isPresented: $filtering) {
67 FilterSheet( 67 FilterSheet(
68 title: "Filter Issues", 68 title: "Filter issues",
69 author: Bindable(model).filter.author, 69 author: Bindable(model).filter.author,
70 milestone: Bindable(model).filter.milestone, 70 milestone: Bindable(model).filter.milestone,
71 milestones: model.milestones, 71 milestones: model.milestones,
gitbay/Views/Issues/IssueView.swift +1 −1
@@ -72,7 +72,7 @@ struct IssueView: View {
72 Text(issue.title) 72 Text(issue.title)
73 .font(.gbSans(.headline)) 73 .font(.gbSans(.headline))
74 HStack(spacing: 6) { 74 HStack(spacing: 6) {
75 Label(issue.state, systemImage: issue.isOpen ? "circle" : "checkmark.circle.fill") 75 Label(stateLabel(issue.state), systemImage: issue.isOpen ? "circle" : "checkmark.circle.fill")
76 .font(.gbSans(.caption).weight(.medium)) 76 .font(.gbSans(.caption).weight(.medium))
77 .foregroundStyle(issue.isOpen ? Color.gbOK : Color.gbBad) 77 .foregroundStyle(issue.isOpen ? Color.gbOK : Color.gbBad)
78 Text("by \(issue.author)") 78 Text("by \(issue.author)")
gitbay/Views/MRs/MRListView.swift +3 −3
@@ -32,7 +32,7 @@ struct MRListView: View {
32 } 32 }
33 .overlay { LoadStateOverlay(state: model.state) } 33 .overlay { LoadStateOverlay(state: model.state) }
34 .searchable(text: Bindable(model).filter.search, prompt: "Search titles and bodies") 34 .searchable(text: Bindable(model).filter.search, prompt: "Search titles and bodies")
35 .navigationTitle("Merge Requests") 35 .navigationTitle("Merge requests")
36 .navigationBarTitleDisplayMode(.inline) 36 .navigationBarTitleDisplayMode(.inline)
37 .toolbar { 37 .toolbar {
38 ToolbarItem(placement: .topBarTrailing) { 38 ToolbarItem(placement: .topBarTrailing) {
@@ -58,7 +58,7 @@ struct MRListView: View {
58 } 58 }
59 .sheet(isPresented: $filtering) { 59 .sheet(isPresented: $filtering) {
60 FilterSheet( 60 FilterSheet(
61 title: "Filter Merge Requests", 61 title: "Filter merge requests",
62 author: Bindable(model).filter.author, 62 author: Bindable(model).filter.author,
63 milestone: Bindable(model).filter.milestone, 63 milestone: Bindable(model).filter.milestone,
64 milestones: model.milestones, 64 milestones: model.milestones,
@@ -243,7 +243,7 @@ struct MRStateBadge: View {
243 let state: String 243 let state: String
244 244
245 var body: some View { 245 var body: some View {
246 GBChip(state.replacingOccurrences(of: "_", with: " "), color) 246 GBChip(stateLabel(state), color)
247 } 247 }
248 248
249 /// The web's chip mapping: open=ok, merged=done, closed=bad, 249 /// The web's chip mapping: open=ok, merged=done, closed=bad,
gitbay/Views/Repos/RepoView.swift +1 −1
@@ -53,7 +53,7 @@ struct RepoView: View {
53 Grid(horizontalSpacing: 0, verticalSpacing: 0) { 53 Grid(horizontalSpacing: 0, verticalSpacing: 0) {
54 GridRow { 54 GridRow {
55 tile("Files", systemImage: "folder", id: "files", to: .files) 55 tile("Files", systemImage: "folder", id: "files", to: .files)
56 tile("Merge Requests", systemImage: "arrow.triangle.merge", id: "merge-requests", 56 tile("Merge requests", systemImage: "arrow.triangle.merge", id: "merge-requests",
57 to: .mergeRequests, count: model.openMRs) 57 to: .mergeRequests, count: model.openMRs)
58 } 58 }
59 GridRow { 59 GridRow {
gitbayTests/ListFilterTests.swift +11 −1
@@ -107,7 +107,17 @@ struct MRFilterTests {
107 var f = MRFilter() 107 var f = MRFilter()
108 f.state = .sourceGone 108 f.state = .sourceGone
109 #expect(f.flags() == ["--state", "source_gone"]) 109 #expect(f.flags() == ["--state", "source_gone"])
110 #expect(MRFilter.State.sourceGone.label == "Gone") 110 #expect(MRFilter.State.sourceGone.label == "Source gone")
111 }
112
113 /// Filters, chips and headers all say a state the same way.
114 @Test func statesReadInSentenceCase() {
115 #expect(stateLabel("open") == "Open")
116 #expect(stateLabel("merged") == "Merged")
117 #expect(stateLabel("source_gone") == "Source gone")
118 #expect(MRFilter.State.allCases.map(\.label)
119 == ["Open", "Merged", "Closed", "Source gone", "All"])
120 #expect(IssueFilter.State.allCases.map(\.label) == ["Open", "Closed", "All"])
111 } 121 }
112 122
113 @Test func everyStateTheCommandAcceptsIsOffered() { 123 @Test func everyStateTheCommandAcceptsIsOffered() {
gitbayUITests/LiveSmokeUITests.swift +1 −1
@@ -412,7 +412,7 @@ final class LiveSmokeUITests: XCTestCase {
412 XCTAssertTrue(confirm.waitForExistence(timeout: 5), "close confirmation missing") 412 XCTAssertTrue(confirm.waitForExistence(timeout: 5), "close confirmation missing")
413 confirm.tap() 413 confirm.tap()
414 414
415 XCTAssertTrue(app.staticTexts["closed"].firstMatch 415 XCTAssertTrue(app.staticTexts["Closed"].firstMatch
416 .waitForExistence(timeout: 15), "MR did not show closed after close") 416 .waitForExistence(timeout: 15), "MR did not show closed after close")
417 } 417 }
418 418