feat(sdk-android): snapshot property delegate + feature-scoped files + screen fix (#34)

* rename web to inspect ui

* feat(sdk-android): add camelToSnakeCase conversion

* feat(sdk-android): add snapshot() property delegate with tests

* feat(folio): add feature-scoped sanderling snapshot objects

* refactor(folio): slim FolioApplication to snapshot object references

* fix(folio): rename route snapshot to screen, update spec.ts
This commit is contained in:
pj authored and GitHub committed 2026-04-22 19:55:45 +07:00
1 parent d5b6f6ccaf
commit 46abb28ed6
51 files changed
+297 -84

No files matched your search

@@ -3,14 +3,10 @@ package app.folio
import android.app.Application
import app.folio.data.AndroidLedgerContext
import app.folio.data.Repository
import app.folio.data.TxnType
import app.folio.feature.account.AddAccountUiState
import app.folio.feature.auth.LoginUiState
import app.folio.feature.ledger.AddTransactionUiState
import app.folio.navigation.Navigator
import app.folio.navigation.Route
import app.folio.platform.balanceOf
import app.folio.platform.signedAmount
import app.folio.sanderling.AccountSnapshots
import app.folio.sanderling.AuthSnapshots
import app.folio.sanderling.LedgerSnapshots
import app.folio.sanderling.NavigationSnapshots
import dev.sanderling.sdk.Sanderling
class FolioApplication : Application() {
@@ -19,75 +15,9 @@ class FolioApplication : Application() {
AndroidLedgerContext.context = applicationContext
Repository.init()
Sanderling.start(this)
Sanderling.extract("logged_in") { Repository.session.value != null }
Sanderling.extract("account_count") { Repository.accounts.value.size }
Sanderling.extract("total_balance") { balanceOf(Repository.transactions.value) }
Sanderling.extract("route") {
when (Navigator.current.value) {
Route.Login -> "login"
Route.Home -> "home"
Route.AddAccount -> "add-account"
is Route.Ledger -> "ledger"
is Route.AddTransaction -> "add-transaction"
}
}
Sanderling.extract("auth_status") {
if (Repository.session.value != null) "logged-in" else "logged-out"
}
Sanderling.extract("accounts") {
val txns = Repository.transactions.value
Repository.accounts.value.map { a ->
val rows = txns.filter { it.accountId == a.id }
mapOf(
"id" to a.id,
"name" to a.name,
"balance" to balanceOf(rows),
"txnCount" to rows.size,
)
}
}
Sanderling.extract("active_account_id") {
when (val r = Navigator.current.value) {
is Route.Ledger -> r.accountId
is Route.AddTransaction -> r.accountId
else -> null
}
}
Sanderling.extract("ledger_rows") {
val active = when (val r = Navigator.current.value) {
is Route.Ledger -> r.accountId
is Route.AddTransaction -> r.accountId
else -> null
}
if (active == null) emptyList()
else Repository.transactions.value
.filter { it.accountId == active }
.map {
mapOf(
"id" to it.id,
"accountId" to it.accountId,
"type" to if (it.type == TxnType.credit) "credit" else "debit",
"amount" to it.amount,
"signed" to signedAmount(it),
)
}
}
Sanderling.extract("ledger_balance") {
val active = when (val r = Navigator.current.value) {
is Route.Ledger -> r.accountId
is Route.AddTransaction -> r.accountId
else -> null
}
if (active == null) 0L
else balanceOf(Repository.transactions.value.filter { it.accountId == active })
}
Sanderling.extract("focused_input") { FocusTracker.current.value }
Sanderling.extract("txn_form_type") { AddTransactionUiState.txnFormType.value }
Sanderling.extract("txn_form_account_id") {
(Navigator.current.value as? Route.AddTransaction)?.accountId
}
Sanderling.extract("login_error") { LoginUiState.loginError.value }
Sanderling.extract("add_account_error") { AddAccountUiState.addAccountError.value }
Sanderling.extract("txn_error") { AddTransactionUiState.txnError.value }
AuthSnapshots
AccountSnapshots
LedgerSnapshots
NavigationSnapshots
}
}
@@ -0,0 +1,19 @@
package app.folio.sanderling
import app.folio.data.Repository
import app.folio.feature.account.AddAccountUiState
import app.folio.platform.balanceOf
import dev.sanderling.sdk.Sanderling
object AccountSnapshots {
val accountCount by Sanderling.snapshot { Repository.accounts.value.size }
val totalBalance by Sanderling.snapshot { balanceOf(Repository.transactions.value) }
val accounts by Sanderling.snapshot {
val txns = Repository.transactions.value
Repository.accounts.value.map { a ->
val rows = txns.filter { it.accountId == a.id }
mapOf("id" to a.id, "name" to a.name, "balance" to balanceOf(rows), "txnCount" to rows.size)
}
}
val addAccountError by Sanderling.snapshot { AddAccountUiState.addAccountError.value }
}
@@ -0,0 +1,11 @@
package app.folio.sanderling
import app.folio.data.Repository
import app.folio.feature.auth.LoginUiState
import dev.sanderling.sdk.Sanderling
object AuthSnapshots {
val loggedIn by Sanderling.snapshot { Repository.session.value != null }
val authStatus by Sanderling.snapshot { if (Repository.session.value != null) "logged-in" else "logged-out" }
val loginError by Sanderling.snapshot { LoginUiState.loginError.value }
}
@@ -0,0 +1,42 @@
package app.folio.sanderling
import app.folio.FocusTracker
import app.folio.data.Repository
import app.folio.data.TxnType
import app.folio.feature.ledger.AddTransactionUiState
import app.folio.navigation.Navigator
import app.folio.navigation.Route
import app.folio.platform.balanceOf
import app.folio.platform.signedAmount
import dev.sanderling.sdk.Sanderling
object LedgerSnapshots {
private val activeId
get() = when (val r = Navigator.current.value) {
is Route.Ledger -> r.accountId
is Route.AddTransaction -> r.accountId
else -> null
}
val activeAccountId by Sanderling.snapshot { activeId }
val ledgerRows by Sanderling.snapshot {
val active = activeId ?: return@snapshot emptyList<Any>()
Repository.transactions.value.filter { it.accountId == active }.map {
mapOf(
"id" to it.id,
"accountId" to it.accountId,
"type" to if (it.type == TxnType.credit) "credit" else "debit",
"amount" to it.amount,
"signed" to signedAmount(it),
)
}
}
val ledgerBalance by Sanderling.snapshot {
val active = activeId ?: return@snapshot 0L
balanceOf(Repository.transactions.value.filter { it.accountId == active })
}
val focusedInput by Sanderling.snapshot { FocusTracker.current.value }
val txnFormType by Sanderling.snapshot { AddTransactionUiState.txnFormType.value }
val txnFormAccountId by Sanderling.snapshot { (Navigator.current.value as? Route.AddTransaction)?.accountId }
val txnError by Sanderling.snapshot { AddTransactionUiState.txnError.value }
}
@@ -0,0 +1,17 @@
package app.folio.sanderling
import app.folio.navigation.Navigator
import app.folio.navigation.Route
import dev.sanderling.sdk.Sanderling
object NavigationSnapshots {
val screen by Sanderling.snapshot {
when (Navigator.current.value) {
Route.Login -> "login"
Route.Home -> "home"
Route.AddAccount -> "add-account"
is Route.Ledger -> "ledger"
is Route.AddTransaction -> "add-transaction"
}
}
}
+1 -1
View File
@@ -35,7 +35,7 @@ const loggedIn = extract<boolean>(
(state) => (state.snapshots.logged_in as boolean) ?? false,
);
const route = extract<string>(
(state) => (state.snapshots.route as string) ?? "",
(state) => (state.snapshots.screen as string) ?? "",
);
const accounts = extract<AccountSnapshot[]>(
(state) => (state.snapshots.accounts as AccountSnapshot[]) ?? [],