From 0e66f2323f741962e773858f5ac4fb38b78d3717 Mon Sep 17 00:00:00 2001 From: makearmy Date: Sat, 4 Oct 2025 14:02:01 -0400 Subject: [PATCH] edit prefill fix for dropdowns --- components/forms/SettingsSubmit.tsx | 167 +++++++++++++++++++++------- 1 file changed, 129 insertions(+), 38 deletions(-) diff --git a/components/forms/SettingsSubmit.tsx b/components/forms/SettingsSubmit.tsx index cff8a6a5..f0f949a4 100644 --- a/components/forms/SettingsSubmit.tsx +++ b/components/forms/SettingsSubmit.tsx @@ -55,8 +55,22 @@ function shortId(s?: string) { } // ───────────────────────────────────────────────────────────── -// Props (Create vs Edit) + type guard +/** Normalizers for edit-mode prefill (IDs + enums) */ // ───────────────────────────────────────────────────────────── +function idToString(v: any): string { + if (v == null || v === "") return ""; + if (typeof v === "object") { + if ((v as any).id != null) return String((v as any).id); + if ((v as any).submission_id != null) return String((v as any).submission_id); + } + return String(v); +} + +function normalizeEnums(value: any, allowed: string[], fallback: string) { + const v = value == null ? "" : String(value).toLowerCase(); + return allowed.includes(v) ? v : fallback; +} + type BaseProps = { initialTarget?: Target }; type EditInitialValues = { @@ -84,6 +98,50 @@ type EditInitialValues = { raster_settings?: any[] | null; }; +function normalizeForReset(iv: EditInitialValues) { + return { + ...iv, + // Single-selects → string IDs + mat: idToString(iv.mat), + mat_coat: idToString(iv.mat_coat), + mat_color: idToString(iv.mat_color), + mat_opacity: idToString(iv.mat_opacity), + source: idToString(iv.source), + lens: idToString(iv.lens), + laser_soft: idToString(iv.laser_soft), + + // Arrays: coerce dropdown-ish fields to internal enum keys + fill_settings: (iv.fill_settings ?? []).map((r: any) => ({ + ...r, + type: normalizeEnums(r?.type, ["uni", "bi", "offset"], "uni"), + })), + raster_settings: (iv.raster_settings ?? []).map((r: any) => ({ + ...r, + type: normalizeEnums(r?.type, ["uni", "bi", "offset"], "uni"), + dither: normalizeEnums( + r?.dither, + [ + "threshold", + "ordered", + "atkinson", + "dither", + "stucki", + "jarvis", + "newsprint", + "halftone", + "sketch", + "grayscale", + ], + "threshold" + ), + })), + line_settings: (iv.line_settings ?? []).map((r: any) => ({ ...r })), + }; +} + +// ───────────────────────────────────────────────────────────── +// Props (Create vs Edit) + type guard +// ───────────────────────────────────────────────────────────── type CreateProps = BaseProps & { mode?: "create"; submissionId?: never; @@ -103,11 +161,13 @@ function isEditProps(p: CreateProps | EditProps): p is EditProps { // ───────────────────────────────────────────────────────────── // Options loader (materials, lenses, etc.) // ───────────────────────────────────────────────────────────── -function useOptions(path: string) { - const [opts, setOpts] = useState([]); +function useOptions(path: string, forceIncludeId?: string, opts?: { disableNmFilter?: boolean }) { + const [optsState, setOptsState] = useState([]); const [loading, setLoading] = useState(false); const [q, setQ] = useState(""); + const disableNmFilter = opts?.disableNmFilter === true; + // helpers for the "laser_source" nm filtering const parseNum = (v: any): number | null => { if (v == null) return null; @@ -153,7 +213,7 @@ function useOptions(path: string) { url = `${API}/items/laser_software?fields=id,name&limit=1000&sort=name`; } else if (rawPath === "laser_source") { url = `${API}/items/laser_source?fields=submission_id,make,model,nm&limit=2000&sort=make,model`; - const range = nmRangeFor(target); + const range = disableNmFilter ? null : nmRangeFor(target); normalize = (rows) => { const filtered = range ? rows.filter((r: any) => { @@ -190,7 +250,7 @@ function useOptions(path: string) { } } else { // unknown path → empty - setOpts([]); + setOptsState([]); setLoading(false); return; } @@ -201,21 +261,27 @@ function useOptions(path: string) { const rows = json?.data ?? []; const mapped = normalize(rows); + // Ensure the currently selected value is present even if filters/pagination miss it + let ensured = mapped; + if (forceIncludeId && !mapped.some((o: any) => String(o.id) === String(forceIncludeId))) { + ensured = [{ id: String(forceIncludeId), label: "(current selection)" }, ...mapped]; + } + // client-side text filter const needle = (q || "").trim().toLowerCase(); - const filtered = needle ? mapped.filter((o) => o.label.toLowerCase().includes(needle)) : mapped; + const filtered = needle ? ensured.filter((o) => o.label.toLowerCase().includes(needle)) : ensured; - if (alive) setOpts(filtered); + if (alive) setOptsState(filtered); })() - .catch(() => alive && setOpts([])) + .catch(() => alive && setOptsState([])) .finally(() => alive && setLoading(false)); return () => { alive = false; }; - }, [path, q]); + }, [path, q, forceIncludeId, disableNmFilter]); - return { opts, loading, setQ }; + return { opts: optsState, loading, setQ }; } // ───────────────────────────────────────────────────────────── @@ -332,9 +398,19 @@ export default function SettingsSubmit(props: CreateProps | EditProps) { const isEdit = isEditProps(props); const edit = isEdit ? props : null; // strongly-typed local when edit - const initialFromQuery = (sp.get("target") as Target) || props.initialTarget || "settings_fiber"; + // Initialize as CO2-galvo in edit mode (unless explicitly overridden) + const initialFromQuery = + (sp.get("target") as Target) || + props.initialTarget || + (isEdit ? "settings_co2gal" : "settings_fiber"); const [target, setTarget] = useState(initialFromQuery); + useEffect(() => { + if (isEdit && props.initialTarget && props.initialTarget !== target) { + setTarget(props.initialTarget); + } + }, [isEdit, props.initialTarget, target]); + // Map collection -> slug used by options selectors const typeForOptions = useMemo(() => { switch (target) { @@ -385,16 +461,24 @@ export default function SettingsSubmit(props: CreateProps | EditProps) { const meLabel = me?.username ?? ""; + // For edit-mode, compute normalized current values once to seed option lists + const current = useMemo( + () => (isEdit && edit?.initialValues ? normalizeForReset(edit.initialValues) : null), + [isEdit, edit?.initialValues] + ); + // Options - const mats = useOptions("material"); - const coats = useOptions("material_coating"); - const colors = useOptions("material_color"); - const opacs = useOptions("material_opacity"); - const soft = useOptions("laser_software"); // required for ALL targets + const mats = useOptions("material", current?.mat || undefined); + const coats = useOptions("material_coating", current?.mat_coat || undefined); + const colors = useOptions("material_color", current?.mat_color || undefined); + const opacs = useOptions("material_opacity", current?.mat_opacity || undefined); + const soft = useOptions("laser_software", current?.laser_soft || undefined); // required for ALL targets // these two need ?target= - const srcs = useOptions(`laser_source?target=${typeForOptions}`); - const lens = useOptions(`lens?target=${typeForOptions}`); + const srcs = useOptions(`laser_source?target=${typeForOptions}`, current?.source || undefined, { + disableNmFilter: isEdit, // show the exact current source even if nm is out-of-range + }); + const lens = useOptions(`lens?target=${typeForOptions}`, current?.lens || undefined); // Repeater choice options (LOCAL now, no network) const fillType = { opts: toOpts(FILL_TYPE_OPTIONS), loading: false, setQ: (_: string) => {} }; @@ -434,24 +518,25 @@ export default function SettingsSubmit(props: CreateProps | EditProps) { // Prefill the form in edit mode useEffect(() => { if (isEdit && edit?.initialValues) { + const iv = normalizeForReset(edit.initialValues); reset({ - setting_title: edit.initialValues.setting_title ?? "", - setting_notes: edit.initialValues.setting_notes ?? "", - photo: edit.initialValues.photo ?? null, - screen: edit.initialValues.screen ?? null, - mat: edit.initialValues.mat ?? "", - mat_coat: edit.initialValues.mat_coat ?? "", - mat_color: edit.initialValues.mat_color ?? "", - mat_opacity: edit.initialValues.mat_opacity ?? "", - mat_thickness: edit.initialValues.mat_thickness ?? "", - source: edit.initialValues.source ?? "", - lens: edit.initialValues.lens ?? "", - focus: edit.initialValues.focus ?? "", - laser_soft: edit.initialValues.laser_soft ?? "", - repeat_all: edit.initialValues.repeat_all ?? "", - fill_settings: edit.initialValues.fill_settings ?? [], - line_settings: edit.initialValues.line_settings ?? [], - raster_settings: edit.initialValues.raster_settings ?? [], + setting_title: iv.setting_title ?? "", + setting_notes: iv.setting_notes ?? "", + photo: iv.photo ?? null, + screen: iv.screen ?? null, + mat: iv.mat ?? "", + mat_coat: iv.mat_coat ?? "", + mat_color: iv.mat_color ?? "", + mat_opacity: iv.mat_opacity ?? "", + mat_thickness: iv.mat_thickness ?? "", + source: iv.source ?? "", + lens: iv.lens ?? "", + focus: iv.focus ?? "", + laser_soft: iv.laser_soft ?? "", + repeat_all: iv.repeat_all ?? "", + fill_settings: iv.fill_settings ?? [], + line_settings: iv.line_settings ?? [], + raster_settings: iv.raster_settings ?? [], }); } }, [isEdit, edit?.initialValues, reset]); @@ -687,7 +772,9 @@ export default function SettingsSubmit(props: CreateProps | EditProps) { />

{photoFile ? ( - <>Selected: {photoFile.name} + <> + Selected: {photoFile.name} + ) : ( "Max 25 MB. JPG/PNG/WebP recommended." )} @@ -711,7 +798,9 @@ export default function SettingsSubmit(props: CreateProps | EditProps) { />

{screenFile ? ( - <>Selected: {screenFile.name} + <> + Selected: {screenFile.name} + ) : ( "Max 25 MB. JPG/PNG/WebP recommended." )} @@ -789,7 +878,9 @@ export default function SettingsSubmit(props: CreateProps | EditProps) { -

0 = in focus. Negative = focus closer. Positive = focus further.

+

+ 0 = in focus. Negative = focus closer. Positive = focus further. +

{/* FILL */}