fix to populate results in my-settings

This commit is contained in:
makearmy 2025-10-02 22:25:26 -04:00
parent 9c7cfb3aaa
commit 59d2d98a8c
2 changed files with 85 additions and 29 deletions

View 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 });
}
}

View file

@ -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 were 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>