Pin selected after edit
This commit is contained in:
@@ -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,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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(
|
||||||
setState(next);
|
(next: CreateFlowState | ((prev: CreateFlowState) => CreateFlowState)) => {
|
||||||
}, []);
|
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;
|
||||||
@@ -163,33 +166,60 @@ 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 {
|
||||||
sections: sectionsWithoutValues,
|
...base,
|
||||||
templateReviewBackSlug: templateReviewSlug,
|
...(hasValuesSeed
|
||||||
...(hasCommunityName
|
? {
|
||||||
? { pendingTemplateAction: undefined }
|
selectedCoreValueIds: customizePrefill.selectedCoreValueIds,
|
||||||
: {
|
coreValuesChipsSnapshot:
|
||||||
pendingTemplateAction: {
|
customizePrefill.coreValuesChipsSnapshot,
|
||||||
slug: templateReviewSlug,
|
}
|
||||||
mode: "useWithoutChanges",
|
: {}),
|
||||||
},
|
...(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(
|
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,
|
||||||
|
|||||||
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user