mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 15:13:07 +00:00
Merge pull request #366 from fsolla/agent/353-escala-dry-acoes-rapidas
C86+ — pathname de lista das ações rápidas em fonte única (isListPath)
This commit is contained in:
commit
d2fa396717
12 changed files with 185 additions and 20 deletions
127
docs/plans/escala-dry-pos-acoes-rapidas-impl.md
Normal file
127
docs/plans/escala-dry-pos-acoes-rapidas-impl.md
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
# Impl: Escala/DRY pós-B86 — normalização de pathname de lista nas ações rápidas
|
||||
|
||||
Status: aprovado
|
||||
Atualizado em: 2026-08-04
|
||||
Issue: #353
|
||||
Intenção: docs/plans/escala-dry-pos-acoes-rapidas.md
|
||||
Appetite restante: herdado (~0,25–0,5 dia eng; sem migration)
|
||||
|
||||
## Leitura da intenção
|
||||
|
||||
- **Outcome:** o conhecimento "normalizar pathname de lista (match exato + trailing slash)" deixa de estar duplicado nos módulos da família de ações rápidas; um helper compartilhado em `campaignQuickActionPaths.ts` é a fonte única; os exports públicos `isXListPath` continuam existindo como wrappers delegando.
|
||||
- **O que NÃO negociar:** comportamento idêntico (refactor puramente estrutural); exports públicos preservados (specs + registry importam `isDemandsListPath` etc.); client-safe (zero server-only); sem migration; wrappers de superfície activity/organization intactos além de reusar o `normalizePathname` exportado.
|
||||
- **O que reavaliar:**
|
||||
- A intenção lista "6+ call sites" e nomeia 7 módulos. O inventário real tem **9 sites com o mesmo shape** — `isMunicipalitiesListPath` (`campaignMunicipalityQuickActions.ts`) e `isCampaignHomePath` (`campaignQuickActionMount.ts`) ficaram de fora da lista.
|
||||
- A intenção fala de um `normalizePathname` privado "só usado pelos parsers de superfície activity/organization". Há uma **3ª cópia idêntica** em `campaignPageChrome.ts` (usada por `resolveCampaignPageChrome`), não citada na intenção.
|
||||
|
||||
## Abordagem recomendada
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph helper["campaignQuickActionPaths.ts (home)"]
|
||||
norm["export normalizePathname"] --> list["export isListPath(pathname, home)"]
|
||||
end
|
||||
list --> w1["isAdvisorsListPath"]
|
||||
list --> w2["isDemandsListPath"]
|
||||
list --> w3["isStateDeputyListPath"]
|
||||
list --> w4["isConceptsPath + isProfilePath"]
|
||||
list --> w5["isLeadershipListPath"]
|
||||
list --> w6["isSupportersListPath"]
|
||||
list --> w7["isMunicipalitiesListPath"]
|
||||
list --> w8["isCampaignHomePath"]
|
||||
norm --> chrome["campaignPageChrome (3ª cópia removida)"]
|
||||
```
|
||||
|
||||
**Opções consideradas:** ver Decisões D1–D4.
|
||||
|
||||
**Recomendação:** exportar `normalizePathname` + novo `isListPath(pathname, home)` em `campaignQuickActionPaths.ts`; os 9 wrappers de lista (7 públicos + 2 privados) delegam; `campaignPageChrome` importa o `normalizePathname` exportado (mata a 3ª cópia); testes novos para o helper compartilhado.
|
||||
|
||||
**Rejeitadas:** só `normalizePathname` exportado (o spelling de comparação continua duplicado em cada site); só `isListPath` sem exportar `normalizePathname` (`campaignPageChrome` mantém a cópia); migrar apenas os 7 nomeados (deixa gêmeos do mesmo conhecimento); derivar as regex de detalhe numérico agora (gatilho não atingido).
|
||||
|
||||
### Decisões de engenharia
|
||||
|
||||
**D1 — Shape do helper.**
|
||||
Opções: A) exportar `normalizePathname` **e** `isListPath(pathname, home)` | B) só `normalizePathname`; cada site faz `normalizePathname(pathname) === HOME` | C) só `isListPath`; `normalizePathname` segue privado.
|
||||
Recomendação: **A** — `isListPath` nomeia o conceito "lista casa exatamente, com trailing slash opcional" (a semântica que os 9 sites repetem), e `normalizePathname` é a primitiva que os parsers activity/organization já usam no mesmo arquivo e que `campaignPageChrome` duplica. Ambos são puros, client-safe, 1–2 linhas.
|
||||
Rejeitadas: B — a comparação `=== HOME` fica repetida em cada site, que é exatamente a duplicação que o lote remove; C — `campaignPageChrome` não tem uso de list-match, só de normalize; continuaria com cópia própria.
|
||||
|
||||
**D2 — Escopo da migração.**
|
||||
Opções: A) só os 7 módulos nomeados na intenção | B) os 9 sites com o mesmo shape (`+ isMunicipalitiesListPath`, `+ isCampaignHomePath`) e também a 3ª cópia de `normalizePathname` em `campaignPageChrome`.
|
||||
Recomendação: **B** — `isMunicipalitiesListPath` e `isCampaignHomePath` são byte-idênticos em shape, semântica e intenção de comentário ("matches exactly"); consolidar 7 e deixar 2 gêmeos seria o anti-padrão que o lote existe para corrigir. `campaignPageChrome.normalizePathname` é a função idêntica — vira import de 2 linhas e a 3ª cópia morre. Nenhum comportamento muda; cada superfície tem spec própria pinando o resultado.
|
||||
Rejeitadas: A — duplicação residual de 2 sites + 1 cópia de normalize; o "6+" da intenção já sinaliza inventário não-exaustivo.
|
||||
|
||||
**D3 — Derivar regex de detalhe numérico da home.**
|
||||
Opções: A) `escapeRegExp(home)` novo + regex derivada para advisor/supporter (`(\d+)(?:\/|$)`) | B) defer com gatilho (3º vertical com `[id]` numérico).
|
||||
Recomendação: **B** — `escapeRegExp` não existe em `src/` (helper novo para 2 sites); `parseLeadershipDetailId` usa âncora diferente (`(\d+)$`, sem tolerância a trailing slash) — derivação compartilhada mudaria semântica ou precisaria de opção; o diff deixa de ser limpo. O gatilho da própria intenção ("3º vertical com [id] numérico — hoje são 2: advisor + supporter") não está atingido.
|
||||
Rejeitadas: A — helper novo + diff sujo para consolidar conhecimento que só existe em 2 sites (abaixo do limiar de DRY do repo, <3 call sites).
|
||||
|
||||
**D4 — Testes do helper compartilhado.**
|
||||
Opções: A) specs existentes dos 9 wrappers cobrem a delegação; helper novo sem testes diretos | B) + describe novo em `campaignQuickAction.unit.spec.ts` para `isListPath`/`normalizePathname` (bordas: `/`, `/campanha/`, multi-slash, home `'/campanha'`).
|
||||
Recomendação: **B** — o helper é API pública nova; os specs dos wrappers pinam a delegação, não as bordas do helper (ex. `normalizePathname` com `length > 1` preservando `/`, `isListPath` recusando `/campanha/foo`). O arquivo já hospeda os testes da família (`campaignQuickActionPaths` activities/organizations).
|
||||
Rejeitadas: A — regressão das bordas ficaria descoberta quando um 3º/4º vertical passar a usar o helper.
|
||||
|
||||
### Componentes / mudanças
|
||||
|
||||
- **`normalizePathname` + `isListPath`** (`src/lib/campaignQuickActionPaths.ts`): `normalizePathname` deixa de ser privado (mesma implementação); `isListPath(pathname, home)` = `normalizePathname(pathname) === home`. Home nunca é `/` no repo, então o guard `length > 1` é inócuo para os 9 sites — nota no JSDoc.
|
||||
- **Wrappers (delegam a `isListPath`, exports/contrato intactos):**
|
||||
- `isAdvisorsListPath` (`campaignAdvisorQuickActions.ts`, público)
|
||||
- `isDemandsListPath` (`campaignQuickActionDemands.ts`, público — importado pelo registry)
|
||||
- `isStateDeputyListPath` (`campaignQuickActionDobradinhas.ts`, privado)
|
||||
- `isConceptsPath` / `isProfilePath` (`campaignReferenceQuickActions.ts`, públicos)
|
||||
- `isLeadershipListPath` (`campaignQuickActionLeadership.ts`, privado)
|
||||
- `isSupportersListPath` (`supporterQuickActions.ts`, público)
|
||||
- `isMunicipalitiesListPath` (`campaignMunicipalityQuickActions.ts`, público)
|
||||
- `isCampaignHomePath` (`campaignQuickActionMount.ts`, público)
|
||||
- **`campaignPageChrome.ts`**: remove o `normalizePathname` privado; importa o exportado. Zero mudança em `resolveCampaignPageChrome`.
|
||||
- **Migration:** nenhuma (refactor client-safe, sem schema).
|
||||
- **Access / Consent:** nenhum (path helpers puros; nada de escrita, PII ou chave nova).
|
||||
- **UI:** Impeccable A — sem superfície de UI.
|
||||
|
||||
### Dados → forma
|
||||
|
||||
N/A — refactor estrutural; nenhuma métrica, série, ranking ou mapa novo.
|
||||
|
||||
## Fases verificáveis
|
||||
|
||||
1. **Helper + migração** (todo o appetite): exportar `normalizePathname` + `isListPath` → migrar os 9 wrappers → `campaignPageChrome` → testes novos do helper. `pnpm gate:fast` (lint + typecheck + unit) — os specs existentes de cada módulo pinam a delegação.
|
||||
2. **Gates de entrega:** `pnpm check:cycles` (madge — `campaignQuickActionPaths` não importa nada, sem ciclo), `pnpm format:check`, `pnpm exec knip` (nenhum export morre: todos os `isXListPath` seguem públicos/usados), `pnpm push`.
|
||||
|
||||
## Rabbit holes / Não escopo (engenharia)
|
||||
|
||||
- Renomear `campaignQuickActionPaths.ts` ou mover para outro diretório — o arquivo já é o home designado pela intenção; renomear é noise de diff.
|
||||
- Derivação de regex de detalhe numérico (D3) — defer com gatilho.
|
||||
- Tocar `isDemandDetailPath`, `parseStateDeputyDetailSlug`, `parseMunicipalityDetailSlug`, `parseLeadershipDetailId` — não são list-match; ficam como estão.
|
||||
- `isLeaderContactsPath` / `isCampaignActionsPath` (`campaignQuickActionMount.ts`) — prefix-match de descendentes, semântica diferente; fora.
|
||||
- Normalizar em vez de comparar nos wrappers (mudar `isListPath` para aceitar descendants) — mudaria comportamento.
|
||||
- Cache `Map<CampaignRole>` (S7 da intenção) — defer com gatilho (4º site com shape compatível); não é deste lote.
|
||||
|
||||
## Riscos e mitigação
|
||||
|
||||
- **Diff ruidoso (9 sites + chrome):** wrappers de 1 linha delegando mantêm cada diff mínimo; specs de cada módulo já existem e pinam o comportamento (`campaignAdvisorQuickActions`, `campaignQuickAction`, `campaignReferenceQuickActions`, `supporterQuickActions`, `campaignMunicipalityQuickActions`, `campaignPageChrome` unit specs).
|
||||
- **Ciclo de import:** `campaignQuickActionPaths.ts` não importa nada; todos os importadores apontam para ele (folha do DAG) — `check:cycles` confirma.
|
||||
- **Regressão de trailing slash em chrome:** `resolveCampaignPageChrome('/campanha/')` já é coberto por spec (`campaignPageChrome.unit.spec.ts` L10-13) e o normalize exportado é idêntico ao removido.
|
||||
|
||||
## Aceite de engenharia
|
||||
|
||||
- [ ] Aceite de produto da intenção ainda coberto: conhecimento de list-match em fonte única; exports preservados; comportamento idêntico
|
||||
- [ ] Invariantes AGENTS/engineering-standards: client-safe, sem migration, sem access/Consent, identificadores em inglês
|
||||
- [ ] Testes de domínio: specs existentes dos 9 wrappers + describe novo de `isListPath`/`normalizePathname`
|
||||
- [ ] Self-score decision-quality: 5/5 (decisões caras com rejeitadas; appetite respeitado; rabbit holes nomeados; depth check reusa o home existente; outcome preservado)
|
||||
|
||||
## Simplify (2026-08-04) — aplicado e deferido
|
||||
|
||||
3 revisores paralelos (read-only) no diff da sessão; veredito do revisor de corretude: **bordas fechadas, comportamento preservado** (tabela de equivalência borda a borda dos matchers antigo × `isListPath`). Revisor de reuse: **sem duplicação real remanescente** (zero matchers exatos fora do diff; prefix-matches de descendentes deixados de fora por semântica diferente).
|
||||
|
||||
**Aplicados (fixes pontuais que preservam comportamento):**
|
||||
|
||||
- JSDoc de `normalizePathname` documenta o invariante "homes nunca são `/` nem terminam com slash" (S1 quality).
|
||||
- Wording do JSDoc de `isListPath` corrigido ("equals `home`, optionally with a single trailing slash") (S3 quality).
|
||||
- Testes novos pinando multi-slash (`/campanha//`) e inputs degenerados (`''`, `'/'` como home, double trailing slash) (S2 quality + S1 correctness).
|
||||
- Comentário de `isCampaignHomePath` encurtado para o why ("same exact-match rule as the sidebar nav") (S4 quality).
|
||||
|
||||
**Descartados:**
|
||||
|
||||
- `isLeadershipListPath` como wrapper privado com 1 call site — manter (consistência de padrão com os 7 irmãos; inline seria micro-otimização de estilo) (S1 reuse, score 1).
|
||||
- Contrato `home` sem tipo restrito — JSDoc documenta; gatilho: chamador futuro com home terminando em `/` (S2 correctness, score 1).
|
||||
|
||||
**Defer com gatilho (já registrados na intenção, não reabrir):** derivação de regex numérica de detalhe (3º vertical com `[id]` numérico — hoje 2: advisor + supporter); cache `Map<CampaignRole>` (4º site com shape compatível); id `register-supporter` sobrecarregado (agregação de catálogos).
|
||||
|
|
@ -2,6 +2,7 @@ import { UserPlus } from 'lucide-react'
|
|||
|
||||
import { CAMPAIGN_ADVISORS_HOME } from '@/lib/campaignPaths'
|
||||
import type { CampaignQuickActionContext } from '@/lib/campaignQuickActionContext'
|
||||
import { isListPath } from '@/lib/campaignQuickActionPaths'
|
||||
import type { CampaignQuickAction } from '@/lib/campaignQuickActionTypes'
|
||||
import { isUnrestrictedCampaignRole, type CampaignRole } from '@/lib/campaignRoles'
|
||||
|
||||
|
|
@ -13,7 +14,7 @@ export const advisorQuickCreateHref =
|
|||
const advisorDetailPathPattern = /^\/campanha\/assessores\/(\d+)(?:\/|$)/
|
||||
|
||||
export const isAdvisorsListPath = (pathname: string): boolean =>
|
||||
pathname === CAMPAIGN_ADVISORS_HOME || pathname === `${CAMPAIGN_ADVISORS_HOME}/`
|
||||
isListPath(pathname, CAMPAIGN_ADVISORS_HOME)
|
||||
|
||||
export const isAdvisorsPath = (pathname: string): boolean =>
|
||||
isAdvisorsListPath(pathname) || parseAdvisorDetailId(pathname) !== undefined
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import {
|
|||
type ResolvedCampaignHomeAction,
|
||||
} from '@/lib/campaignHomeActions'
|
||||
import type { CampaignQuickActionContext } from '@/lib/campaignQuickActionContext'
|
||||
import { isListPath } from '@/lib/campaignQuickActionPaths'
|
||||
import type { CampaignRole } from '@/lib/campaignRoles'
|
||||
import { isStaffCampaignRole } from '@/lib/campaignRoles'
|
||||
|
||||
|
|
@ -13,7 +14,7 @@ const MUNICIPALITIES_LIST_PATH = '/campanha/municipios' as const
|
|||
const municipalityDetailPathPattern = /^\/campanha\/municipios\/([^/]+)(?:\/|$)/
|
||||
|
||||
export const isMunicipalitiesListPath = (pathname: string): boolean =>
|
||||
pathname === MUNICIPALITIES_LIST_PATH || pathname === `${MUNICIPALITIES_LIST_PATH}/`
|
||||
isListPath(pathname, MUNICIPALITIES_LIST_PATH)
|
||||
|
||||
export const parseMunicipalityDetailSlug = (pathname: string): string | undefined => {
|
||||
const match = pathname.match(municipalityDetailPathPattern)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import type { Metadata } from 'next'
|
||||
|
||||
import { CAMPAIGN_HOME, LEADER_CONTACTS_HOME } from '@/lib/campaignPaths'
|
||||
import { normalizePathname } from '@/lib/campaignQuickActionPaths'
|
||||
import type { CampaignUser } from '@/payload-types'
|
||||
|
||||
export type CampaignPageChrome = {
|
||||
|
|
@ -10,13 +11,6 @@ export type CampaignPageChrome = {
|
|||
|
||||
type CampaignRole = CampaignUser['role']
|
||||
|
||||
const normalizePathname = (pathname: string): string => {
|
||||
if (pathname.length > 1 && pathname.endsWith('/')) {
|
||||
return pathname.slice(0, -1)
|
||||
}
|
||||
return pathname
|
||||
}
|
||||
|
||||
/** Client-safe vocabulary for shell chrome and tab titles (B123). */
|
||||
export const campaignPageChromeCatalog = {
|
||||
home: null,
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import {
|
|||
import { homeActionsForRole } from '@/lib/campaignHomeActions'
|
||||
import { CAMPAIGN_DEMANDS_HOME } from '@/lib/campaignPaths'
|
||||
import type { CampaignQuickActionContext } from '@/lib/campaignQuickActionContext'
|
||||
import { isListPath } from '@/lib/campaignQuickActionPaths'
|
||||
import type { CampaignQuickAction } from '@/lib/campaignQuickActionTypes'
|
||||
import { isStaffCampaignRole, type CampaignRole } from '@/lib/campaignRoles'
|
||||
|
||||
|
|
@ -27,7 +28,7 @@ export const demandCreateHref = (municipalityId?: number): string => {
|
|||
}
|
||||
|
||||
export const isDemandsListPath = (pathname: string): boolean =>
|
||||
pathname === CAMPAIGN_DEMANDS_HOME || pathname === `${CAMPAIGN_DEMANDS_HOME}/`
|
||||
isListPath(pathname, CAMPAIGN_DEMANDS_HOME)
|
||||
|
||||
export const isDemandDetailPath = (pathname: string): boolean => {
|
||||
if (!pathname.startsWith(`${CAMPAIGN_DEMANDS_HOME}/`)) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { Plus } from 'lucide-react'
|
||||
|
||||
import { resolveStaffHomeQuickActions } from '@/lib/campaignHomeActions'
|
||||
import { isListPath } from '@/lib/campaignQuickActionPaths'
|
||||
import type { CampaignQuickAction } from '@/lib/campaignQuickActionTypes'
|
||||
import { isStaffCampaignRole, type CampaignRole } from '@/lib/campaignRoles'
|
||||
|
||||
|
|
@ -9,7 +10,7 @@ const STATE_DEPUTY_CREATE_PATH = '/campanha/dobradinhas/nova' as const
|
|||
const STATE_DEPUTY_PATH_PREFIX = `${STATE_DEPUTY_LIST_PATH}/` as const
|
||||
|
||||
const isStateDeputyListPath = (pathname: string): boolean =>
|
||||
pathname === STATE_DEPUTY_LIST_PATH || pathname === `${STATE_DEPUTY_LIST_PATH}/`
|
||||
isListPath(pathname, STATE_DEPUTY_LIST_PATH)
|
||||
|
||||
const isStateDeputyCreatePath = (pathname: string): boolean => pathname === STATE_DEPUTY_CREATE_PATH
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import {
|
|||
type ResolvedCampaignHomeAction,
|
||||
} from '@/lib/campaignHomeActions'
|
||||
import type { CampaignQuickActionContext } from '@/lib/campaignQuickActionContext'
|
||||
import { isListPath } from '@/lib/campaignQuickActionPaths'
|
||||
import type { CampaignRole } from '@/lib/campaignRoles'
|
||||
import { isStaffCampaignRole } from '@/lib/campaignRoles'
|
||||
|
||||
|
|
@ -25,7 +26,7 @@ const LEADERSHIP_DETAIL_WIZARD_IDS: readonly CampaignWizardActionId[] = [
|
|||
]
|
||||
|
||||
const isLeadershipListPath = (pathname: string): boolean =>
|
||||
pathname === LEADERSHIP_LIST_PATH || pathname === `${LEADERSHIP_LIST_PATH}/`
|
||||
isListPath(pathname, LEADERSHIP_LIST_PATH)
|
||||
|
||||
const parseLeadershipDetailId = (pathname: string): number | undefined => {
|
||||
const match = pathname.match(/^\/campanha\/liderancas\/(\d+)$/)
|
||||
|
|
|
|||
|
|
@ -1,16 +1,15 @@
|
|||
import { CAMPAIGN_ACTIONS_HOME } from '@/lib/campaignActionRoutes'
|
||||
import { isAdvisorsPath } from '@/lib/campaignAdvisorQuickActions'
|
||||
import { CAMPAIGN_HOME, LEADER_CONTACTS_HOME } from '@/lib/campaignPaths'
|
||||
import { isActivityTourComposerPath } from '@/lib/campaignQuickActionPaths'
|
||||
import { isActivityTourComposerPath, isListPath } from '@/lib/campaignQuickActionPaths'
|
||||
import {
|
||||
isStaffCampaignRole,
|
||||
isUnrestrictedCampaignRole,
|
||||
type CampaignRole,
|
||||
} from '@/lib/campaignRoles'
|
||||
|
||||
/** Início matches only exactly — same rule as sidebar nav. */
|
||||
export const isCampaignHomePath = (pathname: string): boolean =>
|
||||
pathname === CAMPAIGN_HOME || pathname === `${CAMPAIGN_HOME}/`
|
||||
/** Same exact-match rule as the sidebar nav. */
|
||||
export const isCampaignHomePath = (pathname: string): boolean => isListPath(pathname, CAMPAIGN_HOME)
|
||||
|
||||
export const isCampaignActionsPath = (pathname: string): boolean =>
|
||||
pathname === CAMPAIGN_ACTIONS_HOME || pathname.startsWith(`${CAMPAIGN_ACTIONS_HOME}/`)
|
||||
|
|
|
|||
|
|
@ -16,9 +16,18 @@ export type OrganizationQuickActionSurface =
|
|||
| { kind: 'list' }
|
||||
| { kind: 'detail'; organizationSlug: string }
|
||||
|
||||
const normalizePathname = (pathname: string): string =>
|
||||
/**
|
||||
* Strips a single trailing slash for client-side path comparisons, preserving `/`
|
||||
* (`/campanha/` → `/campanha`, while `/` stays `/`). Homes in this codebase never
|
||||
* are `/` nor end with a slash — `isListPath` relies on that contract.
|
||||
*/
|
||||
export const normalizePathname = (pathname: string): string =>
|
||||
pathname.length > 1 && pathname.endsWith('/') ? pathname.slice(0, -1) : pathname
|
||||
|
||||
/** Whether `pathname` equals `home`, optionally with a single trailing slash. */
|
||||
export const isListPath = (pathname: string, home: string): boolean =>
|
||||
normalizePathname(pathname) === home
|
||||
|
||||
const listDetailSlug = (
|
||||
pathname: string,
|
||||
listPath: string,
|
||||
|
|
|
|||
|
|
@ -1,14 +1,15 @@
|
|||
import { CAMPAIGN_CONCEPTS_PATH } from '@/lib/campaignIntelligenceConcepts'
|
||||
import { CAMPAIGN_PROFILE_HOME } from '@/lib/campaignPaths'
|
||||
import type { CampaignQuickActionContext } from '@/lib/campaignQuickActionContext'
|
||||
import { isListPath } from '@/lib/campaignQuickActionPaths'
|
||||
import type { CampaignQuickAction } from '@/lib/campaignQuickActionTypes'
|
||||
import { isStaffCampaignRole, type CampaignRole } from '@/lib/campaignRoles'
|
||||
|
||||
export const isConceptsPath = (pathname: string): boolean =>
|
||||
pathname === CAMPAIGN_CONCEPTS_PATH || pathname === `${CAMPAIGN_CONCEPTS_PATH}/`
|
||||
isListPath(pathname, CAMPAIGN_CONCEPTS_PATH)
|
||||
|
||||
export const isProfilePath = (pathname: string): boolean =>
|
||||
pathname === CAMPAIGN_PROFILE_HOME || pathname === `${CAMPAIGN_PROFILE_HOME}/`
|
||||
isListPath(pathname, CAMPAIGN_PROFILE_HOME)
|
||||
|
||||
/** B90 — reference/account surfaces with search-only drawer (no action catalog). */
|
||||
export const isReferenceQuickActionPath = (pathname: string): boolean =>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { FileUp, UserPlus } from 'lucide-react'
|
|||
|
||||
import { CAMPAIGN_SUPPORTERS_HOME } from '@/lib/campaignPaths'
|
||||
import type { CampaignQuickActionContext } from '@/lib/campaignQuickActionContext'
|
||||
import { isListPath } from '@/lib/campaignQuickActionPaths'
|
||||
import type { CampaignQuickAction } from '@/lib/campaignQuickActionTypes'
|
||||
import { isStaffCampaignRole, type CampaignRole } from '@/lib/campaignRoles'
|
||||
|
||||
|
|
@ -12,7 +13,7 @@ export const SUPPORTER_IMPORT_HREF = `${CAMPAIGN_SUPPORTERS_HOME}/importar` as c
|
|||
const supporterDetailPathPattern = /^\/campanha\/apoiadores\/(\d+)(?:\/|$)/
|
||||
|
||||
export const isSupportersListPath = (pathname: string): boolean =>
|
||||
pathname === CAMPAIGN_SUPPORTERS_HOME || pathname === `${CAMPAIGN_SUPPORTERS_HOME}/`
|
||||
isListPath(pathname, CAMPAIGN_SUPPORTERS_HOME)
|
||||
|
||||
export const parseSupporterDetailId = (pathname: string): number | undefined => {
|
||||
const match = pathname.match(supporterDetailPathPattern)
|
||||
|
|
|
|||
|
|
@ -32,6 +32,8 @@ import {
|
|||
ORGANIZATIONS_LIST_PATH,
|
||||
ORGANIZATION_NEW_PATH,
|
||||
isActivityTourComposerPath,
|
||||
isListPath,
|
||||
normalizePathname,
|
||||
parseActivityQuickActionSurface,
|
||||
parseOrganizationQuickActionSurface,
|
||||
} from '@/lib/campaignQuickActionPaths'
|
||||
|
|
@ -114,6 +116,33 @@ describe('campaignQuickActionPaths (activities)', () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe('campaignQuickActionPaths (shared list helpers, C86+)', () => {
|
||||
it('matches exactly with an optional trailing slash', () => {
|
||||
expect(isListPath('/campanha/demandas', '/campanha/demandas')).toBe(true)
|
||||
expect(isListPath('/campanha/demandas/', '/campanha/demandas')).toBe(true)
|
||||
})
|
||||
|
||||
it('rejects siblings and descendants', () => {
|
||||
expect(isListPath('/campanha/demandas/nova', '/campanha/demandas')).toBe(false)
|
||||
expect(isListPath('/campanha/demandas/pedido-cairu', '/campanha/demandas')).toBe(false)
|
||||
expect(isListPath('/campanha/municipios', '/campanha/demandas')).toBe(false)
|
||||
})
|
||||
|
||||
it('normalizes a single trailing slash while preserving /', () => {
|
||||
expect(normalizePathname('/campanha/')).toBe('/campanha')
|
||||
expect(normalizePathname('/campanha')).toBe('/campanha')
|
||||
expect(normalizePathname('/')).toBe('/')
|
||||
expect(normalizePathname('/campanha/apoiadores/')).toBe('/campanha/apoiadores')
|
||||
expect(normalizePathname('/campanha//')).toBe('/campanha/')
|
||||
})
|
||||
|
||||
it('pins the degenerate inputs the inline matchers used to handle', () => {
|
||||
expect(isListPath('/campanha/demandas//', '/campanha/demandas')).toBe(false)
|
||||
expect(isListPath('', '/campanha/demandas')).toBe(false)
|
||||
expect(isListPath('/', '/')).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('organizationQuickActions (B88)', () => {
|
||||
it('parses list and detail surfaces', () => {
|
||||
expect(parseOrganizationQuickActionSurface(ORGANIZATIONS_LIST_PATH)).toEqual({ kind: 'list' })
|
||||
|
|
|
|||
Loading…
Reference in a new issue