Let Use without changes from a template collect identity instead of the full questionnaire.

Direct catalog picks walk name, description, and photo, then stakeholders; leftover drafts no longer skip that path. In-flow template picks keep community identity. Exit on a direct template preview leaves immediately because nothing has been collected yet.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
adilallo
2026-09-02 17:00:17 -06:00
co-authored by Cursor
parent f91ac7a893
commit b6afdb6e32
14 changed files with 457 additions and 100 deletions
@@ -34,9 +34,12 @@ const blurActiveElement = (): void => {
*
* Resolves the active step from `/create/{screenId}` via
* {@link parseCreateFlowScreenFromPathname} (flowSteps). Footer Back uses
* {@link resolveCreateFlowBackTarget} so template **Use without changes**
* (which skips the custom-rule segment) returns to `/create/review-template/{slug}`
* from `confirm-stakeholders` instead of `conflict-management`.
* {@link resolveCreateFlowBackTarget}: template **Use without changes** that
* still needs identity (direct from a template) walks name / description /
* photo, then Back from `community-name` returns to
* `/create/review-template/{slug}`. In-flow picks (`?fromFlow=1`) skip
* identity and land on `confirm-stakeholders`; Back from that step returns
* to the template instead of `conflict-management`.
*
* Template review footer Back uses {@link buildTemplateReviewHref}s
* `?fromFlow=1` marker (and persisted `templateReviewEntryFromCreateFlow`) so
@@ -83,17 +86,27 @@ export function useCreateFlowNavigation(
updateState,
]);
const nextStep = getNextStep(validStep, options);
const previousStep = getPreviousStep(validStep, options);
const identityOnly =
state.pendingTemplateAction?.mode === "useWithoutChanges";
const navigationOptions = useMemo(
(): CreateFlowNavigationOptions => ({
skipCommunitySave: options?.skipCommunitySave,
useWithoutChangesIdentityOnly: identityOnly,
}),
[options?.skipCommunitySave, identityOnly],
);
const nextStep = getNextStep(validStep, navigationOptions);
const previousStep = getPreviousStep(validStep, navigationOptions);
const backTarget = useMemo(
() =>
resolveCreateFlowBackTarget(
validStep,
options,
navigationOptions,
state.templateReviewBackSlug,
),
[validStep, options?.skipCommunitySave, state.templateReviewBackSlug],
[validStep, navigationOptions, state.templateReviewBackSlug],
);
const goToNextStep = useCallback(() => {
@@ -10,6 +10,7 @@ import type {
CreateFlowContextValue,
CreateFlowState,
} from "../types";
import { createFlowStepPath } from "../utils/createFlowPaths";
type AppRouterLike = { push: (_href: string) => void };
type UpdateState = CreateFlowContextValue["updateState"];
@@ -36,9 +37,10 @@ export type UseTemplateReviewActionsResult = {
* Use without changes: scrub any prior customize picks, seed core values +
* method-card selections from the template body (same id mapping as
* Customize) so drilling from final-review via + shows selected cards, drop
* the Values row from `state.sections`, and route to
* `/create/confirm-stakeholders` (or `/create/informational` with a pin to
* skip past `/create/review` to `/create/confirm-stakeholders` later).
* the Values row from `state.sections`. Direct template entry routes to
* `/create/community-name` with a pin so Next/Back collect only name →
* description → photo. In-flow (`?fromFlow=1`, community steps already done)
* routes to `/create/confirm-stakeholders`.
*/
handleUseWithoutChanges: () => Promise<void>;
};
@@ -59,7 +61,10 @@ export type UseTemplateReviewActionsResult = {
* setTemplateReviewApplyError,
* handleCustomize,
* handleUseWithoutChanges,
* } = useTemplateReviewActions({ pathname, state, updateState, replaceState, router });
* } = useTemplateReviewActions({
* pathname, state, updateState, replaceState, router,
* fromCreateWizard, markCreateFlowInteraction,
* });
*/
export function useTemplateReviewActions({
pathname,
@@ -67,12 +72,21 @@ export function useTemplateReviewActions({
updateState,
replaceState,
router,
fromCreateWizard = false,
markCreateFlowInteraction,
}: {
pathname: string | null | undefined;
state: CreateFlowState;
updateState: UpdateState;
replaceState: ReplaceStateFn;
router: AppRouterLike;
/**
* True when this template-review URL was opened from the create wizard
* (`?fromFlow=1`). Direct catalog / marketing entry is false — a leftover
* draft title must not count as “community steps already done”.
*/
fromCreateWizard?: boolean;
markCreateFlowInteraction: () => void;
}): UseTemplateReviewActionsResult {
const [isApplyingTemplate, setIsApplyingTemplate] = useState(false);
const [templateReviewApplyError, setTemplateReviewApplyError] = useState<
@@ -90,6 +104,7 @@ export function useTemplateReviewActions({
const handleCustomize = useCallback(async () => {
if (!templateReviewSlug) return;
markCreateFlowInteraction();
setTemplateReviewApplyError(null);
setIsApplyingTemplate(true);
const loaded = await loadTemplateReviewBySlug(templateReviewSlug);
@@ -119,7 +134,9 @@ export function useTemplateReviewActions({
}),
});
router.push(
hasCommunityName ? "/create/core-values" : "/create/informational",
hasCommunityName
? createFlowStepPath("core-values")
: createFlowStepPath("informational"),
);
}, [
router,
@@ -127,10 +144,12 @@ export function useTemplateReviewActions({
state.title,
templateReviewSlug,
updateState,
markCreateFlowInteraction,
]);
const handleUseWithoutChanges = useCallback(async () => {
if (!templateReviewSlug) return;
markCreateFlowInteraction();
setTemplateReviewApplyError(null);
setIsApplyingTemplate(true);
const loaded = await loadTemplateReviewBySlug(templateReviewSlug);
@@ -158,9 +177,6 @@ export function useTemplateReviewActions({
return;
}
const hasCommunityName =
typeof state.title === "string" && state.title.trim().length > 0;
// Atomic read-modify-write: strip prior custom-rule picks and merge template
// body in one replaceState so method ids are never lost across React batching
// (reset + update separately could leave selections undefined in Strict Mode).
@@ -179,13 +195,10 @@ export function useTemplateReviewActions({
})
: sections;
const hasCommunityName =
typeof prev.title === "string" && prev.title.trim().length > 0;
const pinPatch =
methodSectionsPinsForHydratedSelections(customizePrefill);
return {
const seeded: CreateFlowState = {
...base,
...(hasValuesSeed
? {
@@ -221,22 +234,40 @@ export function useTemplateReviewActions({
sections: sectionsWithoutValues,
methodSectionsPinCommitted: pinPatch,
templateReviewBackSlug: templateReviewSlug,
...(hasCommunityName
? { pendingTemplateAction: undefined }
: {
pendingTemplateAction: {
slug: templateReviewSlug,
mode: "useWithoutChanges",
},
}),
};
if (fromCreateWizard) {
return {
...seeded,
pendingTemplateAction: undefined,
};
}
return {
...seeded,
title: undefined,
communityContext: undefined,
communityAvatarUrl: undefined,
currentStep: undefined,
templateReviewEntryFromCreateFlow: undefined,
pendingTemplateAction: {
slug: templateReviewSlug,
mode: "useWithoutChanges",
},
};
});
router.push(
hasCommunityName
? "/create/confirm-stakeholders"
: "/create/informational",
fromCreateWizard
? createFlowStepPath("confirm-stakeholders")
: createFlowStepPath("community-name"),
);
}, [replaceState, router, state.title, templateReviewSlug]);
}, [
fromCreateWizard,
markCreateFlowInteraction,
replaceState,
router,
templateReviewSlug,
]);
return {
isTemplateReviewRoute,