teqo-backup/src/components/campaign/shared/WizardStepFormChrome.tsx
Francisco Solla 9467e0f292 fix(campaign): C140 — manual dispatch in 18 forms prevents React 19 form reset on error
Fix manual dispatch em 18 formulários de /campanha que usavam <form action={submitAction}>: campos não controlados voltavam ao defaultValue após qualquer action assentada (sucesso ou erro) do React 19.

- WizardStepFormChrome alterado uma vez (cobre 3 wizard steps)
- 15 forms recebem dispatch manual arquivo a arquivo
- AdvisorPasswordResetButton (2 micro-forms sem campo digitável) ficou como está
- Unit spec novo campaignWizardStepFormChrome.unit.spec.tsx
- CHANGELOG-AGENTS.md cleaned (98 duplicates removed)

Also fixes:
- campaignPageActor writable gate only blocks advisors (was blocking coordinators too)
- campaignLeaderships e2e stability (waitForLoadState before attribute assertion)
2026-08-23 17:54:19 -03:00

101 lines
2.9 KiB
TypeScript

'use client'
import { startTransition, type FormEvent, type ReactNode } from 'react'
import { Button } from '@/components/ui/button'
import { Spinner } from '@/components/ui/Spinner'
import { cn } from '@/lib/utils'
type WizardStepFormChromeCommonProps = {
isPending: boolean
/** sr-only announcement read out while the step is saving. */
pendingAnnouncement: string
ctaLabel: string
children: ReactNode
leadingSubmit?: ReactNode
submitBarClassName?: string
ctaClassName?: string
}
/**
* Shared chrome of the wizard final steps (D11): pending attributes, submit
* bar with Spinner + "Salvando…" and the sr-only pending announcement. Two
* modes — `action` renders a `<form>` with a submit CTA (the three
* `useActionState` steps); `onCtaClick` renders a plain `<div>` with a
* button CTA (the votes step, which saves via `useTransition` + fetch).
*/
export const WizardStepFormChrome = (
props:
| ({
action: (formData: FormData) => void | Promise<void>
} & WizardStepFormChromeCommonProps)
| ({ onCtaClick: () => void } & WizardStepFormChromeCommonProps),
) => {
const {
isPending,
pendingAnnouncement,
ctaLabel,
children,
leadingSubmit,
submitBarClassName,
ctaClassName,
} = props
const submitButton = (
<>
{leadingSubmit}
<Button
type={'action' in props ? 'submit' : 'button'}
disabled={isPending}
onClick={'action' in props ? undefined : props.onCtaClick}
className={cn('min-h-11 min-w-[7rem]', ctaClassName)}
>
{isPending ? (
<>
<Spinner data-icon="inline-start" aria-hidden="true" />
Salvando…
</>
) : (
ctaLabel
)}
</Button>
</>
)
const pendingAttrs = {
'aria-busy': isPending || undefined,
'data-pending': isPending ? '' : undefined,
}
const content = (
<>
{children}
<div className={cn('flex items-center justify-end', submitBarClassName)}>{submitButton}</div>
<div aria-live="polite" className="sr-only">
{isPending ? pendingAnnouncement : null}
</div>
</>
)
// C140 — manual dispatch (no `action={props.action}`): React 19 resets
// uncontrolled fields after any settled form action, wiping typed values
// on a validation error — the step stays visible, so the wipe showed.
// `startTransition` keeps `isPending` correct for the imperative dispatch.
const handleSubmit =
'action' in props
? (event: FormEvent<HTMLFormElement>) => {
event.preventDefault()
startTransition(() => props.action(new FormData(event.currentTarget)))
}
: undefined
return 'action' in props ? (
<form onSubmit={handleSubmit} className="flex flex-col gap-6" {...pendingAttrs}>
{content}
</form>
) : (
<div className="flex flex-col gap-6" {...pendingAttrs}>
{content}
</div>
)
}