list and details cleanup

This commit is contained in:
makearmy 2025-10-05 21:52:16 -04:00
parent e8ba98aa91
commit c59ef98fd9
2 changed files with 269 additions and 250 deletions

View file

@ -51,43 +51,14 @@ export default function CO2GalvoList({
const [rows, setRows] = useState<Row[]>([]);
const [loading, setLoading] = useState(true);
const [localQuery, setLocalQuery] = useState(queryText ?? "");
const [meId, setMeId] = useState<string | null>(null);
// id -> username map (fix showing UUIDs)
const [ownerMap, setOwnerMap] = useState<Record<string, string>>({});
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 () => {
@ -121,9 +92,62 @@ export default function CO2GalvoList({
};
}, []);
// Resolve owner usernames when we only have an id/UUID
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 batch errors */
}
}
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") return String(o);
if (typeof o === "string" || typeof o === "number") {
const id = String(o);
return ownerMap[id] || id; // prefer resolved username
}
return (
o.username ||
[o.first_name, o.last_name].filter(Boolean).join(" ").trim() ||
@ -132,15 +156,6 @@ export default function CO2GalvoList({
);
};
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;
@ -149,7 +164,7 @@ export default function CO2GalvoList({
.filter(Boolean)
.some((v) => String(v).toLowerCase().includes(q))
);
}, [rows, localQuery]);
}, [rows, localQuery, ownerMap]);
return (
<div className="space-y-3">
@ -170,43 +185,29 @@ export default function CO2GalvoList({
<table className="w-full table-fixed text-sm">
<thead className="border-b">
<tr>
<th className="px-2 py-2 text-left w-[28%]">Title</th>
<th className="px-2 py-2 text-left w-[30%]">Title</th>
<th className="px-2 py-2 text-left w-[16%]">Owner</th>
<th className="px-2 py-2 text-left w-[14%]">Material</th>
<th className="px-2 py-2 text-left w-[14%]">Coating</th>
<th className="px-2 py-2 text-left w-[14%]">Model</th>
<th className="px-2 py-2 text-left w-[10%]">Field</th>
<th className="px-2 py-2 text-left w-[4%]">Edit</th>
</tr>
</thead>
<tbody className="divide-y">
{filtered.map((r) => {
const href = linkFor(r.submission_id);
const mine = isMine(r.owner);
return (
<tr key={r.submission_id} className="hover:bg-muted/40">
<td className="px-2 py-2 truncate">
<Link href={href} className="underline">
{r.setting_title || "Untitled"}
</Link>
</td>
<td className="px-2 py-2 truncate">{ownerLabel(r.owner)}{mine ? " (you)" : ""}</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">
{mine ? (
<Link href={withEdit(href)} className="underline">
Edit
</Link>
) : (
<span className="opacity-50"></span>
)}
</td>
</tr>
);
})}
{filtered.map((r) => (
<tr key={r.submission_id} className="hover:bg-muted/40">
<td className="px-2 py-2 truncate">
<Link href={linkFor(r.submission_id)} 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">{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>
</tr>
))}
</tbody>
</table>
</div>