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 -2
View File
@@ -182,6 +182,7 @@ export function DigitalInlamning({ periodId }: { periodId: string }) {
const [submissions, setSubmissions] = useState<SubmissionRow[]>([])
const [loadingSubmissions, setLoadingSubmissions] = useState(false)
const [submissionsError, setSubmissionsError] = useState<string | null>(null)
const [pollingEvents, setPollingEvents] = useState(false)
useEffect(() => {
const signer = versions.find((version) => version.id === selectedVersionId)
@@ -384,6 +385,7 @@ export function DigitalInlamning({ periodId }: { periodId: string }) {
}
const handlePollEvents = async () => {
setPollingEvents(true)
try {
const res = await fetch('/api/extensions/ext/bolagsverket/poll-events', {
method: 'POST',
@@ -399,6 +401,8 @@ export function DigitalInlamning({ periodId }: { periodId: string }) {
toast({ title: 'Status uppdaterad från Bolagsverket' })
} catch {
toast({ title: 'Kunde inte hämta händelser', variant: 'destructive' })
} finally {
setPollingEvents(false)
}
}
@@ -794,8 +798,19 @@ export function DigitalInlamning({ periodId }: { periodId: string }) {
</CardHeader>
<CardContent className="space-y-4 text-sm">
<div className="flex justify-end">
<Button className="min-h-11" variant="outline" size="sm" onClick={() => void handlePollEvents()}>
<RefreshCcw className="mr-2 h-4 w-4" /> Uppdatera status
<Button
className="min-h-11"
variant="outline"
size="sm"
disabled={pollingEvents}
onClick={() => void handlePollEvents()}
>
{pollingEvents ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
) : (
<RefreshCcw className="mr-2 h-4 w-4" />
)}
{pollingEvents ? 'Uppdaterar …' : 'Uppdatera status'}
</Button>
</div>
{submissionsError && <p className="text-xs text-destructive">{submissionsError}</p>}