Show seven cards on the four method decks and open each as a routing overlay of its parts #99
No files matched your search
@@ -0,0 +1,144 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Figma: Overlay / Modal / Card (24973:1852) with Has Steps on. A deck card
|
||||||
|
* opens as a routing view of its parts; each row opens that field's Step
|
||||||
|
* Modal 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 { ValueDialogStepCard } from "./ValueDialogStepCard";
|
||||||
|
|
||||||
|
export type MethodCardPolicyStep = {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
emptyDescription: string;
|
||||||
|
done: boolean;
|
||||||
|
summary: string;
|
||||||
|
renderEditor: () => ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MethodCardPolicyDialogProps = {
|
||||||
|
isOpen: boolean;
|
||||||
|
cardLabel: string;
|
||||||
|
description?: string;
|
||||||
|
editing: boolean;
|
||||||
|
steps: MethodCardPolicyStep[];
|
||||||
|
onClose: () => void;
|
||||||
|
onConfirm: () => void;
|
||||||
|
onDeselect?: () => void;
|
||||||
|
kebabTriggerAriaLabel?: string;
|
||||||
|
kebabMenuAriaLabel?: string;
|
||||||
|
kebabMenuItems?: ModalHeaderMenuItem[];
|
||||||
|
ariaLabel?: string;
|
||||||
|
/** Customized field-blocks body. Replaces the routing steps. */
|
||||||
|
children?: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
function MethodCardPolicyDialogComponent({
|
||||||
|
isOpen,
|
||||||
|
cardLabel,
|
||||||
|
description = "",
|
||||||
|
editing,
|
||||||
|
steps,
|
||||||
|
onClose,
|
||||||
|
onConfirm,
|
||||||
|
onDeselect,
|
||||||
|
kebabTriggerAriaLabel,
|
||||||
|
kebabMenuAriaLabel,
|
||||||
|
kebabMenuItems,
|
||||||
|
ariaLabel,
|
||||||
|
children,
|
||||||
|
}: MethodCardPolicyDialogProps) {
|
||||||
|
const m = useMessages();
|
||||||
|
const t = m.create.customRule.cardDialog;
|
||||||
|
const [activeStepId, setActiveStepId] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOpen) {
|
||||||
|
setActiveStepId(null);
|
||||||
|
}
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
const activeStep =
|
||||||
|
activeStepId == null
|
||||||
|
? null
|
||||||
|
: (steps.find((step) => step.id === activeStepId) ?? null);
|
||||||
|
const useCustomBody = Boolean(children);
|
||||||
|
const showSteps = !useCustomBody && activeStep == null;
|
||||||
|
|
||||||
|
const handleRequestClose = useCallback(() => {
|
||||||
|
if (activeStepId != null) {
|
||||||
|
setActiveStepId(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onClose();
|
||||||
|
}, [activeStepId, onClose]);
|
||||||
|
|
||||||
|
const handleNext = useCallback(() => {
|
||||||
|
if (activeStepId != null) {
|
||||||
|
setActiveStepId(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onConfirm();
|
||||||
|
}, [activeStepId, onConfirm]);
|
||||||
|
|
||||||
|
const title = activeStep ? activeStep.title : cardLabel;
|
||||||
|
const primaryLabel = activeStepId != null || editing ? t.save : t.addToRule;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Create
|
||||||
|
isOpen={isOpen}
|
||||||
|
onClose={handleRequestClose}
|
||||||
|
title={title}
|
||||||
|
description={activeStep ? undefined : description || undefined}
|
||||||
|
showBackButton={false}
|
||||||
|
showCancelButton
|
||||||
|
showNextButton
|
||||||
|
showRemoveButton={editing && activeStepId == null}
|
||||||
|
onCancel={handleRequestClose}
|
||||||
|
onNext={handleNext}
|
||||||
|
onRemove={onDeselect}
|
||||||
|
cancelButtonText={t.cancel}
|
||||||
|
nextButtonText={primaryLabel}
|
||||||
|
removeButtonText={t.deselect}
|
||||||
|
stepper={false}
|
||||||
|
backdropVariant="blurredYellow"
|
||||||
|
kebabTriggerAriaLabel={kebabTriggerAriaLabel}
|
||||||
|
kebabMenuAriaLabel={kebabMenuAriaLabel}
|
||||||
|
kebabMenuItems={
|
||||||
|
activeStepId == null && kebabMenuItems && kebabMenuItems.length > 0
|
||||||
|
? kebabMenuItems
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
ariaLabel={ariaLabel || cardLabel}
|
||||||
|
>
|
||||||
|
{useCustomBody ? (
|
||||||
|
children
|
||||||
|
) : showSteps ? (
|
||||||
|
<div className="flex w-full flex-col gap-[8px]">
|
||||||
|
{steps.map((step, index) => (
|
||||||
|
<ValueDialogStepCard
|
||||||
|
key={step.id}
|
||||||
|
stepNumber={index + 1}
|
||||||
|
stepBadgeLabel={t.stepBadge}
|
||||||
|
title={step.title}
|
||||||
|
description={step.done ? step.summary : step.emptyDescription}
|
||||||
|
done={step.done}
|
||||||
|
onClick={() => setActiveStepId(step.id)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
activeStep?.renderEditor()
|
||||||
|
)}
|
||||||
|
</Create>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
MethodCardPolicyDialogComponent.displayName = "MethodCardPolicyDialog";
|
||||||
|
|
||||||
|
export const MethodCardPolicyDialog = memo(MethodCardPolicyDialogComponent);
|
||||||
@@ -0,0 +1,325 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import IncrementerBlock from "../../../components/controls/IncrementerBlock";
|
||||||
|
import { useMessages } from "../../../contexts/MessagesContext";
|
||||||
|
import { formatConflictApplicableScopeForTextarea } from "../../../../lib/create/ruleSectionsFromMethodSelections";
|
||||||
|
import { withDecisionApproachKeyResourceScopes } from "../../../../lib/create/decisionApproachKeyResources";
|
||||||
|
import type {
|
||||||
|
CommunicationMethodDetailEntry,
|
||||||
|
ConflictManagementDetailEntry,
|
||||||
|
DecisionApproachDetailEntry,
|
||||||
|
MembershipMethodDetailEntry,
|
||||||
|
} from "../types";
|
||||||
|
import ApplicableScopeField from "./ApplicableScopeField";
|
||||||
|
import type { MethodCardPolicyStep } from "./MethodCardPolicyDialog";
|
||||||
|
import ModalTextAreaField from "./ModalTextAreaField";
|
||||||
|
|
||||||
|
const CONSENSUS_LEVEL_MIN = 0;
|
||||||
|
const CONSENSUS_LEVEL_MAX = 100;
|
||||||
|
const CONSENSUS_LEVEL_STEP = 5;
|
||||||
|
|
||||||
|
function textStep(args: {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
emptyDescription: string;
|
||||||
|
value: string;
|
||||||
|
onChange: (next: string) => void;
|
||||||
|
generateDraftText?: string;
|
||||||
|
generateDraftLabel: string;
|
||||||
|
generateDraftHint: string;
|
||||||
|
filledHint: string;
|
||||||
|
placeholder: string;
|
||||||
|
}): MethodCardPolicyStep {
|
||||||
|
const done = args.value.trim().length > 0;
|
||||||
|
return {
|
||||||
|
id: args.id,
|
||||||
|
title: args.title,
|
||||||
|
emptyDescription: args.emptyDescription,
|
||||||
|
done,
|
||||||
|
summary: args.value,
|
||||||
|
renderEditor: () => (
|
||||||
|
<ModalTextAreaField
|
||||||
|
hideLabel
|
||||||
|
label={args.title}
|
||||||
|
value={args.value}
|
||||||
|
onChange={args.onChange}
|
||||||
|
placeholder={args.placeholder}
|
||||||
|
generateDraftText={args.generateDraftText}
|
||||||
|
generateDraftLabel={args.generateDraftLabel}
|
||||||
|
generateDraftHint={args.generateDraftHint}
|
||||||
|
filledHint={args.filledHint}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function usePolicyFieldCopy() {
|
||||||
|
const m = useMessages();
|
||||||
|
return {
|
||||||
|
dialog: m.create.customRule.cardDialog,
|
||||||
|
draftField: m.create.draftField,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCommunicationPolicySteps(
|
||||||
|
value: CommunicationMethodDetailEntry,
|
||||||
|
onChange: (next: CommunicationMethodDetailEntry) => void,
|
||||||
|
generateDraft?: CommunicationMethodDetailEntry,
|
||||||
|
): MethodCardPolicyStep[] {
|
||||||
|
const { dialog, draftField } = usePolicyFieldCopy();
|
||||||
|
const headings = useMessages().create.customRule.communication.sectionHeadings;
|
||||||
|
const fields: Array<keyof CommunicationMethodDetailEntry> = [
|
||||||
|
"corePrinciple",
|
||||||
|
"logisticsAdmin",
|
||||||
|
"codeOfConduct",
|
||||||
|
];
|
||||||
|
return fields.map((field) =>
|
||||||
|
textStep({
|
||||||
|
id: field,
|
||||||
|
title: headings[field],
|
||||||
|
emptyDescription: dialog.emptyStepDescription,
|
||||||
|
value: value[field],
|
||||||
|
onChange: (next) => onChange({ ...value, [field]: next }),
|
||||||
|
generateDraftText: generateDraft?.[field],
|
||||||
|
generateDraftLabel: draftField.generateDraft,
|
||||||
|
generateDraftHint: draftField.generateDraftHint,
|
||||||
|
filledHint: dialog.filledHint,
|
||||||
|
placeholder: draftField.placeholder,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useMembershipPolicySteps(
|
||||||
|
value: MembershipMethodDetailEntry,
|
||||||
|
onChange: (next: MembershipMethodDetailEntry) => void,
|
||||||
|
generateDraft?: MembershipMethodDetailEntry,
|
||||||
|
): MethodCardPolicyStep[] {
|
||||||
|
const { dialog, draftField } = usePolicyFieldCopy();
|
||||||
|
const headings = useMessages().create.customRule.membership.sectionHeadings;
|
||||||
|
const fields: Array<keyof MembershipMethodDetailEntry> = [
|
||||||
|
"eligibility",
|
||||||
|
"joiningProcess",
|
||||||
|
"expectations",
|
||||||
|
];
|
||||||
|
return fields.map((field) =>
|
||||||
|
textStep({
|
||||||
|
id: field,
|
||||||
|
title: headings[field],
|
||||||
|
emptyDescription: dialog.emptyStepDescription,
|
||||||
|
value: value[field],
|
||||||
|
onChange: (next) => onChange({ ...value, [field]: next }),
|
||||||
|
generateDraftText: generateDraft?.[field],
|
||||||
|
generateDraftLabel: draftField.generateDraft,
|
||||||
|
generateDraftHint: draftField.generateDraftHint,
|
||||||
|
filledHint: dialog.filledHint,
|
||||||
|
placeholder: draftField.placeholder,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useConflictPolicySteps(
|
||||||
|
value: ConflictManagementDetailEntry,
|
||||||
|
onChange: (next: ConflictManagementDetailEntry) => void,
|
||||||
|
generateDraft?: ConflictManagementDetailEntry,
|
||||||
|
): MethodCardPolicyStep[] {
|
||||||
|
const { dialog, draftField } = usePolicyFieldCopy();
|
||||||
|
const t = useMessages().create.customRule.conflictManagement;
|
||||||
|
const scopeText = formatConflictApplicableScopeForTextarea(
|
||||||
|
value.selectedApplicableScope,
|
||||||
|
value.applicableScope,
|
||||||
|
);
|
||||||
|
const generateScope =
|
||||||
|
generateDraft && generateDraft.applicableScope.length > 0
|
||||||
|
? generateDraft.applicableScope.join("\n")
|
||||||
|
: generateDraft
|
||||||
|
? formatConflictApplicableScopeForTextarea(
|
||||||
|
generateDraft.selectedApplicableScope,
|
||||||
|
generateDraft.applicableScope,
|
||||||
|
)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return [
|
||||||
|
textStep({
|
||||||
|
id: "corePrinciple",
|
||||||
|
title: t.sectionHeadings.corePrinciple,
|
||||||
|
emptyDescription: dialog.emptyStepDescription,
|
||||||
|
value: value.corePrinciple,
|
||||||
|
onChange: (next) => onChange({ ...value, corePrinciple: next }),
|
||||||
|
generateDraftText: generateDraft?.corePrinciple,
|
||||||
|
generateDraftLabel: draftField.generateDraft,
|
||||||
|
generateDraftHint: draftField.generateDraftHint,
|
||||||
|
filledHint: dialog.filledHint,
|
||||||
|
placeholder: draftField.placeholder,
|
||||||
|
}),
|
||||||
|
textStep({
|
||||||
|
id: "applicableScope",
|
||||||
|
title: t.sectionHeadings.applicableScope,
|
||||||
|
emptyDescription: t.applicableScopePlaceholder,
|
||||||
|
value: scopeText,
|
||||||
|
onChange: (next) => {
|
||||||
|
const lines = next
|
||||||
|
.split("\n")
|
||||||
|
.map((line) => line.trim())
|
||||||
|
.filter((line) => line.length > 0);
|
||||||
|
onChange({
|
||||||
|
...value,
|
||||||
|
applicableScope: lines,
|
||||||
|
selectedApplicableScope: [...lines],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
generateDraftText: generateScope,
|
||||||
|
generateDraftLabel: draftField.generateDraft,
|
||||||
|
generateDraftHint: draftField.generateDraftHint,
|
||||||
|
filledHint: dialog.filledHint,
|
||||||
|
placeholder: t.applicableScopePlaceholder,
|
||||||
|
}),
|
||||||
|
textStep({
|
||||||
|
id: "processProtocol",
|
||||||
|
title: t.sectionHeadings.processProtocol,
|
||||||
|
emptyDescription: dialog.emptyStepDescription,
|
||||||
|
value: value.processProtocol,
|
||||||
|
onChange: (next) => onChange({ ...value, processProtocol: next }),
|
||||||
|
generateDraftText: generateDraft?.processProtocol,
|
||||||
|
generateDraftLabel: draftField.generateDraft,
|
||||||
|
generateDraftHint: draftField.generateDraftHint,
|
||||||
|
filledHint: dialog.filledHint,
|
||||||
|
placeholder: draftField.placeholder,
|
||||||
|
}),
|
||||||
|
textStep({
|
||||||
|
id: "restorationFallbacks",
|
||||||
|
title: t.sectionHeadings.restorationFallbacks,
|
||||||
|
emptyDescription: dialog.emptyStepDescription,
|
||||||
|
value: value.restorationFallbacks,
|
||||||
|
onChange: (next) => onChange({ ...value, restorationFallbacks: next }),
|
||||||
|
generateDraftText: generateDraft?.restorationFallbacks,
|
||||||
|
generateDraftLabel: draftField.generateDraft,
|
||||||
|
generateDraftHint: draftField.generateDraftHint,
|
||||||
|
filledHint: dialog.filledHint,
|
||||||
|
placeholder: draftField.placeholder,
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDecisionApproachPolicySteps(
|
||||||
|
value: DecisionApproachDetailEntry,
|
||||||
|
onChange: (next: DecisionApproachDetailEntry) => void,
|
||||||
|
generateDraft?: DecisionApproachDetailEntry,
|
||||||
|
): MethodCardPolicyStep[] {
|
||||||
|
const { dialog, draftField } = usePolicyFieldCopy();
|
||||||
|
const t = useMessages().create.customRule.decisionApproaches;
|
||||||
|
const scopes = withDecisionApproachKeyResourceScopes(value.applicableScope);
|
||||||
|
const selectedSummary = value.selectedApplicableScope.join(", ");
|
||||||
|
|
||||||
|
const consensusStep: MethodCardPolicyStep = {
|
||||||
|
id: "consensusLevel",
|
||||||
|
title: t.sectionHeadings.consensusLevel,
|
||||||
|
emptyDescription: t.consensusLevelDisplay.supermajorityShare,
|
||||||
|
done: true,
|
||||||
|
summary:
|
||||||
|
value.consensusLevel === undefined
|
||||||
|
? t.consensusLevelDisplay.supermajorityShare
|
||||||
|
: value.consensusLevel === 0
|
||||||
|
? t.consensusLevelDisplay.noSetShare
|
||||||
|
: `${value.consensusLevel}%`,
|
||||||
|
renderEditor: () =>
|
||||||
|
value.consensusLevel === undefined ? (
|
||||||
|
<ModalTextAreaField
|
||||||
|
hideLabel
|
||||||
|
label={t.sectionHeadings.consensusLevel}
|
||||||
|
value={t.consensusLevelDisplay.supermajorityShare}
|
||||||
|
onChange={() => {}}
|
||||||
|
disabled
|
||||||
|
rows={1}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<IncrementerBlock
|
||||||
|
label={t.sectionHeadings.consensusLevel}
|
||||||
|
helpIcon={false}
|
||||||
|
value={value.consensusLevel}
|
||||||
|
min={CONSENSUS_LEVEL_MIN}
|
||||||
|
max={CONSENSUS_LEVEL_MAX}
|
||||||
|
step={CONSENSUS_LEVEL_STEP}
|
||||||
|
onChange={(next) => onChange({ ...value, consensusLevel: next })}
|
||||||
|
formatValue={(v) =>
|
||||||
|
v === 0 ? t.consensusLevelDisplay.noSetShare : `${v}%`
|
||||||
|
}
|
||||||
|
decrementAriaLabel={dialog.decreaseConsensusLevel}
|
||||||
|
incrementAriaLabel={dialog.increaseConsensusLevel}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
const scopeStep: MethodCardPolicyStep = {
|
||||||
|
id: "applicableScope",
|
||||||
|
title: t.sectionHeadings.applicableScope,
|
||||||
|
emptyDescription: dialog.scopeEmptyDescription,
|
||||||
|
done: value.selectedApplicableScope.length > 0,
|
||||||
|
summary: selectedSummary,
|
||||||
|
renderEditor: () => (
|
||||||
|
<ApplicableScopeField
|
||||||
|
label={t.sectionHeadings.applicableScope}
|
||||||
|
addLabel={t.scopeAddButtonLabel}
|
||||||
|
scopes={scopes}
|
||||||
|
selectedScopes={value.selectedApplicableScope}
|
||||||
|
onToggleScope={(scope) =>
|
||||||
|
onChange({
|
||||||
|
...value,
|
||||||
|
selectedApplicableScope: value.selectedApplicableScope.includes(
|
||||||
|
scope,
|
||||||
|
)
|
||||||
|
? value.selectedApplicableScope.filter((item) => item !== scope)
|
||||||
|
: [...value.selectedApplicableScope, scope],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
onAddScope={(scope) =>
|
||||||
|
onChange({
|
||||||
|
...value,
|
||||||
|
applicableScope: [...value.applicableScope, scope],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
return [
|
||||||
|
textStep({
|
||||||
|
id: "corePrinciple",
|
||||||
|
title: t.sectionHeadings.corePrinciple,
|
||||||
|
emptyDescription: dialog.emptyStepDescription,
|
||||||
|
value: value.corePrinciple,
|
||||||
|
onChange: (next) => onChange({ ...value, corePrinciple: next }),
|
||||||
|
generateDraftText: generateDraft?.corePrinciple,
|
||||||
|
generateDraftLabel: draftField.generateDraft,
|
||||||
|
generateDraftHint: draftField.generateDraftHint,
|
||||||
|
filledHint: dialog.filledHint,
|
||||||
|
placeholder: draftField.placeholder,
|
||||||
|
}),
|
||||||
|
scopeStep,
|
||||||
|
textStep({
|
||||||
|
id: "stepByStepInstructions",
|
||||||
|
title: t.sectionHeadings.stepByStepInstructions,
|
||||||
|
emptyDescription: dialog.emptyStepDescription,
|
||||||
|
value: value.stepByStepInstructions,
|
||||||
|
onChange: (next) => onChange({ ...value, stepByStepInstructions: next }),
|
||||||
|
generateDraftText: generateDraft?.stepByStepInstructions,
|
||||||
|
generateDraftLabel: draftField.generateDraft,
|
||||||
|
generateDraftHint: draftField.generateDraftHint,
|
||||||
|
filledHint: dialog.filledHint,
|
||||||
|
placeholder: draftField.placeholder,
|
||||||
|
}),
|
||||||
|
consensusStep,
|
||||||
|
textStep({
|
||||||
|
id: "objectionsDeadlocks",
|
||||||
|
title: t.sectionHeadings.objectionsDeadlocks,
|
||||||
|
emptyDescription: dialog.emptyStepDescription,
|
||||||
|
value: value.objectionsDeadlocks,
|
||||||
|
onChange: (next) => onChange({ ...value, objectionsDeadlocks: next }),
|
||||||
|
generateDraftText: generateDraft?.objectionsDeadlocks,
|
||||||
|
generateDraftLabel: draftField.generateDraft,
|
||||||
|
generateDraftHint: draftField.generateDraftHint,
|
||||||
|
filledHint: dialog.filledHint,
|
||||||
|
placeholder: draftField.placeholder,
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -14,7 +14,10 @@ import {
|
|||||||
deckData,
|
deckData,
|
||||||
recommendDeck,
|
recommendDeck,
|
||||||
} from "../../../../lib/create/deckRecommend";
|
} from "../../../../lib/create/deckRecommend";
|
||||||
import type { GroupAnswers } from "../../../../lib/create/deckTypes";
|
import type {
|
||||||
|
GroupAnswers,
|
||||||
|
RecommendedCard,
|
||||||
|
} from "../../../../lib/create/deckTypes";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Card-deck section ids served by `/api/create-flow/methods` (CR-88 §9.2).
|
* Card-deck section ids served by `/api/create-flow/methods` (CR-88 §9.2).
|
||||||
@@ -143,8 +146,13 @@ export function deriveCompactCards(
|
|||||||
compactCardIds: string[];
|
compactCardIds: string[];
|
||||||
recommendedIds: Set<string>;
|
recommendedIds: Set<string>;
|
||||||
orderedIds: string[];
|
orderedIds: string[];
|
||||||
|
fallback: boolean;
|
||||||
|
recommendedById: Map<string, RecommendedCard>;
|
||||||
} {
|
} {
|
||||||
const recommendation = recommendDeck(deckData(section), answers, { limit });
|
const recommendation = recommendDeck(deckData(section), answers, { limit });
|
||||||
|
const recommendedById = new Map(
|
||||||
|
recommendation.recommended.map((card) => [card.id, card]),
|
||||||
|
);
|
||||||
const orderedIds = recommendation.fallback
|
const orderedIds = recommendation.fallback
|
||||||
? recommendation.seeAll
|
? recommendation.seeAll
|
||||||
: [
|
: [
|
||||||
@@ -156,6 +164,8 @@ export function deriveCompactCards(
|
|||||||
compactCardIds: orderedIds.slice(0, limit),
|
compactCardIds: orderedIds.slice(0, limit),
|
||||||
recommendedIds: new Set<string>(),
|
recommendedIds: new Set<string>(),
|
||||||
orderedIds,
|
orderedIds,
|
||||||
|
fallback: true,
|
||||||
|
recommendedById,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
@@ -166,6 +176,8 @@ export function deriveCompactCards(
|
|||||||
.map((card) => card.id),
|
.map((card) => card.id),
|
||||||
),
|
),
|
||||||
orderedIds,
|
orderedIds,
|
||||||
|
fallback: false,
|
||||||
|
recommendedById,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,12 +2,15 @@
|
|||||||
|
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { mergeCompactCardIdsWithPinnedSelected } from "../../../../lib/create/methodCardDisplayOrder";
|
import { mergeCompactCardIdsWithPinnedSelected } from "../../../../lib/create/methodCardDisplayOrder";
|
||||||
|
import { formatRecommendationWhyLine } from "../../../../lib/create/formatRecommendationWhyLine";
|
||||||
import {
|
import {
|
||||||
deriveCompactCards,
|
deriveCompactCards,
|
||||||
groupAnswersForRecommendations,
|
groupAnswersForRecommendations,
|
||||||
type RecommendationSection,
|
type RecommendationSection,
|
||||||
} from "./useFacetRecommendations";
|
} from "./useFacetRecommendations";
|
||||||
|
import { deckData } from "../../../../lib/create/deckRecommend";
|
||||||
import { useCreateFlow } from "../context/CreateFlowContext";
|
import { useCreateFlow } from "../context/CreateFlowContext";
|
||||||
|
import { useMessages } from "../../../contexts/MessagesContext";
|
||||||
|
|
||||||
type MethodEntry = { id: string; label: string; supportText: string };
|
type MethodEntry = { id: string; label: string; supportText: string };
|
||||||
|
|
||||||
@@ -25,9 +28,8 @@ function orderByIds<T extends { id: string }>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deck order and the compact stack come from the job rule. Current screens
|
* Deck order and the compact stack come from the job rule. Compact shows
|
||||||
* show five cards; a deck screen passes seven. Selected cards still pin
|
* at most seven recommended cards. Selected cards still pin into the stack.
|
||||||
* into the compact stack.
|
|
||||||
*/
|
*/
|
||||||
export function useMethodCardDeckOrdering(
|
export function useMethodCardDeckOrdering(
|
||||||
section: RecommendationSection,
|
section: RecommendationSection,
|
||||||
@@ -35,13 +37,15 @@ export function useMethodCardDeckOrdering(
|
|||||||
selectedIds: readonly string[],
|
selectedIds: readonly string[],
|
||||||
) {
|
) {
|
||||||
const { state } = useCreateFlow();
|
const { state } = useCreateFlow();
|
||||||
|
const copy = useMessages().create.customRule.recommendations;
|
||||||
|
const compactLimit = deckData(section).config.displayLimit;
|
||||||
const answers = useMemo(
|
const answers = useMemo(
|
||||||
() => groupAnswersForRecommendations(state),
|
() => groupAnswersForRecommendations(state),
|
||||||
[state],
|
[state],
|
||||||
);
|
);
|
||||||
const deck = useMemo(
|
const deck = useMemo(
|
||||||
() => deriveCompactCards(section, answers, 5),
|
() => deriveCompactCards(section, answers, compactLimit),
|
||||||
[section, answers],
|
[section, answers, compactLimit],
|
||||||
);
|
);
|
||||||
|
|
||||||
const rankedMethods = useMemo(
|
const rankedMethods = useMemo(
|
||||||
@@ -59,25 +63,57 @@ export function useMethodCardDeckOrdering(
|
|||||||
deck.compactCardIds,
|
deck.compactCardIds,
|
||||||
selectedIds,
|
selectedIds,
|
||||||
selectionShowcaseActive,
|
selectionShowcaseActive,
|
||||||
5,
|
compactLimit,
|
||||||
),
|
),
|
||||||
[
|
[
|
||||||
rankedMethods,
|
rankedMethods,
|
||||||
deck.compactCardIds,
|
deck.compactCardIds,
|
||||||
selectedIds,
|
selectedIds,
|
||||||
selectionShowcaseActive,
|
selectionShowcaseActive,
|
||||||
|
compactLimit,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const compactIdSet = useMemo(
|
||||||
|
() => new Set(compactCardIds),
|
||||||
|
[compactCardIds],
|
||||||
|
);
|
||||||
|
|
||||||
const sampleCards = useMemo(
|
const sampleCards = useMemo(
|
||||||
() =>
|
() =>
|
||||||
rankedMethods.map((entry) => ({
|
rankedMethods.map((entry) => {
|
||||||
|
const recommendation = deck.recommendedById.get(entry.id);
|
||||||
|
const whyLine =
|
||||||
|
recommendation && recommendation.tag !== "everyRuleHas"
|
||||||
|
? formatRecommendationWhyLine(recommendation.reason, answers, copy)
|
||||||
|
: "";
|
||||||
|
const alternativeId = recommendation?.alternative;
|
||||||
|
const alternativeMethod = alternativeId
|
||||||
|
? rankedMethods.find((method) => method.id === alternativeId) ??
|
||||||
|
methods.find((method) => method.id === alternativeId)
|
||||||
|
: undefined;
|
||||||
|
return {
|
||||||
id: entry.id,
|
id: entry.id,
|
||||||
label: entry.label,
|
label: entry.label,
|
||||||
supportText: entry.supportText,
|
supportText: entry.supportText,
|
||||||
|
compactSupportText:
|
||||||
|
whyLine && compactIdSet.has(entry.id) ? whyLine : undefined,
|
||||||
recommended: recommendedIds.has(entry.id),
|
recommended: recommendedIds.has(entry.id),
|
||||||
})),
|
alternative:
|
||||||
[rankedMethods, recommendedIds],
|
alternativeMethod && compactIdSet.has(entry.id)
|
||||||
|
? { id: alternativeMethod.id, label: alternativeMethod.label }
|
||||||
|
: undefined,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
[
|
||||||
|
answers,
|
||||||
|
compactIdSet,
|
||||||
|
copy,
|
||||||
|
deck.recommendedById,
|
||||||
|
methods,
|
||||||
|
rankedMethods,
|
||||||
|
recommendedIds,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const methodById = useMemo(
|
const methodById = useMemo(
|
||||||
@@ -92,6 +128,8 @@ export function useMethodCardDeckOrdering(
|
|||||||
recommendedIds,
|
recommendedIds,
|
||||||
sampleCards,
|
sampleCards,
|
||||||
methodById,
|
methodById,
|
||||||
|
showDeckLine: !deck.fallback,
|
||||||
|
compactLimit,
|
||||||
recommendationsReady: true,
|
recommendationsReady: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -8,11 +8,11 @@
|
|||||||
* ({@link CreateFlowTwoColumnSelectShell}); options are {@link CardStack} with
|
* ({@link CreateFlowTwoColumnSelectShell}); options are {@link CardStack} with
|
||||||
* `layout="singleStack"` and {@link Selection} size `l`.
|
* `layout="singleStack"` and {@link Selection} size `l`.
|
||||||
*
|
*
|
||||||
* Card click opens the Figma create modal (node `20246-15829`) with three
|
* Card click opens Overlay / Modal / Card as a routing view of its parts
|
||||||
* editable sections rendered by {@link CommunicationMethodEditFields}. The
|
* ({@link MethodCardPolicyDialog}). The primary action is **Add to Rule** for
|
||||||
* primary action is **Add Platform** for an unselected card and **Save** for a
|
* an unselected card and **Save** for a selected card. **Deselect** is the
|
||||||
* selected card. **Remove** is a danger footer button on selected cards (and
|
* footer on selected cards (**Remove** stays in the kebab) via
|
||||||
* remains in the kebab) via {@link removeMethodCardFromFacetSelection}.
|
* {@link removeMethodCardFromFacetSelection}.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useState, useCallback, useMemo, useRef } from "react";
|
import { useState, useCallback, useMemo, useRef } from "react";
|
||||||
@@ -24,9 +24,9 @@ import { useMethodCardDeckOrdering } from "../../hooks/useMethodCardDeckOrdering
|
|||||||
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
|
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
|
||||||
import { CreateFlowTwoColumnSelectShell } from "../../components/CreateFlowTwoColumnSelectShell";
|
import { CreateFlowTwoColumnSelectShell } from "../../components/CreateFlowTwoColumnSelectShell";
|
||||||
import CardStack from "../../../../components/cards/CardStack";
|
import CardStack from "../../../../components/cards/CardStack";
|
||||||
import Create from "../../../../components/modals/Create";
|
|
||||||
import InlineTextButton from "../../../../components/buttons/InlineTextButton";
|
import InlineTextButton from "../../../../components/buttons/InlineTextButton";
|
||||||
import { CommunicationMethodEditFields } from "../../components/methodEditFields";
|
import { MethodCardPolicyDialog } from "../../components/MethodCardPolicyDialog";
|
||||||
|
import { useCommunicationPolicySteps } from "../../components/methodCardPolicySteps";
|
||||||
import CustomMethodCardWizard from "../../components/CustomMethodCardWizard";
|
import CustomMethodCardWizard from "../../components/CustomMethodCardWizard";
|
||||||
import { uploadCreateFlowFile } from "../../../../../lib/create/uploadToServer";
|
import { uploadCreateFlowFile } from "../../../../../lib/create/uploadToServer";
|
||||||
import { communicationPresetFor, emptyCommunicationDetails } from "../../../../../lib/create/finalReviewChipPresets";
|
import { communicationPresetFor, emptyCommunicationDetails } from "../../../../../lib/create/finalReviewChipPresets";
|
||||||
@@ -105,7 +105,8 @@ export function CommunicationMethodsScreen() {
|
|||||||
[comm.methods, selectedIds, state.customMethodCardMetaById],
|
[comm.methods, selectedIds, state.customMethodCardMetaById],
|
||||||
);
|
);
|
||||||
|
|
||||||
const { sampleCards, compactCardIds, methodById, recommendationsReady } =
|
const rec = m.create.customRule.recommendations;
|
||||||
|
const { sampleCards, compactCardIds, methodById, recommendationsReady, showDeckLine, compactLimit } =
|
||||||
useMethodCardDeckOrdering(
|
useMethodCardDeckOrdering(
|
||||||
"communication",
|
"communication",
|
||||||
mergedMethods,
|
mergedMethods,
|
||||||
@@ -188,10 +189,14 @@ export function CommunicationMethodsScreen() {
|
|||||||
},
|
},
|
||||||
[markCreateFlowInteraction],
|
[markCreateFlowInteraction],
|
||||||
);
|
);
|
||||||
|
const communicationSteps = useCommunicationPolicySteps(
|
||||||
|
pendingDraft ?? emptyCommunicationDetails(),
|
||||||
|
handleDraftChange,
|
||||||
|
pendingCardId ? communicationPresetFor(pendingCardId) : undefined,
|
||||||
|
);
|
||||||
|
|
||||||
const isSelectedCardModal =
|
const isSelectedCardModal =
|
||||||
pendingCardId !== null && selectedIds.includes(pendingCardId);
|
pendingCardId !== null && selectedIds.includes(pendingCardId);
|
||||||
const showMethodModalPrimary = true;
|
|
||||||
|
|
||||||
const customFacetDetailsMatchPreset = useMemo(() => {
|
const customFacetDetailsMatchPreset = useMemo(() => {
|
||||||
if (!pendingCardId || !pendingDraft) return false;
|
if (!pendingCardId || !pendingDraft) return false;
|
||||||
@@ -710,34 +715,36 @@ export function CommunicationMethodsScreen() {
|
|||||||
markCreateFlowInteraction();
|
markCreateFlowInteraction();
|
||||||
setExpanded((prev) => !prev);
|
setExpanded((prev) => !prev);
|
||||||
}}
|
}}
|
||||||
hasMore={true}
|
hasMore={sampleCards.length > compactLimit}
|
||||||
toggleLabel={comm.page.seeAllLink}
|
toggleLabel={comm.page.seeAllLink}
|
||||||
compactRecommendedLimit={5}
|
compactRecommendedLimit={compactLimit}
|
||||||
compactCardIds={compactCardIds}
|
compactCardIds={compactCardIds}
|
||||||
layout="singleStack"
|
layout="singleStack"
|
||||||
|
deckLine={showDeckLine ? rec.deckLine : undefined}
|
||||||
|
searchPlaceholder={rec.searchPlaceholder}
|
||||||
|
alternativePrefix={rec.alternative}
|
||||||
|
onAlternativeSelect={handleCardClick}
|
||||||
className="w-full lg:h-full lg:min-h-0 lg:flex-1"
|
className="w-full lg:h-full lg:min-h-0 lg:flex-1"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</CreateFlowTwoColumnSelectShell>
|
</CreateFlowTwoColumnSelectShell>
|
||||||
<Create
|
<MethodCardPolicyDialog
|
||||||
isOpen={createModalOpen}
|
isOpen={createModalOpen}
|
||||||
onClose={handleCreateModalClose}
|
cardLabel={modalConfig.title}
|
||||||
onNext={handleCreateModalPrimary}
|
|
||||||
title={modalConfig.title}
|
|
||||||
description={modalConfig.description}
|
description={modalConfig.description}
|
||||||
nextButtonText={modalConfig.nextButtonText}
|
editing={isSelectedCardModal}
|
||||||
showBackButton={false}
|
steps={communicationSteps}
|
||||||
showNextButton={showMethodModalPrimary}
|
onClose={handleCreateModalClose}
|
||||||
showRemoveButton={isSelectedCardModal}
|
onConfirm={handleCreateModalPrimary}
|
||||||
onRemove={handleRemoveSelectedFromModal}
|
onDeselect={
|
||||||
backdropVariant="blurredYellow"
|
isSelectedCardModal ? handleRemoveSelectedFromModal : undefined
|
||||||
|
}
|
||||||
kebabTriggerAriaLabel={modalKebabMenu.triggerAriaLabel}
|
kebabTriggerAriaLabel={modalKebabMenu.triggerAriaLabel}
|
||||||
kebabMenuAriaLabel={modalKebabMenu.menuAriaLabel}
|
kebabMenuAriaLabel={modalKebabMenu.menuAriaLabel}
|
||||||
kebabMenuItems={kebabMenuItems}
|
kebabMenuItems={kebabMenuItems}
|
||||||
>
|
>
|
||||||
{pendingCardId && pendingDraft ? (
|
{pendingCardId && modalUsesWizardFieldBlocksBody ? (
|
||||||
modalUsesWizardFieldBlocksBody ? (
|
|
||||||
<CustomMethodCardModalBody
|
<CustomMethodCardModalBody
|
||||||
cardId={pendingCardId}
|
cardId={pendingCardId}
|
||||||
blocksById={state.customMethodCardFieldBlocksById}
|
blocksById={state.customMethodCardFieldBlocksById}
|
||||||
@@ -754,15 +761,8 @@ export function CommunicationMethodsScreen() {
|
|||||||
: (next) => setDraftFieldBlocks(next)
|
: (next) => setDraftFieldBlocks(next)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
) : (
|
|
||||||
<CommunicationMethodEditFields
|
|
||||||
value={pendingDraft}
|
|
||||||
onChange={handleDraftChange}
|
|
||||||
generateDraft={communicationPresetFor(pendingCardId)}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
) : null}
|
) : null}
|
||||||
</Create>
|
</MethodCardPolicyDialog>
|
||||||
<CustomMethodCardWizard
|
<CustomMethodCardWizard
|
||||||
isOpen={addCustomWizardOpen}
|
isOpen={addCustomWizardOpen}
|
||||||
onClose={handleCloseAddWizard}
|
onClose={handleCloseAddWizard}
|
||||||
|
|||||||
@@ -7,12 +7,9 @@
|
|||||||
* Page chrome is the shared question-and-options shell
|
* Page chrome is the shared question-and-options shell
|
||||||
* (`CreateFlowTwoColumnSelectShell`); options are CardStack `singleStack`.
|
* (`CreateFlowTwoColumnSelectShell`); options are CardStack `singleStack`.
|
||||||
*
|
*
|
||||||
* Card click opens the Figma "Add Approach" create modal (node `20874-172292`)
|
* Card click opens Overlay / Modal / Card as a routing view of its parts
|
||||||
* with four controls rendered by {@link ConflictManagementEditFields}: Core
|
* ({@link MethodCardPolicyDialog}). Confirm persists the chip selection and
|
||||||
* Principle, Applicable Scope (text area), Process Protocol, and Restoration
|
* any edits as a `conflictManagementDetailsById[id]` override.
|
||||||
* & Fallbacks. The same field set is reused on `/create/final-review` — see
|
|
||||||
* `FinalReviewChipEditModal`. Confirm persists both the chip selection and
|
|
||||||
* any user edits as a `conflictManagementDetailsById[id]` override.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useState, useCallback, useMemo, useRef } from "react";
|
import { useState, useCallback, useMemo, useRef } from "react";
|
||||||
@@ -24,9 +21,9 @@ import { useMethodCardDeckOrdering } from "../../hooks/useMethodCardDeckOrdering
|
|||||||
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
|
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
|
||||||
import { CreateFlowTwoColumnSelectShell } from "../../components/CreateFlowTwoColumnSelectShell";
|
import { CreateFlowTwoColumnSelectShell } from "../../components/CreateFlowTwoColumnSelectShell";
|
||||||
import CardStack from "../../../../components/cards/CardStack";
|
import CardStack from "../../../../components/cards/CardStack";
|
||||||
import Create from "../../../../components/modals/Create";
|
|
||||||
import InlineTextButton from "../../../../components/buttons/InlineTextButton";
|
import InlineTextButton from "../../../../components/buttons/InlineTextButton";
|
||||||
import { ConflictManagementEditFields } from "../../components/methodEditFields";
|
import { MethodCardPolicyDialog } from "../../components/MethodCardPolicyDialog";
|
||||||
|
import { useConflictPolicySteps } from "../../components/methodCardPolicySteps";
|
||||||
import CustomMethodCardWizard from "../../components/CustomMethodCardWizard";
|
import CustomMethodCardWizard from "../../components/CustomMethodCardWizard";
|
||||||
import { uploadCreateFlowFile } from "../../../../../lib/create/uploadToServer";
|
import { uploadCreateFlowFile } from "../../../../../lib/create/uploadToServer";
|
||||||
import { conflictManagementPresetFor, emptyConflictManagementDetails } from "../../../../../lib/create/finalReviewChipPresets";
|
import { conflictManagementPresetFor, emptyConflictManagementDetails } from "../../../../../lib/create/finalReviewChipPresets";
|
||||||
@@ -105,7 +102,8 @@ export function ConflictManagementScreen() {
|
|||||||
[cm.methods, selectedIds, state.customMethodCardMetaById],
|
[cm.methods, selectedIds, state.customMethodCardMetaById],
|
||||||
);
|
);
|
||||||
|
|
||||||
const { sampleCards, compactCardIds, methodById, recommendationsReady } =
|
const rec = m.create.customRule.recommendations;
|
||||||
|
const { sampleCards, compactCardIds, methodById, recommendationsReady, showDeckLine, compactLimit } =
|
||||||
useMethodCardDeckOrdering(
|
useMethodCardDeckOrdering(
|
||||||
"conflictManagement",
|
"conflictManagement",
|
||||||
mergedMethods,
|
mergedMethods,
|
||||||
@@ -192,10 +190,14 @@ export function ConflictManagementScreen() {
|
|||||||
},
|
},
|
||||||
[markCreateFlowInteraction],
|
[markCreateFlowInteraction],
|
||||||
);
|
);
|
||||||
|
const conflictSteps = useConflictPolicySteps(
|
||||||
|
pendingDraft ?? emptyConflictManagementDetails(),
|
||||||
|
handleDraftChange,
|
||||||
|
pendingCardId ? conflictManagementPresetFor(pendingCardId) : undefined,
|
||||||
|
);
|
||||||
|
|
||||||
const isSelectedCardModal =
|
const isSelectedCardModal =
|
||||||
pendingCardId !== null && selectedIds.includes(pendingCardId);
|
pendingCardId !== null && selectedIds.includes(pendingCardId);
|
||||||
const showMethodModalPrimary = true;
|
|
||||||
|
|
||||||
const customFacetDetailsMatchPreset = useMemo(() => {
|
const customFacetDetailsMatchPreset = useMemo(() => {
|
||||||
if (!pendingCardId || !pendingDraft) return false;
|
if (!pendingCardId || !pendingDraft) return false;
|
||||||
@@ -714,34 +716,36 @@ export function ConflictManagementScreen() {
|
|||||||
markCreateFlowInteraction();
|
markCreateFlowInteraction();
|
||||||
setExpanded((prev) => !prev);
|
setExpanded((prev) => !prev);
|
||||||
}}
|
}}
|
||||||
hasMore={true}
|
hasMore={sampleCards.length > compactLimit}
|
||||||
toggleLabel={cm.page.seeAllLink}
|
toggleLabel={cm.page.seeAllLink}
|
||||||
compactRecommendedLimit={5}
|
compactRecommendedLimit={compactLimit}
|
||||||
compactCardIds={compactCardIds}
|
compactCardIds={compactCardIds}
|
||||||
layout="singleStack"
|
layout="singleStack"
|
||||||
|
deckLine={showDeckLine ? rec.deckLine : undefined}
|
||||||
|
searchPlaceholder={rec.searchPlaceholder}
|
||||||
|
alternativePrefix={rec.alternative}
|
||||||
|
onAlternativeSelect={handleCardClick}
|
||||||
className="w-full lg:h-full lg:min-h-0 lg:flex-1"
|
className="w-full lg:h-full lg:min-h-0 lg:flex-1"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</CreateFlowTwoColumnSelectShell>
|
</CreateFlowTwoColumnSelectShell>
|
||||||
<Create
|
<MethodCardPolicyDialog
|
||||||
isOpen={createModalOpen}
|
isOpen={createModalOpen}
|
||||||
onClose={handleCreateModalClose}
|
cardLabel={modalConfig.title}
|
||||||
onNext={handleCreateModalPrimary}
|
|
||||||
title={modalConfig.title}
|
|
||||||
description={modalConfig.description}
|
description={modalConfig.description}
|
||||||
nextButtonText={modalConfig.nextButtonText}
|
editing={isSelectedCardModal}
|
||||||
showBackButton={false}
|
steps={conflictSteps}
|
||||||
showNextButton={showMethodModalPrimary}
|
onClose={handleCreateModalClose}
|
||||||
showRemoveButton={isSelectedCardModal}
|
onConfirm={handleCreateModalPrimary}
|
||||||
onRemove={handleRemoveSelectedFromModal}
|
onDeselect={
|
||||||
backdropVariant="blurredYellow"
|
isSelectedCardModal ? handleRemoveSelectedFromModal : undefined
|
||||||
|
}
|
||||||
kebabTriggerAriaLabel={modalKebabMenu.triggerAriaLabel}
|
kebabTriggerAriaLabel={modalKebabMenu.triggerAriaLabel}
|
||||||
kebabMenuAriaLabel={modalKebabMenu.menuAriaLabel}
|
kebabMenuAriaLabel={modalKebabMenu.menuAriaLabel}
|
||||||
kebabMenuItems={kebabMenuItems}
|
kebabMenuItems={kebabMenuItems}
|
||||||
>
|
>
|
||||||
{pendingCardId && pendingDraft ? (
|
{pendingCardId && modalUsesWizardFieldBlocksBody ? (
|
||||||
modalUsesWizardFieldBlocksBody ? (
|
|
||||||
<CustomMethodCardModalBody
|
<CustomMethodCardModalBody
|
||||||
cardId={pendingCardId}
|
cardId={pendingCardId}
|
||||||
blocksById={state.customMethodCardFieldBlocksById}
|
blocksById={state.customMethodCardFieldBlocksById}
|
||||||
@@ -758,15 +762,8 @@ export function ConflictManagementScreen() {
|
|||||||
: (next) => setDraftFieldBlocks(next)
|
: (next) => setDraftFieldBlocks(next)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
) : (
|
|
||||||
<ConflictManagementEditFields
|
|
||||||
value={pendingDraft}
|
|
||||||
onChange={handleDraftChange}
|
|
||||||
generateDraft={conflictManagementPresetFor(pendingCardId)}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
) : null}
|
) : null}
|
||||||
</Create>
|
</MethodCardPolicyDialog>
|
||||||
<CustomMethodCardWizard
|
<CustomMethodCardWizard
|
||||||
isOpen={addCustomWizardOpen}
|
isOpen={addCustomWizardOpen}
|
||||||
onClose={handleCloseAddWizard}
|
onClose={handleCloseAddWizard}
|
||||||
|
|||||||
@@ -7,13 +7,9 @@
|
|||||||
* Page chrome is the shared question-and-options shell
|
* Page chrome is the shared question-and-options shell
|
||||||
* (`CreateFlowTwoColumnSelectShell`); options are CardStack `singleStack`.
|
* (`CreateFlowTwoColumnSelectShell`); options are CardStack `singleStack`.
|
||||||
*
|
*
|
||||||
* Card click opens the Figma create modal (node `20858-13948`) with three
|
* Card click opens Overlay / Modal / Card as a routing view of its parts
|
||||||
* editable sections rendered by {@link MembershipMethodEditFields}. The same
|
* ({@link MethodCardPolicyDialog}). Confirm persists the chip selection and
|
||||||
* field set is reused on `/create/final-review` — see `FinalReviewChipEditModal`.
|
* any edits as a `membershipMethodDetailsById[id]` override.
|
||||||
* Confirm persists both the chip selection and any user edits as a
|
|
||||||
* `membershipMethodDetailsById[id]` override; section defaults come from
|
|
||||||
* `messages/en/create/customRule/membership.json` and will be replaced with
|
|
||||||
* DB-driven content.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useState, useCallback, useMemo, useRef } from "react";
|
import { useState, useCallback, useMemo, useRef } from "react";
|
||||||
@@ -25,9 +21,9 @@ import { useMethodCardDeckOrdering } from "../../hooks/useMethodCardDeckOrdering
|
|||||||
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
|
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
|
||||||
import { CreateFlowTwoColumnSelectShell } from "../../components/CreateFlowTwoColumnSelectShell";
|
import { CreateFlowTwoColumnSelectShell } from "../../components/CreateFlowTwoColumnSelectShell";
|
||||||
import CardStack from "../../../../components/cards/CardStack";
|
import CardStack from "../../../../components/cards/CardStack";
|
||||||
import Create from "../../../../components/modals/Create";
|
|
||||||
import InlineTextButton from "../../../../components/buttons/InlineTextButton";
|
import InlineTextButton from "../../../../components/buttons/InlineTextButton";
|
||||||
import { MembershipMethodEditFields } from "../../components/methodEditFields";
|
import { MethodCardPolicyDialog } from "../../components/MethodCardPolicyDialog";
|
||||||
|
import { useMembershipPolicySteps } from "../../components/methodCardPolicySteps";
|
||||||
import CustomMethodCardWizard from "../../components/CustomMethodCardWizard";
|
import CustomMethodCardWizard from "../../components/CustomMethodCardWizard";
|
||||||
import { uploadCreateFlowFile } from "../../../../../lib/create/uploadToServer";
|
import { uploadCreateFlowFile } from "../../../../../lib/create/uploadToServer";
|
||||||
import { membershipPresetFor, emptyMembershipDetails } from "../../../../../lib/create/finalReviewChipPresets";
|
import { membershipPresetFor, emptyMembershipDetails } from "../../../../../lib/create/finalReviewChipPresets";
|
||||||
@@ -106,7 +102,8 @@ export function MembershipMethodsScreen() {
|
|||||||
[mem.methods, selectedIds, state.customMethodCardMetaById],
|
[mem.methods, selectedIds, state.customMethodCardMetaById],
|
||||||
);
|
);
|
||||||
|
|
||||||
const { sampleCards, compactCardIds, methodById, recommendationsReady } =
|
const rec = m.create.customRule.recommendations;
|
||||||
|
const { sampleCards, compactCardIds, methodById, recommendationsReady, showDeckLine, compactLimit } =
|
||||||
useMethodCardDeckOrdering(
|
useMethodCardDeckOrdering(
|
||||||
"membership",
|
"membership",
|
||||||
mergedMethods,
|
mergedMethods,
|
||||||
@@ -189,10 +186,14 @@ export function MembershipMethodsScreen() {
|
|||||||
},
|
},
|
||||||
[markCreateFlowInteraction],
|
[markCreateFlowInteraction],
|
||||||
);
|
);
|
||||||
|
const membershipSteps = useMembershipPolicySteps(
|
||||||
|
pendingDraft ?? emptyMembershipDetails(),
|
||||||
|
handleDraftChange,
|
||||||
|
pendingCardId ? membershipPresetFor(pendingCardId) : undefined,
|
||||||
|
);
|
||||||
|
|
||||||
const isSelectedCardModal =
|
const isSelectedCardModal =
|
||||||
pendingCardId !== null && selectedIds.includes(pendingCardId);
|
pendingCardId !== null && selectedIds.includes(pendingCardId);
|
||||||
const showMethodModalPrimary = true;
|
|
||||||
|
|
||||||
const customFacetDetailsMatchPreset = useMemo(() => {
|
const customFacetDetailsMatchPreset = useMemo(() => {
|
||||||
if (!pendingCardId || !pendingDraft) return false;
|
if (!pendingCardId || !pendingDraft) return false;
|
||||||
@@ -707,34 +708,36 @@ export function MembershipMethodsScreen() {
|
|||||||
markCreateFlowInteraction();
|
markCreateFlowInteraction();
|
||||||
setExpanded((prev) => !prev);
|
setExpanded((prev) => !prev);
|
||||||
}}
|
}}
|
||||||
hasMore={true}
|
hasMore={sampleCards.length > compactLimit}
|
||||||
toggleLabel={mem.page.seeAllLink}
|
toggleLabel={mem.page.seeAllLink}
|
||||||
compactRecommendedLimit={5}
|
compactRecommendedLimit={compactLimit}
|
||||||
compactCardIds={compactCardIds}
|
compactCardIds={compactCardIds}
|
||||||
layout="singleStack"
|
layout="singleStack"
|
||||||
|
deckLine={showDeckLine ? rec.deckLine : undefined}
|
||||||
|
searchPlaceholder={rec.searchPlaceholder}
|
||||||
|
alternativePrefix={rec.alternative}
|
||||||
|
onAlternativeSelect={handleCardClick}
|
||||||
className="w-full lg:h-full lg:min-h-0 lg:flex-1"
|
className="w-full lg:h-full lg:min-h-0 lg:flex-1"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</CreateFlowTwoColumnSelectShell>
|
</CreateFlowTwoColumnSelectShell>
|
||||||
<Create
|
<MethodCardPolicyDialog
|
||||||
isOpen={createModalOpen}
|
isOpen={createModalOpen}
|
||||||
onClose={handleCreateModalClose}
|
cardLabel={modalConfig.title}
|
||||||
onNext={handleCreateModalPrimary}
|
|
||||||
title={modalConfig.title}
|
|
||||||
description={modalConfig.description}
|
description={modalConfig.description}
|
||||||
nextButtonText={modalConfig.nextButtonText}
|
editing={isSelectedCardModal}
|
||||||
showBackButton={false}
|
steps={membershipSteps}
|
||||||
showNextButton={showMethodModalPrimary}
|
onClose={handleCreateModalClose}
|
||||||
showRemoveButton={isSelectedCardModal}
|
onConfirm={handleCreateModalPrimary}
|
||||||
onRemove={handleRemoveSelectedFromModal}
|
onDeselect={
|
||||||
backdropVariant="blurredYellow"
|
isSelectedCardModal ? handleRemoveSelectedFromModal : undefined
|
||||||
|
}
|
||||||
kebabTriggerAriaLabel={modalKebabMenu.triggerAriaLabel}
|
kebabTriggerAriaLabel={modalKebabMenu.triggerAriaLabel}
|
||||||
kebabMenuAriaLabel={modalKebabMenu.menuAriaLabel}
|
kebabMenuAriaLabel={modalKebabMenu.menuAriaLabel}
|
||||||
kebabMenuItems={kebabMenuItems}
|
kebabMenuItems={kebabMenuItems}
|
||||||
>
|
>
|
||||||
{pendingCardId && pendingDraft ? (
|
{pendingCardId && modalUsesWizardFieldBlocksBody ? (
|
||||||
modalUsesWizardFieldBlocksBody ? (
|
|
||||||
<CustomMethodCardModalBody
|
<CustomMethodCardModalBody
|
||||||
cardId={pendingCardId}
|
cardId={pendingCardId}
|
||||||
blocksById={state.customMethodCardFieldBlocksById}
|
blocksById={state.customMethodCardFieldBlocksById}
|
||||||
@@ -751,15 +754,8 @@ export function MembershipMethodsScreen() {
|
|||||||
: (next) => setDraftFieldBlocks(next)
|
: (next) => setDraftFieldBlocks(next)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
) : (
|
|
||||||
<MembershipMethodEditFields
|
|
||||||
value={pendingDraft}
|
|
||||||
onChange={handleDraftChange}
|
|
||||||
generateDraft={membershipPresetFor(pendingCardId)}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
) : null}
|
) : null}
|
||||||
</Create>
|
</MethodCardPolicyDialog>
|
||||||
<CustomMethodCardWizard
|
<CustomMethodCardWizard
|
||||||
isOpen={addCustomWizardOpen}
|
isOpen={addCustomWizardOpen}
|
||||||
onClose={handleCloseAddWizard}
|
onClose={handleCloseAddWizard}
|
||||||
|
|||||||
@@ -7,20 +7,17 @@
|
|||||||
* Layout matches {@link CreateFlowTwoColumnSelectShell}: one column below `lg` (1024px), two columns
|
* Layout matches {@link CreateFlowTwoColumnSelectShell}: one column below `lg` (1024px), two columns
|
||||||
* at `lg+` with a scrollable rail — same breakpoint and height chain as select steps, distinct content.
|
* at `lg+` with a scrollable rail — same breakpoint and height chain as select steps, distinct content.
|
||||||
*
|
*
|
||||||
* Card click opens the Figma "Add Approach" create modal (node `20870-72155`) with five controls
|
* Card click opens Overlay / Modal / Card as a routing view of its parts
|
||||||
* rendered by {@link DecisionApproachEditFields}: Core Principle, Applicable Scope, Step-by-Step
|
* ({@link MethodCardPolicyDialog}). Confirm persists the chip selection and
|
||||||
* Instructions, Consensus Level, and Objections & Deadlocks. The same field set is reused on
|
* any edits as a `decisionApproachDetailsById[id]` override.
|
||||||
* `/create/final-review` — see `FinalReviewChipEditModal`. Confirm persists both the chip
|
|
||||||
* selection and any user edits as a `decisionApproachDetailsById[id]` override; section
|
|
||||||
* defaults come from `messages/en/create/customRule/decisionApproaches.json` and will be
|
|
||||||
* replaced with DB-driven content.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useState, useCallback, useMemo, useRef } from "react";
|
import { useState, useCallback, useMemo, useRef } from "react";
|
||||||
import CardStack from "../../../../components/cards/CardStack";
|
import CardStack from "../../../../components/cards/CardStack";
|
||||||
import HeaderLockup from "../../../../components/type/HeaderLockup";
|
import HeaderLockup from "../../../../components/type/HeaderLockup";
|
||||||
import Create from "../../../../components/modals/Create";
|
|
||||||
import InlineTextButton from "../../../../components/buttons/InlineTextButton";
|
import InlineTextButton from "../../../../components/buttons/InlineTextButton";
|
||||||
|
import { MethodCardPolicyDialog } from "../../components/MethodCardPolicyDialog";
|
||||||
|
import { useDecisionApproachPolicySteps } from "../../components/methodCardPolicySteps";
|
||||||
import InfoMessageBox from "../../../../components/controls/InfoMessageBox";
|
import InfoMessageBox from "../../../../components/controls/InfoMessageBox";
|
||||||
import type { InfoMessageBoxItem } from "../../../../components/controls/InfoMessageBox/InfoMessageBox.types";
|
import type { InfoMessageBoxItem } from "../../../../components/controls/InfoMessageBox/InfoMessageBox.types";
|
||||||
import { useMessages } from "../../../../contexts/MessagesContext";
|
import { useMessages } from "../../../../contexts/MessagesContext";
|
||||||
@@ -30,7 +27,6 @@ import { useBeforeUnloadGuard } from "../../../../hooks/useBeforeUnloadGuard";
|
|||||||
import { useDiscardCustomizeConfirm } from "../../hooks/useDiscardCustomizeConfirm";
|
import { useDiscardCustomizeConfirm } from "../../hooks/useDiscardCustomizeConfirm";
|
||||||
import { useMethodCardDeckOrdering } from "../../hooks/useMethodCardDeckOrdering";
|
import { useMethodCardDeckOrdering } from "../../hooks/useMethodCardDeckOrdering";
|
||||||
import { CreateFlowTwoColumnSelectShell } from "../../components/CreateFlowTwoColumnSelectShell";
|
import { CreateFlowTwoColumnSelectShell } from "../../components/CreateFlowTwoColumnSelectShell";
|
||||||
import { DecisionApproachEditFields } from "../../components/methodEditFields";
|
|
||||||
import CustomMethodCardWizard from "../../components/CustomMethodCardWizard";
|
import CustomMethodCardWizard from "../../components/CustomMethodCardWizard";
|
||||||
import { uploadCreateFlowFile } from "../../../../../lib/create/uploadToServer";
|
import { uploadCreateFlowFile } from "../../../../../lib/create/uploadToServer";
|
||||||
import { decisionApproachPresetFor, emptyDecisionApproachTextFields } from "../../../../../lib/create/finalReviewChipPresets";
|
import { decisionApproachPresetFor, emptyDecisionApproachTextFields } from "../../../../../lib/create/finalReviewChipPresets";
|
||||||
@@ -130,7 +126,8 @@ export function DecisionApproachesScreen() {
|
|||||||
[da.methods, selectedIds, state.customMethodCardMetaById],
|
[da.methods, selectedIds, state.customMethodCardMetaById],
|
||||||
);
|
);
|
||||||
|
|
||||||
const { sampleCards, compactCardIds, methodById, recommendationsReady } =
|
const rec = m.create.customRule.recommendations;
|
||||||
|
const { sampleCards, compactCardIds, methodById, recommendationsReady, showDeckLine, compactLimit } =
|
||||||
useMethodCardDeckOrdering(
|
useMethodCardDeckOrdering(
|
||||||
"decisionApproaches",
|
"decisionApproaches",
|
||||||
mergedMethods,
|
mergedMethods,
|
||||||
@@ -237,10 +234,17 @@ export function DecisionApproachesScreen() {
|
|||||||
},
|
},
|
||||||
[markCreateFlowInteraction],
|
[markCreateFlowInteraction],
|
||||||
);
|
);
|
||||||
|
const decisionSteps = useDecisionApproachPolicySteps(
|
||||||
|
pendingDraft ??
|
||||||
|
emptyDecisionApproachTextFields(
|
||||||
|
decisionApproachPresetFor(pendingCardId ?? ""),
|
||||||
|
),
|
||||||
|
handleDraftChange,
|
||||||
|
pendingCardId ? decisionApproachPresetFor(pendingCardId) : undefined,
|
||||||
|
);
|
||||||
|
|
||||||
const isSelectedCardModal =
|
const isSelectedCardModal =
|
||||||
pendingCardId !== null && selectedIds.includes(pendingCardId);
|
pendingCardId !== null && selectedIds.includes(pendingCardId);
|
||||||
const showMethodModalPrimary = true;
|
|
||||||
|
|
||||||
const customFacetDetailsMatchPreset = useMemo(() => {
|
const customFacetDetailsMatchPreset = useMemo(() => {
|
||||||
if (!pendingCardId || !pendingDraft) return false;
|
if (!pendingCardId || !pendingDraft) return false;
|
||||||
@@ -776,7 +780,7 @@ export function DecisionApproachesScreen() {
|
|||||||
onCardSelect={handleCardSelect}
|
onCardSelect={handleCardSelect}
|
||||||
expanded={expanded}
|
expanded={expanded}
|
||||||
onToggleExpand={handleToggleExpand}
|
onToggleExpand={handleToggleExpand}
|
||||||
hasMore={true}
|
hasMore={sampleCards.length > compactLimit}
|
||||||
toggleLabel={da.cardStack.toggleSeeAll}
|
toggleLabel={da.cardStack.toggleSeeAll}
|
||||||
showLessLabel={da.cardStack.toggleShowLess}
|
showLessLabel={da.cardStack.toggleShowLess}
|
||||||
title=""
|
title=""
|
||||||
@@ -794,32 +798,34 @@ export function DecisionApproachesScreen() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
layout="singleStack"
|
layout="singleStack"
|
||||||
compactRecommendedLimit={5}
|
compactRecommendedLimit={compactLimit}
|
||||||
compactCardIds={compactCardIds}
|
compactCardIds={compactCardIds}
|
||||||
|
deckLine={showDeckLine ? rec.deckLine : undefined}
|
||||||
|
searchPlaceholder={rec.searchPlaceholder}
|
||||||
|
alternativePrefix={rec.alternative}
|
||||||
|
onAlternativeSelect={handleCardSelect}
|
||||||
className="w-full lg:h-full lg:min-h-0 lg:flex-1"
|
className="w-full lg:h-full lg:min-h-0 lg:flex-1"
|
||||||
headerLockupSize={mdUp ? "L" : "M"}
|
headerLockupSize={mdUp ? "L" : "M"}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Create
|
<MethodCardPolicyDialog
|
||||||
isOpen={createModalOpen}
|
isOpen={createModalOpen}
|
||||||
onClose={handleCreateModalClose}
|
cardLabel={modalConfig.title}
|
||||||
onNext={handleCreateModalPrimary}
|
|
||||||
title={modalConfig.title}
|
|
||||||
description={modalConfig.description}
|
description={modalConfig.description}
|
||||||
nextButtonText={modalConfig.nextButtonText}
|
editing={isSelectedCardModal}
|
||||||
showBackButton={false}
|
steps={decisionSteps}
|
||||||
showNextButton={showMethodModalPrimary}
|
onClose={handleCreateModalClose}
|
||||||
showRemoveButton={isSelectedCardModal}
|
onConfirm={handleCreateModalPrimary}
|
||||||
onRemove={handleRemoveSelectedFromModal}
|
onDeselect={
|
||||||
backdropVariant="blurredYellow"
|
isSelectedCardModal ? handleRemoveSelectedFromModal : undefined
|
||||||
|
}
|
||||||
kebabTriggerAriaLabel={modalKebabMenu.triggerAriaLabel}
|
kebabTriggerAriaLabel={modalKebabMenu.triggerAriaLabel}
|
||||||
kebabMenuAriaLabel={modalKebabMenu.menuAriaLabel}
|
kebabMenuAriaLabel={modalKebabMenu.menuAriaLabel}
|
||||||
kebabMenuItems={kebabMenuItems}
|
kebabMenuItems={kebabMenuItems}
|
||||||
>
|
>
|
||||||
{pendingCardId && pendingDraft ? (
|
{pendingCardId && modalUsesWizardFieldBlocksBody ? (
|
||||||
modalUsesWizardFieldBlocksBody ? (
|
|
||||||
<CustomMethodCardModalBody
|
<CustomMethodCardModalBody
|
||||||
cardId={pendingCardId}
|
cardId={pendingCardId}
|
||||||
blocksById={state.customMethodCardFieldBlocksById}
|
blocksById={state.customMethodCardFieldBlocksById}
|
||||||
@@ -836,15 +842,8 @@ export function DecisionApproachesScreen() {
|
|||||||
: (next) => setDraftFieldBlocks(next)
|
: (next) => setDraftFieldBlocks(next)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
) : (
|
|
||||||
<DecisionApproachEditFields
|
|
||||||
value={pendingDraft}
|
|
||||||
onChange={handleDraftChange}
|
|
||||||
generateDraft={decisionApproachPresetFor(pendingCardId)}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
) : null}
|
) : null}
|
||||||
</Create>
|
</MethodCardPolicyDialog>
|
||||||
</CreateFlowTwoColumnSelectShell>
|
</CreateFlowTwoColumnSelectShell>
|
||||||
<CustomMethodCardWizard
|
<CustomMethodCardWizard
|
||||||
isOpen={addCustomWizardOpen}
|
isOpen={addCustomWizardOpen}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { memo, useCallback, useState } from "react";
|
import { memo, useCallback, useEffect, useState } from "react";
|
||||||
import { useTranslation } from "../../../contexts/MessagesContext";
|
import { useTranslation } from "../../../contexts/MessagesContext";
|
||||||
import { CardStackView } from "./CardStack.view";
|
import { CardStackView } from "./CardStack.view";
|
||||||
import type { CardStackProps } from "./CardStack.types";
|
import type { CardStackProps } from "./CardStack.types";
|
||||||
@@ -29,7 +29,7 @@ const CardStackContainer = memo<CardStackProps>(
|
|||||||
title = "",
|
title = "",
|
||||||
description = "",
|
description = "",
|
||||||
layout = "default",
|
layout = "default",
|
||||||
compactRecommendedLimit = 5,
|
compactRecommendedLimit = 7,
|
||||||
compactCardIds,
|
compactCardIds,
|
||||||
compactDesktopLayout: compactDesktopLayoutProp = "grid",
|
compactDesktopLayout: compactDesktopLayoutProp = "grid",
|
||||||
headerLockupSize,
|
headerLockupSize,
|
||||||
@@ -39,16 +39,25 @@ const CardStackContainer = memo<CardStackProps>(
|
|||||||
addCardLabel = "",
|
addCardLabel = "",
|
||||||
addCardAriaLabel = "",
|
addCardAriaLabel = "",
|
||||||
onAddCard,
|
onAddCard,
|
||||||
|
deckLine = "",
|
||||||
|
searchPlaceholder = "",
|
||||||
|
alternativePrefix = "",
|
||||||
|
onAlternativeSelect,
|
||||||
}) => {
|
}) => {
|
||||||
const t = useTranslation("controlsChrome");
|
const t = useTranslation("controlsChrome");
|
||||||
const [internalExpanded, setInternalExpanded] = useState(false);
|
const [internalExpanded, setInternalExpanded] = useState(false);
|
||||||
const [internalSelectedIds, setInternalSelectedIds] = useState<string[]>(
|
const [internalSelectedIds, setInternalSelectedIds] = useState<string[]>(
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
|
||||||
const expanded =
|
const expanded =
|
||||||
controlledExpanded !== undefined ? controlledExpanded : internalExpanded;
|
controlledExpanded !== undefined ? controlledExpanded : internalExpanded;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!expanded) setSearchQuery("");
|
||||||
|
}, [expanded]);
|
||||||
|
|
||||||
const handleToggleExpand = useCallback(() => {
|
const handleToggleExpand = useCallback(() => {
|
||||||
if (controlledOnToggleExpand) {
|
if (controlledOnToggleExpand) {
|
||||||
controlledOnToggleExpand();
|
controlledOnToggleExpand();
|
||||||
@@ -102,6 +111,12 @@ const CardStackContainer = memo<CardStackProps>(
|
|||||||
addCardLabel={addCardLabel}
|
addCardLabel={addCardLabel}
|
||||||
addCardAriaLabel={addCardAriaLabel}
|
addCardAriaLabel={addCardAriaLabel}
|
||||||
onAddCard={onAddCard}
|
onAddCard={onAddCard}
|
||||||
|
deckLine={deckLine}
|
||||||
|
searchPlaceholder={searchPlaceholder}
|
||||||
|
searchQuery={searchQuery}
|
||||||
|
onSearchQueryChange={setSearchQuery}
|
||||||
|
alternativePrefix={alternativePrefix}
|
||||||
|
onAlternativeSelect={onAlternativeSelect}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ export interface CardStackItem {
|
|||||||
label: string;
|
label: string;
|
||||||
supportText?: string;
|
supportText?: string;
|
||||||
recommended?: boolean;
|
recommended?: boolean;
|
||||||
|
/** Compact-seven why line; See all keeps `supportText`. */
|
||||||
|
compactSupportText?: string;
|
||||||
|
/** One alternative per job, shown under the compact card. */
|
||||||
|
alternative?: { id: string; label: string };
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CardStackProps {
|
export interface CardStackProps {
|
||||||
@@ -23,9 +27,16 @@ export interface CardStackProps {
|
|||||||
/** "default" = compact grid/column + expanded 2-col 142px tiles; "singleStack" = always one column, expand shows more in same stack */
|
/** "default" = compact grid/column + expanded 2-col 142px tiles; "singleStack" = always one column, expand shows more in same stack */
|
||||||
layout?: "default" | "singleStack";
|
layout?: "default" | "singleStack";
|
||||||
/**
|
/**
|
||||||
* Max recommended cards in compact (non-expanded) mode. Default 5; Figma compact stack uses 3.
|
* Max recommended cards in compact (non-expanded) mode. Default 7.
|
||||||
*/
|
*/
|
||||||
compactRecommendedLimit?: number;
|
compactRecommendedLimit?: number;
|
||||||
|
/** Line above the compact seven, in place of a tag on every card. */
|
||||||
|
deckLine?: string;
|
||||||
|
/** See-all search field placeholder. */
|
||||||
|
searchPlaceholder?: string;
|
||||||
|
/** Prefix for the compact alternative row. */
|
||||||
|
alternativePrefix?: string;
|
||||||
|
onAlternativeSelect?: (id: string) => void;
|
||||||
/**
|
/**
|
||||||
* Optional explicit list of card ids to render in the compact slot, in
|
* Optional explicit list of card ids to render in the compact slot, in
|
||||||
* order. When provided, this overrides the default
|
* order. When provided, this overrides the default
|
||||||
@@ -78,4 +89,10 @@ export interface CardStackViewProps {
|
|||||||
addCardLabel: string;
|
addCardLabel: string;
|
||||||
addCardAriaLabel: string;
|
addCardAriaLabel: string;
|
||||||
onAddCard?: () => void;
|
onAddCard?: () => void;
|
||||||
|
deckLine: string;
|
||||||
|
searchPlaceholder: string;
|
||||||
|
searchQuery: string;
|
||||||
|
onSearchQueryChange: (value: string) => void;
|
||||||
|
alternativePrefix: string;
|
||||||
|
onAlternativeSelect?: (id: string) => void;
|
||||||
}
|
}
|
||||||
@@ -4,7 +4,7 @@ import type { ReactNode } from "react";
|
|||||||
import HeaderLockup from "../../type/HeaderLockup";
|
import HeaderLockup from "../../type/HeaderLockup";
|
||||||
import type { HeaderLockupSizeValue } from "../../type/HeaderLockup/HeaderLockup.types";
|
import type { HeaderLockupSizeValue } from "../../type/HeaderLockup/HeaderLockup.types";
|
||||||
import Selection from "../Selection";
|
import Selection from "../Selection";
|
||||||
import type { CardStackViewProps } from "./CardStack.types";
|
import type { CardStackItem, CardStackViewProps } from "./CardStack.types";
|
||||||
|
|
||||||
/** Figma Card / CardSelection on Compact + Expanded Card Stack (`281×142`). */
|
/** Figma Card / CardSelection on Compact + Expanded Card Stack (`281×142`). */
|
||||||
const FIGMA_CARD_SELECTION_TILE_CLASS =
|
const FIGMA_CARD_SELECTION_TILE_CLASS =
|
||||||
@@ -38,6 +38,87 @@ function CardStackHeaderLockup({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function matchesSearch(item: CardStackItem, query: string): boolean {
|
||||||
|
if (!query) return true;
|
||||||
|
const haystack = `${item.label} ${item.supportText ?? ""}`.toLowerCase();
|
||||||
|
return haystack.includes(query.toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
function SearchGlyph() {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
aria-hidden
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
className="pointer-events-none absolute right-3 top-1/2 size-6 -translate-y-1/2 text-[var(--color-content-default-tertiary)]"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<circle cx="11" cy="11" r="7" stroke="currentColor" strokeWidth="1.5" />
|
||||||
|
<path
|
||||||
|
d="M20 20l-3.5-3.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
strokeLinecap="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function StackCard({
|
||||||
|
item,
|
||||||
|
expanded,
|
||||||
|
selected,
|
||||||
|
size,
|
||||||
|
className,
|
||||||
|
onSelect,
|
||||||
|
onAlternativeSelect,
|
||||||
|
alternativePrefix,
|
||||||
|
}: {
|
||||||
|
item: CardStackItem;
|
||||||
|
expanded: boolean;
|
||||||
|
selected: boolean;
|
||||||
|
size: "m" | "l";
|
||||||
|
className?: string;
|
||||||
|
onSelect: (id: string) => void;
|
||||||
|
onAlternativeSelect?: (id: string) => void;
|
||||||
|
alternativePrefix: string;
|
||||||
|
}) {
|
||||||
|
const supportText =
|
||||||
|
!expanded && !selected && item.compactSupportText
|
||||||
|
? item.compactSupportText
|
||||||
|
: item.supportText;
|
||||||
|
const recommended = Boolean(expanded && item.recommended);
|
||||||
|
const alternative =
|
||||||
|
!expanded && !selected && item.alternative && onAlternativeSelect ? (
|
||||||
|
<p className="w-full text-x-small-paragraph text-[var(--color-content-invert-tertiary)]">
|
||||||
|
{alternativePrefix}{" "}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="underline text-[var(--color-content-invert-primary)]"
|
||||||
|
onClick={() => onAlternativeSelect(item.alternative!.id)}
|
||||||
|
>
|
||||||
|
{item.alternative.label}
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
) : null;
|
||||||
|
return (
|
||||||
|
<div className="flex w-full flex-col gap-1">
|
||||||
|
<Selection
|
||||||
|
id={item.id}
|
||||||
|
label={item.label}
|
||||||
|
supportText={supportText}
|
||||||
|
recommended={recommended}
|
||||||
|
selected={selected}
|
||||||
|
size={size}
|
||||||
|
showInfoIcon={false}
|
||||||
|
className={className}
|
||||||
|
onClick={() => onSelect(item.id)}
|
||||||
|
/>
|
||||||
|
{alternative}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function CardStackAddCardButton({
|
function CardStackAddCardButton({
|
||||||
label,
|
label,
|
||||||
ariaLabel,
|
ariaLabel,
|
||||||
@@ -98,6 +179,12 @@ export function CardStackView({
|
|||||||
addCardLabel,
|
addCardLabel,
|
||||||
addCardAriaLabel,
|
addCardAriaLabel,
|
||||||
onAddCard,
|
onAddCard,
|
||||||
|
deckLine,
|
||||||
|
searchPlaceholder,
|
||||||
|
searchQuery,
|
||||||
|
onSearchQueryChange,
|
||||||
|
alternativePrefix,
|
||||||
|
onAlternativeSelect,
|
||||||
}: CardStackViewProps) {
|
}: CardStackViewProps) {
|
||||||
const addTile =
|
const addTile =
|
||||||
showAddCard && onAddCard && addCardLabel.length > 0 ? (
|
showAddCard && onAddCard && addCardLabel.length > 0 ? (
|
||||||
@@ -130,7 +217,9 @@ export function CardStackView({
|
|||||||
// At lg+ the cards scroll inside this column and the See all / Show less
|
// At lg+ the cards scroll inside this column and the See all / Show less
|
||||||
// control stays pinned below (Figma Flow — Right Rail).
|
// control stays pinned below (Figma Flow — Right Rail).
|
||||||
if (layout === "singleStack") {
|
if (layout === "singleStack") {
|
||||||
const displayedCards = expanded ? cards : compactCards;
|
const displayedCards = (expanded ? cards : compactCards).filter((item) =>
|
||||||
|
expanded ? matchesSearch(item, searchQuery) : true,
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`flex w-full min-w-0 flex-col gap-6 lg:h-full lg:max-h-full lg:min-h-0 lg:flex-1 lg:overflow-hidden ${className}`}
|
className={`flex w-full min-w-0 flex-col gap-6 lg:h-full lg:max-h-full lg:min-h-0 lg:flex-1 lg:overflow-hidden ${className}`}
|
||||||
@@ -142,18 +231,35 @@ export function CardStackView({
|
|||||||
size={lockupSize}
|
size={lockupSize}
|
||||||
wrapperClassName="min-w-0 shrink-0"
|
wrapperClassName="min-w-0 shrink-0"
|
||||||
/>
|
/>
|
||||||
|
{!expanded && deckLine ? (
|
||||||
|
<p className="shrink-0 text-x-small-paragraph text-[var(--color-content-default-tertiary)]">
|
||||||
|
{deckLine}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
{expanded && searchPlaceholder ? (
|
||||||
|
<div className="relative w-full shrink-0">
|
||||||
|
<input
|
||||||
|
type="search"
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(event) => onSearchQueryChange(event.target.value)}
|
||||||
|
placeholder={searchPlaceholder}
|
||||||
|
aria-label={searchPlaceholder}
|
||||||
|
className="h-10 w-full rounded-[var(--radius-200)] border border-[var(--color-border-default-control)] bg-[var(--color-surface-default-secondary)] px-3 py-2.5 pr-11 text-medium-paragraph text-[var(--color-content-default-primary)] placeholder:text-[var(--color-content-default-tertiary)] outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-border-invert-primary)]"
|
||||||
|
/>
|
||||||
|
<SearchGlyph />
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
<div className="flex w-full min-w-0 flex-col gap-2 lg:min-h-0 lg:flex-1 lg:overflow-y-auto scrollbar-design">
|
<div className="flex w-full min-w-0 flex-col gap-2 lg:min-h-0 lg:flex-1 lg:overflow-y-auto scrollbar-design">
|
||||||
{displayedCards.map((item) => (
|
{displayedCards.map((item) => (
|
||||||
<Selection
|
<StackCard
|
||||||
key={item.id}
|
key={item.id}
|
||||||
id={item.id}
|
item={item}
|
||||||
label={item.label}
|
expanded={expanded}
|
||||||
supportText={item.supportText}
|
|
||||||
recommended={item.recommended ?? false}
|
|
||||||
selected={isSelected(item.id)}
|
selected={isSelected(item.id)}
|
||||||
size="l"
|
size="l"
|
||||||
showInfoIcon={false}
|
onSelect={onCardSelect}
|
||||||
onClick={() => onCardSelect(item.id)}
|
onAlternativeSelect={onAlternativeSelect}
|
||||||
|
alternativePrefix={alternativePrefix}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{addTile}
|
{addTile}
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ function chipSurfaceStyle(
|
|||||||
return paint(
|
return paint(
|
||||||
"transparent",
|
"transparent",
|
||||||
"var(--color-content-default-brand-primary)",
|
"var(--color-content-default-brand-primary)",
|
||||||
"var(--color-border-default-control)",
|
"var(--color-content-default-tertiary)",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -95,14 +95,24 @@ export function stringArraysEqual(
|
|||||||
return a.every((value, index) => value === b[index]);
|
return a.every((value, index) => value === b[index]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function decisionApproachDefaultScopeChips(): string[] {
|
||||||
|
const chips = (
|
||||||
|
decisionApproachesMessages as { defaultScopeChips?: unknown }
|
||||||
|
).defaultScopeChips;
|
||||||
|
if (!Array.isArray(chips)) return [];
|
||||||
|
return chips.filter((value): value is string => typeof value === "string");
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Offer the four sidebar labels as Applicable Scope chips without persisting
|
* Offer the signed-off deck chips, then this card's scopes, then the coverage
|
||||||
* them onto `applicableScope` (unchecked keys must not publish as defaults).
|
* checklist labels. Unchecked extras are display-only and must not publish
|
||||||
|
* as defaults.
|
||||||
*/
|
*/
|
||||||
export function withDecisionApproachKeyResourceScopes(
|
export function withDecisionApproachKeyResourceScopes(
|
||||||
scopes: readonly string[],
|
scopes: readonly string[],
|
||||||
): string[] {
|
): string[] {
|
||||||
return uniquePreserveOrder([
|
return uniquePreserveOrder([
|
||||||
|
...decisionApproachDefaultScopeChips(),
|
||||||
...scopes,
|
...scopes,
|
||||||
...decisionApproachKeyResourceLabels(),
|
...decisionApproachKeyResourceLabels(),
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import type { GroupAnswers, RecommendationReason, TraitKey } from "./deckTypes";
|
||||||
|
|
||||||
|
export type RecommendationWhyCopy = {
|
||||||
|
why: {
|
||||||
|
fit: string;
|
||||||
|
fitWithAnswer: string;
|
||||||
|
opened: string;
|
||||||
|
added: string;
|
||||||
|
};
|
||||||
|
group: {
|
||||||
|
kindAndSize: string;
|
||||||
|
kindOnly: string;
|
||||||
|
sizeOnly: string;
|
||||||
|
kind: Record<string, string>;
|
||||||
|
size: Record<string, string>;
|
||||||
|
};
|
||||||
|
answerThirdPerson: Record<string, string>;
|
||||||
|
answerYouSaid: Record<string, string>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function formatGroupPhrase(
|
||||||
|
answers: GroupAnswers,
|
||||||
|
copy: RecommendationWhyCopy,
|
||||||
|
): string {
|
||||||
|
const kind = answers.mainKind ? copy.group.kind[answers.mainKind] : "";
|
||||||
|
const size = answers.size ? copy.group.size[answers.size] : "";
|
||||||
|
if (kind && size) {
|
||||||
|
return copy.group.kindAndSize
|
||||||
|
.replace("{kind}", kind)
|
||||||
|
.replace("{size}", size);
|
||||||
|
}
|
||||||
|
if (kind) return copy.group.kindOnly.replace("{kind}", kind);
|
||||||
|
if (size) return copy.group.sizeOnly.replace("{size}", size);
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function answerPhrase(
|
||||||
|
key: TraitKey,
|
||||||
|
phrases: Record<string, string>,
|
||||||
|
): string {
|
||||||
|
return phrases[key] ?? key;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Why line for a recommended card in the compact seven.
|
||||||
|
* Near-universal cards keep their catalog description instead.
|
||||||
|
*/
|
||||||
|
export function formatRecommendationWhyLine(
|
||||||
|
reason: RecommendationReason,
|
||||||
|
answers: GroupAnswers,
|
||||||
|
copy: RecommendationWhyCopy,
|
||||||
|
): string {
|
||||||
|
if (reason.kind === "added") {
|
||||||
|
return copy.why.added.replace(
|
||||||
|
"{answer}",
|
||||||
|
answerPhrase(reason.answer, copy.answerYouSaid),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (reason.kind === "opened") {
|
||||||
|
return copy.why.opened.replace(
|
||||||
|
"{answer}",
|
||||||
|
answerPhrase(reason.answer, copy.answerYouSaid),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const group = formatGroupPhrase(answers, copy);
|
||||||
|
if (!group) return "";
|
||||||
|
if (reason.answer) {
|
||||||
|
return copy.why.fitWithAnswer
|
||||||
|
.replace("{group}", group)
|
||||||
|
.replace("{answer}", answerPhrase(reason.answer, copy.answerThirdPerson));
|
||||||
|
}
|
||||||
|
return copy.why.fit.replace("{group}", group);
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"_comment": "Shared Overlay / Modal / Card chrome for the four decks (Figma 24973:1852, routing view 25181:55334).",
|
||||||
|
"addToRule": "Add to Rule",
|
||||||
|
"deselect": "Deselect",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"save": "Save",
|
||||||
|
"stepBadge": "Step {n}",
|
||||||
|
"emptyStepDescription": "Write this part of the policy.",
|
||||||
|
"scopeEmptyDescription": "Choose which decisions this card covers.",
|
||||||
|
"filledHint": "These are your community's words. Change anything that does not fit.",
|
||||||
|
"decreaseConsensusLevel": "Decrease consensus level",
|
||||||
|
"increaseConsensusLevel": "Increase consensus level"
|
||||||
|
}
|
||||||
@@ -68,6 +68,12 @@
|
|||||||
"noSetShare": "No set share"
|
"noSetShare": "No set share"
|
||||||
},
|
},
|
||||||
"scopeAddButtonLabel": "Add a scope",
|
"scopeAddButtonLabel": "Add a scope",
|
||||||
|
"defaultScopeChips": [
|
||||||
|
"Changing the Rule",
|
||||||
|
"Money",
|
||||||
|
"Projects",
|
||||||
|
"Asking someone to leave"
|
||||||
|
],
|
||||||
"methods": [
|
"methods": [
|
||||||
{
|
{
|
||||||
"id": "decision-speeds",
|
"id": "decision-speeds",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"_comment": "Create flow, recommendation lines on each deck (Figma CR-291, deck seven board 24716:27074 and card treatments 24717:27255). Draft copy. Dani signs off the why lines and the alternative row.",
|
"_comment": "Create flow, recommendation lines on each deck (Figma deck seven board 25019:114405 and card treatments 25019:114480). Draft copy. Dani signs off the why lines and the alternative row.",
|
||||||
"deckLine": "Recommended for your group",
|
"deckLine": "Recommended for your community",
|
||||||
|
"searchPlaceholder": "Search this deck",
|
||||||
"tags": {
|
"tags": {
|
||||||
"recommended": "RECOMMENDED",
|
"recommended": "RECOMMENDED",
|
||||||
"everyRuleHas": "Every Rule has"
|
"everyRuleHas": "Every Rule has"
|
||||||
@@ -47,5 +48,5 @@
|
|||||||
"records": "you keep information about the people you serve",
|
"records": "you keep information about the people you serve",
|
||||||
"risky": "your work carries physical risk, or needs a license, permit or inspection"
|
"risky": "your work carries physical risk, or needs a license, permit or inspection"
|
||||||
},
|
},
|
||||||
"alternative": "Another way groups do this"
|
"alternative": "Another way communities do this:"
|
||||||
}
|
}
|
||||||
@@ -59,6 +59,8 @@ import createDecisionApproaches from "./create/customRule/decisionApproaches.jso
|
|||||||
import createConflictManagement from "./create/customRule/conflictManagement.json";
|
import createConflictManagement from "./create/customRule/conflictManagement.json";
|
||||||
import createCustomMethodCardWizard from "./create/customRule/customMethodCardWizard.json";
|
import createCustomMethodCardWizard from "./create/customRule/customMethodCardWizard.json";
|
||||||
import createModalKebabMenu from "./create/customRule/modalKebabMenu.json";
|
import createModalKebabMenu from "./create/customRule/modalKebabMenu.json";
|
||||||
|
import createCardDialog from "./create/customRule/cardDialog.json";
|
||||||
|
import createRecommendations from "./create/customRule/recommendations.json";
|
||||||
|
|
||||||
// create – stage 3: reviewAndComplete
|
// create – stage 3: reviewAndComplete
|
||||||
import createConfirmStakeholders from "./create/reviewAndComplete/confirmStakeholders.json";
|
import createConfirmStakeholders from "./create/reviewAndComplete/confirmStakeholders.json";
|
||||||
@@ -133,6 +135,8 @@ export default {
|
|||||||
conflictManagement: createConflictManagement,
|
conflictManagement: createConflictManagement,
|
||||||
customMethodCardWizard: createCustomMethodCardWizard,
|
customMethodCardWizard: createCustomMethodCardWizard,
|
||||||
modalKebabMenu: createModalKebabMenu,
|
modalKebabMenu: createModalKebabMenu,
|
||||||
|
cardDialog: createCardDialog,
|
||||||
|
recommendations: createRecommendations,
|
||||||
},
|
},
|
||||||
reviewAndComplete: {
|
reviewAndComplete: {
|
||||||
confirmStakeholders: createConfirmStakeholders,
|
confirmStakeholders: createConfirmStakeholders,
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ describe("Chip", () => {
|
|||||||
expect(chip.style.color).toContain("--color-content-default-brand-primary");
|
expect(chip.style.color).toContain("--color-content-default-brand-primary");
|
||||||
expect(chip.style.backgroundColor).toBe("transparent");
|
expect(chip.style.backgroundColor).toBe("transparent");
|
||||||
expect(chip.style.borderColor).toContain(
|
expect(chip.style.borderColor).toContain(
|
||||||
"--color-border-default-control",
|
"--color-content-default-tertiary",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -62,6 +62,27 @@ function ScreenWithStateProbe({
|
|||||||
* snapping back to preset copy. See {@link CommunicationMethodEditFields}
|
* snapping back to preset copy. See {@link CommunicationMethodEditFields}
|
||||||
* and `buildPublishPayload` for the read side.
|
* and `buildPublishPayload` for the read side.
|
||||||
*/
|
*/
|
||||||
|
const COMMUNICATION_STEPS = [
|
||||||
|
"Core principle and scope",
|
||||||
|
"How it works",
|
||||||
|
"Norms",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
function openPolicyStep(dialog: HTMLElement, name: string) {
|
||||||
|
fireEvent.click(within(dialog).getByRole("button", { name }));
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateAllCommunicationSteps(dialog: HTMLElement) {
|
||||||
|
for (const name of COMMUNICATION_STEPS) {
|
||||||
|
openPolicyStep(dialog, name);
|
||||||
|
const generate = within(dialog).queryByRole("button", {
|
||||||
|
name: "Generate draft",
|
||||||
|
});
|
||||||
|
if (generate) fireEvent.click(generate);
|
||||||
|
fireEvent.click(within(dialog).getByRole("button", { name: "Save" }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
describe("CommunicationMethodsScreen — Add card persistence", () => {
|
describe("CommunicationMethodsScreen — Add card persistence", () => {
|
||||||
it("starts empty and Generate draft inserts the catalog preset", async () => {
|
it("starts empty and Generate draft inserts the catalog preset", async () => {
|
||||||
let latest: CreateFlowState = {};
|
let latest: CreateFlowState = {};
|
||||||
@@ -79,24 +100,24 @@ describe("CommunicationMethodsScreen — Add card persistence", () => {
|
|||||||
|
|
||||||
const dialog = await screen.findByRole("dialog");
|
const dialog = await screen.findByRole("dialog");
|
||||||
expect(within(dialog).queryByAltText("Help")).not.toBeInTheDocument();
|
expect(within(dialog).queryByAltText("Help")).not.toBeInTheDocument();
|
||||||
const textboxes = within(dialog).getAllByRole("textbox");
|
openPolicyStep(dialog, "Core principle and scope");
|
||||||
expect(textboxes.length).toBe(3);
|
const corePrincipleField = within(dialog).getByRole(
|
||||||
const corePrincipleField = textboxes[0] as HTMLTextAreaElement;
|
"textbox",
|
||||||
|
) as HTMLTextAreaElement;
|
||||||
expect(corePrincipleField.value).toBe("");
|
expect(corePrincipleField.value).toBe("");
|
||||||
|
|
||||||
const generateButtons = within(dialog).getAllByRole("button", {
|
fireEvent.click(
|
||||||
name: "Generate draft",
|
within(dialog).getByRole("button", { name: "Generate draft" }),
|
||||||
});
|
);
|
||||||
expect(generateButtons.length).toBe(3);
|
|
||||||
fireEvent.click(generateButtons[0]);
|
|
||||||
expect(corePrincipleField.value).toBe(
|
expect(corePrincipleField.value).toBe(
|
||||||
communicationPresetFor("signal").corePrinciple,
|
communicationPresetFor("signal").corePrinciple,
|
||||||
);
|
);
|
||||||
|
|
||||||
fireEvent.change(corePrincipleField, { target: { value: "Custom principle" } });
|
fireEvent.change(corePrincipleField, { target: { value: "Custom principle" } });
|
||||||
|
fireEvent.click(within(dialog).getByRole("button", { name: "Save" }));
|
||||||
fireEvent.click(
|
fireEvent.click(
|
||||||
within(dialog).getByRole("button", {
|
within(dialog).getByRole("button", {
|
||||||
name: "Add card",
|
name: "Add to Rule",
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -160,13 +181,11 @@ describe("CommunicationMethodsScreen — Add card persistence", () => {
|
|||||||
screen.getAllByRole("button", { name: /Signal: We coordinate in encrypted Signal groups/ })[0],
|
screen.getAllByRole("button", { name: /Signal: We coordinate in encrypted Signal groups/ })[0],
|
||||||
);
|
);
|
||||||
const dialog = await screen.findByRole("dialog");
|
const dialog = await screen.findByRole("dialog");
|
||||||
const textareas = within(dialog).getAllByRole(
|
expect(within(dialog).getByText("Saved principle")).toBeInTheDocument();
|
||||||
"textbox",
|
expect(within(dialog).getByText("Saved logistics")).toBeInTheDocument();
|
||||||
) as HTMLTextAreaElement[];
|
expect(within(dialog).getByText("Saved coc")).toBeInTheDocument();
|
||||||
expect(textareas.length).toBe(3);
|
openPolicyStep(dialog, "Core principle and scope");
|
||||||
expect(textareas[0].value).toBe("Saved principle");
|
expect(within(dialog).getByRole("textbox")).toHaveValue("Saved principle");
|
||||||
expect(textareas[1].value).toBe("Saved logistics");
|
|
||||||
expect(textareas[2].value).toBe("Saved coc");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("persists edits and closes when Save is clicked on an already-selected platform", async () => {
|
it("persists edits and closes when Save is clicked on an already-selected platform", async () => {
|
||||||
@@ -193,10 +212,11 @@ describe("CommunicationMethodsScreen — Add card persistence", () => {
|
|||||||
screen.getAllByRole("button", { name: /Signal: We coordinate in encrypted Signal groups/ })[0],
|
screen.getAllByRole("button", { name: /Signal: We coordinate in encrypted Signal groups/ })[0],
|
||||||
);
|
);
|
||||||
const dialog = await screen.findByRole("dialog");
|
const dialog = await screen.findByRole("dialog");
|
||||||
const textareas = within(dialog).getAllByRole(
|
openPolicyStep(dialog, "Core principle and scope");
|
||||||
"textbox",
|
fireEvent.change(within(dialog).getByRole("textbox"), {
|
||||||
) as HTMLTextAreaElement[];
|
target: { value: "Edited principle" },
|
||||||
fireEvent.change(textareas[0], { target: { value: "Edited principle" } });
|
});
|
||||||
|
fireEvent.click(within(dialog).getByRole("button", { name: "Save" }));
|
||||||
fireEvent.click(within(dialog).getByRole("button", { name: "Save" }));
|
fireEvent.click(within(dialog).getByRole("button", { name: "Save" }));
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
@@ -239,14 +259,14 @@ describe("CommunicationMethodsScreen — Add card persistence", () => {
|
|||||||
})[0],
|
})[0],
|
||||||
);
|
);
|
||||||
const dialog = await screen.findByRole("dialog");
|
const dialog = await screen.findByRole("dialog");
|
||||||
const textareas = within(dialog).getAllByRole(
|
|
||||||
"textbox",
|
|
||||||
) as HTMLTextAreaElement[];
|
|
||||||
expect(textareas).toHaveLength(3);
|
|
||||||
expect(textareas[0].value).toBe(details.corePrinciple);
|
|
||||||
expect(textareas[1].value).toBe(details.logisticsAdmin);
|
|
||||||
expect(textareas[2].value).toBe(details.codeOfConduct);
|
|
||||||
expect(within(dialog).queryByText(noFieldsHint)).not.toBeInTheDocument();
|
expect(within(dialog).queryByText(noFieldsHint)).not.toBeInTheDocument();
|
||||||
|
expect(within(dialog).getByText(details.corePrinciple)).toBeInTheDocument();
|
||||||
|
expect(within(dialog).getByText(details.logisticsAdmin)).toBeInTheDocument();
|
||||||
|
expect(within(dialog).getByText(details.codeOfConduct)).toBeInTheDocument();
|
||||||
|
openPolicyStep(dialog, "Core principle and scope");
|
||||||
|
expect(within(dialog).getByRole("textbox")).toHaveValue(
|
||||||
|
details.corePrinciple,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("opens meaning fields editable without Customize", async () => {
|
it("opens meaning fields editable without Customize", async () => {
|
||||||
@@ -262,14 +282,12 @@ describe("CommunicationMethodsScreen — Add card persistence", () => {
|
|||||||
screen.getAllByRole("button", { name: /Signal: We coordinate in encrypted Signal groups/ })[0],
|
screen.getAllByRole("button", { name: /Signal: We coordinate in encrypted Signal groups/ })[0],
|
||||||
);
|
);
|
||||||
const dialog = await screen.findByRole("dialog");
|
const dialog = await screen.findByRole("dialog");
|
||||||
const textareas = within(dialog).getAllByRole(
|
|
||||||
"textbox",
|
|
||||||
) as HTMLTextAreaElement[];
|
|
||||||
expect(textareas[0]).not.toBeDisabled();
|
|
||||||
fireEvent.click(within(dialog).getByRole("button", { name: "More options" }));
|
fireEvent.click(within(dialog).getByRole("button", { name: "More options" }));
|
||||||
expect(
|
expect(
|
||||||
screen.getByRole("menuitem", { name: "Customize" }),
|
screen.getByRole("menuitem", { name: "Customize" }),
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
|
openPolicyStep(dialog, "Core principle and scope");
|
||||||
|
expect(within(dialog).getByRole("textbox")).not.toBeDisabled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("closing the wizard without Finalize leaves the card modal and does not persist", async () => {
|
it("closing the wizard without Finalize leaves the card modal and does not persist", async () => {
|
||||||
@@ -347,11 +365,12 @@ describe("CommunicationMethodsScreen — Add card persistence", () => {
|
|||||||
screen.getAllByRole("button", { name: /Signal: We coordinate in encrypted Signal groups/ })[0],
|
screen.getAllByRole("button", { name: /Signal: We coordinate in encrypted Signal groups/ })[0],
|
||||||
);
|
);
|
||||||
const dialog = await screen.findByRole("dialog");
|
const dialog = await screen.findByRole("dialog");
|
||||||
const textboxes = within(dialog).getAllByRole(
|
openPolicyStep(dialog, "Core principle and scope");
|
||||||
"textbox",
|
fireEvent.change(within(dialog).getByRole("textbox"), {
|
||||||
) as HTMLTextAreaElement[];
|
target: { value: "Edited principle" },
|
||||||
fireEvent.change(textboxes[0], { target: { value: "Edited principle" } });
|
});
|
||||||
|
|
||||||
|
fireEvent.keyDown(document, { key: "Escape" });
|
||||||
fireEvent.keyDown(document, { key: "Escape" });
|
fireEvent.keyDown(document, { key: "Escape" });
|
||||||
const keepEditing = await screen.findByRole("button", {
|
const keepEditing = await screen.findByRole("button", {
|
||||||
name: "Keep editing",
|
name: "Keep editing",
|
||||||
@@ -390,10 +409,11 @@ describe("CommunicationMethodsScreen — Add card persistence", () => {
|
|||||||
screen.getAllByRole("button", { name: /Signal: We coordinate in encrypted Signal groups/ })[0],
|
screen.getAllByRole("button", { name: /Signal: We coordinate in encrypted Signal groups/ })[0],
|
||||||
);
|
);
|
||||||
const dialog = await screen.findByRole("dialog");
|
const dialog = await screen.findByRole("dialog");
|
||||||
const textboxes = within(dialog).getAllByRole(
|
openPolicyStep(dialog, "Core principle and scope");
|
||||||
"textbox",
|
fireEvent.change(within(dialog).getByRole("textbox"), {
|
||||||
) as HTMLTextAreaElement[];
|
target: { value: "Edited principle" },
|
||||||
fireEvent.change(textboxes[0], { target: { value: "Edited principle" } });
|
});
|
||||||
|
fireEvent.keyDown(document, { key: "Escape" });
|
||||||
fireEvent.keyDown(document, { key: "Escape" });
|
fireEvent.keyDown(document, { key: "Escape" });
|
||||||
await confirmDiscardCustomizeEdits();
|
await confirmDiscardCustomizeEdits();
|
||||||
|
|
||||||
@@ -473,11 +493,7 @@ describe("CommunicationMethodsScreen — Add card persistence", () => {
|
|||||||
screen.getAllByRole("button", { name: /Signal: We coordinate in encrypted Signal groups/ })[0],
|
screen.getAllByRole("button", { name: /Signal: We coordinate in encrypted Signal groups/ })[0],
|
||||||
);
|
);
|
||||||
const dialog = await screen.findByRole("dialog");
|
const dialog = await screen.findByRole("dialog");
|
||||||
for (const button of within(dialog).getAllByRole("button", {
|
generateAllCommunicationSteps(dialog);
|
||||||
name: "Generate draft",
|
|
||||||
})) {
|
|
||||||
fireEvent.click(button);
|
|
||||||
}
|
|
||||||
fireEvent.click(within(dialog).getByRole("button", { name: "More options" }));
|
fireEvent.click(within(dialog).getByRole("button", { name: "More options" }));
|
||||||
fireEvent.click(screen.getByRole("menuitem", { name: "Customize" }));
|
fireEvent.click(screen.getByRole("menuitem", { name: "Customize" }));
|
||||||
|
|
||||||
@@ -619,10 +635,10 @@ describe("CommunicationMethodsScreen — Add card persistence", () => {
|
|||||||
screen.getAllByRole("button", { name: /Signal: We coordinate in encrypted Signal groups/ })[0],
|
screen.getAllByRole("button", { name: /Signal: We coordinate in encrypted Signal groups/ })[0],
|
||||||
);
|
);
|
||||||
const dialog = await screen.findByRole("dialog");
|
const dialog = await screen.findByRole("dialog");
|
||||||
const textboxes = within(dialog).getAllByRole(
|
openPolicyStep(dialog, "Core principle and scope");
|
||||||
"textbox",
|
fireEvent.change(within(dialog).getByRole("textbox"), {
|
||||||
) as HTMLTextAreaElement[];
|
target: { value: "Unsaved principle" },
|
||||||
fireEvent.change(textboxes[0], { target: { value: "Unsaved principle" } });
|
});
|
||||||
expect(dispatchBeforeUnload()).toBe(true);
|
expect(dispatchBeforeUnload()).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -22,13 +22,8 @@ describe("ConflictManagementScreen", () => {
|
|||||||
})[0],
|
})[0],
|
||||||
);
|
);
|
||||||
const dialog = await screen.findByRole("dialog");
|
const dialog = await screen.findByRole("dialog");
|
||||||
const fields = within(dialog).getAllByRole("textbox");
|
|
||||||
expect(fields.length).toBeGreaterThan(0);
|
|
||||||
for (const field of fields) {
|
|
||||||
expect(field).toBeEnabled();
|
|
||||||
}
|
|
||||||
expect(
|
expect(
|
||||||
within(dialog).queryByRole("button", { name: "Remove" }),
|
within(dialog).queryByRole("button", { name: "Deselect" }),
|
||||||
).not.toBeInTheDocument();
|
).not.toBeInTheDocument();
|
||||||
fireEvent.click(
|
fireEvent.click(
|
||||||
within(dialog).getByRole("button", { name: "More options" }),
|
within(dialog).getByRole("button", { name: "More options" }),
|
||||||
@@ -36,5 +31,13 @@ describe("ConflictManagementScreen", () => {
|
|||||||
expect(
|
expect(
|
||||||
screen.getByRole("menuitem", { name: "Customize" }),
|
screen.getByRole("menuitem", { name: "Customize" }),
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
|
fireEvent.click(
|
||||||
|
within(dialog).getByRole("button", { name: "Core principle" }),
|
||||||
|
);
|
||||||
|
const fields = within(dialog).getAllByRole("textbox");
|
||||||
|
expect(fields.length).toBeGreaterThan(0);
|
||||||
|
for (const field of fields) {
|
||||||
|
expect(field).toBeEnabled();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -22,13 +22,8 @@ describe("MembershipMethodsScreen", () => {
|
|||||||
})[0],
|
})[0],
|
||||||
);
|
);
|
||||||
const dialog = await screen.findByRole("dialog");
|
const dialog = await screen.findByRole("dialog");
|
||||||
const fields = within(dialog).getAllByRole("textbox");
|
|
||||||
expect(fields.length).toBeGreaterThan(0);
|
|
||||||
for (const field of fields) {
|
|
||||||
expect(field).toBeEnabled();
|
|
||||||
}
|
|
||||||
expect(
|
expect(
|
||||||
within(dialog).queryByRole("button", { name: "Remove" }),
|
within(dialog).queryByRole("button", { name: "Deselect" }),
|
||||||
).not.toBeInTheDocument();
|
).not.toBeInTheDocument();
|
||||||
fireEvent.click(
|
fireEvent.click(
|
||||||
within(dialog).getByRole("button", { name: "More options" }),
|
within(dialog).getByRole("button", { name: "More options" }),
|
||||||
@@ -36,5 +31,13 @@ describe("MembershipMethodsScreen", () => {
|
|||||||
expect(
|
expect(
|
||||||
screen.getByRole("menuitem", { name: "Customize" }),
|
screen.getByRole("menuitem", { name: "Customize" }),
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
|
fireEvent.click(
|
||||||
|
within(dialog).getByRole("button", { name: "Who it covers and why" }),
|
||||||
|
);
|
||||||
|
const fields = within(dialog).getAllByRole("textbox");
|
||||||
|
expect(fields.length).toBeGreaterThan(0);
|
||||||
|
for (const field of fields) {
|
||||||
|
expect(field).toBeEnabled();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
import { fireEvent } from "@testing-library/react";
|
||||||
|
import "@testing-library/jest-dom/vitest";
|
||||||
|
import {
|
||||||
|
renderWithProviders as render,
|
||||||
|
screen,
|
||||||
|
within,
|
||||||
|
} from "../utils/test-utils";
|
||||||
|
import { MethodCardPolicyDialog } from "../../app/(app)/create/components/MethodCardPolicyDialog";
|
||||||
|
import type { MethodCardPolicyStep } from "../../app/(app)/create/components/MethodCardPolicyDialog";
|
||||||
|
|
||||||
|
const steps: MethodCardPolicyStep[] = [
|
||||||
|
{
|
||||||
|
id: "core",
|
||||||
|
title: "Core principle and scope",
|
||||||
|
emptyDescription: "Write this part of the policy.",
|
||||||
|
done: false,
|
||||||
|
summary: "",
|
||||||
|
renderEditor: () => (
|
||||||
|
<textarea aria-label="Core principle and scope" defaultValue="" />
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
describe("MethodCardPolicyDialog", () => {
|
||||||
|
it("routes through a step then Add to Rule", () => {
|
||||||
|
const onConfirm = vi.fn();
|
||||||
|
render(
|
||||||
|
<MethodCardPolicyDialog
|
||||||
|
isOpen
|
||||||
|
cardLabel="Signal"
|
||||||
|
editing={false}
|
||||||
|
steps={steps}
|
||||||
|
onClose={() => undefined}
|
||||||
|
onConfirm={onConfirm}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const dialog = screen.getByRole("dialog");
|
||||||
|
expect(
|
||||||
|
within(dialog).getByRole("button", { name: "Add to Rule" }),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
fireEvent.click(
|
||||||
|
within(dialog).getByRole("button", { name: "Core principle and scope" }),
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
within(dialog).getByRole("textbox", { name: "Core principle and scope" }),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
fireEvent.click(within(dialog).getByRole("button", { name: "Save" }));
|
||||||
|
expect(
|
||||||
|
within(dialog).getByRole("button", { name: "Add to Rule" }),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
fireEvent.click(within(dialog).getByRole("button", { name: "Add to Rule" }));
|
||||||
|
expect(onConfirm).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -37,10 +37,10 @@ describe("Create flow communication-methods page", () => {
|
|||||||
const dialog = screen.getByRole("dialog");
|
const dialog = screen.getByRole("dialog");
|
||||||
expect(dialog).toBeInTheDocument();
|
expect(dialog).toBeInTheDocument();
|
||||||
expect(within(dialog).getByText("Signal")).toBeInTheDocument();
|
expect(within(dialog).getByText("Signal")).toBeInTheDocument();
|
||||||
expect(within(dialog).getByText("Add card")).toBeInTheDocument();
|
expect(within(dialog).getByText("Add to Rule")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("re-opening a selected method shows Save and footer Remove; Remove stays in the kebab", async () => {
|
test("re-opening a selected method shows Save and footer Deselect; Remove stays in the kebab", async () => {
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
render(<CommunicationMethodsScreen />);
|
render(<CommunicationMethodsScreen />);
|
||||||
|
|
||||||
@@ -49,15 +49,15 @@ describe("Create flow communication-methods page", () => {
|
|||||||
});
|
});
|
||||||
await user.click(signalCards[0]);
|
await user.click(signalCards[0]);
|
||||||
const dialog = screen.getByRole("dialog");
|
const dialog = screen.getByRole("dialog");
|
||||||
await user.click(within(dialog).getByRole("button", { name: "Add card" }));
|
await user.click(within(dialog).getByRole("button", { name: "Add to Rule" }));
|
||||||
|
|
||||||
await user.click(signalCards[0]);
|
await user.click(signalCards[0]);
|
||||||
const dialogAgain = screen.getByRole("dialog");
|
const dialogAgain = screen.getByRole("dialog");
|
||||||
expect(
|
expect(
|
||||||
within(dialogAgain).getByRole("button", { name: "Remove" }),
|
within(dialogAgain).getByRole("button", { name: "Deselect" }),
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
expect(
|
expect(
|
||||||
within(dialogAgain).queryByRole("button", { name: "Add card" }),
|
within(dialogAgain).queryByRole("button", { name: "Add to Rule" }),
|
||||||
).not.toBeInTheDocument();
|
).not.toBeInTheDocument();
|
||||||
expect(
|
expect(
|
||||||
within(dialogAgain).getByRole("button", { name: "Save" }),
|
within(dialogAgain).getByRole("button", { name: "Save" }),
|
||||||
@@ -67,7 +67,7 @@ describe("Create flow communication-methods page", () => {
|
|||||||
expect(screen.getByRole("menuitem", { name: "Remove" })).toBeInTheDocument();
|
expect(screen.getByRole("menuitem", { name: "Remove" })).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Remove from the footer deselects the method", async () => {
|
test("Deselect from the footer deselects the method", async () => {
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
render(<CommunicationMethodsScreen />);
|
render(<CommunicationMethodsScreen />);
|
||||||
|
|
||||||
@@ -77,7 +77,7 @@ describe("Create flow communication-methods page", () => {
|
|||||||
await user.click(signalCards[0]);
|
await user.click(signalCards[0]);
|
||||||
await user.click(
|
await user.click(
|
||||||
within(screen.getByRole("dialog")).getByRole("button", {
|
within(screen.getByRole("dialog")).getByRole("button", {
|
||||||
name: "Add card",
|
name: "Add to Rule",
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -85,7 +85,9 @@ describe("Create flow communication-methods page", () => {
|
|||||||
|
|
||||||
await user.click(signalCards[0]);
|
await user.click(signalCards[0]);
|
||||||
await user.click(
|
await user.click(
|
||||||
within(screen.getByRole("dialog")).getByRole("button", { name: "Remove" }),
|
within(screen.getByRole("dialog")).getByRole("button", {
|
||||||
|
name: "Deselect",
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(signalCards[0]).not.toHaveTextContent("SELECTED");
|
expect(signalCards[0]).not.toHaveTextContent("SELECTED");
|
||||||
@@ -101,7 +103,7 @@ describe("Create flow communication-methods page", () => {
|
|||||||
await user.click(signalCards[0]);
|
await user.click(signalCards[0]);
|
||||||
await user.click(
|
await user.click(
|
||||||
within(screen.getByRole("dialog")).getByRole("button", {
|
within(screen.getByRole("dialog")).getByRole("button", {
|
||||||
name: "Add card",
|
name: "Add to Rule",
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -143,11 +145,13 @@ describe("Create flow communication-methods page", () => {
|
|||||||
await user.click(signalCards[0]);
|
await user.click(signalCards[0]);
|
||||||
|
|
||||||
const dialog = screen.getByRole("dialog");
|
const dialog = screen.getByRole("dialog");
|
||||||
const textbox = within(dialog).getAllByRole("textbox")[0];
|
|
||||||
expect(textbox).not.toBeDisabled();
|
|
||||||
|
|
||||||
await user.click(within(dialog).getByRole("button", { name: "More options" }));
|
await user.click(within(dialog).getByRole("button", { name: "More options" }));
|
||||||
expect(screen.getByRole("menuitem", { name: "Customize" })).toBeInTheDocument();
|
expect(screen.getByRole("menuitem", { name: "Customize" })).toBeInTheDocument();
|
||||||
|
await user.click(
|
||||||
|
within(dialog).getByRole("button", { name: "Core principle and scope" }),
|
||||||
|
);
|
||||||
|
const textbox = within(dialog).getAllByRole("textbox")[0];
|
||||||
|
expect(textbox).not.toBeDisabled();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("meta-only custom policy opens with editable section fields", async () => {
|
test("meta-only custom policy opens with editable section fields", async () => {
|
||||||
@@ -167,14 +171,35 @@ describe("Create flow communication-methods page", () => {
|
|||||||
screen.getAllByRole("button", { name: /My policy: Desc/ })[0],
|
screen.getAllByRole("button", { name: /My policy: Desc/ })[0],
|
||||||
);
|
);
|
||||||
const dialog = await screen.findByRole("dialog");
|
const dialog = await screen.findByRole("dialog");
|
||||||
|
expect(
|
||||||
|
within(dialog).queryByText("No custom fields yet."),
|
||||||
|
).not.toBeInTheDocument();
|
||||||
|
await user.click(
|
||||||
|
within(dialog).getByRole("button", { name: "Core principle and scope" }),
|
||||||
|
);
|
||||||
const textboxes = within(dialog).getAllByRole("textbox");
|
const textboxes = within(dialog).getAllByRole("textbox");
|
||||||
expect(textboxes.length).toBeGreaterThan(0);
|
expect(textboxes.length).toBeGreaterThan(0);
|
||||||
for (const field of textboxes) {
|
for (const field of textboxes) {
|
||||||
expect(field).not.toBeDisabled();
|
expect(field).not.toBeDisabled();
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("compact stack shows seven cards and See all search", async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
render(<CommunicationMethodsScreen />);
|
||||||
|
|
||||||
|
const compactCards = screen
|
||||||
|
.getAllByRole("button")
|
||||||
|
.filter((el) => (el.getAttribute("aria-label") || "").includes(": "));
|
||||||
|
expect(compactCards).toHaveLength(7);
|
||||||
|
expect(screen.queryByText("RECOMMENDED")).not.toBeInTheDocument();
|
||||||
|
|
||||||
|
await user.click(
|
||||||
|
screen.getByRole("button", { name: "See all communication cards" }),
|
||||||
|
);
|
||||||
expect(
|
expect(
|
||||||
within(dialog).queryByText("No custom fields yet."),
|
screen.getByRole("searchbox", { name: "Search this deck" }),
|
||||||
).not.toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("renders without error", () => {
|
test("renders without error", () => {
|
||||||
@@ -419,7 +444,7 @@ describe("Create flow communication-methods page", () => {
|
|||||||
const dupId = dupIds[0];
|
const dupId = dupIds[0];
|
||||||
expect(latest.selectedCommunicationMethodIds).toEqual([CUSTOM_POLICY_ID]);
|
expect(latest.selectedCommunicationMethodIds).toEqual([CUSTOM_POLICY_ID]);
|
||||||
expect(
|
expect(
|
||||||
within(dialog).getByRole("button", { name: "Add card" }),
|
within(dialog).getByRole("button", { name: "Add to Rule" }),
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
expect(metaAfterDup[dupId].label.endsWith(" (copy)")).toBe(true);
|
expect(metaAfterDup[dupId].label.endsWith(" (copy)")).toBe(true);
|
||||||
expect(within(dialog).getByRole("heading", { level: 1 })).toHaveTextContent(
|
expect(within(dialog).getByRole("heading", { level: 1 })).toHaveTextContent(
|
||||||
|
|||||||
@@ -185,7 +185,7 @@ describe("Create flow decision-approaches page", () => {
|
|||||||
await user.click(screen.getByRole("button", { name: /^Consent \(sociocracy\):/ }));
|
await user.click(screen.getByRole("button", { name: /^Consent \(sociocracy\):/ }));
|
||||||
await user.click(
|
await user.click(
|
||||||
within(await screen.findByRole("dialog")).getByRole("button", {
|
within(await screen.findByRole("dialog")).getByRole("button", {
|
||||||
name: "Add card",
|
name: "Add to Rule",
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
@@ -211,17 +211,20 @@ describe("Create flow decision-approaches page", () => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
const dialog = await screen.findByRole("dialog");
|
const dialog = await screen.findByRole("dialog");
|
||||||
const textboxes = within(dialog).getAllByRole("textbox");
|
|
||||||
expect(textboxes.length).toBeGreaterThan(0);
|
|
||||||
for (const field of textboxes) {
|
|
||||||
expect(field).not.toBeDisabled();
|
|
||||||
}
|
|
||||||
await user.click(
|
await user.click(
|
||||||
within(dialog).getByRole("button", { name: "More options" }),
|
within(dialog).getByRole("button", { name: "More options" }),
|
||||||
);
|
);
|
||||||
expect(
|
expect(
|
||||||
screen.getByRole("menuitem", { name: "Customize" }),
|
screen.getByRole("menuitem", { name: "Customize" }),
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
|
await user.click(
|
||||||
|
within(dialog).getByRole("button", { name: "Core principle" }),
|
||||||
|
);
|
||||||
|
const textboxes = within(dialog).getAllByRole("textbox");
|
||||||
|
expect(textboxes.length).toBeGreaterThan(0);
|
||||||
|
for (const field of textboxes) {
|
||||||
|
expect(field).not.toBeDisabled();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test("clicking a card opens the create modal and confirming selects it", async () => {
|
test("clicking a card opens the create modal and confirming selects it", async () => {
|
||||||
@@ -237,14 +240,14 @@ describe("Create flow decision-approaches page", () => {
|
|||||||
expect(dialog).toBeInTheDocument();
|
expect(dialog).toBeInTheDocument();
|
||||||
|
|
||||||
const confirmButton = within(dialog).getByRole("button", {
|
const confirmButton = within(dialog).getByRole("button", {
|
||||||
name: "Add card",
|
name: "Add to Rule",
|
||||||
});
|
});
|
||||||
await user.click(confirmButton);
|
await user.click(confirmButton);
|
||||||
|
|
||||||
expect(screen.getByText("SELECTED")).toBeInTheDocument();
|
expect(screen.getByText("SELECTED")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("re-opening a selected approach shows Save and footer Remove; Remove stays in the kebab", async () => {
|
test("re-opening a selected approach shows Save and footer Deselect; Remove stays in the kebab", async () => {
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
render(<DecisionApproachesScreen />);
|
render(<DecisionApproachesScreen />);
|
||||||
|
|
||||||
@@ -255,17 +258,17 @@ describe("Create flow decision-approaches page", () => {
|
|||||||
const dialog = await screen.findByRole("dialog");
|
const dialog = await screen.findByRole("dialog");
|
||||||
await user.click(
|
await user.click(
|
||||||
within(dialog).getByRole("button", {
|
within(dialog).getByRole("button", {
|
||||||
name: "Add card",
|
name: "Add to Rule",
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
await user.click(card);
|
await user.click(card);
|
||||||
const dialogAgain = screen.getByRole("dialog");
|
const dialogAgain = screen.getByRole("dialog");
|
||||||
expect(
|
expect(
|
||||||
within(dialogAgain).getByRole("button", { name: "Remove" }),
|
within(dialogAgain).getByRole("button", { name: "Deselect" }),
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
expect(
|
expect(
|
||||||
within(dialogAgain).queryByRole("button", { name: "Add card" }),
|
within(dialogAgain).queryByRole("button", { name: "Add to Rule" }),
|
||||||
).not.toBeInTheDocument();
|
).not.toBeInTheDocument();
|
||||||
expect(
|
expect(
|
||||||
within(dialogAgain).getByRole("button", { name: "Save" }),
|
within(dialogAgain).getByRole("button", { name: "Save" }),
|
||||||
@@ -275,7 +278,7 @@ describe("Create flow decision-approaches page", () => {
|
|||||||
expect(screen.getByRole("menuitem", { name: "Remove" })).toBeInTheDocument();
|
expect(screen.getByRole("menuitem", { name: "Remove" })).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Remove from the footer deselects the approach", async () => {
|
test("Deselect from the footer deselects the approach", async () => {
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
render(<DecisionApproachesScreen />);
|
render(<DecisionApproachesScreen />);
|
||||||
|
|
||||||
@@ -285,7 +288,7 @@ describe("Create flow decision-approaches page", () => {
|
|||||||
await user.click(card);
|
await user.click(card);
|
||||||
await user.click(
|
await user.click(
|
||||||
within(await screen.findByRole("dialog")).getByRole("button", {
|
within(await screen.findByRole("dialog")).getByRole("button", {
|
||||||
name: "Add card",
|
name: "Add to Rule",
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -293,7 +296,9 @@ describe("Create flow decision-approaches page", () => {
|
|||||||
|
|
||||||
await user.click(card);
|
await user.click(card);
|
||||||
await user.click(
|
await user.click(
|
||||||
within(screen.getByRole("dialog")).getByRole("button", { name: "Remove" }),
|
within(screen.getByRole("dialog")).getByRole("button", {
|
||||||
|
name: "Deselect",
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(card).not.toHaveTextContent("SELECTED");
|
expect(card).not.toHaveTextContent("SELECTED");
|
||||||
@@ -309,17 +314,21 @@ describe("Create flow decision-approaches page", () => {
|
|||||||
await user.click(card);
|
await user.click(card);
|
||||||
const dialog = await screen.findByRole("dialog");
|
const dialog = await screen.findByRole("dialog");
|
||||||
await user.click(
|
await user.click(
|
||||||
within(dialog).getByRole("button", { name: "Add card" }),
|
within(dialog).getByRole("button", { name: "Add to Rule" }),
|
||||||
);
|
);
|
||||||
|
|
||||||
await user.click(card);
|
await user.click(card);
|
||||||
const dialogAgain = await screen.findByRole("dialog");
|
const dialogAgain = await screen.findByRole("dialog");
|
||||||
|
await user.click(
|
||||||
|
within(dialogAgain).getByRole("button", { name: "Core principle" }),
|
||||||
|
);
|
||||||
const principleField = within(dialogAgain).getByRole("textbox", {
|
const principleField = within(dialogAgain).getByRole("textbox", {
|
||||||
name: /core principle/i,
|
name: /core principle/i,
|
||||||
});
|
});
|
||||||
await user.clear(principleField);
|
await user.clear(principleField);
|
||||||
await user.type(principleField, "Edited principle");
|
await user.type(principleField, "Edited principle");
|
||||||
await user.click(within(dialogAgain).getByRole("button", { name: "Save" }));
|
await user.click(within(dialogAgain).getByRole("button", { name: "Save" }));
|
||||||
|
await user.click(within(dialogAgain).getByRole("button", { name: "Save" }));
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
||||||
@@ -336,7 +345,7 @@ describe("Create flow decision-approaches page", () => {
|
|||||||
await user.click(card);
|
await user.click(card);
|
||||||
await user.click(
|
await user.click(
|
||||||
within(await screen.findByRole("dialog")).getByRole("button", {
|
within(await screen.findByRole("dialog")).getByRole("button", {
|
||||||
name: "Add card",
|
name: "Add to Rule",
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -397,6 +406,14 @@ describe("Create flow decision-approaches page", () => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
const dialog = await screen.findByRole("dialog");
|
const dialog = await screen.findByRole("dialog");
|
||||||
|
await user.click(
|
||||||
|
within(dialog).getByRole("button", { name: "Applicable scope" }),
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
within(dialog).getByRole("button", {
|
||||||
|
name: "Select Changing the Rule",
|
||||||
|
}),
|
||||||
|
).toBeInTheDocument();
|
||||||
expect(
|
expect(
|
||||||
within(dialog).getByRole("button", {
|
within(dialog).getByRole("button", {
|
||||||
name: "Select Changing this Rule",
|
name: "Select Changing this Rule",
|
||||||
@@ -431,6 +448,9 @@ describe("Create flow decision-approaches page", () => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
const lazyDialog = await screen.findByRole("dialog");
|
const lazyDialog = await screen.findByRole("dialog");
|
||||||
|
await user.click(
|
||||||
|
within(lazyDialog).getByRole("button", { name: "Applicable scope" }),
|
||||||
|
);
|
||||||
expect(
|
expect(
|
||||||
within(lazyDialog).getByRole("button", {
|
within(lazyDialog).getByRole("button", {
|
||||||
name: "Select Shared money",
|
name: "Select Shared money",
|
||||||
@@ -448,6 +468,9 @@ describe("Create flow decision-approaches page", () => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
const lazyDialog = await screen.findByRole("dialog");
|
const lazyDialog = await screen.findByRole("dialog");
|
||||||
|
await user.click(
|
||||||
|
within(lazyDialog).getByRole("button", { name: "Applicable scope" }),
|
||||||
|
);
|
||||||
await user.click(
|
await user.click(
|
||||||
within(lazyDialog).getByRole("button", {
|
within(lazyDialog).getByRole("button", {
|
||||||
name: "Select Shared money",
|
name: "Select Shared money",
|
||||||
@@ -457,7 +480,10 @@ describe("Create flow decision-approaches page", () => {
|
|||||||
screen.getByRole("checkbox", { name: "Shared money" }),
|
screen.getByRole("checkbox", { name: "Shared money" }),
|
||||||
).toBeChecked();
|
).toBeChecked();
|
||||||
await user.click(
|
await user.click(
|
||||||
within(lazyDialog).getByRole("button", { name: "Add card" }),
|
within(lazyDialog).getByRole("button", { name: "Save" }),
|
||||||
|
);
|
||||||
|
await user.click(
|
||||||
|
within(lazyDialog).getByRole("button", { name: "Add to Rule" }),
|
||||||
);
|
);
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
||||||
@@ -469,6 +495,9 @@ describe("Create flow decision-approaches page", () => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
const doocracyDialog = await screen.findByRole("dialog");
|
const doocracyDialog = await screen.findByRole("dialog");
|
||||||
|
await user.click(
|
||||||
|
within(doocracyDialog).getByRole("button", { name: "Applicable scope" }),
|
||||||
|
);
|
||||||
expect(
|
expect(
|
||||||
within(doocracyDialog).getByRole("button", {
|
within(doocracyDialog).getByRole("button", {
|
||||||
name: "Select Shared money",
|
name: "Select Shared money",
|
||||||
@@ -482,6 +511,9 @@ describe("Create flow decision-approaches page", () => {
|
|||||||
expect(
|
expect(
|
||||||
screen.getByRole("checkbox", { name: "Shared money" }),
|
screen.getByRole("checkbox", { name: "Shared money" }),
|
||||||
).not.toBeChecked();
|
).not.toBeChecked();
|
||||||
|
await user.click(
|
||||||
|
within(doocracyDialog).getByRole("button", { name: "Save" }),
|
||||||
|
);
|
||||||
await user.click(
|
await user.click(
|
||||||
within(doocracyDialog).getByRole("button", { name: "Close" }),
|
within(doocracyDialog).getByRole("button", { name: "Close" }),
|
||||||
);
|
);
|
||||||
@@ -499,6 +531,9 @@ describe("Create flow decision-approaches page", () => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
const lazyAgain = await screen.findByRole("dialog");
|
const lazyAgain = await screen.findByRole("dialog");
|
||||||
|
await user.click(
|
||||||
|
within(lazyAgain).getByRole("button", { name: "Applicable scope" }),
|
||||||
|
);
|
||||||
expect(
|
expect(
|
||||||
within(lazyAgain).getByRole("button", {
|
within(lazyAgain).getByRole("button", {
|
||||||
name: "Deselect Shared money",
|
name: "Deselect Shared money",
|
||||||
@@ -520,7 +555,7 @@ describe("Create flow decision-approaches page", () => {
|
|||||||
);
|
);
|
||||||
const lazyDialog = await screen.findByRole("dialog");
|
const lazyDialog = await screen.findByRole("dialog");
|
||||||
await user.click(
|
await user.click(
|
||||||
within(lazyDialog).getByRole("button", { name: "Add card" }),
|
within(lazyDialog).getByRole("button", { name: "Add to Rule" }),
|
||||||
);
|
);
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
||||||
@@ -532,6 +567,9 @@ describe("Create flow decision-approaches page", () => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
const doocracyDialog = await screen.findByRole("dialog");
|
const doocracyDialog = await screen.findByRole("dialog");
|
||||||
|
await user.click(
|
||||||
|
within(doocracyDialog).getByRole("button", { name: "Applicable scope" }),
|
||||||
|
);
|
||||||
await user.click(
|
await user.click(
|
||||||
screen.getByRole("checkbox", { name: "Shared money" }),
|
screen.getByRole("checkbox", { name: "Shared money" }),
|
||||||
);
|
);
|
||||||
@@ -540,6 +578,9 @@ describe("Create flow decision-approaches page", () => {
|
|||||||
name: "Deselect Shared money",
|
name: "Deselect Shared money",
|
||||||
}),
|
}),
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
|
await user.click(
|
||||||
|
within(doocracyDialog).getByRole("button", { name: "Save" }),
|
||||||
|
);
|
||||||
await user.click(
|
await user.click(
|
||||||
within(doocracyDialog).getByRole("button", { name: "Close" }),
|
within(doocracyDialog).getByRole("button", { name: "Close" }),
|
||||||
);
|
);
|
||||||
@@ -554,6 +595,9 @@ describe("Create flow decision-approaches page", () => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
const lazyAgain = await screen.findByRole("dialog");
|
const lazyAgain = await screen.findByRole("dialog");
|
||||||
|
await user.click(
|
||||||
|
within(lazyAgain).getByRole("button", { name: "Applicable scope" }),
|
||||||
|
);
|
||||||
expect(
|
expect(
|
||||||
within(lazyAgain).getByRole("button", {
|
within(lazyAgain).getByRole("button", {
|
||||||
name: "Select Shared money",
|
name: "Select Shared money",
|
||||||
@@ -571,6 +615,9 @@ describe("Create flow decision-approaches page", () => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
const lazyDialog = await screen.findByRole("dialog");
|
const lazyDialog = await screen.findByRole("dialog");
|
||||||
|
await user.click(
|
||||||
|
within(lazyDialog).getByRole("button", { name: "Applicable scope" }),
|
||||||
|
);
|
||||||
await user.click(
|
await user.click(
|
||||||
within(lazyDialog).getByRole("button", {
|
within(lazyDialog).getByRole("button", {
|
||||||
name: "Select Shared money",
|
name: "Select Shared money",
|
||||||
@@ -580,7 +627,10 @@ describe("Create flow decision-approaches page", () => {
|
|||||||
screen.getByRole("checkbox", { name: "Shared money" }),
|
screen.getByRole("checkbox", { name: "Shared money" }),
|
||||||
).toBeChecked();
|
).toBeChecked();
|
||||||
await user.click(
|
await user.click(
|
||||||
within(lazyDialog).getByRole("button", { name: "Add card" }),
|
within(lazyDialog).getByRole("button", { name: "Save" }),
|
||||||
|
);
|
||||||
|
await user.click(
|
||||||
|
within(lazyDialog).getByRole("button", { name: "Add to Rule" }),
|
||||||
);
|
);
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
||||||
@@ -592,6 +642,9 @@ describe("Create flow decision-approaches page", () => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
const doocracyDialog = await screen.findByRole("dialog");
|
const doocracyDialog = await screen.findByRole("dialog");
|
||||||
|
await user.click(
|
||||||
|
within(doocracyDialog).getByRole("button", { name: "Applicable scope" }),
|
||||||
|
);
|
||||||
await user.click(
|
await user.click(
|
||||||
within(doocracyDialog).getByRole("button", {
|
within(doocracyDialog).getByRole("button", {
|
||||||
name: "Select Big decisions",
|
name: "Select Big decisions",
|
||||||
@@ -609,7 +662,10 @@ describe("Create flow decision-approaches page", () => {
|
|||||||
}),
|
}),
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
await user.click(
|
await user.click(
|
||||||
within(doocracyDialog).getByRole("button", { name: "Add card" }),
|
within(doocracyDialog).getByRole("button", { name: "Save" }),
|
||||||
|
);
|
||||||
|
await user.click(
|
||||||
|
within(doocracyDialog).getByRole("button", { name: "Add to Rule" }),
|
||||||
);
|
);
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
||||||
@@ -628,6 +684,9 @@ describe("Create flow decision-approaches page", () => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
const lazyAgain = await screen.findByRole("dialog");
|
const lazyAgain = await screen.findByRole("dialog");
|
||||||
|
await user.click(
|
||||||
|
within(lazyAgain).getByRole("button", { name: "Applicable scope" }),
|
||||||
|
);
|
||||||
expect(
|
expect(
|
||||||
within(lazyAgain).getByRole("button", {
|
within(lazyAgain).getByRole("button", {
|
||||||
name: "Deselect Shared money",
|
name: "Deselect Shared money",
|
||||||
@@ -638,6 +697,9 @@ describe("Create flow decision-approaches page", () => {
|
|||||||
name: "Select Big decisions",
|
name: "Select Big decisions",
|
||||||
}),
|
}),
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
|
await user.click(
|
||||||
|
within(lazyAgain).getByRole("button", { name: "Save" }),
|
||||||
|
);
|
||||||
await user.click(
|
await user.click(
|
||||||
within(lazyAgain).getByRole("button", { name: "Close" }),
|
within(lazyAgain).getByRole("button", { name: "Close" }),
|
||||||
);
|
);
|
||||||
@@ -651,6 +713,9 @@ describe("Create flow decision-approaches page", () => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
const doocracyAgain = await screen.findByRole("dialog");
|
const doocracyAgain = await screen.findByRole("dialog");
|
||||||
|
await user.click(
|
||||||
|
within(doocracyAgain).getByRole("button", { name: "Applicable scope" }),
|
||||||
|
);
|
||||||
expect(
|
expect(
|
||||||
within(doocracyAgain).getByRole("button", {
|
within(doocracyAgain).getByRole("button", {
|
||||||
name: "Deselect Big decisions",
|
name: "Deselect Big decisions",
|
||||||
|
|||||||
@@ -155,6 +155,115 @@ describe("CardStack Component", () => {
|
|||||||
expect(screen.getAllByText("SELECTED").length).toBeGreaterThanOrEqual(1);
|
expect(screen.getAllByText("SELECTED").length).toBeGreaterThanOrEqual(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("compact single-stack shows seven recommended cards and hides the tag", () => {
|
||||||
|
const cards = Array.from({ length: 8 }, (_, index) => ({
|
||||||
|
id: String(index),
|
||||||
|
label: `Card ${index}`,
|
||||||
|
supportText: `Desc ${index}`,
|
||||||
|
recommended: true,
|
||||||
|
}));
|
||||||
|
render(
|
||||||
|
<CardStack cards={cards} layout="singleStack" expanded={false} />,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
screen.getAllByRole("button", { name: /Card \d: Desc \d/ }),
|
||||||
|
).toHaveLength(7);
|
||||||
|
expect(screen.queryByText("RECOMMENDED")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("See all search filters the expanded single-stack", () => {
|
||||||
|
render(
|
||||||
|
<CardStack
|
||||||
|
cards={SAMPLE_CARDS}
|
||||||
|
layout="singleStack"
|
||||||
|
expanded={true}
|
||||||
|
searchPlaceholder="Search this deck"
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const search = screen.getByRole("searchbox", { name: "Search this deck" });
|
||||||
|
fireEvent.change(search, { target: { value: "Option B" } });
|
||||||
|
expect(
|
||||||
|
screen.getByRole("button", { name: "Option B: Description B" }),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.queryByRole("button", { name: "Option A: Description A" }),
|
||||||
|
).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("selected compact card keeps catalog copy and hides the alternative", () => {
|
||||||
|
render(
|
||||||
|
<CardStack
|
||||||
|
cards={[
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
label: "Option A",
|
||||||
|
supportText: "Catalog copy",
|
||||||
|
compactSupportText: "Fits a worker co-op",
|
||||||
|
recommended: true,
|
||||||
|
alternative: { id: "2", label: "Option B" },
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
selectedIds={["1"]}
|
||||||
|
layout="singleStack"
|
||||||
|
alternativePrefix="Another way communities do this:"
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
screen.getByRole("button", { name: "Option A: Catalog copy" }),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText(/Fits a worker co-op/)).not.toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.queryByRole("button", { name: "Option B" }),
|
||||||
|
).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("unselected compact card shows the why line", () => {
|
||||||
|
render(
|
||||||
|
<CardStack
|
||||||
|
cards={[
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
label: "Option A",
|
||||||
|
supportText: "Catalog copy",
|
||||||
|
compactSupportText: "Fits a worker co-op",
|
||||||
|
recommended: true,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
layout="singleStack"
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
screen.getByRole("button", { name: "Option A: Fits a worker co-op" }),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("compact alternative row opens the named card", () => {
|
||||||
|
const onAlternativeSelect = vi.fn();
|
||||||
|
render(
|
||||||
|
<CardStack
|
||||||
|
cards={[
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
label: "Option A",
|
||||||
|
supportText: "Description A",
|
||||||
|
recommended: true,
|
||||||
|
alternative: { id: "2", label: "Option B" },
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
layout="singleStack"
|
||||||
|
alternativePrefix="Another way communities do this:"
|
||||||
|
onAlternativeSelect={onAlternativeSelect}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Option B" }));
|
||||||
|
expect(onAlternativeSelect).toHaveBeenCalledWith("2");
|
||||||
|
});
|
||||||
|
|
||||||
test("calls onCardSelect when re-clicking an already selected card", () => {
|
test("calls onCardSelect when re-clicking an already selected card", () => {
|
||||||
const onCardSelect = vi.fn();
|
const onCardSelect = vi.fn();
|
||||||
render(
|
render(
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
selectedKeyResourceLabelsFromCheckedIds,
|
selectedKeyResourceLabelsFromCheckedIds,
|
||||||
stringArraysEqual,
|
stringArraysEqual,
|
||||||
syncDecisionApproachKeyResourceDetails,
|
syncDecisionApproachKeyResourceDetails,
|
||||||
|
decisionApproachDefaultScopeChips,
|
||||||
withDecisionApproachKeyResourceScopes,
|
withDecisionApproachKeyResourceScopes,
|
||||||
} from "../../lib/create/decisionApproachKeyResources";
|
} from "../../lib/create/decisionApproachKeyResources";
|
||||||
import type { DecisionApproachDetailEntry } from "../../app/(app)/create/types";
|
import type { DecisionApproachDetailEntry } from "../../app/(app)/create/types";
|
||||||
@@ -158,10 +159,20 @@ describe("decisionApproachKeyResources", () => {
|
|||||||
expect(decisionApproachKeyResourceLabels()).toHaveLength(7);
|
expect(decisionApproachKeyResourceLabels()).toHaveLength(7);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("unions key-resource labels onto an existing scope list", () => {
|
it("unions signed-off default chips then key-resource labels onto an existing scope list", () => {
|
||||||
|
expect(decisionApproachDefaultScopeChips()).toEqual([
|
||||||
|
"Changing the Rule",
|
||||||
|
"Money",
|
||||||
|
"Projects",
|
||||||
|
"Asking someone to leave",
|
||||||
|
]);
|
||||||
expect(
|
expect(
|
||||||
withDecisionApproachKeyResourceScopes(["Daily Operations"]),
|
withDecisionApproachKeyResourceScopes(["Daily Operations"]),
|
||||||
).toEqual([
|
).toEqual([
|
||||||
|
"Changing the Rule",
|
||||||
|
"Money",
|
||||||
|
"Projects",
|
||||||
|
"Asking someone to leave",
|
||||||
"Daily Operations",
|
"Daily Operations",
|
||||||
"Everyday decisions",
|
"Everyday decisions",
|
||||||
"Big decisions",
|
"Big decisions",
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { formatRecommendationWhyLine } from "../../lib/create/formatRecommendationWhyLine";
|
||||||
|
import type { GroupAnswers } from "../../lib/create/deckTypes";
|
||||||
|
import messages from "../../messages/en/index";
|
||||||
|
|
||||||
|
const copy = messages.create.customRule.recommendations;
|
||||||
|
const emptyAnswers: GroupAnswers = {
|
||||||
|
size: null,
|
||||||
|
mainKind: null,
|
||||||
|
otherKinds: [],
|
||||||
|
scale: [],
|
||||||
|
stage: null,
|
||||||
|
traits: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("formatRecommendationWhyLine", () => {
|
||||||
|
it("builds a fit line from kind and size", () => {
|
||||||
|
expect(
|
||||||
|
formatRecommendationWhyLine(
|
||||||
|
{ kind: "fit", matched: ["kind"] },
|
||||||
|
{ ...emptyAnswers, mainKind: "workersCoop", size: "sixToTwelve" },
|
||||||
|
copy,
|
||||||
|
),
|
||||||
|
).toBe("Fits a worker co-op of 6 to 12 people");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("builds an added line from the setup answer", () => {
|
||||||
|
expect(
|
||||||
|
formatRecommendationWhyLine(
|
||||||
|
{ kind: "added", answer: "employer" },
|
||||||
|
emptyAnswers,
|
||||||
|
copy,
|
||||||
|
),
|
||||||
|
).toBe("Added because you said you pay some people to do your work");
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in new issue
Block a user