mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-12 22:13:05 +00:00
B110 — wizard return path via allowlisted from query
Propagate return path from quick actions through the B98 wizard chain so dismiss, skip, and chain end navigate back to the originating page instead of always redirecting to Início. Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
This commit is contained in:
parent
5c21552621
commit
d5697bcfcb
28 changed files with 315 additions and 85 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,13 @@ 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 +84,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 +106,8 @@ export default async function CampaignActionWizardPage({
|
|||
return (
|
||||
<WizardMunicipalitySearchStep
|
||||
actionSlug={slug}
|
||||
previousHref={CAMPAIGN_HOME}
|
||||
previousHref={wizardChainEndHref(returnPath)}
|
||||
returnPath={returnPath}
|
||||
accessibleMunicipalities={accessibleMunicipalities}
|
||||
/>
|
||||
)
|
||||
|
|
@ -142,6 +145,7 @@ export default async function CampaignActionWizardPage({
|
|||
municipalitySlug={municipality.slug}
|
||||
initialExpectedVotes={toVoteEstimateScenarioViewModel(municipality.expectedVotes)}
|
||||
entryAction={entryAction}
|
||||
returnPath={returnPath}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
@ -164,6 +168,7 @@ export default async function CampaignActionWizardPage({
|
|||
entryAction={entryAction}
|
||||
initialTiles={tiles}
|
||||
initialLeadershipId={initialLeadershipId}
|
||||
returnPath={returnPath}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
@ -200,9 +205,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 +227,7 @@ export default async function CampaignActionWizardPage({
|
|||
trendStatus={trendStatus}
|
||||
initialNote={initialNote}
|
||||
entryAction={entryAction}
|
||||
returnPath={returnPath}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
@ -232,6 +240,7 @@ export default async function CampaignActionWizardPage({
|
|||
currentStatus={currentStatus}
|
||||
entryAction={entryAction}
|
||||
prefillExtraParams={prefillExtraParams}
|
||||
returnPath={returnPath}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
@ -257,6 +266,7 @@ export default async function CampaignActionWizardPage({
|
|||
municipalitySlug={municipality.slug}
|
||||
signalType={signalType}
|
||||
entryAction={entryAction}
|
||||
returnPath={returnPath}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
@ -267,9 +277,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,7 @@ 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 +49,7 @@ type WizardLeadershipStepProps = {
|
|||
entryAction?: CampaignWizardActionId
|
||||
initialTiles: WizardLeadershipTileViewModel[]
|
||||
initialLeadershipId?: number
|
||||
returnPath?: string
|
||||
}
|
||||
|
||||
type WizardLeadershipMode =
|
||||
|
|
@ -70,6 +70,7 @@ export const WizardLeadershipStep = ({
|
|||
entryAction,
|
||||
initialTiles,
|
||||
initialLeadershipId,
|
||||
returnPath,
|
||||
}: WizardLeadershipStepProps) => {
|
||||
const router = useRouter()
|
||||
const [isContinuing, startContinueTransition] = useTransition()
|
||||
|
|
@ -84,11 +85,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 +125,8 @@ export const WizardLeadershipStep = ({
|
|||
flowTitle={wizardFlowTitleForSlug(actionSlug)}
|
||||
stepTitle={stepTitle}
|
||||
isEntryStep={false}
|
||||
previousHref={wizardActionHref(actionSlug)}
|
||||
dismissHref={CAMPAIGN_HOME}
|
||||
previousHref={wizardActionHref(actionSlug, municipalitySlug, { returnPath })}
|
||||
dismissHref={wizardChainEndHref(returnPath)}
|
||||
municipalityLabel={municipalityName}
|
||||
skip={skipConfig}
|
||||
trailingAction={trailingAction}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ 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 +31,7 @@ type WizardSignalBodyStepProps = {
|
|||
municipalitySlug: string
|
||||
signalType: MunicipalitySignalType
|
||||
entryAction?: CampaignWizardActionId
|
||||
returnPath?: string
|
||||
}
|
||||
|
||||
export const WizardSignalBodyStep = ({
|
||||
|
|
@ -40,19 +41,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 +64,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}
|
||||
|
|
|
|||
|
|
@ -26,12 +26,14 @@ import {
|
|||
import type { MunicipalitySignalType } from '@/lib/schemas/municipalityUpdate'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { resolveWizardSignalSkip, WIZARD_SIGNAL_TYPE_STEP_TITLE } from '@/lib/wizardSignalUi'
|
||||
import { wizardChainEndHref } from '@/lib/wizardActionChain'
|
||||
|
||||
type WizardSignalTypeStepProps = {
|
||||
actionSlug: string
|
||||
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, municipalitySlug, { 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,7 @@ 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 +32,7 @@ type WizardTrendNoteStepProps = {
|
|||
trendStatus: PoliticalTrendStatusValue
|
||||
initialNote: string
|
||||
entryAction?: CampaignWizardActionId
|
||||
returnPath?: string
|
||||
}
|
||||
|
||||
export const WizardTrendNoteStep = ({
|
||||
|
|
@ -42,6 +43,7 @@ export const WizardTrendNoteStep = ({
|
|||
trendStatus,
|
||||
initialNote,
|
||||
entryAction,
|
||||
returnPath,
|
||||
}: WizardTrendNoteStepProps) => {
|
||||
const router = useRouter()
|
||||
const [note, setNote] = useState(initialNote)
|
||||
|
|
@ -49,13 +51,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 +67,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,7 @@ 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 +44,7 @@ type WizardExpectedVotesStepProps = {
|
|||
municipalitySlug: string
|
||||
initialExpectedVotes: VoteEstimateScenarioViewModel
|
||||
entryAction?: CampaignWizardActionId
|
||||
returnPath?: string
|
||||
}
|
||||
|
||||
export const WizardExpectedVotesStep = ({
|
||||
|
|
@ -54,6 +54,7 @@ export const WizardExpectedVotesStep = ({
|
|||
municipalitySlug,
|
||||
initialExpectedVotes,
|
||||
entryAction,
|
||||
returnPath,
|
||||
}: WizardExpectedVotesStepProps) => {
|
||||
const router = useRouter()
|
||||
const [isPending, startTransition] = useTransition()
|
||||
|
|
@ -61,7 +62,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 +81,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 +134,8 @@ export const WizardExpectedVotesStep = ({
|
|||
flowTitle={wizardFlowTitleForSlug(actionSlug)}
|
||||
stepTitle={wizardNextStepTitle(actionSlug)}
|
||||
isEntryStep={false}
|
||||
previousHref={wizardActionHref(actionSlug)}
|
||||
dismissHref={CAMPAIGN_HOME}
|
||||
previousHref={wizardActionHref(actionSlug, municipalitySlug, { 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,6 +1,6 @@
|
|||
import { CampaignWizardShell } from '@/components/campaign/shared/CampaignWizardShell'
|
||||
import { wizardActionHref } from '@/lib/campaignActionRoutes'
|
||||
import { CAMPAIGN_HOME } from '@/lib/campaignPaths'
|
||||
import { wizardChainEndHref } from '@/lib/wizardActionChain'
|
||||
import {
|
||||
wizardFlowTitleForSlug,
|
||||
wizardNextStepPlaceholder,
|
||||
|
|
@ -10,18 +10,20 @@ import {
|
|||
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,50 @@ 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 +120,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 +132,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 +174,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 +188,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 +214,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 +233,7 @@ export const wizardTrendHref = (
|
|||
if (value) params.set(key, value)
|
||||
}
|
||||
}
|
||||
return `${base}?${params.toString()}`
|
||||
return appendWizardReturnPath(`${base}?${params.toString()}`, returnPath)
|
||||
}
|
||||
|
||||
export const resolveWizardTrendStatusParam = (
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import {
|
|||
import {
|
||||
CAMPAIGN_WIZARD_ACTION_SLUGS,
|
||||
campaignActionEntryHref,
|
||||
appendWizardReturnPath,
|
||||
isCampaignWizardActionId,
|
||||
wizardActionHref,
|
||||
type CampaignWizardActionId,
|
||||
|
|
@ -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,13 @@ 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,7 +94,7 @@ export const resolveLeadershipQuickActions = (
|
|||
if (!isStaffCampaignRole(role)) return null
|
||||
|
||||
if (isLeadershipListPath(pathname)) {
|
||||
return resolveLeadershipListActions(role)
|
||||
return resolveLeadershipListActions(role, pathname)
|
||||
}
|
||||
|
||||
const leadershipId = parseLeadershipDetailId(pathname)
|
||||
|
|
@ -95,7 +102,7 @@ export const resolveLeadershipQuickActions = (
|
|||
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,17 @@ 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 +73,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