Commit 1dab2a3e20
1dab2a3e20690cd3d98d21fa537e15bbc3fbf6f6
parent: 5d30f8235f
Unsigned
cmc <hello@cleberg.net> · 2026-09-07 17:10 UTC
Confirm before removing a team member, revoking a grant, or removing a topic (!65)
The three destructive actions that fired on a swipe or an x now go through a confirmationDialog like the rest. The live suite drives the new dialogs.
Layout: unified · split
gitbay/Views/Orgs/TeamView.swift
+34 −2
| @@ -7,6 +7,8 @@ struct TeamView: View { |
| 7 | 7 | @State private var newMember = "" |
| 8 | 8 | @State private var grantRepo = "" |
| 9 | 9 | @State private var grantRole = "read" |
| 10 | @State private var removingMember: String? |
| 11 | @State private var revokingGrant: TeamViewModel.Grant? |
| 10 | 12 | |
| 11 | 13 | init(client: GitbayClient, org: String, team: String) { |
| 12 | 14 | _model = State(initialValue: TeamViewModel(client: client, orgName: org, teamName: team)) |
| @@ -27,7 +29,7 @@ struct TeamView: View { |
| 27 | 29 | .font(.gbSans(.subheadline)) |
| 28 | 30 | .swipeActions { |
| 29 | 31 | Button("Remove", role: .destructive) { |
| 30 | | Task { await model.removeMember(user) } |
| 32 | removingMember = user |
| 31 | 33 | } |
| 32 | 34 | } |
| 33 | 35 | } |
| @@ -58,7 +60,7 @@ struct TeamView: View { |
| 58 | 60 | } |
| 59 | 61 | .swipeActions { |
| 60 | 62 | Button("Revoke", role: .destructive) { |
| 61 | | Task { await model.revoke(grant) } |
| 63 | revokingGrant = grant |
| 62 | 64 | } |
| 63 | 65 | } |
| 64 | 66 | } |
| @@ -92,6 +94,36 @@ struct TeamView: View { |
| 92 | 94 | .navigationTitle(model.teamName) |
| 93 | 95 | .navigationBarTitleDisplayMode(.inline) |
| 94 | 96 | .task { await model.load() } |
| 97 | .confirmationDialog( |
| 98 | "Remove \(removingMember ?? "") from \(model.teamName)?", |
| 99 | isPresented: Binding( |
| 100 | get: { removingMember != nil }, |
| 101 | set: { if !$0 { removingMember = nil } } |
| 102 | ) |
| 103 | ) { |
| 104 | Button("Remove", role: .destructive) { |
| 105 | if let user = removingMember { |
| 106 | Task { await model.removeMember(user) } |
| 107 | } |
| 108 | removingMember = nil |
| 109 | } |
| 110 | Button("Cancel", role: .cancel) {} |
| 111 | } |
| 112 | .confirmationDialog( |
| 113 | "Revoke \(model.teamName)'s \(revokingGrant?.role ?? "") access to \(revokingGrant?.repo ?? "")?", |
| 114 | isPresented: Binding( |
| 115 | get: { revokingGrant != nil }, |
| 116 | set: { if !$0 { revokingGrant = nil } } |
| 117 | ) |
| 118 | ) { |
| 119 | Button("Revoke", role: .destructive) { |
| 120 | if let grant = revokingGrant { |
| 121 | Task { await model.revoke(grant) } |
| 122 | } |
| 123 | revokingGrant = nil |
| 124 | } |
| 125 | Button("Cancel", role: .cancel) {} |
| 126 | } |
| 95 | 127 | .refreshable { await model.load() } |
| 96 | 128 | } |
| 97 | 129 | } |
gitbay/Views/Repos/RepoSettingsView.swift
+17 −1
| @@ -10,6 +10,7 @@ struct RepoSettingsView: View { |
| 10 | 10 | @State private var newTopic = "" |
| 11 | 11 | @State private var newBranch = "" |
| 12 | 12 | @State private var loadedOnce = false |
| 13 | @State private var removingTopic: String? |
| 13 | 14 | |
| 14 | 15 | init(client: GitbayClient, repo: String) { |
| 15 | 16 | _model = State(initialValue: RepoSettingsViewModel(client: client, repoPath: repo)) |
| @@ -48,6 +49,21 @@ struct RepoSettingsView: View { |
| 48 | 49 | await model.load() |
| 49 | 50 | await model.loadDeps() |
| 50 | 51 | } |
| 52 | .confirmationDialog( |
| 53 | "Remove topic \(removingTopic ?? "")?", |
| 54 | isPresented: Binding( |
| 55 | get: { removingTopic != nil }, |
| 56 | set: { if !$0 { removingTopic = nil } } |
| 57 | ) |
| 58 | ) { |
| 59 | Button("Remove", role: .destructive) { |
| 60 | if let topic = removingTopic { |
| 61 | Task { await model.removeTopic(topic) } |
| 62 | } |
| 63 | removingTopic = nil |
| 64 | } |
| 65 | Button("Cancel", role: .cancel) {} |
| 66 | } |
| 51 | 67 | } |
| 52 | 68 | |
| 53 | 69 | // MARK: - Sections |
| @@ -93,7 +109,7 @@ struct RepoSettingsView: View { |
| 93 | 109 | HStack(spacing: 3) { |
| 94 | 110 | Text(topic) |
| 95 | 111 | Button { |
| 96 | | Task { await model.removeTopic(topic) } |
| 112 | removingTopic = topic |
| 97 | 113 | } label: { |
| 98 | 114 | Image(systemName: "xmark.circle.fill") |
| 99 | 115 | .foregroundStyle(.tertiary) |
gitbayUITests/LiveSmokeUITests.swift
+9 −1
| @@ -450,8 +450,12 @@ extension LiveSmokeUITests { |
| 450 | 450 | .firstMatch.tap() |
| 451 | 451 | let chip = app.staticTexts["ios"].firstMatch |
| 452 | 452 | XCTAssertTrue(chip.waitForExistence(timeout: 10), "topic did not appear") |
| 453 | | // Remove it again: the chip's own x button is the next button. |
| 453 | // Remove it again: the chip's own x button is the next button, |
| 454 | // and it asks before removing. |
| 454 | 455 | app.scrollViews.buttons.firstMatch.tap() |
| 456 | let confirmRemove = app.buttons["Remove"].firstMatch |
| 457 | XCTAssertTrue(confirmRemove.waitForExistence(timeout: 5), "topic remove confirmation missing") |
| 458 | confirmRemove.tap() |
| 455 | 459 | XCTAssertTrue(waitForDisappearance(chip, timeout: 10), "topic did not remove") |
| 456 | 460 | |
| 457 | 461 | let resolved = app.switches["Require threads resolved"].firstMatch |
| @@ -684,6 +688,10 @@ extension LiveSmokeUITests { |
| 684 | 688 | XCTAssertTrue(grantRow.waitForExistence(timeout: 15), "grant not listed") |
| 685 | 689 | grantRow.swipeLeft() |
| 686 | 690 | app.buttons["Revoke"].firstMatch.tap() |
| 691 | // The confirmation dialog's destructive Revoke. |
| 692 | let confirmRevoke = app.buttons["Revoke"].firstMatch |
| 693 | XCTAssertTrue(confirmRevoke.waitForExistence(timeout: 5), "revoke confirmation missing") |
| 694 | confirmRevoke.tap() |
| 687 | 695 | XCTAssertTrue(waitForDisappearance(grantRow, timeout: 15), "grant not revoked") |
| 688 | 696 | |
| 689 | 697 | back() |