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>
|
</div>
|
||||||
) : showCustomRuleFooterConfirm &&
|
) : showCustomRuleFooterConfirm &&
|
||||||
customRuleConfirmFooter ? (
|
customRuleConfirmFooter ? (
|
||||||
<Button
|
<div className="flex flex-shrink-0 items-center gap-3 md:gap-4">
|
||||||
buttonType="filled"
|
{customRuleConfirmFooter.step === "core-values" ? (
|
||||||
palette="default"
|
<Button
|
||||||
size="xsmall"
|
buttonType="ghost"
|
||||||
disabled={
|
palette="default"
|
||||||
isPublishing ||
|
size="xsmall"
|
||||||
customRuleConfirmFooter.selectionIds(state).length === 0
|
disabled={isPublishing}
|
||||||
}
|
className={CREATE_FLOW_FOOTER_BUTTON_ON_DARK_CLASS}
|
||||||
className={CREATE_FLOW_FOOTER_BUTTON_CLASS}
|
onClick={() => {
|
||||||
onClick={() => {
|
if (reviewReturnTarget) {
|
||||||
const cf = customRuleConfirmFooter;
|
router.push(
|
||||||
const facet = methodCardFacetSectionForConfirmStep(cf.step);
|
createFlowStepPathAfterStrippingReviewReturn(
|
||||||
if (facet != null && cf.selectionIds(state).length > 0) {
|
reviewReturnTarget,
|
||||||
setMethodSectionsPinCommitted(facet, true);
|
searchParams,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
goToNextStep();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{footer.skipForNow}
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
<Button
|
||||||
|
buttonType="filled"
|
||||||
|
palette="default"
|
||||||
|
size="xsmall"
|
||||||
|
disabled={
|
||||||
|
isPublishing ||
|
||||||
|
customRuleConfirmFooter.selectionIds(state).length === 0
|
||||||
}
|
}
|
||||||
if (reviewReturnTarget) {
|
className={CREATE_FLOW_FOOTER_BUTTON_CLASS}
|
||||||
router.push(
|
onClick={() => {
|
||||||
createFlowStepPathAfterStrippingReviewReturn(
|
const cf = customRuleConfirmFooter;
|
||||||
reviewReturnTarget,
|
const facet = methodCardFacetSectionForConfirmStep(cf.step);
|
||||||
searchParams,
|
if (facet != null && cf.selectionIds(state).length > 0) {
|
||||||
),
|
setMethodSectionsPinCommitted(facet, true);
|
||||||
);
|
}
|
||||||
return;
|
if (reviewReturnTarget) {
|
||||||
}
|
router.push(
|
||||||
goToNextStep();
|
createFlowStepPathAfterStrippingReviewReturn(
|
||||||
}}
|
reviewReturnTarget,
|
||||||
>
|
searchParams,
|
||||||
{footer[customRuleConfirmFooter.footerMessageKey]}
|
),
|
||||||
</Button>
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
goToNextStep();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{footer[customRuleConfirmFooter.footerMessageKey]}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
) : isConfirmStakeholdersManagePublished ? (
|
) : isConfirmStakeholdersManagePublished ? (
|
||||||
<Button
|
<Button
|
||||||
buttonType="filled"
|
buttonType="filled"
|
||||||
|
|||||||
-1
@@ -244,7 +244,6 @@ const CustomMethodCardWizardContainer = memo<CustomMethodCardWizardProps>(
|
|||||||
titleTrim.length > 0 &&
|
titleTrim.length > 0 &&
|
||||||
titleTrim.length <= CUSTOM_METHOD_CARD_WIZARD_MAX_FIELD_CHARS;
|
titleTrim.length <= CUSTOM_METHOD_CARD_WIZARD_MAX_FIELD_CHARS;
|
||||||
const descriptionOk =
|
const descriptionOk =
|
||||||
descriptionTrim.length > 0 &&
|
|
||||||
descriptionTrim.length <= CUSTOM_METHOD_CARD_WIZARD_MAX_DESCRIPTION_CHARS;
|
descriptionTrim.length <= CUSTOM_METHOD_CARD_WIZARD_MAX_DESCRIPTION_CHARS;
|
||||||
if (wizardStep === 1) return titleOk;
|
if (wizardStep === 1) return titleOk;
|
||||||
if (wizardStep === 2) return descriptionOk;
|
if (wizardStep === 2) return descriptionOk;
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Final-review chip modal: **Core values** and **method** facets share the
|
* Final-review chip modal: **Core values** use {@link ValueDialog} (Deselect +
|
||||||
* kebab → **Duplicate** (values only when under the cap) / **Remove** pattern
|
* Cancel + Save; kebab Customize / Duplicate under the cap). **Method** facets
|
||||||
* from the create-card facet modals (`Create` +
|
* keep `Create` with kebab Duplicate/Remove plus a footer **Remove** when the
|
||||||
* {@link buildCustomRuleModalKebabMenu}), plus a footer **Remove** when the
|
|
||||||
* chip is already in the selection. Values and method chips also offer
|
* chip is already in the selection. Values and method chips also offer
|
||||||
* **Customize**, which opens {@link CustomMethodCardWizard} prefilled from the
|
* **Customize**, which opens {@link CustomMethodCardWizard} prefilled from the
|
||||||
* chip. Fields are editable on open; Save persists body edits without renaming.
|
* chip. Fields are editable on open; Save persists body edits without renaming.
|
||||||
@@ -22,10 +21,10 @@ import { useMessages, useTranslation } from "../../../contexts/MessagesContext";
|
|||||||
import {
|
import {
|
||||||
CommunicationMethodEditFields,
|
CommunicationMethodEditFields,
|
||||||
ConflictManagementEditFields,
|
ConflictManagementEditFields,
|
||||||
CoreValueEditFields,
|
|
||||||
DecisionApproachEditFields,
|
DecisionApproachEditFields,
|
||||||
MembershipMethodEditFields,
|
MembershipMethodEditFields,
|
||||||
} from "./methodEditFields";
|
} from "./methodEditFields";
|
||||||
|
import { ValueDialog } from "./ValueDialog";
|
||||||
import CustomMethodCardModalBody from "./CustomMethodCardModalBody";
|
import CustomMethodCardModalBody from "./CustomMethodCardModalBody";
|
||||||
import { buildCustomRuleModalKebabMenu } from "./customRuleModalKebabMenu";
|
import { buildCustomRuleModalKebabMenu } from "./customRuleModalKebabMenu";
|
||||||
import { useDiscardCustomizeConfirm } from "../hooks/useDiscardCustomizeConfirm";
|
import { useDiscardCustomizeConfirm } from "../hooks/useDiscardCustomizeConfirm";
|
||||||
@@ -34,6 +33,7 @@ import {
|
|||||||
communicationPresetFor,
|
communicationPresetFor,
|
||||||
conflictManagementPresetFor,
|
conflictManagementPresetFor,
|
||||||
coreValuePresetFor,
|
coreValuePresetFor,
|
||||||
|
coreValuePresetForLabel,
|
||||||
decisionApproachPresetFor,
|
decisionApproachPresetFor,
|
||||||
emptyCommunicationDetails,
|
emptyCommunicationDetails,
|
||||||
emptyConflictManagementDetails,
|
emptyConflictManagementDetails,
|
||||||
@@ -66,6 +66,7 @@ import { uploadCreateFlowFile } from "../../../../lib/create/uploadToServer";
|
|||||||
import CustomMethodCardWizard from "./CustomMethodCardWizard";
|
import CustomMethodCardWizard from "./CustomMethodCardWizard";
|
||||||
import {
|
import {
|
||||||
duplicateCoreValueChipInDraft,
|
duplicateCoreValueChipInDraft,
|
||||||
|
MAX_SELECTED_CORE_VALUES,
|
||||||
removeCoreValueChipFromDraft,
|
removeCoreValueChipFromDraft,
|
||||||
} from "../../../../lib/create/coreValueChipFacet";
|
} from "../../../../lib/create/coreValueChipFacet";
|
||||||
import {
|
import {
|
||||||
@@ -485,8 +486,7 @@ export function FinalReviewChipEditModal({
|
|||||||
buildMethodCardWizardInitialValues({
|
buildMethodCardWizardInitialValues({
|
||||||
cardId: target.overrideKey,
|
cardId: target.overrideKey,
|
||||||
fallbackTitle: target.chipLabel,
|
fallbackTitle: target.chipLabel,
|
||||||
fallbackDescription:
|
fallbackDescription: value.supportText?.trim() || "",
|
||||||
value.supportText?.trim() || tCv.detailModal.subtitle,
|
|
||||||
meta: {},
|
meta: {},
|
||||||
persistedBlocks: state.customMethodCardFieldBlocksById,
|
persistedBlocks: state.customMethodCardFieldBlocksById,
|
||||||
draftFieldBlocks: null,
|
draftFieldBlocks: null,
|
||||||
@@ -559,7 +559,6 @@ export function FinalReviewChipEditModal({
|
|||||||
tComm.sectionHeadings,
|
tComm.sectionHeadings,
|
||||||
tCv.detailModal.meaningLabel,
|
tCv.detailModal.meaningLabel,
|
||||||
tCv.detailModal.signalsLabel,
|
tCv.detailModal.signalsLabel,
|
||||||
tCv.detailModal.subtitle,
|
|
||||||
tDa.sectionHeadings,
|
tDa.sectionHeadings,
|
||||||
tMem.sectionHeadings,
|
tMem.sectionHeadings,
|
||||||
target,
|
target,
|
||||||
@@ -652,7 +651,7 @@ export function FinalReviewChipEditModal({
|
|||||||
if ((state.editingPublishedRuleId?.trim() ?? "") !== "") {
|
if ((state.editingPublishedRuleId?.trim() ?? "") !== "") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if ((state.selectedCoreValueIds ?? []).length >= 5) {
|
if ((state.selectedCoreValueIds ?? []).length >= MAX_SELECTED_CORE_VALUES) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
@@ -727,11 +726,9 @@ export function FinalReviewChipEditModal({
|
|||||||
onCustomize: handleCustomize,
|
onCustomize: handleCustomize,
|
||||||
onDuplicate:
|
onDuplicate:
|
||||||
(state.editingPublishedRuleId?.trim() ?? "") !== "" ||
|
(state.editingPublishedRuleId?.trim() ?? "") !== "" ||
|
||||||
(state.selectedCoreValueIds ?? []).length >= 5
|
(state.selectedCoreValueIds ?? []).length >= MAX_SELECTED_CORE_VALUES
|
||||||
? undefined
|
? undefined
|
||||||
: handleDuplicateCoreValue,
|
: handleDuplicateCoreValue,
|
||||||
showRemove: true,
|
|
||||||
onRemove: handleRemoveCoreValueFromModal,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (!isMethodFacetGroup(target.groupKey)) return [];
|
if (!isMethodFacetGroup(target.groupKey)) return [];
|
||||||
@@ -744,7 +741,6 @@ export function FinalReviewChipEditModal({
|
|||||||
}, [
|
}, [
|
||||||
handleCustomize,
|
handleCustomize,
|
||||||
handleDuplicateCoreValue,
|
handleDuplicateCoreValue,
|
||||||
handleRemoveCoreValueFromModal,
|
|
||||||
handleRemoveSelectedFromModal,
|
handleRemoveSelectedFromModal,
|
||||||
isChipInSelection,
|
isChipInSelection,
|
||||||
modalKebabMenu,
|
modalKebabMenu,
|
||||||
@@ -780,8 +776,12 @@ export function FinalReviewChipEditModal({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
onInteract?.();
|
onInteract?.();
|
||||||
const existingBlocks =
|
const persistedBlocks =
|
||||||
state.customMethodCardFieldBlocksById?.[target.overrideKey];
|
state.customMethodCardFieldBlocksById?.[target.overrideKey];
|
||||||
|
const existingBlocks =
|
||||||
|
draftFieldBlocks && draftFieldBlocks.length > 0
|
||||||
|
? draftFieldBlocks
|
||||||
|
: persistedBlocks;
|
||||||
onSave({
|
onSave({
|
||||||
groupKey: "coreValues",
|
groupKey: "coreValues",
|
||||||
overrideKey: target.overrideKey,
|
overrideKey: target.overrideKey,
|
||||||
@@ -1080,12 +1080,61 @@ export function FinalReviewChipEditModal({
|
|||||||
|
|
||||||
const showFooterRemove =
|
const showFooterRemove =
|
||||||
target != null &&
|
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 (
|
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
|
<Create
|
||||||
isOpen={isOpen && !addCustomWizardOpen}
|
isOpen={
|
||||||
|
isOpen && !addCustomWizardOpen && !isCoreValuesTarget
|
||||||
|
}
|
||||||
onClose={handleModalClose}
|
onClose={handleModalClose}
|
||||||
onBack={handleModalClose}
|
onBack={handleModalClose}
|
||||||
showBackButton={!showFooterRemove}
|
showBackButton={!showFooterRemove}
|
||||||
@@ -1113,29 +1162,6 @@ export function FinalReviewChipEditModal({
|
|||||||
ariaLabel={target?.chipLabel || "Edit chip details"}
|
ariaLabel={target?.chipLabel || "Edit chip details"}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col gap-[var(--space-600)] pb-2">
|
<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" &&
|
{draft?.groupKey === "communication" &&
|
||||||
target &&
|
target &&
|
||||||
(modalUsesWizardFieldBlocksBody ? (
|
(modalUsesWizardFieldBlocksBody ? (
|
||||||
@@ -1451,3 +1477,12 @@ function findMethodSupportText(
|
|||||||
}
|
}
|
||||||
return "";
|
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;
|
label: string;
|
||||||
/** Show the help "?" icon next to the label. Off until tooltip behavior ships. */
|
/** Show the help "?" icon next to the label. Off until tooltip behavior ships. */
|
||||||
helpIcon?: boolean;
|
helpIcon?: boolean;
|
||||||
|
/** Hide the label when the modal title already asks the question. */
|
||||||
|
hideLabel?: boolean;
|
||||||
/** Current text value. */
|
/** Current text value. */
|
||||||
value: string;
|
value: string;
|
||||||
/** Fired on every change with the new value (no event). */
|
/** Fired on every change with the new value (no event). */
|
||||||
@@ -35,6 +37,12 @@ export interface ModalTextAreaFieldProps {
|
|||||||
generateDraftLabel?: string;
|
generateDraftLabel?: string;
|
||||||
/** Hint beside Generate draft. */
|
/** Hint beside Generate draft. */
|
||||||
generateDraftHint?: string;
|
generateDraftHint?: string;
|
||||||
|
/**
|
||||||
|
* Shown under a filled field instead of Generate draft (picked value
|
||||||
|
* field view).
|
||||||
|
*/
|
||||||
|
filledHint?: string;
|
||||||
|
autoFocus?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,6 +71,7 @@ function PlusGlyph() {
|
|||||||
function ModalTextAreaFieldComponent({
|
function ModalTextAreaFieldComponent({
|
||||||
label,
|
label,
|
||||||
helpIcon = false,
|
helpIcon = false,
|
||||||
|
hideLabel = false,
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
rows = 4,
|
rows = 4,
|
||||||
@@ -71,26 +80,34 @@ function ModalTextAreaFieldComponent({
|
|||||||
generateDraftText,
|
generateDraftText,
|
||||||
generateDraftLabel,
|
generateDraftLabel,
|
||||||
generateDraftHint,
|
generateDraftHint,
|
||||||
|
filledHint,
|
||||||
|
autoFocus = false,
|
||||||
className = "",
|
className = "",
|
||||||
}: ModalTextAreaFieldProps) {
|
}: ModalTextAreaFieldProps) {
|
||||||
const labelId = useId();
|
const labelId = useId();
|
||||||
const draftSource = generateDraftText?.trim() ?? "";
|
const draftSource = generateDraftText?.trim() ?? "";
|
||||||
|
const filled = value.trim().length > 0;
|
||||||
|
const showFilledHint = filled && Boolean(filledHint) && !disabled;
|
||||||
const showGenerateDraft =
|
const showGenerateDraft =
|
||||||
draftSource.length > 0 && Boolean(generateDraftLabel);
|
!showFilledHint &&
|
||||||
const compose = showGenerateDraft;
|
draftSource.length > 0 &&
|
||||||
|
Boolean(generateDraftLabel);
|
||||||
|
const compose = showGenerateDraft || showFilledHint || hideLabel;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`flex flex-col ${compose ? "gap-[12px]" : "gap-2"} ${className}`.trim()}
|
className={`flex flex-col ${compose ? "gap-[12px]" : "gap-2"} ${className}`.trim()}
|
||||||
>
|
>
|
||||||
<div id={labelId}>
|
{hideLabel ? null : (
|
||||||
<InputLabel
|
<div id={labelId}>
|
||||||
label={label}
|
<InputLabel
|
||||||
helpIcon={helpIcon}
|
label={label}
|
||||||
size="s"
|
helpIcon={helpIcon}
|
||||||
palette="default"
|
size="s"
|
||||||
/>
|
palette="default"
|
||||||
</div>
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<TextArea
|
<TextArea
|
||||||
formHeader={false}
|
formHeader={false}
|
||||||
value={value}
|
value={value}
|
||||||
@@ -100,7 +117,9 @@ function ModalTextAreaFieldComponent({
|
|||||||
appearance={compose ? "compose" : "embedded"}
|
appearance={compose ? "compose" : "embedded"}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
aria-labelledby={labelId}
|
autoFocus={autoFocus}
|
||||||
|
aria-label={hideLabel ? label : undefined}
|
||||||
|
aria-labelledby={hideLabel ? undefined : labelId}
|
||||||
/>
|
/>
|
||||||
{showGenerateDraft && !disabled ? (
|
{showGenerateDraft && !disabled ? (
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
@@ -119,6 +138,11 @@ function ModalTextAreaFieldComponent({
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
{showFilledHint ? (
|
||||||
|
<p className="text-[length:var(--sizing-300)] leading-normal text-[color:var(--color-content-default-tertiary,#b4b4b4)]">
|
||||||
|
{filledHint}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
</div>
|
</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 { default as CommunicationMethodEditFields } from "./CommunicationMethodEditFields";
|
||||||
export type { CommunicationMethodEditFieldsProps } from "./CommunicationMethodEditFields";
|
export type { CommunicationMethodEditFieldsProps } from "./CommunicationMethodEditFields";
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,7 @@
|
|||||||
import { useState, useEffect, useCallback, useMemo, useRef } from "react";
|
import { useState, useEffect, useCallback, useMemo, useRef } from "react";
|
||||||
import MultiSelect from "../../../../components/controls/MultiSelect";
|
import MultiSelect from "../../../../components/controls/MultiSelect";
|
||||||
import type { ChipOption } from "../../../../components/controls/MultiSelect/MultiSelect.types";
|
import type { ChipOption } from "../../../../components/controls/MultiSelect/MultiSelect.types";
|
||||||
import Create from "../../../../components/modals/Create";
|
import InlineTextButton from "../../../../components/buttons/InlineTextButton";
|
||||||
import ContentLockup from "../../../../components/type/ContentLockup";
|
|
||||||
import { useMessages } from "../../../../contexts/MessagesContext";
|
import { useMessages } from "../../../../contexts/MessagesContext";
|
||||||
import { buildCoreValueChipOptionsFromDraft } from "../../../../../lib/create/coreValueChipOptionsFromDraft";
|
import { buildCoreValueChipOptionsFromDraft } from "../../../../../lib/create/coreValueChipOptionsFromDraft";
|
||||||
import { useCreateFlow } from "../../context/CreateFlowContext";
|
import { useCreateFlow } from "../../context/CreateFlowContext";
|
||||||
@@ -16,8 +15,8 @@ import type {
|
|||||||
} from "../../types";
|
} from "../../types";
|
||||||
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
|
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
|
||||||
import { CreateFlowTwoColumnSelectShell } from "../../components/CreateFlowTwoColumnSelectShell";
|
import { CreateFlowTwoColumnSelectShell } from "../../components/CreateFlowTwoColumnSelectShell";
|
||||||
import { CoreValueEditFields } from "../../components/methodEditFields";
|
|
||||||
import CustomMethodCardModalBody from "../../components/CustomMethodCardModalBody";
|
import CustomMethodCardModalBody from "../../components/CustomMethodCardModalBody";
|
||||||
|
import { ValueDialog } from "../../components/ValueDialog";
|
||||||
import CustomMethodCardWizard from "../../components/CustomMethodCardWizard";
|
import CustomMethodCardWizard from "../../components/CustomMethodCardWizard";
|
||||||
import { usesWizardFieldBlocksModalBody } from "../../../../../lib/create/usesWizardFieldBlocksModalBody";
|
import { usesWizardFieldBlocksModalBody } from "../../../../../lib/create/usesWizardFieldBlocksModalBody";
|
||||||
import { buildCustomRuleModalKebabMenu } from "../../components/customRuleModalKebabMenu";
|
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
|
* - `editing` — chip already exists & is selected; modal reopened to
|
||||||
* tweak meaning/signals. Dismiss = no-op (chip stays as-is).
|
* tweak meaning/signals. Dismiss = no-op (chip stays as-is).
|
||||||
*
|
*
|
||||||
* **Add value** (and the header "add" link) opens an empty custom-policy
|
* The header **add your own** link opens an empty custom-policy wizard.
|
||||||
* wizard. Finalize writes a selected chip named from the wizard title.
|
* Finalize writes a selected chip named from the wizard title.
|
||||||
*/
|
*/
|
||||||
type ModalSession = "pending" | "customPending" | "editing";
|
type ModalSession = "pending" | "customPending" | "editing";
|
||||||
|
|
||||||
@@ -98,7 +97,7 @@ function chipOptionsToSnapshotRows(
|
|||||||
|
|
||||||
const EMPTY_DETAIL: CoreValueDetailEntry = { meaning: "", signals: "" };
|
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() {
|
export function CoreValuesSelectScreen() {
|
||||||
const m = useMessages();
|
const m = useMessages();
|
||||||
const cv = m.create.customRule.coreValues;
|
const cv = m.create.customRule.coreValues;
|
||||||
@@ -448,8 +447,6 @@ export function CoreValuesSelectScreen() {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const showFooterPrimary = Boolean(modalSession);
|
|
||||||
|
|
||||||
const handleCustomize = useCallback(() => {
|
const handleCustomize = useCallback(() => {
|
||||||
if (!activeModalChipId) return;
|
if (!activeModalChipId) return;
|
||||||
markCreateFlowInteraction();
|
markCreateFlowInteraction();
|
||||||
@@ -457,8 +454,7 @@ export function CoreValuesSelectScreen() {
|
|||||||
buildMethodCardWizardInitialValues({
|
buildMethodCardWizardInitialValues({
|
||||||
cardId: activeModalChipId,
|
cardId: activeModalChipId,
|
||||||
fallbackTitle: modalChipLabel,
|
fallbackTitle: modalChipLabel,
|
||||||
fallbackDescription:
|
fallbackDescription: draft.supportText?.trim() || "",
|
||||||
draft.supportText?.trim() || cv.detailModal.subtitle,
|
|
||||||
meta: {},
|
meta: {},
|
||||||
persistedBlocks: state.customMethodCardFieldBlocksById,
|
persistedBlocks: state.customMethodCardFieldBlocksById,
|
||||||
draftFieldBlocks,
|
draftFieldBlocks,
|
||||||
@@ -478,7 +474,6 @@ export function CoreValuesSelectScreen() {
|
|||||||
activeModalChipId,
|
activeModalChipId,
|
||||||
cv.detailModal.meaningLabel,
|
cv.detailModal.meaningLabel,
|
||||||
cv.detailModal.signalsLabel,
|
cv.detailModal.signalsLabel,
|
||||||
cv.detailModal.subtitle,
|
|
||||||
draft,
|
draft,
|
||||||
draftFieldBlocks,
|
draftFieldBlocks,
|
||||||
markCreateFlowInteraction,
|
markCreateFlowInteraction,
|
||||||
@@ -606,15 +601,12 @@ export function CoreValuesSelectScreen() {
|
|||||||
atSelectionLimit
|
atSelectionLimit
|
||||||
? undefined
|
? undefined
|
||||||
: handleDuplicateCoreChip,
|
: handleDuplicateCoreChip,
|
||||||
showRemove: modalSession === "editing",
|
|
||||||
onRemove: handleRemoveFromKebab,
|
|
||||||
});
|
});
|
||||||
}, [
|
}, [
|
||||||
activeModalChipId,
|
activeModalChipId,
|
||||||
atSelectionLimit,
|
atSelectionLimit,
|
||||||
handleCustomize,
|
handleCustomize,
|
||||||
handleDuplicateCoreChip,
|
handleDuplicateCoreChip,
|
||||||
handleRemoveFromKebab,
|
|
||||||
modalKebabMenu,
|
modalKebabMenu,
|
||||||
modalSession,
|
modalSession,
|
||||||
state.editingPublishedRuleId,
|
state.editingPublishedRuleId,
|
||||||
@@ -677,7 +669,7 @@ export function CoreValuesSelectScreen() {
|
|||||||
queueMicrotask(() => {
|
queueMicrotask(() => {
|
||||||
syncCoreValuesToDraft(next);
|
syncCoreValuesToDraft(next);
|
||||||
// Both branches treat the chip as a brand-new draft until the
|
// 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);
|
openModal(chipId, "customPending", value);
|
||||||
});
|
});
|
||||||
return next;
|
return next;
|
||||||
@@ -695,26 +687,17 @@ export function CoreValuesSelectScreen() {
|
|||||||
|
|
||||||
const description = (
|
const description = (
|
||||||
<>
|
<>
|
||||||
<span className="leading-[1.3] text-[color:var(--color-content-default-tertiary,#b4b4b4)]">
|
{cv.header.descriptionLead}
|
||||||
{cv.header.descriptionLead}{" "}
|
<InlineTextButton
|
||||||
</span>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={addHandlers.onAddClick}
|
onClick={addHandlers.onAddClick}
|
||||||
disabled={atSelectionLimit}
|
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}
|
{cv.header.addLink}
|
||||||
</button>
|
</InlineTextButton>
|
||||||
<span className="leading-[1.3] text-[color:var(--color-content-default-tertiary,#b4b4b4)]">
|
{cv.header.descriptionTrail}
|
||||||
{" "}
|
|
||||||
{cv.header.descriptionTrail}
|
|
||||||
</span>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
const detailModal = cv.detailModal;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<CreateFlowTwoColumnSelectShell
|
<CreateFlowTwoColumnSelectShell
|
||||||
@@ -735,75 +718,54 @@ export function CoreValuesSelectScreen() {
|
|||||||
onAddClick={addHandlers.onAddClick}
|
onAddClick={addHandlers.onAddClick}
|
||||||
onCustomChipConfirm={addHandlers.onCustomChipConfirm}
|
onCustomChipConfirm={addHandlers.onCustomChipConfirm}
|
||||||
onCustomChipClose={addHandlers.onCustomChipClose}
|
onCustomChipClose={addHandlers.onCustomChipClose}
|
||||||
addButton
|
addButton={false}
|
||||||
addButtonText={cv.multiSelect.addButtonText}
|
|
||||||
maxSelections={MAX_CORE_VALUES}
|
maxSelections={MAX_CORE_VALUES}
|
||||||
selectionCountText={selectionCountText}
|
selectionCountText={selectionCountText}
|
||||||
limitReachedAnnouncement={cv.multiSelect.limitReached}
|
limitReachedAnnouncement={cv.multiSelect.limitReached}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{detailModal && (
|
<ValueDialog
|
||||||
<Create
|
isOpen={activeModalChipId !== null && !addCustomWizardOpen}
|
||||||
isOpen={activeModalChipId !== null && !addCustomWizardOpen}
|
valueLabel={modalChipLabel}
|
||||||
onClose={handleModalDismiss}
|
session={modalSession === "editing" ? "editing" : "pending"}
|
||||||
backdropVariant="blurredYellow"
|
draft={draft}
|
||||||
headerContent={
|
onDraftChange={handleDraftChange}
|
||||||
<div className="bg-[var(--color-surface-default-primary)] px-[24px] py-[12px] shrink-0">
|
generateDraft={getPresetTexts(modalChipLabel)}
|
||||||
<ContentLockup
|
onClose={() => {
|
||||||
title={modalChipLabel}
|
void handleModalDismiss();
|
||||||
description={
|
}}
|
||||||
draft.supportText?.trim() || detailModal.subtitle
|
onConfirm={handleModalConfirm}
|
||||||
}
|
onDeselect={
|
||||||
variant="modal"
|
modalSession === "editing" ? handleRemoveFromKebab : undefined
|
||||||
alignment="left"
|
}
|
||||||
/>
|
kebabTriggerAriaLabel={modalKebabMenu.triggerAriaLabel}
|
||||||
</div>
|
kebabMenuAriaLabel={modalKebabMenu.menuAriaLabel}
|
||||||
}
|
kebabMenuItems={
|
||||||
showBackButton={false}
|
kebabMenuItems.length > 0 ? kebabMenuItems : undefined
|
||||||
showNextButton={showFooterPrimary}
|
}
|
||||||
showRemoveButton={modalSession === "editing"}
|
ariaLabel={modalChipLabel || "Core value details"}
|
||||||
onRemove={handleRemoveFromKebab}
|
>
|
||||||
onNext={handleModalConfirm}
|
{modalUsesWizardFieldBlocksBody && activeModalChipId ? (
|
||||||
nextButtonText={
|
<CustomMethodCardModalBody
|
||||||
modalSession === "editing"
|
cardId={activeModalChipId}
|
||||||
? modalKebabMenu.saveEdits
|
blocksById={state.customMethodCardFieldBlocksById}
|
||||||
: detailModal.addValueButton
|
blocksOverride={
|
||||||
}
|
draftFieldBlocks !== null ? draftFieldBlocks : undefined
|
||||||
kebabTriggerAriaLabel={modalKebabMenu.triggerAriaLabel}
|
}
|
||||||
kebabMenuAriaLabel={modalKebabMenu.menuAriaLabel}
|
showPolicyContentLockupWhenNoBlocks={false}
|
||||||
kebabMenuItems={
|
onFieldBlocksChange={
|
||||||
kebabMenuItems.length > 0 ? kebabMenuItems : undefined
|
draftFieldBlocks === null
|
||||||
}
|
? undefined
|
||||||
ariaLabel={modalChipLabel || "Core value details"}
|
: (next) => {
|
||||||
>
|
setDraftFieldBlocks(next);
|
||||||
{modalUsesWizardFieldBlocksBody && activeModalChipId ? (
|
setDraft(
|
||||||
<CustomMethodCardModalBody
|
coreValueDetailsFromWizardFieldBlocks(next, draft),
|
||||||
cardId={activeModalChipId}
|
);
|
||||||
blocksById={state.customMethodCardFieldBlocksById}
|
}
|
||||||
blocksOverride={
|
}
|
||||||
draftFieldBlocks !== null ? draftFieldBlocks : undefined
|
/>
|
||||||
}
|
) : undefined}
|
||||||
showPolicyContentLockupWhenNoBlocks={false}
|
</ValueDialog>
|
||||||
onFieldBlocksChange={
|
|
||||||
draftFieldBlocks === null
|
|
||||||
? undefined
|
|
||||||
: (next) => {
|
|
||||||
setDraftFieldBlocks(next);
|
|
||||||
setDraft(
|
|
||||||
coreValueDetailsFromWizardFieldBlocks(next, draft),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<CoreValueEditFields
|
|
||||||
value={draft}
|
|
||||||
onChange={handleDraftChange}
|
|
||||||
generateDraft={getPresetTexts(modalChipLabel)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Create>
|
|
||||||
)}
|
|
||||||
</CreateFlowTwoColumnSelectShell>
|
</CreateFlowTwoColumnSelectShell>
|
||||||
<CustomMethodCardWizard
|
<CustomMethodCardWizard
|
||||||
isOpen={addCustomWizardOpen}
|
isOpen={addCustomWizardOpen}
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ function MultiSelectView({
|
|||||||
|
|
||||||
{!formHeader || !label ? (
|
{!formHeader || !label ? (
|
||||||
headerHelper && typeof headerHelper === "string" ? (
|
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}
|
{headerHelper}
|
||||||
</p>
|
</p>
|
||||||
) : null
|
) : null
|
||||||
|
|||||||
@@ -21,11 +21,14 @@ const CreateContainer = memo<CreateProps>(
|
|||||||
footerClassName,
|
footerClassName,
|
||||||
showBackButton = true,
|
showBackButton = true,
|
||||||
showNextButton = true,
|
showNextButton = true,
|
||||||
|
showCancelButton = false,
|
||||||
showRemoveButton = false,
|
showRemoveButton = false,
|
||||||
onBack,
|
onBack,
|
||||||
onNext,
|
onNext,
|
||||||
|
onCancel,
|
||||||
onRemove,
|
onRemove,
|
||||||
backButtonText = "Back",
|
backButtonText = "Back",
|
||||||
|
cancelButtonText,
|
||||||
nextButtonText = "Next",
|
nextButtonText = "Next",
|
||||||
removeButtonText,
|
removeButtonText,
|
||||||
nextButtonDisabled = false,
|
nextButtonDisabled = false,
|
||||||
@@ -59,11 +62,14 @@ const CreateContainer = memo<CreateProps>(
|
|||||||
footerClassName={footerClassName}
|
footerClassName={footerClassName}
|
||||||
showBackButton={showBackButton}
|
showBackButton={showBackButton}
|
||||||
showNextButton={showNextButton}
|
showNextButton={showNextButton}
|
||||||
|
showCancelButton={showCancelButton}
|
||||||
showRemoveButton={showRemoveButton}
|
showRemoveButton={showRemoveButton}
|
||||||
onBack={onBack}
|
onBack={onBack}
|
||||||
onNext={onNext}
|
onNext={onNext}
|
||||||
|
onCancel={onCancel}
|
||||||
onRemove={onRemove}
|
onRemove={onRemove}
|
||||||
backButtonText={backButtonText}
|
backButtonText={backButtonText}
|
||||||
|
cancelButtonText={cancelButtonText}
|
||||||
nextButtonText={nextButtonText}
|
nextButtonText={nextButtonText}
|
||||||
removeButtonText={removeButtonText}
|
removeButtonText={removeButtonText}
|
||||||
nextButtonDisabled={nextButtonDisabled}
|
nextButtonDisabled={nextButtonDisabled}
|
||||||
|
|||||||
@@ -16,11 +16,14 @@ export interface CreateProps {
|
|||||||
footerClassName?: string;
|
footerClassName?: string;
|
||||||
showBackButton?: boolean;
|
showBackButton?: boolean;
|
||||||
showNextButton?: boolean;
|
showNextButton?: boolean;
|
||||||
|
showCancelButton?: boolean;
|
||||||
showRemoveButton?: boolean;
|
showRemoveButton?: boolean;
|
||||||
onBack?: () => void;
|
onBack?: () => void;
|
||||||
onNext?: () => void;
|
onNext?: () => void;
|
||||||
|
onCancel?: () => void;
|
||||||
onRemove?: () => void;
|
onRemove?: () => void;
|
||||||
backButtonText?: string;
|
backButtonText?: string;
|
||||||
|
cancelButtonText?: string;
|
||||||
nextButtonText?: string;
|
nextButtonText?: string;
|
||||||
removeButtonText?: string;
|
removeButtonText?: string;
|
||||||
nextButtonDisabled?: boolean;
|
nextButtonDisabled?: boolean;
|
||||||
@@ -61,11 +64,14 @@ export interface CreateViewProps {
|
|||||||
footerClassName?: string;
|
footerClassName?: string;
|
||||||
showBackButton: boolean;
|
showBackButton: boolean;
|
||||||
showNextButton: boolean;
|
showNextButton: boolean;
|
||||||
|
showCancelButton: boolean;
|
||||||
showRemoveButton: boolean;
|
showRemoveButton: boolean;
|
||||||
onBack?: () => void;
|
onBack?: () => void;
|
||||||
onNext?: () => void;
|
onNext?: () => void;
|
||||||
|
onCancel?: () => void;
|
||||||
onRemove?: () => void;
|
onRemove?: () => void;
|
||||||
backButtonText: string;
|
backButtonText: string;
|
||||||
|
cancelButtonText?: string;
|
||||||
nextButtonText: string;
|
nextButtonText: string;
|
||||||
removeButtonText?: string;
|
removeButtonText?: string;
|
||||||
nextButtonDisabled: boolean;
|
nextButtonDisabled: boolean;
|
||||||
|
|||||||
@@ -17,11 +17,14 @@ export function CreateView({
|
|||||||
footerClassName,
|
footerClassName,
|
||||||
showBackButton,
|
showBackButton,
|
||||||
showNextButton,
|
showNextButton,
|
||||||
|
showCancelButton,
|
||||||
showRemoveButton,
|
showRemoveButton,
|
||||||
onBack,
|
onBack,
|
||||||
onNext,
|
onNext,
|
||||||
|
onCancel,
|
||||||
onRemove,
|
onRemove,
|
||||||
backButtonText,
|
backButtonText,
|
||||||
|
cancelButtonText,
|
||||||
nextButtonText,
|
nextButtonText,
|
||||||
removeButtonText,
|
removeButtonText,
|
||||||
nextButtonDisabled,
|
nextButtonDisabled,
|
||||||
@@ -79,11 +82,14 @@ export function CreateView({
|
|||||||
<ModalFooter
|
<ModalFooter
|
||||||
showBackButton={showBackButton}
|
showBackButton={showBackButton}
|
||||||
showNextButton={showNextButton}
|
showNextButton={showNextButton}
|
||||||
|
showCancelButton={showCancelButton}
|
||||||
showRemoveButton={showRemoveButton}
|
showRemoveButton={showRemoveButton}
|
||||||
onBack={onBack}
|
onBack={onBack}
|
||||||
onNext={onNext}
|
onNext={onNext}
|
||||||
|
onCancel={onCancel}
|
||||||
onRemove={onRemove}
|
onRemove={onRemove}
|
||||||
backButtonText={backButtonText}
|
backButtonText={backButtonText}
|
||||||
|
cancelButtonText={cancelButtonText}
|
||||||
nextButtonText={nextButtonText}
|
nextButtonText={nextButtonText}
|
||||||
removeButtonText={removeButtonText}
|
removeButtonText={removeButtonText}
|
||||||
nextButtonDisabled={nextButtonDisabled}
|
nextButtonDisabled={nextButtonDisabled}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import type { ModalFooterProps } from "./ModalFooter.types";
|
|||||||
const ModalFooterContainer = memo<ModalFooterProps>((props) => {
|
const ModalFooterContainer = memo<ModalFooterProps>((props) => {
|
||||||
const t = useTranslation("common");
|
const t = useTranslation("common");
|
||||||
const resolvedBackText = props.backButtonText ?? t("buttons.back");
|
const resolvedBackText = props.backButtonText ?? t("buttons.back");
|
||||||
|
const resolvedCancelText = props.cancelButtonText ?? t("buttons.cancel");
|
||||||
const resolvedNextText = props.nextButtonText ?? t("buttons.next");
|
const resolvedNextText = props.nextButtonText ?? t("buttons.next");
|
||||||
const resolvedRemoveText = props.removeButtonText ?? t("buttons.remove");
|
const resolvedRemoveText = props.removeButtonText ?? t("buttons.remove");
|
||||||
|
|
||||||
@@ -20,6 +21,7 @@ const ModalFooterContainer = memo<ModalFooterProps>((props) => {
|
|||||||
<ModalFooterView
|
<ModalFooterView
|
||||||
{...props}
|
{...props}
|
||||||
backButtonText={resolvedBackText}
|
backButtonText={resolvedBackText}
|
||||||
|
cancelButtonText={resolvedCancelText}
|
||||||
nextButtonText={resolvedNextText}
|
nextButtonText={resolvedNextText}
|
||||||
removeButtonText={resolvedRemoveText}
|
removeButtonText={resolvedRemoveText}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
export interface ModalFooterProps {
|
export interface ModalFooterProps {
|
||||||
showBackButton?: boolean;
|
showBackButton?: boolean;
|
||||||
showNextButton?: 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).
|
* Danger **Remove** in the left footer slot (selected create-flow modules).
|
||||||
* Takes the left slot instead of Back when both would otherwise show.
|
* Takes the left slot instead of Back when both would otherwise show.
|
||||||
@@ -8,11 +12,16 @@ export interface ModalFooterProps {
|
|||||||
showRemoveButton?: boolean;
|
showRemoveButton?: boolean;
|
||||||
onBack?: () => void;
|
onBack?: () => void;
|
||||||
onNext?: () => void;
|
onNext?: () => void;
|
||||||
|
onCancel?: () => void;
|
||||||
onRemove?: () => void;
|
onRemove?: () => void;
|
||||||
/**
|
/**
|
||||||
* Custom back button text. If not provided, uses localized "Back" from common.json
|
* Custom back button text. If not provided, uses localized "Back" from common.json
|
||||||
*/
|
*/
|
||||||
backButtonText?: string;
|
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
|
* 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({
|
export function ModalFooterView({
|
||||||
showBackButton = false,
|
showBackButton = false,
|
||||||
showNextButton = false,
|
showNextButton = false,
|
||||||
|
showCancelButton = false,
|
||||||
showRemoveButton = false,
|
showRemoveButton = false,
|
||||||
onBack,
|
onBack,
|
||||||
onNext,
|
onNext,
|
||||||
|
onCancel,
|
||||||
onRemove,
|
onRemove,
|
||||||
backButtonText,
|
backButtonText,
|
||||||
|
cancelButtonText,
|
||||||
nextButtonText,
|
nextButtonText,
|
||||||
removeButtonText,
|
removeButtonText,
|
||||||
nextButtonDisabled = false,
|
nextButtonDisabled = false,
|
||||||
@@ -26,6 +29,7 @@ export function ModalFooterView({
|
|||||||
? stepperProp
|
? stepperProp
|
||||||
: currentStep !== undefined && totalSteps !== undefined;
|
: currentStep !== undefined && totalSteps !== undefined;
|
||||||
const showStartBack = showBackButton && !showRemoveButton;
|
const showStartBack = showBackButton && !showRemoveButton;
|
||||||
|
const showTrailing = showCancelButton || showNextButton;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -64,17 +68,29 @@ export function ModalFooterView({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{showNextButton && (
|
{showTrailing && (
|
||||||
<div className="absolute right-[16px] top-[12px]">
|
<div className="absolute right-[16px] top-[12px] flex items-center gap-[8px]">
|
||||||
<Button
|
{showCancelButton ? (
|
||||||
buttonType="filled"
|
<Button
|
||||||
palette="default"
|
buttonType="outline"
|
||||||
size="medium"
|
palette="default"
|
||||||
onClick={onNext}
|
size="medium"
|
||||||
disabled={nextButtonDisabled}
|
onClick={onCancel}
|
||||||
>
|
>
|
||||||
{nextButtonText}
|
{cancelButtonText}
|
||||||
</Button>
|
</Button>
|
||||||
|
) : null}
|
||||||
|
{showNextButton ? (
|
||||||
|
<Button
|
||||||
|
buttonType="filled"
|
||||||
|
palette="default"
|
||||||
|
size="medium"
|
||||||
|
onClick={onNext}
|
||||||
|
disabled={nextButtonDisabled}
|
||||||
|
>
|
||||||
|
{nextButtonText}
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
"learnMore": "Learn more",
|
"learnMore": "Learn more",
|
||||||
"askOrganizer": "Ask an organizer",
|
"askOrganizer": "Ask an organizer",
|
||||||
"back": "Back",
|
"back": "Back",
|
||||||
|
"cancel": "Cancel",
|
||||||
"next": "Next",
|
"next": "Next",
|
||||||
"remove": "Remove",
|
"remove": "Remove",
|
||||||
"finish": "Finish"
|
"finish": "Finish"
|
||||||
|
|||||||
@@ -1,20 +1,32 @@
|
|||||||
{
|
{
|
||||||
"header": {
|
"header": {
|
||||||
"title": "Choose up to six core values",
|
"title": "Choose up to six core values",
|
||||||
"descriptionLead": "What does your group hold most dear? You can also combine or",
|
"descriptionLead": "Pick what your community holds most dear or ",
|
||||||
"addLink": "add",
|
"addLink": "add your own",
|
||||||
"descriptionTrail": "new values to the list."
|
"descriptionTrail": "."
|
||||||
},
|
},
|
||||||
"multiSelect": {
|
"multiSelect": {
|
||||||
"addButtonText": "Add value",
|
"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."
|
"limitReached": "Six of six values selected. Remaining values are unavailable until one is removed."
|
||||||
},
|
},
|
||||||
"detailModal": {
|
"detailModal": {
|
||||||
"subtitle": "Edit or add to this description to say what this value means to your group.",
|
"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?",
|
"meaningLabel": "What does this value mean to your community?",
|
||||||
"signalsLabel": "Signals of Violation",
|
"meaningDescription": "How does your community interpret the meaning of this value?",
|
||||||
"addValueButton": "Add 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",
|
"customizeValueNameLabel": "Value name",
|
||||||
"discardTitle": "Discard unsaved changes?",
|
"discardTitle": "Discard unsaved changes?",
|
||||||
"discardPendingDescription": "This value will not be added.",
|
"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 { renderWithProviders, dispatchBeforeUnload } from "../utils/test-utils";
|
||||||
import { CoreValuesSelectScreen } from "../../app/(app)/create/screens/select/CoreValuesSelectScreen";
|
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", () => {
|
describe("CoreValuesSelectScreen", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
@@ -13,19 +18,50 @@ describe("CoreValuesSelectScreen", () => {
|
|||||||
fireEvent.click(await screen.findByRole("button", { name: "Discard" }));
|
fireEvent.click(await screen.findByRole("button", { name: "Discard" }));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function editMeaningInOpenDialog(next: string) {
|
async function openValueDialog(label = "Accessibility") {
|
||||||
const dialog = await screen.findByRole("dialog");
|
fireEvent.click(screen.getByRole("button", { name: `Select ${label}` }));
|
||||||
const fields = within(dialog).getAllByRole("textbox");
|
return screen.findByRole("dialog");
|
||||||
fireEvent.change(fields[0], { target: { value: next } });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
it("opens core value detail modal when a preset chip is clicked", async () => {
|
async function editMeaningInOpenDialog(next: string) {
|
||||||
renderWithProviders(<CoreValuesSelectScreen />);
|
|
||||||
fireEvent.click(screen.getByText("Accessibility"));
|
|
||||||
const dialog = await screen.findByRole("dialog");
|
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(
|
expect(
|
||||||
within(dialog).getByRole("button", { name: "Add Value" }),
|
within(dialog).getByRole("button", { name: MEANING_STEP }),
|
||||||
).toBeInTheDocument();
|
).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(
|
expect(
|
||||||
within(dialog).queryByRole("button", { name: "Remove" }),
|
within(dialog).queryByRole("button", { name: "Remove" }),
|
||||||
).not.toBeInTheDocument();
|
).not.toBeInTheDocument();
|
||||||
@@ -48,23 +84,73 @@ describe("CoreValuesSelectScreen", () => {
|
|||||||
).toBeInTheDocument();
|
).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 />);
|
renderWithProviders(<CoreValuesSelectScreen />);
|
||||||
fireEvent.click(screen.getByRole("button", { name: "Select Accessibility" }));
|
expect(
|
||||||
const dialog = await screen.findByRole("dialog");
|
screen.getByRole("heading", { name: "Choose up to six core values" }),
|
||||||
const fields = within(dialog).getAllByRole("textbox") as HTMLTextAreaElement[];
|
).toBeInTheDocument();
|
||||||
expect(fields[0].value).toBe("");
|
expect(screen.getByRole("button", { name: ADD_OWN })).toBeInTheDocument();
|
||||||
expect(fields[1].value).toBe("");
|
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(
|
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 () => {
|
it("closes a pending value without confirm when fields are unchanged", async () => {
|
||||||
renderWithProviders(<CoreValuesSelectScreen />);
|
renderWithProviders(<CoreValuesSelectScreen />);
|
||||||
fireEvent.click(screen.getByText("Accessibility"));
|
await openValueDialog();
|
||||||
await screen.findByRole("dialog");
|
|
||||||
fireEvent.keyDown(document, { key: "Escape" });
|
fireEvent.keyDown(document, { key: "Escape" });
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
||||||
@@ -72,6 +158,9 @@ describe("CoreValuesSelectScreen", () => {
|
|||||||
expect(
|
expect(
|
||||||
screen.queryByRole("button", { name: "Keep editing" }),
|
screen.queryByRole("button", { name: "Keep editing" }),
|
||||||
).not.toBeInTheDocument();
|
).not.toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.getByRole("button", { name: "Select Accessibility" }),
|
||||||
|
).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("asks to discard when closing a pending value after an edit", async () => {
|
it("asks to discard when closing a pending value after an edit", async () => {
|
||||||
@@ -79,6 +168,7 @@ describe("CoreValuesSelectScreen", () => {
|
|||||||
fireEvent.click(screen.getByText("Accessibility"));
|
fireEvent.click(screen.getByText("Accessibility"));
|
||||||
await editMeaningInOpenDialog("Changed meaning");
|
await editMeaningInOpenDialog("Changed meaning");
|
||||||
fireEvent.keyDown(document, { key: "Escape" });
|
fireEvent.keyDown(document, { key: "Escape" });
|
||||||
|
fireEvent.keyDown(document, { key: "Escape" });
|
||||||
expect(
|
expect(
|
||||||
await screen.findByRole("button", { name: "Keep editing" }),
|
await screen.findByRole("button", { name: "Keep editing" }),
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
@@ -90,8 +180,7 @@ describe("CoreValuesSelectScreen", () => {
|
|||||||
|
|
||||||
it("does not block tab close when a pending value is unchanged", async () => {
|
it("does not block tab close when a pending value is unchanged", async () => {
|
||||||
renderWithProviders(<CoreValuesSelectScreen />);
|
renderWithProviders(<CoreValuesSelectScreen />);
|
||||||
fireEvent.click(screen.getByText("Accessibility"));
|
await openValueDialog();
|
||||||
await screen.findByRole("dialog");
|
|
||||||
expect(dispatchBeforeUnload()).toBe(false);
|
expect(dispatchBeforeUnload()).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -107,53 +196,55 @@ describe("CoreValuesSelectScreen", () => {
|
|||||||
fireEvent.click(screen.getByText("Accessibility"));
|
fireEvent.click(screen.getByText("Accessibility"));
|
||||||
await editMeaningInOpenDialog("Changed meaning");
|
await editMeaningInOpenDialog("Changed meaning");
|
||||||
fireEvent.keyDown(document, { key: "Escape" });
|
fireEvent.keyDown(document, { key: "Escape" });
|
||||||
|
fireEvent.keyDown(document, { key: "Escape" });
|
||||||
fireEvent.click(await screen.findByRole("button", { name: "Keep editing" }));
|
fireEvent.click(await screen.findByRole("button", { name: "Keep editing" }));
|
||||||
const dialog = await screen.findByRole("dialog");
|
const dialog = await screen.findByRole("dialog");
|
||||||
expect(
|
expect(
|
||||||
within(dialog).getByRole("button", { name: "Add Value" }),
|
within(dialog).getByRole("button", { name: ADD_TO_RULE }),
|
||||||
).toBeInTheDocument();
|
).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 />);
|
renderWithProviders(<CoreValuesSelectScreen />);
|
||||||
fireEvent.click(screen.getByText("Accessibility"));
|
const dialog = await openValueDialog();
|
||||||
const dialog = await screen.findByRole("dialog");
|
await confirmPendingValue(dialog);
|
||||||
fireEvent.click(
|
expect(
|
||||||
within(dialog).getByRole("button", { name: "Add Value" }),
|
screen.getByRole("button", { name: "Deselect Accessibility" }),
|
||||||
);
|
).toBeInTheDocument();
|
||||||
await waitFor(() => {
|
expect(screen.getByText("1 of 6 picked")).toBeInTheDocument();
|
||||||
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("opens meaning and signals fields editable without Customize", async () => {
|
it("opens meaning and signals steps editable without Customize", async () => {
|
||||||
renderWithProviders(<CoreValuesSelectScreen />);
|
renderWithProviders(<CoreValuesSelectScreen />);
|
||||||
fireEvent.click(screen.getByText("Accessibility"));
|
const dialog = await openValueDialog();
|
||||||
const dialog = await screen.findByRole("dialog");
|
fireEvent.click(within(dialog).getByRole("button", { name: MEANING_STEP }));
|
||||||
const fields = within(dialog).getAllByRole("textbox");
|
expect(within(dialog).getByRole("textbox")).toBeEnabled();
|
||||||
expect(fields.length).toBeGreaterThanOrEqual(2);
|
fireEvent.click(within(dialog).getByRole("button", { name: "Cancel" }));
|
||||||
for (const field of fields) {
|
fireEvent.click(within(dialog).getByRole("button", { name: SIGNALS_STEP }));
|
||||||
expect(field).toBeEnabled();
|
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 />);
|
renderWithProviders(<CoreValuesSelectScreen />);
|
||||||
fireEvent.click(screen.getByText("Accessibility"));
|
const pending = await openValueDialog();
|
||||||
const pending = await screen.findByRole("dialog");
|
await confirmPendingValue(pending);
|
||||||
fireEvent.click(within(pending).getByRole("button", { name: "Add Value" }));
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
|
||||||
});
|
|
||||||
fireEvent.click(screen.getByText("Accessibility"));
|
fireEvent.click(screen.getByText("Accessibility"));
|
||||||
const editing = await screen.findByRole("dialog");
|
const editing = await screen.findByRole("dialog");
|
||||||
expect(
|
expect(
|
||||||
within(editing).getByRole("button", { name: "Remove" }),
|
within(editing).getByRole("button", { name: "Deselect" }),
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
within(editing).getByRole("button", { name: "Save" }),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
within(editing).queryByRole("button", { name: "Remove" }),
|
||||||
|
).not.toBeInTheDocument();
|
||||||
fireEvent.click(
|
fireEvent.click(
|
||||||
within(editing).getByRole("button", { name: "More options" }),
|
within(editing).getByRole("button", { name: "More options" }),
|
||||||
);
|
);
|
||||||
expect(screen.getByRole("menuitem", { name: "Remove" })).toBeInTheDocument();
|
expect(
|
||||||
|
screen.queryByRole("menuitem", { name: "Remove" }),
|
||||||
|
).not.toBeInTheDocument();
|
||||||
expect(
|
expect(
|
||||||
screen.getByRole("menuitem", { name: "Customize" }),
|
screen.getByRole("menuitem", { name: "Customize" }),
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
@@ -162,10 +253,25 @@ describe("CoreValuesSelectScreen", () => {
|
|||||||
).toBeInTheDocument();
|
).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 () => {
|
it("Customize walks name and description before policy details", async () => {
|
||||||
renderWithProviders(<CoreValuesSelectScreen />);
|
renderWithProviders(<CoreValuesSelectScreen />);
|
||||||
fireEvent.click(screen.getByText("Accessibility"));
|
const dialog = await openValueDialog();
|
||||||
const dialog = await screen.findByRole("dialog");
|
|
||||||
fireEvent.click(
|
fireEvent.click(
|
||||||
within(dialog).getByRole("button", { name: "More options" }),
|
within(dialog).getByRole("button", { name: "More options" }),
|
||||||
);
|
);
|
||||||
@@ -176,13 +282,14 @@ describe("CoreValuesSelectScreen", () => {
|
|||||||
);
|
);
|
||||||
expect(screen.queryByText("Custom policy details")).not.toBeInTheDocument();
|
expect(screen.queryByText("Custom policy details")).not.toBeInTheDocument();
|
||||||
fireEvent.click(screen.getByRole("button", { name: "Next" }));
|
fireEvent.click(screen.getByRole("button", { name: "Next" }));
|
||||||
|
expect(screen.getByPlaceholderText("Policy description")).toHaveValue("");
|
||||||
fireEvent.click(screen.getByRole("button", { name: "Next" }));
|
fireEvent.click(screen.getByRole("button", { name: "Next" }));
|
||||||
expect(
|
expect(
|
||||||
await screen.findByText("Custom policy details"),
|
await screen.findByText("Custom policy details"),
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
expect(
|
expect(
|
||||||
screen.getByRole("button", {
|
screen.getByRole("button", {
|
||||||
name: "What does this value mean to your group?",
|
name: MEANING_STEP,
|
||||||
}),
|
}),
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
@@ -190,15 +297,9 @@ describe("CoreValuesSelectScreen", () => {
|
|||||||
it("Customize shows meaning saved from the value modal", async () => {
|
it("Customize shows meaning saved from the value modal", async () => {
|
||||||
renderWithProviders(<CoreValuesSelectScreen />);
|
renderWithProviders(<CoreValuesSelectScreen />);
|
||||||
fireEvent.click(screen.getByText("Accessibility"));
|
fireEvent.click(screen.getByText("Accessibility"));
|
||||||
const pending = await screen.findByRole("dialog");
|
await editMeaningInOpenDialog("Edited meaning from value modal");
|
||||||
const fields = within(pending).getAllByRole("textbox");
|
const pending = screen.getByRole("dialog");
|
||||||
fireEvent.change(fields[0], {
|
await confirmPendingValue(pending);
|
||||||
target: { value: "Edited meaning from value modal" },
|
|
||||||
});
|
|
||||||
fireEvent.click(within(pending).getByRole("button", { name: "Add Value" }));
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
|
||||||
});
|
|
||||||
fireEvent.click(screen.getByText("Accessibility"));
|
fireEvent.click(screen.getByText("Accessibility"));
|
||||||
const editing = await screen.findByRole("dialog");
|
const editing = await screen.findByRole("dialog");
|
||||||
fireEvent.click(
|
fireEvent.click(
|
||||||
@@ -207,10 +308,11 @@ describe("CoreValuesSelectScreen", () => {
|
|||||||
fireEvent.click(screen.getByRole("menuitem", { name: "Customize" }));
|
fireEvent.click(screen.getByRole("menuitem", { name: "Customize" }));
|
||||||
expect(await screen.findByPlaceholderText("Policy name")).toBeInTheDocument();
|
expect(await screen.findByPlaceholderText("Policy name")).toBeInTheDocument();
|
||||||
fireEvent.click(screen.getByRole("button", { name: "Next" }));
|
fireEvent.click(screen.getByRole("button", { name: "Next" }));
|
||||||
|
expect(screen.getByPlaceholderText("Policy description")).toHaveValue("");
|
||||||
fireEvent.click(screen.getByRole("button", { name: "Next" }));
|
fireEvent.click(screen.getByRole("button", { name: "Next" }));
|
||||||
fireEvent.click(
|
fireEvent.click(
|
||||||
await screen.findByRole("button", {
|
await screen.findByRole("button", {
|
||||||
name: "What does this value mean to your group?",
|
name: MEANING_STEP,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
expect(
|
expect(
|
||||||
@@ -220,8 +322,7 @@ describe("CoreValuesSelectScreen", () => {
|
|||||||
|
|
||||||
it("closing Customize without Finalize returns to the value modal", async () => {
|
it("closing Customize without Finalize returns to the value modal", async () => {
|
||||||
renderWithProviders(<CoreValuesSelectScreen />);
|
renderWithProviders(<CoreValuesSelectScreen />);
|
||||||
fireEvent.click(screen.getByText("Accessibility"));
|
const dialog = await openValueDialog();
|
||||||
const dialog = await screen.findByRole("dialog");
|
|
||||||
fireEvent.click(
|
fireEvent.click(
|
||||||
within(dialog).getByRole("button", { name: "More options" }),
|
within(dialog).getByRole("button", { name: "More options" }),
|
||||||
);
|
);
|
||||||
@@ -237,14 +338,13 @@ describe("CoreValuesSelectScreen", () => {
|
|||||||
});
|
});
|
||||||
const valueDialog = screen.getByRole("dialog");
|
const valueDialog = screen.getByRole("dialog");
|
||||||
expect(
|
expect(
|
||||||
within(valueDialog).getByRole("button", { name: "Add Value" }),
|
within(valueDialog).getByRole("button", { name: ADD_TO_RULE }),
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Customize Finalize shows reordered fields in the value modal", async () => {
|
it("Customize Finalize shows reordered fields in the value modal", async () => {
|
||||||
renderWithProviders(<CoreValuesSelectScreen />);
|
renderWithProviders(<CoreValuesSelectScreen />);
|
||||||
fireEvent.click(screen.getByText("Accessibility"));
|
const dialog = await openValueDialog();
|
||||||
const dialog = await screen.findByRole("dialog");
|
|
||||||
fireEvent.click(
|
fireEvent.click(
|
||||||
within(dialog).getByRole("button", { name: "More options" }),
|
within(dialog).getByRole("button", { name: "More options" }),
|
||||||
);
|
);
|
||||||
@@ -292,21 +392,13 @@ describe("CoreValuesSelectScreen", () => {
|
|||||||
? (document.getElementById(labelledby)?.textContent ?? "").trim()
|
? (document.getElementById(labelledby)?.textContent ?? "").trim()
|
||||||
: "";
|
: "";
|
||||||
});
|
});
|
||||||
expect(labels[0]).toMatch(/Signals of Violation/);
|
expect(labels[0]).toMatch(/Signals of violation/i);
|
||||||
expect(labels[1]).toMatch(/What does this value mean to your group/);
|
expect(labels[1]).toMatch(/What does this value mean to your community/);
|
||||||
});
|
});
|
||||||
|
|
||||||
// The "Add value" control opens an empty custom-policy wizard. Dismissing
|
describe("add your own wizard", () => {
|
||||||
// without Finalize leaves the chip list unchanged. Finalize writes a
|
|
||||||
// selected chip named from the wizard title.
|
|
||||||
describe("Add value wizard", () => {
|
|
||||||
const CUSTOM_LABEL = "ZZTopBespokeValue";
|
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) {
|
function countCustomChips(label: string) {
|
||||||
return screen
|
return screen
|
||||||
.queryAllByRole("button", {
|
.queryAllByRole("button", {
|
||||||
@@ -316,12 +408,13 @@ describe("CoreValuesSelectScreen", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function completeAddValueWizard(label: string) {
|
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");
|
const nameInput = await screen.findByPlaceholderText("Policy name");
|
||||||
fireEvent.change(nameInput, { target: { value: label } });
|
fireEvent.change(nameInput, { target: { value: label } });
|
||||||
fireEvent.click(screen.getByRole("button", { name: "Next" }));
|
fireEvent.click(screen.getByRole("button", { name: "Next" }));
|
||||||
const descriptionInput =
|
const descriptionInput =
|
||||||
await screen.findByPlaceholderText("Policy description");
|
await screen.findByPlaceholderText("Policy description");
|
||||||
|
expect(descriptionInput).toHaveValue("");
|
||||||
fireEvent.change(descriptionInput, {
|
fireEvent.change(descriptionInput, {
|
||||||
target: { value: "A community-authored value." },
|
target: { value: "A community-authored value." },
|
||||||
});
|
});
|
||||||
@@ -337,7 +430,7 @@ describe("CoreValuesSelectScreen", () => {
|
|||||||
|
|
||||||
it("collapses the field-type picker after adding a custom field", async () => {
|
it("collapses the field-type picker after adding a custom field", async () => {
|
||||||
renderWithProviders(<CoreValuesSelectScreen />);
|
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"), {
|
fireEvent.change(await screen.findByPlaceholderText("Policy name"), {
|
||||||
target: { value: CUSTOM_LABEL },
|
target: { value: CUSTOM_LABEL },
|
||||||
});
|
});
|
||||||
@@ -373,18 +466,18 @@ describe("CoreValuesSelectScreen", () => {
|
|||||||
).toBeInTheDocument();
|
).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 />);
|
renderWithProviders(<CoreValuesSelectScreen />);
|
||||||
fireEvent.click(screen.getByRole("button", { name: "Add value" }));
|
fireEvent.click(screen.getByRole("button", { name: ADD_OWN }));
|
||||||
expect(
|
expect(
|
||||||
await screen.findByPlaceholderText("Policy name"),
|
await screen.findByPlaceholderText("Policy name"),
|
||||||
).toHaveValue("");
|
).toHaveValue("");
|
||||||
expect(screen.queryByPlaceholderText("Type to add")).not.toBeInTheDocument();
|
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 />);
|
renderWithProviders(<CoreValuesSelectScreen />);
|
||||||
fireEvent.click(screen.getByRole("button", { name: "Add value" }));
|
fireEvent.click(screen.getByRole("button", { name: ADD_OWN }));
|
||||||
await screen.findByPlaceholderText("Policy name");
|
await screen.findByPlaceholderText("Policy name");
|
||||||
fireEvent.keyDown(document, { key: "Escape" });
|
fireEvent.keyDown(document, { key: "Escape" });
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
@@ -412,7 +505,7 @@ describe("CoreValuesSelectScreen", () => {
|
|||||||
async function addPresetValue(label: string) {
|
async function addPresetValue(label: string) {
|
||||||
fireEvent.click(screen.getByText(label));
|
fireEvent.click(screen.getByText(label));
|
||||||
const dialog = await screen.findByRole("dialog");
|
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(() => {
|
await waitFor(() => {
|
||||||
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
@@ -420,7 +513,7 @@ describe("CoreValuesSelectScreen", () => {
|
|||||||
|
|
||||||
it("shows a counter, disables remaining chips, and announces the limit", async () => {
|
it("shows a counter, disables remaining chips, and announces the limit", async () => {
|
||||||
renderWithProviders(<CoreValuesSelectScreen />);
|
renderWithProviders(<CoreValuesSelectScreen />);
|
||||||
expect(screen.getByText("0 of 6")).toBeInTheDocument();
|
expect(screen.getByText("0 of 6 picked")).toBeInTheDocument();
|
||||||
|
|
||||||
await addPresetValue("Accessibility");
|
await addPresetValue("Accessibility");
|
||||||
await addPresetValue("Accountability");
|
await addPresetValue("Accountability");
|
||||||
@@ -429,7 +522,7 @@ describe("CoreValuesSelectScreen", () => {
|
|||||||
await addPresetValue("Anti-oppression");
|
await addPresetValue("Anti-oppression");
|
||||||
await addPresetValue("Autonomy");
|
await addPresetValue("Autonomy");
|
||||||
|
|
||||||
expect(screen.getByText("6 of 6")).toBeInTheDocument();
|
expect(screen.getByText("6 of 6 picked")).toBeInTheDocument();
|
||||||
expect(
|
expect(
|
||||||
screen.getByText(
|
screen.getByText(
|
||||||
"Six of six values selected. Remaining values are unavailable until one is removed.",
|
"Six of six values selected. Remaining values are unavailable until one is removed.",
|
||||||
@@ -438,8 +531,7 @@ describe("CoreValuesSelectScreen", () => {
|
|||||||
expect(
|
expect(
|
||||||
screen.getByRole("button", { name: "Select Capacity building" }),
|
screen.getByRole("button", { name: "Select Capacity building" }),
|
||||||
).toBeDisabled();
|
).toBeDisabled();
|
||||||
expect(screen.getByRole("button", { name: "Add value" })).toBeDisabled();
|
expect(screen.getByRole("button", { name: ADD_OWN })).toBeDisabled();
|
||||||
expect(screen.getByRole("button", { name: /^add$/i })).toBeDisabled();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -122,6 +122,26 @@ describe("Create", () => {
|
|||||||
expect(onBack).toHaveBeenCalledTimes(1);
|
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", () => {
|
it("calls onNext when next button is clicked", () => {
|
||||||
const onNext = vi.fn();
|
const onNext = vi.fn();
|
||||||
renderWithProviders(
|
renderWithProviders(
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
import "@testing-library/jest-dom/vitest";
|
import "@testing-library/jest-dom/vitest";
|
||||||
import { FinalReviewScreen } from "../../app/(app)/create/screens/review/FinalReviewScreen";
|
import { FinalReviewScreen } from "../../app/(app)/create/screens/review/FinalReviewScreen";
|
||||||
import { useCreateFlow } from "../../app/(app)/create/context/CreateFlowContext";
|
import { useCreateFlow } from "../../app/(app)/create/context/CreateFlowContext";
|
||||||
|
import { clearCoreValueDetailsLocalStorage } from "../../app/(app)/create/utils/coreValueDetailsLocalStorage";
|
||||||
import type { CreateFlowState } from "../../app/(app)/create/types";
|
import type { CreateFlowState } from "../../app/(app)/create/types";
|
||||||
|
|
||||||
function catalogChipName(label: string) {
|
function catalogChipName(label: string) {
|
||||||
@@ -358,6 +359,176 @@ describe("FinalReviewScreen — chip detail modal", () => {
|
|||||||
expect(
|
expect(
|
||||||
screen.getByRole("menuitem", { name: /^customize$/i }),
|
screen.getByRole("menuitem", { name: /^customize$/i }),
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.queryByRole("menuitem", { name: /^remove$/i }),
|
||||||
|
).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("offers Duplicate with five values picked and hides it at six", async () => {
|
||||||
|
function CoreValuesHarness({ count }: { count: number }) {
|
||||||
|
const { replaceState } = useCreateFlow();
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
const ids = Array.from({ length: count }, (_, i) => String(i + 1));
|
||||||
|
replaceState({
|
||||||
|
selectedCoreValueIds: ids,
|
||||||
|
coreValuesChipsSnapshot: ids.map((id, i) => ({
|
||||||
|
id,
|
||||||
|
label: `Value ${i + 1}`,
|
||||||
|
state: "selected" as const,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
}, [count, replaceState]);
|
||||||
|
return <FinalReviewScreen />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { unmount } = render(<CoreValuesHarness count={5} />);
|
||||||
|
fireEvent.click(
|
||||||
|
await screen.findByRole("button", { name: catalogChipName("Value 1") }),
|
||||||
|
);
|
||||||
|
fireEvent.click(
|
||||||
|
within(await screen.findByRole("dialog")).getByRole("button", {
|
||||||
|
name: /more options/i,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
await screen.findByRole("menuitem", { name: /^duplicate$/i }),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
unmount();
|
||||||
|
|
||||||
|
render(<CoreValuesHarness count={6} />);
|
||||||
|
fireEvent.click(
|
||||||
|
await screen.findByRole("button", { name: catalogChipName("Value 1") }),
|
||||||
|
);
|
||||||
|
fireEvent.click(
|
||||||
|
within(await screen.findByRole("dialog")).getByRole("button", {
|
||||||
|
name: /more options/i,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(
|
||||||
|
screen.getByRole("menuitem", { name: /^customize$/i }),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
screen.queryByRole("menuitem", { name: /^duplicate$/i }),
|
||||||
|
).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("duplicates a fifth value into the remaining slot", async () => {
|
||||||
|
let latest: CreateFlowState = {};
|
||||||
|
render(
|
||||||
|
<FinalReviewWithStateProbe
|
||||||
|
onState={(s) => {
|
||||||
|
latest = s;
|
||||||
|
}}
|
||||||
|
initial={{
|
||||||
|
selectedCoreValueIds: ["1", "2", "3", "4", "5"],
|
||||||
|
coreValuesChipsSnapshot: ["1", "2", "3", "4", "5"].map((id, i) => ({
|
||||||
|
id,
|
||||||
|
label: `Value ${i + 1}`,
|
||||||
|
state: "selected" as const,
|
||||||
|
})),
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
fireEvent.click(
|
||||||
|
await screen.findByRole("button", { name: catalogChipName("Value 1") }),
|
||||||
|
);
|
||||||
|
fireEvent.click(
|
||||||
|
within(await screen.findByRole("dialog")).getByRole("button", {
|
||||||
|
name: /more options/i,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
fireEvent.click(
|
||||||
|
await screen.findByRole("menuitem", { name: /^duplicate$/i }),
|
||||||
|
);
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(latest.selectedCoreValueIds).toHaveLength(6);
|
||||||
|
});
|
||||||
|
expect(latest.coreValuesChipsSnapshot?.map((row) => row.label)).toContain(
|
||||||
|
"Value 1 (copy)",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("saves in-modal edits to extra value field blocks", async () => {
|
||||||
|
let latest: CreateFlowState = {};
|
||||||
|
let view: ReturnType<typeof render> | undefined;
|
||||||
|
try {
|
||||||
|
view = render(
|
||||||
|
<FinalReviewWithStateProbe
|
||||||
|
onState={(s) => {
|
||||||
|
latest = s;
|
||||||
|
}}
|
||||||
|
initial={{
|
||||||
|
selectedCoreValueIds: ["1"],
|
||||||
|
coreValuesChipsSnapshot: [
|
||||||
|
{ id: "1", label: "Accessibility", state: "selected" },
|
||||||
|
],
|
||||||
|
coreValueDetailsByChipId: {
|
||||||
|
"1": {
|
||||||
|
meaning: "Everyone can participate.",
|
||||||
|
signals: "Captions and ramps.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
customMethodCardFieldBlocksById: {
|
||||||
|
"1": [
|
||||||
|
{
|
||||||
|
kind: "text",
|
||||||
|
id: "extra-note",
|
||||||
|
blockTitle: "House note",
|
||||||
|
placeholderText: "Be kind.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: "text",
|
||||||
|
id: "facet-meaning",
|
||||||
|
blockTitle: "What this value means",
|
||||||
|
placeholderText: "Everyone can participate.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: "text",
|
||||||
|
id: "facet-signals",
|
||||||
|
blockTitle: "Signals",
|
||||||
|
placeholderText: "Captions and ramps.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
fireEvent.click(
|
||||||
|
await screen.findByRole("button", {
|
||||||
|
name: catalogChipName("Accessibility"),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const dialog = await screen.findByRole("dialog");
|
||||||
|
fireEvent.change(
|
||||||
|
within(dialog).getByRole("textbox", { name: "House note" }),
|
||||||
|
{ target: { value: "Leave the ramp clear." } },
|
||||||
|
);
|
||||||
|
const save = within(dialog).getByRole("button", { name: "Save" });
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(save).toBeEnabled();
|
||||||
|
});
|
||||||
|
fireEvent.click(save);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(
|
||||||
|
latest.customMethodCardFieldBlocksById?.["1"]?.map(
|
||||||
|
(block) => block.id,
|
||||||
|
),
|
||||||
|
).toEqual(["extra-note", "facet-meaning", "facet-signals"]);
|
||||||
|
});
|
||||||
|
const extra = latest.customMethodCardFieldBlocksById?.["1"]?.find(
|
||||||
|
(block) => block.id === "extra-note",
|
||||||
|
);
|
||||||
|
expect(extra?.kind === "text" ? extra.placeholderText : "").toBe(
|
||||||
|
"Leave the ramp clear.",
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
view?.unmount();
|
||||||
|
clearCoreValueDetailsLocalStorage();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("values Customize starts on the policy name step", async () => {
|
it("values Customize starts on the policy name step", async () => {
|
||||||
@@ -388,6 +559,8 @@ describe("FinalReviewScreen — chip detail modal", () => {
|
|||||||
await screen.findByPlaceholderText("Policy name"),
|
await screen.findByPlaceholderText("Policy name"),
|
||||||
).toHaveValue("Accessibility");
|
).toHaveValue("Accessibility");
|
||||||
expect(screen.queryByText("Custom policy details")).not.toBeInTheDocument();
|
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 () => {
|
it("opens method chip modal editable, with Save disabled until a field changes", async () => {
|
||||||
@@ -432,14 +605,48 @@ describe("FinalReviewScreen — chip detail modal", () => {
|
|||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(
|
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();
|
).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
expect(
|
expect(
|
||||||
screen.getByText(/signals of violation/i),
|
screen.getByRole("button", { name: /signals of violation/i }),
|
||||||
).toBeInTheDocument();
|
).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 () => {
|
it("opens the editable Save modal for a values chip", async () => {
|
||||||
// Plain custom-rule path: snapshot is set, sections is not.
|
// Plain custom-rule path: snapshot is set, sections is not.
|
||||||
function CoreValuesHarness() {
|
function CoreValuesHarness() {
|
||||||
@@ -463,6 +670,12 @@ describe("FinalReviewScreen — chip detail modal", () => {
|
|||||||
expect(
|
expect(
|
||||||
within(dialog).getByRole("button", { name: "Save" }),
|
within(dialog).getByRole("button", { name: "Save" }),
|
||||||
).toBeInTheDocument();
|
).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(
|
expect(within(dialog).getAllByRole("button", { name: "Close" })).toHaveLength(
|
||||||
1,
|
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 {
|
import {
|
||||||
componentTestSuite,
|
componentTestSuite,
|
||||||
type ComponentTestSuiteConfig,
|
type ComponentTestSuiteConfig,
|
||||||
@@ -27,4 +30,26 @@ const config: ComponentTestSuiteConfig<Props> = {
|
|||||||
|
|
||||||
describe("ModalFooter", () => {
|
describe("ModalFooter", () => {
|
||||||
componentTestSuite<Props>(config);
|
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");
|
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", () => {
|
it("hides Generate draft when there is no catalog copy", () => {
|
||||||
renderWithProviders(
|
renderWithProviders(
|
||||||
<ModalTextAreaField
|
<ModalTextAreaField
|
||||||
|
|||||||
@@ -189,6 +189,18 @@ describe("MultiSelect – behaviour specifics", () => {
|
|||||||
).not.toBeInTheDocument();
|
).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 () => {
|
it("handles custom chip confirm", async () => {
|
||||||
const handleConfirm = vi.fn();
|
const handleConfirm = vi.fn();
|
||||||
const customOptions = [
|
const customOptions = [
|
||||||
|
|||||||
@@ -36,4 +36,8 @@ describe("create footer messages", () => {
|
|||||||
expect(messages.create.footer.skipForNow).toBe("Skip for now");
|
expect(messages.create.footer.skipForNow).toBe("Skip for now");
|
||||||
expect(messages.create.footer.submitEmail).toBe("Submit email");
|
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