diff --git a/src/components/item-form.tsx b/src/components/item-form.tsx index 97a3189..b0d3734 100644 --- a/src/components/item-form.tsx +++ b/src/components/item-form.tsx @@ -127,7 +127,7 @@ export function ItemForm({ }) const [stagedFiles, setStagedFiles] = useState([]) - // Staged marks & notes (create mode only) + // Staged marks & notes const [stagedMarks, setStagedMarks] = useState([]) const [stagedNotes, setStagedNotes] = useState([]) @@ -144,7 +144,8 @@ export function ItemForm({ "provenance", ...(typeDef && typeDef.fields.length > 0 ? ["typeFields"] : []), "dimensions", - ...(!isEdit ? ["marks", "notes"] : []), + "marks", + "notes", ] const allExpanded = sectionKeys.every((k) => openSections[k]) const toggleAll = () => { @@ -517,27 +518,23 @@ export function ItemForm({ - {/* Marks section (create only) */} - {!isEdit && ( - toggleSection("marks", v)} - > - - - )} + {/* Marks section */} + toggleSection("marks", v)} + > + + - {/* Notes section (create only) */} - {!isEdit && ( - toggleSection("notes", v)} - > - - - )} + {/* Notes section */} + toggleSection("notes", v)} + > + +
)} - {/* Tabs */} - - - Details - Images - Provenance - Dimensions - Marks - Notes - - - - {item.description && ( - - )} - - - - - - {item.type_fields && typeDef && typeDef.fields.length > 0 && ( - <> - -

- {typeDef.label} Details -

- {typeDef.fields.map((field) => { - const raw = ( - item.type_fields as Record - )?.[field.name] - if (!raw) return null - let display = String(raw) - if (field.type === "enum" && field.options) { - const opt = field.options.find((o) => o.value === raw) - if (opt) display = opt.label - } - return ( - - ) - })} - - )} - {!item.description && - !item.location && - !item.acquisition_date && - !item.acquisition_price && - !item.estimated_value && - !item.acquisition_source && - !item.type_fields && ( - + {/* Expand / Collapse all */} + + + {/* Sections */} +
+ toggleSection("details", v)} + > +
+ {item.description && ( + + )} + + + + + + {item.type_fields && typeDef && typeDef.fields.length > 0 && ( + <> + +

+ {typeDef.label} Details +

+ {typeDef.fields.map((field) => { + const raw = ( + item.type_fields as Record + )?.[field.name] + if (!raw) return null + let display = String(raw) + if (field.type === "enum" && field.options) { + const opt = field.options.find((o) => o.value === raw) + if (opt) display = opt.label + } + return ( + + ) + })} + )} - + {!item.description && + !item.location && + !item.acquisition_date && + !item.acquisition_price && + !item.estimated_value && + !item.acquisition_source && + !item.type_fields && ( + + )} +
+
- + toggleSection("images", v)} + > - - - - - - - {(item.artist_maker || item.origin || item.date_era) && ( - - )} - - + - - - - - - - {!item.height_cm && - !item.width_cm && - !item.depth_cm && - !item.weight_kg && - !item.materials && ( - + toggleSection("provenance", v)} + > +
+ + + + {(item.artist_maker || item.origin || item.date_era) && ( + )} - + +
+
- + toggleSection("dimensions", v)} + > +
+ + + + + + {!item.height_cm && + !item.width_cm && + !item.depth_cm && + !item.weight_kg && + !item.materials && ( + + )} +
+
+ + toggleSection("marks", v)} + > -
+ - + toggleSection("notes", v)} + > - - + +
{message}

) } +function DetailSection({ + title, + open, + onOpenChange, + children, +}: { + title: string + open: boolean + onOpenChange: (open: boolean) => void + children: React.ReactNode +}) { + return ( + + + + + + {children} + + + ) +} + function ItemHeaderSkeleton() { return (
@@ -347,11 +451,25 @@ function EditItemDialog({ const queryClient = useQueryClient() const [uploading, setUploading] = useState(false) const imageUpload = useUploadItemImageItemsItemIdImagesPost() + const createMark = useCreateMarkItemsItemIdMarksPost() + const markImageUpload = useUploadMarkImageItemsItemIdMarksMarkIdImagesPost() + const createNote = useCreateItemNoteItemsItemIdNotesPost() const handleSuccess = useCallback( - async (_updatedItem: ItemRead, stagedFiles: File[]) => { + async ( + _updatedItem: ItemRead, + stagedFiles: File[], + stagedMarks: StagedMark[], + stagedNotes: StagedNote[] + ) => { + const hasUploads = + stagedFiles.length > 0 || + stagedMarks.length > 0 || + stagedNotes.length > 0 + if (hasUploads) setUploading(true) + + // 1. Upload item images if (stagedFiles.length > 0) { - setUploading(true) const failed: string[] = [] for (const file of stagedFiles) { try { @@ -363,12 +481,56 @@ function EditItemDialog({ failed.push(file.name) } } - setUploading(false) if (failed.length > 0) { toast.error(`Failed to upload: ${failed.join(", ")}`) } } + // 2. Create marks + upload mark images + for (const sm of stagedMarks) { + try { + const markRes = await createMark.mutateAsync({ + itemId: item.id, + data: { + title: sm.title || undefined, + description: sm.description || undefined, + }, + }) + if (markRes.status === 201 && sm.files.length > 0) { + for (const file of sm.files) { + try { + await markImageUpload.mutateAsync({ + itemId: item.id, + markId: markRes.data.id, + data: { file }, + }) + } catch { + toast.error(`Failed to upload mark image: ${file.name}`) + } + } + } + } catch { + toast.error(`Failed to create mark: ${sm.title || "Untitled"}`) + } + } + + // 3. Create notes + for (const sn of stagedNotes) { + try { + await createNote.mutateAsync({ + itemId: item.id, + data: { + title: sn.title || undefined, + body: sn.body, + }, + }) + } catch { + toast.error(`Failed to create note: ${sn.title || "Untitled"}`) + } + } + + if (hasUploads) setUploading(false) + queryClient.invalidateQueries({ queryKey: getGetItemItemsItemIdGetQueryKey(item.id), }) @@ -386,7 +548,16 @@ function EditItemDialog({ } onOpenChange(false) }, - [item.id, item.collection_id, onOpenChange, queryClient, imageUpload] + [ + item.id, + item.collection_id, + onOpenChange, + queryClient, + imageUpload, + createMark, + markImageUpload, + createNote, + ] ) return ( @@ -397,7 +568,7 @@ function EditItemDialog({ {uploading ? (

- Uploading images… + Saving…

) : (