From c59ef98fd9cafe24bbbb75ca5e8184de30ae6122 Mon Sep 17 00:00:00 2001 From: makearmy Date: Sun, 5 Oct 2025 21:52:16 -0400 Subject: [PATCH] list and details cleanup --- components/details/CO2GalvoDetail.tsx | 374 ++++++++++++++------------ components/lists/CO2GalvoList.tsx | 145 +++++----- 2 files changed, 269 insertions(+), 250 deletions(-) diff --git a/components/details/CO2GalvoDetail.tsx b/components/details/CO2GalvoDetail.tsx index 9fa14e77..82cc1eb7 100644 --- a/components/details/CO2GalvoDetail.tsx +++ b/components/details/CO2GalvoDetail.tsx @@ -2,128 +2,133 @@ "use client"; import { useEffect, useMemo, useState } from "react"; -import { useSearchParams, useRouter } from "next/navigation"; -import SettingsSubmit from "@/components/forms/SettingsSubmit"; type Rec = { submission_id: string | number; setting_title?: string | null; setting_notes?: string | null; + photo?: { id?: string } | string | null; screen?: { id?: string } | string | null; + // Material mat?: { id?: string | number; name?: string | null } | null; mat_coat?: { id?: string | number; name?: string | null } | null; mat_color?: { id?: string | number; name?: string | null } | null; mat_opacity?: { id?: string | number; opacity?: string | number | null } | null; mat_thickness?: number | null; + // Rig & Optics + laser_soft?: { id?: string | number; name?: string | null } | string | number | null; source?: { submission_id?: string | number; make?: string | null; model?: string | null; nm?: string | null } | null; lens?: { id?: string | number; field_size?: string | number | null; focal_length?: string | number | null } | null; + focus?: number | null; + // CO₂ Galvo fixed lists lens_conf?: { id?: string | number; name?: string | null } | null; lens_apt?: { id?: string | number; name?: string | null } | null; lens_exp?: { id?: string | number; name?: string | null } | null; - focus?: number | null; - laser_soft?: { id?: string | number; name?: string | null } | string | number | null; repeat_all?: number | null; + // Repeaters fill_settings?: any[] | null; line_settings?: any[] | null; raster_settings?: any[] | null; owner?: { id?: string | number; username?: string | null } | string | number | null; uploader?: string | null; + last_modified_date?: string | null; }; -type Me = { id: string | number; username?: string; email?: string } | null; - -const API_BASE = (process.env.NEXT_PUBLIC_API_BASE_URL || "").replace(/\/$/, ""); -const fileUrl = (id?: string) => (id ? (API_BASE ? `${API_BASE}/assets/${id}` : `/api/dx/assets/${id}`) : ""); - -async function readJson(res: Response) { - const t = await res.text(); - try { return t ? JSON.parse(t) : null; } catch { return null; } -} - export default function CO2GalvoDetail({ id, editable }: { id: string | number; editable?: boolean }) { - const sp = useSearchParams(); - const router = useRouter(); - const editParam = sp.get("edit") === "1"; - const [rec, setRec] = useState(null); const [loading, setLoading] = useState(true); const [err, setErr] = useState(null); - const [me, setMe] = useState(null); - - // robust me + // Lightbox + const [viewerSrc, setViewerSrc] = useState(null); useEffect(() => { - let live = true; - (async () => { - try { - const r1 = await fetch(`/api/me`, { credentials: "include", cache: "no-store" }); - if (r1.ok) { - const j = await readJson(r1); - const id = j?.id ?? j?.data?.id ?? null; - const username = j?.username ?? j?.data?.username ?? null; - const email = j?.email ?? j?.data?.email ?? null; - if (live && id) { setMe({ id, username: username ?? undefined, email: email ?? undefined }); return; } - } - const r2 = await fetch(`/api/dx/users/me?fields=id,username,email`, { credentials: "include", cache: "no-store" }); - if (live && r2.ok) { - const j2 = await readJson(r2); - const d = j2?.data ?? j2 ?? null; - setMe(d?.id ? { id: d.id, username: d.username ?? undefined, email: d.email ?? undefined } : null); - } - } catch { if (live) setMe(null); } - })(); - return () => { live = false; }; - }, []); + const onKey = (e: KeyboardEvent) => e.key === "Escape" && setViewerSrc(null); + if (viewerSrc) window.addEventListener("keydown", onKey); + return () => window.removeEventListener("keydown", onKey); + }, [viewerSrc]); + + const API_BASE = (process.env.NEXT_PUBLIC_API_BASE_URL || "").replace(/\/$/, ""); + const fileUrl = (assetId?: string) => (assetId ? (API_BASE ? `${API_BASE}/assets/${assetId}` : `/api/dx/assets/${assetId}`) : ""); + + function ownerLabel(o: Rec["owner"]) { + if (!o) return "—"; + if (typeof o === "string" || typeof o === "number") return String(o); + return o.username || String(o.id ?? "—"); + } + const yesNo = (v: any) => (v ? "Yes" : "No"); - // load record (with readable fields) useEffect(() => { if (!id) return; let dead = false; - (async () => { try { setLoading(true); setErr(null); - const fields = [ "submission_id", "setting_title", "setting_notes", "photo.id", "screen.id", - "mat.id","mat.name", - "mat_coat.id","mat_coat.name", - "mat_color.id","mat_color.name", - "mat_opacity.id","mat_opacity.opacity", + + // Material + "mat.id", + "mat.name", + "mat_coat.id", + "mat_coat.name", + "mat_color.id", + "mat_color.name", + "mat_opacity.id", + "mat_opacity.opacity", "mat_thickness", - "source.submission_id","source.make","source.model","source.nm", - "laser_soft.id","laser_soft.name", - "lens_conf.id","lens_conf.name", - "lens_apt.id","lens_apt.name", - "lens_exp.id","lens_exp.name", - "lens.id","lens.field_size","lens.focal_length", - "focus","repeat_all", - "fill_settings","line_settings","raster_settings", - "owner.id","owner.username", + + // Rig & Optics + "laser_soft.id", + "laser_soft.name", + "source.submission_id", + "source.make", + "source.model", + "source.nm", + "lens.id", + "lens.field_size", + "lens.focal_length", + "lens_conf.id", + "lens_conf.name", + "lens_apt.id", + "lens_apt.name", + "lens_exp.id", + "lens_exp.name", + "focus", + "repeat_all", + + // Repeaters + "fill_settings", + "line_settings", + "raster_settings", + + // Meta + "owner.id", + "owner.username", "uploader", "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" }); - if (!r.ok) { - const j = await readJson(r); - throw new Error(j?.errors?.[0]?.message || `HTTP ${r.status}`); - } - const j = await readJson(r); + const text = await r.text(); + const j = text ? JSON.parse(text) : null; + if (!r.ok) throw new Error(j?.errors?.[0]?.message || `HTTP ${r.status}`); const row: Rec | null = Array.isArray(j?.data) ? j.data[0] || null : null; if (!row) throw new Error("Setting not found."); if (!dead) setRec(row); @@ -133,155 +138,121 @@ export default function CO2GalvoDetail({ id, editable }: { id: string | number; if (!dead) setLoading(false); } })(); - - return () => { dead = true; }; + return () => { + dead = true; + }; }, [id]); - const ownerId = useMemo(() => { - if (!rec?.owner) return null; - return typeof rec.owner === "object" ? (rec.owner.id != null ? String(rec.owner.id) : null) : String(rec.owner); - }, [rec]); - const isMine = me?.id != null && ownerId != null && String(me.id) === String(ownerId); - - // Edit-mode initialValues shape for SettingsSubmit - const initialValues = useMemo(() => { - if (!rec) return null; - const toId = (v: any) => (v == null ? "" : typeof v === "object" ? (v.id ?? v.submission_id ?? "") : String(v)); - const photoId = typeof rec.photo === "string" || typeof rec.photo === "number" ? String(rec.photo) : rec.photo?.id ?? null; - const screenId = typeof rec.screen === "string" || typeof rec.screen === "number" ? String(rec.screen) : rec.screen?.id ?? null; - - return { - submission_id: rec.submission_id, - setting_title: rec.setting_title ?? "", - setting_notes: rec.setting_notes ?? "", - photo: photoId, - screen: screenId, - // Material - mat: toId(rec.mat) || null, - mat_coat: toId(rec.mat_coat) || null, - mat_color: toId(rec.mat_color) || null, - mat_opacity: toId(rec.mat_opacity) || null, - mat_thickness: rec.mat_thickness ?? null, - // Rig & Optics - laser_soft: typeof rec.laser_soft === "object" ? String(rec.laser_soft?.id ?? "") : (rec.laser_soft != null ? String(rec.laser_soft) : null), - source: rec.source && typeof rec.source === "object" ? (rec.source.submission_id != null ? String(rec.source.submission_id) : null) : (rec.source as any) ?? null, - lens_conf: toId(rec.lens_conf) || null, - lens_apt: toId(rec.lens_apt) || null, - lens_exp: toId(rec.lens_exp) || null, - lens: toId(rec.lens) || null, - focus: rec.focus ?? null, - repeat_all: rec.repeat_all ?? null, - // Repeaters - fill_settings: rec.fill_settings ?? [], - line_settings: rec.line_settings ?? [], - raster_settings: rec.raster_settings ?? [], - }; - }, [rec]); - - function setEdit(on: boolean) { - const q = new URLSearchParams(sp.toString()); - if (on) q.set("edit", "1"); else q.delete("edit"); - router.replace(`?${q.toString()}`, { scroll: false }); - } - if (loading) return

Loading setting…

; - if (err) return
{err}
; + if (err) + return ( +
+
{err}
+
+ ); if (!rec) return

Setting not found.

; - // EDIT MODE - if (editable && editParam && initialValues) { - return ( -
-
-

Edit CO₂ Galvo Setting

- -
- -
- ); - } - - // VIEW MODE (sections + order mirrors the form) const photoId = typeof rec.photo === "object" ? rec.photo?.id : (rec.photo as any); const screenId = typeof rec.screen === "object" ? rec.screen?.id : (rec.screen as any); - const photoSrc = photoId ? fileUrl(String(photoId)) : ""; - const screenSrc = screenId ? fileUrl(String(screenId)) : ""; - const softName = typeof rec.laser_soft === "object" ? (rec.laser_soft?.name ?? "—") : "—"; + const photoSrc = fileUrl(photoId ? String(photoId) : ""); + const screenSrc = fileUrl(screenId ? String(screenId) : ""); + + 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})` : ""); + + // Small field renderer: label on top, value below + const Field = ({ label, value }: { label: string; value: any }) => ( +
+
{label}
+
{value ?? "—"}
+
+ ); return (
+ {/* Header */}
-

{rec.setting_title || "Untitled"}

- {editable && isMine ? ( - - ) : null} -
Last modified: {rec.last_modified_date || "—"}
- {/* Info */} -
-

Info

- {rec.setting_notes ?

{rec.setting_notes}

:

No notes.

} + {/* Info (full width) */} +
+ + + {rec.setting_notes ? {rec.setting_notes}

} /> : null}
- {/* Images */} + {/* Images (full width, click to expand) */} {(photoSrc || screenSrc) && ( -
-

Images

-
+
{photoSrc ? (
- Result +
setViewerSrc(photoSrc)} + > + Result +
Result
) : null} {screenSrc ? (
- Settings Screenshot +
setViewerSrc(screenSrc)} + > + Settings Screenshot +
Settings Screenshot
) : null} -
)} + {/* Two columns: left Rig & Optics, right Material */} +
+ {/* Rig & Optics */} +
+

Rig & Optics

+
+ + + + + + + + +
+
+ {/* Material */} -
+

Material

-
-
Material: {rec.mat?.name || "—"}
-
Coating: {rec.mat_coat?.name || "—"}
-
Color: {rec.mat_color?.name || "—"}
-
Opacity: {rec.mat_opacity?.opacity ?? "—"}
-
Material Thickness (mm): {rec.mat_thickness ?? "—"}
+
+ + + + + +
- {/* Rig & Optics (ordered) */} -
-

Rig & Optics

-
-
Laser Software: {softName}
-
- Laser Source:{" "} - {[rec.source?.make, rec.source?.model].filter(Boolean).join(" ") || "—"} - {rec.source?.nm ? ` (${rec.source.nm})` : ""} -
-
Lens Configuration: {rec.lens_conf?.name ?? "—"}
-
Scan Head Aperture: {rec.lens_apt?.name ?? "—"}
-
Beam Expander: {rec.lens_exp?.name ?? "—"}
-
- Scan Lens:{" "} - {rec.lens?.field_size || "—"}{rec.lens?.focal_length ? ` / ${rec.lens.focal_length} mm` : ""} -
-
Focus (mm): {rec.focus ?? "—"}
-
Repeat All: {rec.repeat_all ?? "—"}
-
-
- - {/* Process Settings */} + {/* Repeaters (full width) */} {(rec.fill_settings?.length ?? 0) > 0 && (

Fill Settings

@@ -298,6 +269,10 @@ export default function CO2GalvoDetail({ id, editable }: { id: string | number; Pass Freq (kHz) Pulse (ns) + Auto + Cross + Flood + Air @@ -312,6 +287,10 @@ export default function CO2GalvoDetail({ id, editable }: { id: string | number; {r.pass ?? "—"} {r.frequency ?? "—"} {r.pulse ?? "—"} + {yesNo(r.auto)} + {yesNo(r.cross)} + {yesNo(r.flood)} + {yesNo(r.air)} ))} @@ -333,6 +312,10 @@ export default function CO2GalvoDetail({ id, editable }: { id: string | number; Freq Pulse Pass + Perf + Cut + Skip + Wobble Air @@ -345,7 +328,11 @@ export default function CO2GalvoDetail({ id, editable }: { id: string | number; {r.frequency ?? "—"} {r.pulse ?? "—"} {r.pass ?? "—"} - {r.air ? "Yes" : "No"} + {yesNo(r.perf)} + {yesNo(r.cut)} + {yesNo(r.skip)} + {yesNo(r.wobble)} + {yesNo(r.air)} ))} @@ -368,6 +355,14 @@ export default function CO2GalvoDetail({ id, editable }: { id: string | number; Speed Interval Pass + Cross + Inversion + Air + Freq (kHz) + Pulse (ns) + Halftone Cell + Halftone Angle + Dot @@ -380,6 +375,14 @@ export default function CO2GalvoDetail({ id, editable }: { id: string | number; {r.speed ?? "—"} {r.interval ?? "—"} {r.pass ?? "—"} + {yesNo(r.cross)} + {yesNo(r.inversion)} + {yesNo(r.air)} + {r.frequency ?? "—"} + {r.pulse ?? "—"} + {r.halftone_cell ?? "—"} + {r.halftone_angle ?? "—"} + {r.dot ?? "—"} ))} @@ -387,6 +390,21 @@ export default function CO2GalvoDetail({ id, editable }: { id: string | number;
)} + + {/* Lightbox */} + {viewerSrc && ( +
setViewerSrc(null)} + > + e.stopPropagation()} + /> +
+ )} ); } diff --git a/components/lists/CO2GalvoList.tsx b/components/lists/CO2GalvoList.tsx index 56e1b376..a1aac22a 100644 --- a/components/lists/CO2GalvoList.tsx +++ b/components/lists/CO2GalvoList.tsx @@ -51,43 +51,14 @@ export default function CO2GalvoList({ const [rows, setRows] = useState([]); const [loading, setLoading] = useState(true); const [localQuery, setLocalQuery] = useState(queryText ?? ""); - const [meId, setMeId] = useState(null); + + // id -> username map (fix showing UUIDs) + const [ownerMap, setOwnerMap] = useState>({}); useEffect(() => { if (queryText !== undefined) setLocalQuery(queryText); }, [queryText]); - // Robust current-user id fetch so we can show "Edit" for owners - useEffect(() => { - let live = true; - (async () => { - try { - // 1) Try app-level /api/me - const r1 = await fetch(`/api/me`, { cache: "no-store", credentials: "include" }); - if (r1.ok) { - const j1 = await readJson(r1); - const id1 = j1?.id ?? j1?.data?.id ?? null; - if (live && id1 != null) { - setMeId(String(id1)); - return; - } - } - // 2) Fallback to Directus /users/me - const r2 = await fetch(`/api/dx/users/me?fields=id`, { cache: "no-store", credentials: "include" }); - if (!r2.ok) return; - const j2 = await readJson(r2); - const id2 = j2?.data?.id ?? j2?.id ?? null; - if (live && id2 != null) setMeId(String(id2)); - } catch { - /* ignore */ - } - })(); - return () => { - live = false; - }; - }, []); - - // Load rows useEffect(() => { let live = true; (async () => { @@ -121,9 +92,62 @@ export default function CO2GalvoList({ }; }, []); + // Resolve owner usernames when we only have an id/UUID + 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 batch errors */ + } + } + 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") return String(o); + if (typeof o === "string" || typeof o === "number") { + const id = String(o); + return ownerMap[id] || id; // prefer resolved username + } return ( o.username || [o.first_name, o.last_name].filter(Boolean).join(" ").trim() || @@ -132,15 +156,6 @@ export default function CO2GalvoList({ ); }; - 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 withEdit = (href: string) => (href.includes("?") ? `${href}&edit=1` : `${href}?edit=1`); - const filtered = useMemo(() => { const q = (localQuery || "").toLowerCase(); if (!q) return rows; @@ -149,7 +164,7 @@ export default function CO2GalvoList({ .filter(Boolean) .some((v) => String(v).toLowerCase().includes(q)) ); - }, [rows, localQuery]); + }, [rows, localQuery, ownerMap]); return (
@@ -170,43 +185,29 @@ export default function CO2GalvoList({ - + - - {filtered.map((r) => { - const href = linkFor(r.submission_id); - const mine = isMine(r.owner); - return ( - - - - - - - - - - ); - })} + {filtered.map((r) => ( + + + + + + + + + ))}
TitleTitle Owner Material Coating Model FieldEdit
- - {r.setting_title || "Untitled"} - - {ownerLabel(r.owner)}{mine ? " (you)" : ""}{r.mat?.name || "—"}{r.mat_coat?.name || "—"}{r.source?.model || "—"}{r.lens?.field_size || "—"} - {mine ? ( - - Edit - - ) : ( - - )} -
+ + {r.setting_title || "Untitled"} + + {ownerLabel(r.owner)}{r.mat?.name || "—"}{r.mat_coat?.name || "—"}{r.source?.model || "—"}{r.lens?.field_size || "—"}