fix(C94): draft mirror p/ picks cumulativos no omnibox + simplificações do /simplify

This commit is contained in:
Francisco Solla 2026-08-09 02:39:53 -03:00
parent ad39602d04
commit f36cc244b7
6 changed files with 44 additions and 29 deletions

View file

@ -2,7 +2,7 @@
import { MapPinnedIcon, PlusIcon } from 'lucide-react'
import Link from 'next/link'
import { useMemo, useState } from 'react'
import { useEffect, useMemo, useState } from 'react'
import { CampaignListOmnibox } from '@/components/campaign/shared/CampaignListOmnibox'
import type { RelationOption } from '@/components/campaign/shared/RelationMultiSelect'
@ -35,6 +35,10 @@ type ActivityAgendaFiltersProps = {
* Deputado presente (chip bool), with the desktop create entries as icon
* buttons beside the bar. Mobile creates live in the FAB drawer, so the
* trailing icons are desktop-only. Filter state stays on the URL.
*
* A local `draft` mirrors the URL state so consecutive picks within the same
* RSC pending window apply cumulatively (a bare `state` prop would drop the
* first selection while the router.replace round-trip is in flight).
*/
export const ActivityAgendaFilters = ({
state,
@ -45,8 +49,11 @@ export const ActivityAgendaFilters = ({
state,
toHref: buildActivityAgendaHref,
})
const [draft, setDraft] = useState(state)
const [query, setQuery] = useState('')
useEffect(() => setDraft(state), [state])
const municipalityLabelsById = useMemo(() => {
const map = new Map<number, string>()
for (const option of municipalityOptions) {
@ -65,8 +72,8 @@ export const ActivityAgendaFilters = ({
)
const chips = useMemo(
() => buildActivityAgendaOmniboxChips({ state, municipalityLabelsById }),
[state, municipalityLabelsById],
() => buildActivityAgendaOmniboxChips({ state: draft, municipalityLabelsById }),
[draft, municipalityLabelsById],
)
const suggestionSeeds = useMemo(
@ -86,9 +93,11 @@ export const ActivityAgendaFilters = ({
const runAction = (action: ActivityAgendaOmniboxAction) => {
if (action.kind === 'clear') {
setQuery('')
setDraft({})
navigate({})
return
}
setDraft(action.state)
navigate(action.state)
}
@ -103,10 +112,10 @@ export const ActivityAgendaFilters = ({
onQueryChange={setQuery}
isPending={isPending}
onSelectSuggestion={(suggestionId) => {
runAction(applyActivityAgendaOmniboxSuggestion({ state, suggestionId }))
runAction(applyActivityAgendaOmniboxSuggestion({ state: draft, suggestionId }))
}}
onRemoveChip={(chipId) => {
runAction(removeActivityAgendaOmniboxChip({ state, chipId }))
runAction(removeActivityAgendaOmniboxChip({ state: draft, chipId }))
}}
onClearAll={() => {
runAction(clearActivityAgendaOmnibox())

View file

@ -1,7 +1,7 @@
'use client'
import { CalendarIcon } from 'lucide-react'
import { useCallback, useEffect, useState } from 'react'
import { useCallback, useEffect, useMemo, useState } from 'react'
import {
CalendarFeedDialog,
@ -42,22 +42,27 @@ export const AgendaFeedChrome = ({
}
}, [setContext, openFeed])
const headerButton = useMemo(
() => (
<Button
type="button"
variant="ghost"
size="icon"
className="size-11 hidden shrink-0 md:inline-flex"
aria-label="Link de import"
title={canGenerate ? 'Link de import' : 'Aplique filtros para gerar um link de import'}
disabled={!canGenerate}
onClick={openFeed}
>
<CalendarIcon className="size-5" aria-hidden />
</Button>
),
[canGenerate, openFeed],
)
return (
<>
<SetCampaignHeaderAction id="calendar-feed">
<Button
type="button"
variant="ghost"
size="icon"
className="size-11 hidden shrink-0 md:inline-flex"
aria-label="Link de import"
title={canGenerate ? 'Link de import' : 'Aplique filtros para gerar um link de import'}
disabled={!canGenerate}
onClick={openFeed}
>
<CalendarIcon className="size-5" aria-hidden />
</Button>
</SetCampaignHeaderAction>
<SetCampaignHeaderAction id="calendar-feed">{headerButton}</SetCampaignHeaderAction>
<CalendarFeedDialog
open={open}

View file

@ -85,10 +85,6 @@ export const CalendarFeedDialog = ({
setTimeout(() => setCopied(false), 2000)
}
const handleRevoke = async (feedId: number) => {
await onRevokeFeed(feedId)
}
const handleClose = (next: boolean) => {
if (next) return
setCreatedUrl(null)
@ -198,7 +194,7 @@ export const CalendarFeedDialog = ({
type="button"
variant="ghost"
size="icon"
onClick={() => handleRevoke(feed.id)}
onClick={() => void onRevokeFeed(feed.id)}
aria-label={`Revogar feed ${feed.label}`}
>
<Trash2Icon className="h-4 w-4 text-muted-foreground" />

View file

@ -1,6 +1,6 @@
'use client'
import type { ReactNode } from 'react'
import { Fragment, type ReactNode } from 'react'
import { CampaignAIHeaderButton } from '@/components/campaign/shell/ai/CampaignAIHeaderButton'
import { useCampaignHeaderActions } from '@/components/campaign/shell/CampaignPageChromeContext'
@ -15,7 +15,9 @@ export const CampaignDesktopHeader = ({ notificationBell }: { notificationBell?:
<SidebarTrigger />
<CampaignPageChromeDisplay layout="desktop" className="flex-1" />
<div className="ml-auto flex shrink-0 items-center gap-1">
{Object.values(headerActions)}
{Object.entries(headerActions).map(([id, node]) => (
<Fragment key={id}>{node}</Fragment>
))}
{notificationBell ?? null}
<CampaignAIHeaderButton />
</div>

View file

@ -56,7 +56,10 @@ const OverlayActionsChrome = ({
label: action.label,
icon: action.icon,
description: action.description,
href: action.href,
// Dialog-style actions win over a route when both are present
// (`CampaignHomeActionButton` renders the href branch without
// onClick, which would silently drop the action and the close).
href: action.onAction ? undefined : action.href,
onClick: action.onAction
? () => {
action.onAction?.()

View file

@ -20,7 +20,7 @@ export type ActivityAgendaOmniboxAction =
| { kind: 'url'; state: ActivityAgendaState }
| { kind: 'clear' }
export type ActivityAgendaFilterOption = {
type ActivityAgendaFilterOption = {
value: string
label: string
}