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]);
|
||||
|
||||
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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue