Pin selected after edit
This commit is contained in:
@@ -135,7 +135,7 @@ function CreateFlowLayoutContent({
|
||||
} = useCreateFlowNavigation(
|
||||
skipCommunitySave ? { skipCommunitySave: true } : undefined,
|
||||
);
|
||||
const { state, clearState, updateState, resetCustomRuleSelections, setMethodSectionsPinCommitted } =
|
||||
const { state, clearState, updateState, resetCustomRuleSelections, setMethodSectionsPinCommitted, replaceState } =
|
||||
useCreateFlow();
|
||||
const { draftSaveBannerMessage, setDraftSaveBannerMessage } =
|
||||
useCreateFlowDraftSaveBanner();
|
||||
@@ -177,7 +177,7 @@ function CreateFlowLayoutContent({
|
||||
pathname,
|
||||
state,
|
||||
updateState,
|
||||
resetCustomRuleSelections,
|
||||
replaceState,
|
||||
router,
|
||||
});
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
readAnonymousCreateFlowState,
|
||||
writeAnonymousCreateFlowState,
|
||||
} from "../utils/anonymousDraftStorage";
|
||||
import { stripCustomRuleSelectionFields } from "../../../../lib/create/stripCustomRuleSelectionFields";
|
||||
import {
|
||||
clearCoreValueDetailsLocalStorage,
|
||||
readCoreValueDetailsFromLocalStorage,
|
||||
@@ -170,9 +171,12 @@ export function CreateFlowProvider({
|
||||
});
|
||||
}, []);
|
||||
|
||||
const replaceState = useCallback((next: CreateFlowState) => {
|
||||
setState(next);
|
||||
}, []);
|
||||
const replaceState = useCallback(
|
||||
(next: CreateFlowState | ((prev: CreateFlowState) => CreateFlowState)) => {
|
||||
setState(next);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const clearState = useCallback(() => {
|
||||
setState({});
|
||||
@@ -184,20 +188,7 @@ export function CreateFlowProvider({
|
||||
// Keys produced by the Create Custom stage screens + `buildTemplateCustomizePrefill`.
|
||||
// Kept in sync with `CreateFlowState` comments marked "Create Custom —".
|
||||
const resetCustomRuleSelections = useCallback(() => {
|
||||
setState((prev) => {
|
||||
const {
|
||||
selectedCoreValueIds: _a,
|
||||
coreValuesChipsSnapshot: _b,
|
||||
coreValueDetailsByChipId: _c,
|
||||
selectedCommunicationMethodIds: _d,
|
||||
selectedMembershipMethodIds: _e,
|
||||
selectedDecisionApproachIds: _f,
|
||||
selectedConflictManagementIds: _g,
|
||||
methodSectionsPinCommitted: _h,
|
||||
...rest
|
||||
} = prev;
|
||||
return rest;
|
||||
});
|
||||
setState((prev) => stripCustomRuleSelectionFields(prev));
|
||||
// Effect on `state.coreValueDetailsByChipId` clears its dedicated
|
||||
// localStorage key when the field goes undefined, so we don't need to
|
||||
// touch `clearCoreValueDetailsLocalStorage()` directly here.
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import {
|
||||
buildCoreValuesPrefillFromTemplateBody,
|
||||
buildTemplateCustomizePrefill,
|
||||
} from "../../../../lib/create/applyTemplatePrefill";
|
||||
import { buildTemplateCustomizePrefill } from "../../../../lib/create/applyTemplatePrefill";
|
||||
import { loadTemplateReviewBySlug } from "../../../../lib/create/loadTemplateReviewBySlug";
|
||||
import { stripCustomRuleSelectionFields } from "../../../../lib/create/stripCustomRuleSelectionFields";
|
||||
import messages from "../../../../messages/en/index";
|
||||
import type { CreateFlowState } from "../types";
|
||||
|
||||
type AppRouterLike = { push: (_href: string) => void };
|
||||
type UpdateState = (_patch: Partial<CreateFlowState>) => void;
|
||||
type ReplaceStateFn = (
|
||||
_next: CreateFlowState | ((_prev: CreateFlowState) => CreateFlowState),
|
||||
) => void;
|
||||
|
||||
export type UseTemplateReviewActionsResult = {
|
||||
/** True iff the current pathname is a template-review route (locale/basePath tolerant). */
|
||||
@@ -30,11 +31,12 @@ export type UseTemplateReviewActionsResult = {
|
||||
*/
|
||||
handleCustomize: () => Promise<void>;
|
||||
/**
|
||||
* Use without changes: scrub any prior customize picks, seed the core-values
|
||||
* snapshot from the template's Values section, drop that section from
|
||||
* `state.sections`, and route to `/create/confirm-stakeholders` (or
|
||||
* `/create/informational` with a pin to skip past `/create/review` to
|
||||
* `/create/confirm-stakeholders` later).
|
||||
* Use without changes: scrub any prior customize picks, seed core values +
|
||||
* method-card selections from the template body (same id mapping as
|
||||
* Customize) so drilling from final-review via + shows selected cards, drop
|
||||
* the Values row from `state.sections`, and route to
|
||||
* `/create/confirm-stakeholders` (or `/create/informational` with a pin to
|
||||
* skip past `/create/review` to `/create/confirm-stakeholders` later).
|
||||
*/
|
||||
handleUseWithoutChanges: () => Promise<void>;
|
||||
};
|
||||
@@ -55,19 +57,19 @@ export type UseTemplateReviewActionsResult = {
|
||||
* setTemplateReviewApplyError,
|
||||
* handleCustomize,
|
||||
* handleUseWithoutChanges,
|
||||
* } = useTemplateReviewActions({ pathname, state, updateState, resetCustomRuleSelections, router });
|
||||
* } = useTemplateReviewActions({ pathname, state, updateState, replaceState, router });
|
||||
*/
|
||||
export function useTemplateReviewActions({
|
||||
pathname,
|
||||
state,
|
||||
updateState,
|
||||
resetCustomRuleSelections,
|
||||
replaceState,
|
||||
router,
|
||||
}: {
|
||||
pathname: string | null | undefined;
|
||||
state: CreateFlowState;
|
||||
updateState: UpdateState;
|
||||
resetCustomRuleSelections: () => void;
|
||||
replaceState: ReplaceStateFn;
|
||||
router: AppRouterLike;
|
||||
}): UseTemplateReviewActionsResult {
|
||||
const [isApplyingTemplate, setIsApplyingTemplate] = useState(false);
|
||||
@@ -143,18 +145,19 @@ export function useTemplateReviewActions({
|
||||
return;
|
||||
}
|
||||
|
||||
// Using the template verbatim: scrub any prior customize picks so they
|
||||
// don't bleed into `document.coreValues` at publish time.
|
||||
resetCustomRuleSelections();
|
||||
const hasCommunityName =
|
||||
typeof state.title === "string" && state.title.trim().length > 0;
|
||||
|
||||
// Seed the core-values snapshot from the Values section so the
|
||||
// final-review chip modal can edit them (it keys edits by chip id).
|
||||
// The Values entries themselves are then dropped from `sections` to
|
||||
// avoid publishing `document.coreValues` and `document.sections.Values`
|
||||
// for the same data — matches the "Customize" path's data shape.
|
||||
const coreValuesPrefill = buildCoreValuesPrefillFromTemplateBody(doc);
|
||||
const sectionsWithoutValues =
|
||||
Object.keys(coreValuesPrefill).length > 0
|
||||
// Atomic read-modify-write: strip prior custom-rule picks and merge template
|
||||
// body in one replaceState so method ids are never lost across React batching
|
||||
// (reset + update separately could leave selections undefined in Strict Mode).
|
||||
replaceState((prev) => {
|
||||
const base = stripCustomRuleSelectionFields(prev);
|
||||
const customizePrefill = buildTemplateCustomizePrefill(doc);
|
||||
const hasValuesSeed =
|
||||
customizePrefill.selectedCoreValueIds !== undefined;
|
||||
|
||||
const sectionsWithoutValues = hasValuesSeed
|
||||
? sections.filter((s) => {
|
||||
const name = (s as { categoryName?: unknown }).categoryName;
|
||||
if (typeof name !== "string") return true;
|
||||
@@ -163,33 +166,60 @@ export function useTemplateReviewActions({
|
||||
})
|
||||
: sections;
|
||||
|
||||
const hasCommunityName =
|
||||
typeof state.title === "string" && state.title.trim().length > 0;
|
||||
updateState({
|
||||
...coreValuesPrefill,
|
||||
sections: sectionsWithoutValues,
|
||||
templateReviewBackSlug: templateReviewSlug,
|
||||
...(hasCommunityName
|
||||
? { pendingTemplateAction: undefined }
|
||||
: {
|
||||
pendingTemplateAction: {
|
||||
slug: templateReviewSlug,
|
||||
mode: "useWithoutChanges",
|
||||
},
|
||||
}),
|
||||
const hasCommunityName =
|
||||
typeof prev.title === "string" && prev.title.trim().length > 0;
|
||||
|
||||
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,
|
||||
templateReviewBackSlug: templateReviewSlug,
|
||||
...(hasCommunityName
|
||||
? { pendingTemplateAction: undefined }
|
||||
: {
|
||||
pendingTemplateAction: {
|
||||
slug: templateReviewSlug,
|
||||
mode: "useWithoutChanges",
|
||||
},
|
||||
}),
|
||||
};
|
||||
});
|
||||
router.push(
|
||||
hasCommunityName
|
||||
? "/create/confirm-stakeholders"
|
||||
: "/create/informational",
|
||||
);
|
||||
}, [
|
||||
resetCustomRuleSelections,
|
||||
router,
|
||||
state.title,
|
||||
templateReviewSlug,
|
||||
updateState,
|
||||
]);
|
||||
}, [replaceState, router, state.title, templateReviewSlug]);
|
||||
|
||||
return {
|
||||
isTemplateReviewRoute,
|
||||
|
||||
@@ -212,8 +212,15 @@ export interface CreateFlowContextValue {
|
||||
state: CreateFlowState;
|
||||
currentStep: CreateFlowStep | null;
|
||||
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. */
|
||||
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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user