Improve creator studio browsing and versioning

This commit is contained in:
2026-04-16 15:01:15 +02:00
parent 56eaa3bcbf
commit cdd42a0186
12 changed files with 728 additions and 140 deletions

View File

@@ -45,6 +45,28 @@ function itemReadiness(item) {
return item?.workflow?.readiness ?? null
}
function buildPaginationPages(current, last) {
if (last <= 1) return [1]
if (last <= 7) {
return Array.from({ length: last }, (_, index) => index + 1)
}
const pages = new Set([1, 2, current - 1, current, current + 1, last - 1, last])
const sorted = [...pages]
.filter((page) => page >= 1 && page <= last)
.sort((left, right) => left - right)
const result = []
for (let index = 0; index < sorted.length; index += 1) {
if (index > 0 && sorted[index] - sorted[index - 1] > 1) {
result.push('ellipsis')
}
result.push(sorted[index])
}
return result
}
function bulkErrorMessage(payload, fallback = 'Bulk action failed.') {
if (Array.isArray(payload?.errors) && payload.errors.length > 0) {
return payload.errors[0]
@@ -284,6 +306,24 @@ function ListRow({ item, onExecuteAction, busyKey }) {
)
}
function materializeFilter(filter, pendingFilters) {
if (filter?.key !== 'category') {
return filter
}
const selectedContentType = pendingFilters?.content_type || 'all'
const options = Array.isArray(filter.options)
? filter.options.filter((option) => option.value === 'all'
|| selectedContentType === 'all'
|| option.content_type_slug === selectedContentType)
: filter.options
return {
...filter,
options,
}
}
function AdvancedFilterControl({ filter, onChange, value }) {
const controlValue = value ?? filter.value
@@ -337,6 +377,7 @@ export default function StudioContentBrowser({
q: '',
bucket: 'all',
sort: 'updated_desc',
content_type: 'all',
category: 'all',
tag: '',
})
@@ -362,6 +403,12 @@ export default function StudioContentBrowser({
const allVisibleSelected = selectableIds.length > 0 && selectableIds.every((id) => selectedIds.includes(id))
const selectedOnPage = selectedIds.filter((id) => selectableIds.includes(id))
const visibleTotal = Math.max(0, Number(meta.total || 0) - optimisticRemovedIds.length)
const currentPage = Math.max(1, Number(meta.current_page || 1))
const lastPage = Math.max(1, Number(meta.last_page || 1))
const perPage = Math.max(1, Number(meta.per_page || visibleItems.length || 24))
const rangeStart = visibleTotal === 0 ? 0 : ((currentPage - 1) * perPage) + 1
const rangeEnd = visibleTotal === 0 ? 0 : Math.min(visibleTotal, rangeStart + Math.max(visibleItems.length, 1) - 1)
const paginationPages = buildPaginationPages(currentPage, lastPage)
const filterControlCount = 1 + (hideModuleFilter ? 0 : 1) + (hideBucketFilter ? 0 : 1) + 1 + advancedFilters.length + 1
const filterGridClass = filterControlCount <= 4
? 'xl:grid-cols-4'
@@ -396,10 +443,11 @@ export default function StudioContentBrowser({
q: filters.q || '',
bucket: filters.bucket || 'all',
sort: filters.sort || 'updated_desc',
content_type: filters.content_type || 'all',
category: filters.category || 'all',
tag: filters.tag || '',
})
}, [filters.q, filters.bucket, filters.sort, filters.category, filters.tag])
}, [filters.q, filters.bucket, filters.sort, filters.content_type, filters.category, filters.tag])
const updateQuery = (patch) => {
const next = {
@@ -439,10 +487,20 @@ export default function StudioContentBrowser({
}
const setPendingFilter = (key, value) => {
setPendingFilters((current) => ({
...current,
[key]: value,
}))
setPendingFilters((current) => {
if (key === 'content_type') {
return {
...current,
content_type: value,
category: 'all',
}
}
return {
...current,
[key]: value,
}
})
}
const submitSearch = () => {
@@ -450,6 +508,7 @@ export default function StudioContentBrowser({
q: pendingFilters.q,
bucket: pendingFilters.bucket,
sort: pendingFilters.sort,
content_type: pendingFilters.content_type,
category: pendingFilters.category,
tag: pendingFilters.tag,
})
@@ -818,13 +877,16 @@ export default function StudioContentBrowser({
</select>
</label>
{advancedFilters.map((filter) => (
{advancedFilters.map((filter) => {
const resolvedFilter = materializeFilter(filter, pendingFilters)
return (
<AdvancedFilterControl
key={filter.key}
filter={filter}
value={filter.key === 'category' || filter.key === 'tag' ? pendingFilters[filter.key] : undefined}
filter={resolvedFilter}
value={filter.key === 'content_type' || filter.key === 'category' || filter.key === 'tag' ? pendingFilters[filter.key] : undefined}
onChange={(key, value) => {
if (key === 'category' || key === 'tag') {
if (key === 'content_type' || key === 'category' || key === 'tag') {
setPendingFilter(key, value)
return
}
@@ -832,7 +894,8 @@ export default function StudioContentBrowser({
updateQuery({ [key]: value })
}}
/>
))}
)
})}
<div className="flex items-end">
<button
@@ -891,7 +954,7 @@ export default function StudioContentBrowser({
<p>
Showing <span className="font-semibold text-white">{visibleItems.length}</span> of <span className="font-semibold text-white">{visibleTotal.toLocaleString()}</span> items
</p>
<p>Page {meta.current_page || 1} of {meta.last_page || 1}</p>
<p>Page {currentPage} of {lastPage}</p>
</div>
{viewMode === 'table' && supportsArtworkBulk && (
@@ -1071,28 +1134,85 @@ export default function StudioContentBrowser({
</section>
)}
<div className="flex items-center justify-between rounded-[24px] border border-white/10 bg-white/[0.03] px-4 py-3 text-sm text-slate-300">
<button
type="button"
disabled={(meta.current_page || 1) <= 1}
onClick={() => updateQuery({ page: Math.max(1, (meta.current_page || 1) - 1) })}
className="inline-flex items-center gap-2 rounded-full border border-white/10 px-4 py-2 disabled:cursor-not-allowed disabled:opacity-40"
>
<i className="fa-solid fa-arrow-left" />
Previous
</button>
<div className="rounded-[24px] border border-white/10 bg-white/[0.03] px-4 py-4 text-sm text-slate-300">
<div className="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
<div className="space-y-1">
<div className="text-xs uppercase tracking-[0.2em] text-slate-500">Creator Studio</div>
<p className="text-sm text-slate-400">
{visibleTotal > 0
? <>Showing <span className="font-semibold text-white">{rangeStart.toLocaleString()}-{rangeEnd.toLocaleString()}</span> of <span className="font-semibold text-white">{visibleTotal.toLocaleString()}</span></>
: 'No items to display'}
</p>
</div>
<span className="text-xs uppercase tracking-[0.2em] text-slate-500">Creator Studio</span>
{lastPage > 1 && (
<nav aria-label="Studio pagination" className="flex flex-col gap-3 lg:items-end">
<div className="flex flex-wrap items-center gap-2">
<button
type="button"
disabled={currentPage <= 1}
onClick={() => updateQuery({ page: 1 })}
className="inline-flex items-center gap-2 rounded-full border border-white/10 px-3 py-2 text-xs font-semibold text-slate-300 transition hover:border-white/20 hover:bg-white/[0.04] disabled:cursor-not-allowed disabled:opacity-40"
>
<i className="fa-solid fa-angles-left" />
<span className="hidden sm:inline">First</span>
</button>
<button
type="button"
disabled={(meta.current_page || 1) >= (meta.last_page || 1)}
onClick={() => updateQuery({ page: (meta.current_page || 1) + 1 })}
className="inline-flex items-center gap-2 rounded-full border border-white/10 px-4 py-2 disabled:cursor-not-allowed disabled:opacity-40"
>
Next
<i className="fa-solid fa-arrow-right" />
</button>
<button
type="button"
disabled={currentPage <= 1}
onClick={() => updateQuery({ page: Math.max(1, currentPage - 1) })}
className="inline-flex items-center gap-2 rounded-full border border-white/10 px-3 py-2 text-xs font-semibold text-slate-300 transition hover:border-white/20 hover:bg-white/[0.04] disabled:cursor-not-allowed disabled:opacity-40"
>
<i className="fa-solid fa-arrow-left" />
<span>Previous</span>
</button>
<div className="flex flex-wrap items-center gap-1.5">
{paginationPages.map((page, index) => page === 'ellipsis' ? (
<span key={`ellipsis-${index}`} className="inline-flex h-10 min-w-[2.5rem] items-center justify-center text-xs font-semibold uppercase tracking-[0.16em] text-slate-500">
...
</span>
) : (
<button
key={page}
type="button"
aria-current={page === currentPage ? 'page' : undefined}
onClick={() => updateQuery({ page })}
className={`inline-flex h-10 min-w-[2.5rem] items-center justify-center rounded-2xl border px-3 text-sm font-semibold transition ${page === currentPage ? 'border-sky-400/30 bg-sky-300/15 text-white shadow-[0_10px_24px_rgba(14,165,233,0.16)]' : 'border-white/10 text-slate-300 hover:border-white/20 hover:bg-white/[0.04]'}`}
>
{page}
</button>
))}
</div>
<button
type="button"
disabled={currentPage >= lastPage}
onClick={() => updateQuery({ page: currentPage + 1 })}
className="inline-flex items-center gap-2 rounded-full border border-white/10 px-3 py-2 text-xs font-semibold text-slate-300 transition hover:border-white/20 hover:bg-white/[0.04] disabled:cursor-not-allowed disabled:opacity-40"
>
<span>Next</span>
<i className="fa-solid fa-arrow-right" />
</button>
<button
type="button"
disabled={currentPage >= lastPage}
onClick={() => updateQuery({ page: lastPage })}
className="inline-flex items-center gap-2 rounded-full border border-white/10 px-3 py-2 text-xs font-semibold text-slate-300 transition hover:border-white/20 hover:bg-white/[0.04] disabled:cursor-not-allowed disabled:opacity-40"
>
<span className="hidden sm:inline">Last</span>
<i className="fa-solid fa-angles-right" />
</button>
</div>
<p className="text-xs text-slate-500">
Jump by page number or use first/last for longer queues.
</p>
</nav>
)}
</div>
</div>
<ConfirmDangerModal