form fixes

This commit is contained in:
makearmy 2025-10-05 08:50:01 -04:00
parent d04613ffdc
commit fd97d67080

View file

@ -93,10 +93,10 @@ type EditInitialValues = {
laser_soft?: any;
repeat_all?: number | null;
// may exist on CO2 targets
lens_conf?: number | null;
lens_apt?: number | null;
lens_exp?: number | null;
// CO2 extras (stored as relations -> ids)
lens_conf?: any;
lens_apt?: any;
lens_exp?: any;
fill_settings?: any[] | null;
line_settings?: any[] | null;
@ -114,6 +114,9 @@ function normalizeForReset(iv: EditInitialValues) {
source: idToString(iv.source),
lens: idToString(iv.lens),
laser_soft: idToString(iv.laser_soft),
lens_conf: idToString(iv.lens_conf),
lens_apt: idToString(iv.lens_apt),
lens_exp: idToString(iv.lens_exp),
// Arrays: coerce dropdown-ish fields to internal enum keys
fill_settings: (iv.fill_settings ?? []).map((r: any) => ({
@ -166,7 +169,6 @@ const DIRECTUS_FIELDS: Record<Target, readonly string[]> = {
"fill_settings",
"line_settings",
"raster_settings",
// extras
"uploader",
"lens_conf",
"lens_apt",
@ -304,7 +306,7 @@ function useOptions(path: string, forceIncludeId?: string, opts?: { disableNmFil
let url = "";
let normalize: (rows: any[]) => Opt[] = (rows) =>
rows.map((r) => ({
id: String(r.id),
id: String(r.id ?? r.submission_id ?? r.value),
label: String(r.name ?? r.label ?? r.title ?? r.value ?? r.id),
}));
@ -335,12 +337,10 @@ function useOptions(path: string, forceIncludeId?: string, opts?: { disableNmFil
}));
};
} else if (rawPath === "lens") {
// CO2 gantry uses focus lenses; all others use scan lenses
if (target === "co2-gantry") {
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) }));
} 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`;
normalize = (rows) => {
const toNum = (v: any) => {
@ -356,8 +356,15 @@ function useOptions(path: string, forceIncludeId?: string, opts?: { disableNmFil
});
};
}
}
// NEW: fixed lists for config / aperture / expander
else if (rawPath === "laser_scan_lens_config") {
url = `${API}/items/laser_scan_lens_config?fields=id,name&limit=1000&sort=name`;
} else if (rawPath === "laser_scan_lens_apt") {
url = `${API}/items/laser_scan_lens_apt?fields=id,name&limit=1000&sort=name`;
} else if (rawPath === "laser_scan_lens_exp") {
url = `${API}/items/laser_scan_lens_exp?fields=id,name&limit=1000&sort=name`;
} else {
// unknown path → empty
setOptsState([]);
setLoading(false);
return;
@ -375,7 +382,6 @@ function useOptions(path: string, forceIncludeId?: string, opts?: { disableNmFil
ensured = [{ id: String(forceIncludeId), label: "(current selection)" }, ...mapped];
}
// client-side text filter
const needle = (q || "").trim().toLowerCase();
const filtered = needle ? ensured.filter((o) => o.label.toLowerCase().includes(needle)) : ensured;
@ -501,9 +507,8 @@ export default function SettingsSubmit(props: CreateProps | EditProps) {
const sp = useSearchParams();
const isEdit = isEditProps(props);
const edit = isEdit ? props : null; // strongly-typed local when edit
const edit = isEdit ? props : null;
// Initialize as CO2-galvo in edit mode (unless explicitly overridden)
const initialFromQuery =
(sp.get("target") as Target) ||
props.initialTarget ||
@ -516,19 +521,13 @@ export default function SettingsSubmit(props: CreateProps | EditProps) {
}
}, [isEdit, props.initialTarget, target]);
// Map collection -> slug used by options selectors
const typeForOptions = useMemo(() => {
switch (target) {
case "settings_fiber":
return "fiber";
case "settings_uv":
return "uv";
case "settings_co2gan":
return "co2-gantry";
case "settings_co2gal":
return "co2-galvo";
default:
return "fiber";
case "settings_fiber": return "fiber";
case "settings_uv": return "uv";
case "settings_co2gan": return "co2-gantry";
case "settings_co2gal": return "co2-galvo";
default: return "fiber";
}
}, [target]);
@ -547,46 +546,39 @@ export default function SettingsSubmit(props: CreateProps | EditProps) {
useEffect(() => {
let alive = true;
// use our bearer-only API
fetch(`/api/me`, { cache: "no-store", credentials: "include" })
.then((r) => (r.ok ? r.json() : Promise.reject(r)))
.then((j) => {
if (!alive) return;
setMe(j || null);
})
.catch(() => {
if (alive) setMeErr("not-signed-in");
});
return () => {
alive = false;
};
.then((j) => { if (!alive) return; setMe(j || null); })
.catch(() => { if (alive) setMeErr("not-signed-in"); });
return () => { alive = false; };
}, []);
const meLabel = me?.username ?? "";
// For edit-mode, compute normalized current values once to seed option lists
// For edit-mode, compute normalized current values once
const current = useMemo(
() => (isEdit && edit?.initialValues ? normalizeForReset(edit.initialValues) : null),
[isEdit, edit?.initialValues]
);
// Options
const mats = useOptions("material", current?.mat || undefined);
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
const srcs = useOptions(`laser_source?target=${typeForOptions}`, current?.source || undefined, {
disableNmFilter: isEdit,
});
const lens = useOptions(`lens?target=${typeForOptions}`, current?.lens || undefined);
const soft = useOptions("laser_software", current?.laser_soft || undefined); // required for ALL targets
const srcs = useOptions(`laser_source?target=${typeForOptions}`, current?.source || undefined, { disableNmFilter: isEdit });
const lens = useOptions(`lens?target=${typeForOptions}`, current?.lens || undefined);
// NEW: fixed-value dropdowns for galvo configs
const lensConf = useOptions("laser_scan_lens_config", current?.lens_conf || undefined);
const lensApt = useOptions("laser_scan_lens_apt", current?.lens_apt || undefined);
const lensExp = useOptions("laser_scan_lens_exp", current?.lens_exp || undefined);
// Repeater choice options (LOCAL now, no network)
const fillType = { opts: toOpts(FILL_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 fillType = { opts: toOpts(FILL_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 {
register,
@ -609,8 +601,8 @@ export default function SettingsSubmit(props: CreateProps | EditProps) {
lens: "",
focus: "",
laser_soft: "",
repeat_all: "", // on all targets
// lens config (may be required on CO2 targets)
repeat_all: "",
// dropdown ids
lens_conf: "",
lens_apt: "",
lens_exp: "",
@ -620,8 +612,8 @@ export default function SettingsSubmit(props: CreateProps | EditProps) {
},
});
const fills = useFieldArray({ control, name: "fill_settings" });
const lines = useFieldArray({ control, name: "line_settings" });
const fills = useFieldArray({ control, name: "fill_settings" });
const lines = useFieldArray({ control, name: "line_settings" });
const rasters = useFieldArray({ control, name: "raster_settings" });
// Prefill the form in edit mode
@ -643,31 +635,23 @@ export default function SettingsSubmit(props: CreateProps | EditProps) {
focus: iv.focus ?? "",
laser_soft: iv.laser_soft ?? "",
repeat_all: iv.repeat_all ?? "",
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 ?? [],
lens_conf: iv.lens_conf ?? "",
lens_apt: iv.lens_apt ?? "",
lens_exp: iv.lens_exp ?? "",
fill_settings: iv.fill_settings ?? [],
line_settings: iv.line_settings ?? [],
raster_settings: iv.raster_settings ?? [],
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isEdit, edit?.initialValues, reset]);
// After reset, force RHF values once (covers early case)
useEffect(() => {
if (!isEdit || !current) return;
const fieldNames = [
"laser_soft",
"mat",
"mat_coat",
"mat_color",
"mat_opacity",
"source",
"lens",
"laser_soft", "mat", "mat_coat", "mat_color", "mat_opacity", "source", "lens",
"lens_conf", "lens_apt", "lens_exp",
] as const;
const values = getValues();
fieldNames.forEach((name) => {
const cur = (current as any)[name];
@ -685,29 +669,11 @@ export default function SettingsSubmit(props: CreateProps | EditProps) {
const cur = (current as any)[name];
if (cur) setValue(name as any, cur, { shouldDirty: false, shouldValidate: false });
};
apply("mat");
apply("mat_coat");
apply("mat_color");
apply("mat_opacity");
apply("laser_soft");
apply("source");
apply("lens");
}, [
isEdit,
current,
setValue,
mats.opts,
coats.opts,
colors.opts,
opacs.opts,
soft.opts,
srcs.opts,
lens.opts,
]);
["mat","mat_coat","mat_color","mat_opacity","laser_soft","source","lens","lens_conf","lens_apt","lens_exp"]
.forEach((n) => apply(n as any));
}, [isEdit, current, setValue, mats.opts, coats.opts, colors.opts, opacs.opts, soft.opts, srcs.opts, lens.opts, lensConf.opts, lensApt.opts, lensExp.opts]);
function num(v: any) {
return v === "" || v == null ? null : Number(v);
}
function num(v: any) { return v === "" || v == null ? null : Number(v); }
const bool = (v: any) => !!v;
// ─────────────────────────────────────────────────────────────
@ -727,9 +693,8 @@ export default function SettingsSubmit(props: CreateProps | EditProps) {
return;
}
// full UI payload (same shape the form uses)
const fullPayload: any = {
target, // kept top-level for route parity
target,
setting_title: values.setting_title,
setting_notes: values.setting_notes || "",
mat: values.mat || null,
@ -740,16 +705,16 @@ export default function SettingsSubmit(props: CreateProps | EditProps) {
source: values.source || null,
lens: values.lens || null,
focus: num(values.focus),
laser_soft: values.laser_soft || null, // all targets
repeat_all: num(values.repeat_all), // all targets
laser_soft: values.laser_soft || null,
repeat_all: num(values.repeat_all),
// uploader: set automatically from owner; include only if present on client
// uploader: include if we have it; server will also set from owner
...(me?.username || me?.email ? { uploader: me?.username ?? me?.email! } : {}),
// lens config (required for CO2 targets per your list)
lens_conf: num(values.lens_conf),
lens_apt: num(values.lens_apt),
lens_exp: num(values.lens_exp),
// CO2 dropdown ids (relations)
lens_conf: values.lens_conf || null,
lens_apt: values.lens_apt || null,
lens_exp: values.lens_exp || null,
fill_settings: (values.fill_settings || []).map((r: any) => ({
name: r.name || "",
@ -801,16 +766,12 @@ export default function SettingsSubmit(props: CreateProps | EditProps) {
})),
};
// Whitelist to match collection fields and drop empty strings
const directusData = toDirectusData(target, fullPayload);
// Early guard for the common required field
if (!directusData.setting_title) {
setSubmitErr("Title is required.");
return;
}
// Build prod-compatible flat payload and ALWAYS send multipart
const base = isEdit && edit?.submissionId
? { target, mode: "edit" as const, submission_id: edit.submissionId }
: { target };
@ -818,13 +779,12 @@ export default function SettingsSubmit(props: CreateProps | EditProps) {
const flatPayload = {
...base,
...directusData,
// Keep top-level setting_title for route validators
setting_title: directusData.setting_title,
};
try {
const form = new FormData();
form.set("payload", JSON.stringify(flatPayload)); // prod-compatible key
form.set("payload", JSON.stringify(flatPayload));
if (photoFile) form.set("photo", photoFile, photoFile.name || "photo");
if (screenFile) form.set("screen", screenFile, screenFile.name || "screen");
@ -840,7 +800,6 @@ export default function SettingsSubmit(props: CreateProps | EditProps) {
throw new Error((data as any)?.error || "Submission failed");
}
// Success
if (!isEdit) {
reset();
setPhotoFile(null);
@ -864,16 +823,12 @@ export default function SettingsSubmit(props: CreateProps | EditProps) {
function onPick(file: File | null, setFile: (f: File | null) => void, setPreview: (s: string) => void) {
setFile(file);
if (!file) {
setPreview("");
return;
}
if (!file) { setPreview(""); return; }
const reader = new FileReader();
reader.onload = () => setPreview(String(reader.result || ""));
reader.readAsDataURL(file);
}
// Convenience strings for “Current:” (edit mode)
const currentPhotoId =
isEdit && typeof edit?.initialValues?.photo === "string" ? (edit!.initialValues.photo as string) : null;
const currentScreenId =
@ -881,7 +836,7 @@ export default function SettingsSubmit(props: CreateProps | EditProps) {
return (
<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>
<label className="block text-sm mb-1">Target</label>
@ -905,7 +860,7 @@ export default function SettingsSubmit(props: CreateProps | EditProps) {
options={soft.opts}
loading={soft.loading}
onQuery={soft.setQ}
required={true}
required
/>
</div>
</div>
@ -953,18 +908,11 @@ export default function SettingsSubmit(props: CreateProps | EditProps) {
type="file"
accept="image/*"
data-role="photo"
// Required when creating OR when editing without an existing photo id (until a new file is chosen)
required={!currentPhotoId && !photoFile}
onChange={(e) => onPick(e.target.files?.[0] ?? null, setPhotoFile, setPhotoPreview)}
/>
<p className="text-xs text-muted-foreground mt-1">
{photoFile ? (
<>
Selected: <span className="font-mono">{photoFile.name}</span>
</>
) : (
"Max 25 MB. JPG/PNG/WebP recommended."
)}
{photoFile ? <>Selected: <span className="font-mono">{photoFile.name}</span></> : "Max 25 MB. JPG/PNG/WebP recommended."}
</p>
{photoPreview ? <img src={photoPreview} alt="Result preview" className="mt-2 rounded border" /> : null}
</div>
@ -984,13 +932,7 @@ export default function SettingsSubmit(props: CreateProps | EditProps) {
onChange={(e) => onPick(e.target.files?.[0] ?? null, setScreenFile, setScreenPreview)}
/>
<p className="text-xs text-muted-foreground mt-1">
{screenFile ? (
<>
Selected: <span className="font-mono">{screenFile.name}</span>
</>
) : (
"Max 25 MB. JPG/PNG/WebP recommended."
)}
{screenFile ? <>Selected: <span className="font-mono">{screenFile.name}</span></> : "Max 25 MB. JPG/PNG/WebP recommended."}
</p>
{screenPreview ? <img src={screenPreview} alt="Settings preview" className="mt-2 rounded border" /> : null}
</div>
@ -1004,60 +946,12 @@ export default function SettingsSubmit(props: CreateProps | EditProps) {
{/* Material / Source / Lens */}
<div className="grid md:grid-cols-2 gap-3">
<FilterableSelect
label="Material"
name="mat"
register={register}
options={mats.opts}
loading={mats.loading}
onQuery={mats.setQ}
required
/>
<FilterableSelect
label="Coating"
name="mat_coat"
register={register}
options={coats.opts}
loading={coats.loading}
onQuery={coats.setQ}
required
/>
<FilterableSelect
label="Color"
name="mat_color"
register={register}
options={colors.opts}
loading={colors.loading}
onQuery={colors.setQ}
required
/>
<FilterableSelect
label="Opacity"
name="mat_opacity"
register={register}
options={opacs.opts}
loading={opacs.loading}
onQuery={opacs.setQ}
required
/>
<FilterableSelect
label="Laser Source"
name="source"
register={register}
options={srcs.opts}
loading={srcs.loading}
onQuery={srcs.setQ}
required
/>
<FilterableSelect
label="Lens"
name="lens"
register={register}
options={lens.opts}
loading={lens.loading}
onQuery={lens.setQ}
required
/>
<FilterableSelect label="Material" name="mat" register={register} options={mats.opts} loading={mats.loading} onQuery={mats.setQ} required />
<FilterableSelect label="Coating" name="mat_coat" register={register} options={coats.opts} loading={coats.loading} onQuery={coats.setQ} required />
<FilterableSelect label="Color" name="mat_color" register={register} options={colors.opts} loading={colors.loading} onQuery={colors.setQ} required />
<FilterableSelect label="Opacity" name="mat_opacity" register={register} options={opacs.opts} loading={opacs.loading} onQuery={opacs.setQ} required />
<FilterableSelect label="Laser Source" name="source" register={register} options={srcs.opts} loading={srcs.loading} onQuery={srcs.setQ} required />
<FilterableSelect label="Lens" name="lens" register={register} options={lens.opts} loading={lens.loading} onQuery={lens.setQ} required />
</div>
{/* Focus, thickness, repeat_all */}
@ -1070,16 +964,40 @@ export default function SettingsSubmit(props: CreateProps | EditProps) {
</p>
</div>
{/* Lens Configuration (required on CO2 targets per your list) */}
{/* Lens Configuration (dropdowns with fixed options) */}
{(target === "settings_co2gan" || target === "settings_co2gal") && (
<fieldset className="border rounded p-3 space-y-2">
<legend className="font-semibold">Lens Configuration</legend>
<legend className="font-semibold">Lens Options</legend>
<div className="grid md:grid-cols-3 gap-3">
<LabeledInput label="Lens Conf" name="lens_conf" type="number" step="1" register={register} required />
<FilterableSelect
label="Lens Configuration"
name="lens_conf"
register={register}
options={lensConf.opts}
loading={lensConf.loading}
onQuery={lensConf.setQ}
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 />
<FilterableSelect
label="Scan Head Aperture"
name="lens_apt"
register={register}
options={lensApt.opts}
loading={lensApt.loading}
onQuery={lensApt.setQ}
required
/>
<FilterableSelect
label="Beam Expander"
name="lens_exp"
register={register}
options={lensExp.opts}
loading={lensExp.loading}
onQuery={lensExp.setQ}
required
/>
</>
)}
</div>
@ -1097,32 +1015,23 @@ export default function SettingsSubmit(props: CreateProps | EditProps) {
{fills.fields.map((f, i) => (
<div key={f.id} className="grid md:grid-cols-4 gap-2">
<LabeledInput label="Name" name={`fill_settings.${i}.name`} register={register} />
<FilterableSelect
label="Type"
name={`fill_settings.${i}.type`}
register={register}
options={fillType.opts}
loading={false}
onQuery={() => {}}
placeholder="Select type"
/>
<FilterableSelect label="Type" name={`fill_settings.${i}.type`} register={register} options={toOpts(FILL_TYPE_OPTIONS)} loading={false} onQuery={() => {}} placeholder="Select type" />
<LabeledInput label="Frequency (kHz)" name={`fill_settings.${i}.frequency`} type="number" step="0.1" register={register} />
<LabeledInput label="Pulse (ns)" name={`fill_settings.${i}.pulse`} type="number" step="0.1" register={register} />
<LabeledInput label="Power (%)" name={`fill_settings.${i}.power`} type="number" step="0.1" register={register} />
<LabeledInput label="Speed (mm/s)" name={`fill_settings.${i}.speed`} type="number" step="0.1" register={register} />
<LabeledInput label="Interval (mm)" name={`fill_settings.${i}.interval`} type="number" step="0.001" register={register} />
<LabeledInput label="Pass" name={`fill_settings.${i}.pass`} type="number" step="1" register={register} />
<LabeledInput label="Angle (°)" name={`fill_settings.${i}.angle`} type="number" step="1" register={register} />
<LabeledInput label="Increment" name={`fill_settings.${i}.increment`} type="number" step="0.001" register={register} />
<LabeledInput label="Pulse (ns)" name={`fill_settings.${i}.pulse`} type="number" step="0.1" register={register} />
<LabeledInput label="Power (%)" name={`fill_settings.${i}.power`} type="number" step="0.1" register={register} />
<LabeledInput label="Speed (mm/s)" name={`fill_settings.${i}.speed`} type="number" step="0.1" register={register} />
<LabeledInput label="Interval (mm)" name={`fill_settings.${i}.interval`} type="number" step="0.001" register={register} />
<LabeledInput label="Pass" name={`fill_settings.${i}.pass`} type="number" step="1" register={register} />
<LabeledInput label="Angle (°)" name={`fill_settings.${i}.angle`} type="number" step="1" register={register} />
<LabeledInput label="Increment" name={`fill_settings.${i}.increment`} type="number" step="0.001" register={register} />
<div className="flex items-center gap-3">
<BoolBox label="Auto" name={`fill_settings.${i}.auto`} register={register} />
<BoolBox label="Auto" name={`fill_settings.${i}.auto`} register={register} />
<BoolBox label="Cross" name={`fill_settings.${i}.cross`} register={register} />
</div>
<div className="flex items-center gap-3">
<BoolBox label="Flood" name={`fill_settings.${i}.flood`} register={register} />
<BoolBox label="Air" name={`fill_settings.${i}.air`} register={register} />
<BoolBox label="Air" name={`fill_settings.${i}.air`} register={register} />
</div>
<button type="button" className="px-2 py-1 border rounded md:col-span-4" onClick={() => fills.remove(i)}>
Remove
</button>
@ -1140,20 +1049,19 @@ export default function SettingsSubmit(props: CreateProps | EditProps) {
</div>
{lines.fields.map((f, i) => (
<div key={f.id} className="grid md:grid-cols-4 gap-2">
<LabeledInput label="Name" name={`line_settings.${i}.name`} register={register} />
<LabeledInput label="Frequency (kHz)" name={`line_settings.${i}.frequency`} type="number" step="0.1" register={register} />
<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} />
<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} />
<BoolBox label="Wobble" name={`line_settings.${i}.wobble`} register={register} />
<BoolBox label="Air" name={`line_settings.${i}.air`} register={register} />
<LabeledInput label="Name" name={`line_settings.${i}.name`} register={register} />
<LabeledInput label="Frequency (kHz)" name={`line_settings.${i}.frequency`} type="number" step="0.1" register={register} />
<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} />
<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} />
<BoolBox label="Wobble" name={`line_settings.${i}.wobble`} register={register} />
<BoolBox label="Air" name={`line_settings.${i}.air`} register={register} />
<button type="button" className="px-2 py-1 border rounded md:col-span-4" onClick={() => lines.remove(i)}>
Remove
</button>
@ -1165,50 +1073,29 @@ export default function SettingsSubmit(props: CreateProps | EditProps) {
<fieldset className="border rounded p-3 space-y-2">
<div className="flex items-center justify-between">
<legend className="font-semibold">Raster Settings</legend>
<button
type="button"
className="px-2 py-1 border rounded"
onClick={() => rasters.append({ type: "uni", dither: "threshold" })}
>
<button type="button" className="px-2 py-1 border rounded" onClick={() => rasters.append({ type: "uni", dither: "threshold" })}>
+ Add
</button>
</div>
{rasters.fields.map((f, i) => (
<div key={f.id} className="grid md:grid-cols-4 gap-2">
<LabeledInput label="Name" name={`raster_settings.${i}.name`} register={register} />
<LabeledInput label="Frequency (kHz)" name={`raster_settings.${i}.frequency`} type="number" step="0.1" register={register} />
<LabeledInput label="Pulse (ns)" name={`raster_settings.${i}.pulse`} type="number" step="0.1" register={register} />
<FilterableSelect
label="Type"
name={`raster_settings.${i}.type`}
register={register}
options={rasterType.opts}
loading={false}
onQuery={() => {}}
placeholder="Select type"
/>
<FilterableSelect
label="Dither"
name={`raster_settings.${i}.dither`}
register={register}
options={rasterDither.opts}
loading={false}
onQuery={() => {}}
placeholder="Select dither"
/>
<LabeledInput label="Power (%)" name={`raster_settings.${i}.power`} type="number" step="0.1" register={register} />
<LabeledInput label="Speed (mm/s)" name={`raster_settings.${i}.speed`} type="number" step="0.1" register={register} />
<LabeledInput label="Halftone Cell" name={`raster_settings.${i}.halftone_cell`} type="number" step="1" register={register} />
<LabeledInput label="Halftone Angle" name={`raster_settings.${i}.halftone_angle`} type="number" step="1" register={register} />
<LabeledInput label="Interval (mm)" name={`raster_settings.${i}.interval`} type="number" step="0.001" register={register} />
<LabeledInput label="Dot" name={`raster_settings.${i}.dot`} type="number" step="0.1" register={register} />
<LabeledInput label="Pass" name={`raster_settings.${i}.pass`} type="number" step="1" register={register} />
<BoolBox label="Cross" name={`raster_settings.${i}.cross`} register={register} />
<LabeledInput label="Name" name={`raster_settings.${i}.name`} register={register} />
<LabeledInput label="Frequency (kHz)" name={`raster_settings.${i}.frequency`} type="number" step="0.1" register={register} />
<LabeledInput label="Pulse (ns)" name={`raster_settings.${i}.pulse`} type="number" step="0.1" register={register} />
<FilterableSelect label="Type" name={`raster_settings.${i}.type`} register={register} options={toOpts(RASTER_TYPE_OPTIONS)} loading={false} onQuery={() => {}} placeholder="Select type" />
<FilterableSelect label="Dither" name={`raster_settings.${i}.dither`} register={register} options={toOpts(RASTER_DITHER_OPTIONS)} loading={false} onQuery={() => {}} placeholder="Select dither" />
<LabeledInput label="Power (%)" name={`raster_settings.${i}.power`} type="number" step="0.1" register={register} />
<LabeledInput label="Speed (mm/s)" name={`raster_settings.${i}.speed`} type="number" step="0.1" register={register} />
<LabeledInput label="Halftone Cell" name={`raster_settings.${i}.halftone_cell`} type="number" step="1" register={register} />
<LabeledInput label="Halftone Angle" name={`raster_settings.${i}.halftone_angle`} type="number" step="1" register={register} />
<LabeledInput label="Interval (mm)" name={`raster_settings.${i}.interval`} type="number" step="0.001" register={register} />
<LabeledInput label="Dot" name={`raster_settings.${i}.dot`} type="number" step="0.1" register={register} />
<LabeledInput label="Pass" name={`raster_settings.${i}.pass`} type="number" step="1" register={register} />
<BoolBox label="Cross" name={`raster_settings.${i}.cross`} register={register} />
<div className="flex items-center gap-3">
<BoolBox label="Inversion" name={`raster_settings.${i}.inversion`} register={register} />
<BoolBox label="Air" name={`raster_settings.${i}.air`} register={register} />
<BoolBox label="Air" name={`raster_settings.${i}.air`} register={register} />
</div>
<button type="button" className="px-2 py-1 border rounded md:col-span-4" onClick={() => rasters.remove(i)}>
Remove
</button>