Fix /campanha/liderancas 500: RSC-safe phone display
CampaignCopyableCell is a Client Component; passing formatDisplay as a function from the leaderships RSC page broke serialization in production. Use a preformatted displayValue string at the call sites instead.
This commit is contained in:
parent
3d2cd4e163
commit
013f167d47
4 changed files with 14 additions and 8 deletions
|
|
@ -9,6 +9,8 @@ Responsável: —
|
|||
|
||||
**Nota de revisão (2026-07-26):** entregue como especificado — `CampaignCopyableCell` nasceu compartilhado (`shared/`) já no 2º call site (`AdvisorsTable.tsx` migrado no mesmo PR), `whatsAppHrefForPhone` promovido a `lib/phone.ts`, `LeadershipRowViewModel.email` populado sem query nova, e `loadLeadershipDetail` deduplicado (reusa `row.email` de `toLeadershipRows` em vez de reparsear `doc.contact`). Ordem de colunas final: Nome → E-mail → Celular → Status → Municípios → Organizações → Acesso ao app → Ações (WhatsApp), exatamente como travado abaixo. Gate completo (tsc/lint/format/knip/cycles/test/build) verde; `pnpm exec knip` mantém o erro pré-existente ao carregar `payload.config.ts` (P3, não relacionado); `pnpm test:e2e` reproduziu apenas o flake pré-existente já documentado em `campaignMunicipalities.e2e.spec.ts` (checkbox de consentimento do `LeaderContactsPanel` antes da hidratação) — nenhuma falha nas rotas tocadas por este item.
|
||||
|
||||
**Hotfix produção (2026-07-26 noite):** `/campanha/liderancas` 500 em prod — RSC não pode passar `formatDisplay` (função) para o Client Component `CampaignCopyableCell`. API trocada para `displayValue?: string` (formatação no caller); Vercel runtime error: "Functions cannot be passed directly to Client Components". Assessores (`AdvisorsTable`, já `'use client'`) não quebrava, mas foi alinhado.
|
||||
|
||||
## Design (Impeccable)
|
||||
|
||||
Âncoras: `PRODUCT.md` (princípios 2 clareza sob pressão, 8 Feel the action; anti spreadsheet) / `DESIGN.md` (register `product`; Field Desk) · tema `data-theme='campaign'` · precedente de interação [`AdvisorsTable.tsx`](../../src/components/campaign/advisor/AdvisorsTable.tsx) (B19 ✓) · shells `CampaignTable` / `CampaignPageShell`.
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ const leadershipColumns = (
|
|||
<CampaignCopyableCell
|
||||
value={row.phone}
|
||||
label="Celular"
|
||||
formatDisplay={formatBrazilianPhoneInput}
|
||||
displayValue={row.phone ? formatBrazilianPhoneInput(row.phone) : undefined}
|
||||
className="tabular-nums"
|
||||
/>
|
||||
),
|
||||
|
|
|
|||
|
|
@ -357,7 +357,9 @@ export const AdvisorsTable = ({
|
|||
<CampaignCopyableCell
|
||||
value={row.phone}
|
||||
label="Celular"
|
||||
formatDisplay={formatBrazilianPhoneInput}
|
||||
displayValue={
|
||||
row.phone ? formatBrazilianPhoneInput(row.phone) : undefined
|
||||
}
|
||||
className="tabular-nums"
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -22,8 +22,12 @@ type CampaignCopyableCellProps = {
|
|||
value: string | null
|
||||
/** Used in the toast ("`label` copiado.") and the button's `aria-label`. */
|
||||
label: string
|
||||
/** Transforms `value` for display only — the clipboard still gets the raw value (e.g. phone masking). */
|
||||
formatDisplay?: (value: string) => string
|
||||
/**
|
||||
* Preformatted label (e.g. masked phone). Clipboard still gets raw `value`.
|
||||
* String, not a formatter — Client Component; RSC cannot pass functions
|
||||
* (broke `/campanha/liderancas` after B28).
|
||||
*/
|
||||
displayValue?: string
|
||||
emptyDisplay?: string
|
||||
className?: string
|
||||
}
|
||||
|
|
@ -37,7 +41,7 @@ type CampaignCopyableCellProps = {
|
|||
export const CampaignCopyableCell = ({
|
||||
value,
|
||||
label,
|
||||
formatDisplay,
|
||||
displayValue,
|
||||
emptyDisplay = '—',
|
||||
className,
|
||||
}: CampaignCopyableCellProps) => {
|
||||
|
|
@ -49,8 +53,6 @@ export const CampaignCopyableCell = ({
|
|||
)
|
||||
}
|
||||
|
||||
const displayValue = formatDisplay ? formatDisplay(value) : value
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -58,7 +60,7 @@ export const CampaignCopyableCell = ({
|
|||
aria-label={`Copiar ${label.toLowerCase()}`}
|
||||
onClick={() => void copyText(label, value)}
|
||||
>
|
||||
<span className="truncate">{displayValue}</span>
|
||||
<span className="truncate">{displayValue ?? value}</span>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue