Merge pull request #323 from fsolla/cursor/pool-i311-f0d9725a-dfcb

B143 — Lideranças — filtrar por organização e dobradinha na omnibox
This commit is contained in:
Francisco Solla 2026-08-02 20:19:41 -03:00 committed by GitHub
commit c90c7d3722
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 444 additions and 16 deletions

View file

@ -0,0 +1,63 @@
# Impl: Lideranças — filtrar por organização e dobradinha na omnibox
Status: aprovado
Atualizado em: 2026-08-02
Issue: #311
Intenção: docs/plans/liderancas-omnibox-org-dobradinha.md
Appetite restante: herdado (~0,5–1 dia eng)
## Leitura da intenção
- **Outcome:** Staff filtra a lista `/campanha/liderancas` por organização e/ou dobradinha via omnibox (sugestões tipadas + chips), com semântica OR inclusiva por dimensão, sem expor estimativas.
- **O que NÃO negociar:** lista continua staff-only; sem criação de org/dobradinha na barra; sem filtro “sem organização/dobradinha”; sem estimatedVotes.
- **O que reavaliar:** facet cross-filtered (como município) em vez de carregar catálogo inteiro; params URL `organization` / `stateDeputy` (singular, espelho de `municipality`).
## Abordagem recomendada
```mermaid
flowchart LR
URL["leadershipListUrl"] --> Where["buildLeadershipListWhere"]
URL --> Omnibox["leadershipOmnibox"]
Data["leadershipData facets"] --> Page["liderancas/page"]
Page --> Filters["LeadershipFilters"]
Filters --> Omnibox
```
**Opções consideradas:** A) facet cross-filtered + omnibox · B) catálogo completo no client · C) só uma dimensão
**Recomendação:** A — precedente município/status; busca tipada escala; URL canônica reutiliza `campaignListUrl` helpers.
**Rejeitadas:** B (lista grande, sem cross-filter); C (aceite pede ambas).
### Componentes / mudanças
- **`leadershipListUrl.ts`:** state `organizations` / `stateDeputies`; parse/serialize `organization` / `stateDeputy`; `where` com `in` OR.
- **`leadershipListFilters.ts`:** toggles multi-valor (mesmo contrato de município).
- **`leadershipData.ts`:** facets `organizationIDs` / `stateDeputyIDs` (ignoram próprio filtro, unem seleção).
- **`leadershipOmnibox.ts`:** chips, seeds, apply/remove para `organization:` / `stateDeputy:`.
- **`LeadershipFilters.tsx` + `page.tsx`:** opções de facet com rótulos; placeholder atualizado.
- **Migration:** sem migration.
- **Access / Consent:** inalterado (staff guard existente).
- **UI:** Impeccable B — encaixe na omnibox existente; sem header filter novo.
## Fases verificáveis
1. **URL + where + filters** — parsers, toggles, `buildLeadershipListWhere`; unit `leadershipListUrl`.
2. **Facets + page wiring** — `leadershipData`; int filter por organização.
3. **Omnibox** — adapter + `LeadershipFilters`; unit B128.
4. **Gates** — `pnpm gate:fast`; push via `pnpm push`.
## Rabbit holes / Não escopo (engenharia)
- Header filter popover para org/dobradinha (só omnibox neste item).
- Saved filters; sentinela “sem vínculo”.
- Alterar omnibox de organizações/dobradinhas (Issues irmãs).
## Riscos e mitigação
- **Facet pesado:** `select` só do campo relacional, `limit: 0` — mesmo padrão município.
- **Rótulo de chip ausente:** unir ID selecionado no facet mesmo fora do recorte atual.
## Aceite de engenharia
- [x] Aceite de produto da intenção ainda coberto
- [x] Invariantes AGENTS/engineering-standards
- [x] Testes de domínio previstos (unit URL/omnibox; int filter org)

View file

