mirror of
https://github.com/gotson/komga.git
synced 2025-12-06 16:42:24 +01:00
start adding libraries in drawer
This commit is contained in:
parent
8f3f8c1d9a
commit
ba41c110f6
10 changed files with 315 additions and 1 deletions
49
next-ui/src/colada/client-settings.ts
Normal file
49
next-ui/src/colada/client-settings.ts
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
import { defineMutation, defineQuery, useMutation, useQuery, useQueryCache } from '@pinia/colada'
|
||||||
|
import { komgaClient } from '@/api/komga-client'
|
||||||
|
import type { components } from '@/generated/openapi/komga'
|
||||||
|
import { CLIENT_SETTING_USER, type ClientSettingUserLibrary } from '@/types/ClientSettingsUser'
|
||||||
|
|
||||||
|
export const QUERY_KEYS_CLIENT_SETTINGS = {
|
||||||
|
root: ['client-settings'] as const,
|
||||||
|
global: () => [...QUERY_KEYS_CLIENT_SETTINGS.root, 'global'] as const,
|
||||||
|
user: () => [...QUERY_KEYS_CLIENT_SETTINGS.root, 'user'] as const,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useClientSettingsUser = defineQuery(() => {
|
||||||
|
const { data, ...rest } = useQuery({
|
||||||
|
key: () => QUERY_KEYS_CLIENT_SETTINGS.user(),
|
||||||
|
query: () =>
|
||||||
|
komgaClient
|
||||||
|
.GET('/api/v1/client-settings/user/list')
|
||||||
|
// unwrap the openapi-fetch structure on success
|
||||||
|
.then((res) => res.data),
|
||||||
|
// 1 hour
|
||||||
|
staleTime: 60 * 60 * 1000,
|
||||||
|
gcTime: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const userLibraries = computed(() => {
|
||||||
|
const json = data.value?.[CLIENT_SETTING_USER.NEXTUI_LIBRARIES]?.value
|
||||||
|
if (json) return JSON.parse(json) as Record<string, ClientSettingUserLibrary>
|
||||||
|
return {}
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
...rest,
|
||||||
|
userLibraries,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export const useUpdateClientSettingsUser = defineMutation(() => {
|
||||||
|
const queryCache = useQueryCache()
|
||||||
|
return useMutation({
|
||||||
|
mutation: (settings: Record<string, components['schemas']['ClientSettingUserUpdateDto']>) =>
|
||||||
|
komgaClient.PATCH('/api/v1/client-settings/user', {
|
||||||
|
body: settings,
|
||||||
|
}),
|
||||||
|
onSuccess: () => {
|
||||||
|
void queryCache.invalidateQueries({ key: QUERY_KEYS_CLIENT_SETTINGS.user() })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
import { defineQuery, useQuery } from '@pinia/colada'
|
import { defineQuery, useQuery } from '@pinia/colada'
|
||||||
import { komgaClient } from '@/api/komga-client'
|
import { komgaClient } from '@/api/komga-client'
|
||||||
|
import { useClientSettingsUser } from '@/colada/client-settings'
|
||||||
|
|
||||||
export const useLibraries = defineQuery(() => {
|
export const useLibraries = defineQuery(() => {
|
||||||
return useQuery({
|
const { data, ...rest } = useQuery({
|
||||||
key: () => ['libraries'],
|
key: () => ['libraries'],
|
||||||
query: () =>
|
query: () =>
|
||||||
komgaClient
|
komgaClient
|
||||||
|
|
@ -13,4 +14,28 @@ export const useLibraries = defineQuery(() => {
|
||||||
staleTime: 60 * 60 * 1000,
|
staleTime: 60 * 60 * 1000,
|
||||||
gcTime: false,
|
gcTime: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const { userLibraries } = useClientSettingsUser()
|
||||||
|
|
||||||
|
const ordered = computed(() =>
|
||||||
|
data?.value?.sort(
|
||||||
|
(a, b) =>
|
||||||
|
(userLibraries.value?.[a.id]?.order || 0) - (userLibraries.value?.[b.id]?.order || 0),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
const unpinned = computed(
|
||||||
|
() => ordered.value?.filter((it) => userLibraries.value?.[it.id]?.unpinned) || [],
|
||||||
|
)
|
||||||
|
const pinned = computed(
|
||||||
|
() => ordered.value?.filter((it) => !userLibraries.value?.[it.id]?.unpinned) || [],
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
ordered,
|
||||||
|
unpinned,
|
||||||
|
pinned,
|
||||||
|
...rest,
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import {
|
||||||
import { komgaClient } from '@/api/komga-client'
|
import { komgaClient } from '@/api/komga-client'
|
||||||
import { UserRoles } from '@/types/UserRoles'
|
import { UserRoles } from '@/types/UserRoles'
|
||||||
import type { components } from '@/generated/openapi/komga'
|
import type { components } from '@/generated/openapi/komga'
|
||||||
|
import { QUERY_KEYS_CLIENT_SETTINGS } from '@/colada/client-settings'
|
||||||
|
|
||||||
export const QUERY_KEYS_USERS = {
|
export const QUERY_KEYS_USERS = {
|
||||||
root: ['users'] as const,
|
root: ['users'] as const,
|
||||||
|
|
@ -78,6 +79,8 @@ export const useLogin = defineMutation(() => {
|
||||||
onSuccess: ({ data }) => {
|
onSuccess: ({ data }) => {
|
||||||
queryCache.setQueryData(QUERY_KEYS_USERS.currentUser, data)
|
queryCache.setQueryData(QUERY_KEYS_USERS.currentUser, data)
|
||||||
queryCache.cancelQueries({ key: QUERY_KEYS_USERS.currentUser })
|
queryCache.cancelQueries({ key: QUERY_KEYS_USERS.currentUser })
|
||||||
|
|
||||||
|
void queryCache.invalidateQueries({ key: QUERY_KEYS_CLIENT_SETTINGS.root })
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
@ -88,6 +91,8 @@ export const useLogout = defineMutation(() => {
|
||||||
mutation: () => komgaClient.POST('/api/logout'),
|
mutation: () => komgaClient.POST('/api/logout'),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
void queryCache.invalidateQueries({ key: QUERY_KEYS_USERS.currentUser })
|
void queryCache.invalidateQueries({ key: QUERY_KEYS_USERS.currentUser })
|
||||||
|
|
||||||
|
void queryCache.invalidateQueries({ key: QUERY_KEYS_CLIENT_SETTINGS.root })
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
1
next-ui/src/components.d.ts
vendored
1
next-ui/src/components.d.ts
vendored
|
|
@ -49,6 +49,7 @@ declare module 'vue' {
|
||||||
LayoutAppDrawerMenuAccount: typeof import('./components/layout/app/drawer/menu/Account.vue')['default']
|
LayoutAppDrawerMenuAccount: typeof import('./components/layout/app/drawer/menu/Account.vue')['default']
|
||||||
LayoutAppDrawerMenuHistory: typeof import('./components/layout/app/drawer/menu/History.vue')['default']
|
LayoutAppDrawerMenuHistory: typeof import('./components/layout/app/drawer/menu/History.vue')['default']
|
||||||
LayoutAppDrawerMenuImport: typeof import('./components/layout/app/drawer/menu/Import.vue')['default']
|
LayoutAppDrawerMenuImport: typeof import('./components/layout/app/drawer/menu/Import.vue')['default']
|
||||||
|
LayoutAppDrawerMenuLibraries: typeof import('./components/layout/app/drawer/menu/Libraries.vue')['default']
|
||||||
LayoutAppDrawerMenuLogout: typeof import('./components/layout/app/drawer/menu/Logout.vue')['default']
|
LayoutAppDrawerMenuLogout: typeof import('./components/layout/app/drawer/menu/Logout.vue')['default']
|
||||||
LayoutAppDrawerMenuMedia: typeof import('./components/layout/app/drawer/menu/Media.vue')['default']
|
LayoutAppDrawerMenuMedia: typeof import('./components/layout/app/drawer/menu/Media.vue')['default']
|
||||||
LayoutAppDrawerMenuServer: typeof import('./components/layout/app/drawer/menu/Server.vue')['default']
|
LayoutAppDrawerMenuServer: typeof import('./components/layout/app/drawer/menu/Server.vue')['default']
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,102 @@
|
||||||
|
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||||
|
|
||||||
|
import Libraries from './Libraries.vue'
|
||||||
|
import { httpTyped } from '@/mocks/api/httpTyped'
|
||||||
|
import { userRegular } from '@/mocks/api/handlers/users'
|
||||||
|
import { expect, waitFor } from 'storybook/test'
|
||||||
|
import { CLIENT_SETTING_USER, type ClientSettingUserLibrary } from '@/types/ClientSettingsUser'
|
||||||
|
import type { components } from '@/generated/openapi/komga'
|
||||||
|
import { VList } from 'vuetify/components'
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
component: Libraries,
|
||||||
|
render: (args: object) => ({
|
||||||
|
components: { Libraries, VList },
|
||||||
|
setup() {
|
||||||
|
return { args }
|
||||||
|
},
|
||||||
|
template: '<v-list nav><Libraries /></v-list>',
|
||||||
|
}),
|
||||||
|
parameters: {
|
||||||
|
// More on how to position stories at: https://storybook.js.org/docs/configure/story-layout
|
||||||
|
},
|
||||||
|
args: {},
|
||||||
|
} satisfies Meta<typeof Libraries>
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
type Story = StoryObj<typeof meta>
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {},
|
||||||
|
play: async ({ canvas }) => {
|
||||||
|
await waitFor(() => expect(canvas.queryByLabelText(/add library/i)).not.toBeNull())
|
||||||
|
await waitFor(() => expect(canvas.queryByLabelText(/libraries menu/i)).not.toBeNull())
|
||||||
|
await waitFor(() => expect(canvas.queryAllByLabelText(/library menu/i)).not.toBeNull())
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NonAdmin: Story = {
|
||||||
|
parameters: {
|
||||||
|
msw: {
|
||||||
|
handlers: [
|
||||||
|
httpTyped.get('/api/v2/users/me', ({ response }) => response(200).json(userRegular)),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
play: async ({ canvas }) => {
|
||||||
|
await waitFor(() => expect(canvas.queryByLabelText(/add library/i)).toBeNull())
|
||||||
|
await waitFor(() => expect(canvas.queryByLabelText(/libraries menu/i)).not.toBeNull())
|
||||||
|
await waitFor(() => expect(canvas.queryAllByLabelText(/library menu/i)).toHaveLength(0))
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Unpinned: Story = {
|
||||||
|
parameters: {
|
||||||
|
msw: {
|
||||||
|
handlers: [
|
||||||
|
httpTyped.get('/api/v1/client-settings/user/list', ({ response }) => {
|
||||||
|
const userLibraries: Record<string, ClientSettingUserLibrary> = {
|
||||||
|
'2': {
|
||||||
|
unpinned: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const settings: Record<string, components['schemas']['ClientSettingUserUpdateDto']> = {
|
||||||
|
[CLIENT_SETTING_USER.NEXTUI_LIBRARIES]: {
|
||||||
|
value: JSON.stringify(userLibraries),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return response(200).json(settings)
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
play: async ({ canvas, userEvent }) => {
|
||||||
|
await waitFor(() => userEvent.click(canvas.getByText(/more/i)))
|
||||||
|
await waitFor(() => expect(canvas.queryByText(/comics/i)).toBeVisible())
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Ordered: Story = {
|
||||||
|
parameters: {
|
||||||
|
msw: {
|
||||||
|
handlers: [
|
||||||
|
httpTyped.get('/api/v1/client-settings/user/list', ({ response }) => {
|
||||||
|
const userLibraries: Record<string, ClientSettingUserLibrary> = {
|
||||||
|
'1': {
|
||||||
|
order: 2,
|
||||||
|
},
|
||||||
|
'2': {
|
||||||
|
order: 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const settings: Record<string, components['schemas']['ClientSettingUserUpdateDto']> = {
|
||||||
|
[CLIENT_SETTING_USER.NEXTUI_LIBRARIES]: {
|
||||||
|
value: JSON.stringify(userLibraries),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return response(200).json(settings)
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
105
next-ui/src/components/layout/app/drawer/menu/Libraries.vue
Normal file
105
next-ui/src/components/layout/app/drawer/menu/Libraries.vue
Normal file
|
|
@ -0,0 +1,105 @@
|
||||||
|
<template>
|
||||||
|
<v-list-item
|
||||||
|
:title="
|
||||||
|
$formatMessage({
|
||||||
|
description: 'Drawer menu for Libraries',
|
||||||
|
defaultMessage: 'Libraries',
|
||||||
|
id: 'eyYZUe',
|
||||||
|
})
|
||||||
|
"
|
||||||
|
prepend-icon="i-mdi:bookshelf"
|
||||||
|
>
|
||||||
|
<template #append>
|
||||||
|
<v-icon-btn
|
||||||
|
v-if="isAdmin"
|
||||||
|
icon="i-mdi:plus"
|
||||||
|
:aria-label="
|
||||||
|
$formatMessage({
|
||||||
|
description: 'Add library button: aria label',
|
||||||
|
defaultMessage: 'add library',
|
||||||
|
id: '90yqRq',
|
||||||
|
})
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<v-icon-btn
|
||||||
|
icon="i-mdi:dots-vertical"
|
||||||
|
:aria-label="
|
||||||
|
$formatMessage({
|
||||||
|
description: 'Libraries menu button: aria label',
|
||||||
|
defaultMessage: 'libraries menu',
|
||||||
|
id: 'hJEc5M',
|
||||||
|
})
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</v-list-item>
|
||||||
|
|
||||||
|
<v-list-item
|
||||||
|
v-for="library in pinned"
|
||||||
|
:key="library.id"
|
||||||
|
:title="library.name"
|
||||||
|
prepend-icon="blank"
|
||||||
|
>
|
||||||
|
<template #append>
|
||||||
|
<v-icon-btn
|
||||||
|
v-if="isAdmin"
|
||||||
|
icon="i-mdi:dots-vertical"
|
||||||
|
:aria-label="
|
||||||
|
$formatMessage({
|
||||||
|
description: 'Library menu button: aria label',
|
||||||
|
defaultMessage: 'library menu',
|
||||||
|
id: '3gimvl',
|
||||||
|
})
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</v-list-item>
|
||||||
|
|
||||||
|
<v-list-group
|
||||||
|
v-if="unpinned.length > 0"
|
||||||
|
value="Unpinned"
|
||||||
|
>
|
||||||
|
<template #activator="{ props }">
|
||||||
|
<v-list-item
|
||||||
|
v-bind="props"
|
||||||
|
prepend-icon="blank"
|
||||||
|
:title="
|
||||||
|
$formatMessage({
|
||||||
|
description: 'Drawer menu for Unpinned libraries',
|
||||||
|
defaultMessage: 'More',
|
||||||
|
id: 'XDV3Si',
|
||||||
|
})
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<v-list-item
|
||||||
|
v-for="library in unpinned"
|
||||||
|
:key="library.id"
|
||||||
|
:title="library.name"
|
||||||
|
prepend-icon="blank"
|
||||||
|
>
|
||||||
|
<template #append>
|
||||||
|
<v-icon-btn
|
||||||
|
v-if="isAdmin"
|
||||||
|
icon="i-mdi:dots-vertical"
|
||||||
|
:aria-label="
|
||||||
|
$formatMessage({
|
||||||
|
description: 'Library menu button: aria label',
|
||||||
|
defaultMessage: 'library menu',
|
||||||
|
id: '3gimvl',
|
||||||
|
})
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list-group>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useLibraries } from '@/colada/libraries'
|
||||||
|
import { useCurrentUser } from '@/colada/users'
|
||||||
|
|
||||||
|
const { unpinned, pinned } = useLibraries()
|
||||||
|
const { isAdmin } = useCurrentUser()
|
||||||
|
</script>
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<v-list nav>
|
<v-list nav>
|
||||||
|
<LayoutAppDrawerMenuLibraries />
|
||||||
<LayoutAppDrawerMenuImport v-if="isAdmin" />
|
<LayoutAppDrawerMenuImport v-if="isAdmin" />
|
||||||
<LayoutAppDrawerMenuMedia v-if="isAdmin" />
|
<LayoutAppDrawerMenuMedia v-if="isAdmin" />
|
||||||
<LayoutAppDrawerMenuHistory v-if="isAdmin" />
|
<LayoutAppDrawerMenuHistory v-if="isAdmin" />
|
||||||
|
|
|
||||||
|
|
@ -14,12 +14,14 @@ import { filesystemHandlers } from '@/mocks/api/handlers/filesystem'
|
||||||
import { transientBooksHandlers } from '@/mocks/api/handlers/transient-books'
|
import { transientBooksHandlers } from '@/mocks/api/handlers/transient-books'
|
||||||
import { readListsHandlers } from '@/mocks/api/handlers/readlists'
|
import { readListsHandlers } from '@/mocks/api/handlers/readlists'
|
||||||
import { pageHashesHandlers } from '@/mocks/api/handlers/page-hashes'
|
import { pageHashesHandlers } from '@/mocks/api/handlers/page-hashes'
|
||||||
|
import { clientSettingsHandlers } from '@/mocks/api/handlers/client-settings'
|
||||||
|
|
||||||
export const handlers = [
|
export const handlers = [
|
||||||
...actuatorHandlers,
|
...actuatorHandlers,
|
||||||
...announcementHandlers,
|
...announcementHandlers,
|
||||||
...booksHandlers,
|
...booksHandlers,
|
||||||
...claimHandlers,
|
...claimHandlers,
|
||||||
|
...clientSettingsHandlers,
|
||||||
...filesystemHandlers,
|
...filesystemHandlers,
|
||||||
...historyHandlers,
|
...historyHandlers,
|
||||||
...librariesHandlers,
|
...librariesHandlers,
|
||||||
|
|
|
||||||
16
next-ui/src/mocks/api/handlers/client-settings.ts
Normal file
16
next-ui/src/mocks/api/handlers/client-settings.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { httpTyped } from '@/mocks/api/httpTyped'
|
||||||
|
import { CLIENT_SETTING_USER } from '@/types/ClientSettingsUser'
|
||||||
|
import type { components } from '@/generated/openapi/komga'
|
||||||
|
|
||||||
|
const settings: Record<string, components['schemas']['ClientSettingUserUpdateDto']> = {
|
||||||
|
[CLIENT_SETTING_USER.NEXTUI_LIBRARIES]: {
|
||||||
|
value: JSON.stringify({}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const clientSettingsHandlers = [
|
||||||
|
httpTyped.get('/api/v1/client-settings/user/list', ({ response }) =>
|
||||||
|
response(200).json(settings),
|
||||||
|
),
|
||||||
|
httpTyped.patch('/api/v1/client-settings/user', ({ response }) => response(204).empty()),
|
||||||
|
]
|
||||||
8
next-ui/src/types/ClientSettingsUser.ts
Normal file
8
next-ui/src/types/ClientSettingsUser.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
export enum CLIENT_SETTING_USER {
|
||||||
|
NEXTUI_LIBRARIES = 'komga.nextui.libraries',
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ClientSettingUserLibrary = {
|
||||||
|
unpinned?: boolean
|
||||||
|
order?: number
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue