show my settings ONLY if exact owner match, removes uploader fallback

This commit is contained in:
makearmy 2025-10-02 22:42:28 -04:00
parent 59d2d98a8c
commit def73b0aa5
2 changed files with 193 additions and 64 deletions

View file

@ -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 were 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(() => {

View file

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