diff --git a/app/(app)/create/components/ApplicableScopeField.tsx b/app/(app)/create/components/ApplicableScopeField.tsx index 035593f..e31ef3e 100644 --- a/app/(app)/create/components/ApplicableScopeField.tsx +++ b/app/(app)/create/components/ApplicableScopeField.tsx @@ -1,10 +1,11 @@ "use client"; /** - * Shared "Applicable Scope" field used by the `decision-approaches` and - * `conflict-management` create flow modals. Pairs an `InputLabel` with a - * horizontally-wrapping list of toggle-chips plus an inline "+ Add" affordance - * that reveals a pill text input for creating new scope values. + * Shared "Applicable Scope" field used by the `decision-approaches` create-flow + * modal. Pairs an `InputLabel` with a horizontally-wrapping list of + * toggle-chips plus an inline "+ Add" affordance that reveals a pill text input + * for creating new scope values. Conflict management uses + * `ModalTextAreaField` instead (Figma `20874:172292`). */ import { memo, useState } from "react"; diff --git a/app/(app)/create/components/methodEditFields/ConflictManagementEditFields.tsx b/app/(app)/create/components/methodEditFields/ConflictManagementEditFields.tsx index 3c0da4f..ddb1d47 100644 --- a/app/(app)/create/components/methodEditFields/ConflictManagementEditFields.tsx +++ b/app/(app)/create/components/methodEditFields/ConflictManagementEditFields.tsx @@ -7,11 +7,33 @@ */ import { memo, useCallback } from "react"; +import { formatConflictApplicableScopeForTextarea } from "../../../../../lib/create/ruleSectionsFromMethodSelections"; import { useMessages } from "../../../../contexts/MessagesContext"; import ModalTextAreaField from "../ModalTextAreaField"; -import ApplicableScopeField from "../ApplicableScopeField"; import type { ConflictManagementDetailEntry } from "../../types"; +function conflictScopeTextareaValue(value: ConflictManagementDetailEntry): string { + return formatConflictApplicableScopeForTextarea( + value.selectedApplicableScope, + value.applicableScope, + ); +} + +function conflictDetailWithScopeTextarea( + value: ConflictManagementDetailEntry, + text: string, +): ConflictManagementDetailEntry { + const lines = text + .split("\n") + .map((s) => s.trim()) + .filter((s) => s.length > 0); + return { + ...value, + applicableScope: lines, + selectedApplicableScope: [...lines], + }; +} + export interface ConflictManagementEditFieldsProps { value: ConflictManagementDetailEntry; onChange: (_next: ConflictManagementDetailEntry) => void; @@ -41,22 +63,12 @@ function ConflictManagementEditFieldsComponent({ value={value.corePrinciple} onChange={(v) => patch("corePrinciple", v)} /> - - patch( - "selectedApplicableScope", - value.selectedApplicableScope.includes(scope) - ? value.selectedApplicableScope.filter((s) => s !== scope) - : [...value.selectedApplicableScope, scope], - ) - } - onAddScope={(scope) => - patch("applicableScope", [...value.applicableScope, scope]) - } + value={conflictScopeTextareaValue(value)} + placeholder={t.applicableScopePlaceholder} + onChange={(v) => onChange(conflictDetailWithScopeTextarea(value, v))} + rows={4} /> - typeof x === "string" && x.trim().length > 0, + ); + const app = applicableScope.filter( + (x): x is string => typeof x === "string" && x.trim().length > 0, + ); + const parts = sel.length > 0 ? sel : app; + if (parts.length === 0) return ""; + return parts.join(", "); +} + export function blocksFromKeyedRecord( sections: Record, labelByKey: Record, diff --git a/messages/en/create/customRule/conflictManagement.json b/messages/en/create/customRule/conflictManagement.json index 5f467a5..dbeea52 100644 --- a/messages/en/create/customRule/conflictManagement.json +++ b/messages/en/create/customRule/conflictManagement.json @@ -24,6 +24,7 @@ "restorationFallbacks": "Restoration & Fallbacks" }, "scopeAddButtonLabel": "Add Applicable Scope", + "applicableScopePlaceholder": "Describe when and where this approach applies", "methods": [ { "id": "peer-mediation", diff --git a/stories/create-flow/ApplicableScopeField.stories.js b/stories/create-flow/ApplicableScopeField.stories.js index 107417c..3dae965 100644 --- a/stories/create-flow/ApplicableScopeField.stories.js +++ b/stories/create-flow/ApplicableScopeField.stories.js @@ -9,7 +9,7 @@ export default { docs: { description: { component: - "Shared 'Applicable Scope' field used by the `decision-approaches` and `conflict-management` create-flow modals. Pairs an `InputLabel` with a row of toggle-chips plus an inline pill input for adding new scope values.", + "Shared 'Applicable Scope' field used by the `decision-approaches` create-flow modal. Toggle-chips plus an inline pill input for adding new scope values. Conflict management uses `ModalTextAreaField` (see Figma `20874:172292`).", }, }, }, diff --git a/tests/unit/formatConflictApplicableScopeForTextarea.test.ts b/tests/unit/formatConflictApplicableScopeForTextarea.test.ts new file mode 100644 index 0000000..ca11a8f --- /dev/null +++ b/tests/unit/formatConflictApplicableScopeForTextarea.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, it } from "vitest"; +import { formatConflictApplicableScopeForTextarea } from "../../lib/create/ruleSectionsFromMethodSelections"; + +describe("formatConflictApplicableScopeForTextarea", () => { + it("joins legacy preset fragments with comma-space as one sentence", () => { + expect( + formatConflictApplicableScopeForTextarea( + [], + [ + "Low-level friction", + "misunderstandings", + "and minor grievances between peers.", + ], + ), + ).toBe( + "Low-level friction, misunderstandings, and minor grievances between peers.", + ); + }); + + it("prefers selected scopes when non-empty", () => { + expect( + formatConflictApplicableScopeForTextarea(["only this"], ["a", "b"]), + ).toBe("only this"); + }); + + it("returns empty string when both lists are empty", () => { + expect(formatConflictApplicableScopeForTextarea([], [])).toBe(""); + }); +});