diff --git a/components/details/CO2GalvoDetail.tsx b/components/details/CO2GalvoDetail.tsx index b7cf3a42..d9926c1a 100644 --- a/components/details/CO2GalvoDetail.tsx +++ b/components/details/CO2GalvoDetail.tsx @@ -3,6 +3,7 @@ import { useEffect, useMemo, useState } from "react"; import { useRouter, useSearchParams } from "next/navigation"; +import SettingsSubmit from "@/components/forms/SettingsSubmit"; type Rec = { submission_id: string | number; @@ -46,12 +47,13 @@ type Rec = { export default function CO2GalvoDetail({ id, editable }: { id: string | number; editable?: boolean }) { const router = useRouter(); const sp = useSearchParams(); + const editMode = sp.get("edit") === "1"; const [rec, setRec] = useState(null); const [loading, setLoading] = useState(true); const [err, setErr] = useState(null); - // current user id to show "Edit" for owners + // me id for owner-only edit const [meId, setMeId] = useState(null); // Lightbox @@ -78,18 +80,13 @@ export default function CO2GalvoDetail({ id, editable }: { id: string | number; (async () => { try { const r = await fetch(`/api/dx/users/me?fields=id`, { cache: "no-store", credentials: "include" }); - if (!r.ok) return; const t = await r.text(); const j = t ? JSON.parse(t) : null; const idVal = j?.data?.id ?? j?.id ?? null; if (alive) setMeId(idVal ? String(idVal) : null); - } catch { - /* ignore */ - } + } catch { /* ignore */ } })(); - return () => { - alive = false; - }; + return () => { alive = false; }; }, []); useEffect(() => { @@ -148,9 +145,9 @@ export default function CO2GalvoDetail({ id, editable }: { id: string | number; "last_modified_date", ].join(","); - const url = `/api/dx/items/settings_co2gal?fields=${encodeURIComponent( - fields - )}&filter[submission_id][_eq]=${encodeURIComponent(String(id))}&limit=1`; + const url = `/api/dx/items/settings_co2gal?fields=${encodeURIComponent(fields)}&filter[submission_id][_eq]=${encodeURIComponent( + String(id) + )}&limit=1`; const r = await fetch(url, { cache: "no-store", credentials: "include" }); const text = await r.text(); @@ -165,9 +162,7 @@ export default function CO2GalvoDetail({ id, editable }: { id: string | number; if (!dead) setLoading(false); } })(); - return () => { - dead = true; - }; + return () => { dead = true; }; }, [id]); if (loading) return

Loading setting…

; @@ -186,42 +181,49 @@ export default function CO2GalvoDetail({ id, editable }: { id: string | number; const softName = typeof rec.laser_soft === "object" ? rec.laser_soft?.name ?? "—" : "—"; const sourceText = - [rec.source?.make, rec.source?.model].filter(Boolean).join(" ") + - (rec.source?.nm ? ` (${rec.source.nm})` : ""); + [rec.source?.make, rec.source?.model].filter(Boolean).join(" ") + (rec.source?.nm ? ` (${rec.source.nm})` : ""); const ownerId = - typeof rec.owner === "object" - ? rec.owner?.id != null - ? String(rec.owner.id) - : null - : rec.owner != null - ? String(rec.owner) - : null; + typeof rec.owner === "object" ? (rec.owner?.id != null ? String(rec.owner.id) : null) : rec.owner != null ? String(rec.owner) : null; const isMine = meId && ownerId ? meId === ownerId : false; - // Small field renderer: label on top, value below (+ optional suffix) - const Field = ({ label, value, suffix }: { label: string; value: any; suffix?: string }) => ( -
-
{label}
-
- {value != null && value !== "" ? ( - <> - {String(value)} - {suffix ? {suffix} : null} - - ) : ( - "—" - )} -
-
- ); + // Small field renderer (label on top, value below). Accepts React nodes. + const Field = ({ label, value, suffix }: { label: string; value: React.ReactNode | string | number | null | undefined; suffix?: string }) => { + const primitive = + typeof value === "string" || typeof value === "number" || typeof value === "boolean"; + const isEmpty = value == null || value === "" || (typeof value === "number" && isNaN(value as number)); + + return ( +
+
{label}
+
+ {isEmpty ? ( + "—" + ) : primitive ? ( + <> + {String(value)} + {suffix ? {suffix} : null} + + ) : ( + // render React node directly (e.g., Notes paragraph) + value + )} +
+
+ ); + }; const openEdit = () => { const q = new URLSearchParams(sp.toString()); q.set("edit", "1"); router.replace(`?${q.toString()}`, { scroll: false }); }; + const closeEdit = () => { + const q = new URLSearchParams(sp.toString()); + q.delete("edit"); + router.replace(`?${q.toString()}`, { scroll: false }); + }; // Pretty labels const TYPE_LABEL: Record = { @@ -231,6 +233,52 @@ export default function CO2GalvoDetail({ id, editable }: { id: string | number; }; const DITHER_LABEL = (v: string | undefined) => (v ? v.charAt(0).toUpperCase() + v.slice(1) : "—"); + // ----- EDIT MODE (restore working behavior) ----- + if (editMode && rec) { + const toId = (v: any) => + v == null ? "" : typeof v === "object" ? (v.id ?? v.submission_id ?? "") : String(v); + + const initialValues = { + submission_id: rec.submission_id, + setting_title: rec.setting_title ?? "", + setting_notes: rec.setting_notes ?? "", + photo: photoId ? String(photoId) : null, + screen: screenId ? String(screenId) : null, + // Material + mat: toId(rec.mat) || "", + mat_coat: toId(rec.mat_coat) || "", + mat_color: toId(rec.mat_color) || "", + mat_opacity: toId(rec.mat_opacity) || "", + mat_thickness: rec.mat_thickness ?? null, + // Rig & Optics + laser_soft: typeof rec.laser_soft === "object" ? String(rec.laser_soft?.id ?? "") : String(rec.laser_soft ?? "") || "", + source: rec.source && typeof rec.source === "object" ? String(rec.source.submission_id ?? "") : String(rec.source ?? "") || "", + lens: toId(rec.lens) || "", + focus: rec.focus ?? null, + // CO2 triplet + lens_conf: toId(rec.lens_conf) || "", + lens_apt: toId(rec.lens_apt) || "", + lens_exp: toId(rec.lens_exp) || "", + repeat_all: rec.repeat_all ?? null, + // Repeaters + fill_settings: rec.fill_settings ?? [], + line_settings: rec.line_settings ?? [], + raster_settings: rec.raster_settings ?? [], + }; + + return ( +
+
+

Edit CO₂ Galvo Setting

+ +
+ +
+ ); + } + return (
{/* Header */} @@ -246,16 +294,20 @@ export default function CO2GalvoDetail({ id, editable }: { id: string | number;
Last modified: {rec.last_modified_date || "—"}
- {/* Info (full width) */} -
+ {/* Top row: Info (left) + Images (right) */} +
+ {/* Info */} +
- {rec.setting_notes ? {rec.setting_notes}

} /> : null} -
+ {rec.setting_notes ? ( + {rec.setting_notes}

} /> + ) : null} +
- {/* Images (full width, click to expand, ~75% smaller) */} + {/* Images (side-by-side thumbnails) */} {(photoSrc || screenSrc) && ( -
+
{photoSrc ? (
Settings Screenshot
) : null} -
+ )} + - {/* Two columns: left Rig & Optics, right Material */} + {/* Two columns below: left Rig & Optics, right Material */}
{/* Rig & Optics */}
@@ -334,7 +387,7 @@ export default function CO2GalvoDetail({ id, editable }: { id: string | number; - + @@ -391,7 +444,7 @@ export default function CO2GalvoDetail({ id, editable }: { id: string | number; - + @@ -410,16 +463,8 @@ export default function CO2GalvoDetail({ id, editable }: { id: string | number; {/* Lightbox */} {viewerSrc && ( -
setViewerSrc(null)} - > - e.stopPropagation()} - /> +
setViewerSrc(null)}> + e.stopPropagation()} />
)}
diff --git a/components/lists/CO2GalvoList.tsx b/components/lists/CO2GalvoList.tsx index 5e76feea..85e13f45 100644 --- a/components/lists/CO2GalvoList.tsx +++ b/components/lists/CO2GalvoList.tsx @@ -51,32 +51,27 @@ export default function CO2GalvoList({ const [rows, setRows] = useState([]); const [loading, setLoading] = useState(true); const [localQuery, setLocalQuery] = useState(queryText ?? ""); - - // id -> username map (fix showing UUIDs) - const [ownerMap, setOwnerMap] = useState>({}); - // current user id for "Edit" visibility const [meId, setMeId] = useState(null); useEffect(() => { if (queryText !== undefined) setLocalQuery(queryText); }, [queryText]); - // Load current user id + // who am I? useEffect(() => { - let alive = true; + let live = true; (async () => { try { - const r = await fetch(`${API}/users/me?fields=id`, { credentials: "include", cache: "no-store" }); - if (!r.ok) return; - const j = await r.json().catch(() => null); - const id = j?.data?.id ?? j?.id ?? null; - if (alive) setMeId(id ? String(id) : null); + const r = await fetch(`/api/dx/users/me?fields=id`, { cache: "no-store", credentials: "include" }); + const j = await readJson(r); + const idVal = j?.data?.id ?? j?.id ?? null; + if (live) setMeId(idVal ? String(idVal) : null); } catch { - /* ignore */ + if (live) setMeId(null); } })(); return () => { - alive = false; + live = false; }; }, []); @@ -113,62 +108,9 @@ export default function CO2GalvoList({ }; }, []); - // Resolve owner usernames when only id/UUID is present - useEffect(() => { - const ids = new Set(); - for (const r of rows) { - const o = r.owner; - if (!o) continue; - if (typeof o === "string" || typeof o === "number") { - const id = String(o); - if (!ownerMap[id]) ids.add(id); - } else { - const id = o.id != null ? String(o.id) : ""; - const hasUsername = !!o.username; - if (id && !hasUsername && !ownerMap[id]) ids.add(id); - } - } - if (!ids.size) return; - - let cancelled = false; - (async () => { - const all = Array.from(ids); - const updates: Record = {}; - const chunkSize = 100; - for (let i = 0; i < all.length; i += chunkSize) { - const slice = all.slice(i, i + chunkSize); - const qs = new URLSearchParams(); - qs.set("fields", "id,username"); - qs.set("limit", String(slice.length)); - qs.set("filter[id][_in]", slice.join(",")); - const url = `${API}/users?${qs.toString()}`; - try { - const r = await fetch(url, { credentials: "include", cache: "no-store" }); - if (!r.ok) continue; - const j = await r.json().catch(() => null); - const arr: Array<{ id: string | number; username?: string | null }> = j?.data || []; - for (const u of arr) { - updates[String(u.id)] = u.username || String(u.id); - } - } catch { - /* ignore */ - } - } - if (!cancelled && Object.keys(updates).length) { - setOwnerMap((prev) => ({ ...prev, ...updates })); - } - })(); - return () => { - cancelled = true; - }; - }, [rows, ownerMap]); - const ownerLabel = (o: Owner) => { if (!o) return "—"; - if (typeof o === "string" || typeof o === "number") { - const id = String(o); - return ownerMap[id] || id; - } + if (typeof o === "string" || typeof o === "number") return String(o); return ( o.username || [o.first_name, o.last_name].filter(Boolean).join(" ").trim() || @@ -177,15 +119,13 @@ export default function CO2GalvoList({ ); }; - const isMine = (o: Owner) => { + const isMine = (o: Owner): boolean => { if (!meId || !o) return false; if (typeof o === "string" || typeof o === "number") return String(o) === meId; if (o.id != null) return String(o.id) === meId; return false; }; - const withEditParam = (href: string) => (href.includes("?") ? `${href}&edit=1` : `${href}?edit=1`); - const filtered = useMemo(() => { const q = (localQuery || "").toLowerCase(); if (!q) return rows; @@ -194,7 +134,9 @@ export default function CO2GalvoList({ .filter(Boolean) .some((v) => String(v).toLowerCase().includes(q)) ); - }, [rows, localQuery, ownerMap]); + }, [rows, localQuery]); + + const addEditParam = (href: string) => (href.includes("?") ? `${href}&edit=1` : `${href}?edit=1`); return (
@@ -226,22 +168,25 @@ export default function CO2GalvoList({ {filtered.map((r) => { - const href = linkFor(r.submission_id); + const mine = isMine(r.owner); + const ownerText = ownerLabel(r.owner) + (mine ? " (you)" : ""); + const viewHref = linkFor(r.submission_id); + const editHref = addEditParam(viewHref); return ( - + {r.setting_title || "Untitled"} - {ownerLabel(r.owner)} + {ownerText} {r.mat?.name || "—"} {r.mat_coat?.name || "—"} {r.source?.model || "—"} {r.lens?.field_size || "—"} - {isMine(r.owner) ? ( - + {mine ? ( + Edit ) : (