refactor for envelope updates on submissions

This commit is contained in:
makearmy 2025-10-04 23:09:39 -04:00
parent f6a275cbc4
commit 8654653589

View file

@ -93,6 +93,11 @@ type EditInitialValues = {
laser_soft?: any;
repeat_all?: number | null;
// may be present in existing data
lens_conf?: number | null;
lens_apt?: number | null;
lens_exp?: number | null;
fill_settings?: any[] | null;
line_settings?: any[] | null;
raster_settings?: any[] | null;
@ -161,6 +166,11 @@ const DIRECTUS_FIELDS: Record<Target, readonly string[]> = {
"fill_settings",
"line_settings",
"raster_settings",
// extras
"uploader",
"lens_conf",
"lens_apt",
"lens_exp",
],
settings_co2gan: [
"setting_title",
@ -180,6 +190,8 @@ const DIRECTUS_FIELDS: Record<Target, readonly string[]> = {
"fill_settings",
"line_settings",
"raster_settings",
"uploader",
"lens_conf",
],
settings_fiber: [
"setting_title",
@ -199,6 +211,7 @@ const DIRECTUS_FIELDS: Record<Target, readonly string[]> = {
"fill_settings",
"line_settings",
"raster_settings",
"uploader",
],
settings_uv: [
"setting_title",
@ -218,6 +231,7 @@ const DIRECTUS_FIELDS: Record<Target, readonly string[]> = {
"fill_settings",
"line_settings",
"raster_settings",
"uploader",
],
} as const;
@ -423,10 +437,7 @@ function FilterableSelect({
onChange={(e) => setFilter(e.target.value)}
/>
<select className="w-full border rounded px-2 py-1" {...register(name, { required })}>
<option value="">
{placeholder}
{loading ? " (loading…)" : ""}
</option>
<option value="">{placeholder}{loading ? " (loading…)" : ""}</option>
{filtered.map((o) => (
<option key={o.id} value={o.id}>
{o.label}
@ -530,7 +541,7 @@ export default function SettingsSubmit(props: CreateProps | EditProps) {
// UX error for auth/submit
const [submitErr, setSubmitErr] = useState<string | null>(null);
// Current signed-in user (banner only)
// Current signed-in user (banner + uploader)
const [me, setMe] = useState<Me | null>(null);
const [meErr, setMeErr] = useState<string | null>(null);
@ -599,6 +610,11 @@ export default function SettingsSubmit(props: CreateProps | EditProps) {
focus: "",
laser_soft: "",
repeat_all: "", // on all targets
// extras
uploader: "",
lens_conf: "",
lens_apt: "",
lens_exp: "",
fill_settings: [],
line_settings: [],
raster_settings: [],
@ -628,12 +644,17 @@ export default function SettingsSubmit(props: CreateProps | EditProps) {
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 ?? [],
uploader: (me?.username ?? me?.email ?? "") || "",
lens_conf: (iv as any).lens_conf ?? "",
lens_apt: (iv as any).lens_apt ?? "",
lens_exp: (iv as any).lens_exp ?? "",
fill_settings: iv.fill_settings ?? [],
line_settings: iv.line_settings ?? [],
raster_settings: iv.raster_settings ?? [],
});
}
}, [isEdit, edit?.initialValues, reset]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isEdit, edit?.initialValues, reset, me?.username, me?.email]);
// After reset, force RHF values once (covers early case)
useEffect(() => {
@ -692,7 +713,7 @@ export default function SettingsSubmit(props: CreateProps | EditProps) {
const bool = (v: any) => !!v;
// ─────────────────────────────────────────────────────────────
// SUBMIT: Build clean Directus payload + include route metadata
// SUBMIT: Match prod envelope → ALWAYS multipart with "payload"
// ─────────────────────────────────────────────────────────────
async function onSubmit(values: any) {
setSubmitErr(null);
@ -705,13 +726,9 @@ export default function SettingsSubmit(props: CreateProps | EditProps) {
return;
}
// map UI target -> backend slug as helper
const target_slug = typeForOptions;
// full UI payload (same shape the form uses)
const fullPayload: any = {
target,
target_slug,
target, // kept top-level for route parity
setting_title: values.setting_title,
setting_notes: values.setting_notes || "",
mat: values.mat || null,
@ -724,6 +741,13 @@ export default function SettingsSubmit(props: CreateProps | EditProps) {
focus: num(values.focus),
laser_soft: values.laser_soft || null, // all targets
repeat_all: num(values.repeat_all), // all targets
// new/extra fields
uploader: (me?.username ?? me?.email ?? "") || "",
lens_conf: num(values.lens_conf),
lens_apt: num(values.lens_apt),
lens_exp: num(values.lens_exp),
fill_settings: (values.fill_settings || []).map((r: any) => ({
name: r.name || "",
power: num(r.power),
@ -745,8 +769,8 @@ export default function SettingsSubmit(props: CreateProps | EditProps) {
power: num(r.power),
speed: num(r.speed),
perf: bool(r.perf),
cut: r.cut || "",
skip: r.skip || "",
cut: bool(r.cut),
skip: bool(r.skip),
pass: num(r.pass),
air: bool(r.air),
frequency: num(r.frequency),
@ -774,7 +798,7 @@ export default function SettingsSubmit(props: CreateProps | EditProps) {
})),
};
// ✅ Exact Directus data object (whitelisted fields only, empty strings removed)
// Whitelist to match collection fields and drop empty strings
const directusData = toDirectusData(target, fullPayload);
// Early guard for the common required field
@ -783,52 +807,29 @@ export default function SettingsSubmit(props: CreateProps | EditProps) {
return;
}
// Edit meta travels outside `data`
const meta: any = {};
if (isEdit && edit?.submissionId != null) {
meta.mode = "edit";
meta.submission_id = edit.submissionId;
}
// Build prod-compatible flat payload and ALWAYS send multipart
const base = isEdit && edit?.submissionId
? { target, mode: "edit" as const, submission_id: edit.submissionId }
: { target };
const flatPayload = {
...base,
...directusData,
// Keep top-level setting_title for route validators
setting_title: directusData.setting_title,
};
try {
let res: Response;
const form = new FormData();
form.set("payload", JSON.stringify(flatPayload)); // << prod-compatible key
if (photoFile) form.set("photo", photoFile, photoFile.name || "photo");
if (screenFile) form.set("screen", screenFile, screenFile.name || "screen");
if (photoFile || screenFile) {
// multipart: pack everything your route needs under one JSON field
const form = new FormData();
form.set(
"data",
JSON.stringify({
target,
target_slug,
...meta,
data: directusData,
// 🔑 Compat for API route validators that expect top-level title
setting_title: directusData.setting_title,
})
);
// 🔑 Also add a flat field for extreme route handlers that read form fields directly
form.set("setting_title", String(directusData.setting_title || ""));
if (photoFile) form.set("photo", photoFile, photoFile.name || "photo");
if (screenFile) form.set("screen", screenFile, screenFile.name || "screen");
res = await fetch("/api/submit/settings", { method: "POST", body: form, credentials: "include" });
} else {
// JSON parity with multipart
res = await fetch("/api/submit/settings", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
target,
target_slug,
...meta,
data: directusData,
// 🔑 Compat for API route validators
setting_title: directusData.setting_title,
}),
credentials: "include",
});
}
const res = await fetch("/api/submit/settings", {
method: "POST",
body: form,
credentials: "include",
});
const data = await res.json().catch(() => ({}));
if (!res.ok) {
@ -921,6 +922,9 @@ export default function SettingsSubmit(props: CreateProps | EditProps) {
<div className="border border-red-500 text-red-600 bg-red-50 rounded p-2 text-sm">{submitErr}</div>
) : null}
{/* hidden uploader so it's always sent */}
<input type="hidden" {...register("uploader", { required: true })} value={me?.username ?? me?.email ?? ""} />
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
{/* Title */}
<div className="grid md:grid-cols-2 gap-3">
@ -1065,6 +1069,22 @@ export default function SettingsSubmit(props: CreateProps | EditProps) {
</p>
</div>
{/* Lens Configuration (target-specific requireds) */}
{(target === "settings_co2gan" || target === "settings_co2gal") && (
<fieldset className="border rounded p-3 space-y-2">
<legend className="font-semibold">Lens Configuration</legend>
<div className="grid md:grid-cols-3 gap-3">
<LabeledInput label="Lens Conf" name="lens_conf" type="number" step="1" register={register} required />
{target === "settings_co2gal" && (
<>
<LabeledInput label="Lens Aperture" name="lens_apt" type="number" step="1" register={register} required />
<LabeledInput label="Lens Expansion" name="lens_exp" type="number" step="1" register={register} required />
</>
)}
</div>
</fieldset>
)}
{/* FILL */}
<fieldset className="border rounded p-3 space-y-2">
<div className="flex items-center justify-between">
@ -1124,9 +1144,9 @@ export default function SettingsSubmit(props: CreateProps | EditProps) {
<LabeledInput label="Pulse (ns)" name={`line_settings.${i}.pulse`} type="number" step="0.1" register={register} />
<LabeledInput label="Power (%)" name={`line_settings.${i}.power`} type="number" step="0.1" register={register} />
<LabeledInput label="Speed (mm/s)" name={`line_settings.${i}.speed`} type="number" step="0.1" register={register} />
<LabeledInput label="Perf" name={`line_settings.${i}.perf`} register={register} />
<LabeledInput label="Cut" name={`line_settings.${i}.cut`} register={register} />
<LabeledInput label="Skip" name={`line_settings.${i}.skip`} register={register} />
<BoolBox label="Perf" name={`line_settings.${i}.perf`} register={register} />
<BoolBox label="Cut" name={`line_settings.${i}.cut`} register={register} />
<BoolBox label="Skip" name={`line_settings.${i}.skip`} register={register} />
<LabeledInput label="Pass" name={`line_settings.${i}.pass`} type="number" step="1" register={register} />
<LabeledInput label="Step" name={`line_settings.${i}.step`} type="number" step="0.001" register={register} />
<LabeledInput label="Size" name={`line_settings.${i}.size`} type="number" step="0.001" register={register} />