238 lines
8.3 KiB
TypeScript
238 lines
8.3 KiB
TypeScript
import type {
|
|
CreateFlowMethodCardFacetSection,
|
|
CreateFlowState,
|
|
} from "../../app/(app)/create/types";
|
|
import {
|
|
CUSTOM_RULE_FACETS,
|
|
PUBLISHED_CUSTOM_RULE_SELECTION_KEYS,
|
|
} from "./customRuleFacets";
|
|
import type { PublishedMethodSelections } from "./buildPublishPayload";
|
|
import type { StoredLastPublishedRule } from "./lastPublishedRule";
|
|
import { normalizePublishedDocumentForEdit } from "./normalizePublishedDocumentForEdit";
|
|
import { methodLabelFor } from "./finalReviewChipPresets";
|
|
import type { TemplateFacetGroupKey } from "./templateReviewMapping";
|
|
|
|
function customMethodCardMetaFromPublishedSelections(
|
|
ms: PublishedMethodSelections,
|
|
): CreateFlowState["customMethodCardMetaById"] | undefined {
|
|
const meta: NonNullable<CreateFlowState["customMethodCardMetaById"]> = {};
|
|
const absorb = (
|
|
groupKey: TemplateFacetGroupKey,
|
|
rows:
|
|
| Array<{
|
|
id: string;
|
|
label: string;
|
|
}>
|
|
| undefined,
|
|
) => {
|
|
if (!rows) return;
|
|
for (const row of rows) {
|
|
const id = typeof row.id === "string" ? row.id.trim() : "";
|
|
if (!id) continue;
|
|
if (methodLabelFor(groupKey, id).length > 0) continue;
|
|
const label = typeof row.label === "string" ? row.label.trim() : "";
|
|
if (!label) continue;
|
|
meta[id] = { label, supportText: "" };
|
|
}
|
|
};
|
|
absorb("communication", ms.communication);
|
|
absorb("membership", ms.membership);
|
|
absorb("decisionApproaches", ms.decisionApproaches);
|
|
absorb("conflictManagement", ms.conflictManagement);
|
|
return Object.keys(meta).length > 0 ? meta : undefined;
|
|
}
|
|
|
|
/**
|
|
* True when `patch` (from {@link createFlowStateFromPublishedRule}) expects
|
|
* non-empty facet selections but `state` still has none for that facet.
|
|
*
|
|
* Used so `/create/edit-rule` hydration is not skipped after TopNav **Edit**
|
|
* pre-clears `sections` (which made `sections?.length === 0` look like a
|
|
* finished hydrate even though method ids were never merged).
|
|
*/
|
|
export function isPublishedRuleSelectionMissing(
|
|
state: CreateFlowState,
|
|
patch: Partial<CreateFlowState>,
|
|
): boolean {
|
|
for (const k of PUBLISHED_CUSTOM_RULE_SELECTION_KEYS) {
|
|
const desired = patch[k];
|
|
if (!Array.isArray(desired) || desired.length === 0) continue;
|
|
const actualRaw = state[k];
|
|
const actual = Array.isArray(actualRaw) ? actualRaw : [];
|
|
if (actual.length === 0) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* True when published-rule hydration should run (or continue) — facet ids still
|
|
* empty, or {@link createFlowStateFromPublishedRule} produced
|
|
* `customMethodCardMetaById` for user-authored method UUIDs that `state` does
|
|
* not have yet (final-review chips use meta + id when no preset label exists).
|
|
*/
|
|
export function isPublishedRuleHydratePatchIncomplete(
|
|
state: CreateFlowState,
|
|
patch: Partial<CreateFlowState>,
|
|
): boolean {
|
|
if (isPublishedRuleSelectionMissing(state, patch)) return true;
|
|
const pm = patch.customMethodCardMetaById;
|
|
if (!pm || Object.keys(pm).length === 0) return false;
|
|
const sm = state.customMethodCardMetaById ?? {};
|
|
for (const key of Object.keys(pm)) {
|
|
if (!sm[key]) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Pin flags for method-card facets: persisted for hydration and footer Confirm.
|
|
* Card Stack display pulls selections to the top whenever `selected*` ids are
|
|
* non-empty (`useMethodCardDeckOrdering`); this map still merges on edit /
|
|
* template paths so drafts mirror post-Confirm expectations.
|
|
*
|
|
* Caller should spread onto existing `methodSectionsPinCommitted` so unrelated facets stay
|
|
* as-is (`{ ...prior, ...this }`).
|
|
*/
|
|
export function methodSectionsPinsForHydratedSelections(
|
|
patch: Partial<CreateFlowState>,
|
|
): Partial<Record<CreateFlowMethodCardFacetSection, boolean>> {
|
|
const out: Partial<Record<CreateFlowMethodCardFacetSection, boolean>> = {};
|
|
for (const row of CUSTOM_RULE_FACETS) {
|
|
if (row.kind !== "method" || row.apiMethodSectionId == null) continue;
|
|
const sel = patch[row.selectedIdsStateKey];
|
|
if (Array.isArray(sel) && sel.length > 0) {
|
|
out[row.apiMethodSectionId] = true;
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/** @see {@link methodSectionsPinsForHydratedSelections} — published-rule hydrate naming. */
|
|
export function methodSectionsPinsFromPublishedHydratePatch(
|
|
patch: Partial<CreateFlowState>,
|
|
): Partial<Record<CreateFlowMethodCardFacetSection, boolean>> {
|
|
return methodSectionsPinsForHydratedSelections(patch);
|
|
}
|
|
|
|
/**
|
|
* Rehydrate create-flow fields from a stored published rule so `/create/edit-rule`
|
|
* can render final-review editors after refresh or when branching from completed.
|
|
*/
|
|
export function createFlowStateFromPublishedRule(
|
|
rule: StoredLastPublishedRule,
|
|
): Partial<CreateFlowState> {
|
|
const doc = normalizePublishedDocumentForEdit(
|
|
rule.document,
|
|
) as StoredLastPublishedRule["document"];
|
|
const out: Partial<CreateFlowState> = {
|
|
title: rule.title,
|
|
editingPublishedRuleId: rule.id,
|
|
};
|
|
const sum = typeof rule.summary === "string" ? rule.summary.trim() : "";
|
|
if (sum.length > 0) {
|
|
out.communityContext = sum;
|
|
out.summary = sum;
|
|
}
|
|
|
|
const coreValues = doc.coreValues;
|
|
if (Array.isArray(coreValues) && coreValues.length > 0) {
|
|
const selectedCoreValueIds: string[] = [];
|
|
const coreValuesChipsSnapshot: NonNullable<
|
|
CreateFlowState["coreValuesChipsSnapshot"]
|
|
> = [];
|
|
const coreValueDetailsByChipId: NonNullable<
|
|
CreateFlowState["coreValueDetailsByChipId"]
|
|
> = {};
|
|
|
|
for (const row of coreValues) {
|
|
if (!row || typeof row !== "object") continue;
|
|
const o = row as Record<string, unknown>;
|
|
const chipIdRaw = typeof o.chipId === "string" ? o.chipId.trim() : "";
|
|
const label = typeof o.label === "string" ? o.label.trim() : "";
|
|
if (!label) continue;
|
|
const chipId =
|
|
chipIdRaw.length > 0 ? chipIdRaw : `hydrated-${label.toLowerCase()}`;
|
|
selectedCoreValueIds.push(chipId);
|
|
coreValuesChipsSnapshot.push({
|
|
id: chipId,
|
|
label,
|
|
state: "selected",
|
|
});
|
|
coreValueDetailsByChipId[chipId] = {
|
|
meaning: typeof o.meaning === "string" ? o.meaning : "",
|
|
signals: typeof o.signals === "string" ? o.signals : "",
|
|
};
|
|
}
|
|
out.selectedCoreValueIds = selectedCoreValueIds;
|
|
out.coreValuesChipsSnapshot = coreValuesChipsSnapshot;
|
|
out.coreValueDetailsByChipId = coreValueDetailsByChipId;
|
|
}
|
|
|
|
const avatarUrl =
|
|
typeof doc.communityAvatarUrl === "string"
|
|
? doc.communityAvatarUrl.trim()
|
|
: "";
|
|
if (avatarUrl.length > 0) {
|
|
out.communityAvatarUrl = avatarUrl;
|
|
}
|
|
|
|
const blocksRaw = doc.customMethodCardFieldBlocksById;
|
|
if (
|
|
blocksRaw &&
|
|
typeof blocksRaw === "object" &&
|
|
!Array.isArray(blocksRaw)
|
|
) {
|
|
out.customMethodCardFieldBlocksById =
|
|
blocksRaw as NonNullable<CreateFlowState["customMethodCardFieldBlocksById"]>;
|
|
}
|
|
|
|
const msRaw = doc.methodSelections;
|
|
if (!msRaw || typeof msRaw !== "object" || Array.isArray(msRaw)) {
|
|
out.sections = [];
|
|
return out;
|
|
}
|
|
const ms = msRaw as PublishedMethodSelections;
|
|
|
|
if (Array.isArray(ms.communication) && ms.communication.length > 0) {
|
|
out.selectedCommunicationMethodIds = ms.communication.map((x) => x.id);
|
|
out.communicationMethodDetailsById = Object.fromEntries(
|
|
ms.communication.map((x) => [x.id, x.sections]),
|
|
);
|
|
}
|
|
if (Array.isArray(ms.membership) && ms.membership.length > 0) {
|
|
out.selectedMembershipMethodIds = ms.membership.map((x) => x.id);
|
|
out.membershipMethodDetailsById = Object.fromEntries(
|
|
ms.membership.map((x) => [x.id, x.sections]),
|
|
);
|
|
}
|
|
if (
|
|
Array.isArray(ms.decisionApproaches) &&
|
|
ms.decisionApproaches.length > 0
|
|
) {
|
|
out.selectedDecisionApproachIds = ms.decisionApproaches.map((x) => x.id);
|
|
out.decisionApproachDetailsById = Object.fromEntries(
|
|
ms.decisionApproaches.map((x) => [x.id, x.sections]),
|
|
);
|
|
}
|
|
if (
|
|
Array.isArray(ms.conflictManagement) &&
|
|
ms.conflictManagement.length > 0
|
|
) {
|
|
out.selectedConflictManagementIds = ms.conflictManagement.map(
|
|
(x) => x.id,
|
|
);
|
|
out.conflictManagementDetailsById = Object.fromEntries(
|
|
ms.conflictManagement.map((x) => [x.id, x.sections]),
|
|
);
|
|
}
|
|
|
|
const customMeta = customMethodCardMetaFromPublishedSelections(ms);
|
|
if (customMeta) {
|
|
out.customMethodCardMetaById = customMeta;
|
|
}
|
|
|
|
/** Drop template `sections` so final-review uses `methodSelections` / selected ids (edit path). */
|
|
out.sections = [];
|
|
return out;
|
|
}
|