more filters

This commit is contained in:
Gauthier Roebroeck 2026-03-09 16:54:12 +08:00
parent 4d1c87d765
commit 3a4894fde0
14 changed files with 977 additions and 11 deletions

View file

@ -63,3 +63,168 @@ export const authorsQuery = defineQueryOptions(
}
},
)
export const genresQuery = defineQueryOptions(
({
search,
library_id,
collection_id,
pageRequest,
pause = false,
placeholder = true,
}: {
search?: string
library_id?: string[]
collection_id?: string[]
pageRequest?: PageRequest
pause?: boolean
placeholder?: boolean
}) => {
const queryParams = {
search: search,
library_id: library_id,
collection_id: collection_id,
...pageRequest,
}
return {
key: ['genres', queryParams],
query: () =>
komgaClient
.GET('/api/v2/genres', {
params: {
query: queryParams,
},
})
// unwrap the openapi-fetch structure on success
.then((res) => res.data),
enabled: !pause,
placeholderData: placeholder ? (previousData: any) => previousData : undefined, // eslint-disable-line @typescript-eslint/no-explicit-any
}
},
)
export const tagsQuery = defineQueryOptions(
({
search,
library_id,
collection_id,
series_id,
readlist_id,
include,
pageRequest,
pause = false,
placeholder = true,
}: {
search?: string
library_id?: string[]
collection_id?: string[]
series_id?: string[]
readlist_id?: string[]
include?: 'SERIES' | 'BOOK' | 'BOTH'
pageRequest?: PageRequest
pause?: boolean
placeholder?: boolean
}) => {
const queryParams = {
search: search,
library_id: library_id,
collection_id: collection_id,
series_id: series_id,
readlist_id: readlist_id,
include: include,
...pageRequest,
}
return {
key: ['tags', queryParams],
query: () =>
komgaClient
.GET('/api/v2/tags', {
params: {
query: queryParams,
},
})
// unwrap the openapi-fetch structure on success
.then((res) => res.data),
enabled: !pause,
placeholderData: placeholder ? (previousData: any) => previousData : undefined, // eslint-disable-line @typescript-eslint/no-explicit-any
}
},
)
export const publishersQuery = defineQueryOptions(
({
search,
library_id,
collection_id,
pageRequest,
pause = false,
placeholder = true,
}: {
search?: string
library_id?: string[]
collection_id?: string[]
pageRequest?: PageRequest
pause?: boolean
placeholder?: boolean
}) => {
const queryParams = {
search: search,
library_id: library_id,
collection_id: collection_id,
...pageRequest,
}
return {
key: ['publishers', queryParams],
query: () =>
komgaClient
.GET('/api/v2/publishers', {
params: {
query: queryParams,
},
})
// unwrap the openapi-fetch structure on success
.then((res) => res.data),
enabled: !pause,
placeholderData: placeholder ? (previousData: any) => previousData : undefined, // eslint-disable-line @typescript-eslint/no-explicit-any
}
},
)
export const sharingLabelsQuery = defineQueryOptions(
({
search,
library_id,
collection_id,
pageRequest,
pause = false,
placeholder = true,
}: {
search?: string
library_id?: string[]
collection_id?: string[]
pageRequest?: PageRequest
pause?: boolean
placeholder?: boolean
}) => {
const queryParams = {
search: search,
library_id: library_id,
collection_id: collection_id,
...pageRequest,
}
return {
key: ['sharing-labels', queryParams],
query: () =>
komgaClient
.GET('/api/v2/sharing-labels', {
params: {
query: queryParams,
},
})
// unwrap the openapi-fetch structure on success
.then((res) => res.data),
enabled: !pause,
placeholderData: placeholder ? (previousData: any) => previousData : undefined, // eslint-disable-line @typescript-eslint/no-explicit-any
}
},
)

View file

@ -38,7 +38,11 @@ declare module 'vue' {
EmptyStateNetworkError: typeof import('./components/EmptyStateNetworkError.vue')['default']
FilterAnyAll: typeof import('./components/filter/AnyAll.vue')['default']
FilterByAuthor: typeof import('./components/filter/by/Author.vue')['default']
FilterByGenre: typeof import('./components/filter/by/Genre.vue')['default']
FilterByPublisher: typeof import('./components/filter/by/Publisher.vue')['default']
FilterBySeriesStatus: typeof import('./components/filter/by/SeriesStatus.vue')['default']
FilterBySharingLabel: typeof import('./components/filter/by/SharingLabel.vue')['default']
FilterByTag: typeof import('./components/filter/by/Tag.vue')['default']
FilterExpansionPanel: typeof import('./components/filter/ExpansionPanel.vue')['default']
FilterList: typeof import('./components/filter/List.vue')['default']
FilterSearchList: typeof import('./components/filter/SearchList.vue')['default']

View file

@ -0,0 +1,65 @@
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import Genre from './Genre.vue'
import { fn } from 'storybook/test'
import { httpTyped } from '@/mocks/api/httpTyped'
import { mockPage } from '@/mocks/api/pageable'
import { PageRequest } from '@/types/PageRequest'
const meta = {
component: Genre,
render: (args: object) => ({
components: { Genre },
setup() {
return { args }
},
template: '<Genre v-model="args.modelValue"/>',
}),
parameters: {
// More on how to position stories at: https://storybook.js.org/docs/configure/story-layout
docs: {
description: {
component: 'Genre filter.',
},
},
},
args: {
'onUpdate:modelValue': fn(),
modelValue: [],
},
} satisfies Meta<typeof Genre>
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
args: {},
}
export const NoData: Story = {
parameters: {
msw: {
handlers: [
httpTyped.get('/api/v2/genres', ({ response }) =>
response(200).json(mockPage([], new PageRequest())),
),
],
},
},
}
export const InitialValue: Story = {
args: {
modelValue: [
{ i: 'e', v: 'Genre 3' },
{ i: 'i', v: 'Genre 5' },
{ i: 'i', v: 'Genre 8' },
],
},
}
export const InitialValueOutsideShown: Story = {
args: {
modelValue: [{ i: 'i', v: 'Genre 100' }],
},
}

View file

@ -0,0 +1,94 @@
<template>
<FilterSearchList
v-model="model"
v-model:mode="modelMode"
v-model:search="search"
:items="infiniteItems"
:search-items="searchResults"
:search-loading="searchLoading"
show-mode-selector
@load-more="loadNextPage()"
>
</FilterSearchList>
</template>
<script setup lang="ts">
import { useInfiniteQuery, useQuery } from '@pinia/colada'
import { genresQuery } from '@/colada/referential'
import { PageRequest } from '@/types/PageRequest'
import { komgaClient } from '@/api/komga-client'
import * as v from 'valibot'
import { type AnyAll, filterKeys, filterMessages, SchemaString } from '@/types/filter'
import type { ItemType } from '@/components/filter/List.vue'
import { refDebounced } from '@vueuse/core'
import { useIntl } from 'vue-intl'
type SchString = v.InferOutput<typeof SchemaString>
const intl = useIntl()
const model = defineModel<SchString[]>({ default: [] })
const modelMode = defineModel<AnyAll>('mode', { default: 'anyOf' })
const search = ref()
const searchDebounced = refDebounced(search, 500)
const filterContext = inject(filterKeys.context, {})
const apiQuery = {
...filterContext,
}
const { data: searchItems, isLoading: searchLoading } = useQuery(genresQuery, () => ({
pageRequest: PageRequest.Unpaged(),
search: searchDebounced.value,
pause: !searchDebounced.value,
placeholder: false,
...apiQuery,
}))
const searchResults = computed(() => searchItems.value?.content?.map((it) => toItemType(it)))
const { data: infiniteData, loadNextPage } = useInfiniteQuery({
key: () => ['infinite_genres', apiQuery],
initialPageParam: new PageRequest(0, 50),
query: ({ pageParam }) =>
komgaClient
.GET('/api/v2/genres', {
params: {
query: {
...apiQuery,
...pageParam,
},
},
})
// unwrap the openapi-fetch structure on success
.then((res) => res.data),
getNextPageParam: (lastPage) =>
!lastPage?.last ? new PageRequest((lastPage?.number ?? 0) + 1, lastPage?.size) : null,
})
const infiniteItems = computed(() => {
const itemTypes = (infiniteData.value?.pages.flatMap((it) => it?.content ?? []) ?? []).map((it) =>
toItemType(it),
)
return [
{
title: intl.formatMessage(filterMessages.any!),
value: { a: 'any' },
valueExclude: { a: 'none' },
},
...itemTypes,
]
})
function toItemType(value: string): ItemType<SchString> {
return {
title: value,
value: { i: 'i', v: value },
valueExclude: { i: 'e', v: value },
}
}
</script>
<script lang="ts"></script>
<style scoped></style>

View file

@ -0,0 +1,65 @@
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import Publisher from './Publisher.vue'
import { fn } from 'storybook/test'
import { httpTyped } from '@/mocks/api/httpTyped'
import { mockPage } from '@/mocks/api/pageable'
import { PageRequest } from '@/types/PageRequest'
const meta = {
component: Publisher,
render: (args: object) => ({
components: { Publisher },
setup() {
return { args }
},
template: '<Publisher v-model="args.modelValue"/>',
}),
parameters: {
// More on how to position stories at: https://storybook.js.org/docs/configure/story-layout
docs: {
description: {
component: 'Publisher filter.',
},
},
},
args: {
'onUpdate:modelValue': fn(),
modelValue: [],
},
} satisfies Meta<typeof Publisher>
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
args: {},
}
export const NoData: Story = {
parameters: {
msw: {
handlers: [
httpTyped.get('/api/v2/publishers', ({ response }) =>
response(200).json(mockPage([], new PageRequest())),
),
],
},
},
}
export const InitialValue: Story = {
args: {
modelValue: [
{ i: 'e', v: 'Publisher 3' },
{ i: 'i', v: 'Publisher 5' },
{ i: 'i', v: 'Publisher 8' },
],
},
}
export const InitialValueOutsideShown: Story = {
args: {
modelValue: [{ i: 'i', v: 'Publisher 100' }],
},
}

View file

@ -0,0 +1,94 @@
<template>
<FilterSearchList
v-model="model"
v-model:mode="modelMode"
v-model:search="search"
:items="infiniteItems"
:search-items="searchResults"
:search-loading="searchLoading"
show-mode-selector
@load-more="loadNextPage()"
>
</FilterSearchList>
</template>
<script setup lang="ts">
import { useInfiniteQuery, useQuery } from '@pinia/colada'
import { publishersQuery } from '@/colada/referential'
import { PageRequest } from '@/types/PageRequest'
import { komgaClient } from '@/api/komga-client'
import * as v from 'valibot'
import { type AnyAll, filterKeys, filterMessages, SchemaString } from '@/types/filter'
import type { ItemType } from '@/components/filter/List.vue'
import { refDebounced } from '@vueuse/core'
import { useIntl } from 'vue-intl'
type SchString = v.InferOutput<typeof SchemaString>
const intl = useIntl()
const model = defineModel<SchString[]>({ default: [] })
const modelMode = defineModel<AnyAll>('mode', { default: 'anyOf' })
const search = ref()
const searchDebounced = refDebounced(search, 500)
const filterContext = inject(filterKeys.context, {})
const apiQuery = {
...filterContext,
}
const { data: searchItems, isLoading: searchLoading } = useQuery(publishersQuery, () => ({
pageRequest: PageRequest.Unpaged(),
search: searchDebounced.value,
pause: !searchDebounced.value,
placeholder: false,
...apiQuery,
}))
const searchResults = computed(() => searchItems.value?.content?.map((it) => toItemType(it)))
const { data: infiniteData, loadNextPage } = useInfiniteQuery({
key: () => ['infinite_publishers', apiQuery],
initialPageParam: new PageRequest(0, 50),
query: ({ pageParam }) =>
komgaClient
.GET('/api/v2/publishers', {
params: {
query: {
...apiQuery,
...pageParam,
},
},
})
// unwrap the openapi-fetch structure on success
.then((res) => res.data),
getNextPageParam: (lastPage) =>
!lastPage?.last ? new PageRequest((lastPage?.number ?? 0) + 1, lastPage?.size) : null,
})
const infiniteItems = computed(() => {
const itemTypes = (infiniteData.value?.pages.flatMap((it) => it?.content ?? []) ?? []).map((it) =>
toItemType(it),
)
return [
{
title: intl.formatMessage(filterMessages.any!),
value: { a: 'any' },
valueExclude: { a: 'none' },
},
...itemTypes,
]
})
function toItemType(value: string): ItemType<SchString> {
return {
title: value,
value: { i: 'i', v: value },
valueExclude: { i: 'e', v: value },
}
}
</script>
<script lang="ts"></script>
<style scoped></style>

View file

@ -0,0 +1,65 @@
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import SharingLabel from './SharingLabel.vue'
import { fn } from 'storybook/test'
import { httpTyped } from '@/mocks/api/httpTyped'
import { mockPage } from '@/mocks/api/pageable'
import { PageRequest } from '@/types/PageRequest'
const meta = {
component: SharingLabel,
render: (args: object) => ({
components: { SharingLabel },
setup() {
return { args }
},
template: '<SharingLabel v-model="args.modelValue"/>',
}),
parameters: {
// More on how to position stories at: https://storybook.js.org/docs/configure/story-layout
docs: {
description: {
component: 'SharingLabel filter.',
},
},
},
args: {
'onUpdate:modelValue': fn(),
modelValue: [],
},
} satisfies Meta<typeof SharingLabel>
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
args: {},
}
export const NoData: Story = {
parameters: {
msw: {
handlers: [
httpTyped.get('/api/v2/sharing-labels', ({ response }) =>
response(200).json(mockPage([], new PageRequest())),
),
],
},
},
}
export const InitialValue: Story = {
args: {
modelValue: [
{ i: 'e', v: 'SharingLabel 3' },
{ i: 'i', v: 'SharingLabel 5' },
{ i: 'i', v: 'SharingLabel 8' },
],
},
}
export const InitialValueOutsideShown: Story = {
args: {
modelValue: [{ i: 'i', v: 'SharingLabel 100' }],
},
}

View file

@ -0,0 +1,94 @@
<template>
<FilterSearchList
v-model="model"
v-model:mode="modelMode"
v-model:search="search"
:items="infiniteItems"
:search-items="searchResults"
:search-loading="searchLoading"
show-mode-selector
@load-more="loadNextPage()"
>
</FilterSearchList>
</template>
<script setup lang="ts">
import { useInfiniteQuery, useQuery } from '@pinia/colada'
import { sharingLabelsQuery } from '@/colada/referential'
import { PageRequest } from '@/types/PageRequest'
import { komgaClient } from '@/api/komga-client'
import * as v from 'valibot'
import { type AnyAll, filterKeys, filterMessages, SchemaString } from '@/types/filter'
import type { ItemType } from '@/components/filter/List.vue'
import { refDebounced } from '@vueuse/core'
import { useIntl } from 'vue-intl'
type SchString = v.InferOutput<typeof SchemaString>
const intl = useIntl()
const model = defineModel<SchString[]>({ default: [] })
const modelMode = defineModel<AnyAll>('mode', { default: 'anyOf' })
const search = ref()
const searchDebounced = refDebounced(search, 500)
const filterContext = inject(filterKeys.context, {})
const apiQuery = {
...filterContext,
}
const { data: searchItems, isLoading: searchLoading } = useQuery(sharingLabelsQuery, () => ({
pageRequest: PageRequest.Unpaged(),
search: searchDebounced.value,
pause: !searchDebounced.value,
placeholder: false,
...apiQuery,
}))
const searchResults = computed(() => searchItems.value?.content?.map((it) => toItemType(it)))
const { data: infiniteData, loadNextPage } = useInfiniteQuery({
key: () => ['infinite_sharing-labels', apiQuery],
initialPageParam: new PageRequest(0, 50),
query: ({ pageParam }) =>
komgaClient
.GET('/api/v2/sharing-labels', {
params: {
query: {
...apiQuery,
...pageParam,
},
},
})
// unwrap the openapi-fetch structure on success
.then((res) => res.data),
getNextPageParam: (lastPage) =>
!lastPage?.last ? new PageRequest((lastPage?.number ?? 0) + 1, lastPage?.size) : null,
})
const infiniteItems = computed(() => {
const itemTypes = (infiniteData.value?.pages.flatMap((it) => it?.content ?? []) ?? []).map((it) =>
toItemType(it),
)
return [
{
title: intl.formatMessage(filterMessages.any!),
value: { a: 'any' },
valueExclude: { a: 'none' },
},
...itemTypes,
]
})
function toItemType(value: string): ItemType<SchString> {
return {
title: value,
value: { i: 'i', v: value },
valueExclude: { i: 'e', v: value },
}
}
</script>
<script lang="ts"></script>
<style scoped></style>

View file

@ -0,0 +1,65 @@
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import Tag from './Tag.vue'
import { fn } from 'storybook/test'
import { httpTyped } from '@/mocks/api/httpTyped'
import { mockPage } from '@/mocks/api/pageable'
import { PageRequest } from '@/types/PageRequest'
const meta = {
component: Tag,
render: (args: object) => ({
components: { Tag },
setup() {
return { args }
},
template: '<Tag v-model="args.modelValue"/>',
}),
parameters: {
// More on how to position stories at: https://storybook.js.org/docs/configure/story-layout
docs: {
description: {
component: 'Tag filter.',
},
},
},
args: {
'onUpdate:modelValue': fn(),
modelValue: [],
},
} satisfies Meta<typeof Tag>
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
args: {},
}
export const NoData: Story = {
parameters: {
msw: {
handlers: [
httpTyped.get('/api/v2/tags', ({ response }) =>
response(200).json(mockPage([], new PageRequest())),
),
],
},
},
}
export const InitialValue: Story = {
args: {
modelValue: [
{ i: 'e', v: 'Tag 3' },
{ i: 'i', v: 'Tag 5' },
{ i: 'i', v: 'Tag 8' },
],
},
}
export const InitialValueOutsideShown: Story = {
args: {
modelValue: [{ i: 'i', v: 'Tag 100' }],
},
}

View file

@ -0,0 +1,99 @@
<template>
<FilterSearchList
v-model="model"
v-model:mode="modelMode"
v-model:search="search"
:items="infiniteItems"
:search-items="searchResults"
:search-loading="searchLoading"
show-mode-selector
@load-more="loadNextPage()"
>
</FilterSearchList>
</template>
<script setup lang="ts">
import { useInfiniteQuery, useQuery } from '@pinia/colada'
import { tagsQuery } from '@/colada/referential'
import { PageRequest } from '@/types/PageRequest'
import { komgaClient } from '@/api/komga-client'
import * as v from 'valibot'
import { type AnyAll, filterKeys, filterMessages, SchemaString } from '@/types/filter'
import type { ItemType } from '@/components/filter/List.vue'
import { refDebounced } from '@vueuse/core'
import { useIntl } from 'vue-intl'
type SchString = v.InferOutput<typeof SchemaString>
const intl = useIntl()
const model = defineModel<SchString[]>({ default: [] })
const modelMode = defineModel<AnyAll>('mode', { default: 'anyOf' })
const search = ref()
const searchDebounced = refDebounced(search, 500)
const { include = 'BOTH' } = defineProps<{
include?: 'SERIES' | 'BOOK' | 'BOTH'
}>()
const filterContext = inject(filterKeys.context, {})
const apiQuery = {
...filterContext,
include: include,
}
const { data: searchItems, isLoading: searchLoading } = useQuery(tagsQuery, () => ({
pageRequest: PageRequest.Unpaged(),
search: searchDebounced.value,
pause: !searchDebounced.value,
placeholder: false,
...apiQuery,
}))
const searchResults = computed(() => searchItems.value?.content?.map((it) => toItemType(it)))
const { data: infiniteData, loadNextPage } = useInfiniteQuery({
key: () => ['infinite_tags', apiQuery],
initialPageParam: new PageRequest(0, 50),
query: ({ pageParam }) =>
komgaClient
.GET('/api/v2/tags', {
params: {
query: {
...apiQuery,
...pageParam,
},
},
})
// unwrap the openapi-fetch structure on success
.then((res) => res.data),
getNextPageParam: (lastPage) =>
!lastPage?.last ? new PageRequest((lastPage?.number ?? 0) + 1, lastPage?.size) : null,
})
const infiniteItems = computed(() => {
const itemTypes = (infiniteData.value?.pages.flatMap((it) => it?.content ?? []) ?? []).map((it) =>
toItemType(it),
)
return [
{
title: intl.formatMessage(filterMessages.any!),
value: { a: 'any' },
valueExclude: { a: 'none' },
},
...itemTypes,
]
})
function toItemType(value: string): ItemType<SchString> {
return {
title: value,
value: { i: 'i', v: value },
valueExclude: { i: 'e', v: value },
}
}
</script>
<script lang="ts"></script>
<style scoped></style>

View file

@ -1,4 +1,9 @@
import { SchemaAnyNone, SchemaFilterAuthors, type SchemaFilterSeriesStatus } from '@/types/filter'
import {
SchemaAnyNone,
SchemaFilterAuthors,
type SchemaFilterSeriesStatus,
SchemaFilterStrings,
} from '@/types/filter'
import type { InferOutput } from 'valibot'
import * as v from 'valibot'
@ -53,3 +58,34 @@ export function schemaFilterAuthorsToConditions(
anyOf: list,
}
}
export function schemaFilterNullableStringToConditions(
filter: InferOutput<typeof SchemaFilterStrings>,
key: string,
) {
const list = filter.v.map((it) => {
if (v.is(SchemaAnyNone, it)) {
return {
[key]: {
operator: it.a === 'any' ? 'isNotNull' : 'isNull',
},
}
} else {
return {
[key]: {
operator: it.i === 'e' ? 'isNot' : 'is',
value: it.v,
},
}
}
})
if (filter.m === 'allOf')
return {
allOf: list,
}
else
return {
anyOf: list,
}
}

View file

@ -9644,8 +9644,8 @@ export interface operations {
search?: string;
library_id?: string[];
collection_id?: string[];
series_id?: string;
readlist_id?: string;
series_id?: string[];
readlist_id?: string[];
include?: "SERIES" | "BOOK" | "BOTH";
unpaged?: boolean;
/** @description Zero-based page index (0..N) */

View file

@ -25,11 +25,79 @@ function doMockAuthors(count: number) {
} as components['schemas']['AuthorDto']
})
}
const mockAuthors = doMockAuthors(10000)
function doMockStrings(count: number, prefix: string) {
return [...Array(count).keys()].map((index) => {
return `${prefix} ${index}`
})
}
const mockGenres = doMockStrings(10000, 'Genre')
const mockTags = doMockStrings(10000, 'Tag')
const mockPublishers = doMockStrings(10000, 'Publisher')
const mockSharingLabels = doMockStrings(150, 'SharingLabel')
function filterAndPage(
search: string | null,
data: string[],
page: string | null,
size: string | null,
unpaged: string | null,
) {
const selected = search ? data.filter((it) => !!it.match(new RegExp(search, 'i'))) : data
return mockPage(
selected,
new PageRequest(Number(page), Number(size), undefined, Boolean(unpaged)),
)
}
export const referentialHandlers = [
httpTyped.get('/api/v1/sharing-labels', ({ response }) => response(200).json(sharingLabels)),
httpTyped.get('/api/v2/genres', ({ query, response }) =>
response(200).json(
filterAndPage(
query.get('search'),
mockGenres,
query.get('page'),
query.get('size'),
query.get('unpaged'),
),
),
),
httpTyped.get('/api/v2/tags', ({ query, response }) =>
response(200).json(
filterAndPage(
query.get('search'),
mockTags,
query.get('page'),
query.get('size'),
query.get('unpaged'),
),
),
),
httpTyped.get('/api/v2/publishers', ({ query, response }) =>
response(200).json(
filterAndPage(
query.get('search'),
mockPublishers,
query.get('page'),
query.get('size'),
query.get('unpaged'),
),
),
),
httpTyped.get('/api/v2/sharing-labels', ({ query, response }) =>
response(200).json(
filterAndPage(
query.get('search'),
mockSharingLabels,
query.get('page'),
query.get('size'),
query.get('unpaged'),
),
),
),
httpTyped.get('/api/v2/authors', ({ query, response }) => {
const search = query.get('search')
const role = query.get('role')

View file

@ -59,6 +59,50 @@
>
<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="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 />
@ -90,12 +134,6 @@
</v-navigation-drawer>
</Teleport>
<div>FILTER AUTHORS</div>
<p>{{ filterAuthors }}</p>
<div>AUTHOR ROLES</div>
<p>{{ authorRoles }}</p>
<div>FILTER</div>
<div>{{ filterSeriesStatus }}</div>
<div>CONDITION</div>
<div>{{ conds }}</div>
@ -175,10 +213,16 @@ import { useSelectionStore } from '@/stores/selection'
import { useDisplay } from 'vuetify'
import {
schemaFilterAuthorsToConditions,
schemaFilterNullableStringToConditions,
schemaFilterSeriesStatusToConditions,
} from '@/functions/filter'
import * as v from 'valibot'
import { type FilterType, SchemaFilterAuthors, SchemaFilterSeriesStatus } from '@/types/filter'
import {
type FilterType,
SchemaFilterAuthors,
SchemaFilterSeriesStatus,
SchemaFilterStrings,
} from '@/types/filter'
import { useRouteQuerySchema } from '@/composables/useRouteQuerySchema'
import { authorRoles } from '@/types/referential'
import { useIntl } from 'vue-intl'
@ -232,11 +276,19 @@ function clearFilter(filter: FilterType) {
}
const { data: filterSeriesStatus } = useRouteQuerySchema('status', SchemaFilterSeriesStatus)
const { data: filterGenre } = useRouteQuerySchema('genre', SchemaFilterStrings)
const { data: filterTag } = useRouteQuerySchema('tag', SchemaFilterStrings)
const { data: filterPublisher } = useRouteQuerySchema('publisher', SchemaFilterStrings)
const { data: filterSharingLabel } = useRouteQuerySchema('sharingLabel', SchemaFilterStrings)
const conds = computed(() => ({
allOf: [
librariesCondition.value as components['schemas']['AnyOfSeries'],
schemaFilterSeriesStatusToConditions(filterSeriesStatus.value),
schemaFilterNullableStringToConditions(filterGenre.value, 'genre'),
schemaFilterNullableStringToConditions(filterTag.value, 'tag'),
schemaFilterNullableStringToConditions(filterPublisher.value, 'publisher'),
schemaFilterNullableStringToConditions(filterSharingLabel.value, 'sharingLabel'),
...Object.entries(filterAuthors).map(([, filter]) =>
schemaFilterAuthorsToConditions(toValue(filter.filter), toValue(filter.role)),
),