feat: add status filter chips and sort controls to library
Adds an "All / Ready / Processing / Error / Live" pill filter row and a "Newest / Oldest / Name / Duration / Size" sort selector to the asset toolbar. Both operate client-side on the loaded asset list so there is no additional API overhead. State resets to "All / Newest" whenever a different project or bin is selected.
This commit is contained in:
parent
08e8377309
commit
1e4fcb62f5
1 changed files with 113 additions and 6 deletions
|
|
@ -93,10 +93,11 @@
|
||||||
border-bottom: 1px solid var(--border);
|
border-bottom: 1px solid var(--border);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
gap: var(--sp-3);
|
gap: var(--sp-3);
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.asset-toolbar-left { display: flex; align-items: center; gap: var(--sp-3); }
|
.asset-toolbar-left { display: flex; align-items: center; gap: var(--sp-3); }
|
||||||
.asset-toolbar-right { display: flex; align-items: center; gap: var(--sp-2); }
|
.asset-toolbar-right { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; }
|
||||||
|
|
||||||
.asset-count {
|
.asset-count {
|
||||||
font-size: var(--text-xs);
|
font-size: var(--text-xs);
|
||||||
|
|
@ -105,7 +106,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-input {
|
.search-input {
|
||||||
width: 220px;
|
width: 200px;
|
||||||
height: 28px;
|
height: 28px;
|
||||||
padding: 0 var(--sp-3);
|
padding: 0 var(--sp-3);
|
||||||
background: var(--bg-surface);
|
background: var(--bg-surface);
|
||||||
|
|
@ -119,6 +120,41 @@
|
||||||
.search-input:focus { border-color: var(--accent-border); }
|
.search-input:focus { border-color: var(--accent-border); }
|
||||||
.search-input::placeholder { color: var(--text-tertiary); }
|
.search-input::placeholder { color: var(--text-tertiary); }
|
||||||
|
|
||||||
|
/* Filter chips */
|
||||||
|
.filter-chips { display: flex; gap: 3px; }
|
||||||
|
.filter-chip {
|
||||||
|
padding: 2px 9px;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 500;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all var(--t-fast);
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
.filter-chip:hover { border-color: var(--border-strong); color: var(--text-primary); }
|
||||||
|
.filter-chip.active {
|
||||||
|
background: var(--accent-subtle);
|
||||||
|
border-color: var(--accent-border);
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sort select */
|
||||||
|
.sort-select {
|
||||||
|
height: 26px;
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 0 20px 0 8px;
|
||||||
|
background: var(--bg-surface);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--r-md);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
outline: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.sort-select:focus { border-color: var(--accent-border); }
|
||||||
|
|
||||||
.asset-grid-wrap {
|
.asset-grid-wrap {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|
@ -422,6 +458,21 @@
|
||||||
<span class="asset-count" id="assetCount">0 assets</span>
|
<span class="asset-count" id="assetCount">0 assets</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="asset-toolbar-right">
|
<div class="asset-toolbar-right">
|
||||||
|
<div class="filter-chips" id="filterChips" role="group" aria-label="Filter by status">
|
||||||
|
<button class="filter-chip active" data-status="">All</button>
|
||||||
|
<button class="filter-chip" data-status="ready">Ready</button>
|
||||||
|
<button class="filter-chip" data-status="processing">Processing</button>
|
||||||
|
<button class="filter-chip" data-status="error">Error</button>
|
||||||
|
<button class="filter-chip" data-status="live">Live</button>
|
||||||
|
</div>
|
||||||
|
<select class="sort-select" id="sortSelect" aria-label="Sort order">
|
||||||
|
<option value="newest">Newest</option>
|
||||||
|
<option value="oldest">Oldest</option>
|
||||||
|
<option value="name">Name A–Z</option>
|
||||||
|
<option value="name-desc">Name Z–A</option>
|
||||||
|
<option value="duration">Longest</option>
|
||||||
|
<option value="size">Largest</option>
|
||||||
|
</select>
|
||||||
<input class="search-input" id="searchInput" type="text" placeholder="Search assets…" aria-label="Search assets">
|
<input class="search-input" id="searchInput" type="text" placeholder="Search assets…" aria-label="Search assets">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -515,6 +566,8 @@
|
||||||
assets: [],
|
assets: [],
|
||||||
thumbCache: {},
|
thumbCache: {},
|
||||||
searchTerm: '',
|
searchTerm: '',
|
||||||
|
statusFilter: '',
|
||||||
|
sortBy: 'newest',
|
||||||
};
|
};
|
||||||
|
|
||||||
const thumbObserver = new IntersectionObserver((entries) => {
|
const thumbObserver = new IntersectionObserver((entries) => {
|
||||||
|
|
@ -565,6 +618,7 @@
|
||||||
await loadProjects();
|
await loadProjects();
|
||||||
setupDrag();
|
setupDrag();
|
||||||
setupSearch();
|
setupSearch();
|
||||||
|
setupFilters();
|
||||||
|
|
||||||
// Multi-select bulk actions
|
// Multi-select bulk actions
|
||||||
if (window.SelectionManager) {
|
if (window.SelectionManager) {
|
||||||
|
|
@ -619,6 +673,10 @@
|
||||||
function handleProjectChange() {
|
function handleProjectChange() {
|
||||||
state.currentProjectId = document.getElementById('projectSelect').value || null;
|
state.currentProjectId = document.getElementById('projectSelect').value || null;
|
||||||
state.currentBinId = null;
|
state.currentBinId = null;
|
||||||
|
state.statusFilter = '';
|
||||||
|
state.sortBy = 'newest';
|
||||||
|
document.querySelectorAll('.filter-chip').forEach(c => c.classList.toggle('active', c.dataset.status === ''));
|
||||||
|
document.getElementById('sortSelect').value = 'newest';
|
||||||
loadBinsAndAssets();
|
loadBinsAndAssets();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -635,7 +693,6 @@
|
||||||
|
|
||||||
function renderBins(bins) {
|
function renderBins(bins) {
|
||||||
const tree = document.getElementById('binTree');
|
const tree = document.getElementById('binTree');
|
||||||
const allItem = document.getElementById('allAssetsItem');
|
|
||||||
tree.querySelectorAll('.bin-item:not(#allAssetsItem)').forEach(n => n.remove());
|
tree.querySelectorAll('.bin-item:not(#allAssetsItem)').forEach(n => n.remove());
|
||||||
bins.forEach(bin => {
|
bins.forEach(bin => {
|
||||||
const el = document.createElement('a');
|
const el = document.createElement('a');
|
||||||
|
|
@ -655,6 +712,10 @@
|
||||||
|
|
||||||
function selectBin(binId) {
|
function selectBin(binId) {
|
||||||
state.currentBinId = binId;
|
state.currentBinId = binId;
|
||||||
|
state.statusFilter = '';
|
||||||
|
state.sortBy = 'newest';
|
||||||
|
document.querySelectorAll('.filter-chip').forEach(c => c.classList.toggle('active', c.dataset.status === ''));
|
||||||
|
document.getElementById('sortSelect').value = 'newest';
|
||||||
updateBinActive();
|
updateBinActive();
|
||||||
loadAssets();
|
loadAssets();
|
||||||
}
|
}
|
||||||
|
|
@ -683,12 +744,40 @@
|
||||||
|
|
||||||
function renderAssets(assets) {
|
function renderAssets(assets) {
|
||||||
const term = state.searchTerm.toLowerCase();
|
const term = state.searchTerm.toLowerCase();
|
||||||
const filtered = term ? assets.filter(a => a.filename?.toLowerCase().includes(term) || a.display_name?.toLowerCase().includes(term)) : assets;
|
|
||||||
|
// Apply status filter
|
||||||
|
let filtered = state.statusFilter
|
||||||
|
? assets.filter(a => {
|
||||||
|
if (state.statusFilter === 'processing') return a.status === 'processing' || a.status === 'ingesting';
|
||||||
|
return a.status === state.statusFilter;
|
||||||
|
})
|
||||||
|
: assets;
|
||||||
|
|
||||||
|
// Apply text search
|
||||||
|
if (term) filtered = filtered.filter(a =>
|
||||||
|
a.filename?.toLowerCase().includes(term) || a.display_name?.toLowerCase().includes(term)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Sort
|
||||||
|
filtered = [...filtered].sort((a, b) => {
|
||||||
|
switch (state.sortBy) {
|
||||||
|
case 'oldest': return new Date(a.created_at) - new Date(b.created_at);
|
||||||
|
case 'name': return (a.display_name || a.filename || '').localeCompare(b.display_name || b.filename || '');
|
||||||
|
case 'name-desc': return (b.display_name || b.filename || '').localeCompare(a.display_name || a.filename || '');
|
||||||
|
case 'duration': return (b.duration_ms || 0) - (a.duration_ms || 0);
|
||||||
|
case 'size': return (b.file_size || 0) - (a.file_size || 0);
|
||||||
|
default: return new Date(b.created_at) - new Date(a.created_at);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const grid = document.getElementById('assetGrid');
|
const grid = document.getElementById('assetGrid');
|
||||||
grid.innerHTML = '';
|
grid.innerHTML = '';
|
||||||
|
|
||||||
const count = filtered.length;
|
const count = filtered.length;
|
||||||
document.getElementById('assetCount').textContent = `${count} asset${count !== 1 ? 's' : ''}`;
|
const totalCount = assets.length;
|
||||||
|
document.getElementById('assetCount').textContent =
|
||||||
|
count === totalCount ? `${count} asset${count !== 1 ? 's' : ''}` :
|
||||||
|
`${count} of ${totalCount} asset${totalCount !== 1 ? 's' : ''}`;
|
||||||
document.getElementById('assetEmpty').style.display = count === 0 ? 'flex' : 'none';
|
document.getElementById('assetEmpty').style.display = count === 0 ? 'flex' : 'none';
|
||||||
|
|
||||||
filtered.forEach(asset => {
|
filtered.forEach(asset => {
|
||||||
|
|
@ -729,7 +818,7 @@
|
||||||
else img.style.display = 'none';
|
else img.style.display = 'none';
|
||||||
|
|
||||||
card.addEventListener('click', (e) => {
|
card.addEventListener('click', (e) => {
|
||||||
if (e.target.closest('.asset-action-btn')) return; // delete button etc.
|
if (e.target.closest('.asset-action-btn')) return;
|
||||||
if (window.openAssetPreview) window.openAssetPreview(asset.id);
|
if (window.openAssetPreview) window.openAssetPreview(asset.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -781,6 +870,24 @@
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Filter chips + sort ───────────────────
|
||||||
|
function setupFilters() {
|
||||||
|
document.querySelectorAll('.filter-chip').forEach(chip => {
|
||||||
|
chip.addEventListener('click', () => {
|
||||||
|
state.statusFilter = chip.dataset.status;
|
||||||
|
document.querySelectorAll('.filter-chip').forEach(c =>
|
||||||
|
c.classList.toggle('active', c.dataset.status === state.statusFilter)
|
||||||
|
);
|
||||||
|
renderAssets(state.assets);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('sortSelect').addEventListener('change', e => {
|
||||||
|
state.sortBy = e.target.value;
|
||||||
|
renderAssets(state.assets);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// ── New project ───────────────────────────
|
// ── New project ───────────────────────────
|
||||||
async function saveProject() {
|
async function saveProject() {
|
||||||
const name = document.getElementById('newProjectName').value.trim();
|
const name = document.getElementById('newProjectName').value.trim();
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue