Open each core value as a card of meaning and signals steps, so groups write their own words from an empty field with Generate draft, skip values without blocking the flow, and Customize without keeping helper text as the description.
This commit is contained in:
1 parent
2a7b9e6da7
commit
64a542f456
25 files changed
+877
-354
No files matched your search
@@ -1065,35 +1065,60 @@ function CreateFlowLayoutContent({
|
||||
</div>
|
||||
) : showCustomRuleFooterConfirm &&
|
||||
customRuleConfirmFooter ? (
|
||||
<Button
|
||||
buttonType="filled"
|
||||
palette="default"
|
||||
size="xsmall"
|
||||
disabled={
|
||||
isPublishing ||
|
||||
customRuleConfirmFooter.selectionIds(state).length === 0
|
||||
}
|
||||
className={CREATE_FLOW_FOOTER_BUTTON_CLASS}
|
||||
onClick={() => {
|
||||
const cf = customRuleConfirmFooter;
|
||||
const facet = methodCardFacetSectionForConfirmStep(cf.step);
|
||||
if (facet != null && cf.selectionIds(state).length > 0) {
|
||||
setMethodSectionsPinCommitted(facet, true);
|
||||
<div className="flex flex-shrink-0 items-center gap-3 md:gap-4">
|
||||
{customRuleConfirmFooter.step === "core-values" ? (
|
||||
<Button
|
||||
buttonType="ghost"
|
||||
palette="default"
|
||||
size="xsmall"
|
||||
disabled={isPublishing}
|
||||
className={CREATE_FLOW_FOOTER_BUTTON_ON_DARK_CLASS}
|
||||
onClick={() => {
|
||||
if (reviewReturnTarget) {
|
||||
router.push(
|
||||
createFlowStepPathAfterStrippingReviewReturn(
|
||||
reviewReturnTarget,
|
||||
searchParams,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
goToNextStep();
|
||||
}}
|
||||
>
|
||||
{footer.skipForNow}
|
||||
</Button>
|
||||
) : null}
|
||||
<Button
|
||||
buttonType="filled"
|
||||
palette="default"
|
||||
size="xsmall"
|
||||
disabled={
|
||||
isPublishing ||
|
||||
customRuleConfirmFooter.selectionIds(state).length === 0
|
||||
}
|
||||
if (reviewReturnTarget) {
|
||||
router.push(
|
||||
createFlowStepPathAfterStrippingReviewReturn(
|
||||
reviewReturnTarget,
|
||||
searchParams,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
goToNextStep();
|
||||
}}
|
||||
>
|
||||
{footer[customRuleConfirmFooter.footerMessageKey]}
|
||||
</Button>
|
||||
className={CREATE_FLOW_FOOTER_BUTTON_CLASS}
|
||||
onClick={() => {
|
||||
const cf = customRuleConfirmFooter;
|
||||
const facet = methodCardFacetSectionForConfirmStep(cf.step);
|
||||
if (facet != null && cf.selectionIds(state).length > 0) {
|
||||
setMethodSectionsPinCommitted(facet, true);
|
||||
}
|
||||
if (reviewReturnTarget) {
|
||||
router.push(
|
||||
createFlowStepPathAfterStrippingReviewReturn(
|
||||
reviewReturnTarget,
|
||||
searchParams,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
goToNextStep();
|
||||
}}
|
||||
>
|
||||
{footer[customRuleConfirmFooter.footerMessageKey]}
|
||||
</Button>
|
||||
</div>
|
||||
) : isConfirmStakeholdersManagePublished ? (
|
||||
<Button
|
||||
buttonType="filled"
|
||||
|
||||
-1
@@ -244,7 +244,6 @@ const CustomMethodCardWizardContainer = memo<CustomMethodCardWizardProps>(
|
||||
titleTrim.length > 0 &&
|
||||
titleTrim.length <= CUSTOM_METHOD_CARD_WIZARD_MAX_FIELD_CHARS;
|
||||
const descriptionOk =
|
||||
descriptionTrim.length > 0 &&
|
||||
descriptionTrim.length <= CUSTOM_METHOD_CARD_WIZARD_MAX_DESCRIPTION_CHARS;
|
||||
if (wizardStep === 1) return titleOk;
|
||||
if (wizardStep === 2) return descriptionOk;
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* Final-review chip modal: **Core values** and **method** facets share the
|
||||
* kebab → **Duplicate** (values only when under the cap) / **Remove** pattern
|
||||
* from the create-card facet modals (`Create` +
|
||||
* {@link buildCustomRuleModalKebabMenu}), plus a footer **Remove** when the
|
||||
* Final-review chip modal: **Core values** use {@link ValueDialog} (Deselect +
|
||||
* Cancel + Save; kebab Customize / Duplicate under the cap). **Method** facets
|
||||
* keep `Create` with kebab Duplicate/Remove plus a footer **Remove** when the
|
||||
* chip is already in the selection. Values and method chips also offer
|
||||
* **Customize**, which opens {@link CustomMethodCardWizard} prefilled from the
|
||||
* chip. Fields are editable on open; Save persists body edits without renaming.
|
||||
@@ -22,10 +21,10 @@ import { useMessages, useTranslation } from "../../../contexts/MessagesContext";
|
||||
import {
|
||||
CommunicationMethodEditFields,
|
||||
ConflictManagementEditFields,
|
||||
CoreValueEditFields,
|
||||
DecisionApproachEditFields,
|
||||
MembershipMethodEditFields,
|
||||
} from "./methodEditFields";
|
||||
import { ValueDialog } from "./ValueDialog";
|
||||
import CustomMethodCardModalBody from "./CustomMethodCardModalBody";
|
||||
import { buildCustomRuleModalKebabMenu } from "./customRuleModalKebabMenu";
|
||||
import { useDiscardCustomizeConfirm } from "../hooks/useDiscardCustomizeConfirm";
|
||||
@@ -34,6 +33,7 @@ import {
|
||||
communicationPresetFor,
|
||||
conflictManagementPresetFor,
|
||||
coreValuePresetFor,
|
||||
coreValuePresetForLabel,
|
||||
decisionApproachPresetFor,
|
||||
emptyCommunicationDetails,
|
||||
emptyConflictManagementDetails,
|
||||
@@ -485,8 +485,7 @@ export function FinalReviewChipEditModal({
|
||||
buildMethodCardWizardInitialValues({
|
||||
cardId: target.overrideKey,
|
||||
fallbackTitle: target.chipLabel,
|
||||
fallbackDescription:
|
||||
value.supportText?.trim() || tCv.detailModal.subtitle,
|
||||
fallbackDescription: value.supportText?.trim() || "",
|
||||
meta: {},
|
||||
persistedBlocks: state.customMethodCardFieldBlocksById,
|
||||
draftFieldBlocks: null,
|
||||
@@ -559,7 +558,6 @@ export function FinalReviewChipEditModal({
|
||||
tComm.sectionHeadings,
|
||||
tCv.detailModal.meaningLabel,
|
||||
tCv.detailModal.signalsLabel,
|
||||
tCv.detailModal.subtitle,
|
||||
tDa.sectionHeadings,
|
||||
tMem.sectionHeadings,
|
||||
target,
|
||||
@@ -730,8 +728,6 @@ export function FinalReviewChipEditModal({
|
||||
(state.selectedCoreValueIds ?? []).length >= 5
|
||||
? undefined
|
||||
: handleDuplicateCoreValue,
|
||||
showRemove: true,
|
||||
onRemove: handleRemoveCoreValueFromModal,
|
||||
});
|
||||
}
|
||||
if (!isMethodFacetGroup(target.groupKey)) return [];
|
||||
@@ -744,7 +740,6 @@ export function FinalReviewChipEditModal({
|
||||
}, [
|
||||
handleCustomize,
|
||||
handleDuplicateCoreValue,
|
||||
handleRemoveCoreValueFromModal,
|
||||
handleRemoveSelectedFromModal,
|
||||
isChipInSelection,
|
||||
modalKebabMenu,
|
||||
@@ -1080,12 +1075,61 @@ export function FinalReviewChipEditModal({
|
||||
|
||||
const showFooterRemove =
|
||||
target != null &&
|
||||
(target.groupKey === "coreValues" || isChipInSelection);
|
||||
target.groupKey !== "coreValues" &&
|
||||
isChipInSelection;
|
||||
|
||||
const isCoreValuesTarget = target?.groupKey === "coreValues";
|
||||
const coreDraftValue =
|
||||
draft?.groupKey === "coreValues" ? draft.value : { meaning: "", signals: "" };
|
||||
|
||||
return (
|
||||
<>
|
||||
<ValueDialog
|
||||
isOpen={isOpen && !addCustomWizardOpen && isCoreValuesTarget}
|
||||
valueLabel={target?.chipLabel ?? ""}
|
||||
session="editing"
|
||||
draft={coreDraftValue}
|
||||
onDraftChange={(value) => setDraft({ groupKey: "coreValues", value })}
|
||||
generateDraft={
|
||||
target
|
||||
? catalogCoreValueDraft(target.overrideKey, target.chipLabel)
|
||||
: undefined
|
||||
}
|
||||
onClose={() => {
|
||||
void handleModalClose();
|
||||
}}
|
||||
onConfirm={handleNext}
|
||||
onDeselect={handleRemoveCoreValueFromModal}
|
||||
nextButtonDisabled={coreCustomizeSaveDisabled}
|
||||
kebabTriggerAriaLabel={modalKebabMenu.triggerAriaLabel}
|
||||
kebabMenuAriaLabel={modalKebabMenu.menuAriaLabel}
|
||||
kebabMenuItems={
|
||||
isCoreValuesTarget && kebabMenuItems.length > 0
|
||||
? kebabMenuItems
|
||||
: undefined
|
||||
}
|
||||
ariaLabel={target?.chipLabel || "Edit chip details"}
|
||||
>
|
||||
{isCoreValuesTarget && modalUsesWizardFieldBlocksBody && target ? (
|
||||
<CustomMethodCardModalBody
|
||||
cardId={target.overrideKey}
|
||||
blocksById={state.customMethodCardFieldBlocksById}
|
||||
blocksOverride={
|
||||
draftFieldBlocks !== null ? draftFieldBlocks : undefined
|
||||
}
|
||||
showPolicyContentLockupWhenNoBlocks={false}
|
||||
onFieldBlocksChange={
|
||||
draftFieldBlocks === null
|
||||
? undefined
|
||||
: handleWizardFieldBlocksChange
|
||||
}
|
||||
/>
|
||||
) : undefined}
|
||||
</ValueDialog>
|
||||
<Create
|
||||
isOpen={isOpen && !addCustomWizardOpen}
|
||||
isOpen={
|
||||
isOpen && !addCustomWizardOpen && !isCoreValuesTarget
|
||||
}
|
||||
onClose={handleModalClose}
|
||||
onBack={handleModalClose}
|
||||
showBackButton={!showFooterRemove}
|
||||
@@ -1113,29 +1157,6 @@ export function FinalReviewChipEditModal({
|
||||
ariaLabel={target?.chipLabel || "Edit chip details"}
|
||||
>
|
||||
<div className="flex flex-col gap-[var(--space-600)] pb-2">
|
||||
{draft?.groupKey === "coreValues" &&
|
||||
target &&
|
||||
(modalUsesWizardFieldBlocksBody ? (
|
||||
<CustomMethodCardModalBody
|
||||
cardId={target.overrideKey}
|
||||
blocksById={state.customMethodCardFieldBlocksById}
|
||||
blocksOverride={
|
||||
draftFieldBlocks !== null ? draftFieldBlocks : undefined
|
||||
}
|
||||
showPolicyContentLockupWhenNoBlocks={false}
|
||||
onFieldBlocksChange={
|
||||
draftFieldBlocks === null
|
||||
? undefined
|
||||
: handleWizardFieldBlocksChange
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<CoreValueEditFields
|
||||
value={draft.value}
|
||||
onChange={(value) => setDraft({ groupKey: "coreValues", value })}
|
||||
generateDraft={coreValuePresetFor(target.overrideKey)}
|
||||
/>
|
||||
))}
|
||||
{draft?.groupKey === "communication" &&
|
||||
target &&
|
||||
(modalUsesWizardFieldBlocksBody ? (
|
||||
@@ -1451,3 +1472,12 @@ function findMethodSupportText(
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function catalogCoreValueDraft(
|
||||
chipId: string,
|
||||
label: string,
|
||||
): CoreValueDetailEntry {
|
||||
const fromId = coreValuePresetFor(chipId);
|
||||
if (fromId.meaning.trim() || fromId.signals.trim()) return fromId;
|
||||
return coreValuePresetForLabel(label);
|
||||
}
|
||||
@@ -16,6 +16,8 @@ export interface ModalTextAreaFieldProps {
|
||||
label: string;
|
||||
/** Show the help "?" icon next to the label. Off until tooltip behavior ships. */
|
||||
helpIcon?: boolean;
|
||||
/** Hide the label when the modal title already asks the question. */
|
||||
hideLabel?: boolean;
|
||||
/** Current text value. */
|
||||
value: string;
|
||||
/** Fired on every change with the new value (no event). */
|
||||
@@ -35,6 +37,12 @@ export interface ModalTextAreaFieldProps {
|
||||
generateDraftLabel?: string;
|
||||
/** Hint beside Generate draft. */
|
||||
generateDraftHint?: string;
|
||||
/**
|
||||
* Shown under a filled field instead of Generate draft (picked value
|
||||
* field view).
|
||||
*/
|
||||
filledHint?: string;
|
||||
autoFocus?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
@@ -63,6 +71,7 @@ function PlusGlyph() {
|
||||
function ModalTextAreaFieldComponent({
|
||||
label,
|
||||
helpIcon = false,
|
||||
hideLabel = false,
|
||||
value,
|
||||
onChange,
|
||||
rows = 4,
|
||||
@@ -71,26 +80,34 @@ function ModalTextAreaFieldComponent({
|
||||
generateDraftText,
|
||||
generateDraftLabel,
|
||||
generateDraftHint,
|
||||
filledHint,
|
||||
autoFocus = false,
|
||||
className = "",
|
||||
}: ModalTextAreaFieldProps) {
|
||||
const labelId = useId();
|
||||
const draftSource = generateDraftText?.trim() ?? "";
|
||||
const filled = value.trim().length > 0;
|
||||
const showFilledHint = filled && Boolean(filledHint) && !disabled;
|
||||
const showGenerateDraft =
|
||||
draftSource.length > 0 && Boolean(generateDraftLabel);
|
||||
const compose = showGenerateDraft;
|
||||
!showFilledHint &&
|
||||
draftSource.length > 0 &&
|
||||
Boolean(generateDraftLabel);
|
||||
const compose = showGenerateDraft || showFilledHint || hideLabel;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex flex-col ${compose ? "gap-[12px]" : "gap-2"} ${className}`.trim()}
|
||||
>
|
||||
<div id={labelId}>
|
||||
<InputLabel
|
||||
label={label}
|
||||
helpIcon={helpIcon}
|
||||
size="s"
|
||||
palette="default"
|
||||
/>
|
||||
</div>
|
||||
{hideLabel ? null : (
|
||||
<div id={labelId}>
|
||||
<InputLabel
|
||||
label={label}
|
||||
helpIcon={helpIcon}
|
||||
size="s"
|
||||
palette="default"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<TextArea
|
||||
formHeader={false}
|
||||
value={value}
|
||||
@@ -100,7 +117,9 @@ function ModalTextAreaFieldComponent({
|
||||
appearance={compose ? "compose" : "embedded"}
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
aria-labelledby={labelId}
|
||||
autoFocus={autoFocus}
|
||||
aria-label={hideLabel ? label : undefined}
|
||||
aria-labelledby={hideLabel ? undefined : labelId}
|
||||
/>
|
||||
{showGenerateDraft && !disabled ? (
|
||||
<div className="flex items-center">
|
||||
@@ -119,6 +138,11 @@ function ModalTextAreaFieldComponent({
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
{showFilledHint ? (
|
||||
<p className="text-[length:var(--sizing-300)] leading-normal text-[color:var(--color-content-default-tertiary,#b4b4b4)]">
|
||||
{filledHint}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* Figma: Overlay / Modal / Card (24973:1852) with Has Text off and Has Steps
|
||||
* on. A value opens a card of meaning + signals rows; each row opens that
|
||||
* field's Step Modal (23746:49780) with Generate draft on an empty field.
|
||||
*/
|
||||
|
||||
import { memo, useCallback, useEffect, useState, type ReactNode } from "react";
|
||||
import Create from "../../../components/modals/Create";
|
||||
import { useMessages } from "../../../contexts/MessagesContext";
|
||||
import type { ModalHeaderMenuItem } from "../../../components/modals/ModalHeader/ModalHeader.types";
|
||||
import type { CoreValueDetailEntry } from "../types";
|
||||
import ModalTextAreaField from "./ModalTextAreaField";
|
||||
import { ValueDialogStepCard } from "./ValueDialogStepCard";
|
||||
|
||||
export type ValueDialogSession = "pending" | "editing";
|
||||
export type ValueDialogStep = "meaning" | "signals";
|
||||
|
||||
export type ValueDialogProps = {
|
||||
isOpen: boolean;
|
||||
valueLabel: string;
|
||||
session: ValueDialogSession | null;
|
||||
draft: CoreValueDetailEntry;
|
||||
onDraftChange: (_next: CoreValueDetailEntry) => void;
|
||||
generateDraft?: CoreValueDetailEntry;
|
||||
onClose: () => void;
|
||||
onConfirm: () => void;
|
||||
onDeselect?: () => void;
|
||||
nextButtonDisabled?: boolean;
|
||||
kebabTriggerAriaLabel?: string;
|
||||
kebabMenuAriaLabel?: string;
|
||||
kebabMenuItems?: ModalHeaderMenuItem[];
|
||||
ariaLabel?: string;
|
||||
/** Customized field-blocks body. Replaces the meaning/signals step cards. */
|
||||
children?: ReactNode;
|
||||
};
|
||||
|
||||
function ValueDialogComponent({
|
||||
isOpen,
|
||||
valueLabel,
|
||||
session,
|
||||
draft,
|
||||
onDraftChange,
|
||||
generateDraft,
|
||||
onClose,
|
||||
onConfirm,
|
||||
onDeselect,
|
||||
nextButtonDisabled = false,
|
||||
kebabTriggerAriaLabel,
|
||||
kebabMenuAriaLabel,
|
||||
kebabMenuItems,
|
||||
ariaLabel,
|
||||
children,
|
||||
}: ValueDialogProps) {
|
||||
const m = useMessages();
|
||||
const t = m.create.customRule.coreValues.detailModal;
|
||||
const draftField = m.create.draftField;
|
||||
const [activeStep, setActiveStep] = useState<ValueDialogStep | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
setActiveStep(null);
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
const handleRequestClose = useCallback(() => {
|
||||
if (activeStep != null) {
|
||||
setActiveStep(null);
|
||||
return;
|
||||
}
|
||||
onClose();
|
||||
}, [activeStep, onClose]);
|
||||
|
||||
const meaningDone = draft.meaning.trim().length > 0;
|
||||
const signalsDone = draft.signals.trim().length > 0;
|
||||
const useCustomBody = Boolean(children);
|
||||
const showSteps = !useCustomBody && activeStep == null;
|
||||
const editing = session === "editing";
|
||||
const primaryLabel = editing ? t.save : t.addToRule;
|
||||
|
||||
const stepTitle =
|
||||
activeStep === "meaning"
|
||||
? t.meaningStepTitle.replace("{value}", valueLabel)
|
||||
: activeStep === "signals"
|
||||
? t.signalsLabel
|
||||
: valueLabel;
|
||||
const stepDescription =
|
||||
activeStep === "meaning"
|
||||
? t.meaningDescription
|
||||
: activeStep === "signals"
|
||||
? t.signalsStepDescription.replace("{value}", valueLabel)
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<Create
|
||||
isOpen={isOpen}
|
||||
onClose={handleRequestClose}
|
||||
title={stepTitle}
|
||||
description={activeStep != null ? stepDescription : undefined}
|
||||
showBackButton={false}
|
||||
showCancelButton
|
||||
showNextButton
|
||||
showRemoveButton={editing && activeStep == null}
|
||||
onCancel={handleRequestClose}
|
||||
onNext={onConfirm}
|
||||
onRemove={onDeselect}
|
||||
cancelButtonText={t.cancel}
|
||||
nextButtonText={primaryLabel}
|
||||
removeButtonText={t.deselect}
|
||||
nextButtonDisabled={nextButtonDisabled}
|
||||
stepper={false}
|
||||
backdropVariant="blurredYellow"
|
||||
kebabTriggerAriaLabel={kebabTriggerAriaLabel}
|
||||
kebabMenuAriaLabel={kebabMenuAriaLabel}
|
||||
kebabMenuItems={
|
||||
activeStep == null && kebabMenuItems && kebabMenuItems.length > 0
|
||||
? kebabMenuItems
|
||||
: undefined
|
||||
}
|
||||
ariaLabel={ariaLabel || valueLabel}
|
||||
>
|
||||
{useCustomBody ? (
|
||||
children
|
||||
) : showSteps ? (
|
||||
<div className="flex w-full flex-col gap-[8px]">
|
||||
<ValueDialogStepCard
|
||||
stepNumber={1}
|
||||
stepBadgeLabel={t.stepBadge}
|
||||
title={t.meaningLabel}
|
||||
description={
|
||||
meaningDone ? draft.meaning : t.meaningDescription
|
||||
}
|
||||
done={meaningDone}
|
||||
onClick={() => setActiveStep("meaning")}
|
||||
/>
|
||||
<ValueDialogStepCard
|
||||
stepNumber={2}
|
||||
stepBadgeLabel={t.stepBadge}
|
||||
title={t.signalsLabel}
|
||||
description={
|
||||
signalsDone ? draft.signals : t.signalsDescription
|
||||
}
|
||||
done={signalsDone}
|
||||
onClick={() => setActiveStep("signals")}
|
||||
/>
|
||||
</div>
|
||||
) : activeStep === "meaning" ? (
|
||||
<ModalTextAreaField
|
||||
hideLabel
|
||||
label={t.meaningLabel}
|
||||
value={draft.meaning}
|
||||
onChange={(meaning) => onDraftChange({ ...draft, meaning })}
|
||||
placeholder={t.placeholder}
|
||||
generateDraftText={generateDraft?.meaning}
|
||||
generateDraftLabel={draftField.generateDraft}
|
||||
generateDraftHint={t.generateDraftHint}
|
||||
filledHint={t.filledHint}
|
||||
autoFocus
|
||||
/>
|
||||
) : (
|
||||
<ModalTextAreaField
|
||||
hideLabel
|
||||
label={t.signalsLabel}
|
||||
value={draft.signals}
|
||||
onChange={(signals) => onDraftChange({ ...draft, signals })}
|
||||
placeholder={t.placeholder}
|
||||
generateDraftText={generateDraft?.signals}
|
||||
generateDraftLabel={draftField.generateDraft}
|
||||
generateDraftHint={t.generateDraftHint}
|
||||
filledHint={t.filledHint}
|
||||
autoFocus
|
||||
/>
|
||||
)}
|
||||
</Create>
|
||||
);
|
||||
}
|
||||
|
||||
ValueDialogComponent.displayName = "ValueDialog";
|
||||
|
||||
export const ValueDialog = memo(ValueDialogComponent);
|
||||
@@ -0,0 +1,100 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* Figma: ".utility / Step Card" (23742:49583). One row in Overlay / Modal / Card
|
||||
* with Has Steps on — open (step badge) or done (check).
|
||||
*/
|
||||
|
||||
import { memo } from "react";
|
||||
import Icon from "../../../components/asset/icon";
|
||||
|
||||
export type ValueDialogStepCardProps = {
|
||||
stepNumber: number;
|
||||
stepBadgeLabel: string;
|
||||
title: string;
|
||||
description: string;
|
||||
done: boolean;
|
||||
onClick: () => void;
|
||||
};
|
||||
|
||||
function CheckGlyph() {
|
||||
return (
|
||||
<svg
|
||||
aria-hidden
|
||||
width="10"
|
||||
height="10"
|
||||
viewBox="0 0 12 12"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="text-[var(--color-content-invert-primary,white)]"
|
||||
>
|
||||
<path
|
||||
d="M10 3L4.5 8.5L2 6"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function ValueDialogStepCardComponent({
|
||||
stepNumber,
|
||||
stepBadgeLabel,
|
||||
title,
|
||||
description,
|
||||
done,
|
||||
onClick,
|
||||
}: ValueDialogStepCardProps) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
aria-label={title}
|
||||
data-figma-node={done ? "23740:49582" : "23740:49581"}
|
||||
className={`flex w-full shrink-0 items-center gap-[8px] rounded-[12px] border border-solid p-[12px] text-left ${
|
||||
done
|
||||
? "border-[var(--color-border-default-positive-primary,#45ea97)] bg-[var(--color-surface-default-secondary,#141414)]"
|
||||
: "border-[var(--color-border-default-secondary,#303030)] bg-[var(--color-surface-default-secondary,#141414)] hover:border-[var(--color-border-default-brand-primary,#fdfaa8)]"
|
||||
} focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-border-invert-primary)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--color-surface-default-primary)]`}
|
||||
>
|
||||
{done ? (
|
||||
<span
|
||||
aria-hidden
|
||||
className="flex size-4 shrink-0 items-center justify-center rounded-[8px] bg-[var(--color-surface-default-positive-primary,#16c472)]"
|
||||
>
|
||||
<CheckGlyph />
|
||||
</span>
|
||||
) : (
|
||||
<span
|
||||
aria-hidden
|
||||
className="flex w-[48px] shrink-0 items-center justify-center rounded-[12px] bg-[var(--color-surface-default-primary)] py-[4px] text-[length:var(--sizing-250)] font-medium leading-[12px] text-[color:var(--color-content-default-secondary,#c2c2c2)]"
|
||||
>
|
||||
{stepBadgeLabel.replace("{n}", String(stepNumber))}
|
||||
</span>
|
||||
)}
|
||||
<span className="flex min-w-0 flex-1 flex-col items-start gap-[2px]">
|
||||
<span className="w-full text-x-small-paragraph text-[color:var(--color-content-default-primary)]">
|
||||
{title}
|
||||
</span>
|
||||
<span
|
||||
className={`w-full text-[length:var(--sizing-250)] leading-[14px] ${
|
||||
done
|
||||
? "text-[color:var(--color-content-default-secondary,#c2c2c2)]"
|
||||
: "text-[color:var(--color-content-default-tertiary,#a4a4a4)]"
|
||||
}`}
|
||||
>
|
||||
{description}
|
||||
</span>
|
||||
</span>
|
||||
<span className="flex size-4 shrink-0 items-center justify-center text-[color:var(--color-content-default-primary)]">
|
||||
<Icon name="chevron_right" size={16} />
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
ValueDialogStepCardComponent.displayName = "ValueDialogStepCard";
|
||||
|
||||
export const ValueDialogStepCard = memo(ValueDialogStepCardComponent);
|
||||
@@ -1,74 +0,0 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* Controlled meaning/signals field set for a core-value chip. Rendered both
|
||||
* by `core-values` (custom-rule selection step) and `final-review` (chip
|
||||
* edit modal). Holds no state — the parent owns the draft and decides when
|
||||
* to persist (`updateState`) or discard.
|
||||
*/
|
||||
|
||||
import { memo, useCallback } from "react";
|
||||
import { useMessages } from "../../../../contexts/MessagesContext";
|
||||
import ModalTextAreaField from "../ModalTextAreaField";
|
||||
import type { CoreValueDetailEntry } from "../../types";
|
||||
|
||||
export interface CoreValueEditFieldsProps {
|
||||
value: CoreValueDetailEntry;
|
||||
onChange: (_next: CoreValueDetailEntry) => void;
|
||||
/** Disable meaning/signals. Facet and final-review callers omit this so fields stay editable on open. */
|
||||
readOnly?: boolean;
|
||||
/** Catalog copy inserted by Generate draft. */
|
||||
generateDraft?: CoreValueDetailEntry;
|
||||
}
|
||||
|
||||
function CoreValueEditFieldsComponent({
|
||||
value,
|
||||
onChange,
|
||||
readOnly = false,
|
||||
generateDraft,
|
||||
}: CoreValueEditFieldsProps) {
|
||||
const m = useMessages();
|
||||
const t = m.create.customRule.coreValues.detailModal;
|
||||
const draftField = m.create.draftField;
|
||||
|
||||
const patch = useCallback(
|
||||
<K extends keyof CoreValueDetailEntry>(
|
||||
key: K,
|
||||
next: CoreValueDetailEntry[K],
|
||||
) => {
|
||||
onChange({ ...value, [key]: next });
|
||||
},
|
||||
[value, onChange],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-[var(--space-600)] pb-2">
|
||||
<ModalTextAreaField
|
||||
label={t.meaningLabel}
|
||||
value={value.meaning}
|
||||
onChange={(v) => patch("meaning", v)}
|
||||
rows={4}
|
||||
disabled={readOnly}
|
||||
placeholder={draftField.placeholder}
|
||||
generateDraftText={generateDraft?.meaning}
|
||||
generateDraftLabel={draftField.generateDraft}
|
||||
generateDraftHint={draftField.generateDraftHint}
|
||||
/>
|
||||
<ModalTextAreaField
|
||||
label={t.signalsLabel}
|
||||
value={value.signals}
|
||||
onChange={(v) => patch("signals", v)}
|
||||
rows={4}
|
||||
disabled={readOnly}
|
||||
placeholder={draftField.placeholder}
|
||||
generateDraftText={generateDraft?.signals}
|
||||
generateDraftLabel={draftField.generateDraft}
|
||||
generateDraftHint={draftField.generateDraftHint}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
CoreValueEditFieldsComponent.displayName = "CoreValueEditFields";
|
||||
|
||||
export default memo(CoreValueEditFieldsComponent);
|
||||
@@ -1,6 +1,3 @@
|
||||
export { default as CoreValueEditFields } from "./CoreValueEditFields";
|
||||
export type { CoreValueEditFieldsProps } from "./CoreValueEditFields";
|
||||
|
||||
export { default as CommunicationMethodEditFields } from "./CommunicationMethodEditFields";
|
||||
export type { CommunicationMethodEditFieldsProps } from "./CommunicationMethodEditFields";
|
||||
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
import { useState, useEffect, useCallback, useMemo, useRef } from "react";
|
||||
import MultiSelect from "../../../../components/controls/MultiSelect";
|
||||
import type { ChipOption } from "../../../../components/controls/MultiSelect/MultiSelect.types";
|
||||
import Create from "../../../../components/modals/Create";
|
||||
import ContentLockup from "../../../../components/type/ContentLockup";
|
||||
import InlineTextButton from "../../../../components/buttons/InlineTextButton";
|
||||
import { useMessages } from "../../../../contexts/MessagesContext";
|
||||
import { buildCoreValueChipOptionsFromDraft } from "../../../../../lib/create/coreValueChipOptionsFromDraft";
|
||||
import { useCreateFlow } from "../../context/CreateFlowContext";
|
||||
@@ -16,8 +15,8 @@ import type {
|
||||
} from "../../types";
|
||||
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
|
||||
import { CreateFlowTwoColumnSelectShell } from "../../components/CreateFlowTwoColumnSelectShell";
|
||||
import { CoreValueEditFields } from "../../components/methodEditFields";
|
||||
import CustomMethodCardModalBody from "../../components/CustomMethodCardModalBody";
|
||||
import { ValueDialog } from "../../components/ValueDialog";
|
||||
import CustomMethodCardWizard from "../../components/CustomMethodCardWizard";
|
||||
import { usesWizardFieldBlocksModalBody } from "../../../../../lib/create/usesWizardFieldBlocksModalBody";
|
||||
import { buildCustomRuleModalKebabMenu } from "../../components/customRuleModalKebabMenu";
|
||||
@@ -49,8 +48,8 @@ const MAX_CORE_VALUES = MAX_SELECTED_CORE_VALUES;
|
||||
* - `editing` — chip already exists & is selected; modal reopened to
|
||||
* tweak meaning/signals. Dismiss = no-op (chip stays as-is).
|
||||
*
|
||||
* **Add value** (and the header "add" link) opens an empty custom-policy
|
||||
* wizard. Finalize writes a selected chip named from the wizard title.
|
||||
* The header **add your own** link opens an empty custom-policy wizard.
|
||||
* Finalize writes a selected chip named from the wizard title.
|
||||
*/
|
||||
type ModalSession = "pending" | "customPending" | "editing";
|
||||
|
||||
@@ -98,7 +97,7 @@ function chipOptionsToSnapshotRows(
|
||||
|
||||
const EMPTY_DETAIL: CoreValueDetailEntry = { meaning: "", signals: "" };
|
||||
|
||||
/** Create Custom — Core Values (Figma `20264:68378`). Up to six selections; preset list + custom chips. */
|
||||
/** Create Custom — Core Values (Figma `20264:68378`). Overlay / Modal / Card `24973:1852`. Up to six selections; preset list + custom chips. */
|
||||
export function CoreValuesSelectScreen() {
|
||||
const m = useMessages();
|
||||
const cv = m.create.customRule.coreValues;
|
||||
@@ -448,8 +447,6 @@ export function CoreValuesSelectScreen() {
|
||||
}),
|
||||
);
|
||||
|
||||
const showFooterPrimary = Boolean(modalSession);
|
||||
|
||||
const handleCustomize = useCallback(() => {
|
||||
if (!activeModalChipId) return;
|
||||
markCreateFlowInteraction();
|
||||
@@ -457,8 +454,7 @@ export function CoreValuesSelectScreen() {
|
||||
buildMethodCardWizardInitialValues({
|
||||
cardId: activeModalChipId,
|
||||
fallbackTitle: modalChipLabel,
|
||||
fallbackDescription:
|
||||
draft.supportText?.trim() || cv.detailModal.subtitle,
|
||||
fallbackDescription: draft.supportText?.trim() || "",
|
||||
meta: {},
|
||||
persistedBlocks: state.customMethodCardFieldBlocksById,
|
||||
draftFieldBlocks,
|
||||
@@ -478,7 +474,6 @@ export function CoreValuesSelectScreen() {
|
||||
activeModalChipId,
|
||||
cv.detailModal.meaningLabel,
|
||||
cv.detailModal.signalsLabel,
|
||||
cv.detailModal.subtitle,
|
||||
draft,
|
||||
draftFieldBlocks,
|
||||
markCreateFlowInteraction,
|
||||
@@ -606,15 +601,12 @@ export function CoreValuesSelectScreen() {
|
||||
atSelectionLimit
|
||||
? undefined
|
||||
: handleDuplicateCoreChip,
|
||||
showRemove: modalSession === "editing",
|
||||
onRemove: handleRemoveFromKebab,
|
||||
});
|
||||
}, [
|
||||
activeModalChipId,
|
||||
atSelectionLimit,
|
||||
handleCustomize,
|
||||
handleDuplicateCoreChip,
|
||||
handleRemoveFromKebab,
|
||||
modalKebabMenu,
|
||||
modalSession,
|
||||
state.editingPublishedRuleId,
|
||||
@@ -677,7 +669,7 @@ export function CoreValuesSelectScreen() {
|
||||
queueMicrotask(() => {
|
||||
syncCoreValuesToDraft(next);
|
||||
// Both branches treat the chip as a brand-new draft until the
|
||||
// user confirms via Add Value — dismissal removes it.
|
||||
// user confirms via Add to Rule — dismissal removes it.
|
||||
openModal(chipId, "customPending", value);
|
||||
});
|
||||
return next;
|
||||
@@ -695,26 +687,17 @@ export function CoreValuesSelectScreen() {
|
||||
|
||||
const description = (
|
||||
<>
|
||||
<span className="leading-[1.3] text-[color:var(--color-content-default-tertiary,#b4b4b4)]">
|
||||
{cv.header.descriptionLead}{" "}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
{cv.header.descriptionLead}
|
||||
<InlineTextButton
|
||||
onClick={addHandlers.onAddClick}
|
||||
disabled={atSelectionLimit}
|
||||
className="cursor-pointer font-normal leading-[1.3] text-[color:var(--color-content-default-tertiary,#b4b4b4)] underline decoration-solid underline-offset-[3px] hover:opacity-90 disabled:cursor-not-allowed disabled:no-underline disabled:opacity-60 disabled:hover:opacity-60"
|
||||
>
|
||||
{cv.header.addLink}
|
||||
</button>
|
||||
<span className="leading-[1.3] text-[color:var(--color-content-default-tertiary,#b4b4b4)]">
|
||||
{" "}
|
||||
{cv.header.descriptionTrail}
|
||||
</span>
|
||||
</InlineTextButton>
|
||||
{cv.header.descriptionTrail}
|
||||
</>
|
||||
);
|
||||
|
||||
const detailModal = cv.detailModal;
|
||||
|
||||
return (
|
||||
<>
|
||||
<CreateFlowTwoColumnSelectShell
|
||||
@@ -735,75 +718,54 @@ export function CoreValuesSelectScreen() {
|
||||
onAddClick={addHandlers.onAddClick}
|
||||
onCustomChipConfirm={addHandlers.onCustomChipConfirm}
|
||||
onCustomChipClose={addHandlers.onCustomChipClose}
|
||||
addButton
|
||||
addButtonText={cv.multiSelect.addButtonText}
|
||||
addButton={false}
|
||||
maxSelections={MAX_CORE_VALUES}
|
||||
selectionCountText={selectionCountText}
|
||||
limitReachedAnnouncement={cv.multiSelect.limitReached}
|
||||
/>
|
||||
|
||||
{detailModal && (
|
||||
<Create
|
||||
isOpen={activeModalChipId !== null && !addCustomWizardOpen}
|
||||
onClose={handleModalDismiss}
|
||||
backdropVariant="blurredYellow"
|
||||
headerContent={
|
||||
<div className="bg-[var(--color-surface-default-primary)] px-[24px] py-[12px] shrink-0">
|
||||
<ContentLockup
|
||||
title={modalChipLabel}
|
||||
description={
|
||||
draft.supportText?.trim() || detailModal.subtitle
|
||||
}
|
||||
variant="modal"
|
||||
alignment="left"
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
showBackButton={false}
|
||||
showNextButton={showFooterPrimary}
|
||||
showRemoveButton={modalSession === "editing"}
|
||||
onRemove={handleRemoveFromKebab}
|
||||
onNext={handleModalConfirm}
|
||||
nextButtonText={
|
||||
modalSession === "editing"
|
||||
? modalKebabMenu.saveEdits
|
||||
: detailModal.addValueButton
|
||||
}
|
||||
kebabTriggerAriaLabel={modalKebabMenu.triggerAriaLabel}
|
||||
kebabMenuAriaLabel={modalKebabMenu.menuAriaLabel}
|
||||
kebabMenuItems={
|
||||
kebabMenuItems.length > 0 ? kebabMenuItems : undefined
|
||||
}
|
||||
ariaLabel={modalChipLabel || "Core value details"}
|
||||
>
|
||||
{modalUsesWizardFieldBlocksBody && activeModalChipId ? (
|
||||
<CustomMethodCardModalBody
|
||||
cardId={activeModalChipId}
|
||||
blocksById={state.customMethodCardFieldBlocksById}
|
||||
blocksOverride={
|
||||
draftFieldBlocks !== null ? draftFieldBlocks : undefined
|
||||
}
|
||||
showPolicyContentLockupWhenNoBlocks={false}
|
||||
onFieldBlocksChange={
|
||||
draftFieldBlocks === null
|
||||
? undefined
|
||||
: (next) => {
|
||||
setDraftFieldBlocks(next);
|
||||
setDraft(
|
||||
coreValueDetailsFromWizardFieldBlocks(next, draft),
|
||||
);
|
||||
}
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<CoreValueEditFields
|
||||
value={draft}
|
||||
onChange={handleDraftChange}
|
||||
generateDraft={getPresetTexts(modalChipLabel)}
|
||||
/>
|
||||
)}
|
||||
</Create>
|
||||
)}
|
||||
<ValueDialog
|
||||
isOpen={activeModalChipId !== null && !addCustomWizardOpen}
|
||||
valueLabel={modalChipLabel}
|
||||
session={modalSession === "editing" ? "editing" : "pending"}
|
||||
draft={draft}
|
||||
onDraftChange={handleDraftChange}
|
||||
generateDraft={getPresetTexts(modalChipLabel)}
|
||||
onClose={() => {
|
||||
void handleModalDismiss();
|
||||
}}
|
||||
onConfirm={handleModalConfirm}
|
||||
onDeselect={
|
||||
modalSession === "editing" ? handleRemoveFromKebab : undefined
|
||||
}
|
||||
kebabTriggerAriaLabel={modalKebabMenu.triggerAriaLabel}
|
||||
kebabMenuAriaLabel={modalKebabMenu.menuAriaLabel}
|
||||
kebabMenuItems={
|
||||
kebabMenuItems.length > 0 ? kebabMenuItems : undefined
|
||||
}
|
||||
ariaLabel={modalChipLabel || "Core value details"}
|
||||
>
|
||||
{modalUsesWizardFieldBlocksBody && activeModalChipId ? (
|
||||
<CustomMethodCardModalBody
|
||||
cardId={activeModalChipId}
|
||||
blocksById={state.customMethodCardFieldBlocksById}
|
||||
blocksOverride={
|
||||
draftFieldBlocks !== null ? draftFieldBlocks : undefined
|
||||
}
|
||||
showPolicyContentLockupWhenNoBlocks={false}
|
||||
onFieldBlocksChange={
|
||||
draftFieldBlocks === null
|
||||
? undefined
|
||||
: (next) => {
|
||||
setDraftFieldBlocks(next);
|
||||
setDraft(
|
||||
coreValueDetailsFromWizardFieldBlocks(next, draft),
|
||||
);
|
||||
}
|
||||
}
|
||||
/>
|
||||
) : undefined}
|
||||
</ValueDialog>
|
||||
</CreateFlowTwoColumnSelectShell>
|
||||
<CustomMethodCardWizard
|
||||
isOpen={addCustomWizardOpen}
|
||||
|
||||
Reference in new issue
Block a user