show my settings ONLY if exact owner match, removes uploader fallback
This commit is contained in:
parent
59d2d98a8c
commit
def73b0aa5
2 changed files with 193 additions and 64 deletions
|
|
@ -1,3 +1,4 @@
|
|||
// app/portal/my-settings/page.tsx
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
|
|
@ -33,7 +34,7 @@ function detailHref(coll: Coll, submissionId: string | number | null | undefined
|
|||
export default function MySettingsPage() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [meId, setMeId] = useState<string | null>(null);
|
||||
const [meUsername, setMeUsername] = useState<string | null>(null);
|
||||
const [meUsername, setMeUsername] = useState<string | null>(null); // used only for display
|
||||
const [q, setQ] = useState("");
|
||||
const [byColl, setByColl] = useState<Record<Coll, Row[]>>({
|
||||
settings_co2gal: [],
|
||||
|
|
@ -51,7 +52,11 @@ export default function MySettingsPage() {
|
|||
|
||||
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})`); }
|
||||
try {
|
||||
return text ? JSON.parse(text) : null;
|
||||
} catch {
|
||||
throw new Error(`Unexpected response (HTTP ${res.status})`);
|
||||
}
|
||||
}
|
||||
|
||||
// 1) Load current user id + username
|
||||
|
|
@ -79,9 +84,9 @@ export default function MySettingsPage() {
|
|||
return () => { dead = true; };
|
||||
}, []);
|
||||
|
||||
// 2) Load my items per collection (no id/status fields requested)
|
||||
// 2) Load my items per collection (STRICT owner-only)
|
||||
useEffect(() => {
|
||||
if (!meId && !meUsername) return;
|
||||
if (!meId) return; // we require the user id to filter by owner
|
||||
let dead = false;
|
||||
setLoading(true);
|
||||
setErrs((e) => ({ ...e, settings_co2gal: null, settings_co2gan: null, settings_fiber: null, settings_uv: null }));
|
||||
|
|
@ -93,17 +98,13 @@ export default function MySettingsPage() {
|
|||
const qs = new URLSearchParams();
|
||||
qs.set("limit", "-1");
|
||||
qs.set("sort", "-last_modified_date");
|
||||
qs.set("fields", "submission_id,setting_title,last_modified_date"); // ← only fields we’re allowed
|
||||
qs.set("fields", "submission_id,setting_title,last_modified_date"); // minimal, safe fields
|
||||
|
||||
// OR filter by owner and legacy uploader mirror
|
||||
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++;
|
||||
}
|
||||
// STRICT owner filter. We OR the two shapes:
|
||||
// - owner stored as primitive id
|
||||
// - owner stored as relation object { id: ... }
|
||||
qs.set(`filter[_or][0][owner][_eq]`, meId);
|
||||
qs.set(`filter[_or][1][owner][id][_eq]`, meId);
|
||||
|
||||
const url = `/api/dx/items/${coll}?${qs.toString()}`;
|
||||
|
||||
|
|
@ -129,7 +130,7 @@ export default function MySettingsPage() {
|
|||
})();
|
||||
|
||||
return () => { dead = true; };
|
||||
}, [meId, meUsername]);
|
||||
}, [meId]);
|
||||
|
||||
// 3) Filter client-side
|
||||
const filtered = useMemo(() => {
|
||||
|
|
|
|||
|
|
@ -1,79 +1,207 @@
|
|||
// app/settings/co2-galvo/[id]/co2-galvo.tsx
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useParams } from "next/navigation";
|
||||
import Markdown from "react-markdown";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useParams, useSearchParams } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import SettingsSubmit from "@/components/forms/SettingsSubmit";
|
||||
|
||||
type Rec = {
|
||||
submission_id: string | number;
|
||||
setting_title?: string | null;
|
||||
setting_notes?: string | null;
|
||||
|
||||
// files can be id or object with id
|
||||
photo?: { id?: string } | string | null;
|
||||
screen?: { id?: string } | string | null;
|
||||
|
||||
// shapes pass-through for form
|
||||
mat?: any;
|
||||
mat_coat?: any;
|
||||
mat_color?: any;
|
||||
mat_opacity?: any;
|
||||
mat_thickness?: number | null;
|
||||
|
||||
source?: any;
|
||||
lens?: any;
|
||||
focus?: number | null;
|
||||
|
||||
laser_soft?: any;
|
||||
repeat_all?: number | null;
|
||||
|
||||
fill_settings?: any[] | null;
|
||||
line_settings?: any[] | null;
|
||||
raster_settings?: any[] | null;
|
||||
|
||||
owner?: { id?: string | number; username?: string | null } | string | number | null;
|
||||
uploader?: string | null;
|
||||
|
||||
last_modified_date?: string | null;
|
||||
};
|
||||
|
||||
function ownerLabel(o: Rec["owner"]) {
|
||||
if (!o) return "—";
|
||||
if (typeof o === "string" || typeof o === "number") return String(o);
|
||||
return o.username || String(o.id ?? "—");
|
||||
}
|
||||
|
||||
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})`); }
|
||||
}
|
||||
|
||||
export default function CO2GalvoSettingDetailPage() {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const [setting, setSetting] = useState<any>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
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);
|
||||
|
||||
// Load record by submission_id (that's what the list links use)
|
||||
useEffect(() => {
|
||||
if (!id) return;
|
||||
let dead = false;
|
||||
|
||||
const fields = [
|
||||
"submission_id",
|
||||
"setting_title",
|
||||
"uploader",
|
||||
// ✅ parent + subfields for resilient owner
|
||||
"owner",
|
||||
"owner.id",
|
||||
"owner.username",
|
||||
"owner.first_name",
|
||||
"owner.last_name",
|
||||
"owner.email",
|
||||
// ... (rest of your fields)
|
||||
].join(",");
|
||||
(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setErr(null);
|
||||
|
||||
const url = `/api/dx/items/settings_co2gal/${encodeURIComponent(String(id))}?fields=${encodeURIComponent(fields)}`;
|
||||
const fields = [
|
||||
"submission_id",
|
||||
"setting_title",
|
||||
"setting_notes",
|
||||
"photo.id",
|
||||
"screen.id",
|
||||
"mat",
|
||||
"mat_coat",
|
||||
"mat_color",
|
||||
"mat_opacity",
|
||||
"mat_thickness",
|
||||
"source",
|
||||
"lens",
|
||||
"focus",
|
||||
"laser_soft",
|
||||
"repeat_all",
|
||||
"fill_settings",
|
||||
"line_settings",
|
||||
"raster_settings",
|
||||
"owner.id",
|
||||
"owner.username",
|
||||
"uploader",
|
||||
"last_modified_date",
|
||||
].join(",");
|
||||
|
||||
fetch(url, { cache: "no-store", credentials: "include" })
|
||||
.then(async (res) => {
|
||||
const txt = await res.text();
|
||||
const j = txt ? JSON.parse(txt) : null;
|
||||
if (!res.ok) throw new Error(j?.errors?.[0]?.message || j?.message || `HTTP ${res.status}`);
|
||||
return j;
|
||||
})
|
||||
.then((json) => setSetting(json?.data ?? null))
|
||||
.catch((e) => {
|
||||
console.error("CO2 Galvo detail fetch failed:", e);
|
||||
setSetting(null);
|
||||
})
|
||||
.finally(() => setLoading(false));
|
||||
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" });
|
||||
if (!r.ok) {
|
||||
const j = await readJson(r).catch(() => null);
|
||||
throw new Error(j?.errors?.[0]?.message || `HTTP ${r.status}`);
|
||||
}
|
||||
const j = await readJson(r);
|
||||
const row: Rec | null = Array.isArray(j?.data) ? j.data[0] || null : null;
|
||||
if (!row) throw new Error("Setting not found.");
|
||||
if (!dead) setRec(row);
|
||||
} catch (e: any) {
|
||||
if (!dead) setErr(e?.message || String(e));
|
||||
} finally {
|
||||
if (!dead) setLoading(false);
|
||||
}
|
||||
})();
|
||||
|
||||
return () => { dead = true; };
|
||||
}, [id]);
|
||||
|
||||
if (loading) return <p className="p-6">Loading setting...</p>;
|
||||
if (!setting) return <p className="p-6">Setting not found.</p>;
|
||||
const initialValues = useMemo(() => {
|
||||
if (!rec) return null;
|
||||
|
||||
const ownerDisplay =
|
||||
typeof setting?.owner === "object"
|
||||
? (setting.owner?.username ||
|
||||
[setting.owner?.first_name, setting.owner?.last_name].filter(Boolean).join(" ").trim() ||
|
||||
setting.owner?.email ||
|
||||
setting.owner?.id ||
|
||||
"—")
|
||||
: typeof setting?.owner === "string" || typeof setting?.owner === "number"
|
||||
? String(setting.owner)
|
||||
: "—";
|
||||
// normalize existing file refs to ids for the form
|
||||
const photoId = typeof rec.photo === "string" ? rec.photo : rec.photo?.id ?? null;
|
||||
const screenId = typeof rec.screen === "string" ? rec.screen : rec.screen?.id ?? null;
|
||||
|
||||
return {
|
||||
setting_title: rec.setting_title ?? "",
|
||||
setting_notes: rec.setting_notes ?? "",
|
||||
|
||||
photo: photoId,
|
||||
screen: screenId,
|
||||
|
||||
mat: rec.mat ?? null,
|
||||
mat_coat: rec.mat_coat ?? null,
|
||||
mat_color: rec.mat_color ?? null,
|
||||
mat_opacity: rec.mat_opacity ?? null,
|
||||
mat_thickness: rec.mat_thickness ?? null,
|
||||
|
||||
source: rec.source ?? null,
|
||||
lens: rec.lens ?? null,
|
||||
focus: rec.focus ?? null,
|
||||
|
||||
laser_soft: rec.laser_soft ?? null,
|
||||
repeat_all: rec.repeat_all ?? null,
|
||||
|
||||
fill_settings: rec.fill_settings ?? [],
|
||||
line_settings: rec.line_settings ?? [],
|
||||
raster_settings: rec.raster_settings ?? [],
|
||||
};
|
||||
}, [rec]);
|
||||
|
||||
if (loading) return <p className="p-6">Loading setting…</p>;
|
||||
if (err) return (
|
||||
<div className="p-6">
|
||||
<div className="rounded-md border border-red-300 bg-red-50 px-3 py-2 text-sm text-red-700">
|
||||
{err}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
if (!rec) return <p className="p-6">Setting not found.</p>;
|
||||
|
||||
// ── EDIT MODE ───────────────────────────────────────────────
|
||||
if (editMode && initialValues) {
|
||||
return (
|
||||
<main className="p-6 max-w-5xl mx-auto space-y-4">
|
||||
<h1 className="text-2xl font-semibold">Edit CO₂ Galvo Setting</h1>
|
||||
<SettingsSubmit
|
||||
mode="edit"
|
||||
initialTarget="settings_co2gal"
|
||||
submissionId={rec.submission_id}
|
||||
initialValues={initialValues}
|
||||
/>
|
||||
<div className="text-sm">
|
||||
<Link className="underline" href={`/settings/co2-galvo/${rec.submission_id}`}>← Back to view</Link>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
// ── VIEW MODE (read-only) ───────────────────────────────────
|
||||
const ownerDisplay = ownerLabel(rec.owner);
|
||||
|
||||
return (
|
||||
<div className="p-6 max-w-7xl mx-auto">
|
||||
<div className="card bg-card p-4 mb-4">
|
||||
<h1 className="text-3xl font-bold mb-2">{setting.setting_title || "Untitled"}</h1>
|
||||
<h1 className="text-3xl font-bold mb-2">{rec.setting_title || "Untitled"}</h1>
|
||||
|
||||
{/* TEMP DEBUG — remove once owner shows */}
|
||||
{/* Keep the debug block for now */}
|
||||
<pre className="text-xs bg-muted/30 rounded p-2 mb-2">
|
||||
{JSON.stringify({ owner: setting?.owner }, null, 2)}
|
||||
{JSON.stringify({ owner: rec?.owner }, null, 2)}
|
||||
</pre>
|
||||
|
||||
<div className="space-y-1 text-sm text-muted-foreground">
|
||||
<p><strong>Owner:</strong> {ownerDisplay}</p>
|
||||
<p><strong>Uploader:</strong> {setting.uploader || "—"}</p>
|
||||
<p><strong>Uploader:</strong> {rec.uploader || "—"}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* …rest of the page… */}
|
||||
<div className="pt-2">
|
||||
<Link className="underline" href={`/settings/co2-galvo/${rec.submission_id}?edit=1`}>
|
||||
Edit this setting
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue