review(B155+): simplify — useCallback nos shims, constantes no padrão dos irmãos, copy B36 preservada no caminho de erro, impl plan alinhado

- 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.
This commit is contained in:
Francisco Solla 2026-08-24 17:47:57 -03:00
parent e4fc5a9d5f
commit 2e8f6f6544
4 changed files with 61 additions and 56 deletions

View file

@ -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<CampaignFormActionState>` — 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<CampaignFormActionState>` — 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`).

View file

@ -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."
/>
),
},

View file

@ -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<ComponentProps<typeof MunicipalityPortfolioCell>, 'commitAction'>
@ -18,32 +18,31 @@ type Props = Omit<ComponentProps<typeof MunicipalityPortfolioCell>, '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<CampaignFormActionState> => {
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<CampaignFormActionState> => {
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<LeadershipListMunicipalitiesResponse>(
ENDPOINT,
{ leadershipId, municipalityIds, assigned },
)
try {
const { ok, payload } = await postCampaignJson<LeadershipListMunicipalitiesResponse>(
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 }
}
},
[],
)

View file

@ -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<ComponentProps<typeof LeadershipStateDeputyRelationCell>, 'membershipAction'>
type Props = Omit<ComponentProps<typeof LeadershipStateDeputyRelationCell>, '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<ComponentProps<typeof LeadershipStateDeputyRelationCell>, '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<CampaignFormActionState> => {
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<CampaignFormActionState> => {
const leadershipId = Number(formData.get('leadershipId'))
const stateDeputyId = Number(formData.get('stateDeputyId'))
const assigned = formData.get('assigned') === 'true'
try {
const { ok, payload } = await postCampaignJson<LeadershipListStateDeputiesResponse>(
ENDPOINT,
{ leadershipId, stateDeputyId, assigned },
)
try {
const { ok, payload } = await postCampaignJson<LeadershipListStateDeputiesResponse>(
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 <LeadershipStateDeputyRelationCell {...props} membershipAction={membershipAction} />