Final review edit modals created
This commit is contained in:
@@ -0,0 +1,335 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* Editable mirror of {@link TemplateChipDetailModal} for the final-review
|
||||
* screen. Each chip on `/create/final-review` opens this modal — same field
|
||||
* set as the matching custom-rule add-method modals, but with a **Save**
|
||||
* button instead of **Add**:
|
||||
*
|
||||
* - Initial field values come from the matching `{group}DetailsById` state
|
||||
* override when present; otherwise from the preset defaults shipped in
|
||||
* `messages/en/create/customRule/*.json` (see {@link finalReviewChipPresets}).
|
||||
* - Save is disabled until the user edits any field (cheap structural
|
||||
* compare against the seeded snapshot). Saving writes the draft into
|
||||
* `CreateFlowState` via the caller's `onSave` handler and closes; the
|
||||
* state then rides along through the existing localStorage mirror,
|
||||
* signed-in server draft PUT (Save & Exit), and `buildPublishPayload`
|
||||
* (Finalize).
|
||||
* - Closing the modal without saving discards any edits — the parent never
|
||||
* hears about them.
|
||||
*
|
||||
* The actual field rendering lives in `components/methodEditFields/*` and
|
||||
* is shared with the custom-rule add-method modals so the two surfaces stay
|
||||
* in lockstep automatically.
|
||||
*/
|
||||
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import Create from "../../../components/modals/Create";
|
||||
import ContentLockup from "../../../components/type/ContentLockup";
|
||||
import { useMessages, useTranslation } from "../../../contexts/MessagesContext";
|
||||
import {
|
||||
CommunicationMethodEditFields,
|
||||
ConflictManagementEditFields,
|
||||
CoreValueEditFields,
|
||||
DecisionApproachEditFields,
|
||||
MembershipMethodEditFields,
|
||||
} from "./methodEditFields";
|
||||
import {
|
||||
communicationPresetFor,
|
||||
conflictManagementPresetFor,
|
||||
coreValuePresetFor,
|
||||
decisionApproachPresetFor,
|
||||
membershipPresetFor,
|
||||
} from "../../../../lib/create/finalReviewChipPresets";
|
||||
import type {
|
||||
CommunicationMethodDetailEntry,
|
||||
ConflictManagementDetailEntry,
|
||||
CoreValueDetailEntry,
|
||||
CreateFlowState,
|
||||
DecisionApproachDetailEntry,
|
||||
MembershipMethodDetailEntry,
|
||||
} from "../types";
|
||||
import type { TemplateFacetGroupKey } from "../../../../lib/create/templateReviewMapping";
|
||||
|
||||
export type FinalReviewChipEditTarget = {
|
||||
/** Stable key for override lookup: preset id (methods) or chip id (core values). */
|
||||
overrideKey: string;
|
||||
/** Category group that decides which field set to render. */
|
||||
groupKey: TemplateFacetGroupKey;
|
||||
/** Display label shown at the top of the modal (localized chip label). */
|
||||
chipLabel: string;
|
||||
};
|
||||
|
||||
export type FinalReviewChipEditPatch =
|
||||
| { groupKey: "coreValues"; overrideKey: string; value: CoreValueDetailEntry }
|
||||
| {
|
||||
groupKey: "communication";
|
||||
overrideKey: string;
|
||||
value: CommunicationMethodDetailEntry;
|
||||
}
|
||||
| {
|
||||
groupKey: "membership";
|
||||
overrideKey: string;
|
||||
value: MembershipMethodDetailEntry;
|
||||
}
|
||||
| {
|
||||
groupKey: "decisionApproaches";
|
||||
overrideKey: string;
|
||||
value: DecisionApproachDetailEntry;
|
||||
}
|
||||
| {
|
||||
groupKey: "conflictManagement";
|
||||
overrideKey: string;
|
||||
value: ConflictManagementDetailEntry;
|
||||
};
|
||||
|
||||
export interface FinalReviewChipEditModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
/**
|
||||
* Chip being edited. Passed `null` while the modal is closing so the
|
||||
* component can cleanly reset its internal draft state.
|
||||
*/
|
||||
target: FinalReviewChipEditTarget | null;
|
||||
/** Current flow state — used to seed the modal from saved overrides. */
|
||||
state: CreateFlowState;
|
||||
/** Called with the typed patch when the user clicks Save. */
|
||||
onSave: (_patch: FinalReviewChipEditPatch) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Discriminated union of every group's draft + value. Storing both the
|
||||
* `groupKey` and the `value` together keeps render-time switches exhaustive
|
||||
* and prevents the four method group states from drifting apart (which is
|
||||
* the bug that motivated extracting `methodEditFields/*` in the first place).
|
||||
*/
|
||||
type Draft =
|
||||
| { groupKey: "coreValues"; value: CoreValueDetailEntry }
|
||||
| { groupKey: "communication"; value: CommunicationMethodDetailEntry }
|
||||
| { groupKey: "membership"; value: MembershipMethodDetailEntry }
|
||||
| { groupKey: "decisionApproaches"; value: DecisionApproachDetailEntry }
|
||||
| { groupKey: "conflictManagement"; value: ConflictManagementDetailEntry };
|
||||
|
||||
export function FinalReviewChipEditModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
target,
|
||||
state,
|
||||
onSave,
|
||||
}: FinalReviewChipEditModalProps) {
|
||||
const m = useMessages();
|
||||
const tCv = m.create.customRule.coreValues;
|
||||
const tComm = m.create.customRule.communication;
|
||||
const tMem = m.create.customRule.membership;
|
||||
const tDa = m.create.customRule.decisionApproaches;
|
||||
const tCm = m.create.customRule.conflictManagement;
|
||||
const tModal = useTranslation(
|
||||
"create.reviewAndComplete.finalReview.chipEditModal",
|
||||
);
|
||||
|
||||
const [draft, setDraft] = useState<Draft | null>(null);
|
||||
/**
|
||||
* JSON-stringified seed used for the cheap dirty check. Re-captured on
|
||||
* every (re)open so reopening a chip after a save shows Save-disabled
|
||||
* again until the user makes a fresh edit.
|
||||
*/
|
||||
const initialSnapshotRef = useRef<string>("");
|
||||
const seededTargetRef = useRef<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen || !target) return;
|
||||
const targetKey = `${target.groupKey}:${target.overrideKey}`;
|
||||
if (seededTargetRef.current === targetKey) return;
|
||||
|
||||
const seed = seedDraftForTarget(target, state);
|
||||
setDraft(seed);
|
||||
initialSnapshotRef.current = JSON.stringify(seed.value);
|
||||
seededTargetRef.current = targetKey;
|
||||
}, [isOpen, target, state]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) seededTargetRef.current = null;
|
||||
}, [isOpen]);
|
||||
|
||||
const isDirty = useMemo(() => {
|
||||
if (!draft) return false;
|
||||
return JSON.stringify(draft.value) !== initialSnapshotRef.current;
|
||||
}, [draft]);
|
||||
|
||||
const handleSave = () => {
|
||||
if (!target || !draft || !isDirty) return;
|
||||
onSave({
|
||||
groupKey: draft.groupKey,
|
||||
overrideKey: target.overrideKey,
|
||||
value: draft.value,
|
||||
} as FinalReviewChipEditPatch);
|
||||
onClose();
|
||||
};
|
||||
|
||||
const subtitle = useMemo(() => {
|
||||
if (!target) return "";
|
||||
return subtitleForTarget(target, { tCv, tComm, tMem, tDa, tCm });
|
||||
}, [target, tCv, tComm, tMem, tDa, tCm]);
|
||||
|
||||
return (
|
||||
<Create
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
backdropVariant="loginYellow"
|
||||
headerContent={
|
||||
<div className="bg-[var(--color-surface-default-primary)] px-[24px] py-[12px] shrink-0">
|
||||
<ContentLockup
|
||||
title={target?.chipLabel ?? ""}
|
||||
description={subtitle}
|
||||
variant="modal"
|
||||
alignment="left"
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
showBackButton={false}
|
||||
showNextButton
|
||||
nextButtonText={tModal("saveButton")}
|
||||
nextButtonDisabled={!isDirty}
|
||||
onNext={handleSave}
|
||||
ariaLabel={target?.chipLabel || "Edit chip details"}
|
||||
>
|
||||
<div className="flex flex-col gap-[var(--measures-spacing-600,24px)] pb-2">
|
||||
{draft?.groupKey === "coreValues" && (
|
||||
<CoreValueEditFields
|
||||
value={draft.value}
|
||||
onChange={(value) => setDraft({ groupKey: "coreValues", value })}
|
||||
/>
|
||||
)}
|
||||
{draft?.groupKey === "communication" && (
|
||||
<CommunicationMethodEditFields
|
||||
value={draft.value}
|
||||
onChange={(value) =>
|
||||
setDraft({ groupKey: "communication", value })
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{draft?.groupKey === "membership" && (
|
||||
<MembershipMethodEditFields
|
||||
value={draft.value}
|
||||
onChange={(value) => setDraft({ groupKey: "membership", value })}
|
||||
/>
|
||||
)}
|
||||
{draft?.groupKey === "decisionApproaches" && (
|
||||
<DecisionApproachEditFields
|
||||
value={draft.value}
|
||||
onChange={(value) =>
|
||||
setDraft({ groupKey: "decisionApproaches", value })
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{draft?.groupKey === "conflictManagement" && (
|
||||
<ConflictManagementEditFields
|
||||
value={draft.value}
|
||||
onChange={(value) =>
|
||||
setDraft({ groupKey: "conflictManagement", value })
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Create>
|
||||
);
|
||||
}
|
||||
|
||||
// ---------- helpers ------------------------------------------------------
|
||||
|
||||
function seedDraftForTarget(
|
||||
target: FinalReviewChipEditTarget,
|
||||
state: CreateFlowState,
|
||||
): Draft {
|
||||
switch (target.groupKey) {
|
||||
case "coreValues": {
|
||||
const saved = state.coreValueDetailsByChipId?.[target.overrideKey];
|
||||
const preset = coreValuePresetFor(target.overrideKey);
|
||||
return {
|
||||
groupKey: "coreValues",
|
||||
value: {
|
||||
meaning: saved?.meaning ?? preset.meaning,
|
||||
signals: saved?.signals ?? preset.signals,
|
||||
},
|
||||
};
|
||||
}
|
||||
case "communication": {
|
||||
const saved =
|
||||
state.communicationMethodDetailsById?.[target.overrideKey] ??
|
||||
communicationPresetFor(target.overrideKey);
|
||||
return { groupKey: "communication", value: { ...saved } };
|
||||
}
|
||||
case "membership": {
|
||||
const saved =
|
||||
state.membershipMethodDetailsById?.[target.overrideKey] ??
|
||||
membershipPresetFor(target.overrideKey);
|
||||
return { groupKey: "membership", value: { ...saved } };
|
||||
}
|
||||
case "decisionApproaches": {
|
||||
const saved =
|
||||
state.decisionApproachDetailsById?.[target.overrideKey] ??
|
||||
decisionApproachPresetFor(target.overrideKey);
|
||||
return {
|
||||
groupKey: "decisionApproaches",
|
||||
value: {
|
||||
...saved,
|
||||
applicableScope: [...saved.applicableScope],
|
||||
selectedApplicableScope: [...saved.selectedApplicableScope],
|
||||
},
|
||||
};
|
||||
}
|
||||
case "conflictManagement": {
|
||||
const saved =
|
||||
state.conflictManagementDetailsById?.[target.overrideKey] ??
|
||||
conflictManagementPresetFor(target.overrideKey);
|
||||
return {
|
||||
groupKey: "conflictManagement",
|
||||
value: {
|
||||
...saved,
|
||||
applicableScope: [...saved.applicableScope],
|
||||
selectedApplicableScope: [...saved.selectedApplicableScope],
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type SubtitleMessages = {
|
||||
tCv: ReturnType<typeof useMessages>["create"]["customRule"]["coreValues"];
|
||||
tComm: ReturnType<typeof useMessages>["create"]["customRule"]["communication"];
|
||||
tMem: ReturnType<typeof useMessages>["create"]["customRule"]["membership"];
|
||||
tDa: ReturnType<
|
||||
typeof useMessages
|
||||
>["create"]["customRule"]["decisionApproaches"];
|
||||
tCm: ReturnType<
|
||||
typeof useMessages
|
||||
>["create"]["customRule"]["conflictManagement"];
|
||||
};
|
||||
|
||||
function subtitleForTarget(
|
||||
target: FinalReviewChipEditTarget,
|
||||
msgs: SubtitleMessages,
|
||||
): string {
|
||||
switch (target.groupKey) {
|
||||
case "coreValues":
|
||||
return msgs.tCv.detailModal.subtitle;
|
||||
case "communication":
|
||||
return findMethodSupportText(msgs.tComm.methods, target.overrideKey);
|
||||
case "membership":
|
||||
return findMethodSupportText(msgs.tMem.methods, target.overrideKey);
|
||||
case "decisionApproaches":
|
||||
return findMethodSupportText(msgs.tDa.methods, target.overrideKey);
|
||||
case "conflictManagement":
|
||||
return findMethodSupportText(msgs.tCm.methods, target.overrideKey);
|
||||
}
|
||||
}
|
||||
|
||||
function findMethodSupportText(
|
||||
methods: readonly { id: string; supportText: string }[],
|
||||
id: string,
|
||||
): string {
|
||||
for (const method of methods) {
|
||||
if (method.id === id) return method.supportText;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* Controlled section editor for a communication-method chip. Used by both
|
||||
* the custom-rule `communication-methods` add-method modal and the
|
||||
* `final-review` chip edit modal — caller owns draft state and decides when
|
||||
* to persist or discard.
|
||||
*/
|
||||
|
||||
import { memo, useCallback } from "react";
|
||||
import { useMessages } from "../../../../contexts/MessagesContext";
|
||||
import ModalTextAreaField from "../ModalTextAreaField";
|
||||
import type { CommunicationMethodDetailEntry } from "../../types";
|
||||
|
||||
export interface CommunicationMethodEditFieldsProps {
|
||||
value: CommunicationMethodDetailEntry;
|
||||
onChange: (_next: CommunicationMethodDetailEntry) => void;
|
||||
}
|
||||
|
||||
const FIELDS: ReadonlyArray<keyof CommunicationMethodDetailEntry> = [
|
||||
"corePrinciple",
|
||||
"logisticsAdmin",
|
||||
"codeOfConduct",
|
||||
];
|
||||
|
||||
function CommunicationMethodEditFieldsComponent({
|
||||
value,
|
||||
onChange,
|
||||
}: CommunicationMethodEditFieldsProps) {
|
||||
const m = useMessages();
|
||||
const t = m.create.customRule.communication;
|
||||
|
||||
const patch = useCallback(
|
||||
<K extends keyof CommunicationMethodDetailEntry>(
|
||||
key: K,
|
||||
next: CommunicationMethodDetailEntry[K],
|
||||
) => {
|
||||
onChange({ ...value, [key]: next });
|
||||
},
|
||||
[value, onChange],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
{FIELDS.map((field) => (
|
||||
<ModalTextAreaField
|
||||
key={field}
|
||||
label={t.sectionHeadings[field]}
|
||||
rows={6}
|
||||
value={value[field]}
|
||||
onChange={(v) => patch(field, v)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
CommunicationMethodEditFieldsComponent.displayName =
|
||||
"CommunicationMethodEditFields";
|
||||
|
||||
export default memo(CommunicationMethodEditFieldsComponent);
|
||||
@@ -0,0 +1,78 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* Controlled section editor for a conflict-management chip. Used by both the
|
||||
* custom-rule `conflict-management` add-method modal and the `final-review`
|
||||
* chip edit modal. Caller owns draft state and persistence.
|
||||
*/
|
||||
|
||||
import { memo, useCallback } from "react";
|
||||
import { useMessages } from "../../../../contexts/MessagesContext";
|
||||
import ModalTextAreaField from "../ModalTextAreaField";
|
||||
import ApplicableScopeField from "../ApplicableScopeField";
|
||||
import type { ConflictManagementDetailEntry } from "../../types";
|
||||
|
||||
export interface ConflictManagementEditFieldsProps {
|
||||
value: ConflictManagementDetailEntry;
|
||||
onChange: (_next: ConflictManagementDetailEntry) => void;
|
||||
}
|
||||
|
||||
function ConflictManagementEditFieldsComponent({
|
||||
value,
|
||||
onChange,
|
||||
}: ConflictManagementEditFieldsProps) {
|
||||
const m = useMessages();
|
||||
const t = m.create.customRule.conflictManagement;
|
||||
|
||||
const patch = useCallback(
|
||||
<K extends keyof ConflictManagementDetailEntry>(
|
||||
key: K,
|
||||
next: ConflictManagementDetailEntry[K],
|
||||
) => {
|
||||
onChange({ ...value, [key]: next });
|
||||
},
|
||||
[value, onChange],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
<ModalTextAreaField
|
||||
label={t.sectionHeadings.corePrinciple}
|
||||
value={value.corePrinciple}
|
||||
onChange={(v) => patch("corePrinciple", v)}
|
||||
/>
|
||||
<ApplicableScopeField
|
||||
label={t.sectionHeadings.applicableScope}
|
||||
addLabel={t.scopeAddButtonLabel}
|
||||
scopes={value.applicableScope}
|
||||
selectedScopes={value.selectedApplicableScope}
|
||||
onToggleScope={(scope) =>
|
||||
patch(
|
||||
"selectedApplicableScope",
|
||||
value.selectedApplicableScope.includes(scope)
|
||||
? value.selectedApplicableScope.filter((s) => s !== scope)
|
||||
: [...value.selectedApplicableScope, scope],
|
||||
)
|
||||
}
|
||||
onAddScope={(scope) =>
|
||||
patch("applicableScope", [...value.applicableScope, scope])
|
||||
}
|
||||
/>
|
||||
<ModalTextAreaField
|
||||
label={t.sectionHeadings.processProtocol}
|
||||
value={value.processProtocol}
|
||||
onChange={(v) => patch("processProtocol", v)}
|
||||
/>
|
||||
<ModalTextAreaField
|
||||
label={t.sectionHeadings.restorationFallbacks}
|
||||
value={value.restorationFallbacks}
|
||||
onChange={(v) => patch("restorationFallbacks", v)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ConflictManagementEditFieldsComponent.displayName =
|
||||
"ConflictManagementEditFields";
|
||||
|
||||
export default memo(ConflictManagementEditFieldsComponent);
|
||||
@@ -0,0 +1,57 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* Controlled meaning/signals field set for a core-value chip. Rendered both
|
||||
* by `core-values` (custom-rule selection step) and `final-review` (chip
|
||||
* edit modal). Holds no state — the parent owns the draft and decides when
|
||||
* to persist (`updateState`) or discard.
|
||||
*/
|
||||
|
||||
import { memo, useCallback } from "react";
|
||||
import { useMessages } from "../../../../contexts/MessagesContext";
|
||||
import ModalTextAreaField from "../ModalTextAreaField";
|
||||
import type { CoreValueDetailEntry } from "../../types";
|
||||
|
||||
export interface CoreValueEditFieldsProps {
|
||||
value: CoreValueDetailEntry;
|
||||
onChange: (_next: CoreValueDetailEntry) => void;
|
||||
}
|
||||
|
||||
function CoreValueEditFieldsComponent({
|
||||
value,
|
||||
onChange,
|
||||
}: CoreValueEditFieldsProps) {
|
||||
const m = useMessages();
|
||||
const t = m.create.customRule.coreValues.detailModal;
|
||||
|
||||
const patch = useCallback(
|
||||
<K extends keyof CoreValueDetailEntry>(
|
||||
key: K,
|
||||
next: CoreValueDetailEntry[K],
|
||||
) => {
|
||||
onChange({ ...value, [key]: next });
|
||||
},
|
||||
[value, onChange],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-[var(--measures-spacing-600,24px)] pb-2">
|
||||
<ModalTextAreaField
|
||||
label={t.meaningLabel}
|
||||
value={value.meaning}
|
||||
onChange={(v) => patch("meaning", v)}
|
||||
rows={4}
|
||||
/>
|
||||
<ModalTextAreaField
|
||||
label={t.signalsLabel}
|
||||
value={value.signals}
|
||||
onChange={(v) => patch("signals", v)}
|
||||
rows={4}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
CoreValueEditFieldsComponent.displayName = "CoreValueEditFields";
|
||||
|
||||
export default memo(CoreValueEditFieldsComponent);
|
||||
@@ -0,0 +1,95 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* Controlled section editor for a decision-approach chip. Used by both the
|
||||
* custom-rule `decision-approaches` add-method modal and the `final-review`
|
||||
* chip edit modal. Caller owns draft state — Confirm/Save persistence and
|
||||
* `markCreateFlowInteraction` live in the parent.
|
||||
*/
|
||||
|
||||
import { memo, useCallback } from "react";
|
||||
import { useMessages } from "../../../../contexts/MessagesContext";
|
||||
import ModalTextAreaField from "../ModalTextAreaField";
|
||||
import ApplicableScopeField from "../ApplicableScopeField";
|
||||
import IncrementerBlock from "../../../../components/controls/IncrementerBlock";
|
||||
import type { DecisionApproachDetailEntry } from "../../types";
|
||||
|
||||
export interface DecisionApproachEditFieldsProps {
|
||||
value: DecisionApproachDetailEntry;
|
||||
onChange: (_next: DecisionApproachDetailEntry) => void;
|
||||
}
|
||||
|
||||
const CONSENSUS_LEVEL_MIN = 0;
|
||||
const CONSENSUS_LEVEL_MAX = 100;
|
||||
const CONSENSUS_LEVEL_STEP = 5;
|
||||
|
||||
function DecisionApproachEditFieldsComponent({
|
||||
value,
|
||||
onChange,
|
||||
}: DecisionApproachEditFieldsProps) {
|
||||
const m = useMessages();
|
||||
const t = m.create.customRule.decisionApproaches;
|
||||
|
||||
const patch = useCallback(
|
||||
<K extends keyof DecisionApproachDetailEntry>(
|
||||
key: K,
|
||||
next: DecisionApproachDetailEntry[K],
|
||||
) => {
|
||||
onChange({ ...value, [key]: next });
|
||||
},
|
||||
[value, onChange],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
<ModalTextAreaField
|
||||
label={t.sectionHeadings.corePrinciple}
|
||||
value={value.corePrinciple}
|
||||
onChange={(v) => patch("corePrinciple", v)}
|
||||
/>
|
||||
<ApplicableScopeField
|
||||
label={t.sectionHeadings.applicableScope}
|
||||
addLabel={t.scopeAddButtonLabel}
|
||||
scopes={value.applicableScope}
|
||||
selectedScopes={value.selectedApplicableScope}
|
||||
onToggleScope={(scope) =>
|
||||
patch(
|
||||
"selectedApplicableScope",
|
||||
value.selectedApplicableScope.includes(scope)
|
||||
? value.selectedApplicableScope.filter((s) => s !== scope)
|
||||
: [...value.selectedApplicableScope, scope],
|
||||
)
|
||||
}
|
||||
onAddScope={(scope) =>
|
||||
patch("applicableScope", [...value.applicableScope, scope])
|
||||
}
|
||||
/>
|
||||
<ModalTextAreaField
|
||||
label={t.sectionHeadings.stepByStepInstructions}
|
||||
value={value.stepByStepInstructions}
|
||||
onChange={(v) => patch("stepByStepInstructions", v)}
|
||||
/>
|
||||
<IncrementerBlock
|
||||
label={t.sectionHeadings.consensusLevel}
|
||||
value={value.consensusLevel}
|
||||
min={CONSENSUS_LEVEL_MIN}
|
||||
max={CONSENSUS_LEVEL_MAX}
|
||||
step={CONSENSUS_LEVEL_STEP}
|
||||
onChange={(next) => patch("consensusLevel", next)}
|
||||
formatValue={(v) => `${v}%`}
|
||||
decrementAriaLabel="Decrease consensus level"
|
||||
incrementAriaLabel="Increase consensus level"
|
||||
/>
|
||||
<ModalTextAreaField
|
||||
label={t.sectionHeadings.objectionsDeadlocks}
|
||||
value={value.objectionsDeadlocks}
|
||||
onChange={(v) => patch("objectionsDeadlocks", v)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
DecisionApproachEditFieldsComponent.displayName =
|
||||
"DecisionApproachEditFields";
|
||||
|
||||
export default memo(DecisionApproachEditFieldsComponent);
|
||||
@@ -0,0 +1,61 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* Controlled section editor for a membership-method chip. Used by both the
|
||||
* custom-rule `membership-methods` add-method modal and the `final-review`
|
||||
* chip edit modal — caller owns draft state and decides when to persist or
|
||||
* discard.
|
||||
*/
|
||||
|
||||
import { memo, useCallback } from "react";
|
||||
import { useMessages } from "../../../../contexts/MessagesContext";
|
||||
import ModalTextAreaField from "../ModalTextAreaField";
|
||||
import type { MembershipMethodDetailEntry } from "../../types";
|
||||
|
||||
export interface MembershipMethodEditFieldsProps {
|
||||
value: MembershipMethodDetailEntry;
|
||||
onChange: (_next: MembershipMethodDetailEntry) => void;
|
||||
}
|
||||
|
||||
const FIELDS: ReadonlyArray<keyof MembershipMethodDetailEntry> = [
|
||||
"eligibility",
|
||||
"joiningProcess",
|
||||
"expectations",
|
||||
];
|
||||
|
||||
function MembershipMethodEditFieldsComponent({
|
||||
value,
|
||||
onChange,
|
||||
}: MembershipMethodEditFieldsProps) {
|
||||
const m = useMessages();
|
||||
const t = m.create.customRule.membership;
|
||||
|
||||
const patch = useCallback(
|
||||
<K extends keyof MembershipMethodDetailEntry>(
|
||||
key: K,
|
||||
next: MembershipMethodDetailEntry[K],
|
||||
) => {
|
||||
onChange({ ...value, [key]: next });
|
||||
},
|
||||
[value, onChange],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
{FIELDS.map((field) => (
|
||||
<ModalTextAreaField
|
||||
key={field}
|
||||
label={t.sectionHeadings[field]}
|
||||
rows={6}
|
||||
value={value[field]}
|
||||
onChange={(v) => patch(field, v)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
MembershipMethodEditFieldsComponent.displayName =
|
||||
"MembershipMethodEditFields";
|
||||
|
||||
export default memo(MembershipMethodEditFieldsComponent);
|
||||
@@ -0,0 +1,14 @@
|
||||
export { default as CoreValueEditFields } from "./CoreValueEditFields";
|
||||
export type { CoreValueEditFieldsProps } from "./CoreValueEditFields";
|
||||
|
||||
export { default as CommunicationMethodEditFields } from "./CommunicationMethodEditFields";
|
||||
export type { CommunicationMethodEditFieldsProps } from "./CommunicationMethodEditFields";
|
||||
|
||||
export { default as MembershipMethodEditFields } from "./MembershipMethodEditFields";
|
||||
export type { MembershipMethodEditFieldsProps } from "./MembershipMethodEditFields";
|
||||
|
||||
export { default as DecisionApproachEditFields } from "./DecisionApproachEditFields";
|
||||
export type { DecisionApproachEditFieldsProps } from "./DecisionApproachEditFields";
|
||||
|
||||
export { default as ConflictManagementEditFields } from "./ConflictManagementEditFields";
|
||||
export type { ConflictManagementEditFieldsProps } from "./ConflictManagementEditFields";
|
||||
Reference in New Issue
Block a user