Update conflict management modal
This commit is contained in:
@@ -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("");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user