From 2e8f6f6544a6da4dc7cd024699be1028725cf41d Mon Sep 17 00:00:00 2001 From: Francisco Solla Date: Mon, 24 Aug 2026 17:47:57 -0300 Subject: [PATCH] =?UTF-8?q?review(B155+):=20simplify=20=E2=80=94=20useCall?= =?UTF-8?q?back=20nos=20shims,=20constantes=20no=20padr=C3=A3o=20dos=20irm?= =?UTF-8?q?=C3=A3os,=20copy=20B36=20preservada=20no=20caminho=20de=20erro,?= =?UTF-8?q?=20impl=20plan=20alinhado?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - useMemo(() => fn, []) → useCallback (idiom do codebase; shim sem props no de municípios, saveErrorMessage no de state-deputies). - ENDPOINT/GENERIC_ERROR → STATE_DEPUTIES_ENDPOINT / MUNICIPALITIES_ENDPOINT + SAVE_ERROR_MESSAGE (espelho de MunicipalityListAdvisorsControl). - Prop opcional saveErrorMessage no wrapper de state-deputies: o call site B36 (fromStateDeputy) volta a falar em 'lideranças' no caminho de erro, como a form action removida. - impl plan: LeadershipListStateDeputiesResponse (não ToggleResponse), useCallback, saveErrorMessage. --- .../escala-dry-pos-b155-padrao-rota-impl.md | 6 +- .../campanha/(app)/dobradinhas/page.tsx | 1 + .../LeadershipMunicipalitiesColumnCell.tsx | 49 ++++++++------- .../LeadershipStateDeputiesColumnCell.tsx | 61 ++++++++++--------- 4 files changed, 61 insertions(+), 56 deletions(-) diff --git a/docs/plans/escala-dry-pos-b155-padrao-rota-impl.md b/docs/plans/escala-dry-pos-b155-padrao-rota-impl.md index e3d82d62..044a26ca 100644 --- a/docs/plans/escala-dry-pos-b155-padrao-rota-impl.md +++ b/docs/plans/escala-dry-pos-b155-padrao-rota-impl.md @@ -46,10 +46,10 @@ flowchart LR ### Componentes / mudanças -- **`POST /campanha/liderancas/state-deputies/route.ts`** (novo, `src/app/(campaign)/campanha/(app)/liderancas/state-deputies/route.ts`): `export const POST = campaignJsonMutationRoute(` (literal exigido pelo sweep) com `bodySchema = leadershipStateDeputyMembershipSchema` (reuso direto de `src/lib/schemas/leadership.ts:148`), `safeMessages: [...leadershipStaffEditSafeMessages, LEADERSHIP_STATE_DEPUTIES_CAP_MESSAGE]`, `genericMessage: 'Não foi possível atualizar as dobradinhas. Tente novamente.'` (cópia da form action). Handler: `await setLeadershipStateDeputyMembership(body)` → `NextResponse.json({ status: 'success', message: 'Dobradinhas atualizadas.' })`. `types.ts` colado: `LeadershipStateDeputiesToggleResponse = { status: 'success'; message: string } | { status: 'error'; message: string }` (padrão `liderancas/support-status/types.ts`). `export const dynamic = 'force-dynamic'` (padrão das irmãs). +- **`POST /campanha/liderancas/state-deputies/route.ts`** (novo, `src/app/(campaign)/campanha/(app)/liderancas/state-deputies/route.ts`): `export const POST = campaignJsonMutationRoute(` (literal exigido pelo sweep) com `bodySchema = leadershipStateDeputyMembershipSchema` (reuso direto de `src/lib/schemas/leadership.ts:148`), `safeMessages: [...leadershipStaffEditSafeMessages, LEADERSHIP_STATE_DEPUTIES_CAP_MESSAGE]`, `genericMessage: 'Não foi possível atualizar as dobradinhas. Tente novamente.'` (cópia da form action). Handler: `await setLeadershipStateDeputyMembership(body)` → `NextResponse.json({ status: 'success', message: 'Dobradinhas atualizadas.' })`. `types.ts` colado: `LeadershipListStateDeputiesResponse = { status: 'success'; message: string } | { status: 'error'; message: string }` (espelho de `liderancas/support-status/types.ts`). `export const dynamic = 'force-dynamic'` (padrão das irmãs). - **`POST /campanha/liderancas/municipalities/route.ts`** (novo, `.../liderancas/municipalities/route.ts`): `bodySchema = leadershipMunicipalitiesMembershipSchema` (o cap já é o ceiling do schema; o floor de 1 é a action record contra o array resultante), `safeMessages: [...leadershipStaffEditSafeMessages, LEADERSHIP_MUNICIPALITY_FLOOR_MESSAGE, LEADERSHIP_MUNICIPALITY_CAP_MESSAGE, LEADERSHIP_MUNICIPALITY_SCOPE_MESSAGE]`, `genericMessage: 'Não foi possível atualizar os municípios. Tente novamente.'`. Handler: `await setLeadershipMunicipalitiesMembership(body)` → `{ status: 'success', message: 'Municípios atualizados.' }`. `types.ts` + `force-dynamic` iguais. **Nota de body:** a shared cell envia `ownerId` no FormData; o shim converte para `leadershipId` no JSON (a action record exige `leadershipId`). -- **`LeadershipStateDeputiesColumnCell`** (novo, `src/components/campaign/leadership/LeadershipStateDeputiesColumnCell.tsx`, `'use client'`): wrapper que recebe as props da página, monta com `useCallback` o shim `(state, formData) => Promise` — lê `leadershipId`/`stateDeputyId`/`assigned` do FormData (parse idêntico ao de `liderancas/formActions.ts:92-95`, hoje `requiredRelationshipFormValue`/`requiredFormBoolean`) e chama `postCampaignJson('/campanha/liderancas/state-deputies', { leadershipId, stateDeputyId, assigned })`; `ok` → `{ status: 'success', message: payload.message }`; senão → `{ message: payload.message }` (sem `status` — o caminho de erro da shared cell reverter o otimismo). Renderiza `LeadershipStateDeputyRelationCell` com `membershipAction={shim}` e o restante das props repassadas. -- **`LeadershipMunicipalitiesColumnCell`** (novo, `src/components/campaign/leadership/LeadershipMunicipalitiesColumnCell.tsx`, `'use client'`): idem para `MunicipalityPortfolioCell` com `commitAction={shim}`; o shim lê `ownerId` (campo que a cell envia — `MunicipalityPortfolioCell.tsx:190`), `municipalityIds` repetidos e `assigned`, e chama `postCampaignJson('/campanha/liderancas/municipalities', { leadershipId: ownerId, municipalityIds, assigned })`. +- **`LeadershipStateDeputiesColumnCell`** (novo, `src/components/campaign/leadership/LeadershipStateDeputiesColumnCell.tsx`, `'use client'`): wrapper que recebe as props da página, monta com `useCallback` o shim `(state, formData) => Promise` — lê `leadershipId`/`stateDeputyId`/`assigned` do FormData (parse idêntico ao de `liderancas/formActions.ts:92-95`, hoje `requiredRelationshipFormValue`/`requiredFormBoolean`) e chama `postCampaignJson('/campanha/liderancas/state-deputies', { leadershipId, stateDeputyId, assigned })`; `ok` → `{ status: 'success', message: payload.message }`; senão → `{ message: payload.message }` (sem `status` — o caminho de erro da shared cell reverter o otimismo). Renderiza `LeadershipStateDeputyRelationCell` com `membershipAction={shim}` e o restante das props repassadas; prop opcional `saveErrorMessage` preserva a copy do call site B36 (`fromStateDeputy` fala em "lideranças", não "dobradinhas") no caminho de erro. +- **`LeadershipMunicipalitiesColumnCell`** (novo, `src/components/campaign/leadership/LeadershipMunicipalitiesColumnCell.tsx`, `'use client'`): idem para `MunicipalityPortfolioCell` com `commitAction={shim}` (useCallback); o shim lê `ownerId` (campo que a cell envia — `MunicipalityPortfolioCell.tsx:190`), `municipalityIds` repetidos e `assigned`, e chama `postCampaignJson('/campanha/liderancas/municipalities', { leadershipId: ownerId, municipalityIds, assigned })`. - **`liderancas/page.tsx`** (editado): troca `MunicipalityPortfolioCell` (`:232-243`) por `LeadershipMunicipalitiesColumnCell` (remove a prop `commitAction`) e `LeadershipStateDeputyRelationCell` (`:257-270`) por `LeadershipStateDeputiesColumnCell` (remove `membershipAction`); remove os imports de `./formActions` (`:83-84`). O restante (filter head, drawerTitle, min/max, options) permanece. - **`liderancas/formActions.ts`** (editado): remove `setLeadershipStateDeputyMembershipFormAction` (`:86-101`) e `setLeadershipMunicipalitiesFormAction` (`:109-124`). `updateLeadershipContactFormAction` (B153, `:42-83`) PERMANECE — fora do lote (ver decisão de espelhos). Limpar imports órfãos (`requiredFormBoolean`, `repeatedRelationshipFormValues`, `LEADERSHIP_STATE_DEPUTIES_CAP_MESSAGE`, floor/cap/scope) e reenxugar o `safeMessages` local para o de B153. - **`tests/e2e/setup.e2e.spec.ts`** (editado): adicionar `'/campanha/liderancas/state-deputies'` e `'/campanha/liderancas/municipalities'` ao prewarm de POSTs (`:73-93`). diff --git a/src/app/(campaign)/campanha/(app)/dobradinhas/page.tsx b/src/app/(campaign)/campanha/(app)/dobradinhas/page.tsx index 44210b2c..a2022c94 100644 --- a/src/app/(campaign)/campanha/(app)/dobradinhas/page.tsx +++ b/src/app/(campaign)/campanha/(app)/dobradinhas/page.tsx @@ -260,6 +260,7 @@ const stateDeputyColumns = ( href: `/campanha/liderancas/${leadership.id}`, }))} options={leadershipOptions} + saveErrorMessage="Não foi possível atualizar as lideranças. Tente novamente." /> ), }, diff --git a/src/components/campaign/leadership/LeadershipMunicipalitiesColumnCell.tsx b/src/components/campaign/leadership/LeadershipMunicipalitiesColumnCell.tsx index 118c5acf..ff8f1279 100644 --- a/src/components/campaign/leadership/LeadershipMunicipalitiesColumnCell.tsx +++ b/src/components/campaign/leadership/LeadershipMunicipalitiesColumnCell.tsx @@ -1,14 +1,14 @@ 'use client' -import { useMemo, type ComponentProps } from 'react' +import { useCallback, type ComponentProps } from 'react' import type { LeadershipListMunicipalitiesResponse } from '@/app/(campaign)/campanha/(app)/liderancas/municipalities/types' import { MunicipalityPortfolioCell } from '@/components/campaign/shared/MunicipalityPortfolioCell' import { postCampaignJson } from '@/lib/campaignJsonRequest' import type { CampaignFormActionState } from '@/utilities/campaignFormActionError' -const ENDPOINT = '/campanha/liderancas/municipalities' -const GENERIC_ERROR = 'Não foi possível atualizar os municípios. Tente novamente.' +const MUNICIPALITIES_ENDPOINT = '/campanha/liderancas/municipalities' +const SAVE_ERROR_MESSAGE = 'Não foi possível atualizar os municípios. Tente novamente.' type Props = Omit, 'commitAction'> @@ -18,32 +18,31 @@ type Props = Omit, 'commitActio * — the cell sends `ownerId`, which is the leadership id on this call site. */ export const LeadershipMunicipalitiesColumnCell = (props: Props) => { - const commitAction = useMemo( - () => - async ( - _state: CampaignFormActionState, - formData: FormData, - ): Promise => { - const leadershipId = Number(formData.get('ownerId')) - const assigned = formData.get('assigned') === 'true' - const municipalityIds = formData.getAll('municipalityIds').map(Number) + const commitAction = useCallback( + async ( + _state: CampaignFormActionState, + formData: FormData, + ): Promise => { + const leadershipId = Number(formData.get('ownerId')) + const assigned = formData.get('assigned') === 'true' + const municipalityIds = formData.getAll('municipalityIds').map(Number) - try { - const { ok, payload } = await postCampaignJson( - ENDPOINT, - { leadershipId, municipalityIds, assigned }, - ) + try { + const { ok, payload } = await postCampaignJson( + MUNICIPALITIES_ENDPOINT, + { leadershipId, municipalityIds, assigned }, + ) - if (!ok || payload.status !== 'success') { - return { - message: payload.status === 'error' ? payload.message : GENERIC_ERROR, - } + if (!ok || payload.status !== 'success') { + return { + message: payload.status === 'error' ? payload.message : SAVE_ERROR_MESSAGE, } - return { status: 'success' as const, message: payload.message } - } catch { - return { message: GENERIC_ERROR } } - }, + return { status: 'success' as const, message: payload.message } + } catch { + return { message: SAVE_ERROR_MESSAGE } + } + }, [], ) diff --git a/src/components/campaign/leadership/LeadershipStateDeputiesColumnCell.tsx b/src/components/campaign/leadership/LeadershipStateDeputiesColumnCell.tsx index 7efce9da..ed88489f 100644 --- a/src/components/campaign/leadership/LeadershipStateDeputiesColumnCell.tsx +++ b/src/components/campaign/leadership/LeadershipStateDeputiesColumnCell.tsx @@ -1,16 +1,19 @@ 'use client' -import { useMemo, type ComponentProps } from 'react' +import { useCallback, type ComponentProps } from 'react' import type { LeadershipListStateDeputiesResponse } from '@/app/(campaign)/campanha/(app)/liderancas/state-deputies/types' import { LeadershipStateDeputyRelationCell } from '@/components/campaign/shared/LeadershipStateDeputyRelationCell' import { postCampaignJson } from '@/lib/campaignJsonRequest' import type { CampaignFormActionState } from '@/utilities/campaignFormActionError' -const ENDPOINT = '/campanha/liderancas/state-deputies' -const GENERIC_ERROR = 'Não foi possível atualizar as dobradinhas. Tente novamente.' +const STATE_DEPUTIES_ENDPOINT = '/campanha/liderancas/state-deputies' +const SAVE_ERROR_MESSAGE = 'Não foi possível atualizar as dobradinhas. Tente novamente.' -type Props = Omit, 'membershipAction'> +type Props = Omit, 'membershipAction'> & { + /** B36 — the `fromStateDeputy` side speaks of "lideranças", not "dobradinhas". */ + saveErrorMessage?: string +} /** * Thin adapter (B155+): wraps `LeadershipStateDeputyRelationCell` with a @@ -18,34 +21,36 @@ type Props = Omit, 'mem * `POST /campanha/liderancas/state-deputies` — same `(state, formData)` * contract the cell already calls, transport swapped from server action. */ -export const LeadershipStateDeputiesColumnCell = (props: Props) => { - const membershipAction = useMemo( - () => - async ( - _state: CampaignFormActionState, - formData: FormData, - ): Promise => { - const leadershipId = Number(formData.get('leadershipId')) - const stateDeputyId = Number(formData.get('stateDeputyId')) - const assigned = formData.get('assigned') === 'true' +export const LeadershipStateDeputiesColumnCell = ({ saveErrorMessage, ...props }: Props) => { + const membershipAction = useCallback( + async ( + _state: CampaignFormActionState, + formData: FormData, + ): Promise => { + const leadershipId = Number(formData.get('leadershipId')) + const stateDeputyId = Number(formData.get('stateDeputyId')) + const assigned = formData.get('assigned') === 'true' - try { - const { ok, payload } = await postCampaignJson( - ENDPOINT, - { leadershipId, stateDeputyId, assigned }, - ) + try { + const { ok, payload } = await postCampaignJson( + STATE_DEPUTIES_ENDPOINT, + { leadershipId, stateDeputyId, assigned }, + ) - if (!ok || payload.status !== 'success') { - return { - message: payload.status === 'error' ? payload.message : GENERIC_ERROR, - } + if (!ok || payload.status !== 'success') { + return { + message: + payload.status === 'error' + ? payload.message + : (saveErrorMessage ?? SAVE_ERROR_MESSAGE), } - return { status: 'success' as const, message: payload.message } - } catch { - return { message: GENERIC_ERROR } } - }, - [], + return { status: 'success' as const, message: payload.message } + } catch { + return { message: saveErrorMessage ?? SAVE_ERROR_MESSAGE } + } + }, + [saveErrorMessage], ) return