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