Align DB with create community stage

This commit is contained in:
adilallo
2026-04-15 21:23:48 -06:00
parent 92149d9fb0
commit b15f0d6226
6 changed files with 170 additions and 24 deletions
@@ -49,8 +49,8 @@ function UploadView({
/> />
)} )}
{/* Upload container */} {/* Upload container — max width per create-flow spec */}
<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"> <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 */} {/* Upload button */}
<button <button
type="button" type="button"
+10 -4
View File
@@ -77,10 +77,16 @@ export function CreateFlowProvider({
}, []); }, []);
const updateState = useCallback((updates: Partial<CreateFlowState>) => { const updateState = useCallback((updates: Partial<CreateFlowState>) => {
setState((prevState) => ({ setState((prevState) => {
...prevState, const merged: CreateFlowState = { ...prevState, ...updates };
...updates, if (updates.communityStructureChipSnapshots !== undefined) {
})); merged.communityStructureChipSnapshots = {
...(prevState.communityStructureChipSnapshots ?? {}),
...updates.communityStructureChipSnapshots,
};
}
return merged;
});
}, []); }, []);
const replaceState = useCallback((next: CreateFlowState) => { 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 type { ChipOption } from "../../../components/controls/MultiSelect/MultiSelect.types";
import { useMessages } from "../../../contexts/MessagesContext"; import { useMessages } from "../../../contexts/MessagesContext";
import { useCreateFlow } from "../../context/CreateFlowContext"; import { useCreateFlow } from "../../context/CreateFlowContext";
import type { CommunityStructureChipSnapshotRow } from "../../types";
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup"; import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
import { CreateFlowStepShell } from "../../components/CreateFlowStepShell"; import { CreateFlowStepShell } from "../../components/CreateFlowStepShell";
import { CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS } from "../../components/createFlowLayoutTokens"; import { CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS } from "../../components/createFlowLayoutTokens";
@@ -79,6 +80,30 @@ function selectedIdsFromOptions(options: ChipOption[]): string[] {
.map((o) => o.id); .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`). */ /** Create Community step 3 — Figma `20094:18244` (responsive grid + column caps via `createFlowLayoutTokens`). */
export function CommunityStructureSelectScreen() { export function CommunityStructureSelectScreen() {
const m = useMessages(); const m = useMessages();
@@ -87,42 +112,84 @@ export function CommunityStructureSelectScreen() {
const [organizationTypeOptions, setOrganizationTypeOptions] = useState< const [organizationTypeOptions, setOrganizationTypeOptions] = useState<
ChipOption[] ChipOption[]
>(() => >(() => {
applySavedSelection( const fromSnap = snapshotRowsToChipOptions(
state.communityStructureChipSnapshots?.organizationTypes,
);
if (fromSnap) return fromSnap;
return applySavedSelection(
chipRowsFromLabels(cs.organizationTypes), chipRowsFromLabels(cs.organizationTypes),
state.selectedOrganizationTypeIds, state.selectedOrganizationTypeIds,
),
); );
});
const [scaleOptions, setScaleOptions] = useState<ChipOption[]>(() => const [scaleOptions, setScaleOptions] = useState<ChipOption[]>(() => {
applySavedSelection( const fromSnap = snapshotRowsToChipOptions(
state.communityStructureChipSnapshots?.scale,
);
if (fromSnap) return fromSnap;
return applySavedSelection(
chipRowsFromLabels(cs.scaleOptions), chipRowsFromLabels(cs.scaleOptions),
state.selectedScaleIds, state.selectedScaleIds,
),
); );
});
const [maturityOptions, setMaturityOptions] = useState<ChipOption[]>(() => const [maturityOptions, setMaturityOptions] = useState<ChipOption[]>(() => {
applySavedSelection( const fromSnap = snapshotRowsToChipOptions(
state.communityStructureChipSnapshots?.maturity,
);
if (fromSnap) return fromSnap;
return applySavedSelection(
chipRowsFromLabels(cs.maturityOptions), chipRowsFromLabels(cs.maturityOptions),
state.selectedMaturityIds, state.selectedMaturityIds,
),
); );
});
useEffect(() => { useEffect(() => {
const fromSnap = snapshotRowsToChipOptions(
state.communityStructureChipSnapshots?.organizationTypes,
);
if (fromSnap) {
setOrganizationTypeOptions(fromSnap);
return;
}
setOrganizationTypeOptions((prev) => setOrganizationTypeOptions((prev) =>
applySavedSelection(prev, state.selectedOrganizationTypeIds), applySavedSelection(prev, state.selectedOrganizationTypeIds),
); );
}, [state.selectedOrganizationTypeIds]); }, [
state.communityStructureChipSnapshots?.organizationTypes,
state.selectedOrganizationTypeIds,
]);
useEffect(() => { useEffect(() => {
const fromSnap = snapshotRowsToChipOptions(
state.communityStructureChipSnapshots?.scale,
);
if (fromSnap) {
setScaleOptions(fromSnap);
return;
}
setScaleOptions((prev) => applySavedSelection(prev, state.selectedScaleIds)); setScaleOptions((prev) => applySavedSelection(prev, state.selectedScaleIds));
}, [state.selectedScaleIds]); }, [
state.communityStructureChipSnapshots?.scale,
state.selectedScaleIds,
]);
useEffect(() => { useEffect(() => {
const fromSnap = snapshotRowsToChipOptions(
state.communityStructureChipSnapshots?.maturity,
);
if (fromSnap) {
setMaturityOptions(fromSnap);
return;
}
setMaturityOptions((prev) => setMaturityOptions((prev) =>
applySavedSelection(prev, state.selectedMaturityIds), applySavedSelection(prev, state.selectedMaturityIds),
); );
}, [state.selectedMaturityIds]); }, [
state.communityStructureChipSnapshots?.maturity,
state.selectedMaturityIds,
]);
const organizationCustomHandlers = useMemo( const organizationCustomHandlers = useMemo(
() => () =>
@@ -155,19 +222,34 @@ export function CommunityStructureSelectScreen() {
const persistOrg = (next: ChipOption[]) => { const persistOrg = (next: ChipOption[]) => {
markCreateFlowInteraction(); markCreateFlowInteraction();
setOrganizationTypeOptions(next); setOrganizationTypeOptions(next);
updateState({ selectedOrganizationTypeIds: selectedIdsFromOptions(next) }); updateState({
selectedOrganizationTypeIds: selectedIdsFromOptions(next),
communityStructureChipSnapshots: {
organizationTypes: chipOptionsToSnapshotRows(next),
},
});
}; };
const persistScale = (next: ChipOption[]) => { const persistScale = (next: ChipOption[]) => {
markCreateFlowInteraction(); markCreateFlowInteraction();
setScaleOptions(next); setScaleOptions(next);
updateState({ selectedScaleIds: selectedIdsFromOptions(next) }); updateState({
selectedScaleIds: selectedIdsFromOptions(next),
communityStructureChipSnapshots: {
scale: chipOptionsToSnapshotRows(next),
},
});
}; };
const persistMaturity = (next: ChipOption[]) => { const persistMaturity = (next: ChipOption[]) => {
markCreateFlowInteraction(); markCreateFlowInteraction();
setMaturityOptions(next); setMaturityOptions(next);
updateState({ selectedMaturityIds: selectedIdsFromOptions(next) }); updateState({
selectedMaturityIds: selectedIdsFromOptions(next),
communityStructureChipSnapshots: {
maturity: chipOptionsToSnapshotRows(next),
},
});
}; };
const handleOrganizationTypeClick = (chipId: string) => { const handleOrganizationTypeClick = (chipId: string) => {
+19
View File
@@ -31,6 +31,16 @@ export type CreateFlowTextStateField =
| "communityContext" | "communityContext"
| "communitySaveEmail"; | "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. * Flow state for inputs across create-flow steps.
* Validated on `PUT /api/drafts/me` via `createFlowStateSchema` (Zod + JSON safety checks). * Validated on `PUT /api/drafts/me` via `createFlowStateSchema` (Zod + JSON safety checks).
@@ -51,6 +61,15 @@ export interface CreateFlowState {
selectedScaleIds?: string[]; selectedScaleIds?: string[];
/** Selected chip ids from `community-structure` (maturity). */ /** Selected chip ids from `community-structure` (maturity). */
selectedMaturityIds?: string[]; 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; currentStep?: CreateFlowStep;
/** Section drafts; structure will tighten as steps persist real shapes. */ /** Section drafts; structure will tighten as steps persist real shapes. */
sections?: Record<string, unknown>[]; sections?: Record<string, unknown>[];
@@ -6,6 +6,20 @@ const flowStepTuple = FLOW_STEP_ORDER as unknown as [string, ...string[]];
const createFlowStepSchema = z.enum(flowStepTuple); 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. * Published rule `document` column: arbitrary JSON object with safety bounds.
*/ */
@@ -35,6 +49,8 @@ export const createFlowStateSchema = z
selectedOrganizationTypeIds: z.array(z.string()).optional(), selectedOrganizationTypeIds: z.array(z.string()).optional(),
selectedScaleIds: z.array(z.string()).optional(), selectedScaleIds: z.array(z.string()).optional(),
selectedMaturityIds: z.array(z.string()).optional(), selectedMaturityIds: z.array(z.string()).optional(),
communityStructureChipSnapshots:
communityStructureChipSnapshotsSchema.optional(),
currentStep: createFlowStepSchema.optional(), currentStep: createFlowStepSchema.optional(),
sections: z.array(z.unknown()).optional(), sections: z.array(z.unknown()).optional(),
stakeholders: z.array(z.unknown()).optional(), stakeholders: z.array(z.unknown()).optional(),
+23
View File
@@ -78,6 +78,29 @@ describe("createFlowStateSchema", () => {
}); });
expect(r.success).toBe(false); 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", () => { describe("putDraftBodySchema", () => {