prefilled settings edits

This commit is contained in:
makearmy 2025-10-03 21:45:57 -04:00
parent 2d0d02765a
commit f0831a6c42

View file

@ -1,3 +1,4 @@
// components/forms/SettingsSubmit.tsx
"use client"; "use client";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
@ -62,8 +63,6 @@ type EditInitialValues = {
setting_title?: string; setting_title?: string;
setting_notes?: string; setting_notes?: string;
// In edit mode these may be an existing Directus file id string.
// (We treat them as string here; if you later pass File objects it still compiles.)
photo?: string | File | { id?: string } | null; photo?: string | File | { id?: string } | null;
screen?: string | File | { id?: string } | null; screen?: string | File | { id?: string } | null;
@ -153,7 +152,6 @@ function useOptions(path: string) {
} else if (rawPath === "laser_software") { } else if (rawPath === "laser_software") {
url = `${API}/items/laser_software?fields=id,name&limit=1000&sort=name`; url = `${API}/items/laser_software?fields=id,name&limit=1000&sort=name`;
} else if (rawPath === "laser_source") { } else if (rawPath === "laser_source") {
// fetch all and client-filter by nm until/if a numeric mirror field exists
url = `${API}/items/laser_source?fields=submission_id,make,model,nm&limit=2000&sort=make,model`; url = `${API}/items/laser_source?fields=submission_id,make,model,nm&limit=2000&sort=make,model`;
const range = nmRangeFor(target); const range = nmRangeFor(target);
normalize = (rows) => { normalize = (rows) => {
@ -169,12 +167,10 @@ function useOptions(path: string) {
})); }));
}; };
} else if (rawPath === "lens") { } else if (rawPath === "lens") {
// CO2 gantry uses focus lenses; all others use scan lenses
if (target === "co2-gantry") { if (target === "co2-gantry") {
url = `${API}/items/laser_focus_lens?fields=id,name&limit=1000`; url = `${API}/items/laser_focus_lens?fields=id,name&limit=1000`;
normalize = (rows) => rows.map((r) => ({ id: String(r.id), label: String(r.name ?? r.id) })); normalize = (rows) => rows.map((r) => ({ id: String(r.id), label: String(r.name ?? r.id) }));
} else { } else {
// SCAN LENSES (fiber, uv, co2-galvo): sort numerically by focal_length
url = `${API}/items/laser_scan_lens?fields=id,field_size,focal_length&limit=1000`; url = `${API}/items/laser_scan_lens?fields=id,field_size,focal_length&limit=1000`;
normalize = (rows) => { normalize = (rows) => {
const toNum = (v: any) => { const toNum = (v: any) => {
@ -191,7 +187,6 @@ function useOptions(path: string) {
}; };
} }
} else { } else {
// unknown path → empty
setOpts([]); setOpts([]);
setLoading(false); setLoading(false);
return; return;
@ -203,7 +198,6 @@ function useOptions(path: string) {
const rows = json?.data ?? []; const rows = json?.data ?? [];
const mapped = normalize(rows); const mapped = normalize(rows);
// client-side text filter
const needle = (q || "").trim().toLowerCase(); const needle = (q || "").trim().toLowerCase();
const filtered = needle ? mapped.filter((o) => o.label.toLowerCase().includes(needle)) : mapped; const filtered = needle ? mapped.filter((o) => o.label.toLowerCase().includes(needle)) : mapped;
@ -295,12 +289,11 @@ export default function SettingsSubmit(props: CreateProps | EditProps) {
const sp = useSearchParams(); const sp = useSearchParams();
const isEdit = isEditProps(props); const isEdit = isEditProps(props);
const edit = isEdit ? props : null; // strongly-typed local when edit const edit = isEdit ? props : null;
const initialFromQuery = (sp.get("target") as Target) || props.initialTarget || "settings_fiber"; const initialFromQuery = (sp.get("target") as Target) || props.initialTarget || "settings_fiber";
const [target, setTarget] = useState<Target>(initialFromQuery); const [target, setTarget] = useState<Target>(initialFromQuery);
// Map collection -> slug used by options selectors
const typeForOptions = useMemo(() => { const typeForOptions = useMemo(() => {
switch (target) { switch (target) {
case "settings_fiber": case "settings_fiber":
@ -332,7 +325,6 @@ export default function SettingsSubmit(props: CreateProps | EditProps) {
useEffect(() => { useEffect(() => {
let alive = true; let alive = true;
// use our bearer-only API
fetch(`/api/me`, { cache: "no-store", credentials: "include" }) fetch(`/api/me`, { cache: "no-store", credentials: "include" })
.then((r) => (r.ok ? r.json() : Promise.reject(r))) .then((r) => (r.ok ? r.json() : Promise.reject(r)))
.then((j) => { .then((j) => {
@ -349,24 +341,19 @@ export default function SettingsSubmit(props: CreateProps | EditProps) {
}, []); }, []);
const meLabel = const meLabel =
me?.display_name?.trim() || me?.username() ||
[me?.first_name, me?.last_name].filter(Boolean).join(" ").trim() ||
me?.username?.trim() ||
me?.email?.trim() ||
(me?.id ? `User ${me.id.slice(0, 8)}${me.id.slice(-4)}` : "Unknown user");
// Options // Options
const mats = useOptions("material"); const mats = useOptions("material");
const coats = useOptions("material_coating"); const coats = useOptions("material_coating");
const colors = useOptions("material_color"); const colors = useOptions("material_color");
const opacs = useOptions("material_opacity"); const opacs = useOptions("material_opacity");
const soft = useOptions("laser_software"); // required for ALL targets const soft = useOptions("laser_software");
// these two need ?target=
const srcs = useOptions(`laser_source?target=${typeForOptions}`); const srcs = useOptions(`laser_source?target=${typeForOptions}`);
const lens = useOptions(`lens?target=${typeForOptions}`); const lens = useOptions(`lens?target=${typeForOptions}`);
// Repeater choice options (LOCAL now, no network) // Repeater choice options (LOCAL)
const fillType = { opts: toOpts(FILL_TYPE_OPTIONS), loading: false, setQ: (_: string) => {} }; const fillType = { opts: toOpts(FILL_TYPE_OPTIONS), loading: false, setQ: (_: string) => {} };
const rasterType = { opts: toOpts(RASTER_TYPE_OPTIONS), loading: false, setQ: (_: string) => {} }; const rasterType = { opts: toOpts(RASTER_TYPE_OPTIONS), loading: false, setQ: (_: string) => {} };
const rasterDither = { opts: toOpts(RASTER_DITHER_OPTIONS), loading: false, setQ: (_: string) => {} }; const rasterDither = { opts: toOpts(RASTER_DITHER_OPTIONS), loading: false, setQ: (_: string) => {} };
@ -390,7 +377,7 @@ export default function SettingsSubmit(props: CreateProps | EditProps) {
lens: "", lens: "",
focus: "", focus: "",
laser_soft: "", laser_soft: "",
repeat_all: "", // on all targets repeat_all: "",
fill_settings: [], fill_settings: [],
line_settings: [], line_settings: [],
raster_settings: [], raster_settings: [],
@ -401,9 +388,31 @@ export default function SettingsSubmit(props: CreateProps | EditProps) {
const lines = useFieldArray({ control, name: "line_settings" }); const lines = useFieldArray({ control, name: "line_settings" });
const rasters = useFieldArray({ control, name: "raster_settings" }); const rasters = useFieldArray({ control, name: "raster_settings" });
// If you later want to prefill the form in edit mode, call reset(...) here // ✅ Prefill when editing
// with string IDs for selects. (The record page currently passes raw objects, useEffect(() => {
// so were not auto-resetting yet to avoid mismatched shapes.) if (!isEdit || !edit?.initialValues) return;
const S = (v: any) => (v == null ? "" : String(v));
reset({
setting_title: edit.initialValues.setting_title ?? "",
setting_notes: edit.initialValues.setting_notes ?? "",
mat: S(edit.initialValues.mat),
mat_coat: S(edit.initialValues.mat_coat),
mat_color: S(edit.initialValues.mat_color),
mat_opacity: S(edit.initialValues.mat_opacity),
mat_thickness: edit.initialValues.mat_thickness ?? "",
source: S(edit.initialValues.source),
lens: S(edit.initialValues.lens),
focus: edit.initialValues.focus ?? "",
laser_soft: S(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 ?? [],
});
// we keep previews empty; “Current: <id>” is shown below file inputs
}, [isEdit, edit?.initialValues, reset]);
function num(v: any) { function num(v: any) {
return v === "" || v == null ? null : Number(v); return v === "" || v == null ? null : Number(v);
@ -413,9 +422,9 @@ export default function SettingsSubmit(props: CreateProps | EditProps) {
async function onSubmit(values: any) { async function onSubmit(values: any) {
setSubmitErr(null); setSubmitErr(null);
// In edit mode, allow keeping the existing photo (no new file) if one exists.
const hasExistingPhotoId = const hasExistingPhotoId =
!!(edit && typeof edit.initialValues?.photo === "string" && edit.initialValues.photo); !!(isEdit && typeof edit?.initialValues?.photo === "string" && edit?.initialValues?.photo);
if (!photoFile && !hasExistingPhotoId) { if (!photoFile && !hasExistingPhotoId) {
(document.querySelector('input[type="file"][data-role="photo"]') as HTMLInputElement | null)?.focus(); (document.querySelector('input[type="file"][data-role="photo"]') as HTMLInputElement | null)?.focus();
return; return;
@ -433,8 +442,8 @@ export default function SettingsSubmit(props: CreateProps | EditProps) {
source: values.source || null, source: values.source || null,
lens: values.lens || null, lens: values.lens || null,
focus: num(values.focus), focus: num(values.focus),
laser_soft: values.laser_soft || null, // all targets laser_soft: values.laser_soft || null,
repeat_all: num(values.repeat_all), // all targets repeat_all: num(values.repeat_all),
fill_settings: (values.fill_settings || []).map((r: any) => ({ fill_settings: (values.fill_settings || []).map((r: any) => ({
name: r.name || "", name: r.name || "",
power: num(r.power), power: num(r.power),
@ -485,11 +494,15 @@ export default function SettingsSubmit(props: CreateProps | EditProps) {
})), })),
}; };
// ✅ Tell the API we're editing and which record to patch
if (isEdit) {
payload.mode = "edit";
payload.submission_id = edit!.submissionId;
}
try { try {
let res: Response; let res: Response;
// In edit mode you may later switch this to a dedicated update endpoint.
// For now we keep the existing create endpoint behavior.
if (photoFile || screenFile) { if (photoFile || screenFile) {
const form = new FormData(); const form = new FormData();
form.set("payload", JSON.stringify(payload)); form.set("payload", JSON.stringify(payload));
@ -511,14 +524,20 @@ export default function SettingsSubmit(props: CreateProps | EditProps) {
throw new Error(data?.error || "Submission failed"); throw new Error(data?.error || "Submission failed");
} }
reset(); // Reset only on create; for edit, keep values visible
setPhotoFile(null); if (!isEdit) {
setScreenFile(null); reset();
setPhotoPreview(""); setPhotoFile(null);
setScreenPreview(""); setScreenFile(null);
setPhotoPreview("");
setScreenPreview("");
}
const id = data?.id ? String(data.id) : ""; const id = data?.id ? String(data.id) : String(edit?.submissionId ?? "");
router.push(`/submit/settings/success?target=${encodeURIComponent(target)}&id=${encodeURIComponent(id)}`); const next = isEdit
? `/submit/settings/success?target=${encodeURIComponent(target)}&id=${encodeURIComponent(id)}`
: `/submit/settings/success?target=${encodeURIComponent(target)}&id=${encodeURIComponent(id)}`;
router.push(next);
} catch (e: any) { } catch (e: any) {
setSubmitErr(e?.message || "Submission failed"); setSubmitErr(e?.message || "Submission failed");
} }
@ -535,15 +554,14 @@ export default function SettingsSubmit(props: CreateProps | EditProps) {
reader.readAsDataURL(file); reader.readAsDataURL(file);
} }
// Convenience strings for “Current:” (edit mode)
const currentPhotoId = const currentPhotoId =
edit && typeof edit.initialValues?.photo === "string" ? edit.initialValues.photo : null; isEdit && typeof edit?.initialValues?.photo === "string" ? edit!.initialValues!.photo : null;
const currentScreenId = const currentScreenId =
edit && typeof edit.initialValues?.screen === "string" ? edit.initialValues.screen : null; isEdit && typeof edit?.initialValues?.screen === "string" ? edit!.initialValues!.screen : null;
return ( return (
<div className="max-w-3xl mx-auto space-y-4"> <div className="max-w-3xl mx-auto space-y-4">
{/* Target + Software (Software required for ALL targets) */} {/* Target + Software */}
<div className="flex flex-wrap gap-3 items-end"> <div className="flex flex-wrap gap-3 items-end">
<div> <div>
<label className="block text-sm mb-1">Target</label> <label className="block text-sm mb-1">Target</label>
@ -615,7 +633,7 @@ export default function SettingsSubmit(props: CreateProps | EditProps) {
type="file" type="file"
accept="image/*" accept="image/*"
data-role="photo" data-role="photo"
required={!currentPhotoId} // in edit mode, only required if there isn't an existing id required={!currentPhotoId}
onChange={(e) => onPick(e.target.files?.[0] ?? null, setPhotoFile, setPhotoPreview)} onChange={(e) => onPick(e.target.files?.[0] ?? null, setPhotoFile, setPhotoPreview)}
/> />
<p className="text-xs text-muted-foreground mt-1"> <p className="text-xs text-muted-foreground mt-1">
@ -758,7 +776,7 @@ export default function SettingsSubmit(props: CreateProps | EditProps) {
<button <button
type="button" type="button"
className="px-2 py-1 border rounded" className="px-2 py-1 border rounded"
onClick={() => fills.append({ type: "uni" })} // default ensures value populated onClick={() => fills.append({ type: "uni" })}
> >
+ Add + Add
</button> </button>
@ -837,7 +855,7 @@ export default function SettingsSubmit(props: CreateProps | EditProps) {
<button <button
type="button" type="button"
className="px-2 py-1 border rounded" className="px-2 py-1 border rounded"
onClick={() => rasters.append({ type: "uni", dither: "threshold" })} // defaults ensure values are set onClick={() => rasters.append({ type: "uni", dither: "threshold" })}
> >
+ Add + Add
</button> </button>