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 (