mirror of
https://github.com/gotson/komga.git
synced 2026-05-07 12:01:40 +02:00
add age rating filter
This commit is contained in:
parent
a1d734e5a5
commit
44d2ca6d2d
11 changed files with 395 additions and 63 deletions
|
|
@ -247,3 +247,33 @@ export const releaseYearsQuery = defineQueryOptions(
|
|||
}
|
||||
},
|
||||
)
|
||||
|
||||
export const ageRatingsQuery = defineQueryOptions(
|
||||
({
|
||||
library_id,
|
||||
collection_id,
|
||||
pageRequest,
|
||||
}: {
|
||||
library_id?: string[]
|
||||
collection_id?: string[]
|
||||
pageRequest?: PageRequest
|
||||
}) => {
|
||||
const queryParams = {
|
||||
library_id: library_id,
|
||||
collection_id: collection_id,
|
||||
...pageRequest,
|
||||
}
|
||||
return {
|
||||
key: ['age-ratings', queryParams],
|
||||
query: () =>
|
||||
komgaClient
|
||||
.GET('/api/v2/age-ratings', {
|
||||
params: {
|
||||
query: queryParams,
|
||||
},
|
||||
})
|
||||
// unwrap the openapi-fetch structure on success
|
||||
.then((res) => res.data),
|
||||
}
|
||||
},
|
||||
)
|
||||
|
|
|
|||
2
next-ui/src/components.d.ts
vendored
2
next-ui/src/components.d.ts
vendored
|
|
@ -35,6 +35,7 @@ declare module 'vue' {
|
|||
EmptyStateConstruction: typeof import('./components/EmptyStateConstruction.vue')['default']
|
||||
EmptyStateNetworkError: typeof import('./components/EmptyStateNetworkError.vue')['default']
|
||||
FilterAnyAll: typeof import('./components/filter/AnyAll.vue')['default']
|
||||
FilterByAgeRating: typeof import('./components/filter/by/AgeRating.vue')['default']
|
||||
FilterByAuthor: typeof import('./components/filter/by/Author.vue')['default']
|
||||
FilterByGenre: typeof import('./components/filter/by/Genre.vue')['default']
|
||||
FilterByLanguage: typeof import('./components/filter/by/Language.vue')['default']
|
||||
|
|
@ -46,6 +47,7 @@ declare module 'vue' {
|
|||
FilterExpansionPanel: typeof import('./components/filter/ExpansionPanel.vue')['default']
|
||||
FilterList: typeof import('./components/filter/List.vue')['default']
|
||||
FilterSearchList: typeof import('./components/filter/SearchList.vue')['default']
|
||||
FilterSelectRange: typeof import('./components/filter/SelectRange.vue')['default']
|
||||
FilterTriState: typeof import('./components/filter/TriState.vue')['default']
|
||||
FormattedMessage: typeof import('./components/FormattedMessage.ts')['default']
|
||||
HelloWorld: typeof import('./components/HelloWorld.vue')['default']
|
||||
|
|
|
|||
86
next-ui/src/components/filter/SelectRange.vue
Normal file
86
next-ui/src/components/filter/SelectRange.vue
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
<template>
|
||||
<v-select
|
||||
v-model="model.is"
|
||||
:label="labelSelect"
|
||||
clearable
|
||||
hide-details
|
||||
:items="selectItems"
|
||||
@click:clear="model.is = undefined"
|
||||
/>
|
||||
<div class="px-2 pt-2">
|
||||
<div class="d-flex justify-space-between align-center mb-2">
|
||||
<span
|
||||
class="text-body-large"
|
||||
style="min-height: 26px"
|
||||
>{{ labelRange }}</span
|
||||
>
|
||||
<v-chip
|
||||
v-if="!!model.min && !!model.max"
|
||||
:disabled="isSingle"
|
||||
closable
|
||||
color="primary"
|
||||
rounded
|
||||
size="small"
|
||||
variant="elevated"
|
||||
@click:close="clearRange()"
|
||||
>{{ model.min }} - {{ model.max }}
|
||||
</v-chip>
|
||||
</div>
|
||||
|
||||
<v-range-slider
|
||||
v-model="modelRange"
|
||||
strict
|
||||
hide-details
|
||||
:disabled="disabled || isSingle"
|
||||
:step="1"
|
||||
:min="min"
|
||||
:max="max"
|
||||
thumb-label="hover"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { type FilterTypeSelectRange } from '@/types/filter'
|
||||
|
||||
const model = defineModel<FilterTypeSelectRange>({ required: true })
|
||||
|
||||
const {
|
||||
labelSelect,
|
||||
labelRange,
|
||||
selectItems,
|
||||
min,
|
||||
max,
|
||||
disabled = false,
|
||||
rangeMapper = (it) => it,
|
||||
} = defineProps<{
|
||||
labelSelect: string
|
||||
labelRange: string
|
||||
selectItems: unknown[]
|
||||
min?: number
|
||||
max?: number
|
||||
disabled?: boolean
|
||||
rangeMapper?: (number: number) => string | number
|
||||
}>()
|
||||
|
||||
const isSingle = computed(() => model.value.is !== undefined)
|
||||
|
||||
const modelRange = computed({
|
||||
get: () => [model.value.min || 0, model.value.max || 10000],
|
||||
set: (newValue) => {
|
||||
if (newValue) {
|
||||
model.value.min = rangeMapper(newValue[0] as number)
|
||||
model.value.max = rangeMapper(newValue[1] as number)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
function clearRange() {
|
||||
model.value.min = undefined
|
||||
model.value.max = undefined
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts"></script>
|
||||
|
||||
<style scoped></style>
|
||||
64
next-ui/src/components/filter/by/AgeRating.stories.ts
Normal file
64
next-ui/src/components/filter/by/AgeRating.stories.ts
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import AgeRating from './AgeRating.vue'
|
||||
import { fn } from 'storybook/test'
|
||||
|
||||
const meta = {
|
||||
component: AgeRating,
|
||||
render: (args: object) => ({
|
||||
components: { AgeRating },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
template: '<AgeRating v-model="args.modelValue" v-bind="args"/>',
|
||||
}),
|
||||
parameters: {
|
||||
// More on how to position stories at: https://storybook.js.org/docs/configure/story-layout
|
||||
docs: {
|
||||
description: {
|
||||
component: 'Age rating filter.',
|
||||
},
|
||||
},
|
||||
},
|
||||
args: {
|
||||
'onUpdate:modelValue': fn(),
|
||||
modelValue: { is: undefined },
|
||||
},
|
||||
} satisfies Meta<typeof AgeRating>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {},
|
||||
}
|
||||
|
||||
export const InitialValue: Story = {
|
||||
args: {
|
||||
modelValue: { is: 16 },
|
||||
},
|
||||
}
|
||||
|
||||
export const InitialValueAny: Story = {
|
||||
args: {
|
||||
modelValue: { is: 'any' },
|
||||
},
|
||||
}
|
||||
|
||||
export const InitialValueNone: Story = {
|
||||
args: {
|
||||
modelValue: { is: 'none' },
|
||||
},
|
||||
}
|
||||
|
||||
export const InitialRange: Story = {
|
||||
args: {
|
||||
modelValue: { min: 16, max: 18 },
|
||||
},
|
||||
}
|
||||
|
||||
export const InitialBoth: Story = {
|
||||
args: {
|
||||
modelValue: { is: 14, min: 16, max: 18 },
|
||||
},
|
||||
}
|
||||
59
next-ui/src/components/filter/by/AgeRating.vue
Normal file
59
next-ui/src/components/filter/by/AgeRating.vue
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
<template>
|
||||
<FilterSelectRange
|
||||
v-model="model"
|
||||
label-select="Age rating"
|
||||
label-range="Age range"
|
||||
:select-items="selectItems"
|
||||
:min="min"
|
||||
:max="max"
|
||||
:disabled="disabled"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import * as v from 'valibot'
|
||||
import { filterKeys, filterMessages, SchemaSeriesAgeRatings } from '@/types/filter'
|
||||
import { useQuery } from '@pinia/colada'
|
||||
import { ageRatingsQuery } from '@/colada/referential'
|
||||
import { PageRequest } from '@/types/PageRequest'
|
||||
import { useIntl } from 'vue-intl'
|
||||
|
||||
const intl = useIntl()
|
||||
|
||||
type AgeRatings = v.InferOutput<typeof SchemaSeriesAgeRatings>
|
||||
|
||||
const model = defineModel<AgeRatings>({ required: true })
|
||||
|
||||
const filterContext = inject(filterKeys.context, {})
|
||||
|
||||
const apiQuery = {
|
||||
...filterContext,
|
||||
}
|
||||
|
||||
const { data: items } = useQuery(() => ({
|
||||
...ageRatingsQuery({
|
||||
pageRequest: PageRequest.Unpaged(),
|
||||
...apiQuery,
|
||||
}),
|
||||
}))
|
||||
const disabled = computed(() => (items.value?.totalElements || 0) === 0)
|
||||
|
||||
const selectItems = computed(() => [
|
||||
{
|
||||
title: intl.formatMessage(filterMessages.any!),
|
||||
value: 'any',
|
||||
},
|
||||
{
|
||||
title: intl.formatMessage(filterMessages.none!),
|
||||
value: 'none',
|
||||
},
|
||||
...(items.value?.content?.map((it) => ({ title: it, value: it })) || []),
|
||||
])
|
||||
|
||||
const min = computed(() => items.value?.content?.map((it) => Number(it))?.at(0))
|
||||
const max = computed(() => items.value?.content?.map((it) => Number(it))?.at(-1))
|
||||
</script>
|
||||
|
||||
<script lang="ts"></script>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
@ -1,43 +1,14 @@
|
|||
<template>
|
||||
<v-select
|
||||
v-model="model.is"
|
||||
label="Year"
|
||||
clearable
|
||||
hide-details
|
||||
:items="selectItems"
|
||||
@click:clear="model.is = undefined"
|
||||
<FilterSelectRange
|
||||
v-model="model"
|
||||
label-select="Year"
|
||||
label-range="Year range"
|
||||
:select-items="selectItems"
|
||||
:min="min"
|
||||
:max="max"
|
||||
:disabled="disabled"
|
||||
:range-mapper="(number) => number.toString()"
|
||||
/>
|
||||
<div class="px-2 pt-2">
|
||||
<div class="d-flex justify-space-between align-center mb-2">
|
||||
<span
|
||||
class="text-body-large"
|
||||
style="min-height: 26px"
|
||||
>Year range</span
|
||||
>
|
||||
<v-chip
|
||||
v-if="!!model.min && !!model.max"
|
||||
:disabled="isSingle"
|
||||
closable
|
||||
color="primary"
|
||||
rounded
|
||||
size="small"
|
||||
variant="elevated"
|
||||
@click:close="clearRange()"
|
||||
>{{ model.min }} - {{ model.max }}
|
||||
</v-chip>
|
||||
</div>
|
||||
|
||||
<v-range-slider
|
||||
v-model="modelRange"
|
||||
strict
|
||||
hide-details
|
||||
:disabled="disabled || isSingle"
|
||||
:step="1"
|
||||
:min="min"
|
||||
:max="max"
|
||||
thumb-label="hover"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
|
@ -52,7 +23,7 @@ const intl = useIntl()
|
|||
|
||||
type ReleaseYears = v.InferOutput<typeof SchemaSeriesReleaseYears>
|
||||
|
||||
const model = defineModel<ReleaseYears>({ default: [] })
|
||||
const model = defineModel<ReleaseYears>({ required: true })
|
||||
|
||||
const filterContext = inject(filterKeys.context, {})
|
||||
|
||||
|
|
@ -67,7 +38,6 @@ const { data: items } = useQuery(() => ({
|
|||
}),
|
||||
}))
|
||||
const disabled = computed(() => (items.value?.totalElements || 0) === 0)
|
||||
const isSingle = computed(() => model.value.is !== undefined)
|
||||
|
||||
const selectItems = computed(() => [
|
||||
{
|
||||
|
|
@ -83,19 +53,6 @@ const selectItems = computed(() => [
|
|||
|
||||
const min = computed(() => items.value?.content?.map((it) => Number(it))?.at(-1))
|
||||
const max = computed(() => items.value?.content?.map((it) => Number(it))?.at(0))
|
||||
|
||||
const modelRange = computed({
|
||||
get: () => [model.value.min || 0, model.value.max || 10000],
|
||||
set: (newValue) => {
|
||||
model.value.min = newValue?.[0]?.toString()
|
||||
model.value.max = newValue?.[1]?.toString()
|
||||
},
|
||||
})
|
||||
|
||||
function clearRange() {
|
||||
model.value.min = undefined
|
||||
model.value.max = undefined
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts"></script>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import {
|
|||
SchemaFilterAuthors,
|
||||
type SchemaFilterSeriesStatus,
|
||||
SchemaFilterStrings,
|
||||
SchemaSeriesAgeRatings,
|
||||
SchemaSeriesReleaseYears,
|
||||
} from '@/types/filter'
|
||||
import type { InferOutput } from 'valibot'
|
||||
|
|
@ -140,3 +141,44 @@ export function schemaFilterReleaseYearToConditions(
|
|||
allOf: conds,
|
||||
}
|
||||
}
|
||||
|
||||
export function schemaFilterAgeRatingToConditions(
|
||||
filter: InferOutput<typeof SchemaSeriesAgeRatings>,
|
||||
) {
|
||||
const conds = []
|
||||
if (filter.is === 'any') {
|
||||
conds.push({
|
||||
ageRating: {
|
||||
operator: 'isNotNull',
|
||||
},
|
||||
})
|
||||
} else if (filter.is === 'none') {
|
||||
conds.push({
|
||||
ageRating: {
|
||||
operator: 'isNull',
|
||||
},
|
||||
})
|
||||
} else {
|
||||
if (!!filter.is || !!filter.min) {
|
||||
const v = Number(filter.is || filter.min)
|
||||
conds.push({
|
||||
ageRating: {
|
||||
operator: 'greaterThan',
|
||||
value: v,
|
||||
},
|
||||
})
|
||||
}
|
||||
if (!!filter.is || !!filter.max) {
|
||||
const v = Number(filter.is || filter.max)
|
||||
conds.push({
|
||||
ageRating: {
|
||||
operator: 'lessThan',
|
||||
value: v,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
return {
|
||||
allOf: conds,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ const mockPublishers = doMockStrings(10000, 'Publisher')
|
|||
const mockSharingLabels = doMockStrings(150, 'SharingLabel')
|
||||
const mockLanguages = ['de', 'en', 'en-US', 'es', 'fr', 'fr-CA', 'ja', 'it']
|
||||
const mockReleaseYears = ['2022', '2021', '2020', '2019', '2018', '2016', '1988', '1970']
|
||||
const mockAgeRatings = [5, 6, 7, 8, 12, 14, 16, 18]
|
||||
|
||||
function filterAndPage(
|
||||
search: string | null,
|
||||
|
|
@ -119,6 +120,19 @@ export const referentialHandlers = [
|
|||
),
|
||||
),
|
||||
),
|
||||
httpTyped.get('/api/v2/age-ratings', ({ query, response }) =>
|
||||
response(200).json(
|
||||
mockPage(
|
||||
mockAgeRatings,
|
||||
new PageRequest(
|
||||
Number(query.get('page')),
|
||||
Number(query.get('size')),
|
||||
undefined,
|
||||
Boolean(query.get('unpaged')),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
httpTyped.get('/api/v2/authors', ({ query, response }) => {
|
||||
const search = query.get('search')
|
||||
const role = query.get('role')
|
||||
|
|
|
|||
|
|
@ -96,11 +96,19 @@
|
|||
<FilterExpansionPanel
|
||||
title="Release year"
|
||||
:count="!!filterReleaseYear.is ? 1 : !!filterReleaseYear.min ? 1 : 0"
|
||||
@clear="clearFilterYear()"
|
||||
@clear="clearFilterSelectRange(filterReleaseYear)"
|
||||
>
|
||||
<FilterByReleaseYear v-model="filterReleaseYear" />
|
||||
</FilterExpansionPanel>
|
||||
|
||||
<FilterExpansionPanel
|
||||
title="Age rating"
|
||||
:count="!!filterAgeRating.is ? 1 : !!filterAgeRating.min ? 1 : 0"
|
||||
@clear="clearFilterSelectRange(filterAgeRating)"
|
||||
>
|
||||
<FilterByAgeRating v-model="filterAgeRating" />
|
||||
</FilterExpansionPanel>
|
||||
|
||||
<FilterExpansionPanel
|
||||
title="Language"
|
||||
:count="filterLanguage.v.length"
|
||||
|
|
@ -235,13 +243,16 @@ import {
|
|||
schemaFilterStringToConditions,
|
||||
schemaFilterSeriesStatusToConditions,
|
||||
schemaFilterReleaseYearToConditions,
|
||||
schemaFilterAgeRatingToConditions,
|
||||
} from '@/functions/filter'
|
||||
import * as v from 'valibot'
|
||||
import {
|
||||
type FilterType,
|
||||
type FilterTypeSelectRange,
|
||||
SchemaFilterAuthors,
|
||||
SchemaFilterSeriesStatus,
|
||||
SchemaFilterStrings,
|
||||
SchemaSeriesAgeRatings,
|
||||
SchemaSeriesReleaseYears,
|
||||
} from '@/types/filter'
|
||||
import { useRouteQuerySchema } from '@/composables/useRouteQuerySchema'
|
||||
|
|
@ -296,10 +307,10 @@ function clearFilter(filter: FilterType) {
|
|||
if ('m' in filter) filter.m = 'anyOf'
|
||||
}
|
||||
|
||||
function clearFilterYear() {
|
||||
filterReleaseYear.value.is = undefined
|
||||
filterReleaseYear.value.min = undefined
|
||||
filterReleaseYear.value.max = undefined
|
||||
function clearFilterSelectRange(filter: FilterTypeSelectRange) {
|
||||
filter.is = undefined
|
||||
filter.min = undefined
|
||||
filter.max = undefined
|
||||
}
|
||||
|
||||
const { data: filterSeriesStatus } = useRouteQuerySchema('status', SchemaFilterSeriesStatus)
|
||||
|
|
@ -309,6 +320,7 @@ const { data: filterPublisher } = useRouteQuerySchema('publisher', SchemaFilterS
|
|||
const { data: filterSharingLabel } = useRouteQuerySchema('sharingLabel', SchemaFilterStrings)
|
||||
const { data: filterLanguage } = useRouteQuerySchema('language', SchemaFilterStrings)
|
||||
const { data: filterReleaseYear } = useRouteQuerySchema('year', SchemaSeriesReleaseYears)
|
||||
const { data: filterAgeRating } = useRouteQuerySchema('age', SchemaSeriesAgeRatings)
|
||||
|
||||
const conds = computed(() => ({
|
||||
allOf: [
|
||||
|
|
@ -320,6 +332,7 @@ const conds = computed(() => ({
|
|||
schemaFilterStringToConditions(filterSharingLabel.value, 'sharingLabel', true),
|
||||
schemaFilterStringToConditions(filterLanguage.value, 'language', false),
|
||||
schemaFilterReleaseYearToConditions(filterReleaseYear.value),
|
||||
schemaFilterAgeRatingToConditions(filterAgeRating.value),
|
||||
...Object.entries(filterAuthors).map(([, filter]) =>
|
||||
schemaFilterAuthorsToConditions(toValue(filter.filter), toValue(filter.role)),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import {
|
|||
SchemaFilterAuthors,
|
||||
SchemaFilterSeriesStatus,
|
||||
SchemaFilterStrings,
|
||||
SchemaSeriesAgeRatings,
|
||||
SchemaSeriesReleaseYears,
|
||||
SchemaSeriesStatus,
|
||||
} from '@/types/filter'
|
||||
|
|
@ -103,6 +104,49 @@ describe('schema series release years', () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe('schema series age ratings', () => {
|
||||
test('correct value exact', () => {
|
||||
const input = { is: 10 }
|
||||
const result = v.parse(SchemaSeriesAgeRatings, input)
|
||||
|
||||
expect(result).toStrictEqual(input)
|
||||
})
|
||||
|
||||
test('correct value min', () => {
|
||||
const input = { min: 12 }
|
||||
const result = v.parse(SchemaSeriesAgeRatings, input)
|
||||
|
||||
expect(result).toStrictEqual(input)
|
||||
})
|
||||
|
||||
test('correct value max', () => {
|
||||
const input = { max: 15 }
|
||||
const result = v.parse(SchemaSeriesAgeRatings, input)
|
||||
|
||||
expect(result).toStrictEqual(input)
|
||||
})
|
||||
|
||||
test('correct value is: any', () => {
|
||||
const input = { is: 'any' }
|
||||
const result = v.parse(SchemaSeriesAgeRatings, input)
|
||||
|
||||
expect(result).toStrictEqual(input)
|
||||
})
|
||||
|
||||
test('correct value is: none', () => {
|
||||
const input = { is: 'none' }
|
||||
const result = v.parse(SchemaSeriesAgeRatings, input)
|
||||
|
||||
expect(result).toStrictEqual(input)
|
||||
})
|
||||
|
||||
test('other value throws error', () => {
|
||||
const input = { is: '22' }
|
||||
|
||||
expect(() => v.parse(SchemaSeriesAgeRatings, input)).toThrowError()
|
||||
})
|
||||
})
|
||||
|
||||
describe('filter schemas have a default value', () => {
|
||||
test('SchemaFilterSeriesStatus', () => {
|
||||
const expected = { v: [] }
|
||||
|
|
@ -131,4 +175,11 @@ describe('filter schemas have a default value', () => {
|
|||
|
||||
expect(defaults).toStrictEqual(expected)
|
||||
})
|
||||
|
||||
test('SchemaSeriesAgeRatings', () => {
|
||||
const expected = { is: undefined, min: undefined, max: undefined }
|
||||
const defaults = v.getDefaults(SchemaSeriesAgeRatings)
|
||||
|
||||
expect(defaults).toStrictEqual(expected)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -95,6 +95,14 @@ function createSchemaFilterArray<T extends v.GenericSchema>(schema: T) {
|
|||
})
|
||||
}
|
||||
|
||||
function createSchemaFilterSelectRange<T extends v.GenericSchema>(schema: T) {
|
||||
return v.strictObject({
|
||||
is: v.optional(v.union([v.picklist(['any', 'none']), schema])),
|
||||
min: v.optional(schema),
|
||||
max: v.optional(schema),
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Schema for Series Status.
|
||||
*/
|
||||
|
|
@ -103,11 +111,12 @@ export const SchemaFilterSeriesStatus = createSchemaFilterArray(SchemaSeriesStat
|
|||
/**
|
||||
* Schema for Series Release Years
|
||||
*/
|
||||
export const SchemaSeriesReleaseYears = v.strictObject({
|
||||
is: v.optional(v.union([v.picklist(['any', 'none']), SchemaYear])),
|
||||
min: v.optional(SchemaYear),
|
||||
max: v.optional(SchemaYear),
|
||||
})
|
||||
export const SchemaSeriesReleaseYears = createSchemaFilterSelectRange(SchemaYear)
|
||||
|
||||
/**
|
||||
* Schema for Series Age Ratings
|
||||
*/
|
||||
export const SchemaSeriesAgeRatings = createSchemaFilterSelectRange(v.number())
|
||||
|
||||
/**
|
||||
* Schema for a list of string.
|
||||
|
|
@ -126,4 +135,9 @@ export type FilterTypeAnyAll = v.InferOutput<typeof SchemaFilterAnyAll>
|
|||
export const SchemaFilterArray = createSchemaFilterArray(v.unknown())
|
||||
export type FilterTypeSimpleList = v.InferOutput<typeof SchemaFilterArray>
|
||||
|
||||
export const SchemaFilterSelectRange = createSchemaFilterSelectRange(
|
||||
v.union([v.string(), v.number()]),
|
||||
)
|
||||
export type FilterTypeSelectRange = v.InferOutput<typeof SchemaFilterSelectRange>
|
||||
|
||||
export type FilterType = FilterTypeAnyAll | FilterTypeSimpleList
|
||||
|
|
|
|||
Loading…
Reference in a new issue