co2-galvo owner testing

This commit is contained in:
makearmy 2025-10-02 17:21:43 -04:00
parent a100fefc77
commit 30ac27815f
2 changed files with 69 additions and 242 deletions

View file

@ -1,18 +1,13 @@
// app/settings/co2-galvo/[id]/co2-galvo.tsx
"use client";
import { useEffect, useState } from "react";
import { useParams } from "next/navigation";
import Image from "next/image";
import Markdown from "react-markdown";
export default function CO2GalvoSettingDetailPage() {
const { id } = useParams<{ id: string }>();
const [setting, setSetting] = useState<any>(null);
const [loading, setLoading] = useState(true);
const [claimBusy, setClaimBusy] = useState(false);
const [claimMsg, setClaimMsg] = useState<string | null>(null);
const [claimErr, setClaimErr] = useState<string | null>(null);
useEffect(() => {
if (!id) return;
@ -21,58 +16,23 @@ export default function CO2GalvoSettingDetailPage() {
"submission_id",
"setting_title",
"uploader",
// include parent + subfields to survive restricted expansion
// ✅ parent + subfields for resilient owner
"owner",
"owner.id",
"owner.username",
"owner.first_name",
"owner.last_name",
"owner.email",
"setting_notes",
"photo.filename_disk",
"photo.title",
"screen.filename_disk",
"screen.title",
"mat.name",
"mat_coat.name",
"mat_color.name",
"mat_opacity.opacity",
"mat_thickness",
"source.make",
"source.model",
"lens.field_size",
"lens.focal_length",
"lens_conf.name",
"lens_apt.name",
"lens_exp.name",
"focus",
// string-or-relation safe
"laser_soft",
"laser_soft.name",
"repeat_all",
"fill_settings",
"line_settings",
"raster_settings",
// ... (rest of your fields)
].join(",");
// use the authenticated proxy (sends ma_at automatically)
const url = `/api/dx/items/settings_co2gal/${encodeURIComponent(String(id))}?fields=${fields}`;
const url = `/api/dx/items/settings_co2gal/${encodeURIComponent(String(id))}?fields=${encodeURIComponent(fields)}`;
setLoading(true);
fetch(url, { cache: "no-store", credentials: "include" })
.then(async (res) => {
const txt = await res.text();
let j: any = null;
try {
j = txt ? JSON.parse(txt) : null;
} catch {}
if (!res.ok) {
throw new Error(j?.errors?.[0]?.message || j?.message || `HTTP ${res.status}`);
}
const j = txt ? JSON.parse(txt) : null;
if (!res.ok) throw new Error(j?.errors?.[0]?.message || j?.message || `HTTP ${res.status}`);
return j;
})
.then((json) => setSetting(json?.data ?? null))
@ -86,87 +46,34 @@ export default function CO2GalvoSettingDetailPage() {
if (loading) return <p className="p-6">Loading setting...</p>;
if (!setting) return <p className="p-6">Setting not found.</p>;
// Owner label: username → name → email → id → —
const ownerDisplay: string =
const ownerDisplay =
typeof setting?.owner === "object"
? (setting.owner?.username
|| [setting.owner?.first_name, setting.owner?.last_name].filter(Boolean).join(" ").trim()
|| setting.owner?.email
|| setting.owner?.id
|| "—")
: typeof setting?.owner === "string"
? setting.owner
? (setting.owner?.username ||
[setting.owner?.first_name, setting.owner?.last_name].filter(Boolean).join(" ").trim() ||
setting.owner?.email ||
setting.owner?.id ||
"—")
: typeof setting?.owner === "string" || typeof setting?.owner === "number"
? String(setting.owner)
: "—";
const softwareLabel: string =
typeof setting?.laser_soft === "object"
? (setting.laser_soft?.name ?? "—")
: (setting?.laser_soft ?? "—");
const formatBoolean = (val: any) =>
val ? "Enabled" : val === false ? "Disabled" : "—";
const onClaim = async () => {
setClaimBusy(true);
setClaimErr(null);
setClaimMsg(null);
try {
const r = await fetch("/api/claims", {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify({
target_collection: "settings_co2gal",
target_id: setting.submission_id,
}),
});
const j = await r.json().catch(() => ({}));
if (!r.ok) throw new Error(j?.error || j?.message || `HTTP ${r.status}`);
setClaimMsg("Claim request submitted. We'll update the owner shortly.");
} catch (e: any) {
setClaimErr(e?.message || "Failed to submit claim.");
} finally {
setClaimBusy(false);
}
};
return (
<div className="p-6 max-w-7xl mx-auto">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
{/* Title / Meta / Ownership */}
<div className="card bg-card p-4 flex flex-col justify-between">
<div>
<h1 className="text-3xl font-bold mb-1">{setting.setting_title}</h1>
<div className="card bg-card p-4 mb-4">
<h1 className="text-3xl font-bold mb-2">{setting.setting_title || "Untitled"}</h1>
{/* DEBUG: remove after verifying */}
<pre className="text-xs whitespace-pre-wrap bg-muted/30 p-2 rounded">
{/* TEMP DEBUG — remove once owner shows */}
<pre className="text-xs bg-muted/30 rounded p-2 mb-2">
{JSON.stringify({ owner: setting?.owner }, null, 2)}
</pre>
<div className="space-y-1 text-sm text-muted-foreground mb-3">
<p><strong>Owner:</strong> <span>{ownerDisplay}</span></p>
<div className="space-y-1 text-sm text-muted-foreground">
<p><strong>Owner:</strong> {ownerDisplay}</p>
<p><strong>Uploader:</strong> {setting.uploader || "—"}</p>
</div>
{ownerDisplay === "—" && (
<div className="flex items-center gap-2">
<button
onClick={onClaim}
disabled={claimBusy}
className="px-3 py-1.5 rounded bg-accent text-background text-sm disabled:opacity-60"
>
{claimBusy ? "Submitting…" : "Claim this setting"}
</button>
{claimErr && <span className="text-red-600 text-sm">{claimErr}</span>}
{claimMsg && <span className="text-green-600 text-sm">{claimMsg}</span>}
</div>
)}
</div>
</div>
{/* ... rest of your panels ... */}
</div>
{/* ... rest of the component ... */}
{/* …rest of the page… */}
</div>
);
}

View file

@ -2,16 +2,14 @@
import { useEffect, useState, useMemo } from "react";
import Link from "next/link";
import Image from "next/image";
import { useSearchParams } from "next/navigation";
type Owner = {
id?: string | number;
username?: string | null;
first_name?: string | null;
last_name?: string | null;
email?: string | null;
};
type Owner =
| string
| number
| { id?: string | number; username?: string | null; first_name?: string | null; last_name?: string | null; email?: string | null }
| null
| undefined;
export default function CO2GalvoSettingsPage() {
const searchParams = useSearchParams();
@ -22,28 +20,23 @@ export default function CO2GalvoSettingsPage() {
const [settings, setSettings] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const detailHref = (id: string | number) => `/settings/co2-galvo/${id}`;
useEffect(() => {
const t = setTimeout(() => setDebouncedQuery(query), 300);
return () => clearTimeout(t);
}, [query]);
useEffect(() => {
// Use the auth proxy and request BOTH the parent field and subfields.
// This guarantees you get a raw id when expansion is restricted.
// ✅ include parent field `owner` AND subfields; use auth proxy
const fields = [
"submission_id",
"setting_title",
"uploader",
// owner (m2o -> directus_users)
"owner",
"owner", // parent → ensures raw id comes through if expansion blocked
"owner.id",
"owner.username",
"owner.first_name",
"owner.last_name",
"owner.email",
// assets / denorms
"photo.id",
"photo.title",
"mat.name",
@ -52,10 +45,7 @@ export default function CO2GalvoSettingsPage() {
"lens.field_size",
].join(",");
const url =
`/api/dx/items/settings_co2gal` +
`?fields=${encodeURIComponent(fields)}` +
`&limit=-1`;
const url = `/api/dx/items/settings_co2gal?fields=${encodeURIComponent(fields)}&limit=-1`;
fetch(url, { cache: "no-store", credentials: "include" })
.then(async (res) => {
@ -67,14 +57,13 @@ export default function CO2GalvoSettingsPage() {
})
.then((json) => setSettings(json?.data || []))
.catch((e) => {
console.error("CO2 Galvo settings fetch failed:", e);
console.error("CO2 Galvo list fetch failed:", e);
setSettings([]);
})
.finally(() => setLoading(false));
}, []);
// Robust owner label: object → username | name | email | id; primitive → id; missing → —
const ownerLabel = (o?: any) => {
const ownerLabel = (o: Owner) => {
if (!o) return "—";
if (typeof o === "string" || typeof o === "number") return String(o);
return (
@ -87,7 +76,7 @@ export default function CO2GalvoSettingsPage() {
const highlight = (text?: string) => {
if (!debouncedQuery) return text || "";
const regex = new RegExp(`(${debouncedQuery})`, "gi");
const regex = new RegExp(`(${debouncedQuery.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")})`, "gi");
return (text || "").replace(regex, "<mark>$1</mark>");
};
@ -109,27 +98,8 @@ export default function CO2GalvoSettingsPage() {
});
}, [settings, debouncedQuery]);
const total = settings.length;
const uniqueMaterials = new Set(
settings.map((s) => s.mat?.name).filter(Boolean)
).size;
const lensCounts = settings.reduce((acc: Record<string, number>, cur) => {
const v = cur.lens?.field_size;
if (v) acc[v] = (acc[v] || 0) + 1;
return acc;
}, {});
return (
<div className="container mx-auto p-4">
<div className="mb-6">
<h1 className="text-2xl font-bold">CO Galvo Settings</h1>
<p className="text-sm text-muted-foreground">
Browse community CO galvo settings. Use search to narrow results.
</p>
</div>
{/* search box */}
<div className="p-6 max-w-7xl mx-auto">
<div className="mb-4">
<input
value={query}
@ -139,93 +109,43 @@ export default function CO2GalvoSettingsPage() {
/>
</div>
{/* stats */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
<div className="card bg-card text-card-foreground p-4">
<h2 className="text-lg font-semibold mb-2">How to Use</h2>
<p className="text-sm">
Click a row to view full configuration, notes, and photos.
</p>
</div>
<div className="card bg-card text-card-foreground p-4">
<h2 className="text-lg font-semibold mb-2">Stats Summary</h2>
<ul className="text-sm space-y-1">
<li>Total Settings: {total}</li>
<li>Unique Materials: {uniqueMaterials}</li>
<li>
Lens Fields:{" "}
{Object.entries(lensCounts)
.map(([k, v]) => `${k} (${v})`)
.join(", ") || "—"}
</li>
</ul>
</div>
</div>
{/* table */}
{loading ? (
<p>Loading</p>
) : (
<div className="overflow-x-auto">
<table className="min-w-full text-sm">
<thead>
<tr className="bg-muted">
<th className="px-2 py-2 text-left">Title</th>
<th className="px-2 py-2 text-left">Owner</th>
<th className="px-2 py-2 text-left">Material</th>
<th className="px-2 py-2 text-left">Coating</th>
<th className="px-2 py-2 text-left">Model</th>
<th className="px-2 py-2 text-left">Field</th>
{loading ? (
<p>Loading</p>
) : (
<div className="overflow-x-auto">
<table className="min-w-full text-sm">
<thead>
<tr className="bg-muted">
<th className="px-2 py-2 text-left">Title</th>
<th className="px-2 py-2 text-left">Owner</th>
<th className="px-2 py-2 text-left">Material</th>
<th className="px-2 py-2 text-left">Coating</th>
<th className="px-2 py-2 text-left">Model</th>
<th className="px-2 py-2 text-left">Field</th>
</tr>
</thead>
<tbody>
{filtered.map((s) => (
<tr key={s.submission_id} className="border-b hover:bg-muted/30">
<td className="px-2 py-2 whitespace-nowrap">
<Link href={`/settings/co2-galvo/${s.submission_id}`} className="underline">
<span dangerouslySetInnerHTML={{ __html: highlight(s.setting_title || "Untitled") }} />
</Link>
</td>
<td
className="px-2 py-2 whitespace-nowrap"
dangerouslySetInnerHTML={{ __html: highlight(ownerLabel(s.owner)) }}
/>
<td className="px-2 py-2 whitespace-nowrap">{s.mat?.name || "—"}</td>
<td className="px-2 py-2 whitespace-nowrap">{s.mat_coat?.name || "—"}</td>
<td className="px-2 py-2 whitespace-nowrap">{s.source?.model || "—"}</td>
<td className="px-2 py-2 whitespace-nowrap">{s.lens?.field_size || "—"}</td>
</tr>
</thead>
<tbody>
{filtered.map((s) => (
<tr key={s.submission_id} className="border-b hover:bg-muted/30">
<td className="px-2 py-2 whitespace-nowrap">
<Link href={detailHref(s.submission_id)} className="underline">
<span
dangerouslySetInnerHTML={{
__html: highlight(s.setting_title || "Untitled"),
}}
/>
</Link>
</td>
<td
className="px-2 py-2 whitespace-nowrap"
dangerouslySetInnerHTML={{
__html: highlight(ownerLabel(s.owner)),
}}
/>
<td
className="px-2 py-2 whitespace-nowrap"
dangerouslySetInnerHTML={{
__html: highlight(s.mat?.name || "—"),
}}
/>
<td
className="px-2 py-2 whitespace-nowrap"
dangerouslySetInnerHTML={{
__html: highlight(s.mat_coat?.name || "—"),
}}
/>
<td
className="px-2 py-2 whitespace-nowrap"
dangerouslySetInnerHTML={{
__html: highlight(s.source?.model || "—"),
}}
/>
<td
className="px-2 py-2 whitespace-nowrap"
dangerouslySetInnerHTML={{
__html: highlight(s.lens?.field_size || "—"),
}}
/>
</tr>
))}
</tbody>
</table>
</div>
)}
))}
</tbody>
</table>
</div>
)}
</div>
);
}