Merge pull request #81 from fsolla/cursor/pool-i24-e7575d3c-a5b4

B88 — Ações rápidas — Organizações
This commit is contained in:
Francisco Solla 2026-08-01 02:24:16 -03:00 committed by GitHub
commit 374e27f7eb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 129 additions and 5 deletions

View file

@ -1,4 +1,4 @@
/** Client-safe `/campanha/atividades` path helpers for the B84 quick-action registry. */
/** Client-safe quick-action path helpers for the B84+ vertical registries. */
export const ACTIVITY_LIST_PATH = '/campanha/atividades' as const
@ -6,20 +6,35 @@ export const ACTIVITY_NEW_PATH = '/campanha/atividades/nova' as const
export const ACTIVITY_TOUR_COMPOSER_PATH = '/campanha/atividades/giros' as const
export const ORGANIZATIONS_LIST_PATH = '/campanha/organizacoes' as const
export const ORGANIZATION_NEW_PATH = '/campanha/organizacoes/nova' as const
export type ActivityQuickActionSurface = { kind: 'list' } | { kind: 'detail'; activitySlug: string }
export type OrganizationQuickActionSurface =
| { kind: 'list' }
| { kind: 'detail'; organizationSlug: string }
const normalizePathname = (pathname: string): string =>
pathname.length > 1 && pathname.endsWith('/') ? pathname.slice(0, -1) : pathname
const activityDetailSlug = (pathname: string): string | null => {
const prefix = `${ACTIVITY_LIST_PATH}/`
const listDetailSlug = (
pathname: string,
listPath: string,
excludedSegments: readonly string[],
): string | null => {
const prefix = `${listPath}/`
if (!pathname.startsWith(prefix)) return null
const rest = pathname.slice(prefix.length)
if (!rest || rest.includes('/')) return null
if (rest === 'nova' || rest === 'giros') return null
if (excludedSegments.includes(rest)) return null
return rest
}
const activityDetailSlug = (pathname: string): string | null =>
listDetailSlug(pathname, ACTIVITY_LIST_PATH, ['nova', 'giros'])
export const parseActivityQuickActionSurface = (
pathname: string,
): ActivityQuickActionSurface | null => {
@ -29,5 +44,14 @@ export const parseActivityQuickActionSurface = (
return slug ? { kind: 'detail', activitySlug: slug } : null
}
export const parseOrganizationQuickActionSurface = (
pathname: string,
): OrganizationQuickActionSurface | null => {
const normalized = normalizePathname(pathname)
if (normalized === ORGANIZATIONS_LIST_PATH) return { kind: 'list' }
const slug = listDetailSlug(normalized, ORGANIZATIONS_LIST_PATH, ['nova'])
return slug ? { kind: 'detail', organizationSlug: slug } : null
}
export const isActivityTourComposerPath = (pathname: string): boolean =>
normalizePathname(pathname) === ACTIVITY_TOUR_COMPOSER_PATH

View file

@ -3,9 +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 { parseActivityQuickActionSurface } from '@/lib/campaignQuickActionPaths'
import {
parseActivityQuickActionSurface,
parseOrganizationQuickActionSurface,
} from '@/lib/campaignQuickActionPaths'
import type { CampaignQuickAction } from '@/lib/campaignQuickActionTypes'
import type { CampaignRole } from '@/lib/campaignRoles'
import { resolveOrganizationQuickActions } from '@/lib/organizationQuickActions'
const isTerritoriesListPath = (pathname: string): boolean =>
pathname === CAMPAIGN_TERRITORIES_HOME || pathname.startsWith(`${CAMPAIGN_TERRITORIES_HOME}/`)
@ -25,6 +29,11 @@ export const resolveQuickActionsForPath = (
return resolveActivityQuickActions(activitySurface, role, context)
}
const organizationSurface = parseOrganizationQuickActionSurface(pathname)
if (organizationSurface) {
return resolveOrganizationQuickActions(organizationSurface, role, context)
}
const municipalityActions = resolveMunicipalityQuickActionsForPath(pathname, role, context)
if (municipalityActions.length > 0) return municipalityActions

View file

@ -0,0 +1,35 @@
import { Plus } from 'lucide-react'
import type { CampaignQuickActionContext } from '@/lib/campaignQuickActionContext'
import {
ORGANIZATION_NEW_PATH,
type OrganizationQuickActionSurface,
} from '@/lib/campaignQuickActionPaths'
import type { CampaignQuickAction } from '@/lib/campaignQuickActionTypes'
import { isStaffCampaignRole, type CampaignRole } from '@/lib/campaignRoles'
const listQuickActions = (): readonly CampaignQuickAction[] => [
{
id: 'new-organization',
label: 'Nova organização',
icon: Plus,
description: 'Cadastrar sindicato, associação ou movimento de apoio',
href: ORGANIZATION_NEW_PATH,
},
]
export const resolveOrganizationQuickActions = (
surface: OrganizationQuickActionSurface,
role: CampaignRole,
_context: CampaignQuickActionContext,
): readonly CampaignQuickAction[] => {
if (!isStaffCampaignRole(role)) return []
if (surface.kind === 'list') {
return listQuickActions()
}
// Detail: leadership list has no organization filter URL yet (B88 as-built);
// orgs rarely have a single obvious municipality for A1–A5 prefills.
return []
}

View file

@ -13,8 +13,11 @@ import {
ACTIVITY_LIST_PATH,
ACTIVITY_NEW_PATH,
ACTIVITY_TOUR_COMPOSER_PATH,
ORGANIZATIONS_LIST_PATH,
ORGANIZATION_NEW_PATH,
isActivityTourComposerPath,
parseActivityQuickActionSurface,
parseOrganizationQuickActionSurface,
} from '@/lib/campaignQuickActionPaths'
import { resolveQuickActionsForPath } from '@/lib/campaignQuickActionRegistry'
import {
@ -22,6 +25,7 @@ import {
QUICK_ACTIONS_SNAP_EXPANDED,
quickActionsSnapIsExpanded,
} from '@/lib/campaignQuickActionSnap'
import { resolveOrganizationQuickActions } from '@/lib/organizationQuickActions'
describe('campaignQuickActionMount', () => {
it('treats Início as exact match only', () => {
@ -77,6 +81,44 @@ describe('campaignQuickActionPaths (activities)', () => {
})
})
describe('organizationQuickActions (B88)', () => {
it('parses list and detail surfaces', () => {
expect(parseOrganizationQuickActionSurface(ORGANIZATIONS_LIST_PATH)).toEqual({ kind: 'list' })
expect(parseOrganizationQuickActionSurface(`${ORGANIZATIONS_LIST_PATH}/`)).toEqual({
kind: 'list',
})
expect(parseOrganizationQuickActionSurface('/campanha/organizacoes/sindmed')).toEqual({
kind: 'detail',
organizationSlug: 'sindmed',
})
})
it('ignores nova and nested routes', () => {
expect(parseOrganizationQuickActionSurface(ORGANIZATION_NEW_PATH)).toBeNull()
expect(parseOrganizationQuickActionSurface('/campanha/organizacoes/foo/editar')).toBeNull()
})
it('lists new-organization on the catalog page', () => {
const actions = resolveOrganizationQuickActions({ kind: 'list' }, 'coordinator', {})
expect(actions.map((action) => action.id)).toEqual(['new-organization'])
expect(actions[0]?.href).toBe(ORGANIZATION_NEW_PATH)
})
it('returns empty detail catalog without leadership filter URL', () => {
expect(
resolveOrganizationQuickActions(
{ kind: 'detail', organizationSlug: 'sindmed' },
'coordinator',
{ organizationSlug: 'sindmed' },
),
).toEqual([])
})
it('returns empty catalog for leader lockdown', () => {
expect(resolveOrganizationQuickActions({ kind: 'list' }, 'leader', {})).toEqual([])
})
})
describe('activityQuickActions (B84)', () => {
it('lists vertical verbs on the activity list', () => {
const actions = resolveActivityQuickActions({ kind: 'list' }, 'coordinator', {})
@ -125,6 +167,20 @@ describe('activityQuickActions (B84)', () => {
describe('campaignQuickActionRegistry', () => {
const staffActionIds = homeActionsForRole('coordinator').map((action) => action.id)
it('delegates organization routes to the B88 catalog', () => {
const actions = resolveQuickActionsForPath(ORGANIZATIONS_LIST_PATH, 'coordinator', {})
expect(actions.map((action) => action.id)).toEqual(['new-organization'])
expect(actions[0]?.href).toBe(ORGANIZATION_NEW_PATH)
})
it('returns empty organization detail catalog in the registry', () => {
expect(
resolveQuickActionsForPath('/campanha/organizacoes/sindmed', 'advisor', {
organizationSlug: 'sindmed',
}),
).toEqual([])
})
it('returns empty catalog for unregistered paths', () => {
expect(resolveQuickActionsForPath('/campanha/apoiadores', 'coordinator', {})).toEqual([])
})