Files
community-rule/lib/create/methodCardWizardPrefill.ts
T
adilalloandCursor 511c3efb4c Stop seeding 75% consensus on custom decision cards so wizard copy shows on the published rule.
Wire Back on the final-review chip editor to the same dismiss path as close, instead of a no-op.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-31 11:28:32 -06:00

399 lines
12 KiB
TypeScript

import type {
CommunicationMethodDetailEntry,
ConflictManagementDetailEntry,
CoreValueDetailEntry,
CreateFlowState,
DecisionApproachDetailEntry,
MembershipMethodDetailEntry,
} from "../../app/(app)/create/types";
import type { CustomMethodCardFieldBlock } from "./customMethodCardFieldBlocks";
import { formatConflictApplicableScopeForTextarea } from "./ruleSectionsFromMethodSelections";
/** Seed for {@link CustomMethodCardWizard} when Customize opens on an existing card. */
export type MethodCardWizardInitialValues = {
title: string;
description: string;
fieldBlocks: CustomMethodCardFieldBlock[];
};
export type MethodCardWizardFacetPrefill =
| {
group: "communication";
draft: CommunicationMethodDetailEntry;
headings: {
corePrinciple: string;
logisticsAdmin: string;
codeOfConduct: string;
};
}
| {
group: "membership";
draft: MembershipMethodDetailEntry;
headings: {
eligibility: string;
joiningProcess: string;
expectations: string;
};
}
| {
group: "decisionApproaches";
draft: DecisionApproachDetailEntry;
headings: {
corePrinciple: string;
applicableScope: string;
stepByStepInstructions: string;
consensusLevel: string;
objectionsDeadlocks: string;
};
}
| {
group: "conflictManagement";
draft: ConflictManagementDetailEntry;
headings: {
corePrinciple: string;
applicableScope: string;
processProtocol: string;
restorationFallbacks: string;
};
}
| {
group: "coreValues";
draft: CoreValueDetailEntry;
headings: {
meaning: string;
signals: string;
};
};
const TEXT_BODY_MAX = 8000;
function textBlock(
id: string,
blockTitle: string,
placeholderText: string,
): CustomMethodCardFieldBlock {
return {
kind: "text",
id,
blockTitle,
placeholderText: placeholderText.slice(0, TEXT_BODY_MAX),
};
}
function hasTrimmedText(value: string): boolean {
return value.trim().length > 0;
}
function clampPercent(n: number): number {
if (!Number.isFinite(n)) return 1;
return Math.min(100, Math.max(1, Math.round(n)));
}
function facetPrefillHasContent(prefill: MethodCardWizardFacetPrefill): boolean {
switch (prefill.group) {
case "communication":
return (
hasTrimmedText(prefill.draft.corePrinciple) ||
hasTrimmedText(prefill.draft.logisticsAdmin) ||
hasTrimmedText(prefill.draft.codeOfConduct)
);
case "membership":
return (
hasTrimmedText(prefill.draft.eligibility) ||
hasTrimmedText(prefill.draft.joiningProcess) ||
hasTrimmedText(prefill.draft.expectations)
);
case "decisionApproaches":
return (
hasTrimmedText(prefill.draft.corePrinciple) ||
hasTrimmedText(prefill.draft.stepByStepInstructions) ||
hasTrimmedText(prefill.draft.objectionsDeadlocks) ||
prefill.draft.applicableScope.length > 0 ||
prefill.draft.selectedApplicableScope.length > 0
);
case "conflictManagement":
return (
hasTrimmedText(prefill.draft.corePrinciple) ||
hasTrimmedText(prefill.draft.processProtocol) ||
hasTrimmedText(prefill.draft.restorationFallbacks) ||
formatConflictApplicableScopeForTextarea(
prefill.draft.selectedApplicableScope,
prefill.draft.applicableScope,
).trim().length > 0
);
case "coreValues":
return (
hasTrimmedText(prefill.draft.meaning) ||
hasTrimmedText(prefill.draft.signals)
);
}
}
function mapFacetPrefillToWizardFieldBlocks(
prefill: MethodCardWizardFacetPrefill,
): CustomMethodCardFieldBlock[] {
switch (prefill.group) {
case "communication":
return [
textBlock(
"facet-corePrinciple",
prefill.headings.corePrinciple,
prefill.draft.corePrinciple,
),
textBlock(
"facet-logisticsAdmin",
prefill.headings.logisticsAdmin,
prefill.draft.logisticsAdmin,
),
textBlock(
"facet-codeOfConduct",
prefill.headings.codeOfConduct,
prefill.draft.codeOfConduct,
),
];
case "membership":
return [
textBlock(
"facet-eligibility",
prefill.headings.eligibility,
prefill.draft.eligibility,
),
textBlock(
"facet-joiningProcess",
prefill.headings.joiningProcess,
prefill.draft.joiningProcess,
),
textBlock(
"facet-expectations",
prefill.headings.expectations,
prefill.draft.expectations,
),
];
case "decisionApproaches": {
const scopeOptions =
prefill.draft.applicableScope.length > 0
? [...prefill.draft.applicableScope]
: [...prefill.draft.selectedApplicableScope];
const blocks: CustomMethodCardFieldBlock[] = [
textBlock(
"facet-corePrinciple",
prefill.headings.corePrinciple,
prefill.draft.corePrinciple,
),
];
if (scopeOptions.length > 0) {
blocks.push({
kind: "badges",
id: "facet-applicableScope",
blockTitle: prefill.headings.applicableScope,
options: scopeOptions,
});
}
blocks.push(
textBlock(
"facet-stepByStepInstructions",
prefill.headings.stepByStepInstructions,
prefill.draft.stepByStepInstructions,
),
);
if (
typeof prefill.draft.consensusLevel === "number" &&
facetPrefillHasContent(prefill)
) {
blocks.push({
kind: "proportion",
id: "facet-consensusLevel",
blockTitle: prefill.headings.consensusLevel,
defaultPercent: clampPercent(prefill.draft.consensusLevel),
});
}
blocks.push(
textBlock(
"facet-objectionsDeadlocks",
prefill.headings.objectionsDeadlocks,
prefill.draft.objectionsDeadlocks,
),
);
return blocks;
}
case "conflictManagement":
return [
textBlock(
"facet-corePrinciple",
prefill.headings.corePrinciple,
prefill.draft.corePrinciple,
),
textBlock(
"facet-applicableScope",
prefill.headings.applicableScope,
formatConflictApplicableScopeForTextarea(
prefill.draft.selectedApplicableScope,
prefill.draft.applicableScope,
),
),
textBlock(
"facet-processProtocol",
prefill.headings.processProtocol,
prefill.draft.processProtocol,
),
textBlock(
"facet-restorationFallbacks",
prefill.headings.restorationFallbacks,
prefill.draft.restorationFallbacks,
),
];
case "coreValues":
return [
textBlock(
"facet-meaning",
prefill.headings.meaning,
prefill.draft.meaning,
),
textBlock(
"facet-signals",
prefill.headings.signals,
prefill.draft.signals,
),
];
}
}
/**
* Map a method card's facet editors onto wizard field blocks so Customize
* can show and edit the same sections on step 3.
*/
export function facetDetailsToWizardFieldBlocks(
prefill: MethodCardWizardFacetPrefill,
): CustomMethodCardFieldBlock[] {
if (!facetPrefillHasContent(prefill)) {
return [];
}
return mapFacetPrefillToWizardFieldBlocks(prefill);
}
function facetBlockHasContent(block: CustomMethodCardFieldBlock): boolean {
switch (block.kind) {
case "text":
return hasTrimmedText(block.placeholderText);
case "badges":
return block.options.length > 0;
case "proportion":
return true;
case "upload":
return Boolean(block.fileName?.trim() || block.assetUrl?.trim());
}
}
function overlayBlockValue(
existing: CustomMethodCardFieldBlock,
incoming: CustomMethodCardFieldBlock,
): CustomMethodCardFieldBlock {
if (existing.kind === "text" && incoming.kind === "text") {
return { ...existing, placeholderText: incoming.placeholderText };
}
if (existing.kind === "badges" && incoming.kind === "badges") {
return { ...existing, options: [...incoming.options] };
}
if (existing.kind === "proportion" && incoming.kind === "proportion") {
return { ...existing, defaultPercent: incoming.defaultPercent };
}
if (existing.kind === "upload" && incoming.kind === "upload") {
return {
...existing,
fileName: incoming.fileName,
...(incoming.assetUrl !== undefined
? { assetUrl: incoming.assetUrl }
: {}),
};
}
return incoming;
}
/**
* Keep extra wizard-only blocks, but refresh facet-* field values from the
* current modal editors so Customize does not show a stale copy.
*/
export function overlayFacetPrefillValues(
blocks: CustomMethodCardFieldBlock[],
prefill: MethodCardWizardFacetPrefill,
): CustomMethodCardFieldBlock[] {
const incoming = mapFacetPrefillToWizardFieldBlocks(prefill);
const incomingById = new Map(incoming.map((block) => [block.id, block]));
const seen = new Set<string>();
const next = blocks.map((block) => {
const overlay = incomingById.get(block.id);
if (!overlay) {
return block;
}
seen.add(block.id);
return overlayBlockValue(block, overlay);
});
for (const block of incoming) {
if (seen.has(block.id) || !facetBlockHasContent(block)) {
continue;
}
next.push(block);
}
return next;
}
function textPlaceholderForBlockId(
blocks: CustomMethodCardFieldBlock[],
id: string,
): string | undefined {
const block = blocks.find((b) => b.id === id);
if (!block || block.kind !== "text") {
return undefined;
}
return block.placeholderText;
}
/** Map Customize wizard field blocks back onto meaning/signals for a value chip. */
export function coreValueDetailsFromWizardFieldBlocks(
blocks: CustomMethodCardFieldBlock[],
fallback: CoreValueDetailEntry,
): CoreValueDetailEntry {
return {
meaning: textPlaceholderForBlockId(blocks, "facet-meaning") ?? fallback.meaning,
signals:
textPlaceholderForBlockId(blocks, "facet-signals") ?? fallback.signals,
...(fallback.supportText !== undefined
? { supportText: fallback.supportText }
: {}),
};
}
/**
* Title, support text, and field blocks for the custom-policy wizard, preferring
* in-modal drafts over persisted meta/blocks. When the card has no wizard
* blocks yet, facet body fields are mapped onto step-3 blocks.
*/
export function buildMethodCardWizardInitialValues(args: {
cardId: string;
fallbackTitle: string;
fallbackDescription: string;
meta: CreateFlowState["customMethodCardMetaById"];
persistedBlocks: CreateFlowState["customMethodCardFieldBlocksById"];
draftFieldBlocks: CustomMethodCardFieldBlock[] | null;
facetPrefill?: MethodCardWizardFacetPrefill;
}): MethodCardWizardInitialValues {
const metaRow = args.meta?.[args.cardId];
const persisted = args.persistedBlocks?.[args.cardId];
let fieldBlocks =
args.draftFieldBlocks !== null
? structuredClone(args.draftFieldBlocks)
: Array.isArray(persisted) && persisted.length > 0
? structuredClone(persisted)
: [];
if (fieldBlocks.length === 0 && args.facetPrefill) {
fieldBlocks = facetDetailsToWizardFieldBlocks(args.facetPrefill);
} else if (fieldBlocks.length > 0 && args.facetPrefill) {
fieldBlocks = overlayFacetPrefillValues(fieldBlocks, args.facetPrefill);
}
return {
title: metaRow?.label ?? args.fallbackTitle,
description: metaRow?.supportText ?? args.fallbackDescription,
fieldBlocks,
};
}