From 0dc3d226468e2e12ded48d9b674272e4289ec950 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 2 Aug 2026 23:13:34 +0000 Subject: [PATCH 1/3] =?UTF-8?q?feat(nav):=20add=20Organiza=C3=A7=C3=B5es?= =?UTF-8?q?=20to=20staff=20sidebar=20(B144,=20#313)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Insert Organizações after Lideranças in staffNav so staff can discover the existing /campanha/organizacoes list from the structural nav. Leaders remain on leaderNav; isCampaignNavActive already covers list, detail, and /nova paths. Co-authored-by: Francisco Solla --- docs/plans/sidebar-organizacoes-impl.md | 61 +++++++++++++++++++++++++ src/components/campaign/shell/nav.ts | 2 + tests/unit/campaignNav.unit.spec.ts | 38 +++++++++++++++ 3 files changed, 101 insertions(+) create mode 100644 docs/plans/sidebar-organizacoes-impl.md create mode 100644 tests/unit/campaignNav.unit.spec.ts diff --git a/docs/plans/sidebar-organizacoes-impl.md b/docs/plans/sidebar-organizacoes-impl.md new file mode 100644 index 00000000..54837e0a --- /dev/null +++ b/docs/plans/sidebar-organizacoes-impl.md @@ -0,0 +1,61 @@ +# Impl: Sidebar — entrada para Organizações + +Status: aprovado +Atualizado em: 2026-08-02 +Issue: #313 +Intenção: docs/plans/sidebar-organizacoes.md +Appetite restante: ~0,25 dia eng (um item de nav; sem migration) + +## Leitura da intenção + +- **Outcome:** Staff (coordenador, assessor, candidato) descobre `/campanha/organizacoes` pela sidebar desktop e Sheet mobile; liderança não vê o item; destino ativo em lista, detalhe e `/nova`. +- **O que NÃO negociar:** lockdown de `leader`; não redesenhar sidebar; não duplicar lista; href canônico `/campanha/organizacoes`. +- **O que reavaliar:** hipótese de importar `ORGANIZATIONS_LIST_PATH` — `nav.ts` usa literais (precedente Conceitos) para não puxar módulos ao client bundle; manter literal. + +## Abordagem recomendada + +```mermaid +flowchart LR + staffNav["staffNav + Organizações"] --> getCampaignNav + getCampaignNav --> CampaignSidebar + getCampaignNav --> MobileSheet["Sheet mobile"] + isCampaignNavActive --> Active["lista / slug / nova"] +``` + +**Opções consideradas:** A) item após Lideranças | B) após Dobradinhas | C) antes de Apoiadores +**Recomendação:** **A** — afinidade domínio orgs ↔ lideranças; alinhado à recomendação da intenção. +**Rejeitadas:** B/C — sem ganho de descoberta; A é o modelo mental acordado. + +### Componentes / mudanças + +- **`staffNav`** (`src/components/campaign/shell/nav.ts`): inserir `{ title: 'Organizações', href: '/campanha/organizacoes', icon: LandmarkIcon }` logo após Lideranças; import `LandmarkIcon` de lucide-react (instituições — distinto de `HandshakeIcon` e `Building2` usado em município no dashboard). +- **Migration:** sem migration +- **Access / Consent:** nenhum — `getCampaignNav` já exclui `leader`; staff vê por default (mesmo que Lideranças/Demandas) +- **UI:** Impeccable B — encaixe no shell existente; `CampaignSidebar` e Sheet já consomem `getCampaignNav`; `isCampaignNavActive` já cobre paths aninhados (`/nova`, `[slug]`) + +### Dados → forma + +N/A — só navegação. + +## Fases verificáveis + +1. **Nav** — uma linha em `staffNav` + ícone +2. **Testes** — unit: href presente para staff, ausente para leader; `isCampaignNavActive` em lista/detalhe/nova +3. **Gates** — `pnpm gate:fast`; entrega `pnpm push` + +## Rabbit holes / Não escopo (engenharia) + +- Reordenar demais itens além do encaixe após Lideranças +- Exportar constante de href (só `MUNICIPALITY_NAV_HREF` tem consumidor especial B18) +- `staffSecondaryNav` — destino de trabalho, não referência + +## Riscos e mitigação + +- **Leader vê link:** mitigado — `getCampaignNav('leader')` retorna `leaderNav` separado +- **Active state em `/nova`:** mitigado — `isCampaignNavActive` usa `pathname.startsWith(\`${href}/\`)` + +## Aceite de engenharia + +- [x] Aceite de produto da intenção ainda coberto +- [x] Invariantes AGENTS/engineering-standards +- [x] Testes de domínio previstos (unit) para nav + active state diff --git a/src/components/campaign/shell/nav.ts b/src/components/campaign/shell/nav.ts index d40aa546..63bf5a9f 100644 --- a/src/components/campaign/shell/nav.ts +++ b/src/components/campaign/shell/nav.ts @@ -4,6 +4,7 @@ import { HandshakeIcon, HomeIcon, InboxIcon, + LandmarkIcon, LayoutDashboardIcon, MapIcon, MapPinIcon, @@ -37,6 +38,7 @@ const staffNav: CampaignNavItem[] = [ { title: 'Municípios', href: MUNICIPALITY_NAV_HREF, icon: MapPinIcon }, { title: 'Territórios', href: '/campanha/territorios', icon: MapIcon }, { title: 'Lideranças', href: '/campanha/liderancas', icon: HandshakeIcon }, + { title: 'Organizações', href: '/campanha/organizacoes', icon: LandmarkIcon }, { title: 'Dobradinhas', href: '/campanha/dobradinhas', icon: Users2Icon }, { title: 'Atividades', href: '/campanha/atividades', icon: CalendarDaysIcon }, { title: 'Demandas', href: '/campanha/demandas', icon: InboxIcon }, diff --git a/tests/unit/campaignNav.unit.spec.ts b/tests/unit/campaignNav.unit.spec.ts new file mode 100644 index 00000000..ac85f5f5 --- /dev/null +++ b/tests/unit/campaignNav.unit.spec.ts @@ -0,0 +1,38 @@ +// @vitest-environment node + +import { describe, expect, it } from 'vitest' + +import { + getCampaignNav, + isCampaignNavActive, +} from '@/components/campaign/shell/nav' + +const ORGANIZATIONS_LIST_PATH = '/campanha/organizacoes' + +describe('organizations sidebar entry', () => { + it('is offered to staff after Lideranças', () => { + const hrefs = getCampaignNav('coordinator').map((item) => item.href) + const leadershipIndex = hrefs.indexOf('/campanha/liderancas') + const organizationsIndex = hrefs.indexOf(ORGANIZATIONS_LIST_PATH) + + expect(organizationsIndex).toBeGreaterThan(-1) + expect(organizationsIndex).toBe(leadershipIndex + 1) + }) + + it('is visible to advisor and candidate', () => { + expect(getCampaignNav('advisor').map((item) => item.href)).toContain(ORGANIZATIONS_LIST_PATH) + expect(getCampaignNav('candidate').map((item) => item.href)).toContain(ORGANIZATIONS_LIST_PATH) + }) + + it('is hidden from leaders', () => { + expect(getCampaignNav('leader').map((item) => item.href)).not.toContain(ORGANIZATIONS_LIST_PATH) + }) + + it('marks list, detail, and nova as active', () => { + expect(isCampaignNavActive(ORGANIZATIONS_LIST_PATH, ORGANIZATIONS_LIST_PATH)).toBe(true) + expect(isCampaignNavActive('/campanha/organizacoes/sindmed', ORGANIZATIONS_LIST_PATH)).toBe( + true, + ) + expect(isCampaignNavActive('/campanha/organizacoes/nova', ORGANIZATIONS_LIST_PATH)).toBe(true) + }) +}) From ea6641071ece8173a1d1498fabd5073eb83a5f25 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 2 Aug 2026 23:14:38 +0000 Subject: [PATCH 2/3] test(nav): reuse ORGANIZATIONS_LIST_PATH in sidebar spec Co-authored-by: Francisco Solla --- tests/unit/campaignNav.unit.spec.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/tests/unit/campaignNav.unit.spec.ts b/tests/unit/campaignNav.unit.spec.ts index ac85f5f5..fa645355 100644 --- a/tests/unit/campaignNav.unit.spec.ts +++ b/tests/unit/campaignNav.unit.spec.ts @@ -2,12 +2,8 @@ import { describe, expect, it } from 'vitest' -import { - getCampaignNav, - isCampaignNavActive, -} from '@/components/campaign/shell/nav' - -const ORGANIZATIONS_LIST_PATH = '/campanha/organizacoes' +import { getCampaignNav, isCampaignNavActive } from '@/components/campaign/shell/nav' +import { ORGANIZATIONS_LIST_PATH } from '@/lib/campaignQuickActionPaths' describe('organizations sidebar entry', () => { it('is offered to staff after Lideranças', () => { From 7fe42a5fa5e1c12d4ddd03a08e90dec56fdc53cb Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 2 Aug 2026 23:25:50 +0000 Subject: [PATCH 3/3] test(omnibox): cover search-only helpers for knip graph (#313) Knip could not see exports only referenced from client filter components; extend B128 unit spec so CI knip stays green. --- tests/unit/listOmniboxB128.unit.spec.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/unit/listOmniboxB128.unit.spec.ts b/tests/unit/listOmniboxB128.unit.spec.ts index 02ab4b39..9320e25e 100644 --- a/tests/unit/listOmniboxB128.unit.spec.ts +++ b/tests/unit/listOmniboxB128.unit.spec.ts @@ -3,6 +3,9 @@ import { describe, expect, it } from 'vitest' import { applySearchOnlyOmniboxSuggestion, buildSearchOnlyOmniboxChips, + buildSearchOnlyOmniboxSuggestions, + clearSearchOnlyOmnibox, + removeSearchOnlyOmniboxChip, } from '@/lib/searchOnlyListOmnibox' import { applyActivityOmniboxSuggestion, @@ -144,5 +147,20 @@ describe('list omnibox adapters (B128)', () => { withPageReset: (next) => ({ ...next, page: 1 }), }) expect(applied).toEqual({ kind: 'url', state: { page: 1, q: 'ana' } }) + + expect(buildSearchOnlyOmniboxSuggestions('x')[0]?.id).toBe('q:x') + + const removed = removeSearchOnlyOmniboxChip({ + state: { q: 'sindicato', page: 2 }, + chipId: 'q', + withPageReset: (next) => ({ ...next, page: 1 }), + }) + expect(removed).toEqual({ kind: 'url', state: { page: 1, q: undefined } }) + + const cleared = clearSearchOnlyOmnibox({ + state: { q: 'sindicato' }, + cleared: {}, + }) + expect(cleared).toEqual({ kind: 'clear', state: {} }) }) })