Update conflict management modal

This commit is contained in:
adilallo
2026-04-30 09:23:40 -06:00
parent b7446873cd
commit 58d0e33500
8 changed files with 95 additions and 24 deletions
@@ -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";
@@ -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)}
/>
<ApplicableScopeField
<ModalTextAreaField
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])
}
value={conflictScopeTextareaValue(value)}
placeholder={t.applicableScopePlaceholder}
onChange={(v) => onChange(conflictDetailWithScopeTextarea(value, v))}
rows={4}
/>
<ModalTextAreaField
label={t.sectionHeadings.processProtocol}
@@ -6,7 +6,7 @@
*
* Card click opens the Figma "Add Approach" create modal (node `20874-172292`)
* with four controls rendered by {@link ConflictManagementEditFields}: Core
* Principle, Applicable Scope (capsules), Process Protocol, and Restoration
* Principle, Applicable Scope (text area), Process Protocol, and Restoration
* & 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.
@@ -8,6 +8,7 @@ import ContentLockup from "../../type/ContentLockup";
import ModalTextAreaField from "../../../(app)/create/components/ModalTextAreaField";
import { useMessages, useTranslation } from "../../../contexts/MessagesContext";
import type { TemplateChipDetail } from "../../../../lib/create/templateReviewMapping";
import { formatConflictApplicableScopeForTextarea } from "../../../../lib/create/ruleSectionsFromMethodSelections";
export interface TemplateChipDetailModalProps {
isOpen: boolean;
@@ -235,9 +236,15 @@ function resolveChipContent(
disabled
rows={4}
/>
<ReadOnlyScopeField
<ModalTextAreaField
label={cm.sectionHeadings.applicableScope}
scopes={preset.sections.applicableScope}
value={formatConflictApplicableScopeForTextarea(
[],
preset.sections.applicableScope,
)}
onChange={noop}
disabled
rows={4}
/>
<ModalTextAreaField
label={cm.sectionHeadings.processProtocol}
@@ -56,6 +56,27 @@ export function formatScopePayload(val: unknown): string | null {
return lines.join("\n");
}
/**
* Conflict-management applicable scope is a single textarea; preset JSON often
* splits one sentence across multiple strings (legacy chip fragments). Join
* with ", " for normal sentence display. Prefer non-empty `selectedApplicableScope`
* when present, otherwise `applicableScope`.
*/
export function formatConflictApplicableScopeForTextarea(
selectedApplicableScope: readonly string[],
applicableScope: readonly string[],
): string {
const sel = selectedApplicableScope.filter(
(x): x is string => 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<string, unknown>,
labelByKey: Record<string, string>,
@@ -24,6 +24,7 @@
"restorationFallbacks": "Restoration & Fallbacks"
},
"scopeAddButtonLabel": "Add Applicable Scope",
"applicableScopePlaceholder": "Describe when and where this approach applies",
"methods": [
{
"id": "peer-mediation",
@@ -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`).",
},
},
},
@@ -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("");
});
});