mirror of
https://github.com/gotson/komga.git
synced 2026-04-19 05:20:54 +02:00
add storybook
This commit is contained in:
parent
143637dda5
commit
336a2fb87f
28 changed files with 2679 additions and 45 deletions
3
next-ui/.gitignore
vendored
3
next-ui/.gitignore
vendored
|
|
@ -24,3 +24,6 @@ pnpm-debug.log*
|
|||
# FormatJS compiled translation files
|
||||
!/src/i18n/README.md
|
||||
src/i18n
|
||||
|
||||
*storybook.log
|
||||
storybook-static
|
||||
|
|
|
|||
17
next-ui/.storybook/StoryWrapper.vue
Normal file
17
next-ui/.storybook/StoryWrapper.vue
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<template>
|
||||
<v-theme-provider
|
||||
with-background
|
||||
:theme="themeName"
|
||||
class="pa-2"
|
||||
>
|
||||
<slot name="story"></slot>
|
||||
</v-theme-provider>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
props: {
|
||||
themeName: String,
|
||||
},
|
||||
}
|
||||
</script>
|
||||
19
next-ui/.storybook/main.ts
Normal file
19
next-ui/.storybook/main.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import type { StorybookConfig } from '@storybook/vue3-vite'
|
||||
|
||||
const config: StorybookConfig = {
|
||||
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
|
||||
addons: [
|
||||
'@chromatic-com/storybook',
|
||||
'@storybook/addon-docs',
|
||||
'@storybook/addon-onboarding',
|
||||
'@storybook/addon-a11y',
|
||||
'@storybook/addon-vitest',
|
||||
'@storybook/addon-themes',
|
||||
],
|
||||
framework: {
|
||||
name: '@storybook/vue3-vite',
|
||||
options: {},
|
||||
},
|
||||
staticDirs: ['../public-msw'],
|
||||
}
|
||||
export default config
|
||||
6
next-ui/.storybook/manager.ts
Normal file
6
next-ui/.storybook/manager.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { addons } from 'storybook/manager-api'
|
||||
import theme from './theme'
|
||||
|
||||
addons.setConfig({
|
||||
theme: theme,
|
||||
})
|
||||
62
next-ui/.storybook/preview.ts
Normal file
62
next-ui/.storybook/preview.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import type { Preview } from '@storybook/vue3-vite'
|
||||
import { setup } from '@storybook/vue3'
|
||||
import { withVuetifyTheme } from './withVuetifyTheme.decorator'
|
||||
|
||||
import { initialize, mswLoader } from 'msw-storybook-addon'
|
||||
import { handlers } from '@/mocks/api/handlers'
|
||||
import { vuetify, vuetifyRulesPlugin } from '@/plugins/vuetify'
|
||||
import { createPinia } from 'pinia'
|
||||
import { PiniaColada } from '@pinia/colada'
|
||||
import { PiniaColadaAutoRefetch } from '@pinia/colada-plugin-auto-refetch'
|
||||
import { vueIntl } from '@/plugins/vue-intl'
|
||||
|
||||
initialize(
|
||||
{
|
||||
onUnhandledRequest: 'bypass',
|
||||
},
|
||||
handlers,
|
||||
)
|
||||
|
||||
const preview: Preview = {
|
||||
parameters: {
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/i,
|
||||
},
|
||||
},
|
||||
|
||||
a11y: {
|
||||
// 'todo' - show a11y violations in the test UI only
|
||||
// 'error' - fail CI on a11y violations
|
||||
// 'off' - skip a11y checks entirely
|
||||
test: 'todo',
|
||||
},
|
||||
},
|
||||
loaders: [mswLoader],
|
||||
}
|
||||
|
||||
export default preview
|
||||
|
||||
setup((app) => {
|
||||
// Registers your app's plugins into Storybook
|
||||
app.use(vuetify)
|
||||
app.use(vuetifyRulesPlugin)
|
||||
app.use(vueIntl)
|
||||
app.use(createPinia())
|
||||
app.use(PiniaColada, {
|
||||
plugins: [PiniaColadaAutoRefetch()],
|
||||
})
|
||||
})
|
||||
|
||||
export const decorators = [
|
||||
withVuetifyTheme({
|
||||
// These keys are the labels that will be displayed in the toolbar theme switcher
|
||||
// The values must match the theme keys from your VuetifyOptions
|
||||
themes: {
|
||||
light: 'light',
|
||||
dark: 'dark',
|
||||
},
|
||||
defaultTheme: 'light', // The key of your default theme
|
||||
}),
|
||||
]
|
||||
0
next-ui/.storybook/storybook.d.ts
vendored
Normal file
0
next-ui/.storybook/storybook.d.ts
vendored
Normal file
8
next-ui/.storybook/theme.ts
Normal file
8
next-ui/.storybook/theme.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import { create } from 'storybook/theming'
|
||||
|
||||
export default create({
|
||||
base: 'light',
|
||||
brandTitle: 'Komga Storybook',
|
||||
brandUrl: 'https://komga.org',
|
||||
brandImage: '../src/assets/komga.svg',
|
||||
})
|
||||
7
next-ui/.storybook/vitest.setup.ts
Normal file
7
next-ui/.storybook/vitest.setup.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import * as a11yAddonAnnotations from '@storybook/addon-a11y/preview'
|
||||
import { setProjectAnnotations } from '@storybook/vue3-vite'
|
||||
import * as projectAnnotations from './preview'
|
||||
|
||||
// This is an important step to apply the right configuration when testing your stories.
|
||||
// More info at: https://storybook.js.org/docs/api/portable-stories/portable-stories-vitest#setprojectannotations
|
||||
setProjectAnnotations([a11yAddonAnnotations, projectAnnotations])
|
||||
36
next-ui/.storybook/withVuetifyTheme.decorator.ts
Normal file
36
next-ui/.storybook/withVuetifyTheme.decorator.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import { h } from 'vue'
|
||||
import { DecoratorHelpers } from '@storybook/addon-themes'
|
||||
import StoryWrapper from './StoryWrapper.vue'
|
||||
import type { StoryContext } from '@storybook/vue3-vite'
|
||||
|
||||
const { initializeThemeState, pluckThemeFromContext } = DecoratorHelpers
|
||||
|
||||
export const withVuetifyTheme = ({
|
||||
themes,
|
||||
defaultTheme,
|
||||
}: {
|
||||
themes: object
|
||||
defaultTheme: string
|
||||
}) => {
|
||||
initializeThemeState(Object.keys(themes), defaultTheme)
|
||||
|
||||
return (storyFn: () => Component, context: StoryContext) => {
|
||||
const selectedTheme = pluckThemeFromContext(context)
|
||||
const { themeOverride } = context.parameters.themes ?? {}
|
||||
|
||||
const selected = themeOverride || selectedTheme || defaultTheme
|
||||
|
||||
const story = storyFn()
|
||||
|
||||
return () => {
|
||||
return h(
|
||||
StoryWrapper,
|
||||
{ themeName: selected }, // Props for StoryWrapper
|
||||
{
|
||||
// Puts your story into StoryWrapper's "story" slot with your story args
|
||||
story: () => h(story, { ...context.args }),
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
5
next-ui/dir2json.d.ts
vendored
5
next-ui/dir2json.d.ts
vendored
|
|
@ -4,7 +4,10 @@
|
|||
// noinspection JSUnusedGlobalSymbols
|
||||
// Auto generated by vite-plugin-dir2json
|
||||
declare module "*i18n?dir2json&ext=.json&1" {
|
||||
const json: {};
|
||||
const json: {
|
||||
"en": string;
|
||||
"fr": string;
|
||||
};
|
||||
export default json;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import storybook from 'eslint-plugin-storybook'
|
||||
|
||||
/**
|
||||
* .eslint.js
|
||||
*
|
||||
|
|
@ -23,6 +25,8 @@ export default defineConfigWithVueTs(
|
|||
'**/coverage/**',
|
||||
'openapi-generator.mts',
|
||||
'**/generated/openapi/komga.d.ts',
|
||||
'public-msw/**/*',
|
||||
'eslint.config.ts',
|
||||
],
|
||||
},
|
||||
|
||||
|
|
@ -66,4 +70,15 @@ export default defineConfigWithVueTs(
|
|||
},
|
||||
|
||||
eslintConfigPrettier,
|
||||
|
||||
...storybook.configs['flat/recommended'],
|
||||
|
||||
{
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
projectService: true,
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
|
@ -35,6 +35,10 @@
|
|||
"defaultMessage": "No restriction",
|
||||
"description": "User creation/edit dialog: Age restriction field possible option"
|
||||
},
|
||||
"AjWlka": {
|
||||
"defaultMessage": "Invalid login or password",
|
||||
"description": "Login screen: error message displayed when login failed"
|
||||
},
|
||||
"CUxhzL": {
|
||||
"defaultMessage": "Roles",
|
||||
"description": "User creation/edit dialog: Roles field"
|
||||
|
|
@ -75,6 +79,10 @@
|
|||
"defaultMessage": "Users",
|
||||
"description": "Drawer menu for Server > Users"
|
||||
},
|
||||
"JbF1nK": {
|
||||
"defaultMessage": "Password changed for user: {email}",
|
||||
"description": "Snackbar notification shown upon successful user's password modification"
|
||||
},
|
||||
"LaxrEO": {
|
||||
"defaultMessage": "Passwords must be identical",
|
||||
"description": "User password change dialog: Error message if passwords differ"
|
||||
|
|
@ -111,6 +119,10 @@
|
|||
"defaultMessage": "Shared Libraries",
|
||||
"description": "User creation/edit dialog: Shared Libraries field"
|
||||
},
|
||||
"V/OYJE": {
|
||||
"defaultMessage": "User deleted: {email}",
|
||||
"description": "Snackbar notification shown upon successful user deletion"
|
||||
},
|
||||
"WNY0pu": {
|
||||
"defaultMessage": "The latest version of Komga is already installed",
|
||||
"description": "Updates view: banner shown at the top"
|
||||
|
|
@ -119,6 +131,10 @@
|
|||
"defaultMessage": "New password",
|
||||
"description": "User password change dialog: New Password field label"
|
||||
},
|
||||
"Xz+JXU": {
|
||||
"defaultMessage": "error",
|
||||
"description": "Common message: an unkown error happened"
|
||||
},
|
||||
"Y6VlM9": {
|
||||
"defaultMessage": "Read List",
|
||||
"description": "Drawer menu for Import > Read List"
|
||||
|
|
@ -127,6 +143,10 @@
|
|||
"defaultMessage": "User Interface",
|
||||
"description": "Drawer menu for Server > User Interface"
|
||||
},
|
||||
"Z/EY89": {
|
||||
"defaultMessage": "Network error",
|
||||
"description": "Common message: a network error happened when communicating with the server"
|
||||
},
|
||||
"app.locale-name": {
|
||||
"defaultMessage": "English",
|
||||
"description": "The name of the locale, shown in the language selection menu. Must be translated to the language's name"
|
||||
|
|
@ -163,6 +183,10 @@
|
|||
"defaultMessage": "Duplicate Files",
|
||||
"description": "Drawer menu for Media > Duplicate Files"
|
||||
},
|
||||
"egrxd6": {
|
||||
"defaultMessage": "User created: {email}",
|
||||
"description": "Snackbar notification shown upon successful user creation"
|
||||
},
|
||||
"fQIepD": {
|
||||
"defaultMessage": "Books",
|
||||
"description": "Drawer menu for Import > Books"
|
||||
|
|
@ -183,6 +207,10 @@
|
|||
"defaultMessage": "Age",
|
||||
"description": "User creation/edit dialog: Age Restriction > Age field label"
|
||||
},
|
||||
"kvbi4j": {
|
||||
"defaultMessage": "User updated: {email}",
|
||||
"description": "Snackbar notification shown upon successful user update"
|
||||
},
|
||||
"l/To3S": {
|
||||
"defaultMessage": "History",
|
||||
"description": "Drawer menu for History"
|
||||
|
|
@ -219,6 +247,10 @@
|
|||
"defaultMessage": "Unknown",
|
||||
"description": "Drawer menu for Media > Duplicate Pages > Unknown"
|
||||
},
|
||||
"r6JNfI": {
|
||||
"defaultMessage": "Forgot your password?",
|
||||
"description": "Login screen: Forgot your password link"
|
||||
},
|
||||
"rw/Dkw": {
|
||||
"defaultMessage": "User Interface",
|
||||
"description": "Drawer menu for My Account > User Interface"
|
||||
|
|
|
|||
1750
next-ui/package-lock.json
generated
1750
next-ui/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -9,7 +9,9 @@
|
|||
"preview": "vite preview",
|
||||
"build-only": "vite build",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest watch",
|
||||
"test:unit": "vitest run --project unit",
|
||||
"test:unit:watch": "vitest watch --project unit",
|
||||
"test:storybook": "vitest run --project storybook",
|
||||
"type-check": "vue-tsc --build --force",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "npm run lint -- --fix",
|
||||
|
|
@ -18,7 +20,9 @@
|
|||
"openapi-generate": "npx tsx ./openapi-generator.mts",
|
||||
"i18n:extract": "formatjs extract \"src/**/*.{ts,tsx,vue}\" --ignore=\"**/*.d.ts\" --out-file i18n/en.json",
|
||||
"i18n:compile": "formatjs compile-folder i18n src/i18n",
|
||||
"i18n:verify": "formatjs verify --missing-keys --source-locale=en \"i18n/*.json\""
|
||||
"i18n:verify": "formatjs verify --missing-keys --source-locale=en \"i18n/*.json\"",
|
||||
"storybook:dev": "storybook dev -p 6006",
|
||||
"storybook:build": "storybook build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@pinia/colada": "^0.17.0",
|
||||
|
|
@ -34,27 +38,40 @@
|
|||
"vuetify": "^3.8.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@chromatic-com/storybook": "^4.0.0",
|
||||
"@eslint/js": "^9.28.0",
|
||||
"@formatjs/cli": "^6.7.1",
|
||||
"@iconify-json/mdi": "^1.2.3",
|
||||
"@storybook/addon-a11y": "^9.0.8",
|
||||
"@storybook/addon-docs": "^9.0.8",
|
||||
"@storybook/addon-onboarding": "^9.0.8",
|
||||
"@storybook/addon-themes": "^9.0.8",
|
||||
"@storybook/addon-vitest": "^9.0.8",
|
||||
"@storybook/vue3-vite": "^9.0.8",
|
||||
"@testing-library/vue": "^8.1.0",
|
||||
"@tsconfig/node22": "^22.0.2",
|
||||
"@types/node": "^22.15.29",
|
||||
"@vitejs/plugin-vue": "^5.1.4",
|
||||
"@vitejs/plugin-vue": "^5.2.4",
|
||||
"@vitest/browser": "^3.2.3",
|
||||
"@vitest/coverage-v8": "^3.2.3",
|
||||
"@vue/eslint-config-prettier": "^10.2.0",
|
||||
"@vue/eslint-config-typescript": "^14.1.3",
|
||||
"@vue/tsconfig": "^0.7.0",
|
||||
"eslint": "^9.28.0",
|
||||
"eslint-config-prettier": "^10.1.5",
|
||||
"eslint-plugin-formatjs": "^5.3.1",
|
||||
"eslint-plugin-storybook": "^9.0.8",
|
||||
"eslint-plugin-vue": "^10.1.0",
|
||||
"happy-dom": "^18.0.1",
|
||||
"msw": "^2.10.2",
|
||||
"msw-storybook-addon": "^2.0.5",
|
||||
"npm-run-all2": "^8.0.4",
|
||||
"openapi-typescript": "^7.8.0",
|
||||
"playwright": "^1.53.0",
|
||||
"prettier": "^3.5.3",
|
||||
"sass": "^1.88.0",
|
||||
"sass-embedded": "^1.88.0",
|
||||
"storybook": "^9.0.8",
|
||||
"typescript": "^5.8.3",
|
||||
"unplugin-auto-import": "^19.3.0",
|
||||
"unplugin-fonts": "^1.1.1",
|
||||
|
|
@ -68,5 +85,10 @@
|
|||
"vitest": "^3.2.3",
|
||||
"vue-router": "^4.4.0",
|
||||
"vue-tsc": "^2.1.10"
|
||||
},
|
||||
"msw": {
|
||||
"workerDirectory": [
|
||||
"public-msw"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
336
next-ui/public-msw/mockServiceWorker.js
Normal file
336
next-ui/public-msw/mockServiceWorker.js
Normal file
|
|
@ -0,0 +1,336 @@
|
|||
/* eslint-disable */
|
||||
/* tslint:disable */
|
||||
|
||||
/**
|
||||
* Mock Service Worker.
|
||||
* @see https://github.com/mswjs/msw
|
||||
* - Please do NOT modify this file.
|
||||
*/
|
||||
|
||||
const PACKAGE_VERSION = '2.10.2'
|
||||
const INTEGRITY_CHECKSUM = 'f5825c521429caf22a4dd13b66e243af'
|
||||
const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
|
||||
const activeClientIds = new Set()
|
||||
|
||||
addEventListener('install', function () {
|
||||
self.skipWaiting()
|
||||
})
|
||||
|
||||
addEventListener('activate', function (event) {
|
||||
event.waitUntil(self.clients.claim())
|
||||
})
|
||||
|
||||
addEventListener('message', async function (event) {
|
||||
const clientId = Reflect.get(event.source || {}, 'id')
|
||||
|
||||
if (!clientId || !self.clients) {
|
||||
return
|
||||
}
|
||||
|
||||
const client = await self.clients.get(clientId)
|
||||
|
||||
if (!client) {
|
||||
return
|
||||
}
|
||||
|
||||
const allClients = await self.clients.matchAll({
|
||||
type: 'window',
|
||||
})
|
||||
|
||||
switch (event.data) {
|
||||
case 'KEEPALIVE_REQUEST': {
|
||||
sendToClient(client, {
|
||||
type: 'KEEPALIVE_RESPONSE',
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
case 'INTEGRITY_CHECK_REQUEST': {
|
||||
sendToClient(client, {
|
||||
type: 'INTEGRITY_CHECK_RESPONSE',
|
||||
payload: {
|
||||
packageVersion: PACKAGE_VERSION,
|
||||
checksum: INTEGRITY_CHECKSUM,
|
||||
},
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
case 'MOCK_ACTIVATE': {
|
||||
activeClientIds.add(clientId)
|
||||
|
||||
sendToClient(client, {
|
||||
type: 'MOCKING_ENABLED',
|
||||
payload: {
|
||||
client: {
|
||||
id: client.id,
|
||||
frameType: client.frameType,
|
||||
},
|
||||
},
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
case 'MOCK_DEACTIVATE': {
|
||||
activeClientIds.delete(clientId)
|
||||
break
|
||||
}
|
||||
|
||||
case 'CLIENT_CLOSED': {
|
||||
activeClientIds.delete(clientId)
|
||||
|
||||
const remainingClients = allClients.filter((client) => {
|
||||
return client.id !== clientId
|
||||
})
|
||||
|
||||
// Unregister itself when there are no more clients
|
||||
if (remainingClients.length === 0) {
|
||||
self.registration.unregister()
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
addEventListener('fetch', function (event) {
|
||||
// Bypass navigation requests.
|
||||
if (event.request.mode === 'navigate') {
|
||||
return
|
||||
}
|
||||
|
||||
// Opening the DevTools triggers the "only-if-cached" request
|
||||
// that cannot be handled by the worker. Bypass such requests.
|
||||
if (event.request.cache === 'only-if-cached' && event.request.mode !== 'same-origin') {
|
||||
return
|
||||
}
|
||||
|
||||
// Bypass all requests when there are no active clients.
|
||||
// Prevents the self-unregistered worked from handling requests
|
||||
// after it's been deleted (still remains active until the next reload).
|
||||
if (activeClientIds.size === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const requestId = crypto.randomUUID()
|
||||
event.respondWith(handleRequest(event, requestId))
|
||||
})
|
||||
|
||||
/**
|
||||
* @param {FetchEvent} event
|
||||
* @param {string} requestId
|
||||
*/
|
||||
async function handleRequest(event, requestId) {
|
||||
const client = await resolveMainClient(event)
|
||||
const requestCloneForEvents = event.request.clone()
|
||||
const response = await getResponse(event, client, requestId)
|
||||
|
||||
// Send back the response clone for the "response:*" life-cycle events.
|
||||
// Ensure MSW is active and ready to handle the message, otherwise
|
||||
// this message will pend indefinitely.
|
||||
if (client && activeClientIds.has(client.id)) {
|
||||
const serializedRequest = await serializeRequest(requestCloneForEvents)
|
||||
|
||||
// Clone the response so both the client and the library could consume it.
|
||||
const responseClone = response.clone()
|
||||
|
||||
sendToClient(
|
||||
client,
|
||||
{
|
||||
type: 'RESPONSE',
|
||||
payload: {
|
||||
isMockedResponse: IS_MOCKED_RESPONSE in response,
|
||||
request: {
|
||||
id: requestId,
|
||||
...serializedRequest,
|
||||
},
|
||||
response: {
|
||||
type: responseClone.type,
|
||||
status: responseClone.status,
|
||||
statusText: responseClone.statusText,
|
||||
headers: Object.fromEntries(responseClone.headers.entries()),
|
||||
body: responseClone.body,
|
||||
},
|
||||
},
|
||||
},
|
||||
responseClone.body ? [serializedRequest.body, responseClone.body] : [],
|
||||
)
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the main client for the given event.
|
||||
* Client that issues a request doesn't necessarily equal the client
|
||||
* that registered the worker. It's with the latter the worker should
|
||||
* communicate with during the response resolving phase.
|
||||
* @param {FetchEvent} event
|
||||
* @returns {Promise<Client | undefined>}
|
||||
*/
|
||||
async function resolveMainClient(event) {
|
||||
const client = await self.clients.get(event.clientId)
|
||||
|
||||
if (activeClientIds.has(event.clientId)) {
|
||||
return client
|
||||
}
|
||||
|
||||
if (client?.frameType === 'top-level') {
|
||||
return client
|
||||
}
|
||||
|
||||
const allClients = await self.clients.matchAll({
|
||||
type: 'window',
|
||||
})
|
||||
|
||||
return allClients
|
||||
.filter((client) => {
|
||||
// Get only those clients that are currently visible.
|
||||
return client.visibilityState === 'visible'
|
||||
})
|
||||
.find((client) => {
|
||||
// Find the client ID that's recorded in the
|
||||
// set of clients that have registered the worker.
|
||||
return activeClientIds.has(client.id)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {FetchEvent} event
|
||||
* @param {Client | undefined} client
|
||||
* @param {string} requestId
|
||||
* @returns {Promise<Response>}
|
||||
*/
|
||||
async function getResponse(event, client, requestId) {
|
||||
// Clone the request because it might've been already used
|
||||
// (i.e. its body has been read and sent to the client).
|
||||
const requestClone = event.request.clone()
|
||||
|
||||
function passthrough() {
|
||||
// Cast the request headers to a new Headers instance
|
||||
// so the headers can be manipulated with.
|
||||
const headers = new Headers(requestClone.headers)
|
||||
|
||||
// Remove the "accept" header value that marked this request as passthrough.
|
||||
// This prevents request alteration and also keeps it compliant with the
|
||||
// user-defined CORS policies.
|
||||
const acceptHeader = headers.get('accept')
|
||||
if (acceptHeader) {
|
||||
const values = acceptHeader.split(',').map((value) => value.trim())
|
||||
const filteredValues = values.filter((value) => value !== 'msw/passthrough')
|
||||
|
||||
if (filteredValues.length > 0) {
|
||||
headers.set('accept', filteredValues.join(', '))
|
||||
} else {
|
||||
headers.delete('accept')
|
||||
}
|
||||
}
|
||||
|
||||
return fetch(requestClone, { headers })
|
||||
}
|
||||
|
||||
// Bypass mocking when the client is not active.
|
||||
if (!client) {
|
||||
return passthrough()
|
||||
}
|
||||
|
||||
// Bypass initial page load requests (i.e. static assets).
|
||||
// The absence of the immediate/parent client in the map of the active clients
|
||||
// means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet
|
||||
// and is not ready to handle requests.
|
||||
if (!activeClientIds.has(client.id)) {
|
||||
return passthrough()
|
||||
}
|
||||
|
||||
// Notify the client that a request has been intercepted.
|
||||
const serializedRequest = await serializeRequest(event.request)
|
||||
const clientMessage = await sendToClient(
|
||||
client,
|
||||
{
|
||||
type: 'REQUEST',
|
||||
payload: {
|
||||
id: requestId,
|
||||
...serializedRequest,
|
||||
},
|
||||
},
|
||||
[serializedRequest.body],
|
||||
)
|
||||
|
||||
switch (clientMessage.type) {
|
||||
case 'MOCK_RESPONSE': {
|
||||
return respondWithMock(clientMessage.data)
|
||||
}
|
||||
|
||||
case 'PASSTHROUGH': {
|
||||
return passthrough()
|
||||
}
|
||||
}
|
||||
|
||||
return passthrough()
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Client} client
|
||||
* @param {any} message
|
||||
* @param {Array<Transferable>} transferrables
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
function sendToClient(client, message, transferrables = []) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const channel = new MessageChannel()
|
||||
|
||||
channel.port1.onmessage = (event) => {
|
||||
if (event.data && event.data.error) {
|
||||
return reject(event.data.error)
|
||||
}
|
||||
|
||||
resolve(event.data)
|
||||
}
|
||||
|
||||
client.postMessage(message, [channel.port2, ...transferrables.filter(Boolean)])
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Response} response
|
||||
* @returns {Response}
|
||||
*/
|
||||
function respondWithMock(response) {
|
||||
// Setting response status code to 0 is a no-op.
|
||||
// However, when responding with a "Response.error()", the produced Response
|
||||
// instance will have status code set to 0. Since it's not possible to create
|
||||
// a Response instance with status code 0, handle that use-case separately.
|
||||
if (response.status === 0) {
|
||||
return Response.error()
|
||||
}
|
||||
|
||||
const mockedResponse = new Response(response.body, response)
|
||||
|
||||
Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, {
|
||||
value: true,
|
||||
enumerable: true,
|
||||
})
|
||||
|
||||
return mockedResponse
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Request} request
|
||||
*/
|
||||
async function serializeRequest(request) {
|
||||
return {
|
||||
url: request.url,
|
||||
mode: request.mode,
|
||||
method: request.method,
|
||||
headers: Object.fromEntries(request.headers.entries()),
|
||||
cache: request.cache,
|
||||
credentials: request.credentials,
|
||||
destination: request.destination,
|
||||
integrity: request.integrity,
|
||||
redirect: request.redirect,
|
||||
referrer: request.referrer,
|
||||
referrerPolicy: request.referrerPolicy,
|
||||
body: await request.arrayBuffer(),
|
||||
keepalive: request.keepalive,
|
||||
}
|
||||
}
|
||||
122
next-ui/src/assets/komga.svg
Normal file
122
next-ui/src/assets/komga.svg
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
height="512pt"
|
||||
viewBox="0 0 1978.5619 512"
|
||||
width="1978.5619pt"
|
||||
version="1.1"
|
||||
id="svg4586"
|
||||
sodipodi:docname="komga.svg"
|
||||
inkscape:version="1.4.2 (ebf0e940, 2025-05-08)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||
<metadata
|
||||
id="metadata4592">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs4590">
|
||||
<linearGradient
|
||||
id="linearGradient6082"
|
||||
inkscape:swatch="solid">
|
||||
<stop
|
||||
style="stop-color:#000000;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop6080" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="linearGradient6076"
|
||||
inkscape:swatch="solid">
|
||||
<stop
|
||||
style="stop-color:#000000;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop6074" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient6082"
|
||||
id="linearGradient6084"
|
||||
x1="77.866814"
|
||||
y1="386.00677"
|
||||
x2="217.20259"
|
||||
y2="386.00677"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
</defs>
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1512"
|
||||
inkscape:window-height="916"
|
||||
id="namedview4588"
|
||||
showgrid="false"
|
||||
inkscape:zoom="0.19440657"
|
||||
inkscape:cx="1646.0349"
|
||||
inkscape:cy="-444.94381"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="748"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="svg4586"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="pt" />
|
||||
<path
|
||||
d="M 512,256 C 512,397.38672 397.38672,512 256,512 114.61328,512 0,397.38672 0,256 0,114.61328 114.61328,0 256,0 397.38672,0 512,114.61328 512,256 Z m 0,0"
|
||||
fill="#005ed3"
|
||||
id="path4556" />
|
||||
<path
|
||||
d="m 512,256 c 0,-11.71094 -0.80469,-23.23047 -2.32422,-34.52344 L 382.48047,94.28125 320.52344,121.85938 256,56.933594 212.69531,131.30469 129.51953,94.28125 141.86719,178.42187 49.949219,193.81641 114.32031,256 l -64.371091,62.18359 82.121091,82.16016 -2.55078,17.375 91.95703,91.95703 C 232.76953,511.19531 244.28906,512 256,512 397.38672,512 512,397.38672 512,256 Z"
|
||||
id="path4558"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#00459f"
|
||||
sodipodi:nodetypes="scccccccccccccss" />
|
||||
<path
|
||||
d="m 256,86.742188 37.10937,63.738282 70.57422,-31.41406 -10.52734,71.71875 77.07812,12.91015 L 376.08984,256 l 54.14453,52.30469 -77.07812,12.91015 10.52734,71.71875 L 293.10937,361.51953 256,425.25781 218.89062,361.51953 148.31641,392.93359 158.84375,321.21484 81.765625,308.30469 135.91016,256 81.765625,203.69531 l 77.078125,-12.91015 -10.52734,-71.71875 70.57421,31.41406 z m 0,0"
|
||||
fill="#ff0335"
|
||||
id="path4560" />
|
||||
<path
|
||||
d="m 430.23047,308.30078 -77.07031,12.91016 10.51953,71.71875 L 293.10938,361.51953 256,425.26172 V 86.738281 l 37.10938,63.742189 70.57031,-31.41016 -6.75781,46.10156 -3.76172,25.61719 58.80078,9.85156 18.26953,3.0586 -13.39063,12.92969 -40.75,39.37109 11.37891,10.98828 z m 0,0"
|
||||
fill="#c2001b"
|
||||
id="path4562" />
|
||||
<path
|
||||
d="m 256,455.06641 -43.30469,-74.3711 -83.17578,37.02344 12.34766,-84.14063 L 49.949219,318.18359 114.32031,256 49.949219,193.81641 141.86719,178.42187 129.51953,94.28125 212.69922,131.30469 256,56.933594 299.30469,131.30469 382.48047,94.28125 370.13281,178.42187 462.05078,193.81641 397.67969,256 l 64.37109,62.18359 -91.91797,15.39844 12.34766,84.13672 -83.17578,-37.02344 z M 225.08203,342.34375 256,395.44531 l 30.91797,-53.10156 57.96484,25.80078 -8.70312,-59.29297 62.23828,-10.42578 L 354.5,256 398.41797,213.57422 336.17969,203.14844 344.88281,143.85547 286.91797,169.65625 256,116.55469 l -30.91797,53.10156 -57.96484,-25.80078 8.70312,59.29297 -62.23828,10.42578 L 157.5,256 l -43.91797,42.42578 62.23828,10.42578 -8.70312,59.29297 z m 0,0"
|
||||
fill="#ffdf47"
|
||||
id="path4564" />
|
||||
<path
|
||||
d="M 403.30859,261.44141 397.67969,256 l 25.16015,-24.30078 39.21094,-37.87891 -55.75,-9.33984 -36.17187,-6.0586 2.80078,-19.09375 9.55078,-65.04687 -83.17969,37.01953 L 256,56.929688 v 59.621092 l 30.92188,53.10938 57.95703,-25.8086 -3.91016,26.66797 -2.54687,17.37891 -2.24219,15.25 2.48047,0.42187 59.76172,10.00781 L 354.5,256 l 16.96875,16.39062 26.95313,26.03125 -62.24219,10.42969 8.69922,59.29688 -57.95703,-25.8086 L 256,395.44922 v 59.62109 l 43.30078,-74.37109 83.17969,37.01953 -12.35156,-84.14063 91.92187,-15.39843 z m 0,0"
|
||||
fill="#fec000"
|
||||
id="path4566" />
|
||||
<g
|
||||
aria-label="K"
|
||||
transform="matrix(1.1590846,-0.34467221,0.22789693,0.794981,0,0)"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:296.56px;line-height:125%;font-family:Impact;-inkscape-font-specification:Impact;letter-spacing:0px;word-spacing:0px;fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1.54529;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="text4596">
|
||||
<path
|
||||
d="m 220.91497,266.9035 -34.89789,105.85211 38.2284,128.58643 H 161.2555 L 136.63873,400.84769 V 501.34204 H 75.676021 V 266.9035 h 60.962709 v 91.08205 l 27.07845,-91.08205 z"
|
||||
style="font-size:296.56px;fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1.54529;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path824" />
|
||||
</g>
|
||||
<path
|
||||
style="font-weight:bold;font-size:354.967px;font-family:Sans;-inkscape-font-specification:'Sans, Bold';stroke-width:5.54637"
|
||||
d="m 902.04822,379.80621 h -81.63548 l -77.12906,-103.30094 -15.59914,18.89228 v 84.40866 H 661.12823 V 121.72717 h 66.55631 v 116.8202 l 92.20823,-116.8202 h 77.12906 L 796.14742,241.14722 Z M 1130.3156,282.5716 q 0,48.01067 -28.0785,75.74247 -27.9051,27.55847 -78.5156,27.55847 -50.61054,0 -78.68898,-27.55847 -27.90512,-27.7318 -27.90512,-75.74247 0,-48.35732 28.07844,-75.9158 28.25177,-27.55847 78.51566,-27.55847 50.9572,0 78.689,27.7318 27.9051,27.73179 27.9051,75.74247 z m -76.7824,47.31738 q 6.0663,-7.45292 9.0128,-17.85235 3.1198,-10.57275 3.1198,-29.11838 0,-17.15905 -3.1198,-28.77174 -3.1198,-11.6127 -8.6662,-18.54564 -5.5463,-7.10628 -13.3459,-10.05278 -7.7996,-2.9465 -16.8124,-2.9465 -9.0128,0 -16.1191,2.42653 -6.933,2.42653 -13.34594,9.70613 -5.71968,6.75962 -9.18616,18.54564 -3.29315,11.78601 -3.29315,29.63836 0,15.94578 2.9465,27.73179 2.94651,11.61269 8.66619,18.71897 5.54636,6.75962 13.17256,9.87945 7.7996,3.11983 17.6791,3.11983 8.4928,0 16.1191,-2.77318 7.7995,-2.94651 13.1726,-9.70613 z m 311.8094,-88.04846 v 137.96569 h -62.7432 V 282.5716 q 0,-14.21255 -0.6933,-24.092 -0.6933,-10.05278 -3.8132,-16.29243 -3.1198,-6.23966 -9.5328,-9.01284 -6.2396,-2.9465 -17.679,-2.9465 -9.1861,0 -17.8523,3.81312 -8.6662,3.6398 -15.5992,7.79957 v 137.96569 h -62.3965 V 185.16366 h 62.3965 v 21.49214 q 16.1191,-12.65263 30.8517,-19.7589 14.7325,-7.10628 32.5848,-7.10628 19.239,0 33.9715,8.66619 14.7325,8.49286 23.052,25.13194 18.719,-15.77246 36.398,-24.78529 17.679,-9.01284 34.6648,-9.01284 31.5449,0 47.8373,18.89229 16.4658,18.89229 16.4658,54.42365 v 126.69965 h -62.7432 V 282.5716 q 0,-14.38587 -0.6933,-24.26533 -0.52,-9.87945 -3.6398,-16.1191 -2.9465,-6.23966 -9.3595,-9.01284 -6.413,-2.9465 -18.0257,-2.9465 -7.7995,0 -15.2525,2.77318 -7.4529,2.59986 -18.1989,8.83951 z m 374.5525,115.60693 q 0,27.38515 -7.7995,46.10412 -7.7996,18.71896 -21.8388,29.29171 -14.0392,10.74607 -33.9715,15.42581 -19.7589,4.85306 -44.7175,4.85306 -20.2789,0 -40.0378,-2.42653 -19.5856,-2.42653 -33.9714,-5.89301 v -48.70397 h 7.6262 q 11.4394,4.50642 27.9051,8.14622 16.4658,3.81312 29.4651,3.81312 17.3323,0 28.0784,-3.29315 10.9194,-3.11983 16.6391,-9.01283 5.373,-5.54636 7.7996,-14.21255 2.4265,-8.66619 2.4265,-20.79885 v -3.6398 q -11.266,9.18616 -24.9586,14.5592 -13.6926,5.37303 -30.505,5.37303 -40.9044,0 -63.0898,-24.61197 -22.1855,-24.61197 -22.1855,-74.87585 0,-24.092 6.7596,-41.5977 6.7597,-17.50569 19.0657,-30.50497 11.4393,-12.13267 28.0784,-18.89229 16.8124,-6.75963 34.3181,-6.75963 15.7725,0 28.5984,3.81312 12.9993,3.6398 23.572,10.22611 l 2.2533,-8.66619 h 60.4899 z m -62.3965,-38.99784 v -88.22178 q -5.373,-2.25321 -13.1726,-3.46648 -7.7996,-1.38659 -14.0392,-1.38659 -24.612,0 -36.918,14.21255 -12.306,14.03922 -12.306,39.34449 0,28.07844 10.3994,39.17116 10.5728,11.09272 31.1983,11.09272 9.3595,0 18.3723,-2.9465 9.0129,-2.9465 16.4658,-7.79957 z m 239.1867,10.57275 V 288.4646 q -12.6526,1.03995 -27.3851,2.94651 -14.7325,1.73324 -22.3588,4.15977 -9.3595,2.9465 -14.3858,8.66618 -4.8531,5.54636 -4.8531,14.73252 0,6.06633 1.0399,9.87946 1.04,3.81312 5.1998,7.27959 3.9864,3.46648 9.5328,5.19971 5.5463,1.55992 17.3323,1.55992 9.3595,0 18.8923,-3.81312 9.7061,-3.81313 16.9857,-10.05278 z m 0,30.15833 q -5.0263,3.81312 -12.4793,9.18616 -7.4529,5.37303 -14.0392,8.49286 -9.1861,4.15977 -19.0656,6.06633 -9.8795,2.07989 -21.6655,2.07989 -27.7318,0 -46.4507,-17.15905 -18.719,-17.15905 -18.719,-43.85091 0,-21.31882 9.5328,-34.83807 9.5328,-13.51925 27.0385,-21.31882 17.3324,-7.79957 42.9843,-11.09272 25.6519,-3.29315 53.2104,-4.85306 v -1.03995 q 0,-16.1191 -13.1726,-22.18543 -13.1726,-6.23966 -38.8245,-6.23966 -15.4258,0 -32.9315,5.54636 -17.5057,5.37304 -25.132,8.31954 h -5.7197 v -46.97073 q 9.8795,-2.59986 32.0649,-6.06633 22.3588,-3.6398 44.7176,-3.6398 53.2103,0 76.7824,16.46575 23.7453,16.29243 23.7453,51.30383 v 132.41933 h -61.8766 z"
|
||||
id="text1"
|
||||
aria-label="Komga" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 10 KiB |
|
|
@ -19,8 +19,8 @@ export const useAppReleases = defineQuery(() => {
|
|||
const latestRelease = computed(() => data.value?.find((x) => x.latest))
|
||||
|
||||
const isLatestVersion = computed(() => {
|
||||
if (buildVersion.value && latestRelease.value)
|
||||
return buildVersion.value == latestRelease.value?.version
|
||||
if (buildVersion.value && data.value)
|
||||
return data.value?.some((x) => x.latest && x.version == buildVersion.value)
|
||||
else return undefined
|
||||
})
|
||||
|
||||
|
|
|
|||
49
next-ui/src/components/BuildCommit.stories.ts
Normal file
49
next-ui/src/components/BuildCommit.stories.ts
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import BuildCommit from './BuildCommit.vue'
|
||||
import { http, HttpResponse, delay } from 'msw'
|
||||
import { baseUrl, response401Unauthorized } from '@/mocks/api/handlers/base'
|
||||
import { actuatorResponseOk } from '@/mocks/api/handlers/actuator'
|
||||
|
||||
const meta = {
|
||||
component: BuildCommit,
|
||||
render: (args: object) => ({
|
||||
components: { BuildCommit },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
template: '<BuildCommit />',
|
||||
}),
|
||||
parameters: {
|
||||
// More on how to position stories at: https://storybook.js.org/docs/configure/story-layout
|
||||
},
|
||||
args: {},
|
||||
} satisfies Meta<typeof BuildCommit>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {},
|
||||
}
|
||||
|
||||
export const Loading: Story = {
|
||||
parameters: {
|
||||
msw: {
|
||||
handlers: [
|
||||
http.get(baseUrl + 'actuator/info', async () => {
|
||||
await delay(5_000)
|
||||
return HttpResponse.json(actuatorResponseOk)
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const Error: Story = {
|
||||
parameters: {
|
||||
msw: {
|
||||
handlers: [http.get(baseUrl + 'actuator/info', response401Unauthorized)],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
@ -1,22 +1,22 @@
|
|||
<template>
|
||||
<template v-if="commitId">
|
||||
<v-btn
|
||||
:prepend-icon="mdiSourceCommit"
|
||||
variant="text"
|
||||
color="grey"
|
||||
size="small"
|
||||
class="text-caption"
|
||||
:href="'https://github.com/gotson/komga/commits/' + commitId"
|
||||
target="_blank"
|
||||
>
|
||||
{{ commitId }}
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-btn
|
||||
:loading="isLoading"
|
||||
:prepend-icon="mdiSourceCommit"
|
||||
variant="text"
|
||||
color="grey"
|
||||
size="small"
|
||||
class="text-caption"
|
||||
:href="'https://github.com/gotson/komga/commits/' + commitId"
|
||||
target="_blank"
|
||||
>
|
||||
{{ commitId || $formatMessage(commonMessages.error) }}
|
||||
</v-btn>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import mdiSourceCommit from '~icons/mdi/source-commit'
|
||||
import { useActuatorInfo } from '@/colada/queries/actuator-info'
|
||||
import { commonMessages } from '@/utils/i18n/common-messages'
|
||||
|
||||
const { commitId } = useActuatorInfo()
|
||||
const { commitId, isLoading } = useActuatorInfo()
|
||||
</script>
|
||||
|
|
|
|||
62
next-ui/src/components/BuildVersion.stories.ts
Normal file
62
next-ui/src/components/BuildVersion.stories.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import { http, HttpResponse, delay } from 'msw'
|
||||
import { baseUrl, response401Unauthorized } from '@/mocks/api/handlers/base'
|
||||
import BuildVersion from './BuildVersion.vue'
|
||||
import { releasesResponseOk, releasesResponseOkNotLatest } from '@/mocks/api/handlers/releases'
|
||||
|
||||
const meta = {
|
||||
component: BuildVersion,
|
||||
render: (args: object) => ({
|
||||
components: { BuildVersion },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
template: '<BuildVersion />',
|
||||
}),
|
||||
parameters: {
|
||||
// More on how to position stories at: https://storybook.js.org/docs/configure/story-layout
|
||||
},
|
||||
args: {},
|
||||
} satisfies Meta<typeof BuildVersion>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {},
|
||||
}
|
||||
|
||||
export const OutdatedVersion: Story = {
|
||||
parameters: {
|
||||
msw: {
|
||||
handlers: [
|
||||
http.get(baseUrl + 'api/v1/releases', () => HttpResponse.json(releasesResponseOkNotLatest)),
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const Loading: Story = {
|
||||
parameters: {
|
||||
msw: {
|
||||
handlers: [
|
||||
http.get(baseUrl + 'api/v1/releases', async () => {
|
||||
await delay(5_000)
|
||||
return HttpResponse.json(releasesResponseOk)
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const Error: Story = {
|
||||
parameters: {
|
||||
msw: {
|
||||
handlers: [
|
||||
http.get(baseUrl + 'actuator/info', response401Unauthorized),
|
||||
http.get(baseUrl + 'api/v1/releases', response401Unauthorized),
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
@ -1,27 +1,27 @@
|
|||
<template>
|
||||
<template v-if="buildVersion">
|
||||
<v-badge
|
||||
dot
|
||||
color="warning"
|
||||
:model-value="isLatestVersion == false"
|
||||
<v-badge
|
||||
dot
|
||||
color="warning"
|
||||
:model-value="isLatestVersion == false"
|
||||
>
|
||||
<v-btn
|
||||
:loading="isLoading"
|
||||
:prepend-icon="mdiTagOutline"
|
||||
variant="text"
|
||||
color="grey"
|
||||
size="small"
|
||||
class="text-caption"
|
||||
to="/server/updates"
|
||||
>
|
||||
<v-btn
|
||||
:prepend-icon="mdiTagOutline"
|
||||
variant="text"
|
||||
color="grey"
|
||||
size="small"
|
||||
class="text-caption"
|
||||
to="/server/updates"
|
||||
>
|
||||
{{ buildVersion }}
|
||||
</v-btn>
|
||||
</v-badge>
|
||||
</template>
|
||||
{{ buildVersion || $formatMessage(commonMessages.error) }}
|
||||
</v-btn>
|
||||
</v-badge>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import mdiTagOutline from '~icons/mdi/tag-outline'
|
||||
import { useAppReleases } from '@/colada/queries/app-releases'
|
||||
import { commonMessages } from '@/utils/i18n/common-messages'
|
||||
|
||||
const { buildVersion, isLatestVersion } = useAppReleases()
|
||||
const { buildVersion, isLatestVersion, isLoading } = useAppReleases()
|
||||
</script>
|
||||
|
|
|
|||
25
next-ui/src/pages/login.stories.ts
Normal file
25
next-ui/src/pages/login.stories.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import login from './login.vue'
|
||||
|
||||
const meta = {
|
||||
component: login,
|
||||
render: (args: object) => ({
|
||||
components: { login },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
template: '<login />',
|
||||
}),
|
||||
parameters: {
|
||||
// More on how to position stories at: https://storybook.js.org/docs/configure/story-layout
|
||||
},
|
||||
args: {},
|
||||
} satisfies Meta<typeof login>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {},
|
||||
}
|
||||
27
next-ui/src/pages/server/users.stories.ts
Normal file
27
next-ui/src/pages/server/users.stories.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import users from './users.vue'
|
||||
|
||||
const meta = {
|
||||
component: users,
|
||||
render: (args: object) => ({
|
||||
components: { users },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
template: '<users />',
|
||||
}),
|
||||
parameters: {
|
||||
// More on how to position stories at: https://storybook.js.org/docs/configure/story-layout
|
||||
},
|
||||
args: {},
|
||||
// This component will have an automatically generated docsPage entry: https://storybook.js.org/docs/writing-docs/autodocs
|
||||
tags: ['autodocs'],
|
||||
} satisfies Meta<typeof users>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {},
|
||||
}
|
||||
|
|
@ -16,4 +16,9 @@ export const commonMessages = {
|
|||
defaultMessage: 'Network error',
|
||||
id: 'Z/EY89',
|
||||
}),
|
||||
error: defineMessage({
|
||||
description: 'Common message: an unkown error happened',
|
||||
defaultMessage: 'error',
|
||||
id: 'Xz+JXU',
|
||||
}),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { defineMessage } from 'vue-intl'
|
||||
import localeMessages from '../i18n?dir2json&ext=.json&1'
|
||||
import localeMessages from '@/i18n?dir2json&ext=.json&1'
|
||||
|
||||
export const defaultLocale = 'en'
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,13 @@
|
|||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||
"include": ["env.d.ts", "src/**/*", "src/**/*.vue", "./dir2json.d.ts"],
|
||||
"include": [
|
||||
"env.d.ts",
|
||||
"src/**/*",
|
||||
"src/**/*.vue",
|
||||
"./dir2json.d.ts",
|
||||
"./.storybook/**/*",
|
||||
"vitest.shims.d.ts"
|
||||
],
|
||||
"exclude": ["src/**/__tests__/*"],
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import dir2json from 'vite-plugin-dir2json'
|
|||
// Utilities
|
||||
import { defineConfig } from 'vite'
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
|
|
@ -84,7 +85,32 @@ export default defineConfig({
|
|||
},
|
||||
},
|
||||
test: {
|
||||
environment: 'happy-dom',
|
||||
restoreMocks: true,
|
||||
projects: [
|
||||
{
|
||||
extends: true,
|
||||
test: {
|
||||
name: 'unit',
|
||||
environment: 'happy-dom',
|
||||
restoreMocks: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
test: {
|
||||
name: 'storybook',
|
||||
browser: {
|
||||
enabled: true,
|
||||
headless: true,
|
||||
provider: 'playwright',
|
||||
instances: [{ browser: 'chromium' }],
|
||||
},
|
||||
setupFiles: ['.storybook/vitest.setup.ts'],
|
||||
},
|
||||
plugins: [
|
||||
// The plugin will run tests for the stories defined in your Storybook config
|
||||
// See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest
|
||||
storybookTest(),
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
|
|
|||
1
next-ui/vitest.shims.d.ts
vendored
Normal file
1
next-ui/vitest.shims.d.ts
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/// <reference types="@vitest/browser/providers/playwright" />
|
||||
Loading…
Reference in a new issue