mirror of
https://github.com/gotson/komga.git
synced 2025-12-06 08:32:25 +01:00
add import read list table
This commit is contained in:
parent
7a2deacb92
commit
3254d329d5
7 changed files with 783 additions and 1 deletions
16
next-ui/src/colada/readlists.ts
Normal file
16
next-ui/src/colada/readlists.ts
Normal file
|
|
@ -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,
|
||||
}),
|
||||
})
|
||||
})
|
||||
1
next-ui/src/components.d.ts
vendored
1
next-ui/src/components.d.ts
vendored
|
|
@ -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']
|
||||
|
|
|
|||
|
|
@ -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: '<Table v-bind="args"/><SnackQueue/>',
|
||||
}),
|
||||
parameters: {
|
||||
// More on how to position stories at: https://storybook.js.org/docs/configure/story-layout
|
||||
},
|
||||
args: {},
|
||||
} satisfies Meta<typeof Table>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
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: '<Table v-bind="args"/>',
|
||||
}),
|
||||
}
|
||||
|
||||
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)],
|
||||
},
|
||||
},
|
||||
}
|
||||
506
next-ui/src/fragments/fragment/import/readlist/Table.vue
Normal file
506
next-ui/src/fragments/fragment/import/readlist/Table.vue
Normal file
|
|
@ -0,0 +1,506 @@
|
|||
<template>
|
||||
<v-data-table
|
||||
v-model="selectedIndices"
|
||||
:loading="creating || loading"
|
||||
:items="readListEntries"
|
||||
item-value="index"
|
||||
:headers="headers"
|
||||
:search="filterRef"
|
||||
:custom-filter="filterFn"
|
||||
:hide-default-footer="hideFooter"
|
||||
fixed-header
|
||||
fixed-footer
|
||||
show-select
|
||||
item-selectable="selectable"
|
||||
select-strategy="page"
|
||||
mobile-breakpoint="md"
|
||||
>
|
||||
<template #no-data>
|
||||
<v-empty-state
|
||||
icon="i-mdi:filter"
|
||||
:title="
|
||||
$formatMessage({
|
||||
description:
|
||||
'Import reading list table: shown when table has no data because of the selected filter - title',
|
||||
defaultMessage: 'No data',
|
||||
id: 'AJa6Tq',
|
||||
})
|
||||
"
|
||||
:text="
|
||||
$formatMessage({
|
||||
description:
|
||||
'Import reading list table: shown when table has no data because of the selected filter - subtitle',
|
||||
defaultMessage: 'Try adjusting the filters',
|
||||
id: 'NpjqFA',
|
||||
})
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #top>
|
||||
<v-toolbar flat>
|
||||
<v-spacer />
|
||||
<v-chip-group
|
||||
v-model="filterSelect"
|
||||
multiple
|
||||
class="ms-2"
|
||||
:disabled="finishedState"
|
||||
>
|
||||
<v-chip
|
||||
v-for="f in filterOptions"
|
||||
:key="f.value"
|
||||
:value="f.value"
|
||||
:text="f.title"
|
||||
filter
|
||||
rounded
|
||||
color="primary"
|
||||
/>
|
||||
</v-chip-group>
|
||||
</v-toolbar>
|
||||
</template>
|
||||
|
||||
<template #[`item.request.request.series`]="{ value }">
|
||||
<div
|
||||
v-for="s in value"
|
||||
:key="s"
|
||||
>
|
||||
{{ s }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #[`item.series`]="{ item, internalItem, isSelected, value }">
|
||||
<div
|
||||
:class="finishedState ? undefined : 'cursor-pointer'"
|
||||
@mouseenter="
|
||||
finishedState
|
||||
? undefined
|
||||
: (dialogSeriesPickerActivator = $event.currentTarget as Element)
|
||||
"
|
||||
@click="finishedState ? undefined : (currentActionedItem = item)"
|
||||
>
|
||||
<template v-if="value">
|
||||
<div>{{ value?.title }}</div>
|
||||
<div v-if="value?.releaseDate">
|
||||
{{ $formatDate(value?.releaseDate, { year: 'numeric', timeZone: 'UTC' }) }}
|
||||
</div>
|
||||
</template>
|
||||
<div
|
||||
v-else
|
||||
style="height: 2em"
|
||||
:class="isSelected(internalItem) ? 'missing' : ''"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #[`item.book`]="{ item, internalItem, isSelected, value }">
|
||||
<div
|
||||
:class="finishedState || !item?.series ? undefined : 'cursor-pointer'"
|
||||
@mouseenter="
|
||||
finishedState || !item?.series
|
||||
? undefined
|
||||
: (dialogBookPickerActivator = $event.currentTarget as Element)
|
||||
"
|
||||
@click="finishedState || !item?.series ? undefined : (currentActionedItem = item)"
|
||||
>
|
||||
<span v-if="value">{{ value.number }} - {{ value.title }}</span>
|
||||
<div
|
||||
v-else
|
||||
style="height: 2em"
|
||||
:class="isSelected(internalItem) && item?.series ? 'missing' : ''"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #[`header.statusMessage`]>
|
||||
<v-icon icon="i-mdi:alert-circle-outline" />
|
||||
</template>
|
||||
|
||||
<template #[`item.statusMessage`]="{ item, value, internalItem, isSelected }">
|
||||
<template v-if="isSelected(internalItem)">
|
||||
<v-icon
|
||||
v-if="duplicateBookIds?.includes(item.book?.bookId)"
|
||||
v-tooltip="
|
||||
$formatMessage({
|
||||
description: 'Import reading list table: tooltip for status - duplicate book',
|
||||
defaultMessage: 'Duplicate book',
|
||||
id: '1MAL38',
|
||||
})
|
||||
"
|
||||
icon="i-mdi:alert-circle"
|
||||
color="warning"
|
||||
/>
|
||||
<v-icon
|
||||
v-else-if="value"
|
||||
v-tooltip="value"
|
||||
icon="i-mdi:alert-circle"
|
||||
color="error"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
</v-data-table>
|
||||
|
||||
<v-container fluid>
|
||||
<v-row
|
||||
justify="space-between"
|
||||
align="end"
|
||||
>
|
||||
<v-col
|
||||
cols="12"
|
||||
sm=""
|
||||
>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-text-field
|
||||
v-model="readListName"
|
||||
:rules="['required']"
|
||||
:disabled="finishedState"
|
||||
clearable
|
||||
:label="
|
||||
$formatMessage({
|
||||
description: 'Import reading list: bottom bar: reading list name',
|
||||
defaultMessage: 'Name',
|
||||
id: 'rrF/Z2',
|
||||
})
|
||||
"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-textarea
|
||||
v-model="readListSummary"
|
||||
rows="2"
|
||||
hide-details
|
||||
:disabled="finishedState"
|
||||
clearable
|
||||
:label="
|
||||
$formatMessage({
|
||||
description: 'Import reading list: bottom bar: reading list summary',
|
||||
defaultMessage: 'Summary',
|
||||
id: 'uW+6XG',
|
||||
})
|
||||
"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="auto">
|
||||
<v-btn
|
||||
:color="finishedState ? 'success' : 'primary'"
|
||||
:text="
|
||||
$formatMessage({
|
||||
description: 'Import reading list: bottom bar: create button',
|
||||
defaultMessage: 'Create',
|
||||
id: 'dipMGb',
|
||||
})
|
||||
"
|
||||
:disabled="!isFormValid || creating || loading || finishedState"
|
||||
:prepend-icon="finishedState ? 'i-mdi:check' : undefined"
|
||||
@click="doCreateReadList"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
|
||||
<FragmentDialogSeriesPicker
|
||||
:activator="dialogSeriesPickerActivator"
|
||||
:fullscreen="display.xs.value"
|
||||
@selected-series="(series) => seriesPicked(series)"
|
||||
/>
|
||||
|
||||
<DialogBookPicker
|
||||
:activator="dialogBookPickerActivator"
|
||||
:fullscreen="display.xs.value"
|
||||
:books="dialogBookPickerBooks"
|
||||
@selected-book="(book) => bookPicked(book)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { components } from '@/generated/openapi/komga'
|
||||
import { useIntl } from 'vue-intl'
|
||||
import {
|
||||
asyncComputed,
|
||||
syncRefs,
|
||||
useArrayFilter,
|
||||
useArrayMap,
|
||||
useMemoize,
|
||||
watchImmediate,
|
||||
} from '@vueuse/core'
|
||||
import { useDisplay } from 'vuetify'
|
||||
import { useQuery } from '@pinia/colada'
|
||||
import { bookListQuery } from '@/colada/books'
|
||||
import { useCreateReadList } from '@/colada/readlists'
|
||||
import { useMessagesStore } from '@/stores/messages'
|
||||
import type { ErrorCause } from '@/api/komga-client'
|
||||
import { commonMessages } from '@/utils/i18n/common-messages'
|
||||
|
||||
class ReadListEntry {
|
||||
index: number
|
||||
request: components['schemas']['ReadListRequestBookMatchesDto']
|
||||
series?: components['schemas']['ReadListRequestBookMatchSeriesDto']
|
||||
book?: components['schemas']['ReadListRequestBookMatchBookDto']
|
||||
|
||||
constructor(request: components['schemas']['ReadListRequestBookMatchesDto'], index: number) {
|
||||
this.index = index
|
||||
this.request = request
|
||||
const match = request.matches.find(Boolean)
|
||||
if (match) {
|
||||
this.series = match.series
|
||||
this.book = match.books.find(Boolean)
|
||||
}
|
||||
}
|
||||
|
||||
public get selectable(): boolean {
|
||||
return !finishedState.value
|
||||
}
|
||||
|
||||
public get importable(): boolean {
|
||||
return !!this.series && !!this.book
|
||||
}
|
||||
|
||||
public get statusMessage(): string {
|
||||
if (!this.series)
|
||||
return intl.formatMessage({
|
||||
description: 'Import reading list: status message: choose a series',
|
||||
defaultMessage: 'Choose a series',
|
||||
id: 'H2B6uF',
|
||||
})
|
||||
if (!this.book)
|
||||
return intl.formatMessage({
|
||||
description: 'Import reading list: status message: choose a book',
|
||||
defaultMessage: 'Choose a book',
|
||||
id: 'xYp/8u',
|
||||
})
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
const display = useDisplay()
|
||||
const intl = useIntl()
|
||||
const messagesStore = useMessagesStore()
|
||||
|
||||
const { match, loading = false } = defineProps<{
|
||||
match: components['schemas']['ReadListRequestMatchDto']
|
||||
loading?: boolean
|
||||
}>()
|
||||
|
||||
// a read-only array of ReadListEntry, kept in sync with the requests prop
|
||||
const readListEntriesRO = useArrayMap(
|
||||
toRef(() => match.requests),
|
||||
(it, index) => new ReadListEntry(it, index + 1),
|
||||
)
|
||||
// read-write array of ReadListEntry, will be modified by the different actions
|
||||
const readListEntries = ref<ReadListEntry[]>([])
|
||||
// 1-way sync from readListEntriesRO to readListEntries
|
||||
syncRefs(readListEntriesRO, readListEntries)
|
||||
|
||||
// the current item being acted upon, used for dialog callback
|
||||
const currentActionedItem = ref<ReadListEntry>()
|
||||
// the selected indices, used to programmatically select items
|
||||
const selectedIndices = ref<number[]>([])
|
||||
// a read-only list of the request indices, used to select all on initialization
|
||||
const entriesIndex = useArrayMap(readListEntriesRO, (it) => it.index)
|
||||
// 1-way sync from entriesIndex to selectedIndices, will effectively select all on initialization
|
||||
syncRefs(entriesIndex, selectedIndices)
|
||||
|
||||
// the selected books
|
||||
const selectedBooks = useArrayFilter(readListEntries, (it) =>
|
||||
selectedIndices.value.includes(it.index),
|
||||
)
|
||||
|
||||
const duplicateBookIds = useArrayFilter(
|
||||
useArrayMap(readListEntries, (it) => it.book?.bookId),
|
||||
(it, index, array) => !!it && array.indexOf(it) !== index,
|
||||
)
|
||||
|
||||
const readListName = ref<string>(match.readListMatch.name)
|
||||
const readListSummary = ref<string>()
|
||||
const readListCreated = ref<components['schemas']['ReadListDto']>()
|
||||
// if the prop changes, reset some data
|
||||
watchImmediate(
|
||||
() => match,
|
||||
(m) => {
|
||||
readListName.value = m.readListMatch.name
|
||||
readListSummary.value = ''
|
||||
readListCreated.value = undefined
|
||||
},
|
||||
)
|
||||
|
||||
const finishedState = computed<boolean>(() => !!readListCreated.value)
|
||||
|
||||
// Table setup
|
||||
const hideFooter = computed(() => readListEntries.value.length < 10)
|
||||
|
||||
const headers = [
|
||||
{
|
||||
title: '#',
|
||||
key: 'index',
|
||||
},
|
||||
{
|
||||
title: intl.formatMessage({
|
||||
description: 'Import reading list table header: requested series',
|
||||
defaultMessage: 'Requested series',
|
||||
id: 'LD5j8J',
|
||||
}),
|
||||
key: 'request.request.series',
|
||||
},
|
||||
{
|
||||
title: intl.formatMessage({
|
||||
description: 'Import reading list table header: requested number',
|
||||
defaultMessage: 'Requested number',
|
||||
id: 'Wlzzv8',
|
||||
}),
|
||||
key: 'request.request.number',
|
||||
},
|
||||
{
|
||||
title: intl.formatMessage({
|
||||
description: 'Import reading list table header: Series',
|
||||
defaultMessage: 'Series',
|
||||
id: 'ThHjN4',
|
||||
}),
|
||||
key: 'series',
|
||||
},
|
||||
{
|
||||
title: intl.formatMessage({
|
||||
description: 'Import reading list table header: Book',
|
||||
defaultMessage: 'Book',
|
||||
id: '700A3r',
|
||||
}),
|
||||
key: 'book',
|
||||
},
|
||||
{
|
||||
title: intl.formatMessage({
|
||||
description: 'Import reading list table header: status message',
|
||||
defaultMessage: 'Status',
|
||||
id: 'J44THG',
|
||||
}),
|
||||
key: 'statusMessage',
|
||||
align: 'end',
|
||||
},
|
||||
] as const // workaround for https://github.com/vuetifyjs/vuetify/issues/18901
|
||||
|
||||
// Filtering
|
||||
const filterSelect = ref<string[]>([])
|
||||
const filterRef = computed(() => filterSelect.value.join(''))
|
||||
const filterOptions = [
|
||||
{
|
||||
title: 'OK',
|
||||
value: 'o',
|
||||
},
|
||||
{
|
||||
title: 'Error',
|
||||
value: 'e',
|
||||
},
|
||||
{
|
||||
title: 'Duplicate',
|
||||
value: 'd',
|
||||
},
|
||||
]
|
||||
|
||||
function filterFn(
|
||||
value: string,
|
||||
query: string,
|
||||
item?: { raw: ReadListEntry },
|
||||
): boolean | number | [number, number] | [number, number][] {
|
||||
const error = item?.raw.statusMessage
|
||||
const duplicate = duplicateBookIds.value.includes(item?.raw.book?.bookId)
|
||||
if (error && query.includes('e')) return true
|
||||
if (!error && duplicate && query.includes('d')) return true
|
||||
if (!error && !duplicate && query.includes('o')) return true
|
||||
return false
|
||||
}
|
||||
|
||||
// Series Picker Dialog
|
||||
const dialogSeriesPickerActivator = ref<Element | undefined>(undefined)
|
||||
|
||||
function seriesPicked(series: components['schemas']['SeriesDto']) {
|
||||
if (currentActionedItem.value) {
|
||||
currentActionedItem.value.series = {
|
||||
seriesId: series.id,
|
||||
title: series.metadata.title,
|
||||
releaseDate: series.booksMetadata?.releaseDate,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Book Picker Dialog
|
||||
const dialogBookPickerActivator = ref<Element | undefined>(undefined)
|
||||
const dialogBookPickerBooks = asyncComputed(async () =>
|
||||
currentActionedItem.value?.series
|
||||
? (await getSeriesBooks(currentActionedItem.value.series.seriesId))?.content
|
||||
: undefined,
|
||||
)
|
||||
|
||||
function bookPicked(book: components['schemas']['BookDto']) {
|
||||
if (currentActionedItem.value) {
|
||||
currentActionedItem.value.book = {
|
||||
bookId: book.id,
|
||||
title: book.metadata.title,
|
||||
number: book.metadata.number,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const getSeriesBooks = useMemoize(async (seriesId: string) =>
|
||||
useQuery(bookListQuery, () => ({
|
||||
search: {
|
||||
condition: {
|
||||
seriesId: { operator: 'Is', value: seriesId },
|
||||
},
|
||||
} as components['schemas']['BookSearch'],
|
||||
}))
|
||||
.refresh()
|
||||
.then(({ data }) => data),
|
||||
)
|
||||
|
||||
const isFormValid = computed<boolean>(() => !!readListName.value)
|
||||
const createPayload = computed(
|
||||
() =>
|
||||
({
|
||||
name: readListName.value,
|
||||
summary: readListSummary.value,
|
||||
ordered: true,
|
||||
bookIds: [],
|
||||
}) as components['schemas']['ReadListCreationDto'],
|
||||
)
|
||||
|
||||
const { mutateAsync: postReadList, isLoading: creating } = useCreateReadList()
|
||||
|
||||
function doCreateReadList() {
|
||||
if (selectedBooks.value.length == 0) {
|
||||
messagesStore.messages.push('Select some books')
|
||||
return
|
||||
}
|
||||
if (selectedBooks.value.some((it) => !it.importable)) {
|
||||
messagesStore.messages.push('Some of the selected books are in error')
|
||||
return
|
||||
}
|
||||
if (selectedBooks.value.some((it) => duplicateBookIds.value?.includes(it.book?.bookId))) {
|
||||
messagesStore.messages.push('Some of the selected books are duplicates')
|
||||
return
|
||||
}
|
||||
|
||||
postReadList(createPayload.value)
|
||||
.then(({ data }) => {
|
||||
readListCreated.value = data
|
||||
//TODO: add link
|
||||
messagesStore.messages.push({
|
||||
text: 'Readlist created',
|
||||
})
|
||||
})
|
||||
.catch((error) => {
|
||||
messagesStore.messages.push({
|
||||
text:
|
||||
(error?.cause as ErrorCause)?.message || intl.formatMessage(commonMessages.networkError),
|
||||
})
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.missing {
|
||||
border: 2px dashed red;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
151
next-ui/src/mocks/api/handlers/readlists.ts
Normal file
151
next-ui/src/mocks/api/handlers/readlists.ts
Normal file
|
|
@ -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)
|
||||
}),
|
||||
]
|
||||
|
|
@ -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[],
|
||||
}),
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in a new issue