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 */}
<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"
+10 -4
View File
@@ -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) => {
+19
View File
@@ -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(),
+23
View File
@@ -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", () => {