From 90db21341387968153295004ab765c7237869e31 Mon Sep 17 00:00:00 2001 From: adilallo <39313955+adilallo@users.noreply.github.com> Date: Thu, 10 Sep 2026 15:22:46 -0600 Subject: [PATCH] Keep the template picker in the create flow, make catalog cards real links, and stop showing a fake community when review has no draft. Co-authored-by: Cursor --- app/(app)/create/CreateFlowLayoutClient.tsx | 38 +++++++------- app/(app)/create/SignedInDraftHydration.tsx | 4 ++ .../components/createFlowLayoutTokens.ts | 3 +- .../create/hooks/useCreateFlowNavigation.ts | 3 +- .../screens/review/CommunityReviewScreen.tsx | 49 +++++++++++++++-- .../CreateFlowTemplatesPageClient.tsx | 45 ++++++++++++++++ app/(app)/create/templates/page.tsx | 18 +++++++ app/(app)/create/types.ts | 2 +- app/(app)/create/utils/createFlowPaths.ts | 2 + app/(app)/create/utils/flowSteps.ts | 20 ++++++- .../templates/TemplatesPageClient.tsx | 15 +++--- .../templates/useTemplatesFacetGridEntries.ts | 7 +-- app/components/cards/Rule/Rule.container.tsx | 4 +- app/components/cards/Rule/Rule.types.ts | 6 +++ app/components/cards/Rule/Rule.view.tsx | 44 ++++++++++++---- .../GovernanceTemplateGrid.tsx | 20 +++++-- .../RuleStack/RuleStack.container.tsx | 5 +- .../sections/RuleStack/RuleStack.types.ts | 1 + .../sections/RuleStack/RuleStack.view.tsx | 2 + docs/create-flow.md | 7 +-- messages/en/create/community/review.json | 6 ++- .../GovernanceTemplateGrid.stories.js | 6 +++ .../GovernanceTemplateGrid.test.tsx | 28 ++++++++-- tests/components/ReviewPage.test.tsx | 52 ++++++++++++++----- tests/pages/create-templates.test.tsx | 34 ++++++++++++ tests/pages/templates.test.jsx | 48 ++++++----------- tests/unit/Rule.test.jsx | 18 +++++++ tests/unit/RuleStack.test.jsx | 19 ++++--- tests/unit/createFlowLayoutTokens.test.ts | 6 +++ tests/unit/createFlowPaths.test.ts | 1 + tests/unit/flowSteps.test.ts | 25 ++++++--- 31 files changed, 412 insertions(+), 126 deletions(-) create mode 100644 app/(app)/create/templates/CreateFlowTemplatesPageClient.tsx create mode 100644 app/(app)/create/templates/page.tsx create mode 100644 tests/pages/create-templates.test.tsx diff --git a/app/(app)/create/CreateFlowLayoutClient.tsx b/app/(app)/create/CreateFlowLayoutClient.tsx index df73786..1115f6b 100644 --- a/app/(app)/create/CreateFlowLayoutClient.tsx +++ b/app/(app)/create/CreateFlowLayoutClient.tsx @@ -27,8 +27,7 @@ import { shouldOfferCreateFlowSaveAndExit, isDirectTemplateReviewEntry, createFlowStepUsesSelectSplitScroll, - TEMPLATES_FACET_RECOMMEND_QUERY, - TEMPLATES_FACET_RECOMMEND_VALUE, + isCreateFlowTemplatesPickerPath, TEMPLATE_REVIEW_FROM_CREATE_FLOW_QUERY, TEMPLATE_REVIEW_FROM_CREATE_FLOW_VALUE, } from "./utils/flowSteps"; @@ -309,6 +308,8 @@ function CreateFlowLayoutContent({ fromCreateWizard, markCreateFlowInteraction, }); + const isCreateTemplatesPickerRoute = + isCreateFlowTemplatesPickerPath(pathname); const runAuthenticatedExit = useCreateFlowExit({ state, @@ -566,9 +567,11 @@ function CreateFlowLayoutContent({ ? "items-stretch overflow-y-auto md:overflow-hidden" : isSelectSplitScrollStep ? "items-start justify-start overflow-y-auto max-lg:overflow-y-auto lg:min-h-0 lg:items-stretch lg:overflow-hidden" - : isTemplateReviewRoute || isFinalReviewLike || isCardLayoutStep - ? CREATE_FLOW_MD_CENTERED_MAIN_CLASS - : "items-start justify-center overflow-y-auto md:items-center"; + : isCreateTemplatesPickerRoute + ? "items-start justify-start overflow-y-auto" + : isTemplateReviewRoute || isFinalReviewLike || isCardLayoutStep + ? CREATE_FLOW_MD_CENTERED_MAIN_CLASS + : "items-start justify-center overflow-y-auto md:items-center"; const isTextStep = createFlowStepUsesCenteredTextLayout(currentStep); const mainMaxMdJustify = @@ -579,10 +582,9 @@ function CreateFlowLayoutContent({ ? "max-md:flex-col max-md:items-stretch" : "max-md:flex-col max-md:items-center"; const mainResponsiveLayout = `${mainMaxMdCross} ${mainMaxMdJustify} md:flex-row md:justify-center`; - const saveDraftOnExit = shouldOfferCreateFlowSaveAndExit( - currentStep, - sessionUser, - ); + const saveDraftOnExit = + shouldOfferCreateFlowSaveAndExit(currentStep, sessionUser) || + isCreateTemplatesPickerRoute; const proportionBarProgress = getProportionBarProgressForCreateFlowStep( currentStep, @@ -814,9 +816,11 @@ function CreateFlowLayoutContent({ contentMaxClass={getCreateFlowContentMaxClass({ step: currentStep, isTemplateReview: isTemplateReviewRoute, + isTemplatesPicker: isCreateTemplatesPickerRoute, })} progressBar={ !isTemplateReviewRoute && + !isCreateTemplatesPickerRoute && !isFinalReviewLike && reviewReturnTarget !== "edit-rule" } @@ -920,15 +924,7 @@ function CreateFlowLayoutContent({ disabled={isPublishing} className={CREATE_FLOW_FOOTER_BUTTON_CLASS} onClick={() => { - // `fromFlow=1` tells `/templates` to skip the fresh-slate - // draft clear it normally runs on template click, so the - // user's in-progress Create Community stage survives this - // detour. Direct entries to `/templates` (no marker) and - // home "Popular templates" clicks always start fresh by - // wiping anonymous draft storage at click time. - router.push( - `/templates?${TEMPLATE_REVIEW_FROM_CREATE_FLOW_QUERY}=${TEMPLATE_REVIEW_FROM_CREATE_FLOW_VALUE}&${TEMPLATES_FACET_RECOMMEND_QUERY}=${TEMPLATES_FACET_RECOMMEND_VALUE}`, - ); + router.push(CREATE_ROUTES.templatesPicker); }} > {footer.createFromTemplate} @@ -1013,7 +1009,11 @@ function CreateFlowLayoutContent({ ) : null } onBackClick={ - isTemplateReviewRoute + isCreateTemplatesPickerRoute + ? () => { + router.push(CREATE_ROUTES.review); + } + : isTemplateReviewRoute ? () => router.push( templateReviewFooterBackToCreateReview diff --git a/app/(app)/create/SignedInDraftHydration.tsx b/app/(app)/create/SignedInDraftHydration.tsx index 4744ab5..8b6e806 100644 --- a/app/(app)/create/SignedInDraftHydration.tsx +++ b/app/(app)/create/SignedInDraftHydration.tsx @@ -15,6 +15,7 @@ import { CREATE_FLOW_PAGE_GUTTER_CLASS } from "./components/createFlowLayoutToke import Alert from "../../components/modals/Alert"; import { isValidStep, + isCreateFlowTemplatesPickerPath, parseCreateFlowScreenFromPathname, } from "./utils/flowSteps"; import { hasFreshEntryPending } from "./utils/prepareFreshCreateFlowEntry"; @@ -111,6 +112,9 @@ export function SignedInDraftHydration({ if (pathname?.includes("/create/review-template/")) { return; } + if (isCreateFlowTemplatesPickerPath(pathname)) { + return; + } if (touchedRef.current) { finishedUserIdRef.current = userId; return; diff --git a/app/(app)/create/components/createFlowLayoutTokens.ts b/app/(app)/create/components/createFlowLayoutTokens.ts index 0b0f3f6..ad7b75c 100644 --- a/app/(app)/create/components/createFlowLayoutTokens.ts +++ b/app/(app)/create/components/createFlowLayoutTokens.ts @@ -71,8 +71,9 @@ export const CREATE_FLOW_CARD_STACK_AREA_MAX_CLASS = export function getCreateFlowContentMaxClass(options: { step: CreateFlowStep | null | undefined; isTemplateReview?: boolean; + isTemplatesPicker?: boolean; }): string { - if (options.isTemplateReview) { + if (options.isTemplateReview || options.isTemplatesPicker) { return CREATE_FLOW_TWO_COLUMN_MAX_WIDTH_CLASS; } if (!options.step) { diff --git a/app/(app)/create/hooks/useCreateFlowNavigation.ts b/app/(app)/create/hooks/useCreateFlowNavigation.ts index b48168d..92780e1 100644 --- a/app/(app)/create/hooks/useCreateFlowNavigation.ts +++ b/app/(app)/create/hooks/useCreateFlowNavigation.ts @@ -43,7 +43,8 @@ const blurActiveElement = (): void => { * * Template review footer Back uses {@link buildTemplateReviewHref}’s * `?fromFlow=1` marker (and persisted `templateReviewEntryFromCreateFlow`) so - * users who came from `/create/review` return there instead of `/`. + * users who came from `/create/review` via `/create/templates` return there + * instead of `/`. */ export function useCreateFlowNavigation( options?: CreateFlowNavigationOptions, diff --git a/app/(app)/create/screens/review/CommunityReviewScreen.tsx b/app/(app)/create/screens/review/CommunityReviewScreen.tsx index 9252158..95ccd9e 100644 --- a/app/(app)/create/screens/review/CommunityReviewScreen.tsx +++ b/app/(app)/create/screens/review/CommunityReviewScreen.tsx @@ -1,8 +1,9 @@ "use client"; -import { useEffect, useRef } from "react"; +import { useEffect, useRef, useState } from "react"; import { useRouter } from "next/navigation"; import Rule from "../../../../components/cards/Rule"; +import Button from "../../../../components/buttons/Button"; import { useTranslation } from "../../../../contexts/MessagesContext"; import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup"; import { useCreateFlow } from "../../context/CreateFlowContext"; @@ -17,7 +18,7 @@ import { vectorMarkPath, } from "../../../../../lib/assetUtils"; import { methodSectionsPinsForHydratedSelections } from "../../../../../lib/create/publishedDocumentToCreateFlowState"; -import { createFlowStepPath } from "../../utils/createFlowPaths"; +import { CREATE_ROUTES, createFlowStepPath } from "../../utils/createFlowPaths"; /** Create Community review — Figma `19706:12135` (`/create/review`; two columns from `lg:`; column caps in `createFlowLayoutTokens`). */ export function CommunityReviewScreen() { @@ -25,6 +26,15 @@ export function CommunityReviewScreen() { const lgUp = useCreateFlowLgUp(); const t = useTranslation("create.community.review"); const { state, updateState } = useCreateFlow(); + /** + * Server layout has an empty context; the client layout may already hold a + * named draft. Defer empty-vs-congrats until after mount so the first paint + * matches SSR (`null`) instead of swapping HeaderLockup titles. + */ + const [reviewReady, setReviewReady] = useState(false); + useEffect(() => { + setReviewReady(true); + }, []); /** * If the user picked **Customize** from a template before finishing community @@ -70,11 +80,10 @@ export function CommunityReviewScreen() { const cardTitle = typeof state.title === "string" && state.title.trim().length > 0 ? state.title.trim() - : t("ruleCard.title"); + : ""; /** * No placeholder fallback: if the user skipped `community-context`, leave - * the card description off rather than render the old "Mutual Aid Monday - * is a grassroots community…" sample, which read as real user copy. + * the card description off rather than render sample copy as real user data. */ const cardDescription = typeof state.communityContext === "string" && @@ -88,6 +97,36 @@ export function CommunityReviewScreen() { ? state.communityAvatarUrl.trim() : null; + if (state.pendingTemplateAction || !reviewReady) { + return null; + } + + if (!cardTitle) { + return ( + +
+ + +
+
+ ); + } + return ( +
+ + + buildTemplateReviewHref(slug, { fromCreateWizard: true }) + } + /> +
+
+ ); +} diff --git a/app/(app)/create/templates/page.tsx b/app/(app)/create/templates/page.tsx new file mode 100644 index 0000000..02dc4ad --- /dev/null +++ b/app/(app)/create/templates/page.tsx @@ -0,0 +1,18 @@ +import type { Metadata } from "next"; +import messages from "../../../../messages/en/index"; +import { listRuleTemplatesFromDb } from "../../../../lib/server/ruleTemplates"; +import { routeMetadata } from "../../../../lib/siteMetadata"; +import { gridEntriesForFullCatalogWithFallback } from "../../../../lib/templates/templateGridPresentation"; +import { CREATE_ROUTES } from "../utils/createFlowPaths"; +import { CreateFlowTemplatesPageClient } from "./CreateFlowTemplatesPageClient"; + +export const metadata: Metadata = routeMetadata(CREATE_ROUTES.templatesPicker, { + title: messages.metadata.templates.title, +}); + +/** In-flow template catalog after review “Create from template”. */ +export default async function CreateFlowTemplatesPage() { + const rows = await listRuleTemplatesFromDb(); + const initialGridEntries = gridEntriesForFullCatalogWithFallback(rows); + return ; +} diff --git a/app/(app)/create/types.ts b/app/(app)/create/types.ts index c70a719..83725bb 100644 --- a/app/(app)/create/types.ts +++ b/app/(app)/create/types.ts @@ -215,7 +215,7 @@ export interface CreateFlowState { templateReviewBackSlug?: string; /** * True when the user opened `/create/review-template/{slug}` from the create - * wizard (`/templates?fromFlow=1` after `/create/review`). Persisted so Back + * wizard (`/create/templates` after `/create/review`). Persisted so Back * from template review targets `/create/review` and so returning from * `confirm-stakeholders` can re-apply `?fromFlow=1` on the template URL. */ diff --git a/app/(app)/create/utils/createFlowPaths.ts b/app/(app)/create/utils/createFlowPaths.ts index 86f9531..de4cab8 100644 --- a/app/(app)/create/utils/createFlowPaths.ts +++ b/app/(app)/create/utils/createFlowPaths.ts @@ -20,6 +20,8 @@ export const CREATE_ROUTES = { /** Direct path to the first wizard step so client navigations skip the redirect hop. */ createFirstStep: `/create/${FIRST_STEP}`, review: "/create/review", + /** In-flow template catalog (wizard chrome). Marketing catalog remains `/templates`. */ + templatesPicker: "/create/templates", finalReview: "/create/final-review", completed: "/create/completed", editRule: "/create/edit-rule", diff --git a/app/(app)/create/utils/flowSteps.ts b/app/(app)/create/utils/flowSteps.ts index 4723425..916847c 100644 --- a/app/(app)/create/utils/flowSteps.ts +++ b/app/(app)/create/utils/flowSteps.ts @@ -235,18 +235,34 @@ export function parseCreateFlowScreenFromPathname( ): CreateFlowStep | null { if (!pathname || pathname.length === 0) return null; if (pathname.includes("/create/review-template/")) return null; + if (isCreateFlowTemplatesPickerPath(pathname)) return null; const parts = pathname.split("/").filter(Boolean); const createIdx = parts.indexOf("create"); if (createIdx === -1 || createIdx >= parts.length - 1) return null; const segment = parts[createIdx + 1]; - if (segment === "review-template") return null; + if (segment === "review-template" || segment === "templates") return null; return isValidStep(segment) ? segment : null; } -/** Same query as `/templates?fromFlow=1` — template was picked after `/create/review`. */ +/** + * `/create/templates` — in-flow catalog after review “Create from template”. + * Not a wizard step; layout keeps Back / Save & Exit and does not use + * marketing `/templates` chrome. + */ +export function isCreateFlowTemplatesPickerPath( + pathname: string | null | undefined, +): boolean { + if (!pathname) return false; + const parts = pathname.split("/").filter(Boolean); + const createIdx = parts.indexOf("create"); + if (createIdx === -1 || createIdx !== parts.length - 2) return false; + return parts[createIdx + 1] === "templates"; +} + +/** Same query as `/create/review-template/…?fromFlow=1` — template was picked in-flow. */ export const TEMPLATE_REVIEW_FROM_CREATE_FLOW_QUERY = "fromFlow" as const; export const TEMPLATE_REVIEW_FROM_CREATE_FLOW_VALUE = "1" as const; diff --git a/app/(marketing)/templates/TemplatesPageClient.tsx b/app/(marketing)/templates/TemplatesPageClient.tsx index dd1b241..1c7780c 100644 --- a/app/(marketing)/templates/TemplatesPageClient.tsx +++ b/app/(marketing)/templates/TemplatesPageClient.tsx @@ -1,7 +1,7 @@ "use client"; import { Suspense } from "react"; -import { useRouter, useSearchParams } from "next/navigation"; +import { useSearchParams } from "next/navigation"; import HeaderLockup from "../../components/type/HeaderLockup"; import { GovernanceTemplateGrid } from "../../components/sections/GovernanceTemplateGrid"; import type { TemplateGridCardEntry } from "../../../lib/templates/templateGridPresentation"; @@ -68,8 +68,8 @@ export default function TemplatesPageClient({ /** * - `fromFlow=1` — skip `prepareFreshCreateFlowEntry` on template click - * (draft preserved). Used by review “Create from template” and profile. - * - `recommendTemplates=1` (with review only) — rank templates + “RECOMMENDED” + * (draft preserved). Used by profile “Create from template”. + * - `recommendTemplates=1` — rank templates + “RECOMMENDED” * from `GET /api/templates?facet.*` using the persisted community draft. */ function TemplatesGridWithSearchParams({ @@ -96,19 +96,18 @@ function TemplatesGrid({ entries: TemplateGridCardEntry[]; fromFlow: boolean; }) { - const router = useRouter(); return ( { + hrefForTemplate={(slug) => + buildTemplateReviewHref(slug, { fromCreateWizard: fromFlow }) + } + onTemplateClick={() => { if (!fromFlow) { // Marketing /templates has no session in hand; DELETE is // best-effort and the sentinel blocks stale-draft hydration. prepareFreshCreateFlowEntrySync({ signedIn: true }); } - router.push( - buildTemplateReviewHref(slug, { fromCreateWizard: fromFlow }), - ); }} /> ); diff --git a/app/(marketing)/templates/useTemplatesFacetGridEntries.ts b/app/(marketing)/templates/useTemplatesFacetGridEntries.ts index 27cadd0..e7ed7ca 100644 --- a/app/(marketing)/templates/useTemplatesFacetGridEntries.ts +++ b/app/(marketing)/templates/useTemplatesFacetGridEntries.ts @@ -18,9 +18,10 @@ type UseTemplatesFacetGridEntriesArgs = { }; /** - * When `enableFacetRecommendations` (review → “Create from template” only), - * re-fetch ranked templates from `GET /api/templates?facet.*` using the - * persisted create-flow draft. Otherwise returns `initialGridEntries` from SSR. + * When `enableFacetRecommendations` (in-flow `/create/templates`, or marketing + * `/templates?recommendTemplates=1`), re-fetch ranked templates from + * `GET /api/templates?facet.*` using the persisted create-flow draft. + * Otherwise returns `initialGridEntries` from SSR. */ export function useTemplatesFacetGridEntries({ initialGridEntries, diff --git a/app/components/cards/Rule/Rule.container.tsx b/app/components/cards/Rule/Rule.container.tsx index fd3503b..f8e2e09 100644 --- a/app/components/cards/Rule/Rule.container.tsx +++ b/app/components/cards/Rule/Rule.container.tsx @@ -35,6 +35,7 @@ const RuleContainer = memo( backgroundColor = "bg-[var(--color-community-teal-100)]", className = "", onClick, + href, expanded = false, size: sizeProp, categories, @@ -95,8 +96,9 @@ const RuleContainer = memo( icon={icon} backgroundColor={backgroundColor} className={className} + href={hasBottomLinks ? undefined : href} onClick={hasBottomLinks ? undefined : handleClick} - onKeyDown={hasBottomLinks ? undefined : handleKeyDown} + onKeyDown={hasBottomLinks || href ? undefined : handleKeyDown} expanded={expanded} size={size} categories={categories} diff --git a/app/components/cards/Rule/Rule.types.ts b/app/components/cards/Rule/Rule.types.ts index efb1586..73684df 100644 --- a/app/components/cards/Rule/Rule.types.ts +++ b/app/components/cards/Rule/Rule.types.ts @@ -50,6 +50,11 @@ export interface RuleProps { backgroundColor?: string; className?: string; onClick?: () => void; + /** + * When set, the card is a real link (focusable, Enter, open in a new tab) + * instead of a click-only `role="button"` surface. + */ + href?: string; expanded?: boolean; size?: RuleSizeValue; categories?: Category[]; @@ -93,6 +98,7 @@ export interface RuleViewProps { backgroundColor: string; className: string; onClick?: () => void; + href?: string; onKeyDown?: (_event: React.KeyboardEvent) => void; expanded: boolean; size: RuleSizeValue; diff --git a/app/components/cards/Rule/Rule.view.tsx b/app/components/cards/Rule/Rule.view.tsx index a618ba5..20cf1ab 100644 --- a/app/components/cards/Rule/Rule.view.tsx +++ b/app/components/cards/Rule/Rule.view.tsx @@ -1,6 +1,7 @@ "use client"; import Image from "next/image"; +import NextLink from "next/link"; import MultiSelect from "../../controls/MultiSelect"; import InlineTextButton from "../../buttons/InlineTextButton"; import NavigationLink from "../../navigation/Link"; @@ -19,6 +20,7 @@ export function RuleView({ backgroundColor, className, onClick, + href, onKeyDown, expanded, size, @@ -264,16 +266,11 @@ export function RuleView({ ); } - return ( -
+ const cardClassName = `${backgroundColor} ${cardPadding} ${cardGap} ${borderRadiusClass} shadow-[0px_0px_48px_0px_rgba(0,0,0,0.1)] ${interactiveCard ? "hover:shadow-[0px_0px_64px_0px_rgba(0,0,0,0.15)] transition-shadow duration-200" : ""} flex flex-col items-start justify-center relative ${cardWidth || "w-full"} ${href ? "no-underline text-inherit" : ""} ${className || ""}`; + const isLinkCard = Boolean(href) && interactiveCard; + + const cardBody = ( + <> {/* Figma: Header = `border-b` row, `gap-px`, icon `pl-1 pr-2 py-2` + `border-l` on title. */}
+ {cardBody}
); } diff --git a/app/components/sections/GovernanceTemplateGrid/GovernanceTemplateGrid.tsx b/app/components/sections/GovernanceTemplateGrid/GovernanceTemplateGrid.tsx index 69dd25b..f45e9b0 100644 --- a/app/components/sections/GovernanceTemplateGrid/GovernanceTemplateGrid.tsx +++ b/app/components/sections/GovernanceTemplateGrid/GovernanceTemplateGrid.tsx @@ -9,7 +9,13 @@ import type { GovernanceTemplateCatalogEntry } from "../../../../lib/templates/g export interface GovernanceTemplateGridProps { entries: GovernanceTemplateCatalogEntry[]; - onTemplateClick: (_slug: string) => void; + /** + * Real navigation target per card. Cards render as anchors so they are + * keyboard-focusable and work in a new tab. + */ + hrefForTemplate: (_slug: string) => string; + /** Optional side effects on activate (analytics, draft reset). */ + onTemplateClick?: (_slug: string) => void; /** * When true, use project **`md`** (640px) for a 2-column grid (e.g. `/use-cases`). * Default keeps the template shell break at **768px**. @@ -19,6 +25,7 @@ export interface GovernanceTemplateGridProps { export function GovernanceTemplateGrid({ entries, + hrefForTemplate, onTemplateClick, twoColumnsFromMd = false, }: GovernanceTemplateGridProps) { @@ -105,9 +112,14 @@ export function GovernanceTemplateGrid({ /> } backgroundColor={card.backgroundColor} - onClick={() => { - onTemplateClick(card.slug); - }} + href={hrefForTemplate(card.slug)} + onClick={ + onTemplateClick + ? () => { + onTemplateClick(card.slug); + } + : undefined + } /> ))}
diff --git a/app/components/sections/RuleStack/RuleStack.container.tsx b/app/components/sections/RuleStack/RuleStack.container.tsx index e079f95..fc4b3ed 100644 --- a/app/components/sections/RuleStack/RuleStack.container.tsx +++ b/app/components/sections/RuleStack/RuleStack.container.tsx @@ -5,10 +5,10 @@ */ import { memo, useEffect, useState } from "react"; -import { useRouter } from "next/navigation"; import { useTranslation } from "../../../contexts/MessagesContext"; import { logger } from "../../../../lib/logger"; import { prepareFreshCreateFlowEntrySync } from "../../../(app)/create/utils/prepareFreshCreateFlowEntry"; +import { buildTemplateReviewHref } from "../../../(app)/create/utils/flowSteps"; import { fetchTemplates, isTemplatesFetchAborted, @@ -34,7 +34,6 @@ declare global { const RuleStackContainer = memo( ({ className = "", initialGridEntries, translationNamespace, twoColumnsFromMd }) => { - const router = useRouter(); const namespace = translationNamespace ?? "pages.home.ruleStack"; const t = useTranslation(namespace); const [gridEntries, setGridEntries] = useState( @@ -101,12 +100,12 @@ const RuleStackContainer = memo( // `signedIn: true` because this surface has no session in hand; DELETE is // best-effort and the sentinel blocks stale-draft hydration. prepareFreshCreateFlowEntrySync({ signedIn: true }); - router.push(`/create/review-template/${encodeURIComponent(slug)}`); }; return ( buildTemplateReviewHref(slug)} onTemplateClick={handleTemplateClick} gridEntries={gridEntries} sectionTitle={t("title")} diff --git a/app/components/sections/RuleStack/RuleStack.types.ts b/app/components/sections/RuleStack/RuleStack.types.ts index 9c051c0..4df7f7e 100644 --- a/app/components/sections/RuleStack/RuleStack.types.ts +++ b/app/components/sections/RuleStack/RuleStack.types.ts @@ -21,6 +21,7 @@ export interface RuleStackProps { export interface RuleStackViewProps { className: string; onTemplateClick: (_slug: string) => void; + hrefForTemplate: (_slug: string) => string; /** `null` while loading curated templates from the API. */ gridEntries: TemplateGridCardEntry[] | null; sectionTitle: string; diff --git a/app/components/sections/RuleStack/RuleStack.view.tsx b/app/components/sections/RuleStack/RuleStack.view.tsx index 154335d..7ee02f7 100644 --- a/app/components/sections/RuleStack/RuleStack.view.tsx +++ b/app/components/sections/RuleStack/RuleStack.view.tsx @@ -10,6 +10,7 @@ import type { RuleStackViewProps } from "./RuleStack.types"; export function RuleStackView({ className, onTemplateClick, + hrefForTemplate, gridEntries, sectionTitle, sectionSubtitle, @@ -46,6 +47,7 @@ export function RuleStackView({ ) : ( diff --git a/docs/create-flow.md b/docs/create-flow.md index 56a27b3..94b21d4 100644 --- a/docs/create-flow.md +++ b/docs/create-flow.md @@ -76,6 +76,7 @@ Call sites for **`prepareFreshCreateFlowEntry`**: [`Top.container.tsx`](../app/c | Path | Purpose | | --- | --- | | `/create/review-template/[slug]` | Template preview in the create shell; uses the same layout/footer chrome as other create pages but **is not** part of `FLOW_STEP_ORDER` **or** the three Figma stages above. | +| `/create/templates` | In-flow template catalog after review **Create from template**. Same create chrome (Back, Save & Exit); cards link to `/create/review-template/[slug]?fromFlow=1`. Marketing `/templates` stays the public catalog. | From that page, **Customize** pre-fills the custom-rule selections on the current `CreateFlowState` (via [`buildTemplateCustomizePrefill`](../lib/create/applyTemplatePrefill.ts)) and routes to **`/create/core-values`** when the community name (`state.title`) is already set, otherwise to **`/create/informational`**. Name-only is the gate because other community-stage fields (e.g. `communityStructureChipSnapshots`) are sticky once the user lands on those screens; a non-empty title is also the minimum bar [`buildPublishPayload`](../lib/create/buildPublishPayload.ts) enforces, so the two checks stay aligned. No query-param plumbing: state persists via the usual anonymous/server-draft mirrors. @@ -87,7 +88,7 @@ From that page, **Customize** pre-fills the custom-rule selections on the curren - **Customize** routes to `/create/informational` and walks the full Create Community questionnaire. Once the user reaches `/create/review`, [`CommunityReviewScreen`](../app/(app)/create/screens/review/CommunityReviewScreen.tsx) reads the action on mount, clears it via `updateState`, and `router.replace`s to `/create/core-values`. `replace` (not `push`) keeps `community-save` as the Back-button target from that destination. - **Use without changes (direct from a template)** — home Popular templates, `/templates` without `fromFlow`, or a pasted review-template URL. Always routes to `/create/community-name` and walks **name → description → optional photo**, then `/create/confirm-stakeholders`. A leftover draft title is **not** “community already done”; those identity fields are cleared so cache cannot skip or pre-fill the path. Intro, structure, size, save, community review, and custom-rule authoring are skipped. Back from name returns to template review. -- **Use without changes (in-flow)** — `/create/review` → `/templates?fromFlow=1` → template. Community identity is already collected, so this action seeds the template and routes to `/create/confirm-stakeholders`. Back returns to template review. +- **Use without changes (in-flow)** — `/create/review` → `/create/templates` → template. Community identity is already collected, so this action seeds the template and routes to `/create/confirm-stakeholders`. Back returns to template review. The action for Customize is cleared on the first fire so later direct visits to `/create/review` render normally. The review screen is not part of the Use-without-changes identity path. If they land on `/create/review` with a leftover Use-without-changes pin, the screen `replace`s to `community-name`. @@ -97,9 +98,9 @@ The action for Customize is cleared on the first fire so later direct visits to | --- | --- | --- | | Home marketing "Popular templates" ([`RuleStack.container.tsx`](../app/components/sections/RuleStack/RuleStack.container.tsx)) | always `await prepareFreshCreateFlowEntry()` then navigate | `/create/review-template/[slug]` | | `/templates` index ([`TemplatesPageClient.tsx`](../app/(marketing)/templates/TemplatesPageClient.tsx)) visited directly / via pasted URL | `fromFlow` absent → `await prepareFreshCreateFlowEntry()` then navigate | `/create/review-template/[slug]` | -| In-flow: `/create/review` footer "Create from template" → `/templates?fromFlow=1` → template click | `fromFlow=1` → no fresh-entry prep | `/create/review-template/[slug]?fromFlow=1` | +| In-flow: `/create/review` footer "Create from template" → `/create/templates` → template click | no fresh-entry prep; cards are `?fromFlow=1` links | `/create/review-template/[slug]?fromFlow=1` | -Only one `?fromFlow=1` marker exists on `/templates`. Template clicks forward it onto `/create/review-template/[slug]?fromFlow=1` so **Use without changes** can tell in-flow (skip identity) from a direct catalog start (collect name / description / photo). **Customize** still branches on `state.title` (full questionnaire vs `core-values`). +Only one `?fromFlow=1` marker exists on template-review URLs. In-flow catalog clicks on `/create/templates` always include it so **Use without changes** can tell in-flow (skip identity) from a direct catalog start (collect name / description / photo). Profile still uses marketing `/templates?fromFlow=1`. **Customize** still branches on `state.title` (full questionnaire vs `core-values`). **Resume from profile** remains explicit-only: **Continue** clears local mirrors then opens `/create/{step}` so [`SignedInDraftHydration`](../app/(app)/create/SignedInDraftHydration.tsx) can load `/api/drafts/me` when the client buffer is empty. There is no automatic “pick template from marketing → silently merge server draft” path. diff --git a/messages/en/create/community/review.json b/messages/en/create/community/review.json index 15baefa..5fd925d 100644 --- a/messages/en/create/community/review.json +++ b/messages/en/create/community/review.json @@ -3,7 +3,9 @@ "title": "Your community is added - congrats!", "description": "In the next section, we'll go through membership, decision-making, conflict resolution, and community values and create a custom operating manual for your organization based on the specifics you just shared." }, - "ruleCard": { - "title": "Mutual Aid Mondays" + "empty": { + "title": "No community in progress yet", + "description": "This step is for a community you have already named. Start from the beginning to add one.", + "startLabel": "Start creating" } } diff --git a/stories/sections/GovernanceTemplateGrid.stories.js b/stories/sections/GovernanceTemplateGrid.stories.js index 0ed726f..7a836f9 100644 --- a/stories/sections/GovernanceTemplateGrid.stories.js +++ b/stories/sections/GovernanceTemplateGrid.stories.js @@ -12,6 +12,10 @@ export default { control: false, description: "Catalog entries to render as a 2-column grid of Rules", }, + hrefForTemplate: { + control: false, + description: "Builds the review URL for each template card (real href)", + }, onTemplateClick: { action: "template-clicked" }, }, }; @@ -19,11 +23,13 @@ export default { export const Default = { args: { entries: GOVERNANCE_TEMPLATE_CATALOG.slice(0, 4), + hrefForTemplate: (slug) => `/create/review-template/${slug}`, }, }; export const SingleEntry = { args: { entries: GOVERNANCE_TEMPLATE_CATALOG.slice(0, 1), + hrefForTemplate: (slug) => `/create/review-template/${slug}`, }, }; diff --git a/tests/components/GovernanceTemplateGrid.test.tsx b/tests/components/GovernanceTemplateGrid.test.tsx index 8c61694..b08ebc1 100644 --- a/tests/components/GovernanceTemplateGrid.test.tsx +++ b/tests/components/GovernanceTemplateGrid.test.tsx @@ -1,10 +1,13 @@ -import { describe, vi } from "vitest"; +import { describe, expect, it } from "vitest"; +import { screen } from "@testing-library/react"; import { componentTestSuite, type ComponentTestSuiteConfig, } from "../utils/componentTestSuite"; +import { renderWithProviders as render } from "../utils/test-utils"; import { GovernanceTemplateGrid } from "../../app/components/sections/GovernanceTemplateGrid"; import { GOVERNANCE_TEMPLATE_CATALOG } from "../../lib/templates/governanceTemplateCatalog"; +import "@testing-library/jest-dom/vitest"; type Props = React.ComponentProps; @@ -13,10 +16,10 @@ const config: ComponentTestSuiteConfig = { name: "GovernanceTemplateGrid", props: { entries: GOVERNANCE_TEMPLATE_CATALOG.slice(0, 2), - onTemplateClick: vi.fn(), + hrefForTemplate: (slug: string) => `/create/review-template/${slug}`, } as Props, - requiredProps: ["entries", "onTemplateClick"], - primaryRole: "button", + requiredProps: ["entries", "hrefForTemplate"], + primaryRole: "link", testCases: { renders: true, accessibility: true, @@ -25,4 +28,21 @@ const config: ComponentTestSuiteConfig = { describe("GovernanceTemplateGrid", () => { componentTestSuite(config); + + it("renders each catalog card as a link to template review", () => { + const entries = GOVERNANCE_TEMPLATE_CATALOG.slice(0, 2); + render( + `/create/review-template/${slug}`} + />, + ); + + for (const entry of entries) { + expect( + screen.getByRole("link", { name: new RegExp(entry.title, "i") }), + ).toHaveAttribute("href", `/create/review-template/${entry.slug}`); + } + expect(screen.queryByRole("button")).not.toBeInTheDocument(); + }); }); diff --git a/tests/components/ReviewPage.test.tsx b/tests/components/ReviewPage.test.tsx index d0f71f2..09c545c 100644 --- a/tests/components/ReviewPage.test.tsx +++ b/tests/components/ReviewPage.test.tsx @@ -10,6 +10,18 @@ import { CommunityReviewScreen } from "../../app/(app)/create/screens/review/Com import { useCreateFlow } from "../../app/(app)/create/context/CreateFlowContext"; import { testRouter } from "../mocks/navigation"; +function ReviewWithTitle({ title }: { title: string }) { + const { state, updateState } = useCreateFlow(); + const seededRef = React.useRef(false); + useEffect(() => { + if (seededRef.current) return; + seededRef.current = true; + updateState({ title }); + }, [title, updateState]); + if (state.title !== title) return null; + return ; +} + describe("CommunityReviewScreen", () => { beforeEach(() => { testRouter.replace.mockReset(); @@ -21,8 +33,24 @@ describe("CommunityReviewScreen", () => { expect(screen.getByRole("heading", { level: 1 })).toBeInTheDocument(); }); - it("renders HeaderLockup with expected title", () => { + it("shows an empty state instead of a baked-in community name", () => { render(); + expect( + screen.getByRole("heading", { name: "No community in progress yet" }), + ).toBeInTheDocument(); + expect(screen.queryByText("Mutual Aid Mondays")).not.toBeInTheDocument(); + expect( + screen.queryByRole("heading", { + name: "Your community is added - congrats!", + }), + ).not.toBeInTheDocument(); + expect( + screen.getByRole("link", { name: "Start creating" }), + ).toHaveAttribute("href", "/create/informational"); + }); + + it("renders HeaderLockup with expected title when a community name exists", () => { + render(); expect( screen.getByRole("heading", { name: "Your community is added - congrats!", @@ -30,8 +58,8 @@ describe("CommunityReviewScreen", () => { ).toBeInTheDocument(); }); - it("renders HeaderLockup with expected description", () => { - render(); + it("renders HeaderLockup with expected description when a community name exists", () => { + render(); expect( screen.getByText( /In the next section, we'll go through membership, decision-making, conflict resolution, and community values and create a custom operating manual for your organization based on the specifics you just shared./i, @@ -39,13 +67,13 @@ describe("CommunityReviewScreen", () => { ).toBeInTheDocument(); }); - it("renders Rule with title fallback when no community name is set", () => { - render(); - expect(screen.getByText("Mutual Aid Mondays")).toBeInTheDocument(); + it("renders Rule with the community name from state", () => { + render(); + expect(screen.getByText("Garden Club")).toBeInTheDocument(); }); it("omits the Rule description when the user has not entered community context", () => { - render(); + render(); expect( screen.queryByText( /Mutual Aid Monday is a grassroots community in Denver/i, @@ -53,13 +81,13 @@ describe("CommunityReviewScreen", () => { ).not.toBeInTheDocument(); }); - it("renders Rule as a button (card is interactive)", () => { - render(); + it("renders Rule as a button when a community name exists", () => { + render(); const buttons = screen.getAllByRole("button"); expect(buttons.length).toBeGreaterThanOrEqual(1); - expect( - buttons.some((el) => el.textContent?.includes("Mutual Aid Mondays")), - ).toBe(true); + expect(buttons.some((el) => el.textContent?.includes("Garden Club"))).toBe( + true, + ); }); }); diff --git a/tests/pages/create-templates.test.tsx b/tests/pages/create-templates.test.tsx new file mode 100644 index 0000000..31ca58b --- /dev/null +++ b/tests/pages/create-templates.test.tsx @@ -0,0 +1,34 @@ +import { describe, expect, test } from "vitest"; +import { + renderWithProviders as render, + screen, +} from "../utils/test-utils"; +import { CreateFlowTemplatesPageClient } from "../../app/(app)/create/templates/CreateFlowTemplatesPageClient"; +import { GOVERNANCE_TEMPLATE_CATALOG } from "../../lib/templates/governanceTemplateCatalog"; +import "@testing-library/jest-dom/vitest"; + +describe("Create flow templates picker", () => { + test("renders catalog cards as in-flow template review links", () => { + render( + , + ); + + expect( + screen.getByRole("heading", { name: "Templates", level: 1 }), + ).toBeInTheDocument(); + expect( + screen.getByRole("link", { name: /Consensus/i }), + ).toHaveAttribute( + "href", + "/create/review-template/consensus?fromFlow=1", + ); + expect( + screen.getByRole("link", { name: /Solidarity Network/i }), + ).toHaveAttribute( + "href", + "/create/review-template/solidarity-network?fromFlow=1", + ); + }); +}); diff --git a/tests/pages/templates.test.jsx b/tests/pages/templates.test.jsx index 4aae7de..0ac5795 100644 --- a/tests/pages/templates.test.jsx +++ b/tests/pages/templates.test.jsx @@ -54,30 +54,18 @@ describe("Templates page (/templates)", () => { } }); - test("each template card navigates to review flow for its slug", async () => { - const user = userEvent.setup(); + test("each template card is a link to review flow for its slug", () => { render( , ); - await user.click( - screen.getByRole("button", { name: /Consensus/i }), - ); - await waitFor(() => { - expect(testRouter.push).toHaveBeenCalledWith( - "/create/review-template/consensus", - ); - }); - - testRouter.push.mockClear(); - await user.click( - screen.getByRole("button", { name: /Solidarity Network/i }), - ); - await waitFor(() => { - expect(testRouter.push).toHaveBeenCalledWith( - "/create/review-template/solidarity-network", - ); - }); + for (const entry of GOVERNANCE_TEMPLATE_CATALOG) { + expect( + screen.getByRole("link", { + name: `Learn more about ${entry.title} governance pattern`, + }), + ).toHaveAttribute("href", `/create/review-template/${entry.slug}`); + } }); test("direct entry (no ?fromFlow=1): wipes anonymous draft before navigating", async () => { @@ -88,7 +76,7 @@ describe("Templates page (/templates)", () => { ); await user.click( - screen.getByRole("button", { name: /Consensus/i }), + screen.getByRole("link", { name: /Consensus/i }), ); await waitFor(() => { @@ -96,9 +84,6 @@ describe("Templates page (/templates)", () => { expect( window.localStorage.getItem(CORE_VALUE_DETAILS_STORAGE_KEY), ).toBeNull(); - expect(testRouter.push).toHaveBeenCalledWith( - "/create/review-template/consensus", - ); }); }); @@ -113,7 +98,7 @@ describe("Templates page (/templates)", () => { ); await user.click( - screen.getByRole("button", { name: /Consensus/i }), + screen.getByRole("link", { name: /Consensus/i }), ); expect(window.localStorage.getItem(CREATE_FLOW_ANONYMOUS_KEY)).toBe( @@ -124,12 +109,11 @@ describe("Templates page (/templates)", () => { ).toBe( JSON.stringify({ "1": { meaning: "stale", signals: "stale" } }), ); - // In-flow picks also pass `?fromFlow=1` on the template review URL so - // footer Back on `/create/review-template/…` returns to `/create/review`. - await waitFor(() => { - expect(testRouter.push).toHaveBeenCalledWith( - "/create/review-template/consensus?fromFlow=1", - ); - }); + expect( + screen.getByRole("link", { name: /Consensus/i }), + ).toHaveAttribute( + "href", + "/create/review-template/consensus?fromFlow=1", + ); }); }); diff --git a/tests/unit/Rule.test.jsx b/tests/unit/Rule.test.jsx index b6309c2..a3b69b0 100644 --- a/tests/unit/Rule.test.jsx +++ b/tests/unit/Rule.test.jsx @@ -76,6 +76,24 @@ describe("Rule Component", () => { expect(handleClick).toHaveBeenCalledTimes(2); }); + it("renders as a link when href is set", () => { + const handleClick = vi.fn(); + render( + , + ); + + const card = screen.getByRole("link", { + name: "Learn more about Test Rule governance pattern", + }); + expect(card).toHaveAttribute("href", "/create/review-template/consensus"); + fireEvent.click(card, { preventDefault() {} }); + expect(handleClick).toHaveBeenCalledTimes(1); + }); + it("applies hover effects correctly", () => { render(); diff --git a/tests/unit/RuleStack.test.jsx b/tests/unit/RuleStack.test.jsx index 8a1c8ac..f7604de 100644 --- a/tests/unit/RuleStack.test.jsx +++ b/tests/unit/RuleStack.test.jsx @@ -226,13 +226,14 @@ describe("RuleStack Component", () => { render(); await waitForRuleStackCards(); - const consensusCard = screen.getByText("Consensus").closest("div"); + const consensusCard = screen.getByRole("link", { name: /Consensus/i }); + expect(consensusCard).toHaveAttribute( + "href", + "/create/review-template/consensus", + ); await user.click(consensusCard); expect(debugSpy).toHaveBeenCalledWith("consensus template clicked"); - expect(testRouter.push).toHaveBeenCalledWith( - "/create/review-template/consensus", - ); debugSpy.mockRestore(); }); @@ -251,7 +252,7 @@ describe("RuleStack Component", () => { render(); await waitForRuleStackCards(); - const consensusCard = screen.getByText("Consensus").closest("div"); + const consensusCard = screen.getByRole("link", { name: /Consensus/i }); await user.click(consensusCard); expect(window.localStorage.getItem(CREATE_FLOW_ANONYMOUS_KEY)).toBeNull(); @@ -319,8 +320,10 @@ describe("RuleStack Component", () => { render(); await waitForRuleStackCards(); - const buttons = document.querySelectorAll('[role="button"]'); - const templateSurfaces = [...buttons].filter((el) => + const cards = screen.getAllByRole("link").filter((el) => + el.getAttribute("href")?.includes("/create/review-template/"), + ); + const templateSurfaces = [...cards].filter((el) => el.className.includes("--color-surface-invert"), ); expect(templateSurfaces.length).toBe(homeFeatured.length); @@ -375,7 +378,7 @@ describe("RuleStack Component", () => { render(); await waitForRuleStackCards(); - const doOcracyCard = screen.getByText("Do-ocracy").closest("div"); + const doOcracyCard = screen.getByRole("link", { name: /Do-ocracy/i }); await user.click(doOcracyCard); expect(gtagSpy).toHaveBeenCalledWith("event", "template_click", { diff --git a/tests/unit/createFlowLayoutTokens.test.ts b/tests/unit/createFlowLayoutTokens.test.ts index 03ee8e8..e66943e 100644 --- a/tests/unit/createFlowLayoutTokens.test.ts +++ b/tests/unit/createFlowLayoutTokens.test.ts @@ -60,5 +60,11 @@ describe("createFlowLayoutTokens", () => { isTemplateReview: true, }), ).toBe(CREATE_FLOW_TWO_COLUMN_MAX_WIDTH_CLASS); + expect( + getCreateFlowContentMaxClass({ + step: null, + isTemplatesPicker: true, + }), + ).toBe(CREATE_FLOW_TWO_COLUMN_MAX_WIDTH_CLASS); }); }); diff --git a/tests/unit/createFlowPaths.test.ts b/tests/unit/createFlowPaths.test.ts index 9e7cbd2..86b634c 100644 --- a/tests/unit/createFlowPaths.test.ts +++ b/tests/unit/createFlowPaths.test.ts @@ -41,6 +41,7 @@ describe("createFlowPaths (CR-92 §2)", () => { it("CREATE_ROUTES constants", () => { expect(CREATE_ROUTES.review).toBe("/create/review"); + expect(CREATE_ROUTES.templatesPicker).toBe("/create/templates"); expect(CREATE_ROUTES.completed).toBe("/create/completed"); }); diff --git a/tests/unit/flowSteps.test.ts b/tests/unit/flowSteps.test.ts index b87a72c..f92c797 100644 --- a/tests/unit/flowSteps.test.ts +++ b/tests/unit/flowSteps.test.ts @@ -7,14 +7,12 @@ import { isValidStep, getStepIndex, parseReviewReturnSearchParam, + parseCreateFlowScreenFromPathname, resolveCreateFlowBackTarget, shouldOfferCreateFlowSaveAndExit, isDirectTemplateReviewEntry, + isCreateFlowTemplatesPickerPath, createFlowStepUsesSelectSplitScroll, - TEMPLATES_FACET_RECOMMEND_QUERY, - TEMPLATES_FACET_RECOMMEND_VALUE, - TEMPLATE_REVIEW_FROM_CREATE_FLOW_QUERY, - TEMPLATE_REVIEW_FROM_CREATE_FLOW_VALUE, } from "../../app/(app)/create/utils/flowSteps"; describe("flowSteps", () => { @@ -158,10 +156,23 @@ describe("flowSteps", () => { ); }); - it("review Create from template uses fromFlow and recommendTemplates together", () => { + it("isCreateFlowTemplatesPickerPath is true only for /create/templates", () => { + expect(isCreateFlowTemplatesPickerPath("/create/templates")).toBe(true); + expect(isCreateFlowTemplatesPickerPath("/create/templates/")).toBe(true); + expect(isCreateFlowTemplatesPickerPath("/create/review-template/consensus")).toBe( + false, + ); + expect(isCreateFlowTemplatesPickerPath("/templates")).toBe(false); + expect(isCreateFlowTemplatesPickerPath("/create/review")).toBe(false); + expect(isCreateFlowTemplatesPickerPath(null)).toBe(false); + }); + + it("parseCreateFlowScreenFromPathname ignores template auxiliary routes", () => { + expect(parseCreateFlowScreenFromPathname("/create/review")).toBe("review"); + expect(parseCreateFlowScreenFromPathname("/create/templates")).toBeNull(); expect( - `/templates?${TEMPLATE_REVIEW_FROM_CREATE_FLOW_QUERY}=${TEMPLATE_REVIEW_FROM_CREATE_FLOW_VALUE}&${TEMPLATES_FACET_RECOMMEND_QUERY}=${TEMPLATES_FACET_RECOMMEND_VALUE}`, - ).toBe("/templates?fromFlow=1&recommendTemplates=1"); + parseCreateFlowScreenFromPathname("/create/review-template/consensus"), + ).toBeNull(); }); it("parseReviewReturnSearchParam accepts only final-review and edit-rule", () => {