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
+16 -2
View File
@@ -25,6 +25,7 @@ import {
getNextStep,
parseReviewReturnSearchParam,
shouldOfferCreateFlowSaveAndExit,
isDirectTemplateReviewEntry,
createFlowStepUsesSelectSplitScroll,
TEMPLATES_FACET_RECOMMEND_QUERY,
TEMPLATES_FACET_RECOMMEND_VALUE,
@@ -155,9 +156,9 @@ function CreateFlowLayoutContent({
const {
currentStep,
nextStep,
previousStep,
goToNextStep,
goToPreviousStep,
canGoBack,
templateReviewFooterBackToCreateReview,
} = useCreateFlowNavigation(
skipCommunitySave ? { skipCommunitySave: true } : undefined,
@@ -281,6 +282,9 @@ function CreateFlowLayoutContent({
},
});
const fromCreateWizard =
searchParams.get(TEMPLATE_REVIEW_FROM_CREATE_FLOW_QUERY) ===
TEMPLATE_REVIEW_FROM_CREATE_FLOW_VALUE;
const {
isTemplateReviewRoute,
templateReviewSlug,
@@ -295,6 +299,8 @@ function CreateFlowLayoutContent({
updateState,
replaceState,
router,
fromCreateWizard,
markCreateFlowInteraction,
});
const runAuthenticatedExit = useCreateFlowExit({
@@ -308,6 +314,14 @@ function CreateFlowLayoutContent({
});
const handleExit = async (opts?: { saveDraft?: boolean }) => {
// Direct template preview: browsing a catalog card, nothing collected yet.
// In-flow (`?fromFlow=1`) still has community progress — keep save / confirm.
if (isDirectTemplateReviewEntry(pathname, searchParams)) {
clearState();
router.push(CREATE_ROUTES.root);
return;
}
if (!sessionResolved) return;
// Completed is post-publish. Guests still need Save & Exit → keep-this-rule
@@ -979,7 +993,7 @@ function CreateFlowLayoutContent({
),
);
}
: previousStep
: canGoBack()
? goToPreviousStep
: undefined
}
@@ -102,6 +102,18 @@ export function SignedInDraftHydration({
if (urlStep === "completed") {
return;
}
/**
* Direct template preview is a new-rule entry, not “continue draft”.
* Applying a saved `currentStep` here is what yanked Use without changes
* past name / description / photo to stakeholders.
*/
if (pathname?.includes("/create/review-template/")) {
return;
}
if (touchedRef.current) {
finishedUserIdRef.current = userId;
return;
}
let cancelled = false;
setLoadingHydration(true);
@@ -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,
@@ -27,14 +27,11 @@ export function CommunityReviewScreen() {
const { state, updateState } = useCreateFlow();
/**
* If the user picked "Customize" or "Use without changes" from a template
* before entering community stage, we pinned `pendingTemplateAction` so
* this screen can skip itself — they already expressed their intent, no
* reason to make them re-pick from the review footer. We `replace` (not
* `push`) so Back from the destination goes to `community-save` instead of
* bouncing through here again. The action is cleared synchronously via
* `updateState` to guarantee the redirect only fires once: later visits to
* `/create/review` (e.g. navigating here directly) render normally.
* If the user picked **Customize** from a template before finishing community
* stage, we pinned `pendingTemplateAction` so this screen can skip itself
* and `replace` to `core-values`. **Use without changes** does not skip
* identity: if they land here with that pin, send them to `community-name`
* and leave the pin so name → description → photo stay in the skip list.
*
* Ref guard covers React 18 StrictMode's double-mount in dev so we don't
* fire `router.replace` twice on the same transition.
@@ -44,20 +41,19 @@ export function CommunityReviewScreen() {
if (firedRedirectRef.current) return;
const pending = state.pendingTemplateAction;
if (!pending) return;
const target =
pending.mode === "customize"
? createFlowStepPath("core-values")
: createFlowStepPath("confirm-stakeholders");
if (pending.mode !== "customize") {
firedRedirectRef.current = true;
router.replace(createFlowStepPath("community-name"));
return;
}
const target = createFlowStepPath("core-values");
firedRedirectRef.current = true;
const pinMerge =
pending.mode === "customize"
? {
methodSectionsPinCommitted: {
...state.methodSectionsPinCommitted,
...methodSectionsPinsForHydratedSelections(state),
},
}
: {};
const pinMerge = {
methodSectionsPinCommitted: {
...state.methodSectionsPinCommitted,
...methodSectionsPinsForHydratedSelections(state),
},
};
updateState({ pendingTemplateAction: undefined, ...pinMerge });
router.replace(target);
}, [
+10 -11
View File
@@ -194,11 +194,13 @@ export interface CreateFlowState {
customMethodCardFieldBlocksById?: Record<string, CustomMethodCardFieldBlock[]>;
/**
* Set when a user picks a template (Customize or Use without changes) before
* completing the community stage. The community-review screen consumes this
* to `router.replace` past `/create/review` to the correct downstream step
* (`core-values` for customize; `confirm-stakeholders` for use-without-changes)
* once community data is captured. Cleared the moment the redirect fires, so
* later visits to `/create/review` render normally.
* completing the community stage. Customize: the community-review screen
* `router.replace`s past `/create/review` to `core-values`. Use without
* changes: navigation skips to name → description → optional photo, then
* `confirm-stakeholders` (the pin keeps that skip list active). Set only for
* **direct** template entry, not when community identity was already collected
* in-flow. Cleared when community-review consumes a customize pin, or when
* the flow is reset.
*/
pendingTemplateAction?: {
slug: string;
@@ -206,12 +208,9 @@ export interface CreateFlowState {
};
/**
* Set when the user chooses **Use without changes** on a template-review
* page. The custom-rule segment (`core-values` … `conflict-management`) is
* skipped, so linear `getPreviousStep("confirm-stakeholders")` would wrongly
* point at `conflict-management`. Navigation uses this slug so Back from
* `confirm-stakeholders` returns to `/create/review-template/{slug}`.
* Cleared when the user picks **Customize** from template review (normal
* linear back applies) or when the flow state is cleared.
* page. Used so Back from `community-name` (identity-only path) returns to
* `/create/review-template/{slug}` instead of the intro. Cleared when the user
* picks **Customize** from template review or when the flow state is cleared.
*/
templateReviewBackSlug?: string;
/**
+86 -15
View File
@@ -47,8 +47,44 @@ export const FIRST_STEP: CreateFlowStep = FLOW_STEP_ORDER[0];
/** Options for navigation when the email / magic-link save step is not shown (signed-in users). */
export type CreateFlowNavigationOptions = {
skipCommunitySave?: boolean;
/**
* Template **Use without changes**: collect community name, description, and
* optional photo only. Skips intro, structure, size, save, community review,
* and the custom-rule authoring segment.
*/
useWithoutChangesIdentityOnly?: boolean;
};
/**
* Community + custom-rule steps skipped when
* {@link CreateFlowNavigationOptions.useWithoutChangesIdentityOnly} is set.
* Remaining identity steps: `community-name` → `community-context` →
* `community-upload`, then `confirm-stakeholders`.
*/
const USE_WITHOUT_CHANGES_IDENTITY_SKIP = new Set<CreateFlowStep>([
"informational",
"community-structure",
"community-size",
"community-save",
"review",
"core-values",
"communication-methods",
"membership-methods",
"decision-approaches",
"conflict-management",
]);
function shouldSkipStep(
step: CreateFlowStep,
options?: CreateFlowNavigationOptions,
): boolean {
if (options?.skipCommunitySave && step === "community-save") return true;
return Boolean(
options?.useWithoutChangesIdentityOnly &&
USE_WITHOUT_CHANGES_IDENTITY_SKIP.has(step),
);
}
/**
* Returns the next step in the flow, or null if current is last/invalid
*/
@@ -57,13 +93,14 @@ export function getNextStep(
options?: CreateFlowNavigationOptions,
): CreateFlowStep | null {
if (!currentStep) return null;
const index = FLOW_STEP_ORDER.indexOf(currentStep);
if (index === -1 || index === FLOW_STEP_ORDER.length - 1) return null;
const next = FLOW_STEP_ORDER[index + 1] as CreateFlowStep;
if (options?.skipCommunitySave && next === "community-save") {
return getNextStep("community-save", options);
let index = FLOW_STEP_ORDER.indexOf(currentStep);
if (index === -1) return null;
while (index < FLOW_STEP_ORDER.length - 1) {
index += 1;
const next = FLOW_STEP_ORDER[index] as CreateFlowStep;
if (!shouldSkipStep(next, options)) return next;
}
return next;
return null;
}
/**
@@ -74,20 +111,27 @@ export function getPreviousStep(
options?: CreateFlowNavigationOptions,
): CreateFlowStep | null {
if (!currentStep) return null;
const index = FLOW_STEP_ORDER.indexOf(currentStep);
let index = FLOW_STEP_ORDER.indexOf(currentStep);
if (index <= 0) return null;
const prev = FLOW_STEP_ORDER[index - 1] as CreateFlowStep;
if (options?.skipCommunitySave && prev === "community-save") {
return getPreviousStep("community-save", options);
while (index > 0) {
index -= 1;
const prev = FLOW_STEP_ORDER[index] as CreateFlowStep;
if (!shouldSkipStep(prev, options)) return prev;
}
return prev;
return null;
}
/**
* Where the create-flow footer Back action should go. Usually the previous
* step in {@link FLOW_STEP_ORDER}; when the user reached `confirm-stakeholders`
* via template **Use without changes**, Back returns to template review instead
* of `conflict-management` (that segment was skipped).
* step in {@link FLOW_STEP_ORDER}.
*
* Template **Use without changes** exceptions:
* - Direct from a template (`useWithoutChangesIdentityOnly`): Back from
* `community-name` returns to template review; Back from
* `confirm-stakeholders` is `community-upload`.
* - In-flow (`?fromFlow=1`, community identity already collected): Back
* from `confirm-stakeholders` returns to template review instead of
* `conflict-management`.
*/
export type CreateFlowBackTarget =
| { kind: "step"; step: CreateFlowStep }
@@ -102,7 +146,18 @@ export function resolveCreateFlowBackTarget(
typeof templateReviewBackSlug === "string"
? templateReviewBackSlug.trim()
: "";
if (currentStep === "confirm-stakeholders" && slug.length > 0) {
if (
currentStep === "community-name" &&
options?.useWithoutChangesIdentityOnly &&
slug.length > 0
) {
return { kind: "templateReview", slug };
}
if (
currentStep === "confirm-stakeholders" &&
slug.length > 0 &&
!options?.useWithoutChangesIdentityOnly
) {
return { kind: "templateReview", slug };
}
const prev = getPreviousStep(currentStep, options);
@@ -194,6 +249,22 @@ export function parseCreateFlowScreenFromPathname(
export const TEMPLATE_REVIEW_FROM_CREATE_FLOW_QUERY = "fromFlow" as const;
export const TEMPLATE_REVIEW_FROM_CREATE_FLOW_VALUE = "1" as const;
/**
* Catalog / marketing template preview (`/create/review-template/[slug]`
* without `?fromFlow=1`). There is nothing to save yet; top-nav Exit should
* leave immediately. In-flow picks keep the usual save / leave-confirm path.
*/
export function isDirectTemplateReviewEntry(
pathname: string | null | undefined,
searchParams?: { get: (name: string) => string | null } | null,
): boolean {
if (!pathname?.includes("/create/review-template/")) return false;
return (
searchParams?.get(TEMPLATE_REVIEW_FROM_CREATE_FLOW_QUERY) !==
TEMPLATE_REVIEW_FROM_CREATE_FLOW_VALUE
);
}
/**
* Only set from `/create/review` “Create from template” with `fromFlow=1`.
* Enables facet-ranked `GET /api/templates` + “RECOMMENDED” on the grid; omit