415 lines
12 KiB
TypeScript
415 lines
12 KiB
TypeScript
"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 { useCreateFlowContinueAttempt } from "../../context/CreateFlowContinueAttemptContext";
|
|
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 { id?: string; label: string }[],
|
|
): ChipOption[] {
|
|
return rows.map((row, i) => ({
|
|
id: row.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 withSoleSelection(
|
|
options: ChipOption[],
|
|
selectedId: string | null,
|
|
): ChipOption[] {
|
|
return options.map((opt) =>
|
|
opt.state === "custom"
|
|
? opt
|
|
: {
|
|
...opt,
|
|
state:
|
|
opt.id === selectedId
|
|
? ("selected" as const)
|
|
: ("unselected" as const),
|
|
},
|
|
);
|
|
}
|
|
|
|
function selectedIdsFromOptions(options: ChipOption[]): string[] {
|
|
return options.filter((o) => o.state === "selected").map((o) => o.id);
|
|
}
|
|
|
|
function firstSelectedId(options: ChipOption[]): string | null {
|
|
return selectedIdsFromOptions(options)[0] ?? null;
|
|
}
|
|
|
|
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"] }
|
|
: {}),
|
|
}));
|
|
}
|
|
|
|
function organizationIdsForPersist(
|
|
mainId: string | null,
|
|
otherIds: readonly string[],
|
|
): string[] {
|
|
if (!mainId) return [];
|
|
return [mainId, ...otherIds.filter((id) => id !== mainId)];
|
|
}
|
|
|
|
/** Create Community type step — Figma `25224:105767`. */
|
|
export function CommunityStructureSelectScreen() {
|
|
const m = useMessages();
|
|
const cs = m.create.community.communityStructure;
|
|
const { markCreateFlowInteraction, updateState, state } = useCreateFlow();
|
|
const { attempted } = useCreateFlowContinueAttempt();
|
|
|
|
const savedKindIds = state.selectedOrganizationTypeIds ?? [];
|
|
const savedMainIndex = savedKindIds.findIndex((id) => id.length > 0);
|
|
const savedMainId =
|
|
savedMainIndex >= 0 ? savedKindIds[savedMainIndex]! : null;
|
|
const savedOtherIds =
|
|
savedMainIndex >= 0 ? savedKindIds.slice(savedMainIndex + 1) : [];
|
|
|
|
const [mainKindOptions, setMainKindOptions] = useState<ChipOption[]>(() => {
|
|
const fromSnap = snapshotRowsToChipOptions(
|
|
state.communityStructureChipSnapshots?.organizationTypes,
|
|
);
|
|
return withSoleSelection(
|
|
fromSnap ?? chipRowsFromLabels(cs.organizationTypes),
|
|
savedMainId,
|
|
);
|
|
});
|
|
|
|
const [otherKindOptions, setOtherKindOptions] = useState<ChipOption[]>(() => {
|
|
const fromSnap = snapshotRowsToChipOptions(
|
|
state.communityStructureChipSnapshots?.otherOrganizationTypes,
|
|
);
|
|
if (fromSnap) return fromSnap;
|
|
return applySavedSelection(
|
|
chipRowsFromLabels(cs.organizationTypes),
|
|
savedOtherIds,
|
|
);
|
|
});
|
|
|
|
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,
|
|
);
|
|
const base = fromSnap ?? chipRowsFromLabels(cs.maturityOptions);
|
|
const maturityId =
|
|
(state.selectedMaturityIds ?? []).find((id) => id.length > 0) ?? null;
|
|
return withSoleSelection(base, maturityId);
|
|
});
|
|
|
|
useEffect(() => {
|
|
const fromSnap = snapshotRowsToChipOptions(
|
|
state.communityStructureChipSnapshots?.organizationTypes,
|
|
);
|
|
const mainId =
|
|
(state.selectedOrganizationTypeIds ?? []).find((id) => id.length > 0) ??
|
|
null;
|
|
setMainKindOptions(
|
|
withSoleSelection(
|
|
fromSnap ?? chipRowsFromLabels(cs.organizationTypes),
|
|
mainId,
|
|
),
|
|
);
|
|
}, [
|
|
cs.organizationTypes,
|
|
state.communityStructureChipSnapshots?.organizationTypes,
|
|
state.selectedOrganizationTypeIds,
|
|
]);
|
|
|
|
useEffect(() => {
|
|
const fromSnap = snapshotRowsToChipOptions(
|
|
state.communityStructureChipSnapshots?.otherOrganizationTypes,
|
|
);
|
|
const ids = state.selectedOrganizationTypeIds ?? [];
|
|
const mainId = ids.find((id) => id.length > 0);
|
|
const otherIds = mainId ? ids.slice(ids.indexOf(mainId) + 1) : [];
|
|
if (fromSnap) {
|
|
setOtherKindOptions(fromSnap);
|
|
return;
|
|
}
|
|
setOtherKindOptions((prev) => applySavedSelection(prev, otherIds));
|
|
}, [
|
|
state.communityStructureChipSnapshots?.otherOrganizationTypes,
|
|
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,
|
|
);
|
|
const maturityId =
|
|
(state.selectedMaturityIds ?? []).find((id) => id.length > 0) ?? null;
|
|
if (fromSnap) {
|
|
setMaturityOptions(withSoleSelection(fromSnap, maturityId));
|
|
return;
|
|
}
|
|
setMaturityOptions((prev) => withSoleSelection(prev, maturityId));
|
|
}, [
|
|
state.communityStructureChipSnapshots?.maturity,
|
|
state.selectedMaturityIds,
|
|
]);
|
|
|
|
const mainKindCustomHandlers = useMemo(
|
|
() =>
|
|
createListCustomHandlers(
|
|
setMainKindOptions,
|
|
"unselected",
|
|
markCreateFlowInteraction,
|
|
),
|
|
[markCreateFlowInteraction],
|
|
);
|
|
const otherKindCustomHandlers = useMemo(
|
|
() =>
|
|
createListCustomHandlers(
|
|
setOtherKindOptions,
|
|
"unselected",
|
|
markCreateFlowInteraction,
|
|
),
|
|
[markCreateFlowInteraction],
|
|
);
|
|
|
|
const persistKinds = (main: ChipOption[], others: ChipOption[]) => {
|
|
markCreateFlowInteraction();
|
|
setMainKindOptions(main);
|
|
setOtherKindOptions(others);
|
|
const mainId = firstSelectedId(main);
|
|
updateState({
|
|
selectedOrganizationTypeIds: organizationIdsForPersist(
|
|
mainId,
|
|
selectedIdsFromOptions(others),
|
|
),
|
|
communityStructureChipSnapshots: {
|
|
organizationTypes: chipOptionsToSnapshotRows(main),
|
|
otherOrganizationTypes: chipOptionsToSnapshotRows(others),
|
|
},
|
|
});
|
|
};
|
|
|
|
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 handleMainKindClick = (chipId: string) => {
|
|
const current = firstSelectedId(mainKindOptions);
|
|
const nextId = current === chipId ? null : chipId;
|
|
persistKinds(withSoleSelection(mainKindOptions, nextId), otherKindOptions);
|
|
};
|
|
|
|
const handleOtherKindClick = (chipId: string) => {
|
|
persistKinds(
|
|
mainKindOptions,
|
|
otherKindOptions.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) => {
|
|
const current = firstSelectedId(maturityOptions);
|
|
const nextId = current === chipId ? null : chipId;
|
|
persistMaturity(withSoleSelection(maturityOptions, nextId));
|
|
};
|
|
|
|
const showMainError = attempted && firstSelectedId(mainKindOptions) == null;
|
|
const showMaturityError =
|
|
attempted && firstSelectedId(maturityOptions) == null;
|
|
|
|
const multiSelectBlock = (
|
|
<>
|
|
<MultiSelect
|
|
label={cs.mainKindMultiSelect.label}
|
|
helperText={cs.mainKindMultiSelect.helperText}
|
|
size="s"
|
|
options={mainKindOptions}
|
|
onChipClick={handleMainKindClick}
|
|
{...mainKindCustomHandlers}
|
|
addButton
|
|
errorMessage={showMainError ? cs.pickOneError : undefined}
|
|
/>
|
|
<MultiSelect
|
|
label={cs.otherKindMultiSelect.label}
|
|
size="s"
|
|
options={otherKindOptions}
|
|
onChipClick={handleOtherKindClick}
|
|
{...otherKindCustomHandlers}
|
|
addButton
|
|
/>
|
|
<MultiSelect
|
|
label={cs.scaleMultiSelect.label}
|
|
size="s"
|
|
options={scaleOptions}
|
|
onChipClick={handleScaleClick}
|
|
addButton={false}
|
|
/>
|
|
<MultiSelect
|
|
label={cs.maturityMultiSelect.label}
|
|
size="s"
|
|
options={maturityOptions}
|
|
onChipClick={handleMaturityClick}
|
|
addButton={false}
|
|
errorMessage={showMaturityError ? cs.pickOneError : undefined}
|
|
/>
|
|
</>
|
|
);
|
|
|
|
return (
|
|
<CreateFlowTwoColumnSelectShell
|
|
header={
|
|
<CreateFlowHeaderLockup
|
|
title={cs.header.title}
|
|
description={cs.header.description}
|
|
justification="left"
|
|
/>
|
|
}
|
|
>
|
|
{multiSelectBlock}
|
|
</CreateFlowTwoColumnSelectShell>
|
|
);
|
|
}
|