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 {
77 @State private var newMember = ""
88 @State private var grantRepo = ""
99 @State private var grantRole = "read"
10 @State private var removingMember: String?
11 @State private var revokingGrant: TeamViewModel.Grant?
1012
1113 init(client: GitbayClient, org: String, team: String) {
1214 _model = State(initialValue: TeamViewModel(client: client, orgName: org, teamName: team))
@@ -27,7 +29,7 @@ struct TeamView: View {
2729 .font(.gbSans(.subheadline))
2830 .swipeActions {
2931 Button("Remove", role: .destructive) {
30 Task { await model.removeMember(user) }
32 removingMember = user
3133 }
3234 }
3335 }
@@ -58,7 +60,7 @@ struct TeamView: View {
5860 }
5961 .swipeActions {
6062 Button("Revoke", role: .destructive) {
61 Task { await model.revoke(grant) }
63 revokingGrant = grant
6264 }
6365 }
6466 }
@@ -92,6 +94,36 @@ struct TeamView: View {
9294 .navigationTitle(model.teamName)
9395 .navigationBarTitleDisplayMode(.inline)
9496 .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 }
95127 .refreshable { await model.load() }
96128 }
97129}
gitbay/Views/Repos/RepoSettingsView.swift +17 −1
@@ -10,6 +10,7 @@ struct RepoSettingsView: View {
1010 @State private var newTopic = ""
1111 @State private var newBranch = ""
1212 @State private var loadedOnce = false
13 @State private var removingTopic: String?
1314
1415 init(client: GitbayClient, repo: String) {
1516 _model = State(initialValue: RepoSettingsViewModel(client: client, repoPath: repo))
@@ -48,6 +49,21 @@ struct RepoSettingsView: View {
4849 await model.load()
4950 await model.loadDeps()
5051 }
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 }
5167 }
5268
5369 // MARK: - Sections
@@ -93,7 +109,7 @@ struct RepoSettingsView: View {
93109 HStack(spacing: 3) {
94110 Text(topic)
95111 Button {
96 Task { await model.removeTopic(topic) }
112 removingTopic = topic
97113 } label: {
98114 Image(systemName: "xmark.circle.fill")
99115 .foregroundStyle(.tertiary)
gitbayUITests/LiveSmokeUITests.swift +9 −1
@@ -450,8 +450,12 @@ extension LiveSmokeUITests {
450450 .firstMatch.tap()
451451 let chip = app.staticTexts["ios"].firstMatch
452452 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.
454455 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()
455459 XCTAssertTrue(waitForDisappearance(chip, timeout: 10), "topic did not remove")
456460
457461 let resolved = app.switches["Require threads resolved"].firstMatch
@@ -684,6 +688,10 @@ extension LiveSmokeUITests {
684688 XCTAssertTrue(grantRow.waitForExistence(timeout: 15), "grant not listed")
685689 grantRow.swipeLeft()
686690 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()
687695 XCTAssertTrue(waitForDisappearance(grantRow, timeout: 15), "grant not revoked")
688696
689697 back()