B145 — move entity identity to shell header on detail pages
- Detail routes return null from campaignPageChrome catalog (override per page) - SetCampaignPageChrome + generateMetadata on 8 entity detail routes - Remove body hero (h1, Voltar links, duplicate identity chips) - Keep operational badges/chips in page body - Update zone map e2e to assert shell chrome title slot Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
This commit is contained in:
parent
deefe566b0
commit
5696aef9ed
12 changed files with 347 additions and 207 deletions
77
docs/plans/detalhe-entidade-titulo-no-header-impl.md
Normal file
77
docs/plans/detalhe-entidade-titulo-no-header-impl.md
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
# Impl: Detalhe de entidade — título no header (sem hero espaçoso)
|
||||
|
||||
Status: aprovado
|
||||
Atualizado em: 2026-08-02
|
||||
Issue: #315
|
||||
Intenção: docs/plans/detalhe-entidade-titulo-no-header.md
|
||||
Appetite restante: ~0,5 dia eng (8 rotas de detalhe + catálogo + e2e)
|
||||
|
||||
## Leitura da intenção
|
||||
|
||||
- **Outcome:** Em cada detalhe de entidade em `/campanha`, a identidade do registro (título + subtítulo) vive no header da shell; o corpo começa direto em abas/seções/chips operacionais, sem hero nem “Voltar para…”.
|
||||
- **O que NÃO negociar:** listas mantêm header de seção (B123/B133); edit routes inalteradas; chips operacionais no corpo; leader lockdown; aba do browser alinhada à entidade.
|
||||
- **O que reavaliar:** hipótese de tocar só `campaignPageChrome` — insuficiente: títulos são dinâmicos por registro; precedente de `SetCampaignPageChrome` + `generateMetadata` nas rotas `/editar` é o dono.
|
||||
|
||||
## Abordagem recomendada
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
RSC[Detail page RSC] --> MD[generateMetadata]
|
||||
RSC --> SC[SetCampaignPageChrome]
|
||||
SC --> Shell[CampaignPageChromeDisplay]
|
||||
RSC --> Body[Corpo sem hero]
|
||||
```
|
||||
|
||||
**Opções consideradas:**
|
||||
- A) Estender `resolveCampaignPageChrome` com dados de entidade (impossível — catálogo é estático/client-safe)
|
||||
- B) `SetCampaignPageChrome` + `generateMetadata` por rota (precedente edit routes)
|
||||
- C) Novo provider server-side de chrome
|
||||
|
||||
**Recomendação:** B — reusa `SetCampaignPageChrome` e `campaignPageMetadata` já usados em `/editar` e `MunicipalityListPageChrome`; path rules de detalhe passam a `null` (como edit) para não flashar título de seção antes do override.
|
||||
|
||||
**Rejeitadas:** C (twin desnecessário); A (dados dinâmicos não cabem no catálogo).
|
||||
|
||||
### Componentes / mudanças
|
||||
|
||||
- **`campaignPageChrome.ts`:** regras de detalhe retornam `null`; teste unitário atualizado.
|
||||
- **8 rotas de detalhe** (`municipios/[slug]`, `liderancas/[id]`, `atividades/[slug]`, `demandas/[slug]`, `dobradinhas/[slug]`, `organizacoes/[slug]`, `apoiadores/[id]`, `assessores/[id]`): `generateMetadata` + `SetCampaignPageChrome`; remover hero (`h1`, “Voltar…”, chips de identidade duplicados, linha Assessoria no município).
|
||||
- **`campaignZoneMap.e2e.spec.ts`:** assert no chrome da shell (`data-slot=campaign-page-chrome-title`) em vez de `h1` no corpo.
|
||||
- **Migration:** sem migration
|
||||
- **Access / Consent:** inalterado
|
||||
|
||||
### Mapeamento chrome por rota
|
||||
|
||||
| Rota | title | subtitle |
|
||||
| ---- | ----- | -------- |
|
||||
| municipios/[slug] | view.name | formatMunicipalityGeographyLabel(view) |
|
||||
| liderancas/[id] | Liderança | leadership.name |
|
||||
| atividades/[slug] | view.title | view.locationLabel ou municipality.name |
|
||||
| demandas/[slug] | demand.title | `${kind} · ${municipalityName}` |
|
||||
| dobradinhas/[slug] | stateDeputy.name | party (opcional) |
|
||||
| organizacoes/[slug] | organization.name | organizationKindLabels[kind] |
|
||||
| apoiadores/[id] | Apoiador | supporter.name |
|
||||
| assessores/[id] | Assessor | advisor.name |
|
||||
|
||||
## Fases verificáveis
|
||||
|
||||
1. **Catálogo + testes** — path rules `null` para detalhes; unit spec
|
||||
2. **UI — 8 páginas** — chrome override + remoção de hero; metadata dinâmica
|
||||
3. **Gates** — `pnpm gate:fast`; push via `pnpm push`
|
||||
|
||||
## Rabbit holes / Não escopo (engenharia)
|
||||
|
||||
- Novo bloco UI para Assessoria no município
|
||||
- Wizard, giros, perfil, contatos, rotas `/editar`, listas
|
||||
- Refatorar `CampaignPageChromeText` (layout já suporta subtitle)
|
||||
|
||||
## Riscos e mitigação
|
||||
|
||||
- **Flash de título de seção:** path rule `null` + `useLayoutEffect` do override (mesmo que edit routes)
|
||||
- **E2E que asserta `h1`:** atualizar para chrome slot
|
||||
- **Atividade sem locationLabel:** fallback para `municipality?.name`
|
||||
|
||||
## Aceite de engenharia
|
||||
|
||||
- [x] Aceite de produto da intenção ainda coberto
|
||||
- [x] Invariantes AGENTS/engineering-standards
|
||||
- [x] Testes de domínio previstos (unit chrome + e2e zone map)
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import config from '@payload-config'
|
||||
import { ArrowLeftIcon, ShieldCheckIcon } from 'lucide-react'
|
||||
import { ShieldCheckIcon } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { notFound, redirect } from 'next/navigation'
|
||||
import { getPayload } from 'payload'
|
||||
|
|
@ -9,10 +9,10 @@ import { ConsentText } from '@/components/campaign/shared/ConsentText'
|
|||
import { RemoveSupporterDataButton } from '@/components/campaign/supporter/RemoveSupporterDataButton'
|
||||
import { SupporterShareKit } from '@/components/campaign/supporter/SupporterShareKit'
|
||||
import { VoteIntentionControl } from '@/components/campaign/supporter/VoteIntentionControl'
|
||||
import { SetCampaignPageChrome } from '@/components/campaign/shell/CampaignPageChromeContext'
|
||||
import { Badge } from '@/components/ui/Badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { campaignPageMetadataFromCatalog } from '@/lib/campaignPageChrome'
|
||||
import { campaignPageMetadata } from '@/lib/campaignPageChrome'
|
||||
import { requireCampaignPageActor } from '@/utilities/campaignPageActor'
|
||||
import {
|
||||
loadSupporterDetailConsentData,
|
||||
|
|
@ -25,7 +25,21 @@ import {
|
|||
} from '@/utilities/supporter/supporterUi'
|
||||
import { parseSupporterId } from '@/utilities/supporter/supporterViewModels'
|
||||
|
||||
export const metadata = campaignPageMetadataFromCatalog('apoiadores')
|
||||
export async function generateMetadata({ params }: SupporterDetailPageProps) {
|
||||
const { id: rawId } = await params
|
||||
const supporterId = parseSupporterId(rawId)
|
||||
if (supporterId === null) return campaignPageMetadata({ title: 'Apoiador' })
|
||||
|
||||
const [user, payload] = await Promise.all([requireCampaignPageActor(), getPayload({ config })])
|
||||
if (!canAccessSupporterArea(user.role)) return campaignPageMetadata({ title: 'Apoiador' })
|
||||
|
||||
try {
|
||||
const supporter = await loadSupporterDetailPageData(payload, user, supporterId)
|
||||
return campaignPageMetadata({ title: 'Apoiador', subtitle: supporter.name })
|
||||
} catch {
|
||||
return campaignPageMetadata({ title: 'Apoiador' })
|
||||
}
|
||||
}
|
||||
|
||||
type SupporterDetailPageProps = {
|
||||
params: Promise<{ id: string }>
|
||||
|
|
@ -65,19 +79,11 @@ export default async function SupporterDetailPage({ params }: SupporterDetailPag
|
|||
|
||||
return (
|
||||
<div className="mx-auto flex w-full max-w-3xl flex-col gap-6">
|
||||
<header className="flex flex-col gap-2">
|
||||
<Button asChild variant="ghost" className="w-fit px-0">
|
||||
<Link href="/campanha/apoiadores">
|
||||
<ArrowLeftIcon data-icon="inline-start" aria-hidden="true" />
|
||||
Voltar para apoiadores
|
||||
</Link>
|
||||
</Button>
|
||||
<h1 className="text-2xl font-semibold tracking-tight">{supporter.name}</h1>
|
||||
<p className="text-muted-foreground">
|
||||
{supporter.city ?? 'Cidade não informada'}
|
||||
{supporter.territory ? ` · ${supporter.territory}` : ''}
|
||||
</p>
|
||||
</header>
|
||||
<SetCampaignPageChrome chrome={{ title: 'Apoiador', subtitle: supporter.name }} />
|
||||
<p className="text-muted-foreground">
|
||||
{supporter.city ?? 'Cidade não informada'}
|
||||
{supporter.territory ? ` · ${supporter.territory}` : ''}
|
||||
</p>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import config from '@payload-config'
|
||||
import { ArrowLeftIcon, MapPinIcon } from 'lucide-react'
|
||||
import { MapPinIcon } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { notFound } from 'next/navigation'
|
||||
import { getPayload } from 'payload'
|
||||
|
|
@ -7,16 +7,28 @@ import { getPayload } from 'payload'
|
|||
import { AdvisorDebouncedTextCell } from '@/components/campaign/advisor/AdvisorDebouncedTextCell'
|
||||
import { AdvisorPasswordResetButton } from '@/components/campaign/advisor/AdvisorPasswordResetButton'
|
||||
import { CampaignPageShell } from '@/components/campaign/shell/CampaignPageShell'
|
||||
import { SetCampaignPageChrome } from '@/components/campaign/shell/CampaignPageChromeContext'
|
||||
import { Badge } from '@/components/ui/Badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { campaignPageMetadataFromCatalog } from '@/lib/campaignPageChrome'
|
||||
import { campaignPageMetadata } from '@/lib/campaignPageChrome'
|
||||
import { formatBrazilianPhoneInput } from '@/lib/phone'
|
||||
import { isPlanilhaPlaceholderEmail } from '@/lib/schemas/advisor'
|
||||
import { loadAdvisorDetail } from '@/utilities/advisorData'
|
||||
import { requireCampaignPageActor } from '@/utilities/campaignPageActor'
|
||||
import { sendAdvisorPasswordResetFormAction, updateAdvisorProfileFormAction } from '../formActions'
|
||||
|
||||
export const metadata = campaignPageMetadataFromCatalog('assessores')
|
||||
export async function generateMetadata({ params }: AdvisorDetailPageProps) {
|
||||
const { id: rawId } = await params
|
||||
const advisorId = Number(rawId)
|
||||
if (!Number.isInteger(advisorId) || advisorId <= 0) {
|
||||
return campaignPageMetadata({ title: 'Assessor' })
|
||||
}
|
||||
|
||||
const payload = await getPayload({ config })
|
||||
const advisor = await loadAdvisorDetail(payload, advisorId)
|
||||
if (!advisor) return campaignPageMetadata({ title: 'Assessor' })
|
||||
|
||||
return campaignPageMetadata({ title: 'Assessor', subtitle: advisor.name })
|
||||
}
|
||||
|
||||
type AdvisorDetailPageProps = {
|
||||
params: Promise<{ id: string }>
|
||||
|
|
@ -39,19 +51,11 @@ export default async function AdvisorDetailPage({ params }: AdvisorDetailPagePro
|
|||
|
||||
return (
|
||||
<CampaignPageShell>
|
||||
<header className="flex flex-col gap-2">
|
||||
<Button asChild variant="ghost" className="min-h-11 self-start">
|
||||
<Link href="/campanha/assessores">
|
||||
<ArrowLeftIcon data-icon="inline-start" aria-hidden="true" />
|
||||
Voltar para assessores
|
||||
</Link>
|
||||
</Button>
|
||||
<h1 className="text-2xl font-semibold tracking-tight">{advisor.name}</h1>
|
||||
<p className="text-muted-foreground">
|
||||
E-mail e celular salvam sozinhos ao digitar. A carteira de municípios é editada no modo
|
||||
edição da lista.
|
||||
</p>
|
||||
</header>
|
||||
<SetCampaignPageChrome chrome={{ title: 'Assessor', subtitle: advisor.name }} />
|
||||
<p className="text-muted-foreground">
|
||||
E-mail e celular salvam sozinhos ao digitar. A carteira de municípios é editada no modo
|
||||
edição da lista.
|
||||
</p>
|
||||
|
||||
<section aria-labelledby="advisor-account-title" className="flex flex-col gap-2">
|
||||
<h2 id="advisor-account-title" className="text-base font-medium">
|
||||
|
|
|
|||
|
|
@ -14,10 +14,11 @@ import { ActivityTaskChecklist } from '@/components/campaign/activity/ActivityTa
|
|||
import { ActivityUpdateFeed } from '@/components/campaign/activity/ActivityUpdateFeed'
|
||||
import { ActivityUpdateForm } from '@/components/campaign/activity/ActivityUpdateForm'
|
||||
import { CampaignQuickActionContextBridge } from '@/components/campaign/shell/CampaignQuickActionContextBridge'
|
||||
import { SetCampaignPageChrome } from '@/components/campaign/shell/CampaignPageChromeContext'
|
||||
import { Badge } from '@/components/ui/Badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { campaignPageMetadataFromCatalog } from '@/lib/campaignPageChrome'
|
||||
import { campaignPageMetadata } from '@/lib/campaignPageChrome'
|
||||
import { formatBahiaDateTimeLabel } from '@/lib/campaignTime'
|
||||
import { activityKindLabels } from '@/lib/schemas/activity'
|
||||
import { getActivityDetailPageData } from '@/utilities/activityDetailPageData'
|
||||
|
|
@ -35,7 +36,27 @@ import { requireCampaignPageActor } from '@/utilities/campaignPageActor'
|
|||
|
||||
import { ActivityOverviewTab } from './ActivityOverviewTab'
|
||||
|
||||
export const metadata = campaignPageMetadataFromCatalog('atividades')
|
||||
export async function generateMetadata({ params }: ActivityDetailPageProps) {
|
||||
const { slug } = await params
|
||||
if (!slug) return campaignPageMetadata({ title: 'Atividade' })
|
||||
|
||||
const [user, payload] = await Promise.all([
|
||||
requireCampaignPageActor({ gate: 'staff' }),
|
||||
getPayload({ config }),
|
||||
])
|
||||
|
||||
try {
|
||||
const activeTab = 'overview' as const
|
||||
const context = await resolveAccessibleActivityContext(payload, user, slug, activeTab)
|
||||
const view = await getActivityDetailPageData(payload, user, context, activeTab, 1)
|
||||
const subtitle = view.locationLabel || view.municipality?.name
|
||||
return campaignPageMetadata(
|
||||
subtitle ? { title: view.title, subtitle } : { title: view.title },
|
||||
)
|
||||
} catch {
|
||||
return campaignPageMetadata({ title: 'Atividade' })
|
||||
}
|
||||
}
|
||||
|
||||
type ActivityDetailPageProps = {
|
||||
params: Promise<{ slug: string }>
|
||||
|
|
@ -73,30 +94,31 @@ export default async function ActivityDetailPage({
|
|||
|
||||
const isStaff = isCampaignStaff(user)
|
||||
const canManageLifecycle = isStaff && view.status !== 'realizado' && view.status !== 'cancelado'
|
||||
const chromeSubtitle = view.locationLabel || view.municipality?.name
|
||||
|
||||
return (
|
||||
<div className="mx-auto flex w-full max-w-6xl flex-col gap-6">
|
||||
<SetCampaignPageChrome
|
||||
chrome={
|
||||
chromeSubtitle
|
||||
? { title: view.title, subtitle: chromeSubtitle }
|
||||
: { title: view.title }
|
||||
}
|
||||
/>
|
||||
<CampaignQuickActionContextBridge
|
||||
activitySlug={view.slug}
|
||||
municipalitySlug={view.municipality?.slug}
|
||||
/>
|
||||
<header className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
|
||||
<div className="flex min-w-0 flex-col gap-2">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<h1 className="text-2xl font-semibold tracking-tight">{view.title}</h1>
|
||||
<ActivityStatusBadge status={view.status} />
|
||||
{view.deputyPresent ? <Badge>Deputado presente</Badge> : null}
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2 text-muted-foreground">
|
||||
<Badge variant="secondary">{activityKindLabels[view.kind]}</Badge>
|
||||
<span>
|
||||
{view.startAt ? formatBahiaDateTimeLabel(view.startAt) : 'Data a definir'}
|
||||
</span>
|
||||
{view.locationLabel ? <span>· {view.locationLabel}</span> : null}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<div className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<ActivityStatusBadge status={view.status} />
|
||||
{view.deputyPresent ? <Badge>Deputado presente</Badge> : null}
|
||||
<Badge variant="secondary">{activityKindLabels[view.kind]}</Badge>
|
||||
<span className="text-muted-foreground">
|
||||
{view.startAt ? formatBahiaDateTimeLabel(view.startAt) : 'Data a definir'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{isStaff ? (
|
||||
<Button asChild variant="outline" className="min-h-11">
|
||||
<Link href={`/campanha/atividades/${view.slug}/editar`}>
|
||||
|
|
@ -119,7 +141,6 @@ export default async function ActivityDetailPage({
|
|||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<ActivityTabNav activeTab={activeTab} activitySlug={view.slug} searchParams={query} />
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import config from '@payload-config'
|
||||
import { ArrowLeftIcon, PaperclipIcon } from 'lucide-react'
|
||||
import { PaperclipIcon } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { notFound } from 'next/navigation'
|
||||
import { getPayload } from 'payload'
|
||||
|
|
@ -7,9 +7,9 @@ import { getPayload } from 'payload'
|
|||
import { DemandWorkflowCard } from '@/components/campaign/demand/DemandWorkflowCard'
|
||||
import { CampaignPageShell } from '@/components/campaign/shell/CampaignPageShell'
|
||||
import { CampaignQuickActionContextSync } from '@/components/campaign/shell/CampaignQuickActionContextSync'
|
||||
import { SetCampaignPageChrome } from '@/components/campaign/shell/CampaignPageChromeContext'
|
||||
import { Badge } from '@/components/ui/Badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { campaignPageMetadataFromCatalog } from '@/lib/campaignPageChrome'
|
||||
import { campaignPageMetadata } from '@/lib/campaignPageChrome'
|
||||
import {
|
||||
campaignDemandKindLabels,
|
||||
campaignDemandStatusLabels,
|
||||
|
|
@ -24,7 +24,21 @@ import {
|
|||
transitionDemandFormAction,
|
||||
} from './formActions'
|
||||
|
||||
export const metadata = campaignPageMetadataFromCatalog('demandas')
|
||||
export async function generateMetadata({ params }: DemandDetailPageProps) {
|
||||
const { slug } = await params
|
||||
const [user, payload] = await Promise.all([
|
||||
requireCampaignPageActor({ gate: 'staff' }),
|
||||
getPayload({ config }),
|
||||
])
|
||||
|
||||
const demand = await loadDemandDetail(payload, user, slug)
|
||||
if (!demand) return campaignPageMetadata({ title: 'Demanda' })
|
||||
|
||||
return campaignPageMetadata({
|
||||
title: demand.title,
|
||||
subtitle: `${campaignDemandKindLabels[demand.kind]} · ${demand.municipalityName}`,
|
||||
})
|
||||
}
|
||||
|
||||
type DemandDetailPageProps = {
|
||||
params: Promise<{ slug: string }>
|
||||
|
|
@ -63,48 +77,44 @@ export default async function DemandDetailPage({ params }: DemandDetailPageProps
|
|||
|
||||
return (
|
||||
<CampaignPageShell>
|
||||
<SetCampaignPageChrome
|
||||
chrome={{
|
||||
title: demand.title,
|
||||
subtitle: `${campaignDemandKindLabels[demand.kind]} · ${demand.municipalityName}`,
|
||||
}}
|
||||
/>
|
||||
<CampaignQuickActionContextSync
|
||||
municipalitySlug={demand.municipalitySlug}
|
||||
municipalityId={demand.municipalityId > 0 ? demand.municipalityId : undefined}
|
||||
demandSlug={demand.slug}
|
||||
/>
|
||||
<header className="flex flex-col gap-2">
|
||||
<Button asChild variant="ghost" className="min-h-11 self-start">
|
||||
<Link href="/campanha/demandas">
|
||||
<ArrowLeftIcon data-icon="inline-start" aria-hidden="true" />
|
||||
Voltar para demandas
|
||||
</Link>
|
||||
</Button>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<h1 className="text-2xl font-semibold tracking-tight">{demand.title}</h1>
|
||||
<Badge variant={statusVariant[demand.status]}>
|
||||
{campaignDemandStatusLabels[demand.status]}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-muted-foreground">
|
||||
{campaignDemandKindLabels[demand.kind]} ·{' '}
|
||||
<Link
|
||||
href={`/campanha/municipios/${demand.municipalitySlug}`}
|
||||
className="text-primary underline-offset-4 hover:underline"
|
||||
>
|
||||
{demand.municipalityName}
|
||||
</Link>
|
||||
{demand.activity ? (
|
||||
<>
|
||||
{' '}
|
||||
· Atividade:{' '}
|
||||
<Link
|
||||
href={`/campanha/atividades/${demand.activity.slug}`}
|
||||
className="text-primary underline-offset-4 hover:underline"
|
||||
>
|
||||
{demand.activity.title}
|
||||
</Link>
|
||||
</>
|
||||
) : null}
|
||||
{demand.requesterName ? ` · Solicitada por ${demand.requesterName}` : ''} · Aberta em{' '}
|
||||
{dateTimeFormatter.format(new Date(demand.createdAt))}
|
||||
</p>
|
||||
</header>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Badge variant={statusVariant[demand.status]}>
|
||||
{campaignDemandStatusLabels[demand.status]}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-muted-foreground">
|
||||
<Link
|
||||
href={`/campanha/municipios/${demand.municipalitySlug}`}
|
||||
className="text-primary underline-offset-4 hover:underline"
|
||||
>
|
||||
{demand.municipalityName}
|
||||
</Link>
|
||||
{demand.activity ? (
|
||||
<>
|
||||
{' '}
|
||||
· Atividade:{' '}
|
||||
<Link
|
||||
href={`/campanha/atividades/${demand.activity.slug}`}
|
||||
className="text-primary underline-offset-4 hover:underline"
|
||||
>
|
||||
{demand.activity.title}
|
||||
</Link>
|
||||
</>
|
||||
) : null}
|
||||
{demand.requesterName ? ` · Solicitada por ${demand.requesterName}` : ''} · Aberta em{' '}
|
||||
{dateTimeFormatter.format(new Date(demand.createdAt))}
|
||||
</p>
|
||||
|
||||
{demand.description ? (
|
||||
<section aria-label="Descrição da demanda" className="rounded-xl border p-4">
|
||||
|
|
|
|||
|
|
@ -1,19 +1,34 @@
|
|||
import config from '@payload-config'
|
||||
import { ArrowLeftIcon, HandshakeIcon, MapPinIcon } from 'lucide-react'
|
||||
import { HandshakeIcon, MapPinIcon } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { notFound } from 'next/navigation'
|
||||
import { getPayload } from 'payload'
|
||||
|
||||
import { CampaignPageShell } from '@/components/campaign/shell/CampaignPageShell'
|
||||
import { SetCampaignPageChrome } from '@/components/campaign/shell/CampaignPageChromeContext'
|
||||
import { StateDeputyForm } from '@/components/campaign/stateDeputy/StateDeputyForm'
|
||||
import { Badge } from '@/components/ui/Badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { campaignPageMetadataFromCatalog } from '@/lib/campaignPageChrome'
|
||||
import { campaignPageMetadata } from '@/lib/campaignPageChrome'
|
||||
import { requireCampaignPageActor } from '@/utilities/campaignPageActor'
|
||||
import { loadStateDeputyDetail } from '@/utilities/stateDeputyData'
|
||||
import { updateStateDeputyFormAction } from './formActions'
|
||||
|
||||
export const metadata = campaignPageMetadataFromCatalog('dobradinhas')
|
||||
export async function generateMetadata({ params }: StateDeputyDetailPageProps) {
|
||||
const { slug } = await params
|
||||
const [user, payload] = await Promise.all([
|
||||
requireCampaignPageActor({ gate: 'staff' }),
|
||||
getPayload({ config }),
|
||||
])
|
||||
|
||||
const stateDeputy = await loadStateDeputyDetail(payload, user, slug)
|
||||
if (!stateDeputy) return campaignPageMetadata({ title: 'Dobradinha' })
|
||||
|
||||
return campaignPageMetadata(
|
||||
stateDeputy.party
|
||||
? { title: stateDeputy.name, subtitle: stateDeputy.party }
|
||||
: { title: stateDeputy.name },
|
||||
)
|
||||
}
|
||||
|
||||
type StateDeputyDetailPageProps = {
|
||||
params: Promise<{ slug: string }>
|
||||
|
|
@ -31,21 +46,16 @@ export default async function StateDeputyDetailPage({ params }: StateDeputyDetai
|
|||
|
||||
return (
|
||||
<CampaignPageShell>
|
||||
<header className="flex flex-col gap-2">
|
||||
<Button asChild variant="ghost" className="min-h-11 self-start">
|
||||
<Link href="/campanha/dobradinhas">
|
||||
<ArrowLeftIcon data-icon="inline-start" aria-hidden="true" />
|
||||
Voltar para dobradinhas
|
||||
</Link>
|
||||
</Button>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<h1 className="text-2xl font-semibold tracking-tight">{stateDeputy.name}</h1>
|
||||
{stateDeputy.party ? <Badge variant="secondary">{stateDeputy.party}</Badge> : null}
|
||||
</div>
|
||||
{stateDeputy.notes ? (
|
||||
<p className="whitespace-pre-wrap text-sm text-muted-foreground">{stateDeputy.notes}</p>
|
||||
) : null}
|
||||
</header>
|
||||
<SetCampaignPageChrome
|
||||
chrome={
|
||||
stateDeputy.party
|
||||
? { title: stateDeputy.name, subtitle: stateDeputy.party }
|
||||
: { title: stateDeputy.name }
|
||||
}
|
||||
/>
|
||||
{stateDeputy.notes ? (
|
||||
<p className="whitespace-pre-wrap text-sm text-muted-foreground">{stateDeputy.notes}</p>
|
||||
) : null}
|
||||
|
||||
<div className="grid gap-6 lg:grid-cols-2">
|
||||
<section
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
import config from '@payload-config'
|
||||
import { ArrowLeftIcon } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { notFound } from 'next/navigation'
|
||||
import { getPayload } from 'payload'
|
||||
|
||||
|
|
@ -9,10 +7,10 @@ import { LeadershipInternalForm } from '@/components/campaign/leadership/Leaders
|
|||
import { SupportStatusBadge } from '@/components/campaign/leadership/SupportStatusBadge'
|
||||
import { CampaignPageShell } from '@/components/campaign/shell/CampaignPageShell'
|
||||
import { CampaignQuickActionContextSync } from '@/components/campaign/shell/CampaignQuickActionContextSync'
|
||||
import { SetCampaignPageChrome } from '@/components/campaign/shell/CampaignPageChromeContext'
|
||||
import { StateDeputyChips } from '@/components/campaign/stateDeputy/StateDeputyChips'
|
||||
import { Badge } from '@/components/ui/Badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { campaignPageMetadataFromCatalog } from '@/lib/campaignPageChrome'
|
||||
import { campaignPageMetadata } from '@/lib/campaignPageChrome'
|
||||
import { resolvedPortfolioEntriesById } from '@/lib/municipalityPortfolio'
|
||||
import { requireCampaignPageActor } from '@/utilities/campaignPageActor'
|
||||
import {
|
||||
|
|
@ -24,7 +22,20 @@ import { loadLeadershipDetail } from '@/utilities/leadership/leadershipData'
|
|||
import { loadMunicipalityPortfolioIndex } from '@/utilities/municipality/municipalityPortfolioIndex'
|
||||
import { updateLeadershipInternalFormAction } from './formActions'
|
||||
|
||||
export const metadata = campaignPageMetadataFromCatalog('liderancas')
|
||||
export async function generateMetadata({ params }: LeadershipDetailPageProps) {
|
||||
const { id } = await params
|
||||
if (!/^[1-9]\d*$/.test(id)) return campaignPageMetadata({ title: 'Liderança' })
|
||||
|
||||
const [user, payload] = await Promise.all([
|
||||
requireCampaignPageActor({ gate: 'staff' }),
|
||||
getPayload({ config }),
|
||||
])
|
||||
|
||||
const leadership = await loadLeadershipDetail(payload, user, Number(id))
|
||||
if (!leadership) return campaignPageMetadata({ title: 'Liderança' })
|
||||
|
||||
return campaignPageMetadata({ title: 'Liderança', subtitle: leadership.name })
|
||||
}
|
||||
|
||||
type LeadershipDetailPageProps = {
|
||||
params: Promise<{ id: string }>
|
||||
|
|
@ -58,34 +69,26 @@ export default async function LeadershipDetailPage({ params }: LeadershipDetailP
|
|||
|
||||
return (
|
||||
<CampaignPageShell>
|
||||
<SetCampaignPageChrome chrome={{ title: 'Liderança', subtitle: leadership.name }} />
|
||||
<CampaignQuickActionContextSync
|
||||
leadershipId={leadership.id}
|
||||
municipalitySlug={singleMunicipalitySlug}
|
||||
/>
|
||||
<header className="flex flex-col gap-2">
|
||||
<Button asChild variant="ghost" className="min-h-11 self-start">
|
||||
<Link href="/campanha/liderancas">
|
||||
<ArrowLeftIcon data-icon="inline-start" aria-hidden="true" />
|
||||
Voltar para lideranças
|
||||
</Link>
|
||||
</Button>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<h1 className="text-2xl font-semibold tracking-tight">{leadership.name}</h1>
|
||||
{leadership.supportStatus ? (
|
||||
<SupportStatusBadge status={leadership.supportStatus} />
|
||||
) : null}
|
||||
<Badge variant={leadership.hasAppAccess ? 'estimate-confirmed' : 'outline'}>
|
||||
{leadership.hasAppAccess ? 'Com acesso ao app' : 'Sem acesso ao app'}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-muted-foreground">
|
||||
{leadership.phone ? `Celular ${leadership.phone}` : 'Sem celular registrado'}
|
||||
{leadership.email ? ` · ${leadership.email}` : ''}
|
||||
</p>
|
||||
{leadership.stateDeputies.length > 0 ? (
|
||||
<StateDeputyChips deputies={leadership.stateDeputies} />
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{leadership.supportStatus ? (
|
||||
<SupportStatusBadge status={leadership.supportStatus} />
|
||||
) : null}
|
||||
</header>
|
||||
<Badge variant={leadership.hasAppAccess ? 'estimate-confirmed' : 'outline'}>
|
||||
{leadership.hasAppAccess ? 'Com acesso ao app' : 'Sem acesso ao app'}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-muted-foreground">
|
||||
{leadership.phone ? `Celular ${leadership.phone}` : 'Sem celular registrado'}
|
||||
{leadership.email ? ` · ${leadership.email}` : ''}
|
||||
</p>
|
||||
{leadership.stateDeputies.length > 0 ? (
|
||||
<StateDeputyChips deputies={leadership.stateDeputies} />
|
||||
) : null}
|
||||
|
||||
<section
|
||||
aria-labelledby="leadership-invites-title"
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@ import { Suspense } from 'react'
|
|||
import { RecentVisitTracker } from '@/components/campaign/dashboard/RecentVisitTracker'
|
||||
import { MunicipalityTabNav } from '@/components/campaign/municipality/MunicipalityTabNav'
|
||||
import { CampaignPageShell } from '@/components/campaign/shell/CampaignPageShell'
|
||||
import { Badge } from '@/components/ui/Badge'
|
||||
import { campaignPageMetadataFromCatalog } from '@/lib/campaignPageChrome'
|
||||
import { SetCampaignPageChrome } from '@/components/campaign/shell/CampaignPageChromeContext'
|
||||
import { campaignPageMetadata } from '@/lib/campaignPageChrome'
|
||||
import { requireCampaignPageActor } from '@/utilities/campaignPageActor'
|
||||
import {
|
||||
resolveMunicipalityDetailTab,
|
||||
|
|
@ -16,7 +16,6 @@ import {
|
|||
} from '@/utilities/municipality/municipalityDetailTabUi'
|
||||
import {
|
||||
formatMunicipalityGeographyLabel,
|
||||
municipalityKindLabels,
|
||||
} from '@/utilities/municipality/municipalityLabels'
|
||||
import {
|
||||
getMunicipalityDetailViewModel,
|
||||
|
|
@ -34,15 +33,28 @@ import {
|
|||
UpdatesTab,
|
||||
} from './MunicipalityDetailTabs'
|
||||
|
||||
export const metadata = campaignPageMetadataFromCatalog('municipios')
|
||||
export async function generateMetadata({ params }: MunicipalityDetailPageProps) {
|
||||
const { slug } = await params
|
||||
const payload = await getPayload({ config })
|
||||
const user = await requireCampaignPageActor({ gate: 'noLeader' })
|
||||
|
||||
try {
|
||||
const context = await resolveAccessibleMunicipalityContext(payload, user, slug)
|
||||
const view = await getMunicipalityDetailViewModel(payload, context, user)
|
||||
return campaignPageMetadata({
|
||||
title: view.name,
|
||||
subtitle: formatMunicipalityGeographyLabel(view),
|
||||
})
|
||||
} catch {
|
||||
return campaignPageMetadata({ title: 'Município' })
|
||||
}
|
||||
}
|
||||
|
||||
type MunicipalityDetailPageProps = {
|
||||
params: Promise<{ slug: string }>
|
||||
searchParams: Promise<MunicipalityDetailSearchParams>
|
||||
}
|
||||
|
||||
const dateFormatter = new Intl.DateTimeFormat('pt-BR')
|
||||
|
||||
export default async function MunicipalityDetailPage({
|
||||
params,
|
||||
searchParams,
|
||||
|
|
@ -68,22 +80,12 @@ export default async function MunicipalityDetailPage({
|
|||
|
||||
return (
|
||||
<CampaignPageShell>
|
||||
{/* print: the dossier tab carries its own capa; page chrome stays out. */}
|
||||
<header className="flex flex-col gap-2 print:hidden">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<h1 className="text-2xl font-semibold tracking-tight">{view.name}</h1>
|
||||
<Badge variant="scope">{municipalityKindLabels[view.kind]}</Badge>
|
||||
</div>
|
||||
<p className="text-muted-foreground">{formatMunicipalityGeographyLabel(view)}</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{advisorSummaries.length
|
||||
? `Assessoria: ${advisorSummaries.map((advisor) => advisor.name).join(', ')}`
|
||||
: 'Sem assessor designado.'}
|
||||
{view.lastUpdateAt
|
||||
? ` · Última atualização em ${dateFormatter.format(new Date(view.lastUpdateAt))}`
|
||||
: ''}
|
||||
</p>
|
||||
</header>
|
||||
<SetCampaignPageChrome
|
||||
chrome={{
|
||||
title: view.name,
|
||||
subtitle: formatMunicipalityGeographyLabel(view),
|
||||
}}
|
||||
/>
|
||||
|
||||
<MunicipalityTabNav
|
||||
activeTab={activeTab}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import config from '@payload-config'
|
||||
import { ArrowLeftIcon, CalendarDaysIcon, HandshakeIcon } from 'lucide-react'
|
||||
import { CalendarDaysIcon, HandshakeIcon } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { notFound } from 'next/navigation'
|
||||
import { getPayload } from 'payload'
|
||||
|
|
@ -7,16 +7,30 @@ 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 { SetCampaignPageChrome } from '@/components/campaign/shell/CampaignPageChromeContext'
|
||||
import { Badge } from '@/components/ui/Badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { campaignPageMetadataFromCatalog } from '@/lib/campaignPageChrome'
|
||||
import { campaignPageMetadata } from '@/lib/campaignPageChrome'
|
||||
import { organizationKindLabels } from '@/lib/schemas/organization'
|
||||
import { requireCampaignPageActor } from '@/utilities/campaignPageActor'
|
||||
import { loadMunicipalityOptions } from '@/utilities/campaignRelationOptions'
|
||||
import { loadOrganizationDetail } from '@/utilities/organizationData'
|
||||
import { updateOrganizationFormAction } from './formActions'
|
||||
|
||||
export const metadata = campaignPageMetadataFromCatalog('organizacoes')
|
||||
export async function generateMetadata({ params }: OrganizationDetailPageProps) {
|
||||
const { slug } = await params
|
||||
const [user, payload] = await Promise.all([
|
||||
requireCampaignPageActor({ gate: 'staff' }),
|
||||
getPayload({ config }),
|
||||
])
|
||||
|
||||
const organization = await loadOrganizationDetail(payload, user, slug)
|
||||
if (!organization) return campaignPageMetadata({ title: 'Organização' })
|
||||
|
||||
return campaignPageMetadata({
|
||||
title: organization.name,
|
||||
subtitle: organizationKindLabels[organization.kind],
|
||||
})
|
||||
}
|
||||
|
||||
type OrganizationDetailPageProps = {
|
||||
params: Promise<{ slug: string }>
|
||||
|
|
@ -38,26 +52,20 @@ export default async function OrganizationDetailPage({ params }: OrganizationDet
|
|||
|
||||
return (
|
||||
<CampaignPageShell>
|
||||
<header className="flex flex-col gap-2">
|
||||
<Button asChild variant="ghost" className="min-h-11 self-start">
|
||||
<Link href="/campanha/organizacoes">
|
||||
<ArrowLeftIcon data-icon="inline-start" aria-hidden="true" />
|
||||
Voltar para organizações
|
||||
</Link>
|
||||
</Button>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<h1 className="text-2xl font-semibold tracking-tight">{organization.name}</h1>
|
||||
<Badge variant="secondary">{organizationKindLabels[organization.kind]}</Badge>
|
||||
</div>
|
||||
{organization.municipalityNames.length ? (
|
||||
<p className="text-muted-foreground">
|
||||
Atua em {organization.municipalityNames.join(', ')}
|
||||
</p>
|
||||
) : null}
|
||||
{organization.notes ? (
|
||||
<p className="whitespace-pre-wrap text-sm text-muted-foreground">{organization.notes}</p>
|
||||
) : null}
|
||||
</header>
|
||||
<SetCampaignPageChrome
|
||||
chrome={{
|
||||
title: organization.name,
|
||||
subtitle: organizationKindLabels[organization.kind],
|
||||
}}
|
||||
/>
|
||||
{organization.municipalityNames.length ? (
|
||||
<p className="text-muted-foreground">
|
||||
Atua em {organization.municipalityNames.join(', ')}
|
||||
</p>
|
||||
) : null}
|
||||
{organization.notes ? (
|
||||
<p className="whitespace-pre-wrap text-sm text-muted-foreground">{organization.notes}</p>
|
||||
) : null}
|
||||
|
||||
<div className="grid gap-6 lg:grid-cols-2">
|
||||
<section
|
||||
|
|
|
|||
|
|
@ -102,8 +102,6 @@ const resolveCatalogEntry = (
|
|||
: { title: entry.title }
|
||||
}
|
||||
|
||||
const sectionOnly = (title: string): CampaignPageChrome => ({ title })
|
||||
|
||||
type PathRule = {
|
||||
match: (pathname: string) => boolean
|
||||
resolve: () => CampaignPageChrome | null
|
||||
|
|
@ -128,12 +126,12 @@ const pathRules: PathRule[] = [
|
|||
},
|
||||
{
|
||||
match: (pathname) => /^\/campanha\/municipios\/[^/]+$/.test(pathname),
|
||||
resolve: () => sectionOnly(campaignPageChromeCatalog.municipios.title),
|
||||
resolve: () => null,
|
||||
},
|
||||
{
|
||||
// B147 parallel v2 — soft-dep B145 for entity name in chrome; section title for now.
|
||||
match: (pathname) => /^\/campanha\/municipio\/[^/]+\/v2$/.test(pathname),
|
||||
resolve: () => sectionOnly(campaignPageChromeCatalog.municipios.title),
|
||||
resolve: () => ({ title: campaignPageChromeCatalog.municipios.title }),
|
||||
},
|
||||
{
|
||||
match: (pathname) => pathname === '/campanha/territorios',
|
||||
|
|
@ -145,7 +143,7 @@ const pathRules: PathRule[] = [
|
|||
},
|
||||
{
|
||||
match: (pathname) => /^\/campanha\/liderancas\/[^/]+$/.test(pathname),
|
||||
resolve: () => sectionOnly(campaignPageChromeCatalog.liderancas.title),
|
||||
resolve: () => null,
|
||||
},
|
||||
{
|
||||
match: (pathname) => pathname === '/campanha/liderancas',
|
||||
|
|
@ -157,7 +155,7 @@ const pathRules: PathRule[] = [
|
|||
},
|
||||
{
|
||||
match: (pathname) => /^\/campanha\/dobradinhas\/[^/]+$/.test(pathname),
|
||||
resolve: () => sectionOnly(campaignPageChromeCatalog.dobradinhas.title),
|
||||
resolve: () => null,
|
||||
},
|
||||
{
|
||||
match: (pathname) => pathname === '/campanha/dobradinhas',
|
||||
|
|
@ -177,7 +175,7 @@ const pathRules: PathRule[] = [
|
|||
},
|
||||
{
|
||||
match: (pathname) => /^\/campanha\/atividades\/[^/]+$/.test(pathname),
|
||||
resolve: () => sectionOnly(campaignPageChromeCatalog.atividades.title),
|
||||
resolve: () => null,
|
||||
},
|
||||
{
|
||||
match: (pathname) => pathname === '/campanha/atividades',
|
||||
|
|
@ -189,7 +187,7 @@ const pathRules: PathRule[] = [
|
|||
},
|
||||
{
|
||||
match: (pathname) => /^\/campanha\/demandas\/[^/]+$/.test(pathname),
|
||||
resolve: () => sectionOnly(campaignPageChromeCatalog.demandas.title),
|
||||
resolve: () => null,
|
||||
},
|
||||
{
|
||||
match: (pathname) => pathname === '/campanha/demandas',
|
||||
|
|
@ -205,7 +203,7 @@ const pathRules: PathRule[] = [
|
|||
},
|
||||
{
|
||||
match: (pathname) => /^\/campanha\/apoiadores\/[^/]+$/.test(pathname),
|
||||
resolve: () => sectionOnly(campaignPageChromeCatalog.apoiadores.title),
|
||||
resolve: () => null,
|
||||
},
|
||||
{
|
||||
match: (pathname) => pathname === '/campanha/apoiadores',
|
||||
|
|
@ -213,7 +211,7 @@ const pathRules: PathRule[] = [
|
|||
},
|
||||
{
|
||||
match: (pathname) => /^\/campanha\/assessores\/[^/]+$/.test(pathname),
|
||||
resolve: () => sectionOnly(campaignPageChromeCatalog.assessores.title),
|
||||
resolve: () => null,
|
||||
},
|
||||
{
|
||||
match: (pathname) => pathname === '/campanha/assessores',
|
||||
|
|
@ -225,7 +223,7 @@ const pathRules: PathRule[] = [
|
|||
},
|
||||
{
|
||||
match: (pathname) => /^\/campanha\/organizacoes\/[^/]+$/.test(pathname),
|
||||
resolve: () => sectionOnly(campaignPageChromeCatalog.organizacoes.title),
|
||||
resolve: () => null,
|
||||
},
|
||||
{
|
||||
match: (pathname) => pathname === '/campanha/organizacoes',
|
||||
|
|
|
|||
|
|
@ -49,9 +49,7 @@ test('the dashboard map paints Salvador zone by zone and opens one', async ({ ca
|
|||
await expect(readout).toContainText(/Salvador — ZE \d+/)
|
||||
|
||||
await readout.getByRole('link', { name: 'Abrir município' }).click()
|
||||
// The slug is the assertion: the detail page's own <h1> reads the municipality
|
||||
// ROW, which other specs rename, while the polygon's name comes from the
|
||||
// static catalog.
|
||||
// B145: entity identity lives in the shell header, not a body h1.
|
||||
await expect(page).toHaveURL(/\/campanha\/municipios\/salvador-ze-\d+$/)
|
||||
await expect(page.getByRole('heading', { level: 1 })).toBeVisible()
|
||||
await expect(page.locator('[data-slot="campaign-page-chrome-title"]')).toBeVisible()
|
||||
})
|
||||
|
|
|
|||
|
|
@ -21,10 +21,13 @@ describe('campaignPageChrome', () => {
|
|||
})
|
||||
})
|
||||
|
||||
it('resolves detail pages with section title only', () => {
|
||||
expect(resolveCampaignPageChrome('/campanha/municipios/cairu', 'coordinator')).toEqual({
|
||||
title: 'Municípios',
|
||||
})
|
||||
it('returns null for entity detail pages (chrome set per route)', () => {
|
||||
expect(resolveCampaignPageChrome('/campanha/municipios/cairu', 'coordinator')).toBeNull()
|
||||
expect(resolveCampaignPageChrome('/campanha/liderancas/42', 'advisor')).toBeNull()
|
||||
expect(resolveCampaignPageChrome('/campanha/atividades/foo', 'coordinator')).toBeNull()
|
||||
})
|
||||
|
||||
it('keeps section title on parallel municipio v2 route (B147 soft-dep)', () => {
|
||||
expect(resolveCampaignPageChrome('/campanha/municipio/cairu/v2', 'coordinator')).toEqual({
|
||||
title: 'Municípios',
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in a new issue