Align DB with create community stage
This commit is contained in:
@@ -49,8 +49,8 @@ function UploadView({
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Upload container */}
|
||||
<div className="bg-[var(--color-surface-default-secondary,#141414)] flex gap-[24px] items-center justify-center px-[var(--measures-spacing-600,24px)] py-[var(--measures-spacing-1200,48px)] rounded-[var(--measures-radius-200,8px)] shrink-0 w-full">
|
||||
{/* Upload container — max width per create-flow spec */}
|
||||
<div className="bg-[var(--color-surface-default-secondary,#141414)] mx-auto flex w-full max-w-[474px] shrink-0 items-center justify-center gap-[24px] rounded-[var(--measures-radius-200,8px)] px-[var(--measures-spacing-600,24px)] py-[var(--measures-spacing-1200,48px)]">
|
||||
{/* Upload button */}
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -77,10 +77,16 @@ export function CreateFlowProvider({
|
||||
}, []);
|
||||
|
||||
const updateState = useCallback((updates: Partial<CreateFlowState>) => {
|
||||
setState((prevState) => ({
|
||||
...prevState,
|
||||
...updates,
|
||||
}));
|
||||
setState((prevState) => {
|
||||
const merged: CreateFlowState = { ...prevState, ...updates };
|
||||
if (updates.communityStructureChipSnapshots !== undefined) {
|
||||
merged.communityStructureChipSnapshots = {
|
||||
...(prevState.communityStructureChipSnapshots ?? {}),
|
||||
...updates.communityStructureChipSnapshots,
|
||||
};
|
||||
}
|
||||
return merged;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const replaceState = useCallback((next: CreateFlowState) => {
|
||||
|
||||
@@ -11,6 +11,7 @@ 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 { CreateFlowStepShell } from "../../components/CreateFlowStepShell";
|
||||
import { CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS } from "../../components/createFlowLayoutTokens";
|
||||
@@ -79,6 +80,30 @@ function selectedIdsFromOptions(options: ChipOption[]): string[] {
|
||||
.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();
|
||||
@@ -87,42 +112,84 @@ export function CommunityStructureSelectScreen() {
|
||||
|
||||
const [organizationTypeOptions, setOrganizationTypeOptions] = useState<
|
||||
ChipOption[]
|
||||
>(() =>
|
||||
applySavedSelection(
|
||||
>(() => {
|
||||
const fromSnap = snapshotRowsToChipOptions(
|
||||
state.communityStructureChipSnapshots?.organizationTypes,
|
||||
);
|
||||
if (fromSnap) return fromSnap;
|
||||
return applySavedSelection(
|
||||
chipRowsFromLabels(cs.organizationTypes),
|
||||
state.selectedOrganizationTypeIds,
|
||||
),
|
||||
);
|
||||
);
|
||||
});
|
||||
|
||||
const [scaleOptions, setScaleOptions] = useState<ChipOption[]>(() =>
|
||||
applySavedSelection(
|
||||
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[]>(() =>
|
||||
applySavedSelection(
|
||||
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.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.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.selectedMaturityIds]);
|
||||
}, [
|
||||
state.communityStructureChipSnapshots?.maturity,
|
||||
state.selectedMaturityIds,
|
||||
]);
|
||||
|
||||
const organizationCustomHandlers = useMemo(
|
||||
() =>
|
||||
@@ -155,19 +222,34 @@ export function CommunityStructureSelectScreen() {
|
||||
const persistOrg = (next: ChipOption[]) => {
|
||||
markCreateFlowInteraction();
|
||||
setOrganizationTypeOptions(next);
|
||||
updateState({ selectedOrganizationTypeIds: selectedIdsFromOptions(next) });
|
||||
updateState({
|
||||
selectedOrganizationTypeIds: selectedIdsFromOptions(next),
|
||||
communityStructureChipSnapshots: {
|
||||
organizationTypes: chipOptionsToSnapshotRows(next),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const persistScale = (next: ChipOption[]) => {
|
||||
markCreateFlowInteraction();
|
||||
setScaleOptions(next);
|
||||
updateState({ selectedScaleIds: selectedIdsFromOptions(next) });
|
||||
updateState({
|
||||
selectedScaleIds: selectedIdsFromOptions(next),
|
||||
communityStructureChipSnapshots: {
|
||||
scale: chipOptionsToSnapshotRows(next),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const persistMaturity = (next: ChipOption[]) => {
|
||||
markCreateFlowInteraction();
|
||||
setMaturityOptions(next);
|
||||
updateState({ selectedMaturityIds: selectedIdsFromOptions(next) });
|
||||
updateState({
|
||||
selectedMaturityIds: selectedIdsFromOptions(next),
|
||||
communityStructureChipSnapshots: {
|
||||
maturity: chipOptionsToSnapshotRows(next),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleOrganizationTypeClick = (chipId: string) => {
|
||||
|
||||
@@ -31,6 +31,16 @@ export type CreateFlowTextStateField =
|
||||
| "communityContext"
|
||||
| "communitySaveEmail";
|
||||
|
||||
/**
|
||||
* Serialized chip row for `community-structure` (preset + custom labels).
|
||||
* Stored in drafts so custom chips survive refresh and server sync.
|
||||
*/
|
||||
export type CommunityStructureChipSnapshotRow = {
|
||||
id: string;
|
||||
label: string;
|
||||
state?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Flow state for inputs across create-flow steps.
|
||||
* Validated on `PUT /api/drafts/me` via `createFlowStateSchema` (Zod + JSON safety checks).
|
||||
@@ -51,6 +61,15 @@ export interface CreateFlowState {
|
||||
selectedScaleIds?: string[];
|
||||
/** Selected chip ids from `community-structure` (maturity). */
|
||||
selectedMaturityIds?: string[];
|
||||
/**
|
||||
* Full chip lists for `community-structure` (needed so custom chips round-trip in drafts).
|
||||
* IDs alone are insufficient because custom rows are not reconstructible from copy JSON.
|
||||
*/
|
||||
communityStructureChipSnapshots?: {
|
||||
organizationTypes?: CommunityStructureChipSnapshotRow[];
|
||||
scale?: CommunityStructureChipSnapshotRow[];
|
||||
maturity?: CommunityStructureChipSnapshotRow[];
|
||||
};
|
||||
currentStep?: CreateFlowStep;
|
||||
/** Section drafts; structure will tighten as steps persist real shapes. */
|
||||
sections?: Record<string, unknown>[];
|
||||
|
||||
@@ -6,6 +6,20 @@ const flowStepTuple = FLOW_STEP_ORDER as unknown as [string, ...string[]];
|
||||
|
||||
const createFlowStepSchema = z.enum(flowStepTuple);
|
||||
|
||||
const communityStructureChipSnapshotRowSchema = z.object({
|
||||
id: z.string().max(200),
|
||||
label: z.string().max(2000),
|
||||
state: z.string().max(32).optional(),
|
||||
});
|
||||
|
||||
const communityStructureChipSnapshotsSchema = z
|
||||
.object({
|
||||
organizationTypes: z.array(communityStructureChipSnapshotRowSchema).optional(),
|
||||
scale: z.array(communityStructureChipSnapshotRowSchema).optional(),
|
||||
maturity: z.array(communityStructureChipSnapshotRowSchema).optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
/**
|
||||
* Published rule `document` column: arbitrary JSON object with safety bounds.
|
||||
*/
|
||||
@@ -35,6 +49,8 @@ export const createFlowStateSchema = z
|
||||
selectedOrganizationTypeIds: z.array(z.string()).optional(),
|
||||
selectedScaleIds: z.array(z.string()).optional(),
|
||||
selectedMaturityIds: z.array(z.string()).optional(),
|
||||
communityStructureChipSnapshots:
|
||||
communityStructureChipSnapshotsSchema.optional(),
|
||||
currentStep: createFlowStepSchema.optional(),
|
||||
sections: z.array(z.unknown()).optional(),
|
||||
stakeholders: z.array(z.unknown()).optional(),
|
||||
|
||||
@@ -78,6 +78,29 @@ describe("createFlowStateSchema", () => {
|
||||
});
|
||||
expect(r.success).toBe(false);
|
||||
});
|
||||
|
||||
it("accepts communityStructureChipSnapshots with custom chip rows", () => {
|
||||
const r = createFlowStateSchema.safeParse({
|
||||
communityStructureChipSnapshots: {
|
||||
organizationTypes: [
|
||||
{ id: "1", label: "Co-op", state: "Selected" },
|
||||
{ id: "custom-uuid", label: "My type", state: "Selected" },
|
||||
],
|
||||
scale: [{ id: "1", label: "Local" }],
|
||||
maturity: [],
|
||||
},
|
||||
});
|
||||
expect(r.success).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects invalid chip snapshot row types", () => {
|
||||
const r = createFlowStateSchema.safeParse({
|
||||
communityStructureChipSnapshots: {
|
||||
organizationTypes: [{ id: "1", label: 123 }],
|
||||
},
|
||||
});
|
||||
expect(r.success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("putDraftBodySchema", () => {
|
||||
|
||||
Reference in New Issue
Block a user