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 @@
+
+ emit('selection', val, event)"
+ @click-quick-action="showEditMetadataDialog()"
+ @card-long-press="bottomSheet = true"
+ />
+
+
+
+
+
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 @@
- COLLECTIONS
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ toggleSelect(event as MouseEvent)"
+ />
+
+
+
+
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,
+}