Merge pull request #85 from fsolla/cursor/pool-i55-655812f2-d74e

B91 — Busca global no bottom drawer (mobile)
This commit is contained in:
Francisco Solla 2026-08-01 02:59:57 -03:00 committed by GitHub
commit 03418faa2b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 132 additions and 23 deletions

View file

@ -0,0 +1,23 @@
'use client'
import type { ReactNode } from 'react'
import { CampaignStaffGlobalSearchBody } from '@/components/campaign/dashboard/CampaignStaffGlobalSearch'
import { HomeSearchProvider } from '@/components/campaign/dashboard/HomeSearchContext'
import { useHomeSearchQuery } from '@/components/campaign/dashboard/useHomeSearchQuery'
/** Owns debounce + focus state for any global search surface (Início, drawer B91). */
export const CampaignGlobalSearchProvider = ({ children }: { children: ReactNode }) => {
const searchState = useHomeSearchQuery()
return <HomeSearchProvider value={searchState}>{children}</HomeSearchProvider>
}
/** Input + result groups — requires `CampaignGlobalSearchProvider` unless using the mount. */
export const CampaignGlobalSearchBody = CampaignStaffGlobalSearchBody
/** Provider + default global search body (drawer, smoke tests). */
export const CampaignGlobalSearchMount = () => (
<CampaignGlobalSearchProvider>
<CampaignGlobalSearchBody />
</CampaignGlobalSearchProvider>
)

View file

