mirror of
https://github.com/gotson/komga.git
synced 2026-05-08 12:35:30 +02:00
reorder libraries component
This commit is contained in:
parent
2597df1ede
commit
ebd9ee3af6
8 changed files with 306 additions and 2 deletions
|
|
@ -55,7 +55,7 @@ export default defineConfigWithVueTs(
|
|||
'PascalCase',
|
||||
{
|
||||
registeredComponentsOnly: false,
|
||||
ignores: ['/^v-/'],
|
||||
ignores: ['/^v-/', 'draggable'],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
|||
19
next-ui/package-lock.json
generated
19
next-ui/package-lock.json
generated
|
|
@ -21,6 +21,7 @@
|
|||
"pinia-plugin-persistedstate": "^4.7.1",
|
||||
"vue": "^3.5.25",
|
||||
"vue-intl": "^6.5.27",
|
||||
"vuedraggable": "^4.1.0",
|
||||
"vuetify": "^3.11.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
@ -10113,6 +10114,12 @@
|
|||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/sortablejs": {
|
||||
"version": "1.14.0",
|
||||
"resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.14.0.tgz",
|
||||
"integrity": "sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
|
|
@ -11708,6 +11715,18 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/vuedraggable": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/vuedraggable/-/vuedraggable-4.1.0.tgz",
|
||||
"integrity": "sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"sortablejs": "1.14.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^3.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/vuetify": {
|
||||
"version": "3.11.2",
|
||||
"resolved": "https://registry.npmjs.org/vuetify/-/vuetify-3.11.2.tgz",
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@
|
|||
"pinia-plugin-persistedstate": "^4.7.1",
|
||||
"vue": "^3.5.25",
|
||||
"vue-intl": "^6.5.27",
|
||||
"vuedraggable": "^4.1.0",
|
||||
"vuetify": "^3.11.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
|||
|
|
@ -38,7 +38,9 @@ export const useClientSettingsUser = defineQuery(() => {
|
|||
export const useUpdateClientSettingsUser = defineMutation(() => {
|
||||
const queryCache = useQueryCache()
|
||||
return useMutation({
|
||||
mutation: (settings: Record<string, components['schemas']['ClientSettingUserUpdateDto']>) =>
|
||||
mutation: (
|
||||
settings: Record<CLIENT_SETTING_USER, components['schemas']['ClientSettingUserUpdateDto']>,
|
||||
) =>
|
||||
komgaClient.PATCH('/api/v1/client-settings/user', {
|
||||
body: settings,
|
||||
}),
|
||||
|
|
|
|||
1
next-ui/src/components.d.ts
vendored
1
next-ui/src/components.d.ts
vendored
|
|
@ -53,6 +53,7 @@ declare module 'vue' {
|
|||
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']
|
||||
LayoutAppDrawerReorderLibraries: typeof import('./components/layout/app/drawer/ReorderLibraries.vue')['default']
|
||||
LocaleSelector: typeof import('./components/LocaleSelector.vue')['default']
|
||||
PageHashKnownTable: typeof import('./components/pageHash/KnownTable.vue')['default']
|
||||
PageHashMatchTable: typeof import('./components/pageHash/MatchTable.vue')['default']
|
||||
|
|
|
|||
30
next-ui/src/components/layout/app/drawer/Drawer.stories.ts
Normal file
30
next-ui/src/components/layout/app/drawer/Drawer.stories.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import Drawer from './Drawer.vue'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
|
||||
const meta = {
|
||||
component: Drawer,
|
||||
render: (args: object) => ({
|
||||
components: { Drawer },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
template: '<Drawer />',
|
||||
}),
|
||||
parameters: {
|
||||
// More on how to position stories at: https://storybook.js.org/docs/configure/story-layout
|
||||
},
|
||||
args: {},
|
||||
} satisfies Meta<typeof Drawer>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {},
|
||||
play: () => {
|
||||
const appStore = useAppStore()
|
||||
appStore.drawer = true
|
||||
},
|
||||
}
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import ReorderLibraries from './ReorderLibraries.vue'
|
||||
import { httpTyped } from '@/mocks/api/httpTyped'
|
||||
import { CLIENT_SETTING_USER, type ClientSettingUserLibrary } from '@/types/ClientSettingsUser'
|
||||
import type { components } from '@/generated/openapi/komga'
|
||||
import { libraries } from '@/mocks/api/handlers/libraries'
|
||||
|
||||
const meta = {
|
||||
component: ReorderLibraries,
|
||||
render: (args: object) => ({
|
||||
components: { ReorderLibraries },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
template: '<ReorderLibraries />',
|
||||
}),
|
||||
parameters: {
|
||||
// More on how to position stories at: https://storybook.js.org/docs/configure/story-layout
|
||||
},
|
||||
args: {},
|
||||
} satisfies Meta<typeof ReorderLibraries>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
const libHandler = httpTyped.get('/api/v1/libraries', ({ response }) => {
|
||||
const bds = {
|
||||
...libraries[0],
|
||||
id: '3',
|
||||
name: 'BDs',
|
||||
} as components['schemas']['LibraryDto']
|
||||
const magazines = {
|
||||
...libraries[0],
|
||||
id: '4',
|
||||
name: 'Magazines',
|
||||
} as components['schemas']['LibraryDto']
|
||||
const manga = {
|
||||
...libraries[0],
|
||||
id: '5',
|
||||
name: 'Mangas',
|
||||
} as components['schemas']['LibraryDto']
|
||||
const libs = [...libraries, bds, magazines, manga]
|
||||
return response(200).json(libs)
|
||||
})
|
||||
|
||||
export const AllPinned: Story = {
|
||||
args: {},
|
||||
parameters: {
|
||||
msw: {
|
||||
handlers: [libHandler],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const SomeUnpinned: Story = {
|
||||
args: {},
|
||||
parameters: {
|
||||
msw: {
|
||||
handlers: [
|
||||
libHandler,
|
||||
httpTyped.get('/api/v1/client-settings/user/list', ({ response }) => {
|
||||
const userLibraries: Record<string, ClientSettingUserLibrary> = {
|
||||
'2': {
|
||||
unpinned: true,
|
||||
},
|
||||
'4': {
|
||||
unpinned: true,
|
||||
},
|
||||
}
|
||||
const settings: Record<string, components['schemas']['ClientSettingUserUpdateDto']> = {
|
||||
[CLIENT_SETTING_USER.NEXTUI_LIBRARIES]: {
|
||||
value: JSON.stringify(userLibraries),
|
||||
},
|
||||
}
|
||||
return response(200).json(settings)
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const AllUnpinned: Story = {
|
||||
args: {},
|
||||
parameters: {
|
||||
msw: {
|
||||
handlers: [
|
||||
httpTyped.get('/api/v1/client-settings/user/list', ({ response }) => {
|
||||
const userLibraries: Record<string, ClientSettingUserLibrary> = {
|
||||
'1': {
|
||||
unpinned: true,
|
||||
},
|
||||
'2': {
|
||||
unpinned: true,
|
||||
},
|
||||
}
|
||||
const settings: Record<string, components['schemas']['ClientSettingUserUpdateDto']> = {
|
||||
[CLIENT_SETTING_USER.NEXTUI_LIBRARIES]: {
|
||||
value: JSON.stringify(userLibraries),
|
||||
},
|
||||
}
|
||||
return response(200).json(settings)
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
144
next-ui/src/components/layout/app/drawer/ReorderLibraries.vue
Normal file
144
next-ui/src/components/layout/app/drawer/ReorderLibraries.vue
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
<template>
|
||||
<v-list>
|
||||
<v-list-item variant="tonal">
|
||||
<v-list-item-title class="text-uppercase">{{
|
||||
$formatMessage({
|
||||
description: 'Reorder library drawer: title',
|
||||
defaultMessage: 'reorder',
|
||||
id: 'g+OQSw',
|
||||
})
|
||||
}}</v-list-item-title>
|
||||
<template #append>
|
||||
<v-icon-btn
|
||||
icon="i-mdi:close"
|
||||
variant="text"
|
||||
:aria-label="
|
||||
$formatMessage({
|
||||
description: 'Reorder library drawer: aria label',
|
||||
defaultMessage: 'close',
|
||||
id: 'Ept33T',
|
||||
})
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-subheader
|
||||
:title="
|
||||
$formatMessage({
|
||||
description: 'Reorder library drawer: pinned section header',
|
||||
defaultMessage: 'Pinned',
|
||||
id: 'OZSDCE',
|
||||
})
|
||||
"
|
||||
/>
|
||||
|
||||
<draggable
|
||||
v-model="localPinned"
|
||||
v-bind="draggableConfig"
|
||||
>
|
||||
<template #[`header`]>
|
||||
<v-list-item
|
||||
v-if="localPinned.length == 0"
|
||||
:title="
|
||||
$formatMessage({
|
||||
description: 'Reorder library drawer: placeholder if no libraries are pinned',
|
||||
defaultMessage: 'Drag here to pin a library',
|
||||
id: '0MePSx',
|
||||
})
|
||||
"
|
||||
class="text-grey"
|
||||
/>
|
||||
</template>
|
||||
<template #[`item`]="{ element: library }">
|
||||
<v-list-item
|
||||
:title="library.name"
|
||||
prepend-icon="i-mdi:drag-horizontal"
|
||||
class="cursor-grab"
|
||||
/>
|
||||
</template>
|
||||
</draggable>
|
||||
|
||||
<v-divider />
|
||||
|
||||
<v-list-subheader
|
||||
:title="
|
||||
$formatMessage({
|
||||
description: 'Reorder library drawer: unpinned section header',
|
||||
defaultMessage: 'Unpinned',
|
||||
id: 'sj2JGj',
|
||||
})
|
||||
"
|
||||
/>
|
||||
|
||||
<draggable
|
||||
v-model="localUnpinned"
|
||||
v-bind="draggableConfig"
|
||||
>
|
||||
<template #[`header`]>
|
||||
<v-list-item
|
||||
v-if="localUnpinned.length == 0"
|
||||
:title="
|
||||
$formatMessage({
|
||||
description: 'Reorder library drawer: placeholder if no libraries are unpinned',
|
||||
defaultMessage: 'Drag here to unpin a library',
|
||||
id: 'H+LXXE',
|
||||
})
|
||||
"
|
||||
class="text-grey"
|
||||
/>
|
||||
</template>
|
||||
<template #[`item`]="{ element: library }">
|
||||
<v-list-item
|
||||
:title="library.name"
|
||||
prepend-icon="i-mdi:drag-horizontal"
|
||||
class="cursor-grab"
|
||||
/>
|
||||
</template>
|
||||
</draggable>
|
||||
</v-list>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import draggable from 'vuedraggable'
|
||||
import { useLibraries } from '@/colada/libraries'
|
||||
import type { components } from '@/generated/openapi/komga'
|
||||
import { CLIENT_SETTING_USER, type ClientSettingUserLibrary } from '@/types/ClientSettingsUser'
|
||||
import { useUpdateClientSettingsUser } from '@/colada/client-settings'
|
||||
|
||||
const { unpinned, pinned, refresh } = useLibraries()
|
||||
const localPinned = ref<components['schemas']['LibraryDto'][]>([])
|
||||
const localUnpinned = ref<components['schemas']['LibraryDto'][]>([])
|
||||
|
||||
// one time copy to local refs
|
||||
void refresh().then(() => {
|
||||
localPinned.value = pinned.value
|
||||
localUnpinned.value = unpinned.value
|
||||
})
|
||||
|
||||
const draggableConfig = {
|
||||
group: 'libs',
|
||||
itemKey: 'id',
|
||||
ghostClass: 'ghost',
|
||||
chosenClass: 'chosen',
|
||||
animation: 150,
|
||||
}
|
||||
|
||||
const { mutate } = useUpdateClientSettingsUser()
|
||||
|
||||
watch([localPinned, localUnpinned], ([newPinned, newUnpinned]) => {
|
||||
const newSettings: Record<string, ClientSettingUserLibrary> = {}
|
||||
newPinned.forEach((it, index) => (newSettings[it.id] = { order: index, unpinned: false }))
|
||||
newUnpinned.forEach(
|
||||
(it, index) => (newSettings[it.id] = { order: newPinned.length + index, unpinned: true }),
|
||||
)
|
||||
mutate({ [CLIENT_SETTING_USER.NEXTUI_LIBRARIES]: { value: JSON.stringify(newSettings) } })
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.ghost {
|
||||
opacity: 0.5;
|
||||
background: #c8ebfb;
|
||||
}
|
||||
</style>
|
||||
Loading…
Reference in a new issue