feat(campanha): B168 — desfazer encadeamento automático das ações rápidas do wizard
Ações de ajuste (votos, tendência, atualização, liderança) agora retornam à origem ao salvar (toast + from allowlist B110 / Início), sem avançar sozinhas para outro wizard. Removidos fila de encadeamento (wizardActionChain.ts), param entry, skip/Pular (chrome mobile sempre X) e prefill de nota sem produtor. Liderança: botão pós-save 'Continuar' → 'Concluir' (origem). Sem migration.
This commit is contained in:
parent
b838d19447
commit
7faf3ed27f
29 changed files with 376 additions and 1104 deletions
|
|
@ -4,6 +4,8 @@ Conteúdo movido do `AGENTS.md` em 2026-07-30 (fatiamento do paradigma de agente
|
|||
|
||||
---
|
||||
|
||||
**Recently resolved (2026-08-08):** **B168** (Issue #422) — desfeito o **encadeamento automático** das ações rápidas do wizard (B98 era o precedente que este item inverte; evidência de campo: o coordenador rejeitou a tela avançando sozinha). Ao salvar **qualquer** ação de ajuste (votos, tendência, atualização, liderança), o usuário volta à origem (`from` allowlist B110 mantido; Início sem `from`) com toast — nunca avança para outro wizard. Removidos a fila/pipeline `wizardActionChain.ts` (fundido `wizardReturnHref` + `wizardPreviousHref` só com passos internos em `campaignActionRoutes.ts`), o param `entry`, os skip resolvers/`Pular` (`WizardSkipTrailing`/`WizardTrendSkipTrailing` deletados, chrome mobile sempre “X”) e o prefill de nota sem produtor (`notePrefill`/`voteFrom`/`voteTo`). Tendência: pós-save o RSC refresha com status já aplicado e o **client** é o único dono da navegação (toast + retorno) — o redirect servidor engolia o toast (B97 corrigido no caminho). Liderança: botão flutuante pós-save passou de “Continuar” (avançava) para **“Concluir”** (retorna à origem). Sem migration. Intenção [`desfazer-encadeamento-acoes-wizard.md`](plans/desfazer-encadeamento-acoes-wizard.md); impl [`desfazer-encadeamento-acoes-wizard-impl.md`](plans/desfazer-encadeamento-acoes-wizard-impl.md).
|
||||
|
||||
**Recently resolved (2026-08-08):** **worktree next provisiona ambiente isolado por worktree** — agentes em paralelo não disputam mais porta 3000, o `teqo_test` compartilhado nem um segundo Postgres no 5432: `pnpm worktree next [--go] [--no-migrate]` deriva deterministicamente slot do código da Issue (C15→15; hash sem dígitos; bump em colisão entre worktrees vivos) → dev server `3100+slot`, bancos `teqo_wt<slot>`/`teqo_wt<slot>_test` criados no container compartilhado com migrations aplicadas; escreve `.env.local` + `.env.test.local` (gitignored, marcador gerado; envs manuais nunca sobrescritos) e copia PAYLOAD*SECRET/Blob/VAPID do main. `kill` dropa os bancos gerados (best-effort, WITH FORCE). Suites leem `.env.test.local` depois de `.env.test` (vitest.setup/playwright/db-doctor/gate-ci espelham o layering); `assertTestDatabase` aceita `teqo_test` ou `teqo*<worktree>\_test`em localhost só (fail-closed preservado).`db:start`/`db:stop`fixam`-p teqo`(um container, qualquer diretório); max_connections 200→400. Sem Docker (Cursor Cloud): degrada ao compartilhado com aviso. Derivação pura`scripts/lib/worktree-env.mjs` (unit-tested).
|
||||
|
||||
**Recently resolved (2026-08-07):** **C15** (Issue #390) — `/campanha/agenda` virou a entrada operacional staff com FullCalendar v7 route-local (semana/mês/lista), filtros compartilháveis por município/deputado/tag, criação pelo slot, detalhe por URL e remarcação drag/resize sob RBAC, transação e lock por atividade; `/campanha/atividades` e giros permanecem como lista/fluxos legados. Sem migration. Impl [`fullcalendar-agenda-campanha-impl.md`](plans/fullcalendar-agenda-campanha-impl.md).
|
||||
|
|
|
|||
81
docs/plans/desfazer-encadeamento-acoes-wizard-impl.md
Normal file
81
docs/plans/desfazer-encadeamento-acoes-wizard-impl.md
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
# Impl: Desfazer o encadeamento automático das ações rápidas do wizard
|
||||
|
||||
Status: aprovado
|
||||
Atualizado em: 2026-08-08
|
||||
Issue: #422
|
||||
Intenção: docs/plans/desfazer-encadeamento-acoes-wizard.md
|
||||
Appetite restante: herdado (~0,5–1 dia eng); sem migration
|
||||
|
||||
## Leitura da intenção
|
||||
|
||||
- **Outcome:** ao concluir **qualquer** ação rápida de ajuste (votos, tendência, atualização, liderança), o usuário volta à origem — **nunca** avança sozinho para outro wizard. Chrome sem “Pular” em etapa nenhuma; “X” (sair da ação) em todas as etapas e retorna à origem. Fila encadeada, `entryAction` de cadeia e “próxima etapa sugerida” pós-save não existem mais.
|
||||
- **O que NÃO negociar:** retorno à origem via `from` allowlist (**B110** — manter); wizards standalone intactos; liderança continua fora (já gateada); sem migration; sem redesenhos de chassis/header; sem mexer no catálogo/FAB/strip das ações.
|
||||
- **O que reavaliar:** a “Direção no codebase” da intenção mira `wizardActionChain.ts` (matriz + continue/end), os skip resolvers e as pontas de sucesso. Confirmei e ampliei: a cadeia está **ativa em toda entrada**, não só via deep-link — `resolveWizardChainEntry(undefined, X) → X` faz TODO wizard salvo avançar para o próximo elo (o bug reportado acontece até partindo do card do Início, sem `entry=`). A cadeia é o único produtor de `entry=`. Conclusão: o caminho é **remoção total**, não “desativar pontas”.
|
||||
|
||||
## Abordagem recomendada
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A["wizardActionChain.ts<br/>(matriz + continue + entry)"] --> D["DELETAR"]
|
||||
B["skip resolvers + WIZARD_CHAIN_SKIP_LABEL<br/>(4 libs)"] --> D
|
||||
C["skip UI: Chrome state, MobileTopBar,<br/>Shell trailingAction, Wizard*SkipTrailing"] --> D
|
||||
E["Fusão em campaignActionRoutes.ts:<br/>wizardReturnHref (ex chainEnd) +<br/>wizardPreviousHref (só passos internos)"] --> F["5 componentes + page.tsx:<br/>sucesso → router.push(wizardReturnHref)<br/>sem skips, sem entryAction"] --> G["Tests: unit + e2e"]
|
||||
```
|
||||
|
||||
**Opções consideradas:** A (neutralizar sem limpar) | B (remoção total) | C (B + manter prefill `notePrefill`)
|
||||
**Recomendação:** **B** — porque:
|
||||
|
||||
- A aceita de produto diz “limpeza total do plumbing/código morto é decisão de implementação” e “o resultado não pode ter 'Pular'/fila morta aparente”.
|
||||
- A cadeia é o único produtor de `entry=`, `notePrefill`, `voteFrom`/`voteTo` (verifiquei: nenhuma registry passa esses params hoje). Manter o plumbing = código morto que o `knip`/`tsc` vão apontar + “dois sistemas de navegação” que a intenção proíbe.
|
||||
- B é mecanicamente pequeno (módulos pequenos, call sites enumerados).
|
||||
**Rejeitadas:** A porque deixa o módulo cadeia órfão e o smash do `skip` no chrome vivo (fila morta visível); C porque manter `notePrefill` sem produtor é especulação (adapter com 1 call site).
|
||||
|
||||
### Componentes / mudanças
|
||||
|
||||
- **`src/lib/wizardActionChain.ts`** → **deletar arquivo inteiro**. `wizardReturnHref` (ex `wizardChainEndHref`; allowlist B110 → senão `CAMPAIGN_HOME`) e `wizardPreviousHref` (só passos internos; cair a ramificação de elo da cadeia) migram para o módulo profundo que já constrói URIs de wizard — **`src/lib/campaignActionRoutes.ts`** (evita um `wizardNavigation.ts` raso; sem ciclo).
|
||||
- **`src/lib/campaignActionRoutes.ts`**: remover `WIZARD_ENTRY_ACTION_QUERY_KEY`, `parseWizardEntryActionParam`, `WizardActionHrefOptions.entryAction`, param `entryAction` de `wizardTrendHref`, `WIZARD_NOTE_PREFILL_QUERY_KEY`, `WIZARD_VOTE_FROM_QUERY_KEY`, `WIZARD_VOTE_TO_QUERY_KEY`. `wizardTrendHref` final: `(actionSlug, municipalitySlug?, trendStatus?, returnPath?)`. **Manter**: `isWizardReturnPath`/`parseWizardReturnPath`/`appendWizardReturnPath` (B110), `campaignActionEntryHref`, `isCampaignWizardActionId`, `wizardActionHref` (`leadershipId`/`returnPath`), `parseWizardLeadershipIdParam`.
|
||||
- **`wizardUpdateUi.ts` / `politicalTrendWizardUi.ts` / `wizardLeadershipContract.ts` / `campaignWizardCopy.ts`**: remover `*SkipAction`, `resolve*Skip`, `shouldShow*/show*`, `WIZARD_CHAIN_SKIP_LABEL`. Em `politicalTrendWizardUi` remover também `PoliticalTrendNotePrefillSource`/`buildPoliticalTrendNotePrefill`/`resolvePoliticalTrendNotePrefillSource` (sem produtor). **Manter** labels/títulos e `resolveWizardTrendNoteDestination` (redirect pós-save RSC).
|
||||
- **Page `acoes/[slug]/page.tsx`**: remover parsing de `entry`/prefill; ramo `noteDestination === 'home'` simplifica para `redirect(wizardReturnHref(returnPath))`; drop de props `entryAction`/`prefillExtraParams`/`initialNote`.
|
||||
- **Componentes**: `WizardExpectedVotesStep`, `WizardTrendNoteStep`, `WizardTrendChoiceStep`, `WizardUpdateBodyStep`, `WizardLeadershipStep` — drop `entryAction`+`skip`; sucesso → `router.push(wizardReturnHref(returnPath))`. `CampaignWizardShell` drop `skip`/`trailingAction`; `CampaignWizardChromeContext` drop `CampaignWizardChromeSkip`/campo `skip`; `CampaignMobileTopBar` **sempre** X (`chrome.skip` some). Deletar `WizardSkipTrailing.tsx` + `WizardTrendSkipTrailing.tsx`.
|
||||
- **Liderança — botão flutuante pós-save:** destino → `wizardReturnHref(returnPath)`; **rótulo “Continuar” → “Concluir”** (opção A recomendada; ver Rabbit holes). `handleSaved` (grid + dirty) inalterado.
|
||||
- **Migration:** sem migration (nenhuma mudança de schema).
|
||||
- **Access / Consent:** nenhuma mudança.
|
||||
- **UI:** Impeccable A/B — mudanças pontuais no chrome (remover skip) e copy; sem redesign.
|
||||
|
||||
## Fases verificáveis
|
||||
|
||||
1. **Libs + tests de domínio** — remoção em `campaignActionRoutes` + libs de wizard; `wizardActionChain.ts` → deletado (helpers fundidos); reescrever `wizardActionChain.unit.spec.ts` → `wizardNavigation`-equivalent em `campaignActionRoutes.unit.spec.ts`; atender `tests/unit` iterativamente.
|
||||
2. **UI** — page + 5 steps + chrome (shell/context/mobile top bar); deletar skip components; testes de componente (`wizardExpectedVotesStep.unit.spec.tsx`) + e2e afetadas (`campaignHomeActions.e2e.spec.ts`, `campaignWizardChrome.e2e.spec.ts`).
|
||||
3. **Gates** — `pnpm gate:fast` na iteração; `pnpm test:e2e:affected`; entrega com `pnpm push` (gate:ci).
|
||||
|
||||
## Rabbit holes / Não escopo (engenharia)
|
||||
|
||||
- Não redesenhos de chassis/header (nenhum “X” de desktop novo — hoje o desktop só usa o shell; mobile já tem X e passa a tê-lo sempre).
|
||||
- Não mexer no catálogo/FAB/strip do Início nem nas registries de quick actions (nenhuma passa `entry` hoje).
|
||||
- Não adicionar “continuar para a próxima” pós-save (rabbit hole de produto da intenção).
|
||||
- Não reter `notePrefill`/`voteFrom`/`voteTo` como “feature futura” (nenhum produtor).
|
||||
- **Decisão em aberto para o gate humano:** rótulo do botão flutuante de liderança — **A) “Concluir”** (recomendado: a ação terminou e sai) vs **B) manter “Continuar”** (menor churn, mas remete a encadeamento). Aplica-se também se o botão deve sumir (ver Riscos).
|
||||
|
||||
## Riscos e mitigação
|
||||
|
||||
- **Deep-link externo com `entry=` antigo** passa a se comportar como standalone (param ignorado) — sem guarda de URL pública; aceitável (params internos, sem contrato). E2E cobre com asserção “sem Pular mesmo com `entry=` stale”.
|
||||
- **Prefill de nota de tendência some** — reduz superfície; hoje sem produtor. Se o produto quiser reabrir, vira issue nova.
|
||||
- **E2E de tendência inexistente** — preciso adicionar “mudar tendência → salvar → volta à origem” (o próprio bug reportado). Custo de login; mantenho mínimo.
|
||||
- **knip/tsc** — enumerei todas as referências de símbolos removidos (8 arquivos de teste + 7 src); verifico `git grep` antes de deletar.
|
||||
- **Dupla navegação pós-save da tendência** (redirect servidor `noteDestination==='home'` + push client) converge para a MESMA origem — comportamento já existente hoje no standalone; manter.
|
||||
|
||||
## Aceite de engenharia
|
||||
|
||||
- [x] Aceite de produto da intenção ainda coberto (sem encadeamento, sem Pular, X em toda etapa, retorno à origem, B110 mantido)
|
||||
- [x] Invariantes AGENTS/engineering-standards (sem migration; module-owner editado; copy pt-BR)
|
||||
- [x] Testes de domínio previstos (unit `wizardReturnHref`/`wizardPreviousHref`/routes; componente votes; e2e retorno à origem para as 4 ações + tendência)
|
||||
|
||||
## Self-score decision-quality
|
||||
|
||||
1. Decisões caras têm rejeitadas? **Sim** (A/B/C explícitas, rótulo liderança aberto)
|
||||
2. Cabe no appetite? **Sim** (remoção pequena, todo call site enumerado)
|
||||
3. Rabbit holes nomeados? **Sim**
|
||||
4. Depth check reusa shells/helpers? **Sim** (funde no módulo profundo de routes; zero módulos novos rasos)
|
||||
5. Intenção satisfeita? **Sim** — engenharia remove o mecanismo, não reescreve o outcome.
|
||||
|
||||
**Score: 5/5**
|
||||
|
|
@ -11,62 +11,28 @@ import {
|
|||
CAMPAIGN_WIZARD_ACTION_SLUGS,
|
||||
hasWizardScenarioParam,
|
||||
isCampaignWizardActionSlug,
|
||||
parseWizardEntryActionParam,
|
||||
parseWizardLeadershipIdParam,
|
||||
parseWizardMunicipioParam,
|
||||
parseWizardReturnPath,
|
||||
resolveWizardTrendStatusParam,
|
||||
WIZARD_ENTRY_ACTION_QUERY_KEY,
|
||||
WIZARD_LEADERSHIP_ID_QUERY_KEY,
|
||||
WIZARD_MUNICIPIO_QUERY_KEY,
|
||||
WIZARD_NOTE_PREFILL_QUERY_KEY,
|
||||
WIZARD_RETURN_PATH_QUERY_KEY,
|
||||
WIZARD_SCENARIO_QUERY_KEY,
|
||||
WIZARD_TREND_STATUS_QUERY_KEY,
|
||||
WIZARD_VOTE_FROM_QUERY_KEY,
|
||||
WIZARD_VOTE_TO_QUERY_KEY,
|
||||
wizardActionHref,
|
||||
wizardReturnHref,
|
||||
wizardTrendHref,
|
||||
} from '@/lib/campaignActionRoutes'
|
||||
import { isStaffCampaignRole } from '@/lib/campaignRoles'
|
||||
import {
|
||||
buildPoliticalTrendNotePrefill,
|
||||
resolvePoliticalTrendNotePrefillSource,
|
||||
resolveWizardTrendNoteDestination,
|
||||
} from '@/lib/politicalTrendWizardUi'
|
||||
import { resolveWizardTrendNoteDestination } from '@/lib/politicalTrendWizardUi'
|
||||
import { toVoteEstimateScenarioViewModel } from '@/lib/voteEstimate'
|
||||
import {
|
||||
isWizardChainActionId,
|
||||
wizardChainContinueHref,
|
||||
wizardChainEndHref,
|
||||
} from '@/lib/wizardActionChain'
|
||||
import config from '@/payload.config'
|
||||
import { requireCampaignPageActor } from '@/utilities/campaignPageActor'
|
||||
import { loadWizardLeadershipTiles } from '@/utilities/leadership/leadershipData'
|
||||
import { loadMunicipalityScope } from '@/utilities/municipality/campaignMunicipalityScope'
|
||||
import { getPayload } from 'payload'
|
||||
|
||||
const politicalTrendWizardPrefillParams = (
|
||||
searchParams: Record<string, string | string[] | undefined>,
|
||||
): Record<string, string> | undefined => {
|
||||
const keys = [
|
||||
WIZARD_NOTE_PREFILL_QUERY_KEY,
|
||||
WIZARD_VOTE_FROM_QUERY_KEY,
|
||||
WIZARD_VOTE_TO_QUERY_KEY,
|
||||
] as const
|
||||
const result: Record<string, string> = {}
|
||||
|
||||
for (const key of keys) {
|
||||
const raw = searchParams[key]
|
||||
const value = Array.isArray(raw) ? raw[0] : raw
|
||||
if (value?.trim()) {
|
||||
result[key] = value.trim()
|
||||
}
|
||||
}
|
||||
|
||||
return Object.keys(result).length > 0 ? result : undefined
|
||||
}
|
||||
|
||||
type CampaignActionWizardPageProps = {
|
||||
params: Promise<{ slug: string }>
|
||||
searchParams: Promise<Record<string, string | string[] | undefined>>
|
||||
|
|
@ -104,7 +70,7 @@ export default async function CampaignActionWizardPage({
|
|||
return (
|
||||
<WizardMunicipalitySearchStep
|
||||
actionSlug={slug}
|
||||
previousHref={wizardChainEndHref(returnPath)}
|
||||
previousHref={wizardReturnHref(returnPath)}
|
||||
returnPath={returnPath}
|
||||
accessibleMunicipalities={accessibleMunicipalities}
|
||||
/>
|
||||
|
|
@ -131,10 +97,6 @@ export default async function CampaignActionWizardPage({
|
|||
redirect(wizardActionHref(slug, municipalitySlug))
|
||||
}
|
||||
|
||||
const entryAction = parseWizardEntryActionParam(
|
||||
resolvedSearchParams[WIZARD_ENTRY_ACTION_QUERY_KEY],
|
||||
)
|
||||
|
||||
return (
|
||||
<WizardExpectedVotesStep
|
||||
actionSlug={slug}
|
||||
|
|
@ -142,16 +104,12 @@ export default async function CampaignActionWizardPage({
|
|||
municipalityName={municipality.name}
|
||||
municipalitySlug={municipality.slug}
|
||||
initialExpectedVotes={toVoteEstimateScenarioViewModel(municipality.expectedVotes)}
|
||||
entryAction={entryAction}
|
||||
returnPath={returnPath}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if (slug === CAMPAIGN_WIZARD_ACTION_SLUGS['update-leadership']) {
|
||||
const entryAction = parseWizardEntryActionParam(
|
||||
resolvedSearchParams[WIZARD_ENTRY_ACTION_QUERY_KEY],
|
||||
)
|
||||
const initialLeadershipId = parseWizardLeadershipIdParam(
|
||||
resolvedSearchParams[WIZARD_LEADERSHIP_ID_QUERY_KEY],
|
||||
)
|
||||
|
|
@ -163,7 +121,6 @@ export default async function CampaignActionWizardPage({
|
|||
municipalityId={municipality.id}
|
||||
municipalityName={municipality.name}
|
||||
municipalitySlug={municipality.slug}
|
||||
entryAction={entryAction}
|
||||
initialTiles={tiles}
|
||||
initialLeadershipId={initialLeadershipId}
|
||||
returnPath={returnPath}
|
||||
|
|
@ -172,43 +129,25 @@ export default async function CampaignActionWizardPage({
|
|||
}
|
||||
|
||||
if (slug === CAMPAIGN_WIZARD_ACTION_SLUGS['change-trend']) {
|
||||
const entryAction = parseWizardEntryActionParam(
|
||||
resolvedSearchParams[WIZARD_ENTRY_ACTION_QUERY_KEY],
|
||||
)
|
||||
const prefillExtraParams = politicalTrendWizardPrefillParams(resolvedSearchParams)
|
||||
const prefillSource = resolvePoliticalTrendNotePrefillSource({
|
||||
entryAction,
|
||||
notePrefill: resolvedSearchParams[WIZARD_NOTE_PREFILL_QUERY_KEY],
|
||||
voteFrom: resolvedSearchParams[WIZARD_VOTE_FROM_QUERY_KEY],
|
||||
voteTo: resolvedSearchParams[WIZARD_VOTE_TO_QUERY_KEY],
|
||||
})
|
||||
const initialNote = buildPoliticalTrendNotePrefill(prefillSource)
|
||||
const currentStatus = municipality.politicalTrend?.status ?? null
|
||||
const { trendStatus, invalid } = resolveWizardTrendStatusParam(
|
||||
resolvedSearchParams[WIZARD_TREND_STATUS_QUERY_KEY],
|
||||
)
|
||||
|
||||
if (invalid) {
|
||||
redirect(wizardTrendHref(slug, municipalitySlug, undefined, entryAction, prefillExtraParams))
|
||||
redirect(wizardTrendHref(slug, municipalitySlug))
|
||||
}
|
||||
|
||||
if (trendStatus) {
|
||||
const noteDestination = resolveWizardTrendNoteDestination(trendStatus, currentStatus)
|
||||
|
||||
if (noteDestination === 'home') {
|
||||
// Chained session: advance the queue (B98). Standalone stale deep-link: Início (B97).
|
||||
if (isWizardChainActionId(entryAction)) {
|
||||
redirect(
|
||||
wizardChainContinueHref(entryAction, 'change-trend', municipalitySlug, returnPath),
|
||||
)
|
||||
}
|
||||
redirect(wizardChainEndHref(returnPath))
|
||||
}
|
||||
|
||||
// Post-save RSC refresh (status already matches) lands in 'home': render
|
||||
// the note step and let the client own the toast + return navigation
|
||||
// (B97 / B168) — a server redirect here would swallow the toast. The
|
||||
// choice tiles only ever link selectable (≠ current) statuses, so a fresh
|
||||
// deep-link cannot realistically reach 'home'.
|
||||
if (noteDestination === 'choice') {
|
||||
redirect(
|
||||
wizardTrendHref(slug, municipalitySlug, undefined, entryAction, prefillExtraParams),
|
||||
)
|
||||
redirect(wizardTrendHref(slug, municipalitySlug))
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -218,8 +157,6 @@ export default async function CampaignActionWizardPage({
|
|||
municipalityName={municipality.name}
|
||||
municipalitySlug={municipality.slug}
|
||||
trendStatus={trendStatus}
|
||||
initialNote={initialNote}
|
||||
entryAction={entryAction}
|
||||
returnPath={returnPath}
|
||||
/>
|
||||
)
|
||||
|
|
@ -231,25 +168,18 @@ export default async function CampaignActionWizardPage({
|
|||
municipalityName={municipality.name}
|
||||
municipalitySlug={municipality.slug}
|
||||
currentStatus={currentStatus}
|
||||
entryAction={entryAction}
|
||||
prefillExtraParams={prefillExtraParams}
|
||||
returnPath={returnPath}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if (slug === CAMPAIGN_WIZARD_ACTION_SLUGS['register-update']) {
|
||||
const entryAction = parseWizardEntryActionParam(
|
||||
resolvedSearchParams[WIZARD_ENTRY_ACTION_QUERY_KEY],
|
||||
)
|
||||
|
||||
return (
|
||||
<WizardUpdateBodyStep
|
||||
actionSlug={slug}
|
||||
municipalityId={municipality.id}
|
||||
municipalityName={municipality.name}
|
||||
municipalitySlug={municipality.slug}
|
||||
entryAction={entryAction}
|
||||
returnPath={returnPath}
|
||||
isStaff={isStaffCampaignRole(user.role)}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import { toast } from 'sonner'
|
|||
|
||||
import { SupportStatusBadge } from '@/components/campaign/leadership/SupportStatusBadge'
|
||||
import { WizardLeadershipForm } from '@/components/campaign/leadership/WizardLeadershipForm'
|
||||
import { CampaignWizardNavLink } from '@/components/campaign/shared/CampaignWizardNavLink'
|
||||
import { CampaignWizardShell } from '@/components/campaign/shared/CampaignWizardShell'
|
||||
import { Badge } from '@/components/ui/Badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
|
|
@ -19,11 +18,11 @@ import {
|
|||
DrawerTitle,
|
||||
} from '@/components/ui/Drawer'
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import type { CampaignWizardActionId } from '@/lib/campaignActionRoutes'
|
||||
import { wizardPreviousHref, wizardReturnHref } from '@/lib/campaignActionRoutes'
|
||||
import { recordLastActedMunicipality } from '@/lib/campaignLastActedMunicipality'
|
||||
import {
|
||||
WIZARD_LEADERSHIP_ADD_TILE_LABEL,
|
||||
WIZARD_LEADERSHIP_CONTINUE_LABEL,
|
||||
WIZARD_LEADERSHIP_CONCLUDE_LABEL,
|
||||
WIZARD_LEADERSHIP_EMPTY_GRID,
|
||||
WIZARD_LEADERSHIP_EMPTY_NOTES,
|
||||
WIZARD_LEADERSHIP_FORM_CREATE_TITLE,
|
||||
|
|
@ -34,23 +33,13 @@ import {
|
|||
} from '@/lib/campaignWizardCopy'
|
||||
import { truncateNameAtWordBoundary } from '@/lib/leadershipNameTruncate'
|
||||
import { cn } from '@/lib/utils'
|
||||
import {
|
||||
resolveWizardChainEntry,
|
||||
wizardChainContinueHref,
|
||||
wizardChainEndHref,
|
||||
wizardPreviousHref,
|
||||
} from '@/lib/wizardActionChain'
|
||||
import {
|
||||
resolveWizardLeadershipSkip,
|
||||
type WizardLeadershipTileViewModel,
|
||||
} from '@/lib/wizardLeadershipContract'
|
||||
import { type WizardLeadershipTileViewModel } from '@/lib/wizardLeadershipContract'
|
||||
|
||||
type WizardLeadershipStepProps = {
|
||||
actionSlug: string
|
||||
municipalityId: number
|
||||
municipalityName: string
|
||||
municipalitySlug: string
|
||||
entryAction?: CampaignWizardActionId
|
||||
initialTiles: WizardLeadershipTileViewModel[]
|
||||
initialLeadershipId?: number
|
||||
returnPath?: string
|
||||
|
|
@ -71,7 +60,6 @@ export const WizardLeadershipStep = ({
|
|||
municipalityId,
|
||||
municipalityName,
|
||||
municipalitySlug,
|
||||
entryAction,
|
||||
initialTiles,
|
||||
initialLeadershipId,
|
||||
returnPath,
|
||||
|
|
@ -89,14 +77,6 @@ export const WizardLeadershipStep = ({
|
|||
const [dirty, setDirty] = useState(false)
|
||||
const [infoTile, setInfoTile] = useState<WizardLeadershipTileViewModel | null>(null)
|
||||
|
||||
const skipConfig = resolveWizardLeadershipSkip(entryAction, municipalitySlug, returnPath)
|
||||
const chainContinueHref = wizardChainContinueHref(
|
||||
resolveWizardChainEntry(entryAction, 'update-leadership'),
|
||||
'update-leadership',
|
||||
municipalitySlug,
|
||||
returnPath,
|
||||
)
|
||||
|
||||
const stepTitle =
|
||||
mode.kind === 'grid'
|
||||
? WIZARD_LEADERSHIP_GRID_TITLE
|
||||
|
|
@ -104,12 +84,6 @@ export const WizardLeadershipStep = ({
|
|||
? WIZARD_LEADERSHIP_FORM_EDIT_TITLE
|
||||
: WIZARD_LEADERSHIP_FORM_CREATE_TITLE
|
||||
|
||||
const trailingAction = skipConfig ? (
|
||||
<Button variant="ghost" size="sm" className="min-h-11 px-2 text-sm" asChild>
|
||||
<CampaignWizardNavLink href={skipConfig.href}>{skipConfig.label}</CampaignWizardNavLink>
|
||||
</Button>
|
||||
) : undefined
|
||||
|
||||
const handleSaved = useCallback(() => {
|
||||
recordLastActedMunicipality(municipalitySlug)
|
||||
setDirty(true)
|
||||
|
|
@ -120,7 +94,7 @@ export const WizardLeadershipStep = ({
|
|||
|
||||
const handleContinue = () => {
|
||||
startContinueTransition(() => {
|
||||
router.push(chainContinueHref)
|
||||
router.push(wizardReturnHref(returnPath))
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -133,13 +107,10 @@ export const WizardLeadershipStep = ({
|
|||
actionSlug,
|
||||
stepKind: mode.kind === 'form' ? 'leadership-form' : 'leadership-grid',
|
||||
municipalitySlug,
|
||||
entryAction,
|
||||
returnPath,
|
||||
})}
|
||||
dismissHref={wizardChainEndHref(returnPath)}
|
||||
dismissHref={wizardReturnHref(returnPath)}
|
||||
municipalityLabel={municipalityName}
|
||||
skip={skipConfig}
|
||||
trailingAction={trailingAction}
|
||||
contentFocus={mode.kind === 'form' ? 'none' : 'title'}
|
||||
>
|
||||
{mode.kind === 'form' ? (
|
||||
|
|
@ -184,7 +155,7 @@ export const WizardLeadershipStep = ({
|
|||
{dirty && mode.kind === 'grid' ? (
|
||||
<div className="fixed bottom-4 right-4 z-20 pb-[max(0px,env(safe-area-inset-bottom))]">
|
||||
<Button className="min-h-11 shadow-md" onClick={handleContinue} disabled={isContinuing}>
|
||||
{WIZARD_LEADERSHIP_CONTINUE_LABEL}
|
||||
{WIZARD_LEADERSHIP_CONCLUDE_LABEL}
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
|
|
|
|||
|
|
@ -1,13 +0,0 @@
|
|||
import { CampaignWizardNavLink } from '@/components/campaign/shared/CampaignWizardNavLink'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import type { WizardUpdateSkipAction } from '@/lib/wizardUpdateUi'
|
||||
|
||||
type WizardSkipTrailingProps = {
|
||||
skip: WizardUpdateSkipAction
|
||||
}
|
||||
|
||||
export const WizardSkipTrailing = ({ skip }: WizardSkipTrailingProps) => (
|
||||
<Button variant="link" size="sm" className="h-auto px-2 py-1 text-xs" asChild>
|
||||
<CampaignWizardNavLink href={skip.href}>{skip.label}</CampaignWizardNavLink>
|
||||
</Button>
|
||||
)
|
||||
|
|
@ -3,7 +3,6 @@
|
|||
import { Info } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
|
||||
import { WizardTrendSkipTrailing } from '@/components/campaign/municipality/WizardTrendSkipTrailing'
|
||||
import { CampaignWizardNavLink } from '@/components/campaign/shared/CampaignWizardNavLink'
|
||||
import { CampaignWizardShell } from '@/components/campaign/shared/CampaignWizardShell'
|
||||
import { Button } from '@/components/ui/button'
|
||||
|
|
@ -16,29 +15,24 @@ import {
|
|||
DrawerHeader,
|
||||
DrawerTitle,
|
||||
} from '@/components/ui/Drawer'
|
||||
import type { CampaignWizardActionId } from '@/lib/campaignActionRoutes'
|
||||
import { wizardTrendHref } from '@/lib/campaignActionRoutes'
|
||||
import { wizardPreviousHref, wizardReturnHref, wizardTrendHref } from '@/lib/campaignActionRoutes'
|
||||
import { wizardFlowTitleForSlug } from '@/lib/campaignWizardCopy'
|
||||
import {
|
||||
politicalTrendWizardMetaByStatus,
|
||||
type PoliticalTrendWizardMetaEntry,
|
||||
} from '@/lib/politicalTrendWizardMeta'
|
||||
import {
|
||||
resolveWizardTrendSkip,
|
||||
selectablePoliticalTrendStatuses,
|
||||
wizardTrendChoiceStepTitle,
|
||||
} from '@/lib/politicalTrendWizardUi'
|
||||
import type { PoliticalTrendStatusValue } from '@/lib/schemas/municipality'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { wizardChainEndHref, wizardPreviousHref } from '@/lib/wizardActionChain'
|
||||
|
||||
type WizardTrendChoiceStepProps = {
|
||||
actionSlug: string
|
||||
municipalityName: string
|
||||
municipalitySlug: string
|
||||
currentStatus: PoliticalTrendStatusValue | null
|
||||
entryAction?: CampaignWizardActionId
|
||||
prefillExtraParams?: Record<string, string>
|
||||
returnPath?: string
|
||||
}
|
||||
|
||||
|
|
@ -47,12 +41,9 @@ export const WizardTrendChoiceStep = ({
|
|||
municipalityName,
|
||||
municipalitySlug,
|
||||
currentStatus,
|
||||
entryAction,
|
||||
prefillExtraParams,
|
||||
returnPath,
|
||||
}: WizardTrendChoiceStepProps) => {
|
||||
const [infoEntry, setInfoEntry] = useState<PoliticalTrendWizardMetaEntry | null>(null)
|
||||
const skip = resolveWizardTrendSkip(entryAction, municipalitySlug, returnPath)
|
||||
const options = selectablePoliticalTrendStatuses(currentStatus)
|
||||
|
||||
return (
|
||||
|
|
@ -65,13 +56,10 @@ export const WizardTrendChoiceStep = ({
|
|||
actionSlug,
|
||||
stepKind: 'trend-choice',
|
||||
municipalitySlug,
|
||||
entryAction,
|
||||
returnPath,
|
||||
})}
|
||||
dismissHref={wizardChainEndHref(returnPath)}
|
||||
dismissHref={wizardReturnHref(returnPath)}
|
||||
municipalityLabel={municipalityName}
|
||||
skip={skip}
|
||||
trailingAction={skip ? <WizardTrendSkipTrailing skip={skip} /> : undefined}
|
||||
>
|
||||
<ul className="grid list-none grid-cols-2 gap-3 md:grid-cols-3">
|
||||
{options.map((status) => {
|
||||
|
|
@ -80,14 +68,7 @@ export const WizardTrendChoiceStep = ({
|
|||
return (
|
||||
<li key={status} className="relative">
|
||||
<CampaignWizardNavLink
|
||||
href={wizardTrendHref(
|
||||
actionSlug,
|
||||
municipalitySlug,
|
||||
status,
|
||||
entryAction,
|
||||
prefillExtraParams,
|
||||
returnPath,
|
||||
)}
|
||||
href={wizardTrendHref(actionSlug, municipalitySlug, status, returnPath)}
|
||||
className={cn(
|
||||
'flex aspect-square w-full flex-col justify-between rounded-lg border bg-background p-3 pr-10 text-left',
|
||||
entry.tileClassName,
|
||||
|
|
|
|||
|
|
@ -4,28 +4,17 @@ import { useRouter } from 'next/navigation'
|
|||
import { useActionState, useState } from 'react'
|
||||
|
||||
import { setMunicipalityPoliticalTrendFormAction } from '@/app/(campaign)/campanha/(app)/municipios/municipalityStaffFormActions'
|
||||
import { WizardTrendSkipTrailing } from '@/components/campaign/municipality/WizardTrendSkipTrailing'
|
||||
import { CampaignFormActionMessage } from '@/components/campaign/shared/CampaignFormActionMessage'
|
||||
import { CampaignWizardShell } from '@/components/campaign/shared/CampaignWizardShell'
|
||||
import { useCampaignFormSuccessToast } from '@/components/campaign/shared/useCampaignFormSuccessToast'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Spinner } from '@/components/ui/Spinner'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import type { CampaignWizardActionId } from '@/lib/campaignActionRoutes'
|
||||
import { wizardPreviousHref, wizardReturnHref } from '@/lib/campaignActionRoutes'
|
||||
import { recordLastActedMunicipality } from '@/lib/campaignLastActedMunicipality'
|
||||
import { wizardFlowTitleForSlug } from '@/lib/campaignWizardCopy'
|
||||
import {
|
||||
resolveWizardTrendSkip,
|
||||
WIZARD_TREND_CLEAR_LABEL,
|
||||
WIZARD_TREND_SAVE_LABEL,
|
||||
} from '@/lib/politicalTrendWizardUi'
|
||||
import { WIZARD_TREND_CLEAR_LABEL, WIZARD_TREND_SAVE_LABEL } from '@/lib/politicalTrendWizardUi'
|
||||
import type { PoliticalTrendStatusValue } from '@/lib/schemas/municipality'
|
||||
import {
|
||||
resolveWizardChainEntry,
|
||||
wizardChainContinueHref,
|
||||
wizardChainEndHref,
|
||||
wizardPreviousHref,
|
||||
} from '@/lib/wizardActionChain'
|
||||
import { politicalTrendLabels } from '@/utilities/municipality/municipalityLabels'
|
||||
|
||||
type WizardTrendNoteStepProps = {
|
||||
|
|
@ -34,8 +23,6 @@ type WizardTrendNoteStepProps = {
|
|||
municipalityName: string
|
||||
municipalitySlug: string
|
||||
trendStatus: PoliticalTrendStatusValue
|
||||
initialNote: string
|
||||
entryAction?: CampaignWizardActionId
|
||||
returnPath?: string
|
||||
}
|
||||
|
||||
|
|
@ -45,23 +32,19 @@ export const WizardTrendNoteStep = ({
|
|||
municipalityName,
|
||||
municipalitySlug,
|
||||
trendStatus,
|
||||
initialNote,
|
||||
entryAction,
|
||||
returnPath,
|
||||
}: WizardTrendNoteStepProps) => {
|
||||
const router = useRouter()
|
||||
const [note, setNote] = useState(initialNote)
|
||||
const [note, setNote] = useState('')
|
||||
const [state, submitAction, isPending] = useActionState(
|
||||
setMunicipalityPoliticalTrendFormAction,
|
||||
{},
|
||||
)
|
||||
const skip = resolveWizardTrendSkip(entryAction, municipalitySlug, returnPath)
|
||||
const stepTitle = `Mudar tendência para ${politicalTrendLabels[trendStatus]}`
|
||||
|
||||
useCampaignFormSuccessToast(state, () => {
|
||||
recordLastActedMunicipality(municipalitySlug)
|
||||
const sessionEntry = resolveWizardChainEntry(entryAction, 'change-trend')
|
||||
router.push(wizardChainContinueHref(sessionEntry, 'change-trend', municipalitySlug, returnPath))
|
||||
router.push(wizardReturnHref(returnPath))
|
||||
})
|
||||
|
||||
return (
|
||||
|
|
@ -73,13 +56,10 @@ export const WizardTrendNoteStep = ({
|
|||
actionSlug,
|
||||
stepKind: 'trend-note',
|
||||
municipalitySlug,
|
||||
entryAction,
|
||||
returnPath,
|
||||
})}
|
||||
dismissHref={wizardChainEndHref(returnPath)}
|
||||
dismissHref={wizardReturnHref(returnPath)}
|
||||
municipalityLabel={municipalityName}
|
||||
skip={skip}
|
||||
trailingAction={skip ? <WizardTrendSkipTrailing skip={skip} /> : undefined}
|
||||
contentFocus="none"
|
||||
>
|
||||
<form
|
||||
|
|
|
|||
|
|
@ -1,13 +0,0 @@
|
|||
import { CampaignWizardNavLink } from '@/components/campaign/shared/CampaignWizardNavLink'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import type { WizardTrendSkipAction } from '@/lib/politicalTrendWizardUi'
|
||||
|
||||
type WizardTrendSkipTrailingProps = {
|
||||
skip: WizardTrendSkipAction
|
||||
}
|
||||
|
||||
export const WizardTrendSkipTrailing = ({ skip }: WizardTrendSkipTrailingProps) => (
|
||||
<Button variant="link" size="sm" className="h-auto px-2 py-1 text-xs" asChild>
|
||||
<CampaignWizardNavLink href={skip.href}>{skip.label}</CampaignWizardNavLink>
|
||||
</Button>
|
||||
)
|
||||
|
|
@ -4,7 +4,6 @@ import { useRouter } from 'next/navigation'
|
|||
import { useActionState } from 'react'
|
||||
|
||||
import { createMunicipalityUpdateFormAction } from '@/app/(campaign)/campanha/(app)/municipios/[slug]/updateFormActions'
|
||||
import { WizardSkipTrailing } from '@/components/campaign/municipality/WizardSkipTrailing'
|
||||
import { CampaignFormActionMessage } from '@/components/campaign/shared/CampaignFormActionMessage'
|
||||
import { CampaignWizardShell } from '@/components/campaign/shared/CampaignWizardShell'
|
||||
import { useCampaignFormSuccessToast } from '@/components/campaign/shared/useCampaignFormSuccessToast'
|
||||
|
|
@ -14,28 +13,17 @@ import { Field, FieldDescription, FieldError, FieldLabel } from '@/components/ui
|
|||
import { NativeSelect, NativeSelectOption } from '@/components/ui/native-select'
|
||||
import { Spinner } from '@/components/ui/Spinner'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import type { CampaignWizardActionId } from '@/lib/campaignActionRoutes'
|
||||
import { wizardPreviousHref, wizardReturnHref } from '@/lib/campaignActionRoutes'
|
||||
import { recordLastActedMunicipality } from '@/lib/campaignLastActedMunicipality'
|
||||
import { wizardFlowTitleForSlug } from '@/lib/campaignWizardCopy'
|
||||
import { municipalityUpdatePolarityLabels } from '@/lib/schemas/municipalityUpdate'
|
||||
import {
|
||||
resolveWizardChainEntry,
|
||||
wizardChainContinueHref,
|
||||
wizardChainEndHref,
|
||||
wizardPreviousHref,
|
||||
} from '@/lib/wizardActionChain'
|
||||
import {
|
||||
resolveWizardUpdateSkip,
|
||||
WIZARD_UPDATE_BODY_STEP_TITLE,
|
||||
WIZARD_UPDATE_SAVE_LABEL,
|
||||
} from '@/lib/wizardUpdateUi'
|
||||
import { WIZARD_UPDATE_BODY_STEP_TITLE, WIZARD_UPDATE_SAVE_LABEL } from '@/lib/wizardUpdateUi'
|
||||
|
||||
type WizardUpdateBodyStepProps = {
|
||||
actionSlug: string
|
||||
municipalityId: number
|
||||
municipalityName: string
|
||||
municipalitySlug: string
|
||||
entryAction?: CampaignWizardActionId
|
||||
returnPath?: string
|
||||
isStaff: boolean
|
||||
}
|
||||
|
|
@ -45,21 +33,16 @@ export const WizardUpdateBodyStep = ({
|
|||
municipalityId,
|
||||
municipalityName,
|
||||
municipalitySlug,
|
||||
entryAction,
|
||||
returnPath,
|
||||
isStaff,
|
||||
}: WizardUpdateBodyStepProps) => {
|
||||
const router = useRouter()
|
||||
const [state, submitAction, isPending] = useActionState(createMunicipalityUpdateFormAction, {})
|
||||
const skip = resolveWizardUpdateSkip(entryAction, municipalitySlug, returnPath)
|
||||
const stepTitle = WIZARD_UPDATE_BODY_STEP_TITLE
|
||||
|
||||
useCampaignFormSuccessToast(state, () => {
|
||||
recordLastActedMunicipality(municipalitySlug)
|
||||
const sessionEntry = resolveWizardChainEntry(entryAction, 'register-update')
|
||||
router.push(
|
||||
wizardChainContinueHref(sessionEntry, 'register-update', municipalitySlug, returnPath),
|
||||
)
|
||||
router.push(wizardReturnHref(returnPath))
|
||||
})
|
||||
|
||||
return (
|
||||
|
|
@ -71,13 +54,10 @@ export const WizardUpdateBodyStep = ({
|
|||
actionSlug,
|
||||
stepKind: 'update-body',
|
||||
municipalitySlug,
|
||||
entryAction,
|
||||
returnPath,
|
||||
})}
|
||||
dismissHref={wizardChainEndHref(returnPath)}
|
||||
dismissHref={wizardReturnHref(returnPath)}
|
||||
municipalityLabel={municipalityName}
|
||||
skip={skip}
|
||||
trailingAction={skip ? <WizardSkipTrailing skip={skip} /> : undefined}
|
||||
contentFocus="none"
|
||||
>
|
||||
<form
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import { CampaignListResults } from '@/components/campaign/shared/CampaignListPe
|
|||
import {
|
||||
toCampaignWizardChromeState,
|
||||
useSetCampaignWizardChrome,
|
||||
type CampaignWizardChromeSkip,
|
||||
} from '@/components/campaign/shell/CampaignWizardChromeContext'
|
||||
import { CAMPAIGN_HOME } from '@/lib/campaignPaths'
|
||||
import { WIZARD_STEP_PENDING_MESSAGE, wizardFlowChromeAriaLabel } from '@/lib/campaignWizardCopy'
|
||||
|
|
@ -19,8 +18,6 @@ export type CampaignWizardShellProps = {
|
|||
previousHref: string
|
||||
dismissHref?: string
|
||||
municipalityLabel?: string
|
||||
skip?: CampaignWizardChromeSkip
|
||||
trailingAction?: ReactNode
|
||||
contentAlign?: 'start' | 'end'
|
||||
/** When `'none'`, the shell does not move focus to the step title — use on form steps that autofocus an input. */
|
||||
contentFocus?: 'title' | 'none'
|
||||
|
|
@ -34,8 +31,6 @@ export const CampaignWizardShell = ({
|
|||
previousHref,
|
||||
dismissHref = CAMPAIGN_HOME,
|
||||
municipalityLabel,
|
||||
skip,
|
||||
trailingAction,
|
||||
contentAlign = 'start',
|
||||
contentFocus = 'title',
|
||||
children,
|
||||
|
|
@ -52,9 +47,8 @@ export const CampaignWizardShell = ({
|
|||
previousHref,
|
||||
dismissHref,
|
||||
municipalityLabel,
|
||||
skip,
|
||||
}),
|
||||
[dismissHref, flowTitle, isEntryStep, municipalityLabel, previousHref, skip],
|
||||
[dismissHref, flowTitle, isEntryStep, municipalityLabel, previousHref],
|
||||
)
|
||||
|
||||
useSetCampaignWizardChrome(chrome)
|
||||
|
|
@ -66,8 +60,6 @@ export const CampaignWizardShell = ({
|
|||
|
||||
return (
|
||||
<div className="flex min-h-full w-full flex-col">
|
||||
{trailingAction ? <div className="hidden justify-end md:flex">{trailingAction}</div> : null}
|
||||
|
||||
<main
|
||||
aria-labelledby={hasStepTitle ? titleId : undefined}
|
||||
aria-label={hasStepTitle ? undefined : wizardFlowChromeAriaLabel(flowTitle)}
|
||||
|
|
|
|||
|
|
@ -9,29 +9,21 @@ import {
|
|||
MUNICIPALITY_EXPECTED_VOTES_SAVE_ERROR_MESSAGE,
|
||||
type MunicipalityListExpectedVotesResponse,
|
||||
} from '@/app/(campaign)/campanha/(app)/municipios/expected-votes/types'
|
||||
import { CampaignWizardNavLink } from '@/components/campaign/shared/CampaignWizardNavLink'
|
||||
import { CampaignWizardShell } from '@/components/campaign/shared/CampaignWizardShell'
|
||||
import { VoteEstimateScenarioInputs } from '@/components/campaign/votePledge/VoteEstimateScenarioInputs'
|
||||
import { Alert, AlertDescription } from '@/components/ui/Alert'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Spinner } from '@/components/ui/Spinner'
|
||||
import type { CampaignWizardActionId } from '@/lib/campaignActionRoutes'
|
||||
import { wizardPreviousHref, wizardReturnHref } from '@/lib/campaignActionRoutes'
|
||||
import { postCampaignJson } from '@/lib/campaignJsonRequest'
|
||||
import { recordLastActedMunicipality } from '@/lib/campaignLastActedMunicipality'
|
||||
import {
|
||||
resolveWizardVotesSkip,
|
||||
WIZARD_VOTES_FINAL_CTA_LABEL,
|
||||
WIZARD_VOTES_SAVED_MESSAGE,
|
||||
wizardFlowTitleForSlug,
|
||||
wizardNextStepTitle,
|
||||
} from '@/lib/campaignWizardCopy'
|
||||
import { type VoteEstimateScenario, type VoteEstimateScenarioViewModel } from '@/lib/voteEstimate'
|
||||
import {
|
||||
resolveWizardChainEntry,
|
||||
wizardChainContinueHref,
|
||||
wizardChainEndHref,
|
||||
wizardPreviousHref,
|
||||
} from '@/lib/wizardActionChain'
|
||||
import {
|
||||
applyVoteShortcut,
|
||||
getWizardVoteViolation,
|
||||
|
|
@ -47,7 +39,6 @@ type WizardExpectedVotesStepProps = {
|
|||
municipalityName: string
|
||||
municipalitySlug: string
|
||||
initialExpectedVotes: VoteEstimateScenarioViewModel
|
||||
entryAction?: CampaignWizardActionId
|
||||
returnPath?: string
|
||||
}
|
||||
|
||||
|
|
@ -57,7 +48,6 @@ export const WizardExpectedVotesStep = ({
|
|||
municipalityName,
|
||||
municipalitySlug,
|
||||
initialExpectedVotes,
|
||||
entryAction,
|
||||
returnPath,
|
||||
}: WizardExpectedVotesStepProps) => {
|
||||
const router = useRouter()
|
||||
|
|
@ -66,7 +56,6 @@ export const WizardExpectedVotesStep = ({
|
|||
const [focusedScenario, setFocusedScenario] = useState<VoteEstimateScenario>('central')
|
||||
const [violation, setViolation] = useState<WizardVoteViolation | null>(null)
|
||||
const [saveError, setSaveError] = useState<string | null>(null)
|
||||
const skip = resolveWizardVotesSkip(entryAction, municipalitySlug, returnPath)
|
||||
|
||||
const clearViolationState = () => {
|
||||
if (violation) setViolation(null)
|
||||
|
|
@ -84,8 +73,7 @@ export const WizardExpectedVotesStep = ({
|
|||
}
|
||||
|
||||
const continueAfterVotes = () => {
|
||||
const sessionEntry = resolveWizardChainEntry(entryAction, 'update-votes')
|
||||
router.push(wizardChainContinueHref(sessionEntry, 'update-votes', municipalitySlug, returnPath))
|
||||
router.push(wizardReturnHref(returnPath))
|
||||
}
|
||||
|
||||
const handleConfirm = () => {
|
||||
|
|
@ -125,12 +113,6 @@ export const WizardExpectedVotesStep = ({
|
|||
})
|
||||
}
|
||||
|
||||
const trailingAction = skip ? (
|
||||
<Button variant="ghost" size="sm" className="min-h-11 px-2 text-sm" asChild>
|
||||
<CampaignWizardNavLink href={skip.href}>{skip.label}</CampaignWizardNavLink>
|
||||
</Button>
|
||||
) : undefined
|
||||
|
||||
return (
|
||||
<CampaignWizardShell
|
||||
flowTitle={wizardFlowTitleForSlug(actionSlug)}
|
||||
|
|
@ -140,13 +122,10 @@ export const WizardExpectedVotesStep = ({
|
|||
actionSlug,
|
||||
stepKind: 'votes',
|
||||
municipalitySlug,
|
||||
entryAction,
|
||||
returnPath,
|
||||
})}
|
||||
dismissHref={wizardChainEndHref(returnPath)}
|
||||
dismissHref={wizardReturnHref(returnPath)}
|
||||
municipalityLabel={municipalityName}
|
||||
skip={skip}
|
||||
trailingAction={trailingAction}
|
||||
contentFocus="none"
|
||||
>
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
import { CampaignWizardShell } from '@/components/campaign/shared/CampaignWizardShell'
|
||||
import { wizardActionHref } from '@/lib/campaignActionRoutes'
|
||||
import { wizardActionHref, wizardReturnHref } from '@/lib/campaignActionRoutes'
|
||||
import {
|
||||
wizardFlowTitleForSlug,
|
||||
wizardNextStepPlaceholder,
|
||||
wizardNextStepTitle,
|
||||
} from '@/lib/campaignWizardCopy'
|
||||
import { wizardChainEndHref } from '@/lib/wizardActionChain'
|
||||
|
||||
type WizardMunicipalitySelectedStubProps = {
|
||||
actionSlug: string
|
||||
|
|
@ -23,7 +22,7 @@ export const WizardMunicipalitySelectedStub = ({
|
|||
stepTitle={wizardNextStepTitle(actionSlug)}
|
||||
isEntryStep={false}
|
||||
previousHref={wizardActionHref(actionSlug, undefined, { returnPath })}
|
||||
dismissHref={wizardChainEndHref(returnPath)}
|
||||
dismissHref={wizardReturnHref(returnPath)}
|
||||
municipalityLabel={municipalityName}
|
||||
>
|
||||
<p className="text-sm text-muted-foreground">{wizardNextStepPlaceholder(actionSlug)}</p>
|
||||
|
|
|
|||
|
|
@ -26,9 +26,6 @@ const wizardNavButtonClass =
|
|||
const wizardDismissButtonClass =
|
||||
'size-11 shrink-0 text-primary-foreground hover:bg-primary-foreground/10 hover:text-primary-foreground'
|
||||
|
||||
const wizardSkipButtonClass =
|
||||
'min-h-11 max-w-[9rem] px-2 text-primary-foreground hover:bg-primary-foreground/10 hover:text-primary-foreground'
|
||||
|
||||
const wizardTitleSkeletonClass = 'mx-auto motion-reduce:animate-none bg-primary-foreground/20'
|
||||
|
||||
export const CampaignMobileTopBar = ({ notificationBell }: { notificationBell?: ReactNode }) => {
|
||||
|
|
@ -90,34 +87,17 @@ export const CampaignMobileTopBar = ({ notificationBell }: { notificationBell?:
|
|||
</div>
|
||||
|
||||
<div className="flex shrink-0 items-center">
|
||||
{chrome.skip ? (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className={wizardSkipButtonClass}
|
||||
asChild
|
||||
disabled={isPending}
|
||||
>
|
||||
<CampaignWizardNavLink href={chrome.skip.href} title={chrome.skip.label}>
|
||||
{chrome.skip.label}
|
||||
</CampaignWizardNavLink>
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className={wizardDismissButtonClass}
|
||||
asChild
|
||||
disabled={isPending}
|
||||
>
|
||||
<CampaignWizardNavLink
|
||||
href={chrome.dismissHref}
|
||||
aria-label={WIZARD_DISMISS_ARIA_LABEL}
|
||||
>
|
||||
<X className="size-5" aria-hidden />
|
||||
</CampaignWizardNavLink>
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className={wizardDismissButtonClass}
|
||||
asChild
|
||||
disabled={isPending}
|
||||
>
|
||||
<CampaignWizardNavLink href={chrome.dismissHref} aria-label={WIZARD_DISMISS_ARIA_LABEL}>
|
||||
<X className="size-5" aria-hidden />
|
||||
</CampaignWizardNavLink>
|
||||
</Button>
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -11,18 +11,12 @@ import {
|
|||
type SetStateAction,
|
||||
} from 'react'
|
||||
|
||||
export type CampaignWizardChromeSkip = {
|
||||
label: string
|
||||
href: string
|
||||
}
|
||||
|
||||
export type CampaignWizardChromeState = {
|
||||
flowTitle: string
|
||||
municipalityLabel?: string
|
||||
stepKind: 'entry' | 'continue'
|
||||
previousHref?: string
|
||||
dismissHref: string
|
||||
skip?: CampaignWizardChromeSkip
|
||||
}
|
||||
|
||||
type CampaignWizardChromeContextValue = {
|
||||
|
|
@ -60,7 +54,6 @@ type CampaignWizardChromeInput = {
|
|||
previousHref: string
|
||||
dismissHref: string
|
||||
municipalityLabel?: string
|
||||
skip?: CampaignWizardChromeSkip
|
||||
}
|
||||
|
||||
export const toCampaignWizardChromeState = ({
|
||||
|
|
@ -69,14 +62,12 @@ export const toCampaignWizardChromeState = ({
|
|||
previousHref,
|
||||
dismissHref,
|
||||
municipalityLabel,
|
||||
skip,
|
||||
}: CampaignWizardChromeInput): CampaignWizardChromeState => ({
|
||||
flowTitle,
|
||||
municipalityLabel,
|
||||
stepKind: isEntryStep ? 'entry' : 'continue',
|
||||
previousHref: isEntryStep ? undefined : previousHref,
|
||||
dismissHref,
|
||||
skip,
|
||||
})
|
||||
|
||||
export const useSetCampaignWizardChrome = (state: CampaignWizardChromeState | null): void => {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
/** Client-safe entry paths for UX-1 action wizards under `/campanha/acoes`. */
|
||||
|
||||
import { CAMPAIGN_HOME } from '@/lib/campaignPaths'
|
||||
import {
|
||||
parsePoliticalTrendStatusFormValue,
|
||||
type PoliticalTrendStatusValue,
|
||||
|
|
@ -47,16 +48,8 @@ export const WIZARD_MUNICIPIO_QUERY_KEY = 'municipio' as const
|
|||
/** Legacy B61 ritual param — canonical URLs omit it (B77). */
|
||||
export const WIZARD_SCENARIO_QUERY_KEY = 'cenario' as const
|
||||
|
||||
export const WIZARD_ENTRY_ACTION_QUERY_KEY = 'entry' as const
|
||||
|
||||
export const WIZARD_TREND_STATUS_QUERY_KEY = 'trendStatus' as const
|
||||
|
||||
export const WIZARD_NOTE_PREFILL_QUERY_KEY = 'notePrefill' as const
|
||||
|
||||
export const WIZARD_VOTE_FROM_QUERY_KEY = 'voteFrom' as const
|
||||
|
||||
export const WIZARD_VOTE_TO_QUERY_KEY = 'voteTo' as const
|
||||
|
||||
export const WIZARD_LEADERSHIP_ID_QUERY_KEY = 'leadershipId' as const
|
||||
|
||||
export const WIZARD_RETURN_PATH_QUERY_KEY = 'from' as const
|
||||
|
|
@ -68,7 +61,7 @@ const WIZARD_RETURN_PATH_AUTH_PREFIXES = [
|
|||
'/campanha/webauthn',
|
||||
] as const
|
||||
|
||||
/** Allowlisted internal return paths for wizard dismiss / chain end (B110). */
|
||||
/** Allowlisted internal return paths for wizard dismiss / return (B110). */
|
||||
export const isWizardReturnPath = (pathname: string): boolean => {
|
||||
if (!pathname.startsWith('/campanha')) return false
|
||||
if (pathname.startsWith('/campanha/acoes')) return false
|
||||
|
|
@ -98,7 +91,6 @@ export const appendWizardReturnPath = (href: string, returnPath?: string): strin
|
|||
}
|
||||
|
||||
export type WizardActionHrefOptions = {
|
||||
entryAction?: CampaignWizardActionId
|
||||
leadershipId?: number
|
||||
returnPath?: string
|
||||
}
|
||||
|
|
@ -109,16 +101,13 @@ export const wizardActionHref = (
|
|||
options?: WizardActionHrefOptions,
|
||||
): string => {
|
||||
const base = `${CAMPAIGN_ACTIONS_HOME}/${actionSlug}`
|
||||
if (!municipalitySlug && !options?.leadershipId && !options?.entryAction) {
|
||||
if (!municipalitySlug && options?.leadershipId === undefined) {
|
||||
return appendWizardReturnPath(base, options?.returnPath)
|
||||
}
|
||||
const params = new URLSearchParams()
|
||||
if (municipalitySlug) {
|
||||
params.set(WIZARD_MUNICIPIO_QUERY_KEY, municipalitySlug)
|
||||
}
|
||||
if (options?.entryAction) {
|
||||
params.set(WIZARD_ENTRY_ACTION_QUERY_KEY, options.entryAction)
|
||||
}
|
||||
if (options?.leadershipId !== undefined) {
|
||||
params.set(WIZARD_LEADERSHIP_ID_QUERY_KEY, String(options.leadershipId))
|
||||
}
|
||||
|
|
@ -148,17 +137,6 @@ export const parseWizardLeadershipIdParam = (
|
|||
export const hasWizardScenarioParam = (value: string | string[] | undefined): boolean =>
|
||||
parseWizardMunicipioParam(value) !== undefined
|
||||
|
||||
export const parseWizardEntryActionParam = (
|
||||
value: string | string[] | undefined,
|
||||
): CampaignWizardActionId | undefined => {
|
||||
const raw = Array.isArray(value) ? value[0] : value
|
||||
const trimmed = raw?.trim()
|
||||
if (!trimmed || !isCampaignWizardActionId(trimmed)) {
|
||||
return undefined
|
||||
}
|
||||
return trimmed
|
||||
}
|
||||
|
||||
export const resolveWizardTrendStatusParam = (
|
||||
value: string | string[] | undefined,
|
||||
): { trendStatus?: PoliticalTrendStatusValue; invalid: boolean } => {
|
||||
|
|
@ -180,8 +158,6 @@ export const wizardTrendHref = (
|
|||
actionSlug: string,
|
||||
municipalitySlug?: string,
|
||||
trendStatus?: PoliticalTrendStatusValue,
|
||||
entryAction?: CampaignWizardActionId,
|
||||
extraParams?: Record<string, string>,
|
||||
returnPath?: string,
|
||||
): string => {
|
||||
const base = `${CAMPAIGN_ACTIONS_HOME}/${actionSlug}`
|
||||
|
|
@ -193,13 +169,51 @@ export const wizardTrendHref = (
|
|||
if (trendStatus) {
|
||||
params.set(WIZARD_TREND_STATUS_QUERY_KEY, trendStatus)
|
||||
}
|
||||
if (entryAction) {
|
||||
params.set(WIZARD_ENTRY_ACTION_QUERY_KEY, entryAction)
|
||||
}
|
||||
if (extraParams) {
|
||||
for (const [key, value] of Object.entries(extraParams)) {
|
||||
if (value) params.set(key, value)
|
||||
}
|
||||
}
|
||||
return appendWizardReturnPath(`${base}?${params.toString()}`, returnPath)
|
||||
}
|
||||
|
||||
/** Dismiss / return target for wizard steps: allowlisted `from`, else Início (B110). */
|
||||
export const wizardReturnHref = (returnPath?: string): string =>
|
||||
returnPath && isWizardReturnPath(returnPath) ? returnPath : CAMPAIGN_HOME
|
||||
|
||||
/**
|
||||
* Principal wizard steps (register-update, update-votes, change-trend,
|
||||
* update-leadership) share the municipality-search route for "back".
|
||||
*/
|
||||
type WizardStepKind =
|
||||
| 'municipality-search'
|
||||
| 'votes'
|
||||
| 'trend-choice'
|
||||
| 'trend-note'
|
||||
| 'update-body'
|
||||
| 'leadership-grid'
|
||||
| 'leadership-form'
|
||||
|
||||
type WizardPreviousHrefInput = {
|
||||
actionSlug: string
|
||||
stepKind: WizardStepKind
|
||||
municipalitySlug?: string
|
||||
returnPath?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Href for the logically previous wizard step — internal sub-steps only. Every
|
||||
* principal wizard step goes back to the municipality search of its own flow.
|
||||
*/
|
||||
export const wizardPreviousHref = (input: WizardPreviousHrefInput): string => {
|
||||
const { actionSlug, stepKind, municipalitySlug, returnPath } = input
|
||||
|
||||
if (stepKind === 'municipality-search') {
|
||||
return wizardReturnHref(returnPath)
|
||||
}
|
||||
|
||||
if (stepKind === 'trend-note') {
|
||||
return wizardTrendHref(actionSlug, municipalitySlug, undefined, returnPath)
|
||||
}
|
||||
|
||||
if (stepKind === 'leadership-form') {
|
||||
return wizardActionHref(actionSlug, municipalitySlug, { returnPath })
|
||||
}
|
||||
|
||||
return wizardActionHref(actionSlug, undefined, { returnPath })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,8 @@
|
|||
import {
|
||||
CAMPAIGN_WIZARD_ACTION_SLUGS,
|
||||
campaignWizardActionIdForSlug,
|
||||
type CampaignWizardActionId,
|
||||
} from '@/lib/campaignActionRoutes'
|
||||
import { wizardFlowTitleForActionId } from '@/lib/campaignHomeActions'
|
||||
import { resolveWizardChainEntry, wizardChainContinueHref } from '@/lib/wizardActionChain'
|
||||
|
||||
export const WIZARD_MUNICIPALITY_STEP_TITLE = 'Em qual município?' as const
|
||||
|
||||
|
|
@ -32,37 +30,10 @@ export const WIZARD_MUNICIPALITY_SEARCH_EMPTY = 'Nenhum município encontrado.'
|
|||
|
||||
export const WIZARD_VOTES_SAVED_MESSAGE = 'Votos estimados atualizados.' as const
|
||||
|
||||
export const WIZARD_CHAIN_SKIP_LABEL = 'Pular' as const
|
||||
|
||||
export const WIZARD_VOTES_FINAL_CTA_LABEL = 'Salvar estimativas →' as const
|
||||
|
||||
const WIZARD_NEXT_STEP_GENERIC_PLACEHOLDER = 'Próximo passo deste fluxo em breve.' as const
|
||||
|
||||
export type WizardVotesSkipAction = {
|
||||
label: string
|
||||
href: string
|
||||
}
|
||||
|
||||
const shouldShowWizardVotesSkip = (entryAction: CampaignWizardActionId | undefined): boolean =>
|
||||
entryAction != null && entryAction !== 'update-votes'
|
||||
|
||||
export const resolveWizardVotesSkip = (
|
||||
entryAction: CampaignWizardActionId | undefined,
|
||||
municipalitySlug: string,
|
||||
returnPath?: string,
|
||||
): WizardVotesSkipAction | undefined =>
|
||||
shouldShowWizardVotesSkip(entryAction)
|
||||
? {
|
||||
label: WIZARD_CHAIN_SKIP_LABEL,
|
||||
href: wizardChainContinueHref(
|
||||
resolveWizardChainEntry(entryAction, 'update-votes'),
|
||||
'update-votes',
|
||||
municipalitySlug,
|
||||
returnPath,
|
||||
),
|
||||
}
|
||||
: undefined
|
||||
|
||||
export const wizardNextStepTitle = (actionSlug: string): string => {
|
||||
if (actionSlug === CAMPAIGN_WIZARD_ACTION_SLUGS['update-votes']) {
|
||||
return 'Ajustar votos estimados'
|
||||
|
|
@ -79,7 +50,7 @@ export const WIZARD_LEADERSHIP_FORM_CREATE_TITLE = 'Nova liderança' as const
|
|||
|
||||
export const WIZARD_LEADERSHIP_FORM_EDIT_TITLE = 'Atualizar liderança' as const
|
||||
|
||||
export const WIZARD_LEADERSHIP_CONTINUE_LABEL = 'Continuar' as const
|
||||
export const WIZARD_LEADERSHIP_CONCLUDE_LABEL = 'Concluir' as const
|
||||
|
||||
export const WIZARD_LEADERSHIP_ADD_TILE_LABEL = 'Adicionar liderança' as const
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,4 @@
|
|||
import type { CampaignWizardActionId } from '@/lib/campaignActionRoutes'
|
||||
import { WIZARD_CHAIN_SKIP_LABEL } from '@/lib/campaignWizardCopy'
|
||||
import { politicalTrendStatuses, type PoliticalTrendStatusValue } from '@/lib/schemas/municipality'
|
||||
import { resolveWizardChainEntry, wizardChainContinueHref } from '@/lib/wizardActionChain'
|
||||
|
||||
const politicalTrendDisplayLabels: Record<PoliticalTrendStatusValue, string> = {
|
||||
favoravel: 'Favorável',
|
||||
|
|
@ -17,11 +14,6 @@ export const WIZARD_TREND_SAVED_MESSAGE = 'Tendência política registrada.' as
|
|||
|
||||
export const WIZARD_TREND_UNREGISTERED_TITLE = 'Tendência não registrada' as const
|
||||
|
||||
export type WizardTrendSkipAction = {
|
||||
label: string
|
||||
href: string
|
||||
}
|
||||
|
||||
export const wizardTrendChoiceStepTitle = (
|
||||
currentStatus: PoliticalTrendStatusValue | null,
|
||||
): string =>
|
||||
|
|
@ -57,79 +49,3 @@ export const resolveWizardTrendNoteDestination = (
|
|||
|
||||
return 'note'
|
||||
}
|
||||
|
||||
export const shouldShowWizardTrendSkip = (
|
||||
entryAction: CampaignWizardActionId | undefined,
|
||||
): boolean => entryAction != null && entryAction !== 'change-trend'
|
||||
|
||||
export const resolveWizardTrendSkip = (
|
||||
entryAction: CampaignWizardActionId | undefined,
|
||||
municipalitySlug: string,
|
||||
returnPath?: string,
|
||||
): WizardTrendSkipAction | undefined =>
|
||||
shouldShowWizardTrendSkip(entryAction)
|
||||
? {
|
||||
label: WIZARD_CHAIN_SKIP_LABEL,
|
||||
href: wizardChainContinueHref(
|
||||
resolveWizardChainEntry(entryAction, 'change-trend'),
|
||||
'change-trend',
|
||||
municipalitySlug,
|
||||
returnPath,
|
||||
),
|
||||
}
|
||||
: undefined
|
||||
|
||||
export type PoliticalTrendNotePrefillSource =
|
||||
| { kind: 'none' }
|
||||
| { kind: 'voteAdjustment'; previousValue: number; newValue: number }
|
||||
| { kind: 'custom'; text: string }
|
||||
|
||||
export const buildPoliticalTrendNotePrefill = (source: PoliticalTrendNotePrefillSource): string => {
|
||||
switch (source.kind) {
|
||||
case 'none':
|
||||
return ''
|
||||
case 'voteAdjustment':
|
||||
return `Ajuste de votos: ${source.previousValue} → ${source.newValue}`
|
||||
case 'custom':
|
||||
return source.text
|
||||
default: {
|
||||
const exhaustive: never = source
|
||||
return exhaustive
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const parseOptionalIntParam = (value: string | string[] | undefined): number | undefined => {
|
||||
const raw = Array.isArray(value) ? value[0] : value
|
||||
const trimmed = raw?.trim()
|
||||
if (!trimmed) return undefined
|
||||
const parsed = Number.parseInt(trimmed, 10)
|
||||
return Number.isFinite(parsed) ? parsed : undefined
|
||||
}
|
||||
|
||||
export const resolvePoliticalTrendNotePrefillSource = ({
|
||||
entryAction,
|
||||
notePrefill,
|
||||
voteFrom,
|
||||
voteTo,
|
||||
}: {
|
||||
entryAction?: CampaignWizardActionId
|
||||
notePrefill?: string | string[] | undefined
|
||||
voteFrom?: string | string[] | undefined
|
||||
voteTo?: string | string[] | undefined
|
||||
}): PoliticalTrendNotePrefillSource => {
|
||||
const customText = Array.isArray(notePrefill) ? notePrefill[0] : notePrefill
|
||||
if (customText?.trim()) {
|
||||
return { kind: 'custom', text: customText.trim() }
|
||||
}
|
||||
|
||||
if (entryAction === 'update-votes') {
|
||||
const previousValue = parseOptionalIntParam(voteFrom)
|
||||
const newValue = parseOptionalIntParam(voteTo)
|
||||
if (previousValue !== undefined && newValue !== undefined) {
|
||||
return { kind: 'voteAdjustment', previousValue, newValue }
|
||||
}
|
||||
}
|
||||
|
||||
return { kind: 'none' }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,240 +0,0 @@
|
|||
/**
|
||||
* B98 — ordered sub-flow queue after the principal wizard action succeeds.
|
||||
* Pure navigation helpers; each link still writes via its existing action.
|
||||
*/
|
||||
|
||||
import {
|
||||
CAMPAIGN_WIZARD_ACTION_SLUGS,
|
||||
campaignWizardActionIdForSlug,
|
||||
isWizardReturnPath,
|
||||
wizardActionHref,
|
||||
wizardTrendHref,
|
||||
type CampaignWizardActionId,
|
||||
} from '@/lib/campaignActionRoutes'
|
||||
import { CAMPAIGN_HOME } from '@/lib/campaignPaths'
|
||||
|
||||
/** Actions that participate in the v1 adjustment chain (excludes demand). */
|
||||
export type WizardChainActionId = Exclude<CampaignWizardActionId, 'register-demand'>
|
||||
|
||||
const WIZARD_CHAIN_AFTER = {
|
||||
'update-votes': ['change-trend', 'register-update', 'update-leadership'],
|
||||
'register-update': ['change-trend', 'update-votes', 'update-leadership'],
|
||||
'change-trend': ['register-update', 'update-votes', 'update-leadership'],
|
||||
'update-leadership': ['register-update', 'update-votes', 'change-trend'],
|
||||
} as const satisfies Record<WizardChainActionId, readonly WizardChainActionId[]>
|
||||
|
||||
export const isWizardChainActionId = (
|
||||
id: CampaignWizardActionId | undefined,
|
||||
): id is WizardChainActionId => id != null && id in WIZARD_CHAIN_AFTER
|
||||
|
||||
/**
|
||||
* Remaining chained steps after `completedAction` in a session owned by `entryAction`.
|
||||
* When `completedAction` is the principal (or omitted), returns the full queue.
|
||||
*/
|
||||
export const wizardChainAfter = (
|
||||
entryAction: WizardChainActionId,
|
||||
completedAction?: WizardChainActionId,
|
||||
): WizardChainActionId[] => {
|
||||
const queue: WizardChainActionId[] = [...WIZARD_CHAIN_AFTER[entryAction]]
|
||||
if (!completedAction || completedAction === entryAction) {
|
||||
return queue
|
||||
}
|
||||
const index = queue.indexOf(completedAction)
|
||||
if (index === -1) {
|
||||
return []
|
||||
}
|
||||
return queue.slice(index + 1)
|
||||
}
|
||||
|
||||
export const nextWizardChainStep = (
|
||||
entryAction: WizardChainActionId,
|
||||
completedAction: WizardChainActionId,
|
||||
): WizardChainActionId | undefined => wizardChainAfter(entryAction, completedAction)[0]
|
||||
|
||||
const wizardHrefForChainStep = (
|
||||
action: WizardChainActionId,
|
||||
municipalitySlug: string,
|
||||
entryAction: WizardChainActionId,
|
||||
returnPath?: string,
|
||||
): string => {
|
||||
const actionSlug = CAMPAIGN_WIZARD_ACTION_SLUGS[action]
|
||||
switch (action) {
|
||||
case 'update-votes':
|
||||
case 'update-leadership':
|
||||
case 'register-update':
|
||||
return wizardActionHref(actionSlug, municipalitySlug, { entryAction, returnPath })
|
||||
case 'change-trend':
|
||||
return wizardTrendHref(
|
||||
actionSlug,
|
||||
municipalitySlug,
|
||||
undefined,
|
||||
entryAction,
|
||||
undefined,
|
||||
returnPath,
|
||||
)
|
||||
default: {
|
||||
const exhaustive: never = action
|
||||
return exhaustive
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Chain end or dismiss target: allowlisted `from`, else Início (B110). */
|
||||
export const wizardChainEndHref = (returnPath?: string): string =>
|
||||
returnPath && isWizardReturnPath(returnPath) ? returnPath : CAMPAIGN_HOME
|
||||
|
||||
/**
|
||||
* Href for the next chained wizard after completing/skipping `completedAction`,
|
||||
* or the return path (default Início) when the queue is empty / entry is not chainable.
|
||||
*/
|
||||
export const wizardChainContinueHref = (
|
||||
entryAction: CampaignWizardActionId,
|
||||
completedAction: WizardChainActionId,
|
||||
municipalitySlug: string,
|
||||
returnPath?: string,
|
||||
): string => {
|
||||
if (!isWizardChainActionId(entryAction)) {
|
||||
return wizardChainEndHref(returnPath)
|
||||
}
|
||||
const next = nextWizardChainStep(entryAction, completedAction)
|
||||
if (!next) {
|
||||
return wizardChainEndHref(returnPath)
|
||||
}
|
||||
return wizardHrefForChainStep(next, municipalitySlug, entryAction, returnPath)
|
||||
}
|
||||
|
||||
/** Session owner for navigation: explicit `entry` query, else the current principal. */
|
||||
export const resolveWizardChainEntry = (
|
||||
entryAction: CampaignWizardActionId | undefined,
|
||||
currentAction: WizardChainActionId,
|
||||
): WizardChainActionId => (isWizardChainActionId(entryAction) ? entryAction : currentAction)
|
||||
|
||||
/**
|
||||
* register-update, update-votes, update-leadership all use wizardActionHref.
|
||||
*/
|
||||
type WizardStepKind =
|
||||
| 'municipality-search'
|
||||
| 'votes'
|
||||
| 'trend-choice'
|
||||
| 'trend-note'
|
||||
| 'update-body'
|
||||
| 'leadership-grid'
|
||||
| 'leadership-form'
|
||||
|
||||
const wizardPrincipalStepHref = (
|
||||
action: WizardChainActionId,
|
||||
municipalitySlug: string,
|
||||
sessionEntry: WizardChainActionId,
|
||||
returnPath?: string,
|
||||
): string => {
|
||||
const actionSlug = CAMPAIGN_WIZARD_ACTION_SLUGS[action]
|
||||
switch (action) {
|
||||
case 'update-votes':
|
||||
case 'update-leadership':
|
||||
case 'register-update':
|
||||
return wizardActionHref(actionSlug, municipalitySlug, {
|
||||
entryAction: sessionEntry,
|
||||
returnPath,
|
||||
})
|
||||
case 'change-trend':
|
||||
return wizardTrendHref(
|
||||
actionSlug,
|
||||
municipalitySlug,
|
||||
undefined,
|
||||
sessionEntry,
|
||||
undefined,
|
||||
returnPath,
|
||||
)
|
||||
default: {
|
||||
const exhaustive: never = action
|
||||
return exhaustive
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Principal step of the chain link immediately before `current` in a session owned by
|
||||
* `sessionEntry`. Undefined when `current` is the session principal (standalone entry).
|
||||
*/
|
||||
const wizardChainPreviousPrincipalHref = (
|
||||
current: WizardChainActionId,
|
||||
sessionEntry: WizardChainActionId,
|
||||
municipalitySlug: string,
|
||||
returnPath?: string,
|
||||
): string | undefined => {
|
||||
if (current === sessionEntry) {
|
||||
return undefined
|
||||
}
|
||||
const queue = wizardChainAfter(sessionEntry)
|
||||
const index = queue.indexOf(current)
|
||||
if (index === -1) {
|
||||
return undefined
|
||||
}
|
||||
if (index === 0) {
|
||||
return wizardPrincipalStepHref(sessionEntry, municipalitySlug, sessionEntry, returnPath)
|
||||
}
|
||||
return wizardPrincipalStepHref(queue[index - 1], municipalitySlug, sessionEntry, returnPath)
|
||||
}
|
||||
|
||||
type WizardPreviousHrefInput = {
|
||||
actionSlug: string
|
||||
stepKind: WizardStepKind
|
||||
municipalitySlug?: string
|
||||
entryAction?: CampaignWizardActionId
|
||||
returnPath?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Href for the logically previous wizard step — internal sub-steps, principal search,
|
||||
* or the preceding B98 chain link's principal step (same município).
|
||||
*/
|
||||
export const wizardPreviousHref = (input: WizardPreviousHrefInput): string => {
|
||||
const { actionSlug, stepKind, municipalitySlug, entryAction, returnPath } = input
|
||||
|
||||
if (stepKind === 'municipality-search') {
|
||||
return wizardChainEndHref(returnPath)
|
||||
}
|
||||
|
||||
if (stepKind === 'trend-note') {
|
||||
return wizardTrendHref(
|
||||
actionSlug,
|
||||
municipalitySlug,
|
||||
undefined,
|
||||
entryAction,
|
||||
undefined,
|
||||
returnPath,
|
||||
)
|
||||
}
|
||||
|
||||
if (stepKind === 'leadership-form') {
|
||||
return wizardActionHref(actionSlug, municipalitySlug, { entryAction, returnPath })
|
||||
}
|
||||
|
||||
const currentActionId = campaignWizardActionIdForSlug(actionSlug)
|
||||
const searchHref = wizardActionHref(actionSlug, undefined, { returnPath })
|
||||
|
||||
if (!municipalitySlug || !currentActionId || !isWizardChainActionId(currentActionId)) {
|
||||
return searchHref
|
||||
}
|
||||
|
||||
const sessionEntry = resolveWizardChainEntry(entryAction, currentActionId)
|
||||
|
||||
if (
|
||||
(stepKind === 'votes' || stepKind === 'leadership-grid') &&
|
||||
currentActionId === sessionEntry
|
||||
) {
|
||||
return searchHref
|
||||
}
|
||||
|
||||
const chainPrev = wizardChainPreviousPrincipalHref(
|
||||
currentActionId,
|
||||
sessionEntry,
|
||||
municipalitySlug,
|
||||
returnPath,
|
||||
)
|
||||
if (chainPrev) {
|
||||
return chainPrev
|
||||
}
|
||||
|
||||
return searchHref
|
||||
}
|
||||
|
|
@ -1,7 +1,4 @@
|
|||
import type { CampaignWizardActionId } from '@/lib/campaignActionRoutes'
|
||||
import { WIZARD_CHAIN_SKIP_LABEL } from '@/lib/campaignWizardCopy'
|
||||
import type { SupportStatus } from '@/lib/schemas/leadership'
|
||||
import { resolveWizardChainEntry, wizardChainContinueHref } from '@/lib/wizardActionChain'
|
||||
|
||||
/** Client-safe tile payload for the B70 leadership wizard grid. */
|
||||
export type WizardLeadershipTileViewModel = {
|
||||
|
|
@ -13,30 +10,3 @@ export type WizardLeadershipTileViewModel = {
|
|||
exclusive: boolean
|
||||
notes: string | null
|
||||
}
|
||||
|
||||
export type WizardLeadershipSkipAction = {
|
||||
label: string
|
||||
href: string
|
||||
}
|
||||
|
||||
export const showLeadershipWizardSkip = (entryAction?: CampaignWizardActionId): boolean => {
|
||||
const effectiveEntry = entryAction ?? 'update-leadership'
|
||||
return effectiveEntry !== 'update-leadership'
|
||||
}
|
||||
|
||||
export const resolveWizardLeadershipSkip = (
|
||||
entryAction: CampaignWizardActionId | undefined,
|
||||
municipalitySlug: string,
|
||||
returnPath?: string,
|
||||
): WizardLeadershipSkipAction | undefined =>
|
||||
showLeadershipWizardSkip(entryAction)
|
||||
? {
|
||||
label: WIZARD_CHAIN_SKIP_LABEL,
|
||||
href: wizardChainContinueHref(
|
||||
resolveWizardChainEntry(entryAction, 'update-leadership'),
|
||||
'update-leadership',
|
||||
municipalitySlug,
|
||||
returnPath,
|
||||
),
|
||||
}
|
||||
: undefined
|
||||
|
|
|
|||
|
|
@ -1,33 +1,3 @@
|
|||
import type { CampaignWizardActionId } from '@/lib/campaignActionRoutes'
|
||||
import { WIZARD_CHAIN_SKIP_LABEL } from '@/lib/campaignWizardCopy'
|
||||
import { resolveWizardChainEntry, wizardChainContinueHref } from '@/lib/wizardActionChain'
|
||||
|
||||
export const WIZARD_UPDATE_BODY_STEP_TITLE = 'Registrar atualização' as const
|
||||
|
||||
export const WIZARD_UPDATE_SAVE_LABEL = 'Salvar' as const
|
||||
|
||||
export type WizardUpdateSkipAction = {
|
||||
label: string
|
||||
href: string
|
||||
}
|
||||
|
||||
export const shouldShowWizardUpdateSkip = (
|
||||
entryAction: CampaignWizardActionId | undefined,
|
||||
): boolean => entryAction != null && entryAction !== 'register-update'
|
||||
|
||||
export const resolveWizardUpdateSkip = (
|
||||
entryAction: CampaignWizardActionId | undefined,
|
||||
municipalitySlug: string,
|
||||
returnPath?: string,
|
||||
): WizardUpdateSkipAction | undefined =>
|
||||
shouldShowWizardUpdateSkip(entryAction)
|
||||
? {
|
||||
label: WIZARD_CHAIN_SKIP_LABEL,
|
||||
href: wizardChainContinueHref(
|
||||
resolveWizardChainEntry(entryAction, 'register-update'),
|
||||
'register-update',
|
||||
municipalitySlug,
|
||||
returnPath,
|
||||
),
|
||||
}
|
||||
: undefined
|
||||
|
|
|
|||
|
|
@ -239,15 +239,37 @@ test.describe('Wizard — ajuste de votos (B61 / B77)', () => {
|
|||
await step.getByRole('textbox', { name: 'Otimista' }).fill('350')
|
||||
await page.getByRole('button', { name: 'Salvar estimativas →' }).click()
|
||||
|
||||
await page.waitForURL(
|
||||
new RegExp(
|
||||
`/campanha/acoes/mudar-tendencia\\?municipio=${municipality.slug}&entry=update-votes`,
|
||||
),
|
||||
)
|
||||
await page.waitForURL(/^https?:\/\/[^/]+\/campanha\/?$/)
|
||||
await expect(page.getByText('Votos estimados atualizados.')).toBeVisible({ timeout: 15000 })
|
||||
expect(page.url()).not.toContain('cenario=')
|
||||
})
|
||||
|
||||
test('saving votes returns to the allowlisted origin (B110 backdrop)', async ({
|
||||
campaign,
|
||||
page,
|
||||
}) => {
|
||||
const { fixtures } = campaign
|
||||
const coordinator = await fixtures.createCampaignUser('coordinator', {
|
||||
name: fixtures.value('Coordenadora Geral'),
|
||||
})
|
||||
const municipality = await fixtures.claimMunicipality()
|
||||
|
||||
await campaign.login(page, coordinator.email!, coordinator.password)
|
||||
await page.goto(
|
||||
`/campanha/acoes/atualizar-votos?municipio=${municipality.slug}&from=${encodeURIComponent(`/campanha/municipios/${municipality.slug}`)}`,
|
||||
)
|
||||
|
||||
await expect(page.getByRole('heading', { name: 'Ajustar votos estimados' })).toBeVisible({
|
||||
timeout: 15000,
|
||||
})
|
||||
|
||||
const step = wizardVoteStep(page)
|
||||
await step.getByRole('textbox', { name: 'Média' }).fill('210')
|
||||
await page.getByRole('button', { name: 'Salvar estimativas →' }).click()
|
||||
|
||||
await page.waitForURL(new RegExp(`/campanha/municipios/${municipality.slug}\\/?$`))
|
||||
})
|
||||
|
||||
test('incoherent estimates show inline warning without changing URL', async ({
|
||||
campaign,
|
||||
page,
|
||||
|
|
@ -298,7 +320,7 @@ test.describe('Wizard — ajuste de votos (B61 / B77)', () => {
|
|||
})
|
||||
|
||||
test.describe('Wizard — atualizar liderança (B70)', () => {
|
||||
test('create leadership, save, and continue into the chain', async ({ campaign, page }) => {
|
||||
test('create leadership, save, and return to origin (B168)', async ({ campaign, page }) => {
|
||||
const { fixtures } = campaign
|
||||
const coordinator = await fixtures.createCampaignUser('coordinator', {
|
||||
name: fixtures.value('Coordenadora Geral'),
|
||||
|
|
@ -323,20 +345,19 @@ test.describe('Wizard — atualizar liderança (B70)', () => {
|
|||
await expect(page.getByRole('heading', { name: 'Quem coordena por aqui?' })).toBeVisible({
|
||||
timeout: 15000,
|
||||
})
|
||||
await expect(page.getByRole('button', { name: 'Continuar' })).toBeVisible()
|
||||
await expect(page.getByRole('button', { name: 'Concluir' })).toBeVisible()
|
||||
|
||||
await page.getByRole('button', { name: 'Continuar' }).click()
|
||||
await page.waitForURL(
|
||||
new RegExp(
|
||||
`/campanha/acoes/registrar-atualizacao\\?municipio=${municipality.slug}&entry=update-leadership`,
|
||||
),
|
||||
)
|
||||
await expect(page.getByRole('heading', { name: 'Registrar atualização' })).toBeVisible()
|
||||
await page.getByRole('button', { name: 'Concluir' }).click()
|
||||
await page.waitForURL(/^https?:\/\/[^/]+\/campanha\/?$/)
|
||||
await expect(page.getByRole('link', { name: /Voltar/ })).toHaveCount(0)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Wizard — registrar atualização (C87)', () => {
|
||||
test('standalone flow: unified form, save continues the chain', async ({ campaign, page }) => {
|
||||
test('standalone flow: unified form, save returns to origin (B168)', async ({
|
||||
campaign,
|
||||
page,
|
||||
}) => {
|
||||
const { fixtures } = campaign
|
||||
const coordinator = await fixtures.createCampaignUser('coordinator', {
|
||||
name: fixtures.value('Coordenadora Geral'),
|
||||
|
|
@ -358,11 +379,7 @@ test.describe('Wizard — registrar atualização (C87)', () => {
|
|||
await page.getByLabel('Polaridade').selectOption('ruim')
|
||||
await page.getByRole('button', { name: 'Salvar' }).click()
|
||||
|
||||
await page.waitForURL(
|
||||
new RegExp(
|
||||
`/campanha/acoes/mudar-tendencia\\?municipio=${municipality.slug}&entry=register-update`,
|
||||
),
|
||||
)
|
||||
await page.waitForURL(/^https?:\/\/[^/]+\/campanha\/?$/)
|
||||
await expect(page.getByText('Atualização registrada com sucesso.')).toBeVisible()
|
||||
})
|
||||
|
||||
|
|
@ -389,7 +406,7 @@ test.describe('Wizard — registrar atualização (C87)', () => {
|
|||
await expect(page.getByRole('heading', { name: 'Que tipo de sinal?' })).toHaveCount(0)
|
||||
})
|
||||
|
||||
test('embedded flow shows skip link to the next chain step', async ({ campaign, page }) => {
|
||||
test('stale entry param is ignored — never a skip link (B168)', async ({ campaign, page }) => {
|
||||
const { fixtures } = campaign
|
||||
const coordinator = await fixtures.createCampaignUser('coordinator', {
|
||||
name: fixtures.value('Coordenadora Geral'),
|
||||
|
|
@ -401,11 +418,40 @@ test.describe('Wizard — registrar atualização (C87)', () => {
|
|||
`/campanha/acoes/registrar-atualizacao?municipio=${municipality.slug}&entry=update-votes`,
|
||||
)
|
||||
|
||||
const skip = page.getByRole('link', { name: 'Pular' })
|
||||
await expect(skip).toBeVisible({ timeout: 15000 })
|
||||
await expect(skip).toHaveAttribute(
|
||||
'href',
|
||||
`/campanha/acoes/atualizar-lideranca?municipio=${municipality.slug}&entry=update-votes`,
|
||||
)
|
||||
await expect(page.getByRole('heading', { name: 'Registrar atualização' })).toBeVisible({
|
||||
timeout: 15000,
|
||||
})
|
||||
await expect(page.getByRole('link', { name: 'Pular' })).toHaveCount(0)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Wizard — mudar tendência (B97 / B168)', () => {
|
||||
test('changing trend and saving returns to origin instead of advancing', async ({
|
||||
campaign,
|
||||
page,
|
||||
}) => {
|
||||
const { fixtures } = campaign
|
||||
const coordinator = await fixtures.createCampaignUser('coordinator', {
|
||||
name: fixtures.value('Coordenadora Geral'),
|
||||
})
|
||||
const municipality = await fixtures.claimMunicipality()
|
||||
|
||||
await campaign.login(page, coordinator.email!, coordinator.password)
|
||||
await page.goto(`/campanha/acoes/mudar-tendencia?municipio=${municipality.slug}`)
|
||||
|
||||
await expect(page.getByRole('heading', { name: /Tendência/ })).toBeVisible({
|
||||
timeout: 15000,
|
||||
})
|
||||
|
||||
await page.locator('main a[href*="trendStatus"]').first().click()
|
||||
await expect(page.getByRole('heading', { name: /^Mudar tendência para / })).toBeVisible({
|
||||
timeout: 15000,
|
||||
})
|
||||
await expect(page.getByRole('link', { name: 'Pular' })).toHaveCount(0)
|
||||
|
||||
await page.getByRole('button', { name: 'Salvar' }).click()
|
||||
|
||||
await page.waitForURL(/^https?:\/\/[^/]+\/campanha\/?$/)
|
||||
await expect(page.getByText('Tendência política registrada.')).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -8,13 +8,15 @@ import {
|
|||
hasWizardScenarioParam,
|
||||
isCampaignWizardActionId,
|
||||
isCampaignWizardActionSlug,
|
||||
parseWizardEntryActionParam,
|
||||
parseWizardLeadershipIdParam,
|
||||
parseWizardMunicipioParam,
|
||||
resolveWizardTrendStatusParam,
|
||||
wizardActionHref,
|
||||
wizardPreviousHref,
|
||||
wizardReturnHref,
|
||||
wizardTrendHref,
|
||||
} from '@/lib/campaignActionRoutes'
|
||||
import { CAMPAIGN_HOME } from '@/lib/campaignPaths'
|
||||
|
||||
describe('campaignActionRoutes', () => {
|
||||
it.each(
|
||||
|
|
@ -40,9 +42,6 @@ describe('campaignActionRoutes', () => {
|
|||
expect(wizardActionHref('atualizar-votos', 'cairu')).toBe(
|
||||
`${CAMPAIGN_ACTIONS_HOME}/atualizar-votos?municipio=cairu`,
|
||||
)
|
||||
expect(wizardActionHref('atualizar-lideranca', 'cairu', { entryAction: 'update-votes' })).toBe(
|
||||
`${CAMPAIGN_ACTIONS_HOME}/atualizar-lideranca?municipio=cairu&entry=update-votes`,
|
||||
)
|
||||
expect(wizardActionHref('atualizar-lideranca', 'cairu', { leadershipId: 42 })).toBe(
|
||||
`${CAMPAIGN_ACTIONS_HOME}/atualizar-lideranca?municipio=cairu&leadershipId=42`,
|
||||
)
|
||||
|
|
@ -74,22 +73,16 @@ describe('campaignActionRoutes', () => {
|
|||
expect(campaignWizardActionIdForSlug('inventado')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('parses entry action search param', () => {
|
||||
expect(parseWizardEntryActionParam('update-leadership')).toBe('update-leadership')
|
||||
expect(parseWizardEntryActionParam('update-votes')).toBe('update-votes')
|
||||
expect(parseWizardEntryActionParam('invalid')).toBeUndefined()
|
||||
expect(parseWizardEntryActionParam(undefined)).toBeUndefined()
|
||||
})
|
||||
|
||||
it('builds trend wizard hrefs with optional trendStatus and entry', () => {
|
||||
it('builds trend wizard hrefs with optional trendStatus and return path', () => {
|
||||
expect(wizardTrendHref('mudar-tendencia', 'cairu')).toBe(
|
||||
`${CAMPAIGN_ACTIONS_HOME}/mudar-tendencia?municipio=cairu`,
|
||||
)
|
||||
expect(wizardTrendHref('mudar-tendencia', 'cairu', 'favoravel')).toBe(
|
||||
`${CAMPAIGN_ACTIONS_HOME}/mudar-tendencia?municipio=cairu&trendStatus=favoravel`,
|
||||
)
|
||||
expect(wizardTrendHref('mudar-tendencia', 'cairu', 'neutra', 'update-votes')).toBe(
|
||||
`${CAMPAIGN_ACTIONS_HOME}/mudar-tendencia?municipio=cairu&trendStatus=neutra&entry=update-votes`,
|
||||
const origin = '/campanha/municipios/cairu'
|
||||
expect(wizardTrendHref('mudar-tendencia', 'cairu', 'neutra', origin)).toBe(
|
||||
`${CAMPAIGN_ACTIONS_HOME}/mudar-tendencia?municipio=cairu&trendStatus=neutra&from=${encodeURIComponent(origin)}`,
|
||||
)
|
||||
})
|
||||
|
||||
|
|
@ -108,3 +101,93 @@ describe('campaignActionRoutes', () => {
|
|||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('wizardReturnHref (B110)', () => {
|
||||
it('falls back to Início without a return path', () => {
|
||||
expect(wizardReturnHref(undefined)).toBe(CAMPAIGN_HOME)
|
||||
})
|
||||
|
||||
it('honors an allowlisted return path', () => {
|
||||
expect(wizardReturnHref('/campanha/municipios/cairu')).toBe('/campanha/municipios/cairu')
|
||||
})
|
||||
|
||||
it.each(['/campanha/acoes/atualizar-votos', '/campanha/login', 'https://evil.example'])(
|
||||
'rejects %s and falls back to Início',
|
||||
(path) => {
|
||||
expect(wizardReturnHref(path)).toBe(CAMPAIGN_HOME)
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
describe('wizardPreviousHref', () => {
|
||||
it('returns the return target for municipality search', () => {
|
||||
expect(
|
||||
wizardPreviousHref({
|
||||
actionSlug: 'atualizar-votos',
|
||||
stepKind: 'municipality-search',
|
||||
}),
|
||||
).toBe(CAMPAIGN_HOME)
|
||||
})
|
||||
|
||||
it('returns municipality search for principal post-municipio steps', () => {
|
||||
expect(
|
||||
wizardPreviousHref({
|
||||
actionSlug: 'atualizar-votos',
|
||||
stepKind: 'votes',
|
||||
municipalitySlug: 'cairu',
|
||||
}),
|
||||
).toBe(`${CAMPAIGN_ACTIONS_HOME}/atualizar-votos`)
|
||||
expect(
|
||||
wizardPreviousHref({
|
||||
actionSlug: 'mudar-tendencia',
|
||||
stepKind: 'trend-choice',
|
||||
municipalitySlug: 'cairu',
|
||||
}),
|
||||
).toBe(`${CAMPAIGN_ACTIONS_HOME}/mudar-tendencia`)
|
||||
expect(
|
||||
wizardPreviousHref({
|
||||
actionSlug: 'registrar-atualizacao',
|
||||
stepKind: 'update-body',
|
||||
municipalitySlug: 'cairu',
|
||||
}),
|
||||
).toBe(`${CAMPAIGN_ACTIONS_HOME}/registrar-atualizacao`)
|
||||
expect(
|
||||
wizardPreviousHref({
|
||||
actionSlug: 'atualizar-lideranca',
|
||||
stepKind: 'leadership-grid',
|
||||
municipalitySlug: 'cairu',
|
||||
}),
|
||||
).toBe(`${CAMPAIGN_ACTIONS_HOME}/atualizar-lideranca`)
|
||||
})
|
||||
|
||||
it('returns the trend choice step from the trend note step', () => {
|
||||
expect(
|
||||
wizardPreviousHref({
|
||||
actionSlug: 'mudar-tendencia',
|
||||
stepKind: 'trend-note',
|
||||
municipalitySlug: 'cairu',
|
||||
}),
|
||||
).toBe(`${CAMPAIGN_ACTIONS_HOME}/mudar-tendencia?municipio=cairu`)
|
||||
})
|
||||
|
||||
it('returns the leadership grid from the leadership form step', () => {
|
||||
expect(
|
||||
wizardPreviousHref({
|
||||
actionSlug: 'atualizar-lideranca',
|
||||
stepKind: 'leadership-form',
|
||||
municipalitySlug: 'cairu',
|
||||
}),
|
||||
).toBe(`${CAMPAIGN_ACTIONS_HOME}/atualizar-lideranca?municipio=cairu`)
|
||||
})
|
||||
|
||||
it('honors return path on municipality search back target', () => {
|
||||
const origin = '/campanha/municipios/cairu'
|
||||
expect(
|
||||
wizardPreviousHref({
|
||||
actionSlug: 'atualizar-votos',
|
||||
stepKind: 'municipality-search',
|
||||
returnPath: origin,
|
||||
}),
|
||||
).toBe(origin)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -135,24 +135,20 @@ describe('CampaignMobileTopBar', () => {
|
|||
expect(screen.getByRole('link', { name: 'Sair da ação' })).toBeTruthy()
|
||||
})
|
||||
|
||||
it('renders skip link instead of dismiss when chained flow defines skip', () => {
|
||||
it('renders dismiss on every wizard step and never a skip link (B168)', () => {
|
||||
renderTopBar({
|
||||
wizardChrome: {
|
||||
flowTitle: 'Registrar atualização',
|
||||
stepKind: 'continue',
|
||||
previousHref: '/campanha/acoes/atualizar-votos?municipio=cairu',
|
||||
dismissHref: '/campanha',
|
||||
skip: {
|
||||
label: 'Pular',
|
||||
href: '/campanha/acoes/mudar-tendencia?municipio=cairu',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(screen.getByRole('link', { name: 'Pular' }).getAttribute('href')).toBe(
|
||||
'/campanha/acoes/mudar-tendencia?municipio=cairu',
|
||||
expect(screen.getByRole('link', { name: 'Sair da ação' }).getAttribute('href')).toBe(
|
||||
'/campanha',
|
||||
)
|
||||
expect(screen.queryByRole('link', { name: 'Sair da ação' })).toBeNull()
|
||||
expect(screen.queryByRole('link', { name: 'Pular' })).toBeNull()
|
||||
})
|
||||
|
||||
it('keeps wizard mode when chrome updates between steps', () => {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { CAMPAIGN_ACTIONS_HOME } from '@/lib/campaignActionRoutes'
|
||||
import {
|
||||
resolveWizardVotesSkip,
|
||||
WIZARD_CHAIN_SKIP_LABEL,
|
||||
WIZARD_DISMISS_ARIA_LABEL,
|
||||
wizardFlowChromeAriaLabel,
|
||||
wizardFlowTitleForSlug,
|
||||
|
|
@ -22,14 +19,4 @@ describe('campaignWizardCopy', () => {
|
|||
expect(wizardMunicipalityChromeAriaLabel('Cairu')).toBe('Município em atualização: Cairu')
|
||||
expect(WIZARD_DISMISS_ARIA_LABEL).toBe('Sair da ação')
|
||||
})
|
||||
|
||||
it('uses the shared chain skip label for embedded vote adjustments', () => {
|
||||
expect(WIZARD_CHAIN_SKIP_LABEL).toBe('Pular')
|
||||
expect(resolveWizardVotesSkip(undefined, 'cairu')).toBeUndefined()
|
||||
expect(resolveWizardVotesSkip('update-votes', 'cairu')).toBeUndefined()
|
||||
expect(resolveWizardVotesSkip('change-trend', 'cairu')).toEqual({
|
||||
label: WIZARD_CHAIN_SKIP_LABEL,
|
||||
href: `${CAMPAIGN_ACTIONS_HOME}/atualizar-lideranca?municipio=cairu&entry=change-trend`,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,18 +1,12 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { CAMPAIGN_ACTIONS_HOME } from '@/lib/campaignActionRoutes'
|
||||
import { WIZARD_CHAIN_SKIP_LABEL } from '@/lib/campaignWizardCopy'
|
||||
import {
|
||||
politicalTrendWizardMeta,
|
||||
politicalTrendWizardMetaByStatus,
|
||||
} from '@/lib/politicalTrendWizardMeta'
|
||||
import {
|
||||
buildPoliticalTrendNotePrefill,
|
||||
resolvePoliticalTrendNotePrefillSource,
|
||||
resolveWizardTrendNoteDestination,
|
||||
resolveWizardTrendSkip,
|
||||
selectablePoliticalTrendStatuses,
|
||||
shouldShowWizardTrendSkip,
|
||||
WIZARD_TREND_UNREGISTERED_TITLE,
|
||||
wizardTrendChoiceStepTitle,
|
||||
} from '@/lib/politicalTrendWizardUi'
|
||||
|
|
@ -35,58 +29,6 @@ describe('politicalTrendWizardUi', () => {
|
|||
expect(resolveWizardTrendNoteDestination('favoravel', 'favoravel')).toBe('home')
|
||||
expect(resolveWizardTrendNoteDestination('favoravel', null)).toBe('note')
|
||||
})
|
||||
|
||||
it('hides skip for standalone change-trend entry', () => {
|
||||
expect(shouldShowWizardTrendSkip(undefined)).toBe(false)
|
||||
expect(shouldShowWizardTrendSkip('change-trend')).toBe(false)
|
||||
expect(resolveWizardTrendSkip(undefined, 'cairu')).toBeUndefined()
|
||||
expect(resolveWizardTrendSkip('change-trend', 'cairu')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('shows skip to the next chain step when embedded from another wizard', () => {
|
||||
expect(shouldShowWizardTrendSkip('update-votes')).toBe(true)
|
||||
expect(shouldShowWizardTrendSkip('register-update')).toBe(true)
|
||||
expect(resolveWizardTrendSkip('update-votes', 'cairu')).toEqual({
|
||||
label: WIZARD_CHAIN_SKIP_LABEL,
|
||||
href: `${CAMPAIGN_ACTIONS_HOME}/registrar-atualizacao?municipio=cairu&entry=update-votes`,
|
||||
})
|
||||
})
|
||||
|
||||
it('builds note prefills from embedded wizard sources', () => {
|
||||
expect(buildPoliticalTrendNotePrefill({ kind: 'none' })).toBe('')
|
||||
expect(
|
||||
buildPoliticalTrendNotePrefill({
|
||||
kind: 'voteAdjustment',
|
||||
previousValue: 120,
|
||||
newValue: 180,
|
||||
}),
|
||||
).toBe('Ajuste de votos: 120 → 180')
|
||||
expect(buildPoliticalTrendNotePrefill({ kind: 'custom', text: 'Contexto do fluxo' })).toBe(
|
||||
'Contexto do fluxo',
|
||||
)
|
||||
})
|
||||
|
||||
it('resolves prefill sources from entry action and query params', () => {
|
||||
expect(
|
||||
resolvePoliticalTrendNotePrefillSource({
|
||||
entryAction: 'update-votes',
|
||||
voteFrom: '100',
|
||||
voteTo: '150',
|
||||
}),
|
||||
).toEqual({ kind: 'voteAdjustment', previousValue: 100, newValue: 150 })
|
||||
|
||||
expect(
|
||||
resolvePoliticalTrendNotePrefillSource({
|
||||
entryAction: 'register-update',
|
||||
}),
|
||||
).toEqual({ kind: 'none' })
|
||||
|
||||
expect(
|
||||
resolvePoliticalTrendNotePrefillSource({
|
||||
notePrefill: 'Texto direto',
|
||||
}),
|
||||
).toEqual({ kind: 'custom', text: 'Texto direto' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('politicalTrendWizardMeta', () => {
|
||||
|
|
|
|||
|
|
@ -1,168 +0,0 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { CAMPAIGN_ACTIONS_HOME } from '@/lib/campaignActionRoutes'
|
||||
import { CAMPAIGN_HOME } from '@/lib/campaignPaths'
|
||||
import {
|
||||
isWizardChainActionId,
|
||||
nextWizardChainStep,
|
||||
resolveWizardChainEntry,
|
||||
wizardChainAfter,
|
||||
wizardChainContinueHref,
|
||||
wizardPreviousHref,
|
||||
} from '@/lib/wizardActionChain'
|
||||
|
||||
describe('wizardActionChain', () => {
|
||||
it('excludes register-demand from the chainable set', () => {
|
||||
expect(isWizardChainActionId('update-votes')).toBe(true)
|
||||
expect(isWizardChainActionId('register-demand')).toBe(false)
|
||||
expect(isWizardChainActionId(undefined)).toBe(false)
|
||||
})
|
||||
|
||||
it('returns the full queue after the principal action', () => {
|
||||
expect(wizardChainAfter('update-votes')).toEqual([
|
||||
'change-trend',
|
||||
'register-update',
|
||||
'update-leadership',
|
||||
])
|
||||
expect(wizardChainAfter('update-votes', 'update-votes')).toEqual([
|
||||
'change-trend',
|
||||
'register-update',
|
||||
'update-leadership',
|
||||
])
|
||||
expect(wizardChainAfter('register-update', 'register-update')).toEqual([
|
||||
'change-trend',
|
||||
'update-votes',
|
||||
'update-leadership',
|
||||
])
|
||||
expect(wizardChainAfter('change-trend', 'change-trend')).toEqual([
|
||||
'register-update',
|
||||
'update-votes',
|
||||
'update-leadership',
|
||||
])
|
||||
expect(wizardChainAfter('update-leadership', 'update-leadership')).toEqual([
|
||||
'register-update',
|
||||
'update-votes',
|
||||
'change-trend',
|
||||
])
|
||||
})
|
||||
|
||||
it('slices the remaining queue after a chained step', () => {
|
||||
expect(wizardChainAfter('update-votes', 'change-trend')).toEqual([
|
||||
'register-update',
|
||||
'update-leadership',
|
||||
])
|
||||
expect(wizardChainAfter('update-votes', 'register-update')).toEqual(['update-leadership'])
|
||||
expect(wizardChainAfter('update-votes', 'update-leadership')).toEqual([])
|
||||
expect(nextWizardChainStep('change-trend', 'register-update')).toBe('update-votes')
|
||||
expect(nextWizardChainStep('update-leadership', 'change-trend')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('builds continue hrefs with entry + municipio, or Início at the end', () => {
|
||||
expect(wizardChainContinueHref('update-votes', 'update-votes', 'cairu')).toBe(
|
||||
`${CAMPAIGN_ACTIONS_HOME}/mudar-tendencia?municipio=cairu&entry=update-votes`,
|
||||
)
|
||||
expect(wizardChainContinueHref('update-votes', 'change-trend', 'cairu')).toBe(
|
||||
`${CAMPAIGN_ACTIONS_HOME}/registrar-atualizacao?municipio=cairu&entry=update-votes`,
|
||||
)
|
||||
expect(wizardChainContinueHref('register-update', 'change-trend', 'cairu')).toBe(
|
||||
`${CAMPAIGN_ACTIONS_HOME}/atualizar-votos?municipio=cairu&entry=register-update`,
|
||||
)
|
||||
expect(wizardChainContinueHref('change-trend', 'update-leadership', 'cairu')).toBe(
|
||||
CAMPAIGN_HOME,
|
||||
)
|
||||
expect(wizardChainContinueHref('register-demand', 'update-votes', 'cairu')).toBe(CAMPAIGN_HOME)
|
||||
})
|
||||
|
||||
it('honors allowlisted return path when the chain ends', () => {
|
||||
const origin = '/campanha/municipios/cairu'
|
||||
expect(wizardChainContinueHref('change-trend', 'update-leadership', 'cairu', origin)).toBe(
|
||||
origin,
|
||||
)
|
||||
expect(wizardChainContinueHref('update-votes', 'change-trend', 'cairu', origin)).toBe(
|
||||
`${CAMPAIGN_ACTIONS_HOME}/registrar-atualizacao?municipio=cairu&entry=update-votes&from=${encodeURIComponent(origin)}`,
|
||||
)
|
||||
})
|
||||
|
||||
it('resolves session entry from query or current principal', () => {
|
||||
expect(resolveWizardChainEntry(undefined, 'update-votes')).toBe('update-votes')
|
||||
expect(resolveWizardChainEntry('register-update', 'update-votes')).toBe('register-update')
|
||||
expect(resolveWizardChainEntry('register-demand', 'change-trend')).toBe('change-trend')
|
||||
})
|
||||
|
||||
describe('wizardPreviousHref', () => {
|
||||
it('returns chain-end for municipality search', () => {
|
||||
expect(
|
||||
wizardPreviousHref({
|
||||
actionSlug: 'atualizar-votos',
|
||||
stepKind: 'municipality-search',
|
||||
}),
|
||||
).toBe(CAMPAIGN_HOME)
|
||||
})
|
||||
|
||||
it('returns municipality search for principal post-municipio steps', () => {
|
||||
expect(
|
||||
wizardPreviousHref({
|
||||
actionSlug: 'atualizar-votos',
|
||||
stepKind: 'votes',
|
||||
municipalitySlug: 'cairu',
|
||||
}),
|
||||
).toBe(`${CAMPAIGN_ACTIONS_HOME}/atualizar-votos`)
|
||||
expect(
|
||||
wizardPreviousHref({
|
||||
actionSlug: 'mudar-tendencia',
|
||||
stepKind: 'trend-choice',
|
||||
municipalitySlug: 'cairu',
|
||||
}),
|
||||
).toBe(`${CAMPAIGN_ACTIONS_HOME}/mudar-tendencia`)
|
||||
})
|
||||
|
||||
it('returns previous chain principal when chained from another entry', () => {
|
||||
expect(
|
||||
wizardPreviousHref({
|
||||
actionSlug: 'mudar-tendencia',
|
||||
stepKind: 'trend-choice',
|
||||
municipalitySlug: 'cairu',
|
||||
entryAction: 'update-votes',
|
||||
}),
|
||||
).toBe(`${CAMPAIGN_ACTIONS_HOME}/atualizar-votos?municipio=cairu&entry=update-votes`)
|
||||
expect(
|
||||
wizardPreviousHref({
|
||||
actionSlug: 'registrar-atualizacao',
|
||||
stepKind: 'update-body',
|
||||
municipalitySlug: 'cairu',
|
||||
entryAction: 'update-votes',
|
||||
}),
|
||||
).toBe(`${CAMPAIGN_ACTIONS_HOME}/mudar-tendencia?municipio=cairu&entry=update-votes`)
|
||||
expect(
|
||||
wizardPreviousHref({
|
||||
actionSlug: 'atualizar-lideranca',
|
||||
stepKind: 'leadership-grid',
|
||||
municipalitySlug: 'cairu',
|
||||
entryAction: 'register-update',
|
||||
}),
|
||||
).toBe(`${CAMPAIGN_ACTIONS_HOME}/atualizar-votos?municipio=cairu&entry=register-update`)
|
||||
})
|
||||
|
||||
it('returns internal previous step within a subflow', () => {
|
||||
expect(
|
||||
wizardPreviousHref({
|
||||
actionSlug: 'mudar-tendencia',
|
||||
stepKind: 'trend-note',
|
||||
municipalitySlug: 'cairu',
|
||||
entryAction: 'update-votes',
|
||||
}),
|
||||
).toBe(`${CAMPAIGN_ACTIONS_HOME}/mudar-tendencia?municipio=cairu&entry=update-votes`)
|
||||
})
|
||||
|
||||
it('honors return path on municipality search back target', () => {
|
||||
const origin = '/campanha/municipios/cairu'
|
||||
expect(
|
||||
wizardPreviousHref({
|
||||
actionSlug: 'atualizar-votos',
|
||||
stepKind: 'municipality-search',
|
||||
returnPath: origin,
|
||||
}),
|
||||
).toBe(origin)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -3,7 +3,6 @@ import { afterEach, describe, expect, it, vi } from 'vitest'
|
|||
|
||||
import { WizardExpectedVotesStep } from '@/components/campaign/shared/WizardExpectedVotesStep'
|
||||
import { CampaignWizardChromeProvider } from '@/components/campaign/shell/CampaignWizardChromeContext'
|
||||
import { CAMPAIGN_ACTIONS_HOME } from '@/lib/campaignActionRoutes'
|
||||
import { postCampaignJson } from '@/lib/campaignJsonRequest'
|
||||
|
||||
const pushMock = vi.fn()
|
||||
|
|
@ -96,7 +95,7 @@ describe('WizardExpectedVotesStep', () => {
|
|||
expect(screen.getByLabelText('Pessimista').getAttribute('aria-invalid')).toBeNull()
|
||||
})
|
||||
|
||||
it('posts batch expected votes and continues the wizard chain', async () => {
|
||||
it('posts batch expected votes and returns to origin', async () => {
|
||||
vi.mocked(postCampaignJson).mockResolvedValue({
|
||||
ok: true,
|
||||
payload: {
|
||||
|
|
@ -120,18 +119,32 @@ describe('WizardExpectedVotesStep', () => {
|
|||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(pushMock).toHaveBeenCalledWith(
|
||||
`${CAMPAIGN_ACTIONS_HOME}/mudar-tendencia?municipio=cairu&entry=update-votes`,
|
||||
)
|
||||
expect(pushMock).toHaveBeenCalledWith('/campanha')
|
||||
})
|
||||
})
|
||||
|
||||
it('shows skip to the next chain step when votes is chained', () => {
|
||||
renderVotesStep({ ...defaultProps, entryAction: 'register-update' })
|
||||
it('returns to the allowlisted origin when a return path is set (B110)', async () => {
|
||||
vi.mocked(postCampaignJson).mockResolvedValue({
|
||||
ok: true,
|
||||
payload: {
|
||||
status: 'success',
|
||||
savedExpectedVotes: { pessimistic: 150, central: 250, optimistic: 350 },
|
||||
},
|
||||
})
|
||||
|
||||
const skip = screen.getByRole('link', { name: /^Pular$/i })
|
||||
expect(skip.getAttribute('href')).toBe(
|
||||
`${CAMPAIGN_ACTIONS_HOME}/atualizar-lideranca?municipio=cairu&entry=register-update`,
|
||||
)
|
||||
renderVotesStep({ ...defaultProps, returnPath: '/campanha/municipios/cairu' })
|
||||
|
||||
fireEvent.change(screen.getByLabelText('Média'), { target: { value: '250' } })
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Salvar estimativas →' }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(pushMock).toHaveBeenCalledWith('/campanha/municipios/cairu')
|
||||
})
|
||||
})
|
||||
|
||||
it('never renders a chain skip link (B168)', () => {
|
||||
renderVotesStep({ ...defaultProps })
|
||||
|
||||
expect(screen.queryByRole('link', { name: /^Pular$/i })).toBeNull()
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,41 +0,0 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { CAMPAIGN_HOME } from '@/lib/campaignPaths'
|
||||
import { WIZARD_CHAIN_SKIP_LABEL } from '@/lib/campaignWizardCopy'
|
||||
import {
|
||||
resolveWizardLeadershipSkip,
|
||||
showLeadershipWizardSkip,
|
||||
} from '@/lib/wizardLeadershipContract'
|
||||
|
||||
describe('showLeadershipWizardSkip', () => {
|
||||
it('hides skip for standalone entry (implicit update-leadership)', () => {
|
||||
expect(showLeadershipWizardSkip(undefined)).toBe(false)
|
||||
})
|
||||
|
||||
it('hides skip when entry is update-leadership', () => {
|
||||
expect(showLeadershipWizardSkip('update-leadership')).toBe(false)
|
||||
})
|
||||
|
||||
it('shows skip when embedded from another wizard action', () => {
|
||||
expect(showLeadershipWizardSkip('update-votes')).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('resolveWizardLeadershipSkip', () => {
|
||||
it('returns undefined for standalone entry', () => {
|
||||
expect(resolveWizardLeadershipSkip(undefined, 'cairu')).toBeUndefined()
|
||||
expect(resolveWizardLeadershipSkip('update-leadership', 'cairu')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('points skip to origin when from is set, else Início at chain end', () => {
|
||||
const origin = '/campanha/liderancas/7'
|
||||
expect(resolveWizardLeadershipSkip('update-votes', 'cairu', origin)).toEqual({
|
||||
label: WIZARD_CHAIN_SKIP_LABEL,
|
||||
href: origin,
|
||||
})
|
||||
expect(resolveWizardLeadershipSkip('update-votes', 'cairu')).toEqual({
|
||||
label: WIZARD_CHAIN_SKIP_LABEL,
|
||||
href: CAMPAIGN_HOME,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { CAMPAIGN_ACTIONS_HOME } from '@/lib/campaignActionRoutes'
|
||||
import { WIZARD_CHAIN_SKIP_LABEL } from '@/lib/campaignWizardCopy'
|
||||
import { resolveWizardUpdateSkip, shouldShowWizardUpdateSkip } from '@/lib/wizardUpdateUi'
|
||||
|
||||
describe('wizardUpdateUi', () => {
|
||||
it('hides skip for standalone register-update entry', () => {
|
||||
expect(shouldShowWizardUpdateSkip(undefined)).toBe(false)
|
||||
expect(shouldShowWizardUpdateSkip('register-update')).toBe(false)
|
||||
expect(resolveWizardUpdateSkip(undefined, 'cairu')).toBeUndefined()
|
||||
expect(resolveWizardUpdateSkip('register-update', 'cairu')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('shows skip to the next chain step when embedded from another wizard', () => {
|
||||
expect(shouldShowWizardUpdateSkip('update-votes')).toBe(true)
|
||||
expect(shouldShowWizardUpdateSkip('change-trend')).toBe(true)
|
||||
expect(resolveWizardUpdateSkip('update-votes', 'cairu')).toEqual({
|
||||
label: WIZARD_CHAIN_SKIP_LABEL,
|
||||
href: `${CAMPAIGN_ACTIONS_HOME}/atualizar-lideranca?municipio=cairu&entry=update-votes`,
|
||||
})
|
||||
expect(resolveWizardUpdateSkip('change-trend', 'cairu')).toEqual({
|
||||
label: WIZARD_CHAIN_SKIP_LABEL,
|
||||
href: `${CAMPAIGN_ACTIONS_HOME}/atualizar-votos?municipio=cairu&entry=change-trend`,
|
||||
})
|
||||
})
|
||||
})
|
||||
Loading…
Reference in a new issue