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 @State private var newMember = "" 7 @State private var newMember = ""
8 @State private var grantRepo = "" 8 @State private var grantRepo = ""
9 @State private var grantRole = "read" 9 @State private var grantRole = "read"
10 @State private var removingMember: String?
11 @State private var revokingGrant: TeamViewModel.Grant?
10 12
11 init(client: GitbayClient, org: String, team: String) { 13 init(client: GitbayClient, org: String, team: String) {
12 _model = State(initialValue: TeamViewModel(client: client, orgName: org, teamName: team)) 14 _model = State(initialValue: TeamViewModel(client: client, orgName: org, teamName: team))
@@ -27,7 +29,7 @@ struct TeamView: View {
27 .font(.gbSans(.subheadline)) 29 .font(.gbSans(.subheadline))
28 .swipeActions { 30 .swipeActions {
29 Button("Remove", role: .destructive) { 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 .swipeActions { 61 .swipeActions {
60 Button("Revoke", role: .destructive) { 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 .navigationTitle(model.teamName) 94 .navigationTitle(model.teamName)
93 .navigationBarTitleDisplayMode(.inline) 95 .navigationBarTitleDisplayMode(.inline)
94 .task { await model.load() } 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 .refreshable { await model.load() } 127 .refreshable { await model.load() }
96 } 128 }
97} 129}
gitbay/Views/Repos/RepoSettingsView.swift +17 −1
@@ -10,6 +10,7 @@ struct RepoSettingsView: View {
10 @State private var newTopic = "" 10 @State private var newTopic = ""
11 @State private var newBranch = "" 11 @State private var newBranch = ""
12 @State private var loadedOnce = false 12 @State private var loadedOnce = false
13 @State private var removingTopic: String?
13 14
14 init(client: GitbayClient, repo: String) { 15 init(client: GitbayClient, repo: String) {
15 _model = State(initialValue: RepoSettingsViewModel(client: client, repoPath: repo)) 16 _model = State(initialValue: RepoSettingsViewModel(client: client, repoPath: repo))
@@ -48,6 +49,21 @@ struct RepoSettingsView: View {
48 await model.load() 49 await model.load()
49 await model.loadDeps() 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 // MARK: - Sections 69 // MARK: - Sections
@@ -93,7 +109,7 @@ struct RepoSettingsView: View {
93 HStack(spacing: 3) { 109 HStack(spacing: 3) {
94 Text(topic) 110 Text(topic)
95 Button { 111 Button {
96 Task { await model.removeTopic(topic) } 112 removingTopic = topic
97 } label: { 113 } label: {
98 Image(systemName: "xmark.circle.fill") 114 Image(systemName: "xmark.circle.fill")
99 .foregroundStyle(.tertiary) 115 .foregroundStyle(.tertiary)
gitbayUITests/LiveSmokeUITests.swift +9 −1
@@ -450,8 +450,12 @@ extension LiveSmokeUITests {
450 .firstMatch.tap() 450 .firstMatch.tap()
451 let chip = app.staticTexts["ios"].firstMatch 451 let chip = app.staticTexts["ios"].firstMatch
452 XCTAssertTrue(chip.waitForExistence(timeout: 10), "topic did not appear") 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 app.scrollViews.buttons.firstMatch.tap() 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 XCTAssertTrue(waitForDisappearance(chip, timeout: 10), "topic did not remove") 459 XCTAssertTrue(waitForDisappearance(chip, timeout: 10), "topic did not remove")
456 460
457 let resolved = app.switches["Require threads resolved"].firstMatch 461 let resolved = app.switches["Require threads resolved"].firstMatch
@@ -684,6 +688,10 @@ extension LiveSmokeUITests {
684 XCTAssertTrue(grantRow.waitForExistence(timeout: 15), "grant not listed") 688 XCTAssertTrue(grantRow.waitForExistence(timeout: 15), "grant not listed")
685 grantRow.swipeLeft() 689 grantRow.swipeLeft()
686 app.buttons["Revoke"].firstMatch.tap() 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 XCTAssertTrue(waitForDisappearance(grantRow, timeout: 15), "grant not revoked") 695 XCTAssertTrue(waitForDisappearance(grantRow, timeout: 15), "grant not revoked")
688 696
689 back() 697 back()