added edit flow

This commit is contained in:
makearmy 2025-10-02 23:12:24 -04:00
parent 4e2fd5d813
commit fd0553f49f
3 changed files with 771 additions and 512 deletions

View file

@ -15,15 +15,15 @@ type Rec = {
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;
// relations (may be id or object)
mat?: { id?: string | number } | string | number | null;
mat_coat?: { id?: string | number } | string | number | null;
mat_color?: { id?: string | number } | string | number | null;
mat_opacity?: { id?: string | number } | string | number | null;
mat_thickness?: number | null;
source?: any;
lens?: any;
source?: { submission_id?: string | number } | string | number | null;
lens?: { id?: string | number } | string | number | null;
focus?: number | null;
laser_soft?: any;
@ -69,25 +69,35 @@ export default function CO2GalvoSettingDetailPage() {
setLoading(true);
setErr(null);
// Request the specific IDs needed to prefill selects
const fields = [
"submission_id",
"setting_title",
"setting_notes",
"photo.id",
"screen.id",
"mat",
"mat_coat",
"mat_color",
"mat_opacity",
// relations: request their ids explicitly
"mat.id",
"mat_coat.id",
"mat_color.id",
"mat_opacity.id",
"mat_thickness",
"source",
"lens",
// source is keyed by submission_id in the selector
"source.submission_id",
// lens select expects numeric id
"lens.id",
"focus",
"laser_soft",
"repeat_all",
"fill_settings",
"line_settings",
"raster_settings",
"owner.id",
"owner.username",
"uploader",
@ -121,8 +131,20 @@ export default function CO2GalvoSettingDetailPage() {
if (!rec) return null;
// 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;
const photoId = typeof rec.photo === "string" || typeof rec.photo === "number" ? String(rec.photo) : rec.photo?.id ?? null;
const screenId = typeof rec.screen === "string" || typeof rec.screen === "number" ? String(rec.screen) : rec.screen?.id ?? null;
// normalize relations to id strings expected by <select>
const toId = (v: any) =>
v == null ? null : (typeof v === "object" ? (v.id ?? v.submission_id ?? null) : v);
const matId = toId(rec.mat);
const coatId = toId(rec.mat_coat);
const colorId = toId(rec.mat_color);
const opacityId = toId(rec.mat_opacity);
const lensId = toId(rec.lens);
// laser_source options use submission_id as value
const sourceId = rec.source && typeof rec.source === "object" ? (rec.source.submission_id ?? null) : rec.source ?? null;
return {
setting_title: rec.setting_title ?? "",
@ -131,22 +153,22 @@ export default function CO2GalvoSettingDetailPage() {
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,
mat: matId ? String(matId) : null,
mat_coat: coatId ? String(coatId) : null,
mat_color: colorId ? String(colorId) : null,
mat_opacity: opacityId ? String(opacityId) : null,
mat_thickness: rec.mat_thickness ?? null,
source: rec.source ?? null,
lens: rec.lens ?? null,
focus: rec.focus ?? null,
source: sourceId != null ? String(sourceId) : null,
lens: lensId != null ? String(lensId) : null,
focus: rec.focus ?? null,
laser_soft: rec.laser_soft ?? null,
repeat_all: rec.repeat_all ?? 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 ?? [],
fill_settings: rec.fill_settings ?? [],
line_settings: rec.line_settings ?? [],
raster_settings: rec.raster_settings ?? [],
};
}, [rec]);

View file

@ -8,7 +8,13 @@ import { useSearchParams } from "next/navigation";
type Owner =
| string
| number
| { id?: string | number; username?: string | null; first_name?: string | null; last_name?: string | null; email?: string | null }
| {
id?: string | number;
username?: string | null;
first_name?: string | null;
last_name?: string | null;
email?: string | null;
}
| null
| undefined;
@ -34,6 +40,9 @@ export default function CO2GalvoSettingsPage() {
const [loading, setLoading] = useState(true);
const [resolvingOwners, setResolvingOwners] = useState(false);
// current signed-in user id (for "Edit" visibility)
const [meId, setMeId] = useState<string | null>(null);
// Debounce search box
useEffect(() => {
const t = setTimeout(() => setDebouncedQuery(query), 300);
@ -50,13 +59,35 @@ export default function CO2GalvoSettingsPage() {
}
}
// Load current user id (ignore errors; unauth just means no edit buttons)
useEffect(() => {
let dead = false;
(async () => {
try {
const r = await fetch(`/api/dx/users/me?fields=id`, {
cache: "no-store",
credentials: "include",
});
if (!r.ok) return; // unauth or error → no edit display
const j = await readJson(r);
const id = j?.data?.id ?? j?.id ?? null;
if (!dead) setMeId(id ? String(id) : null);
} catch {
/* swallow */
}
})();
return () => {
dead = true;
};
}, []);
useEffect(() => {
// ✅ include parent field `owner` AND subfields; use auth proxy
const fields = [
"submission_id",
"setting_title",
"uploader",
"owner", // ← add comma here
"owner",
"owner.id",
"owner.username",
"photo.id",
@ -67,7 +98,9 @@ export default function CO2GalvoSettingsPage() {
"lens.field_size",
].join(",");
const url = `/api/dx/items/settings_co2gal?fields=${encodeURIComponent(fields)}&limit=-1`;
const url = `/api/dx/items/settings_co2gal?fields=${encodeURIComponent(
fields
)}&limit=-1`;
fetch(url, { cache: "no-store", credentials: "include" })
.then(async (res) => {
@ -154,6 +187,13 @@ export default function CO2GalvoSettingsPage() {
})();
}, [settings, ownerMap]);
const isMine = (o: Owner) => {
if (!meId || !o) return false;
if (typeof o === "string" || typeof o === "number") return String(o) === meId;
if (o.id != null) return String(o.id) === meId;
return false;
};
const ownerLabel = (o: Owner) => {
if (!o) return "—";
@ -173,7 +213,10 @@ export default function CO2GalvoSettingsPage() {
const highlight = (text?: string) => {
if (!debouncedQuery) return text || "";
const regex = new RegExp(`(${debouncedQuery.replace(/[.*+?^${}()|[\\]\\\\]/g, "\\$&")})`, "gi");
const regex = new RegExp(
`(${debouncedQuery.replace(/[.*+?^${}()|[\\]\\\\]/g, "\\$&")})`,
"gi"
);
return (text || "").replace(regex, "<mark>$1</mark>");
};
@ -214,31 +257,71 @@ export default function CO2GalvoSettingsPage() {
<thead>
<tr className="bg-muted">
<th className="px-2 py-2 text-left">Title</th>
<th className="px-2 py-2 text-left">Owner {resolvingOwners ? "…resolving" : ""}</th>
<th className="px-2 py-2 text-left">
Owner {resolvingOwners ? "…resolving" : ""}
</th>
<th className="px-2 py-2 text-left">Material</th>
<th className="px-2 py-2 text-left">Coating</th>
<th className="px-2 py-2 text-left">Model</th>
<th className="px-2 py-2 text-left">Field</th>
<th className="px-2 py-2 text-left">Actions</th>
</tr>
</thead>
<tbody>
{filtered.map((s) => (
<tr key={s.submission_id} className="border-b hover:bg-muted/30">
<td className="px-2 py-2 whitespace-nowrap">
<Link href={`/settings/co2-galvo/${s.submission_id}`} className="underline">
<span dangerouslySetInnerHTML={{ __html: highlight(s.setting_title || "Untitled") }} />
</Link>
</td>
<td
className="px-2 py-2 whitespace-nowrap"
dangerouslySetInnerHTML={{ __html: highlight(ownerLabel(s.owner)) }}
/>
<td className="px-2 py-2 whitespace-nowrap">{s.mat?.name || "—"}</td>
<td className="px-2 py-2 whitespace-nowrap">{s.mat_coat?.name || "—"}</td>
<td className="px-2 py-2 whitespace-nowrap">{s.source?.model || "—"}</td>
<td className="px-2 py-2 whitespace-nowrap">{s.lens?.field_size || "—"}</td>
</tr>
))}
{filtered.map((s) => {
const mine = isMine(s.owner);
const ownerText =
ownerLabel(s.owner) + (mine ? " (you)" : "");
return (
<tr
key={s.submission_id}
className="border-b hover:bg-muted/30"
>
<td className="px-2 py-2 whitespace-nowrap">
<Link
href={`/settings/co2-galvo/${s.submission_id}`}
className="underline"
>
<span
dangerouslySetInnerHTML={{
__html: highlight(s.setting_title || "Untitled"),
}}
/>
</Link>
</td>
<td
className="px-2 py-2 whitespace-nowrap"
dangerouslySetInnerHTML={{
__html: highlight(ownerText),
}}
/>
<td className="px-2 py-2 whitespace-nowrap">
{s.mat?.name || "—"}
</td>
<td className="px-2 py-2 whitespace-nowrap">
{s.mat_coat?.name || "—"}
</td>
<td className="px-2 py-2 whitespace-nowrap">
{s.source?.model || "—"}
</td>
<td className="px-2 py-2 whitespace-nowrap">
{s.lens?.field_size || "—"}
</td>
<td className="px-2 py-2 whitespace-nowrap">
{mine ? (
<Link
href={`/settings/co2-galvo/${s.submission_id}?edit=1`}
className="underline"
>
Edit
</Link>
) : (
<span className="opacity-50"></span>
)}
</td>
</tr>
);
})}
</tbody>
</table>
</div>

File diff suppressed because it is too large Load diff