fix(library): RenameAssetModal replaces prompt(), inline bin name input replaces prompt()
This commit is contained in:
parent
13906cd0fe
commit
6fe5f7d450
1 changed files with 90 additions and 21 deletions
|
|
@ -23,18 +23,18 @@ function Library({ navigate, onOpenAsset, openProject }) {
|
||||||
}, [openProject]);
|
}, [openProject]);
|
||||||
|
|
||||||
const createBin = () => {
|
const createBin = () => {
|
||||||
if (!openProject) {
|
if (!openProject) { window.alert('Open a project first (Projects → click a project), then create a bin inside it.'); return; }
|
||||||
window.alert('Open a project first (Projects → click a project), then create a bin inside it.');
|
setNewBinName(''); setCreatingBin(true);
|
||||||
return;
|
};
|
||||||
}
|
|
||||||
const name = prompt('Bin name', '');
|
const submitBin = (name) => {
|
||||||
if (!name || !name.trim()) return;
|
if (!name || !name.trim()) { setCreatingBin(false); return; }
|
||||||
|
setCreatingBin(false);
|
||||||
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() }),
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
// Re-fetch project-scoped list to get the count column.
|
|
||||||
window.ZAMPP_API.fetch('/bins?project_id=' + openProject.id)
|
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 => setBins((list || []).map(b => ({ ...b, count: b.asset_count || 0, icon: b.type || 'grid' }))));
|
||||||
})
|
})
|
||||||
|
|
@ -48,6 +48,9 @@ function Library({ navigate, onOpenAsset, openProject }) {
|
||||||
// a full app reload — keeps ZAMPP_DATA in sync as the cache of record.
|
// a full app reload — keeps ZAMPP_DATA in sync as the cache of record.
|
||||||
const [allAssets, setAllAssets] = React.useState(window.ZAMPP_DATA.ASSETS || []);
|
const [allAssets, setAllAssets] = React.useState(window.ZAMPP_DATA.ASSETS || []);
|
||||||
const [ctxMenu, setCtxMenu] = React.useState(null); // { asset, x, y }
|
const [ctxMenu, setCtxMenu] = React.useState(null); // { asset, x, y }
|
||||||
|
const [renamingAsset, setRenamingAsset] = React.useState(null);
|
||||||
|
const [creatingBin, setCreatingBin] = React.useState(false);
|
||||||
|
const [newBinName, setNewBinName] = React.useState('');
|
||||||
|
|
||||||
const refreshAssets = React.useCallback(() => {
|
const refreshAssets = React.useCallback(() => {
|
||||||
window.ZAMPP_API.fetch('/assets?limit=500')
|
window.ZAMPP_API.fetch('/assets?limit=500')
|
||||||
|
|
@ -83,14 +86,21 @@ function Library({ navigate, onOpenAsset, openProject }) {
|
||||||
setCtxMenu({ asset, x: e.clientX, y: e.clientY });
|
setCtxMenu({ asset, 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]);
|
||||||
let assets = openProject
|
let assets = openProject
|
||||||
? allAssets.filter(function(a) { return a.project_id === openProject.id; })
|
? allAssets.filter(function(a) { return a.project_id === openProject.id; })
|
||||||
: allAssets;
|
: allAssets;
|
||||||
const ALL_ASSETS = allAssets;
|
const ALL_ASSETS = allAssets;
|
||||||
if (filter !== 'all') assets = assets.filter(function(a) { return a.status === filter; });
|
if (filter !== 'all') 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; });
|
||||||
|
|
||||||
const displayTitle = openProject ? openProject.name : 'All Assets';
|
const activeBin = selectedBinId ? BINS.find(b => b.id === selectedBinId) : null;
|
||||||
|
const displayTitle = activeBin
|
||||||
|
? (openProject ? openProject.name + ' · ' : '') + activeBin.name
|
||||||
|
: (openProject ? openProject.name : 'All Assets');
|
||||||
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;
|
||||||
|
|
||||||
|
|
@ -127,13 +137,35 @@ function Library({ navigate, onOpenAsset, openProject }) {
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="rail-list">
|
<div className="rail-list">
|
||||||
{BINS.length === 0 ? (
|
{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 ? (
|
||||||
<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) {
|
) : BINS.map(function(b) {
|
||||||
|
const isActive = selectedBinId === b.id;
|
||||||
return (
|
return (
|
||||||
<div key={b.id} className="rail-item">
|
<div key={b.id}
|
||||||
|
className={'rail-item' + (isActive ? ' active' : '')}
|
||||||
|
onClick={function() { setSelectedBinId(isActive ? null : b.id); }}
|
||||||
|
style={{ cursor: 'pointer' }}
|
||||||
|
title={isActive ? 'Click to clear bin filter' : 'Filter to this bin'}>
|
||||||
<Icon name={binIcon(b.icon)} size={13} className="rail-icon" />
|
<Icon name={binIcon(b.icon)} size={13} className="rail-icon" />
|
||||||
<span>{b.name}</span>
|
<span>{b.name}</span>
|
||||||
<span className="rail-count">{b.count}</span>
|
<span className="rail-count">{b.count}</span>
|
||||||
|
|
@ -224,13 +256,21 @@ function Library({ navigate, onOpenAsset, openProject }) {
|
||||||
onClose={function() { setCtxMenu(null); }}
|
onClose={function() { setCtxMenu(null); }}
|
||||||
onChanged={refreshAssets}
|
onChanged={refreshAssets}
|
||||||
onOpen={function() { onOpenAsset(ctxMenu.asset); }}
|
onOpen={function() { onOpenAsset(ctxMenu.asset); }}
|
||||||
|
onRename={function(a) { setCtxMenu(null); setRenamingAsset(a); }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{renamingAsset && (
|
||||||
|
<RenameAssetModal
|
||||||
|
asset={renamingAsset}
|
||||||
|
onClose={function() { setRenamingAsset(null); }}
|
||||||
|
onSaved={function() { setRenamingAsset(null); refreshAssets(); }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function AssetContextMenu({ asset, x, y, bins, onClose, onChanged, onOpen }) {
|
function AssetContextMenu({ asset, x, y, bins, onClose, onChanged, onOpen, onRename }) {
|
||||||
const ref = React.useRef(null);
|
const ref = React.useRef(null);
|
||||||
// Pin the menu inside the viewport even if the user right-clicked near
|
// Pin the menu inside the viewport even if the user right-clicked near
|
||||||
// the bottom-right edge of the grid.
|
// the bottom-right edge of the grid.
|
||||||
|
|
@ -245,16 +285,7 @@ function AssetContextMenu({ asset, x, y, bins, onClose, onChanged, onOpen }) {
|
||||||
setPos({ left: Math.max(margin, nx), top: Math.max(margin, ny) });
|
setPos({ left: Math.max(margin, nx), top: Math.max(margin, ny) });
|
||||||
}, [x, y]);
|
}, [x, y]);
|
||||||
|
|
||||||
const rename = function() {
|
const rename = function() { if (onRename) onRename(asset); else onClose(); };
|
||||||
onClose();
|
|
||||||
const next = prompt('Rename asset', asset.display_name || asset.name || '');
|
|
||||||
if (next == null) return;
|
|
||||||
const trimmed = next.trim();
|
|
||||||
if (!trimmed || trimmed === (asset.display_name || asset.name)) return;
|
|
||||||
window.ZAMPP_API.fetch('/assets/' + asset.id, { method: 'PATCH', body: JSON.stringify({ display_name: trimmed }) })
|
|
||||||
.then(onChanged)
|
|
||||||
.catch(function(e) { alert('Rename failed: ' + e.message); });
|
|
||||||
};
|
|
||||||
|
|
||||||
const moveToBin = function(binId) {
|
const moveToBin = function(binId) {
|
||||||
onClose();
|
onClose();
|
||||||
|
|
@ -398,5 +429,43 @@ function binIcon(name) {
|
||||||
return { grid: 'library', live: 'record', film: 'film', proxy: 'proxy', audio: 'audio', package: 'package' }[name] || 'folder';
|
return { grid: 'library', live: 'record', film: 'film', proxy: 'proxy', audio: 'audio', package: 'package' }[name] || 'folder';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function RenameAssetModal({ asset, onClose, onSaved }) {
|
||||||
|
const [name, setName] = React.useState(asset.display_name || asset.name || '');
|
||||||
|
const [saving, setSaving] = React.useState(false);
|
||||||
|
const [err, setErr] = React.useState(null);
|
||||||
|
const original = asset.display_name || asset.name || '';
|
||||||
|
const submit = function() {
|
||||||
|
const trimmed = name.trim();
|
||||||
|
if (!trimmed || trimmed === original) { onClose(); return; }
|
||||||
|
setSaving(true); setErr(null);
|
||||||
|
window.ZAMPP_API.fetch('/assets/' + asset.id, { method: 'PATCH', body: JSON.stringify({ display_name: trimmed }) })
|
||||||
|
.then(onSaved)
|
||||||
|
.catch(function(e) { setSaving(false); setErr(e.message); });
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div className="modal-backdrop" onClick={onClose}>
|
||||||
|
<div className="modal" style={{ width: 420 }} onClick={function(e) { e.stopPropagation(); }}>
|
||||||
|
<div className="modal-head">
|
||||||
|
<div style={{ fontSize: 15, fontWeight: 600 }}>Rename asset</div>
|
||||||
|
<button className="icon-btn" onClick={onClose}><Icon name="x" /></button>
|
||||||
|
</div>
|
||||||
|
<div className="modal-body">
|
||||||
|
<div className="field">
|
||||||
|
<label className="field-label">Display name</label>
|
||||||
|
<input className="field-input" autoFocus value={name}
|
||||||
|
onChange={function(e) { setName(e.target.value); }}
|
||||||
|
onKeyDown={function(e) { if (e.key === 'Enter') submit(); if (e.key === 'Escape') onClose(); }} />
|
||||||
|
</div>
|
||||||
|
{err && <div style={{ fontSize: 12, color: 'var(--danger)', marginTop: 4 }}>{err}</div>}
|
||||||
|
</div>
|
||||||
|
<div className="modal-foot">
|
||||||
|
<button className="btn ghost sm" onClick={onClose}>Cancel</button>
|
||||||
|
<button className="btn primary sm" onClick={submit} disabled={saving || !name.trim()}>{saving ? 'Saving…' : 'Rename'}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
window.Library = Library;
|
window.Library = Library;
|
||||||
window.AssetCard = AssetCard;
|
window.AssetCard = AssetCard;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue