commit 85ee2a59c012383d3a86f21df033ba12400d5427
parent 9879566cf26cc936e5c470826e8ca2d88a9e5e32
Author: triesap <tyson@radroots.org>
Date: Sat, 1 Aug 2026 18:37:59 +0000
ui: add confirmed account removal controls
- request removal from each explicit account row
- present inline confirmation and cancellation actions
- retain accounts when cancellation is selected
- verify confirmed removal and valid selection through the store
Diffstat:
2 files changed, 106 insertions(+), 0 deletions(-)
diff --git a/app/desktop/src/main/kotlin/org/radroots/studio/accounts/ui/AccountsScreen.kt b/app/desktop/src/main/kotlin/org/radroots/studio/accounts/ui/AccountsScreen.kt
@@ -80,6 +80,16 @@ fun AccountsScreen(
onLogOut = {
onAction(AccountsAction.LogOut(account.id))
},
+ isPendingRemoval = account.id == state.pendingRemovalAccountId,
+ onRequestRemoval = {
+ onAction(AccountsAction.RequestRemoveAccount(account.id))
+ },
+ onConfirmRemoval = {
+ onAction(AccountsAction.ConfirmRemoveAccount(account.id))
+ },
+ onCancelRemoval = {
+ onAction(AccountsAction.CancelRemoveAccount)
+ },
)
}
}
@@ -176,6 +186,10 @@ private fun AccountRow(
onSelect: () -> Unit,
onLogIn: () -> Unit,
onLogOut: () -> Unit,
+ isPendingRemoval: Boolean,
+ onRequestRemoval: () -> Unit,
+ onConfirmRemoval: () -> Unit,
+ onCancelRemoval: () -> Unit,
) {
Column(
modifier = Modifier
@@ -205,6 +219,27 @@ private fun AccountRow(
onClick = onLogOut,
)
}
+ if (isPendingRemoval) {
+ BasicText("Remove this account?")
+ Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
+ TextAction(
+ text = "Confirm",
+ testTag = "remove-confirm",
+ onClick = onConfirmRemoval,
+ )
+ TextAction(
+ text = "Cancel",
+ testTag = "remove-cancel",
+ onClick = onCancelRemoval,
+ )
+ }
+ } else {
+ TextAction(
+ text = "Remove",
+ testTag = "account-remove:${account.id.value}",
+ onClick = onRequestRemoval,
+ )
+ }
}
}
diff --git a/app/desktop/src/test/kotlin/org/radroots/studio/accounts/ui/AccountsScreenTest.kt b/app/desktop/src/test/kotlin/org/radroots/studio/accounts/ui/AccountsScreenTest.kt
@@ -3,6 +3,8 @@ package org.radroots.studio.accounts.ui
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsSelected
+import androidx.compose.ui.test.assertCountEquals
+import androidx.compose.ui.test.onAllNodesWithTag
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
@@ -186,4 +188,73 @@ class AccountsScreenTest {
.assertIsDisplayed()
onNodeWithText("Status: LoggedIn").assertIsDisplayed()
}
+
+ @Test
+ fun removalControlsEmitRequestCancelAndExplicitConfirmation() = runComposeUiTest {
+ val account = testAccount()
+ val actions = mutableListOf<AccountsAction>()
+ setContent {
+ var state by remember { mutableStateOf(testAccountsState(account)) }
+ AccountsScreen(
+ state = state,
+ onAction = { action ->
+ actions.add(action)
+ state = when (action) {
+ is AccountsAction.RequestRemoveAccount -> state.copy(
+ pendingRemovalAccountId = action.accountId,
+ )
+ AccountsAction.CancelRemoveAccount -> state.copy(
+ pendingRemovalAccountId = null,
+ )
+ else -> state
+ }
+ },
+ )
+ }
+
+ onNodeWithTag("account-remove:${account.id.value}", useUnmergedTree = true)
+ .performClick()
+ onNodeWithTag("remove-cancel", useUnmergedTree = true).performClick()
+ onNodeWithTag("account-remove:${account.id.value}", useUnmergedTree = true)
+ .performClick()
+ onNodeWithTag("remove-confirm", useUnmergedTree = true).performClick()
+
+ assertEquals(
+ listOf(
+ AccountsAction.RequestRemoveAccount(account.id),
+ AccountsAction.CancelRemoveAccount,
+ AccountsAction.RequestRemoveAccount(account.id),
+ AccountsAction.ConfirmRemoveAccount(account.id),
+ ),
+ actions,
+ )
+ }
+
+ @Test
+ fun integratedRemovalCanBeCancelledThenConfirmed() = runComposeUiTest {
+ val first = testAccount()
+ val second = testAccount(id = "account-2", displayName = "Second Account")
+ val store = AccountsStore(
+ initialState = testAccountsState(first, second),
+ reducer = AccountsReducer { AccountId("unused") },
+ )
+ setContent {
+ AccountsScreen(
+ state = store.state.value,
+ onAction = store::dispatch,
+ )
+ }
+
+ onNodeWithTag("account-remove:${second.id.value}", useUnmergedTree = true)
+ .performClick()
+ onNodeWithTag("remove-cancel", useUnmergedTree = true).performClick()
+ onNodeWithTag("account-row:${second.id.value}").assertIsDisplayed()
+
+ onNodeWithTag("account-remove:${second.id.value}", useUnmergedTree = true)
+ .performClick()
+ onNodeWithTag("remove-confirm", useUnmergedTree = true).performClick()
+
+ onAllNodesWithTag("account-row:${second.id.value}").assertCountEquals(0)
+ onNodeWithTag("account-row:${first.id.value}").assertIsSelected()
+ }
}