@ -2,15 +2,17 @@
import type { ReactNode } from 'react'
import {
CampaignGlobalSearchBody,
CampaignGlobalSearchProvider,
} from '@/components/campaign/dashboard/CampaignGlobalSearchMount'
import { CampaignHomeLayout } from '@/components/campaign/dashboard/CampaignHomeLayout'
import { CampaignHomeSearch } from '@/components/campaign/dashboard/CampaignHomeSearch'
import { CampaignStaffGlobalSearchBody } from '@/components/campaign/dashboard/CampaignStaffGlobalSearch'
import { HomeSearchProvider } from '@/components/campaign/dashboard/HomeSearchContext'
import { useHomeSearch } from '@/components/campaign/dashboard/HomeSearchContext'
import {
HomeSearchResultsProvider,
useHomeSearchResultsState,
} from '@/components/campaign/dashboard/HomeSearchResultsContext'
import { useHomeSearchQuery } from '@/components/campaign/dashboard/useHomeSearchQuery'
const CampaignHomeStaffSearchSlot = ({ searchResults }: { searchResults: ReactNode }) => {
const searchResultsState = useHomeSearchResultsState()
@ -24,7 +26,7 @@ const CampaignHomeStaffSearchSlot = ({ searchResults }: { searchResults: ReactNo
)
}
export const CampaignHomeStaffChrome = ({
const CampaignHomeStaffChromeInner = ({
actions,
searchResults,
summarySlot,
@ -34,25 +36,40 @@ export const CampaignHomeStaffChrome = ({
searchResults?: ReactNode
summarySlot?: ReactNode
}) => {
const searchState = useHomeSearchQuery()
const focused = searchState.uiFocused
const { uiFocused: focused } = useHomeSearch()
return (
<HomeSearchProvider value={searchState}>
<div className="h-full min-h-0 w-full" data-home-focused={focused || undefined}>
<CampaignHomeLayout
actions={actions}
focused={focused}
searchSlot={
searchResults ? (
<CampaignHomeStaffSearchSlot searchResults={searchResults} />
) : (
<CampaignStaffGlobalSearchBody />
)
}
summarySlot={summarySlot}
/>
</div>
</HomeSearchProvider>
<div className="h-full min-h-0 w-full" data-home-focused={focused || undefined}>
<CampaignHomeLayout
actions={actions}
focused={focused}
searchSlot={
searchResults ? (
<CampaignHomeStaffSearchSlot searchResults={searchResults} />
) : (
<CampaignGlobalSearchBody />
)
}
summarySlot={summarySlot}
/>
</div>
)
}
export const CampaignHomeStaffChrome = ({
actions,
searchResults,
summarySlot,
}: {
actions: ReactNode
searchResults?: ReactNode
summarySlot?: ReactNode
}) => (
<CampaignGlobalSearchProvider>
<CampaignHomeStaffChromeInner
actions={actions}
searchResults={searchResults}
summarySlot={summarySlot}
/>
</CampaignGlobalSearchProvider>
)

View file

@ -2,6 +2,7 @@
import { useCallback, useState } from 'react'
import { CampaignGlobalSearchMount } from '@/components/campaign/dashboard/CampaignGlobalSearchMount'
import { CampaignHomeActionStrip } from '@/components/campaign/dashboard/CampaignHomeActionStrip'
import { Drawer, DrawerContent, DrawerTitle } from '@/components/ui/Drawer'
import {
@ -103,6 +104,7 @@ export const CampaignQuickActionsDrawer = ({
) : (
<p className="text-sm text-muted-foreground">Nenhuma ação rápida nesta página.</p>
)}
{expanded ? <CampaignGlobalSearchMount /> : null}
</div>
</DrawerContent>
</Drawer>

View file

@ -3,13 +3,13 @@ import { resolveStaffHomeQuickActions } from '@/lib/campaignHomeActions'
import { resolveMunicipalityQuickActionsForPath } from '@/lib/campaignMunicipalityQuickActions'
import { CAMPAIGN_TERRITORIES_HOME } from '@/lib/campaignPaths'
import type { CampaignQuickActionContext } from '@/lib/campaignQuickActionContext'
import { isLeaderContactsPath } from '@/lib/campaignQuickActionMount'
import {
isDemandDetailPath,
isDemandsListPath,
resolveDemandDetailQuickActions,
resolveDemandsListQuickActions,
} from '@/lib/campaignQuickActionDemands'
import { isLeaderContactsPath } from '@/lib/campaignQuickActionMount'
import {
parseActivityQuickActionSurface,
parseOrganizationQuickActionSurface,

View file

@ -0,0 +1,67 @@
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { CampaignQuickActionsDrawer } from '@/components/campaign/shell/CampaignQuickActionsDrawer'
import { postCampaignJson } from '@/lib/campaignJsonRequest'
vi.mock('@/lib/campaignJsonRequest', () => ({
postCampaignJson: vi.fn(),
}))
const idleSuggestPayload = {
status: 'success' as const,
resultKind: 'suggest' as const,
municipalities: [],
territories: [],
advisors: [],
leaderships: [],
stateDeputies: [],
activities: [],
demands: [],
}
afterEach(() => {
cleanup()
vi.mocked(postCampaignJson).mockReset()
})
describe('CampaignQuickActionsDrawer (B91)', () => {
it('shows global search in the expanded snap', () => {
vi.mocked(postCampaignJson).mockResolvedValue({
ok: true,
payload: idleSuggestPayload,
})
render(<CampaignQuickActionsDrawer actions={[]} />)
expect(screen.queryByLabelText('Buscar na campanha')).toBeNull()
fireEvent.click(screen.getByRole('button', { name: 'Mostrar ações rápidas' }))
const context = document.getElementById('quickActionContext')
expect(context?.className).not.toContain('hidden')
expect(context?.getAttribute('data-snap')).toBe('expanded')
expect(screen.getByLabelText('Buscar na campanha')).toBeTruthy()
expect(screen.getByRole('region', { name: 'Resultados da busca' })).toBeTruthy()
})
it('posts home-search when the drawer query is active', async () => {
vi.mocked(postCampaignJson).mockResolvedValue({
ok: true,
payload: idleSuggestPayload,
})
render(<CampaignQuickActionsDrawer actions={[]} />)
fireEvent.click(screen.getByRole('button', { name: 'Mostrar ações rápidas' }))
fireEvent.change(screen.getByLabelText('Buscar na campanha'), { target: { value: 'cairu' } })
await waitFor(() => {
expect(postCampaignJson).toHaveBeenCalledWith(
'/campanha/home-search',
{ mode: 'search', query: 'cairu' },
expect.any(AbortSignal),
)
})
})
})