Add custom intervention modals
This commit is contained in:
@@ -0,0 +1,279 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* Controlled field blocks for wizard-authored method cards in Create modals
|
||||
* (facet screens + final-review chip edit). When `onBlocksChange` is omitted,
|
||||
* blocks render read-only (disabled controls).
|
||||
*
|
||||
* Layout matches preset method editors ({@link CommunicationMethodEditFields},
|
||||
* {@link DecisionApproachEditFields}): {@link ModalTextAreaField},
|
||||
* {@link ApplicableScopeField} chip rows, {@link IncrementerBlock}.
|
||||
*/
|
||||
|
||||
import { memo, useCallback, useRef } from "react";
|
||||
import { useMessages } from "../../../contexts/MessagesContext";
|
||||
import Chip from "../../../components/controls/Chip";
|
||||
import IncrementerBlock from "../../../components/controls/IncrementerBlock";
|
||||
import InlineTextButton from "../../../components/buttons/InlineTextButton";
|
||||
import Upload from "../../../components/controls/Upload";
|
||||
import ApplicableScopeField from "./ApplicableScopeField";
|
||||
import InputLabel from "../../../components/type/InputLabel";
|
||||
import type { CustomMethodCardFieldBlock } from "../../../../lib/create/customMethodCardFieldBlocks";
|
||||
import ModalTextAreaField from "./ModalTextAreaField";
|
||||
|
||||
const TEXT_VALUE_MAX = 8000;
|
||||
|
||||
export interface CustomMethodCardFieldBlocksSummaryProps {
|
||||
blocks: CustomMethodCardFieldBlock[];
|
||||
/** When set, fields update the draft via immutable block-array replacements. */
|
||||
onBlocksChange?: (_next: CustomMethodCardFieldBlock[]) => void;
|
||||
}
|
||||
|
||||
function mapBlockById(
|
||||
blocks: CustomMethodCardFieldBlock[],
|
||||
blockId: string,
|
||||
mapFn: (_b: CustomMethodCardFieldBlock) => CustomMethodCardFieldBlock,
|
||||
): CustomMethodCardFieldBlock[] {
|
||||
return blocks.map((b) => (b.id === blockId ? mapFn(b) : b));
|
||||
}
|
||||
|
||||
function CustomMethodCardUploadBlockRow({
|
||||
block,
|
||||
blocks,
|
||||
patch,
|
||||
uploadFileInputAriaLabel,
|
||||
uploadHint,
|
||||
clearFileLabel,
|
||||
noFileChosen,
|
||||
}: {
|
||||
block: Extract<CustomMethodCardFieldBlock, { kind: "upload" }>;
|
||||
blocks: CustomMethodCardFieldBlock[];
|
||||
patch: (_next: CustomMethodCardFieldBlock[]) => void;
|
||||
uploadFileInputAriaLabel: string;
|
||||
uploadHint: string;
|
||||
clearFileLabel: string;
|
||||
noFileChosen: string;
|
||||
}) {
|
||||
const uploadInputRef = useRef<HTMLInputElement | null>(null);
|
||||
const displayName = block.fileName?.trim() ? block.fileName : noFileChosen;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<InputLabel
|
||||
label={block.blockTitle}
|
||||
helpIcon
|
||||
size="s"
|
||||
palette="default"
|
||||
/>
|
||||
<p className="font-[family-name:var(--font-body)] text-[length:var(--font-size-body-m)] text-[var(--color-content-default-secondary)]">
|
||||
{displayName}
|
||||
</p>
|
||||
<input
|
||||
ref={uploadInputRef}
|
||||
type="file"
|
||||
className="sr-only"
|
||||
tabIndex={-1}
|
||||
aria-label={uploadFileInputAriaLabel}
|
||||
onChange={(e) => {
|
||||
const file = e.target.files?.[0];
|
||||
const name = file?.name?.trim();
|
||||
patch(
|
||||
mapBlockById(blocks, block.id, (b) =>
|
||||
b.kind === "upload"
|
||||
? {
|
||||
...b,
|
||||
...(name ? { fileName: name } : {}),
|
||||
}
|
||||
: b,
|
||||
),
|
||||
);
|
||||
e.target.value = "";
|
||||
}}
|
||||
/>
|
||||
<Upload
|
||||
hintText={uploadHint}
|
||||
onClick={() => uploadInputRef.current?.click()}
|
||||
/>
|
||||
{block.fileName?.trim() ? (
|
||||
<InlineTextButton
|
||||
onClick={() =>
|
||||
patch(
|
||||
mapBlockById(blocks, block.id, (b) =>
|
||||
b.kind === "upload" ? { ...b, fileName: undefined } : b,
|
||||
),
|
||||
)
|
||||
}
|
||||
>
|
||||
{clearFileLabel}
|
||||
</InlineTextButton>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CustomMethodCardFieldBlocksSummaryComponent({
|
||||
blocks,
|
||||
onBlocksChange,
|
||||
}: CustomMethodCardFieldBlocksSummaryProps) {
|
||||
const m = useMessages();
|
||||
const wiz = m.create.customRule.customMethodCardWizard;
|
||||
const fm = wiz.fieldModals;
|
||||
const em = wiz.editModal;
|
||||
const emptyValue = em.readout.emptyValue;
|
||||
const noFileChosen = em.readout.noFileChosen;
|
||||
const readOnly = !onBlocksChange;
|
||||
|
||||
const patch = useCallback(
|
||||
(next: CustomMethodCardFieldBlock[]) => {
|
||||
onBlocksChange?.(next);
|
||||
},
|
||||
[onBlocksChange],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
{blocks.map((block) => {
|
||||
if (block.kind === "text") {
|
||||
return (
|
||||
<ModalTextAreaField
|
||||
key={block.id}
|
||||
label={block.blockTitle}
|
||||
rows={6}
|
||||
value={block.placeholderText}
|
||||
onChange={(v) =>
|
||||
patch(
|
||||
mapBlockById(blocks, block.id, (b) =>
|
||||
b.kind === "text"
|
||||
? { ...b, placeholderText: v.slice(0, TEXT_VALUE_MAX) }
|
||||
: b,
|
||||
),
|
||||
)
|
||||
}
|
||||
disabled={readOnly}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (block.kind === "badges") {
|
||||
if (readOnly) {
|
||||
return (
|
||||
<div key={block.id} className="flex flex-col gap-2">
|
||||
<InputLabel
|
||||
label={block.blockTitle}
|
||||
helpIcon
|
||||
size="s"
|
||||
palette="default"
|
||||
/>
|
||||
{block.options.length > 0 ? (
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{block.options.map((opt, idx) => (
|
||||
<Chip
|
||||
key={`${block.id}-${idx}`}
|
||||
label={opt}
|
||||
state="selected"
|
||||
palette="default"
|
||||
size="s"
|
||||
disabled
|
||||
ariaLabel={opt}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="font-[family-name:var(--font-body)] text-[length:var(--font-size-body-m)] text-[var(--color-content-default-secondary)]">
|
||||
{emptyValue}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<ApplicableScopeField
|
||||
key={block.id}
|
||||
label={block.blockTitle}
|
||||
addLabel={fm.badges.addOptionLabel}
|
||||
scopes={block.options}
|
||||
selectedScopes={block.options}
|
||||
onToggleScope={(scope) =>
|
||||
patch(
|
||||
mapBlockById(blocks, block.id, (b) =>
|
||||
b.kind === "badges"
|
||||
? { ...b, options: b.options.filter((o) => o !== scope) }
|
||||
: b,
|
||||
),
|
||||
)
|
||||
}
|
||||
onAddScope={(scope) =>
|
||||
patch(
|
||||
mapBlockById(blocks, block.id, (b) => {
|
||||
if (b.kind !== "badges") return b;
|
||||
if (b.options.includes(scope) || b.options.length >= 50)
|
||||
return b;
|
||||
return { ...b, options: [...b.options, scope] };
|
||||
}),
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (block.kind === "upload") {
|
||||
return (
|
||||
<div key={block.id}>
|
||||
{readOnly ? (
|
||||
<ModalTextAreaField
|
||||
label={block.blockTitle}
|
||||
rows={2}
|
||||
value={
|
||||
block.fileName?.trim() ? block.fileName : noFileChosen
|
||||
}
|
||||
onChange={() => {}}
|
||||
disabled
|
||||
/>
|
||||
) : (
|
||||
<CustomMethodCardUploadBlockRow
|
||||
block={block}
|
||||
blocks={blocks}
|
||||
patch={patch}
|
||||
uploadFileInputAriaLabel={fm.upload.uploadFileInputAriaLabel}
|
||||
uploadHint={fm.upload.uploadHint}
|
||||
clearFileLabel={em.clearFileLabel}
|
||||
noFileChosen={noFileChosen}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<IncrementerBlock
|
||||
key={block.id}
|
||||
label={block.blockTitle}
|
||||
value={block.defaultPercent}
|
||||
min={1}
|
||||
max={100}
|
||||
step={1}
|
||||
disabled={readOnly}
|
||||
onChange={(v) =>
|
||||
patch(
|
||||
mapBlockById(blocks, block.id, (b) =>
|
||||
b.kind === "proportion" ? { ...b, defaultPercent: v } : b,
|
||||
),
|
||||
)
|
||||
}
|
||||
formatValue={(v) => `${v}%`}
|
||||
decrementAriaLabel={fm.proportion.decrementAriaLabel}
|
||||
incrementAriaLabel={fm.proportion.incrementAriaLabel}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const CustomMethodCardFieldBlocksSummary = memo(
|
||||
CustomMethodCardFieldBlocksSummaryComponent,
|
||||
);
|
||||
CustomMethodCardFieldBlocksSummary.displayName =
|
||||
"CustomMethodCardFieldBlocksSummary";
|
||||
|
||||
export default CustomMethodCardFieldBlocksSummary;
|
||||
@@ -0,0 +1,31 @@
|
||||
"use client";
|
||||
|
||||
import type { CustomMethodCardFieldBlock } from "../../../../lib/create/customMethodCardFieldBlocks";
|
||||
import type { CreateFlowState } from "../types";
|
||||
import CustomMethodCardFieldBlocksSummary from "./CustomMethodCardFieldBlocksSummary";
|
||||
import CustomMethodCardPresetEditPlaceholder from "./CustomMethodCardPresetEditPlaceholder";
|
||||
|
||||
/** Body for Create modals when the card is user-authored (custom UUID). */
|
||||
export default function CustomMethodCardModalBody({
|
||||
cardId,
|
||||
blocksById,
|
||||
/** When set, used instead of `blocksById[cardId]` (e.g. final-review draft). */
|
||||
blocksOverride,
|
||||
onFieldBlocksChange,
|
||||
}: {
|
||||
cardId: string;
|
||||
blocksById: CreateFlowState["customMethodCardFieldBlocksById"];
|
||||
blocksOverride?: CustomMethodCardFieldBlock[] | null;
|
||||
onFieldBlocksChange?: (_blocks: CustomMethodCardFieldBlock[]) => void;
|
||||
}) {
|
||||
const blocks = blocksOverride ?? blocksById?.[cardId];
|
||||
if (blocks && blocks.length > 0) {
|
||||
return (
|
||||
<CustomMethodCardFieldBlocksSummary
|
||||
blocks={blocks}
|
||||
onBlocksChange={onFieldBlocksChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return <CustomMethodCardPresetEditPlaceholder />;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* Shown in method-card Create modals and final-review chip edit when the chip
|
||||
* is user-authored (`customMethodCardMetaById`) — preset section editors do
|
||||
* not apply until structured parity exists with wizard field blocks.
|
||||
*/
|
||||
|
||||
import { memo } from "react";
|
||||
import { useMessages } from "../../../contexts/MessagesContext";
|
||||
|
||||
function CustomMethodCardPresetEditPlaceholderComponent() {
|
||||
const m = useMessages();
|
||||
const body = m.create.customRule.customMethodCardWizard.editModal.placeholderBody;
|
||||
|
||||
return (
|
||||
<p className="font-[family-name:var(--font-body)] text-[length:var(--font-size-body-m,15px)] leading-[var(--line-height-body-m,22px)] text-[var(--color-content-default-secondary)]">
|
||||
{body}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
CustomMethodCardPresetEditPlaceholderComponent.displayName =
|
||||
"CustomMethodCardPresetEditPlaceholder";
|
||||
|
||||
export default memo(CustomMethodCardPresetEditPlaceholderComponent);
|
||||
+358
@@ -0,0 +1,358 @@
|
||||
"use client";
|
||||
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useMessages, useTranslation } from "../../../../contexts/MessagesContext";
|
||||
import type { CustomMethodCardFieldBlock } from "../../../../../lib/create/customMethodCardFieldBlocks";
|
||||
import { CUSTOM_METHOD_CARD_WIZARD_MAX_FIELD_CHARS } from "../../../../../lib/create/customMethodCardWizardConstants";
|
||||
import type { AddCustomFieldType } from "../../../../components/controls/AddCustomField/AddCustomField.types";
|
||||
import { CustomMethodCardWizardView } from "./CustomMethodCardWizard.view";
|
||||
import type { CustomMethodCardWizardProps } from "./CustomMethodCardWizard.types";
|
||||
|
||||
/**
|
||||
* Shared 3-step add-custom-method-card flow (Figma Modal / Create — nodes
|
||||
* `20066:14748`, `20094:48551`, `20066:14361`).
|
||||
*/
|
||||
const CustomMethodCardWizardContainer = memo<CustomMethodCardWizardProps>(
|
||||
({ isOpen, onClose, onFinalize }) => {
|
||||
const m = useMessages();
|
||||
const t = useTranslation("common");
|
||||
const w = m.create.customRule.customMethodCardWizard;
|
||||
|
||||
const copy = useMemo(
|
||||
() => ({
|
||||
step1: w.steps["1"],
|
||||
step2: w.steps["2"],
|
||||
step3: w.steps["3"],
|
||||
footerFinalize: w.footer.finalize,
|
||||
fieldModals: w.fieldModals,
|
||||
}),
|
||||
[w.fieldModals, w.footer.finalize, w.steps],
|
||||
);
|
||||
|
||||
const fieldBodiesCopy = useMemo(
|
||||
() => ({
|
||||
requiredHint: copy.fieldModals.requiredHint,
|
||||
text: copy.fieldModals.text,
|
||||
badges: copy.fieldModals.badges,
|
||||
upload: copy.fieldModals.upload,
|
||||
proportion: copy.fieldModals.proportion,
|
||||
}),
|
||||
[copy.fieldModals],
|
||||
);
|
||||
|
||||
const [wizardStep, setWizardStep] = useState<1 | 2 | 3>(1);
|
||||
const [policyTitle, setPolicyTitle] = useState("");
|
||||
const [policyDescription, setPolicyDescription] = useState("");
|
||||
const [addFieldExpanded, setAddFieldExpanded] = useState(false);
|
||||
const [fieldTypeModal, setFieldTypeModal] =
|
||||
useState<AddCustomFieldType | null>(null);
|
||||
const [draftFieldBlocks, setDraftFieldBlocks] = useState<
|
||||
CustomMethodCardFieldBlock[]
|
||||
>([]);
|
||||
|
||||
const [textBlockTitle, setTextBlockTitle] = useState("");
|
||||
const [textPlaceholderBody, setTextPlaceholderBody] = useState("");
|
||||
const [badgeBlockTitle, setBadgeBlockTitle] = useState("");
|
||||
const [badgeOptions, setBadgeOptions] = useState<string[]>([]);
|
||||
const [uploadBlockTitle, setUploadBlockTitle] = useState("");
|
||||
const [uploadFileName, setUploadFileName] = useState<string | undefined>(
|
||||
undefined,
|
||||
);
|
||||
const [proportionBlockTitle, setProportionBlockTitle] = useState("");
|
||||
const [proportionDefault, setProportionDefault] = useState(50);
|
||||
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const resetFieldTypeDrafts = useCallback(() => {
|
||||
setTextBlockTitle("");
|
||||
setTextPlaceholderBody("");
|
||||
setBadgeBlockTitle("");
|
||||
setBadgeOptions([]);
|
||||
setUploadBlockTitle("");
|
||||
setUploadFileName(undefined);
|
||||
setProportionBlockTitle("");
|
||||
setProportionDefault(50);
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = "";
|
||||
}
|
||||
}, []);
|
||||
|
||||
const reset = useCallback(() => {
|
||||
setWizardStep(1);
|
||||
setPolicyTitle("");
|
||||
setPolicyDescription("");
|
||||
setAddFieldExpanded(false);
|
||||
setFieldTypeModal(null);
|
||||
setDraftFieldBlocks([]);
|
||||
resetFieldTypeDrafts();
|
||||
}, [resetFieldTypeDrafts]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
reset();
|
||||
}
|
||||
}, [isOpen, reset]);
|
||||
|
||||
const dismiss = useCallback(() => {
|
||||
reset();
|
||||
onClose();
|
||||
}, [onClose, reset]);
|
||||
|
||||
const titleTrim = policyTitle.trim();
|
||||
const descriptionTrim = policyDescription.trim();
|
||||
|
||||
const stepValid = useMemo(() => {
|
||||
const titleOk =
|
||||
titleTrim.length > 0 &&
|
||||
titleTrim.length <= CUSTOM_METHOD_CARD_WIZARD_MAX_FIELD_CHARS;
|
||||
const descriptionOk =
|
||||
descriptionTrim.length > 0 &&
|
||||
descriptionTrim.length <= CUSTOM_METHOD_CARD_WIZARD_MAX_FIELD_CHARS;
|
||||
if (wizardStep === 1) return titleOk;
|
||||
if (wizardStep === 2) return descriptionOk;
|
||||
return titleOk && descriptionOk;
|
||||
}, [
|
||||
descriptionTrim.length,
|
||||
titleTrim.length,
|
||||
wizardStep,
|
||||
]);
|
||||
|
||||
const fieldModalStepValid = useMemo(() => {
|
||||
if (!fieldTypeModal) return false;
|
||||
if (fieldTypeModal === "text") {
|
||||
const t0 = textBlockTitle.trim();
|
||||
return (
|
||||
t0.length > 0 &&
|
||||
t0.length <= CUSTOM_METHOD_CARD_WIZARD_MAX_FIELD_CHARS
|
||||
);
|
||||
}
|
||||
if (fieldTypeModal === "badges") {
|
||||
const t0 = badgeBlockTitle.trim();
|
||||
return (
|
||||
t0.length > 0 &&
|
||||
t0.length <= CUSTOM_METHOD_CARD_WIZARD_MAX_FIELD_CHARS
|
||||
);
|
||||
}
|
||||
if (fieldTypeModal === "upload") {
|
||||
const t0 = uploadBlockTitle.trim();
|
||||
return (
|
||||
t0.length > 0 &&
|
||||
t0.length <= CUSTOM_METHOD_CARD_WIZARD_MAX_FIELD_CHARS
|
||||
);
|
||||
}
|
||||
const t0 = proportionBlockTitle.trim();
|
||||
return (
|
||||
t0.length > 0 &&
|
||||
t0.length <= CUSTOM_METHOD_CARD_WIZARD_MAX_FIELD_CHARS &&
|
||||
proportionDefault >= 1 &&
|
||||
proportionDefault <= 100
|
||||
);
|
||||
}, [
|
||||
badgeBlockTitle,
|
||||
fieldTypeModal,
|
||||
proportionBlockTitle,
|
||||
proportionDefault,
|
||||
textBlockTitle,
|
||||
uploadBlockTitle,
|
||||
]);
|
||||
|
||||
const headerTitle =
|
||||
wizardStep === 1
|
||||
? copy.step1.title
|
||||
: wizardStep === 2
|
||||
? copy.step2.title
|
||||
: copy.step3.title;
|
||||
|
||||
const headerDescription =
|
||||
wizardStep === 1
|
||||
? copy.step1.description
|
||||
: wizardStep === 2
|
||||
? copy.step2.description
|
||||
: copy.step3.description;
|
||||
|
||||
const fieldModalHeader = fieldTypeModal
|
||||
? copy.fieldModals[fieldTypeModal]
|
||||
: null;
|
||||
|
||||
const shellTitle = fieldModalHeader?.title ?? headerTitle;
|
||||
const shellDescription = fieldModalHeader?.description ?? headerDescription;
|
||||
|
||||
const nextLabel = fieldTypeModal
|
||||
? copy.fieldModals.addField
|
||||
: wizardStep === 3
|
||||
? copy.footerFinalize
|
||||
: t("buttons.next");
|
||||
|
||||
const shellNextDisabled = fieldTypeModal
|
||||
? !fieldModalStepValid
|
||||
: !stepValid;
|
||||
|
||||
const handleShellClose = useCallback(() => {
|
||||
if (fieldTypeModal) {
|
||||
setFieldTypeModal(null);
|
||||
return;
|
||||
}
|
||||
dismiss();
|
||||
}, [dismiss, fieldTypeModal]);
|
||||
|
||||
const handleBack = useCallback(() => {
|
||||
if (fieldTypeModal) {
|
||||
setFieldTypeModal(null);
|
||||
return;
|
||||
}
|
||||
if (wizardStep === 1) {
|
||||
dismiss();
|
||||
return;
|
||||
}
|
||||
setWizardStep((s) => (s === 2 ? 1 : 2));
|
||||
}, [dismiss, fieldTypeModal, wizardStep]);
|
||||
|
||||
const handleSelectFieldType = useCallback((ft: AddCustomFieldType) => {
|
||||
resetFieldTypeDrafts();
|
||||
setFieldTypeModal(ft);
|
||||
}, [resetFieldTypeDrafts]);
|
||||
|
||||
const handleFileChosen = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
setUploadFileName(file?.name);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const handleBadgeAddOption = useCallback((label: string) => {
|
||||
setBadgeOptions((prev) =>
|
||||
prev.includes(label) ? prev : [...prev, label],
|
||||
);
|
||||
}, []);
|
||||
|
||||
const appendFieldBlock = useCallback(() => {
|
||||
if (!fieldTypeModal || !fieldModalStepValid) return;
|
||||
const id = crypto.randomUUID();
|
||||
let block: CustomMethodCardFieldBlock;
|
||||
switch (fieldTypeModal) {
|
||||
case "text":
|
||||
block = {
|
||||
kind: "text",
|
||||
id,
|
||||
blockTitle: textBlockTitle.trim(),
|
||||
placeholderText: textPlaceholderBody,
|
||||
};
|
||||
break;
|
||||
case "badges":
|
||||
block = {
|
||||
kind: "badges",
|
||||
id,
|
||||
blockTitle: badgeBlockTitle.trim(),
|
||||
options: [...badgeOptions],
|
||||
};
|
||||
break;
|
||||
case "upload":
|
||||
block = {
|
||||
kind: "upload",
|
||||
id,
|
||||
blockTitle: uploadBlockTitle.trim(),
|
||||
fileName: uploadFileName,
|
||||
};
|
||||
break;
|
||||
default:
|
||||
block = {
|
||||
kind: "proportion",
|
||||
id,
|
||||
blockTitle: proportionBlockTitle.trim(),
|
||||
defaultPercent: proportionDefault,
|
||||
};
|
||||
}
|
||||
setDraftFieldBlocks((prev) => [...prev, block]);
|
||||
setFieldTypeModal(null);
|
||||
}, [
|
||||
badgeBlockTitle,
|
||||
badgeOptions,
|
||||
fieldModalStepValid,
|
||||
fieldTypeModal,
|
||||
proportionBlockTitle,
|
||||
proportionDefault,
|
||||
textBlockTitle,
|
||||
textPlaceholderBody,
|
||||
uploadBlockTitle,
|
||||
uploadFileName,
|
||||
]);
|
||||
|
||||
const handleNext = useCallback(() => {
|
||||
if (fieldTypeModal) {
|
||||
appendFieldBlock();
|
||||
return;
|
||||
}
|
||||
if (!stepValid) return;
|
||||
if (wizardStep === 3) {
|
||||
onFinalize({
|
||||
title: titleTrim,
|
||||
description: descriptionTrim,
|
||||
fieldBlocks: draftFieldBlocks,
|
||||
});
|
||||
dismiss();
|
||||
return;
|
||||
}
|
||||
setWizardStep((s) => (s === 1 ? 2 : 3));
|
||||
}, [
|
||||
appendFieldBlock,
|
||||
descriptionTrim,
|
||||
dismiss,
|
||||
draftFieldBlocks,
|
||||
fieldTypeModal,
|
||||
onFinalize,
|
||||
stepValid,
|
||||
titleTrim,
|
||||
wizardStep,
|
||||
]);
|
||||
|
||||
return (
|
||||
<CustomMethodCardWizardView
|
||||
isOpen={isOpen}
|
||||
onDismiss={handleShellClose}
|
||||
wizardStep={wizardStep}
|
||||
title={shellTitle}
|
||||
description={shellDescription}
|
||||
policyTitle={policyTitle}
|
||||
policyDescription={policyDescription}
|
||||
addFieldExpanded={addFieldExpanded}
|
||||
copy={copy}
|
||||
maxChars={CUSTOM_METHOD_CARD_WIZARD_MAX_FIELD_CHARS}
|
||||
onPolicyTitleChange={setPolicyTitle}
|
||||
onPolicyDescriptionChange={setPolicyDescription}
|
||||
onPressAddCustomField={() => setAddFieldExpanded(true)}
|
||||
onSelectFieldType={handleSelectFieldType}
|
||||
fieldTypeModal={fieldTypeModal}
|
||||
fieldBodiesCopy={fieldBodiesCopy}
|
||||
fieldBodiesProps={{
|
||||
textBlockTitle,
|
||||
textPlaceholderBody,
|
||||
onTextBlockTitleChange: setTextBlockTitle,
|
||||
onTextPlaceholderBodyChange: setTextPlaceholderBody,
|
||||
badgeBlockTitle,
|
||||
badgeOptions,
|
||||
onBadgeBlockTitleChange: setBadgeBlockTitle,
|
||||
onBadgeAddOption: handleBadgeAddOption,
|
||||
uploadBlockTitle,
|
||||
onUploadBlockTitleChange: setUploadBlockTitle,
|
||||
fileInputRef,
|
||||
onFileChosen: handleFileChosen,
|
||||
proportionBlockTitle,
|
||||
proportionDefault,
|
||||
onProportionBlockTitleChange: setProportionBlockTitle,
|
||||
onProportionDefaultChange: setProportionDefault,
|
||||
}}
|
||||
nextDisabled={shellNextDisabled}
|
||||
nextLabel={nextLabel}
|
||||
showBackButton
|
||||
onBack={handleBack}
|
||||
onNext={handleNext}
|
||||
stepper={!fieldTypeModal}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
CustomMethodCardWizardContainer.displayName = "CustomMethodCardWizard";
|
||||
|
||||
export default CustomMethodCardWizardContainer;
|
||||
@@ -0,0 +1,120 @@
|
||||
import type { RefObject } from "react";
|
||||
import type { AddCustomFieldType } from "../../../../components/controls/AddCustomField/AddCustomField.types";
|
||||
import type { CustomMethodCardFieldBlock } from "../../../../../lib/create/customMethodCardFieldBlocks";
|
||||
|
||||
export interface CustomMethodCardWizardFieldBodiesCopy {
|
||||
requiredHint: string;
|
||||
text: {
|
||||
blockTitleLabel: string;
|
||||
blockTitlePlaceholder: string;
|
||||
placeholderLabel: string;
|
||||
placeholderFieldPlaceholder: string;
|
||||
};
|
||||
badges: {
|
||||
blockTitleLabel: string;
|
||||
blockTitlePlaceholder: string;
|
||||
optionsLabel: string;
|
||||
addOptionLabel: string;
|
||||
};
|
||||
upload: {
|
||||
blockTitleLabel: string;
|
||||
blockTitlePlaceholder: string;
|
||||
uploadFileInputAriaLabel: string;
|
||||
uploadHint: string;
|
||||
};
|
||||
proportion: {
|
||||
blockTitleLabel: string;
|
||||
blockTitlePlaceholder: string;
|
||||
defaultLabel: string;
|
||||
decrementAriaLabel: string;
|
||||
incrementAriaLabel: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface CustomMethodCardWizardCopy {
|
||||
step1: { title: string; description: string; fieldPlaceholder: string };
|
||||
step2: { title: string; description: string; fieldPlaceholder: string };
|
||||
step3: { title: string; description: string };
|
||||
footerFinalize: string;
|
||||
fieldModals: {
|
||||
addField: string;
|
||||
requiredHint: string;
|
||||
text: CustomMethodCardWizardFieldBodiesCopy["text"] & {
|
||||
title: string;
|
||||
description: string;
|
||||
};
|
||||
badges: CustomMethodCardWizardFieldBodiesCopy["badges"] & {
|
||||
title: string;
|
||||
description: string;
|
||||
};
|
||||
upload: CustomMethodCardWizardFieldBodiesCopy["upload"] & {
|
||||
title: string;
|
||||
description: string;
|
||||
};
|
||||
proportion: CustomMethodCardWizardFieldBodiesCopy["proportion"] & {
|
||||
title: string;
|
||||
description: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface CustomMethodCardWizardProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
/** Called when the user completes step 3; parent assigns id and persists state. */
|
||||
onFinalize: (payload: {
|
||||
title: string;
|
||||
description: string;
|
||||
fieldBlocks: CustomMethodCardFieldBlock[];
|
||||
}) => void;
|
||||
}
|
||||
|
||||
export interface CustomMethodCardWizardFieldBodiesViewProps {
|
||||
fieldType: AddCustomFieldType;
|
||||
copy: CustomMethodCardWizardFieldBodiesCopy;
|
||||
textBlockTitle: string;
|
||||
textPlaceholderBody: string;
|
||||
onTextBlockTitleChange: (_v: string) => void;
|
||||
onTextPlaceholderBodyChange: (_v: string) => void;
|
||||
badgeBlockTitle: string;
|
||||
badgeOptions: string[];
|
||||
onBadgeBlockTitleChange: (_v: string) => void;
|
||||
onBadgeAddOption: (_v: string) => void;
|
||||
uploadBlockTitle: string;
|
||||
onUploadBlockTitleChange: (_v: string) => void;
|
||||
fileInputRef: RefObject<HTMLInputElement | null>;
|
||||
onFileChosen: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
proportionBlockTitle: string;
|
||||
proportionDefault: number;
|
||||
onProportionBlockTitleChange: (_v: string) => void;
|
||||
onProportionDefaultChange: (_v: number) => void;
|
||||
}
|
||||
|
||||
export interface CustomMethodCardWizardViewProps {
|
||||
isOpen: boolean;
|
||||
onDismiss: () => void;
|
||||
wizardStep: 1 | 2 | 3;
|
||||
title: string;
|
||||
description: string;
|
||||
policyTitle: string;
|
||||
policyDescription: string;
|
||||
addFieldExpanded: boolean;
|
||||
copy: CustomMethodCardWizardCopy;
|
||||
maxChars: number;
|
||||
onPolicyTitleChange: (v: string) => void;
|
||||
onPolicyDescriptionChange: (v: string) => void;
|
||||
onPressAddCustomField: () => void;
|
||||
onSelectFieldType: (t: AddCustomFieldType) => void;
|
||||
fieldTypeModal: AddCustomFieldType | null;
|
||||
fieldBodiesCopy: CustomMethodCardWizardFieldBodiesCopy;
|
||||
fieldBodiesProps: Omit<
|
||||
CustomMethodCardWizardFieldBodiesViewProps,
|
||||
"fieldType" | "copy"
|
||||
>;
|
||||
nextDisabled: boolean;
|
||||
nextLabel: string;
|
||||
showBackButton: boolean;
|
||||
onBack: () => void;
|
||||
onNext: () => void;
|
||||
stepper: boolean;
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import Create from "../../../../components/modals/Create";
|
||||
import InputWithCounter from "../../../../components/controls/InputWithCounter";
|
||||
import TextArea from "../../../../components/controls/TextArea";
|
||||
import AddCustomField from "../../../../components/controls/AddCustomField";
|
||||
import { CustomMethodCardWizardFieldBodiesView } from "./CustomMethodCardWizardFieldBodies.view";
|
||||
import type { CustomMethodCardWizardViewProps } from "./CustomMethodCardWizard.types";
|
||||
|
||||
function CustomMethodCardWizardViewComponent({
|
||||
isOpen,
|
||||
onDismiss,
|
||||
wizardStep,
|
||||
title,
|
||||
description,
|
||||
policyTitle,
|
||||
policyDescription,
|
||||
addFieldExpanded,
|
||||
copy,
|
||||
maxChars,
|
||||
onPolicyTitleChange,
|
||||
onPolicyDescriptionChange,
|
||||
onPressAddCustomField,
|
||||
onSelectFieldType,
|
||||
fieldTypeModal,
|
||||
fieldBodiesCopy,
|
||||
fieldBodiesProps,
|
||||
nextDisabled,
|
||||
nextLabel,
|
||||
showBackButton,
|
||||
onBack,
|
||||
onNext,
|
||||
stepper,
|
||||
}: CustomMethodCardWizardViewProps) {
|
||||
return (
|
||||
<Create
|
||||
isOpen={isOpen}
|
||||
onClose={onDismiss}
|
||||
title={title}
|
||||
description={description}
|
||||
showBackButton={showBackButton}
|
||||
showNextButton
|
||||
onBack={onBack}
|
||||
onNext={onNext}
|
||||
nextButtonText={nextLabel}
|
||||
nextButtonDisabled={nextDisabled}
|
||||
currentStep={wizardStep}
|
||||
totalSteps={3}
|
||||
stepper={stepper}
|
||||
backdropVariant="blurredYellow"
|
||||
>
|
||||
{fieldTypeModal ? (
|
||||
<CustomMethodCardWizardFieldBodiesView
|
||||
fieldType={fieldTypeModal}
|
||||
copy={fieldBodiesCopy}
|
||||
{...fieldBodiesProps}
|
||||
/>
|
||||
) : null}
|
||||
{!fieldTypeModal && wizardStep === 1 ? (
|
||||
<InputWithCounter
|
||||
placeholder={copy.step1.fieldPlaceholder}
|
||||
value={policyTitle}
|
||||
onChange={onPolicyTitleChange}
|
||||
maxLength={maxChars}
|
||||
/>
|
||||
) : null}
|
||||
{!fieldTypeModal && wizardStep === 2 ? (
|
||||
<TextArea
|
||||
appearance="default"
|
||||
formHeader={false}
|
||||
placeholder={copy.step2.fieldPlaceholder}
|
||||
value={policyDescription}
|
||||
maxLength={maxChars}
|
||||
onChange={(e) => onPolicyDescriptionChange(e.target.value)}
|
||||
textHint={`${policyDescription.length}/${maxChars}`}
|
||||
className="w-full"
|
||||
rows={4}
|
||||
/>
|
||||
) : null}
|
||||
{!fieldTypeModal && wizardStep === 3 ? (
|
||||
<AddCustomField
|
||||
active={addFieldExpanded}
|
||||
onPressAdd={onPressAddCustomField}
|
||||
onSelectFieldType={onSelectFieldType}
|
||||
/>
|
||||
) : null}
|
||||
</Create>
|
||||
);
|
||||
}
|
||||
|
||||
export const CustomMethodCardWizardView = memo(
|
||||
CustomMethodCardWizardViewComponent,
|
||||
);
|
||||
CustomMethodCardWizardView.displayName = "CustomMethodCardWizardView";
|
||||
+161
@@ -0,0 +1,161 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import InputWithCounter from "../../../../components/controls/InputWithCounter";
|
||||
import TextArea from "../../../../components/controls/TextArea";
|
||||
import TextInput from "../../../../components/controls/TextInput";
|
||||
import Upload from "../../../../components/controls/Upload";
|
||||
import IncrementerBlock from "../../../../components/controls/IncrementerBlock";
|
||||
import InputLabel from "../../../../components/type/InputLabel";
|
||||
import ApplicableScopeField from "../ApplicableScopeField";
|
||||
import { CUSTOM_METHOD_CARD_WIZARD_MAX_FIELD_CHARS } from "../../../../../lib/create/customMethodCardWizardConstants";
|
||||
import type { CustomMethodCardWizardFieldBodiesViewProps } from "./CustomMethodCardWizard.types";
|
||||
|
||||
const TEXT_PLACEHOLDER_MAX = 8000;
|
||||
|
||||
function CustomMethodCardWizardFieldBodiesViewComponent({
|
||||
fieldType,
|
||||
copy,
|
||||
textBlockTitle,
|
||||
textPlaceholderBody,
|
||||
onTextBlockTitleChange,
|
||||
onTextPlaceholderBodyChange,
|
||||
badgeBlockTitle,
|
||||
badgeOptions,
|
||||
onBadgeBlockTitleChange,
|
||||
onBadgeAddOption,
|
||||
uploadBlockTitle,
|
||||
onUploadBlockTitleChange,
|
||||
fileInputRef,
|
||||
onFileChosen,
|
||||
proportionBlockTitle,
|
||||
proportionDefault,
|
||||
onProportionBlockTitleChange,
|
||||
onProportionDefaultChange,
|
||||
}: CustomMethodCardWizardFieldBodiesViewProps) {
|
||||
if (fieldType === "text") {
|
||||
return (
|
||||
<div className="flex flex-col gap-[var(--spacing-scale-024)]">
|
||||
<InputWithCounter
|
||||
label={copy.text.blockTitleLabel}
|
||||
placeholder={copy.text.blockTitlePlaceholder}
|
||||
value={textBlockTitle}
|
||||
onChange={onTextBlockTitleChange}
|
||||
maxLength={CUSTOM_METHOD_CARD_WIZARD_MAX_FIELD_CHARS}
|
||||
showHelpIcon
|
||||
/>
|
||||
<div className="flex flex-col gap-2">
|
||||
<InputLabel
|
||||
label={copy.text.placeholderLabel}
|
||||
helpIcon
|
||||
size="s"
|
||||
palette="default"
|
||||
/>
|
||||
<TextArea
|
||||
formHeader={false}
|
||||
appearance="embedded"
|
||||
value={textPlaceholderBody}
|
||||
onChange={(e) => onTextPlaceholderBodyChange(e.target.value)}
|
||||
maxLength={TEXT_PLACEHOLDER_MAX}
|
||||
placeholder={copy.text.placeholderFieldPlaceholder}
|
||||
textHint={`${textPlaceholderBody.length}/${TEXT_PLACEHOLDER_MAX}`}
|
||||
className="w-full"
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (fieldType === "badges") {
|
||||
return (
|
||||
<div className="flex flex-col gap-[var(--spacing-scale-024)]">
|
||||
<div className="flex flex-col gap-2">
|
||||
<InputLabel
|
||||
label={copy.badges.blockTitleLabel}
|
||||
helpIcon
|
||||
helperText={copy.requiredHint}
|
||||
size="s"
|
||||
palette="default"
|
||||
/>
|
||||
<TextInput
|
||||
formHeader={false}
|
||||
placeholder={copy.badges.blockTitlePlaceholder}
|
||||
value={badgeBlockTitle}
|
||||
onChange={(e) => onBadgeBlockTitleChange(e.target.value)}
|
||||
maxLength={CUSTOM_METHOD_CARD_WIZARD_MAX_FIELD_CHARS}
|
||||
showHelpIcon={false}
|
||||
/>
|
||||
</div>
|
||||
<ApplicableScopeField
|
||||
label={copy.badges.optionsLabel}
|
||||
addLabel={copy.badges.addOptionLabel}
|
||||
scopes={badgeOptions}
|
||||
selectedScopes={badgeOptions}
|
||||
onToggleScope={() => {
|
||||
/* product: all badge options stay selected */
|
||||
}}
|
||||
onAddScope={onBadgeAddOption}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (fieldType === "upload") {
|
||||
return (
|
||||
<div className="flex flex-col gap-[var(--spacing-scale-024)]">
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
className="sr-only"
|
||||
tabIndex={-1}
|
||||
aria-label={copy.upload.uploadFileInputAriaLabel}
|
||||
onChange={onFileChosen}
|
||||
/>
|
||||
<InputWithCounter
|
||||
label={copy.upload.blockTitleLabel}
|
||||
placeholder={copy.upload.blockTitlePlaceholder}
|
||||
value={uploadBlockTitle}
|
||||
onChange={onUploadBlockTitleChange}
|
||||
maxLength={CUSTOM_METHOD_CARD_WIZARD_MAX_FIELD_CHARS}
|
||||
showHelpIcon
|
||||
/>
|
||||
<Upload
|
||||
hintText={copy.upload.uploadHint}
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-[var(--spacing-scale-024)]">
|
||||
<InputWithCounter
|
||||
label={copy.proportion.blockTitleLabel}
|
||||
placeholder={copy.proportion.blockTitlePlaceholder}
|
||||
value={proportionBlockTitle}
|
||||
onChange={onProportionBlockTitleChange}
|
||||
maxLength={CUSTOM_METHOD_CARD_WIZARD_MAX_FIELD_CHARS}
|
||||
showHelpIcon
|
||||
/>
|
||||
<IncrementerBlock
|
||||
label={copy.proportion.defaultLabel}
|
||||
value={proportionDefault}
|
||||
min={1}
|
||||
max={100}
|
||||
step={1}
|
||||
onChange={onProportionDefaultChange}
|
||||
formatValue={(v) => `${v}%`}
|
||||
decrementAriaLabel={copy.proportion.decrementAriaLabel}
|
||||
incrementAriaLabel={copy.proportion.incrementAriaLabel}
|
||||
blockClassName="w-full"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const CustomMethodCardWizardFieldBodiesView = memo(
|
||||
CustomMethodCardWizardFieldBodiesViewComponent,
|
||||
);
|
||||
CustomMethodCardWizardFieldBodiesView.displayName =
|
||||
"CustomMethodCardWizardFieldBodiesView";
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default } from "./CustomMethodCardWizard.container";
|
||||
export type { CustomMethodCardWizardProps } from "./CustomMethodCardWizard.types";
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
DecisionApproachEditFields,
|
||||
MembershipMethodEditFields,
|
||||
} from "./methodEditFields";
|
||||
import CustomMethodCardModalBody from "./CustomMethodCardModalBody";
|
||||
import {
|
||||
communicationPresetFor,
|
||||
conflictManagementPresetFor,
|
||||
@@ -41,6 +42,8 @@ import {
|
||||
decisionApproachPresetFor,
|
||||
membershipPresetFor,
|
||||
} from "../../../../lib/create/finalReviewChipPresets";
|
||||
import { isCustomMethodCardId } from "../../../../lib/create/isCustomMethodCardId";
|
||||
import type { CustomMethodCardFieldBlock } from "../../../../lib/create/customMethodCardFieldBlocks";
|
||||
import type {
|
||||
CommunicationMethodDetailEntry,
|
||||
ConflictManagementDetailEntry,
|
||||
@@ -66,21 +69,25 @@ export type FinalReviewChipEditPatch =
|
||||
groupKey: "communication";
|
||||
overrideKey: string;
|
||||
value: CommunicationMethodDetailEntry;
|
||||
customMethodCardFieldBlocks?: CustomMethodCardFieldBlock[];
|
||||
}
|
||||
| {
|
||||
groupKey: "membership";
|
||||
overrideKey: string;
|
||||
value: MembershipMethodDetailEntry;
|
||||
customMethodCardFieldBlocks?: CustomMethodCardFieldBlock[];
|
||||
}
|
||||
| {
|
||||
groupKey: "decisionApproaches";
|
||||
overrideKey: string;
|
||||
value: DecisionApproachDetailEntry;
|
||||
customMethodCardFieldBlocks?: CustomMethodCardFieldBlock[];
|
||||
}
|
||||
| {
|
||||
groupKey: "conflictManagement";
|
||||
overrideKey: string;
|
||||
value: ConflictManagementDetailEntry;
|
||||
customMethodCardFieldBlocks?: CustomMethodCardFieldBlock[];
|
||||
};
|
||||
|
||||
export interface FinalReviewChipEditModalProps {
|
||||
@@ -128,12 +135,16 @@ export function FinalReviewChipEditModal({
|
||||
);
|
||||
|
||||
const [draft, setDraft] = useState<Draft | null>(null);
|
||||
const [fieldBlocksDraft, setFieldBlocksDraft] = useState<
|
||||
CustomMethodCardFieldBlock[] | null
|
||||
>(null);
|
||||
/**
|
||||
* JSON-stringified seed used for the cheap dirty check. Re-captured on
|
||||
* every (re)open so reopening a chip after a save shows Save-disabled
|
||||
* again until the user makes a fresh edit.
|
||||
*/
|
||||
const initialSnapshotRef = useRef<string>("");
|
||||
const initialFieldBlocksSnapshotRef = useRef<string>("");
|
||||
const seededTargetRef = useRef<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -144,6 +155,18 @@ export function FinalReviewChipEditModal({
|
||||
const seed = seedDraftForTarget(target, state);
|
||||
setDraft(seed);
|
||||
initialSnapshotRef.current = JSON.stringify(seed.value);
|
||||
if (
|
||||
target.groupKey !== "coreValues" &&
|
||||
isCustomMethodCardId(target.overrideKey, state.customMethodCardMetaById)
|
||||
) {
|
||||
const blocks =
|
||||
state.customMethodCardFieldBlocksById?.[target.overrideKey] ?? [];
|
||||
setFieldBlocksDraft(blocks);
|
||||
initialFieldBlocksSnapshotRef.current = JSON.stringify(blocks);
|
||||
} else {
|
||||
setFieldBlocksDraft(null);
|
||||
initialFieldBlocksSnapshotRef.current = "";
|
||||
}
|
||||
seededTargetRef.current = targetKey;
|
||||
}, [isOpen, target, state]);
|
||||
|
||||
@@ -152,24 +175,86 @@ export function FinalReviewChipEditModal({
|
||||
}, [isOpen]);
|
||||
|
||||
const isDirty = useMemo(() => {
|
||||
if (!draft) return false;
|
||||
return JSON.stringify(draft.value) !== initialSnapshotRef.current;
|
||||
}, [draft]);
|
||||
if (!draft || !target) return false;
|
||||
const valueDirty =
|
||||
JSON.stringify(draft.value) !== initialSnapshotRef.current;
|
||||
const customMethod =
|
||||
target.groupKey !== "coreValues" &&
|
||||
isCustomMethodCardId(target.overrideKey, state.customMethodCardMetaById);
|
||||
const blocksDirty =
|
||||
customMethod &&
|
||||
fieldBlocksDraft !== null &&
|
||||
JSON.stringify(fieldBlocksDraft) !==
|
||||
initialFieldBlocksSnapshotRef.current;
|
||||
return valueDirty || Boolean(blocksDirty);
|
||||
}, [draft, target, state.customMethodCardMetaById, fieldBlocksDraft]);
|
||||
|
||||
const handleSave = () => {
|
||||
if (!target || !draft || !isDirty) return;
|
||||
onSave({
|
||||
groupKey: draft.groupKey,
|
||||
overrideKey: target.overrideKey,
|
||||
value: draft.value,
|
||||
} as FinalReviewChipEditPatch);
|
||||
const { overrideKey } = target;
|
||||
const customBlocks =
|
||||
draft.groupKey !== "coreValues" &&
|
||||
isCustomMethodCardId(overrideKey, state.customMethodCardMetaById) &&
|
||||
fieldBlocksDraft !== null
|
||||
? fieldBlocksDraft
|
||||
: undefined;
|
||||
|
||||
switch (draft.groupKey) {
|
||||
case "coreValues":
|
||||
onSave({
|
||||
groupKey: "coreValues",
|
||||
overrideKey,
|
||||
value: draft.value,
|
||||
});
|
||||
break;
|
||||
case "communication":
|
||||
onSave({
|
||||
groupKey: "communication",
|
||||
overrideKey,
|
||||
value: draft.value,
|
||||
...(customBlocks !== undefined
|
||||
? { customMethodCardFieldBlocks: customBlocks }
|
||||
: {}),
|
||||
});
|
||||
break;
|
||||
case "membership":
|
||||
onSave({
|
||||
groupKey: "membership",
|
||||
overrideKey,
|
||||
value: draft.value,
|
||||
...(customBlocks !== undefined
|
||||
? { customMethodCardFieldBlocks: customBlocks }
|
||||
: {}),
|
||||
});
|
||||
break;
|
||||
case "decisionApproaches":
|
||||
onSave({
|
||||
groupKey: "decisionApproaches",
|
||||
overrideKey,
|
||||
value: draft.value,
|
||||
...(customBlocks !== undefined
|
||||
? { customMethodCardFieldBlocks: customBlocks }
|
||||
: {}),
|
||||
});
|
||||
break;
|
||||
case "conflictManagement":
|
||||
onSave({
|
||||
groupKey: "conflictManagement",
|
||||
overrideKey,
|
||||
value: draft.value,
|
||||
...(customBlocks !== undefined
|
||||
? { customMethodCardFieldBlocks: customBlocks }
|
||||
: {}),
|
||||
});
|
||||
break;
|
||||
}
|
||||
onClose();
|
||||
};
|
||||
|
||||
const subtitle = useMemo(() => {
|
||||
if (!target) return "";
|
||||
return subtitleForTarget(target, { tCv, tComm, tMem, tDa, tCm });
|
||||
}, [target, tCv, tComm, tMem, tDa, tCm]);
|
||||
return subtitleForTarget(target, { tCv, tComm, tMem, tDa, tCm }, state.customMethodCardMetaById);
|
||||
}, [target, tCv, tComm, tMem, tDa, tCm, state.customMethodCardMetaById]);
|
||||
|
||||
return (
|
||||
<Create
|
||||
@@ -200,36 +285,84 @@ export function FinalReviewChipEditModal({
|
||||
onChange={(value) => setDraft({ groupKey: "coreValues", value })}
|
||||
/>
|
||||
)}
|
||||
{draft?.groupKey === "communication" && (
|
||||
<CommunicationMethodEditFields
|
||||
value={draft.value}
|
||||
onChange={(value) =>
|
||||
setDraft({ groupKey: "communication", value })
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{draft?.groupKey === "membership" && (
|
||||
<MembershipMethodEditFields
|
||||
value={draft.value}
|
||||
onChange={(value) => setDraft({ groupKey: "membership", value })}
|
||||
/>
|
||||
)}
|
||||
{draft?.groupKey === "decisionApproaches" && (
|
||||
<DecisionApproachEditFields
|
||||
value={draft.value}
|
||||
onChange={(value) =>
|
||||
setDraft({ groupKey: "decisionApproaches", value })
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{draft?.groupKey === "conflictManagement" && (
|
||||
<ConflictManagementEditFields
|
||||
value={draft.value}
|
||||
onChange={(value) =>
|
||||
setDraft({ groupKey: "conflictManagement", value })
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{draft?.groupKey === "communication" &&
|
||||
(target &&
|
||||
isCustomMethodCardId(
|
||||
target.overrideKey,
|
||||
state.customMethodCardMetaById,
|
||||
) ? (
|
||||
<CustomMethodCardModalBody
|
||||
cardId={target.overrideKey}
|
||||
blocksById={state.customMethodCardFieldBlocksById}
|
||||
blocksOverride={fieldBlocksDraft}
|
||||
onFieldBlocksChange={setFieldBlocksDraft}
|
||||
/>
|
||||
) : (
|
||||
<CommunicationMethodEditFields
|
||||
value={draft.value}
|
||||
onChange={(value) =>
|
||||
setDraft({ groupKey: "communication", value })
|
||||
}
|
||||
/>
|
||||
))}
|
||||
{draft?.groupKey === "membership" &&
|
||||
(target &&
|
||||
isCustomMethodCardId(
|
||||
target.overrideKey,
|
||||
state.customMethodCardMetaById,
|
||||
) ? (
|
||||
<CustomMethodCardModalBody
|
||||
cardId={target.overrideKey}
|
||||
blocksById={state.customMethodCardFieldBlocksById}
|
||||
blocksOverride={fieldBlocksDraft}
|
||||
onFieldBlocksChange={setFieldBlocksDraft}
|
||||
/>
|
||||
) : (
|
||||
<MembershipMethodEditFields
|
||||
value={draft.value}
|
||||
onChange={(value) => setDraft({ groupKey: "membership", value })}
|
||||
/>
|
||||
))}
|
||||
{draft?.groupKey === "decisionApproaches" &&
|
||||
(target &&
|
||||
isCustomMethodCardId(
|
||||
target.overrideKey,
|
||||
state.customMethodCardMetaById,
|
||||
) ? (
|
||||
<CustomMethodCardModalBody
|
||||
cardId={target.overrideKey}
|
||||
blocksById={state.customMethodCardFieldBlocksById}
|
||||
blocksOverride={fieldBlocksDraft}
|
||||
onFieldBlocksChange={setFieldBlocksDraft}
|
||||
/>
|
||||
) : (
|
||||
<DecisionApproachEditFields
|
||||
value={draft.value}
|
||||
onChange={(value) =>
|
||||
setDraft({ groupKey: "decisionApproaches", value })
|
||||
}
|
||||
/>
|
||||
))}
|
||||
{draft?.groupKey === "conflictManagement" &&
|
||||
(target &&
|
||||
isCustomMethodCardId(
|
||||
target.overrideKey,
|
||||
state.customMethodCardMetaById,
|
||||
) ? (
|
||||
<CustomMethodCardModalBody
|
||||
cardId={target.overrideKey}
|
||||
blocksById={state.customMethodCardFieldBlocksById}
|
||||
blocksOverride={fieldBlocksDraft}
|
||||
onFieldBlocksChange={setFieldBlocksDraft}
|
||||
/>
|
||||
) : (
|
||||
<ConflictManagementEditFields
|
||||
value={draft.value}
|
||||
onChange={(value) =>
|
||||
setDraft({ groupKey: "conflictManagement", value })
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Create>
|
||||
);
|
||||
@@ -309,18 +442,31 @@ type SubtitleMessages = {
|
||||
function subtitleForTarget(
|
||||
target: FinalReviewChipEditTarget,
|
||||
msgs: SubtitleMessages,
|
||||
customMeta?: CreateFlowState["customMethodCardMetaById"],
|
||||
): string {
|
||||
switch (target.groupKey) {
|
||||
case "coreValues":
|
||||
return msgs.tCv.detailModal.subtitle;
|
||||
case "communication":
|
||||
case "communication": {
|
||||
const fromCustom = customMeta?.[target.overrideKey]?.supportText?.trim();
|
||||
if (fromCustom) return fromCustom;
|
||||
return findMethodSupportText(msgs.tComm.methods, target.overrideKey);
|
||||
case "membership":
|
||||
}
|
||||
case "membership": {
|
||||
const fromCustom = customMeta?.[target.overrideKey]?.supportText?.trim();
|
||||
if (fromCustom) return fromCustom;
|
||||
return findMethodSupportText(msgs.tMem.methods, target.overrideKey);
|
||||
case "decisionApproaches":
|
||||
}
|
||||
case "decisionApproaches": {
|
||||
const fromCustom = customMeta?.[target.overrideKey]?.supportText?.trim();
|
||||
if (fromCustom) return fromCustom;
|
||||
return findMethodSupportText(msgs.tDa.methods, target.overrideKey);
|
||||
case "conflictManagement":
|
||||
}
|
||||
case "conflictManagement": {
|
||||
const fromCustom = customMeta?.[target.overrideKey]?.supportText?.trim();
|
||||
if (fromCustom) return fromCustom;
|
||||
return findMethodSupportText(msgs.tCm.methods, target.overrideKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback } from "react";
|
||||
import { useCreateFlow } from "../context/CreateFlowContext";
|
||||
import type { CustomMethodCardFieldBlock } from "../../../../lib/create/customMethodCardFieldBlocks";
|
||||
|
||||
/**
|
||||
* Stable writer for `customMethodCardFieldBlocksById[id]` used from facet card
|
||||
* modals. Uses {@link replaceState} so merges read the latest draft (no stale
|
||||
* closure over `customMethodCardFieldBlocksById`).
|
||||
*/
|
||||
export function useCustomMethodCardFieldBlocksChange(cardId: string | null) {
|
||||
const { replaceState, markCreateFlowInteraction } = useCreateFlow();
|
||||
|
||||
return useCallback(
|
||||
(nextBlocks: CustomMethodCardFieldBlock[]) => {
|
||||
if (!cardId) return;
|
||||
markCreateFlowInteraction();
|
||||
replaceState((prev) => ({
|
||||
...prev,
|
||||
customMethodCardFieldBlocksById: {
|
||||
...(prev.customMethodCardFieldBlocksById ?? {}),
|
||||
[cardId]: nextBlocks,
|
||||
},
|
||||
}));
|
||||
},
|
||||
[cardId, markCreateFlowInteraction, replaceState],
|
||||
);
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo } from "react";
|
||||
import { useCreateFlow } from "../context/CreateFlowContext";
|
||||
import type { CreateFlowMethodCardFacetSection } from "../types";
|
||||
import {
|
||||
mergeCompactCardIdsWithPinnedSelected,
|
||||
orderRankedMethodsWithPinnedSelection,
|
||||
@@ -17,38 +15,33 @@ import {
|
||||
type MethodEntry = { id: string; label: string; supportText: string };
|
||||
|
||||
/**
|
||||
* Applies score ranking, compact-slot rules, optional “pinned selection” showcase
|
||||
* order. Rows stay pinned across navigation while `methodSectionsPinCommitted` is true
|
||||
* and the section still has selections; we do **not** clear the flag when selection
|
||||
* arrays briefly go empty during draft hydration (`replaceState` / merge flashes) —
|
||||
* display order already ignores the pin until `pinActive` is true again.
|
||||
* Applies score ranking, compact-slot rules, then surfaces selected ids first in
|
||||
* `selected*Ids` order (most-recent add at index 0 via
|
||||
* {@link moveFacetSelectionIdToFront}). Selection-first applies whenever the facet
|
||||
* has any selection — not only after footer Confirm (`methodSectionsPinCommitted`).
|
||||
*/
|
||||
export function useMethodCardDeckOrdering(
|
||||
section: RecommendationSection,
|
||||
methods: readonly MethodEntry[],
|
||||
selectedIds: readonly string[],
|
||||
) {
|
||||
const { state } = useCreateFlow();
|
||||
const facetKey = section as CreateFlowMethodCardFacetSection;
|
||||
const { scoresBySlug, hasAnyFacets } = useFacetRecommendations(section);
|
||||
|
||||
const pinStored =
|
||||
state.methodSectionsPinCommitted?.[facetKey] === true;
|
||||
const pinActive = Boolean(pinStored && selectedIds.length > 0);
|
||||
|
||||
const rankedMethods = useMemo(
|
||||
() => rankMethodsByScore(methods, scoresBySlug),
|
||||
[methods, scoresBySlug],
|
||||
);
|
||||
|
||||
const selectionShowcaseActive = selectedIds.length > 0;
|
||||
|
||||
const displayMethods = useMemo(
|
||||
() =>
|
||||
orderRankedMethodsWithPinnedSelection(
|
||||
rankedMethods,
|
||||
selectedIds,
|
||||
pinActive,
|
||||
selectionShowcaseActive,
|
||||
),
|
||||
[rankedMethods, selectedIds, pinActive],
|
||||
[rankedMethods, selectedIds, selectionShowcaseActive],
|
||||
);
|
||||
|
||||
const { compactCardIds: baseCompactCardIds, recommendedIds } = useMemo(
|
||||
@@ -68,10 +61,10 @@ export function useMethodCardDeckOrdering(
|
||||
displayMethods.map((m) => m.id),
|
||||
baseCompactCardIds,
|
||||
selectedIds,
|
||||
pinActive,
|
||||
selectionShowcaseActive,
|
||||
5,
|
||||
),
|
||||
[displayMethods, baseCompactCardIds, selectedIds, pinActive],
|
||||
[displayMethods, baseCompactCardIds, selectedIds, selectionShowcaseActive],
|
||||
);
|
||||
|
||||
const sampleCards = useMemo(
|
||||
|
||||
@@ -8,13 +8,14 @@
|
||||
* two-column chip **select** frames. Future card-stack steps get their own `*Screen.tsx` here and
|
||||
* reuse `CardStack` / `CreateFlowStepShell` as needed.
|
||||
*
|
||||
* Card click opens the Figma "Add Platform" create modal (node `20246-15829`) with three
|
||||
* editable sections rendered by {@link CommunicationMethodEditFields}. The same field set is
|
||||
* reused on `/create/final-review` — see `FinalReviewChipEditModal`. Confirm persists both
|
||||
* the chip selection and any user edits as a `communicationMethodDetailsById[id]` override.
|
||||
* Card click opens the Figma create modal (node `20246-15829`) with three
|
||||
* editable sections rendered by {@link CommunicationMethodEditFields}. The primary
|
||||
* action is **Add Platform** for an unselected card or **Remove** when the card is
|
||||
* already selected — remove clears `selectedCommunicationMethodIds` and
|
||||
* `communicationMethodDetailsById` via {@link removeMethodCardFromFacetSelection}.
|
||||
*/
|
||||
|
||||
import { useState, useCallback } from "react";
|
||||
import { useState, useCallback, useMemo } from "react";
|
||||
import { useMessages } from "../../../../contexts/MessagesContext";
|
||||
import { useCreateFlow } from "../../context/CreateFlowContext";
|
||||
import { useCreateFlowMdUp } from "../../hooks/useCreateFlowMdUp";
|
||||
@@ -29,8 +30,16 @@ import {
|
||||
CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS,
|
||||
} from "../../components/createFlowLayoutTokens";
|
||||
import { CommunicationMethodEditFields } from "../../components/methodEditFields";
|
||||
import CustomMethodCardWizard from "../../components/CustomMethodCardWizard";
|
||||
import { communicationPresetFor } from "../../../../../lib/create/finalReviewChipPresets";
|
||||
import type { CustomMethodCardFieldBlock } from "../../../../../lib/create/customMethodCardFieldBlocks";
|
||||
import { mergePresetMethodsWithCustom } from "../../../../../lib/create/mergePresetMethodsWithCustom";
|
||||
import { moveFacetSelectionIdToFront } from "../../../../../lib/create/methodCardSelectionOrder";
|
||||
import { isCustomMethodCardId } from "../../../../../lib/create/isCustomMethodCardId";
|
||||
import { removeMethodCardFromFacetSelection } from "../../../../../lib/create/removeMethodCardFromFacetSelection";
|
||||
import type { CommunicationMethodDetailEntry } from "../../types";
|
||||
import CustomMethodCardModalBody from "../../components/CustomMethodCardModalBody";
|
||||
import { useCustomMethodCardFieldBlocksChange } from "../../hooks/useCustomMethodCardFieldBlocksChange";
|
||||
|
||||
export function CommunicationMethodsScreen() {
|
||||
const m = useMessages();
|
||||
@@ -42,28 +51,45 @@ export function CommunicationMethodsScreen() {
|
||||
const [pendingCardId, setPendingCardId] = useState<string | null>(null);
|
||||
const [pendingDraft, setPendingDraft] =
|
||||
useState<CommunicationMethodDetailEntry | null>(null);
|
||||
const [addCustomWizardOpen, setAddCustomWizardOpen] = useState(false);
|
||||
|
||||
const selectedIds = state.selectedCommunicationMethodIds ?? [];
|
||||
|
||||
const mergedMethods = useMemo(
|
||||
() =>
|
||||
mergePresetMethodsWithCustom(
|
||||
comm.methods,
|
||||
selectedIds,
|
||||
state.customMethodCardMetaById,
|
||||
),
|
||||
[comm.methods, selectedIds, state.customMethodCardMetaById],
|
||||
);
|
||||
|
||||
const { sampleCards, compactCardIds, methodById } = useMethodCardDeckOrdering(
|
||||
"communication",
|
||||
comm.methods,
|
||||
mergedMethods,
|
||||
selectedIds,
|
||||
);
|
||||
|
||||
const handleOpenAddWizard = useCallback(() => {
|
||||
markCreateFlowInteraction();
|
||||
setAddCustomWizardOpen(true);
|
||||
}, [markCreateFlowInteraction]);
|
||||
|
||||
const title = expanded ? comm.page.expandedTitle : comm.page.compactTitle;
|
||||
|
||||
const description = expanded ? (
|
||||
comm.page.expandedDescription
|
||||
<>
|
||||
{comm.page.expandedDescriptionBefore}
|
||||
<InlineTextButton onClick={handleOpenAddWizard}>
|
||||
{comm.page.compactDescriptionLinkLabel}
|
||||
</InlineTextButton>
|
||||
{comm.page.expandedDescriptionAfter}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{comm.page.compactDescriptionBefore}
|
||||
<InlineTextButton
|
||||
onClick={() => {
|
||||
markCreateFlowInteraction();
|
||||
setExpanded(true);
|
||||
}}
|
||||
>
|
||||
<InlineTextButton onClick={handleOpenAddWizard}>
|
||||
{comm.page.compactDescriptionLinkLabel}
|
||||
</InlineTextButton>
|
||||
{comm.page.compactDescriptionAfter}
|
||||
@@ -73,10 +99,13 @@ export function CommunicationMethodsScreen() {
|
||||
const modalConfig = pendingCardId
|
||||
? (() => {
|
||||
const method = methodById.get(pendingCardId);
|
||||
const alreadySelected = selectedIds.includes(pendingCardId);
|
||||
return {
|
||||
title: method?.label ?? comm.confirmModal.title,
|
||||
description: method?.supportText ?? comm.confirmModal.description,
|
||||
nextButtonText: comm.addPlatform.nextButtonText,
|
||||
nextButtonText: alreadySelected
|
||||
? comm.removePlatform.nextButtonText
|
||||
: comm.addPlatform.nextButtonText,
|
||||
};
|
||||
})()
|
||||
: {
|
||||
@@ -114,22 +143,87 @@ export function CommunicationMethodsScreen() {
|
||||
[markCreateFlowInteraction],
|
||||
);
|
||||
|
||||
const onCustomFieldBlocksChange = useCustomMethodCardFieldBlocksChange(
|
||||
createModalOpen ? pendingCardId : null,
|
||||
);
|
||||
|
||||
const handleCreateModalClose = useCallback(() => {
|
||||
setCreateModalOpen(false);
|
||||
setPendingCardId(null);
|
||||
setPendingDraft(null);
|
||||
}, []);
|
||||
|
||||
const handleCreateModalConfirm = useCallback(() => {
|
||||
if (!pendingCardId || !pendingDraft) {
|
||||
const handleCloseAddWizard = useCallback(() => {
|
||||
setAddCustomWizardOpen(false);
|
||||
}, []);
|
||||
|
||||
const handleFinalizeCustomCard = useCallback(
|
||||
({
|
||||
title,
|
||||
description,
|
||||
fieldBlocks,
|
||||
}: {
|
||||
title: string;
|
||||
description: string;
|
||||
fieldBlocks: CustomMethodCardFieldBlock[];
|
||||
}) => {
|
||||
markCreateFlowInteraction();
|
||||
const id = crypto.randomUUID();
|
||||
updateState({
|
||||
selectedCommunicationMethodIds: moveFacetSelectionIdToFront(
|
||||
selectedIds,
|
||||
id,
|
||||
),
|
||||
customMethodCardMetaById: {
|
||||
...(state.customMethodCardMetaById ?? {}),
|
||||
[id]: { label: title, supportText: description },
|
||||
},
|
||||
communicationMethodDetailsById: {
|
||||
...(state.communicationMethodDetailsById ?? {}),
|
||||
[id]: communicationPresetFor(id),
|
||||
},
|
||||
customMethodCardFieldBlocksById: {
|
||||
...(state.customMethodCardFieldBlocksById ?? {}),
|
||||
[id]: fieldBlocks,
|
||||
},
|
||||
});
|
||||
},
|
||||
[
|
||||
markCreateFlowInteraction,
|
||||
selectedIds,
|
||||
state.communicationMethodDetailsById,
|
||||
state.customMethodCardFieldBlocksById,
|
||||
state.customMethodCardMetaById,
|
||||
updateState,
|
||||
],
|
||||
);
|
||||
|
||||
const handleCreateModalPrimary = useCallback(() => {
|
||||
if (!pendingCardId) {
|
||||
handleCreateModalClose();
|
||||
return;
|
||||
}
|
||||
markCreateFlowInteraction();
|
||||
if (selectedIds.includes(pendingCardId)) {
|
||||
updateState(
|
||||
removeMethodCardFromFacetSelection(
|
||||
state,
|
||||
"communication",
|
||||
pendingCardId,
|
||||
),
|
||||
);
|
||||
handleCreateModalClose();
|
||||
return;
|
||||
}
|
||||
if (!pendingDraft) {
|
||||
handleCreateModalClose();
|
||||
return;
|
||||
}
|
||||
updateState({
|
||||
selectedCommunicationMethodIds: selectedIds.includes(pendingCardId)
|
||||
? selectedIds
|
||||
: [...selectedIds, pendingCardId],
|
||||
selectedCommunicationMethodIds: moveFacetSelectionIdToFront(
|
||||
selectedIds,
|
||||
pendingCardId,
|
||||
),
|
||||
communicationMethodDetailsById: {
|
||||
...(state.communicationMethodDetailsById ?? {}),
|
||||
[pendingCardId]: pendingDraft,
|
||||
@@ -142,11 +236,12 @@ export function CommunicationMethodsScreen() {
|
||||
pendingCardId,
|
||||
pendingDraft,
|
||||
selectedIds,
|
||||
state.communicationMethodDetailsById,
|
||||
state,
|
||||
updateState,
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<CreateFlowStepShell
|
||||
variant="wideGridLoosePadding"
|
||||
contentTopBelowMd="space-800"
|
||||
@@ -182,7 +277,7 @@ export function CommunicationMethodsScreen() {
|
||||
<Create
|
||||
isOpen={createModalOpen}
|
||||
onClose={handleCreateModalClose}
|
||||
onNext={handleCreateModalConfirm}
|
||||
onNext={handleCreateModalPrimary}
|
||||
title={modalConfig.title}
|
||||
description={modalConfig.description}
|
||||
nextButtonText={modalConfig.nextButtonText}
|
||||
@@ -190,13 +285,31 @@ export function CommunicationMethodsScreen() {
|
||||
backdropVariant="blurredYellow"
|
||||
>
|
||||
{pendingCardId && pendingDraft ? (
|
||||
<CommunicationMethodEditFields
|
||||
key={pendingCardId}
|
||||
value={pendingDraft}
|
||||
onChange={handleDraftChange}
|
||||
/>
|
||||
isCustomMethodCardId(
|
||||
pendingCardId,
|
||||
state.customMethodCardMetaById,
|
||||
) ? (
|
||||
<CustomMethodCardModalBody
|
||||
key={pendingCardId}
|
||||
cardId={pendingCardId}
|
||||
blocksById={state.customMethodCardFieldBlocksById}
|
||||
onFieldBlocksChange={onCustomFieldBlocksChange}
|
||||
/>
|
||||
) : (
|
||||
<CommunicationMethodEditFields
|
||||
key={pendingCardId}
|
||||
value={pendingDraft}
|
||||
onChange={handleDraftChange}
|
||||
/>
|
||||
)
|
||||
) : null}
|
||||
</Create>
|
||||
</CreateFlowStepShell>
|
||||
<CustomMethodCardWizard
|
||||
isOpen={addCustomWizardOpen}
|
||||
onClose={handleCloseAddWizard}
|
||||
onFinalize={handleFinalizeCustomCard}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
* any user edits as a `conflictManagementDetailsById[id]` override.
|
||||
*/
|
||||
|
||||
import { useState, useCallback } from "react";
|
||||
import { useState, useCallback, useMemo } from "react";
|
||||
import { useMessages } from "../../../../contexts/MessagesContext";
|
||||
import { useCreateFlow } from "../../context/CreateFlowContext";
|
||||
import { useCreateFlowMdUp } from "../../hooks/useCreateFlowMdUp";
|
||||
@@ -27,8 +27,16 @@ import {
|
||||
CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS,
|
||||
} from "../../components/createFlowLayoutTokens";
|
||||
import { ConflictManagementEditFields } from "../../components/methodEditFields";
|
||||
import CustomMethodCardWizard from "../../components/CustomMethodCardWizard";
|
||||
import { conflictManagementPresetFor } from "../../../../../lib/create/finalReviewChipPresets";
|
||||
import type { CustomMethodCardFieldBlock } from "../../../../../lib/create/customMethodCardFieldBlocks";
|
||||
import { mergePresetMethodsWithCustom } from "../../../../../lib/create/mergePresetMethodsWithCustom";
|
||||
import { moveFacetSelectionIdToFront } from "../../../../../lib/create/methodCardSelectionOrder";
|
||||
import { isCustomMethodCardId } from "../../../../../lib/create/isCustomMethodCardId";
|
||||
import { removeMethodCardFromFacetSelection } from "../../../../../lib/create/removeMethodCardFromFacetSelection";
|
||||
import type { ConflictManagementDetailEntry } from "../../types";
|
||||
import CustomMethodCardModalBody from "../../components/CustomMethodCardModalBody";
|
||||
import { useCustomMethodCardFieldBlocksChange } from "../../hooks/useCustomMethodCardFieldBlocksChange";
|
||||
|
||||
export function ConflictManagementScreen() {
|
||||
const m = useMessages();
|
||||
@@ -40,28 +48,45 @@ export function ConflictManagementScreen() {
|
||||
const [pendingCardId, setPendingCardId] = useState<string | null>(null);
|
||||
const [pendingDraft, setPendingDraft] =
|
||||
useState<ConflictManagementDetailEntry | null>(null);
|
||||
const [addCustomWizardOpen, setAddCustomWizardOpen] = useState(false);
|
||||
|
||||
const selectedIds = state.selectedConflictManagementIds ?? [];
|
||||
|
||||
const mergedMethods = useMemo(
|
||||
() =>
|
||||
mergePresetMethodsWithCustom(
|
||||
cm.methods,
|
||||
selectedIds,
|
||||
state.customMethodCardMetaById,
|
||||
),
|
||||
[cm.methods, selectedIds, state.customMethodCardMetaById],
|
||||
);
|
||||
|
||||
const { sampleCards, compactCardIds, methodById } = useMethodCardDeckOrdering(
|
||||
"conflictManagement",
|
||||
cm.methods,
|
||||
mergedMethods,
|
||||
selectedIds,
|
||||
);
|
||||
|
||||
const handleOpenAddWizard = useCallback(() => {
|
||||
markCreateFlowInteraction();
|
||||
setAddCustomWizardOpen(true);
|
||||
}, [markCreateFlowInteraction]);
|
||||
|
||||
const title = expanded ? cm.page.expandedTitle : cm.page.compactTitle;
|
||||
|
||||
const description = expanded ? (
|
||||
cm.page.expandedDescription
|
||||
<>
|
||||
{cm.page.expandedDescriptionBefore}
|
||||
<InlineTextButton onClick={handleOpenAddWizard}>
|
||||
{cm.page.compactDescriptionLinkLabel}
|
||||
</InlineTextButton>
|
||||
{cm.page.expandedDescriptionAfter}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{cm.page.compactDescriptionBefore}
|
||||
<InlineTextButton
|
||||
onClick={() => {
|
||||
markCreateFlowInteraction();
|
||||
setExpanded(true);
|
||||
}}
|
||||
>
|
||||
<InlineTextButton onClick={handleOpenAddWizard}>
|
||||
{cm.page.compactDescriptionLinkLabel}
|
||||
</InlineTextButton>
|
||||
{cm.page.compactDescriptionAfter}
|
||||
@@ -71,10 +96,13 @@ export function ConflictManagementScreen() {
|
||||
const modalConfig = pendingCardId
|
||||
? (() => {
|
||||
const method = methodById.get(pendingCardId);
|
||||
const alreadySelected = selectedIds.includes(pendingCardId);
|
||||
return {
|
||||
title: method?.label ?? cm.confirmModal.title,
|
||||
description: method?.supportText ?? cm.confirmModal.description,
|
||||
nextButtonText: cm.addApproach.nextButtonText,
|
||||
nextButtonText: alreadySelected
|
||||
? cm.removeApproach.nextButtonText
|
||||
: cm.addApproach.nextButtonText,
|
||||
};
|
||||
})()
|
||||
: {
|
||||
@@ -116,22 +144,87 @@ export function ConflictManagementScreen() {
|
||||
[markCreateFlowInteraction],
|
||||
);
|
||||
|
||||
const onCustomFieldBlocksChange = useCustomMethodCardFieldBlocksChange(
|
||||
createModalOpen ? pendingCardId : null,
|
||||
);
|
||||
|
||||
const handleCreateModalClose = useCallback(() => {
|
||||
setCreateModalOpen(false);
|
||||
setPendingCardId(null);
|
||||
setPendingDraft(null);
|
||||
}, []);
|
||||
|
||||
const handleCreateModalConfirm = useCallback(() => {
|
||||
if (!pendingCardId || !pendingDraft) {
|
||||
const handleCloseAddWizard = useCallback(() => {
|
||||
setAddCustomWizardOpen(false);
|
||||
}, []);
|
||||
|
||||
const handleFinalizeCustomCard = useCallback(
|
||||
({
|
||||
title,
|
||||
description,
|
||||
fieldBlocks,
|
||||
}: {
|
||||
title: string;
|
||||
description: string;
|
||||
fieldBlocks: CustomMethodCardFieldBlock[];
|
||||
}) => {
|
||||
markCreateFlowInteraction();
|
||||
const id = crypto.randomUUID();
|
||||
updateState({
|
||||
selectedConflictManagementIds: moveFacetSelectionIdToFront(
|
||||
selectedIds,
|
||||
id,
|
||||
),
|
||||
customMethodCardMetaById: {
|
||||
...(state.customMethodCardMetaById ?? {}),
|
||||
[id]: { label: title, supportText: description },
|
||||
},
|
||||
conflictManagementDetailsById: {
|
||||
...(state.conflictManagementDetailsById ?? {}),
|
||||
[id]: conflictManagementPresetFor(id),
|
||||
},
|
||||
customMethodCardFieldBlocksById: {
|
||||
...(state.customMethodCardFieldBlocksById ?? {}),
|
||||
[id]: fieldBlocks,
|
||||
},
|
||||
});
|
||||
},
|
||||
[
|
||||
markCreateFlowInteraction,
|
||||
selectedIds,
|
||||
state.conflictManagementDetailsById,
|
||||
state.customMethodCardFieldBlocksById,
|
||||
state.customMethodCardMetaById,
|
||||
updateState,
|
||||
],
|
||||
);
|
||||
|
||||
const handleCreateModalPrimary = useCallback(() => {
|
||||
if (!pendingCardId) {
|
||||
handleCreateModalClose();
|
||||
return;
|
||||
}
|
||||
markCreateFlowInteraction();
|
||||
if (selectedIds.includes(pendingCardId)) {
|
||||
updateState(
|
||||
removeMethodCardFromFacetSelection(
|
||||
state,
|
||||
"conflictManagement",
|
||||
pendingCardId,
|
||||
),
|
||||
);
|
||||
handleCreateModalClose();
|
||||
return;
|
||||
}
|
||||
if (!pendingDraft) {
|
||||
handleCreateModalClose();
|
||||
return;
|
||||
}
|
||||
updateState({
|
||||
selectedConflictManagementIds: selectedIds.includes(pendingCardId)
|
||||
? selectedIds
|
||||
: [...selectedIds, pendingCardId],
|
||||
selectedConflictManagementIds: moveFacetSelectionIdToFront(
|
||||
selectedIds,
|
||||
pendingCardId,
|
||||
),
|
||||
conflictManagementDetailsById: {
|
||||
...(state.conflictManagementDetailsById ?? {}),
|
||||
[pendingCardId]: pendingDraft,
|
||||
@@ -144,11 +237,12 @@ export function ConflictManagementScreen() {
|
||||
pendingCardId,
|
||||
pendingDraft,
|
||||
selectedIds,
|
||||
state.conflictManagementDetailsById,
|
||||
state,
|
||||
updateState,
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<CreateFlowStepShell
|
||||
variant="wideGridLoosePadding"
|
||||
contentTopBelowMd="space-800"
|
||||
@@ -184,7 +278,7 @@ export function ConflictManagementScreen() {
|
||||
<Create
|
||||
isOpen={createModalOpen}
|
||||
onClose={handleCreateModalClose}
|
||||
onNext={handleCreateModalConfirm}
|
||||
onNext={handleCreateModalPrimary}
|
||||
title={modalConfig.title}
|
||||
description={modalConfig.description}
|
||||
nextButtonText={modalConfig.nextButtonText}
|
||||
@@ -192,13 +286,31 @@ export function ConflictManagementScreen() {
|
||||
backdropVariant="blurredYellow"
|
||||
>
|
||||
{pendingCardId && pendingDraft ? (
|
||||
<ConflictManagementEditFields
|
||||
key={pendingCardId}
|
||||
value={pendingDraft}
|
||||
onChange={handleDraftChange}
|
||||
/>
|
||||
isCustomMethodCardId(
|
||||
pendingCardId,
|
||||
state.customMethodCardMetaById,
|
||||
) ? (
|
||||
<CustomMethodCardModalBody
|
||||
key={pendingCardId}
|
||||
cardId={pendingCardId}
|
||||
blocksById={state.customMethodCardFieldBlocksById}
|
||||
onFieldBlocksChange={onCustomFieldBlocksChange}
|
||||
/>
|
||||
) : (
|
||||
<ConflictManagementEditFields
|
||||
key={pendingCardId}
|
||||
value={pendingDraft}
|
||||
onChange={handleDraftChange}
|
||||
/>
|
||||
)
|
||||
) : null}
|
||||
</Create>
|
||||
</CreateFlowStepShell>
|
||||
<CustomMethodCardWizard
|
||||
isOpen={addCustomWizardOpen}
|
||||
onClose={handleCloseAddWizard}
|
||||
onFinalize={handleFinalizeCustomCard}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
* DB-driven content.
|
||||
*/
|
||||
|
||||
import { useState, useCallback } from "react";
|
||||
import { useState, useCallback, useMemo } from "react";
|
||||
import { useMessages } from "../../../../contexts/MessagesContext";
|
||||
import { useCreateFlow } from "../../context/CreateFlowContext";
|
||||
import { useCreateFlowMdUp } from "../../hooks/useCreateFlowMdUp";
|
||||
@@ -28,8 +28,16 @@ import {
|
||||
CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS,
|
||||
} from "../../components/createFlowLayoutTokens";
|
||||
import { MembershipMethodEditFields } from "../../components/methodEditFields";
|
||||
import CustomMethodCardWizard from "../../components/CustomMethodCardWizard";
|
||||
import { membershipPresetFor } from "../../../../../lib/create/finalReviewChipPresets";
|
||||
import type { CustomMethodCardFieldBlock } from "../../../../../lib/create/customMethodCardFieldBlocks";
|
||||
import { mergePresetMethodsWithCustom } from "../../../../../lib/create/mergePresetMethodsWithCustom";
|
||||
import { moveFacetSelectionIdToFront } from "../../../../../lib/create/methodCardSelectionOrder";
|
||||
import { isCustomMethodCardId } from "../../../../../lib/create/isCustomMethodCardId";
|
||||
import { removeMethodCardFromFacetSelection } from "../../../../../lib/create/removeMethodCardFromFacetSelection";
|
||||
import type { MembershipMethodDetailEntry } from "../../types";
|
||||
import CustomMethodCardModalBody from "../../components/CustomMethodCardModalBody";
|
||||
import { useCustomMethodCardFieldBlocksChange } from "../../hooks/useCustomMethodCardFieldBlocksChange";
|
||||
|
||||
export function MembershipMethodsScreen() {
|
||||
const m = useMessages();
|
||||
@@ -41,28 +49,45 @@ export function MembershipMethodsScreen() {
|
||||
const [pendingCardId, setPendingCardId] = useState<string | null>(null);
|
||||
const [pendingDraft, setPendingDraft] =
|
||||
useState<MembershipMethodDetailEntry | null>(null);
|
||||
const [addCustomWizardOpen, setAddCustomWizardOpen] = useState(false);
|
||||
|
||||
const selectedIds = state.selectedMembershipMethodIds ?? [];
|
||||
|
||||
const mergedMethods = useMemo(
|
||||
() =>
|
||||
mergePresetMethodsWithCustom(
|
||||
mem.methods,
|
||||
selectedIds,
|
||||
state.customMethodCardMetaById,
|
||||
),
|
||||
[mem.methods, selectedIds, state.customMethodCardMetaById],
|
||||
);
|
||||
|
||||
const { sampleCards, compactCardIds, methodById } = useMethodCardDeckOrdering(
|
||||
"membership",
|
||||
mem.methods,
|
||||
mergedMethods,
|
||||
selectedIds,
|
||||
);
|
||||
|
||||
const handleOpenAddWizard = useCallback(() => {
|
||||
markCreateFlowInteraction();
|
||||
setAddCustomWizardOpen(true);
|
||||
}, [markCreateFlowInteraction]);
|
||||
|
||||
const title = expanded ? mem.page.expandedTitle : mem.page.compactTitle;
|
||||
|
||||
const description = expanded ? (
|
||||
mem.page.expandedDescription
|
||||
<>
|
||||
{mem.page.expandedDescriptionBefore}
|
||||
<InlineTextButton onClick={handleOpenAddWizard}>
|
||||
{mem.page.compactDescriptionLinkLabel}
|
||||
</InlineTextButton>
|
||||
{mem.page.expandedDescriptionAfter}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{mem.page.compactDescriptionBefore}
|
||||
<InlineTextButton
|
||||
onClick={() => {
|
||||
markCreateFlowInteraction();
|
||||
setExpanded(true);
|
||||
}}
|
||||
>
|
||||
<InlineTextButton onClick={handleOpenAddWizard}>
|
||||
{mem.page.compactDescriptionLinkLabel}
|
||||
</InlineTextButton>
|
||||
{mem.page.compactDescriptionAfter}
|
||||
@@ -72,10 +97,13 @@ export function MembershipMethodsScreen() {
|
||||
const modalConfig = pendingCardId
|
||||
? (() => {
|
||||
const method = methodById.get(pendingCardId);
|
||||
const alreadySelected = selectedIds.includes(pendingCardId);
|
||||
return {
|
||||
title: method?.label ?? mem.confirmModal.title,
|
||||
description: method?.supportText ?? mem.confirmModal.description,
|
||||
nextButtonText: mem.addPlatform.nextButtonText,
|
||||
nextButtonText: alreadySelected
|
||||
? mem.removePlatform.nextButtonText
|
||||
: mem.addPlatform.nextButtonText,
|
||||
};
|
||||
})()
|
||||
: {
|
||||
@@ -113,22 +141,83 @@ export function MembershipMethodsScreen() {
|
||||
[markCreateFlowInteraction],
|
||||
);
|
||||
|
||||
const onCustomFieldBlocksChange = useCustomMethodCardFieldBlocksChange(
|
||||
createModalOpen ? pendingCardId : null,
|
||||
);
|
||||
|
||||
const handleCreateModalClose = useCallback(() => {
|
||||
setCreateModalOpen(false);
|
||||
setPendingCardId(null);
|
||||
setPendingDraft(null);
|
||||
}, []);
|
||||
|
||||
const handleCreateModalConfirm = useCallback(() => {
|
||||
if (!pendingCardId || !pendingDraft) {
|
||||
const handleCloseAddWizard = useCallback(() => {
|
||||
setAddCustomWizardOpen(false);
|
||||
}, []);
|
||||
|
||||
const handleFinalizeCustomCard = useCallback(
|
||||
({
|
||||
title,
|
||||
description,
|
||||
fieldBlocks,
|
||||
}: {
|
||||
title: string;
|
||||
description: string;
|
||||
fieldBlocks: CustomMethodCardFieldBlock[];
|
||||
}) => {
|
||||
markCreateFlowInteraction();
|
||||
const id = crypto.randomUUID();
|
||||
updateState({
|
||||
selectedMembershipMethodIds: moveFacetSelectionIdToFront(
|
||||
selectedIds,
|
||||
id,
|
||||
),
|
||||
customMethodCardMetaById: {
|
||||
...(state.customMethodCardMetaById ?? {}),
|
||||
[id]: { label: title, supportText: description },
|
||||
},
|
||||
membershipMethodDetailsById: {
|
||||
...(state.membershipMethodDetailsById ?? {}),
|
||||
[id]: membershipPresetFor(id),
|
||||
},
|
||||
customMethodCardFieldBlocksById: {
|
||||
...(state.customMethodCardFieldBlocksById ?? {}),
|
||||
[id]: fieldBlocks,
|
||||
},
|
||||
});
|
||||
},
|
||||
[
|
||||
markCreateFlowInteraction,
|
||||
selectedIds,
|
||||
state.customMethodCardFieldBlocksById,
|
||||
state.customMethodCardMetaById,
|
||||
state.membershipMethodDetailsById,
|
||||
updateState,
|
||||
],
|
||||
);
|
||||
|
||||
const handleCreateModalPrimary = useCallback(() => {
|
||||
if (!pendingCardId) {
|
||||
handleCreateModalClose();
|
||||
return;
|
||||
}
|
||||
markCreateFlowInteraction();
|
||||
if (selectedIds.includes(pendingCardId)) {
|
||||
updateState(
|
||||
removeMethodCardFromFacetSelection(state, "membership", pendingCardId),
|
||||
);
|
||||
handleCreateModalClose();
|
||||
return;
|
||||
}
|
||||
if (!pendingDraft) {
|
||||
handleCreateModalClose();
|
||||
return;
|
||||
}
|
||||
updateState({
|
||||
selectedMembershipMethodIds: selectedIds.includes(pendingCardId)
|
||||
? selectedIds
|
||||
: [...selectedIds, pendingCardId],
|
||||
selectedMembershipMethodIds: moveFacetSelectionIdToFront(
|
||||
selectedIds,
|
||||
pendingCardId,
|
||||
),
|
||||
membershipMethodDetailsById: {
|
||||
...(state.membershipMethodDetailsById ?? {}),
|
||||
[pendingCardId]: pendingDraft,
|
||||
@@ -141,11 +230,12 @@ export function MembershipMethodsScreen() {
|
||||
pendingCardId,
|
||||
pendingDraft,
|
||||
selectedIds,
|
||||
state.membershipMethodDetailsById,
|
||||
state,
|
||||
updateState,
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<CreateFlowStepShell
|
||||
variant="wideGridLoosePadding"
|
||||
contentTopBelowMd="space-800"
|
||||
@@ -181,7 +271,7 @@ export function MembershipMethodsScreen() {
|
||||
<Create
|
||||
isOpen={createModalOpen}
|
||||
onClose={handleCreateModalClose}
|
||||
onNext={handleCreateModalConfirm}
|
||||
onNext={handleCreateModalPrimary}
|
||||
title={modalConfig.title}
|
||||
description={modalConfig.description}
|
||||
nextButtonText={modalConfig.nextButtonText}
|
||||
@@ -189,13 +279,31 @@ export function MembershipMethodsScreen() {
|
||||
backdropVariant="blurredYellow"
|
||||
>
|
||||
{pendingCardId && pendingDraft ? (
|
||||
<MembershipMethodEditFields
|
||||
key={pendingCardId}
|
||||
value={pendingDraft}
|
||||
onChange={handleDraftChange}
|
||||
/>
|
||||
isCustomMethodCardId(
|
||||
pendingCardId,
|
||||
state.customMethodCardMetaById,
|
||||
) ? (
|
||||
<CustomMethodCardModalBody
|
||||
key={pendingCardId}
|
||||
cardId={pendingCardId}
|
||||
blocksById={state.customMethodCardFieldBlocksById}
|
||||
onFieldBlocksChange={onCustomFieldBlocksChange}
|
||||
/>
|
||||
) : (
|
||||
<MembershipMethodEditFields
|
||||
key={pendingCardId}
|
||||
value={pendingDraft}
|
||||
onChange={handleDraftChange}
|
||||
/>
|
||||
)
|
||||
) : null}
|
||||
</Create>
|
||||
</CreateFlowStepShell>
|
||||
<CustomMethodCardWizard
|
||||
isOpen={addCustomWizardOpen}
|
||||
onClose={handleCloseAddWizard}
|
||||
onFinalize={handleFinalizeCustomCard}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -29,8 +29,16 @@ import { useCreateFlowMdUp } from "../../hooks/useCreateFlowMdUp";
|
||||
import { useMethodCardDeckOrdering } from "../../hooks/useMethodCardDeckOrdering";
|
||||
import { CreateFlowTwoColumnSelectShell } from "../../components/CreateFlowTwoColumnSelectShell";
|
||||
import { DecisionApproachEditFields } from "../../components/methodEditFields";
|
||||
import CustomMethodCardWizard from "../../components/CustomMethodCardWizard";
|
||||
import { decisionApproachPresetFor } from "../../../../../lib/create/finalReviewChipPresets";
|
||||
import type { CustomMethodCardFieldBlock } from "../../../../../lib/create/customMethodCardFieldBlocks";
|
||||
import { mergePresetMethodsWithCustom } from "../../../../../lib/create/mergePresetMethodsWithCustom";
|
||||
import { moveFacetSelectionIdToFront } from "../../../../../lib/create/methodCardSelectionOrder";
|
||||
import { isCustomMethodCardId } from "../../../../../lib/create/isCustomMethodCardId";
|
||||
import { removeMethodCardFromFacetSelection } from "../../../../../lib/create/removeMethodCardFromFacetSelection";
|
||||
import type { DecisionApproachDetailEntry } from "../../types";
|
||||
import CustomMethodCardModalBody from "../../components/CustomMethodCardModalBody";
|
||||
import { useCustomMethodCardFieldBlocksChange } from "../../hooks/useCustomMethodCardFieldBlocksChange";
|
||||
|
||||
export function DecisionApproachesScreen() {
|
||||
const m = useMessages();
|
||||
@@ -45,6 +53,7 @@ export function DecisionApproachesScreen() {
|
||||
const [pendingCardId, setPendingCardId] = useState<string | null>(null);
|
||||
const [pendingDraft, setPendingDraft] =
|
||||
useState<DecisionApproachDetailEntry | null>(null);
|
||||
const [addCustomWizardOpen, setAddCustomWizardOpen] = useState(false);
|
||||
|
||||
const selectedIds = state.selectedDecisionApproachIds ?? [];
|
||||
|
||||
@@ -57,21 +66,31 @@ export function DecisionApproachesScreen() {
|
||||
[da.messageBox.items],
|
||||
);
|
||||
|
||||
const mergedMethods = useMemo(
|
||||
() =>
|
||||
mergePresetMethodsWithCustom(
|
||||
da.methods,
|
||||
selectedIds,
|
||||
state.customMethodCardMetaById,
|
||||
),
|
||||
[da.methods, selectedIds, state.customMethodCardMetaById],
|
||||
);
|
||||
|
||||
const { sampleCards, compactCardIds, methodById } = useMethodCardDeckOrdering(
|
||||
"decisionApproaches",
|
||||
da.methods,
|
||||
mergedMethods,
|
||||
selectedIds,
|
||||
);
|
||||
|
||||
const handleOpenAddWizard = useCallback(() => {
|
||||
markCreateFlowInteraction();
|
||||
setAddCustomWizardOpen(true);
|
||||
}, [markCreateFlowInteraction]);
|
||||
|
||||
const sidebarDescription = (
|
||||
<>
|
||||
{da.sidebar.descriptionBefore}
|
||||
<InlineTextButton
|
||||
onClick={() => {
|
||||
markCreateFlowInteraction();
|
||||
setExpanded(true);
|
||||
}}
|
||||
>
|
||||
<InlineTextButton onClick={handleOpenAddWizard}>
|
||||
{da.sidebar.descriptionLinkLabel}
|
||||
</InlineTextButton>
|
||||
{da.sidebar.descriptionAfter}
|
||||
@@ -121,6 +140,10 @@ export function DecisionApproachesScreen() {
|
||||
[markCreateFlowInteraction],
|
||||
);
|
||||
|
||||
const onCustomFieldBlocksChange = useCustomMethodCardFieldBlocksChange(
|
||||
createModalOpen ? pendingCardId : null,
|
||||
);
|
||||
|
||||
const handleToggleExpand = useCallback(() => {
|
||||
markCreateFlowInteraction();
|
||||
setExpanded((prev) => !prev);
|
||||
@@ -132,16 +155,77 @@ export function DecisionApproachesScreen() {
|
||||
setPendingDraft(null);
|
||||
}, []);
|
||||
|
||||
const handleCreateModalConfirm = useCallback(() => {
|
||||
if (!pendingCardId || !pendingDraft) {
|
||||
const handleCloseAddWizard = useCallback(() => {
|
||||
setAddCustomWizardOpen(false);
|
||||
}, []);
|
||||
|
||||
const handleFinalizeCustomCard = useCallback(
|
||||
({
|
||||
title,
|
||||
description,
|
||||
fieldBlocks,
|
||||
}: {
|
||||
title: string;
|
||||
description: string;
|
||||
fieldBlocks: CustomMethodCardFieldBlock[];
|
||||
}) => {
|
||||
markCreateFlowInteraction();
|
||||
const id = crypto.randomUUID();
|
||||
updateState({
|
||||
selectedDecisionApproachIds: moveFacetSelectionIdToFront(
|
||||
selectedIds,
|
||||
id,
|
||||
),
|
||||
customMethodCardMetaById: {
|
||||
...(state.customMethodCardMetaById ?? {}),
|
||||
[id]: { label: title, supportText: description },
|
||||
},
|
||||
decisionApproachDetailsById: {
|
||||
...(state.decisionApproachDetailsById ?? {}),
|
||||
[id]: decisionApproachPresetFor(id),
|
||||
},
|
||||
customMethodCardFieldBlocksById: {
|
||||
...(state.customMethodCardFieldBlocksById ?? {}),
|
||||
[id]: fieldBlocks,
|
||||
},
|
||||
});
|
||||
},
|
||||
[
|
||||
markCreateFlowInteraction,
|
||||
selectedIds,
|
||||
state.customMethodCardFieldBlocksById,
|
||||
state.customMethodCardMetaById,
|
||||
state.decisionApproachDetailsById,
|
||||
updateState,
|
||||
],
|
||||
);
|
||||
|
||||
const handleCreateModalPrimary = useCallback(() => {
|
||||
if (!pendingCardId) {
|
||||
handleCreateModalClose();
|
||||
return;
|
||||
}
|
||||
markCreateFlowInteraction();
|
||||
if (selectedIds.includes(pendingCardId)) {
|
||||
updateState(
|
||||
removeMethodCardFromFacetSelection(
|
||||
state,
|
||||
"decisionApproaches",
|
||||
pendingCardId,
|
||||
),
|
||||
);
|
||||
handleCreateModalClose();
|
||||
return;
|
||||
}
|
||||
if (!pendingDraft) {
|
||||
handleCreateModalClose();
|
||||
return;
|
||||
}
|
||||
updateState({
|
||||
selectedDecisionApproachIds: selectedIds.includes(pendingCardId)
|
||||
? selectedIds
|
||||
: [...selectedIds, pendingCardId],
|
||||
selectedDecisionApproachIds: moveFacetSelectionIdToFront(
|
||||
selectedIds,
|
||||
pendingCardId,
|
||||
),
|
||||
decisionApproachDetailsById: {
|
||||
...(state.decisionApproachDetailsById ?? {}),
|
||||
[pendingCardId]: pendingDraft,
|
||||
@@ -154,17 +238,20 @@ export function DecisionApproachesScreen() {
|
||||
pendingCardId,
|
||||
pendingDraft,
|
||||
selectedIds,
|
||||
state.decisionApproachDetailsById,
|
||||
state,
|
||||
updateState,
|
||||
]);
|
||||
|
||||
const modalConfig = pendingCardId
|
||||
? (() => {
|
||||
const method = methodById.get(pendingCardId);
|
||||
const alreadySelected = selectedIds.includes(pendingCardId);
|
||||
return {
|
||||
title: method?.label ?? da.confirmModal.title,
|
||||
description: method?.supportText ?? da.confirmModal.description,
|
||||
nextButtonText: da.addApproach.nextButtonText,
|
||||
nextButtonText: alreadySelected
|
||||
? da.removeApproach.nextButtonText
|
||||
: da.addApproach.nextButtonText,
|
||||
};
|
||||
})()
|
||||
: {
|
||||
@@ -174,6 +261,7 @@ export function DecisionApproachesScreen() {
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<CreateFlowTwoColumnSelectShell
|
||||
contentTopBelowMd="space-800"
|
||||
lgVerticalAlign="start"
|
||||
@@ -205,7 +293,19 @@ export function DecisionApproachesScreen() {
|
||||
toggleLabel={da.cardStack.toggleSeeAll}
|
||||
showLessLabel={da.cardStack.toggleShowLess}
|
||||
title=""
|
||||
description=""
|
||||
description={
|
||||
expanded ? (
|
||||
<>
|
||||
{da.cardStack.expandedStackDescriptionBefore}
|
||||
<InlineTextButton onClick={handleOpenAddWizard}>
|
||||
{da.sidebar.descriptionLinkLabel}
|
||||
</InlineTextButton>
|
||||
{da.cardStack.expandedStackDescriptionAfter}
|
||||
</>
|
||||
) : (
|
||||
""
|
||||
)
|
||||
}
|
||||
layout="singleStack"
|
||||
compactRecommendedLimit={5}
|
||||
compactCardIds={compactCardIds}
|
||||
@@ -217,7 +317,7 @@ export function DecisionApproachesScreen() {
|
||||
<Create
|
||||
isOpen={createModalOpen}
|
||||
onClose={handleCreateModalClose}
|
||||
onNext={handleCreateModalConfirm}
|
||||
onNext={handleCreateModalPrimary}
|
||||
title={modalConfig.title}
|
||||
description={modalConfig.description}
|
||||
nextButtonText={modalConfig.nextButtonText}
|
||||
@@ -225,13 +325,31 @@ export function DecisionApproachesScreen() {
|
||||
backdropVariant="blurredYellow"
|
||||
>
|
||||
{pendingCardId && pendingDraft ? (
|
||||
<DecisionApproachEditFields
|
||||
key={pendingCardId}
|
||||
value={pendingDraft}
|
||||
onChange={handleDraftChange}
|
||||
/>
|
||||
isCustomMethodCardId(
|
||||
pendingCardId,
|
||||
state.customMethodCardMetaById,
|
||||
) ? (
|
||||
<CustomMethodCardModalBody
|
||||
key={pendingCardId}
|
||||
cardId={pendingCardId}
|
||||
blocksById={state.customMethodCardFieldBlocksById}
|
||||
onFieldBlocksChange={onCustomFieldBlocksChange}
|
||||
/>
|
||||
) : (
|
||||
<DecisionApproachEditFields
|
||||
key={pendingCardId}
|
||||
value={pendingDraft}
|
||||
onChange={handleDraftChange}
|
||||
/>
|
||||
)
|
||||
) : null}
|
||||
</Create>
|
||||
</CreateFlowTwoColumnSelectShell>
|
||||
<CustomMethodCardWizard
|
||||
isOpen={addCustomWizardOpen}
|
||||
onClose={handleCloseAddWizard}
|
||||
onFinalize={handleFinalizeCustomCard}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
* including step types, state management, and context interfaces.
|
||||
*/
|
||||
|
||||
import type { CustomMethodCardFieldBlock } from "../../../lib/create/customMethodCardFieldBlocks";
|
||||
import type { MethodFacetApiSectionId } from "../../../lib/create/customRuleFacets";
|
||||
|
||||
/**
|
||||
@@ -162,6 +163,19 @@ export interface CreateFlowState {
|
||||
string,
|
||||
ConflictManagementDetailEntry
|
||||
>;
|
||||
/**
|
||||
* Labels for user-authored method cards (UUID ids) added via the custom-method-card wizard.
|
||||
* Preset rows resolve from messages JSON; these entries supply title/support for publish + final-review.
|
||||
*/
|
||||
customMethodCardMetaById?: Record<
|
||||
string,
|
||||
{ label: string; supportText: string }
|
||||
>;
|
||||
/**
|
||||
* Custom data-field templates authored in the custom-method-card wizard (step 3).
|
||||
* Keyed by the same UUID as `customMethodCardMetaById` for that card.
|
||||
*/
|
||||
customMethodCardFieldBlocksById?: Record<string, CustomMethodCardFieldBlock[]>;
|
||||
/**
|
||||
* Set when a user picks a template (Customize or Use without changes) before
|
||||
* completing the community stage. The community-review screen consumes this
|
||||
|
||||
Reference in New Issue
Block a user