fix to populate results in my-settings
This commit is contained in:
parent
b41fbd98a0
commit
9c7cfb3aaa
1 changed files with 153 additions and 103 deletions
|
|
@ -4,33 +4,27 @@
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
|
type Coll = "settings_co2gal" | "settings_co2gan" | "settings_fiber" | "settings_uv";
|
||||||
|
|
||||||
type Row = {
|
type Row = {
|
||||||
id: string | number;
|
id: string | number;
|
||||||
submission_id?: string | number | null;
|
submission_id?: string | number | null;
|
||||||
setting_title?: string | null;
|
setting_title?: string | null;
|
||||||
status?: string | null;
|
status?: string | null;
|
||||||
last_modified_date?: string | null;
|
last_modified_date?: string | null;
|
||||||
collection: "settings_co2gal" | "settings_co2gan" | "settings_fiber" | "settings_uv";
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const COLLECTIONS: Array<Row["collection"]> = [
|
const COLLECTIONS: Coll[] = ["settings_co2gal", "settings_co2gan", "settings_fiber", "settings_uv"];
|
||||||
"settings_co2gal",
|
const LABEL: Record<Coll, string> = {
|
||||||
"settings_co2gan",
|
|
||||||
"settings_fiber",
|
|
||||||
"settings_uv",
|
|
||||||
];
|
|
||||||
|
|
||||||
const LABEL: Record<Row["collection"], string> = {
|
|
||||||
settings_co2gal: "CO₂ Galvo",
|
settings_co2gal: "CO₂ Galvo",
|
||||||
settings_co2gan: "CO₂ Gantry",
|
settings_co2gan: "CO₂ Gantry",
|
||||||
settings_fiber: "Fiber",
|
settings_fiber: "Fiber",
|
||||||
settings_uv: "UV",
|
settings_uv: "UV",
|
||||||
};
|
};
|
||||||
|
|
||||||
// Route to the existing detail page for view/edit (you can customize)
|
function detailHref(coll: Coll, idOrSubmission: string | number | null | undefined) {
|
||||||
function detailHref(row: Row) {
|
const subId = idOrSubmission ?? "";
|
||||||
const subId = row.submission_id ?? row.id;
|
switch (coll) {
|
||||||
switch (row.collection) {
|
|
||||||
case "settings_co2gal": return `/settings/co2-galvo/${subId}?edit=1`;
|
case "settings_co2gal": return `/settings/co2-galvo/${subId}?edit=1`;
|
||||||
case "settings_co2gan": return `/settings/co2-gantry/${subId}?edit=1`;
|
case "settings_co2gan": return `/settings/co2-gantry/${subId}?edit=1`;
|
||||||
case "settings_fiber": return `/settings/fiber/${subId}?edit=1`;
|
case "settings_fiber": return `/settings/fiber/${subId}?edit=1`;
|
||||||
|
|
@ -40,103 +34,144 @@ function detailHref(row: Row) {
|
||||||
|
|
||||||
export default function MySettingsPage() {
|
export default function MySettingsPage() {
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [me, setMe] = useState<{ id: string; username?: string | null } | null>(null);
|
const [meId, setMeId] = useState<string | null>(null);
|
||||||
const [rows, setRows] = useState<Row[]>([]);
|
const [meUsername, setMeUsername] = useState<string | null>(null);
|
||||||
const [q, setQ] = useState("");
|
const [q, setQ] = useState("");
|
||||||
|
const [byColl, setByColl] = useState<Record<Coll, Row[]>>({
|
||||||
|
settings_co2gal: [],
|
||||||
|
settings_co2gan: [],
|
||||||
|
settings_fiber: [],
|
||||||
|
settings_uv: [],
|
||||||
|
});
|
||||||
|
const [errs, setErrs] = useState<Record<Coll | "me", string | null>>({ me: null, settings_co2gal: null, settings_co2gan: null, settings_fiber: null, settings_uv: null });
|
||||||
|
|
||||||
// 1) get current user id
|
// Safe JSON reader so HTML/404s don't explode parsing
|
||||||
|
async function readJson(res: Response) {
|
||||||
|
const text = await res.text();
|
||||||
|
try { return text ? JSON.parse(text) : null; } catch { throw new Error(`Unexpected response (HTTP ${res.status})`); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1) Who am I (id + username)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let canceled = false;
|
let dead = false;
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
const r = await fetch(`/api/dx/users/me?fields=id,username`, {
|
const r = await fetch(`/api/dx/users/me?fields=id,username`, { credentials: "include", cache: "no-store" });
|
||||||
credentials: "include",
|
if (!r.ok) {
|
||||||
cache: "no-store",
|
const j = await readJson(r).catch(() => null);
|
||||||
});
|
throw new Error(j?.errors?.[0]?.message || `HTTP ${r.status}`);
|
||||||
const j = await r.json();
|
}
|
||||||
const id = j?.data?.id ?? j?.id;
|
const j = await readJson(r);
|
||||||
if (!canceled) setMe(id ? { id: String(id), username: j?.data?.username ?? j?.username } : null);
|
const id = j?.data?.id ?? j?.id ?? null;
|
||||||
} catch {
|
const un = j?.data?.username ?? j?.username ?? null;
|
||||||
if (!canceled) setMe(null);
|
if (!dead) {
|
||||||
|
setMeId(id ? String(id) : null);
|
||||||
|
setMeUsername(un ? String(un) : null);
|
||||||
|
setErrs((e) => ({ ...e, me: null }));
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
if (!dead) setErrs((er) => ({ ...er, me: e?.message || String(e) }));
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
return () => { canceled = true; };
|
return () => { dead = true; };
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 2) fetch my settings from each collection
|
// 2) Load my items per collection with OR(filters)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!me?.id) return;
|
if (!meId && !meUsername) return;
|
||||||
let canceled = false;
|
let dead = false;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
setErrs((e) => ({ ...e, settings_co2gal: null, settings_co2gan: null, settings_fiber: null, settings_uv: null }));
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
const all: Row[] = [];
|
const acc: Record<Coll, Row[]> = { settings_co2gal: [], settings_co2gan: [], settings_fiber: [], settings_uv: [] };
|
||||||
|
|
||||||
for (const coll of COLLECTIONS) {
|
for (const coll of COLLECTIONS) {
|
||||||
const url = new URL(`/api/dx/items/${coll}`, window.location.origin);
|
const qs = new URLSearchParams();
|
||||||
url.searchParams.set("limit", "-1");
|
qs.set("limit", "-1");
|
||||||
url.searchParams.set("sort", "-last_modified_date");
|
qs.set("sort", "-last_modified_date");
|
||||||
url.searchParams.set("fields", "id,submission_id,setting_title,status,last_modified_date");
|
qs.set("fields", "id,submission_id,setting_title,status,last_modified_date");
|
||||||
url.searchParams.set("filter[owner][_eq]", me.id);
|
|
||||||
|
// Robust OR filter:
|
||||||
|
// - owner == meId
|
||||||
|
// - owner.id == meId (some Directus setups require nested id filter)
|
||||||
|
// - uploader == meUsername (fallback for legacy rows with missing owner)
|
||||||
|
let orIdx = 0;
|
||||||
|
if (meId) {
|
||||||
|
qs.set(`filter[_or][${orIdx}][owner][_eq]`, meId); orIdx++;
|
||||||
|
qs.set(`filter[_or][${orIdx}][owner][id][_eq]`, meId); orIdx++;
|
||||||
|
}
|
||||||
|
if (meUsername) {
|
||||||
|
qs.set(`filter[_or][${orIdx}][uploader][_eq]`, meUsername); orIdx++;
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = `/api/dx/items/${coll}?${qs.toString()}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const r = await fetch(url.toString(), { credentials: "include", cache: "no-store" });
|
const r = await fetch(url, { credentials: "include", cache: "no-store" });
|
||||||
const j = await r.json();
|
if (!r.ok) {
|
||||||
const data = Array.isArray(j?.data) ? j.data : [];
|
const j = await readJson(r).catch(() => null);
|
||||||
for (const item of data) {
|
throw new Error(j?.errors?.[0]?.message || `HTTP ${r.status}`);
|
||||||
all.push({
|
|
||||||
id: item.id,
|
|
||||||
submission_id: item.submission_id ?? null,
|
|
||||||
setting_title: item.setting_title ?? null,
|
|
||||||
status: item.status ?? null,
|
|
||||||
last_modified_date: item.last_modified_date ?? null,
|
|
||||||
collection: coll,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
const j = await readJson(r);
|
||||||
console.warn(`Failed to load ${coll}:`, e);
|
const rows: Row[] = Array.isArray(j?.data) ? j.data : [];
|
||||||
|
acc[coll] = rows;
|
||||||
|
} catch (e: any) {
|
||||||
|
acc[coll] = [];
|
||||||
|
setErrs((er) => ({ ...er, [coll]: e?.message || String(e) }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!canceled) {
|
if (!dead) {
|
||||||
setRows(all);
|
setByColl(acc);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
return () => { canceled = true; };
|
return () => { dead = true; };
|
||||||
}, [me?.id]);
|
}, [meId, meUsername]);
|
||||||
|
|
||||||
|
// 3) Filter client-side
|
||||||
const filtered = useMemo(() => {
|
const filtered = useMemo(() => {
|
||||||
const needle = q.trim().toLowerCase();
|
const needle = q.trim().toLowerCase();
|
||||||
if (!needle) return rows;
|
if (!needle) return byColl;
|
||||||
return rows.filter((r) =>
|
const out: Record<Coll, Row[]> = { settings_co2gal: [], settings_co2gan: [], settings_fiber: [], settings_uv: [] };
|
||||||
[r.setting_title, LABEL[r.collection], r.status, r.last_modified_date]
|
for (const coll of COLLECTIONS) {
|
||||||
.filter(Boolean)
|
out[coll] = (byColl[coll] || []).filter(r =>
|
||||||
.some((v) => String(v).toLowerCase().includes(needle))
|
[r.setting_title, r.status, r.last_modified_date]
|
||||||
);
|
.filter(Boolean)
|
||||||
}, [rows, q]);
|
.some(v => String(v).toLowerCase().includes(needle))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}, [byColl, q]);
|
||||||
|
|
||||||
async function onDelete(row: Row) {
|
async function onDelete(coll: Coll, row: Row) {
|
||||||
if (!confirm(`Delete "${row.setting_title || "Untitled"}" from ${LABEL[row.collection]}?`)) return;
|
if (!confirm(`Delete "${row.setting_title || "Untitled"}" from ${LABEL[coll]}?`)) return;
|
||||||
try {
|
try {
|
||||||
const r = await fetch(`/api/dx/items/${row.collection}/${row.id}`, {
|
const r = await fetch(`/api/dx/items/${coll}/${row.id}`, { method: "DELETE", credentials: "include" });
|
||||||
method: "DELETE",
|
|
||||||
credentials: "include",
|
|
||||||
});
|
|
||||||
if (!r.ok) {
|
if (!r.ok) {
|
||||||
const j = await r.json().catch(() => null);
|
const j = await readJson(r).catch(() => null);
|
||||||
throw new Error(j?.errors?.[0]?.message || `HTTP ${r.status}`);
|
throw new Error(j?.errors?.[0]?.message || `HTTP ${r.status}`);
|
||||||
}
|
}
|
||||||
setRows((prev) => prev.filter((x) => !(x.collection === row.collection && String(x.id) === String(row.id))));
|
setByColl(prev => ({ ...prev, [coll]: prev[coll].filter(x => String(x.id) !== String(row.id)) }));
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
alert(`Delete failed: ${e?.message || e}`);
|
alert(`Delete failed: ${e?.message || e}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const total = COLLECTIONS.reduce((n, c) => n + (filtered[c]?.length || 0), 0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="mx-auto max-w-6xl px-4 py-8">
|
<main className="mx-auto max-w-6xl px-4 py-8">
|
||||||
<h1 className="text-2xl font-semibold mb-4">My Settings</h1>
|
<h1 className="text-2xl font-semibold mb-4">My Settings</h1>
|
||||||
|
|
||||||
|
{!!errs.me && (
|
||||||
|
<div className="mb-4 rounded-md border border-red-300 bg-red-50 px-3 py-2 text-sm text-red-700">
|
||||||
|
Couldn’t load your profile: {errs.me}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="mb-4 flex items-center gap-3">
|
<div className="mb-4 flex items-center gap-3">
|
||||||
<input
|
<input
|
||||||
className="border rounded px-3 py-2 w-full max-w-md"
|
className="border rounded px-3 py-2 w-full max-w-md"
|
||||||
|
|
@ -144,45 +179,60 @@ export default function MySettingsPage() {
|
||||||
value={q}
|
value={q}
|
||||||
onChange={(e) => setQ(e.currentTarget.value)}
|
onChange={(e) => setQ(e.currentTarget.value)}
|
||||||
/>
|
/>
|
||||||
<span className="text-sm opacity-70">{rows.length} total</span>
|
<span className="text-sm opacity-70">{total} total</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<p>Loading…</p>
|
<p>Loading…</p>
|
||||||
) : filtered.length === 0 ? (
|
|
||||||
<p className="opacity-70">No settings yet.</p>
|
|
||||||
) : (
|
) : (
|
||||||
<div className="overflow-x-auto">
|
COLLECTIONS.map((coll) => {
|
||||||
<table className="min-w-full text-sm">
|
const rows = filtered[coll] || [];
|
||||||
<thead>
|
return (
|
||||||
<tr className="bg-muted">
|
<section key={coll} className="mb-8">
|
||||||
<th className="px-2 py-2 text-left">Title</th>
|
<div className="mb-2 flex items-center justify-between">
|
||||||
<th className="px-2 py-2 text-left">Collection</th>
|
<h2 className="text-lg font-semibold">
|
||||||
<th className="px-2 py-2 text-left">Status</th>
|
{LABEL[coll]} <span className="text-xs opacity-70">({rows.length})</span>
|
||||||
<th className="px-2 py-2 text-left">Updated</th>
|
</h2>
|
||||||
<th className="px-2 py-2 text-left">Actions</th>
|
{!!errs[coll] && (
|
||||||
</tr>
|
<span className="text-xs text-red-600">Error: {errs[coll]}</span>
|
||||||
</thead>
|
)}
|
||||||
<tbody>
|
</div>
|
||||||
{filtered.map((r) => (
|
{rows.length === 0 ? (
|
||||||
<tr key={`${r.collection}:${r.id}`} className="border-b hover:bg-muted/30">
|
<p className="opacity-70">No items.</p>
|
||||||
<td className="px-2 py-2">{r.setting_title || "Untitled"}</td>
|
) : (
|
||||||
<td className="px-2 py-2">{LABEL[r.collection]}</td>
|
<div className="overflow-x-auto">
|
||||||
<td className="px-2 py-2">{r.status || "—"}</td>
|
<table className="min-w-full text-sm">
|
||||||
<td className="px-2 py-2">{r.last_modified_date ? new Date(r.last_modified_date).toLocaleString() : "—"}</td>
|
<thead>
|
||||||
<td className="px-2 py-2">
|
<tr className="bg-muted">
|
||||||
<div className="flex items-center gap-3">
|
<th className="px-2 py-2 text-left">Title</th>
|
||||||
<Link href={detailHref(r)} className="underline">Edit</Link>
|
<th className="px-2 py-2 text-left">Status</th>
|
||||||
<button className="text-red-600 underline" onClick={() => onDelete(r)}>
|
<th className="px-2 py-2 text-left">Updated</th>
|
||||||
Delete
|
<th className="px-2 py-2 text-left">Actions</th>
|
||||||
</button>
|
</tr>
|
||||||
</div>
|
</thead>
|
||||||
</td>
|
<tbody>
|
||||||
</tr>
|
{rows.map((r) => (
|
||||||
))}
|
<tr key={`${coll}:${r.id}`} className="border-b hover:bg-muted/30">
|
||||||
</tbody>
|
<td className="px-2 py-2">{r.setting_title || "Untitled"}</td>
|
||||||
</table>
|
<td className="px-2 py-2">{r.status || "—"}</td>
|
||||||
</div>
|
<td className="px-2 py-2">
|
||||||
|
{r.last_modified_date ? new Date(r.last_modified_date).toLocaleString() : "—"}
|
||||||
|
</td>
|
||||||
|
<td className="px-2 py-2">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Link href={detailHref(coll, r.submission_id ?? r.id)} className="underline">Edit</Link>
|
||||||
|
<button className="text-red-600 underline" onClick={() => onDelete(coll, r)}>Delete</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
})
|
||||||
)}
|
)}
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue