diff --git a/components/details/CO2GalvoDetail.tsx b/components/details/CO2GalvoDetail.tsx index 85401cca..9fa14e77 100644 --- a/components/details/CO2GalvoDetail.tsx +++ b/components/details/CO2GalvoDetail.tsx @@ -2,12 +2,13 @@ "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; @@ -17,15 +18,15 @@ type Rec = { mat_opacity?: { id?: string | number; opacity?: string | number | null } | null; mat_thickness?: number | null; - 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; 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; fill_settings?: any[] | null; @@ -34,243 +35,358 @@ type Rec = { owner?: { id?: string | number; username?: string | null } | string | number | null; uploader?: string | null; - last_modified_date?: string | null; }; -const API = (process.env.NEXT_PUBLIC_API_BASE_URL || "").replace(/\/$/, ""); -const asset = (id?: string | number) => (id ? `${API}/assets/${id}` : ""); +type Me = { id: string | number; username?: string; email?: string } | null; -async function readJson(r: Response) { - const t = await r.text(); - try { - return t ? JSON.parse(t) : null; - } catch { - return 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 = true }: { id: string | number; editable?: boolean }) { +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 useEffect(() => { let live = true; (async () => { - 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", - "mat_thickness", - "laser_soft.id", - "laser_soft.name", - "source.submission_id", - "source.make", - "source.model", - "source.nm", - "lens.id", - "lens.field_size", - "lens.focal_length", - "focus", - "lens_conf.id", - "lens_conf.name", - "lens_apt.id", - "lens_apt.name", - "lens_exp.id", - "lens_exp.name", - "repeat_all", - "fill_settings", - "line_settings", - "raster_settings", - "owner.id", - "owner.username", - "uploader", - "last_modified_date", - ].join(","); + 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 url = `${API}/items/settings_co2gal?fields=${encodeURIComponent(fields)}&filter[submission_id][_eq]=${encodeURIComponent( - String(id) - )}&limit=1`; + // load record (with readable fields) + useEffect(() => { + if (!id) return; + let dead = false; - const res = await fetch(url, { credentials: "include", cache: "no-store" }); - if (!res.ok) { - const j = await readJson(res); - throw new Error(j?.errors?.[0]?.message || `HTTP ${res.status}`); + (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", + "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", + "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 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 row: Rec | null = Array.isArray(j?.data) ? j.data[0] || null : null; + if (!row) throw new Error("Setting not found."); + if (!dead) setRec(row); + } catch (e: any) { + if (!dead) setErr(e?.message || String(e)); + } finally { + if (!dead) setLoading(false); } - const j = await res.json(); - const row = Array.isArray(j?.data) ? j.data[0] : null; - if (!row) throw new Error("Not found"); - if (live) setRec(row); - })() - .catch((e: any) => live && setErr(e?.message || "Failed")) - .finally(() => live && setLoading(false)); - return () => { - live = false; - }; + })(); + + return () => { dead = true; }; }, [id]); - 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 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); - if (loading) return

Loading…

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

Not found.

; + // 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; - const softName = typeof rec.laser_soft === "object" ? rec.laser_soft?.name ?? "—" : "—"; + 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 (!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 ?? "—") : "—"; return (
-

{rec.setting_title || "Untitled"}

+
+

{rec.setting_title || "Untitled"}

+ {editable && isMine ? ( + + ) : null} +
Last modified: {rec.last_modified_date || "—"}
-
-
-
- Owner: {typeof rec.owner === "object" ? rec.owner?.username || rec.owner?.id : rec.owner || "—"} -
-
- Uploader: {rec.uploader || "—"} -
-
- Material: {rec.mat?.name || "—"} -
-
- Coating: {rec.mat_coat?.name || "—"} -
-
- Color: {rec.mat_color?.name || "—"} -
-
- Opacity: {rec.mat_opacity?.opacity ?? "—"} -
-
- Thickness (mm): {rec.mat_thickness ?? "—"} + {/* Info */} +
+

Info

