From c49f8f78c948ddc39805860017088d81c226c7d5 Mon Sep 17 00:00:00 2001 From: Forbes Date: Sun, 8 Feb 2026 15:11:20 -0600 Subject: [PATCH] fix: render project tags as strings, not objects The /api/items/{pn}/projects endpoint returns Project objects ({id, code, name, created_at}), but MainTab typed them as string[]. React error #31 was thrown when trying to render the object as a child node. Change itemProjects state from string[] to Project[] and use proj.code in all rendering and comparison logic. Closes #33 --- web/src/components/items/MainTab.tsx | 266 ++++++++++++++++++++------- 1 file changed, 202 insertions(+), 64 deletions(-) diff --git a/web/src/components/items/MainTab.tsx b/web/src/components/items/MainTab.tsx index 14a0d99..18e4ff0 100644 --- a/web/src/components/items/MainTab.tsx +++ b/web/src/components/items/MainTab.tsx @@ -1,6 +1,6 @@ -import { useState, useEffect } from 'react'; -import { get, post, del } from '../../api/client'; -import type { Item, Project, Revision } from '../../api/types'; +import { useState, useEffect } from "react"; +import { get, post, del } from "../../api/client"; +import type { Item, Project, Revision } from "../../api/types"; interface MainTabProps { item: Item; @@ -9,8 +9,14 @@ interface MainTabProps { } function formatDate(s: string) { - if (!s) return '—'; - return new Date(s).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }); + if (!s) return "—"; + return new Date(s).toLocaleDateString("en-US", { + year: "numeric", + month: "short", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + }); } function formatFileSize(bytes: number) { @@ -21,19 +27,23 @@ function formatFileSize(bytes: number) { } export function MainTab({ item, onReload, isEditor }: MainTabProps) { - const [itemProjects, setItemProjects] = useState([]); + const [itemProjects, setItemProjects] = useState([]); const [allProjects, setAllProjects] = useState([]); const [latestRev, setLatestRev] = useState(null); - const [addProject, setAddProject] = useState(''); + const [addProject, setAddProject] = useState(""); useEffect(() => { - get(`/api/items/${encodeURIComponent(item.part_number)}/projects`) + get( + `/api/items/${encodeURIComponent(item.part_number)}/projects`, + ) .then(setItemProjects) .catch(() => setItemProjects([])); - get('/api/projects') + get("/api/projects") .then(setAllProjects) .catch(() => {}); - get(`/api/items/${encodeURIComponent(item.part_number)}/revisions`) + get( + `/api/items/${encodeURIComponent(item.part_number)}/revisions`, + ) .then((revs) => { if (revs.length > 0) setLatestRev(revs[revs.length - 1]!); }) @@ -43,67 +53,144 @@ export function MainTab({ item, onReload, isEditor }: MainTabProps) { const handleAddProject = async () => { if (!addProject) return; try { - await post(`/api/items/${encodeURIComponent(item.part_number)}/projects`, { projects: [addProject] }); - setItemProjects((prev) => [...prev, addProject]); - setAddProject(''); + await post( + `/api/items/${encodeURIComponent(item.part_number)}/projects`, + { projects: [addProject] }, + ); + const proj = allProjects.find((p) => p.code === addProject); + if (proj) setItemProjects((prev) => [...prev, proj]); + setAddProject(""); onReload(); } catch (e) { - alert(e instanceof Error ? e.message : 'Failed to add project'); + alert(e instanceof Error ? e.message : "Failed to add project"); } }; const handleRemoveProject = async (code: string) => { try { - await del(`/api/items/${encodeURIComponent(item.part_number)}/projects/${encodeURIComponent(code)}`); - setItemProjects((prev) => prev.filter((p) => p !== code)); + await del( + `/api/items/${encodeURIComponent(item.part_number)}/projects/${encodeURIComponent(code)}`, + ); + setItemProjects((prev) => prev.filter((p) => p.code !== code)); onReload(); } catch (e) { - alert(e instanceof Error ? e.message : 'Failed to remove project'); + alert(e instanceof Error ? e.message : "Failed to remove project"); } }; const row = (label: string, value: React.ReactNode) => ( -
- {label} - {value} +
+ + {label} + + {value}
); return (
- {row('Part Number', {item.part_number})} - {row('Description', item.description)} - {row('Type', item.item_type)} - {row('Sourcing', item.sourcing_type || '—')} - {item.sourcing_link && row('Source Link', {item.sourcing_link})} - {item.standard_cost != null && row('Std Cost', `$${item.standard_cost.toFixed(2)}`)} - {row('Revision', `Rev ${item.current_revision}`)} - {row('Created', formatDate(item.created_at))} - {row('Updated', formatDate(item.updated_at))} + {row( + "Part Number", + + {item.part_number} + , + )} + {row("Description", item.description)} + {row("Type", item.item_type)} + {row("Sourcing", item.sourcing_type || "—")} + {item.sourcing_link && + row( + "Source Link", + + {item.sourcing_link} + , + )} + {item.standard_cost != null && + row("Std Cost", `$${item.standard_cost.toFixed(2)}`)} + {row("Revision", `Rev ${item.current_revision}`)} + {row("Created", formatDate(item.created_at))} + {row("Updated", formatDate(item.updated_at))} {item.long_description && ( -
-
Long Description
-
{item.long_description}
+
+
+ Long Description +
+
{item.long_description}
)} {/* Project Tags */} -
-
Projects
-
- {itemProjects.map((code) => ( - - {code} +
+
+ Projects +
+
+ {itemProjects.map((proj) => ( + + {proj.code} {isEditor && ( @@ -116,22 +203,36 @@ export function MainTab({ item, onReload, isEditor }: MainTabProps) { value={addProject} onChange={(e) => setAddProject(e.target.value)} style={{ - padding: '0.1rem 0.3rem', fontSize: '0.75rem', - backgroundColor: 'var(--ctp-surface0)', border: '1px solid var(--ctp-surface1)', - borderRadius: '0.3rem', color: 'var(--ctp-text)', + padding: "0.1rem 0.3rem", + fontSize: "0.75rem", + backgroundColor: "var(--ctp-surface0)", + border: "1px solid var(--ctp-surface1)", + borderRadius: "0.3rem", + color: "var(--ctp-text)", }} > {allProjects - .filter((p) => !itemProjects.includes(p.code)) - .map((p) => )} + .filter((p) => !itemProjects.some((ip) => ip.code === p.code)) + .map((p) => ( + + ))} {addProject && ( - )} @@ -142,21 +243,58 @@ export function MainTab({ item, onReload, isEditor }: MainTabProps) { {/* File Info */} {latestRev?.file_key && ( -
-
File Attachment (Rev {latestRev.revision_number})
-
- {latestRev.file_size != null && {formatFileSize(latestRev.file_size)}} +
+
+ File Attachment (Rev {latestRev.revision_number}) +
+
+ {latestRev.file_size != null && ( + {formatFileSize(latestRev.file_size)} + )} {latestRev.file_checksum && ( - + SHA256: {latestRev.file_checksum.substring(0, 12)}... )}