diff --git a/next-ui/src/colada/client-settings.ts b/next-ui/src/colada/client-settings.ts new file mode 100644 index 000000000..1c22d0c94 --- /dev/null +++ b/next-ui/src/colada/client-settings.ts @@ -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 + return {} + }) + + return { + data, + ...rest, + userLibraries, + } +}) + +export const useUpdateClientSettingsUser = defineMutation(() => { + const queryCache = useQueryCache() + return useMutation({ + mutation: (settings: Record) => + komgaClient.PATCH('/api/v1/client-settings/user', { + body: settings, + }), + onSuccess: () => { + void queryCache.invalidateQueries({ key: QUERY_KEYS_CLIENT_SETTINGS.user() }) + }, + }) +}) diff --git a/next-ui/src/colada/libraries.ts b/next-ui/src/colada/libraries.ts index 71833ffdb..4cb542f24 100644 --- a/next-ui/src/colada/libraries.ts +++ b/next-ui/src/colada/libraries.ts @@ -1,8 +1,9 @@ import { defineQuery, useQuery } from '@pinia/colada' import { komgaClient } from '@/api/komga-client' +import { useClientSettingsUser } from '@/colada/client-settings' export const useLibraries = defineQuery(() => { - return useQuery({ + const { data, ...rest } = useQuery({ key: () => ['libraries'], query: () => komgaClient @@ -13,4 +14,28 @@ export const useLibraries = defineQuery(() => { staleTime: 60 * 60 * 1000, 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, + } }) diff --git a/next-ui/src/colada/users.ts b/next-ui/src/colada/users.ts index 0422e75f7..67a8cea17 100644 --- a/next-ui/src/colada/users.ts +++ b/next-ui/src/colada/users.ts @@ -9,6 +9,7 @@ import { import { komgaClient } from '@/api/komga-client' import { UserRoles } from '@/types/UserRoles' import type { components } from '@/generated/openapi/komga' +import { QUERY_KEYS_CLIENT_SETTINGS } from '@/colada/client-settings' export const QUERY_KEYS_USERS = { root: ['users'] as const, @@ -78,6 +79,8 @@ export const useLogin = defineMutation(() => { onSuccess: ({ data }) => { queryCache.setQueryData(QUERY_KEYS_USERS.currentUser, data) 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'), onSuccess: () => { void queryCache.invalidateQueries({ key: QUERY_KEYS_USERS.currentUser }) + + void queryCache.invalidateQueries({ key: QUERY_KEYS_CLIENT_SETTINGS.root }) }, }) }) diff --git a/next-ui/src/components.d.ts b/next-ui/src/components.d.ts index c8d1797bb..8d47ddd57 100644 --- a/next-ui/src/components.d.ts +++ b/next-ui/src/components.d.ts @@ -49,6 +49,7 @@ declare module 'vue' { LayoutAppDrawerMenuAccount: typeof import('./components/layout/app/drawer/menu/Account.vue')['default'] LayoutAppDrawerMenuHistory: typeof import('./components/layout/app/drawer/menu/History.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'] LayoutAppDrawerMenuMedia: typeof import('./components/layout/app/drawer/menu/Media.vue')['default'] LayoutAppDrawerMenuServer: typeof import('./components/layout/app/drawer/menu/Server.vue')['default'] diff --git a/next-ui/src/components/layout/app/drawer/menu/Libraries.stories.ts b/next-ui/src/components/layout/app/drawer/menu/Libraries.stories.ts new file mode 100644 index 000000000..353134fd4 --- /dev/null +++ b/next-ui/src/components/layout/app/drawer/menu/Libraries.stories.ts @@ -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: '', + }), + 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: {}, + 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 = { + '2': { + unpinned: true, + }, + } + const settings: Record = { + [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 = { + '1': { + order: 2, + }, + '2': { + order: 1, + }, + } + const settings: Record = { + [CLIENT_SETTING_USER.NEXTUI_LIBRARIES]: { + value: JSON.stringify(userLibraries), + }, + } + return response(200).json(settings) + }), + ], + }, + }, +} diff --git a/next-ui/src/components/layout/app/drawer/menu/Libraries.vue b/next-ui/src/components/layout/app/drawer/menu/Libraries.vue new file mode 100644 index 000000000..f8926e65f --- /dev/null +++ b/next-ui/src/components/layout/app/drawer/menu/Libraries.vue @@ -0,0 +1,105 @@ + + + diff --git a/next-ui/src/components/layout/app/drawer/menu/Menu.vue b/next-ui/src/components/layout/app/drawer/menu/Menu.vue index 72ea89562..aab7e5808 100644 --- a/next-ui/src/components/layout/app/drawer/menu/Menu.vue +++ b/next-ui/src/components/layout/app/drawer/menu/Menu.vue @@ -1,5 +1,6 @@