Pin selected after edit

This commit is contained in:
adilallo
2026-04-29 19:39:08 -06:00
parent a4f0c4bf27
commit 7fde82a94c
6 changed files with 147 additions and 65 deletions
+2 -2
View File
@@ -135,7 +135,7 @@ function CreateFlowLayoutContent({
} = useCreateFlowNavigation( } = useCreateFlowNavigation(
skipCommunitySave ? { skipCommunitySave: true } : undefined, skipCommunitySave ? { skipCommunitySave: true } : undefined,
); );
const { state, clearState, updateState, resetCustomRuleSelections, setMethodSectionsPinCommitted } = const { state, clearState, updateState, resetCustomRuleSelections, setMethodSectionsPinCommitted, replaceState } =
useCreateFlow(); useCreateFlow();
const { draftSaveBannerMessage, setDraftSaveBannerMessage } = const { draftSaveBannerMessage, setDraftSaveBannerMessage } =
useCreateFlowDraftSaveBanner(); useCreateFlowDraftSaveBanner();
@@ -177,7 +177,7 @@ function CreateFlowLayoutContent({
pathname, pathname,
state, state,
updateState, updateState,
resetCustomRuleSelections, replaceState,
router, router,
}); });
+7 -16
View File
@@ -21,6 +21,7 @@ import {
readAnonymousCreateFlowState, readAnonymousCreateFlowState,
writeAnonymousCreateFlowState, writeAnonymousCreateFlowState,
} from "../utils/anonymousDraftStorage"; } from "../utils/anonymousDraftStorage";
import { stripCustomRuleSelectionFields } from "../../../../lib/create/stripCustomRuleSelectionFields";
import { import {
clearCoreValueDetailsLocalStorage, clearCoreValueDetailsLocalStorage,
readCoreValueDetailsFromLocalStorage, readCoreValueDetailsFromLocalStorage,
@@ -170,9 +171,12 @@ export function CreateFlowProvider({
}); });
}, []); }, []);
const replaceState = useCallback((next: CreateFlowState) => { const replaceState = useCallback(
(next: CreateFlowState | ((prev: CreateFlowState) => CreateFlowState)) => {
setState(next); setState(next);
}, []); },
[],
);
const clearState = useCallback(() => { const clearState = useCallback(() => {
setState({}); setState({});
@@ -184,20 +188,7 @@ export function CreateFlowProvider({
// Keys produced by the Create Custom stage screens + `buildTemplateCustomizePrefill`. // Keys produced by the Create Custom stage screens + `buildTemplateCustomizePrefill`.
// Kept in sync with `CreateFlowState` comments marked "Create Custom —". // Kept in sync with `CreateFlowState` comments marked "Create Custom —".
const resetCustomRuleSelections = useCallback(() => { const resetCustomRuleSelections = useCallback(() => {
setState((prev) => { setState((prev) => stripCustomRuleSelectionFields(prev));
const {
selectedCoreValueIds: _a,
coreValuesChipsSnapshot: _b,
coreValueDetailsByChipId: _c,
selectedCommunicationMethodIds: _d,
selectedMembershipMethodIds: _e,
selectedDecisionApproachIds: _f,
selectedConflictManagementIds: _g,
methodSectionsPinCommitted: _h,
...rest
} = prev;
return rest;
});
// Effect on `state.coreValueDetailsByChipId` clears its dedicated // Effect on `state.coreValueDetailsByChipId` clears its dedicated
// localStorage key when the field goes undefined, so we don't need to // localStorage key when the field goes undefined, so we don't need to
// touch `clearCoreValueDetailsLocalStorage()` directly here. // touch `clearCoreValueDetailsLocalStorage()` directly here.
@@ -1,16 +1,17 @@
"use client"; "use client";
import { useCallback, useMemo, useState } from "react"; import { useCallback, useMemo, useState } from "react";
import { import { buildTemplateCustomizePrefill } from "../../../../lib/create/applyTemplatePrefill";
buildCoreValuesPrefillFromTemplateBody,
buildTemplateCustomizePrefill,
} from "../../../../lib/create/applyTemplatePrefill";
import { loadTemplateReviewBySlug } from "../../../../lib/create/loadTemplateReviewBySlug"; import { loadTemplateReviewBySlug } from "../../../../lib/create/loadTemplateReviewBySlug";
import { stripCustomRuleSelectionFields } from "../../../../lib/create/stripCustomRuleSelectionFields";
import messages from "../../../../messages/en/index"; import messages from "../../../../messages/en/index";
import type { CreateFlowState } from "../types"; import type { CreateFlowState } from "../types";
type AppRouterLike = { push: (_href: string) => void }; type AppRouterLike = { push: (_href: string) => void };
type UpdateState = (_patch: Partial<CreateFlowState>) => void; type UpdateState = (_patch: Partial<CreateFlowState>) => void;
type ReplaceStateFn = (
_next: CreateFlowState | ((_prev: CreateFlowState) => CreateFlowState),
) => void;
export type UseTemplateReviewActionsResult = { export type UseTemplateReviewActionsResult = {
/** True iff the current pathname is a template-review route (locale/basePath tolerant). */ /** True iff the current pathname is a template-review route (locale/basePath tolerant). */
@@ -30,11 +31,12 @@ export type UseTemplateReviewActionsResult = {
*/ */
handleCustomize: () => Promise<void>; handleCustomize: () => Promise<void>;
/** /**
* Use without changes: scrub any prior customize picks, seed the core-values * Use without changes: scrub any prior customize picks, seed core values +
* snapshot from the template's Values section, drop that section from * method-card selections from the template body (same id mapping as
* `state.sections`, and route to `/create/confirm-stakeholders` (or * Customize) so drilling from final-review via + shows selected cards, drop
* `/create/informational` with a pin to skip past `/create/review` to * the Values row from `state.sections`, and route to
* `/create/confirm-stakeholders` later). * `/create/confirm-stakeholders` (or `/create/informational` with a pin to
* skip past `/create/review` to `/create/confirm-stakeholders` later).
*/ */
handleUseWithoutChanges: () => Promise<void>; handleUseWithoutChanges: () => Promise<void>;
}; };
@@ -55,19 +57,19 @@ export type UseTemplateReviewActionsResult = {
* setTemplateReviewApplyError, * setTemplateReviewApplyError,
* handleCustomize, * handleCustomize,
* handleUseWithoutChanges, * handleUseWithoutChanges,
* } = useTemplateReviewActions({ pathname, state, updateState, resetCustomRuleSelections, router }); * } = useTemplateReviewActions({ pathname, state, updateState, replaceState, router });
*/ */
export function useTemplateReviewActions({ export function useTemplateReviewActions({
pathname, pathname,
state, state,
updateState, updateState,
resetCustomRuleSelections, replaceState,
router, router,
}: { }: {
pathname: string | null | undefined; pathname: string | null | undefined;
state: CreateFlowState; state: CreateFlowState;
updateState: UpdateState; updateState: UpdateState;
resetCustomRuleSelections: () => void; replaceState: ReplaceStateFn;
router: AppRouterLike; router: AppRouterLike;
}): UseTemplateReviewActionsResult { }): UseTemplateReviewActionsResult {
const [isApplyingTemplate, setIsApplyingTemplate] = useState(false); const [isApplyingTemplate, setIsApplyingTemplate] = useState(false);
@@ -143,18 +145,19 @@ export function useTemplateReviewActions({
return; return;
} }
// Using the template verbatim: scrub any prior customize picks so they const hasCommunityName =
// don't bleed into `document.coreValues` at publish time. typeof state.title === "string" && state.title.trim().length > 0;
resetCustomRuleSelections();
// Seed the core-values snapshot from the Values section so the // Atomic read-modify-write: strip prior custom-rule picks and merge template
// final-review chip modal can edit them (it keys edits by chip id). // body in one replaceState so method ids are never lost across React batching
// The Values entries themselves are then dropped from `sections` to // (reset + update separately could leave selections undefined in Strict Mode).
// avoid publishing `document.coreValues` and `document.sections.Values` replaceState((prev) => {
// for the same data — matches the "Customize" path's data shape. const base = stripCustomRuleSelectionFields(prev);
const coreValuesPrefill = buildCoreValuesPrefillFromTemplateBody(doc); const customizePrefill = buildTemplateCustomizePrefill(doc);
const sectionsWithoutValues = const hasValuesSeed =
Object.keys(coreValuesPrefill).length > 0 customizePrefill.selectedCoreValueIds !== undefined;
const sectionsWithoutValues = hasValuesSeed
? sections.filter((s) => { ? sections.filter((s) => {
const name = (s as { categoryName?: unknown }).categoryName; const name = (s as { categoryName?: unknown }).categoryName;
if (typeof name !== "string") return true; if (typeof name !== "string") return true;
@@ -164,9 +167,41 @@ export function useTemplateReviewActions({
: sections; : sections;
const hasCommunityName = const hasCommunityName =
typeof state.title === "string" && state.title.trim().length > 0; typeof prev.title === "string" && prev.title.trim().length > 0;
updateState({
...coreValuesPrefill, return {
...base,
...(hasValuesSeed
? {
selectedCoreValueIds: customizePrefill.selectedCoreValueIds,
coreValuesChipsSnapshot:
customizePrefill.coreValuesChipsSnapshot,
}
: {}),
...(customizePrefill.selectedCommunicationMethodIds !== undefined
? {
selectedCommunicationMethodIds:
customizePrefill.selectedCommunicationMethodIds,
}
: {}),
...(customizePrefill.selectedMembershipMethodIds !== undefined
? {
selectedMembershipMethodIds:
customizePrefill.selectedMembershipMethodIds,
}
: {}),
...(customizePrefill.selectedDecisionApproachIds !== undefined
? {
selectedDecisionApproachIds:
customizePrefill.selectedDecisionApproachIds,
}
: {}),
...(customizePrefill.selectedConflictManagementIds !== undefined
? {
selectedConflictManagementIds:
customizePrefill.selectedConflictManagementIds,
}
: {}),
sections: sectionsWithoutValues, sections: sectionsWithoutValues,
templateReviewBackSlug: templateReviewSlug, templateReviewBackSlug: templateReviewSlug,
...(hasCommunityName ...(hasCommunityName
@@ -177,19 +212,14 @@ export function useTemplateReviewActions({
mode: "useWithoutChanges", mode: "useWithoutChanges",
}, },
}), }),
};
}); });
router.push( router.push(
hasCommunityName hasCommunityName
? "/create/confirm-stakeholders" ? "/create/confirm-stakeholders"
: "/create/informational", : "/create/informational",
); );
}, [ }, [replaceState, router, state.title, templateReviewSlug]);
resetCustomRuleSelections,
router,
state.title,
templateReviewSlug,
updateState,
]);
return { return {
isTemplateReviewRoute, isTemplateReviewRoute,
+9 -2
View File
@@ -212,8 +212,15 @@ export interface CreateFlowContextValue {
state: CreateFlowState; state: CreateFlowState;
currentStep: CreateFlowStep | null; currentStep: CreateFlowStep | null;
updateState: (_updates: Partial<CreateFlowState>) => void; updateState: (_updates: Partial<CreateFlowState>) => void;
/** Replace entire flow state (e.g. hydrate from server draft). */ /**
replaceState: (_next: CreateFlowState) => void; * Replace entire flow state (e.g. hydrate from server draft), or compute the
* next state from the previous snapshot (atomic read-modify-write).
*/
replaceState: (
_next:
| CreateFlowState
| ((_prev: CreateFlowState) => CreateFlowState),
) => void;
/** Reset flow state and clear anonymous localStorage draft keys when present. */ /** Reset flow state and clear anonymous localStorage draft keys when present. */
clearState: () => void; clearState: () => void;
/** /**
@@ -0,0 +1,22 @@
import type { CreateFlowState } from "../../app/(app)/create/types";
/**
* Same field removal as {@link resetCustomRuleSelections} in CreateFlowProvider.
* Used to apply template "Use without changes" in one atomic replaceState updater.
*/
export function stripCustomRuleSelectionFields(
prev: CreateFlowState,
): CreateFlowState {
const {
selectedCoreValueIds: _a,
coreValuesChipsSnapshot: _b,
coreValueDetailsByChipId: _c,
selectedCommunicationMethodIds: _d,
selectedMembershipMethodIds: _e,
selectedDecisionApproachIds: _f,
selectedConflictManagementIds: _g,
methodSectionsPinCommitted: _h,
...rest
} = prev;
return rest;
}
@@ -0,0 +1,32 @@
import { describe, expect, it } from "vitest";
import { stripCustomRuleSelectionFields } from "../../lib/create/stripCustomRuleSelectionFields";
import type { CreateFlowState } from "../../app/(app)/create/types";
describe("stripCustomRuleSelectionFields", () => {
it("removes custom-rule selection facets and preserves community + template sections", () => {
const prev: CreateFlowState = {
title: "Garden",
communityContext: "...",
selectedCoreValueIds: ["1"],
coreValuesChipsSnapshot: [{ id: "1", label: "X", state: "selected" }],
selectedCommunicationMethodIds: ["signal"],
selectedMembershipMethodIds: ["x"],
selectedDecisionApproachIds: ["y"],
selectedConflictManagementIds: ["z"],
methodSectionsPinCommitted: { communication: true },
coreValueDetailsByChipId: { "1": { meaning: "", signals: "" } },
sections: [{ categoryName: "Communication", entries: [] }],
};
const out = stripCustomRuleSelectionFields(prev);
expect(out.title).toBe("Garden");
expect(out.sections).toEqual(prev.sections);
expect(out.selectedCoreValueIds).toBeUndefined();
expect(out.coreValuesChipsSnapshot).toBeUndefined();
expect(out.selectedCommunicationMethodIds).toBeUndefined();
expect(out.selectedMembershipMethodIds).toBeUndefined();
expect(out.selectedDecisionApproachIds).toBeUndefined();
expect(out.selectedConflictManagementIds).toBeUndefined();
expect(out.methodSectionsPinCommitted).toBeUndefined();
expect(out.coreValueDetailsByChipId).toBeUndefined();
});
});