Merge pull request #154 from fsolla/cursor/pool-i149-d8165a59-5947
B110 — Wizard return path via allowlisted from query
This commit is contained in:
commit
8741eb5151
28 changed files with 351 additions and 88 deletions
|
|
@ -15,12 +15,14 @@ import {
|
|||
parseWizardEntryActionParam,
|
||||
parseWizardLeadershipIdParam,
|
||||
parseWizardMunicipioParam,
|
||||
parseWizardReturnPath,
|
||||
resolveWizardSignalTypeParam,
|
||||
resolveWizardTrendStatusParam,
|
||||
WIZARD_ENTRY_ACTION_QUERY_KEY,
|
||||
WIZARD_LEADERSHIP_ID_QUERY_KEY,
|
||||
WIZARD_MUNICIPIO_QUERY_KEY,
|
||||
WIZARD_NOTE_PREFILL_QUERY_KEY,
|
||||
WIZARD_RETURN_PATH_QUERY_KEY,
|
||||
WIZARD_SCENARIO_QUERY_KEY,
|
||||
WIZARD_SIGNAL_BODY_QUERY_KEY,
|
||||
WIZARD_SIGNAL_TYPE_QUERY_KEY,
|
||||
|
|
@ -31,14 +33,17 @@ import {
|
|||
wizardSignalHref,
|
||||
wizardTrendHref,
|
||||
} from '@/lib/campaignActionRoutes'
|
||||
import { CAMPAIGN_HOME } from '@/lib/campaignPaths'
|
||||
import {
|
||||
buildPoliticalTrendNotePrefill,
|
||||
resolvePoliticalTrendNotePrefillSource,
|
||||
resolveWizardTrendNoteDestination,
|
||||
} from '@/lib/politicalTrendWizardUi'
|
||||
import { toVoteEstimateScenarioViewModel } from '@/lib/voteEstimate'
|
||||
import { isWizardChainActionId, wizardChainContinueHref } from '@/lib/wizardActionChain'
|
||||
import {
|
||||
isWizardChainActionId,
|
||||
wizardChainContinueHref,
|
||||
wizardChainEndHref,
|
||||
} from '@/lib/wizardActionChain'
|
||||
import config from '@/payload.config'
|
||||
import { requireCampaignPageActor } from '@/utilities/campaignPageActor'
|
||||
import { loadWizardLeadershipTiles } from '@/utilities/leadership/leadershipData'
|
||||
|
|
@ -83,6 +88,7 @@ export default async function CampaignActionWizardPage({
|
|||
}: CampaignActionWizardPageProps) {
|
||||
const { slug } = await params
|
||||
const resolvedSearchParams = await searchParams
|
||||
const returnPath = parseWizardReturnPath(resolvedSearchParams[WIZARD_RETURN_PATH_QUERY_KEY])
|
||||
|
||||
if (!isCampaignWizardActionSlug(slug)) {
|
||||
notFound()
|
||||
|
|
@ -104,7 +110,8 @@ export default async function CampaignActionWizardPage({
|
|||
return (
|
||||
<WizardMunicipalitySearchStep
|
||||
actionSlug={slug}
|
||||
previousHref={CAMPAIGN_HOME}
|
||||
previousHref={wizardChainEndHref(returnPath)}
|
||||
returnPath={returnPath}
|
||||
accessibleMunicipalities={accessibleMunicipalities}
|
||||
/>
|
||||
)
|
||||
|
|
@ -142,6 +149,7 @@ export default async function CampaignActionWizardPage({
|
|||
municipalitySlug={municipality.slug}
|
||||
initialExpectedVotes={toVoteEstimateScenarioViewModel(municipality.expectedVotes)}
|
||||
entryAction={entryAction}
|
||||
returnPath={returnPath}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
@ -164,6 +172,7 @@ export default async function CampaignActionWizardPage({
|
|||
entryAction={entryAction}
|
||||
initialTiles={tiles}
|
||||
initialLeadershipId={initialLeadershipId}
|
||||
returnPath={returnPath}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
@ -200,9 +209,11 @@ export default async function CampaignActionWizardPage({
|
|||
if (noteDestination === 'home') {
|
||||
// Chained session: advance the queue (B98). Standalone stale deep-link: Início (B97).
|
||||
if (isWizardChainActionId(entryAction)) {
|
||||
redirect(wizardChainContinueHref(entryAction, 'change-trend', municipalitySlug))
|
||||
redirect(
|
||||
wizardChainContinueHref(entryAction, 'change-trend', municipalitySlug, returnPath),
|
||||
)
|
||||
}
|
||||
redirect(CAMPAIGN_HOME)
|
||||
redirect(wizardChainEndHref(returnPath))
|
||||
}
|
||||
|
||||
if (noteDestination === 'choice') {
|
||||
|
|
@ -220,6 +231,7 @@ export default async function CampaignActionWizardPage({
|
|||
trendStatus={trendStatus}
|
||||
initialNote={initialNote}
|
||||
entryAction={entryAction}
|
||||
returnPath={returnPath}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
@ -232,6 +244,7 @@ export default async function CampaignActionWizardPage({
|
|||
currentStatus={currentStatus}
|
||||
entryAction={entryAction}
|
||||
prefillExtraParams={prefillExtraParams}
|
||||
returnPath={returnPath}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
@ -257,6 +270,7 @@ export default async function CampaignActionWizardPage({
|
|||
municipalitySlug={municipality.slug}
|
||||
signalType={signalType}
|
||||
entryAction={entryAction}
|
||||
returnPath={returnPath}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
@ -267,9 +281,16 @@ export default async function CampaignActionWizardPage({
|
|||
municipalityName={municipality.name}
|
||||
municipalitySlug={municipality.slug}
|
||||
entryAction={entryAction}
|
||||
returnPath={returnPath}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return <WizardMunicipalitySelectedStub actionSlug={slug} municipalityName={municipality.name} />
|
||||
return (
|
||||
<WizardMunicipalitySelectedStub
|
||||
actionSlug={slug}
|
||||
municipalityName={municipality.name}
|
||||
returnPath={returnPath}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import { CampaignHomeActionStrip } from '@/components/campaign/dashboard/CampaignHomeActionStrip'
|
||||
import { homeActionsForRole, toHomeActionButtonProps } from '@/lib/campaignHomeActions'
|
||||
import { CAMPAIGN_HOME } from '@/lib/campaignPaths'
|
||||
import type { CampaignRole } from '@/lib/campaignRoles'
|
||||
|
||||
export const CampaignHomeActions = ({
|
||||
|
|
@ -11,7 +12,10 @@ export const CampaignHomeActions = ({
|
|||
role: CampaignRole
|
||||
uncoveredMunicipalitiesHref?: string
|
||||
}) => {
|
||||
const actions = toHomeActionButtonProps(homeActionsForRole(role), uncoveredMunicipalitiesHref)
|
||||
const actions = toHomeActionButtonProps(homeActionsForRole(role), {
|
||||
uncoveredMunicipalitiesHref,
|
||||
returnPath: CAMPAIGN_HOME,
|
||||
})
|
||||
|
||||
if (actions.length === 0) return null
|
||||
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/comp
|
|||
import type { CampaignWizardActionId } from '@/lib/campaignActionRoutes'
|
||||
import { wizardActionHref } from '@/lib/campaignActionRoutes'
|
||||
import { recordLastActedMunicipality } from '@/lib/campaignLastActedMunicipality'
|
||||
import { CAMPAIGN_HOME } from '@/lib/campaignPaths'
|
||||
import {
|
||||
WIZARD_LEADERSHIP_ADD_TILE_LABEL,
|
||||
WIZARD_LEADERSHIP_CONTINUE_LABEL,
|
||||
|
|
@ -36,7 +35,11 @@ import {
|
|||
} from '@/lib/campaignWizardCopy'
|
||||
import { truncateNameAtWordBoundary } from '@/lib/leadershipNameTruncate'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { resolveWizardChainEntry, wizardChainContinueHref } from '@/lib/wizardActionChain'
|
||||
import {
|
||||
resolveWizardChainEntry,
|
||||
wizardChainContinueHref,
|
||||
wizardChainEndHref,
|
||||
} from '@/lib/wizardActionChain'
|
||||
import {
|
||||
resolveWizardLeadershipSkip,
|
||||
type WizardLeadershipTileViewModel,
|
||||
|
|
@ -50,6 +53,7 @@ type WizardLeadershipStepProps = {
|
|||
entryAction?: CampaignWizardActionId
|
||||
initialTiles: WizardLeadershipTileViewModel[]
|
||||
initialLeadershipId?: number
|
||||
returnPath?: string
|
||||
}
|
||||
|
||||
type WizardLeadershipMode =
|
||||
|
|
@ -70,6 +74,7 @@ export const WizardLeadershipStep = ({
|
|||
entryAction,
|
||||
initialTiles,
|
||||
initialLeadershipId,
|
||||
returnPath,
|
||||
}: WizardLeadershipStepProps) => {
|
||||
const router = useRouter()
|
||||
const [isContinuing, startContinueTransition] = useTransition()
|
||||
|
|
@ -84,11 +89,12 @@ export const WizardLeadershipStep = ({
|
|||
const [dirty, setDirty] = useState(false)
|
||||
const [infoTile, setInfoTile] = useState<WizardLeadershipTileViewModel | null>(null)
|
||||
|
||||
const skipConfig = resolveWizardLeadershipSkip(entryAction, municipalitySlug)
|
||||
const skipConfig = resolveWizardLeadershipSkip(entryAction, municipalitySlug, returnPath)
|
||||
const chainContinueHref = wizardChainContinueHref(
|
||||
resolveWizardChainEntry(entryAction, 'update-leadership'),
|
||||
'update-leadership',
|
||||
municipalitySlug,
|
||||
returnPath,
|
||||
)
|
||||
|
||||
const stepTitle =
|
||||
|
|
@ -123,8 +129,8 @@ export const WizardLeadershipStep = ({
|
|||
flowTitle={wizardFlowTitleForSlug(actionSlug)}
|
||||
stepTitle={stepTitle}
|
||||
isEntryStep={false}
|
||||
previousHref={wizardActionHref(actionSlug)}
|
||||
dismissHref={CAMPAIGN_HOME}
|
||||
previousHref={wizardActionHref(actionSlug, undefined, { returnPath })}
|
||||
dismissHref={wizardChainEndHref(returnPath)}
|
||||
municipalityLabel={municipalityName}
|
||||
skip={skipConfig}
|
||||
trailingAction={trailingAction}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,11 @@ import { recordLastActedMunicipality } from '@/lib/campaignLastActedMunicipality
|
|||
import { wizardFlowTitleForSlug } from '@/lib/campaignWizardCopy'
|
||||
import type { MunicipalitySignalType } from '@/lib/schemas/municipalityUpdate'
|
||||
import { municipalitySignalTypeLabels } from '@/lib/schemas/municipalityUpdate'
|
||||
import { resolveWizardChainEntry, wizardChainContinueHref } from '@/lib/wizardActionChain'
|
||||
import {
|
||||
resolveWizardChainEntry,
|
||||
wizardChainContinueHref,
|
||||
wizardChainEndHref,
|
||||
} from '@/lib/wizardActionChain'
|
||||
import {
|
||||
resolveWizardSignalSkip,
|
||||
WIZARD_SIGNAL_BODY_STEP_TITLE_PREFIX,
|
||||
|
|
@ -31,6 +35,7 @@ type WizardSignalBodyStepProps = {
|
|||
municipalitySlug: string
|
||||
signalType: MunicipalitySignalType
|
||||
entryAction?: CampaignWizardActionId
|
||||
returnPath?: string
|
||||
}
|
||||
|
||||
export const WizardSignalBodyStep = ({
|
||||
|
|
@ -40,19 +45,22 @@ export const WizardSignalBodyStep = ({
|
|||
municipalitySlug,
|
||||
signalType,
|
||||
entryAction,
|
||||
returnPath,
|
||||
}: WizardSignalBodyStepProps) => {
|
||||
const router = useRouter()
|
||||
const [state, submitAction, isPending] = useActionState(
|
||||
createMunicipalityListSignalFormAction,
|
||||
{},
|
||||
)
|
||||
const skip = resolveWizardSignalSkip(entryAction, municipalitySlug)
|
||||
const skip = resolveWizardSignalSkip(entryAction, municipalitySlug, returnPath)
|
||||
const stepTitle = `${WIZARD_SIGNAL_BODY_STEP_TITLE_PREFIX}: ${municipalitySignalTypeLabels[signalType]}`
|
||||
|
||||
useCampaignFormSuccessToast(state, () => {
|
||||
recordLastActedMunicipality(municipalitySlug)
|
||||
const sessionEntry = resolveWizardChainEntry(entryAction, 'register-signal')
|
||||
router.replace(wizardChainContinueHref(sessionEntry, 'register-signal', municipalitySlug))
|
||||
router.replace(
|
||||
wizardChainContinueHref(sessionEntry, 'register-signal', municipalitySlug, returnPath),
|
||||
)
|
||||
})
|
||||
|
||||
return (
|
||||
|
|
@ -60,7 +68,14 @@ export const WizardSignalBodyStep = ({
|
|||
flowTitle={wizardFlowTitleForSlug(actionSlug)}
|
||||
isEntryStep={false}
|
||||
stepTitle={stepTitle}
|
||||
previousHref={wizardSignalHref(actionSlug, municipalitySlug, undefined, entryAction)}
|
||||
previousHref={wizardSignalHref(
|
||||
actionSlug,
|
||||
municipalitySlug,
|
||||
undefined,
|
||||
entryAction,
|
||||
returnPath,
|
||||
)}
|
||||
dismissHref={wizardChainEndHref(returnPath)}
|
||||
municipalityLabel={municipalityName}
|
||||
skip={skip}
|
||||
trailingAction={skip ? <WizardSignalSkipTrailing skip={skip} /> : undefined}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import {
|
|||
} from '@/lib/municipalitySignalTypeMeta'
|
||||
import type { MunicipalitySignalType } from '@/lib/schemas/municipalityUpdate'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { wizardChainEndHref } from '@/lib/wizardActionChain'
|
||||
import { resolveWizardSignalSkip, WIZARD_SIGNAL_TYPE_STEP_TITLE } from '@/lib/wizardSignalUi'
|
||||
|
||||
type WizardSignalTypeStepProps = {
|
||||
|
|
@ -32,6 +33,7 @@ type WizardSignalTypeStepProps = {
|
|||
municipalityName: string
|
||||
municipalitySlug: string
|
||||
entryAction?: CampaignWizardActionId
|
||||
returnPath?: string
|
||||
}
|
||||
|
||||
export const WizardSignalTypeStep = ({
|
||||
|
|
@ -39,10 +41,11 @@ export const WizardSignalTypeStep = ({
|
|||
municipalityName,
|
||||
municipalitySlug,
|
||||
entryAction,
|
||||
returnPath,
|
||||
}: WizardSignalTypeStepProps) => {
|
||||
const [infoType, setInfoType] = useState<MunicipalitySignalType | null>(null)
|
||||
const infoEntry = infoType ? municipalitySignalTypeMetaByType[infoType] : null
|
||||
const skip = resolveWizardSignalSkip(entryAction, municipalitySlug)
|
||||
const skip = resolveWizardSignalSkip(entryAction, municipalitySlug, returnPath)
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -50,7 +53,8 @@ export const WizardSignalTypeStep = ({
|
|||
flowTitle={wizardFlowTitleForSlug(actionSlug)}
|
||||
isEntryStep={false}
|
||||
stepTitle={WIZARD_SIGNAL_TYPE_STEP_TITLE}
|
||||
previousHref={wizardActionHref(actionSlug)}
|
||||
previousHref={wizardActionHref(actionSlug, undefined, { returnPath })}
|
||||
dismissHref={wizardChainEndHref(returnPath)}
|
||||
municipalityLabel={municipalityName}
|
||||
contentAlign="end"
|
||||
skip={skip}
|
||||
|
|
@ -62,7 +66,13 @@ export const WizardSignalTypeStep = ({
|
|||
return (
|
||||
<li key={entry.type} className="relative">
|
||||
<CampaignWizardNavLink
|
||||
href={wizardSignalHref(actionSlug, municipalitySlug, entry.type, entryAction)}
|
||||
href={wizardSignalHref(
|
||||
actionSlug,
|
||||
municipalitySlug,
|
||||
entry.type,
|
||||
entryAction,
|
||||
returnPath,
|
||||
)}
|
||||
className={cn(
|
||||
'flex aspect-square w-full flex-col justify-between rounded-lg border border-border bg-transparent p-3 pr-10 text-left',
|
||||
'transition-colors hover:bg-muted/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import {
|
|||
} from '@/lib/politicalTrendWizardUi'
|
||||
import type { PoliticalTrendStatusValue } from '@/lib/schemas/municipality'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { wizardChainEndHref } from '@/lib/wizardActionChain'
|
||||
|
||||
type WizardTrendChoiceStepProps = {
|
||||
actionSlug: string
|
||||
|
|
@ -38,6 +39,7 @@ type WizardTrendChoiceStepProps = {
|
|||
currentStatus: PoliticalTrendStatusValue | null
|
||||
entryAction?: CampaignWizardActionId
|
||||
prefillExtraParams?: Record<string, string>
|
||||
returnPath?: string
|
||||
}
|
||||
|
||||
export const WizardTrendChoiceStep = ({
|
||||
|
|
@ -47,9 +49,10 @@ export const WizardTrendChoiceStep = ({
|
|||
currentStatus,
|
||||
entryAction,
|
||||
prefillExtraParams,
|
||||
returnPath,
|
||||
}: WizardTrendChoiceStepProps) => {
|
||||
const [infoEntry, setInfoEntry] = useState<PoliticalTrendWizardMetaEntry | null>(null)
|
||||
const skip = resolveWizardTrendSkip(entryAction, municipalitySlug)
|
||||
const skip = resolveWizardTrendSkip(entryAction, municipalitySlug, returnPath)
|
||||
const options = selectablePoliticalTrendStatuses(currentStatus)
|
||||
|
||||
return (
|
||||
|
|
@ -58,7 +61,8 @@ export const WizardTrendChoiceStep = ({
|
|||
flowTitle={wizardFlowTitleForSlug(actionSlug)}
|
||||
isEntryStep={false}
|
||||
stepTitle={wizardTrendChoiceStepTitle(currentStatus)}
|
||||
previousHref={wizardActionHref(actionSlug, municipalitySlug, { entryAction })}
|
||||
previousHref={wizardActionHref(actionSlug, municipalitySlug, { entryAction, returnPath })}
|
||||
dismissHref={wizardChainEndHref(returnPath)}
|
||||
municipalityLabel={municipalityName}
|
||||
skip={skip}
|
||||
trailingAction={skip ? <WizardTrendSkipTrailing skip={skip} /> : undefined}
|
||||
|
|
@ -76,6 +80,7 @@ export const WizardTrendChoiceStep = ({
|
|||
status,
|
||||
entryAction,
|
||||
prefillExtraParams,
|
||||
returnPath,
|
||||
)}
|
||||
className={cn(
|
||||
'flex aspect-square w-full flex-col justify-between rounded-lg border bg-background p-3 pr-10 text-left',
|
||||
|
|
|
|||
|
|
@ -21,7 +21,11 @@ import {
|
|||
WIZARD_TREND_SAVE_LABEL,
|
||||
} from '@/lib/politicalTrendWizardUi'
|
||||
import type { PoliticalTrendStatusValue } from '@/lib/schemas/municipality'
|
||||
import { resolveWizardChainEntry, wizardChainContinueHref } from '@/lib/wizardActionChain'
|
||||
import {
|
||||
resolveWizardChainEntry,
|
||||
wizardChainContinueHref,
|
||||
wizardChainEndHref,
|
||||
} from '@/lib/wizardActionChain'
|
||||
import { politicalTrendLabels } from '@/utilities/municipality/municipalityLabels'
|
||||
|
||||
type WizardTrendNoteStepProps = {
|
||||
|
|
@ -32,6 +36,7 @@ type WizardTrendNoteStepProps = {
|
|||
trendStatus: PoliticalTrendStatusValue
|
||||
initialNote: string
|
||||
entryAction?: CampaignWizardActionId
|
||||
returnPath?: string
|
||||
}
|
||||
|
||||
export const WizardTrendNoteStep = ({
|
||||
|
|
@ -42,6 +47,7 @@ export const WizardTrendNoteStep = ({
|
|||
trendStatus,
|
||||
initialNote,
|
||||
entryAction,
|
||||
returnPath,
|
||||
}: WizardTrendNoteStepProps) => {
|
||||
const router = useRouter()
|
||||
const [note, setNote] = useState(initialNote)
|
||||
|
|
@ -49,13 +55,15 @@ export const WizardTrendNoteStep = ({
|
|||
setMunicipalityPoliticalTrendFormAction,
|
||||
{},
|
||||
)
|
||||
const skip = resolveWizardTrendSkip(entryAction, municipalitySlug)
|
||||
const skip = resolveWizardTrendSkip(entryAction, municipalitySlug, returnPath)
|
||||
const stepTitle = `Mudar tendência para ${politicalTrendLabels[trendStatus]}`
|
||||
|
||||
useCampaignFormSuccessToast(state, () => {
|
||||
recordLastActedMunicipality(municipalitySlug)
|
||||
const sessionEntry = resolveWizardChainEntry(entryAction, 'change-trend')
|
||||
router.replace(wizardChainContinueHref(sessionEntry, 'change-trend', municipalitySlug))
|
||||
router.replace(
|
||||
wizardChainContinueHref(sessionEntry, 'change-trend', municipalitySlug, returnPath),
|
||||
)
|
||||
})
|
||||
|
||||
return (
|
||||
|
|
@ -63,7 +71,15 @@ export const WizardTrendNoteStep = ({
|
|||
flowTitle={wizardFlowTitleForSlug(actionSlug)}
|
||||
isEntryStep={false}
|
||||
stepTitle={stepTitle}
|
||||
previousHref={wizardTrendHref(actionSlug, municipalitySlug, undefined, entryAction)}
|
||||
previousHref={wizardTrendHref(
|
||||
actionSlug,
|
||||
municipalitySlug,
|
||||
undefined,
|
||||
entryAction,
|
||||
undefined,
|
||||
returnPath,
|
||||
)}
|
||||
dismissHref={wizardChainEndHref(returnPath)}
|
||||
municipalityLabel={municipalityName}
|
||||
skip={skip}
|
||||
trailingAction={skip ? <WizardTrendSkipTrailing skip={skip} /> : undefined}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ import type { CampaignWizardActionId } from '@/lib/campaignActionRoutes'
|
|||
import { wizardActionHref } from '@/lib/campaignActionRoutes'
|
||||
import { postCampaignJson } from '@/lib/campaignJsonRequest'
|
||||
import { recordLastActedMunicipality } from '@/lib/campaignLastActedMunicipality'
|
||||
import { CAMPAIGN_HOME } from '@/lib/campaignPaths'
|
||||
import {
|
||||
resolveWizardVotesSkip,
|
||||
WIZARD_VOTES_FINAL_CTA_LABEL,
|
||||
|
|
@ -28,7 +27,11 @@ import {
|
|||
wizardNextStepTitle,
|
||||
} from '@/lib/campaignWizardCopy'
|
||||
import { type VoteEstimateScenario, type VoteEstimateScenarioViewModel } from '@/lib/voteEstimate'
|
||||
import { resolveWizardChainEntry, wizardChainContinueHref } from '@/lib/wizardActionChain'
|
||||
import {
|
||||
resolveWizardChainEntry,
|
||||
wizardChainContinueHref,
|
||||
wizardChainEndHref,
|
||||
} from '@/lib/wizardActionChain'
|
||||
import {
|
||||
applyVoteShortcut,
|
||||
getWizardVoteViolation,
|
||||
|
|
@ -45,6 +48,7 @@ type WizardExpectedVotesStepProps = {
|
|||
municipalitySlug: string
|
||||
initialExpectedVotes: VoteEstimateScenarioViewModel
|
||||
entryAction?: CampaignWizardActionId
|
||||
returnPath?: string
|
||||
}
|
||||
|
||||
export const WizardExpectedVotesStep = ({
|
||||
|
|
@ -54,6 +58,7 @@ export const WizardExpectedVotesStep = ({
|
|||
municipalitySlug,
|
||||
initialExpectedVotes,
|
||||
entryAction,
|
||||
returnPath,
|
||||
}: WizardExpectedVotesStepProps) => {
|
||||
const router = useRouter()
|
||||
const [isPending, startTransition] = useTransition()
|
||||
|
|
@ -61,7 +66,7 @@ export const WizardExpectedVotesStep = ({
|
|||
const [focusedScenario, setFocusedScenario] = useState<VoteEstimateScenario>('central')
|
||||
const [violation, setViolation] = useState<WizardVoteViolation | null>(null)
|
||||
const [saveError, setSaveError] = useState<string | null>(null)
|
||||
const skip = resolveWizardVotesSkip(entryAction, municipalitySlug)
|
||||
const skip = resolveWizardVotesSkip(entryAction, municipalitySlug, returnPath)
|
||||
|
||||
const clearViolationState = () => {
|
||||
if (violation) setViolation(null)
|
||||
|
|
@ -80,7 +85,9 @@ export const WizardExpectedVotesStep = ({
|
|||
|
||||
const continueAfterVotes = () => {
|
||||
const sessionEntry = resolveWizardChainEntry(entryAction, 'update-votes')
|
||||
router.replace(wizardChainContinueHref(sessionEntry, 'update-votes', municipalitySlug))
|
||||
router.replace(
|
||||
wizardChainContinueHref(sessionEntry, 'update-votes', municipalitySlug, returnPath),
|
||||
)
|
||||
}
|
||||
|
||||
const handleConfirm = () => {
|
||||
|
|
@ -131,8 +138,8 @@ export const WizardExpectedVotesStep = ({
|
|||
flowTitle={wizardFlowTitleForSlug(actionSlug)}
|
||||
stepTitle={wizardNextStepTitle(actionSlug)}
|
||||
isEntryStep={false}
|
||||
previousHref={wizardActionHref(actionSlug)}
|
||||
dismissHref={CAMPAIGN_HOME}
|
||||
previousHref={wizardActionHref(actionSlug, undefined, { returnPath })}
|
||||
dismissHref={wizardChainEndHref(returnPath)}
|
||||
municipalityLabel={municipalityName}
|
||||
skip={skip}
|
||||
trailingAction={trailingAction}
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ type WizardSearchResultsState =
|
|||
type WizardMunicipalitySearchStepProps = {
|
||||
actionSlug: string
|
||||
previousHref: string
|
||||
returnPath?: string
|
||||
accessibleMunicipalities: AccessibleMunicipality[]
|
||||
}
|
||||
|
||||
|
|
@ -72,6 +73,7 @@ const buildSuggestSuccessState = (
|
|||
export const WizardMunicipalitySearchStep = ({
|
||||
actionSlug,
|
||||
previousHref,
|
||||
returnPath,
|
||||
accessibleMunicipalities,
|
||||
}: WizardMunicipalitySearchStepProps) => {
|
||||
const { query, setRaw, isDebouncing } = useHomeSearchQuery()
|
||||
|
|
@ -216,7 +218,7 @@ export const WizardMunicipalitySearchStep = ({
|
|||
{displayRows.map(({ hit, continuityReason }) => (
|
||||
<li key={hit.slug}>
|
||||
<HomeSearchHitRow
|
||||
href={wizardActionHref(actionSlug, hit.slug)}
|
||||
href={wizardActionHref(actionSlug, hit.slug, { returnPath })}
|
||||
primary={hit.name}
|
||||
secondary={continuityReason ?? hit.region}
|
||||
showPriority={hit.priority === 'alta'}
|
||||
|
|
|
|||
|
|
@ -1,27 +1,29 @@
|
|||
import { CampaignWizardShell } from '@/components/campaign/shared/CampaignWizardShell'
|
||||
import { wizardActionHref } from '@/lib/campaignActionRoutes'
|
||||
import { CAMPAIGN_HOME } from '@/lib/campaignPaths'
|
||||
import {
|
||||
wizardFlowTitleForSlug,
|
||||
wizardNextStepPlaceholder,
|
||||
wizardNextStepTitle,
|
||||
} from '@/lib/campaignWizardCopy'
|
||||
import { wizardChainEndHref } from '@/lib/wizardActionChain'
|
||||
|
||||
type WizardMunicipalitySelectedStubProps = {
|
||||
actionSlug: string
|
||||
municipalityName: string
|
||||
returnPath?: string
|
||||
}
|
||||
|
||||
export const WizardMunicipalitySelectedStub = ({
|
||||
actionSlug,
|
||||
municipalityName,
|
||||
returnPath,
|
||||
}: WizardMunicipalitySelectedStubProps) => (
|
||||
<CampaignWizardShell
|
||||
flowTitle={wizardFlowTitleForSlug(actionSlug)}
|
||||
stepTitle={wizardNextStepTitle(actionSlug)}
|
||||
isEntryStep={false}
|
||||
previousHref={wizardActionHref(actionSlug)}
|
||||
dismissHref={CAMPAIGN_HOME}
|
||||
previousHref={wizardActionHref(actionSlug, undefined, { returnPath })}
|
||||
dismissHref={wizardChainEndHref(returnPath)}
|
||||
municipalityLabel={municipalityName}
|
||||
>
|
||||
<p className="text-sm text-muted-foreground">{wizardNextStepPlaceholder(actionSlug)}</p>
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ const listQuickActions = (): readonly CampaignQuickAction[] => [
|
|||
const wizardActionsWithMunicipalityPrefill = (
|
||||
role: CampaignRole,
|
||||
municipalitySlug: string,
|
||||
returnPath: string,
|
||||
): readonly CampaignQuickAction[] => {
|
||||
const homeActions = homeActionsForRole(role).filter((action) =>
|
||||
WIZARD_ACTION_IDS.includes(action.id as CampaignWizardActionId),
|
||||
|
|
@ -55,7 +56,9 @@ const wizardActionsWithMunicipalityPrefill = (
|
|||
label: action.label,
|
||||
icon: action.icon,
|
||||
description: action.description,
|
||||
href: wizardActionHref(CAMPAIGN_WIZARD_ACTION_SLUGS[wizardId], municipalitySlug),
|
||||
href: wizardActionHref(CAMPAIGN_WIZARD_ACTION_SLUGS[wizardId], municipalitySlug, {
|
||||
returnPath,
|
||||
}),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -93,6 +96,7 @@ export const resolveActivityQuickActions = (
|
|||
surface: ActivityQuickActionSurface,
|
||||
role: CampaignRole,
|
||||
context: CampaignQuickActionContext,
|
||||
pathname?: string,
|
||||
): readonly CampaignQuickAction[] => {
|
||||
if (!isStaffCampaignRole(role)) return []
|
||||
|
||||
|
|
@ -102,6 +106,7 @@ export const resolveActivityQuickActions = (
|
|||
|
||||
const activitySlug = context.activitySlug ?? surface.activitySlug
|
||||
const municipalitySlug = context.municipalitySlug
|
||||
const returnPath = pathname ?? `/campanha/atividades/${activitySlug}`
|
||||
|
||||
const detailActions = detailVerticalQuickActions(activitySlug)
|
||||
|
||||
|
|
@ -109,5 +114,8 @@ export const resolveActivityQuickActions = (
|
|||
return detailActions
|
||||
}
|
||||
|
||||
return [...wizardActionsWithMunicipalityPrefill(role, municipalitySlug), ...detailActions]
|
||||
return [
|
||||
...wizardActionsWithMunicipalityPrefill(role, municipalitySlug, returnPath),
|
||||
...detailActions,
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -67,9 +67,48 @@ export const WIZARD_VOTE_TO_QUERY_KEY = 'voteTo' as const
|
|||
|
||||
export const WIZARD_LEADERSHIP_ID_QUERY_KEY = 'leadershipId' as const
|
||||
|
||||
export const WIZARD_RETURN_PATH_QUERY_KEY = 'from' as const
|
||||
|
||||
const WIZARD_RETURN_PATH_AUTH_PREFIXES = [
|
||||
'/campanha/login',
|
||||
'/campanha/convite',
|
||||
'/campanha/redefinir-senha',
|
||||
'/campanha/webauthn',
|
||||
] as const
|
||||
|
||||
/** Allowlisted internal return paths for wizard dismiss / chain end (B110). */
|
||||
export const isWizardReturnPath = (pathname: string): boolean => {
|
||||
if (!pathname.startsWith('/campanha')) return false
|
||||
if (pathname.startsWith('/campanha/acoes')) return false
|
||||
for (const prefix of WIZARD_RETURN_PATH_AUTH_PREFIXES) {
|
||||
if (pathname === prefix || pathname.startsWith(`${prefix}/`)) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
export const parseWizardReturnPath = (value: string | string[] | undefined): string | undefined => {
|
||||
const raw = Array.isArray(value) ? value[0] : value
|
||||
const trimmed = raw?.trim()
|
||||
if (!trimmed) return undefined
|
||||
if (trimmed.includes('://') || trimmed.startsWith('//')) return undefined
|
||||
const path = trimmed.startsWith('/') ? trimmed : `/${trimmed}`
|
||||
if (path.includes('?') || path.includes('#')) return undefined
|
||||
return isWizardReturnPath(path) ? path : undefined
|
||||
}
|
||||
|
||||
export const appendWizardReturnPath = (href: string, returnPath?: string): string => {
|
||||
if (!returnPath || !isWizardReturnPath(returnPath)) return href
|
||||
const queryIndex = href.indexOf('?')
|
||||
const base = queryIndex === -1 ? href : href.slice(0, queryIndex)
|
||||
const params = new URLSearchParams(queryIndex === -1 ? '' : href.slice(queryIndex + 1))
|
||||
params.set(WIZARD_RETURN_PATH_QUERY_KEY, returnPath)
|
||||
return `${base}?${params.toString()}`
|
||||
}
|
||||
|
||||
export type WizardActionHrefOptions = {
|
||||
entryAction?: CampaignWizardActionId
|
||||
leadershipId?: number
|
||||
returnPath?: string
|
||||
}
|
||||
|
||||
export const wizardActionHref = (
|
||||
|
|
@ -79,7 +118,7 @@ export const wizardActionHref = (
|
|||
): string => {
|
||||
const base = `${CAMPAIGN_ACTIONS_HOME}/${actionSlug}`
|
||||
if (!municipalitySlug && !options?.leadershipId && !options?.entryAction) {
|
||||
return base
|
||||
return appendWizardReturnPath(base, options?.returnPath)
|
||||
}
|
||||
const params = new URLSearchParams()
|
||||
if (municipalitySlug) {
|
||||
|
|
@ -91,7 +130,8 @@ export const wizardActionHref = (
|
|||
if (options?.leadershipId !== undefined) {
|
||||
params.set(WIZARD_LEADERSHIP_ID_QUERY_KEY, String(options.leadershipId))
|
||||
}
|
||||
return `${base}?${params.toString()}`
|
||||
const href = params.size > 0 ? `${base}?${params.toString()}` : base
|
||||
return appendWizardReturnPath(href, options?.returnPath)
|
||||
}
|
||||
|
||||
export const parseWizardMunicipioParam = (
|
||||
|
|
@ -132,10 +172,11 @@ export const wizardSignalHref = (
|
|||
municipalitySlug?: string,
|
||||
signalType?: MunicipalitySignalType,
|
||||
entryAction?: CampaignWizardActionId,
|
||||
returnPath?: string,
|
||||
): string => {
|
||||
const base = `${CAMPAIGN_ACTIONS_HOME}/${actionSlug}`
|
||||
if (!municipalitySlug) {
|
||||
return base
|
||||
return appendWizardReturnPath(base, returnPath)
|
||||
}
|
||||
|
||||
const params = new URLSearchParams({ [WIZARD_MUNICIPIO_QUERY_KEY]: municipalitySlug })
|
||||
|
|
@ -145,7 +186,7 @@ export const wizardSignalHref = (
|
|||
if (entryAction) {
|
||||
params.set(WIZARD_ENTRY_ACTION_QUERY_KEY, entryAction)
|
||||
}
|
||||
return `${base}?${params.toString()}`
|
||||
return appendWizardReturnPath(`${base}?${params.toString()}`, returnPath)
|
||||
}
|
||||
|
||||
export const resolveWizardSignalTypeParam = (
|
||||
|
|
@ -171,10 +212,11 @@ export const wizardTrendHref = (
|
|||
trendStatus?: PoliticalTrendStatusValue,
|
||||
entryAction?: CampaignWizardActionId,
|
||||
extraParams?: Record<string, string>,
|
||||
returnPath?: string,
|
||||
): string => {
|
||||
const base = `${CAMPAIGN_ACTIONS_HOME}/${actionSlug}`
|
||||
if (!municipalitySlug) {
|
||||
return base
|
||||
return appendWizardReturnPath(base, returnPath)
|
||||
}
|
||||
|
||||
const params = new URLSearchParams({ [WIZARD_MUNICIPIO_QUERY_KEY]: municipalitySlug })
|
||||
|
|
@ -189,7 +231,7 @@ export const wizardTrendHref = (
|
|||
if (value) params.set(key, value)
|
||||
}
|
||||
}
|
||||
return `${base}?${params.toString()}`
|
||||
return appendWizardReturnPath(`${base}?${params.toString()}`, returnPath)
|
||||
}
|
||||
|
||||
export const resolveWizardTrendStatusParam = (
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import {
|
|||
} from 'lucide-react'
|
||||
|
||||
import {
|
||||
appendWizardReturnPath,
|
||||
CAMPAIGN_WIZARD_ACTION_SLUGS,
|
||||
campaignActionEntryHref,
|
||||
isCampaignWizardActionId,
|
||||
|
|
@ -132,6 +133,7 @@ export const UNCOVERED_MUNICIPALITIES_LIST_HREF =
|
|||
export type HomeActionButtonPropsOptions = {
|
||||
uncoveredMunicipalitiesHref?: string
|
||||
municipalitySlug?: string
|
||||
returnPath?: string
|
||||
}
|
||||
|
||||
/** Staff Início catalog with hrefs — shared by quick-action registries (B80+). */
|
||||
|
|
@ -139,14 +141,20 @@ const staffHomeQuickActionsByRole = new Map<CampaignRole, readonly ResolvedCampa
|
|||
|
||||
export const resolveStaffHomeQuickActions = (
|
||||
role: CampaignRole,
|
||||
returnPath?: string,
|
||||
): readonly ResolvedCampaignHomeAction[] => {
|
||||
if (!isStaffCampaignRole(role)) return []
|
||||
const cached = staffHomeQuickActionsByRole.get(role)
|
||||
if (cached) return cached
|
||||
if (!returnPath) {
|
||||
const cached = staffHomeQuickActionsByRole.get(role)
|
||||
if (cached) return cached
|
||||
}
|
||||
const actions = toHomeActionButtonProps(homeActionsForRole(role), {
|
||||
uncoveredMunicipalitiesHref: UNCOVERED_MUNICIPALITIES_LIST_HREF,
|
||||
returnPath,
|
||||
})
|
||||
staffHomeQuickActionsByRole.set(role, actions)
|
||||
if (!returnPath) {
|
||||
staffHomeQuickActionsByRole.set(role, actions)
|
||||
}
|
||||
return actions
|
||||
}
|
||||
|
||||
|
|
@ -164,9 +172,17 @@ export const toHomeActionButtonProps = (
|
|||
} else if (action.id === 'uncovered-municipalities') {
|
||||
href = resolved.uncoveredMunicipalitiesHref
|
||||
} else if (isCampaignWizardActionId(action.id)) {
|
||||
href = resolved.municipalitySlug
|
||||
? wizardActionHref(CAMPAIGN_WIZARD_ACTION_SLUGS[action.id], resolved.municipalitySlug)
|
||||
: campaignActionEntryHref(action.id)
|
||||
if (resolved.municipalitySlug) {
|
||||
href = wizardActionHref(
|
||||
CAMPAIGN_WIZARD_ACTION_SLUGS[action.id],
|
||||
resolved.municipalitySlug,
|
||||
{
|
||||
returnPath: resolved.returnPath,
|
||||
},
|
||||
)
|
||||
} else {
|
||||
href = appendWizardReturnPath(campaignActionEntryHref(action.id), resolved.returnPath)
|
||||
}
|
||||
}
|
||||
return {
|
||||
...action,
|
||||
|
|
|
|||
|
|
@ -22,18 +22,20 @@ export const parseMunicipalityDetailSlug = (pathname: string): string | undefine
|
|||
|
||||
export const resolveMunicipalityListQuickActions = (
|
||||
role: CampaignRole,
|
||||
): readonly ResolvedCampaignHomeAction[] => resolveStaffHomeQuickActions(role)
|
||||
returnPath?: string,
|
||||
): readonly ResolvedCampaignHomeAction[] => resolveStaffHomeQuickActions(role, returnPath)
|
||||
|
||||
export const resolveMunicipalityDetailQuickActions = (
|
||||
role: CampaignRole,
|
||||
municipalitySlug: string,
|
||||
returnPath?: string,
|
||||
): readonly ResolvedCampaignHomeAction[] => {
|
||||
if (!isStaffCampaignRole(role)) return []
|
||||
|
||||
const actions = homeActionsForRole(role).filter(
|
||||
(action) => action.id !== 'uncovered-municipalities',
|
||||
)
|
||||
return toHomeActionButtonProps(actions, { municipalitySlug })
|
||||
return toHomeActionButtonProps(actions, { municipalitySlug, returnPath })
|
||||
}
|
||||
|
||||
export const resolveMunicipalityQuickActionsForPath = (
|
||||
|
|
@ -42,12 +44,12 @@ export const resolveMunicipalityQuickActionsForPath = (
|
|||
context: CampaignQuickActionContext,
|
||||
): readonly ResolvedCampaignHomeAction[] => {
|
||||
if (isMunicipalitiesListPath(pathname)) {
|
||||
return resolveMunicipalityListQuickActions(role)
|
||||
return resolveMunicipalityListQuickActions(role, pathname)
|
||||
}
|
||||
|
||||
const slug = context.municipalitySlug ?? parseMunicipalityDetailSlug(pathname)
|
||||
if (slug) {
|
||||
return resolveMunicipalityDetailQuickActions(role, slug)
|
||||
return resolveMunicipalityDetailQuickActions(role, slug, pathname)
|
||||
}
|
||||
|
||||
return []
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ export const resolveDemandsListQuickActions = (
|
|||
export const resolveDemandDetailQuickActions = (
|
||||
role: CampaignRole,
|
||||
context: CampaignQuickActionContext,
|
||||
returnPath: string,
|
||||
): readonly CampaignQuickAction[] => {
|
||||
if (!isStaffCampaignRole(role)) {
|
||||
return []
|
||||
|
|
@ -93,7 +94,9 @@ export const resolveDemandDetailQuickActions = (
|
|||
return [
|
||||
{
|
||||
...action,
|
||||
href: wizardActionHref(CAMPAIGN_WIZARD_ACTION_SLUGS[id], municipalitySlug),
|
||||
href: wizardActionHref(CAMPAIGN_WIZARD_ACTION_SLUGS[id], municipalitySlug, {
|
||||
returnPath,
|
||||
}),
|
||||
},
|
||||
]
|
||||
})
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ export const resolveDobradinhasQuickActions = (
|
|||
return []
|
||||
}
|
||||
|
||||
const staffActions = resolveStaffHomeQuickActions(role)
|
||||
const staffActions = resolveStaffHomeQuickActions(role, pathname)
|
||||
|
||||
if (isStateDeputyListPath(pathname)) {
|
||||
return [newStateDeputyAction(), ...staffActions]
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import {
|
||||
CAMPAIGN_WIZARD_ACTION_SLUGS,
|
||||
appendWizardReturnPath,
|
||||
campaignActionEntryHref,
|
||||
wizardActionHref,
|
||||
type CampaignWizardActionId,
|
||||
|
|
@ -34,40 +35,46 @@ const parseLeadershipDetailId = (pathname: string): number | undefined => {
|
|||
|
||||
const resolveLeadershipListActions = (
|
||||
role: CampaignRole,
|
||||
returnPath: string,
|
||||
): readonly ResolvedCampaignHomeAction[] => {
|
||||
const actions = homeActionsForRole(role).filter((action) => action.id === 'update-leadership')
|
||||
return actions.map((action) => ({
|
||||
...action,
|
||||
href: campaignActionEntryHref('update-leadership'),
|
||||
href: appendWizardReturnPath(campaignActionEntryHref('update-leadership'), returnPath),
|
||||
}))
|
||||
}
|
||||
|
||||
const hrefForLeadershipDetailAction = (
|
||||
action: CampaignHomeAction,
|
||||
context: CampaignQuickActionContext,
|
||||
returnPath: string,
|
||||
): string => {
|
||||
const wizardSlug = CAMPAIGN_WIZARD_ACTION_SLUGS[action.id as CampaignWizardActionId]
|
||||
const { municipalitySlug, leadershipId } = context
|
||||
|
||||
if (action.id === 'update-leadership') {
|
||||
if (municipalitySlug && leadershipId !== undefined) {
|
||||
return wizardActionHref(wizardSlug, municipalitySlug, { leadershipId })
|
||||
return wizardActionHref(wizardSlug, municipalitySlug, { leadershipId, returnPath })
|
||||
}
|
||||
if (leadershipId !== undefined) {
|
||||
return `${LEADERSHIP_LIST_PATH}/${leadershipId}`
|
||||
}
|
||||
return campaignActionEntryHref('update-leadership')
|
||||
return appendWizardReturnPath(campaignActionEntryHref('update-leadership'), returnPath)
|
||||
}
|
||||
|
||||
if (municipalitySlug) {
|
||||
return wizardActionHref(wizardSlug, municipalitySlug)
|
||||
return wizardActionHref(wizardSlug, municipalitySlug, { returnPath })
|
||||
}
|
||||
return campaignActionEntryHref(action.id as CampaignWizardActionId)
|
||||
return appendWizardReturnPath(
|
||||
campaignActionEntryHref(action.id as CampaignWizardActionId),
|
||||
returnPath,
|
||||
)
|
||||
}
|
||||
|
||||
const resolveLeadershipDetailActions = (
|
||||
role: CampaignRole,
|
||||
context: CampaignQuickActionContext,
|
||||
returnPath: string,
|
||||
): readonly ResolvedCampaignHomeAction[] => {
|
||||
const actions = homeActionsForRole(role).filter((action) =>
|
||||
LEADERSHIP_DETAIL_WIZARD_IDS.includes(action.id as CampaignWizardActionId),
|
||||
|
|
@ -75,7 +82,7 @@ const resolveLeadershipDetailActions = (
|
|||
|
||||
return actions.map((action) => ({
|
||||
...action,
|
||||
href: hrefForLeadershipDetailAction(action, context),
|
||||
href: hrefForLeadershipDetailAction(action, context, returnPath),
|
||||
}))
|
||||
}
|
||||
|
||||
|
|
@ -87,15 +94,19 @@ export const resolveLeadershipQuickActions = (
|
|||
if (!isStaffCampaignRole(role)) return null
|
||||
|
||||
if (isLeadershipListPath(pathname)) {
|
||||
return resolveLeadershipListActions(role)
|
||||
return resolveLeadershipListActions(role, pathname)
|
||||
}
|
||||
|
||||
const leadershipId = parseLeadershipDetailId(pathname)
|
||||
if (leadershipId !== undefined) {
|
||||
return resolveLeadershipDetailActions(role, {
|
||||
...context,
|
||||
leadershipId: context.leadershipId ?? leadershipId,
|
||||
})
|
||||
return resolveLeadershipDetailActions(
|
||||
role,
|
||||
{
|
||||
...context,
|
||||
leadershipId: context.leadershipId ?? leadershipId,
|
||||
},
|
||||
pathname,
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ export const resolveQuickActionsForPath = (
|
|||
|
||||
const activitySurface = parseActivityQuickActionSurface(pathname)
|
||||
if (activitySurface) {
|
||||
return resolveActivityQuickActions(activitySurface, role, context)
|
||||
return resolveActivityQuickActions(activitySurface, role, context, pathname)
|
||||
}
|
||||
|
||||
const organizationSurface = parseOrganizationQuickActionSurface(pathname)
|
||||
|
|
@ -60,7 +60,7 @@ export const resolveQuickActionsForPath = (
|
|||
return resolveDemandsListQuickActions(role)
|
||||
}
|
||||
if (isDemandDetailPath(pathname)) {
|
||||
return resolveDemandDetailQuickActions(role, context)
|
||||
return resolveDemandDetailQuickActions(role, context, pathname)
|
||||
}
|
||||
|
||||
const municipalityActions = resolveMunicipalityQuickActionsForPath(pathname, role, context)
|
||||
|
|
@ -79,7 +79,7 @@ export const resolveQuickActionsForPath = (
|
|||
if (advisorActions.length > 0) return advisorActions
|
||||
|
||||
if (isTerritoriesListPath(pathname)) {
|
||||
return resolveStaffHomeQuickActions(role)
|
||||
return resolveStaffHomeQuickActions(role, pathname)
|
||||
}
|
||||
|
||||
const referenceActions = resolveReferenceQuickActionsForPath(pathname, role, context)
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ const shouldShowWizardVotesSkip = (entryAction: CampaignWizardActionId | undefin
|
|||
export const resolveWizardVotesSkip = (
|
||||
entryAction: CampaignWizardActionId | undefined,
|
||||
municipalitySlug: string,
|
||||
returnPath?: string,
|
||||
): WizardVotesSkipAction | undefined =>
|
||||
shouldShowWizardVotesSkip(entryAction)
|
||||
? {
|
||||
|
|
@ -57,6 +58,7 @@ export const resolveWizardVotesSkip = (
|
|||
resolveWizardChainEntry(entryAction, 'update-votes'),
|
||||
'update-votes',
|
||||
municipalitySlug,
|
||||
returnPath,
|
||||
),
|
||||
}
|
||||
: undefined
|
||||
|
|
|
|||
|
|
@ -69,6 +69,7 @@ export const shouldShowWizardTrendSkip = (
|
|||
export const resolveWizardTrendSkip = (
|
||||
entryAction: CampaignWizardActionId | undefined,
|
||||
municipalitySlug: string,
|
||||
returnPath?: string,
|
||||
): WizardTrendSkipAction | undefined =>
|
||||
shouldShowWizardTrendSkip(entryAction)
|
||||
? {
|
||||
|
|
@ -77,6 +78,7 @@ export const resolveWizardTrendSkip = (
|
|||
resolveWizardChainEntry(entryAction, 'change-trend'),
|
||||
'change-trend',
|
||||
municipalitySlug,
|
||||
returnPath,
|
||||
),
|
||||
}
|
||||
: undefined
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
import {
|
||||
CAMPAIGN_WIZARD_ACTION_SLUGS,
|
||||
type CampaignWizardActionId,
|
||||
isWizardReturnPath,
|
||||
wizardActionHref,
|
||||
wizardSignalHref,
|
||||
wizardTrendHref,
|
||||
|
|
@ -54,16 +55,24 @@ const wizardHrefForChainStep = (
|
|||
action: WizardChainActionId,
|
||||
municipalitySlug: string,
|
||||
entryAction: WizardChainActionId,
|
||||
returnPath?: string,
|
||||
): string => {
|
||||
const actionSlug = CAMPAIGN_WIZARD_ACTION_SLUGS[action]
|
||||
switch (action) {
|
||||
case 'update-votes':
|
||||
case 'update-leadership':
|
||||
return wizardActionHref(actionSlug, municipalitySlug, { entryAction })
|
||||
return wizardActionHref(actionSlug, municipalitySlug, { entryAction, returnPath })
|
||||
case 'register-signal':
|
||||
return wizardSignalHref(actionSlug, municipalitySlug, undefined, entryAction)
|
||||
return wizardSignalHref(actionSlug, municipalitySlug, undefined, entryAction, returnPath)
|
||||
case 'change-trend':
|
||||
return wizardTrendHref(actionSlug, municipalitySlug, undefined, entryAction)
|
||||
return wizardTrendHref(
|
||||
actionSlug,
|
||||
municipalitySlug,
|
||||
undefined,
|
||||
entryAction,
|
||||
undefined,
|
||||
returnPath,
|
||||
)
|
||||
default: {
|
||||
const exhaustive: never = action
|
||||
return exhaustive
|
||||
|
|
@ -71,23 +80,28 @@ const wizardHrefForChainStep = (
|
|||
}
|
||||
}
|
||||
|
||||
/** Chain end or dismiss target: allowlisted `from`, else Início (B110). */
|
||||
export const wizardChainEndHref = (returnPath?: string): string =>
|
||||
returnPath && isWizardReturnPath(returnPath) ? returnPath : CAMPAIGN_HOME
|
||||
|
||||
/**
|
||||
* Href for the next chained wizard after completing/skipping `completedAction`,
|
||||
* or Início when the queue is empty / entry is not chainable.
|
||||
* or the return path (default Início) when the queue is empty / entry is not chainable.
|
||||
*/
|
||||
export const wizardChainContinueHref = (
|
||||
entryAction: CampaignWizardActionId,
|
||||
completedAction: WizardChainActionId,
|
||||
municipalitySlug: string,
|
||||
returnPath?: string,
|
||||
): string => {
|
||||
if (!isWizardChainActionId(entryAction)) {
|
||||
return CAMPAIGN_HOME
|
||||
return wizardChainEndHref(returnPath)
|
||||
}
|
||||
const next = nextWizardChainStep(entryAction, completedAction)
|
||||
if (!next) {
|
||||
return CAMPAIGN_HOME
|
||||
return wizardChainEndHref(returnPath)
|
||||
}
|
||||
return wizardHrefForChainStep(next, municipalitySlug, entryAction)
|
||||
return wizardHrefForChainStep(next, municipalitySlug, entryAction, returnPath)
|
||||
}
|
||||
|
||||
/** Session owner for navigation: explicit `entry` query, else the current principal. */
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ export const showLeadershipWizardSkip = (entryAction?: CampaignWizardActionId):
|
|||
export const resolveWizardLeadershipSkip = (
|
||||
entryAction: CampaignWizardActionId | undefined,
|
||||
municipalitySlug: string,
|
||||
returnPath?: string,
|
||||
): WizardLeadershipSkipAction | undefined =>
|
||||
showLeadershipWizardSkip(entryAction)
|
||||
? {
|
||||
|
|
@ -35,6 +36,7 @@ export const resolveWizardLeadershipSkip = (
|
|||
resolveWizardChainEntry(entryAction, 'update-leadership'),
|
||||
'update-leadership',
|
||||
municipalitySlug,
|
||||
returnPath,
|
||||
),
|
||||
}
|
||||
: undefined
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ export const shouldShowWizardSignalSkip = (
|
|||
export const resolveWizardSignalSkip = (
|
||||
entryAction: CampaignWizardActionId | undefined,
|
||||
municipalitySlug: string,
|
||||
returnPath?: string,
|
||||
): WizardSignalSkipAction | undefined =>
|
||||
shouldShowWizardSignalSkip(entryAction)
|
||||
? {
|
||||
|
|
@ -30,6 +31,7 @@ export const resolveWizardSignalSkip = (
|
|||
resolveWizardChainEntry(entryAction, 'register-signal'),
|
||||
'register-signal',
|
||||
municipalitySlug,
|
||||
returnPath,
|
||||
),
|
||||
}
|
||||
: undefined
|
||||
|
|
|
|||
|
|
@ -218,7 +218,7 @@ describe('campaignQuickActionDobradinhas (B83)', () => {
|
|||
expect(actions.map((action) => action.id)).toEqual(['new-state-deputy', ...staffActionIds])
|
||||
expect(actions[0]?.href).toBe('/campanha/dobradinhas/nova')
|
||||
expect(actions.find((action) => action.id === 'update-votes')?.href).toBe(
|
||||
'/campanha/acoes/atualizar-votos',
|
||||
'/campanha/acoes/atualizar-votos?from=%2Fcampanha%2Fdobradinhas',
|
||||
)
|
||||
expect(actions.find((action) => action.id === 'uncovered-municipalities')?.href).toBe(
|
||||
UNCOVERED_MUNICIPALITIES_LIST_HREF,
|
||||
|
|
@ -279,7 +279,7 @@ describe('campaignQuickActionRegistry', () => {
|
|||
})
|
||||
expect(detail).toHaveLength(5)
|
||||
expect(detail.find((action) => action.id === 'update-votes')?.href).toBe(
|
||||
'/campanha/acoes/atualizar-votos?municipio=foo',
|
||||
'/campanha/acoes/atualizar-votos?municipio=foo&from=%2Fcampanha%2Fmunicipios%2Ffoo',
|
||||
)
|
||||
})
|
||||
|
||||
|
|
@ -303,7 +303,7 @@ describe('campaignQuickActionRegistry', () => {
|
|||
)
|
||||
const registerSignal = actions.find((action) => action.id === 'register-signal')
|
||||
expect(registerSignal?.href).toBe(
|
||||
`/campanha/acoes/${CAMPAIGN_WIZARD_ACTION_SLUGS['register-signal']}?municipio=salvador-ze-01`,
|
||||
`/campanha/acoes/${CAMPAIGN_WIZARD_ACTION_SLUGS['register-signal']}?municipio=salvador-ze-01&from=%2Fcampanha%2Fatividades%2Fevento-zona-1`,
|
||||
)
|
||||
})
|
||||
|
||||
|
|
@ -311,7 +311,7 @@ describe('campaignQuickActionRegistry', () => {
|
|||
const actions = resolveQuickActionsForPath('/campanha/territorios', 'coordinator', {})
|
||||
expect(actions.map((action) => action.id)).toEqual(staffActionIds)
|
||||
expect(actions.find((action) => action.id === 'update-votes')?.href).toBe(
|
||||
'/campanha/acoes/atualizar-votos',
|
||||
'/campanha/acoes/atualizar-votos?from=%2Fcampanha%2Fterritorios',
|
||||
)
|
||||
expect(actions.find((action) => action.id === 'uncovered-municipalities')?.href).toBe(
|
||||
UNCOVERED_MUNICIPALITIES_LIST_HREF,
|
||||
|
|
@ -346,7 +346,7 @@ describe('campaignQuickActionRegistry', () => {
|
|||
'register-demand',
|
||||
])
|
||||
expect(actions.find((action) => action.id === 'update-votes')?.href).toBe(
|
||||
'/campanha/acoes/atualizar-votos?municipio=cairu',
|
||||
'/campanha/acoes/atualizar-votos?municipio=cairu&from=%2Fcampanha%2Fdemandas%2Fpedido-cairu',
|
||||
)
|
||||
expect(actions.find((action) => action.id === 'register-demand')?.href).toBe(
|
||||
demandCreateHref(42),
|
||||
|
|
@ -389,7 +389,11 @@ describe('campaignQuickActionDemands resolvers', () => {
|
|||
it('returns empty catalog for leader lockdown', () => {
|
||||
expect(resolveDemandsListQuickActions('leader')).toEqual([])
|
||||
expect(
|
||||
resolveDemandDetailQuickActions('leader', { municipalitySlug: 'cairu', municipalityId: 1 }),
|
||||
resolveDemandDetailQuickActions(
|
||||
'leader',
|
||||
{ municipalitySlug: 'cairu', municipalityId: 1 },
|
||||
'/campanha/demandas/foo',
|
||||
),
|
||||
).toEqual([])
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -8,7 +8,9 @@ describe('resolveLeadershipQuickActions', () => {
|
|||
const actions = resolveLeadershipQuickActions('/campanha/liderancas', 'coordinator', {})
|
||||
expect(actions).toHaveLength(1)
|
||||
expect(actions?.[0]?.id).toBe('update-leadership')
|
||||
expect(actions?.[0]?.href).toBe(`${CAMPAIGN_ACTIONS_HOME}/atualizar-lideranca`)
|
||||
expect(actions?.[0]?.href).toBe(
|
||||
`${CAMPAIGN_ACTIONS_HOME}/atualizar-lideranca?from=%2Fcampanha%2Fliderancas`,
|
||||
)
|
||||
})
|
||||
|
||||
it('returns five wizard actions on detail with municipality prefill when N=1', () => {
|
||||
|
|
@ -24,10 +26,10 @@ describe('resolveLeadershipQuickActions', () => {
|
|||
'register-demand',
|
||||
])
|
||||
expect(actions?.find((action) => action.id === 'update-votes')?.href).toBe(
|
||||
`${CAMPAIGN_ACTIONS_HOME}/atualizar-votos?municipio=cairu`,
|
||||
`${CAMPAIGN_ACTIONS_HOME}/atualizar-votos?municipio=cairu&from=%2Fcampanha%2Fliderancas%2F42`,
|
||||
)
|
||||
expect(actions?.find((action) => action.id === 'update-leadership')?.href).toBe(
|
||||
`${CAMPAIGN_ACTIONS_HOME}/atualizar-lideranca?municipio=cairu&leadershipId=42`,
|
||||
`${CAMPAIGN_ACTIONS_HOME}/atualizar-lideranca?municipio=cairu&leadershipId=42&from=%2Fcampanha%2Fliderancas%2F42`,
|
||||
)
|
||||
})
|
||||
|
||||
|
|
@ -39,7 +41,7 @@ describe('resolveLeadershipQuickActions', () => {
|
|||
'/campanha/liderancas/7',
|
||||
)
|
||||
expect(actions?.find((action) => action.id === 'update-votes')?.href).toBe(
|
||||
`${CAMPAIGN_ACTIONS_HOME}/atualizar-votos`,
|
||||
`${CAMPAIGN_ACTIONS_HOME}/atualizar-votos?from=%2Fcampanha%2Fliderancas%2F7`,
|
||||
)
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -72,6 +72,16 @@ describe('wizardActionChain', () => {
|
|||
expect(wizardChainContinueHref('register-demand', 'update-votes', 'cairu')).toBe(CAMPAIGN_HOME)
|
||||
})
|
||||
|
||||
it('honors allowlisted return path when the chain ends', () => {
|
||||
const origin = '/campanha/municipios/cairu'
|
||||
expect(wizardChainContinueHref('change-trend', 'update-leadership', 'cairu', origin)).toBe(
|
||||
origin,
|
||||
)
|
||||
expect(wizardChainContinueHref('update-votes', 'change-trend', 'cairu', origin)).toBe(
|
||||
`${CAMPAIGN_ACTIONS_HOME}/registrar-sinal?municipio=cairu&entry=update-votes&from=${encodeURIComponent(origin)}`,
|
||||
)
|
||||
})
|
||||
|
||||
it('resolves session entry from query or current principal', () => {
|
||||
expect(resolveWizardChainEntry(undefined, 'update-votes')).toBe('update-votes')
|
||||
expect(resolveWizardChainEntry('register-signal', 'update-votes')).toBe('register-signal')
|
||||
|
|
|
|||
|
|
@ -27,8 +27,12 @@ describe('resolveWizardLeadershipSkip', () => {
|
|||
expect(resolveWizardLeadershipSkip('update-leadership', 'cairu')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('points skip to Início when leadership is the last chained step', () => {
|
||||
// In the v1 matrix, leadership is always the final chained elo.
|
||||
it('points skip to origin when from is set, else Início at chain end', () => {
|
||||
const origin = '/campanha/liderancas/7'
|
||||
expect(resolveWizardLeadershipSkip('update-votes', 'cairu', origin)).toEqual({
|
||||
label: WIZARD_CHAIN_SKIP_LABEL,
|
||||
href: origin,
|
||||
})
|
||||
expect(resolveWizardLeadershipSkip('update-votes', 'cairu')).toEqual({
|
||||
label: WIZARD_CHAIN_SKIP_LABEL,
|
||||
href: CAMPAIGN_HOME,
|
||||
|
|
|
|||
51
tests/unit/wizardReturnPath.unit.spec.ts
Normal file
51
tests/unit/wizardReturnPath.unit.spec.ts
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import {
|
||||
appendWizardReturnPath,
|
||||
isWizardReturnPath,
|
||||
parseWizardReturnPath,
|
||||
WIZARD_RETURN_PATH_QUERY_KEY,
|
||||
} from '@/lib/campaignActionRoutes'
|
||||
import { CAMPAIGN_HOME } from '@/lib/campaignPaths'
|
||||
|
||||
describe('wizard return path (B110)', () => {
|
||||
it('allowlists internal /campanha paths outside acoes and auth', () => {
|
||||
expect(isWizardReturnPath('/campanha')).toBe(true)
|
||||
expect(isWizardReturnPath('/campanha/municipios/cairu')).toBe(true)
|
||||
expect(isWizardReturnPath('/campanha/liderancas/42')).toBe(true)
|
||||
expect(isWizardReturnPath('/campanha/acoes/mudar-tendencia')).toBe(false)
|
||||
expect(isWizardReturnPath('/campanha/login')).toBe(false)
|
||||
expect(isWizardReturnPath('/campanha/convite/token')).toBe(false)
|
||||
expect(isWizardReturnPath('https://evil.test/phish')).toBe(false)
|
||||
expect(isWizardReturnPath('/public')).toBe(false)
|
||||
})
|
||||
|
||||
it('parseWizardReturnPath fails closed on invalid values', () => {
|
||||
expect(parseWizardReturnPath(undefined)).toBeUndefined()
|
||||
expect(parseWizardReturnPath('/campanha/municipios/foo')).toBe('/campanha/municipios/foo')
|
||||
expect(parseWizardReturnPath('/campanha/acoes/foo')).toBeUndefined()
|
||||
expect(parseWizardReturnPath('//evil.test')).toBeUndefined()
|
||||
expect(parseWizardReturnPath('/campanha/foo?x=1')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('appendWizardReturnPath adds the from query key', () => {
|
||||
const href = appendWizardReturnPath(
|
||||
'/campanha/acoes/mudar-tendencia?municipio=cairu',
|
||||
'/campanha/municipios/cairu',
|
||||
)
|
||||
expect(href).toContain(`${WIZARD_RETURN_PATH_QUERY_KEY}=`)
|
||||
expect(href).toContain(
|
||||
`${WIZARD_RETURN_PATH_QUERY_KEY}=${encodeURIComponent('/campanha/municipios/cairu')}`,
|
||||
)
|
||||
})
|
||||
|
||||
it('appendWizardReturnPath ignores invalid return paths', () => {
|
||||
expect(appendWizardReturnPath('/campanha/acoes/foo', '/campanha/acoes/bar')).toBe(
|
||||
'/campanha/acoes/foo',
|
||||
)
|
||||
})
|
||||
|
||||
it('CAMPAIGN_HOME is a valid return path', () => {
|
||||
expect(parseWizardReturnPath(CAMPAIGN_HOME)).toBe(CAMPAIGN_HOME)
|
||||
})
|
||||
})
|
||||
Loading…
Reference in a new issue