settings overhaul and reset
This commit is contained in:
parent
8b3aa65e2e
commit
7ef13e56ff
3 changed files with 486 additions and 300 deletions
|
|
@ -2,20 +2,13 @@
|
|||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useForm, useFieldArray, type UseFormRegister } from "react-hook-form";
|
||||
import { useForm, useFieldArray, useWatch, type UseFormRegister } from "react-hook-form";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
/**
|
||||
* From-scratch CO₂ Galvo form that follows the data sheet.
|
||||
* - Prefill works via reset() with raw IDs.
|
||||
* - Submits via /app/api/settings (this file does not assume any old helpers).
|
||||
* - Lens options belong to Rig & Optics (not a separate "Lens Options" section).
|
||||
*/
|
||||
|
||||
type Target = "settings_co2gal";
|
||||
|
||||
type Opt = { id: string; label: string };
|
||||
type Me = { id: string; username?: string; email?: string };
|
||||
type Me = { id: string; username?: string; email?: string } | null;
|
||||
|
||||
const API = (process.env.NEXT_PUBLIC_API_BASE_URL || "").replace(/\/$/, "");
|
||||
|
||||
|
|
@ -40,7 +33,8 @@ type EditInitialValues = {
|
|||
source?: string | null; // submission_id
|
||||
lens?: string | null;
|
||||
focus?: number | null;
|
||||
// CO2 Galvo triplet (Rig & Optics)
|
||||
|
||||
// CO2 Galvo triplet
|
||||
lens_conf?: string | null;
|
||||
lens_apt?: string | null;
|
||||
lens_exp?: string | null;
|
||||
|
|
@ -58,21 +52,39 @@ export default function SettingsSubmit({ mode = "create", submissionId, initialV
|
|||
const router = useRouter();
|
||||
const isEdit = mode === "edit";
|
||||
|
||||
const [me, setMe] = useState<Me | null>(null);
|
||||
const [me, setMe] = useState<Me>(null);
|
||||
const [submitErr, setSubmitErr] = useState<string | null>(null);
|
||||
|
||||
// Robust current-user fetch
|
||||
useEffect(() => {
|
||||
let alive = true;
|
||||
fetch(`/api/me`, { cache: "no-store", credentials: "include" })
|
||||
.then((r) => (r.ok ? r.json() : null))
|
||||
.then((j) => alive && setMe(j || null))
|
||||
.catch(() => alive && setMe(null));
|
||||
return () => {
|
||||
alive = false;
|
||||
};
|
||||
(async () => {
|
||||
try {
|
||||
const r1 = await fetch(`/api/me`, { cache: "no-store", credentials: "include" });
|
||||
if (r1.ok) {
|
||||
const j = await r1.json().catch(() => null);
|
||||
if (alive && j) {
|
||||
const id = j?.id ?? j?.data?.id ?? null;
|
||||
const username = j?.username ?? j?.data?.username ?? j?.name ?? null;
|
||||
const email = j?.email ?? j?.data?.email ?? null;
|
||||
setMe(id ? { id, username: username ?? undefined, email: email ?? undefined } : null);
|
||||
return;
|
||||
}
|
||||
}
|
||||
const r2 = await fetch(`/api/dx/users/me?fields=id,username,email`, { cache: "no-store", credentials: "include" });
|
||||
if (alive && r2.ok) {
|
||||
const j2 = await r2.json().catch(() => null);
|
||||
const d = j2?.data ?? j2 ?? null;
|
||||
setMe(d?.id ? { id: d.id, username: d.username ?? undefined, email: d.email ?? undefined } : null);
|
||||
}
|
||||
} catch {
|
||||
if (alive) setMe(null);
|
||||
}
|
||||
})();
|
||||
return () => { alive = false; };
|
||||
}, []);
|
||||
|
||||
// Options loaders (raw Directus reads)
|
||||
// Options loaders (Directus reads)
|
||||
function useOptions(path: string, includeId?: string | null) {
|
||||
const [opts, setOpts] = useState<Opt[]>([]);
|
||||
useEffect(() => {
|
||||
|
|
@ -92,7 +104,6 @@ export default function SettingsSubmit({ mode = "create", submissionId, initialV
|
|||
url = `${API}/items/laser_software?fields=id,name&limit=1000&sort=name`;
|
||||
} else if (path === "laser_source_co2_galvo") {
|
||||
url = `${API}/items/laser_source?fields=submission_id,make,model,nm&limit=2000&sort=make,model`;
|
||||
// Explicitly reference the global Number to avoid the local <Number/> component shadowing it.
|
||||
type Row = { submission_id?: string | number; make?: string; model?: string; nm?: string | number | null };
|
||||
const toNum = (v: unknown): number | null => {
|
||||
if (typeof v === "number") return globalThis.Number.isFinite(v) ? v : null;
|
||||
|
|
@ -147,9 +158,7 @@ export default function SettingsSubmit({ mode = "create", submissionId, initialV
|
|||
if (live) setOpts(list);
|
||||
})().catch(() => live && setOpts([]));
|
||||
|
||||
return () => {
|
||||
live = false;
|
||||
};
|
||||
return () => { live = false; };
|
||||
}, [path, includeId]);
|
||||
|
||||
return { opts };
|
||||
|
|
@ -163,16 +172,7 @@ export default function SettingsSubmit({ mode = "create", submissionId, initialV
|
|||
];
|
||||
const RASTER_TYPES = FILL_TYPES;
|
||||
const RASTER_DITHER: Opt[] = [
|
||||
"threshold",
|
||||
"ordered",
|
||||
"atkinson",
|
||||
"dither",
|
||||
"stucki",
|
||||
"jarvis",
|
||||
"newsprint",
|
||||
"halftone",
|
||||
"sketch",
|
||||
"grayscale",
|
||||
"threshold", "ordered", "atkinson", "dither", "stucki", "jarvis", "newsprint", "halftone", "sketch", "grayscale",
|
||||
].map((x) => ({ id: x, label: x[0].toUpperCase() + x.slice(1) }));
|
||||
|
||||
// react-hook-form
|
||||
|
|
@ -195,11 +195,12 @@ export default function SettingsSubmit({ mode = "create", submissionId, initialV
|
|||
// Rig & Optics
|
||||
laser_soft: "",
|
||||
source: "",
|
||||
lens: "",
|
||||
focus: "",
|
||||
// keep these blank so Select shows "—"
|
||||
lens_conf: "",
|
||||
lens_apt: "",
|
||||
lens_exp: "",
|
||||
lens: "",
|
||||
focus: "",
|
||||
repeat_all: "",
|
||||
// Repeaters
|
||||
fill_settings: [],
|
||||
|
|
@ -228,11 +229,11 @@ export default function SettingsSubmit({ mode = "create", submissionId, initialV
|
|||
// Rig & Optics
|
||||
laser_soft: initialValues.laser_soft ?? "",
|
||||
source: initialValues.source ?? "",
|
||||
lens: initialValues.lens ?? "",
|
||||
focus: initialValues.focus ?? "",
|
||||
lens_conf: initialValues.lens_conf ?? "",
|
||||
lens_apt: initialValues.lens_apt ?? "",
|
||||
lens_exp: initialValues.lens_exp ?? "",
|
||||
lens: initialValues.lens ?? "",
|
||||
focus: initialValues.focus ?? "",
|
||||
repeat_all: initialValues.repeat_all ?? "",
|
||||
// Repeaters
|
||||
fill_settings: initialValues.fill_settings ?? [],
|
||||
|
|
@ -270,16 +271,16 @@ export default function SettingsSubmit({ mode = "create", submissionId, initialV
|
|||
mat_coat: values.mat_coat || null,
|
||||
mat_color: values.mat_color || null,
|
||||
mat_opacity: values.mat_opacity || null,
|
||||
mat_thickness: values.mat_thickness === "" ? null : Number(values.mat_thickness),
|
||||
mat_thickness: values.mat_thickness === "" ? null : globalThis.Number(values.mat_thickness),
|
||||
// Rig & Optics
|
||||
laser_soft: values.laser_soft || null,
|
||||
source: values.source || null,
|
||||
lens: values.lens || null,
|
||||
focus: values.focus === "" ? null : Number(values.focus),
|
||||
lens_conf: values.lens_conf || null,
|
||||
lens_apt: values.lens_apt || null,
|
||||
lens_exp: values.lens_exp || null,
|
||||
repeat_all: values.repeat_all === "" ? null : Number(values.repeat_all),
|
||||
lens: values.lens || null,
|
||||
focus: values.focus === "" ? null : globalThis.Number(values.focus),
|
||||
repeat_all: values.repeat_all === "" ? null : globalThis.Number(values.repeat_all),
|
||||
// Repeaters (raw pass-through; api will normalize nums/bools)
|
||||
fill_settings: values.fill_settings || [],
|
||||
line_settings: values.line_settings || [],
|
||||
|
|
@ -311,11 +312,13 @@ export default function SettingsSubmit({ mode = "create", submissionId, initialV
|
|||
}
|
||||
};
|
||||
|
||||
const meLabel = me?.username || me?.email || "";
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
<header className="space-y-1">
|
||||
<h1 className="text-xl font-semibold">{isEdit ? "Edit CO₂ Galvo Setting" : "Submit CO₂ Galvo Setting"}</h1>
|
||||
{me ? <p className="text-sm text-muted-foreground">Submitting as {me.username || me.email}</p> : null}
|
||||
{meLabel ? <p className="text-sm text-muted-foreground">Submitting as {meLabel}</p> : null}
|
||||
{submitErr ? <div className="border border-red-500 bg-red-50 text-red-700 rounded px-3 py-2 text-sm">{submitErr}</div> : null}
|
||||
</header>
|
||||
|
||||
|
|
@ -325,7 +328,7 @@ export default function SettingsSubmit({ mode = "create", submissionId, initialV
|
|||
<h2 className="text-lg font-semibold">Info</h2>
|
||||
<div className="grid gap-3">
|
||||
<div>
|
||||
<label className="block text-sm mb-1">Title *</label>
|
||||
<label className="block text-sm mb-1">Title</label>
|
||||
<input className="w-full border rounded px-2 py-1" {...register("setting_title", { required: true })} />
|
||||
</div>
|
||||
<div>
|
||||
|
|
@ -340,7 +343,7 @@ export default function SettingsSubmit({ mode = "create", submissionId, initialV
|
|||
<h2 className="text-lg font-semibold">Images</h2>
|
||||
<div className="grid md:grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="block text-sm mb-1">Result Photo {isEdit || initialValues?.photo ? null : <span className="text-red-600">*</span>}</label>
|
||||
<label className="block text-sm mb-1">Result Photo</label>
|
||||
<input type="file" accept="image/*" onChange={onPick(setPhotoFile)} />
|
||||
</div>
|
||||
<div>
|
||||
|
|
@ -354,28 +357,33 @@ export default function SettingsSubmit({ mode = "create", submissionId, initialV
|
|||
<section className="space-y-3">
|
||||
<h2 className="text-lg font-semibold">Material</h2>
|
||||
<div className="grid md:grid-cols-2 gap-3">
|
||||
<Select label="Material *" {...{ name: "mat", register, options: mats.opts, required: true }} />
|
||||
<Select label="Coating *" {...{ name: "mat_coat", register, options: coats.opts, required: true }} />
|
||||
<Select label="Color *" {...{ name: "mat_color", register, options: colors.opts, required: true }} />
|
||||
<Select label="Opacity *" {...{ name: "mat_opacity", register, options: opacs.opts, required: true }} />
|
||||
<Select label="Material" {...{ name: "mat", register, options: mats.opts, required: true }} />
|
||||
<Select label="Coating" {...{ name: "mat_coat", register, options: coats.opts, required: true }} />
|
||||
<Select label="Color" {...{ name: "mat_color", register, options: colors.opts, required: true }} />
|
||||
<Select label="Opacity" {...{ name: "mat_opacity", register, options: opacs.opts, required: true }} />
|
||||
<Number label="Material Thickness (mm)" name="mat_thickness" register={register} step="0.01" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Rig & Optics (includes lens_conf/apt/exp) */}
|
||||
{/* Rig & Optics (order per spec) */}
|
||||
<section className="space-y-3">
|
||||
<h2 className="text-lg font-semibold">Rig & Optics</h2>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-3">
|
||||
<Select label="Software *" {...{ name: "laser_soft", register, options: soft.opts, required: true }} />
|
||||
<Select label="Laser Source *" {...{ name: "source", register, options: srcs.opts, required: true }} />
|
||||
<Select label="Scan Lens *" {...{ name: "lens", register, options: lens.opts, required: true }} />
|
||||
<Number label="Focus (mm) *" name="focus" register={register} step="1" />
|
||||
<Number label="Repeat All" name="repeat_all" register={register} step="1" />
|
||||
<Select label="Laser Software" {...{ name: "laser_soft", register, options: soft.opts, required: true }} />
|
||||
<Select label="Laser Source" {...{ name: "source", register, options: srcs.opts, required: true }} />
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-3 gap-3">
|
||||
<Select label="Lens Configuration *" {...{ name: "lens_conf", register, options: conf.opts, required: true }} />
|
||||
<Select label="Scan Head Aperture *" {...{ name: "lens_apt", register, options: apt.opts, required: true }} />
|
||||
<Select label="Beam Expander *" {...{ name: "lens_exp", register, options: exp.opts, required: true }} />
|
||||
<Select label="Lens Configuration" {...{ name: "lens_conf", register, options: conf.opts, required: true }} />
|
||||
<Select label="Scan Head Aperture" {...{ name: "lens_apt", register, options: apt.opts, required: true }} />
|
||||
<Select label="Beam Expander" {...{ name: "lens_exp", register, options: exp.opts, required: true }} />
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-3 gap-3">
|
||||
<Select label="Scan Lens" {...{ name: "lens", register, options: lens.opts, required: true }} />
|
||||
<Number label="Focus (mm)" name="focus" register={register} step="1" />
|
||||
<Number label="Repeat All" name="repeat_all" register={register} step="1" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
|
@ -387,7 +395,7 @@ export default function SettingsSubmit({ mode = "create", submissionId, initialV
|
|||
<Repeater
|
||||
title="Fill"
|
||||
fields={fills.fields}
|
||||
onAdd={() => fills.append({ type: "uni" })}
|
||||
onAdd={() => fills.append({ type: "" })} // default to "—"
|
||||
onRemove={(i) => fills.remove(i)}
|
||||
render={(i) => (
|
||||
<div className="grid md:grid-cols-4 gap-2">
|
||||
|
|
@ -438,27 +446,35 @@ export default function SettingsSubmit({ mode = "create", submissionId, initialV
|
|||
<Repeater
|
||||
title="Raster"
|
||||
fields={rasters.fields}
|
||||
onAdd={() => rasters.append({ type: "uni", dither: "threshold" })}
|
||||
onAdd={() => rasters.append({ type: "", dither: "" })} // default to "—"
|
||||
onRemove={(i) => rasters.remove(i)}
|
||||
render={(i) => (
|
||||
<div className="grid md:grid-cols-4 gap-2">
|
||||
<Text label="Name" name={`raster_settings.${i}.name`} register={register} />
|
||||
<Number label="Frequency (kHz)" name={`raster_settings.${i}.frequency`} register={register} step="0.1" />
|
||||
<Number label="Pulse (ns)" name={`raster_settings.${i}.pulse`} register={register} step="0.1" />
|
||||
<Select label="Type" name={`raster_settings.${i}.type`} register={register} options={RASTER_TYPES} />
|
||||
<Select label="Dither" name={`raster_settings.${i}.dither`} register={register} options={RASTER_DITHER} />
|
||||
<Number label="Power (%)" name={`raster_settings.${i}.power`} register={register} step="0.1" />
|
||||
<Number label="Speed (mm/s)" name={`raster_settings.${i}.speed`} register={register} step="0.1" />
|
||||
<Number label="Halftone Cell" name={`raster_settings.${i}.halftone_cell`} register={register} step="1" />
|
||||
<Number label="Halftone Angle" name={`raster_settings.${i}.halftone_angle`} register={register} step="1" />
|
||||
<Number label="Interval (mm)" name={`raster_settings.${i}.interval`} register={register} step="0.001" />
|
||||
<Number label="Dot" name={`raster_settings.${i}.dot`} register={register} step="0.1" />
|
||||
<Number label="Pass" name={`raster_settings.${i}.pass`} register={register} step="1" />
|
||||
<Check label="Cross" name={`raster_settings.${i}.cross`} register={register} />
|
||||
<Check label="Inversion" name={`raster_settings.${i}.inversion`} register={register} />
|
||||
<Check label="Air" name={`raster_settings.${i}.air`} register={register} />
|
||||
</div>
|
||||
)}
|
||||
render={(i) => {
|
||||
const ditherVal = useWatch({ control, name: `raster_settings.${i}.dither` }) || "";
|
||||
const isHalftone = ditherVal === "halftone";
|
||||
return (
|
||||
<div className="grid md:grid-cols-4 gap-2">
|
||||
<Text label="Name" name={`raster_settings.${i}.name`} register={register} />
|
||||
<Number label="Frequency (kHz)" name={`raster_settings.${i}.frequency`} register={register} step="0.1" />
|
||||
<Number label="Pulse (ns)" name={`raster_settings.${i}.pulse`} register={register} step="0.1" />
|
||||
<Select label="Type" name={`raster_settings.${i}.type`} register={register} options={RASTER_TYPES} />
|
||||
<Select label="Dither" name={`raster_settings.${i}.dither`} register={register} options={RASTER_DITHER} />
|
||||
<Number label="Power (%)" name={`raster_settings.${i}.power`} register={register} step="0.1" />
|
||||
<Number label="Speed (mm/s)" name={`raster_settings.${i}.speed`} register={register} step="0.1" />
|
||||
{isHalftone && (
|
||||
<>
|
||||
<Number label="Halftone Cell" name={`raster_settings.${i}.halftone_cell`} register={register} step="1" />
|
||||
<Number label="Halftone Angle" name={`raster_settings.${i}.halftone_angle`} register={register} step="1" />
|
||||
</>
|
||||
)}
|
||||
<Number label="Interval (mm)" name={`raster_settings.${i}.interval`} register={register} step="0.001" />
|
||||
<Number label="Dot" name={`raster_settings.${i}.dot`} register={register} step="0.1" />
|
||||
<Number label="Pass" name={`raster_settings.${i}.pass`} register={register} step="1" />
|
||||
<Check label="Cross" name={`raster_settings.${i}.cross`} register={register} />
|
||||
<Check label="Inversion" name={`raster_settings.${i}.inversion`} register={register} />
|
||||
<Check label="Air" name={`raster_settings.${i}.air`} register={register} />
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</section>
|
||||
|
||||
|
|
@ -486,9 +502,7 @@ function Select({
|
|||
<select className="w-full border rounded px-2 py-1" {...register(name, { required })}>
|
||||
<option value="">—</option>
|
||||
{options.map((o) => (
|
||||
<option key={o.id} value={o.id}>
|
||||
{o.label}
|
||||
</option>
|
||||
<option key={o.id} value={o.id}>{o.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
|
@ -522,16 +536,12 @@ function Repeater({ title, fields, onAdd, onRemove, render }: any) {
|
|||
<fieldset className="border rounded p-3 space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<legend className="font-semibold">{title}</legend>
|
||||
<button type="button" className="px-2 py-1 border rounded" onClick={onAdd}>
|
||||
+ Add
|
||||
</button>
|
||||
<button type="button" className="px-2 py-1 border rounded" onClick={onAdd}>+ Add</button>
|
||||
</div>
|
||||
{fields.map((_: any, i: number) => (
|
||||
<div key={i} className="space-y-2">
|
||||
{render(i)}
|
||||
<button type="button" className="px-2 py-1 border rounded" onClick={() => onRemove(i)}>
|
||||
Remove
|
||||
</button>
|
||||
<button type="button" className="px-2 py-1 border rounded" onClick={() => onRemove(i)}>Remove</button>
|
||||
</div>
|
||||
))}
|
||||
</fieldset>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue