App reorganization
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import MultiSelect from "../../../../components/controls/MultiSelect";
|
||||
import type { ChipOption } from "../../../../components/controls/MultiSelect/MultiSelect.types";
|
||||
import { useMessages } from "../../../../contexts/MessagesContext";
|
||||
import { useCreateFlow } from "../../context/CreateFlowContext";
|
||||
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
|
||||
import { CreateFlowTwoColumnSelectShell } from "../../components/CreateFlowTwoColumnSelectShell";
|
||||
|
||||
function chipRowsFromLabels(
|
||||
rows: readonly { label: string }[],
|
||||
): ChipOption[] {
|
||||
return rows.map((row, i) => ({
|
||||
id: String(i + 1),
|
||||
label: row.label,
|
||||
state: "unselected" as const,
|
||||
}));
|
||||
}
|
||||
|
||||
function selectedIdsFromOptions(options: ChipOption[]): string[] {
|
||||
return options
|
||||
.filter((o) => o.state === "selected")
|
||||
.map((o) => o.id);
|
||||
}
|
||||
|
||||
/** Create Community — Figma `20094:41317`, chips only (layout tokens shared with structure select). */
|
||||
export function CommunitySizeSelectScreen() {
|
||||
const m = useMessages();
|
||||
const cs = m.create.communitySize;
|
||||
const { markCreateFlowInteraction, updateState, state } = useCreateFlow();
|
||||
|
||||
const [communitySizeOptions, setCommunitySizeOptions] = useState<
|
||||
ChipOption[]
|
||||
>(() => {
|
||||
const base = chipRowsFromLabels(cs.communitySizes);
|
||||
const selected = new Set(state.selectedCommunitySizeIds ?? []);
|
||||
return base.map((opt) => ({
|
||||
...opt,
|
||||
state: selected.has(opt.id) ? ("selected" as const) : ("unselected" as const),
|
||||
}));
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const selected = new Set(state.selectedCommunitySizeIds ?? []);
|
||||
setCommunitySizeOptions((prev) =>
|
||||
prev.map((opt) =>
|
||||
opt.state === "custom"
|
||||
? opt
|
||||
: {
|
||||
...opt,
|
||||
state: selected.has(opt.id)
|
||||
? ("selected" as const)
|
||||
: ("unselected" as const),
|
||||
},
|
||||
),
|
||||
);
|
||||
}, [state.selectedCommunitySizeIds]);
|
||||
|
||||
const persistSelection = (next: ChipOption[]) => {
|
||||
markCreateFlowInteraction();
|
||||
setCommunitySizeOptions(next);
|
||||
updateState({
|
||||
selectedCommunitySizeIds: selectedIdsFromOptions(next),
|
||||
});
|
||||
};
|
||||
|
||||
const handleCommunitySizeClick = (chipId: string) => {
|
||||
const next: ChipOption[] = communitySizeOptions.map((opt) =>
|
||||
opt.id === chipId
|
||||
? {
|
||||
...opt,
|
||||
state:
|
||||
opt.state === "selected"
|
||||
? ("unselected" as const)
|
||||
: ("selected" as const),
|
||||
}
|
||||
: opt,
|
||||
);
|
||||
persistSelection(next);
|
||||
};
|
||||
|
||||
const multiSelectBlock = (
|
||||
<MultiSelect
|
||||
formHeader={false}
|
||||
size="m"
|
||||
options={communitySizeOptions}
|
||||
onChipClick={handleCommunitySizeClick}
|
||||
addButton={false}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<CreateFlowTwoColumnSelectShell
|
||||
header={
|
||||
<CreateFlowHeaderLockup
|
||||
title={cs.header.title}
|
||||
description={cs.header.description}
|
||||
justification="left"
|
||||
/>
|
||||
}
|
||||
>
|
||||
{multiSelectBlock}
|
||||
</CreateFlowTwoColumnSelectShell>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,350 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
useState,
|
||||
useMemo,
|
||||
useEffect,
|
||||
type Dispatch,
|
||||
type SetStateAction,
|
||||
} from "react";
|
||||
import MultiSelect from "../../../../components/controls/MultiSelect";
|
||||
import type { ChipOption } from "../../../../components/controls/MultiSelect/MultiSelect.types";
|
||||
import { useMessages } from "../../../../contexts/MessagesContext";
|
||||
import { useCreateFlow } from "../../context/CreateFlowContext";
|
||||
import type { CommunityStructureChipSnapshotRow } from "../../types";
|
||||
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
|
||||
import { CreateFlowTwoColumnSelectShell } from "../../components/CreateFlowTwoColumnSelectShell";
|
||||
|
||||
function createListCustomHandlers(
|
||||
setList: Dispatch<SetStateAction<ChipOption[]>>,
|
||||
confirmState: "unselected" | "selected",
|
||||
onInteraction?: () => void,
|
||||
) {
|
||||
const touch = () => onInteraction?.();
|
||||
return {
|
||||
onAddClick: () => {
|
||||
touch();
|
||||
setList((prev) => [
|
||||
...prev,
|
||||
{ id: crypto.randomUUID(), label: "", state: "custom" },
|
||||
]);
|
||||
},
|
||||
onCustomChipConfirm: (chipId: string, value: string) => {
|
||||
touch();
|
||||
setList((prev) =>
|
||||
prev.map((opt) =>
|
||||
opt.id === chipId
|
||||
? { ...opt, label: value, state: confirmState }
|
||||
: opt,
|
||||
),
|
||||
);
|
||||
},
|
||||
onCustomChipClose: (chipId: string) => {
|
||||
touch();
|
||||
setList((prev) => prev.filter((o) => o.id !== chipId));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function chipRowsFromLabels(
|
||||
rows: readonly { label: string }[],
|
||||
): ChipOption[] {
|
||||
return rows.map((row, i) => ({
|
||||
id: String(i + 1),
|
||||
label: row.label,
|
||||
state: "unselected" as const,
|
||||
}));
|
||||
}
|
||||
|
||||
function applySavedSelection(
|
||||
options: ChipOption[],
|
||||
saved: string[] | undefined,
|
||||
): ChipOption[] {
|
||||
const selected = new Set(saved ?? []);
|
||||
return options.map((opt) =>
|
||||
opt.state === "custom"
|
||||
? opt
|
||||
: {
|
||||
...opt,
|
||||
state: selected.has(opt.id)
|
||||
? ("selected" as const)
|
||||
: ("unselected" as const),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function selectedIdsFromOptions(options: ChipOption[]): string[] {
|
||||
return options
|
||||
.filter((o) => o.state === "selected")
|
||||
.map((o) => o.id);
|
||||
}
|
||||
|
||||
function chipOptionsToSnapshotRows(
|
||||
options: ChipOption[],
|
||||
): CommunityStructureChipSnapshotRow[] {
|
||||
return options.map((o) => ({
|
||||
id: o.id,
|
||||
label: o.label,
|
||||
...(o.state !== undefined ? { state: o.state } : {}),
|
||||
}));
|
||||
}
|
||||
|
||||
/** Returns chips when a draft snapshot exists; otherwise null (use preset rows + selected ids). */
|
||||
function snapshotRowsToChipOptions(
|
||||
rows: CommunityStructureChipSnapshotRow[] | undefined,
|
||||
): ChipOption[] | null {
|
||||
if (!Array.isArray(rows) || rows.length === 0) return null;
|
||||
return rows.map((r) => ({
|
||||
id: r.id,
|
||||
label: r.label,
|
||||
...(r.state !== undefined
|
||||
? { state: r.state as ChipOption["state"] }
|
||||
: {}),
|
||||
}));
|
||||
}
|
||||
|
||||
/** Create Community step 3 — Figma `20094:18244` (responsive grid + column caps via `createFlowLayoutTokens`). */
|
||||
export function CommunityStructureSelectScreen() {
|
||||
const m = useMessages();
|
||||
const cs = m.create.communityStructure;
|
||||
const { markCreateFlowInteraction, updateState, state } = useCreateFlow();
|
||||
|
||||
const [organizationTypeOptions, setOrganizationTypeOptions] = useState<
|
||||
ChipOption[]
|
||||
>(() => {
|
||||
const fromSnap = snapshotRowsToChipOptions(
|
||||
state.communityStructureChipSnapshots?.organizationTypes,
|
||||
);
|
||||
if (fromSnap) return fromSnap;
|
||||
return applySavedSelection(
|
||||
chipRowsFromLabels(cs.organizationTypes),
|
||||
state.selectedOrganizationTypeIds,
|
||||
);
|
||||
});
|
||||
|
||||
const [scaleOptions, setScaleOptions] = useState<ChipOption[]>(() => {
|
||||
const fromSnap = snapshotRowsToChipOptions(
|
||||
state.communityStructureChipSnapshots?.scale,
|
||||
);
|
||||
if (fromSnap) return fromSnap;
|
||||
return applySavedSelection(
|
||||
chipRowsFromLabels(cs.scaleOptions),
|
||||
state.selectedScaleIds,
|
||||
);
|
||||
});
|
||||
|
||||
const [maturityOptions, setMaturityOptions] = useState<ChipOption[]>(() => {
|
||||
const fromSnap = snapshotRowsToChipOptions(
|
||||
state.communityStructureChipSnapshots?.maturity,
|
||||
);
|
||||
if (fromSnap) return fromSnap;
|
||||
return applySavedSelection(
|
||||
chipRowsFromLabels(cs.maturityOptions),
|
||||
state.selectedMaturityIds,
|
||||
);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const fromSnap = snapshotRowsToChipOptions(
|
||||
state.communityStructureChipSnapshots?.organizationTypes,
|
||||
);
|
||||
if (fromSnap) {
|
||||
setOrganizationTypeOptions(fromSnap);
|
||||
return;
|
||||
}
|
||||
setOrganizationTypeOptions((prev) =>
|
||||
applySavedSelection(prev, state.selectedOrganizationTypeIds),
|
||||
);
|
||||
}, [
|
||||
state.communityStructureChipSnapshots?.organizationTypes,
|
||||
state.selectedOrganizationTypeIds,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
const fromSnap = snapshotRowsToChipOptions(
|
||||
state.communityStructureChipSnapshots?.scale,
|
||||
);
|
||||
if (fromSnap) {
|
||||
setScaleOptions(fromSnap);
|
||||
return;
|
||||
}
|
||||
setScaleOptions((prev) => applySavedSelection(prev, state.selectedScaleIds));
|
||||
}, [
|
||||
state.communityStructureChipSnapshots?.scale,
|
||||
state.selectedScaleIds,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
const fromSnap = snapshotRowsToChipOptions(
|
||||
state.communityStructureChipSnapshots?.maturity,
|
||||
);
|
||||
if (fromSnap) {
|
||||
setMaturityOptions(fromSnap);
|
||||
return;
|
||||
}
|
||||
setMaturityOptions((prev) =>
|
||||
applySavedSelection(prev, state.selectedMaturityIds),
|
||||
);
|
||||
}, [
|
||||
state.communityStructureChipSnapshots?.maturity,
|
||||
state.selectedMaturityIds,
|
||||
]);
|
||||
|
||||
const organizationCustomHandlers = useMemo(
|
||||
() =>
|
||||
createListCustomHandlers(
|
||||
setOrganizationTypeOptions,
|
||||
"unselected",
|
||||
markCreateFlowInteraction,
|
||||
),
|
||||
[markCreateFlowInteraction],
|
||||
);
|
||||
const scaleCustomHandlers = useMemo(
|
||||
() =>
|
||||
createListCustomHandlers(
|
||||
setScaleOptions,
|
||||
"unselected",
|
||||
markCreateFlowInteraction,
|
||||
),
|
||||
[markCreateFlowInteraction],
|
||||
);
|
||||
const maturityCustomHandlers = useMemo(
|
||||
() =>
|
||||
createListCustomHandlers(
|
||||
setMaturityOptions,
|
||||
"unselected",
|
||||
markCreateFlowInteraction,
|
||||
),
|
||||
[markCreateFlowInteraction],
|
||||
);
|
||||
|
||||
const persistOrg = (next: ChipOption[]) => {
|
||||
markCreateFlowInteraction();
|
||||
setOrganizationTypeOptions(next);
|
||||
updateState({
|
||||
selectedOrganizationTypeIds: selectedIdsFromOptions(next),
|
||||
communityStructureChipSnapshots: {
|
||||
organizationTypes: chipOptionsToSnapshotRows(next),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const persistScale = (next: ChipOption[]) => {
|
||||
markCreateFlowInteraction();
|
||||
setScaleOptions(next);
|
||||
updateState({
|
||||
selectedScaleIds: selectedIdsFromOptions(next),
|
||||
communityStructureChipSnapshots: {
|
||||
scale: chipOptionsToSnapshotRows(next),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const persistMaturity = (next: ChipOption[]) => {
|
||||
markCreateFlowInteraction();
|
||||
setMaturityOptions(next);
|
||||
updateState({
|
||||
selectedMaturityIds: selectedIdsFromOptions(next),
|
||||
communityStructureChipSnapshots: {
|
||||
maturity: chipOptionsToSnapshotRows(next),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleOrganizationTypeClick = (chipId: string) => {
|
||||
persistOrg(
|
||||
organizationTypeOptions.map((opt) =>
|
||||
opt.id === chipId
|
||||
? {
|
||||
...opt,
|
||||
state:
|
||||
opt.state === "selected"
|
||||
? ("unselected" as const)
|
||||
: ("selected" as const),
|
||||
}
|
||||
: opt,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
const handleScaleClick = (chipId: string) => {
|
||||
persistScale(
|
||||
scaleOptions.map((opt) =>
|
||||
opt.id === chipId
|
||||
? {
|
||||
...opt,
|
||||
state:
|
||||
opt.state === "selected"
|
||||
? ("unselected" as const)
|
||||
: ("selected" as const),
|
||||
}
|
||||
: opt,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
const handleMaturityClick = (chipId: string) => {
|
||||
persistMaturity(
|
||||
maturityOptions.map((opt) =>
|
||||
opt.id === chipId
|
||||
? {
|
||||
...opt,
|
||||
state:
|
||||
opt.state === "selected"
|
||||
? ("unselected" as const)
|
||||
: ("selected" as const),
|
||||
}
|
||||
: opt,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
const multiSelectBlock = (
|
||||
<>
|
||||
<MultiSelect
|
||||
label={cs.organizationMultiSelect.label}
|
||||
showHelpIcon
|
||||
size="s"
|
||||
options={organizationTypeOptions}
|
||||
onChipClick={handleOrganizationTypeClick}
|
||||
{...organizationCustomHandlers}
|
||||
addButton
|
||||
addButtonText={cs.organizationMultiSelect.addButtonText}
|
||||
/>
|
||||
<MultiSelect
|
||||
label={cs.scaleMultiSelect.label}
|
||||
showHelpIcon
|
||||
size="s"
|
||||
options={scaleOptions}
|
||||
onChipClick={handleScaleClick}
|
||||
{...scaleCustomHandlers}
|
||||
addButton
|
||||
addButtonText={cs.scaleMultiSelect.addButtonText}
|
||||
/>
|
||||
<MultiSelect
|
||||
label={cs.maturityMultiSelect.label}
|
||||
showHelpIcon
|
||||
size="s"
|
||||
options={maturityOptions}
|
||||
onChipClick={handleMaturityClick}
|
||||
{...maturityCustomHandlers}
|
||||
addButton
|
||||
addButtonText={cs.maturityMultiSelect.addButtonText}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<CreateFlowTwoColumnSelectShell
|
||||
header={
|
||||
<CreateFlowHeaderLockup
|
||||
title={cs.header.title}
|
||||
description={cs.header.description}
|
||||
justification="left"
|
||||
/>
|
||||
}
|
||||
>
|
||||
{multiSelectBlock}
|
||||
</CreateFlowTwoColumnSelectShell>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import MultiSelect from "../../../../components/controls/MultiSelect";
|
||||
import Alert from "../../../../components/modals/Alert";
|
||||
import type { ChipOption } from "../../../../components/controls/MultiSelect/MultiSelect.types";
|
||||
import { useTranslation } from "../../../../contexts/MessagesContext";
|
||||
import { useCreateFlow } from "../../context/CreateFlowContext";
|
||||
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
|
||||
import { CreateFlowStepShell } from "../../components/CreateFlowStepShell";
|
||||
import { CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS } from "../../components/createFlowLayoutTokens";
|
||||
|
||||
export function ConfirmStakeholdersScreen() {
|
||||
const { markCreateFlowInteraction } = useCreateFlow();
|
||||
const t = useTranslation("create.confirmStakeholders");
|
||||
const [toastDismissed, setToastDismissed] = useState(false);
|
||||
const [stakeholderOptions, setStakeholderOptions] = useState<ChipOption[]>(
|
||||
[],
|
||||
);
|
||||
|
||||
const handleAddStakeholder = () => {
|
||||
markCreateFlowInteraction();
|
||||
setStakeholderOptions((prev) => [
|
||||
...prev,
|
||||
{ id: crypto.randomUUID(), label: "", state: "custom" },
|
||||
]);
|
||||
};
|
||||
|
||||
const handleCustomChipConfirm = (chipId: string, value: string) => {
|
||||
markCreateFlowInteraction();
|
||||
setStakeholderOptions((prev) =>
|
||||
prev.map((opt) =>
|
||||
opt.id === chipId ? { ...opt, label: value, state: "selected" } : opt,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
const handleCustomChipClose = (chipId: string) => {
|
||||
markCreateFlowInteraction();
|
||||
setStakeholderOptions((prev) => prev.filter((opt) => opt.id !== chipId));
|
||||
};
|
||||
|
||||
const handleChipClick = (chipId: string) => {
|
||||
markCreateFlowInteraction();
|
||||
setStakeholderOptions((prev) => prev.filter((opt) => opt.id !== chipId));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<CreateFlowStepShell
|
||||
variant="centeredNarrowBottomPad"
|
||||
contentTopBelowMd="space-1400"
|
||||
>
|
||||
<div
|
||||
className={`flex flex-col items-start gap-[var(--measures-spacing-300,12px)] ${CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS}`}
|
||||
>
|
||||
<div className="flex w-full flex-col gap-[var(--measures-spacing-200,8px)] py-[12px]">
|
||||
<CreateFlowHeaderLockup
|
||||
title={t("title")}
|
||||
description={t("description")}
|
||||
justification="left"
|
||||
/>
|
||||
</div>
|
||||
<MultiSelect
|
||||
formHeader={false}
|
||||
showHelpIcon={false}
|
||||
size="s"
|
||||
options={stakeholderOptions}
|
||||
onChipClick={handleChipClick}
|
||||
onAddClick={handleAddStakeholder}
|
||||
onCustomChipConfirm={handleCustomChipConfirm}
|
||||
onCustomChipClose={handleCustomChipClose}
|
||||
addButton
|
||||
addButtonText={t("addStakeholder")}
|
||||
/>
|
||||
</div>
|
||||
</CreateFlowStepShell>
|
||||
|
||||
{!toastDismissed && (
|
||||
<div
|
||||
className="fixed bottom-[5.25rem] left-1/2 z-10 w-[min(640px,calc(100%-2.5rem))] max-w-[640px] -translate-x-1/2 md:bottom-[5.5rem]"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
>
|
||||
<Alert
|
||||
type="banner"
|
||||
status="positive"
|
||||
title={t("draftToastTitle")}
|
||||
hasLeadingIcon={false}
|
||||
hasBodyText={false}
|
||||
onClose={() => setToastDismissed(true)}
|
||||
className="w-full !px-[var(--space-600,24px)] !py-[var(--space-400,16px)] md:!py-4"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,401 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useCallback, useMemo } from "react";
|
||||
import MultiSelect from "../../../../components/controls/MultiSelect";
|
||||
import type { ChipOption } from "../../../../components/controls/MultiSelect/MultiSelect.types";
|
||||
import TextArea from "../../../../components/controls/TextArea";
|
||||
import Create from "../../../../components/modals/Create";
|
||||
import ContentLockup from "../../../../components/type/ContentLockup";
|
||||
import { useMessages } from "../../../../contexts/MessagesContext";
|
||||
import { useCreateFlow } from "../../context/CreateFlowContext";
|
||||
import type { CommunityStructureChipSnapshotRow } from "../../types";
|
||||
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
|
||||
import { CreateFlowTwoColumnSelectShell } from "../../components/CreateFlowTwoColumnSelectShell";
|
||||
|
||||
const MAX_CORE_VALUES = 5;
|
||||
|
||||
type ModalSession = "pending" | "editing";
|
||||
|
||||
/** Row in `coreValues.json` `values` — string (legacy) or `{ label, meaning, signals }`. */
|
||||
type CoreValuePresetJson =
|
||||
| string
|
||||
| { label: string; meaning?: string; signals?: string };
|
||||
|
||||
type CoreValuePreset = {
|
||||
label: string;
|
||||
meaning: string;
|
||||
signals: string;
|
||||
};
|
||||
|
||||
function normalizeCoreValuePresets(
|
||||
values: readonly CoreValuePresetJson[],
|
||||
): CoreValuePreset[] {
|
||||
return values.map((v) => {
|
||||
if (typeof v === "string") {
|
||||
return { label: v, meaning: "", signals: "" };
|
||||
}
|
||||
return {
|
||||
label: v.label,
|
||||
meaning: typeof v.meaning === "string" ? v.meaning : "",
|
||||
signals: typeof v.signals === "string" ? v.signals : "",
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function chipRowsFromPresets(presets: readonly CoreValuePreset[]): ChipOption[] {
|
||||
return presets.map((row, i) => ({
|
||||
id: String(i + 1),
|
||||
label: row.label,
|
||||
state: "unselected" as const,
|
||||
}));
|
||||
}
|
||||
|
||||
function applySavedSelection(
|
||||
options: ChipOption[],
|
||||
saved: string[] | undefined,
|
||||
): ChipOption[] {
|
||||
const selected = new Set(saved ?? []);
|
||||
return options.map((opt) =>
|
||||
opt.state === "custom"
|
||||
? opt
|
||||
: {
|
||||
...opt,
|
||||
state: selected.has(opt.id)
|
||||
? ("selected" as const)
|
||||
: ("unselected" as const),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function selectedIdsFromOptions(options: ChipOption[]): string[] {
|
||||
return options
|
||||
.filter((o) => o.state === "selected")
|
||||
.map((o) => o.id);
|
||||
}
|
||||
|
||||
function chipOptionsToSnapshotRows(
|
||||
options: ChipOption[],
|
||||
): CommunityStructureChipSnapshotRow[] {
|
||||
return options.map((o) => ({
|
||||
id: o.id,
|
||||
label: o.label,
|
||||
...(o.state !== undefined ? { state: o.state } : {}),
|
||||
}));
|
||||
}
|
||||
|
||||
function snapshotRowsToChipOptions(
|
||||
rows: CommunityStructureChipSnapshotRow[] | undefined,
|
||||
): ChipOption[] | null {
|
||||
if (!Array.isArray(rows) || rows.length === 0) return null;
|
||||
return rows.map((r) => ({
|
||||
id: r.id,
|
||||
label: r.label,
|
||||
...(r.state !== undefined
|
||||
? { state: r.state as ChipOption["state"] }
|
||||
: {}),
|
||||
}));
|
||||
}
|
||||
|
||||
/** Create Custom — Core Values (Figma `20264:68378`). Up to five selections; preset list + custom chips. */
|
||||
export function CoreValuesSelectScreen() {
|
||||
const m = useMessages();
|
||||
const cv = m.create.coreValues;
|
||||
const presets = useMemo(
|
||||
() => normalizeCoreValuePresets(cv.values as CoreValuePresetJson[]),
|
||||
[cv.values],
|
||||
);
|
||||
|
||||
const { markCreateFlowInteraction, updateState, state } = useCreateFlow();
|
||||
|
||||
const [coreValueOptions, setCoreValueOptions] = useState<ChipOption[]>(
|
||||
() => {
|
||||
const fromSnap = snapshotRowsToChipOptions(state.coreValuesChipsSnapshot);
|
||||
if (fromSnap) return fromSnap;
|
||||
return applySavedSelection(
|
||||
chipRowsFromPresets(presets),
|
||||
state.selectedCoreValueIds,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
const [activeModalChipId, setActiveModalChipId] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const [modalSession, setModalSession] = useState<ModalSession | null>(null);
|
||||
const [draftMeaning, setDraftMeaning] = useState("");
|
||||
const [draftSignals, setDraftSignals] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
const fromSnap = snapshotRowsToChipOptions(state.coreValuesChipsSnapshot);
|
||||
if (fromSnap) {
|
||||
setCoreValueOptions(fromSnap);
|
||||
return;
|
||||
}
|
||||
setCoreValueOptions((prev) =>
|
||||
applySavedSelection(prev, state.selectedCoreValueIds),
|
||||
);
|
||||
}, [state.coreValuesChipsSnapshot, state.selectedCoreValueIds]);
|
||||
|
||||
/** Sync chips to create-flow draft. Never call `updateState` from inside a `setCoreValueOptions` updater — defer with `queueMicrotask`. */
|
||||
const syncCoreValuesToDraft = useCallback(
|
||||
(next: ChipOption[]) => {
|
||||
updateState({
|
||||
selectedCoreValueIds: selectedIdsFromOptions(next),
|
||||
coreValuesChipsSnapshot: chipOptionsToSnapshotRows(next),
|
||||
});
|
||||
},
|
||||
[updateState],
|
||||
);
|
||||
|
||||
const persistCoreValues = useCallback(
|
||||
(next: ChipOption[]) => {
|
||||
markCreateFlowInteraction();
|
||||
setCoreValueOptions(next);
|
||||
syncCoreValuesToDraft(next);
|
||||
},
|
||||
[markCreateFlowInteraction, syncCoreValuesToDraft],
|
||||
);
|
||||
|
||||
/** Default meaning/signals from `coreValues.json` `values` for each preset label. */
|
||||
const getPresetTexts = useCallback(
|
||||
(valueLabel: string): { meaning: string; signals: string } => {
|
||||
const row = presets.find((p) => p.label === valueLabel);
|
||||
if (!row) return { meaning: "", signals: "" };
|
||||
return { meaning: row.meaning, signals: row.signals };
|
||||
},
|
||||
[presets],
|
||||
);
|
||||
|
||||
const getInitialTexts = useCallback(
|
||||
(chipId: string, valueLabel: string) => {
|
||||
const saved = state.coreValueDetailsByChipId?.[chipId];
|
||||
const preset = getPresetTexts(valueLabel);
|
||||
return {
|
||||
meaning: saved?.meaning ?? preset.meaning,
|
||||
signals: saved?.signals ?? preset.signals,
|
||||
};
|
||||
},
|
||||
[state.coreValueDetailsByChipId, getPresetTexts],
|
||||
);
|
||||
|
||||
const openModal = useCallback(
|
||||
(chipId: string, session: ModalSession, valueLabel: string) => {
|
||||
const initial = getInitialTexts(chipId, valueLabel);
|
||||
setDraftMeaning(initial.meaning);
|
||||
setDraftSignals(initial.signals);
|
||||
setActiveModalChipId(chipId);
|
||||
setModalSession(session);
|
||||
markCreateFlowInteraction();
|
||||
},
|
||||
[getInitialTexts, markCreateFlowInteraction],
|
||||
);
|
||||
|
||||
const handleModalDismiss = useCallback(() => {
|
||||
if (activeModalChipId && modalSession === "pending") {
|
||||
const next = coreValueOptions.map((opt) =>
|
||||
opt.id === activeModalChipId
|
||||
? { ...opt, state: "unselected" as const }
|
||||
: opt,
|
||||
);
|
||||
persistCoreValues(next);
|
||||
}
|
||||
setActiveModalChipId(null);
|
||||
setModalSession(null);
|
||||
}, [activeModalChipId, modalSession, coreValueOptions, persistCoreValues]);
|
||||
|
||||
const handleModalConfirm = useCallback(() => {
|
||||
if (!activeModalChipId) return;
|
||||
markCreateFlowInteraction();
|
||||
updateState({
|
||||
coreValueDetailsByChipId: {
|
||||
[activeModalChipId]: {
|
||||
meaning: draftMeaning,
|
||||
signals: draftSignals,
|
||||
},
|
||||
},
|
||||
});
|
||||
setActiveModalChipId(null);
|
||||
setModalSession(null);
|
||||
}, [
|
||||
activeModalChipId,
|
||||
draftMeaning,
|
||||
draftSignals,
|
||||
markCreateFlowInteraction,
|
||||
updateState,
|
||||
]);
|
||||
|
||||
const handleChipClick = (chipId: string) => {
|
||||
const target = coreValueOptions.find((o) => o.id === chipId);
|
||||
if (!target || target.state === "custom") return;
|
||||
|
||||
const selectedCount = coreValueOptions.filter(
|
||||
(o) => o.state === "selected",
|
||||
).length;
|
||||
|
||||
if (target.state === "selected") {
|
||||
const next: ChipOption[] = coreValueOptions.map((opt) =>
|
||||
opt.id === chipId
|
||||
? { ...opt, state: "unselected" as const }
|
||||
: opt,
|
||||
);
|
||||
persistCoreValues(next);
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedCount >= MAX_CORE_VALUES) return;
|
||||
|
||||
const next: ChipOption[] = coreValueOptions.map((opt) =>
|
||||
opt.id === chipId
|
||||
? { ...opt, state: "selected" as const }
|
||||
: opt,
|
||||
);
|
||||
persistCoreValues(next);
|
||||
openModal(chipId, "pending", target.label);
|
||||
};
|
||||
|
||||
const addHandlers = {
|
||||
onAddClick: () => {
|
||||
markCreateFlowInteraction();
|
||||
setCoreValueOptions((prev) => {
|
||||
const next: ChipOption[] = [
|
||||
...prev,
|
||||
{ id: crypto.randomUUID(), label: "", state: "custom" },
|
||||
];
|
||||
queueMicrotask(() => syncCoreValuesToDraft(next));
|
||||
return next;
|
||||
});
|
||||
},
|
||||
onCustomChipConfirm: (chipId: string, value: string) => {
|
||||
markCreateFlowInteraction();
|
||||
setCoreValueOptions((prev) => {
|
||||
const withLabel = prev.map((opt) =>
|
||||
opt.id === chipId
|
||||
? { ...opt, label: value, state: "unselected" as const }
|
||||
: opt,
|
||||
);
|
||||
const selectedCount = withLabel.filter(
|
||||
(o) => o.state === "selected",
|
||||
).length;
|
||||
const canSelect = selectedCount < MAX_CORE_VALUES;
|
||||
const next = canSelect
|
||||
? withLabel.map((opt) =>
|
||||
opt.id === chipId
|
||||
? { ...opt, state: "selected" as const }
|
||||
: opt,
|
||||
)
|
||||
: withLabel;
|
||||
|
||||
queueMicrotask(() => {
|
||||
syncCoreValuesToDraft(next);
|
||||
if (canSelect) {
|
||||
openModal(chipId, "pending", value);
|
||||
} else {
|
||||
openModal(chipId, "editing", value);
|
||||
}
|
||||
});
|
||||
return next;
|
||||
});
|
||||
},
|
||||
onCustomChipClose: (chipId: string) => {
|
||||
markCreateFlowInteraction();
|
||||
setCoreValueOptions((prev) => {
|
||||
const next = prev.filter((o) => o.id !== chipId);
|
||||
queueMicrotask(() => syncCoreValuesToDraft(next));
|
||||
return next;
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
const modalChipLabel =
|
||||
coreValueOptions.find((o) => o.id === activeModalChipId)?.label ?? "";
|
||||
|
||||
const description = (
|
||||
<>
|
||||
<span className="leading-[1.3] text-[color:var(--color-content-default-tertiary,#b4b4b4)]">
|
||||
{cv.header.descriptionLead}{" "}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={addHandlers.onAddClick}
|
||||
className="cursor-pointer font-inter font-normal leading-[1.3] text-[color:var(--color-content-default-tertiary,#b4b4b4)] underline decoration-solid underline-offset-[3px] hover:opacity-90"
|
||||
>
|
||||
{cv.header.addLink}
|
||||
</button>
|
||||
<span className="leading-[1.3] text-[color:var(--color-content-default-tertiary,#b4b4b4)]">
|
||||
{" "}
|
||||
{cv.header.descriptionTrail}
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
|
||||
const detailModal = cv.detailModal;
|
||||
|
||||
return (
|
||||
<CreateFlowTwoColumnSelectShell
|
||||
lgVerticalAlign="start"
|
||||
header={
|
||||
<CreateFlowHeaderLockup
|
||||
title={cv.header.title}
|
||||
description={description}
|
||||
justification="left"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<MultiSelect
|
||||
formHeader={false}
|
||||
size="m"
|
||||
options={coreValueOptions}
|
||||
onChipClick={handleChipClick}
|
||||
onAddClick={addHandlers.onAddClick}
|
||||
onCustomChipConfirm={addHandlers.onCustomChipConfirm}
|
||||
onCustomChipClose={addHandlers.onCustomChipClose}
|
||||
addButton
|
||||
addButtonText={cv.multiSelect.addButtonText}
|
||||
/>
|
||||
|
||||
{detailModal && (
|
||||
<Create
|
||||
isOpen={activeModalChipId !== null}
|
||||
onClose={handleModalDismiss}
|
||||
backdropVariant="loginYellow"
|
||||
headerContent={
|
||||
<div className="bg-[var(--color-surface-default-primary)] px-[24px] py-[12px] shrink-0">
|
||||
<ContentLockup
|
||||
title={modalChipLabel}
|
||||
description={detailModal.subtitle}
|
||||
variant="modal"
|
||||
alignment="left"
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
showBackButton={false}
|
||||
showNextButton
|
||||
onNext={handleModalConfirm}
|
||||
nextButtonText={detailModal.addValueButton}
|
||||
ariaLabel={modalChipLabel || "Core value details"}
|
||||
>
|
||||
<div className="flex flex-col gap-[var(--measures-spacing-600,24px)] pb-2">
|
||||
<TextArea
|
||||
label={detailModal.meaningLabel}
|
||||
showHelpIcon
|
||||
appearance="embedded"
|
||||
size="medium"
|
||||
value={draftMeaning}
|
||||
onChange={(e) => setDraftMeaning(e.target.value)}
|
||||
rows={4}
|
||||
/>
|
||||
<TextArea
|
||||
label={detailModal.signalsLabel}
|
||||
showHelpIcon
|
||||
appearance="embedded"
|
||||
size="medium"
|
||||
value={draftSignals}
|
||||
onChange={(e) => setDraftSignals(e.target.value)}
|
||||
rows={4}
|
||||
/>
|
||||
</div>
|
||||
</Create>
|
||||
)}
|
||||
</CreateFlowTwoColumnSelectShell>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user