Show seven cards on the four method decks and open each as a routing overlay of its parts, so groups write from empty fields with Generate draft, search the full deck, and see unselected scope chips with enough contrast.
This commit is contained in:
1 parent
dea7dd7fbd
commit
46656f52d3
28 files changed
+1349
-318
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,
|
||||
}),
|
||||
];
|
||||
}
|
||||
Reference in new issue
Block a user