C13 pos-simplify: fecha os residuos da varredura e estreita os guards
O rename mecanico deixou um defeito silencioso e alguns fosseis: o fixture de
recentVisits perdeu o ${index} (todas as entradas com o mesmo label, invisivel
porque o label nao era asserido) e access/shared.ts guardava "action plan" em
prosa, forma que o guard de vocabulario nao cobria. O padrao agora tolera espaco
e a allowlist encolheu: o comentario de campaignMigrationReconciliation cita
activity e o arquivo saiu da excecao.
Na migration, a entrada action_plan_municipality_idx era inalcancavel (nenhuma
migration jamais criou esse nome) e saiu, deixando up() e down() com os mesmos
48 pares; as duas buscas de FK passam a usar to_regclass, que retorna NULL em vez
de lancar -- a unica consulta que contradizia a invariante do proprio cabecalho.
O detalhe de organizacao para de derivar o rotulo de status a mao e reusa
ActivityStatusBadge, com status tipado como ActivityStatus no loader.
Debitos do /simplify registrados: C8 F4b (o guard de formActions so inspeciona
arquivos chamados exatamente formActions.ts) no plano do C8; defers e descartes
com gatilho no plano do C13.
This commit is contained in:
parent
7b93c232ff
commit
619666dd51
21 changed files with 96 additions and 73 deletions
|
|
@ -7,7 +7,7 @@ alwaysApply: false
|
|||
|
||||
## Status
|
||||
|
||||
- **C13 entregue em código (2026-07-25):** "Plano de Ação" → **"Atividade"** em toda a vertical. Collection `actionPlan` → `activity` (`src/collections/Activity.ts`, labels "Atividade"/"Atividades", hooks internos renomeados), campo `campaignDemand.actionPlan` → `activity`, 7 utilities `actionPlan*` → `activity*`, `access/actionPlans.ts` → `access/activities.ts`, `lib/schemas/actionPlan.ts` → `activity.ts`, `actions/actionPlan.ts` → `activity.ts`, `components/campaign/actionPlan/` → `activity/` (13 componentes), rota `(app)/planos/` → `(app)/atividades/` (criação em `atividades/nova`, feminino como `demandas/nova`) e sidebar "Atividades". **Migration data-preserving escrita à mão** `20260725_213000_rename_action_plan_to_activity`: 4 tabelas, 3 enums, 2 colunas, 39 índices (4 pkeys inclusos), 14 FKs, 2 sequences — lista canônica tirada de `pg_indexes`/`pg_constraint`/`pg_type`/`pg_sequences` porque o snapshot do drizzle não conhece o índice parcial `action_plan_upcoming_start_at_idx` (escrito à mão no C8) e porque o gerador do Payload emitiria DROP+CREATE. O `.json` foi derivado do snapshot anterior por transformação textual (o `migrate:create` é interativo para renames) e validado pelo teste que importa: `pnpm migrate:create __probe` responde "No schema changes detected". **Bônus aprovado pelo usuário nesta sessão:** os **8 índices fósseis `%plaza%`** da remodelagem de 23/07 (que renomeou as colunas `plaza_id` mas não os índices, deixando banco e snapshot em desacordo) foram alinhados na mesma migration; `down()` os restaura, para não inventar um estado que nunca existiu. Decisões de vocabulário: sidebar **"Atividades"** (uma palavra só, a da entidade) e rótulos de campo alinhados — **"Tipo de atividade"**, **"Origem da atividade"**, **"Resultado da atividade"** — porque "ação" ao lado de "atividade" reintroduzia a tradução mental que o item existe para matar; valores de enum `kind`/`status`/`origin` **inalterados**. **Guard:** o describe "banned campaign terminology" de `codebaseConventions.unit.spec.ts` virou tabela de termos varrendo `src`+`tests`+`scripts` (migrations fora — história congelada), banindo os compostos `actionPlan`, a copy "plano de ação" e a rota velha, mas **não** `plan`/`plano` isolado (colidiria com os planos de assinatura da Fase 2 white-label); allowlist: o próprio spec, `scripts/generate-remodel-municipalities-migration.mjs` e o comentário histórico em `campaignMigrationReconciliation.int.spec.ts`. Estendê-lo a `tests/` derrubou 8 fósseis "Praça" em fixtures, limpos no mesmo PR. Gate verde (tsc, lint, format, knip, check:cycles, 46 unit / 48 int / 7 e2e, build); Aikido sem achado. **Deploy:** o `pnpm build` da Vercel aplica a migration enquanto o deployment antigo ainda serve — deployar fora do horário de mesa (mesmo perfil da remodelagem de 23/07). Plano: [`renomear-plano-acao-para-atividade.md`](../../../docs/plans/renomear-plano-acao-para-atividade.md).
|
||||
- **C13 entregue em código (2026-07-25):** "Plano de Ação" → **"Atividade"** em toda a vertical. Collection `actionPlan` → `activity` (`src/collections/Activity.ts`, labels "Atividade"/"Atividades", hooks internos renomeados), campo `campaignDemand.actionPlan` → `activity`, 7 utilities `actionPlan*` → `activity*`, `access/actionPlans.ts` → `access/activities.ts`, `lib/schemas/actionPlan.ts` → `activity.ts`, `actions/actionPlan.ts` → `activity.ts`, `components/campaign/actionPlan/` → `activity/` (13 componentes), rota `(app)/planos/` → `(app)/atividades/` (criação em `atividades/nova`, feminino como `demandas/nova`) e sidebar "Atividades". **Migration data-preserving escrita à mão** `20260725_213000_rename_action_plan_to_activity`: 4 tabelas, 3 enums, 2 colunas, 39 índices (4 pkeys inclusos), 14 FKs, 2 sequences — lista canônica tirada de `pg_indexes`/`pg_constraint`/`pg_type`/`pg_sequences` porque o snapshot do drizzle não conhece o índice parcial `action_plan_upcoming_start_at_idx` (escrito à mão no C8) e porque o gerador do Payload emitiria DROP+CREATE. O `.json` foi derivado do snapshot anterior por transformação textual (o `migrate:create` é interativo para renames) e validado pelo teste que importa: `pnpm migrate:create __probe` responde "No schema changes detected". **Bônus aprovado pelo usuário nesta sessão:** os **8 índices fósseis `%plaza%`** da remodelagem de 23/07 (que renomeou as colunas `plaza_id` mas não os índices, deixando banco e snapshot em desacordo) foram alinhados na mesma migration; `down()` os restaura, para não inventar um estado que nunca existiu. Decisões de vocabulário: sidebar **"Atividades"** (uma palavra só, a da entidade) e rótulos de campo alinhados — **"Tipo de atividade"**, **"Origem da atividade"**, **"Resultado da atividade"** — porque "ação" ao lado de "atividade" reintroduzia a tradução mental que o item existe para matar; valores de enum `kind`/`status`/`origin` **inalterados**. **Guard:** o describe "banned campaign terminology" de `codebaseConventions.unit.spec.ts` virou tabela de termos varrendo `src`+`tests`+`scripts` (migrations fora — história congelada), banindo os compostos `actionPlan`, a copy "plano de ação" e a rota velha, mas **não** `plan`/`plano` isolado (colidiria com os planos de assinatura da Fase 2 white-label); allowlist: o próprio spec, `scripts/generate-remodel-municipalities-migration.mjs` e o comentário histórico em `campaignMigrationReconciliation.int.spec.ts`. Estendê-lo a `tests/` derrubou 8 fósseis "Praça" em fixtures, limpos no mesmo PR. Gate verde (tsc, lint, format, knip, check:cycles, 46 unit / 48 int / 7 e2e, build); Aikido sem achado. **Deploy:** o `pnpm build` da Vercel aplica a migration enquanto o deployment antigo ainda serve — deployar fora do horário de mesa (mesmo perfil da remodelagem de 23/07). **Pós-`/simplify`:** a varredura mecânica comeu uma interpolação — `tests/unit/recentVisits.unit.spec.ts` ficou com `label: \`Município \`` para todas as entradas, e passava porque o label não era asserido (restaurado, com assertion nova para fechar a brecha, e os slugs `praca-` do fixture alinhados). O guard ganhou tolerância a espaço (`action[\s_-]?plan`), que era o buraco por onde `access/shared.ts` mantinha "action plan" em prosa; com isso a allowlist encolheu — o comentário de `campaignMigrationReconciliation.int.spec.ts` passou a citar `activity` e o arquivo saiu da exceção (uma spec que cresce não fica fora do guard por causa de um comentário). Deliberadamente **não** ampliei o padrão de "Praça" para a forma sem cedilha: cobraria três entradas novas de allowlist (nomes reais de organização na planilha, o teste de entradas legadas `kind: 'nucleus'`) por um fóssil já corrigido. Também: três resíduos do find-and-replace (banner `// Action activities`, `DOSSIER_*_PLAN_LIMIT`, "a activity" em dois comentários), concordância em pt-BR ("Criada por", "atividades apoiadas" minúsculo), o `??` impossível em `ActivityForm`, e as duas buscas de FK da migration agora usam `to_regclass` em vez de `::regclass` (que lança em vez de retornar NULL, contradizendo a invariante do próprio cabeçalho). A entrada morta `action_plan_municipality_idx` saiu da lista de índices da migration (nenhuma migration jamais criou esse nome — só os snapshots o afirmam, e com `push: false` nenhum banco pode carregá-lo), deixando `up()` e `down()` com os mesmos 48 pares; e o detalhe de organização parou de derivar o rótulo de status à mão e passou a reusar `ActivityStatusBadge`, com `OrganizationDetailViewModel.activities[].status` tipado como `ActivityStatus` em vez de `string` (a origem que forçava o re-narrowing no consumidor). Plano: [`renomear-plano-acao-para-atividade.md`](../../../docs/plans/renomear-plano-acao-para-atividade.md).
|
||||
- **B26 entregue em código (2026-07-25):** registrar sinal a partir da célula "Último sinal" de `/campanha/municipios` — Popover (desktop) / Drawer bottom sheet (mobile) cria `municipalityUpdate` `kind: 'sinal'` (C12) sem sair da fila; submit explícito "Registrar sinal" (exceção do princípio 4); `revalidateMunicipalityListPaths`; sem migration. Plano: [`registrar-sinal-lista-municipios.md`](../../../docs/plans/registrar-sinal-lista-municipios.md).
|
||||
- **B20 entregue (2026-07-25):** Municípios prioritários no Início — `pickDashboardPriorityMunicipalities` (`src/utilities/dashboardPriorityMunicipalities.ts`) ordena `priority=alta` por déficit E9/`central`, amostra 8; `CampaignDashboard` exibe Badge com `highPriorityCount`, CTA “Ver todas” → `?priority=alta`, chips com `list-none`. Unit `dashboardPriorityMunicipalities.unit.spec.ts`. Plano: [`municipios-prioritarios-inicio.md`](../../../docs/plans/municipios-prioritarios-inicio.md).
|
||||
- **B28 registrado (2026-07-25):** e-mail + celular na lista `/campanha/liderancas` — colunas com copy-on-click + WhatsApp (`wa.me`) no padrão de `/campanha/assessores` (B19 ✓); `email` no view model da lista; extrair primitivo compartilhado (2º call site); **sem** toggle Editar in-list (Contact update adiado). Appetite ~0,5–0,75d; Janela 1; cortável; Impeccable B. Plano: [`email-celular-lista-liderancas.md`](../../../docs/plans/email-celular-lista-liderancas.md).
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
# Escala e DRY pós-C6 (apoiadores / import / listas)
|
||||
|
||||
Status: F4 (DRY de forms) **entregue** — twin praça 2026-07-21 + hardening 2026-07-23; **o resto do F4 (10 escadas hand-rolled escritas depois) fechou em 2026-07-25 no Pass 2 W4d** (`runCampaignFormAction`, exceções documentadas em comentário); **F3 (parsers das listas de entidade) fechou no Pass 2 W1-D2**; F1–F2 abertos com gatilho (volume real de import/base nominal pós-Onda 0)
|
||||
Atualizado em: 2026-07-24 (refs sincronizadas pós-remodelagem Municípios + hardening)
|
||||
Status: F4 (DRY de forms) **entregue** — twin praça 2026-07-21 + hardening 2026-07-23; **o resto do F4 (10 escadas hand-rolled escritas depois) fechou em 2026-07-25 no Pass 2 W4d** (`runCampaignFormAction`, exceções documentadas em comentário); **F3 (parsers das listas de entidade) fechou no Pass 2 W1-D2**; **F4b aberta** (o guard do F4 só inspeciona `formActions.ts`, deixando os arquivos de action irmãos sem verificação — achado do `/simplify` do C13); F1–F2 abertos com gatilho (volume real de import/base nominal pós-Onda 0)
|
||||
Atualizado em: 2026-07-25 (F4b registrada pelo `capture-review-debts` pós-C13)
|
||||
Item do roadmap: [docs/roadmap.md](../roadmap.md) (Fill-ins abertos, item C8)
|
||||
Responsável: —
|
||||
|
||||
|
|
@ -88,7 +88,15 @@ flowchart TD
|
|||
- ~~Migrar `fieldError`/`firstError` local em forms legados~~ — `CampaignInviteForm.tsx` já importa de `campaignFormFields.ts`.
|
||||
- **Nota pós-remodel:** paths `nucleos/*`/`pracas/*` foram superseded; não reabrir.
|
||||
|
||||
**Migration:** Fases 1, 3, 4 sem schema. Fase 2: `pnpm migrate:create add_contact_trgm_index` (extensão + 2 GIN indexes). Sem Consent novo.
|
||||
### Fase 4b — o guard do F4 só vê `formActions.ts` — **ABERTA** (registrada 2026-07-25 pós-C13)
|
||||
|
||||
Appetite: **~2h eng, fill-in.** O `/simplify` do C13 mostrou que a invariante do F4 é menor do que o texto acima sugere: o guard `campaign formActions convention` (`tests/unit/codebaseConventions.unit.spec.ts`) filtra `file.endsWith('/formActions.ts')`, então os arquivos de action irmãos que cada rota escreveu depois — `lifecycleFormActions.ts`, `resultFormActions.ts`, `updateFormActions.ts`, `taskActions.ts` — **não são verificados em nenhuma rota**. A exceção documentada na Revisão 2026-07-24 ("`lifecycleFormActions.ts` só retorna safe-messages fixas — sem ladder para migrar") foi escrita quando o alvo era `mapCampaignFormActionError`; com `runCampaignRedirectFormAction` (Pass 2 W4d) a conta mudou, porque esse arquivo faz exatamente a forma que o wrapper padroniza: dois `catch` nus que descartam o erro + `redirect` no sucesso.
|
||||
|
||||
- Ampliar o filtro do guard para os irmãos (`/^\w*[Ff]ormActions\.ts$/` + `taskActions.ts`, ou um sufixo declarado) e allowlistar com racional escrito o que realmente não couber.
|
||||
- Migrar o que o wrapper cobre; onde o `catch` nu ficar, decidir se o erro deve ser observável (hoje é descartado sem log em ambos os caminhos, então não é um débito diferencial — verificar no PR antes de inflar o escopo).
|
||||
- Não escopo: mudar a semântica das safe-messages (copy revisada) e tocar `actions/*.ts` (as server actions em si já usam transação/lock).
|
||||
|
||||
**Migration:** Fases 1, 3, 4, 4b sem schema. Fase 2: `pnpm migrate:create add_contact_trgm_index` (extensão + 2 GIN indexes). Sem Consent novo.
|
||||
|
||||
## Dependências
|
||||
|
||||
|
|
|
|||
|
|
@ -128,6 +128,25 @@ Componentes:
|
|||
- **Rótulo "Agenda" no sidebar.** Revisitar se, no onboarding (ou no R6), ≥2 pessoas do time chamarem o destino de "agenda" ao navegar — aí o rail muda de rótulo mantendo a entidade "Atividade".
|
||||
- **Terceira entrada no guard de vocabulário (extração para módulo próprio).** Revisitar quando existir um 3º rename de termo — antes disso, a tabela no spec basta.
|
||||
|
||||
## Simplify (2026-07-25 pós-C13)
|
||||
|
||||
Passagem `/simplify` (qualidade + perf + reuso) e `capture-review-debts`. O diff é rename mecânico: nenhuma forma de query mudou, o índice parcial `activity_upcoming_start_at_idx` sobreviveu com o predicado intacto (`EXPLAIN` confirma Index Scan sem Filter residual) e o app segue usando os shells compartilhados.
|
||||
|
||||
**Já resolvido (não reabrir):** interpolação `${index}` comida em `tests/unit/recentVisits.unit.spec.ts` (fixture com label único, agora com assertion que fecha a brecha, e slugs `praca-` alinhados); guard tolerante a espaço (`action[\s_-]?plan`), que era o buraco por onde `access/shared.ts` mantinha "action plan" em prosa; allowlist encolhida (o comentário de `campaignMigrationReconciliation.int.spec.ts` cita `activity` e o arquivo saiu da exceção); entrada morta `action_plan_municipality_idx` fora da migration (nenhuma migration jamais criou esse nome — só os snapshots o afirmam), deixando `up()`/`down()` com os mesmos 48 pares; buscas de FK com `to_regclass` em vez de `::regclass`; detalhe de organização reusando `ActivityStatusBadge` com `status` tipado como `ActivityStatus` no loader; resíduos de find-and-replace (banner `// Action activities`, `DOSSIER_*_PLAN_LIMIT`, "a activity", nome de teste "zona Município"); concordância pt-BR ("Criada por", "atividades apoiadas"); `??` impossível em `ActivityForm`.
|
||||
|
||||
**Absorvido em outro plano:** o guard de `formActions` só inspeciona arquivos chamados exatamente `formActions.ts`, deixando `lifecycleFormActions.ts`/`resultFormActions.ts`/`updateFormActions.ts`/`taskActions.ts` sem verificação → [escala-dry-pos-c6.md](escala-dry-pos-c6.md) **Fase 4b** (dono da invariante do C8 F4).
|
||||
|
||||
**Explicitamente fora (triage `capture-review-debts` 2026-07-25 pós-C13):**
|
||||
|
||||
- **Redirect de `/campanha/planos*` → `/campanha/atividades*`** — gatilho: primeiro relato de 404 por bookmark. A rota viveu 7 dias, a navegação é pelo rail, e nada persistido aponta para ela (`recentVisits` só grava páginas de município); um redirect reintroduziria a string que o guard bane.
|
||||
- **Projeção de activity duplicada em `organizationData.ts`** (em vez de `activityListSelect` + `toActivityListViewModel`) — gatilho: a seção passar a mostrar localidade ou progresso de tarefas. Hoje a forma é subconjunto estrito e o view model completo faria over-fetch numa lista de resumo.
|
||||
- **Painel de filtros mobile compartilhado entre `ActivityFilters` e `SupporterFilters`** — gatilho: 3º painel com a mesma mecânica de colapso (municípios usa header rico, não este shape).
|
||||
- **Hoist das listas de rename da migration para constantes + helper local** — recusado: migration é história congelada, o consumidor é único, e a divergência que o helper preveniria está verificada como inexistente (conjuntos idênticos nos dois sentidos).
|
||||
- **Contadores `renamed` + `RAISE NOTICE` do `up()`** — mantidos: são a única evidência, no log do build da Vercel, de que o rename tocou o número esperado de objetos; a assimetria com `down()` é aceitável porque `down()` só roda local.
|
||||
- **`payload.create` sequencial dos rascunhos de demanda em `actions/activity.ts`** — pré-existente, roda sobre os rascunhos de um único submit dentro de uma transação; batchear perderia as semânticas de hook por linha.
|
||||
- **`it.each` do guard re-varrer `src`+`tests`+`scripts` por termo** — 150 ms na suíte unitária; hoisting da leitura é throughput, não duplicação.
|
||||
- **Padrão de "Praça" tolerante à falta de cedilha** — cobraria três allowlists novas (nomes reais de organização vindos da planilha, teste de entradas legadas `kind: 'nucleus'`) para pegar um fóssil já corrigido à mão.
|
||||
|
||||
## Referências
|
||||
|
||||
- `docs/roadmap.md` (Trilha C / "Demais itens abertos"; Janela 1; grafo)
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -4,11 +4,11 @@ import Link from 'next/link'
|
|||
import { notFound, redirect } from 'next/navigation'
|
||||
import { getPayload } from 'payload'
|
||||
|
||||
import { ActivityStatusBadge } from '@/components/campaign/activity/ActivityStatusBadge'
|
||||
import { OrganizationForm } from '@/components/campaign/organization/OrganizationForm'
|
||||
import { CampaignPageShell } from '@/components/campaign/shell/CampaignPageShell'
|
||||
import { Badge } from '@/components/ui/Badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { activityStatusLabels, activityStatuses } from '@/lib/schemas/activity'
|
||||
import { organizationKindLabels } from '@/lib/schemas/organization'
|
||||
import { isCampaignStaff } from '@/utilities/campaignAccess'
|
||||
import { getCampaignUser } from '@/utilities/campaignAuth'
|
||||
|
|
@ -22,11 +22,6 @@ type OrganizationDetailPageProps = {
|
|||
|
||||
const dateFormatter = new Intl.DateTimeFormat('pt-BR', { dateStyle: 'short' })
|
||||
|
||||
const statusLabel = (status: string): string =>
|
||||
activityStatuses.includes(status as (typeof activityStatuses)[number])
|
||||
? activityStatusLabels[status as (typeof activityStatuses)[number]]
|
||||
: status
|
||||
|
||||
export default async function OrganizationDetailPage({ params }: OrganizationDetailPageProps) {
|
||||
const { slug } = await params
|
||||
const [user, payload] = await Promise.all([getCampaignUser(), getPayload({ config })])
|
||||
|
|
@ -108,18 +103,20 @@ export default async function OrganizationDetailPage({ params }: OrganizationDet
|
|||
<ul className="flex flex-col gap-2">
|
||||
{organization.activities.map((activity) => (
|
||||
<li key={activity.id} className="flex items-center justify-between gap-2">
|
||||
<div className="flex min-w-0 flex-col">
|
||||
<Link
|
||||
href={`/campanha/atividades/${activity.slug}`}
|
||||
className="truncate font-medium text-primary underline-offset-4 hover:underline"
|
||||
>
|
||||
{activity.title}
|
||||
</Link>
|
||||
<div className="flex min-w-0 flex-col gap-1">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Link
|
||||
href={`/campanha/atividades/${activity.slug}`}
|
||||
className="truncate font-medium text-primary underline-offset-4 hover:underline"
|
||||
>
|
||||
{activity.title}
|
||||
</Link>
|
||||
<ActivityStatusBadge status={activity.status} />
|
||||
</div>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{statusLabel(activity.status)}
|
||||
{activity.startAt
|
||||
? ` · ${dateFormatter.format(new Date(activity.startAt))}`
|
||||
: ''}
|
||||
? dateFormatter.format(new Date(activity.startAt))
|
||||
: 'Sem data definida'}
|
||||
{activity.deputyPresent ? ' · Deputado presente' : ''}
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -616,7 +616,7 @@ export const Activity: CollectionConfig = {
|
|||
name: 'createdBy',
|
||||
type: 'relationship',
|
||||
relationTo: 'campaignUser',
|
||||
label: 'Criado por',
|
||||
label: 'Criada por',
|
||||
index: true,
|
||||
admin: {
|
||||
readOnly: true,
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ export const Organization: CollectionConfig = {
|
|||
useAsTitle: 'name',
|
||||
defaultColumns: ['name', 'kind', 'updatedAt'],
|
||||
description:
|
||||
'Sindicatos, associações, movimentos e afins. Concentra lideranças associadas e Atividades apoiadas.',
|
||||
'Sindicatos, associações, movimentos e afins. Concentra lideranças associadas e atividades apoiadas.',
|
||||
},
|
||||
access: {
|
||||
create: canCreateOrganization,
|
||||
|
|
|
|||
|
|
@ -179,8 +179,8 @@ const ActivityFormFields = ({
|
|||
</>
|
||||
) : (
|
||||
<FieldDescription>
|
||||
Status atual: {activityStatusLabels[activity?.status ?? ''] ?? activity?.status}
|
||||
. Use as ações do detalhe para marcar como realizado ou cancelado.
|
||||
Status atual: {activity ? activityStatusLabels[activity.status] : ''}. Use as
|
||||
ações do detalhe para marcar como realizado ou cancelado.
|
||||
</FieldDescription>
|
||||
)}
|
||||
</Field>
|
||||
|
|
|
|||
|
|
@ -14,10 +14,11 @@ import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres'
|
|||
* Two index names in the lists are fossils from the Município remodel, which
|
||||
* renamed the `plaza_id` columns but not their indexes: the database holds
|
||||
* `action_plan_plaza_idx` and `campaign_demand_plaza_idx` while the drizzle
|
||||
* snapshot expects `*_municipality_idx`. The six remaining `%plaza%` fossils on
|
||||
* other tables are aligned here too, so the database matches the snapshot in one
|
||||
* pass. Every rename is guarded on the source object existing, so environments
|
||||
* that already carry either name converge.
|
||||
* snapshot expects `*_municipality_idx` — no migration ever created the aligned
|
||||
* name, so the fossil is the only name any database can hold. The six remaining
|
||||
* `%plaza%` fossils on other tables are aligned here too, so the database matches
|
||||
* the snapshot in one pass. Every rename is guarded on the source object existing,
|
||||
* so a partially converged environment still lands on the same state.
|
||||
*/
|
||||
|
||||
const renameTables = sql`
|
||||
|
|
@ -117,7 +118,6 @@ const renameIndexes = sql`
|
|||
('action_plan_start_at_idx', 'activity_start_at_idx'),
|
||||
('action_plan_upcoming_start_at_idx', 'activity_upcoming_start_at_idx'),
|
||||
('action_plan_plaza_idx', 'activity_municipality_idx'),
|
||||
('action_plan_municipality_idx', 'activity_municipality_idx'),
|
||||
('action_plan_responsible_idx', 'activity_responsible_idx'),
|
||||
('action_plan_leadership_idx', 'activity_leadership_idx'),
|
||||
('action_plan_result_recorded_by_idx', 'activity_result_recorded_by_idx'),
|
||||
|
|
@ -188,7 +188,7 @@ const renameForeignKeys = sql`
|
|||
LOOP
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM pg_constraint
|
||||
WHERE conname = r.old_name AND conrelid = format('public.%I', r.table_name)::regclass
|
||||
WHERE conname = r.old_name AND conrelid = to_regclass('public.' || quote_ident(r.table_name))
|
||||
) THEN
|
||||
EXECUTE format(
|
||||
'ALTER TABLE public.%I RENAME CONSTRAINT %I TO %I',
|
||||
|
|
@ -281,7 +281,7 @@ export async function down({ db }: MigrateDownArgs): Promise<void> {
|
|||
LOOP
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM pg_constraint
|
||||
WHERE conname = r.new_name AND conrelid = format('public.%I', r.table_name)::regclass
|
||||
WHERE conname = r.new_name AND conrelid = to_regclass('public.' || quote_ident(r.table_name))
|
||||
) THEN
|
||||
EXECUTE format(
|
||||
'ALTER TABLE public.%I RENAME CONSTRAINT %I TO %I',
|
||||
|
|
|
|||
|
|
@ -479,7 +479,7 @@ export interface StateDeputy {
|
|||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* Sindicatos, associações, movimentos e afins. Concentra lideranças associadas e Atividades apoiadas.
|
||||
* Sindicatos, associações, movimentos e afins. Concentra lideranças associadas e atividades apoiadas.
|
||||
*
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "organization".
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// ---------------------------------------------------------------------------
|
||||
// Action activities
|
||||
// Activities
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
import type { Access, FieldAccess, Where } from 'payload'
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ export const isCampaignStaff = (user: CampaignActor): boolean =>
|
|||
isCampaignUser(user) && isStaffCampaignRole(user.role)
|
||||
|
||||
/**
|
||||
* Eligible relationship targets for advisor assignments (municipality / action plan).
|
||||
* Eligible relationship targets for advisor assignments (municipality / activity).
|
||||
* The candidate is included: the projection sheet lists him as the responsible contact
|
||||
* for some municipalities (decision 2026-07-24).
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ const loadActivityUpdateAuthorNames = async (
|
|||
}
|
||||
|
||||
/**
|
||||
* Display-name lookup for a activity's municipality when the document was loaded at
|
||||
* Display-name lookup for an activity's municipality when the document was loaded at
|
||||
* depth 0 (updates tab). The actor already passed row access on the activity
|
||||
* itself, so reading the municipality's name/slug privileged avoids a second
|
||||
* per-role access round-trip (established display-name pattern).
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ const leadershipLabel = (leadership: Leadership): string | null => {
|
|||
}
|
||||
|
||||
/**
|
||||
* Engaged leaderships visible to the actor (for linking a activity to a leadership).
|
||||
* Engaged leaderships visible to the actor (for linking an activity to a leadership).
|
||||
* Uses overrideAccess: false so Payload row-level access applies.
|
||||
*/
|
||||
export const searchActivityLeadershipOptions = async (
|
||||
|
|
|
|||
|
|
@ -43,8 +43,8 @@ import {
|
|||
*/
|
||||
export const DOSSIER_LEADERSHIP_LIMIT = 8
|
||||
export const DOSSIER_SIGNAL_LIMIT = 5
|
||||
const DOSSIER_UPCOMING_PLAN_LIMIT = 3
|
||||
const DOSSIER_RECENT_PLAN_LIMIT = 2
|
||||
const DOSSIER_UPCOMING_ACTIVITY_LIMIT = 3
|
||||
const DOSSIER_RECENT_ACTIVITY_LIMIT = 2
|
||||
|
||||
export type MunicipalityDossierData = {
|
||||
baseline: MunicipalityElectoralBaseline | null
|
||||
|
|
@ -77,8 +77,8 @@ const loadDossierActivities = async (
|
|||
})
|
||||
|
||||
const [upcoming, recent] = await Promise.all([
|
||||
findActivities('proximos', DOSSIER_UPCOMING_PLAN_LIMIT, 'startAt'),
|
||||
findActivities('realizados', DOSSIER_RECENT_PLAN_LIMIT, '-startAt'),
|
||||
findActivities('proximos', DOSSIER_UPCOMING_ACTIVITY_LIMIT, 'startAt'),
|
||||
findActivities('realizados', DOSSIER_RECENT_ACTIVITY_LIMIT, '-startAt'),
|
||||
])
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import type { Payload } from 'payload'
|
|||
import type { OrganizationKind } from '@/lib/schemas/organization'
|
||||
import { organizationKinds } from '@/lib/schemas/organization'
|
||||
import type { CampaignUser, Organization } from '@/payload-types'
|
||||
import type { ActivityStatus } from '@/utilities/activityUi'
|
||||
import {
|
||||
buildListHref,
|
||||
firstValue,
|
||||
|
|
@ -160,7 +161,7 @@ export type OrganizationDetailViewModel = {
|
|||
id: number
|
||||
title: string
|
||||
slug: string
|
||||
status: string
|
||||
status: ActivityStatus
|
||||
startAt: string | null
|
||||
deputyPresent: boolean
|
||||
}>
|
||||
|
|
@ -239,7 +240,7 @@ export const loadOrganizationDetail = async (
|
|||
id: activity.id,
|
||||
title: activity.title,
|
||||
slug: activity.slug,
|
||||
status: String(activity.status),
|
||||
status: activity.status,
|
||||
startAt: activity.startAt ?? null,
|
||||
deputyPresent: Boolean(activity.deputyPresent),
|
||||
})),
|
||||
|
|
|
|||
|
|
@ -160,7 +160,7 @@ describe('campaign migration existing-schema reconciliation', () => {
|
|||
// NOTE: the pre-remodel "empty path" rollback test (municipalityRemodel.down →
|
||||
// consolidated.down → original.down) is intentionally absent: the municipality
|
||||
// remodel down is currently broken — `DROP TABLE "municipality" CASCADE` already
|
||||
// cascades the supporter/action_plan/locked-documents FKs that the script
|
||||
// cascades the supporter/activity/locked-documents FKs that the script
|
||||
// then tries to DROP CONSTRAINT explicitly, so the rollback always aborts.
|
||||
// Restore that coverage once the migration down uses IF EXISTS (or drops
|
||||
// constraints before the cascaded tables).
|
||||
|
|
|
|||
|
|
@ -192,7 +192,7 @@ describe('ESLint conventions', () => {
|
|||
`'use server'\n\nexport type Result = { ok: true }\nexport const save = async () => ({ ok: true as const })\nexport async function load() { return 1 }\n`,
|
||||
)
|
||||
await expectValid(
|
||||
'src/app/(campaign)/campanha/(app)/pracas/municipalityStaffEditMessages.ts',
|
||||
'src/app/(campaign)/campanha/(app)/municipios/municipalityStaffEditMessages.ts',
|
||||
`export const municipalityStaffEditSafeMessages = ['x'] as const\n`,
|
||||
)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ const canonicalEvidenceRows = (assignments: OfficialEvidence['assignments']): st
|
|||
describe('Municipality zone neighborhood catalog (Salvador only)', () => {
|
||||
const zoneMunicipalities = municipalityCatalog.filter((entry) => entry.kind === 'zona')
|
||||
|
||||
it('covers every zona Município with a non-empty neighborhood list', () => {
|
||||
it('covers every zone municipality with a non-empty neighborhood list', () => {
|
||||
expect(municipalityZoneNeighborhoods).toHaveLength(zoneMunicipalities.length)
|
||||
expect(new Set(municipalityZoneNeighborhoods.map((entry) => entry.municipalitySlug)).size).toBe(
|
||||
zoneMunicipalities.length,
|
||||
|
|
|
|||
|
|
@ -80,43 +80,39 @@ describe('banned campaign terminology', () => {
|
|||
},
|
||||
{
|
||||
id: 'Plano de Ação',
|
||||
pattern:
|
||||
/actionPlan|ActionPlan|ACTION_PLAN|action_plan|action-plan|planos? de aç[aã]o|\/campanha\/planos/i,
|
||||
// Accent-tolerant (agents routinely type "plano de acao") and space-tolerant,
|
||||
// so English prose ("action plan") cannot slip past the identifier forms.
|
||||
pattern: /action[\s_-]?plan|planos? de a[çc][aã]o|\/campanha\/planos/i,
|
||||
remedy: 'use "Atividade" / activity (or allowlist frozen history)',
|
||||
allowlist: [
|
||||
// Emits the frozen remodel migration, whose SQL predates the rename.
|
||||
'scripts/generate-remodel-municipalities-migration.mjs',
|
||||
// Asserts against the frozen migration chain by its historical names.
|
||||
'tests/int/campaignMigrationReconciliation.int.spec.ts',
|
||||
],
|
||||
},
|
||||
] as const
|
||||
|
||||
const searchRoots = ['src', 'tests', 'scripts'] as const
|
||||
|
||||
it.each(bannedTerms.map((term) => [term.id, term] as const))(
|
||||
'keeps %s out of src, tests and scripts',
|
||||
(_id, term) => {
|
||||
// This spec quotes the banned literals itself.
|
||||
const allowlist = new Set<string>([...term.allowlist, repoPath(import.meta.filename)])
|
||||
const offenders: string[] = []
|
||||
it.each(bannedTerms)('keeps $id out of src, tests and scripts', (term) => {
|
||||
// This spec quotes the banned literals itself.
|
||||
const allowlist = new Set<string>([...term.allowlist, repoPath(import.meta.filename)])
|
||||
const offenders: string[] = []
|
||||
|
||||
for (const root of searchRoots) {
|
||||
for (const file of walkSourceFiles(resolve(repoRoot, root), ['.ts', '.tsx', '.mjs'])) {
|
||||
const path = repoPath(file)
|
||||
if (path.startsWith('src/migrations/')) continue
|
||||
if (allowlist.has(path)) continue
|
||||
for (const root of searchRoots) {
|
||||
for (const file of walkSourceFiles(resolve(repoRoot, root), ['.ts', '.tsx', '.mjs'])) {
|
||||
const path = repoPath(file)
|
||||
if (path.startsWith('src/migrations/')) continue
|
||||
if (allowlist.has(path)) continue
|
||||
|
||||
const lines = readFileSync(file, 'utf8').split('\n')
|
||||
for (const [index, line] of lines.entries()) {
|
||||
if (term.pattern.test(line)) offenders.push(`${path}:${index + 1}`)
|
||||
}
|
||||
const lines = readFileSync(file, 'utf8').split('\n')
|
||||
for (const [index, line] of lines.entries()) {
|
||||
if (term.pattern.test(line)) offenders.push(`${path}:${index + 1}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
expect(offenders, term.remedy).toEqual([])
|
||||
},
|
||||
)
|
||||
expect(offenders, term.remedy).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('eslint legacy ignore lists', () => {
|
||||
|
|
|
|||
|
|
@ -63,8 +63,8 @@ describe('recentVisits storage', () => {
|
|||
for (let index = 0; index < MAX_ENTRIES + 2; index += 1) {
|
||||
recordRecentVisit(
|
||||
sampleEntry({
|
||||
href: `/campanha/municipios/praca-${index}`,
|
||||
label: `Município `,
|
||||
href: `/campanha/municipios/municipio-${index}`,
|
||||
label: `Município ${index}`,
|
||||
visitedAt: index,
|
||||
}),
|
||||
)
|
||||
|
|
@ -72,8 +72,9 @@ describe('recentVisits storage', () => {
|
|||
|
||||
const visits = listRecentVisits()
|
||||
expect(visits).toHaveLength(MAX_ENTRIES)
|
||||
expect(visits[0]?.href).toBe(`/campanha/municipios/praca-${MAX_ENTRIES + 1}`)
|
||||
expect(visits.at(-1)?.href).toBe('/campanha/municipios/praca-2')
|
||||
expect(visits[0]?.href).toBe(`/campanha/municipios/municipio-${MAX_ENTRIES + 1}`)
|
||||
expect(visits[0]?.label).toBe(`Município ${MAX_ENTRIES + 1}`)
|
||||
expect(visits.at(-1)?.href).toBe('/campanha/municipios/municipio-2')
|
||||
})
|
||||
|
||||
it('clears stored visits', () => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue