Commit 61e8259c19

61e8259c19f4a797af6bb6a2e63a3366967cc51b

parent: 3ee1c43ddb

Verified · cmc

cmc <hello@cleberg.net> · 2026-09-07 00:40 UTC

Fix build cancel label, release-list refresh, org rename prefill

Relabel the build toolbar action "Cancel Build" so it doesn't read as
dismiss. Reload ReleaseListView on reappear (guarded past the initial
.task) so a deleted release doesn't linger after the pop. Clear the
org rename field instead of prefilling the current name, so an
un-edited tap can't resubmit it. Close three test gaps (createOrg's
return value, the instance isCancellable property, actionError
clearing after a prior failure) and provision cancelSendsTheBuildNumber
for all three requests its flow actually makes.

Layout: unified · split

gitbay/Views/Builds/BuildDetailView.swift +1 −1
@@ -37,7 +37,7 @@ struct BuildDetailView: View {
3737 if model.working {
3838 ProgressView()
3939 } else {
40 Button("Cancel", role: .destructive) {
40 Button("Cancel Build", role: .destructive) {
4141 confirmingCancel = true
4242 }
4343 .accessibilityIdentifier("build-cancel-button")
gitbay/Views/Orgs/OrgView.swift +2 −2
@@ -35,7 +35,7 @@ struct OrgView: View {
3535 .toolbar {
3636 ToolbarItem(placement: .topBarTrailing) {
3737 Button {
38 newOrgName = model.orgName
38 newOrgName = ""
3939 renamingOrg = true
4040 } label: {
4141 Image(systemName: "pencil")
@@ -47,7 +47,7 @@ struct OrgView: View {
4747 .task { await model.load() }
4848 .refreshable { await model.load() }
4949 .alert("Rename \(model.orgName)", isPresented: $renamingOrg) {
50 TextField("New name", text: $newOrgName)
50 TextField(model.orgName, text: $newOrgName)
5151 .autocorrectionDisabled()
5252 .textInputAutocapitalization(.never)
5353 Button("Rename") {
gitbay/Views/Releases/ReleaseListView.swift +12 −1
@@ -7,6 +7,7 @@ struct ReleaseListView: View {
77 @State private var draftTag = ""
88 @State private var draftTitle = ""
99 @State private var draftNotes = ""
10 @State private var hasLoadedOnce = false
1011
1112 init(client: GitbayClient, repo: String) {
1213 _model = State(initialValue: ReleaseListViewModel(client: client, repoPath: repo))
@@ -63,7 +64,17 @@ struct ReleaseListView: View {
6364 composing = false
6465 }
6566 }
66 .task { await model.load() }
67 .task {
68 await model.load()
69 hasLoadedOnce = true
70 }
71 // A deleted release pops back to this list; `.task` does not
72 // re-run on that pop, so a manual refresh here is what drops the
73 // now-gone row. Guarded so the initial `.task` load isn't repeated.
74 .onAppear {
75 guard hasLoadedOnce else { return }
76 Task { await model.load() }
77 }
6778 .refreshable { await model.load() }
6879 }
6980}
gitbayTests/SmallActionTests.swift +50 −3
@@ -37,15 +37,40 @@ struct BuildCancelTests {
3737
3838 @Test func cancelSendsTheBuildNumber() async throws {
3939 let (client, stub) = try makeClient()
40 // The view model's own load response, then the write, then a reload.
40 // The write, then its reload — `build show` and `build log` fire
41 // concurrently, so each needs its own matched stub.
4142 let model = BuildDetailViewModel(client: client, repoPath: "krz/gitbay", number: 966)
4243 stub.enqueue(.init(status: 200, json: ok))
43 stub.enqueue(.init(status: 200, json: ok))
44 stub.enqueue(.init(status: 200, json: """
45 {"protocol_version":1,"data":{"number":966,"job":"test","status":"cancelled",\
46 "sha":"abc123","ref":"refs/heads/main","created_at":"2026-01-01T00:00:00Z"},\
47 "exit_code":0}
48 """, match: "argv=show"))
49 stub.enqueue(.init(status: 200, json: ok, match: "argv=log"))
4450 await model.cancel()
4551
4652 let write = try #require(stub.seen.first { $0.method == "POST" })
4753 #expect(try argvOf(write) == ["build", "cancel", "krz/gitbay", "966"])
4854 }
55
56 /// The view reads the instance property, not the static function —
57 /// nothing pinned that it tracks the loaded build, or that it starts
58 /// out false before anything has loaded.
59 @Test func instanceIsCancellableTracksTheLoadedBuild() async throws {
60 let (client, stub) = try makeClient()
61 let model = BuildDetailViewModel(client: client, repoPath: "krz/gitbay", number: 966)
62 #expect(model.isCancellable == false)
63
64 stub.enqueue(.init(status: 200, json: """
65 {"protocol_version":1,"data":{"number":966,"job":"test","status":"running",\
66 "sha":"abc123","ref":"refs/heads/main","created_at":"2026-01-01T00:00:00Z"},\
67 "exit_code":0}
68 """, match: "argv=show"))
69 stub.enqueue(.init(status: 200, json: ok, match: "argv=log"))
70 await model.load()
71
72 #expect(model.isCancellable)
73 }
4974}
5075
5176@MainActor
@@ -74,6 +99,27 @@ struct ReleaseDeleteTests {
7499 #expect(deleted == false)
75100 #expect(model.actionError?.isEmpty == false)
76101 }
102
103 /// A stale `actionError` surviving an unrelated later call has been the
104 /// bug five times over; every `perform` clears it up front, but nothing
105 /// pinned that.
106 @Test func actionErrorClearsOnALaterSuccess() async throws {
107 let (client, stub) = try makeClient()
108 let model = ReleaseDetailViewModel(client: client, repoPath: "krz/gitbay", tag: "v1.0.0")
109 stub.enqueue(.init(status: 200, json: """
110 {"protocol_version":1,"error":"deleting a release needs write access","exit_code":4}
111 """))
112 let firstDelete = await model.delete()
113
114 #expect(firstDelete == false)
115 #expect(model.actionError != nil)
116
117 stub.enqueue(.init(status: 200, json: ok))
118 let secondDelete = await model.delete()
119
120 #expect(secondDelete)
121 #expect(model.actionError == nil)
122 }
77123}
78124
79125@MainActor
@@ -130,8 +176,9 @@ struct OrgCreateRenameTests {
130176 stub.enqueue(.init(status: 200, json: ok))
131177 stub.enqueue(.init(status: 200, json: ok))
132178 let model = ProfileViewModel(client: client, name: "cmc")
133 _ = await model.createOrg("newco")
179 let created = await model.createOrg("newco")
134180
181 #expect(created)
135182 let write = try #require(stub.seen.first { $0.method == "POST" })
136183 #expect(try argvOf(write) == ["org", "create", "newco"])
137184 }