From c13f55efea428ba9d7092fce8a57c2c613d07740 Mon Sep 17 00:00:00 2001 From: Gauthier Roebroeck Date: Wed, 1 Apr 2026 16:03:42 +0800 Subject: [PATCH] browse collections --- next-ui/src/api/images.ts | 5 + next-ui/src/colada/collections.ts | 41 +++++- next-ui/src/components.d.ts | 4 + .../collection/DeletionWarning.stories.ts | 31 +++++ .../components/collection/DeletionWarning.vue | 36 +++++ .../collection/card/CollectionCard.stories.ts | 67 +++++++++ .../collection/card/CollectionCard.vue | 87 ++++++++++++ .../collection/menu/CollectionMenu.mdx | 9 ++ .../collection/menu/CollectionMenu.stories.ts | 54 ++++++++ .../collection/menu/CollectionMenu.vue | 27 ++++ .../menu/CollectionMenuBottomSheet.mdx | 9 ++ .../menu/CollectionMenuBottomSheet.stories.ts | 48 +++++++ .../menu/CollectionMenuBottomSheet.vue | 34 +++++ next-ui/src/components/item/Browser.vue | 4 +- .../collection/useCollectionActions.ts | 129 ++++++++++++++++++ .../collection/useEditCollectionDialog.ts | 87 ++++++++++++ next-ui/src/composables/libraries.ts | 3 + next-ui/src/mocks/api/handlers/collections.ts | 32 ++--- next-ui/src/pages/libraries/[id]/books.vue | 2 +- .../src/pages/libraries/[id]/collections.vue | 122 ++++++++++++++++- next-ui/src/pages/libraries/[id]/series.vue | 2 +- next-ui/src/types/ItemCard.ts | 2 +- next-ui/src/types/collection.ts | 4 + 23 files changed, 813 insertions(+), 26 deletions(-) create mode 100644 next-ui/src/components/collection/DeletionWarning.stories.ts create mode 100644 next-ui/src/components/collection/DeletionWarning.vue create mode 100644 next-ui/src/components/collection/card/CollectionCard.stories.ts create mode 100644 next-ui/src/components/collection/card/CollectionCard.vue create mode 100644 next-ui/src/components/collection/menu/CollectionMenu.mdx create mode 100644 next-ui/src/components/collection/menu/CollectionMenu.stories.ts create mode 100644 next-ui/src/components/collection/menu/CollectionMenu.vue create mode 100644 next-ui/src/components/collection/menu/CollectionMenuBottomSheet.mdx create mode 100644 next-ui/src/components/collection/menu/CollectionMenuBottomSheet.stories.ts create mode 100644 next-ui/src/components/collection/menu/CollectionMenuBottomSheet.vue create mode 100644 next-ui/src/composables/collection/useCollectionActions.ts create mode 100644 next-ui/src/composables/collection/useEditCollectionDialog.ts create mode 100644 next-ui/src/types/collection.ts diff --git a/next-ui/src/api/images.ts b/next-ui/src/api/images.ts index 0ca07af9..aa65641a 100644 --- a/next-ui/src/api/images.ts +++ b/next-ui/src/api/images.ts @@ -15,6 +15,11 @@ export function bookPageThumbnailUrl(bookId?: string, page?: number): string | u return undefined } +export function collectionThumbnailUrl(collectionId?: string): string | undefined { + if (collectionId) return `${ApiBaseUrl.noSlash}/api/v1/collections/${collectionId}/thumbnail` + return undefined +} + export function pageHashKnownThumbnailUrl(hash?: string): string | undefined { if (hash) return `${ApiBaseUrl.noSlash}/api/v1/page-hashes/${hash}/thumbnail` return undefined diff --git a/next-ui/src/colada/collections.ts b/next-ui/src/colada/collections.ts index 10a883f7..eafcab06 100644 --- a/next-ui/src/colada/collections.ts +++ b/next-ui/src/colada/collections.ts @@ -1,6 +1,7 @@ -import { defineQueryOptions } from '@pinia/colada' +import { defineMutation, defineQueryOptions, useMutation } from '@pinia/colada' import { komgaClient } from '@/api/komga-client' import type { PageRequest } from '@/types/PageRequest' +import type { components } from '@/generated/openapi/komga' export const QUERY_KEYS_COLLECTIONS = { root: ['collections'] as const, @@ -52,3 +53,41 @@ export const collectionDetailQuery = defineQueryOptions( .then((res) => res.data), }), ) + +export const useUpdateCollection = defineMutation(() => { + // const queryCache = useQueryCache() + return useMutation({ + mutation: ({ + collectionId, + data, + }: { + collectionId: string + data: components['schemas']['CollectionUpdateDto'] + }) => + komgaClient.PATCH('/api/v1/collections/{id}', { + params: { + path: { + id: collectionId, + }, + }, + body: data, + }), + onSuccess: () => { + //TODO: check how to invalidate cache + // void queryCache.invalidateQueries({ key: QUERY_KEYS_LIBRARIES.root }) + }, + }) +}) + +export const useDeleteCollection = defineMutation(() => + useMutation({ + mutation: (collectionId: string) => + komgaClient.DELETE('/api/v1/collections/{id}', { + params: { + path: { + id: collectionId, + }, + }, + }), + }), +) diff --git a/next-ui/src/components.d.ts b/next-ui/src/components.d.ts index de0c575d..a2ef1856 100644 --- a/next-ui/src/components.d.ts +++ b/next-ui/src/components.d.ts @@ -24,6 +24,10 @@ declare module 'vue' { BuildCommit: typeof import('./components/BuildCommit.vue')['default'] BuildVersion: typeof import('./components/BuildVersion.vue')['default'] ChipCount: typeof import('./components/ChipCount.vue')['default'] + CollectionCard: typeof import('./components/collection/card/CollectionCard.vue')['default'] + CollectionDeletionWarning: typeof import('./components/collection/DeletionWarning.vue')['default'] + CollectionMenu: typeof import('./components/collection/menu/CollectionMenu.vue')['default'] + CollectionMenuBottomSheet: typeof import('./components/collection/menu/CollectionMenuBottomSheet.vue')['default'] DialogBookPicker: typeof import('./components/dialog/BookPicker.vue')['default'] DialogConfirm: typeof import('./components/dialog/Confirm.vue')['default'] DialogConfirmEdit: typeof import('./components/dialog/ConfirmEdit.vue')['default'] diff --git a/next-ui/src/components/collection/DeletionWarning.stories.ts b/next-ui/src/components/collection/DeletionWarning.stories.ts new file mode 100644 index 00000000..dc3dca4d --- /dev/null +++ b/next-ui/src/components/collection/DeletionWarning.stories.ts @@ -0,0 +1,31 @@ +import type { Meta, StoryObj } from '@storybook/vue3-vite' + +import DeletionWarning from './DeletionWarning.vue' + +const meta = { + component: DeletionWarning, + render: (args: object) => ({ + components: { DeletionWarning }, + setup() { + return { args } + }, + template: '', + }), + parameters: { + // More on how to position stories at: https://storybook.js.org/docs/configure/story-layout + docs: { + description: { + component: + 'Warning shown within a confirmation dialog before deleting a particular series.', + }, + }, + }, + args: {}, +} satisfies Meta + +export default meta +type Story = StoryObj + +export const Default: Story = { + args: {}, +} diff --git a/next-ui/src/components/collection/DeletionWarning.vue b/next-ui/src/components/collection/DeletionWarning.vue new file mode 100644 index 00000000..e29797f9 --- /dev/null +++ b/next-ui/src/components/collection/DeletionWarning.vue @@ -0,0 +1,36 @@ + + + diff --git a/next-ui/src/components/collection/card/CollectionCard.stories.ts b/next-ui/src/components/collection/card/CollectionCard.stories.ts new file mode 100644 index 00000000..01a0438e --- /dev/null +++ b/next-ui/src/components/collection/card/CollectionCard.stories.ts @@ -0,0 +1,67 @@ +import type { Meta, StoryObj } from '@storybook/vue3-vite' + +import CollectionCard from './CollectionCard.vue' +import { fn } from 'storybook/test' +import { httpTyped } from '@/mocks/api/httpTyped' +import { userRegular } from '@/mocks/api/handlers/users' +import DialogConfirmEditInstance from '@/components/dialog/ConfirmEditInstance.vue' +import DialogConfirmInstance from '@/components/dialog/ConfirmInstance.vue' +import { mockCollection } from '@/mocks/api/handlers/collections' + +const meta = { + component: CollectionCard, + render: (args: object) => ({ + components: { CollectionCard, DialogConfirmEditInstance, DialogConfirmInstance }, + setup() { + return { args } + }, + template: + '', + }), + parameters: { + // More on how to position stories at: https://storybook.js.org/docs/configure/story-layout + docs: { + description: { + component: '', + }, + }, + }, + args: { + collection: mockCollection, + onSelection: fn(), + }, +} satisfies Meta + +export default meta +type Story = StoryObj + +export const Default: Story = { + args: {}, +} + +export const Selected: Story = { + args: { + selected: true, + }, +} + +export const Hover: Story = { + args: {}, + play: ({ canvas, userEvent }) => { + userEvent.hover(canvas.getByRole('img')) + }, +} + +export const HoverNonAdmin: Story = { + args: {}, + parameters: { + msw: { + handlers: [ + httpTyped.get('/api/v2/users/me', ({ response }) => response(200).json(userRegular)), + ], + }, + }, + play: ({ canvas, userEvent }) => { + userEvent.hover(canvas.getByRole('img')) + }, +} diff --git a/next-ui/src/components/collection/card/CollectionCard.vue b/next-ui/src/components/collection/card/CollectionCard.vue new file mode 100644 index 00000000..c3b0e92e --- /dev/null +++ b/next-ui/src/components/collection/card/CollectionCard.vue @@ -0,0 +1,87 @@ + + + diff --git a/next-ui/src/components/collection/menu/CollectionMenu.mdx b/next-ui/src/components/collection/menu/CollectionMenu.mdx new file mode 100644 index 00000000..024012dd --- /dev/null +++ b/next-ui/src/components/collection/menu/CollectionMenu.mdx @@ -0,0 +1,9 @@ +import { Meta } from '@storybook/addon-docs/blocks'; + +import * as Stories from './CollectionMenu.stories.ts'; + + + +# CollectionMenu + +Action menu for collection. diff --git a/next-ui/src/components/collection/menu/CollectionMenu.stories.ts b/next-ui/src/components/collection/menu/CollectionMenu.stories.ts new file mode 100644 index 00000000..e969c3f5 --- /dev/null +++ b/next-ui/src/components/collection/menu/CollectionMenu.stories.ts @@ -0,0 +1,54 @@ +import type { Meta, StoryObj } from '@storybook/vue3-vite' + +import CollectionMenu from './CollectionMenu.vue' +import { expect } from 'storybook/test' +import DialogConfirmInstance from '@/components/dialog/ConfirmInstance.vue' +import DialogConfirmEditInstance from '@/components/dialog/ConfirmEditInstance.vue' +import { mockCollection } from '@/mocks/api/handlers/collections' + +const meta = { + component: CollectionMenu, + render: (args: object) => ({ + components: { CollectionMenu, DialogConfirmInstance, DialogConfirmEditInstance }, + setup() { + return { args } + }, + template: + '', + }), + parameters: { + // More on how to position stories at: https://storybook.js.org/docs/configure/story-layout + docs: { + description: { + component: '', + }, + }, + }, + args: { + activator: '#IDce0b073e6b2146e688c1cd32b61f3fef', + collection: mockCollection, + }, + play: async ({ canvas, userEvent }) => { + await expect(canvas.getByRole('button')).toBeEnabled() + + await userEvent.click(canvas.getByRole('button')) + }, +} satisfies Meta + +export default meta +type Story = StoryObj + +export const Default: Story = { + args: {}, +} + +// export const NonAdmin: Story = { +// args: {}, +// parameters: { +// msw: { +// handlers: [ +// httpTyped.get('/api/v2/users/me', ({ response }) => response(200).json(userRegular)), +// ], +// }, +// }, +// } diff --git a/next-ui/src/components/collection/menu/CollectionMenu.vue b/next-ui/src/components/collection/menu/CollectionMenu.vue new file mode 100644 index 00000000..5f97547a --- /dev/null +++ b/next-ui/src/components/collection/menu/CollectionMenu.vue @@ -0,0 +1,27 @@ + + + + + + + diff --git a/next-ui/src/components/collection/menu/CollectionMenuBottomSheet.mdx b/next-ui/src/components/collection/menu/CollectionMenuBottomSheet.mdx new file mode 100644 index 00000000..983c7885 --- /dev/null +++ b/next-ui/src/components/collection/menu/CollectionMenuBottomSheet.mdx @@ -0,0 +1,9 @@ +import { Meta } from '@storybook/addon-docs/blocks'; + +import * as Stories from './CollectionMenuBottomSheet.stories.ts'; + + + +# CollectionMenuBottomSheet + +Action menu for collection for touch screens. diff --git a/next-ui/src/components/collection/menu/CollectionMenuBottomSheet.stories.ts b/next-ui/src/components/collection/menu/CollectionMenuBottomSheet.stories.ts new file mode 100644 index 00000000..4d35f069 --- /dev/null +++ b/next-ui/src/components/collection/menu/CollectionMenuBottomSheet.stories.ts @@ -0,0 +1,48 @@ +import type { Meta, StoryObj } from '@storybook/vue3-vite' + +import CollectionMenuBottomSheet from './CollectionMenuBottomSheet.vue' +import DialogConfirmInstance from '@/components/dialog/ConfirmInstance.vue' +import DialogConfirmEditInstance from '@/components/dialog/ConfirmEditInstance.vue' +import { mockCollection } from '@/mocks/api/handlers/collections' + +const meta = { + component: CollectionMenuBottomSheet, + render: (args: object) => ({ + components: { CollectionMenuBottomSheet, DialogConfirmInstance, DialogConfirmEditInstance }, + setup() { + return { args } + }, + template: + '', + }), + parameters: { + // More on how to position stories at: https://storybook.js.org/docs/configure/story-layout + docs: { + description: { + component: '', + }, + }, + }, + args: { + modelValue: true, + collection: mockCollection, + }, +} satisfies Meta + +export default meta +type Story = StoryObj + +export const Default: Story = { + args: {}, +} + +// export const NonAdmin: Story = { +// args: {}, +// parameters: { +// msw: { +// handlers: [ +// httpTyped.get('/api/v2/users/me', ({ response }) => response(200).json(userRegular)), +// ], +// }, +// }, +// } diff --git a/next-ui/src/components/collection/menu/CollectionMenuBottomSheet.vue b/next-ui/src/components/collection/menu/CollectionMenuBottomSheet.vue new file mode 100644 index 00000000..4cfca019 --- /dev/null +++ b/next-ui/src/components/collection/menu/CollectionMenuBottomSheet.vue @@ -0,0 +1,34 @@ + + + + + + + diff --git a/next-ui/src/components/item/Browser.vue b/next-ui/src/components/item/Browser.vue index 44bf31f4..ca1503c6 100644 --- a/next-ui/src/components/item/Browser.vue +++ b/next-ui/src/components/item/Browser.vue @@ -13,7 +13,7 @@ ('page1', { required: true }) diff --git a/next-ui/src/composables/collection/useCollectionActions.ts b/next-ui/src/composables/collection/useCollectionActions.ts new file mode 100644 index 00000000..5bbf1435 --- /dev/null +++ b/next-ui/src/composables/collection/useCollectionActions.ts @@ -0,0 +1,129 @@ +import type { components } from '@/generated/openapi/komga' +import type { ErrorCause } from '@/api/komga-client' +import { commonMessages } from '@/utils/i18n/common-messages' +import { useDialogsStore } from '@/stores/dialogs' +import { storeToRefs } from 'pinia' +import { useIntl } from 'vue-intl' +import { useDisplay } from 'vuetify' +import { useMessagesStore } from '@/stores/messages' +import { useCurrentUser } from '@/colada/users' +import CollectionDeletionWarning from '@/components/collection/DeletionWarning.vue' +import { CollectionAction } from '@/types/collection' +import { useEditCollectionDialog } from '@/composables/collection/useEditCollectionDialog' +import { useDeleteCollection } from '@/colada/collections' + +export function useCollectionActions( + collection: components['schemas']['CollectionDto'], + callback: (action: CollectionAction) => void = () => {}, +) { + const { isAdmin } = useCurrentUser() + const intl = useIntl() + const { confirm: dialogConfirm } = storeToRefs(useDialogsStore()) + const messagesStore = useMessagesStore() + const display = useDisplay() + + const manageActions = computed(() => [ + ...(isAdmin.value + ? [ + { + title: intl.formatMessage({ + description: 'Collection menu: manage > edit', + defaultMessage: 'Edit', + id: '39afQA', + }), + action: CollectionAction.EDIT, + onMouseenter: (event: Event) => (editActivator.value = event.currentTarget as Element), + onClick: () => { + edit() + callback(CollectionAction.EDIT) + }, + }, + ] + : []), + ...(isAdmin.value + ? [ + { + title: intl.formatMessage({ + description: 'Collection menu: manage > delete', + defaultMessage: 'Delete', + id: 'Ekh3wO', + }), + action: CollectionAction.DELETE, + onMouseenter: (event: Event) => + (dialogConfirm.value.activator = event.currentTarget as Element), + onClick: () => { + deleteCollection() + callback(CollectionAction.DELETE) + }, + }, + ] + : []), + ]) + + //region Edit collection + const { prepareDialog: showEditCollectionDialog, activator: editActivator } = + useEditCollectionDialog() + + function edit() { + showEditCollectionDialog(collection) + } + //endregion + + //region Delete + const { mutateAsync: mutateDelete } = useDeleteCollection() + + function deleteCollection() { + dialogConfirm.value.dialogProps = { + title: intl.formatMessage({ + description: 'Collection delete dialog: title', + defaultMessage: 'Delete collection', + id: 'k6BCzW', + }), + subtitle: collection.name, + maxWidth: 600, + mode: 'checkbox', + color: 'error', + okText: intl.formatMessage({ + description: 'Collection delete dialog: confirm button', + defaultMessage: 'Delete', + id: 'rDBhmQ', + }), + closeOnSave: true, + fullscreen: display.xs.value, + } + dialogConfirm.value.slotWarning = { + component: markRaw(CollectionDeletionWarning), + props: {}, + } + dialogConfirm.value.callback = () => { + mutateDelete(collection.id) + .then(() => { + messagesStore.messages.push({ + text: intl.formatMessage( + { + description: 'Snackbar notification shown upon successful collection deletion', + defaultMessage: 'Collection deleted: {collection}', + id: 'HdsnFp', + }, + { + collection: collection.name, + }, + ), + }) + }) + .catch((error) => { + messagesStore.messages.push({ + text: + (error?.cause as ErrorCause)?.message || + intl.formatMessage(commonMessages.networkError), + }) + }) + } + } + //endregion + + return { + // actions: actions, + manageActions: manageActions, + } +} diff --git a/next-ui/src/composables/collection/useEditCollectionDialog.ts b/next-ui/src/composables/collection/useEditCollectionDialog.ts new file mode 100644 index 00000000..c81ff6dd --- /dev/null +++ b/next-ui/src/composables/collection/useEditCollectionDialog.ts @@ -0,0 +1,87 @@ +import { storeToRefs } from 'pinia' +import { useDialogsStore } from '@/stores/dialogs' +import { useIntl } from 'vue-intl' +import { useDisplay } from 'vuetify/framework' +import { useMessagesStore } from '@/stores/messages' +import type { components } from '@/generated/openapi/komga' +import EditMetadata from '@/components/series/form/EditMetadata.vue' +import type { ErrorCause } from '@/api/komga-client' +import { commonMessages } from '@/utils/i18n/common-messages' +import { useUpdateCollection } from '@/colada/collections' + +export function useEditCollectionDialog() { + const { confirmEdit: dialogConfirmEdit } = storeToRefs(useDialogsStore()) + const intl = useIntl() + const display = useDisplay() + const messagesStore = useMessagesStore() + const { mutateAsync: mutateUpdateCollection } = useUpdateCollection() + + const prepareDialog = (collection: components['schemas']['CollectionDto']) => { + dialogConfirmEdit.value.dialogProps = { + title: intl.formatMessage({ + description: 'Edit collection dialog title', + defaultMessage: 'Edit collection', + id: 'YVQ49g', + }), + subtitle: collection.name, + maxWidth: 600, + okText: 'Save', + cardTextClass: 'px-0', + closeOnSave: false, + scrollable: true, + fullscreen: display.xs.value, + } + dialogConfirmEdit.value.slot = { + component: markRaw(EditMetadata), + } + dialogConfirmEdit.value.record = collection + dialogConfirmEdit.value.callback = ( + hideDialog: () => void, + setLoading: (isLoading: boolean) => void, + ) => { + setLoading(true) + + const updatedData = dialogConfirmEdit.value.record as components['schemas']['CollectionDto'] + + mutateUpdateCollection({ collectionId: collection.id, data: updatedData }) + .then(() => { + hideDialog() + messagesStore.messages.push({ + text: intl.formatMessage( + { + description: 'Snackbar notification shown upon successful collection update', + defaultMessage: 'Collection updated: {collection}', + id: 'E0cw62', + }, + { + collection: updatedData.name, + }, + ), + }) + }) + .catch((error) => { + messagesStore.messages.push({ + text: + (error?.cause as ErrorCause)?.message || + intl.formatMessage(commonMessages.networkError), + }) + setLoading(false) + }) + } + } + + const activatorRef = computed({ + get: () => dialogConfirmEdit.value.activator, + set: (val) => (dialogConfirmEdit.value.activator = val), + }) + + function showDialog() { + dialogConfirmEdit.value.dialogProps.shown = true + } + + return { + prepareDialog: prepareDialog, + activator: activatorRef, + showDialog: showDialog, + } +} diff --git a/next-ui/src/composables/libraries.ts b/next-ui/src/composables/libraries.ts index eb6b335c..7bd0a8b5 100644 --- a/next-ui/src/composables/libraries.ts +++ b/next-ui/src/composables/libraries.ts @@ -31,7 +31,10 @@ export function useGetLibrariesById(libraryId: MaybeRefOrGetter) { return libs }) + const libIds = computed(() => libs.value?.map((it) => it.id)) + return { libraries: libs, + libraryIds: libIds, } } diff --git a/next-ui/src/mocks/api/handlers/collections.ts b/next-ui/src/mocks/api/handlers/collections.ts index f989ff70..6a8e1560 100644 --- a/next-ui/src/mocks/api/handlers/collections.ts +++ b/next-ui/src/mocks/api/handlers/collections.ts @@ -1,8 +1,10 @@ import { httpTyped } from '@/mocks/api/httpTyped' import { PageRequest } from '@/types/PageRequest' import { mockPage } from '@/mocks/api/pageable' +import { http, HttpResponse } from 'msw' +import mockThumbnailUrl from '@/assets/mock-thumbnail.jpg' -const collection1 = { +export const mockCollection = { id: '026801S4HWRZA', name: 'Golden Age', ordered: true, @@ -12,7 +14,7 @@ const collection1 = { filtered: false, } -const collections = [collection1] +const collections = [mockCollection] export const collectionsHandlers = [ httpTyped.get('/api/v1/collections', ({ query, response }) => { @@ -28,20 +30,14 @@ export const collectionsHandlers = [ mockPage(selected, new PageRequest(Number(query.get('page')), Number(query.get('size')))), ) }), - // httpTyped.get('/api/v1/series/{seriesId}', ({ params, response }) => { - // if (params.seriesId === '404') return response(404).empty() - // return response(200).json( - // Object.assign({}, series1, { metadata: { title: `Series ${params.seriesId}` } }), - // ) - // }), - // http.get('*/api/v1/series/*/thumbnail', async () => { - // // Get an ArrayBuffer from reading the file from disk or fetching it. - // const buffer = await fetch(mockThumbnailUrl).then((response) => response.arrayBuffer()) - // - // return HttpResponse.arrayBuffer(buffer, { - // headers: { - // 'content-type': 'image/jpg', - // }, - // }) - // }), + http.get('*/api/v1/collections/*/thumbnail', async () => { + // Get an ArrayBuffer from reading the file from disk or fetching it. + const buffer = await fetch(mockThumbnailUrl).then((response) => response.arrayBuffer()) + + return HttpResponse.arrayBuffer(buffer, { + headers: { + 'content-type': 'image/jpg', + }, + }) + }), ] diff --git a/next-ui/src/pages/libraries/[id]/books.vue b/next-ui/src/pages/libraries/[id]/books.vue index 7b982001..66b18a49 100644 --- a/next-ui/src/pages/libraries/[id]/books.vue +++ b/next-ui/src/pages/libraries/[id]/books.vue @@ -126,7 +126,7 @@ :book="item" :selected="isSelected" :pre-select="preSelect" - :width="display.xs.value ? undefined : appStore.gridCardWidth" + :width="display.xs.value ? 'auto' : appStore.gridCardWidth" @selection="(_val, event) => toggleSelect(event as MouseEvent)" /> diff --git a/next-ui/src/pages/libraries/[id]/collections.vue b/next-ui/src/pages/libraries/[id]/collections.vue index de17f29a..69ff8586 100644 --- a/next-ui/src/pages/libraries/[id]/collections.vue +++ b/next-ui/src/pages/libraries/[id]/collections.vue @@ -1,12 +1,128 @@ + + meta: requiresRole: USER diff --git a/next-ui/src/pages/libraries/[id]/series.vue b/next-ui/src/pages/libraries/[id]/series.vue index a454f3da..6e431c49 100644 --- a/next-ui/src/pages/libraries/[id]/series.vue +++ b/next-ui/src/pages/libraries/[id]/series.vue @@ -195,7 +195,7 @@ :series="item" :selected="isSelected" :pre-select="preSelect" - :width="display.xs.value ? undefined : appStore.gridCardWidth" + :width="display.xs.value ? 'auto' : appStore.gridCardWidth" @selection="(_val, event) => toggleSelect(event as MouseEvent)" /> diff --git a/next-ui/src/types/ItemCard.ts b/next-ui/src/types/ItemCard.ts index d9b14b40..c7cbd009 100644 --- a/next-ui/src/types/ItemCard.ts +++ b/next-ui/src/types/ItemCard.ts @@ -4,7 +4,7 @@ export type ItemCardProps = { * * Defaults to `150`. */ - width?: number + width?: number | 'auto' /** * Disable card selection. */ diff --git a/next-ui/src/types/collection.ts b/next-ui/src/types/collection.ts new file mode 100644 index 00000000..b43caa12 --- /dev/null +++ b/next-ui/src/types/collection.ts @@ -0,0 +1,4 @@ +export enum CollectionAction { + EDIT, + DELETE, +}