Create flow centralization and cleanup

This commit is contained in:
adilallo
2026-04-30 08:11:55 -06:00
parent a37a72c71d
commit b7446873cd
26 changed files with 709 additions and 361 deletions
+78
View File
@@ -0,0 +1,78 @@
/**
* Central `/create/...` path builders (Linear CR-92 §2).
* Prefer these over string literals so layout, redirects, hooks, and tests stay aligned.
*/
import type { CreateFlowStep } from "../types";
import { CREATE_FLOW_REVIEW_RETURN_QUERY_KEY } from "./flowSteps";
export const CREATE_ROUTES = {
root: "/",
createRoot: "/create",
/** First step resolves via redirect from `/create`. */
createFirstStep: "/create",
review: "/create/review",
finalReview: "/create/final-review",
completed: "/create/completed",
editRule: "/create/edit-rule",
} as const;
/**
* Post-login return and session-gate paths on wizard steps.
* (Also used when `pathname` is unknown but `syncDraft` must be appended.)
*/
export const CREATE_FLOW_SYNC_DRAFT_QUERY = "syncDraft" as const;
export const CREATE_FLOW_SYNC_DRAFT_VALUE = "1" as const;
export function createFlowStepPathWithSyncDraft(step: CreateFlowStep): string {
return createFlowStepPath(step, {
[CREATE_FLOW_SYNC_DRAFT_QUERY]: CREATE_FLOW_SYNC_DRAFT_VALUE,
});
}
export type CreateFlowPathQuery = Record<
string,
string | number | boolean | undefined
>;
/**
* Path for a wizard step: `/create/{screenId}` with optional query string.
*/
export function createFlowStepPath(
step: CreateFlowStep,
query?: CreateFlowPathQuery,
): string {
const base = `/create/${step}`;
if (query == null || Object.keys(query).length === 0) return base;
const sp = new URLSearchParams();
for (const [k, v] of Object.entries(query)) {
if (v === undefined) continue;
sp.set(k, String(v));
}
const q = sp.toString();
return q.length > 0 ? `${base}?${q}` : base;
}
export function createCompletedPath(query?: CreateFlowPathQuery): string {
return createFlowStepPath("completed", query);
}
/**
* Navigate back from a facet step to final-review / edit-rule, dropping
* `reviewReturn` from the current query while preserving other params.
*/
export function createFlowStepPathAfterStrippingReviewReturn(
step: CreateFlowStep,
searchParams: URLSearchParams | null | undefined,
): string {
const params = new URLSearchParams(searchParams?.toString() ?? "");
params.delete(CREATE_FLOW_REVIEW_RETURN_QUERY_KEY);
const query: CreateFlowPathQuery = {};
params.forEach((value, key) => {
query[key] = value;
});
return createFlowStepPath(
step,
Object.keys(query).length > 0 ? query : undefined,
);
}
@@ -1,3 +1,4 @@
import { CUSTOM_RULE_FACETS } from "../../../../lib/create/customRuleFacets";
import type {
CreateFlowMethodCardFacetSection,
CreateFlowState,
@@ -11,9 +12,9 @@ type FooterMessageKey = keyof typeof footerMessages;
* Binding for each Custom Rule stage step whose footer primary button
* gates the user on "has at least one chip selected?". All five screens
* render the same `<Button …>`; only the disable predicate and the
* footer message differ — this table is the single source of truth for
* both, so `CreateFlowLayoutClient` can render one JSX block for the
* whole group.
* footer message differ — rows are derived from {@link CUSTOM_RULE_FACETS}
* (Linear CR-92) so `CreateFlowLayoutClient` stays aligned with template
* prefill, strip keys, and API section ids.
*
* `selectionIds` returns the currently-selected ids array from flow
* state for that step (empty array when nothing has been selected or
@@ -39,33 +40,11 @@ export type CustomRuleConfirmFooterStep = {
};
export const CUSTOM_RULE_CONFIRM_FOOTER_STEPS: readonly CustomRuleConfirmFooterStep[] =
[
{
step: "core-values",
footerMessageKey: "confirmCoreValues",
selectionIds: (s) => s.selectedCoreValueIds ?? [],
},
{
step: "communication-methods",
footerMessageKey: "confirmCommunication",
selectionIds: (s) => s.selectedCommunicationMethodIds ?? [],
},
{
step: "membership-methods",
footerMessageKey: "confirmMembership",
selectionIds: (s) => s.selectedMembershipMethodIds ?? [],
},
{
step: "decision-approaches",
footerMessageKey: "confirmDecisionApproaches",
selectionIds: (s) => s.selectedDecisionApproachIds ?? [],
},
{
step: "conflict-management",
footerMessageKey: "confirmConflictManagement",
selectionIds: (s) => s.selectedConflictManagementIds ?? [],
},
] as const;
CUSTOM_RULE_FACETS.filter((r) => r.footerMessageKey != null).map((r) => ({
step: r.createFlowStep as CustomRuleConfirmFooterStep["step"],
footerMessageKey: r.footerMessageKey as FooterMessageKey,
selectionIds: r.selectionIds,
}));
export const CUSTOM_RULE_CONFIRM_FOOTER_STEP_BY_STEP: ReadonlyMap<
CreateFlowStep,
@@ -79,16 +58,9 @@ export const CUSTOM_RULE_CONFIRM_FOOTER_STEP_BY_STEP: ReadonlyMap<
export function methodCardFacetSectionForConfirmStep(
step: CustomRuleConfirmFooterStep["step"],
): CreateFlowMethodCardFacetSection | undefined {
switch (step) {
case "communication-methods":
return "communication";
case "membership-methods":
return "membership";
case "decision-approaches":
return "decisionApproaches";
case "conflict-management":
return "conflictManagement";
default:
return undefined;
const row = CUSTOM_RULE_FACETS.find((r) => r.createFlowStep === step);
if (row == null || row.kind !== "method" || row.apiMethodSectionId == null) {
return undefined;
}
return row.apiMethodSectionId;
}
@@ -1,19 +1,13 @@
import type { TemplateFacetGroupKey } from "../../../../lib/create/templateReviewMapping";
import { createFlowStepForCustomRuleFacetGroup } from "../../../../lib/create/customRuleFacets";
import type { CreateFlowStep } from "../types";
const MAP: Record<TemplateFacetGroupKey, CreateFlowStep> = {
coreValues: "core-values",
communication: "communication-methods",
membership: "membership-methods",
decisionApproaches: "decision-approaches",
conflictManagement: "conflict-management",
};
/**
* Custom-rule URL segment for a final-review category row (`+` navigation).
* Source: {@link CUSTOM_RULE_FACETS} (CR-92).
*/
export function createFlowStepForFacetGroup(
groupKey: TemplateFacetGroupKey,
): CreateFlowStep {
return MAP[groupKey];
return createFlowStepForCustomRuleFacetGroup(groupKey);
}
+20
View File
@@ -117,6 +117,26 @@ export function getStepIndex(step: CreateFlowStep | null | undefined): number {
return FLOW_STEP_ORDER.indexOf(step);
}
/**
* Steps where below `lg` the main column scrolls with split layout
* (`CreateFlowLayoutClient` — Linear CR-92 §4).
*/
export const CREATE_FLOW_SELECT_SPLIT_SCROLL_STEPS: readonly CreateFlowStep[] = [
"community-size",
"community-structure",
"core-values",
"decision-approaches",
] as const;
export function createFlowStepUsesSelectSplitScroll(
step: CreateFlowStep | null | undefined,
): boolean {
if (!step) return false;
return (CREATE_FLOW_SELECT_SPLIT_SCROLL_STEPS as readonly string[]).includes(
step,
);
}
/**
* Whether the given string is a valid create flow step
*/