@ -73,7 +73,9 @@ import {
resolveLeadershipListUrl,
type LeadershipListState,
} from '@/utilities/leadership/leadershipListUrl'
import { loadOrganizationNamesByIds } from '@/utilities/loadNamesByIds'
import { loadMunicipalityPortfolioIndex } from '@/utilities/municipality/municipalityPortfolioIndex'
import { loadStateDeputySummaries } from '@/utilities/stateDeputyData'
import {
setLeadershipMunicipalitiesFormAction,
@ -352,6 +354,30 @@ export default async function LeadershipsPage({ searchParams }: LeadershipsPageP
.filter((option): option is LeadershipFilterOption => option !== null)
.sort((left, right) => left.label.localeCompare(right.label, 'pt-BR'))
const [organizationNames, stateDeputySummaries] = await Promise.all([
loadOrganizationNamesByIds(payload, filterFacets.organizationIDs),
loadStateDeputySummaries(payload, filterFacets.stateDeputyIDs),
])
const organizationFilterOptions: LeadershipFilterOption[] = filterFacets.organizationIDs
.map((id) => {
const label = organizationNames.get(id)
return label ? { value: String(id), label } : null
})
.filter((option): option is LeadershipFilterOption => option !== null)
.sort((left, right) => left.label.localeCompare(right.label, 'pt-BR'))
const stateDeputyById = new Map(stateDeputySummaries.map((summary) => [summary.id, summary]))
const stateDeputyFilterOptions: LeadershipFilterOption[] = filterFacets.stateDeputyIDs
.map((id) => {
const summary = stateDeputyById.get(id)
if (!summary) return null
const label = summary.party ? `${summary.name} (${summary.party})` : summary.name
return { value: String(id), label }
})
.filter((option): option is LeadershipFilterOption => option !== null)
.sort((left, right) => left.label.localeCompare(right.label, 'pt-BR'))
const { sort, dir } = resolveLeadershipListSort(state)
const sortSummary = formatLeadershipListSortSummary(sort, dir)
const columns = leadershipColumns({
@ -384,7 +410,12 @@ export default async function LeadershipsPage({ searchParams }: LeadershipsPageP
</div>
<CampaignListPendingBoundary>
<LeadershipFilters state={state} municipalityFilterOptions={municipalityFilterOptions} />
<LeadershipFilters
state={state}
municipalityFilterOptions={municipalityFilterOptions}
organizationFilterOptions={organizationFilterOptions}
stateDeputyFilterOptions={stateDeputyFilterOptions}
/>
<CampaignListResults>
<p className="text-sm text-muted-foreground" aria-live="polite">

View file

@ -22,9 +22,13 @@ import {
export const LeadershipFilters = ({
state,
municipalityFilterOptions,
organizationFilterOptions,
stateDeputyFilterOptions,
}: {
state: LeadershipListState
municipalityFilterOptions: LeadershipFilterOption[]
organizationFilterOptions: LeadershipFilterOption[]
stateDeputyFilterOptions: LeadershipFilterOption[]
}) => {
const { navigate, isPending } = useCampaignListFilterNavigation({
state,
@ -41,14 +45,49 @@ export const LeadershipFilters = ({
return map
}, [municipalityFilterOptions])
const organizationLabelsById = useMemo(() => {
const map = new Map<number, string>()
for (const option of organizationFilterOptions) {
const id = Number(option.value)
if (Number.isSafeInteger(id) && id > 0) map.set(id, option.label)
}
for (const organizationId of state.organizations ?? []) {
if (!map.has(organizationId)) map.set(organizationId, `Organização #${organizationId}`)
}
return map
}, [organizationFilterOptions, state.organizations])
const stateDeputyLabelsById = useMemo(() => {
const map = new Map<number, string>()
for (const option of stateDeputyFilterOptions) {
const id = Number(option.value)
if (Number.isSafeInteger(id) && id > 0) map.set(id, option.label)
}
for (const stateDeputyId of state.stateDeputies ?? []) {
if (!map.has(stateDeputyId)) map.set(stateDeputyId, `Dobradinha #${stateDeputyId}`)
}
return map
}, [stateDeputyFilterOptions, state.stateDeputies])
const chips = useMemo(
() => buildLeadershipOmniboxChips({ state, municipalityLabelsById }),
[state, municipalityLabelsById],
() =>
buildLeadershipOmniboxChips({
state,
municipalityLabelsById,
organizationLabelsById,
stateDeputyLabelsById,
}),
[state, municipalityLabelsById, organizationLabelsById, stateDeputyLabelsById],
)
const suggestionSeeds = useMemo(
() => buildLeadershipOmniboxSuggestionSeeds({ municipalityFilterOptions }),
[municipalityFilterOptions],
() =>
buildLeadershipOmniboxSuggestionSeeds({
municipalityFilterOptions,
organizationFilterOptions,
stateDeputyFilterOptions,
}),
[municipalityFilterOptions, organizationFilterOptions, stateDeputyFilterOptions],
)
const suggestions = useMemo(
@ -75,7 +114,7 @@ export const LeadershipFilters = ({
<CampaignListOmnibox
id="leadership-omnibox"
label="Filtrar lideranças"
placeholder="Digite para filtrar (status, município, acesso…)"
placeholder="Digite para filtrar (status, município, organização, dobradinha…)"
chips={chips}
suggestions={suggestions}
query={query}

View file

@ -189,6 +189,10 @@ export const loadFreshestMunicipalityLeaderships = async (
export type LeadershipListFilterFacets = {
/** Municipality ids present under the other active filters (cross-filtered). */
municipalityIDs: number[]
/** Organization ids present under the other active filters (cross-filtered). */
organizationIDs: number[]
/** State deputy ids present under the other active filters (cross-filtered). */
stateDeputyIDs: number[]
}
/**
@ -200,7 +204,7 @@ const loadLeadershipMunicipalityFacet = async (
payload: Payload,
user: CampaignUser,
state: LeadershipListState,
): Promise<LeadershipListFilterFacets> => {
): Promise<number[]> => {
const result = await payload.find({
collection: 'leadership',
where: buildLeadershipListWhere({ ...state, municipalities: undefined }),
@ -220,9 +224,74 @@ const loadLeadershipMunicipalityFacet = async (
}
}
return {
municipalityIDs: [...available].sort((left, right) => left - right),
return [...available].sort((left, right) => left - right)
}
const loadLeadershipOrganizationFacet = async (
payload: Payload,
user: CampaignUser,
state: LeadershipListState,
): Promise<number[]> => {
const result = await payload.find({
collection: 'leadership',
where: buildLeadershipListWhere({ ...state, organizations: undefined }),
depth: 0,
limit: 0,
pagination: false,
select: { organizations: true },
user,
overrideAccess: false,
})
const available = new Set<number>(state.organizations ?? [])
for (const doc of result.docs) {
for (const organization of doc.organizations ?? []) {
const id = relationshipId(organization)
if (id !== null) available.add(id)
}
}
return [...available].sort((left, right) => left - right)
}
const loadLeadershipStateDeputyFacet = async (
payload: Payload,
user: CampaignUser,
state: LeadershipListState,
): Promise<number[]> => {
const result = await payload.find({
collection: 'leadership',
where: buildLeadershipListWhere({ ...state, stateDeputies: undefined }),
depth: 0,
limit: 0,
pagination: false,
select: { stateDeputies: true },
user,
overrideAccess: false,
})
const available = new Set<number>(state.stateDeputies ?? [])
for (const doc of result.docs) {
for (const stateDeputy of doc.stateDeputies ?? []) {
const id = relationshipId(stateDeputy)
if (id !== null) available.add(id)
}
}
return [...available].sort((left, right) => left - right)
}
const loadLeadershipFilterFacets = async (
payload: Payload,
user: CampaignUser,
state: LeadershipListState,
): Promise<LeadershipListFilterFacets> => {
const [municipalityIDs, organizationIDs, stateDeputyIDs] = await Promise.all([
loadLeadershipMunicipalityFacet(payload, user, state),
loadLeadershipOrganizationFacet(payload, user, state),
loadLeadershipStateDeputyFacet(payload, user, state),
])
return { municipalityIDs, organizationIDs, stateDeputyIDs }
}
export const loadLeadershipListPageData = async (
@ -236,7 +305,12 @@ export const loadLeadershipListPageData = async (
filterFacets: LeadershipListFilterFacets
}> => {
if (!isCampaignStaff(user)) {
return { rows: [], totalDocs: 0, totalPages: 0, filterFacets: { municipalityIDs: [] } }
return {
rows: [],
totalDocs: 0,
totalPages: 0,
filterFacets: { municipalityIDs: [], organizationIDs: [], stateDeputyIDs: [] },
}
}
const { sort, dir } = resolveLeadershipListSort(state)
@ -251,7 +325,7 @@ export const loadLeadershipListPageData = async (
user,
overrideAccess: false,
}),
loadLeadershipMunicipalityFacet(payload, user, state),
loadLeadershipFilterFacets(payload, user, state),
])
return {

View file

@ -22,7 +22,7 @@ export type LeadershipFilterOption = {
label: string
}
type LeadershipMultiFilterParam = 'status' | 'municipality'
type LeadershipMultiFilterParam = 'status' | 'municipality' | 'organization' | 'stateDeputy'
export const leadershipStatusFilterOptions: LeadershipFilterOption[] =
leadershipSupportStatuses.map((value) => ({ value, label: supportStatusLabels[value] }))
@ -53,6 +53,8 @@ const getLeadershipMultiFilterValues = (
param: LeadershipMultiFilterParam,
): string[] => {
if (param === 'status') return state.statuses ?? []
if (param === 'organization') return (state.organizations ?? []).map(String)
if (param === 'stateDeputy') return (state.stateDeputies ?? []).map(String)
return (state.municipalities ?? []).map(String)
}
@ -89,6 +91,29 @@ export const clearLeadershipMunicipalityFilter = (
state: LeadershipListState,
): LeadershipListState => setLeadershipMultiFilterValues(state, 'municipality', [])
export const toggleLeadershipOrganizationFilter = (
state: LeadershipListState,
value: string,
): LeadershipListState => {
if (strictDecimalInteger(value) === undefined) return state
return toggleLeadershipMultiFilterValue(state, 'organization', value)
}
export const clearLeadershipOrganizationFilter = (
state: LeadershipListState,
): LeadershipListState => setLeadershipMultiFilterValues(state, 'organization', [])
export const toggleLeadershipStateDeputyFilter = (
state: LeadershipListState,
value: string,
): LeadershipListState => {
if (strictDecimalInteger(value) === undefined) return state
return toggleLeadershipMultiFilterValue(state, 'stateDeputy', value)
}
export const clearLeadershipStateDeputyFilter = (state: LeadershipListState): LeadershipListState =>
setLeadershipMultiFilterValues(state, 'stateDeputy', [])
/** Exclusive toggle — selecting the active value clears; selecting the other replaces. */
export const toggleLeadershipAccessFilter = (
state: LeadershipListState,

View file

@ -31,6 +31,8 @@ export type LeadershipListState = {
q?: string
statuses?: SupportStatus[]
municipalities?: number[]
organizations?: number[]
stateDeputies?: number[]
access?: LeadershipListAccessFilter
sort?: LeadershipListSortKey
dir?: LeadershipListSortDirection
@ -42,6 +44,8 @@ const leadershipListParamNames = [
'q',
'status',
'municipality',
'organization',
'stateDeputy',
'access',
'sort',
'dir',
@ -105,6 +109,8 @@ export const leadershipListStateToRawParams = (
q: state.q,
status: state.statuses,
municipality: state.municipalities?.map(String),
organization: state.organizations?.map(String),
stateDeputy: state.stateDeputies?.map(String),
access: state.access,
sort: state.sort,
dir: state.dir,
@ -119,6 +125,12 @@ export const parseLeadershipListParams = (
const municipalities = allParamValues(params.municipality)
.map((token) => strictDecimalInteger(token))
.filter((id): id is number => typeof id === 'number' && id > 0)
const organizations = allParamValues(params.organization)
.map((token) => strictDecimalInteger(token))
.filter((id): id is number => typeof id === 'number' && id > 0)
const stateDeputies = allParamValues(params.stateDeputy)
.map((token) => strictDecimalInteger(token))
.filter((id): id is number => typeof id === 'number' && id > 0)
const rawAccess = firstValue(params.access)
const access =
rawAccess && accessFilterSet.has(rawAccess as LeadershipListAccessFilter)
@ -140,6 +152,8 @@ export const parseLeadershipListParams = (
...(q ? { q } : {}),
...(statuses.length ? { statuses } : {}),
...(municipalities.length ? { municipalities } : {}),
...(organizations.length ? { organizations } : {}),
...(stateDeputies.length ? { stateDeputies } : {}),
...(access ? { access } : {}),
...(sort ? { sort } : {}),
...(dir ? { dir } : {}),
@ -153,6 +167,12 @@ export const buildLeadershipListWhere = (state: LeadershipListState): Where => {
if (state.municipalities?.length) {
filters.push({ municipalities: { in: state.municipalities } })
}
if (state.organizations?.length) {
filters.push({ organizations: { in: state.organizations } })
}
if (state.stateDeputies?.length) {
filters.push({ stateDeputies: { in: state.stateDeputies } })
}
if (state.access) {
filters.push({ user: { exists: state.access === 'com' } })
}
@ -175,6 +195,12 @@ export const serializeCanonicalLeadershipListSearchParams = (
for (const municipality of canonicalState.municipalities ?? []) {
params.append('municipality', String(municipality))
}
for (const organization of canonicalState.organizations ?? []) {
params.append('organization', String(organization))
}
for (const stateDeputy of canonicalState.stateDeputies ?? []) {
params.append('stateDeputy', String(stateDeputy))
}
if (canonicalState.access) params.set('access', canonicalState.access)
if (!isSortDefault) {
params.set('sort', resolvedSort)

View file

@ -17,6 +17,8 @@ import {
leadershipStatusFilterOptions,
toggleLeadershipAccessFilter,
toggleLeadershipMunicipalityFilter,
toggleLeadershipOrganizationFilter,
toggleLeadershipStateDeputyFilter,
toggleLeadershipStatusFilter,
type LeadershipFilterOption,
} from '@/utilities/leadership/leadershipListFilters'
@ -64,9 +66,13 @@ const leadershipListSortOptions = (
export const buildLeadershipOmniboxChips = ({
state,
municipalityLabelsById,
organizationLabelsById,
stateDeputyLabelsById,
}: {
state: LeadershipListState
municipalityLabelsById: ReadonlyMap<number, string>
organizationLabelsById: ReadonlyMap<number, string>
stateDeputyLabelsById: ReadonlyMap<number, string>
}): CampaignListOmniboxChip[] => {
const chips: CampaignListOmniboxChip[] = []
@ -86,6 +92,26 @@ export const buildLeadershipOmniboxChips = ({
})
}
for (const organizationId of state.organizations ?? []) {
chips.push({
id: `organization:${organizationId}`,
label: chipLabel(
'Organização',
organizationLabelsById.get(organizationId) ?? `Organização #${organizationId}`,
),
})
}
for (const stateDeputyId of state.stateDeputies ?? []) {
chips.push({
id: `stateDeputy:${stateDeputyId}`,
label: chipLabel(
'Dobradinha',
stateDeputyLabelsById.get(stateDeputyId) ?? `Dobradinha #${stateDeputyId}`,
),
})
}
if (state.access) {
chips.push({
id: `access:${state.access}`,
@ -109,8 +135,12 @@ export const buildLeadershipOmniboxChips = ({
export const buildLeadershipOmniboxSuggestionSeeds = ({
municipalityFilterOptions,
organizationFilterOptions,
stateDeputyFilterOptions,
}: {
municipalityFilterOptions: readonly LeadershipFilterOption[]
organizationFilterOptions: readonly LeadershipFilterOption[]
stateDeputyFilterOptions: readonly LeadershipFilterOption[]
}) => {
const seeds = []
@ -139,6 +169,28 @@ export const buildLeadershipOmniboxSuggestionSeeds = ({
)
}
for (const option of organizationFilterOptions) {
seeds.push(
createOmniboxSuggestionSeed({
id: `organization:${option.value}`,
group: 'Organização',
label: option.label,
keywords: ['organizacao', 'org', 'sindicato'],
}),
)
}
for (const option of stateDeputyFilterOptions) {
seeds.push(
createOmniboxSuggestionSeed({
id: `stateDeputy:${option.value}`,
group: 'Dobradinha',
label: option.label,
keywords: ['dobradinha', 'deputado'],
}),
)
}
for (const option of leadershipAccessFilterOptions) {
seeds.push(
createOmniboxSuggestionSeed(
@ -198,6 +250,20 @@ export const applyLeadershipOmniboxSuggestion = ({
}
}
if (suggestionId.startsWith('organization:')) {
return {
kind: 'url',
state: toggleLeadershipOrganizationFilter(state, suggestionId.slice(13)),
}
}
if (suggestionId.startsWith('stateDeputy:')) {
return {
kind: 'url',
state: toggleLeadershipStateDeputyFilter(state, suggestionId.slice(12)),
}
}
if (suggestionId.startsWith('access:')) {
return {
kind: 'url',
@ -252,6 +318,20 @@ export const removeLeadershipOmniboxChip = ({
}
}
if (chipId.startsWith('organization:')) {
return {
kind: 'url',
state: toggleLeadershipOrganizationFilter(state, chipId.slice(13)),
}
}
if (chipId.startsWith('stateDeputy:')) {
return {
kind: 'url',
state: toggleLeadershipStateDeputyFilter(state, chipId.slice(12)),
}
}
if (chipId.startsWith('access:')) {
return { kind: 'url', state: toggleLeadershipAccessFilter(state, chipId.slice(7)) }
}

View file

@ -68,7 +68,7 @@ describe('loadLeadershipListPageData', () => {
rows: [],
totalDocs: 0,
totalPages: 0,
filterFacets: { municipalityIDs: [] },
filterFacets: { municipalityIDs: [], organizationIDs: [], stateDeputyIDs: [] },
})
})
@ -82,10 +82,40 @@ describe('loadLeadershipListPageData', () => {
rows: [],
totalDocs: 0,
totalPages: 0,
filterFacets: { municipalityIDs: [] },
filterFacets: { municipalityIDs: [], organizationIDs: [], stateDeputyIDs: [] },
})
})
it('filters by organization with inclusive OR within the dimension', async () => {
const fixtures = campaignFixtures()
const coordinator = await fixtures.createCampaignUser('coordinator')
const municipality = await fixtures.getMunicipality()
const marker = fixtures.value('lead-org-filter')
const organizationA = await fixtures.createOrganization({ name: `Org A ${marker}` })
const organizationB = await fixtures.createOrganization({ name: `Org B ${marker}` })
const matchContact = await fixtures.createContact({ name: `Ana ${marker}` })
const missContact = await fixtures.createContact({ name: `Bruno ${marker}` })
const match = await fixtures.createLeadership({
contact: matchContact.id,
municipalities: [municipality.id],
organizations: [organizationA.id],
})
await fixtures.createLeadership({
contact: missContact.id,
municipalities: [municipality.id],
organizations: [organizationB.id],
})
const byOrganization = await loadLeadershipListPageData(payload, coordinator, {
page: 1,
q: marker,
organizations: [organizationA.id],
})
expect(byOrganization.rows.map((row) => row.id)).toEqual([match.id])
expect(byOrganization.filterFacets.organizationIDs).toContain(organizationA.id)
expect(byOrganization.filterFacets.organizationIDs).toContain(organizationB.id)
})
it('filters by supportStatus, access and municipality', async () => {
const fixtures = campaignFixtures()
const coordinator = await fixtures.createCampaignUser('coordinator')

View file

@ -4,10 +4,14 @@ import {
clearLeadershipAccessFilter,
clearLeadershipListFilters,
clearLeadershipMunicipalityFilter,
clearLeadershipOrganizationFilter,
clearLeadershipStateDeputyFilter,
clearLeadershipStatusFilter,
formatLeadershipActiveFiltersSummary,
toggleLeadershipAccessFilter,
toggleLeadershipMunicipalityFilter,
toggleLeadershipOrganizationFilter,
toggleLeadershipStateDeputyFilter,
toggleLeadershipStatusFilter,
} from '@/utilities/leadership/leadershipListFilters'
import {
@ -44,11 +48,15 @@ describe('leadership list URL contract', () => {
status: ['engajado', 'a_abordar', 'engajado', 'unknown'],
sector: ['religioso'],
municipality: ['12', '12', '0', 'abc', '3'],
organization: ['5', '5', '0', 'abc', '8'],
stateDeputy: ['21', '21', '0', 'abc', '34'],
}),
).toEqual({
page: 1,
statuses: ['engajado', 'a_abordar'],
municipalities: [12, 3],
organizations: [5, 8],
stateDeputies: [21, 34],
})
})
@ -84,12 +92,14 @@ describe('leadership list URL contract', () => {
status: ['a_abordar'],
sector: ['religioso'],
municipality: ['7'],
organization: ['5', '8'],
stateDeputy: ['21'],
access: 'sem',
sort: 'name',
dir: 'asc',
})
expect(buildLeadershipListHref(state, 1)).toBe(
'/campanha/liderancas?q=ana&status=a_abordar&municipality=7&access=sem&sort=name',
'/campanha/liderancas?q=ana&status=a_abordar&municipality=7&organization=5&organization=8&stateDeputy=21&access=sem&sort=name',
)
})
@ -155,6 +165,16 @@ describe('leadership list filter state', () => {
expect(clearLeadershipAccessFilter(withAccess)).toEqual({ page: 1 })
})
it('toggles organization and stateDeputy filters and resets the page', () => {
const withOrganization = toggleLeadershipOrganizationFilter({ page: 2 }, '15')
expect(withOrganization).toEqual({ page: 1, organizations: [15] })
expect(toggleLeadershipOrganizationFilter(withOrganization, '15')).toEqual({ page: 1 })
const withStateDeputy = toggleLeadershipStateDeputyFilter({ page: 2 }, '9')
expect(withStateDeputy).toEqual({ page: 1, stateDeputies: [9] })
expect(toggleLeadershipStateDeputyFilter(withStateDeputy, '9')).toEqual({ page: 1 })
})
it('clears only one filter, keeping q and sort', () => {
expect(
clearLeadershipStatusFilter({
@ -172,6 +192,21 @@ describe('leadership list filter state', () => {
access: 'sem',
}),
).toEqual({ page: 1, access: 'sem' })
expect(
clearLeadershipOrganizationFilter({
page: 1,
organizations: [5],
q: 'ana',
}),
).toEqual({ page: 1, q: 'ana' })
expect(
clearLeadershipStateDeputyFilter({
page: 1,
stateDeputies: [3],
sort: 'name',
dir: 'asc',
}),
).toEqual({ page: 1, sort: 'name', dir: 'asc' })
})
it('clears every filter and the search, keeping sort', () => {

View file

@ -78,11 +78,36 @@ describe('list omnibox adapters (B128)', () => {
expect(applied.state.statuses).toContain('engajado')
}
const seeds = buildLeadershipOmniboxSuggestionSeeds({ municipalityFilterOptions: [] })
const seeds = buildLeadershipOmniboxSuggestionSeeds({
municipalityFilterOptions: [],
organizationFilterOptions: [],
stateDeputyFilterOptions: [],
})
const suggestions = filterLeadershipOmniboxSuggestions(seeds, 'ordenar')
expect(suggestions.some((entry) => entry.group === 'Ordenação')).toBe(true)
})
it('leadership applies organization and stateDeputy toggles', () => {
const base = parseLeadershipListParams({})
const organization = applyLeadershipOmniboxSuggestion({
state: base,
suggestionId: 'organization:12',
})
expect(organization.kind).toBe('url')
if (organization.kind === 'url') {
expect(organization.state.organizations).toEqual([12])
}
const stateDeputy = applyLeadershipOmniboxSuggestion({
state: base,
suggestionId: 'stateDeputy:7',
})
expect(stateDeputy.kind).toBe('url')
if (stateDeputy.kind === 'url') {
expect(stateDeputy.state.stateDeputies).toEqual([7])
}
})
it('state deputy toggles party filter', () => {
const base = parseStateDeputyListParams({})
const applied = applyStateDeputyOmniboxSuggestion({