app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

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:
Mapp/desktop/src/main/kotlin/org/radroots/studio/accounts/ui/AccountsScreen.kt | 35+++++++++++++++++++++++++++++++++++
Mapp/desktop/src/test/kotlin/org/radroots/studio/accounts/ui/AccountsScreenTest.kt | 71+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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() + } }