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"; "use client";
/** /**
* Shared "Applicable Scope" field used by the `decision-approaches` and * Shared "Applicable Scope" field used by the `decision-approaches` create-flow
* `conflict-management` create flow modals. Pairs an `InputLabel` with a * modal. Pairs an `InputLabel` with a horizontally-wrapping list of
* horizontally-wrapping list of toggle-chips plus an inline "+ Add" affordance * toggle-chips plus an inline "+ Add" affordance that reveals a pill text input
* that reveals a pill text input for creating new scope values. * for creating new scope values. Conflict management uses
* `ModalTextAreaField` instead (Figma `20874:172292`).
*/ */
import { memo, useState } from "react"; import { memo, useState } from "react";
@@ -7,11 +7,33 @@
*/ */
import { memo, useCallback } from "react"; import { memo, useCallback } from "react";
import { formatConflictApplicableScopeForTextarea } from "../../../../../lib/create/ruleSectionsFromMethodSelections";
import { useMessages } from "../../../../contexts/MessagesContext"; import { useMessages } from "../../../../contexts/MessagesContext";
import ModalTextAreaField from "../ModalTextAreaField"; import ModalTextAreaField from "../ModalTextAreaField";
import ApplicableScopeField from "../ApplicableScopeField";
import type { ConflictManagementDetailEntry } from "../../types"; 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 { export interface ConflictManagementEditFieldsProps {
value: ConflictManagementDetailEntry; value: ConflictManagementDetailEntry;
onChange: (_next: ConflictManagementDetailEntry) => void; onChange: (_next: ConflictManagementDetailEntry) => void;
@@ -41,22 +63,12 @@ function ConflictManagementEditFieldsComponent({
value={value.corePrinciple} value={value.corePrinciple}
onChange={(v) => patch("corePrinciple", v)} onChange={(v) => patch("corePrinciple", v)}
/> />
<ApplicableScopeField <ModalTextAreaField
label={t.sectionHeadings.applicableScope} label={t.sectionHeadings.applicableScope}
addLabel={t.scopeAddButtonLabel} value={conflictScopeTextareaValue(value)}
scopes={value.applicableScope} placeholder={t.applicableScopePlaceholder}
selectedScopes={value.selectedApplicableScope} onChange={(v) => onChange(conflictDetailWithScopeTextarea(value, v))}
onToggleScope={(scope) => rows={4}
patch(
"selectedApplicableScope",
value.selectedApplicableScope.includes(scope)
? value.selectedApplicableScope.filter((s) => s !== scope)
: [...value.selectedApplicableScope, scope],
)
}
onAddScope={(scope) =>
patch("applicableScope", [...value.applicableScope, scope])
}
/> />
<ModalTextAreaField <ModalTextAreaField
label={t.sectionHeadings.processProtocol} label={t.sectionHeadings.processProtocol}
@@ -6,7 +6,7 @@
* *
* Card click opens the Figma "Add Approach" create modal (node `20874-172292`) * Card click opens the Figma "Add Approach" create modal (node `20874-172292`)
* with four controls rendered by {@link ConflictManagementEditFields}: Core * 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 * & Fallbacks. The same field set is reused on `/create/final-review` — see
* `FinalReviewChipEditModal`. Confirm persists both the chip selection and * `FinalReviewChipEditModal`. Confirm persists both the chip selection and
* any user edits as a `conflictManagementDetailsById[id]` override. * 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 ModalTextAreaField from "../../../(app)/create/components/ModalTextAreaField";
import { useMessages, useTranslation } from "../../../contexts/MessagesContext"; import { useMessages, useTranslation } from "../../../contexts/MessagesContext";
import type { TemplateChipDetail } from "../../../../lib/create/templateReviewMapping"; import type { TemplateChipDetail } from "../../../../lib/create/templateReviewMapping";
import { formatConflictApplicableScopeForTextarea } from "../../../../lib/create/ruleSectionsFromMethodSelections";
export interface TemplateChipDetailModalProps { export interface TemplateChipDetailModalProps {
isOpen: boolean; isOpen: boolean;
@@ -235,9 +236,15 @@ function resolveChipContent(
disabled disabled
rows={4} rows={4}
/> />
<ReadOnlyScopeField <ModalTextAreaField
label={cm.sectionHeadings.applicableScope} label={cm.sectionHeadings.applicableScope}
scopes={preset.sections.applicableScope} value={formatConflictApplicableScopeForTextarea(
[],
preset.sections.applicableScope,
)}
onChange={noop}
disabled
rows={4}
/> />
<ModalTextAreaField <ModalTextAreaField
label={cm.sectionHeadings.processProtocol} label={cm.sectionHeadings.processProtocol}
@@ -56,6 +56,27 @@ export function formatScopePayload(val: unknown): string | null {
return lines.join("\n"); 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( export function blocksFromKeyedRecord(
sections: Record<string, unknown>, sections: Record<string, unknown>,
labelByKey: Record<string, string>, labelByKey: Record<string, string>,
@@ -24,6 +24,7 @@
"restorationFallbacks": "Restoration & Fallbacks" "restorationFallbacks": "Restoration & Fallbacks"
}, },
"scopeAddButtonLabel": "Add Applicable Scope", "scopeAddButtonLabel": "Add Applicable Scope",
"applicableScopePlaceholder": "Describe when and where this approach applies",
"methods": [ "methods": [
{ {
"id": "peer-mediation", "id": "peer-mediation",
@@ -9,7 +9,7 @@ export default {
docs: { docs: {
description: { description: {
component: 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("");
});
});