refactor(C129): simplify — CampaignNameSubline compartilhado (a11y sr-label), guard legenda==nome, sem plumb morto, spec mais forte
This commit is contained in:
parent
d7acb5aa48
commit
3593d84b30
10 changed files with 72 additions and 18 deletions
|
|
@ -43,7 +43,8 @@ const stateDeputyContactSafeMessages = [
|
|||
|
||||
const stateDeputyPartySafeMessages = [STATE_DEPUTY_STAFF_MESSAGE] as const
|
||||
|
||||
const stateDeputyBallotNameSafeMessages = [STATE_DEPUTY_STAFF_MESSAGE] as const
|
||||
/** The party and ballotName inline cells share the same refusal vocabulary. */
|
||||
const stateDeputyInlineFieldSafeMessages = stateDeputyPartySafeMessages
|
||||
|
||||
/** Per-field Contact edit for B163 (lista + ficha). */
|
||||
export const updateStateDeputyContactFormAction = async (
|
||||
|
|
@ -119,7 +120,7 @@ export const updateStateDeputyBallotNameFormAction = async (
|
|||
})
|
||||
return { message: 'Salvo.' }
|
||||
},
|
||||
safeMessages: stateDeputyBallotNameSafeMessages,
|
||||
safeMessages: stateDeputyInlineFieldSafeMessages,
|
||||
genericMessage:
|
||||
'Não foi possível salvar o nome de legenda. Verifique os dados e tente novamente.',
|
||||
})
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import {
|
|||
CampaignTransitionAnchor,
|
||||
} from '@/components/campaign/shared/CampaignListPending'
|
||||
import { CampaignListSheetProvider } from '@/components/campaign/shared/CampaignListSheetHost'
|
||||
import { CampaignNameSubline } from '@/components/campaign/shared/CampaignNameSubline'
|
||||
import {
|
||||
CampaignTable,
|
||||
CampaignTableHead,
|
||||
|
|
@ -136,10 +137,12 @@ const stateDeputyColumns = (
|
|||
/>
|
||||
{/* C129 — the ballot name sits OUTSIDE the name cell's click-to-edit
|
||||
region (sibling, not child), so clicking it never opens the name
|
||||
editor. */}
|
||||
{row.ballotName ? (
|
||||
<span className="truncate text-xs text-muted-foreground">{row.ballotName}</span>
|
||||
) : null}
|
||||
editor. A legenda identical to the real name is skipped (same
|
||||
policy as `peopleNameSubline`). */}
|
||||
<CampaignNameSubline
|
||||
value={row.ballotName !== row.name ? row.ballotName : null}
|
||||
srLabel="Nome de legenda"
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import {
|
|||
CampaignTransitionAnchor,
|
||||
} from '@/components/campaign/shared/CampaignListPending'
|
||||
import { CampaignListSheetProvider } from '@/components/campaign/shared/CampaignListSheetHost'
|
||||
import { CampaignNameSubline } from '@/components/campaign/shared/CampaignNameSubline'
|
||||
import {
|
||||
CampaignTable,
|
||||
CampaignTableHead,
|
||||
|
|
@ -193,9 +194,7 @@ const peopleColumns = ({
|
|||
<span className="shrink-0 text-muted-foreground">({row.party})</span>
|
||||
) : null}
|
||||
</div>
|
||||
{subline ? (
|
||||
<span className="truncate text-xs text-muted-foreground">{subline}</span>
|
||||
) : null}
|
||||
<CampaignNameSubline value={subline} srLabel="Nome de legenda" />
|
||||
</div>
|
||||
)
|
||||
},
|
||||
|
|
|
|||
19
src/components/campaign/shared/CampaignNameSubline.tsx
Normal file
19
src/components/campaign/shared/CampaignNameSubline.tsx
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
/**
|
||||
* The "discreto sob o nome" secondary line (C129): the same muted small text
|
||||
* under a name in every surface — people table, dobradinhas list and ficha.
|
||||
* The position is the visual label; an optional `srLabel` names the line for
|
||||
* screen readers (the muted/small styling does not exist in a linear read).
|
||||
*/
|
||||
export const CampaignNameSubline = ({
|
||||
value,
|
||||
srLabel,
|
||||
}: {
|
||||
value: string | null | undefined
|
||||
srLabel?: string
|
||||
}) =>
|
||||
value ? (
|
||||
<span className="truncate text-xs text-muted-foreground">
|
||||
{srLabel ? <span className="sr-only">{srLabel} </span> : null}
|
||||
{value}
|
||||
</span>
|
||||
) : null
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import { updateStateDeputyContactFormAction } from '@/app/(campaign)/campanha/(app)/dobradinhas/formActions'
|
||||
import { CampaignInlineEditableCell } from '@/components/campaign/shared/CampaignInlineEditableCell'
|
||||
import { CampaignNameSubline } from '@/components/campaign/shared/CampaignNameSubline'
|
||||
import { PhonesFieldEditor } from '@/components/campaign/shared/PhonesFieldEditor'
|
||||
|
||||
type StateDeputyContactSectionProps = {
|
||||
|
|
@ -39,8 +40,8 @@ export const StateDeputyContactSection = ({
|
|||
formAction={updateStateDeputyContactFormAction}
|
||||
href={`/campanha/dobradinhas/${stateDeputyId}`}
|
||||
/>
|
||||
{ballotName ? (
|
||||
<span className="truncate text-xs text-muted-foreground">{ballotName}</span>
|
||||
{ballotName !== name ? (
|
||||
<CampaignNameSubline value={ballotName} srLabel="Nome de legenda" />
|
||||
) : null}
|
||||
</div>
|
||||
</dd>
|
||||
|
|
|
|||
|
|
@ -78,6 +78,9 @@ export type PeopleRowViewModel = {
|
|||
/**
|
||||
* C129 — the Name-cell subline of `/campanha/pessoas`: the dobradinha's
|
||||
* "nome de legenda" (ballot name), discreet under the real name, when present.
|
||||
* A ballot name identical to the real name is redundant noise — it reads as a
|
||||
* duplicated line, so it is skipped (a mesa registering the full name as urn
|
||||
* name is plausible data entry).
|
||||
*
|
||||
* C130 extends this with the base fallback — the FINAL shared rule for the
|
||||
* second line is `ballotName ?? city` (legenda overrides base), the two plans
|
||||
|
|
@ -86,8 +89,10 @@ export type PeopleRowViewModel = {
|
|||
* here (and its unit spec), never markup. Until C130 lands, this branch keeps
|
||||
* the accept "sem nome de legenda, nada muda na linha" — null when absent.
|
||||
*/
|
||||
export const peopleNameSubline = (person: MergedPerson | PeopleRowViewModel): string | null =>
|
||||
person.ballotName
|
||||
export const peopleNameSubline = (
|
||||
person: Pick<MergedPerson, 'name' | 'ballotName'>,
|
||||
): string | null =>
|
||||
person.ballotName && person.ballotName !== person.name ? person.ballotName : null
|
||||
|
||||
/** The merge intermediate carries every field the list/detail row needs. */
|
||||
export type MergedPerson = Omit<PeopleRowViewModel, 'assessoradoNames'> & {
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ export const loadPersonDetail = async (
|
|||
depth: 1,
|
||||
limit: 0,
|
||||
pagination: false,
|
||||
select: { contact: true, slug: true, party: true, advisors: true },
|
||||
select: { contact: true, slug: true, party: true, ballotName: true, advisors: true },
|
||||
user,
|
||||
overrideAccess: false,
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -239,13 +239,13 @@ export type StateDeputySummary = {
|
|||
name: string
|
||||
slug: string
|
||||
party: string | null
|
||||
/** C129 — the "nome de legenda" (ballot name), shown discreet under the name. */
|
||||
ballotName: string | null
|
||||
}
|
||||
|
||||
export type StateDeputyDetailViewModel = StateDeputySummary & {
|
||||
email: string | null
|
||||
phone: string | null
|
||||
/** C129 — the "nome de legenda" (ballot name), shown discreet under the name. */
|
||||
ballotName: string | null
|
||||
/** Every number of the ficha, order = priority (C112) — primary first. */
|
||||
phones: string[]
|
||||
notes: string | null
|
||||
|
|
@ -345,7 +345,7 @@ export const loadStateDeputySummaries = async (
|
|||
limit: 0,
|
||||
pagination: false,
|
||||
sort: 'contact.name',
|
||||
select: { contact: true, slug: true, party: true, ballotName: true },
|
||||
select: { contact: true, slug: true, party: true },
|
||||
// Intentional admin bypass: id lookups for the home-search card, no
|
||||
// actor-scoping possible or needed (B52 precedent).
|
||||
overrideAccess: true,
|
||||
|
|
@ -359,7 +359,6 @@ export const loadStateDeputySummaries = async (
|
|||
name: stateDeputyContactSummary(doc.contact).name,
|
||||
slug: doc.slug,
|
||||
party: doc.party ?? null,
|
||||
ballotName: doc.ballotName ?? null,
|
||||
},
|
||||
]),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -55,6 +55,15 @@ describe('updateStateDeputyBallotNameRecord (C129)', () => {
|
|||
})
|
||||
|
||||
expect(updated.ballotName).toBeNull()
|
||||
|
||||
const reloaded = await payload.findByID({
|
||||
collection: 'stateDeputy',
|
||||
id: stateDeputy.id,
|
||||
depth: 0,
|
||||
select: { ballotName: true },
|
||||
overrideAccess: true,
|
||||
})
|
||||
expect(reloaded.ballotName).toBeNull()
|
||||
})
|
||||
|
||||
it('lets an advisor edit the ballot name of a dobradinha (staff scope, no carteira gate)', async () => {
|
||||
|
|
@ -68,6 +77,15 @@ describe('updateStateDeputyBallotNameRecord (C129)', () => {
|
|||
})
|
||||
|
||||
expect(updated.ballotName).toBe('Apelido da Mesa')
|
||||
|
||||
const reloaded = await payload.findByID({
|
||||
collection: 'stateDeputy',
|
||||
id: stateDeputy.id,
|
||||
depth: 0,
|
||||
select: { ballotName: true },
|
||||
overrideAccess: true,
|
||||
})
|
||||
expect(reloaded.ballotName).toBe('Apelido da Mesa')
|
||||
})
|
||||
|
||||
it('refuses a leader (staff gate)', async () => {
|
||||
|
|
|
|||
|
|
@ -341,6 +341,15 @@ describe('peopleNameSubline (C129)', () => {
|
|||
const maria = byContact(mergePeopleSources(source), 100)
|
||||
expect(peopleNameSubline(maria)).toBeNull()
|
||||
})
|
||||
|
||||
it('skips a ballot name identical to the real name (redundant duplicate line)', () => {
|
||||
const withIdenticalBallotName = mergePeopleSources({
|
||||
...source,
|
||||
deputies: [{ ...source.deputies[0]!, ballotName: 'Ana Lima' }],
|
||||
})
|
||||
const ana = byContact(withIdenticalBallotName, 101)
|
||||
expect(peopleNameSubline(ana)).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('peopleFilterFacetsFromRows', () => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue