co2-galvo owner testing
This commit is contained in:
parent
a100fefc77
commit
30ac27815f
2 changed files with 69 additions and 242 deletions
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue