diff --git a/next-ui/src/colada/readlists.ts b/next-ui/src/colada/readlists.ts new file mode 100644 index 000000000..8d3df9692 --- /dev/null +++ b/next-ui/src/colada/readlists.ts @@ -0,0 +1,16 @@ +import { defineMutation, useMutation } from '@pinia/colada' +import { komgaClient } from '@/api/komga-client' +import type { components } from '@/generated/openapi/komga' + +export const QUERY_KEYS_READLIST = { + root: ['readlists'] as const, +} + +export const useCreateReadList = defineMutation(() => { + return useMutation({ + mutation: (readList: components['schemas']['ReadListCreationDto']) => + komgaClient.POST('/api/v1/readlists', { + body: readList, + }), + }) +}) diff --git a/next-ui/src/components.d.ts b/next-ui/src/components.d.ts index b43034fba..7a9593166 100644 --- a/next-ui/src/components.d.ts +++ b/next-ui/src/components.d.ts @@ -33,6 +33,7 @@ declare module 'vue' { FragmentHistoryExpandTable: typeof import('./fragments/fragment/history/expand/Table.vue')['default'] FragmentHistoryTable: typeof import('./fragments/fragment/history/Table.vue')['default'] FragmentImportBooksTransientBooksTable: typeof import('./fragments/fragment/import/books/TransientBooksTable.vue')['default'] + FragmentImportReadlistTable: typeof import('./fragments/fragment/import/readlist/Table.vue')['default'] FragmentLocaleSelector: typeof import('./fragments/fragment/LocaleSelector.vue')['default'] FragmentRemoteFileList: typeof import('./fragments/fragment/RemoteFileList.vue')['default'] FragmentSnackQueue: typeof import('./fragments/fragment/SnackQueue.vue')['default'] diff --git a/next-ui/src/fragments/fragment/import/readlist/Table.stories.ts b/next-ui/src/fragments/fragment/import/readlist/Table.stories.ts new file mode 100644 index 000000000..31791773a --- /dev/null +++ b/next-ui/src/fragments/fragment/import/readlist/Table.stories.ts @@ -0,0 +1,97 @@ +import type { Meta, StoryObj } from '@storybook/vue3-vite' + +import { http, delay } from 'msw' +import Table from './Table.vue' +import { response400BadRequest } from '@/mocks/api/handlers' +import SnackQueue from '@/fragments/fragment/SnackQueue.vue' +import { matchCbl } from '@/mocks/api/handlers/readlists' +import { expect, waitFor } from 'storybook/test' + +const meta = { + component: Table, + subcomponents: { SnackQueue }, + render: (args: object) => ({ + components: { Table, SnackQueue }, + setup() { + return { args } + }, + template: '', + }), + parameters: { + // More on how to position stories at: https://storybook.js.org/docs/configure/story-layout + }, + args: {}, +} satisfies Meta + +export default meta +type Story = StoryObj + +export const Default: Story = { + args: { + match: matchCbl, + }, +} + +const singleMatch = { + ...matchCbl, + requests: [ + { + request: { series: ['Space Adventures (2018)', 'Space Adventures'], number: '1' }, + matches: [ + { + series: { + seriesId: '63', + title: 'Space Adventures', + releaseDate: '2018-07-10', + }, + books: [{ bookId: '0F99E5W723ENS', number: '1', title: 'Volume 1' }], + }, + ], + }, + ], +} +export const Created: Story = { + args: { + match: singleMatch, + }, + play: async ({ canvas, userEvent }) => { + await userEvent.click(canvas.getByRole('button', { name: /create/i })) + + await waitFor(() => expect(canvas.getByRole('button', { name: /create/i })).toBeDisabled()) + }, + render: (args: object) => ({ + components: { Table, SnackQueue }, + setup() { + return { args } + }, + template: '
', + }), +} + +export const Empty: Story = { + args: { + match: { ...matchCbl, requests: [] }, + }, +} + +export const Loading: Story = { + args: { + match: singleMatch, + }, + parameters: { + msw: { + handlers: [http.all('*', async () => await delay(2_000))], + }, + }, +} + +export const ErrorOnCreation: Story = { + args: { + match: singleMatch, + }, + parameters: { + msw: { + handlers: [http.post('*/v1/readlists', response400BadRequest)], + }, + }, +} diff --git a/next-ui/src/fragments/fragment/import/readlist/Table.vue b/next-ui/src/fragments/fragment/import/readlist/Table.vue new file mode 100644 index 000000000..2df2c524f --- /dev/null +++ b/next-ui/src/fragments/fragment/import/readlist/Table.vue @@ -0,0 +1,506 @@ + + + + + diff --git a/next-ui/src/mocks/api/handlers.ts b/next-ui/src/mocks/api/handlers.ts index 91a549b49..495eb8c49 100644 --- a/next-ui/src/mocks/api/handlers.ts +++ b/next-ui/src/mocks/api/handlers.ts @@ -12,6 +12,7 @@ import { seriesHandlers } from '@/mocks/api/handlers/series' import { booksHandlers } from '@/mocks/api/handlers/books' import { filesystemHandlers } from '@/mocks/api/handlers/filesystem' import { transientBooksHandlers } from '@/mocks/api/handlers/transient-books' +import { readListsHandlers } from '@/mocks/api/handlers/readlists' export const handlers = [ ...actuatorHandlers, @@ -21,6 +22,7 @@ export const handlers = [ ...filesystemHandlers, ...historyHandlers, ...librariesHandlers, + ...readListsHandlers, ...referentialHandlers, ...releasesHandlers, ...seriesHandlers, diff --git a/next-ui/src/mocks/api/handlers/readlists.ts b/next-ui/src/mocks/api/handlers/readlists.ts new file mode 100644 index 000000000..4c681c58b --- /dev/null +++ b/next-ui/src/mocks/api/handlers/readlists.ts @@ -0,0 +1,151 @@ +import { httpTyped } from '@/mocks/api/httpTyped' + +export const matchCbl = { + readListMatch: { name: "Jupiter's Legacy", errorCode: '' }, + requests: [ + { + request: { series: ['Space Adventures (2018)', 'Space Adventures'], number: '1' }, + matches: [ + { + series: { + seriesId: '63', + title: 'Space Adventures', + releaseDate: '2018-07-10', + }, + books: [{ bookId: '0F99E5W723ENS', number: '1', title: 'Volume 1' }], + }, + ], + }, + { + request: { series: ["Jupiter's Legacy (2013)", "Jupiter's Legacy"], number: '2' }, + matches: [ + { + series: { + seriesId: '63', + title: 'Space Adventures', + releaseDate: '2018-07-10', + }, + books: [{ bookId: '0F99E5W723ENS', number: '1', title: 'Volume 1' }], + }, + ], + }, + { + request: { series: ["Jupiter's Legacy (2013)", "Jupiter's Legacy"], number: '3' }, + matches: [ + { + series: { + seriesId: '63', + title: "Jupiter's Legacy", + releaseDate: '2018-07-10', + }, + books: [{ bookId: '0F99E5W763ECC', number: '3', title: 'Volume 3' }], + }, + ], + }, + { + request: { series: ["Jupiter's Legacy (2013)", "Jupiter's Legacy"], number: '4' }, + matches: [ + { + series: { + seriesId: '63', + title: "Jupiter's Legacy", + releaseDate: '2018-07-10', + }, + books: [{ bookId: '0F99E5W723ENT', number: '4', title: 'Volume 4' }], + }, + ], + }, + { + request: { series: ["Jupiter's Legacy (2013)", "Jupiter's Legacy"], number: '5' }, + matches: [ + { + series: { + seriesId: '63', + title: "Jupiter's Legacy", + releaseDate: '2018-07-10', + }, + books: [{ bookId: '0F99E5W723ENR', number: '5', title: 'Volume 5' }], + }, + ], + }, + { + request: { series: ["Jupiter's Legacy 2 (2016)", "Jupiter's Legacy 2"], number: '1' }, + matches: [], + }, + { + request: { series: ["Jupiter's Legacy 2 (2016)", "Jupiter's Legacy 2"], number: '2' }, + matches: [], + }, + { + request: { series: ["Jupiter's Legacy 2 (2016)", "Jupiter's Legacy 2"], number: '3' }, + matches: [], + }, + { + request: { series: ["Jupiter's Legacy 2 (2016)", "Jupiter's Legacy 2"], number: '4' }, + matches: [], + }, + { + request: { series: ["Jupiter's Legacy 2 (2016)", "Jupiter's Legacy 2"], number: '5' }, + matches: [], + }, + { + request: { + series: ["Jupiter's Legacy Requiem (2021)", "Jupiter's Legacy Requiem"], + number: '1', + }, + matches: [], + }, + { + request: { + series: ["Jupiter's Legacy Requiem (2021)", "Jupiter's Legacy Requiem"], + number: '2', + }, + matches: [], + }, + { + request: { + series: ["Jupiter's Legacy Requiem (2021)", "Jupiter's Legacy Requiem"], + number: '3', + }, + matches: [], + }, + { + request: { + series: ["Jupiter's Legacy Requiem (2021)", "Jupiter's Legacy Requiem"], + number: '4', + }, + matches: [], + }, + { + request: { + series: ["Jupiter's Legacy Requiem (2021)", "Jupiter's Legacy Requiem"], + number: '5', + }, + matches: [], + }, + { + request: { + series: ["Jupiter's Legacy Requiem (2021)", "Jupiter's Legacy Requiem"], + number: '6', + }, + matches: [], + }, + ], + errorCode: '', +} + +export const readListsHandlers = [ + httpTyped.post('/api/v1/readlists', async ({ request, response }) => { + const body = await request.json() + return response(200).json({ + ...body, + createdDate: new Date(), + lastModifiedDate: new Date(), + id: (Math.random() + 1).toString(36).substring(7), + filtered: false, + }) + }), + httpTyped.post('/api/v1/readlists/match/comicrack', ({ response }) => { + return response(200).json(matchCbl) + }), +] diff --git a/next-ui/src/stores/messages.ts b/next-ui/src/stores/messages.ts index 69ca869f5..e1b2d8597 100644 --- a/next-ui/src/stores/messages.ts +++ b/next-ui/src/stores/messages.ts @@ -1,8 +1,17 @@ // Utilities import { defineStore } from 'pinia' +type Message = + | { + text: string + color?: string + timer?: string | boolean + timeout?: string | number + } + | string + export const useMessagesStore = defineStore('messages', { state: () => ({ - messages: [] as object[], + messages: [] as Message[], }), })