fix(library): RenameAssetModal replaces prompt(), inline bin name input replaces prompt()

This commit is contained in:
Zac Gaetano 2026-05-23 09:02:09 -04:00
parent 13906cd0fe
commit 6fe5f7d450

View file

@ -23,18 +23,18 @@ function Library({ navigate, onOpenAsset, openProject }) {
}, [openProject]);
const createBin = () => {
if (!openProject) {
window.alert('Open a project first (Projects → click a project), then create a bin inside it.');
return;
}
const name = prompt('Bin name', '');
if (!name || !name.trim()) return;
if (!openProject) { window.alert('Open a project first (Projects → click a project), then create a bin inside it.'); return; }
setNewBinName(''); setCreatingBin(true);
};
const submitBin = (name) => {
if (!name || !name.trim()) { setCreatingBin(false); return; }
setCreatingBin(false);
window.ZAMPP_API.fetch('/bins', {
method: 'POST',
body: JSON.stringify({ project_id: openProject.id, name: name.trim() }),
})
.then(() => {
// Re-fetch project-scoped list to get the count column.
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' }))));
})
@ -48,6 +48,9 @@ function Library({ navigate, onOpenAsset, openProject }) {
// a full app reload keeps ZAMPP_DATA in sync as the cache of record.
const [allAssets, setAllAssets] = React.useState(window.ZAMPP_DATA.ASSETS || []);
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(() => {
window.ZAMPP_API.fetch('/assets?limit=500')
@ -83,14 +86,21 @@ function Library({ navigate, onOpenAsset, openProject }) {
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
? allAssets.filter(function(a) { return a.project_id === openProject.id; })
: allAssets;
const ALL_ASSETS = allAssets;
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 (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 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>
</div>
<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' }}>
{openProject ? 'No bins yet — click + to create one.' : 'Open a project to manage bins.'}
</div>
) : BINS.map(function(b) {
const isActive = selectedBinId === b.id;
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" />
<span>{b.name}</span>
<span className="rail-count">{b.count}</span>
@ -224,13 +256,21 @@ function Library({ navigate, onOpenAsset, openProject }) {
onClose={function() { setCtxMenu(null); }}
onChanged={refreshAssets}
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>
);
}
function AssetContextMenu({ asset, x, y, bins, onClose, onChanged, onOpen }) {
function AssetContextMenu({ asset, x, y, bins, onClose, onChanged, onOpen, onRename }) {
const ref = React.useRef(null);
// Pin the menu inside the viewport even if the user right-clicked near
// 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) });
}, [x, y]);
const rename = function() {
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 rename = function() { if (onRename) onRename(asset); else onClose(); };
const moveToBin = function(binId) {
onClose();
@ -398,5 +429,43 @@ function binIcon(name) {
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.AssetCard = AssetCard;