diff --git a/next-ui/src/components.d.ts b/next-ui/src/components.d.ts
index 3c6fd972..9033835e 100644
--- a/next-ui/src/components.d.ts
+++ b/next-ui/src/components.d.ts
@@ -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']
diff --git a/next-ui/src/components/filter/List.stories.ts b/next-ui/src/components/filter/List.stories.ts
new file mode 100644
index 00000000..849cf6de
--- /dev/null
+++ b/next-ui/src/components/filter/List.stories.ts
@@ -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: '
',
+ }),
+ 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
+
+export default meta
+type Story = StoryObj
+
+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' } },
+ ],
+ },
+}
diff --git a/next-ui/src/components/filter/List.vue b/next-ui/src/components/filter/List.vue
new file mode 100644
index 00000000..13cb69fc
--- /dev/null
+++ b/next-ui/src/components/filter/List.vue
@@ -0,0 +1,84 @@
+
+ (anyAll = v ? 'allOf' : 'anyOf')"
+ />
+
+
+
+
+
+
+
+
+
+
diff --git a/next-ui/src/components/filter/TriState.stories.ts b/next-ui/src/components/filter/TriState.stories.ts
new file mode 100644
index 00000000..0c7f1f8a
--- /dev/null
+++ b/next-ui/src/components/filter/TriState.stories.ts
@@ -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: '',
+ }),
+ 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
+
+export default meta
+type Story = StoryObj
+
+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,
+ },
+}
diff --git a/next-ui/src/components/filter/TriState.vue b/next-ui/src/components/filter/TriState.vue
new file mode 100644
index 00000000..55b18c2b
--- /dev/null
+++ b/next-ui/src/components/filter/TriState.vue
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+
+
+
diff --git a/next-ui/src/pages/libraries/[id]/series.vue b/next-ui/src/pages/libraries/[id]/series.vue
index 57217976..ae3b1038 100644
--- a/next-ui/src/pages/libraries/[id]/series.vue
+++ b/next-ui/src/pages/libraries/[id]/series.vue
@@ -27,6 +27,17 @@
/>
+ FILTER
+ {{ filter }}
+ CONDITION
+ {{ conds }}
+
+
+
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
+ valueExclude?: v.InferOutput
+}[] = [
+ { 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' },
+ },
+]