Move the level control onto B42's shared cell-edit overlay.
B42 landed on main during this session and extracted `CampaignCellEditOverlay` — Popover on `md+`, Drawer below it, where a popover fights the virtual keyboard. The rebase auto-merged E14's "Nível" block into what had become the Drawer, which compiles and would have opened a popover inside a sheet, so the control now takes `variant` like its three siblings. The container's Popover branch is point for point what E14 had hand-written, so the desktop is unchanged; on touch, where there is no hover to carry it, the trigger now spells the level out instead of showing the bare numeral.
This commit is contained in:
parent
c7001b75f0
commit
8d6c8e3313
4 changed files with 130 additions and 133 deletions
File diff suppressed because one or more lines are too long
|
|
@ -107,6 +107,7 @@ O que ficou diferente do plano, e o que só se soube implementando:
|
|||
- **Custo:** `/campanha/municipios` fica em 17,7 kB / 322 kB de First Load JS. Suítes: 64 unit (604 testes) e 53 int (427). Nada novo no bundle do cliente: a revisão de performance conferiu contra o build de produção que todo módulo que a ilha importa já estava nos chunks da rota.
|
||||
- **O que a revisão pós-entrega corrigiu:** (1) um `blocked` vindo do **servidor** — o caso que só existe sob concorrência, quando outra aba moveu o município — virava string no box de erro, sem o checkbox de override ao lado; agora as violações tipadas voltam ao mesmo lugar que as locais, deduplicadas por `id` e presas ao nível para o qual foram levantadas, que é a única forma de o terceiro estado da rota valer para quem mais precisa dele. (2) `formatEngagementLevelLabel` / `EMPTY_ENGAGEMENT_LEVEL_LABEL` mudaram de `municipalityLabels` para `lib/engagementLevel.ts`: importar um valor daquele módulo arrasta o catálogo de 435 entradas e o glossário de conceitos para dentro da ilha (o padrão de 21 kB do B14), hoje inócuo só porque o controle de tendência já mantém a cadeia viva. (3) `isEngagementLevel` saiu de três chamadas onde o tipo gerado pelo Payload já era a união fechada.
|
||||
- **Débito conhecido (não é do E14 sozinho):** `levelNote` entrou em `municipalityListSelect`, e o sort padrão do staff (`deficit`) roda esse select sobre os 435 municípios do escopo para renderizar 25 linhas — no teto de 2000 caracteres são ~870 KB lidos por request. Hoje custa zero (o campo nasce nulo em todos), e `politicalTrend.note` tem exatamente a mesma forma e já estava lá: o conserto é dos dois juntos (ler as notas só para a fatia da página), não de um.
|
||||
- **No rebase, o controle deixou de ter Popover próprio.** O **B42** entrou no `main` durante esta sessão e extraiu `CampaignCellEditOverlay` — Popover no `md+`, Drawer abaixo dele, porque no celular o popover disputa espaço com o teclado virtual. O merge automático encaixou o bloco "Nível" dentro do que virou o Drawer, o que compilava e teria aberto um popover dentro de uma gaveta; o controle passou a receber `variant` como os três irmãos (`popover` na tabela, `sheet` no card). O ramo Popover do container é ponto a ponto o que o E14 tinha escrito à mão (mesma tooltip, mesmas classes de trigger, mesmo `onOpenAutoFocus`), então a troca não muda o desktop — e no toque, onde não há hover, o trigger passa a soletrar o nível em vez de mostrar só o numeral.
|
||||
- **Fora do escopo, confirmado na entrega:** `priority` continua intocado (18 arquivos + o seed `projectionSheetParse.ts` que escreve o campo), e nem o E8 nem o E9 leem o nível.
|
||||
|
||||
## Referências
|
||||
|
|
|
|||
|
|
@ -378,6 +378,7 @@ const municipalityListColumns = ({
|
|||
level={municipality.engagementLevel}
|
||||
levelNote={municipality.levelNote}
|
||||
levelChangedAt={municipality.levelChangedAt}
|
||||
variant="popover"
|
||||
/>
|
||||
) : (
|
||||
<MunicipalityLevelBadge
|
||||
|
|
@ -653,6 +654,7 @@ export const MunicipalityList = (props: MunicipalityListProps) => {
|
|||
level={municipality.engagementLevel}
|
||||
levelNote={municipality.levelNote}
|
||||
levelChangedAt={municipality.levelChangedAt}
|
||||
variant="sheet"
|
||||
/>
|
||||
) : (
|
||||
<MunicipalityLevelBadge
|
||||
|
|
|
|||
|
|
@ -7,13 +7,15 @@ import type {
|
|||
MunicipalityListSavedEngagementLevel,
|
||||
} from '@/app/(campaign)/campanha/(app)/municipios/engagement-level/types'
|
||||
import { MunicipalityLevelBadge } from '@/components/campaign/municipality/MunicipalityLevelBadge'
|
||||
import { CampaignHoverTooltip } from '@/components/campaign/shared/CampaignHoverTooltip'
|
||||
import {
|
||||
CampaignCellEditOverlay,
|
||||
type CampaignCellEditOverlayVariant,
|
||||
} from '@/components/campaign/shared/CampaignCellEditOverlay'
|
||||
import { Alert, AlertDescription } from '@/components/ui/Alert'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Checkbox } from '@/components/ui/Checkbox'
|
||||
import { Field, FieldContent, FieldLabel } from '@/components/ui/field'
|
||||
import { NativeSelect, NativeSelectOption } from '@/components/ui/native-select'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/Popover'
|
||||
import { Spinner } from '@/components/ui/Spinner'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import {
|
||||
|
|
@ -27,7 +29,6 @@ import {
|
|||
type EngagementLevel,
|
||||
type EngagementLevelViolation,
|
||||
} from '@/lib/engagementLevel'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const ENGAGEMENT_LEVEL_ENDPOINT = '/campanha/municipios/engagement-level'
|
||||
const SAVE_ERROR_MESSAGE = 'Não foi possível registrar o nível. Tente novamente.'
|
||||
|
|
@ -38,6 +39,7 @@ type MunicipalityListLevelControlProps = {
|
|||
level: EngagementLevel | null
|
||||
levelNote: string | null
|
||||
levelChangedAt: string | null
|
||||
variant: CampaignCellEditOverlayVariant
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -52,6 +54,7 @@ export const MunicipalityListLevelControl = ({
|
|||
level,
|
||||
levelNote,
|
||||
levelChangedAt,
|
||||
variant,
|
||||
}: MunicipalityListLevelControlProps) => {
|
||||
const [open, setOpen] = useState(false)
|
||||
const [saved, setSaved] = useState<MunicipalityListSavedEngagementLevel | null>(
|
||||
|
|
@ -189,144 +192,135 @@ export const MunicipalityListLevelControl = ({
|
|||
const fieldId = (suffix: string) => `municipality-list-level-${suffix}-${municipalityID}`
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={handleOpenChange}>
|
||||
<CampaignHoverTooltip
|
||||
content={
|
||||
currentLevel ? (
|
||||
<div className="space-y-1">
|
||||
<p>{formatEngagementLevelLabel(currentLevel)}</p>
|
||||
{currentNote ? <p className="whitespace-pre-wrap">{currentNote}</p> : null}
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
align="start"
|
||||
openOnTouch={false}
|
||||
disabled={open}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
aria-expanded={open}
|
||||
aria-haspopup="dialog"
|
||||
className={cn(
|
||||
'min-h-11 rounded-md px-1 hover:bg-muted/60 focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none',
|
||||
open ? 'bg-muted/60' : undefined,
|
||||
)}
|
||||
aria-label={`Nível de envolvimento de ${municipalityName}: ${
|
||||
currentLevel ? formatEngagementLevelLabel(currentLevel) : EMPTY_ENGAGEMENT_LEVEL_LABEL
|
||||
}`}
|
||||
<CampaignCellEditOverlay
|
||||
variant={variant}
|
||||
open={open}
|
||||
onOpenChange={handleOpenChange}
|
||||
title="Registrar nível de envolvimento"
|
||||
description={municipalityName}
|
||||
triggerLabel={`Nível de envolvimento de ${municipalityName}: ${
|
||||
currentLevel ? formatEngagementLevelLabel(currentLevel) : EMPTY_ENGAGEMENT_LEVEL_LABEL
|
||||
}`}
|
||||
tooltipContent={
|
||||
currentLevel ? (
|
||||
<div className="space-y-1">
|
||||
<p>{formatEngagementLevelLabel(currentLevel)}</p>
|
||||
{currentNote ? <p className="whitespace-pre-wrap">{currentNote}</p> : null}
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
contentClassName="w-80 p-3"
|
||||
preventPopoverAutoFocus
|
||||
trigger={
|
||||
// The sheet has no hover to carry the rest, so the trigger spells the
|
||||
// level out there and stays the bare numeral in the table.
|
||||
<MunicipalityLevelBadge
|
||||
level={currentLevel}
|
||||
note={variant === 'sheet' ? currentNote : null}
|
||||
layout={variant === 'sheet' ? 'card' : 'table'}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div className="flex flex-col gap-3">
|
||||
<Field>
|
||||
<FieldLabel htmlFor={fieldId('select')}>Nível de envolvimento</FieldLabel>
|
||||
<NativeSelect
|
||||
id={fieldId('select')}
|
||||
value={draftLevel}
|
||||
onChange={(event) =>
|
||||
setDraftLevel(isEngagementLevel(event.target.value) ? event.target.value : '')
|
||||
}
|
||||
className="min-h-11 w-full"
|
||||
>
|
||||
<MunicipalityLevelBadge level={currentLevel} layout="table" />
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
</CampaignHoverTooltip>
|
||||
<PopoverContent
|
||||
align="start"
|
||||
sideOffset={8}
|
||||
className="w-80 p-3"
|
||||
onOpenAutoFocus={(event) => event.preventDefault()}
|
||||
>
|
||||
<div className="flex flex-col gap-3">
|
||||
<Field>
|
||||
<FieldLabel htmlFor={fieldId('select')}>Nível de envolvimento</FieldLabel>
|
||||
<NativeSelect
|
||||
id={fieldId('select')}
|
||||
value={draftLevel}
|
||||
onChange={(event) =>
|
||||
setDraftLevel(isEngagementLevel(event.target.value) ? event.target.value : '')
|
||||
}
|
||||
className="min-h-11 w-full"
|
||||
>
|
||||
<NativeSelectOption value="">{EMPTY_ENGAGEMENT_LEVEL_LABEL}</NativeSelectOption>
|
||||
{engagementLevels.map((option) => (
|
||||
<NativeSelectOption key={option} value={option}>
|
||||
{formatEngagementLevelLabel(option)}
|
||||
</NativeSelectOption>
|
||||
))}
|
||||
</NativeSelect>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor={fieldId('note')}>Motivo</FieldLabel>
|
||||
<Textarea
|
||||
id={fieldId('note')}
|
||||
value={note}
|
||||
onChange={(event) => setNote(event.target.value)}
|
||||
maxLength={ENGAGEMENT_LEVEL_TEXT_MAX_LENGTH}
|
||||
rows={2}
|
||||
className="min-h-16 resize-y"
|
||||
placeholder="O que mudou para justificar este nível?"
|
||||
<NativeSelectOption value="">{EMPTY_ENGAGEMENT_LEVEL_LABEL}</NativeSelectOption>
|
||||
{engagementLevels.map((option) => (
|
||||
<NativeSelectOption key={option} value={option}>
|
||||
{formatEngagementLevelLabel(option)}
|
||||
</NativeSelectOption>
|
||||
))}
|
||||
</NativeSelect>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor={fieldId('note')}>Motivo</FieldLabel>
|
||||
<Textarea
|
||||
id={fieldId('note')}
|
||||
value={note}
|
||||
onChange={(event) => setNote(event.target.value)}
|
||||
maxLength={ENGAGEMENT_LEVEL_TEXT_MAX_LENGTH}
|
||||
rows={2}
|
||||
className="min-h-16 resize-y"
|
||||
placeholder="O que mudou para justificar este nível?"
|
||||
/>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor={fieldId('reversal')}>O que faria voltar atrás</FieldLabel>
|
||||
<Textarea
|
||||
id={fieldId('reversal')}
|
||||
value={reversalSignals}
|
||||
onChange={(event) => setReversalSignals(event.target.value)}
|
||||
maxLength={ENGAGEMENT_LEVEL_TEXT_MAX_LENGTH}
|
||||
rows={2}
|
||||
className="min-h-16 resize-y"
|
||||
placeholder="Sinais que fariam rever esta decisão."
|
||||
/>
|
||||
</Field>
|
||||
{isJump ? (
|
||||
<Field orientation="horizontal">
|
||||
<Checkbox
|
||||
id={fieldId('shock')}
|
||||
checked={triangulatedShock}
|
||||
onCheckedChange={(checked) => setTriangulatedShock(checked === true)}
|
||||
/>
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor={fieldId('shock')}>
|
||||
Choque triangulado (dois níveis de uma vez)
|
||||
</FieldLabel>
|
||||
</FieldContent>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor={fieldId('reversal')}>O que faria voltar atrás</FieldLabel>
|
||||
<Textarea
|
||||
id={fieldId('reversal')}
|
||||
value={reversalSignals}
|
||||
onChange={(event) => setReversalSignals(event.target.value)}
|
||||
maxLength={ENGAGEMENT_LEVEL_TEXT_MAX_LENGTH}
|
||||
rows={2}
|
||||
className="min-h-16 resize-y"
|
||||
placeholder="Sinais que fariam rever esta decisão."
|
||||
/>
|
||||
</Field>
|
||||
{isJump ? (
|
||||
) : null}
|
||||
{violations.length > 0 ? (
|
||||
<>
|
||||
<Alert className="py-2">
|
||||
<AlertDescription className="text-xs">
|
||||
<ul className="list-disc space-y-1 pl-4">
|
||||
{violations.map((violation) => (
|
||||
<li key={violation.id}>{violation.message}</li>
|
||||
))}
|
||||
</ul>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
<Field orientation="horizontal">
|
||||
<Checkbox
|
||||
id={fieldId('shock')}
|
||||
checked={triangulatedShock}
|
||||
onCheckedChange={(checked) => setTriangulatedShock(checked === true)}
|
||||
id={fieldId('override')}
|
||||
checked={override}
|
||||
onCheckedChange={(checked) => setOverride(checked === true)}
|
||||
/>
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor={fieldId('shock')}>
|
||||
Choque triangulado (dois níveis de uma vez)
|
||||
<FieldLabel htmlFor={fieldId('override')}>
|
||||
Registrar mesmo assim, ciente das ressalvas
|
||||
</FieldLabel>
|
||||
</FieldContent>
|
||||
</Field>
|
||||
) : null}
|
||||
{violations.length > 0 ? (
|
||||
<>
|
||||
<Alert className="py-2">
|
||||
<AlertDescription className="text-xs">
|
||||
<ul className="list-disc space-y-1 pl-4">
|
||||
{violations.map((violation) => (
|
||||
<li key={violation.id}>{violation.message}</li>
|
||||
))}
|
||||
</ul>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
<Field orientation="horizontal">
|
||||
<Checkbox
|
||||
id={fieldId('override')}
|
||||
checked={override}
|
||||
onCheckedChange={(checked) => setOverride(checked === true)}
|
||||
/>
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor={fieldId('override')}>
|
||||
Registrar mesmo assim, ciente das ressalvas
|
||||
</FieldLabel>
|
||||
</FieldContent>
|
||||
</Field>
|
||||
</>
|
||||
) : null}
|
||||
{errorMessage ? (
|
||||
<Alert variant="destructive" className="py-2">
|
||||
<AlertDescription className="text-xs">{errorMessage}</AlertDescription>
|
||||
</Alert>
|
||||
) : null}
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
disabled={!canSubmit || isPending || (violations.length > 0 && !override)}
|
||||
onClick={() => void submit()}
|
||||
>
|
||||
{isPending ? <Spinner className="size-3.5" aria-hidden /> : null}
|
||||
Registrar movimento
|
||||
</Button>
|
||||
<p className="sr-only" aria-live="polite">
|
||||
{isPending ? 'Registrando nível.' : (errorMessage ?? '')}
|
||||
</p>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</>
|
||||
) : null}
|
||||
{errorMessage ? (
|
||||
<Alert variant="destructive" className="py-2">
|
||||
<AlertDescription className="text-xs">{errorMessage}</AlertDescription>
|
||||
</Alert>
|
||||
) : null}
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
disabled={!canSubmit || isPending || (violations.length > 0 && !override)}
|
||||
onClick={() => void submit()}
|
||||
>
|
||||
{isPending ? <Spinner className="size-3.5" aria-hidden /> : null}
|
||||
Registrar movimento
|
||||
</Button>
|
||||
<p className="sr-only" aria-live="polite">
|
||||
{isPending ? 'Registrando nível.' : (errorMessage ?? '')}
|
||||
</p>
|
||||
</div>
|
||||
</CampaignCellEditOverlay>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue