mirror of
https://github.com/gotson/komga.git
synced 2026-05-08 12:35:30 +02:00
filter by read status and other stuff
This commit is contained in:
parent
44d2ca6d2d
commit
92482a9f39
15 changed files with 378 additions and 149 deletions
2
next-ui/src/components.d.ts
vendored
2
next-ui/src/components.d.ts
vendored
|
|
@ -40,6 +40,7 @@ declare module 'vue' {
|
|||
FilterByGenre: typeof import('./components/filter/by/Genre.vue')['default']
|
||||
FilterByLanguage: typeof import('./components/filter/by/Language.vue')['default']
|
||||
FilterByPublisher: typeof import('./components/filter/by/Publisher.vue')['default']
|
||||
FilterByReadStatus: typeof import('./components/filter/by/ReadStatus.vue')['default']
|
||||
FilterByReleaseYear: typeof import('./components/filter/by/ReleaseYear.vue')['default']
|
||||
FilterBySeriesStatus: typeof import('./components/filter/by/SeriesStatus.vue')['default']
|
||||
FilterBySharingLabel: typeof import('./components/filter/by/SharingLabel.vue')['default']
|
||||
|
|
@ -107,6 +108,7 @@ declare module 'vue' {
|
|||
SeriesMenuBottomSheet: typeof import('./components/series/menu/SeriesMenuBottomSheet.vue')['default']
|
||||
ServerSettings: typeof import('./components/server/Settings.vue')['default']
|
||||
SnackQueue: typeof import('./components/SnackQueue.vue')['default']
|
||||
TempDrawer: typeof import('./components/TempDrawer.vue')['default']
|
||||
ThemeSelector: typeof import('./components/ThemeSelector.vue')['default']
|
||||
UserAuthenticationActivityTable: typeof import('./components/user/AuthenticationActivityTable.vue')['default']
|
||||
UserDeletionWarning: typeof import('./components/user/DeletionWarning.vue')['default']
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<v-menu>
|
||||
<template #activator="{ props }">
|
||||
<v-btn
|
||||
<v-icon-btn
|
||||
v-bind="props"
|
||||
icon="i-mdi:translate"
|
||||
:aria-label="
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<v-menu>
|
||||
<template #activator="{ props }">
|
||||
<v-btn
|
||||
<v-icon-btn
|
||||
v-bind="props"
|
||||
icon="i-mdi:view-grid-plus"
|
||||
:aria-label="
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<v-btn
|
||||
<v-icon-btn
|
||||
:id="id"
|
||||
v-tooltip:bottom="allModes[currentMode].title"
|
||||
:icon="allModes[currentMode].icon"
|
||||
|
|
|
|||
30
next-ui/src/components/TempDrawer.vue
Normal file
30
next-ui/src/components/TempDrawer.vue
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
<template>
|
||||
<!-- Teleport is needed so that the scrim covers the whole screen -->
|
||||
<Teleport to="#app">
|
||||
<!-- order=-1 is needed for the drawer to open full height -->
|
||||
<!-- disable-route-watcher is needed, else the drawer closes when the route query params are updated when the filters change -->
|
||||
<v-navigation-drawer
|
||||
v-model="model"
|
||||
:location="location"
|
||||
temporary
|
||||
order="-1"
|
||||
disable-route-watcher
|
||||
>
|
||||
<v-icon-btn
|
||||
icon="i-mdi:close"
|
||||
variant="text"
|
||||
class="position-absolute top-0 right-0 me-2 mt-1"
|
||||
style="z-index: 2"
|
||||
@click="model = false"
|
||||
/>
|
||||
<slot />
|
||||
</v-navigation-drawer>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const model = defineModel<boolean>({ default: false })
|
||||
const { location = 'end' } = defineProps<{
|
||||
location?: 'top' | 'end' | 'bottom' | 'start' | 'left' | 'right'
|
||||
}>()
|
||||
</script>
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<v-btn
|
||||
<v-icon-btn
|
||||
:icon="themeIcon"
|
||||
:aria-label="
|
||||
$formatMessage({
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@
|
|||
rounded
|
||||
closable
|
||||
class="ms-2"
|
||||
variant="elevated"
|
||||
size="small"
|
||||
@click:close="emit('clear')"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@
|
|||
v-model="modelRange"
|
||||
strict
|
||||
hide-details
|
||||
color="primary"
|
||||
:disabled="disabled || isSingle"
|
||||
:step="1"
|
||||
:min="min"
|
||||
|
|
|
|||
43
next-ui/src/components/filter/by/ReadStatus.stories.ts
Normal file
43
next-ui/src/components/filter/by/ReadStatus.stories.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import ReadStatus from './ReadStatus.vue'
|
||||
import { fn } from 'storybook/test'
|
||||
|
||||
const meta = {
|
||||
component: ReadStatus,
|
||||
render: (args: object) => ({
|
||||
components: { ReadStatus },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
template: '<ReadStatus v-model="args.modelValue"/>',
|
||||
}),
|
||||
parameters: {
|
||||
// More on how to position stories at: https://storybook.js.org/docs/configure/story-layout
|
||||
docs: {
|
||||
description: {
|
||||
component: 'Read Status filter.',
|
||||
},
|
||||
},
|
||||
},
|
||||
args: {
|
||||
'onUpdate:modelValue': fn(),
|
||||
modelValue: [],
|
||||
},
|
||||
} satisfies Meta<typeof ReadStatus>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {},
|
||||
}
|
||||
|
||||
export const InitialValue: Story = {
|
||||
args: {
|
||||
modelValue: [
|
||||
{ i: 'e', v: 'UNREAD' },
|
||||
{ i: 'i', v: 'IN_PROGRESS' },
|
||||
],
|
||||
},
|
||||
}
|
||||
30
next-ui/src/components/filter/by/ReadStatus.vue
Normal file
30
next-ui/src/components/filter/by/ReadStatus.vue
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
<template>
|
||||
<FilterList
|
||||
v-model="model"
|
||||
:items="items"
|
||||
color="primary"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import * as v from 'valibot'
|
||||
import { SchemaReadStatus } from '@/types/filter'
|
||||
import { useIntl } from 'vue-intl'
|
||||
import { ReadStatus, readStatusMessages } from '@/types/ReadStatus'
|
||||
|
||||
type SchReadStatus = v.InferOutput<typeof SchemaReadStatus>
|
||||
|
||||
const intl = useIntl()
|
||||
|
||||
const model = defineModel<SchReadStatus[]>({ default: [] })
|
||||
|
||||
const items = Object.values(ReadStatus).map((it) => ({
|
||||
title: intl.formatMessage(readStatusMessages[it]),
|
||||
value: { i: 'i', v: it },
|
||||
valueExclude: { i: 'e', v: it },
|
||||
}))
|
||||
</script>
|
||||
|
||||
<script lang="ts"></script>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import {
|
||||
SchemaAnyNone,
|
||||
SchemaFilterAuthors,
|
||||
SchemaFilterReadStatus,
|
||||
type SchemaFilterSeriesStatus,
|
||||
SchemaFilterStrings,
|
||||
SchemaSeriesAgeRatings,
|
||||
|
|
@ -61,6 +62,22 @@ export function schemaFilterAuthorsToConditions(
|
|||
}
|
||||
}
|
||||
|
||||
export function schemaFilterReadStatusToConditions(
|
||||
filter: InferOutput<typeof SchemaFilterReadStatus>,
|
||||
) {
|
||||
const list = filter.v.map((it) => {
|
||||
return {
|
||||
readStatus: {
|
||||
operator: it.i === 'e' ? 'isNot' : 'is',
|
||||
value: it.v,
|
||||
},
|
||||
}
|
||||
})
|
||||
return {
|
||||
anyOf: list,
|
||||
}
|
||||
}
|
||||
|
||||
export function schemaFilterStringToConditions(
|
||||
filter: InferOutput<typeof SchemaFilterStrings>,
|
||||
key: string,
|
||||
|
|
@ -158,22 +175,27 @@ export function schemaFilterAgeRatingToConditions(
|
|||
operator: 'isNull',
|
||||
},
|
||||
})
|
||||
} else if (!!filter.is) {
|
||||
conds.push({
|
||||
ageRating: {
|
||||
operator: 'is',
|
||||
value: filter.is,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
if (!!filter.is || !!filter.min) {
|
||||
const v = Number(filter.is || filter.min)
|
||||
if (!!filter.min) {
|
||||
conds.push({
|
||||
ageRating: {
|
||||
operator: 'greaterThan',
|
||||
value: v,
|
||||
value: filter.min,
|
||||
},
|
||||
})
|
||||
}
|
||||
if (!!filter.is || !!filter.max) {
|
||||
const v = Number(filter.is || filter.max)
|
||||
if (!!filter.max) {
|
||||
conds.push({
|
||||
ageRating: {
|
||||
operator: 'lessThan',
|
||||
value: v,
|
||||
value: filter.max,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,141 +26,164 @@
|
|||
:sizes="[1, 10, 20]"
|
||||
/>
|
||||
|
||||
<v-icon-btn
|
||||
icon="i-mdi:filter-variant"
|
||||
@click="filterDrawer = true"
|
||||
/>
|
||||
<v-badge
|
||||
location="top right"
|
||||
color="primary"
|
||||
:content="filterCount"
|
||||
:model-value="filterCount > 0"
|
||||
class="pe-4"
|
||||
offset-x="7"
|
||||
offset-y="7"
|
||||
>
|
||||
<v-icon-btn
|
||||
icon="i-mdi:filter-variant"
|
||||
@click="filterDrawer = true"
|
||||
/>
|
||||
</v-badge>
|
||||
</v-app-bar>
|
||||
|
||||
<!-- TODO: Move into its own component -->
|
||||
<!-- Teleport is needed so that the scrim covers the whole screen -->
|
||||
<Teleport to="#app">
|
||||
<!-- order=-1 is needed for the drawer to open full height -->
|
||||
<!-- disable-route-watcher is needed, else the drawer closes when the route query params are updated when the filters change -->
|
||||
<v-navigation-drawer
|
||||
v-model="filterDrawer"
|
||||
location="end"
|
||||
temporary
|
||||
order="-1"
|
||||
disable-route-watcher
|
||||
>
|
||||
<v-list>
|
||||
<v-expansion-panels
|
||||
v-model="filterExpansionPanels"
|
||||
variant="accordion"
|
||||
class="no-padding"
|
||||
flat
|
||||
tile
|
||||
<TempDrawer v-model="filterDrawer">
|
||||
<v-list>
|
||||
<v-list-subheader>
|
||||
<div class="d-flex ga-2 align-center mb-1">
|
||||
<span>FILTERS</span>
|
||||
<v-chip
|
||||
v-if="filterCount > 0"
|
||||
color="primary"
|
||||
rounded
|
||||
closable
|
||||
variant="elevated"
|
||||
size="small"
|
||||
@click:close="clearFilters()"
|
||||
>
|
||||
{{ filterCount }}
|
||||
</v-chip>
|
||||
</div>
|
||||
</v-list-subheader>
|
||||
|
||||
<v-expansion-panels
|
||||
v-model="filterExpansionPanels"
|
||||
variant="accordion"
|
||||
class="no-padding"
|
||||
flat
|
||||
tile
|
||||
>
|
||||
<FilterExpansionPanel
|
||||
title="Read status"
|
||||
:count="filterReadStatus.v.length"
|
||||
@clear="clearFilter(filterReadStatus)"
|
||||
>
|
||||
<FilterExpansionPanel
|
||||
title="Status"
|
||||
:count="filterSeriesStatus.v.length"
|
||||
@clear="clearFilter(filterSeriesStatus)"
|
||||
>
|
||||
<FilterBySeriesStatus v-model="filterSeriesStatus.v" />
|
||||
</FilterExpansionPanel>
|
||||
|
||||
<FilterExpansionPanel
|
||||
title="Genre"
|
||||
:count="filterGenre.v.length"
|
||||
@clear="clearFilter(filterGenre)"
|
||||
>
|
||||
<FilterByGenre
|
||||
v-model="filterGenre.v"
|
||||
v-model:mode="filterGenre.m"
|
||||
/>
|
||||
</FilterExpansionPanel>
|
||||
|
||||
<FilterExpansionPanel
|
||||
title="Tag"
|
||||
:count="filterTag.v.length"
|
||||
@clear="clearFilter(filterTag)"
|
||||
>
|
||||
<FilterByTag
|
||||
v-model="filterTag.v"
|
||||
v-model:mode="filterTag.m"
|
||||
/>
|
||||
</FilterExpansionPanel>
|
||||
|
||||
<FilterExpansionPanel
|
||||
title="Publisher"
|
||||
:count="filterPublisher.v.length"
|
||||
@clear="clearFilter(filterPublisher)"
|
||||
>
|
||||
<FilterByPublisher
|
||||
v-model="filterPublisher.v"
|
||||
v-model:mode="filterPublisher.m"
|
||||
/>
|
||||
</FilterExpansionPanel>
|
||||
|
||||
<FilterExpansionPanel
|
||||
title="Release year"
|
||||
:count="!!filterReleaseYear.is ? 1 : !!filterReleaseYear.min ? 1 : 0"
|
||||
@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"
|
||||
@clear="clearFilter(filterLanguage)"
|
||||
>
|
||||
<FilterByLanguage
|
||||
v-model="filterLanguage.v"
|
||||
v-model:mode="filterLanguage.m"
|
||||
/>
|
||||
</FilterExpansionPanel>
|
||||
|
||||
<FilterExpansionPanel
|
||||
title="Sharing label"
|
||||
:count="filterSharingLabel.v.length"
|
||||
@clear="clearFilter(filterSharingLabel)"
|
||||
>
|
||||
<FilterBySharingLabel
|
||||
v-model="filterSharingLabel.v"
|
||||
v-model:mode="filterSharingLabel.m"
|
||||
/>
|
||||
</FilterExpansionPanel>
|
||||
</v-expansion-panels>
|
||||
|
||||
<v-divider />
|
||||
|
||||
<v-list-subheader>CREATORS</v-list-subheader>
|
||||
|
||||
<v-expansion-panels
|
||||
v-model="filterExpansionPanels"
|
||||
variant="accordion"
|
||||
class="no-padding"
|
||||
flat
|
||||
tile
|
||||
<FilterByReadStatus v-model="filterReadStatus.v" />
|
||||
</FilterExpansionPanel>
|
||||
<FilterExpansionPanel
|
||||
title="Status"
|
||||
:count="filterSeriesStatus.v.length"
|
||||
@clear="clearFilter(filterSeriesStatus)"
|
||||
>
|
||||
<FilterExpansionPanel
|
||||
v-for="(filterAuthor, role) in filterAuthors"
|
||||
:key="role"
|
||||
:title="filterAuthor.text"
|
||||
:count="filterAuthor.filter.v.length"
|
||||
@clear="clearFilter(filterAuthor.filter)"
|
||||
>
|
||||
<FilterByAuthor
|
||||
v-model="filterAuthor.filter.v"
|
||||
v-model:mode="filterAuthor.filter.m"
|
||||
:role="filterAuthor.role"
|
||||
/>
|
||||
</FilterExpansionPanel>
|
||||
</v-expansion-panels>
|
||||
</v-list>
|
||||
</v-navigation-drawer>
|
||||
</Teleport>
|
||||
<FilterBySeriesStatus v-model="filterSeriesStatus.v" />
|
||||
</FilterExpansionPanel>
|
||||
|
||||
<FilterExpansionPanel
|
||||
title="Genre"
|
||||
:count="filterGenre.v.length"
|
||||
@clear="clearFilter(filterGenre)"
|
||||
>
|
||||
<FilterByGenre
|
||||
v-model="filterGenre.v"
|
||||
v-model:mode="filterGenre.m"
|
||||
/>
|
||||
</FilterExpansionPanel>
|
||||
|
||||
<FilterExpansionPanel
|
||||
title="Tag"
|
||||
:count="filterTag.v.length"
|
||||
@clear="clearFilter(filterTag)"
|
||||
>
|
||||
<FilterByTag
|
||||
v-model="filterTag.v"
|
||||
v-model:mode="filterTag.m"
|
||||
/>
|
||||
</FilterExpansionPanel>
|
||||
|
||||
<FilterExpansionPanel
|
||||
title="Publisher"
|
||||
:count="filterPublisher.v.length"
|
||||
@clear="clearFilter(filterPublisher)"
|
||||
>
|
||||
<FilterByPublisher
|
||||
v-model="filterPublisher.v"
|
||||
v-model:mode="filterPublisher.m"
|
||||
/>
|
||||
</FilterExpansionPanel>
|
||||
|
||||
<FilterExpansionPanel
|
||||
title="Release year"
|
||||
:count="!!filterReleaseYear.is ? 1 : !!filterReleaseYear.min ? 1 : 0"
|
||||
@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"
|
||||
@clear="clearFilter(filterLanguage)"
|
||||
>
|
||||
<FilterByLanguage
|
||||
v-model="filterLanguage.v"
|
||||
v-model:mode="filterLanguage.m"
|
||||
/>
|
||||
</FilterExpansionPanel>
|
||||
|
||||
<FilterExpansionPanel
|
||||
title="Sharing label"
|
||||
:count="filterSharingLabel.v.length"
|
||||
@clear="clearFilter(filterSharingLabel)"
|
||||
>
|
||||
<FilterBySharingLabel
|
||||
v-model="filterSharingLabel.v"
|
||||
v-model:mode="filterSharingLabel.m"
|
||||
/>
|
||||
</FilterExpansionPanel>
|
||||
</v-expansion-panels>
|
||||
|
||||
<v-divider><span class="text-body-medium text-medium-emphasis">Creators</span></v-divider>
|
||||
|
||||
<v-expansion-panels
|
||||
v-model="filterExpansionPanels"
|
||||
variant="accordion"
|
||||
class="no-padding"
|
||||
flat
|
||||
tile
|
||||
>
|
||||
<FilterExpansionPanel
|
||||
v-for="(filterAuthor, role) in filterAuthors"
|
||||
:key="role"
|
||||
:title="filterAuthor.text"
|
||||
:count="filterAuthor.filter.v.length"
|
||||
@clear="clearFilter(filterAuthor.filter)"
|
||||
>
|
||||
<FilterByAuthor
|
||||
v-model="filterAuthor.filter.v"
|
||||
v-model:mode="filterAuthor.filter.m"
|
||||
:role="filterAuthor.role"
|
||||
/>
|
||||
</FilterExpansionPanel>
|
||||
</v-expansion-panels>
|
||||
|
||||
<v-divider />
|
||||
|
||||
<v-list-subheader>SORT</v-list-subheader>
|
||||
</v-list>
|
||||
</TempDrawer>
|
||||
<div>CONDITION</div>
|
||||
<div>{{ conds }}</div>
|
||||
|
||||
|
|
@ -244,12 +267,14 @@ import {
|
|||
schemaFilterSeriesStatusToConditions,
|
||||
schemaFilterReleaseYearToConditions,
|
||||
schemaFilterAgeRatingToConditions,
|
||||
schemaFilterReadStatusToConditions,
|
||||
} from '@/functions/filter'
|
||||
import * as v from 'valibot'
|
||||
import {
|
||||
type FilterType,
|
||||
type FilterTypeSelectRange,
|
||||
SchemaFilterAuthors,
|
||||
SchemaFilterReadStatus,
|
||||
SchemaFilterSeriesStatus,
|
||||
SchemaFilterStrings,
|
||||
SchemaSeriesAgeRatings,
|
||||
|
|
@ -313,7 +338,37 @@ function clearFilterSelectRange(filter: FilterTypeSelectRange) {
|
|||
filter.max = undefined
|
||||
}
|
||||
|
||||
function clearFilters() {
|
||||
clearFilter(filterSeriesStatus.value)
|
||||
clearFilter(filterReadStatus.value)
|
||||
clearFilter(filterGenre.value)
|
||||
clearFilter(filterTag.value)
|
||||
clearFilter(filterPublisher.value)
|
||||
clearFilter(filterSharingLabel.value)
|
||||
clearFilter(filterLanguage.value)
|
||||
clearFilterSelectRange(filterReleaseYear.value)
|
||||
clearFilterSelectRange(filterAgeRating.value)
|
||||
Object.entries(filterAuthors).map(([, filter]) => clearFilter(filter.filter))
|
||||
}
|
||||
|
||||
const filterCount = computed(
|
||||
() =>
|
||||
filterReadStatus.value.v.length +
|
||||
filterSeriesStatus.value.v.length +
|
||||
filterGenre.value.v.length +
|
||||
filterTag.value.v.length +
|
||||
filterPublisher.value.v.length +
|
||||
(!!filterReleaseYear.value.is ? 1 : !!filterReleaseYear.value.min ? 1 : 0) +
|
||||
(!!filterAgeRating.value.is ? 1 : !!filterAgeRating.value.min ? 1 : 0) +
|
||||
filterLanguage.value.v.length +
|
||||
filterSharingLabel.value.v.length +
|
||||
Object.entries(filterAuthors)
|
||||
.map(([, filter]) => filter.filter.v.length)
|
||||
.reduce((sum, item) => sum + item, 0),
|
||||
)
|
||||
|
||||
const { data: filterSeriesStatus } = useRouteQuerySchema('status', SchemaFilterSeriesStatus)
|
||||
const { data: filterReadStatus } = useRouteQuerySchema('read', SchemaFilterReadStatus)
|
||||
const { data: filterGenre } = useRouteQuerySchema('genre', SchemaFilterStrings)
|
||||
const { data: filterTag } = useRouteQuerySchema('tag', SchemaFilterStrings)
|
||||
const { data: filterPublisher } = useRouteQuerySchema('publisher', SchemaFilterStrings)
|
||||
|
|
@ -326,6 +381,7 @@ const conds = computed(() => ({
|
|||
allOf: [
|
||||
librariesCondition.value as components['schemas']['AnyOfSeries'],
|
||||
schemaFilterSeriesStatusToConditions(filterSeriesStatus.value),
|
||||
schemaFilterReadStatusToConditions(filterReadStatus.value),
|
||||
schemaFilterStringToConditions(filterGenre.value, 'genre', true),
|
||||
schemaFilterStringToConditions(filterTag.value, 'tag', true),
|
||||
schemaFilterStringToConditions(filterPublisher.value, 'publisher', false),
|
||||
|
|
|
|||
25
next-ui/src/types/ReadStatus.ts
Normal file
25
next-ui/src/types/ReadStatus.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import { defineMessages } from 'vue-intl'
|
||||
|
||||
export enum ReadStatus {
|
||||
UNREAD = 'UNREAD',
|
||||
IN_PROGRESS = 'IN_PROGRESS',
|
||||
READ = 'READ',
|
||||
}
|
||||
|
||||
export const readStatusMessages = defineMessages({
|
||||
[ReadStatus.UNREAD]: {
|
||||
description: 'Read status: UNREAD',
|
||||
defaultMessage: 'Unread',
|
||||
id: 'XUgQvn',
|
||||
},
|
||||
[ReadStatus.IN_PROGRESS]: {
|
||||
description: 'Read status: IN_PROGRESS',
|
||||
defaultMessage: 'In progress',
|
||||
id: '8DRgrr',
|
||||
},
|
||||
[ReadStatus.READ]: {
|
||||
description: 'Read status: READ',
|
||||
defaultMessage: 'Read',
|
||||
id: 'HhmZaG',
|
||||
},
|
||||
})
|
||||
|
|
@ -3,6 +3,7 @@ import * as v from 'valibot'
|
|||
import {
|
||||
SchemaAnyAll,
|
||||
SchemaFilterAuthors,
|
||||
SchemaFilterReadStatus,
|
||||
SchemaFilterSeriesStatus,
|
||||
SchemaFilterStrings,
|
||||
SchemaSeriesAgeRatings,
|
||||
|
|
@ -155,6 +156,13 @@ describe('filter schemas have a default value', () => {
|
|||
expect(defaults).toStrictEqual(expected)
|
||||
})
|
||||
|
||||
test('SchemaFilterReadStatus', () => {
|
||||
const expected = { v: [] }
|
||||
const defaults = v.getDefaults(SchemaFilterReadStatus)
|
||||
|
||||
expect(defaults).toStrictEqual(expected)
|
||||
})
|
||||
|
||||
test('SchemaFilterStrings', () => {
|
||||
const expected = { m: 'anyOf', v: [] }
|
||||
const defaults = v.getDefaults(SchemaFilterStrings)
|
||||
|
|
|
|||
|
|
@ -56,13 +56,11 @@ export const SchemaSeriesStatus = v.pipe(
|
|||
v.picklist(['ENDED', 'ONGOING', 'ABANDONED', 'HIATUS']),
|
||||
)
|
||||
|
||||
export const SchemaString = v.strictObject({
|
||||
...SchemaIncludeExclude.entries,
|
||||
/**
|
||||
* Shorthand for `value`.
|
||||
*/
|
||||
v: v.string(),
|
||||
})
|
||||
export const SchemaReadStatus = createSchemaIncludeExclude(
|
||||
v.pipe(v.string(), v.toUpperCase(), v.picklist(['UNREAD', 'IN_PROGRESS', 'READ'])),
|
||||
)
|
||||
|
||||
export const SchemaString = createSchemaIncludeExclude(v.string())
|
||||
|
||||
export const SchemaAnyNone = v.strictObject({
|
||||
a: v.optional(v.picklist(['any', 'none'])),
|
||||
|
|
@ -76,6 +74,16 @@ const SchemaYear = v.pipe(v.string(), v.regex(/^\d{4}$/, 'Must be exactly 4 digi
|
|||
// All schema filters need to have a default value
|
||||
////////////////////////////////////////////////////
|
||||
|
||||
function createSchemaIncludeExclude<T extends v.GenericSchema>(schema: T) {
|
||||
return v.strictObject({
|
||||
...SchemaIncludeExclude.entries,
|
||||
/**
|
||||
* Shorthand for `value`.
|
||||
*/
|
||||
v: schema,
|
||||
})
|
||||
}
|
||||
|
||||
function createSchemaFilterAnyAll<T extends v.GenericSchema>(schema: T) {
|
||||
return v.strictObject({
|
||||
...SchemaAnyAll.entries,
|
||||
|
|
@ -104,10 +112,15 @@ function createSchemaFilterSelectRange<T extends v.GenericSchema>(schema: T) {
|
|||
}
|
||||
|
||||
/**
|
||||
* Schema for Series Status.
|
||||
* Schema for Series Status
|
||||
*/
|
||||
export const SchemaFilterSeriesStatus = createSchemaFilterArray(SchemaSeriesStatus)
|
||||
|
||||
/**
|
||||
* Schema for Read Status
|
||||
*/
|
||||
export const SchemaFilterReadStatus = createSchemaFilterArray(SchemaReadStatus)
|
||||
|
||||
/**
|
||||
* Schema for Series Release Years
|
||||
*/
|
||||
|
|
|
|||
Loading…
Reference in a new issue