Commit 250c56ddb3
Verified · cmc
Layout: unified · split
gitbay/MRs/MRDetailViewModel.swift +2
| @@ -151,6 +151,8 @@ final class MRDetailViewModel { | |||
| 151 | /// kept. The diff is gone afterwards; the thread records who pruned it. | 151 | /// kept. The diff is gone afterwards; the thread records who pruned it. |
| 152 | func pruneHead() async { | 152 | func pruneHead() async { |
| 153 | await perform(["admin", "mr", "prune", repoPath, String(number), "--yes"]) | 153 | await perform(["admin", "mr", "prune", repoPath, String(number), "--yes"]) |
| 154 | // Done: another prune would only add another system comment. | ||
| 155 | if actionError == nil { canPruneHead = false } | ||
| 154 | } | 156 | } |
| 155 | 157 | ||
| 156 | func cancelQueuedMerge() async { | 158 | func cancelQueuedMerge() async { |
gitbay/Views/Discovery/ProfileView.swift +4
| @@ -80,6 +80,10 @@ struct ProfileView: View { | |||
| 80 | } | 80 | } |
| 81 | .task { await model.load() } | 81 | .task { await model.load() } |
| 82 | .refreshable { await model.load() } | 82 | .refreshable { await model.load() } |
| 83 | // The organization screen, pushed from an org's profile, may delete it. | ||
| 84 | .onReceive(NotificationCenter.default.publisher(for: .gitbayOrganizationsChanged)) { note in | ||
| 85 | if note.object as? String == model.name { Task { await model.load() } } | ||
| 86 | } | ||
| 83 | .sheet(isPresented: $editingProfile) { | 87 | .sheet(isPresented: $editingProfile) { |
| 84 | if let profile = model.state.value { | 88 | if let profile = model.state.value { |
| 85 | ProfileEditSheet(model: model, profile: profile) | 89 | ProfileEditSheet(model: model, profile: profile) |
gitbay/Views/Orgs/OrgView.swift +17 −4
| @@ -16,6 +16,9 @@ struct OrgView: View { | |||
| 16 | @State private var deleteConfirm = "" | 16 | @State private var deleteConfirm = "" |
| 17 | /// Set once the organization is deleted: nothing on this screen applies. | 17 | /// Set once the organization is deleted: nothing on this screen applies. |
| 18 | @State private var deleted = false | 18 | @State private var deleted = false |
| 19 | /// A delete refused, shown where the button is rather than in the | ||
| 20 | /// banner at the top of a list scrolled past it. | ||
| 21 | @State private var refusal: String? | ||
| 19 | 22 | ||
| 20 | init(client: GitbayClient, org: String) { | 23 | init(client: GitbayClient, org: String) { |
| 21 | _model = State(initialValue: OrgViewModel(client: client, orgName: org)) | 24 | _model = State(initialValue: OrgViewModel(client: client, orgName: org)) |
| @@ -60,13 +63,23 @@ struct OrgView: View { | |||
| 60 | } | 63 | } |
| 61 | } | 64 | } |
| 62 | .task { await model.load() } | 65 | .task { await model.load() } |
| 63 | .refreshable { await model.load() } | 66 | .refreshable { |
| 67 | guard !deleted else { return } | ||
| 68 | await model.load() | ||
| 69 | } | ||
| 70 | .alert("Not done", isPresented: Binding(get: { refusal != nil }, set: { if !$0 { refusal = nil } })) { | ||
| 71 | Button("OK", role: .cancel) {} | ||
| 72 | } message: { | ||
| 73 | Text(refusal ?? "") | ||
| 74 | } | ||
| 64 | .alert("Delete \(model.orgName)", isPresented: $deletingOrg) { | 75 | .alert("Delete \(model.orgName)", isPresented: $deletingOrg) { |
| 65 | TextField("type \(model.orgName) to confirm", text: $deleteConfirm) | 76 | TextField("type \(model.orgName) to confirm", text: $deleteConfirm) |
| 66 | .autocorrectionDisabled() | 77 | .autocorrectionDisabled() |
| 67 | .textInputAutocapitalization(.never) | 78 | .textInputAutocapitalization(.never) |
| 68 | Button("Delete", role: .destructive) { | 79 | Button("Delete", role: .destructive) { |
| 69 | Task { deleted = await model.delete() } | 80 | Task { |
| 81 | if await model.delete() { deleted = true } else { refusal = model.actionError } | ||
| 82 | } | ||
| 70 | } | 83 | } |
| 71 | .disabled(deleteConfirm != model.orgName) | 84 | .disabled(deleteConfirm != model.orgName) |
| 72 | Button("Cancel", role: .cancel) {} | 85 | Button("Cancel", role: .cancel) {} |
| @@ -164,8 +177,6 @@ struct OrgView: View { | |||
| 164 | } | 177 | } |
| 165 | } | 178 | } |
| 166 | 179 | ||
| 167 | /// Labels and milestones every repository under the org sees | ||
| 168 | /// (v1.19.0), managed here with `org label` and `org milestone`. | ||
| 169 | private var deleteSection: some View { | 180 | private var deleteSection: some View { |
| 170 | Section { | 181 | Section { |
| 171 | Button(role: .destructive) { | 182 | Button(role: .destructive) { |
| @@ -182,6 +193,8 @@ struct OrgView: View { | |||
| 182 | } | 193 | } |
| 183 | } | 194 | } |
| 184 | 195 | ||
| 196 | /// Labels and milestones every repository under the org sees | ||
| 197 | /// (v1.19.0), managed here with `org label` and `org milestone`. | ||
| 185 | private var issuesSection: some View { | 198 | private var issuesSection: some View { |
| 186 | Section { | 199 | Section { |
| 187 | NavigationLink(value: OrgRoute.labels(model.orgName)) { | 200 | NavigationLink(value: OrgRoute.labels(model.orgName)) { |
gitbayTests/OrgDeleteMRPruneTests.swift +6
| @@ -71,14 +71,20 @@ struct MRPruneTests { | |||
| 71 | 71 | ||
| 72 | @Test func pruneNamesTheMergeRequestAndConfirms() async throws { | 72 | @Test func pruneNamesTheMergeRequestAndConfirms() async throws { |
| 73 | let (client, stub) = try makeClient() | 73 | let (client, stub) = try makeClient() |
| 74 | stub.enqueue(.init(status: 200, json: mergedJSON, match: "argv=show")) | ||
| 75 | stub.enqueue(.init(status: 200, json: #"{"protocol_version":1,"data":{"user":"cmc","admin":true},"exit_code":0}"#, match: "argv=whoami")) | ||
| 74 | stub.enqueue(.init(status: 200, json: #"{"protocol_version":1,"data":[{"number":7,"head":"abc123"}],"exit_code":0}"#, match: "cmd")) | 76 | stub.enqueue(.init(status: 200, json: #"{"protocol_version":1,"data":[{"number":7,"head":"abc123"}],"exit_code":0}"#, match: "cmd")) |
| 75 | stub.enqueue(.init(status: 200, json: mergedJSON, match: "argv=show")) | 77 | stub.enqueue(.init(status: 200, json: mergedJSON, match: "argv=show")) |
| 76 | let model = MRDetailViewModel(client: client, repoPath: "krz/gitbay", number: 7) | 78 | let model = MRDetailViewModel(client: client, repoPath: "krz/gitbay", number: 7) |
| 79 | await model.load() | ||
| 80 | await model.checkCanPruneHead() | ||
| 81 | #expect(model.canPruneHead, "an admin on a merged merge request is offered the prune") | ||
| 77 | 82 | ||
| 78 | await model.pruneHead() | 83 | await model.pruneHead() |
| 79 | 84 | ||
| 80 | #expect(try writes(stub) == [["admin", "mr", "prune", "krz/gitbay", "7", "--yes"]]) | 85 | #expect(try writes(stub) == [["admin", "mr", "prune", "krz/gitbay", "7", "--yes"]]) |
| 81 | #expect(model.actionError == nil) | 86 | #expect(model.actionError == nil) |
| 87 | #expect(model.canPruneHead == false, "a pruned head is not offered again") | ||
| 82 | } | 88 | } |
| 83 | 89 | ||
| 84 | @Test func aStillMergeableHeadIsRefusedWithTheReason() async throws { | 90 | @Test func aStillMergeableHeadIsRefusedWithTheReason() async throws { |