From af905cf9363548c49bf2fff79d73e4aafe87ddae Mon Sep 17 00:00:00 2001 From: Zac Gaetano Date: Sun, 24 May 2026 13:27:24 -0400 Subject: [PATCH] fix: bin creation 500 error + add drag-and-drop + project rename MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix 500 error when creating bins: missing updated_at column on bins table (migration 013 adds the column, schema.sql updated) - Add drag-and-drop support for moving asset cards/list rows onto bin rail items with visual droppable highlight - Add right-click context menu on project rail items (Rename/Delete) - Expose RenameProjectModal via window so Library screen can reuse it - Bins context menu already existed — was hidden by the 500 error --- .../src/db/migrations/013-bins-updated-at.sql | 3 + services/mam-api/src/db/schema.sql | 3 +- services/web-ui/public/screens-library.jsx | 123 +++++++++++++++++- services/web-ui/public/screens-projects.jsx | 1 + services/web-ui/public/styles-screens.css | 1 + 5 files changed, 123 insertions(+), 8 deletions(-) create mode 100644 services/mam-api/src/db/migrations/013-bins-updated-at.sql diff --git a/services/mam-api/src/db/migrations/013-bins-updated-at.sql b/services/mam-api/src/db/migrations/013-bins-updated-at.sql new file mode 100644 index 0000000..4cd858d --- /dev/null +++ b/services/mam-api/src/db/migrations/013-bins-updated-at.sql @@ -0,0 +1,3 @@ +-- 2026-05: Add missing updated_at column to bins table. +-- The INSERT and PATCH handlers already reference updated_at. +ALTER TABLE bins ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ DEFAULT NOW(); diff --git a/services/mam-api/src/db/schema.sql b/services/mam-api/src/db/schema.sql index 4b0fa3c..a20a75e 100644 --- a/services/mam-api/src/db/schema.sql +++ b/services/mam-api/src/db/schema.sql @@ -49,7 +49,8 @@ CREATE TABLE bins ( project_id UUID NOT NULL REFERENCES projects ON DELETE CASCADE, parent_id UUID REFERENCES bins ON DELETE SET NULL, name TEXT NOT NULL, - created_at TIMESTAMPTZ DEFAULT NOW() + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() ); -- Assets table diff --git a/services/web-ui/public/screens-library.jsx b/services/web-ui/public/screens-library.jsx index bacda0f..4b383a1 100644 --- a/services/web-ui/public/screens-library.jsx +++ b/services/web-ui/public/screens-library.jsx @@ -51,6 +51,10 @@ function Library({ navigate, onOpenAsset, openProject }) { const [renamingAsset, setRenamingAsset] = React.useState(null); const [creatingBin, setCreatingBin] = React.useState(false); const [newBinName, setNewBinName] = React.useState(''); + const [draggingAssetId, setDraggingAssetId] = React.useState(null); + const [recentlyMovedId, setRecentlyMovedId] = React.useState(null); + // Rename project state + const [renamingProject, setRenamingProject] = React.useState(null); const refreshAssets = React.useCallback(() => { window.ZAMPP_API.fetch('/assets?limit=500') @@ -104,6 +108,62 @@ function Library({ navigate, onOpenAsset, openProject }) { setCtxMenu({ asset, x: e.clientX, y: e.clientY }); }; + // Drag-and-drop: asset → bin + const onAssetDragStart = function(assetId, e) { + e.dataTransfer.setData('text/plain', assetId); + e.dataTransfer.effectAllowed = 'move'; + setDraggingAssetId(assetId); + }; + + const onBinDragOver = function(e) { + e.preventDefault(); + e.dataTransfer.dropEffect = 'move'; + }; + + const onBinDrop = function(binId, e) { + e.preventDefault(); + setDraggingAssetId(null); + var assetId = e.dataTransfer.getData('text/plain'); + if (!assetId) return; + window.ZAMPP_API.fetch('/assets/' + assetId, { method: 'PATCH', body: JSON.stringify({ bin_id: binId }) }) + .then(function() { + setRecentlyMovedId(assetId); + refreshAssets(); + setTimeout(function() { setRecentlyMovedId(null); }, 2000); + }) + .catch(function(e2) { alert('Move failed: ' + e2.message); }); + }; + + const onBinDragLeave = function(e) { + // Remove highlight — handled via CSS :hover + drag state + }; + + // Project rename + const renameProject = function(p) { setRenamingProject(p); }; + + // Close drag state when drag ends anywhere + React.useEffect(function() { + if (!draggingAssetId) return; + var onEnd = function() { setDraggingAssetId(null); }; + window.addEventListener('dragend', onEnd); + return function() { window.removeEventListener('dragend', onEnd); }; + }, [draggingAssetId]); + + // Project context menu state + var [projectCtx, setProjectCtx] = React.useState(null); + React.useEffect(function() { + if (!projectCtx) return; + var close = function() { setProjectCtx(null); }; + window.addEventListener('click', close); + return function() { window.removeEventListener('click', close); }; + }, [projectCtx]); + + var openProjectCtx = function(p, e) { + e.preventDefault(); + e.stopPropagation(); + setProjectCtx({ project: p, x: e.clientX, y: e.clientY }); + }; + const [selectedBinId, setSelectedBinId] = React.useState(null); // Clear bin filter on project change so a stale id doesn't hide everything. React.useEffect(() => { setSelectedBinId(null); }, [openProject?.id]); @@ -136,7 +196,8 @@ function Library({ navigate, onOpenAsset, openProject }) { {PROJECTS.slice(0, 8).map(function(p) { return (
+ onClick={function() { navigate('projects'); }} + onContextMenu={function(e) { openProjectCtx(p, e); }}> {p.name} {p.assets} @@ -178,12 +239,16 @@ function Library({ navigate, onOpenAsset, openProject }) {
) : BINS.map(function(b) { const isActive = selectedBinId === b.id; + const isDragTarget = draggingAssetId !== null; return (
+ title={isActive ? 'Click to clear bin filter' : 'Filter to this bin'} {b.name} {b.count} @@ -234,7 +299,9 @@ function Library({ navigate, onOpenAsset, openProject }) { {assets.map(function(a) { return ; + onContextMenu={function(e) { openCtx(a, e); }} + onDragStart={function(e) { onAssetDragStart(a.id, e); }} + draggable={true} />; })}
) : ( @@ -244,7 +311,8 @@ function Library({ navigate, onOpenAsset, openProject }) { {assets.map(function(a) { return ( -
+
{a.name}
@@ -284,6 +352,22 @@ function Library({ navigate, onOpenAsset, openProject }) { onSaved={function() { setRenamingAsset(null); refreshAssets(); }} /> )} + {projectCtx && ( + + )} + {renamingProject && ( + + )}
); } @@ -364,7 +448,7 @@ function AssetContextMenu({ asset, x, y, bins, onClose, onChanged, onOpen, onRen ); } -function AssetCard({ asset, onOpen, onContextMenu }) { +function AssetCard({ asset, onOpen, onContextMenu, onDragStart, draggable }) { const [hoverStream, setHoverStream] = React.useState(null); const [hovered, setHovered] = React.useState(false); const timerRef = React.useRef(null); @@ -402,7 +486,8 @@ function AssetCard({ asset, onOpen, onContextMenu }) { const showVideo = hovered && hoverStream; return ( -
+
{showVideo && ( @@ -443,6 +528,30 @@ function AssetCard({ asset, onOpen, onContextMenu }) { ); } +function ProjectContextMenu({ project, x, y, onClose, onRename }) { + var ref = React.useRef(null); + var [pos, setPos] = React.useState({ left: x, top: y }); + React.useLayoutEffect(function() { + if (!ref.current) return; + var r = ref.current.getBoundingClientRect(); + var margin = 8; + var nx = x, ny = y; + if (x + r.width + margin > window.innerWidth) nx = window.innerWidth - r.width - margin; + if (y + r.height + margin > window.innerHeight) ny = window.innerHeight - r.height - margin; + setPos({ left: Math.max(margin, nx), top: Math.max(margin, ny) }); + }, [x, y]); + + return ( +
+
{project.name}
+ + +
+ ); +} + function binIcon(name) { return { grid: 'library', live: 'record', film: 'film', proxy: 'proxy', audio: 'audio', package: 'package' }[name] || 'folder'; } diff --git a/services/web-ui/public/screens-projects.jsx b/services/web-ui/public/screens-projects.jsx index af8d17f..5673ee4 100644 --- a/services/web-ui/public/screens-projects.jsx +++ b/services/web-ui/public/screens-projects.jsx @@ -246,3 +246,4 @@ function ProjectCard({ project, assets, onOpen }) { } window.Projects = Projects; +window.RenameProjectModal = RenameProjectModal; diff --git a/services/web-ui/public/styles-screens.css b/services/web-ui/public/styles-screens.css index c444ef1..de07031 100644 --- a/services/web-ui/public/styles-screens.css +++ b/services/web-ui/public/styles-screens.css @@ -304,6 +304,7 @@ } .rail-item:hover { background: var(--hover); color: var(--text-1); } .rail-item.active { background: var(--accent-soft); color: var(--accent-text); } +.rail-item.droppable { outline: 2px dashed var(--accent); outline-offset: -2px; background: var(--accent-subtle); } .rail-item.active .rail-icon { color: var(--accent); } .rail-item .rail-icon { color: var(--text-3); } .rail-item .rail-count { margin-left: auto; font-family: var(--font-mono); font-size: 10.5px; color: var(--text-3); }