feat(media): pagination
This commit is contained in:
1 parent
04dc151ed7
commit
63c1e7076e
65 files changed
+2960
-365
No files matched your search
@@ -1,5 +1,19 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { DEFAULT_CONTROLS, parseMediaControls, toRequestQuery, toRouteQuery } from '../mediaQuery'
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
||||
import {
|
||||
DEFAULT_CONTROLS,
|
||||
parseMediaControls,
|
||||
toHashRequestQuery,
|
||||
toRequestQuery,
|
||||
toRouteQuery,
|
||||
} from '../mediaQuery'
|
||||
|
||||
beforeEach(() => {
|
||||
localStorage.clear()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
localStorage.clear()
|
||||
})
|
||||
|
||||
describe('parseMediaControls', () => {
|
||||
it('uses the defaults for an empty query', () => {
|
||||
@@ -21,6 +35,34 @@ describe('parseMediaControls', () => {
|
||||
expect(parseMediaControls(query)).toEqual({ ...DEFAULT_CONTROLS, ...expected })
|
||||
})
|
||||
|
||||
it.each([
|
||||
[{ page: '7' }, { page: 7 }],
|
||||
[{ size: '50' }, { size: 50 }],
|
||||
[
|
||||
{ page: '3', size: '5' },
|
||||
{ page: 3, size: 5 },
|
||||
],
|
||||
[{ page: '0' }, {}],
|
||||
[{ page: '-2' }, {}],
|
||||
[{ page: '1.5' }, {}],
|
||||
[{ page: 'abc' }, {}],
|
||||
[{ size: '10' }, {}],
|
||||
[{ size: '0' }, {}],
|
||||
[{ size: '200' }, {}],
|
||||
[{ size: '' }, {}],
|
||||
])('maps the paging of %j', (query, expected) => {
|
||||
expect(parseMediaControls(query)).toEqual({ ...DEFAULT_CONTROLS, ...expected })
|
||||
})
|
||||
|
||||
it('uses the stored size only when the URL has no size', () => {
|
||||
localStorage.setItem('renewcms-page-size-media', '5')
|
||||
|
||||
expect(parseMediaControls({}).size).toBe(5)
|
||||
expect(parseMediaControls({ size: '100' }).size).toBe(100)
|
||||
expect(parseMediaControls({ size: '10' }).size).toBe(25)
|
||||
expect(parseMediaControls({}, false).size).toBe(25)
|
||||
})
|
||||
|
||||
it('takes the first value of a repeated parameter and trims the search', () => {
|
||||
expect(parseMediaControls({ q: [' wall ', 'x'] }).q).toBe('wall')
|
||||
})
|
||||
@@ -33,20 +75,45 @@ describe('toRouteQuery', () => {
|
||||
|
||||
it('keeps only the non-default values', () => {
|
||||
expect(
|
||||
toRouteQuery({ q: 'wall', filter: 'unused', sort: 'largest', view: 'list' }),
|
||||
toRouteQuery({
|
||||
...DEFAULT_CONTROLS,
|
||||
q: 'wall',
|
||||
filter: 'unused',
|
||||
sort: 'largest',
|
||||
view: 'list',
|
||||
}),
|
||||
).toEqual({ q: 'wall', filter: 'unused', sort: 'largest', view: 'list' })
|
||||
})
|
||||
|
||||
it('keeps the page and the size when they are not the defaults', () => {
|
||||
expect(toRouteQuery({ ...DEFAULT_CONTROLS, page: 7, size: 50 })).toEqual({
|
||||
page: '7',
|
||||
size: '50',
|
||||
})
|
||||
})
|
||||
|
||||
it('drops the page and the size on the Duplicates tab', () => {
|
||||
expect(
|
||||
toRouteQuery({ ...DEFAULT_CONTROLS, filter: 'duplicates', page: 4, size: 5 }),
|
||||
).toEqual({ filter: 'duplicates' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('toRequestQuery', () => {
|
||||
it.each([
|
||||
[{}, 'filter=all&sort=newest&limit=100'],
|
||||
[{ filter: 'unused' }, 'filter=unused&sort=newest&limit=100'],
|
||||
[{ filter: 'duplicates' }, 'filter=duplicates&sort=newest&limit=100'],
|
||||
[{ sort: 'largest' }, 'filter=all&sort=largest&limit=100'],
|
||||
[{ q: 'wall' }, 'q=wall&filter=all&sort=newest&limit=100'],
|
||||
[{ q: 'a b&c', filter: 'unused' }, 'q=a%20b%26c&filter=unused&sort=newest&limit=100'],
|
||||
[{}, 'filter=all&sort=newest&page=1&size=25'],
|
||||
[{ filter: 'unused' }, 'filter=unused&sort=newest&page=1&size=25'],
|
||||
[{ filter: 'duplicates' }, 'filter=duplicates&sort=newest&page=1&size=25'],
|
||||
[{ sort: 'largest' }, 'filter=all&sort=largest&page=1&size=25'],
|
||||
[{ q: 'wall' }, 'q=wall&filter=all&sort=newest&page=1&size=25'],
|
||||
[{ q: 'a b&c', filter: 'unused' }, 'q=a%20b%26c&filter=unused&sort=newest&page=1&size=25'],
|
||||
])('builds the request for %j', (overrides, expected) => {
|
||||
expect(toRequestQuery({ ...DEFAULT_CONTROLS, ...overrides })).toBe(expected)
|
||||
})
|
||||
})
|
||||
|
||||
describe('toHashRequestQuery', () => {
|
||||
it('asks for every copy in one page', () => {
|
||||
expect(toHashRequestQuery('abc')).toBe('hash=abc&page=1&size=100')
|
||||
})
|
||||
})
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { LocationQuery } from 'vue-router'
|
||||
import { MEDIA_PAGE_SIZE_KEY, resolvePageSize } from '@/lib/pageSizeMemory'
|
||||
|
||||
export type MediaFilter = 'all' | 'unused' | 'duplicates'
|
||||
export type MediaSort = 'newest' | 'oldest' | 'name' | 'largest' | 'total_size'
|
||||
@@ -11,9 +12,15 @@ export interface MediaControls {
|
||||
sort: MediaSort
|
||||
variants: MediaVariantsFilter
|
||||
view: MediaView
|
||||
page: number
|
||||
size: number
|
||||
}
|
||||
|
||||
export const MEDIA_PAGE_LIMIT = 100
|
||||
export const MEDIA_PAGE_SIZES = [5, 25, 50, 100]
|
||||
export const DEFAULT_MEDIA_PAGE_SIZE = 25
|
||||
export const MEDIA_FULL_SET_PAGE_SIZE = 100
|
||||
const FIRST_PAGE = 1
|
||||
const POSITIVE_INTEGER = /^[1-9]\d*$/
|
||||
|
||||
export const DEFAULT_CONTROLS: MediaControls = {
|
||||
q: '',
|
||||
@@ -21,6 +28,8 @@ export const DEFAULT_CONTROLS: MediaControls = {
|
||||
sort: 'newest',
|
||||
variants: 'all',
|
||||
view: 'grid',
|
||||
page: FIRST_PAGE,
|
||||
size: DEFAULT_MEDIA_PAGE_SIZE,
|
||||
}
|
||||
|
||||
const FILTERS: MediaFilter[] = ['all', 'unused', 'duplicates']
|
||||
@@ -36,18 +45,57 @@ function pick<T extends string>(allowed: T[], value: string, fallback: T) {
|
||||
return (allowed as string[]).includes(value) ? (value as T) : fallback
|
||||
}
|
||||
|
||||
export function parseMediaControls(query: LocationQuery): MediaControls {
|
||||
function parsePage(value: string) {
|
||||
return POSITIVE_INTEGER.test(value) ? Number(value) : FIRST_PAGE
|
||||
}
|
||||
|
||||
function parseSize(value: LocationQuery[string] | undefined, useStoredSize: boolean) {
|
||||
const first = Array.isArray(value) ? value[0] : value
|
||||
const raw = first === null ? '' : first
|
||||
if (raw === undefined && !useStoredSize) {
|
||||
return DEFAULT_MEDIA_PAGE_SIZE
|
||||
}
|
||||
return resolvePageSize(raw, MEDIA_PAGE_SIZE_KEY, MEDIA_PAGE_SIZES, DEFAULT_MEDIA_PAGE_SIZE)
|
||||
}
|
||||
|
||||
export function parseMediaControls(query: LocationQuery, useStoredSize = true): MediaControls {
|
||||
return {
|
||||
q: firstValue(query.q).trim(),
|
||||
filter: pick(FILTERS, firstValue(query.filter), DEFAULT_CONTROLS.filter),
|
||||
sort: pick(SORTS, firstValue(query.sort), DEFAULT_CONTROLS.sort),
|
||||
variants: pick(VARIANTS_FILTERS, firstValue(query.variants), DEFAULT_CONTROLS.variants),
|
||||
view: pick(VIEWS, firstValue(query.view), DEFAULT_CONTROLS.view),
|
||||
page: parsePage(firstValue(query.page)),
|
||||
size: parseSize(query.size, useStoredSize),
|
||||
}
|
||||
}
|
||||
|
||||
export function isFullSet(controls: MediaControls) {
|
||||
return controls.filter === 'duplicates'
|
||||
}
|
||||
|
||||
export function sameMediaControls(left: MediaControls, right: MediaControls) {
|
||||
const samePaging =
|
||||
(isFullSet(left) && isFullSet(right)) ||
|
||||
(left.page === right.page && left.size === right.size)
|
||||
return (
|
||||
left.q === right.q &&
|
||||
left.filter === right.filter &&
|
||||
left.sort === right.sort &&
|
||||
left.variants === right.variants &&
|
||||
left.view === right.view &&
|
||||
samePaging
|
||||
)
|
||||
}
|
||||
|
||||
export function toRouteQuery(controls: MediaControls) {
|
||||
const query: Record<string, string> = {}
|
||||
if (!isFullSet(controls) && controls.page !== DEFAULT_CONTROLS.page) {
|
||||
query.page = String(controls.page)
|
||||
}
|
||||
if (!isFullSet(controls) && controls.size !== DEFAULT_CONTROLS.size) {
|
||||
query.size = String(controls.size)
|
||||
}
|
||||
if (controls.q) {
|
||||
query.q = controls.q
|
||||
}
|
||||
@@ -72,7 +120,12 @@ export function toRequestQuery(controls: MediaControls) {
|
||||
`filter=${controls.filter}`,
|
||||
`sort=${controls.sort}`,
|
||||
...(controls.variants === 'all' ? [] : [`variants=${controls.variants}`]),
|
||||
`limit=${MEDIA_PAGE_LIMIT}`,
|
||||
`page=${controls.page}`,
|
||||
`size=${controls.size}`,
|
||||
]
|
||||
return parts.join('&')
|
||||
}
|
||||
|
||||
export function toHashRequestQuery(hash: string) {
|
||||
return `hash=${hash}&page=${FIRST_PAGE}&size=${MEDIA_FULL_SET_PAGE_SIZE}`
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
export const ARTICLES_PAGE_SIZE_KEY = 'renewcms-page-size-articles'
|
||||
export const ACTIVITY_PAGE_SIZE_KEY = 'renewcms-page-size-activity'
|
||||
export const MEDIA_PAGE_SIZE_KEY = 'renewcms-page-size-media'
|
||||
|
||||
export function readStoredPageSize(key: string, validSizes: number[]) {
|
||||
try {
|
||||
|
||||
Reference in new issue
Block a user