diff --git a/components/lists/CO2GalvoList.tsx b/components/lists/CO2GalvoList.tsx
index 72aeb5b5..d4a1c372 100644
--- a/components/lists/CO2GalvoList.tsx
+++ b/components/lists/CO2GalvoList.tsx
@@ -1,64 +1,286 @@
-return (
-
-
- {
- setLocalQuery(e.currentTarget.value);
- onQueryChange?.(e.currentTarget.value);
- })}
- placeholder="Search by title, owner, material, model…"
- className="w-full border rounded px-3 py-2"
- />
-
+// components/lists/CO2GalvoList.tsx
+"use client";
- {loading ? (
-
Loading…
- ) : (
-
-
-
-
- Title
- Owner
- Material
- Coating
- Model
- Field
- Edit
-
-
-
- {filtered.map((s) => {
- const mine = isMine(s.owner);
- const ownerText = ownerLabel(s.owner) + (mine ? " (you)" : "");
+import { useEffect, useMemo, useState } from "react";
+import Link from "next/link";
+
+type Owner =
+| string
+| number
+| {
+ id?: string | number;
+ username?: string | null;
+ first_name?: string | null;
+ last_name?: string | null;
+ email?: string | null;
+}
+| null
+| undefined;
+
+type SettingsRow = {
+ submission_id: string | number;
+ setting_title?: string | null;
+ uploader?: string | null;
+ owner?: Owner;
+ mat?: { name?: string | null } | null;
+ mat_coat?: { name?: string | null } | null;
+ source?: { model?: string | null } | null;
+ lens?: { field_size?: string | number | null } | null;
+};
+
+export type CO2GalvoListProps = {
+ /** Build the href for a record (portal vs standalone) */
+ linkFor: (submission_id: string | number, opts?: { edit?: boolean }) => string;
+ /** Optional controlled search text (else internal input) */
+ queryText?: string;
+ onQueryChange?: (q: string) => void;
+};
+
+async function readJson(res: Response) {
+ const text = await res.text();
+ try {
+ return text ? JSON.parse(text) : null;
+ } catch {
+ throw new Error(`Unexpected response (status ${res.status})`);
+ }
+}
+
+export default function CO2GalvoList({ linkFor, queryText, onQueryChange }: CO2GalvoListProps) {
+ const [settings, setSettings] = useState([]);
+ const [ownerMap, setOwnerMap] = useState>({});
+ const [loading, setLoading] = useState(true);
+ const [resolvingOwners, setResolvingOwners] = useState(false);
+ const [meId, setMeId] = useState(null);
+ const [localQuery, setLocalQuery] = useState(queryText ?? "");
+
+ // keep local and external search text in sync
+ useEffect(() => {
+ if (queryText !== undefined) setLocalQuery(queryText);
+ }, [queryText]);
+
+ // load current user id (for edit visibility)
+ useEffect(() => {
+ let dead = false;
+ (async () => {
+ try {
+ const r = await fetch(`/api/dx/users/me?fields=id`, {
+ cache: "no-store",
+ credentials: "include",
+ });
+ if (!r.ok) return;
+ const j = await readJson(r);
+ const id = j?.data?.id ?? j?.id ?? null;
+ if (!dead) setMeId(id ? String(id) : null);
+ } catch {
+ /* ignore */
+ }
+ })();
+ return () => {
+ dead = true;
+ };
+ }, []);
+
+ // load list
+ useEffect(() => {
+ const fields = [
+ "submission_id",
+ "setting_title",
+ "uploader",
+ "owner",
+ "owner.id",
+ "owner.username",
+ "photo.id",
+ "photo.title",
+ "mat.name",
+ "mat_coat.name",
+ "source.model",
+ "lens.field_size",
+ ].join(",");
+
+ const url = `/api/dx/items/settings_co2gal?fields=${encodeURIComponent(fields)}&limit=-1`;
+
+ fetch(url, { cache: "no-store", credentials: "include" })
+ .then(async (res) => {
+ if (!res.ok) {
+ const j = await readJson(res).catch(() => null);
+ const msg = (j as any)?.errors?.[0]?.message || `HTTP ${res.status}`;
+ throw new Error(msg);
+ }
+ return readJson(res);
+ })
+ .then((json: any) => setSettings(Array.isArray(json?.data) ? json.data : []))
+ .catch((e) => {
+ console.error("CO2 Galvo list fetch failed:", e);
+ setSettings([]);
+ })
+ .finally(() => setLoading(false));
+ }, []);
+
+ // resolve owner usernames if needed
+ useEffect(() => {
+ if (!settings.length) return;
+
+ const ids = new Set();
+ for (const s of settings) {
+ const o = s.owner;
+ if (!o) continue;
+
+ if (typeof o === "string" || typeof o === "number") {
+ const k = String(o);
+ if (!ownerMap[k]) ids.add(k);
+ } 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;
+
+ const all = Array.from(ids);
+ const chunk = 100;
+ setResolvingOwners(true);
+
+ (async () => {
+ const updates: Record = {};
+ for (let i = 0; i < all.length; i += chunk) {
+ const slice = all.slice(i, i + chunk);
+ const qs = new URLSearchParams();
+ qs.set("fields", "id,username");
+ qs.set("limit", String(slice.length));
+ qs.set("filter[id][_in]", slice.join(","));
+
+ try {
+ const r = await fetch(`/api/dx/users?${qs.toString()}`, {
+ credentials: "include",
+ cache: "no-store",
+ });
+ if (!r.ok) {
+ const j = await readJson(r).catch(() => null);
+ const msg = (j as any)?.errors?.[0]?.message || `HTTP ${r.status}`;
+ console.warn("Owner lookup failed:", msg);
+ if (r.status === 401 || r.status === 403) break;
+ continue;
+ }
+ const j = await readJson(r);
+ const rows: Array<{ id: string; username?: string | null }> = j?.data || [];
+ for (const row of rows) {
+ updates[String(row.id)] = row.username || String(row.id);
+ }
+ } catch (e) {
+ console.warn("Owner lookup error:", e);
+ break;
+ }
+ }
+
+ if (Object.keys(updates).length) {
+ setOwnerMap((prev) => ({ ...prev, ...updates }));
+ }
+ setResolvingOwners(false);
+ })();
+ }, [settings, ownerMap]);
+
+ const isMine = (o: Owner) => {
+ 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 ownerLabel = (o: Owner) => {
+ if (!o) return "—";
+ if (typeof o === "string" || typeof o === "number") {
+ const id = String(o);
+ return ownerMap[id] || id;
+ }
return (
-
-
-
- {s.setting_title || "Untitled"}
-
-
- {ownerText}
- {s.mat?.name || "—"}
- {s.mat_coat?.name || "—"}
- {s.source?.model || "—"}
- {s.lens?.field_size || "—"}
-
- {mine ? (
-
- Edit
-
- ) : (
- —
- )}
-
-
+ o.username ||
+ [o.first_name, o.last_name].filter(Boolean).join(" ").trim() ||
+ o.email ||
+ (o.id != null ? String(o.id) : "—")
);
- })}
-
-
-
- )}
-
- );
+ };
+
+ const filtered = useMemo(() => {
+ const nq = (localQuery || "").toLowerCase();
+ if (!nq) return settings;
+ return settings.filter((entry) => {
+ const fields = [
+ entry.setting_title,
+ ownerLabel(entry.owner),
+ entry.uploader,
+ entry.mat?.name,
+ entry.mat_coat?.name,
+ entry.source?.model,
+ entry.lens?.field_size as any,
+ ].filter(Boolean);
+ return fields.some((v: any) => String(v).toLowerCase().includes(nq));
+ });
+ }, [settings, localQuery, ownerMap]);
+
+ return (
+
+
+ {
+ setLocalQuery(e.currentTarget.value);
+ onQueryChange?.(e.currentTarget.value);
+ }}
+ placeholder="Search by title, owner, material, model…"
+ className="w-full border rounded px-3 py-2"
+ />
+
+
+ {loading ? (
+
Loading…
+ ) : (
+
+
+
+
+ Title
+
+ Owner {resolvingOwners ? "…resolving" : ""}
+
+ Material
+ Coating
+ Model
+ Field
+ Edit
+
+
+
+ {filtered.map((s) => {
+ const mine = isMine(s.owner);
+ const ownerText = ownerLabel(s.owner) + (mine ? " (you)" : "");
+ return (
+
+
+
+ {s.setting_title || "Untitled"}
+
+
+ {ownerText}
+ {s.mat?.name || "—"}
+ {s.mat_coat?.name || "—"}
+ {s.source?.model || "—"}
+ {s.lens?.field_size || "—"}
+
+ {mine ? (
+
+ Edit
+
+ ) : (
+ —
+ )}
+
+
+ );
+ })}
+
+
+
+ )}
+
+ );
+}
diff --git a/components/portal/SettingsSwitcher.tsx b/components/portal/SettingsSwitcher.tsx
index 96a8758e..34727878 100644
--- a/components/portal/SettingsSwitcher.tsx
+++ b/components/portal/SettingsSwitcher.tsx
@@ -30,10 +30,7 @@ const TABS: { key: Tab; label: string }[] = [
];
const isDataTab = (k: string): k is DataTab =>
-k === "fiber" || k === "uv" || k === "co2-ganry" || k === "co2-gantry" || k === "co2-galvo"
-// keep typo guard for "co2-ganry" if you want; remove if unnecessary
-? true
-: (k === "fiber" || k === "uv" || k === "co2-gantry" || k === "co2-galvo");
+k === "fiber" || k === "uv" || k === "co2-gantry" || k === "co2-galvo";
const tabToTarget: Record ;
case "add":
return (
-
+
Add Setting
@@ -61,10 +58,8 @@ function Panel({ tab, lastDataTab }: { tab: Tab; lastDataTab: DataTab }) {
// We navigate away for "My Settings", so this branch is not normally rendered.
// Fallback content (just in case someone forces ?t=my on this page):
return (
-
-
+
Opening My Settings …
-
);
default:
@@ -120,9 +115,8 @@ export default function SettingsSwitcher() {
))}
-
+ {/* Removed the extra border/padding wrapper to avoid double-framing */}
-
);
}