refactor components into fragments

This commit is contained in:
Gauthier Roebroeck 2025-06-26 11:31:59 +08:00
parent 86e1d06280
commit 83e7c6fac3
33 changed files with 108 additions and 60 deletions

View file

@ -10,7 +10,7 @@ The `openapi-generate` tasks will generate bindings, and should be run when the
## Tests
We use Vitest projects to separate different kind of tests:
Vitest projects are used to specify different kind of tests:
- `unit`: unit tests
- `storybook`: component tests, defined in Storybook stories. Those can be run from Storybook directly or through Vitest.
@ -43,3 +43,17 @@ Tasks:
- `i18n:compile`: compiles the translated files in `i18n` into `./src/i18n`. This folder is what the application uses at runtime.
The Vite plugin [dir2json](https://github.com/buddywang/vite-plugin-dir2json) is used to load the available translation files, see `./src/utils/locale-helper.ts` for more details.
## Components
Vue template files are segregated in different categories depending on usage:
- `./src/components`: Pure UI components, driven by model/props. Those are reusable components.
- `./src/fragments`: Fragments interact with other layers of the application, like API or Pinia stores. They are split into separate files for easier organization, but are not necessarily reused.
- `./src/pages`: Pages make use of components/fragments as well as API / Pinia stores. Each Component in that folder is converted to a navigable route using [unplugin-vue-router](https://github.com/posva/unplugin-vue-router). Pages contain a special `<route>` to define the layout to use as well as other router meta attributes.
- `./src/layouts`: Wrapper component around Pages.
Components and Fragments are automatically imported using [unplugin-vue-components](https://github.com/unplugin/unplugin-vue-components).
## Icons
[UnoCSS Icons preset](https://unocss.dev/presets/icons) is used for icons, with the MDI set from Iconify.

View file

@ -2,9 +2,9 @@
<v-app>
<router-view />
<SnackQueue />
<DialogInstanceConfirmEdit />
<DialogInstanceConfirm />
<FragmentSnackQueue />
<FragmentDialogInstanceConfirmEdit />
<FragmentDialogInstanceConfirm />
</v-app>
</template>

View file

@ -8,31 +8,31 @@ export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
AppBar: typeof import('./components/app/Bar.vue')['default']
AppDrawer: typeof import('./components/app/drawer/Drawer.vue')['default']
AppDrawerFooter: typeof import('./components/app/drawer/Footer.vue')['default']
AppDrawerMenu: typeof import('./components/app/drawer/menu/Menu.vue')['default']
AppDrawerMenuAccount: typeof import('./components/app/drawer/menu/Account.vue')['default']
AppDrawerMenuHistory: typeof import('./components/app/drawer/menu/History.vue')['default']
AppDrawerMenuImport: typeof import('./components/app/drawer/menu/Import.vue')['default']
AppDrawerMenuLogout: typeof import('./components/app/drawer/menu/Logout.vue')['default']
AppDrawerMenuMedia: typeof import('./components/app/drawer/menu/Media.vue')['default']
AppDrawerMenuServer: typeof import('./components/app/drawer/menu/Server.vue')['default']
AppFooter: typeof import('./components/AppFooter.vue')['default']
BuildCommit: typeof import('./components/BuildCommit.vue')['default']
BuildVersion: typeof import('./components/BuildVersion.vue')['default']
DialogConfirm: typeof import('./components/dialog/Confirm.vue')['default']
DialogConfirmEdit: typeof import('./components/dialog/ConfirmEdit.vue')['default']
DialogInstanceConfirm: typeof import('./components/dialog/instance/Confirm.vue')['default']
DialogInstanceConfirmEdit: typeof import('./components/dialog/instance/ConfirmEdit.vue')['default']
FormUserChangePassword: typeof import('./components/form/user/ChangePassword.vue')['default']
FormUserEdit: typeof import('./components/form/user/Edit.vue')['default']
FragmentBuildCommit: typeof import('./fragments/fragment/BuildCommit.vue')['default']
FragmentBuildVersion: typeof import('./fragments/fragment/BuildVersion.vue')['default']
FragmentDialogConfirm: typeof import('./fragments/fragment/dialog/Confirm.vue')['default']
FragmentDialogConfirmEdit: typeof import('./fragments/fragment/dialog/ConfirmEdit.vue')['default']
FragmentLocaleSelector: typeof import('./fragments/fragment/LocaleSelector.vue')['default']
FragmentSnackQueue: typeof import('./fragments/fragment/SnackQueue.vue')['default']
FragmentThemeSelector: typeof import('./fragments/fragment/ThemeSelector.vue')['default']
HelloWorld: typeof import('./components/HelloWorld.vue')['default']
LocaleSelector: typeof import('./components/LocaleSelector.vue')['default']
NoticeUserDeletion: typeof import('./components/notice/UserDeletion.vue')['default']
LayoutAppBar: typeof import('./fragments/layout/app/Bar.vue')['default']
LayoutAppDrawer: typeof import('./fragments/layout/app/drawer/Drawer.vue')['default']
LayoutAppDrawerFooter: typeof import('./fragments/layout/app/drawer/Footer.vue')['default']
LayoutAppDrawerMenu: typeof import('./fragments/layout/app/drawer/menu/Menu.vue')['default']
LayoutAppDrawerMenuAccount: typeof import('./fragments/layout/app/drawer/menu/Account.vue')['default']
LayoutAppDrawerMenuHistory: typeof import('./fragments/layout/app/drawer/menu/History.vue')['default']
LayoutAppDrawerMenuImport: typeof import('./fragments/layout/app/drawer/menu/Import.vue')['default']
LayoutAppDrawerMenuLogout: typeof import('./fragments/layout/app/drawer/menu/Logout.vue')['default']
LayoutAppDrawerMenuMedia: typeof import('./fragments/layout/app/drawer/menu/Media.vue')['default']
LayoutAppDrawerMenuServer: typeof import('./fragments/layout/app/drawer/menu/Server.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
SnackQueue: typeof import('./components/SnackQueue.vue')['default']
ThemeSelector: typeof import('./components/ThemeSelector.vue')['default']
UserDeletionWarning: typeof import('./components/user/DeletionWarning.vue')['default']
UserFormChangePassword: typeof import('./components/user/form/ChangePassword.vue')['default']
UserFormCreateEdit: typeof import('./components/user/form/CreateEdit.vue')['default']
}
}

View file

@ -1,16 +0,0 @@
<template>
<v-list nav>
<AppDrawerMenuImport v-if="isAdmin" />
<AppDrawerMenuMedia v-if="isAdmin" />
<AppDrawerMenuHistory v-if="isAdmin" />
<AppDrawerMenuServer v-if="isAdmin" />
<AppDrawerMenuAccount />
<AppDrawerMenuLogout />
</v-list>
</template>
<script setup lang="ts">
import { useCurrentUser } from '@/colada/queries/current-user'
const { isAdmin } = useCurrentUser()
</script>

View file

@ -1 +0,0 @@
Simple forms that can be wrapped by a `v-form`, or used within a `DialogEditConfirm`.

View file

@ -1 +0,0 @@
Components that can be used within a `DialogConfirm`.

View file

@ -0,0 +1,35 @@
# Fragments
Vue template files in this folder are automatically imported.
## 🚀 Usage
Importing is handled by [unplugin-vue-components](https://github.com/unplugin/unplugin-vue-components). This plugin automatically imports `.vue` files created in the `src/components` directory, and registers them as global components. This means that you can use any component in your application without having to manually import it.
The following example assumes a component located at `src/components/MyComponent.vue`:
```vue
<template>
<div>
<MyComponent />
</div>
</template>
<script lang="ts" setup>
//
</script>
```
When your template is rendered, the component's import will automatically be inlined, which renders to this:
```vue
<template>
<div>
<MyComponent />
</div>
</template>
<script lang="ts" setup>
import MyComponent from '@/components/MyComponent.vue'
</script>
```

View file

@ -11,8 +11,8 @@
</RouterLink>
Komga
</v-app-bar-title>
<LocaleSelector />
<ThemeSelector />
<FragmentLocaleSelector />
<FragmentThemeSelector />
</v-app-bar>
</template>

View file

@ -1,9 +1,9 @@
<template>
<v-navigation-drawer v-model="appStore.drawer">
<AppDrawerMenu />
<LayoutAppDrawerMenu />
<template #append>
<AppDrawerFooter />
<LayoutAppDrawerFooter />
</template>
</v-navigation-drawer>
</template>

View file

@ -25,8 +25,8 @@
class="d-flex align-center text-caption text-medium-emphasis pa-2"
>
<div class="d-flex ms-auto">
<BuildCommit class="me-2" />
<BuildVersion />
<FragmentBuildCommit class="me-2" />
<FragmentBuildVersion />
</div>
</div>

View file

@ -0,0 +1,16 @@
<template>
<v-list nav>
<LayoutAppDrawerMenuImport v-if="isAdmin" />
<LayoutAppDrawerMenuMedia v-if="isAdmin" />
<LayoutAppDrawerMenuHistory v-if="isAdmin" />
<LayoutAppDrawerMenuServer v-if="isAdmin" />
<LayoutAppDrawerMenuAccount />
<LayoutAppDrawerMenuLogout />
</v-list>
</template>
<script setup lang="ts">
import { useCurrentUser } from '@/colada/queries/current-user'
const { isAdmin } = useCurrentUser()
</script>

View file

@ -1,7 +1,7 @@
<template>
<AppBar />
<LayoutAppBar />
<AppDrawer />
<LayoutAppDrawer />
<v-main scrollable>
<v-container

View file

@ -104,8 +104,8 @@
<v-row justify="center">
<v-col cols="auto">
<div class="d-flex ga-4">
<LocaleSelector />
<ThemeSelector />
<FragmentLocaleSelector />
<FragmentThemeSelector />
</div>
</v-col>
</v-row>

View file

@ -91,9 +91,6 @@ import {
useUpdateUser,
useUpdateUserPassword,
} from '@/colada/mutations/update-user'
import FormUserChangePassword from '@/components/form/user/ChangePassword.vue'
import FormUserEdit from '@/components/form/user/Edit.vue'
import NoticeUserDeletion from '@/components/notice/UserDeletion.vue'
import { useLibraries } from '@/colada/queries/libraries'
import { commonMessages } from '@/utils/i18n/common-messages'
import { storeToRefs } from 'pinia'
@ -101,6 +98,9 @@ import { useDialogsStore } from '@/stores/dialogs'
import { useMessagesStore } from '@/stores/messages'
import { useIntl } from 'vue-intl'
import { useDisplay } from 'vuetify'
import UserDeletionWarning from '@/components/user/DeletionWarning.vue'
import UserFormCreateEdit from '@/components/user/form/CreateEdit.vue'
import UserFormChangePassword from '@/components/user/form/ChangePassword.vue'
const intl = useIntl()
@ -185,7 +185,7 @@ function showDialog(action: ACTION, user?: components['schemas']['UserDto']) {
fullscreen: display.xs.value,
}
dialogConfirmEdit.value.slot = {
component: markRaw(FormUserEdit),
component: markRaw(UserFormCreateEdit),
props: {},
}
dialogConfirmEdit.value.record = {
@ -213,7 +213,7 @@ function showDialog(action: ACTION, user?: components['schemas']['UserDto']) {
fullscreen: display.xs.value,
}
dialogConfirmEdit.value.slot = {
component: markRaw(FormUserEdit),
component: markRaw(UserFormCreateEdit),
props: {},
}
dialogConfirmEdit.value.record = {
@ -243,7 +243,7 @@ function showDialog(action: ACTION, user?: components['schemas']['UserDto']) {
closeOnSave: false,
}
dialogConfirm.value.slotWarning = {
component: markRaw(NoticeUserDeletion),
component: markRaw(UserDeletionWarning),
props: {},
}
dialogConfirm.value.callback = handleDialogConfirmation
@ -256,7 +256,7 @@ function showDialog(action: ACTION, user?: components['schemas']['UserDto']) {
closeOnSave: false,
}
dialogConfirmEdit.value.slot = {
component: markRaw(FormUserChangePassword),
component: markRaw(UserFormChangePassword),
props: {},
}
// password change initiated with an empty string

View file

@ -38,7 +38,8 @@ export default defineConfig({
}),
Components({
dts: 'src/components.d.ts',
globsExclude: ['src/components/*.stories.vue'],
dirs: ['src/components', 'src/fragments'],
globsExclude: ['src/**/*.stories.vue'],
directoryAsNamespace: true,
collapseSamePrefixes: true,
}),