Open each core value as a card of meaning and signals steps #98
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}
|
||||
|
||||
@@ -63,7 +63,7 @@ function MultiSelectView({
|
||||
|
||||
{!formHeader || !label ? (
|
||||
headerHelper && typeof headerHelper === "string" ? (
|
||||
<p className="w-full text-x-small-paragraph text-[color:var(--color-content-default-tertiary)]">
|
||||
<p className="w-full text-right text-x-small-paragraph text-[color:var(--color-content-default-tertiary)]">
|
||||
{headerHelper}
|
||||
</p>
|
||||
) : null
|
||||
|
||||
@@ -21,11 +21,14 @@ const CreateContainer = memo<CreateProps>(
|
||||
footerClassName,
|
||||
showBackButton = true,
|
||||
showNextButton = true,
|
||||
showCancelButton = false,
|
||||
showRemoveButton = false,
|
||||
onBack,
|
||||
onNext,
|
||||
onCancel,
|
||||
onRemove,
|
||||
backButtonText = "Back",
|
||||
cancelButtonText,
|
||||
nextButtonText = "Next",
|
||||
removeButtonText,
|
||||
nextButtonDisabled = false,
|
||||
@@ -59,11 +62,14 @@ const CreateContainer = memo<CreateProps>(
|
||||
footerClassName={footerClassName}
|
||||
showBackButton={showBackButton}
|
||||
showNextButton={showNextButton}
|
||||
showCancelButton={showCancelButton}
|
||||
showRemoveButton={showRemoveButton}
|
||||
onBack={onBack}
|
||||
onNext={onNext}
|
||||
onCancel={onCancel}
|
||||
onRemove={onRemove}
|
||||
backButtonText={backButtonText}
|
||||
cancelButtonText={cancelButtonText}
|
||||
nextButtonText={nextButtonText}
|
||||
removeButtonText={removeButtonText}
|
||||
nextButtonDisabled={nextButtonDisabled}
|
||||
|
||||
@@ -16,11 +16,14 @@ export interface CreateProps {
|
||||
footerClassName?: string;
|
||||
showBackButton?: boolean;
|
||||
showNextButton?: boolean;
|
||||
showCancelButton?: boolean;
|
||||
showRemoveButton?: boolean;
|
||||
onBack?: () => void;
|
||||
onNext?: () => void;
|
||||
onCancel?: () => void;
|
||||
onRemove?: () => void;
|
||||
backButtonText?: string;
|
||||
cancelButtonText?: string;
|
||||
nextButtonText?: string;
|
||||
removeButtonText?: string;
|
||||
nextButtonDisabled?: boolean;
|
||||
@@ -61,11 +64,14 @@ export interface CreateViewProps {
|
||||
footerClassName?: string;
|
||||
showBackButton: boolean;
|
||||
showNextButton: boolean;
|
||||
showCancelButton: boolean;
|
||||
showRemoveButton: boolean;
|
||||
onBack?: () => void;
|
||||
onNext?: () => void;
|
||||
onCancel?: () => void;
|
||||
onRemove?: () => void;
|
||||
backButtonText: string;
|
||||
cancelButtonText?: string;
|
||||
nextButtonText: string;
|
||||
removeButtonText?: string;
|
||||
nextButtonDisabled: boolean;
|
||||
|
||||
@@ -17,11 +17,14 @@ export function CreateView({
|
||||
footerClassName,
|
||||
showBackButton,
|
||||
showNextButton,
|
||||
showCancelButton,
|
||||
showRemoveButton,
|
||||
onBack,
|
||||
onNext,
|
||||
onCancel,
|
||||
onRemove,
|
||||
backButtonText,
|
||||
cancelButtonText,
|
||||
nextButtonText,
|
||||
removeButtonText,
|
||||
nextButtonDisabled,
|
||||
@@ -79,11 +82,14 @@ export function CreateView({
|
||||
<ModalFooter
|
||||
showBackButton={showBackButton}
|
||||
showNextButton={showNextButton}
|
||||
showCancelButton={showCancelButton}
|
||||
showRemoveButton={showRemoveButton}
|
||||
onBack={onBack}
|
||||
onNext={onNext}
|
||||
onCancel={onCancel}
|
||||
onRemove={onRemove}
|
||||
backButtonText={backButtonText}
|
||||
cancelButtonText={cancelButtonText}
|
||||
nextButtonText={nextButtonText}
|
||||
removeButtonText={removeButtonText}
|
||||
nextButtonDisabled={nextButtonDisabled}
|
||||
|
||||
@@ -13,6 +13,7 @@ import type { ModalFooterProps } from "./ModalFooter.types";
|
||||
const ModalFooterContainer = memo<ModalFooterProps>((props) => {
|
||||
const t = useTranslation("common");
|
||||
const resolvedBackText = props.backButtonText ?? t("buttons.back");
|
||||
const resolvedCancelText = props.cancelButtonText ?? t("buttons.cancel");
|
||||
const resolvedNextText = props.nextButtonText ?? t("buttons.next");
|
||||
const resolvedRemoveText = props.removeButtonText ?? t("buttons.remove");
|
||||
|
||||
@@ -20,6 +21,7 @@ const ModalFooterContainer = memo<ModalFooterProps>((props) => {
|
||||
<ModalFooterView
|
||||
{...props}
|
||||
backButtonText={resolvedBackText}
|
||||
cancelButtonText={resolvedCancelText}
|
||||
nextButtonText={resolvedNextText}
|
||||
removeButtonText={resolvedRemoveText}
|
||||
/>
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
export interface ModalFooterProps {
|
||||
showBackButton?: boolean;
|
||||
showNextButton?: boolean;
|
||||
/**
|
||||
* Outline **Cancel** in the trailing group, left of Next (Overlay / Modal / Card).
|
||||
*/
|
||||
showCancelButton?: boolean;
|
||||
/**
|
||||
* Danger **Remove** in the left footer slot (selected create-flow modules).
|
||||
* Takes the left slot instead of Back when both would otherwise show.
|
||||
@@ -8,11 +12,16 @@ export interface ModalFooterProps {
|
||||
showRemoveButton?: boolean;
|
||||
onBack?: () => void;
|
||||
onNext?: () => void;
|
||||
onCancel?: () => void;
|
||||
onRemove?: () => void;
|
||||
/**
|
||||
* Custom back button text. If not provided, uses localized "Back" from common.json
|
||||
*/
|
||||
backButtonText?: string;
|
||||
/**
|
||||
* Custom cancel button text. If not provided, uses localized "Cancel" from common.json
|
||||
*/
|
||||
cancelButtonText?: string;
|
||||
/**
|
||||
* Custom next button text. If not provided, uses localized "Next" from common.json
|
||||
*/
|
||||
|
||||
@@ -7,11 +7,14 @@ import type { ModalFooterProps } from "./ModalFooter.types";
|
||||
export function ModalFooterView({
|
||||
showBackButton = false,
|
||||
showNextButton = false,
|
||||
showCancelButton = false,
|
||||
showRemoveButton = false,
|
||||
onBack,
|
||||
onNext,
|
||||
onCancel,
|
||||
onRemove,
|
||||
backButtonText,
|
||||
cancelButtonText,
|
||||
nextButtonText,
|
||||
removeButtonText,
|
||||
nextButtonDisabled = false,
|
||||
@@ -26,6 +29,7 @@ export function ModalFooterView({
|
||||
? stepperProp
|
||||
: currentStep !== undefined && totalSteps !== undefined;
|
||||
const showStartBack = showBackButton && !showRemoveButton;
|
||||
const showTrailing = showCancelButton || showNextButton;
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -64,17 +68,29 @@ export function ModalFooterView({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showNextButton && (
|
||||
<div className="absolute right-[16px] top-[12px]">
|
||||
<Button
|
||||
buttonType="filled"
|
||||
palette="default"
|
||||
size="medium"
|
||||
onClick={onNext}
|
||||
disabled={nextButtonDisabled}
|
||||
>
|
||||
{nextButtonText}
|
||||
</Button>
|
||||
{showTrailing && (
|
||||
<div className="absolute right-[16px] top-[12px] flex items-center gap-[8px]">
|
||||
{showCancelButton ? (
|
||||
<Button
|
||||
buttonType="outline"
|
||||
palette="default"
|
||||
size="medium"
|
||||
onClick={onCancel}
|
||||
>
|
||||
{cancelButtonText}
|
||||
</Button>
|
||||
) : null}
|
||||
{showNextButton ? (
|
||||
<Button
|
||||
buttonType="filled"
|
||||
palette="default"
|
||||
size="medium"
|
||||
onClick={onNext}
|
||||
disabled={nextButtonDisabled}
|
||||
>
|
||||
{nextButtonText}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"learnMore": "Learn more",
|
||||
"askOrganizer": "Ask an organizer",
|
||||
"back": "Back",
|
||||
"cancel": "Cancel",
|
||||
"next": "Next",
|
||||
"remove": "Remove",
|
||||
"finish": "Finish"
|
||||
|
||||
@@ -1,20 +1,32 @@
|
||||
{
|
||||
"header": {
|
||||
"title": "Choose up to six core values",
|
||||
"descriptionLead": "What does your group hold most dear? You can also combine or",
|
||||
"addLink": "add",
|
||||
"descriptionTrail": "new values to the list."
|
||||
"descriptionLead": "Pick what your community holds most dear or ",
|
||||
"addLink": "add your own",
|
||||
"descriptionTrail": "."
|
||||
},
|
||||
"multiSelect": {
|
||||
"addButtonText": "Add value",
|
||||
"selectionCount": "{count} of {max}",
|
||||
"selectionCount": "{count} of {max} picked",
|
||||
"limitReached": "Six of six values selected. Remaining values are unavailable until one is removed."
|
||||
},
|
||||
"detailModal": {
|
||||
"subtitle": "Edit or add to this description to say what this value means to your group.",
|
||||
"meaningLabel": "What does this value mean to your group?",
|
||||
"signalsLabel": "Signals of Violation",
|
||||
"addValueButton": "Add Value",
|
||||
"meaningLabel": "What does this value mean to your community?",
|
||||
"meaningDescription": "How does your community interpret the meaning of this value?",
|
||||
"meaningStepTitle": "What does {value} mean to your community?",
|
||||
"signalsLabel": "Signals of violation",
|
||||
"signalsDescription": "How do we know when we are not living up to this value?",
|
||||
"signalsStepDescription": "How do we know when we are not living up to {value}?",
|
||||
"stepBadge": "Step {n}",
|
||||
"addToRule": "Add to Rule",
|
||||
"deselect": "Deselect",
|
||||
"cancel": "Cancel",
|
||||
"save": "Save",
|
||||
"placeholder": "Write a sentence or two.",
|
||||
"generateDraftHint": "See our first draft for this value",
|
||||
"filledHint": "These are your community's words. Change anything that does not fit.",
|
||||
"addValueButton": "Add to Rule",
|
||||
"customizeValueNameLabel": "Value name",
|
||||
"discardTitle": "Discard unsaved changes?",
|
||||
"discardPendingDescription": "This value will not be added.",
|
||||
|
||||
@@ -4,6 +4,11 @@ import "@testing-library/jest-dom/vitest";
|
||||
import { renderWithProviders, dispatchBeforeUnload } from "../utils/test-utils";
|
||||
import { CoreValuesSelectScreen } from "../../app/(app)/create/screens/select/CoreValuesSelectScreen";
|
||||
|
||||
const MEANING_STEP = "What does this value mean to your community?";
|
||||
const SIGNALS_STEP = "Signals of violation";
|
||||
const ADD_TO_RULE = "Add to Rule";
|
||||
const ADD_OWN = "add your own";
|
||||
|
||||
describe("CoreValuesSelectScreen", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
@@ -13,19 +18,50 @@ describe("CoreValuesSelectScreen", () => {
|
||||
fireEvent.click(await screen.findByRole("button", { name: "Discard" }));
|
||||
}
|
||||
|
||||
async function editMeaningInOpenDialog(next: string) {
|
||||
const dialog = await screen.findByRole("dialog");
|
||||
const fields = within(dialog).getAllByRole("textbox");
|
||||
fireEvent.change(fields[0], { target: { value: next } });
|
||||
async function openValueDialog(label = "Accessibility") {
|
||||
fireEvent.click(screen.getByRole("button", { name: `Select ${label}` }));
|
||||
return screen.findByRole("dialog");
|
||||
}
|
||||
|
||||
it("opens core value detail modal when a preset chip is clicked", async () => {
|
||||
renderWithProviders(<CoreValuesSelectScreen />);
|
||||
fireEvent.click(screen.getByText("Accessibility"));
|
||||
async function editMeaningInOpenDialog(next: string) {
|
||||
const dialog = await screen.findByRole("dialog");
|
||||
fireEvent.click(within(dialog).getByRole("button", { name: MEANING_STEP }));
|
||||
fireEvent.change(within(dialog).getByRole("textbox"), {
|
||||
target: { value: next },
|
||||
});
|
||||
}
|
||||
|
||||
async function confirmPendingValue(dialog: HTMLElement) {
|
||||
fireEvent.click(within(dialog).getByRole("button", { name: ADD_TO_RULE }));
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
||||
});
|
||||
}
|
||||
|
||||
it("opens a card of meaning and signals rows when a preset chip is clicked", async () => {
|
||||
renderWithProviders(<CoreValuesSelectScreen />);
|
||||
const dialog = await openValueDialog();
|
||||
expect(within(dialog).getByText("Accessibility")).toBeInTheDocument();
|
||||
expect(
|
||||
within(dialog).getByRole("button", { name: "Add Value" }),
|
||||
within(dialog).getByRole("button", { name: MEANING_STEP }),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
within(dialog).getByRole("button", { name: SIGNALS_STEP }),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
within(dialog).getByText(
|
||||
"How do we know when we are not living up to this value?",
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
within(dialog).getByRole("button", { name: ADD_TO_RULE }),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
within(dialog).getByRole("button", { name: "Cancel" }),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
within(dialog).queryByRole("button", { name: "Deselect" }),
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
within(dialog).queryByRole("button", { name: "Remove" }),
|
||||
).not.toBeInTheDocument();
|
||||
@@ -48,23 +84,73 @@ describe("CoreValuesSelectScreen", () => {
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("starts the value modal empty and Generate draft inserts catalog copy", async () => {
|
||||
it("offers add your own in the header and a of-6 counter", () => {
|
||||
renderWithProviders(<CoreValuesSelectScreen />);
|
||||
fireEvent.click(screen.getByRole("button", { name: "Select Accessibility" }));
|
||||
const dialog = await screen.findByRole("dialog");
|
||||
const fields = within(dialog).getAllByRole("textbox") as HTMLTextAreaElement[];
|
||||
expect(fields[0].value).toBe("");
|
||||
expect(fields[1].value).toBe("");
|
||||
expect(
|
||||
screen.getByRole("heading", { name: "Choose up to six core values" }),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: ADD_OWN })).toBeInTheDocument();
|
||||
expect(screen.getByText("0 of 6 picked")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("starts a meaning step empty and Generate draft inserts catalog copy", async () => {
|
||||
renderWithProviders(<CoreValuesSelectScreen />);
|
||||
const dialog = await openValueDialog();
|
||||
expect(within(dialog).queryByRole("textbox")).not.toBeInTheDocument();
|
||||
fireEvent.click(within(dialog).getByRole("button", { name: MEANING_STEP }));
|
||||
const field = within(dialog).getByRole("textbox") as HTMLTextAreaElement;
|
||||
expect(field.value).toBe("");
|
||||
expect(field).toHaveAttribute("placeholder", "Write a sentence or two.");
|
||||
expect(
|
||||
within(dialog).getByText("See our first draft for this value"),
|
||||
).toBeInTheDocument();
|
||||
fireEvent.click(
|
||||
within(dialog).getAllByRole("button", { name: "Generate draft" })[0],
|
||||
within(dialog).getByRole("button", { name: "Generate draft" }),
|
||||
);
|
||||
expect(fields[0].value).toMatch(/usable by everyone/i);
|
||||
expect(field.value).toMatch(/usable by everyone/i);
|
||||
expect(
|
||||
within(dialog).queryByRole("button", { name: "Generate draft" }),
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
within(dialog).getByText(
|
||||
"These are your community's words. Change anything that does not fit.",
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("opens a signals step with Generate draft on an empty field", async () => {
|
||||
renderWithProviders(<CoreValuesSelectScreen />);
|
||||
const dialog = await openValueDialog();
|
||||
fireEvent.click(within(dialog).getByRole("button", { name: SIGNALS_STEP }));
|
||||
expect(within(dialog).getByRole("textbox")).toHaveValue("");
|
||||
expect(
|
||||
within(dialog).getByRole("button", { name: "Generate draft" }),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
within(dialog).getByText(
|
||||
"How do we know when we are not living up to Accessibility?",
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("returns from a step to the card when Cancel is clicked", async () => {
|
||||
renderWithProviders(<CoreValuesSelectScreen />);
|
||||
const dialog = await openValueDialog();
|
||||
fireEvent.click(within(dialog).getByRole("button", { name: MEANING_STEP }));
|
||||
expect(within(dialog).getByRole("textbox")).toBeInTheDocument();
|
||||
fireEvent.click(within(dialog).getByRole("button", { name: "Cancel" }));
|
||||
expect(within(dialog).queryByRole("textbox")).not.toBeInTheDocument();
|
||||
expect(
|
||||
within(dialog).getByRole("button", { name: MEANING_STEP }),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
within(dialog).getByRole("button", { name: ADD_TO_RULE }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("closes a pending value without confirm when fields are unchanged", async () => {
|
||||
renderWithProviders(<CoreValuesSelectScreen />);
|
||||
fireEvent.click(screen.getByText("Accessibility"));
|
||||
await screen.findByRole("dialog");
|
||||
await openValueDialog();
|
||||
fireEvent.keyDown(document, { key: "Escape" });
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
||||
@@ -72,6 +158,9 @@ describe("CoreValuesSelectScreen", () => {
|
||||
expect(
|
||||
screen.queryByRole("button", { name: "Keep editing" }),
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole("button", { name: "Select Accessibility" }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("asks to discard when closing a pending value after an edit", async () => {
|
||||
@@ -79,6 +168,7 @@ describe("CoreValuesSelectScreen", () => {
|
||||
fireEvent.click(screen.getByText("Accessibility"));
|
||||
await editMeaningInOpenDialog("Changed meaning");
|
||||
fireEvent.keyDown(document, { key: "Escape" });
|
||||
fireEvent.keyDown(document, { key: "Escape" });
|
||||
expect(
|
||||
await screen.findByRole("button", { name: "Keep editing" }),
|
||||
).toBeInTheDocument();
|
||||
@@ -90,8 +180,7 @@ describe("CoreValuesSelectScreen", () => {
|
||||
|
||||
it("does not block tab close when a pending value is unchanged", async () => {
|
||||
renderWithProviders(<CoreValuesSelectScreen />);
|
||||
fireEvent.click(screen.getByText("Accessibility"));
|
||||
await screen.findByRole("dialog");
|
||||
await openValueDialog();
|
||||
expect(dispatchBeforeUnload()).toBe(false);
|
||||
});
|
||||
|
||||
@@ -107,53 +196,55 @@ describe("CoreValuesSelectScreen", () => {
|
||||
fireEvent.click(screen.getByText("Accessibility"));
|
||||
await editMeaningInOpenDialog("Changed meaning");
|
||||
fireEvent.keyDown(document, { key: "Escape" });
|
||||
fireEvent.keyDown(document, { key: "Escape" });
|
||||
fireEvent.click(await screen.findByRole("button", { name: "Keep editing" }));
|
||||
const dialog = await screen.findByRole("dialog");
|
||||
expect(
|
||||
within(dialog).getByRole("button", { name: "Add Value" }),
|
||||
within(dialog).getByRole("button", { name: ADD_TO_RULE }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("saves details when Add Value is clicked", async () => {
|
||||
it("keeps a value with empty meaning and signals when Add to Rule is clicked", async () => {
|
||||
renderWithProviders(<CoreValuesSelectScreen />);
|
||||
fireEvent.click(screen.getByText("Accessibility"));
|
||||
const dialog = await screen.findByRole("dialog");
|
||||
fireEvent.click(
|
||||
within(dialog).getByRole("button", { name: "Add Value" }),
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
||||
});
|
||||
const dialog = await openValueDialog();
|
||||
await confirmPendingValue(dialog);
|
||||
expect(
|
||||
screen.getByRole("button", { name: "Deselect Accessibility" }),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText("1 of 6 picked")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("opens meaning and signals fields editable without Customize", async () => {
|
||||
it("opens meaning and signals steps editable without Customize", async () => {
|
||||
renderWithProviders(<CoreValuesSelectScreen />);
|
||||
fireEvent.click(screen.getByText("Accessibility"));
|
||||
const dialog = await screen.findByRole("dialog");
|
||||
const fields = within(dialog).getAllByRole("textbox");
|
||||
expect(fields.length).toBeGreaterThanOrEqual(2);
|
||||
for (const field of fields) {
|
||||
expect(field).toBeEnabled();
|
||||
}
|
||||
const dialog = await openValueDialog();
|
||||
fireEvent.click(within(dialog).getByRole("button", { name: MEANING_STEP }));
|
||||
expect(within(dialog).getByRole("textbox")).toBeEnabled();
|
||||
fireEvent.click(within(dialog).getByRole("button", { name: "Cancel" }));
|
||||
fireEvent.click(within(dialog).getByRole("button", { name: SIGNALS_STEP }));
|
||||
expect(within(dialog).getByRole("textbox")).toBeEnabled();
|
||||
});
|
||||
|
||||
it("shows Remove only after the value has been added", async () => {
|
||||
it("shows Deselect after the value has been added, not Remove in the kebab", async () => {
|
||||
renderWithProviders(<CoreValuesSelectScreen />);
|
||||
fireEvent.click(screen.getByText("Accessibility"));
|
||||
const pending = await screen.findByRole("dialog");
|
||||
fireEvent.click(within(pending).getByRole("button", { name: "Add Value" }));
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
||||
});
|
||||
const pending = await openValueDialog();
|
||||
await confirmPendingValue(pending);
|
||||
fireEvent.click(screen.getByText("Accessibility"));
|
||||
const editing = await screen.findByRole("dialog");
|
||||
expect(
|
||||
within(editing).getByRole("button", { name: "Remove" }),
|
||||
within(editing).getByRole("button", { name: "Deselect" }),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
within(editing).getByRole("button", { name: "Save" }),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
within(editing).queryByRole("button", { name: "Remove" }),
|
||||
).not.toBeInTheDocument();
|
||||
fireEvent.click(
|
||||
within(editing).getByRole("button", { name: "More options" }),
|
||||
);
|
||||
expect(screen.getByRole("menuitem", { name: "Remove" })).toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByRole("menuitem", { name: "Remove" }),
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole("menuitem", { name: "Customize" }),
|
||||
).toBeInTheDocument();
|
||||
@@ -162,10 +253,25 @@ describe("CoreValuesSelectScreen", () => {
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("Deselect takes a confirmed value out of the selection", async () => {
|
||||
renderWithProviders(<CoreValuesSelectScreen />);
|
||||
const pending = await openValueDialog();
|
||||
await confirmPendingValue(pending);
|
||||
fireEvent.click(screen.getByText("Accessibility"));
|
||||
const editing = await screen.findByRole("dialog");
|
||||
fireEvent.click(within(editing).getByRole("button", { name: "Deselect" }));
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
||||
});
|
||||
expect(
|
||||
screen.getByRole("button", { name: "Select Accessibility" }),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText("0 of 6 picked")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("Customize walks name and description before policy details", async () => {
|
||||
renderWithProviders(<CoreValuesSelectScreen />);
|
||||
fireEvent.click(screen.getByText("Accessibility"));
|
||||
const dialog = await screen.findByRole("dialog");
|
||||
const dialog = await openValueDialog();
|
||||
fireEvent.click(
|
||||
within(dialog).getByRole("button", { name: "More options" }),
|
||||
);
|
||||
@@ -176,13 +282,14 @@ describe("CoreValuesSelectScreen", () => {
|
||||
);
|
||||
expect(screen.queryByText("Custom policy details")).not.toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole("button", { name: "Next" }));
|
||||
expect(screen.getByPlaceholderText("Policy description")).toHaveValue("");
|
||||
fireEvent.click(screen.getByRole("button", { name: "Next" }));
|
||||
expect(
|
||||
await screen.findByText("Custom policy details"),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole("button", {
|
||||
name: "What does this value mean to your group?",
|
||||
name: MEANING_STEP,
|
||||
}),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
@@ -190,15 +297,9 @@ describe("CoreValuesSelectScreen", () => {
|
||||
it("Customize shows meaning saved from the value modal", async () => {
|
||||
renderWithProviders(<CoreValuesSelectScreen />);
|
||||
fireEvent.click(screen.getByText("Accessibility"));
|
||||
const pending = await screen.findByRole("dialog");
|
||||
const fields = within(pending).getAllByRole("textbox");
|
||||
fireEvent.change(fields[0], {
|
||||
target: { value: "Edited meaning from value modal" },
|
||||
});
|
||||
fireEvent.click(within(pending).getByRole("button", { name: "Add Value" }));
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
||||
});
|
||||
await editMeaningInOpenDialog("Edited meaning from value modal");
|
||||
const pending = screen.getByRole("dialog");
|
||||
await confirmPendingValue(pending);
|
||||
fireEvent.click(screen.getByText("Accessibility"));
|
||||
const editing = await screen.findByRole("dialog");
|
||||
fireEvent.click(
|
||||
@@ -207,10 +308,11 @@ describe("CoreValuesSelectScreen", () => {
|
||||
fireEvent.click(screen.getByRole("menuitem", { name: "Customize" }));
|
||||
expect(await screen.findByPlaceholderText("Policy name")).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole("button", { name: "Next" }));
|
||||
expect(screen.getByPlaceholderText("Policy description")).toHaveValue("");
|
||||
fireEvent.click(screen.getByRole("button", { name: "Next" }));
|
||||
fireEvent.click(
|
||||
await screen.findByRole("button", {
|
||||
name: "What does this value mean to your group?",
|
||||
name: MEANING_STEP,
|
||||
}),
|
||||
);
|
||||
expect(
|
||||
@@ -220,8 +322,7 @@ describe("CoreValuesSelectScreen", () => {
|
||||
|
||||
it("closing Customize without Finalize returns to the value modal", async () => {
|
||||
renderWithProviders(<CoreValuesSelectScreen />);
|
||||
fireEvent.click(screen.getByText("Accessibility"));
|
||||
const dialog = await screen.findByRole("dialog");
|
||||
const dialog = await openValueDialog();
|
||||
fireEvent.click(
|
||||
within(dialog).getByRole("button", { name: "More options" }),
|
||||
);
|
||||
@@ -237,14 +338,13 @@ describe("CoreValuesSelectScreen", () => {
|
||||
});
|
||||
const valueDialog = screen.getByRole("dialog");
|
||||
expect(
|
||||
within(valueDialog).getByRole("button", { name: "Add Value" }),
|
||||
within(valueDialog).getByRole("button", { name: ADD_TO_RULE }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("Customize Finalize shows reordered fields in the value modal", async () => {
|
||||
renderWithProviders(<CoreValuesSelectScreen />);
|
||||
fireEvent.click(screen.getByText("Accessibility"));
|
||||
const dialog = await screen.findByRole("dialog");
|
||||
const dialog = await openValueDialog();
|
||||
fireEvent.click(
|
||||
within(dialog).getByRole("button", { name: "More options" }),
|
||||
);
|
||||
@@ -292,21 +392,13 @@ describe("CoreValuesSelectScreen", () => {
|
||||
? (document.getElementById(labelledby)?.textContent ?? "").trim()
|
||||
: "";
|
||||
});
|
||||
expect(labels[0]).toMatch(/Signals of Violation/);
|
||||
expect(labels[1]).toMatch(/What does this value mean to your group/);
|
||||
expect(labels[0]).toMatch(/Signals of violation/i);
|
||||
expect(labels[1]).toMatch(/What does this value mean to your community/);
|
||||
});
|
||||
|
||||
// The "Add value" control opens an empty custom-policy wizard. Dismissing
|
||||
// without Finalize leaves the chip list unchanged. Finalize writes a
|
||||
// selected chip named from the wizard title.
|
||||
describe("Add value wizard", () => {
|
||||
describe("add your own wizard", () => {
|
||||
const CUSTOM_LABEL = "ZZTopBespokeValue";
|
||||
|
||||
/**
|
||||
* The label can also appear in the modal header while the modal
|
||||
* is open, and as the chip's "Remove" button aria-label. Scope to
|
||||
* chip-row buttons by excluding both.
|
||||
*/
|
||||
function countCustomChips(label: string) {
|
||||
return screen
|
||||
.queryAllByRole("button", {
|
||||
@@ -316,12 +408,13 @@ describe("CoreValuesSelectScreen", () => {
|
||||
}
|
||||
|
||||
async function completeAddValueWizard(label: string) {
|
||||
fireEvent.click(screen.getByRole("button", { name: "Add value" }));
|
||||
fireEvent.click(screen.getByRole("button", { name: ADD_OWN }));
|
||||
const nameInput = await screen.findByPlaceholderText("Policy name");
|
||||
fireEvent.change(nameInput, { target: { value: label } });
|
||||
fireEvent.click(screen.getByRole("button", { name: "Next" }));
|
||||
const descriptionInput =
|
||||
await screen.findByPlaceholderText("Policy description");
|
||||
expect(descriptionInput).toHaveValue("");
|
||||
fireEvent.change(descriptionInput, {
|
||||
target: { value: "A community-authored value." },
|
||||
});
|
||||
@@ -337,7 +430,7 @@ describe("CoreValuesSelectScreen", () => {
|
||||
|
||||
it("collapses the field-type picker after adding a custom field", async () => {
|
||||
renderWithProviders(<CoreValuesSelectScreen />);
|
||||
fireEvent.click(screen.getByRole("button", { name: "Add value" }));
|
||||
fireEvent.click(screen.getByRole("button", { name: ADD_OWN }));
|
||||
fireEvent.change(await screen.findByPlaceholderText("Policy name"), {
|
||||
target: { value: CUSTOM_LABEL },
|
||||
});
|
||||
@@ -373,18 +466,18 @@ describe("CoreValuesSelectScreen", () => {
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("opens the empty custom-policy wizard from Add value", async () => {
|
||||
it("opens the empty custom-policy wizard from add your own", async () => {
|
||||
renderWithProviders(<CoreValuesSelectScreen />);
|
||||
fireEvent.click(screen.getByRole("button", { name: "Add value" }));
|
||||
fireEvent.click(screen.getByRole("button", { name: ADD_OWN }));
|
||||
expect(
|
||||
await screen.findByPlaceholderText("Policy name"),
|
||||
).toHaveValue("");
|
||||
expect(screen.queryByPlaceholderText("Type to add")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("leaves no chip when the Add value wizard is dismissed", async () => {
|
||||
it("leaves no chip when the add your own wizard is dismissed", async () => {
|
||||
renderWithProviders(<CoreValuesSelectScreen />);
|
||||
fireEvent.click(screen.getByRole("button", { name: "Add value" }));
|
||||
fireEvent.click(screen.getByRole("button", { name: ADD_OWN }));
|
||||
await screen.findByPlaceholderText("Policy name");
|
||||
fireEvent.keyDown(document, { key: "Escape" });
|
||||
await waitFor(() => {
|
||||
@@ -412,7 +505,7 @@ describe("CoreValuesSelectScreen", () => {
|
||||
async function addPresetValue(label: string) {
|
||||
fireEvent.click(screen.getByText(label));
|
||||
const dialog = await screen.findByRole("dialog");
|
||||
fireEvent.click(within(dialog).getByRole("button", { name: "Add Value" }));
|
||||
fireEvent.click(within(dialog).getByRole("button", { name: ADD_TO_RULE }));
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
||||
});
|
||||
@@ -420,7 +513,7 @@ describe("CoreValuesSelectScreen", () => {
|
||||
|
||||
it("shows a counter, disables remaining chips, and announces the limit", async () => {
|
||||
renderWithProviders(<CoreValuesSelectScreen />);
|
||||
expect(screen.getByText("0 of 6")).toBeInTheDocument();
|
||||
expect(screen.getByText("0 of 6 picked")).toBeInTheDocument();
|
||||
|
||||
await addPresetValue("Accessibility");
|
||||
await addPresetValue("Accountability");
|
||||
@@ -429,7 +522,7 @@ describe("CoreValuesSelectScreen", () => {
|
||||
await addPresetValue("Anti-oppression");
|
||||
await addPresetValue("Autonomy");
|
||||
|
||||
expect(screen.getByText("6 of 6")).toBeInTheDocument();
|
||||
expect(screen.getByText("6 of 6 picked")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(
|
||||
"Six of six values selected. Remaining values are unavailable until one is removed.",
|
||||
@@ -438,8 +531,7 @@ describe("CoreValuesSelectScreen", () => {
|
||||
expect(
|
||||
screen.getByRole("button", { name: "Select Capacity building" }),
|
||||
).toBeDisabled();
|
||||
expect(screen.getByRole("button", { name: "Add value" })).toBeDisabled();
|
||||
expect(screen.getByRole("button", { name: /^add$/i })).toBeDisabled();
|
||||
expect(screen.getByRole("button", { name: ADD_OWN })).toBeDisabled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -122,6 +122,26 @@ describe("Create", () => {
|
||||
expect(onBack).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("calls onCancel when the trailing Cancel button is clicked", () => {
|
||||
const onCancel = vi.fn();
|
||||
renderWithProviders(
|
||||
<Create
|
||||
{...defaultProps}
|
||||
showBackButton={false}
|
||||
showCancelButton
|
||||
showNextButton
|
||||
onCancel={onCancel}
|
||||
cancelButtonText="Cancel"
|
||||
nextButtonText="Add to Rule"
|
||||
/>,
|
||||
);
|
||||
fireEvent.click(screen.getByRole("button", { name: "Cancel" }));
|
||||
expect(onCancel).toHaveBeenCalledTimes(1);
|
||||
expect(
|
||||
screen.getByRole("button", { name: "Add to Rule" }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("calls onNext when next button is clicked", () => {
|
||||
const onNext = vi.fn();
|
||||
renderWithProviders(
|
||||
|
||||
@@ -358,6 +358,9 @@ describe("FinalReviewScreen — chip detail modal", () => {
|
||||
expect(
|
||||
screen.getByRole("menuitem", { name: /^customize$/i }),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByRole("menuitem", { name: /^remove$/i }),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("values Customize starts on the policy name step", async () => {
|
||||
@@ -388,6 +391,8 @@ describe("FinalReviewScreen — chip detail modal", () => {
|
||||
await screen.findByPlaceholderText("Policy name"),
|
||||
).toHaveValue("Accessibility");
|
||||
expect(screen.queryByText("Custom policy details")).not.toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole("button", { name: "Next" }));
|
||||
expect(screen.getByPlaceholderText("Policy description")).toHaveValue("");
|
||||
});
|
||||
|
||||
it("opens method chip modal editable, with Save disabled until a field changes", async () => {
|
||||
@@ -432,14 +437,48 @@ describe("FinalReviewScreen — chip detail modal", () => {
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText(/what does this value mean to your group\?/i),
|
||||
screen.getByRole("button", {
|
||||
name: /what does this value mean to your community\?/i,
|
||||
}),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
expect(
|
||||
screen.getByText(/signals of violation/i),
|
||||
screen.getByRole("button", { name: /signals of violation/i }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("starts a values meaning step empty and Generate draft inserts catalog copy", async () => {
|
||||
function CoreValuesHarness() {
|
||||
const { replaceState } = useCreateFlow();
|
||||
useLayoutEffect(() => {
|
||||
replaceState({
|
||||
selectedCoreValueIds: ["1"],
|
||||
coreValuesChipsSnapshot: [
|
||||
{ id: "1", label: "Accessibility", state: "selected" },
|
||||
],
|
||||
});
|
||||
}, [replaceState]);
|
||||
return <FinalReviewScreen />;
|
||||
}
|
||||
render(<CoreValuesHarness />);
|
||||
|
||||
fireEvent.click(
|
||||
await screen.findByRole("button", { name: catalogChipName("Accessibility") }),
|
||||
);
|
||||
const dialog = await screen.findByRole("dialog");
|
||||
fireEvent.click(
|
||||
within(dialog).getByRole("button", {
|
||||
name: /what does this value mean to your community\?/i,
|
||||
}),
|
||||
);
|
||||
const field = within(dialog).getByRole("textbox") as HTMLTextAreaElement;
|
||||
expect(field.value).toBe("");
|
||||
fireEvent.click(
|
||||
within(dialog).getByRole("button", { name: "Generate draft" }),
|
||||
);
|
||||
expect(field.value).toMatch(/usable by everyone/i);
|
||||
});
|
||||
|
||||
it("opens the editable Save modal for a values chip", async () => {
|
||||
// Plain custom-rule path: snapshot is set, sections is not.
|
||||
function CoreValuesHarness() {
|
||||
@@ -463,6 +502,12 @@ describe("FinalReviewScreen — chip detail modal", () => {
|
||||
expect(
|
||||
within(dialog).getByRole("button", { name: "Save" }),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
within(dialog).getByRole("button", { name: "Deselect" }),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
within(dialog).getByRole("button", { name: "Cancel" }),
|
||||
).toBeInTheDocument();
|
||||
expect(within(dialog).getAllByRole("button", { name: "Close" })).toHaveLength(
|
||||
1,
|
||||
);
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { describe, vi } from "vitest";
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { screen, fireEvent } from "@testing-library/react";
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { renderWithProviders } from "../utils/test-utils";
|
||||
import {
|
||||
componentTestSuite,
|
||||
type ComponentTestSuiteConfig,
|
||||
@@ -27,4 +30,26 @@ const config: ComponentTestSuiteConfig<Props> = {
|
||||
|
||||
describe("ModalFooter", () => {
|
||||
componentTestSuite<Props>(config);
|
||||
|
||||
it("renders Cancel left of Next in the trailing group", () => {
|
||||
const onCancel = vi.fn();
|
||||
const onNext = vi.fn();
|
||||
renderWithProviders(
|
||||
<ModalFooter
|
||||
showBackButton={false}
|
||||
showCancelButton
|
||||
showNextButton
|
||||
onCancel={onCancel}
|
||||
onNext={onNext}
|
||||
cancelButtonText="Cancel"
|
||||
nextButtonText="Add to Rule"
|
||||
stepper={false}
|
||||
/>,
|
||||
);
|
||||
fireEvent.click(screen.getByRole("button", { name: "Cancel" }));
|
||||
expect(onCancel).toHaveBeenCalledTimes(1);
|
||||
expect(onNext).not.toHaveBeenCalled();
|
||||
fireEvent.click(screen.getByRole("button", { name: "Add to Rule" }));
|
||||
expect(onNext).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -135,6 +135,29 @@ describe("ModalTextAreaField behavior", () => {
|
||||
expect(onChange).toHaveBeenCalledWith("Catalog first draft");
|
||||
});
|
||||
|
||||
it("hides Generate draft on a filled field and shows the filled hint", () => {
|
||||
renderWithProviders(
|
||||
<ModalTextAreaField
|
||||
label="Core principle"
|
||||
value="Community words"
|
||||
onChange={() => {}}
|
||||
generateDraftText="Catalog first draft"
|
||||
generateDraftLabel="Generate draft"
|
||||
generateDraftHint="See our first draft for this value"
|
||||
filledHint="These are your community's words. Change anything that does not fit."
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.queryByRole("button", { name: "Generate draft" }),
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(
|
||||
"These are your community's words. Change anything that does not fit.",
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("hides Generate draft when there is no catalog copy", () => {
|
||||
renderWithProviders(
|
||||
<ModalTextAreaField
|
||||
|
||||
@@ -189,6 +189,18 @@ describe("MultiSelect – behaviour specifics", () => {
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders a count-only caption when formHeader is off", () => {
|
||||
render(
|
||||
<MultiSelect
|
||||
options={defaultChipOptions}
|
||||
formHeader={false}
|
||||
addButton={false}
|
||||
selectionCountText="1 of 6 picked"
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByText("1 of 6 picked")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("handles custom chip confirm", async () => {
|
||||
const handleConfirm = vi.fn();
|
||||
const customOptions = [
|
||||
|
||||
@@ -36,4 +36,8 @@ describe("create footer messages", () => {
|
||||
expect(messages.create.footer.skipForNow).toBe("Skip for now");
|
||||
expect(messages.create.footer.submitEmail).toBe("Submit email");
|
||||
});
|
||||
|
||||
it("exposes Confirm values for the core-values footer CTA", () => {
|
||||
expect(messages.create.footer.confirmCoreValues).toBe("Confirm values");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user