Improve creator studio browsing and versioning
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user