app

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

commit 5ad8ff2d3c25165ea2ba2f9fd2872021945615f1
parent 0ac555a6d9935a1097d9f0c010284276aeff6446
Author: triesap <tyson@radroots.org>
Date:   Sat,  1 Aug 2026 18:33:57 +0000

ui: present account list and selection

- replace the placeholder with the Accounts screen boundary
- render empty and lazy account collection states
- expose selected account details and semantics
- emit explicit selection actions from account rows

Diffstat:
Aapp/desktop/src/main/kotlin/org/radroots/studio/accounts/ui/AccountsScreen.kt | 120+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mapp/desktop/src/main/kotlin/org/radroots/studio/application/RadrootsApplication.kt | 19+++++--------------
Aapp/desktop/src/test/kotlin/org/radroots/studio/accounts/ui/AccountsScreenTest.kt | 57+++++++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 182 insertions(+), 14 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 @@ -0,0 +1,120 @@ +package org.radroots.studio.accounts.ui + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.text.BasicText +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.semantics.selected +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.unit.dp +import org.radroots.studio.accounts.model.Account +import org.radroots.studio.accounts.model.AccountsAction +import org.radroots.studio.accounts.model.AccountsState + +@Composable +fun AccountsScreen( + state: AccountsState, + onAction: (AccountsAction) -> Unit, +) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(24.dp) + .testTag("accounts-screen"), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + BasicText("radroots") + BasicText("Accounts") + + if (state.accounts.isEmpty()) { + BasicText( + text = "No accounts yet.", + modifier = Modifier.testTag("accounts-empty"), + ) + } else { + Row( + modifier = Modifier.fillMaxSize(), + horizontalArrangement = Arrangement.spacedBy(24.dp), + ) { + LazyColumn( + modifier = Modifier + .weight(1f) + .testTag("accounts-list"), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + items( + items = state.accounts, + key = { account -> account.id.value }, + ) { account -> + AccountRow( + account = account, + isSelected = account.id == state.selectedAccountId, + onSelect = { + onAction(AccountsAction.SelectAccount(account.id)) + }, + ) + } + } + + state.accounts + .firstOrNull { account -> account.id == state.selectedAccountId } + ?.let { account -> + AccountDetails( + account = account, + modifier = Modifier.weight(1f), + ) + } + } + } + } +} + +@Composable +private fun AccountRow( + account: Account, + isSelected: Boolean, + onSelect: () -> Unit, +) { + Column( + modifier = Modifier + .fillMaxWidth() + .semantics { selected = isSelected } + .testTag("account-row:${account.id.value}") + .clickable(onClick = onSelect) + .padding(8.dp), + ) { + BasicText(account.displayName) + BasicText(account.serverUrl) + if (isSelected) { + BasicText( + text = "Selected", + modifier = Modifier.testTag("account-selected:${account.id.value}"), + ) + } + } +} + +@Composable +private fun AccountDetails( + account: Account, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + BasicText("Selected account") + BasicText(account.displayName) + BasicText(account.serverUrl) + BasicText("Status: ${account.loginStatus.name}") + } +} diff --git a/app/desktop/src/main/kotlin/org/radroots/studio/application/RadrootsApplication.kt b/app/desktop/src/main/kotlin/org/radroots/studio/application/RadrootsApplication.kt @@ -1,32 +1,23 @@ package org.radroots.studio.application -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.text.BasicText import androidx.compose.runtime.Composable -import androidx.compose.runtime.key import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier import java.util.UUID import org.radroots.studio.accounts.model.AccountId import org.radroots.studio.accounts.model.AccountsState import org.radroots.studio.accounts.state.AccountIdFactory import org.radroots.studio.accounts.state.AccountsReducer import org.radroots.studio.accounts.state.AccountsStore +import org.radroots.studio.accounts.ui.AccountsScreen @Composable fun RadrootsApplication( store: AccountsStore = remember { createAccountsStore() }, ) { - key(store.state.value) { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center, - ) { - BasicText("radroots") - } - } + AccountsScreen( + state = store.state.value, + onAction = store::dispatch, + ) } internal fun createAccountsStore( 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 @@ -0,0 +1,57 @@ +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.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.v2.runComposeUiTest +import kotlin.test.Test +import kotlin.test.assertEquals +import org.radroots.studio.accounts.model.AccountsAction +import org.radroots.studio.accounts.testAccount +import org.radroots.studio.accounts.testAccountsState + +@OptIn(ExperimentalTestApi::class) +class AccountsScreenTest { + @Test + fun emptyStateIsVisible() = runComposeUiTest { + setContent { + AccountsScreen( + state = testAccountsState(), + onAction = {}, + ) + } + + onNodeWithTag("accounts-screen").assertIsDisplayed() + onNodeWithTag("accounts-empty").assertIsDisplayed() + onNodeWithText("No accounts yet.").assertIsDisplayed() + } + + @Test + fun accountListShowsSelectionAndEmitsExplicitSelection() = runComposeUiTest { + val first = testAccount() + val second = testAccount(id = "account-2", displayName = "Second Account") + val actions = mutableListOf<AccountsAction>() + setContent { + AccountsScreen( + state = testAccountsState(first, second), + onAction = actions::add, + ) + } + + onNodeWithTag("accounts-list").assertIsDisplayed() + onNodeWithTag("account-row:${first.id.value}").assertIsSelected() + onNodeWithTag( + testTag = "account-selected:${first.id.value}", + useUnmergedTree = true, + ).assertIsDisplayed() + onNodeWithTag("account-row:${second.id.value}").performClick() + + assertEquals( + listOf<AccountsAction>(AccountsAction.SelectAccount(second.id)), + actions, + ) + } +}