polish(ui): loading feedback on sync/refresh buttons system-wide (#1156)

The "Synka bank nu" row in the transactions Importera split button fired
syncAll() with zero visual feedback. SplitButton now takes busy/busyLabel
per option: the primary face and the menu row swap to a spinning Loader2,
show the busy label and go inert until the action resolves. useBankSync
holds isBusy across the whole syncAll loop so the spinner does not
flicker between per-connection syncs.

Sweep of the rest of the system for async buttons missing the same
feedback (convention: disabled + Loader2 animate-spin + label swap):

- bokslut DigitalInlamning "Uppdatera status" (Bolagsverket poll): had no
  feedback at all; now disabled + spinner + "Uppdaterar ..." while polling
- Stripe settings "Synka nu": had disabled + label swap but a static icon
- Skatteverket "Verifiera": had disabled + label swap but no spinner
- AgentMemoryPanel "Dolj"/"Aterstall" row actions: static icons on async
  patch; now swap to spinner for the busy row

Checked and intentionally unchanged: Arcim migration "Synka igen" and
"Ateranslut" (the whole step flips to a spinner view synchronously on
click), skattekonto "Forsok igen" (page flips to loading view), AgentChat
"Generera om" (streaming indicator is the feedback).

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-07-24 19:37:34 +02:00
committed by GitHub
co-authored by Claude Fable 5
parent bb551d1d59
commit 8a9162b948
7 changed files with 77 additions and 25 deletions
+17 -9
View File
@@ -46,6 +46,9 @@ export function useBankSync() {
const hasBankSync = useCapability(CAPABILITY.bank_sync)
const [connections, setConnections] = useState<BankConn[] | null>(null)
const [busyId, setBusyId] = useState<string | null>(null)
// Holds isBusy true across the whole syncAll loop so the spinner doesn't
// flicker off between per-connection syncs.
const [syncingAll, setSyncingAll] = useState(false)
useEffect(() => {
if (!company?.id) return
@@ -156,14 +159,19 @@ export function useBankSync() {
// active connection in turn; with only dead connections it jumps straight
// to re-authorizing the first one (a retry can't revive a closed session).
async function syncAll() {
const conns = connections ?? []
const active = conns.filter((c) => c.status === 'active')
if (active.length === 0) {
if (conns[0]) await reconnect(conns[0])
return
}
for (const conn of active) {
await syncConnection(conn)
setSyncingAll(true)
try {
const conns = connections ?? []
const active = conns.filter((c) => c.status === 'active')
if (active.length === 0) {
if (conns[0]) await reconnect(conns[0])
return
}
for (const conn of active) {
await syncConnection(conn)
}
} finally {
setSyncingAll(false)
}
}
@@ -177,7 +185,7 @@ export function useBankSync() {
return {
connections,
busyId,
isBusy: busyId !== null,
isBusy: busyId !== null || syncingAll,
hasBankSync,
reconnect,
syncConnection,
@@ -26,7 +26,7 @@ export default function TransactionStatusBar({
const t = useTranslations('transactions')
const router = useRouter()
const { uiState, loaded } = useUiState()
const { connections, hasBankSync, syncAll, lastSyncedAt } = useBankSync()
const { connections, hasBankSync, syncAll, lastSyncedAt, isBusy } = useBankSync()
const formatAge = useAgeFormatter()
// "Synka bank nu" (concept: first menu row) only renders once a bank is
@@ -41,6 +41,8 @@ export default function TransactionStatusBar({
key: 'synka',
label: t('create_synka'),
icon: RefreshCw,
busy: isBusy,
busyLabel: t('bank_sync_button_syncing'),
description: lastSyncedAt
? t('create_synka_desc_last', { age: formatAge(lastSyncedAt) })
: t('create_synka_desc'),