+ {rec.setting_notes ?

{rec.setting_notes}

:

No notes.

} +
+ + {/* Images */} + {(photoSrc || screenSrc) && ( +
+

Images

+
+ {photoSrc ? ( +
+ Result +
Result
+
+ ) : null} + {screenSrc ? ( +
+ Settings Screenshot +
Settings Screenshot
+
+ ) : null} +
+
+ )} + + {/* 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` : ""} + Scan Lens:{" "} + {rec.lens?.field_size || "—"}{rec.lens?.focal_length ? ` / ${rec.lens.focal_length} mm` : ""}
-
- Focus (mm): {rec.focus ?? "—"} -
-
- Software: {softName} -
-
- Lens Config: {rec.lens_conf?.name || "—"} -
-
- Scan Head Aperture: {rec.lens_apt?.name || "—"} -
-
- Beam Expander: {rec.lens_exp?.name || "—"} -
-
- Repeat All: {rec.repeat_all ?? "—"} +
Focus (mm): {rec.focus ?? "—"}
+
Repeat All: {rec.repeat_all ?? "—"}
+
- {rec.setting_notes ? ( -
-
Notes
-

{rec.setting_notes}

+ {/* Process Settings */} + {(rec.fill_settings?.length ?? 0) > 0 && ( +
+

Fill Settings

+
+ + + + + + + + + + + + + + + + {rec.fill_settings!.map((r: any, i: number) => ( + + + + + + + + + + + + ))} + +
NameTypePower (%)Speed (mm/s)IntervalAnglePassFreq (kHz)Pulse (ns)
{r.name || "—"}{r.type || "—"}{r.power ?? "—"}{r.speed ?? "—"}{r.interval ?? "—"}{r.angle ?? "—"}{r.pass ?? "—"}{r.frequency ?? "—"}{r.pulse ?? "—"}
- ) : null} -
- -
- {photoId ? ( -
- Result -
Result
-
- ) : null} - {screenId ? ( -
- Settings Screenshot -
Settings
-
- ) : null} -
-
- - {/* Tables */} - {Array.isArray(rec.fill_settings) && rec.fill_settings.length > 0 && ( - [ - r.name ?? "—", - r.type ?? "—", - r.power ?? "—", - r.speed ?? "—", - r.interval ?? "—", - r.angle ?? "—", - r.pass ?? "—", - r.frequency ?? "—", - r.pulse ?? "—", - ])} /> + )} - {Array.isArray(rec.line_settings) && rec.line_settings.length > 0 && ( -
[ - r.name ?? "—", - r.power ?? "—", - r.speed ?? "—", - r.frequency ?? "—", - r.pulse ?? "—", - r.pass ?? "—", - r.air ? "Yes" : "No", - ])} /> + {(rec.line_settings?.length ?? 0) > 0 && ( +
+

Line Settings

+
+
+ + + + + + + + + + + + + {rec.line_settings!.map((r: any, i: number) => ( + + + + + + + + + + ))} + +
NamePowerSpeedFreqPulsePassAir
{r.name || "—"}{r.power ?? "—"}{r.speed ?? "—"}{r.frequency ?? "—"}{r.pulse ?? "—"}{r.pass ?? "—"}{r.air ? "Yes" : "No"}
+
+ )} - {Array.isArray(rec.raster_settings) && rec.raster_settings.length > 0 && ( - [ - r.name ?? "—", - r.type ?? "—", - r.dither ?? "—", - r.power ?? "—", - r.speed ?? "—", - r.interval ?? "—", - r.pass ?? "—", - ])} /> + {(rec.raster_settings?.length ?? 0) > 0 && ( +
+

Raster Settings

+
+
+ + + + + + + + + + + + + {rec.raster_settings!.map((r: any, i: number) => ( + + + + + + + + + + ))} + +
NameTypeDitherPowerSpeedIntervalPass
{r.name || "—"}{r.type || "—"}{r.dither || "—"}{r.power ?? "—"}{r.speed ?? "—"}{r.interval ?? "—"}{r.pass ?? "—"}
+ + )} ); } - -function Table({ title, cols, rows }: { title: string; cols: string[]; rows: any[][] }) { - return ( -
-

{title}

-
- - - {cols.map((c) => )} - - - {rows.map((r, i) => ( - {r.map((v, j) => )} - ))} - -
{c}
{String(v)}
-
-
- ); -} diff --git a/components/forms/SettingsSubmit.tsx b/components/forms/SettingsSubmit.tsx index 6e30b1d1..2e7a9f7f 100644 --- a/components/forms/SettingsSubmit.tsx +++ b/components/forms/SettingsSubmit.tsx @@ -2,20 +2,13 @@ "use client"; import { useEffect, useMemo, useState } from "react"; -import { useForm, useFieldArray, type UseFormRegister } from "react-hook-form"; +import { useForm, useFieldArray, useWatch, type UseFormRegister } from "react-hook-form"; import { useRouter } from "next/navigation"; -/** - * From-scratch CO₂ Galvo form that follows the data sheet. - * - Prefill works via reset() with raw IDs. - * - Submits via /app/api/settings (this file does not assume any old helpers). - * - Lens options belong to Rig & Optics (not a separate "Lens Options" section). - */ - type Target = "settings_co2gal"; type Opt = { id: string; label: string }; -type Me = { id: string; username?: string; email?: string }; +type Me = { id: string; username?: string; email?: string } | null; const API = (process.env.NEXT_PUBLIC_API_BASE_URL || "").replace(/\/$/, ""); @@ -40,7 +33,8 @@ type EditInitialValues = { source?: string | null; // submission_id lens?: string | null; focus?: number | null; - // CO2 Galvo triplet (Rig & Optics) + + // CO2 Galvo triplet lens_conf?: string | null; lens_apt?: string | null; lens_exp?: string | null; @@ -58,21 +52,39 @@ export default function SettingsSubmit({ mode = "create", submissionId, initialV const router = useRouter(); const isEdit = mode === "edit"; - const [me, setMe] = useState(null); + const [me, setMe] = useState(null); const [submitErr, setSubmitErr] = useState(null); + // Robust current-user fetch useEffect(() => { let alive = true; - fetch(`/api/me`, { cache: "no-store", credentials: "include" }) - .then((r) => (r.ok ? r.json() : null)) - .then((j) => alive && setMe(j || null)) - .catch(() => alive && setMe(null)); - return () => { - alive = false; - }; + (async () => { + try { + const r1 = await fetch(`/api/me`, { cache: "no-store", credentials: "include" }); + if (r1.ok) { + const j = await r1.json().catch(() => null); + if (alive && j) { + const id = j?.id ?? j?.data?.id ?? null; + const username = j?.username ?? j?.data?.username ?? j?.name ?? null; + const email = j?.email ?? j?.data?.email ?? null; + setMe(id ? { id, username: username ?? undefined, email: email ?? undefined } : null); + return; + } + } + const r2 = await fetch(`/api/dx/users/me?fields=id,username,email`, { cache: "no-store", credentials: "include" }); + if (alive && r2.ok) { + const j2 = await r2.json().catch(() => null); + const d = j2?.data ?? j2 ?? null; + setMe(d?.id ? { id: d.id, username: d.username ?? undefined, email: d.email ?? undefined } : null); + } + } catch { + if (alive) setMe(null); + } + })(); + return () => { alive = false; }; }, []); - // Options loaders (raw Directus reads) + // Options loaders (Directus reads) function useOptions(path: string, includeId?: string | null) { const [opts, setOpts] = useState([]); useEffect(() => { @@ -92,7 +104,6 @@ export default function SettingsSubmit({ mode = "create", submissionId, initialV url = `${API}/items/laser_software?fields=id,name&limit=1000&sort=name`; } else if (path === "laser_source_co2_galvo") { url = `${API}/items/laser_source?fields=submission_id,make,model,nm&limit=2000&sort=make,model`; - // Explicitly reference the global Number to avoid the local component shadowing it. type Row = { submission_id?: string | number; make?: string; model?: string; nm?: string | number | null }; const toNum = (v: unknown): number | null => { if (typeof v === "number") return globalThis.Number.isFinite(v) ? v : null; @@ -147,9 +158,7 @@ export default function SettingsSubmit({ mode = "create", submissionId, initialV if (live) setOpts(list); })().catch(() => live && setOpts([])); - return () => { - live = false; - }; + return () => { live = false; }; }, [path, includeId]); return { opts }; @@ -163,16 +172,7 @@ export default function SettingsSubmit({ mode = "create", submissionId, initialV ]; const RASTER_TYPES = FILL_TYPES; const RASTER_DITHER: Opt[] = [ - "threshold", - "ordered", - "atkinson", - "dither", - "stucki", - "jarvis", - "newsprint", - "halftone", - "sketch", - "grayscale", + "threshold", "ordered", "atkinson", "dither", "stucki", "jarvis", "newsprint", "halftone", "sketch", "grayscale", ].map((x) => ({ id: x, label: x[0].toUpperCase() + x.slice(1) })); // react-hook-form @@ -195,11 +195,12 @@ export default function SettingsSubmit({ mode = "create", submissionId, initialV // Rig & Optics laser_soft: "", source: "", - lens: "", - focus: "", + // keep these blank so Select shows "—" lens_conf: "", lens_apt: "", lens_exp: "", + lens: "", + focus: "", repeat_all: "", // Repeaters fill_settings: [], @@ -228,11 +229,11 @@ export default function SettingsSubmit({ mode = "create", submissionId, initialV // Rig & Optics laser_soft: initialValues.laser_soft ?? "", source: initialValues.source ?? "", - lens: initialValues.lens ?? "", - focus: initialValues.focus ?? "", lens_conf: initialValues.lens_conf ?? "", lens_apt: initialValues.lens_apt ?? "", lens_exp: initialValues.lens_exp ?? "", + lens: initialValues.lens ?? "", + focus: initialValues.focus ?? "", repeat_all: initialValues.repeat_all ?? "", // Repeaters fill_settings: initialValues.fill_settings ?? [], @@ -270,16 +271,16 @@ export default function SettingsSubmit({ mode = "create", submissionId, initialV mat_coat: values.mat_coat || null, mat_color: values.mat_color || null, mat_opacity: values.mat_opacity || null, - mat_thickness: values.mat_thickness === "" ? null : Number(values.mat_thickness), + mat_thickness: values.mat_thickness === "" ? null : globalThis.Number(values.mat_thickness), // Rig & Optics laser_soft: values.laser_soft || null, source: values.source || null, - lens: values.lens || null, - focus: values.focus === "" ? null : Number(values.focus), lens_conf: values.lens_conf || null, lens_apt: values.lens_apt || null, lens_exp: values.lens_exp || null, - repeat_all: values.repeat_all === "" ? null : Number(values.repeat_all), + lens: values.lens || null, + focus: values.focus === "" ? null : globalThis.Number(values.focus), + repeat_all: values.repeat_all === "" ? null : globalThis.Number(values.repeat_all), // Repeaters (raw pass-through; api will normalize nums/bools) fill_settings: values.fill_settings || [], line_settings: values.line_settings || [], @@ -311,11 +312,13 @@ export default function SettingsSubmit({ mode = "create", submissionId, initialV } }; + const meLabel = me?.username || me?.email || ""; + return (

{isEdit ? "Edit CO₂ Galvo Setting" : "Submit CO₂ Galvo Setting"}

- {me ?

Submitting as {me.username || me.email}

: null} + {meLabel ?

Submitting as {meLabel}

: null} {submitErr ?
{submitErr}
: null}
@@ -325,7 +328,7 @@ export default function SettingsSubmit({ mode = "create", submissionId, initialV

Info

- +
@@ -340,7 +343,7 @@ export default function SettingsSubmit({ mode = "create", submissionId, initialV

Images

- +
@@ -354,28 +357,33 @@ export default function SettingsSubmit({ mode = "create", submissionId, initialV

Material

- - + +
- {/* Rig & Optics (includes lens_conf/apt/exp) */} + {/* Rig & Optics (order per spec) */}

Rig & Optics

+
- - + - + + + +
@@ -387,7 +395,7 @@ export default function SettingsSubmit({ mode = "create", submissionId, initialV fills.append({ type: "uni" })} + onAdd={() => fills.append({ type: "" })} // default to "—" onRemove={(i) => fills.remove(i)} render={(i) => (
@@ -438,27 +446,35 @@ export default function SettingsSubmit({ mode = "create", submissionId, initialV rasters.append({ type: "uni", dither: "threshold" })} + onAdd={() => rasters.append({ type: "", dither: "" })} // default to "—" onRemove={(i) => rasters.remove(i)} - render={(i) => ( -
- - - - - - - - - - - - - - -
- )} + render={(i) => { + const ditherVal = useWatch({ control, name: `raster_settings.${i}.dither` }) || ""; + const isHalftone = ditherVal === "halftone"; + return ( +
+ + + + + + + {isHalftone && ( + <> + + + + )} + + + + + + +
+ ); + }} /> @@ -486,9 +502,7 @@ function Select({
@@ -522,16 +536,12 @@ function Repeater({ title, fields, onAdd, onRemove, render }: any) {
{title} - +
{fields.map((_: any, i: number) => (
{render(i)} - +
))}
diff --git a/components/lists/CO2GalvoList.tsx b/components/lists/CO2GalvoList.tsx index 3b5ba81f..56e1b376 100644 --- a/components/lists/CO2GalvoList.tsx +++ b/components/lists/CO2GalvoList.tsx @@ -51,11 +51,43 @@ export default function CO2GalvoList({ const [rows, setRows] = useState([]); const [loading, setLoading] = useState(true); const [localQuery, setLocalQuery] = useState(queryText ?? ""); + const [meId, setMeId] = useState(null); 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 () => { @@ -92,9 +124,23 @@ export default function CO2GalvoList({ const ownerLabel = (o: Owner) => { if (!o) return "—"; if (typeof o === "string" || typeof o === "number") return String(o); - return o.username || [o.first_name, o.last_name].filter(Boolean).join(" ").trim() || o.email || (o.id != null ? String(o.id) : "—"); + return ( + o.username || + [o.first_name, o.last_name].filter(Boolean).join(" ").trim() || + o.email || + (o.id != null ? String(o.id) : "—") + ); }; + 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; @@ -124,29 +170,43 @@ export default function CO2GalvoList({ - + + - {filtered.map((r) => ( - - - - - - - - - ))} + {filtered.map((r) => { + const href = linkFor(r.submission_id); + const mine = isMine(r.owner); + return ( + + + + + + + + + + ); + })}
TitleTitle Owner Material Coating Model FieldEdit
- - {r.setting_title || "Untitled"} - - {ownerLabel(r.owner)}{r.mat?.name || "—"}{r.mat_coat?.name || "—"}{r.source?.model || "—"}{r.lens?.field_size || "—"}
+ + {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 + + ) : ( + + )} +