list and details cleanup
This commit is contained in:
parent
6829f2840c
commit
2e8297d426
2 changed files with 125 additions and 135 deletions
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import SettingsSubmit from "@/components/forms/SettingsSubmit";
|
||||
|
||||
type Rec = {
|
||||
submission_id: string | number;
|
||||
|
|
@ -46,12 +47,13 @@ type Rec = {
|
|||
export default function CO2GalvoDetail({ id, editable }: { id: string | number; editable?: boolean }) {
|
||||
const router = useRouter();
|
||||
const sp = useSearchParams();
|
||||
const editMode = sp.get("edit") === "1";
|
||||
|
||||
const [rec, setRec] = useState<Rec | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [err, setErr] = useState<string | null>(null);
|
||||
|
||||
// current user id to show "Edit" for owners
|
||||
// me id for owner-only edit
|
||||
const [meId, setMeId] = useState<string | null>(null);
|
||||
|
||||
// Lightbox
|
||||
|
|
@ -78,18 +80,13 @@ export default function CO2GalvoDetail({ id, editable }: { id: string | number;
|
|||
(async () => {
|
||||
try {
|
||||
const r = await fetch(`/api/dx/users/me?fields=id`, { cache: "no-store", credentials: "include" });
|
||||
if (!r.ok) return;
|
||||
const t = await r.text();
|
||||
const j = t ? JSON.parse(t) : null;
|
||||
const idVal = j?.data?.id ?? j?.id ?? null;
|
||||
if (alive) setMeId(idVal ? String(idVal) : null);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
})();
|
||||
return () => {
|
||||
alive = false;
|
||||
};
|
||||
return () => { alive = false; };
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -148,9 +145,9 @@ export default function CO2GalvoDetail({ id, editable }: { id: string | number;
|
|||
"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" });
|
||||
const text = await r.text();
|
||||
|
|
@ -165,9 +162,7 @@ export default function CO2GalvoDetail({ id, editable }: { id: string | number;
|
|||
if (!dead) setLoading(false);
|
||||
}
|
||||
})();
|
||||
return () => {
|
||||
dead = true;
|
||||
};
|
||||
return () => { dead = true; };
|
||||
}, [id]);
|
||||
|
||||
if (loading) return <p className="p-6">Loading setting…</p>;
|
||||
|
|
@ -186,42 +181,49 @@ export default function CO2GalvoDetail({ id, editable }: { id: string | number;
|
|||
|
||||
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})` : "");
|
||||
[rec.source?.make, rec.source?.model].filter(Boolean).join(" ") + (rec.source?.nm ? ` (${rec.source.nm})` : "");
|
||||
|
||||
const ownerId =
|
||||
typeof rec.owner === "object"
|
||||
? rec.owner?.id != null
|
||||
? String(rec.owner.id)
|
||||
: null
|
||||
: rec.owner != null
|
||||
? String(rec.owner)
|
||||
: null;
|
||||
typeof rec.owner === "object" ? (rec.owner?.id != null ? String(rec.owner.id) : null) : rec.owner != null ? String(rec.owner) : null;
|
||||
|
||||
const isMine = meId && ownerId ? meId === ownerId : false;
|
||||
|
||||
// Small field renderer: label on top, value below (+ optional suffix)
|
||||
const Field = ({ label, value, suffix }: { label: string; value: any; suffix?: string }) => (
|
||||
<div className="space-y-1">
|
||||
<div className="text-xs uppercase tracking-wide text-muted-foreground">{label}</div>
|
||||
<div className="text-sm break-words">
|
||||
{value != null && value !== "" ? (
|
||||
<>
|
||||
{String(value)}
|
||||
{suffix ? <span className="opacity-70"> {suffix}</span> : null}
|
||||
</>
|
||||
) : (
|
||||
"—"
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
// Small field renderer (label on top, value below). Accepts React nodes.
|
||||
const Field = ({ label, value, suffix }: { label: string; value: React.ReactNode | string | number | null | undefined; suffix?: string }) => {
|
||||
const primitive =
|
||||
typeof value === "string" || typeof value === "number" || typeof value === "boolean";
|
||||
const isEmpty = value == null || value === "" || (typeof value === "number" && isNaN(value as number));
|
||||
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
<div className="text-xs uppercase tracking-wide text-muted-foreground">{label}</div>
|
||||
<div className="text-sm break-words">
|
||||
{isEmpty ? (
|
||||
"—"
|
||||
) : primitive ? (
|
||||
<>
|
||||
{String(value)}
|
||||
{suffix ? <span className="opacity-70"> {suffix}</span> : null}
|
||||
</>
|
||||
) : (
|
||||
// render React node directly (e.g., Notes paragraph)
|
||||
value
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const openEdit = () => {
|
||||
const q = new URLSearchParams(sp.toString());
|
||||
q.set("edit", "1");
|
||||
router.replace(`?${q.toString()}`, { scroll: false });
|
||||
};
|
||||
const closeEdit = () => {
|
||||
const q = new URLSearchParams(sp.toString());
|
||||
q.delete("edit");
|
||||
router.replace(`?${q.toString()}`, { scroll: false });
|
||||
};
|
||||
|
||||
// Pretty labels
|
||||
const TYPE_LABEL: Record<string, string> = {
|
||||
|
|
@ -231,6 +233,52 @@ export default function CO2GalvoDetail({ id, editable }: { id: string | number;
|
|||
};
|
||||
const DITHER_LABEL = (v: string | undefined) => (v ? v.charAt(0).toUpperCase() + v.slice(1) : "—");
|
||||
|
||||
// ----- EDIT MODE (restore working behavior) -----
|
||||
if (editMode && rec) {
|
||||
const toId = (v: any) =>
|
||||
v == null ? "" : typeof v === "object" ? (v.id ?? v.submission_id ?? "") : String(v);
|
||||
|
||||
const initialValues = {
|
||||
submission_id: rec.submission_id,
|
||||
setting_title: rec.setting_title ?? "",
|
||||
setting_notes: rec.setting_notes ?? "",
|
||||
photo: photoId ? String(photoId) : null,
|
||||
screen: screenId ? String(screenId) : null,
|
||||
// Material
|
||||
mat: toId(rec.mat) || "",
|
||||
mat_coat: toId(rec.mat_coat) || "",
|
||||
mat_color: toId(rec.mat_color) || "",
|
||||
mat_opacity: toId(rec.mat_opacity) || "",
|
||||
mat_thickness: rec.mat_thickness ?? null,
|
||||
// Rig & Optics
|
||||
laser_soft: typeof rec.laser_soft === "object" ? String(rec.laser_soft?.id ?? "") : String(rec.laser_soft ?? "") || "",
|
||||
source: rec.source && typeof rec.source === "object" ? String(rec.source.submission_id ?? "") : String(rec.source ?? "") || "",
|
||||
lens: toId(rec.lens) || "",
|
||||
focus: rec.focus ?? null,
|
||||
// CO2 triplet
|
||||
lens_conf: toId(rec.lens_conf) || "",
|
||||
lens_apt: toId(rec.lens_apt) || "",
|
||||
lens_exp: toId(rec.lens_exp) || "",
|
||||
repeat_all: rec.repeat_all ?? null,
|
||||
// Repeaters
|
||||
fill_settings: rec.fill_settings ?? [],
|
||||
line_settings: rec.line_settings ?? [],
|
||||
raster_settings: rec.raster_settings ?? [],
|
||||
};
|
||||
|
||||
return (
|
||||
<main className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-xl lg:text-2xl font-semibold">Edit CO₂ Galvo Setting</h1>
|
||||
<button className="px-2 py-1 border rounded" onClick={closeEdit}>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
<SettingsSubmit mode="edit" submissionId={rec.submission_id} initialValues={initialValues} />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
|
|
@ -246,16 +294,20 @@ export default function CO2GalvoDetail({ id, editable }: { id: string | number;
|
|||
<div className="text-sm text-muted-foreground">Last modified: {rec.last_modified_date || "—"}</div>
|
||||
</header>
|
||||
|
||||
{/* Info (full width) */}
|
||||
<section className="grid gap-3">
|
||||
{/* Top row: Info (left) + Images (right) */}
|
||||
<section className="grid md:grid-cols-2 gap-6 items-start">
|
||||
{/* Info */}
|
||||
<div className="grid gap-3">
|
||||
<Field label="Owner" value={ownerLabel(rec.owner)} />
|
||||
<Field label="Uploader" value={rec.uploader || "—"} />
|
||||
{rec.setting_notes ? <Field label="Notes" value={<p className="whitespace-pre-wrap">{rec.setting_notes}</p>} /> : null}
|
||||
</section>
|
||||
{rec.setting_notes ? (
|
||||
<Field label="Notes" value={<p className="whitespace-pre-wrap">{rec.setting_notes}</p>} />
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{/* Images (full width, click to expand, ~75% smaller) */}
|
||||
{/* Images (side-by-side thumbnails) */}
|
||||
{(photoSrc || screenSrc) && (
|
||||
<section className="grid grid-cols-2 gap-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{photoSrc ? (
|
||||
<figure className="space-y-1 justify-self-start">
|
||||
<div
|
||||
|
|
@ -280,10 +332,11 @@ export default function CO2GalvoDetail({ id, editable }: { id: string | number;
|
|||
<figcaption className="text-xs text-muted-foreground text-center">Settings Screenshot</figcaption>
|
||||
</figure>
|
||||
) : null}
|
||||
</section>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* Two columns: left Rig & Optics, right Material */}
|
||||
{/* Two columns below: left Rig & Optics, right Material */}
|
||||
<section className="grid md:grid-cols-2 gap-6">
|
||||
{/* Rig & Optics */}
|
||||
<div className="space-y-3">
|
||||
|
|
@ -334,7 +387,7 @@ export default function CO2GalvoDetail({ id, editable }: { id: string | number;
|
|||
<Field label="Type" value={TYPE_LABEL[r.type] || "—"} />
|
||||
<Field label="Power" value={r.power ?? "—"} suffix="%" />
|
||||
<Field label="Speed" value={r.speed ?? "—"} suffix="mm/s" />
|
||||
<Field label="Interval" value={r.interval ?? "—"} />
|
||||
<Field label="Interval" value={r.interval ?? "—"} suffix="mm" />
|
||||
<Field label="Angle" value={r.angle ?? "—"} suffix="°" />
|
||||
<Field label="Pass" value={r.pass ?? "—"} />
|
||||
<Field label="Frequency" value={r.frequency ?? "—"} suffix="kHz" />
|
||||
|
|
@ -391,7 +444,7 @@ export default function CO2GalvoDetail({ id, editable }: { id: string | number;
|
|||
<Field label="Dither" value={DITHER_LABEL(r.dither)} />
|
||||
<Field label="Power" value={r.power ?? "—"} suffix="%" />
|
||||
<Field label="Speed" value={r.speed ?? "—"} suffix="mm/s" />
|
||||
<Field label="Interval" value={r.interval ?? "—"} />
|
||||
<Field label="Interval" value={r.interval ?? "—"} suffix="mm" />
|
||||
<Field label="Pass" value={r.pass ?? "—"} />
|
||||
<Field label="Frequency" value={r.frequency ?? "—"} suffix="kHz" />
|
||||
<Field label="Pulse" value={r.pulse ?? "—"} suffix="ns" />
|
||||
|
|
@ -410,16 +463,8 @@ export default function CO2GalvoDetail({ id, editable }: { id: string | number;
|
|||
|
||||
{/* Lightbox */}
|
||||
{viewerSrc && (
|
||||
<div
|
||||
className="fixed inset-0 z-50 bg-black/80 p-4 flex items-center justify-center"
|
||||
onClick={() => setViewerSrc(null)}
|
||||
>
|
||||
<img
|
||||
src={viewerSrc}
|
||||
alt=""
|
||||
className="max-w-full max-h-full cursor-zoom-out"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
<div className="fixed inset-0 z-50 bg-black/80 p-4 flex items-center justify-center" onClick={() => setViewerSrc(null)}>
|
||||
<img src={viewerSrc} alt="" className="max-w-full max-h-full cursor-zoom-out" onClick={(e) => e.stopPropagation()} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -51,32 +51,27 @@ export default function CO2GalvoList({
|
|||
const [rows, setRows] = useState<Row[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [localQuery, setLocalQuery] = useState(queryText ?? "");
|
||||
|
||||
// id -> username map (fix showing UUIDs)
|
||||
const [ownerMap, setOwnerMap] = useState<Record<string, string>>({});
|
||||
// current user id for "Edit" visibility
|
||||
const [meId, setMeId] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (queryText !== undefined) setLocalQuery(queryText);
|
||||
}, [queryText]);
|
||||
|
||||
// Load current user id
|
||||
// who am I?
|
||||
useEffect(() => {
|
||||
let alive = true;
|
||||
let live = true;
|
||||
(async () => {
|
||||
try {
|
||||
const r = await fetch(`${API}/users/me?fields=id`, { credentials: "include", cache: "no-store" });
|
||||
if (!r.ok) return;
|
||||
const j = await r.json().catch(() => null);
|
||||
const id = j?.data?.id ?? j?.id ?? null;
|
||||
if (alive) setMeId(id ? String(id) : null);
|
||||
const r = await fetch(`/api/dx/users/me?fields=id`, { cache: "no-store", credentials: "include" });
|
||||
const j = await readJson(r);
|
||||
const idVal = j?.data?.id ?? j?.id ?? null;
|
||||
if (live) setMeId(idVal ? String(idVal) : null);
|
||||
} catch {
|
||||
/* ignore */
|
||||
if (live) setMeId(null);
|
||||
}
|
||||
})();
|
||||
return () => {
|
||||
alive = false;
|
||||
live = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
|
@ -113,62 +108,9 @@ export default function CO2GalvoList({
|
|||
};
|
||||
}, []);
|
||||
|
||||
// Resolve owner usernames when only id/UUID is present
|
||||
useEffect(() => {
|
||||
const ids = new Set<string>();
|
||||
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<string, string> = {};
|
||||
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 */
|
||||
}
|
||||
}
|
||||
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") {
|
||||
const id = String(o);
|
||||
return ownerMap[id] || id;
|
||||
}
|
||||
if (typeof o === "string" || typeof o === "number") return String(o);
|
||||
return (
|
||||
o.username ||
|
||||
[o.first_name, o.last_name].filter(Boolean).join(" ").trim() ||
|
||||
|
|
@ -177,15 +119,13 @@ export default function CO2GalvoList({
|
|||
);
|
||||
};
|
||||
|
||||
const isMine = (o: Owner) => {
|
||||
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 withEditParam = (href: string) => (href.includes("?") ? `${href}&edit=1` : `${href}?edit=1`);
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
const q = (localQuery || "").toLowerCase();
|
||||
if (!q) return rows;
|
||||
|
|
@ -194,7 +134,9 @@ export default function CO2GalvoList({
|
|||
.filter(Boolean)
|
||||
.some((v) => String(v).toLowerCase().includes(q))
|
||||
);
|
||||
}, [rows, localQuery, ownerMap]);
|
||||
}, [rows, localQuery]);
|
||||
|
||||
const addEditParam = (href: string) => (href.includes("?") ? `${href}&edit=1` : `${href}?edit=1`);
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
|
|
@ -226,22 +168,25 @@ export default function CO2GalvoList({
|
|||
</thead>
|
||||
<tbody className="divide-y">
|
||||
{filtered.map((r) => {
|
||||
const href = linkFor(r.submission_id);
|
||||
const mine = isMine(r.owner);
|
||||
const ownerText = ownerLabel(r.owner) + (mine ? " (you)" : "");
|
||||
const viewHref = linkFor(r.submission_id);
|
||||
const editHref = addEditParam(viewHref);
|
||||
return (
|
||||
<tr key={r.submission_id} className="hover:bg-muted/40">
|
||||
<td className="px-2 py-2 truncate">
|
||||
<Link href={href} className="underline">
|
||||
<Link href={viewHref} className="underline">
|
||||
{r.setting_title || "Untitled"}
|
||||
</Link>
|
||||
</td>
|
||||
<td className="px-2 py-2 truncate">{ownerLabel(r.owner)}</td>
|
||||
<td className="px-2 py-2 truncate">{ownerText}</td>
|
||||
<td className="px-2 py-2 truncate">{r.mat?.name || "—"}</td>
|
||||
<td className="px-2 py-2 truncate">{r.mat_coat?.name || "—"}</td>
|
||||
<td className="px-2 py-2 truncate">{r.source?.model || "—"}</td>
|
||||
<td className="px-2 py-2 truncate">{r.lens?.field_size || "—"}</td>
|
||||
<td className="px-2 py-2">
|
||||
{isMine(r.owner) ? (
|
||||
<Link href={withEditParam(href)} className="underline">
|
||||
{mine ? (
|
||||
<Link href={editHref} className="underline">
|
||||
Edit
|
||||
</Link>
|
||||
) : (
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue