fix to populate results in my-settings
This commit is contained in:
parent
9c7cfb3aaa
commit
59d2d98a8c
2 changed files with 85 additions and 29 deletions
48
app/api/my-settings/delete/route.ts
Normal file
48
app/api/my-settings/delete/route.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import { NextResponse } from "next/server";
|
||||
import { requireBearer } from "@/app/api/_lib/auth";
|
||||
import { dxGET, directusAdminFetch } from "@/lib/directus";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
|
||||
/**
|
||||
* Deletes a settings row by submission_id, but only if the caller owns it.
|
||||
* Body: { collection: "settings_co2gal" | "settings_co2gan" | "settings_fiber" | "settings_uv", submission_id: string|number }
|
||||
*/
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
const { collection, submission_id } = await req.json();
|
||||
|
||||
if (
|
||||
!collection ||
|
||||
!["settings_co2gal", "settings_co2gan", "settings_fiber", "settings_uv"].includes(collection) ||
|
||||
(submission_id === undefined || submission_id === null || String(submission_id) === "")
|
||||
) {
|
||||
return NextResponse.json({ error: "Invalid request" }, { status: 400 });
|
||||
}
|
||||
|
||||
// Who is calling?
|
||||
const bearer = requireBearer(req);
|
||||
const me = await dxGET<any>("/users/me?fields=id", bearer);
|
||||
const meId = me?.data?.id ?? me?.id;
|
||||
if (!meId) return NextResponse.json({ error: "Unable to resolve user" }, { status: 401 });
|
||||
|
||||
// Find the item by submission_id using admin token (we cannot read id with user perms)
|
||||
const q = `/items/${collection}?filter[submission_id][_eq]=${encodeURIComponent(
|
||||
String(submission_id)
|
||||
)}&fields=id,owner.id&limit=1`;
|
||||
const found = await directusAdminFetch<any>(q);
|
||||
const row = Array.isArray(found?.data) ? found.data[0] : null;
|
||||
if (!row?.id) return NextResponse.json({ error: "Not found" }, { status: 404 });
|
||||
|
||||
const ownerId = row?.owner?.id ?? row?.owner;
|
||||
if (String(ownerId) !== String(meId)) {
|
||||
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
||||
}
|
||||
|
||||
// Delete via admin
|
||||
await directusAdminFetch(`/items/${collection}/${row.id}`, { method: "DELETE" });
|
||||
return NextResponse.json({ ok: true });
|
||||
} catch (e: any) {
|
||||
return NextResponse.json({ error: e?.message || "Unknown error" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
// app/portal/my-settings/page.tsx
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
|
|
@ -7,11 +6,10 @@ import Link from "next/link";
|
|||
type Coll = "settings_co2gal" | "settings_co2gan" | "settings_fiber" | "settings_uv";
|
||||
|
||||
type Row = {
|
||||
id: string | number;
|
||||
submission_id?: string | number | null;
|
||||
submission_id: string | number;
|
||||
setting_title?: string | null;
|
||||
status?: string | null;
|
||||
last_modified_date?: string | null;
|
||||
// NOTE: we intentionally do NOT request id or status due to permissions
|
||||
};
|
||||
|
||||
const COLLECTIONS: Coll[] = ["settings_co2gal", "settings_co2gan", "settings_fiber", "settings_uv"];
|
||||
|
|
@ -22,13 +20,13 @@ const LABEL: Record<Coll, string> = {
|
|||
settings_uv: "UV",
|
||||
};
|
||||
|
||||
function detailHref(coll: Coll, idOrSubmission: string | number | null | undefined) {
|
||||
const subId = idOrSubmission ?? "";
|
||||
function detailHref(coll: Coll, submissionId: string | number | null | undefined) {
|
||||
const sid = submissionId ?? "";
|
||||
switch (coll) {
|
||||
case "settings_co2gal": return `/settings/co2-galvo/${subId}?edit=1`;
|
||||
case "settings_co2gan": return `/settings/co2-gantry/${subId}?edit=1`;
|
||||
case "settings_fiber": return `/settings/fiber/${subId}?edit=1`;
|
||||
case "settings_uv": return `/settings/uv/${subId}?edit=1`;
|
||||
case "settings_co2gal": return `/settings/co2-galvo/${sid}?edit=1`;
|
||||
case "settings_co2gan": return `/settings/co2-gantry/${sid}?edit=1`;
|
||||
case "settings_fiber": return `/settings/fiber/${sid}?edit=1`;
|
||||
case "settings_uv": return `/settings/uv/${sid}?edit=1`;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -43,15 +41,20 @@ export default function MySettingsPage() {
|
|||
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 });
|
||||
const [errs, setErrs] = useState<Record<Coll | "me", string | null>>({
|
||||
me: null,
|
||||
settings_co2gal: null,
|
||||
settings_co2gan: null,
|
||||
settings_fiber: null,
|
||||
settings_uv: null,
|
||||
});
|
||||
|
||||
// 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)
|
||||
// 1) Load current user id + username
|
||||
useEffect(() => {
|
||||
let dead = false;
|
||||
(async () => {
|
||||
|
|
@ -76,7 +79,7 @@ export default function MySettingsPage() {
|
|||
return () => { dead = true; };
|
||||
}, []);
|
||||
|
||||
// 2) Load my items per collection with OR(filters)
|
||||
// 2) Load my items per collection (no id/status fields requested)
|
||||
useEffect(() => {
|
||||
if (!meId && !meUsername) return;
|
||||
let dead = false;
|
||||
|
|
@ -90,12 +93,9 @@ export default function MySettingsPage() {
|
|||
const qs = new URLSearchParams();
|
||||
qs.set("limit", "-1");
|
||||
qs.set("sort", "-last_modified_date");
|
||||
qs.set("fields", "id,submission_id,setting_title,status,last_modified_date");
|
||||
qs.set("fields", "submission_id,setting_title,last_modified_date"); // ← only fields we’re allowed
|
||||
|
||||
// Robust OR filter:
|
||||
// - owner == meId
|
||||
// - owner.id == meId (some Directus setups require nested id filter)
|
||||
// - uploader == meUsername (fallback for legacy rows with missing owner)
|
||||
// OR filter by owner and legacy uploader mirror
|
||||
let orIdx = 0;
|
||||
if (meId) {
|
||||
qs.set(`filter[_or][${orIdx}][owner][_eq]`, meId); orIdx++;
|
||||
|
|
@ -138,7 +138,7 @@ export default function MySettingsPage() {
|
|||
const out: Record<Coll, Row[]> = { settings_co2gal: [], settings_co2gan: [], settings_fiber: [], settings_uv: [] };
|
||||
for (const coll of COLLECTIONS) {
|
||||
out[coll] = (byColl[coll] || []).filter(r =>
|
||||
[r.setting_title, r.status, r.last_modified_date]
|
||||
[r.setting_title, r.last_modified_date]
|
||||
.filter(Boolean)
|
||||
.some(v => String(v).toLowerCase().includes(needle))
|
||||
);
|
||||
|
|
@ -147,14 +147,24 @@ export default function MySettingsPage() {
|
|||
}, [byColl, q]);
|
||||
|
||||
async function onDelete(coll: Coll, row: Row) {
|
||||
if (!row.submission_id) return alert("Missing submission id.");
|
||||
if (!confirm(`Delete "${row.setting_title || "Untitled"}" from ${LABEL[coll]}?`)) return;
|
||||
|
||||
try {
|
||||
const r = await fetch(`/api/dx/items/${coll}/${row.id}`, { method: "DELETE", credentials: "include" });
|
||||
if (!r.ok) {
|
||||
const j = await readJson(r).catch(() => null);
|
||||
throw new Error(j?.errors?.[0]?.message || `HTTP ${r.status}`);
|
||||
const r = await fetch(`/api/my-settings/delete`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ collection: coll, submission_id: row.submission_id }),
|
||||
});
|
||||
const j = await readJson(r).catch(() => null);
|
||||
if (!r.ok || !j?.ok) {
|
||||
throw new Error(j?.error || `HTTP ${r.status}`);
|
||||
}
|
||||
setByColl(prev => ({ ...prev, [coll]: prev[coll].filter(x => String(x.id) !== String(row.id)) }));
|
||||
setByColl(prev => ({
|
||||
...prev,
|
||||
[coll]: prev[coll].filter(x => String(x.submission_id) !== String(row.submission_id))
|
||||
}));
|
||||
} catch (e: any) {
|
||||
alert(`Delete failed: ${e?.message || e}`);
|
||||
}
|
||||
|
|
@ -205,22 +215,20 @@ export default function MySettingsPage() {
|
|||
<thead>
|
||||
<tr className="bg-muted">
|
||||
<th className="px-2 py-2 text-left">Title</th>
|
||||
<th className="px-2 py-2 text-left">Status</th>
|
||||
<th className="px-2 py-2 text-left">Updated</th>
|
||||
<th className="px-2 py-2 text-left">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.map((r) => (
|
||||
<tr key={`${coll}:${r.id}`} className="border-b hover:bg-muted/30">
|
||||
<tr key={`${coll}:${r.submission_id}`} className="border-b hover:bg-muted/30">
|
||||
<td className="px-2 py-2">{r.setting_title || "Untitled"}</td>
|
||||
<td className="px-2 py-2">{r.status || "—"}</td>
|
||||
<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>
|
||||
<Link href={detailHref(coll, r.submission_id)} className="underline">Edit</Link>
|
||||
<button className="text-red-600 underline" onClick={() => onDelete(coll, r)}>Delete</button>
|
||||
</div>
|
||||
</td>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue