mirror of
https://github.com/gotson/komga.git
synced 2026-05-07 20:15:47 +02:00
filter WIP
This commit is contained in:
parent
81a463a741
commit
558e1be0ee
6 changed files with 306 additions and 1 deletions
2
next-ui/src/components.d.ts
vendored
2
next-ui/src/components.d.ts
vendored
|
|
@ -36,6 +36,8 @@ declare module 'vue' {
|
|||
DialogSimpleInstance: typeof import('./components/dialog/DialogSimpleInstance.vue')['default']
|
||||
EmptyStateConstruction: typeof import('./components/EmptyStateConstruction.vue')['default']
|
||||
EmptyStateNetworkError: typeof import('./components/EmptyStateNetworkError.vue')['default']
|
||||
FilterList: typeof import('./components/filter/List.vue')['default']
|
||||
FilterTriState: typeof import('./components/filter/TriState.vue')['default']
|
||||
FormattedMessage: typeof import('./components/FormattedMessage.ts')['default']
|
||||
HelloWorld: typeof import('./components/HelloWorld.vue')['default']
|
||||
HistoryExpandBookConverted: typeof import('./components/history/expand/BookConverted.vue')['default']
|
||||
|
|
|
|||
62
next-ui/src/components/filter/List.stories.ts
Normal file
62
next-ui/src/components/filter/List.stories.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import List from './List.vue'
|
||||
import { fn } from 'storybook/test'
|
||||
|
||||
const meta = {
|
||||
component: List,
|
||||
render: (args: object) => ({
|
||||
components: { List },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
template: '<List />',
|
||||
}),
|
||||
parameters: {
|
||||
// More on how to position stories at: https://storybook.js.org/docs/configure/story-layout
|
||||
docs: {
|
||||
description: {
|
||||
component: 'List of tri-state choices that allows multiple selection.',
|
||||
},
|
||||
},
|
||||
},
|
||||
args: {
|
||||
'onUpdate:modelValue': fn(),
|
||||
items: [
|
||||
{ title: 'Tag 1', value: '+tag1', valueExclude: '-tag1' },
|
||||
{ title: 'Tag 2', value: '+tag2', valueExclude: '-tag2' },
|
||||
{ title: 'Tag 3', value: '+tag3', valueExclude: '-tag3' },
|
||||
{ title: 'Tag include only', value: '+tag4' },
|
||||
],
|
||||
},
|
||||
} satisfies Meta<typeof List>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {},
|
||||
}
|
||||
|
||||
export const InitialValue: Story = {
|
||||
args: {
|
||||
modelValue: ['+tag1', '-tag2', 'crap'],
|
||||
},
|
||||
}
|
||||
|
||||
export const Color: Story = {
|
||||
args: {
|
||||
color: 'red',
|
||||
},
|
||||
}
|
||||
|
||||
export const Objects: Story = {
|
||||
args: {
|
||||
items: [
|
||||
{ title: 'Tag 1', value: { include: 'tag1' }, valueExclude: { exclude: 'tag1' } },
|
||||
{ title: 'Tag 2', value: { include: 'tag2' }, valueExclude: { exclude: 'tag2' } },
|
||||
{ title: 'Tag 3', value: { include: 'tag3' }, valueExclude: { exclude: 'tag3' } },
|
||||
{ title: 'Tag include only', value: { include: 'tag4' } },
|
||||
],
|
||||
},
|
||||
}
|
||||
84
next-ui/src/components/filter/List.vue
Normal file
84
next-ui/src/components/filter/List.vue
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
<template>
|
||||
<v-checkbox
|
||||
:model-value="anyAll === 'allOf'"
|
||||
label="All of"
|
||||
@update:model-value="(v) => (anyAll = v ? 'allOf' : 'anyOf')"
|
||||
/>
|
||||
<v-list>
|
||||
<FilterTriState
|
||||
v-for="(item, i) in items"
|
||||
:key="i"
|
||||
v-model="internalModel[i]"
|
||||
:label="item.title"
|
||||
:tri-state="!!item.valueExclude"
|
||||
:color="color"
|
||||
/>
|
||||
</v-list>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { syncRef } from '@vueuse/core'
|
||||
import type { AnyAll } from '@/types/filter'
|
||||
|
||||
const model = defineModel<unknown[]>({ default: [] })
|
||||
const anyAll = defineModel<AnyAll>('mode', { default: 'anyOf' })
|
||||
|
||||
//TODO: handle mode better
|
||||
//TODO: add clear all?
|
||||
|
||||
const { items = [], color } = defineProps<{
|
||||
items?: {
|
||||
title: string
|
||||
value: unknown
|
||||
valueExclude?: unknown
|
||||
}[]
|
||||
color?: string
|
||||
}>()
|
||||
|
||||
function initializeInternalModel() {
|
||||
const c: Record<number, string | undefined> = {}
|
||||
items.forEach((_it, i) => (c[i] = undefined))
|
||||
return c
|
||||
}
|
||||
|
||||
const internalModel = ref<Record<number, string | undefined>>(initializeInternalModel())
|
||||
|
||||
syncRef(model, internalModel, {
|
||||
direction: 'both',
|
||||
deep: true,
|
||||
transform: {
|
||||
ltr: (left) =>
|
||||
left.reduce((acc, item) => {
|
||||
const indexInclude = items.findIndex(
|
||||
(it) => JSON.stringify(it.value) === JSON.stringify(item),
|
||||
)
|
||||
if (indexInclude >= 0)
|
||||
return {
|
||||
...(acc as Record<number, string | undefined>),
|
||||
[indexInclude]: 'include',
|
||||
}
|
||||
|
||||
const indexExclude = items.findIndex(
|
||||
(it) => JSON.stringify(it.valueExclude) === JSON.stringify(item),
|
||||
)
|
||||
if (indexExclude >= 0)
|
||||
return {
|
||||
...(acc as Record<number, string | undefined>),
|
||||
[indexExclude]: 'exclude',
|
||||
}
|
||||
|
||||
return acc
|
||||
}, {}) as Record<number, string | undefined>,
|
||||
rtl: (right) =>
|
||||
items
|
||||
.map((it, i) =>
|
||||
right[i] === 'include' ? it.value : right[i] === 'exclude' ? it.valueExclude : undefined,
|
||||
)
|
||||
.filter((it) => it != undefined),
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts"></script>
|
||||
|
||||
<style scoped></style>
|
||||
60
next-ui/src/components/filter/TriState.stories.ts
Normal file
60
next-ui/src/components/filter/TriState.stories.ts
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import TriState from './TriState.vue'
|
||||
import { fn } from 'storybook/test'
|
||||
|
||||
const meta = {
|
||||
component: TriState,
|
||||
render: (args: object) => ({
|
||||
components: { TriState },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
template: '<TriState />',
|
||||
}),
|
||||
parameters: {
|
||||
// More on how to position stories at: https://storybook.js.org/docs/configure/story-layout
|
||||
docs: {
|
||||
description: {
|
||||
component:
|
||||
'A tri-state component used for filtering. Can also be configured as a simple checkbox.',
|
||||
},
|
||||
},
|
||||
},
|
||||
args: {
|
||||
label: 'tri state',
|
||||
'onUpdate:modelValue': fn(),
|
||||
},
|
||||
} satisfies Meta<typeof TriState>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {},
|
||||
}
|
||||
|
||||
export const Color: Story = {
|
||||
args: {
|
||||
color: 'primary',
|
||||
},
|
||||
}
|
||||
|
||||
export const ValidModel: Story = {
|
||||
args: {
|
||||
modelValue: 'exclude',
|
||||
},
|
||||
}
|
||||
|
||||
export const InvalidModel: Story = {
|
||||
args: {
|
||||
modelValue: 'nope',
|
||||
},
|
||||
}
|
||||
|
||||
export const BiState: Story = {
|
||||
args: {
|
||||
label: 'bi state',
|
||||
triState: false,
|
||||
},
|
||||
}
|
||||
57
next-ui/src/components/filter/TriState.vue
Normal file
57
next-ui/src/components/filter/TriState.vue
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
<template>
|
||||
<v-list-item
|
||||
:title="label"
|
||||
@click="cycle()"
|
||||
><template #prepend
|
||||
><v-icon
|
||||
:icon="icon"
|
||||
:color="!!model ? color : undefined" /></template
|
||||
></v-list-item>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const model = defineModel<string>()
|
||||
|
||||
const icon = computed(() => states.value.find((it) => it.value === model.value)?.icon)
|
||||
|
||||
const {
|
||||
label,
|
||||
triState = true,
|
||||
color,
|
||||
} = defineProps<{
|
||||
label: string
|
||||
triState?: boolean
|
||||
color?: string
|
||||
}>()
|
||||
|
||||
const states = computed(() => [
|
||||
{
|
||||
icon: 'i-mdi:checkbox-marked',
|
||||
value: 'include',
|
||||
},
|
||||
...(triState
|
||||
? [
|
||||
{
|
||||
icon: 'i-mdi:close-box',
|
||||
value: 'exclude',
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
icon: 'i-mdi:checkbox-blank-outline',
|
||||
value: undefined,
|
||||
},
|
||||
])
|
||||
|
||||
if (!states.value.some((it) => it.value === model.value)) model.value = undefined
|
||||
|
||||
function cycle() {
|
||||
const index = states.value.findIndex((x) => x.value === model.value)
|
||||
const newIndex = (index + 1) % states.value.length
|
||||
model.value = states.value[newIndex]!.value
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts"></script>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
@ -27,6 +27,17 @@
|
|||
/>
|
||||
</v-app-bar>
|
||||
|
||||
<div>FILTER</div>
|
||||
<div>{{ filter }}</div>
|
||||
<div>CONDITION</div>
|
||||
<div>{{ conds }}</div>
|
||||
|
||||
<FilterList
|
||||
v-model="filter.v"
|
||||
v-model:mode="filter.m"
|
||||
:items="filterStatusItems"
|
||||
/>
|
||||
|
||||
<template v-if="series">
|
||||
<v-data-iterator
|
||||
v-model="selectedItems"
|
||||
|
|
@ -101,6 +112,10 @@ import { useSearchConditionLibraries } from '@/composables/search'
|
|||
import { storeToRefs } from 'pinia'
|
||||
import { useSelectionStore } from '@/stores/selection'
|
||||
import { useDisplay } from 'vuetify'
|
||||
import { schemaFilterSeriesStatusToConditions } from '@/functions/filter'
|
||||
import * as v from 'valibot'
|
||||
import { SchemaFilterSeriesStatus } from '@/types/filter'
|
||||
import { useRouteQuerySchema } from '@/composables/useRouteQuerySchema'
|
||||
|
||||
const route = useRoute('/libraries/[id]/series')
|
||||
const libraryId = route.params.id
|
||||
|
|
@ -122,9 +137,19 @@ const selectionStore = useSelectionStore()
|
|||
const { selection: selectedItems } = storeToRefs(selectionStore)
|
||||
const preSelect = computed(() => selectedItems.value.length > 0)
|
||||
|
||||
const { data: filter } = useRouteQuerySchema('status', SchemaFilterSeriesStatus)
|
||||
|
||||
const conds = computed(() => ({
|
||||
allOf: [
|
||||
librariesCondition.value as components['schemas']['AnyOfSeries'],
|
||||
schemaFilterSeriesStatusToConditions(filter),
|
||||
],
|
||||
}))
|
||||
|
||||
const { data: series } = useQuery(seriesListQuery, () => {
|
||||
const search: components['schemas']['SeriesSearch'] = {
|
||||
condition: librariesCondition.value as components['schemas']['AnyOfSeries'],
|
||||
// condition: librariesCondition.value as components['schemas']['AnyOfSeries'],
|
||||
condition: conds.value as components['schemas']['AllOfSeries'],
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
@ -136,6 +161,21 @@ const { data: series } = useQuery(seriesListQuery, () => {
|
|||
watch(series, (newSeries) => {
|
||||
if (newSeries) pageCount.value = newSeries.totalPages ?? 0
|
||||
})
|
||||
|
||||
const filterStatusItems: {
|
||||
title: string
|
||||
value: v.InferOutput<typeof SeriesStatus>
|
||||
valueExclude?: v.InferOutput<typeof SeriesStatus>
|
||||
}[] = [
|
||||
{ title: 'Ended', value: { i: 'i', v: 'ENDED' }, valueExclude: { i: 'e', v: 'ENDED' } },
|
||||
{ title: 'Ongoing', value: { i: 'i', v: 'ONGOING' }, valueExclude: { i: 'e', v: 'ONGOING' } },
|
||||
{ title: 'Hiatus', value: { i: 'i', v: 'HIATUS' }, valueExclude: { i: 'e', v: 'HIATUS' } },
|
||||
{
|
||||
title: 'Abandoned',
|
||||
value: { i: 'i', v: 'ABANDONED' },
|
||||
valueExclude: { i: 'e', v: 'ABANDONED' },
|
||||
},
|
||||
]
|
||||
</script>
|
||||
|
||||
<route lang="yaml">
|
||||
|
|
|
|||
Loading…
Reference in a new issue