edit prefill fix for dropdowns

This commit is contained in:
makearmy 2025-10-04 14:02:01 -04:00
parent 10745a299d
commit 0e66f2323f

View file

@ -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<Opt[]>([]);
function useOptions(path: string, forceIncludeId?: string, opts?: { disableNmFilter?: boolean }) {
const [optsState, setOptsState] = useState<Opt[]>([]);
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<Target>(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) {
/>
<p className="text-xs text-muted-foreground mt-1">
{photoFile ? (
<>Selected: <span className="font-mono">{photoFile.name}</span></>
<>
Selected: <span className="font-mono">{photoFile.name}</span>
</>
) : (
"Max 25 MB. JPG/PNG/WebP recommended."
)}
@ -711,7 +798,9 @@ export default function SettingsSubmit(props: CreateProps | EditProps) {
/>
<p className="text-xs text-muted-foreground mt-1">
{screenFile ? (
<>Selected: <span className="font-mono">{screenFile.name}</span></>
<>
Selected: <span className="font-mono">{screenFile.name}</span>
</>
) : (
"Max 25 MB. JPG/PNG/WebP recommended."
)}
@ -789,7 +878,9 @@ export default function SettingsSubmit(props: CreateProps | EditProps) {
<LabeledInput label="Material Thickness (mm)" name="mat_thickness" type="number" step="0.01" register={register} />
<LabeledInput label="Focus (mm)" name="focus" type="number" min={-10} max={10} step="1" register={register} required />
<LabeledInput label="Repeat All" name="repeat_all" type="number" step="1" register={register} required />
<p className="text-xs text-muted-foreground md:col-span-3">0 = in focus. Negative = focus closer. Positive = focus further.</p>
<p className="text-xs text-muted-foreground md:col-span-3">
0 = in focus. Negative = focus closer. Positive = focus further.
</p>
</div>
{/* FILL */}