B91 — Global search in mobile quick-actions drawer

Extract CampaignGlobalSearchMount (provider + body) from home chrome
so the same home-search contract mounts in CampaignQuickActionsDrawer
when expanded. Refactor CampaignHomeStaffChrome to use the provider.

Closes #55

Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
This commit is contained in:
Cursor Agent 2026-08-01 05:42:59 +00:00
parent 90d130af33
commit d639988b83
No known key found for this signature in database
4 changed files with 128 additions and 21 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,15 @@
import type { ReactNode } from 'react'
import { 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 +24,7 @@ const CampaignHomeStaffSearchSlot = ({ searchResults }: { searchResults: ReactNo
)
}
export const CampaignHomeStaffChrome = ({
const CampaignHomeStaffChromeInner = ({
actions,
searchResults,
summarySlot,
@ -34,25 +34,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} />
) : (
<CampaignStaffGlobalSearchBody />
)
}
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

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

View file

@ -0,0 +1,67 @@
import { cleanup, render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
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', async () => {
vi.mocked(postCampaignJson).mockResolvedValue({
ok: true,
payload: idleSuggestPayload,
})
const user = userEvent.setup()
render(<CampaignQuickActionsDrawer actions={[]} />)
expect(screen.queryByLabelText('Buscar na campanha')).toBeNull()
await user.click(screen.getByRole('button', { name: 'Mostrar ações rápidas' }))
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,
})
const user = userEvent.setup()
render(<CampaignQuickActionsDrawer actions={[]} />)
await user.click(screen.getByRole('button', { name: 'Mostrar ações rápidas' }))
await user.type(screen.getByLabelText('Buscar na campanha'), 'cairu')
await vi.waitFor(() => {
expect(postCampaignJson).toHaveBeenCalledWith(
'/campanha/home-search',
{ mode: 'search', query: 'cairu' },
expect.any(AbortSignal),
)
})
})
})