|
|
|
@ -1,6 +1,9 @@
|
|
|
|
// screens-library.jsx
|
|
|
|
// screens-library.jsx
|
|
|
|
|
|
|
|
|
|
|
|
function Library({ navigate, onOpenAsset, openProject, onClearProject }) {
|
|
|
|
|
|
|
|
|
|
|
|
function buildBinTree(f){const m={};f.forEach(b=>{m[b.id]={...b,children:[]};});const r=[];f.forEach(b=>{if(b.parent_id&&m[b.parent_id])m[b.parent_id].children.push(m[b.id]);else r.push(m[b.id]);});return r;}
|
|
|
|
|
|
|
|
function collectDescendantIds(id,f){const s=new Set([id]);let c=true;while(c){c=false;f.forEach(b=>{if(b.parent_id&&s.has(b.parent_id)&&!s.has(b.id)){s.add(b.id);c=true;}});}return s;}
|
|
|
|
|
|
|
|
function Library({ navigate, onOpenAsset, openProject, onClearProject, onOpenProject }) {
|
|
|
|
const PROJECTS = window.ZAMPP_DATA?.PROJECTS || [];
|
|
|
|
const PROJECTS = window.ZAMPP_DATA?.PROJECTS || [];
|
|
|
|
const [bins, setBins] = React.useState(window.ZAMPP_DATA?.BINS || []);
|
|
|
|
const [bins, setBins] = React.useState(window.ZAMPP_DATA?.BINS || []);
|
|
|
|
const BINS = bins; // legacy local name; keep so the rest of the function reads unchanged
|
|
|
|
const BINS = bins; // legacy local name; keep so the rest of the function reads unchanged
|
|
|
|
@ -14,6 +17,8 @@ function Library({ navigate, onOpenAsset, openProject, onClearProject }) {
|
|
|
|
var normalized = (list || []).map(function(b) { return { ...b, count: b.asset_count != null ? b.asset_count : (b.count || 0), icon: b.type || 'grid' }; });
|
|
|
|
var normalized = (list || []).map(function(b) { return { ...b, count: b.asset_count != null ? b.asset_count : (b.count || 0), icon: b.type || 'grid' }; });
|
|
|
|
if (!openProject) window.ZAMPP_DATA.BINS = normalized;
|
|
|
|
if (!openProject) window.ZAMPP_DATA.BINS = normalized;
|
|
|
|
setBins(normalized);
|
|
|
|
setBins(normalized);
|
|
|
|
|
|
|
|
// Auto-expand all bins so nested children are always visible
|
|
|
|
|
|
|
|
setExpandedBins(function(prev) { var s = new Set(prev); normalized.forEach(function(b){ s.add(b.id); }); return s; });
|
|
|
|
})
|
|
|
|
})
|
|
|
|
.catch(function() {});
|
|
|
|
.catch(function() {});
|
|
|
|
}, [openProject]);
|
|
|
|
}, [openProject]);
|
|
|
|
@ -25,20 +30,32 @@ function Library({ navigate, onOpenAsset, openProject, onClearProject }) {
|
|
|
|
return function() { window.removeEventListener('df:bins-changed', onBinsChanged); };
|
|
|
|
return function() { window.removeEventListener('df:bins-changed', onBinsChanged); };
|
|
|
|
}, [refreshBins]);
|
|
|
|
}, [refreshBins]);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const [creatingChildOf, setCreatingChildOf] = React.useState(null);
|
|
|
|
|
|
|
|
// Start with all bins expanded so nested children are visible immediately
|
|
|
|
|
|
|
|
const [expandedBins, setExpandedBins] = React.useState(() => new Set((window.ZAMPP_DATA?.BINS||[]).map(b=>b.id)));
|
|
|
|
|
|
|
|
|
|
|
|
const createBin = () => {
|
|
|
|
const createBin = () => {
|
|
|
|
if (!openProject) { window.alert('Open a project first (Projects → click a project), then create a bin inside it.'); return; }
|
|
|
|
if (!openProject) { window.alert('Open a project first (Projects → click a project), then create a bin inside it.'); return; }
|
|
|
|
setNewBinName(''); setCreatingBin(true);
|
|
|
|
setCreatingChildOf(null); setNewBinName(''); setCreatingBin(true);
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
const createSubBin = (parentId) => {
|
|
|
|
|
|
|
|
if (!openProject) return;
|
|
|
|
|
|
|
|
setCreatingChildOf(parentId); setNewBinName(''); setCreatingBin(true);
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
const toggleBinExpanded = (binId) => {
|
|
|
|
|
|
|
|
setExpandedBins(prev => { const s = new Set(prev); s.has(binId) ? s.delete(binId) : s.add(binId); return s; });
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const submitBin = (name) => {
|
|
|
|
const submitBin = (name) => {
|
|
|
|
if (!name || !name.trim()) { setCreatingBin(false); return; }
|
|
|
|
if (!name || !name.trim()) { setCreatingBin(false); setCreatingChildOf(null); return; }
|
|
|
|
setCreatingBin(false);
|
|
|
|
setCreatingBin(false);
|
|
|
|
|
|
|
|
const parentId = creatingChildOf;
|
|
|
|
|
|
|
|
setCreatingChildOf(null);
|
|
|
|
window.ZAMPP_API.fetch('/bins', {
|
|
|
|
window.ZAMPP_API.fetch('/bins', {
|
|
|
|
method: 'POST',
|
|
|
|
method: 'POST',
|
|
|
|
body: JSON.stringify({ project_id: openProject.id, name: name.trim() }),
|
|
|
|
body: JSON.stringify({ project_id: openProject.id, name: name.trim(), parent_id: parentId || null }),
|
|
|
|
})
|
|
|
|
})
|
|
|
|
.then(() => window.ZAMPP_API.fetch('/bins?project_id=' + openProject.id))
|
|
|
|
.then(() => window.ZAMPP_API.fetch('/bins?project_id=' + openProject.id))
|
|
|
|
.then(list => setBins((list || []).map(b => ({ ...b, count: b.asset_count || 0, icon: b.type || 'grid' }))))
|
|
|
|
.then(list => { const n = (list||[]).map(b=>({...b,count:b.asset_count||0,icon:b.type||'grid'})); setBins(n); if (parentId) setExpandedBins(prev => { const s=new Set(prev); s.add(parentId); return s; }); })
|
|
|
|
.catch(e => window.alert('Could not create bin: ' + e.message));
|
|
|
|
.catch(e => window.alert('Could not create bin: ' + e.message));
|
|
|
|
};
|
|
|
|
};
|
|
|
|
const [view, setView] = React.useState('grid');
|
|
|
|
const [view, setView] = React.useState('grid');
|
|
|
|
@ -285,12 +302,13 @@ function Library({ navigate, onOpenAsset, openProject, onClearProject }) {
|
|
|
|
assets = assets.filter(function(a) { return a.status === filter; });
|
|
|
|
assets = assets.filter(function(a) { return a.status === filter; });
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (search) assets = assets.filter(function(a) { return a.name.toLowerCase().includes(search.toLowerCase()); });
|
|
|
|
if (search) assets = assets.filter(function(a) { return a.name.toLowerCase().includes(search.toLowerCase()); });
|
|
|
|
if (selectedBinId) assets = assets.filter(function(a) { return a.bin_id === selectedBinId; });
|
|
|
|
if (selectedBinId) { var sids=collectDescendantIds(selectedBinId,BINS); assets=assets.filter(function(a){return sids.has(a.bin_id);}); }
|
|
|
|
|
|
|
|
|
|
|
|
const activeBin = selectedBinId ? BINS.find(b => b.id === selectedBinId) : null;
|
|
|
|
const activeBin = selectedBinId ? BINS.find(b => b.id === selectedBinId) : null;
|
|
|
|
const displayTitle = activeBin
|
|
|
|
const displayTitle = activeBin
|
|
|
|
? (openProject ? openProject.name + ' · ' : '') + activeBin.name
|
|
|
|
? (openProject ? openProject.name + ' · ' : '') + activeBin.name
|
|
|
|
: (openProject ? openProject.name : 'All Assets');
|
|
|
|
: (openProject ? openProject.name : 'All Assets');
|
|
|
|
|
|
|
|
const binTree=React.useMemo(function(){return buildBinTree(BINS);},[BINS]);
|
|
|
|
const errorCount = ALL_ASSETS.filter(function(a) { return a.status === 'error'; }).length;
|
|
|
|
const errorCount = ALL_ASSETS.filter(function(a) { return a.status === 'error'; }).length;
|
|
|
|
const recentCount = ALL_ASSETS.filter(function(a) { return (Date.now() - new Date(a.created_at)) < 86400000; }).length;
|
|
|
|
const recentCount = ALL_ASSETS.filter(function(a) { return (Date.now() - new Date(a.created_at)) < 86400000; }).length;
|
|
|
|
|
|
|
|
|
|
|
|
@ -309,7 +327,7 @@ function Library({ navigate, onOpenAsset, openProject, onClearProject }) {
|
|
|
|
{PROJECTS.slice(0, 8).map(function(p) {
|
|
|
|
{PROJECTS.slice(0, 8).map(function(p) {
|
|
|
|
return (
|
|
|
|
return (
|
|
|
|
<div key={p.id} className={`rail-item ${openProject && openProject.id === p.id ? 'active' : ''}`} style={{ cursor: 'pointer' }}
|
|
|
|
<div key={p.id} className={`rail-item ${openProject && openProject.id === p.id ? 'active' : ''}`} style={{ cursor: 'pointer' }}
|
|
|
|
onClick={function() { navigate('projects'); }}
|
|
|
|
onClick={function() { if (onOpenProject) onOpenProject(p); }}
|
|
|
|
onContextMenu={function(e) { openProjectCtx(p, e); }}>
|
|
|
|
onContextMenu={function(e) { openProjectCtx(p, e); }}>
|
|
|
|
<span className="rail-color-dot" style={{ background: p.color }} />
|
|
|
|
<span className="rail-color-dot" style={{ background: p.color }} />
|
|
|
|
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{p.name}</span>
|
|
|
|
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{p.name}</span>
|
|
|
|
@ -329,45 +347,30 @@ function Library({ navigate, onOpenAsset, openProject, onClearProject }) {
|
|
|
|
</button>
|
|
|
|
</button>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div className="rail-list">
|
|
|
|
<div className="rail-list">
|
|
|
|
{creatingBin && (
|
|
|
|
|
|
|
|
<div style={{ padding: '4px 6px', display: 'flex', gap: 4, alignItems: 'center' }}>
|
|
|
|
|
|
|
|
<input
|
|
|
|
|
|
|
|
className="field-input"
|
|
|
|
|
|
|
|
autoFocus
|
|
|
|
|
|
|
|
value={newBinName}
|
|
|
|
|
|
|
|
onChange={function(e) { setNewBinName(e.target.value); }}
|
|
|
|
|
|
|
|
onKeyDown={function(e) {
|
|
|
|
|
|
|
|
if (e.key === 'Enter') submitBin(newBinName);
|
|
|
|
|
|
|
|
if (e.key === 'Escape') { setCreatingBin(false); }
|
|
|
|
|
|
|
|
}}
|
|
|
|
|
|
|
|
onBlur={function() { submitBin(newBinName); }}
|
|
|
|
|
|
|
|
placeholder="Bin name"
|
|
|
|
|
|
|
|
style={{ fontSize: 12, height: 26, padding: '0 6px', flex: 1 }}
|
|
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
{!creatingBin && BINS.length === 0 ? (
|
|
|
|
{!creatingBin && BINS.length === 0 ? (
|
|
|
|
<div style={{ fontSize: 11, color: 'var(--text-3)', padding: '6px 8px', fontStyle: 'italic' }}>
|
|
|
|
<div style={{ fontSize: 11, color: 'var(--text-3)', padding: '6px 8px', fontStyle: 'italic' }}>
|
|
|
|
{openProject ? 'No bins yet: click + to create one.' : 'Open a project to manage bins.'}
|
|
|
|
{openProject ? 'No bins yet: click + to create one.' : 'Open a project to manage bins.'}
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
) : BINS.map(function(b) {
|
|
|
|
) : (
|
|
|
|
const isActive = selectedBinId === b.id;
|
|
|
|
<BinTreeNodes nodes={binTree} depth={0}
|
|
|
|
const isDragTarget = draggingAssetId !== null && dragOverBinId === b.id;
|
|
|
|
selectedBinId={selectedBinId} setSelectedBinId={setSelectedBinId}
|
|
|
|
return (
|
|
|
|
draggingAssetId={draggingAssetId} dragOverBinId={dragOverBinId}
|
|
|
|
<div key={b.id}
|
|
|
|
onBinDragOver={onBinDragOver} onBinDrop={onBinDrop} onBinDragLeave={onBinDragLeave}
|
|
|
|
className={'rail-item' + (isActive ? ' active' : '') + (isDragTarget ? ' droppable' : '')}
|
|
|
|
expandedBins={expandedBins} toggleBinExpanded={toggleBinExpanded}
|
|
|
|
onClick={function() { setSelectedBinId(isActive ? null : b.id); }}
|
|
|
|
creatingBin={creatingBin} creatingChildOf={creatingChildOf}
|
|
|
|
onDragOver={function(e) { onBinDragOver(b.id, e); }}
|
|
|
|
newBinName={newBinName} setNewBinName={setNewBinName}
|
|
|
|
onDrop={function(e) { onBinDrop(b.id, e); }}
|
|
|
|
submitBin={submitBin} setCreatingBin={setCreatingBin} setCreatingChildOf={setCreatingChildOf}
|
|
|
|
onDragLeave={onBinDragLeave}
|
|
|
|
createSubBin={createSubBin} openProject={openProject} />
|
|
|
|
style={{ cursor: 'pointer' }}
|
|
|
|
)}
|
|
|
|
title={isActive ? 'Click to clear bin filter' : 'Filter to this bin'}>
|
|
|
|
{creatingBin && creatingChildOf === null && (
|
|
|
|
<Icon name={binIcon(b.icon)} size={13} className="rail-icon" />
|
|
|
|
<div style={{ padding: '4px 6px', display: 'flex', gap: 4, alignItems: 'center' }}>
|
|
|
|
<span>{b.name}</span>
|
|
|
|
<input className="field-input" autoFocus value={newBinName}
|
|
|
|
<span className="rail-count">{b.count}</span>
|
|
|
|
onChange={function(e) { setNewBinName(e.target.value); }}
|
|
|
|
|
|
|
|
onKeyDown={function(e) { if (e.key==='Enter') submitBin(newBinName); if (e.key==='Escape') { setCreatingBin(false); } }}
|
|
|
|
|
|
|
|
onBlur={function() { submitBin(newBinName); }}
|
|
|
|
|
|
|
|
placeholder="Bin name" style={{ fontSize: 12, height: 26, padding: '0 6px', flex: 1 }} />
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
)}
|
|
|
|
})}
|
|
|
|
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div>
|
|
|
|
<div>
|
|
|
|
@ -873,5 +876,6 @@ function DownloadWarningModal({ asset, onClose, onConfirm }) {
|
|
|
|
);
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function BinTreeNodes(p){var nodes=p.nodes,depth=p.depth,selId=p.selectedBinId,setSel=p.setSelectedBinId;var drag=p.draggingAssetId,over=p.dragOverBinId;var onOver=p.onBinDragOver,onDrop=p.onBinDrop,onLeave=p.onBinDragLeave;var expanded=p.expandedBins,toggle=p.toggleBinExpanded;var creat=p.creatingBin,parentOf=p.creatingChildOf;var newName=p.newBinName,setName=p.setNewBinName;var submit=p.submitBin,setCreat=p.setCreatingBin,setParent=p.setCreatingChildOf;var createSub=p.createSubBin,proj=p.openProject;if(!nodes||!nodes.length)return null;return nodes.map(function(b){var act=selId===b.id,idt=drag!==null&&over===b.id,hasC=b.children&&b.children.length>0,exp=expanded.has(b.id),isCk=creat&&parentOf===b.id,ind=depth*14;return React.createElement(React.Fragment,{key:b.id},React.createElement("div",{className:"rail-item"+(act?" active":"")+(idt?" droppable":""),onClick:function(){setSel(act?null:b.id);},onDragOver:function(e){onOver(b.id,e);},onDrop:function(e){onDrop(b.id,e);},onDragLeave:onLeave,style:{cursor:"pointer",paddingLeft:8+ind}},React.createElement("span",{style:{display:"inline-flex",alignItems:"center",width:16,height:16,flexShrink:0,marginRight:2,color:hasC?"var(--text-3)":"transparent",transition:"transform 120ms",transform:hasC&&exp?"rotate(90deg)":"none"},onClick:function(e){if(!hasC)return;e.stopPropagation();toggle(b.id);}},hasC&&React.createElement("svg",{width:8,height:8,viewBox:"0 0 8 8",fill:"currentColor"},React.createElement("path",{d:"M2 1l4 3-4 3V1z"}))),React.createElement(Icon,{name:binIcon(b.icon),size:13,className:"rail-icon",style:{marginRight:4}}),React.createElement("span",{style:{flex:1,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"}},b.name),React.createElement("span",{className:"rail-count"},b.count),proj&&React.createElement("button",{className:"icon-btn bin-add-child-btn","aria-label":"Create sub-bin",onClick:function(e){e.stopPropagation();createSub(b.id);},style:{opacity:0,transition:"opacity 100ms",marginLeft:2,flexShrink:0},onFocus:function(e){e.currentTarget.style.opacity="1";},onBlur:function(e){e.currentTarget.style.opacity="";}},React.createElement(Icon,{name:"plus",size:10}))),isCk&&React.createElement("div",{style:{paddingLeft:8+ind+14,paddingRight:6,paddingTop:4,paddingBottom:4,display:"flex",gap:4,alignItems:"center"}},React.createElement("input",{className:"field-input",autoFocus:true,value:newName,onChange:function(e){setName(e.target.value);},onKeyDown:function(e){if(e.key==="Enter")submit(newName);if(e.key==="Escape"){setCreat(false);setParent(null);}},onBlur:function(){submit(newName);},placeholder:"Sub-bin name",style:{fontSize:12,height:26,padding:"0 6px",flex:1}})),hasC&&exp&&React.createElement(BinTreeNodes,Object.assign({},p,{nodes:b.children,depth:depth+1})));});}
|
|
|
|
window.Library = Library;
|
|
|
|
window.Library = Library;
|
|
|
|
window.AssetCard = AssetCard;
|
|
|
|
window.AssetCard = AssetCard;
|
|
|
|
|