Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6ccc1e8c8e | ||
|
|
90db213413 |
@@ -27,8 +27,7 @@ import {
|
|||||||
shouldOfferCreateFlowSaveAndExit,
|
shouldOfferCreateFlowSaveAndExit,
|
||||||
isDirectTemplateReviewEntry,
|
isDirectTemplateReviewEntry,
|
||||||
createFlowStepUsesSelectSplitScroll,
|
createFlowStepUsesSelectSplitScroll,
|
||||||
TEMPLATES_FACET_RECOMMEND_QUERY,
|
isCreateFlowTemplatesPickerPath,
|
||||||
TEMPLATES_FACET_RECOMMEND_VALUE,
|
|
||||||
TEMPLATE_REVIEW_FROM_CREATE_FLOW_QUERY,
|
TEMPLATE_REVIEW_FROM_CREATE_FLOW_QUERY,
|
||||||
TEMPLATE_REVIEW_FROM_CREATE_FLOW_VALUE,
|
TEMPLATE_REVIEW_FROM_CREATE_FLOW_VALUE,
|
||||||
} from "./utils/flowSteps";
|
} from "./utils/flowSteps";
|
||||||
@@ -309,6 +308,8 @@ function CreateFlowLayoutContent({
|
|||||||
fromCreateWizard,
|
fromCreateWizard,
|
||||||
markCreateFlowInteraction,
|
markCreateFlowInteraction,
|
||||||
});
|
});
|
||||||
|
const isCreateTemplatesPickerRoute =
|
||||||
|
isCreateFlowTemplatesPickerPath(pathname);
|
||||||
|
|
||||||
const runAuthenticatedExit = useCreateFlowExit({
|
const runAuthenticatedExit = useCreateFlowExit({
|
||||||
state,
|
state,
|
||||||
@@ -566,6 +567,8 @@ function CreateFlowLayoutContent({
|
|||||||
? "items-stretch overflow-y-auto md:overflow-hidden"
|
? "items-stretch overflow-y-auto md:overflow-hidden"
|
||||||
: isSelectSplitScrollStep
|
: isSelectSplitScrollStep
|
||||||
? "items-start justify-start overflow-y-auto max-lg:overflow-y-auto lg:min-h-0 lg:items-stretch lg:overflow-hidden"
|
? "items-start justify-start overflow-y-auto max-lg:overflow-y-auto lg:min-h-0 lg:items-stretch lg:overflow-hidden"
|
||||||
|
: isCreateTemplatesPickerRoute
|
||||||
|
? "items-start justify-start overflow-y-auto"
|
||||||
: isTemplateReviewRoute || isFinalReviewLike || isCardLayoutStep
|
: isTemplateReviewRoute || isFinalReviewLike || isCardLayoutStep
|
||||||
? CREATE_FLOW_MD_CENTERED_MAIN_CLASS
|
? CREATE_FLOW_MD_CENTERED_MAIN_CLASS
|
||||||
: "items-start justify-center overflow-y-auto md:items-center";
|
: "items-start justify-center overflow-y-auto md:items-center";
|
||||||
@@ -579,10 +582,9 @@ function CreateFlowLayoutContent({
|
|||||||
? "max-md:flex-col max-md:items-stretch"
|
? "max-md:flex-col max-md:items-stretch"
|
||||||
: "max-md:flex-col max-md:items-center";
|
: "max-md:flex-col max-md:items-center";
|
||||||
const mainResponsiveLayout = `${mainMaxMdCross} ${mainMaxMdJustify} md:flex-row md:justify-center`;
|
const mainResponsiveLayout = `${mainMaxMdCross} ${mainMaxMdJustify} md:flex-row md:justify-center`;
|
||||||
const saveDraftOnExit = shouldOfferCreateFlowSaveAndExit(
|
const saveDraftOnExit =
|
||||||
currentStep,
|
shouldOfferCreateFlowSaveAndExit(currentStep, sessionUser) ||
|
||||||
sessionUser,
|
isCreateTemplatesPickerRoute;
|
||||||
);
|
|
||||||
|
|
||||||
const proportionBarProgress = getProportionBarProgressForCreateFlowStep(
|
const proportionBarProgress = getProportionBarProgressForCreateFlowStep(
|
||||||
currentStep,
|
currentStep,
|
||||||
@@ -814,9 +816,11 @@ function CreateFlowLayoutContent({
|
|||||||
contentMaxClass={getCreateFlowContentMaxClass({
|
contentMaxClass={getCreateFlowContentMaxClass({
|
||||||
step: currentStep,
|
step: currentStep,
|
||||||
isTemplateReview: isTemplateReviewRoute,
|
isTemplateReview: isTemplateReviewRoute,
|
||||||
|
isTemplatesPicker: isCreateTemplatesPickerRoute,
|
||||||
})}
|
})}
|
||||||
progressBar={
|
progressBar={
|
||||||
!isTemplateReviewRoute &&
|
!isTemplateReviewRoute &&
|
||||||
|
!isCreateTemplatesPickerRoute &&
|
||||||
!isFinalReviewLike &&
|
!isFinalReviewLike &&
|
||||||
reviewReturnTarget !== "edit-rule"
|
reviewReturnTarget !== "edit-rule"
|
||||||
}
|
}
|
||||||
@@ -920,15 +924,7 @@ function CreateFlowLayoutContent({
|
|||||||
disabled={isPublishing}
|
disabled={isPublishing}
|
||||||
className={CREATE_FLOW_FOOTER_BUTTON_CLASS}
|
className={CREATE_FLOW_FOOTER_BUTTON_CLASS}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
// `fromFlow=1` tells `/templates` to skip the fresh-slate
|
router.push(CREATE_ROUTES.templatesPicker);
|
||||||
// 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}`,
|
|
||||||
);
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{footer.createFromTemplate}
|
{footer.createFromTemplate}
|
||||||
@@ -1013,7 +1009,11 @@ function CreateFlowLayoutContent({
|
|||||||
) : null
|
) : null
|
||||||
}
|
}
|
||||||
onBackClick={
|
onBackClick={
|
||||||
isTemplateReviewRoute
|
isCreateTemplatesPickerRoute
|
||||||
|
? () => {
|
||||||
|
router.push(CREATE_ROUTES.review);
|
||||||
|
}
|
||||||
|
: isTemplateReviewRoute
|
||||||
? () =>
|
? () =>
|
||||||
router.push(
|
router.push(
|
||||||
templateReviewFooterBackToCreateReview
|
templateReviewFooterBackToCreateReview
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { CREATE_FLOW_PAGE_GUTTER_CLASS } from "./components/createFlowLayoutToke
|
|||||||
import Alert from "../../components/modals/Alert";
|
import Alert from "../../components/modals/Alert";
|
||||||
import {
|
import {
|
||||||
isValidStep,
|
isValidStep,
|
||||||
|
isCreateFlowTemplatesPickerPath,
|
||||||
parseCreateFlowScreenFromPathname,
|
parseCreateFlowScreenFromPathname,
|
||||||
} from "./utils/flowSteps";
|
} from "./utils/flowSteps";
|
||||||
import { hasFreshEntryPending } from "./utils/prepareFreshCreateFlowEntry";
|
import { hasFreshEntryPending } from "./utils/prepareFreshCreateFlowEntry";
|
||||||
@@ -111,6 +112,9 @@ export function SignedInDraftHydration({
|
|||||||
if (pathname?.includes("/create/review-template/")) {
|
if (pathname?.includes("/create/review-template/")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (isCreateFlowTemplatesPickerPath(pathname)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (touchedRef.current) {
|
if (touchedRef.current) {
|
||||||
finishedUserIdRef.current = userId;
|
finishedUserIdRef.current = userId;
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -71,8 +71,9 @@ export const CREATE_FLOW_CARD_STACK_AREA_MAX_CLASS =
|
|||||||
export function getCreateFlowContentMaxClass(options: {
|
export function getCreateFlowContentMaxClass(options: {
|
||||||
step: CreateFlowStep | null | undefined;
|
step: CreateFlowStep | null | undefined;
|
||||||
isTemplateReview?: boolean;
|
isTemplateReview?: boolean;
|
||||||
|
isTemplatesPicker?: boolean;
|
||||||
}): string {
|
}): string {
|
||||||
if (options.isTemplateReview) {
|
if (options.isTemplateReview || options.isTemplatesPicker) {
|
||||||
return CREATE_FLOW_TWO_COLUMN_MAX_WIDTH_CLASS;
|
return CREATE_FLOW_TWO_COLUMN_MAX_WIDTH_CLASS;
|
||||||
}
|
}
|
||||||
if (!options.step) {
|
if (!options.step) {
|
||||||
|
|||||||
@@ -43,7 +43,8 @@ const blurActiveElement = (): void => {
|
|||||||
*
|
*
|
||||||
* Template review footer Back uses {@link buildTemplateReviewHref}’s
|
* Template review footer Back uses {@link buildTemplateReviewHref}’s
|
||||||
* `?fromFlow=1` marker (and persisted `templateReviewEntryFromCreateFlow`) so
|
* `?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(
|
export function useCreateFlowNavigation(
|
||||||
options?: CreateFlowNavigationOptions,
|
options?: CreateFlowNavigationOptions,
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import Rule from "../../../../components/cards/Rule";
|
import Rule from "../../../../components/cards/Rule";
|
||||||
|
import Button from "../../../../components/buttons/Button";
|
||||||
import { useTranslation } from "../../../../contexts/MessagesContext";
|
import { useTranslation } from "../../../../contexts/MessagesContext";
|
||||||
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
|
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
|
||||||
import { useCreateFlow } from "../../context/CreateFlowContext";
|
import { useCreateFlow } from "../../context/CreateFlowContext";
|
||||||
@@ -17,7 +18,7 @@ import {
|
|||||||
vectorMarkPath,
|
vectorMarkPath,
|
||||||
} from "../../../../../lib/assetUtils";
|
} from "../../../../../lib/assetUtils";
|
||||||
import { methodSectionsPinsForHydratedSelections } from "../../../../../lib/create/publishedDocumentToCreateFlowState";
|
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`). */
|
/** Create Community review — Figma `19706:12135` (`/create/review`; two columns from `lg:`; column caps in `createFlowLayoutTokens`). */
|
||||||
export function CommunityReviewScreen() {
|
export function CommunityReviewScreen() {
|
||||||
@@ -25,6 +26,15 @@ export function CommunityReviewScreen() {
|
|||||||
const lgUp = useCreateFlowLgUp();
|
const lgUp = useCreateFlowLgUp();
|
||||||
const t = useTranslation("create.community.review");
|
const t = useTranslation("create.community.review");
|
||||||
const { state, updateState } = useCreateFlow();
|
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
|
* If the user picked **Customize** from a template before finishing community
|
||||||
@@ -70,11 +80,10 @@ export function CommunityReviewScreen() {
|
|||||||
const cardTitle =
|
const cardTitle =
|
||||||
typeof state.title === "string" && state.title.trim().length > 0
|
typeof state.title === "string" && state.title.trim().length > 0
|
||||||
? state.title.trim()
|
? state.title.trim()
|
||||||
: t("ruleCard.title");
|
: "";
|
||||||
/**
|
/**
|
||||||
* No placeholder fallback: if the user skipped `community-context`, leave
|
* No placeholder fallback: if the user skipped `community-context`, leave
|
||||||
* the card description off rather than render the old "Mutual Aid Monday
|
* the card description off rather than render sample copy as real user data.
|
||||||
* is a grassroots community…" sample, which read as real user copy.
|
|
||||||
*/
|
*/
|
||||||
const cardDescription =
|
const cardDescription =
|
||||||
typeof state.communityContext === "string" &&
|
typeof state.communityContext === "string" &&
|
||||||
@@ -88,6 +97,36 @@ export function CommunityReviewScreen() {
|
|||||||
? state.communityAvatarUrl.trim()
|
? state.communityAvatarUrl.trim()
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
if (state.pendingTemplateAction || !reviewReady) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!cardTitle) {
|
||||||
|
return (
|
||||||
|
<CreateFlowStepShell
|
||||||
|
variant="centeredNarrow"
|
||||||
|
contentTopBelowMd="space-1400"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`flex flex-col items-start gap-6 ${CREATE_FLOW_MD_UP_GRID_CELL_CLASS}`}
|
||||||
|
>
|
||||||
|
<CreateFlowHeaderLockup
|
||||||
|
title={t("empty.title")}
|
||||||
|
description={t("empty.description")}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
buttonType="filled"
|
||||||
|
palette="default"
|
||||||
|
size="xsmall"
|
||||||
|
href={CREATE_ROUTES.createFirstStep}
|
||||||
|
>
|
||||||
|
{t("empty.startLabel")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CreateFlowStepShell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CreateFlowStepShell
|
<CreateFlowStepShell
|
||||||
variant="wideGridLoosePadding"
|
variant="wideGridLoosePadding"
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { GovernanceTemplateGrid } from "../../../components/sections/GovernanceTemplateGrid";
|
||||||
|
import type { TemplateGridCardEntry } from "../../../../lib/templates/templateGridPresentation";
|
||||||
|
import { useTranslation } from "../../../contexts/MessagesContext";
|
||||||
|
import { useTemplatesFacetGridEntries } from "../../../(marketing)/templates/useTemplatesFacetGridEntries";
|
||||||
|
import { CreateFlowHeaderLockup } from "../components/CreateFlowHeaderLockup";
|
||||||
|
import { CreateFlowStepShell } from "../components/CreateFlowStepShell";
|
||||||
|
import { CREATE_FLOW_TWO_COLUMN_MAX_WIDTH_CLASS } from "../components/createFlowLayoutTokens";
|
||||||
|
import { buildTemplateReviewHref } from "../utils/flowSteps";
|
||||||
|
|
||||||
|
export function CreateFlowTemplatesPageClient({
|
||||||
|
initialGridEntries,
|
||||||
|
}: {
|
||||||
|
initialGridEntries: TemplateGridCardEntry[];
|
||||||
|
}) {
|
||||||
|
const t = useTranslation("pages.templates");
|
||||||
|
const entries = useTemplatesFacetGridEntries({
|
||||||
|
initialGridEntries,
|
||||||
|
enableFacetRecommendations: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CreateFlowStepShell
|
||||||
|
variant="wideGridLoosePadding"
|
||||||
|
contentTopBelowMd="space-1400"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`mx-auto flex w-full min-w-0 flex-col gap-6 pb-8 ${CREATE_FLOW_TWO_COLUMN_MAX_WIDTH_CLASS}`}
|
||||||
|
>
|
||||||
|
<CreateFlowHeaderLockup
|
||||||
|
title={t("title")}
|
||||||
|
description={t("subtitle")}
|
||||||
|
justification="left"
|
||||||
|
/>
|
||||||
|
<GovernanceTemplateGrid
|
||||||
|
entries={entries}
|
||||||
|
hrefForTemplate={(slug) =>
|
||||||
|
buildTemplateReviewHref(slug, { fromCreateWizard: true })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</CreateFlowStepShell>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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 <CreateFlowTemplatesPageClient initialGridEntries={initialGridEntries} />;
|
||||||
|
}
|
||||||
@@ -215,7 +215,7 @@ export interface CreateFlowState {
|
|||||||
templateReviewBackSlug?: string;
|
templateReviewBackSlug?: string;
|
||||||
/**
|
/**
|
||||||
* True when the user opened `/create/review-template/{slug}` from the create
|
* 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
|
* from template review targets `/create/review` and so returning from
|
||||||
* `confirm-stakeholders` can re-apply `?fromFlow=1` on the template URL.
|
* `confirm-stakeholders` can re-apply `?fromFlow=1` on the template URL.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ export const CREATE_ROUTES = {
|
|||||||
/** Direct path to the first wizard step so client navigations skip the redirect hop. */
|
/** Direct path to the first wizard step so client navigations skip the redirect hop. */
|
||||||
createFirstStep: `/create/${FIRST_STEP}`,
|
createFirstStep: `/create/${FIRST_STEP}`,
|
||||||
review: "/create/review",
|
review: "/create/review",
|
||||||
|
/** In-flow template catalog (wizard chrome). Marketing catalog remains `/templates`. */
|
||||||
|
templatesPicker: "/create/templates",
|
||||||
finalReview: "/create/final-review",
|
finalReview: "/create/final-review",
|
||||||
completed: "/create/completed",
|
completed: "/create/completed",
|
||||||
editRule: "/create/edit-rule",
|
editRule: "/create/edit-rule",
|
||||||
|
|||||||
@@ -235,18 +235,34 @@ export function parseCreateFlowScreenFromPathname(
|
|||||||
): CreateFlowStep | null {
|
): CreateFlowStep | null {
|
||||||
if (!pathname || pathname.length === 0) return null;
|
if (!pathname || pathname.length === 0) return null;
|
||||||
if (pathname.includes("/create/review-template/")) return null;
|
if (pathname.includes("/create/review-template/")) return null;
|
||||||
|
if (isCreateFlowTemplatesPickerPath(pathname)) return null;
|
||||||
|
|
||||||
const parts = pathname.split("/").filter(Boolean);
|
const parts = pathname.split("/").filter(Boolean);
|
||||||
const createIdx = parts.indexOf("create");
|
const createIdx = parts.indexOf("create");
|
||||||
if (createIdx === -1 || createIdx >= parts.length - 1) return null;
|
if (createIdx === -1 || createIdx >= parts.length - 1) return null;
|
||||||
|
|
||||||
const segment = parts[createIdx + 1];
|
const segment = parts[createIdx + 1];
|
||||||
if (segment === "review-template") return null;
|
if (segment === "review-template" || segment === "templates") return null;
|
||||||
|
|
||||||
return isValidStep(segment) ? segment : 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_QUERY = "fromFlow" as const;
|
||||||
export const TEMPLATE_REVIEW_FROM_CREATE_FLOW_VALUE = "1" as const;
|
export const TEMPLATE_REVIEW_FROM_CREATE_FLOW_VALUE = "1" as const;
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Suspense } from "react";
|
import { Suspense } from "react";
|
||||||
import { useRouter, useSearchParams } from "next/navigation";
|
import { useSearchParams } from "next/navigation";
|
||||||
import HeaderLockup from "../../components/type/HeaderLockup";
|
import HeaderLockup from "../../components/type/HeaderLockup";
|
||||||
import { GovernanceTemplateGrid } from "../../components/sections/GovernanceTemplateGrid";
|
import { GovernanceTemplateGrid } from "../../components/sections/GovernanceTemplateGrid";
|
||||||
import type { TemplateGridCardEntry } from "../../../lib/templates/templateGridPresentation";
|
import type { TemplateGridCardEntry } from "../../../lib/templates/templateGridPresentation";
|
||||||
@@ -68,8 +68,8 @@ export default function TemplatesPageClient({
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* - `fromFlow=1` — skip `prepareFreshCreateFlowEntry` on template click
|
* - `fromFlow=1` — skip `prepareFreshCreateFlowEntry` on template click
|
||||||
* (draft preserved). Used by review “Create from template” and profile.
|
* (draft preserved). Used by profile “Create from template”.
|
||||||
* - `recommendTemplates=1` (with review only) — rank templates + “RECOMMENDED”
|
* - `recommendTemplates=1` — rank templates + “RECOMMENDED”
|
||||||
* from `GET /api/templates?facet.*` using the persisted community draft.
|
* from `GET /api/templates?facet.*` using the persisted community draft.
|
||||||
*/
|
*/
|
||||||
function TemplatesGridWithSearchParams({
|
function TemplatesGridWithSearchParams({
|
||||||
@@ -96,19 +96,18 @@ function TemplatesGrid({
|
|||||||
entries: TemplateGridCardEntry[];
|
entries: TemplateGridCardEntry[];
|
||||||
fromFlow: boolean;
|
fromFlow: boolean;
|
||||||
}) {
|
}) {
|
||||||
const router = useRouter();
|
|
||||||
return (
|
return (
|
||||||
<GovernanceTemplateGrid
|
<GovernanceTemplateGrid
|
||||||
entries={entries}
|
entries={entries}
|
||||||
onTemplateClick={(slug) => {
|
hrefForTemplate={(slug) =>
|
||||||
|
buildTemplateReviewHref(slug, { fromCreateWizard: fromFlow })
|
||||||
|
}
|
||||||
|
onTemplateClick={() => {
|
||||||
if (!fromFlow) {
|
if (!fromFlow) {
|
||||||
// Marketing /templates has no session in hand; DELETE is
|
// Marketing /templates has no session in hand; DELETE is
|
||||||
// best-effort and the sentinel blocks stale-draft hydration.
|
// best-effort and the sentinel blocks stale-draft hydration.
|
||||||
prepareFreshCreateFlowEntrySync({ signedIn: true });
|
prepareFreshCreateFlowEntrySync({ signedIn: true });
|
||||||
}
|
}
|
||||||
router.push(
|
|
||||||
buildTemplateReviewHref(slug, { fromCreateWizard: fromFlow }),
|
|
||||||
);
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -18,9 +18,10 @@ type UseTemplatesFacetGridEntriesArgs = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When `enableFacetRecommendations` (review → “Create from template” only),
|
* When `enableFacetRecommendations` (in-flow `/create/templates`, or marketing
|
||||||
* re-fetch ranked templates from `GET /api/templates?facet.*` using the
|
* `/templates?recommendTemplates=1`), re-fetch ranked templates from
|
||||||
* persisted create-flow draft. Otherwise returns `initialGridEntries` from SSR.
|
* `GET /api/templates?facet.*` using the persisted create-flow draft.
|
||||||
|
* Otherwise returns `initialGridEntries` from SSR.
|
||||||
*/
|
*/
|
||||||
export function useTemplatesFacetGridEntries({
|
export function useTemplatesFacetGridEntries({
|
||||||
initialGridEntries,
|
initialGridEntries,
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ const RuleContainer = memo<RuleProps>(
|
|||||||
backgroundColor = "bg-[var(--color-community-teal-100)]",
|
backgroundColor = "bg-[var(--color-community-teal-100)]",
|
||||||
className = "",
|
className = "",
|
||||||
onClick,
|
onClick,
|
||||||
|
href,
|
||||||
expanded = false,
|
expanded = false,
|
||||||
size: sizeProp,
|
size: sizeProp,
|
||||||
categories,
|
categories,
|
||||||
@@ -95,8 +96,9 @@ const RuleContainer = memo<RuleProps>(
|
|||||||
icon={icon}
|
icon={icon}
|
||||||
backgroundColor={backgroundColor}
|
backgroundColor={backgroundColor}
|
||||||
className={className}
|
className={className}
|
||||||
|
href={hasBottomLinks ? undefined : href}
|
||||||
onClick={hasBottomLinks ? undefined : handleClick}
|
onClick={hasBottomLinks ? undefined : handleClick}
|
||||||
onKeyDown={hasBottomLinks ? undefined : handleKeyDown}
|
onKeyDown={hasBottomLinks || href ? undefined : handleKeyDown}
|
||||||
expanded={expanded}
|
expanded={expanded}
|
||||||
size={size}
|
size={size}
|
||||||
categories={categories}
|
categories={categories}
|
||||||
|
|||||||
@@ -50,6 +50,11 @@ export interface RuleProps {
|
|||||||
backgroundColor?: string;
|
backgroundColor?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
onClick?: () => void;
|
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;
|
expanded?: boolean;
|
||||||
size?: RuleSizeValue;
|
size?: RuleSizeValue;
|
||||||
categories?: Category[];
|
categories?: Category[];
|
||||||
@@ -93,6 +98,7 @@ export interface RuleViewProps {
|
|||||||
backgroundColor: string;
|
backgroundColor: string;
|
||||||
className: string;
|
className: string;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
|
href?: string;
|
||||||
onKeyDown?: (_event: React.KeyboardEvent<HTMLDivElement>) => void;
|
onKeyDown?: (_event: React.KeyboardEvent<HTMLDivElement>) => void;
|
||||||
expanded: boolean;
|
expanded: boolean;
|
||||||
size: RuleSizeValue;
|
size: RuleSizeValue;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
import NextLink from "next/link";
|
||||||
import MultiSelect from "../../controls/MultiSelect";
|
import MultiSelect from "../../controls/MultiSelect";
|
||||||
import InlineTextButton from "../../buttons/InlineTextButton";
|
import InlineTextButton from "../../buttons/InlineTextButton";
|
||||||
import NavigationLink from "../../navigation/Link";
|
import NavigationLink from "../../navigation/Link";
|
||||||
@@ -19,6 +20,7 @@ export function RuleView({
|
|||||||
backgroundColor,
|
backgroundColor,
|
||||||
className,
|
className,
|
||||||
onClick,
|
onClick,
|
||||||
|
href,
|
||||||
onKeyDown,
|
onKeyDown,
|
||||||
expanded,
|
expanded,
|
||||||
size,
|
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 || ""}`;
|
||||||
<div
|
const isLinkCard = Boolean(href) && interactiveCard;
|
||||||
className={`${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"} ${className || ""}`}
|
|
||||||
tabIndex={interactiveCard ? 0 : undefined}
|
const cardBody = (
|
||||||
role={interactiveCard ? "button" : "article"}
|
<>
|
||||||
aria-label={ariaLabel}
|
|
||||||
aria-expanded={interactiveCard ? expanded : undefined}
|
|
||||||
onClick={interactiveCard ? onClick : undefined}
|
|
||||||
onKeyDown={interactiveCard ? onKeyDown : undefined}
|
|
||||||
>
|
|
||||||
{/* Figma: Header = `border-b` row, `gap-px`, icon `pl-1 pr-2 py-2` + `border-l` on title. */}
|
{/* Figma: Header = `border-b` row, `gap-px`, icon `pl-1 pr-2 py-2` + `border-l` on title. */}
|
||||||
<div
|
<div
|
||||||
className="
|
className="
|
||||||
@@ -483,6 +480,33 @@ export function RuleView({
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isLinkCard && href) {
|
||||||
|
return (
|
||||||
|
<NextLink
|
||||||
|
href={href}
|
||||||
|
className={cardClassName}
|
||||||
|
aria-label={ariaLabel}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
{cardBody}
|
||||||
|
</NextLink>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cardClassName}
|
||||||
|
tabIndex={interactiveCard ? 0 : undefined}
|
||||||
|
role={interactiveCard ? "button" : "article"}
|
||||||
|
aria-label={ariaLabel}
|
||||||
|
aria-expanded={interactiveCard ? expanded : undefined}
|
||||||
|
onClick={interactiveCard ? onClick : undefined}
|
||||||
|
onKeyDown={interactiveCard ? onKeyDown : undefined}
|
||||||
|
>
|
||||||
|
{cardBody}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,13 @@ import type { GovernanceTemplateCatalogEntry } from "../../../../lib/templates/g
|
|||||||
|
|
||||||
export interface GovernanceTemplateGridProps {
|
export interface GovernanceTemplateGridProps {
|
||||||
entries: GovernanceTemplateCatalogEntry[];
|
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`).
|
* When true, use project **`md`** (640px) for a 2-column grid (e.g. `/use-cases`).
|
||||||
* Default keeps the template shell break at **768px**.
|
* Default keeps the template shell break at **768px**.
|
||||||
@@ -19,6 +25,7 @@ export interface GovernanceTemplateGridProps {
|
|||||||
|
|
||||||
export function GovernanceTemplateGrid({
|
export function GovernanceTemplateGrid({
|
||||||
entries,
|
entries,
|
||||||
|
hrefForTemplate,
|
||||||
onTemplateClick,
|
onTemplateClick,
|
||||||
twoColumnsFromMd = false,
|
twoColumnsFromMd = false,
|
||||||
}: GovernanceTemplateGridProps) {
|
}: GovernanceTemplateGridProps) {
|
||||||
@@ -105,9 +112,14 @@ export function GovernanceTemplateGrid({
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
backgroundColor={card.backgroundColor}
|
backgroundColor={card.backgroundColor}
|
||||||
onClick={() => {
|
href={hrefForTemplate(card.slug)}
|
||||||
|
onClick={
|
||||||
|
onTemplateClick
|
||||||
|
? () => {
|
||||||
onTemplateClick(card.slug);
|
onTemplateClick(card.slug);
|
||||||
}}
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,10 +5,10 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { memo, useEffect, useState } from "react";
|
import { memo, useEffect, useState } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
|
||||||
import { useTranslation } from "../../../contexts/MessagesContext";
|
import { useTranslation } from "../../../contexts/MessagesContext";
|
||||||
import { logger } from "../../../../lib/logger";
|
import { logger } from "../../../../lib/logger";
|
||||||
import { prepareFreshCreateFlowEntrySync } from "../../../(app)/create/utils/prepareFreshCreateFlowEntry";
|
import { prepareFreshCreateFlowEntrySync } from "../../../(app)/create/utils/prepareFreshCreateFlowEntry";
|
||||||
|
import { buildTemplateReviewHref } from "../../../(app)/create/utils/flowSteps";
|
||||||
import {
|
import {
|
||||||
fetchTemplates,
|
fetchTemplates,
|
||||||
isTemplatesFetchAborted,
|
isTemplatesFetchAborted,
|
||||||
@@ -34,7 +34,6 @@ declare global {
|
|||||||
|
|
||||||
const RuleStackContainer = memo<RuleStackProps>(
|
const RuleStackContainer = memo<RuleStackProps>(
|
||||||
({ className = "", initialGridEntries, translationNamespace, twoColumnsFromMd }) => {
|
({ className = "", initialGridEntries, translationNamespace, twoColumnsFromMd }) => {
|
||||||
const router = useRouter();
|
|
||||||
const namespace = translationNamespace ?? "pages.home.ruleStack";
|
const namespace = translationNamespace ?? "pages.home.ruleStack";
|
||||||
const t = useTranslation(namespace);
|
const t = useTranslation(namespace);
|
||||||
const [gridEntries, setGridEntries] = useState<TemplateGridCardEntry[] | null>(
|
const [gridEntries, setGridEntries] = useState<TemplateGridCardEntry[] | null>(
|
||||||
@@ -101,12 +100,12 @@ const RuleStackContainer = memo<RuleStackProps>(
|
|||||||
// `signedIn: true` because this surface has no session in hand; DELETE is
|
// `signedIn: true` because this surface has no session in hand; DELETE is
|
||||||
// best-effort and the sentinel blocks stale-draft hydration.
|
// best-effort and the sentinel blocks stale-draft hydration.
|
||||||
prepareFreshCreateFlowEntrySync({ signedIn: true });
|
prepareFreshCreateFlowEntrySync({ signedIn: true });
|
||||||
router.push(`/create/review-template/${encodeURIComponent(slug)}`);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RuleStackView
|
<RuleStackView
|
||||||
className={className}
|
className={className}
|
||||||
|
hrefForTemplate={(slug) => buildTemplateReviewHref(slug)}
|
||||||
onTemplateClick={handleTemplateClick}
|
onTemplateClick={handleTemplateClick}
|
||||||
gridEntries={gridEntries}
|
gridEntries={gridEntries}
|
||||||
sectionTitle={t("title")}
|
sectionTitle={t("title")}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export interface RuleStackProps {
|
|||||||
export interface RuleStackViewProps {
|
export interface RuleStackViewProps {
|
||||||
className: string;
|
className: string;
|
||||||
onTemplateClick: (_slug: string) => void;
|
onTemplateClick: (_slug: string) => void;
|
||||||
|
hrefForTemplate: (_slug: string) => string;
|
||||||
/** `null` while loading curated templates from the API. */
|
/** `null` while loading curated templates from the API. */
|
||||||
gridEntries: TemplateGridCardEntry[] | null;
|
gridEntries: TemplateGridCardEntry[] | null;
|
||||||
sectionTitle: string;
|
sectionTitle: string;
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import type { RuleStackViewProps } from "./RuleStack.types";
|
|||||||
export function RuleStackView({
|
export function RuleStackView({
|
||||||
className,
|
className,
|
||||||
onTemplateClick,
|
onTemplateClick,
|
||||||
|
hrefForTemplate,
|
||||||
gridEntries,
|
gridEntries,
|
||||||
sectionTitle,
|
sectionTitle,
|
||||||
sectionSubtitle,
|
sectionSubtitle,
|
||||||
@@ -46,6 +47,7 @@ export function RuleStackView({
|
|||||||
) : (
|
) : (
|
||||||
<GovernanceTemplateGrid
|
<GovernanceTemplateGrid
|
||||||
entries={gridEntries}
|
entries={gridEntries}
|
||||||
|
hrefForTemplate={hrefForTemplate}
|
||||||
onTemplateClick={onTemplateClick}
|
onTemplateClick={onTemplateClick}
|
||||||
twoColumnsFromMd={twoColumnsFromMd}
|
twoColumnsFromMd={twoColumnsFromMd}
|
||||||
/>
|
/>
|
||||||
|
|||||||
+4
-3
@@ -76,6 +76,7 @@ Call sites for **`prepareFreshCreateFlowEntry`**: [`Top.container.tsx`](../app/c
|
|||||||
| Path | Purpose |
|
| 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/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.
|
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.
|
- **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 (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`.
|
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]` |
|
| 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]` |
|
| `/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.
|
**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.
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,9 @@
|
|||||||
"title": "Your community is added - congrats!",
|
"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."
|
"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": {
|
"empty": {
|
||||||
"title": "Mutual Aid Mondays"
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,10 @@ export default {
|
|||||||
control: false,
|
control: false,
|
||||||
description: "Catalog entries to render as a 2-column grid of Rules",
|
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 <a> href)",
|
||||||
|
},
|
||||||
onTemplateClick: { action: "template-clicked" },
|
onTemplateClick: { action: "template-clicked" },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -19,11 +23,13 @@ export default {
|
|||||||
export const Default = {
|
export const Default = {
|
||||||
args: {
|
args: {
|
||||||
entries: GOVERNANCE_TEMPLATE_CATALOG.slice(0, 4),
|
entries: GOVERNANCE_TEMPLATE_CATALOG.slice(0, 4),
|
||||||
|
hrefForTemplate: (slug) => `/create/review-template/${slug}`,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SingleEntry = {
|
export const SingleEntry = {
|
||||||
args: {
|
args: {
|
||||||
entries: GOVERNANCE_TEMPLATE_CATALOG.slice(0, 1),
|
entries: GOVERNANCE_TEMPLATE_CATALOG.slice(0, 1),
|
||||||
|
hrefForTemplate: (slug) => `/create/review-template/${slug}`,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import { describe, vi } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { screen } from "@testing-library/react";
|
||||||
import {
|
import {
|
||||||
componentTestSuite,
|
componentTestSuite,
|
||||||
type ComponentTestSuiteConfig,
|
type ComponentTestSuiteConfig,
|
||||||
} from "../utils/componentTestSuite";
|
} from "../utils/componentTestSuite";
|
||||||
|
import { renderWithProviders as render } from "../utils/test-utils";
|
||||||
import { GovernanceTemplateGrid } from "../../app/components/sections/GovernanceTemplateGrid";
|
import { GovernanceTemplateGrid } from "../../app/components/sections/GovernanceTemplateGrid";
|
||||||
import { GOVERNANCE_TEMPLATE_CATALOG } from "../../lib/templates/governanceTemplateCatalog";
|
import { GOVERNANCE_TEMPLATE_CATALOG } from "../../lib/templates/governanceTemplateCatalog";
|
||||||
|
import "@testing-library/jest-dom/vitest";
|
||||||
|
|
||||||
type Props = React.ComponentProps<typeof GovernanceTemplateGrid>;
|
type Props = React.ComponentProps<typeof GovernanceTemplateGrid>;
|
||||||
|
|
||||||
@@ -13,10 +16,10 @@ const config: ComponentTestSuiteConfig<Props> = {
|
|||||||
name: "GovernanceTemplateGrid",
|
name: "GovernanceTemplateGrid",
|
||||||
props: {
|
props: {
|
||||||
entries: GOVERNANCE_TEMPLATE_CATALOG.slice(0, 2),
|
entries: GOVERNANCE_TEMPLATE_CATALOG.slice(0, 2),
|
||||||
onTemplateClick: vi.fn(),
|
hrefForTemplate: (slug: string) => `/create/review-template/${slug}`,
|
||||||
} as Props,
|
} as Props,
|
||||||
requiredProps: ["entries", "onTemplateClick"],
|
requiredProps: ["entries", "hrefForTemplate"],
|
||||||
primaryRole: "button",
|
primaryRole: "link",
|
||||||
testCases: {
|
testCases: {
|
||||||
renders: true,
|
renders: true,
|
||||||
accessibility: true,
|
accessibility: true,
|
||||||
@@ -25,4 +28,21 @@ const config: ComponentTestSuiteConfig<Props> = {
|
|||||||
|
|
||||||
describe("GovernanceTemplateGrid", () => {
|
describe("GovernanceTemplateGrid", () => {
|
||||||
componentTestSuite<Props>(config);
|
componentTestSuite<Props>(config);
|
||||||
|
|
||||||
|
it("renders each catalog card as a link to template review", () => {
|
||||||
|
const entries = GOVERNANCE_TEMPLATE_CATALOG.slice(0, 2);
|
||||||
|
render(
|
||||||
|
<GovernanceTemplateGrid
|
||||||
|
entries={entries}
|
||||||
|
hrefForTemplate={(slug) => `/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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,6 +10,18 @@ import { CommunityReviewScreen } from "../../app/(app)/create/screens/review/Com
|
|||||||
import { useCreateFlow } from "../../app/(app)/create/context/CreateFlowContext";
|
import { useCreateFlow } from "../../app/(app)/create/context/CreateFlowContext";
|
||||||
import { testRouter } from "../mocks/navigation";
|
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 <CommunityReviewScreen />;
|
||||||
|
}
|
||||||
|
|
||||||
describe("CommunityReviewScreen", () => {
|
describe("CommunityReviewScreen", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
testRouter.replace.mockReset();
|
testRouter.replace.mockReset();
|
||||||
@@ -21,8 +33,24 @@ describe("CommunityReviewScreen", () => {
|
|||||||
expect(screen.getByRole("heading", { level: 1 })).toBeInTheDocument();
|
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(<CommunityReviewScreen />);
|
render(<CommunityReviewScreen />);
|
||||||
|
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(<ReviewWithTitle title="Garden Club" />);
|
||||||
expect(
|
expect(
|
||||||
screen.getByRole("heading", {
|
screen.getByRole("heading", {
|
||||||
name: "Your community is added - congrats!",
|
name: "Your community is added - congrats!",
|
||||||
@@ -30,8 +58,8 @@ describe("CommunityReviewScreen", () => {
|
|||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders HeaderLockup with expected description", () => {
|
it("renders HeaderLockup with expected description when a community name exists", () => {
|
||||||
render(<CommunityReviewScreen />);
|
render(<ReviewWithTitle title="Garden Club" />);
|
||||||
expect(
|
expect(
|
||||||
screen.getByText(
|
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,
|
/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();
|
).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders Rule with title fallback when no community name is set", () => {
|
it("renders Rule with the community name from state", () => {
|
||||||
render(<CommunityReviewScreen />);
|
render(<ReviewWithTitle title="Garden Club" />);
|
||||||
expect(screen.getByText("Mutual Aid Mondays")).toBeInTheDocument();
|
expect(screen.getByText("Garden Club")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("omits the Rule description when the user has not entered community context", () => {
|
it("omits the Rule description when the user has not entered community context", () => {
|
||||||
render(<CommunityReviewScreen />);
|
render(<ReviewWithTitle title="Garden Club" />);
|
||||||
expect(
|
expect(
|
||||||
screen.queryByText(
|
screen.queryByText(
|
||||||
/Mutual Aid Monday is a grassroots community in Denver/i,
|
/Mutual Aid Monday is a grassroots community in Denver/i,
|
||||||
@@ -53,13 +81,13 @@ describe("CommunityReviewScreen", () => {
|
|||||||
).not.toBeInTheDocument();
|
).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders Rule as a button (card is interactive)", () => {
|
it("renders Rule as a button when a community name exists", () => {
|
||||||
render(<CommunityReviewScreen />);
|
render(<ReviewWithTitle title="Garden Club" />);
|
||||||
const buttons = screen.getAllByRole("button");
|
const buttons = screen.getAllByRole("button");
|
||||||
expect(buttons.length).toBeGreaterThanOrEqual(1);
|
expect(buttons.length).toBeGreaterThanOrEqual(1);
|
||||||
expect(
|
expect(buttons.some((el) => el.textContent?.includes("Garden Club"))).toBe(
|
||||||
buttons.some((el) => el.textContent?.includes("Mutual Aid Mondays")),
|
true,
|
||||||
).toBe(true);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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(
|
||||||
|
<CreateFlowTemplatesPageClient
|
||||||
|
initialGridEntries={GOVERNANCE_TEMPLATE_CATALOG}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
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",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -54,30 +54,18 @@ describe("Templates page (/templates)", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test("each template card navigates to review flow for its slug", async () => {
|
test("each template card is a link to review flow for its slug", () => {
|
||||||
const user = userEvent.setup();
|
|
||||||
render(
|
render(
|
||||||
<TemplatesPageClient initialGridEntries={GOVERNANCE_TEMPLATE_CATALOG} />,
|
<TemplatesPageClient initialGridEntries={GOVERNANCE_TEMPLATE_CATALOG} />,
|
||||||
);
|
);
|
||||||
|
|
||||||
await user.click(
|
for (const entry of GOVERNANCE_TEMPLATE_CATALOG) {
|
||||||
screen.getByRole("button", { name: /Consensus/i }),
|
expect(
|
||||||
);
|
screen.getByRole("link", {
|
||||||
await waitFor(() => {
|
name: `Learn more about ${entry.title} governance pattern`,
|
||||||
expect(testRouter.push).toHaveBeenCalledWith(
|
}),
|
||||||
"/create/review-template/consensus",
|
).toHaveAttribute("href", `/create/review-template/${entry.slug}`);
|
||||||
);
|
}
|
||||||
});
|
|
||||||
|
|
||||||
testRouter.push.mockClear();
|
|
||||||
await user.click(
|
|
||||||
screen.getByRole("button", { name: /Solidarity Network/i }),
|
|
||||||
);
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(testRouter.push).toHaveBeenCalledWith(
|
|
||||||
"/create/review-template/solidarity-network",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("direct entry (no ?fromFlow=1): wipes anonymous draft before navigating", async () => {
|
test("direct entry (no ?fromFlow=1): wipes anonymous draft before navigating", async () => {
|
||||||
@@ -88,7 +76,7 @@ describe("Templates page (/templates)", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
await user.click(
|
await user.click(
|
||||||
screen.getByRole("button", { name: /Consensus/i }),
|
screen.getByRole("link", { name: /Consensus/i }),
|
||||||
);
|
);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
@@ -96,9 +84,6 @@ describe("Templates page (/templates)", () => {
|
|||||||
expect(
|
expect(
|
||||||
window.localStorage.getItem(CORE_VALUE_DETAILS_STORAGE_KEY),
|
window.localStorage.getItem(CORE_VALUE_DETAILS_STORAGE_KEY),
|
||||||
).toBeNull();
|
).toBeNull();
|
||||||
expect(testRouter.push).toHaveBeenCalledWith(
|
|
||||||
"/create/review-template/consensus",
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -113,7 +98,7 @@ describe("Templates page (/templates)", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
await user.click(
|
await user.click(
|
||||||
screen.getByRole("button", { name: /Consensus/i }),
|
screen.getByRole("link", { name: /Consensus/i }),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(window.localStorage.getItem(CREATE_FLOW_ANONYMOUS_KEY)).toBe(
|
expect(window.localStorage.getItem(CREATE_FLOW_ANONYMOUS_KEY)).toBe(
|
||||||
@@ -124,12 +109,11 @@ describe("Templates page (/templates)", () => {
|
|||||||
).toBe(
|
).toBe(
|
||||||
JSON.stringify({ "1": { meaning: "stale", signals: "stale" } }),
|
JSON.stringify({ "1": { meaning: "stale", signals: "stale" } }),
|
||||||
);
|
);
|
||||||
// In-flow picks also pass `?fromFlow=1` on the template review URL so
|
expect(
|
||||||
// footer Back on `/create/review-template/…` returns to `/create/review`.
|
screen.getByRole("link", { name: /Consensus/i }),
|
||||||
await waitFor(() => {
|
).toHaveAttribute(
|
||||||
expect(testRouter.push).toHaveBeenCalledWith(
|
"href",
|
||||||
"/create/review-template/consensus?fromFlow=1",
|
"/create/review-template/consensus?fromFlow=1",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|||||||
@@ -76,6 +76,24 @@ describe("Rule Component", () => {
|
|||||||
expect(handleClick).toHaveBeenCalledTimes(2);
|
expect(handleClick).toHaveBeenCalledTimes(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("renders as a link when href is set", () => {
|
||||||
|
const handleClick = vi.fn();
|
||||||
|
render(
|
||||||
|
<Rule
|
||||||
|
{...defaultProps}
|
||||||
|
href="/create/review-template/consensus"
|
||||||
|
onClick={handleClick}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
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", () => {
|
it("applies hover effects correctly", () => {
|
||||||
render(<Rule {...defaultProps} />);
|
render(<Rule {...defaultProps} />);
|
||||||
|
|
||||||
|
|||||||
@@ -226,13 +226,14 @@ describe("RuleStack Component", () => {
|
|||||||
render(<RuleStack />);
|
render(<RuleStack />);
|
||||||
await waitForRuleStackCards();
|
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);
|
await user.click(consensusCard);
|
||||||
|
|
||||||
expect(debugSpy).toHaveBeenCalledWith("consensus template clicked");
|
expect(debugSpy).toHaveBeenCalledWith("consensus template clicked");
|
||||||
expect(testRouter.push).toHaveBeenCalledWith(
|
|
||||||
"/create/review-template/consensus",
|
|
||||||
);
|
|
||||||
|
|
||||||
debugSpy.mockRestore();
|
debugSpy.mockRestore();
|
||||||
});
|
});
|
||||||
@@ -251,7 +252,7 @@ describe("RuleStack Component", () => {
|
|||||||
render(<RuleStack />);
|
render(<RuleStack />);
|
||||||
await waitForRuleStackCards();
|
await waitForRuleStackCards();
|
||||||
|
|
||||||
const consensusCard = screen.getByText("Consensus").closest("div");
|
const consensusCard = screen.getByRole("link", { name: /Consensus/i });
|
||||||
await user.click(consensusCard);
|
await user.click(consensusCard);
|
||||||
|
|
||||||
expect(window.localStorage.getItem(CREATE_FLOW_ANONYMOUS_KEY)).toBeNull();
|
expect(window.localStorage.getItem(CREATE_FLOW_ANONYMOUS_KEY)).toBeNull();
|
||||||
@@ -319,8 +320,10 @@ describe("RuleStack Component", () => {
|
|||||||
render(<RuleStack />);
|
render(<RuleStack />);
|
||||||
await waitForRuleStackCards();
|
await waitForRuleStackCards();
|
||||||
|
|
||||||
const buttons = document.querySelectorAll('[role="button"]');
|
const cards = screen.getAllByRole("link").filter((el) =>
|
||||||
const templateSurfaces = [...buttons].filter((el) =>
|
el.getAttribute("href")?.includes("/create/review-template/"),
|
||||||
|
);
|
||||||
|
const templateSurfaces = [...cards].filter((el) =>
|
||||||
el.className.includes("--color-surface-invert"),
|
el.className.includes("--color-surface-invert"),
|
||||||
);
|
);
|
||||||
expect(templateSurfaces.length).toBe(homeFeatured.length);
|
expect(templateSurfaces.length).toBe(homeFeatured.length);
|
||||||
@@ -375,7 +378,7 @@ describe("RuleStack Component", () => {
|
|||||||
render(<RuleStack />);
|
render(<RuleStack />);
|
||||||
await waitForRuleStackCards();
|
await waitForRuleStackCards();
|
||||||
|
|
||||||
const doOcracyCard = screen.getByText("Do-ocracy").closest("div");
|
const doOcracyCard = screen.getByRole("link", { name: /Do-ocracy/i });
|
||||||
await user.click(doOcracyCard);
|
await user.click(doOcracyCard);
|
||||||
|
|
||||||
expect(gtagSpy).toHaveBeenCalledWith("event", "template_click", {
|
expect(gtagSpy).toHaveBeenCalledWith("event", "template_click", {
|
||||||
|
|||||||
@@ -60,5 +60,11 @@ describe("createFlowLayoutTokens", () => {
|
|||||||
isTemplateReview: true,
|
isTemplateReview: true,
|
||||||
}),
|
}),
|
||||||
).toBe(CREATE_FLOW_TWO_COLUMN_MAX_WIDTH_CLASS);
|
).toBe(CREATE_FLOW_TWO_COLUMN_MAX_WIDTH_CLASS);
|
||||||
|
expect(
|
||||||
|
getCreateFlowContentMaxClass({
|
||||||
|
step: null,
|
||||||
|
isTemplatesPicker: true,
|
||||||
|
}),
|
||||||
|
).toBe(CREATE_FLOW_TWO_COLUMN_MAX_WIDTH_CLASS);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ describe("createFlowPaths (CR-92 §2)", () => {
|
|||||||
|
|
||||||
it("CREATE_ROUTES constants", () => {
|
it("CREATE_ROUTES constants", () => {
|
||||||
expect(CREATE_ROUTES.review).toBe("/create/review");
|
expect(CREATE_ROUTES.review).toBe("/create/review");
|
||||||
|
expect(CREATE_ROUTES.templatesPicker).toBe("/create/templates");
|
||||||
expect(CREATE_ROUTES.completed).toBe("/create/completed");
|
expect(CREATE_ROUTES.completed).toBe("/create/completed");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -7,14 +7,12 @@ import {
|
|||||||
isValidStep,
|
isValidStep,
|
||||||
getStepIndex,
|
getStepIndex,
|
||||||
parseReviewReturnSearchParam,
|
parseReviewReturnSearchParam,
|
||||||
|
parseCreateFlowScreenFromPathname,
|
||||||
resolveCreateFlowBackTarget,
|
resolveCreateFlowBackTarget,
|
||||||
shouldOfferCreateFlowSaveAndExit,
|
shouldOfferCreateFlowSaveAndExit,
|
||||||
isDirectTemplateReviewEntry,
|
isDirectTemplateReviewEntry,
|
||||||
|
isCreateFlowTemplatesPickerPath,
|
||||||
createFlowStepUsesSelectSplitScroll,
|
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";
|
} from "../../app/(app)/create/utils/flowSteps";
|
||||||
|
|
||||||
describe("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(
|
expect(
|
||||||
`/templates?${TEMPLATE_REVIEW_FROM_CREATE_FLOW_QUERY}=${TEMPLATE_REVIEW_FROM_CREATE_FLOW_VALUE}&${TEMPLATES_FACET_RECOMMEND_QUERY}=${TEMPLATES_FACET_RECOMMEND_VALUE}`,
|
parseCreateFlowScreenFromPathname("/create/review-template/consensus"),
|
||||||
).toBe("/templates?fromFlow=1&recommendTemplates=1");
|
).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("parseReviewReturnSearchParam accepts only final-review and edit-rule", () => {
|
it("parseReviewReturnSearchParam accepts only final-review and edit-rule", () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user