generated from Seekra/repository-template
Merge pull request 'Style: make code style consistent' (#186) from style/make-code-style-consistent into main
Deploy on dev / Deploy on dev (push) Successful in 48s
Deploy on dev / Deploy on dev (push) Successful in 48s
Reviewed-on: #186 Reviewed-by: Jakob Gregory
This commit was merged in pull request #186.
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
.gitea/
|
||||
.github/
|
||||
eslint.config.js
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"tabWidth": 4
|
||||
}
|
||||
+181
-9
@@ -213,6 +213,30 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
|
||||
|
||||
-----------
|
||||
|
||||
The following npm package may be included in this product:
|
||||
|
||||
- rfdc@1.4.1
|
||||
|
||||
This package contains the following license:
|
||||
|
||||
Copyright 2019 "David Mark Clements <david.mark.clements@gmail.com>"
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
|
||||
documentation files (the "Software"), to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and
|
||||
to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions
|
||||
of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
||||
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
||||
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
|
||||
CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
IN THE SOFTWARE.
|
||||
|
||||
-----------
|
||||
|
||||
The following npm package may be included in this product:
|
||||
|
||||
- @jridgewell/resolve-uri@3.1.2
|
||||
@@ -560,6 +584,67 @@ SOFTWARE.
|
||||
|
||||
-----------
|
||||
|
||||
The following npm packages may be included in this product:
|
||||
|
||||
- copy-anything@4.0.5
|
||||
- is-what@5.5.0
|
||||
|
||||
These packages each contain the following license:
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018 Luca Ban - Mesqueeb
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
-----------
|
||||
|
||||
The following npm package may be included in this product:
|
||||
|
||||
- superjson@2.2.6
|
||||
|
||||
This package contains the following license:
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 Simon Knott and superjson contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
-----------
|
||||
|
||||
The following npm packages may be included in this product:
|
||||
|
||||
- birpc@2.9.0
|
||||
@@ -591,6 +676,36 @@ SOFTWARE.
|
||||
|
||||
-----------
|
||||
|
||||
The following npm package may be included in this product:
|
||||
|
||||
- mitt@3.0.1
|
||||
|
||||
This package contains the following license:
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 Jason Miller
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
-----------
|
||||
|
||||
The following npm package may be included in this product:
|
||||
|
||||
- unplugin@3.0.0
|
||||
@@ -683,9 +798,12 @@ SOFTWARE.
|
||||
|
||||
The following npm packages may be included in this product:
|
||||
|
||||
- @vue/devtools-api@8.1.1
|
||||
- @vue/devtools-kit@8.1.1
|
||||
- @vue/devtools-shared@8.1.1
|
||||
- @vue/devtools-api@7.7.10
|
||||
- @vue/devtools-api@8.1.5
|
||||
- @vue/devtools-kit@7.7.10
|
||||
- @vue/devtools-kit@8.1.5
|
||||
- @vue/devtools-shared@7.7.10
|
||||
- @vue/devtools-shared@8.1.5
|
||||
|
||||
These packages each contain the following license:
|
||||
|
||||
@@ -715,7 +833,7 @@ SOFTWARE.
|
||||
|
||||
The following npm package may be included in this product:
|
||||
|
||||
- tinyglobby@0.2.16
|
||||
- tinyglobby@0.2.17
|
||||
|
||||
This package contains the following license:
|
||||
|
||||
@@ -775,7 +893,9 @@ SOFTWARE.
|
||||
|
||||
The following npm packages may be included in this product:
|
||||
|
||||
- defu@6.1.7
|
||||
- mlly@1.8.2
|
||||
- perfect-debounce@1.0.0
|
||||
- perfect-debounce@2.1.0
|
||||
- scule@1.3.0
|
||||
- ufo@1.6.4
|
||||
@@ -1222,6 +1342,57 @@ USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
-----------
|
||||
|
||||
The following npm package may be included in this product:
|
||||
|
||||
- pinia-plugin-persistedstate@4.7.1
|
||||
|
||||
This package contains the following license:
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 praz <https://codeberg.org/praz>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
-----------
|
||||
|
||||
The following npm package may be included in this product:
|
||||
|
||||
- speakingurl@14.0.1
|
||||
|
||||
This package contains the following license:
|
||||
|
||||
The BSD 3-Clause License (BSD3)
|
||||
|
||||
Copyright (c) 2013-2017 Sascha Droste <pid@posteo.net>
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of the author nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
-----------
|
||||
|
||||
The following npm package may be included in this product:
|
||||
|
||||
- chokidar@5.0.0
|
||||
@@ -1284,7 +1455,7 @@ SOFTWARE.
|
||||
|
||||
The following npm package may be included in this product:
|
||||
|
||||
- picomatch@4.0.4
|
||||
- picomatch@4.0.5
|
||||
|
||||
This package contains the following license:
|
||||
|
||||
@@ -1351,11 +1522,12 @@ THE SOFTWARE.
|
||||
|
||||
-----------
|
||||
|
||||
The following npm package may be included in this product:
|
||||
The following npm packages may be included in this product:
|
||||
|
||||
- pinia@3.0.4
|
||||
- vue-router@5.0.6
|
||||
|
||||
This package contains the following license:
|
||||
These packages each contain the following license:
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
@@ -1587,7 +1759,7 @@ THE SOFTWARE.
|
||||
|
||||
The following npm package may be included in this product:
|
||||
|
||||
- postcss@8.5.14
|
||||
- postcss@8.5.26
|
||||
|
||||
This package contains the following license:
|
||||
|
||||
@@ -1616,7 +1788,7 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
The following npm package may be included in this product:
|
||||
|
||||
- nanoid@3.3.12
|
||||
- nanoid@3.3.18
|
||||
|
||||
This package contains the following license:
|
||||
|
||||
|
||||
+4062
-23
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,50 @@
|
||||
import js from '@eslint/js';
|
||||
import { defineConfig } from 'eslint/config';
|
||||
import pluginVue from 'eslint-plugin-vue';
|
||||
import globals from 'globals';
|
||||
|
||||
export default defineConfig([
|
||||
{
|
||||
files: ['**/*.{js,mjs,cjs,vue}'],
|
||||
plugins: { js },
|
||||
extends: ['js/recommended'],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.node,
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'eqeqeq': 'error',
|
||||
|
||||
'no-unused-vars': 'error',
|
||||
'no-undef': 'error',
|
||||
'prefer-const': 'error',
|
||||
'no-var': 'error',
|
||||
'no-duplicate-imports': 'error',
|
||||
'no-import-assign': 'error',
|
||||
|
||||
'no-empty-function': 'error',
|
||||
'default-param-last': 'error',
|
||||
'prefer-arrow-callback': 'error',
|
||||
'require-await': 'warn',
|
||||
'no-async-promise-executor': 'error',
|
||||
|
||||
'no-unreachable': 'error',
|
||||
|
||||
'no-constant-condition': 'error',
|
||||
|
||||
'no-debugger': 'error',
|
||||
'no-alert': 'error',
|
||||
'no-console': 'warn',
|
||||
|
||||
'no-new-object': 'error',
|
||||
'no-array-constructor': 'error',
|
||||
'no-prototype-builtins': 'error',
|
||||
'object-shorthand': 'error',
|
||||
|
||||
'prefer-template': 'error',
|
||||
},
|
||||
},
|
||||
pluginVue.configs['flat/essential'],
|
||||
]);
|
||||
+18
-8
@@ -24,7 +24,19 @@ limitations under the License.
|
||||
<noscript>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
font-family:
|
||||
'Inter',
|
||||
system-ui,
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
'Segoe UI',
|
||||
Roboto,
|
||||
Oxygen,
|
||||
Ubuntu,
|
||||
Cantarell,
|
||||
'Open Sans',
|
||||
'Helvetica Neue',
|
||||
sans-serif;
|
||||
}
|
||||
|
||||
a {
|
||||
@@ -40,17 +52,15 @@ limitations under the License.
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<h1>
|
||||
Seekra
|
||||
</h1>
|
||||
<p>
|
||||
You need to enable JavaScript to use Seekra.
|
||||
</p>
|
||||
<h1>Seekra</h1>
|
||||
<p>You need to enable JavaScript to use Seekra.</p>
|
||||
<section>
|
||||
<h2>Legal Content</h2>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="https://assets.seekra.jcloud-services.ddns.net/2026-08-04-76a067e/legal/legal_notice.html">
|
||||
<a
|
||||
href="https://assets.seekra.jcloud-services.ddns.net/2026-08-04-76a067e/legal/legal_notice.html"
|
||||
>
|
||||
Legal Notice
|
||||
</a>
|
||||
</li>
|
||||
|
||||
Generated
+3119
-19
File diff suppressed because it is too large
Load Diff
+11
-1
@@ -8,7 +8,11 @@
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"test": "vitest",
|
||||
"test:run": "vitest run"
|
||||
"test:run": "vitest run",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"format": "prettier . --write",
|
||||
"format:check": "prettier . --check"
|
||||
},
|
||||
"dependencies": {
|
||||
"pinia": "^3.0.4",
|
||||
@@ -19,10 +23,16 @@
|
||||
"vue-router": "^5.0.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@pinia/testing": "^1.0.3",
|
||||
"@vitejs/plugin-vue": "^6.0.6",
|
||||
"@vue/test-utils": "^2.4.6",
|
||||
"eslint": "^10.8.1",
|
||||
"eslint-plugin-vue": "^10.10.0",
|
||||
"generate-license-file": "^4.2.1",
|
||||
"globals": "^17.9.0",
|
||||
"jsdom": "^29.1.1",
|
||||
"prettier": "^3.9.6",
|
||||
"vite": "^8.0.10",
|
||||
"vite-plugin-html": "^3.2.2",
|
||||
"vitest": "^4.1.7"
|
||||
|
||||
@@ -22,21 +22,16 @@ import { generateLicenseFile } from 'generate-license-file';
|
||||
const packageJson = './package.json';
|
||||
const packageJsonBackup = `${packageJson}~`;
|
||||
|
||||
await generateLicenseFile(
|
||||
packageJson,
|
||||
'THIRD_PARTY_LICENSES'
|
||||
);
|
||||
await generateLicenseFile(packageJson, 'THIRD_PARTY_LICENSES');
|
||||
|
||||
const pkg = JSON.parse(
|
||||
await fs.readFile(packageJson, 'utf-8')
|
||||
);
|
||||
const pkg = JSON.parse(await fs.readFile(packageJson, 'utf-8'));
|
||||
|
||||
await fs.rename(packageJson, packageJsonBackup);
|
||||
|
||||
const devPackage = {
|
||||
...pkg,
|
||||
dependencies: pkg.devDependencies ?? {},
|
||||
devDependencies: {}
|
||||
devDependencies: {},
|
||||
};
|
||||
|
||||
/*
|
||||
@@ -45,14 +40,8 @@ const devPackage = {
|
||||
|
||||
https://github.com/TobyAndToby/generate-license-file/issues/752
|
||||
*/
|
||||
await fs.writeFile(
|
||||
packageJson,
|
||||
JSON.stringify(devPackage)
|
||||
);
|
||||
await fs.writeFile(packageJson, JSON.stringify(devPackage));
|
||||
|
||||
await generateLicenseFile(
|
||||
packageJson,
|
||||
'./THIRD_PARTY_LICENSES_DEV'
|
||||
);
|
||||
await generateLicenseFile(packageJson, './THIRD_PARTY_LICENSES_DEV');
|
||||
|
||||
await fs.rename(packageJsonBackup, packageJson);
|
||||
+10
-10
@@ -15,13 +15,12 @@ limitations under the License.
|
||||
-->
|
||||
|
||||
<script setup>
|
||||
import Navbar from './features/nav/components/Navbar.vue';
|
||||
import Footer from './features/footer/components/Footer.vue';
|
||||
|
||||
import { LANGUAGE_PATH } from './config/featureSettings.js';
|
||||
import { updatePageTitle } from './router';
|
||||
import AppFooter from './features/footer/components/AppFooter.vue';
|
||||
import AppNavbar from './features/nav/components/AppNavbar.vue';
|
||||
import { useSettings } from './features/settings/composables/useSettings.js';
|
||||
import { setLocale } from './i18n';
|
||||
import { updatePageTitle } from './router';
|
||||
import { computed, onMounted, watch, watchEffect } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
@@ -30,11 +29,11 @@ const route = useRoute();
|
||||
const { getSetting } = useSettings();
|
||||
const colorScheme = computed(() => getSetting(['appearance', 'colorScheme']));
|
||||
|
||||
const updateColorScheme = function updateColorScheme (newColorScheme) {
|
||||
const updateColorScheme = function updateColorScheme(newColorScheme) {
|
||||
document.body.style.setProperty(colorScheme, {
|
||||
auto: 'normal',
|
||||
dark: 'dark',
|
||||
light: 'light'
|
||||
light: 'light',
|
||||
});
|
||||
if (newColorScheme === 'dark') {
|
||||
document.body.classList.add('dark');
|
||||
@@ -46,7 +45,7 @@ const updateColorScheme = function updateColorScheme (newColorScheme) {
|
||||
} else {
|
||||
document.body.classList.remove('color-scheme-auto');
|
||||
}
|
||||
}
|
||||
};
|
||||
watch(colorScheme, (newColorScheme) => {
|
||||
updateColorScheme(newColorScheme);
|
||||
});
|
||||
@@ -61,11 +60,11 @@ onMounted(() => {
|
||||
|
||||
<template>
|
||||
<div id="app-wrapper">
|
||||
<Navbar />
|
||||
<AppNavbar />
|
||||
|
||||
<router-view class="main-content" />
|
||||
|
||||
<Footer />
|
||||
<AppFooter />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -73,7 +72,8 @@ onMounted(() => {
|
||||
#app-wrapper {
|
||||
--main-content-padding-x: 30px;
|
||||
--main-content-padding-y: 40px;
|
||||
--main-content-padding: var(--main-content-padding-y) var(--main-content-padding-x);
|
||||
--main-content-padding: var(--main-content-padding-y)
|
||||
var(--main-content-padding-x);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
+2
-2
@@ -14,9 +14,9 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
export const getAssetsUrl = function getAssetsUrl () {
|
||||
export const getAssetsUrl = function getAssetsUrl() {
|
||||
return new URL(
|
||||
`${import.meta.env.VITE_ASSETS_BUILD_ID}/`,
|
||||
import.meta.env.VITE_ASSETS_BASE_URL
|
||||
import.meta.env.VITE_ASSETS_BASE_URL,
|
||||
).href;
|
||||
};
|
||||
|
||||
@@ -15,8 +15,7 @@ limitations under the License.
|
||||
-->
|
||||
|
||||
<script setup>
|
||||
import Icon from '@/features/icons/components/Icon.vue';
|
||||
|
||||
import UIIcon from '@/features/icons/components/UIIcon.vue';
|
||||
import { useSettings } from '@/features/settings/composables/useSettings';
|
||||
import { computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
@@ -27,33 +26,36 @@ const { getSetting, setSetting } = useSettings();
|
||||
const colorScheme = computed(() => getSetting(['appearance', 'colorScheme']));
|
||||
|
||||
const colorSchemeNextMapper = {
|
||||
'light': 'dark',
|
||||
'dark': 'auto',
|
||||
'auto': 'light'
|
||||
light: 'dark',
|
||||
dark: 'auto',
|
||||
auto: 'light',
|
||||
};
|
||||
|
||||
const colorSchemeIconMapper = {
|
||||
'dark': 'crescent-moon',
|
||||
'light': 'sun',
|
||||
'auto': 'circle-black-white'
|
||||
dark: 'crescent-moon',
|
||||
light: 'sun',
|
||||
auto: 'circle-black-white',
|
||||
};
|
||||
|
||||
const getTooltipTranslation = function (colorScheme) {
|
||||
return t(`preferences.colorScheme.switch.${colorSchemeNextMapper[colorScheme]}`);
|
||||
const getTooltipTranslation = function getTooltipTranslation(colorScheme) {
|
||||
return t(
|
||||
`preferences.colorScheme.switch.${colorSchemeNextMapper[colorScheme]}`,
|
||||
);
|
||||
};
|
||||
|
||||
const setColorScheme = function setColorScheme (newColorScheme) {
|
||||
const setColorScheme = function setColorScheme(newColorScheme) {
|
||||
setSetting(['appearance', 'colorScheme'], newColorScheme);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button class="color-scheme-button"
|
||||
<button
|
||||
class="color-scheme-button"
|
||||
@click="setColorScheme(colorSchemeNextMapper[colorScheme])"
|
||||
:aria-label="getTooltipTranslation(colorScheme)"
|
||||
:title="getTooltipTranslation(colorScheme)"
|
||||
>
|
||||
<Icon
|
||||
<UIIcon
|
||||
:name="colorSchemeIconMapper[colorSchemeNextMapper[colorScheme]]"
|
||||
size="16"
|
||||
/>
|
||||
|
||||
@@ -21,9 +21,9 @@ import { describe, expect, test } from 'vitest';
|
||||
|
||||
const { getSetting } = useSettings();
|
||||
|
||||
const colorSchemeSettingPath = ['appearance', 'colorScheme']
|
||||
const colorSchemeSettingPath = ['appearance', 'colorScheme'];
|
||||
|
||||
const getWrapper = function getWrapper () {
|
||||
const getWrapper = function getWrapper() {
|
||||
return mountComponent(ColorSchemeButton);
|
||||
};
|
||||
|
||||
|
||||
+3
-6
@@ -16,20 +16,17 @@ limitations under the License.
|
||||
|
||||
<script setup>
|
||||
import LanguageSwitchButton from '@/features/i18n/components/LanguageSwitchButton.vue';
|
||||
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const startYear = 2026
|
||||
const currentYear = new Date().getFullYear()
|
||||
const startYear = 2026;
|
||||
const currentYear = new Date().getFullYear();
|
||||
|
||||
const copyrightPeriod =
|
||||
startYear === currentYear
|
||||
? `${currentYear}`
|
||||
: `${startYear}-${currentYear}`
|
||||
|
||||
|
||||
: `${startYear}-${currentYear}`;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -15,13 +15,12 @@ limitations under the License.
|
||||
-->
|
||||
|
||||
<script setup>
|
||||
import Icon from '@/features/icons/components/Icon.vue';
|
||||
|
||||
import { LANGUAGE_PATH } from '@/config/featureSettings';
|
||||
import UIIcon from '@/features/icons/components/UIIcon.vue';
|
||||
import { useSettings } from '@/features/settings/composables/useSettings';
|
||||
import { setLocale, SUPPORTED_LANGUAGES } from '@/i18n';
|
||||
import { ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { setLocale, SUPPORTED_LANGUAGES } from '@/i18n';
|
||||
|
||||
const { t, locale } = useI18n();
|
||||
const { setSetting, getSetting } = useSettings();
|
||||
@@ -29,33 +28,33 @@ const { setSetting, getSetting } = useSettings();
|
||||
const isOpen = ref(false);
|
||||
const languageDropdown = ref(null);
|
||||
|
||||
const selectLanguage = function selectLanguage (code) {
|
||||
const selectLanguage = function selectLanguage(code) {
|
||||
setSetting(LANGUAGE_PATH, code);
|
||||
close();
|
||||
};
|
||||
|
||||
watch(() => getSetting(LANGUAGE_PATH), setLocale);
|
||||
|
||||
const close = function () {
|
||||
document.removeEventListener('click', closeWrapperOnClickOutsite);
|
||||
const close = function close() {
|
||||
document.removeEventListener('click', closeWrapperOnClickOutside);
|
||||
isOpen.value = false;
|
||||
};
|
||||
|
||||
const closeWrapperOnClickOutsite = function (e) {
|
||||
const closeWrapperOnClickOutside = function closeWrapperOnClickOutside(e) {
|
||||
if (languageDropdown.value) {
|
||||
if (!languageDropdown.value.contains(e.target)) {
|
||||
close();
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const open = function () {
|
||||
const open = function open() {
|
||||
if (!isOpen.value) {
|
||||
isOpen.value = true;
|
||||
setTimeout(() => {
|
||||
document.addEventListener('click', closeWrapperOnClickOutsite);
|
||||
document.addEventListener('click', closeWrapperOnClickOutside);
|
||||
}, 0);
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -67,9 +66,11 @@ const open = function () {
|
||||
:aria-expanded="isOpen"
|
||||
aria-haspopup="listbox"
|
||||
>
|
||||
<span class="lang-code">{{ t(`preferences.locale.languages.${locale}`) }}</span>
|
||||
<span class="lang-code">{{
|
||||
t(`preferences.locale.languages.${locale}`)
|
||||
}}</span>
|
||||
<span class="chevron" :class="{ open: isOpen }">
|
||||
<Icon name="chevron-down" size="1em" />
|
||||
<UIIcon name="chevron-down" size="1em" />
|
||||
</span>
|
||||
</button>
|
||||
|
||||
@@ -79,7 +80,7 @@ const open = function () {
|
||||
class="language-dropdown"
|
||||
role="listbox"
|
||||
:style="{
|
||||
bottom: 'calc(var(--offset) + 2 * var(--trigger-padding-y) + 1em)' // easier to add auto adaption to the available space
|
||||
bottom: 'calc(var(--offset) + 2 * var(--trigger-padding-y) + 1em)', // easier to add auto adaption to the available space
|
||||
}"
|
||||
>
|
||||
<li
|
||||
@@ -90,7 +91,9 @@ const open = function () {
|
||||
:class="{ active: lang === locale }"
|
||||
@click="selectLanguage(lang)"
|
||||
>
|
||||
<span class="lang-label">{{ t(`preferences.locale.languages.${lang}`) }}</span>
|
||||
<span class="lang-label">{{
|
||||
t(`preferences.locale.languages.${lang}`)
|
||||
}}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -14,13 +14,13 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import LanguageSwitchButton from '../LanguageSwitchButton.vue';
|
||||
import { LANGUAGE_PATH } from '@/config/featureSettings.js';
|
||||
import { useSettings } from '@/features/settings/composables/useSettings.js';
|
||||
import { mountComponent } from '@/test-utils/mountComponent.js';
|
||||
import { expect, test, describe } from 'vitest';
|
||||
import LanguageSwitchButton from '../LanguageSwitchButton.vue';
|
||||
import { describe, expect, test } from 'vitest';
|
||||
|
||||
const getWrapper = function getWrapper () {
|
||||
const getWrapper = function getWrapper() {
|
||||
return mountComponent(LanguageSwitchButton);
|
||||
};
|
||||
|
||||
@@ -30,7 +30,9 @@ describe('LanguageSwitchButton', () => {
|
||||
|
||||
expect(wrapper.find('.language-button').exists()).toBe(true);
|
||||
expect(wrapper.find('.language-dropdown').exists()).toBe(false);
|
||||
expect(wrapper.find('.language-button').attributes('aria-expanded')).toBe('false');
|
||||
expect(
|
||||
wrapper.find('.language-button').attributes('aria-expanded'),
|
||||
).toBe('false');
|
||||
});
|
||||
|
||||
test('opens the dropdown when language button is clicked', async () => {
|
||||
@@ -52,12 +54,12 @@ describe('LanguageSwitchButton', () => {
|
||||
|
||||
const options = wrapper.findAll('.language-dropdown li');
|
||||
|
||||
const enOption = options.find(opt => opt.text().includes('en'));
|
||||
const enOption = options.find((opt) => opt.text().includes('en'));
|
||||
await enOption.trigger('click');
|
||||
expect(getSetting(LANGUAGE_PATH)).toBe('en');
|
||||
expect(wrapper.find('.language-dropdown').exists()).toBe(false);
|
||||
|
||||
const deOption = options.find(opt => opt.text().includes('de'));
|
||||
const deOption = options.find((opt) => opt.text().includes('de'));
|
||||
await deOption.trigger('click');
|
||||
expect(getSetting(LANGUAGE_PATH)).toBe('de');
|
||||
expect(wrapper.find('.language-dropdown').exists()).toBe(false);
|
||||
@@ -69,7 +71,7 @@ describe('LanguageSwitchButton', () => {
|
||||
await wrapper.find('.language-button').trigger('click');
|
||||
expect(wrapper.find('.language-dropdown').exists()).toBe(true);
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
const externalDiv = document.createElement('div');
|
||||
document.body.appendChild(externalDiv);
|
||||
|
||||
@@ -19,33 +19,36 @@ import { describe, expect, test } from 'vitest';
|
||||
|
||||
describe('navigatorLanguage', () => {
|
||||
test.for([
|
||||
{ navigatorLanguage: 'en', expected: 'en'},
|
||||
{ navigatorLanguage: 'de', expected: 'de'},
|
||||
{ navigatorLanguage: 'fr', expected: 'fr'},
|
||||
{ navigatorLanguage: 'en', expected: 'en' },
|
||||
{ navigatorLanguage: 'de', expected: 'de' },
|
||||
{ navigatorLanguage: 'fr', expected: 'fr' },
|
||||
|
||||
{ navigatorLanguage: 'en-US', expected: 'en'},
|
||||
{ navigatorLanguage: 'en-AU', expected: 'en'},
|
||||
{ navigatorLanguage: 'de-DE', expected: 'de'},
|
||||
{ navigatorLanguage: 'fr-FR', expected: 'fr'},
|
||||
{ navigatorLanguage: 'en-US', expected: 'en' },
|
||||
{ navigatorLanguage: 'en-AU', expected: 'en' },
|
||||
{ navigatorLanguage: 'de-DE', expected: 'de' },
|
||||
{ navigatorLanguage: 'fr-FR', expected: 'fr' },
|
||||
|
||||
{ navigatorLanguage: 'en-us', expected: 'en'},
|
||||
{ navigatorLanguage: 'en-au', expected: 'en'},
|
||||
{ navigatorLanguage: 'de-de', expected: 'de'},
|
||||
{ navigatorLanguage: 'fr-fr', expected: 'fr'},
|
||||
{ navigatorLanguage: 'en-us', expected: 'en' },
|
||||
{ navigatorLanguage: 'en-au', expected: 'en' },
|
||||
{ navigatorLanguage: 'de-de', expected: 'de' },
|
||||
{ navigatorLanguage: 'fr-fr', expected: 'fr' },
|
||||
|
||||
{ navigatorLanguage: 'zh-Hans-CN', expected: 'zh'},
|
||||
{ navigatorLanguage: 'zh-Hant-TW', expected: 'zh'},
|
||||
{ navigatorLanguage: 'uz-Latn-UZ', expected: 'uz'},
|
||||
{ navigatorLanguage: 'zh-Hans-CN', expected: 'zh' },
|
||||
{ navigatorLanguage: 'zh-Hant-TW', expected: 'zh' },
|
||||
{ navigatorLanguage: 'uz-Latn-UZ', expected: 'uz' },
|
||||
|
||||
{ navigatorLanguage: 'en-US-u-ca-gregory', expected: 'en'},
|
||||
{ navigatorLanguage: 'de-DE-u-co-phonebk', expected: 'de'},
|
||||
{ navigatorLanguage: 'zh-Hant-TW-u-co-phonebk', expected: 'zh'}
|
||||
])('returns language $expected with navigator language $navigatorLanguage', ({ navigatorLanguage, expected }) => {
|
||||
{ navigatorLanguage: 'en-US-u-ca-gregory', expected: 'en' },
|
||||
{ navigatorLanguage: 'de-DE-u-co-phonebk', expected: 'de' },
|
||||
{ navigatorLanguage: 'zh-Hant-TW-u-co-phonebk', expected: 'zh' },
|
||||
])(
|
||||
'returns language $expected with navigator language $navigatorLanguage',
|
||||
({ navigatorLanguage, expected }) => {
|
||||
Object.defineProperty(navigator, 'language', {
|
||||
value: navigatorLanguage,
|
||||
configurable: true
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
expect(getNavigatorLanguage()).toBe(expected);
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
export const getNavigatorLanguage = function getNavigatorLanguage () {
|
||||
export const getNavigatorLanguage = function getNavigatorLanguage() {
|
||||
const locale = new Intl.Locale(navigator.language);
|
||||
return locale.language;
|
||||
};
|
||||
@@ -15,33 +15,33 @@ limitations under the License.
|
||||
-->
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { ensureUnit } from '@/utils/cssDimensions';
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
name: {
|
||||
required: true,
|
||||
type: String
|
||||
type: String,
|
||||
},
|
||||
size: {
|
||||
type: [Number, String],
|
||||
default: 24
|
||||
}
|
||||
})
|
||||
default: 24,
|
||||
},
|
||||
});
|
||||
|
||||
const icons = import.meta.glob('@/assets/icons/*.svg', {
|
||||
eager: true,
|
||||
import: 'default'
|
||||
})
|
||||
import: 'default',
|
||||
});
|
||||
|
||||
const Icon = computed(() => icons[`/src/assets/icons/${props.name}.svg`])
|
||||
const icon = computed(() => icons[`/src/assets/icons/${props.name}.svg`]);
|
||||
|
||||
const size = computed(() => ensureUnit(props.size))
|
||||
const size = computed(() => ensureUnit(props.size));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="icon-container">
|
||||
<img :src="Icon" :style="{ height: `${size}`, width: `${size}` }" />
|
||||
<img :src="icon" :style="{ height: `${size}`, width: `${size}` }" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -15,8 +15,8 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
export class FetchError extends Error {
|
||||
constructor (message) {
|
||||
constructor(message) {
|
||||
super(message);
|
||||
this.name = 'FetchError';
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,29 +60,25 @@ describe('fetchLegalContent', () => {
|
||||
describe('getLegalNotice', () => {
|
||||
describe('success', () => {
|
||||
test('returns HTML body', async () => {
|
||||
fetchSpy.mockResolvedValue(
|
||||
new Response(exampleHtmlDocument)
|
||||
);
|
||||
fetchSpy.mockResolvedValue(new Response(exampleHtmlDocument));
|
||||
|
||||
expect(
|
||||
(await getLegalNotice()).trim()
|
||||
).toBe(exampleHtmlWithLinkClass);
|
||||
expect((await getLegalNotice()).trim()).toBe(
|
||||
exampleHtmlWithLinkClass,
|
||||
);
|
||||
});
|
||||
|
||||
test('returns HTML <main>', async () => {
|
||||
fetchSpy.mockResolvedValue(
|
||||
new Response(exampleHtmlDocumentWithMain)
|
||||
new Response(exampleHtmlDocumentWithMain),
|
||||
);
|
||||
|
||||
expect(
|
||||
(await getLegalNotice()).trim()
|
||||
).toBe(exampleHtmlWithLinkClass);
|
||||
expect((await getLegalNotice()).trim()).toBe(
|
||||
exampleHtmlWithLinkClass,
|
||||
);
|
||||
});
|
||||
|
||||
test('does not fail on success status code', async () => {
|
||||
fetchSpy.mockResolvedValue(
|
||||
new Response(null, { status: 206 })
|
||||
);
|
||||
fetchSpy.mockResolvedValue(new Response(null, { status: 206 }));
|
||||
|
||||
await getLegalNotice();
|
||||
});
|
||||
@@ -95,9 +91,7 @@ describe('fetchLegalContent', () => {
|
||||
});
|
||||
|
||||
test('throws FetchError on HTTP error', async () => {
|
||||
fetchSpy.mockResolvedValue(
|
||||
new Response(null, { status: 404 })
|
||||
)
|
||||
fetchSpy.mockResolvedValue(new Response(null, { status: 404 }));
|
||||
await expect(getLegalNotice()).rejects.toThrow(FetchError);
|
||||
});
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ limitations under the License.
|
||||
import { FetchError } from '../errors';
|
||||
import { getAssetsUrl } from '@/config/env';
|
||||
|
||||
const fetchHtml = async function fetchHtml (url, options) {
|
||||
const fetchHtml = async function fetchHtml(url, options) {
|
||||
let response;
|
||||
try {
|
||||
response = await fetch(url, options);
|
||||
@@ -29,16 +29,17 @@ const fetchHtml = async function fetchHtml (url, options) {
|
||||
else throw new FetchError();
|
||||
};
|
||||
|
||||
export const getLegalNotice = async function getLegalNotice () {
|
||||
const legalNoticeUrl = new URL('legal/legal_notice.html', getAssetsUrl()).href;
|
||||
export const getLegalNotice = async function getLegalNotice() {
|
||||
const legalNoticeUrl = new URL('legal/legal_notice.html', getAssetsUrl())
|
||||
.href;
|
||||
const html = await fetchHtml(legalNoticeUrl);
|
||||
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(html, 'text/html');
|
||||
|
||||
const legalNoticeContainer = doc.querySelector('main') ?? doc.body;
|
||||
legalNoticeContainer.querySelectorAll('a').forEach(
|
||||
(link) => link.classList.add('link')
|
||||
);
|
||||
legalNoticeContainer
|
||||
.querySelectorAll('a')
|
||||
.forEach((link) => link.classList.add('link'));
|
||||
return legalNoticeContainer.innerHTML;
|
||||
};
|
||||
|
||||
@@ -20,16 +20,18 @@ import { mountComponent } from '@/test-utils/mountComponent';
|
||||
import { flushPromises } from '@vue/test-utils';
|
||||
import { afterEach, describe, expect, test, vi } from 'vitest';
|
||||
|
||||
const getLegalNotice = vi.hoisted(() => vi.fn().mockResolvedValue('legal notice'));
|
||||
const getLegalNotice = vi.hoisted(() =>
|
||||
vi.fn().mockResolvedValue('legal notice'),
|
||||
);
|
||||
vi.mock('../../services/fetchLegalContent', async (importOriginal) => {
|
||||
const actual = await importOriginal();
|
||||
return {
|
||||
...actual,
|
||||
getLegalNotice
|
||||
getLegalNotice,
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => getLegalNotice.mockRestore());
|
||||
afterEach(getLegalNotice.mockRestore);
|
||||
|
||||
describe('LegalNoticeView', () => {
|
||||
test('shows loading indicator', () => {
|
||||
|
||||
@@ -15,9 +15,9 @@ limitations under the License.
|
||||
-->
|
||||
|
||||
<script setup>
|
||||
import ColorSchemeButton from '@/features/colorScheme/components/ColorSchemeButton.vue';
|
||||
import logo from '@/assets/images/logo.svg';
|
||||
import NavbarSearchBarWrapper from './NavbarSearchBarWrapper.vue';
|
||||
import logo from '@/assets/images/logo.svg';
|
||||
import ColorSchemeButton from '@/features/colorScheme/components/ColorSchemeButton.vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -33,7 +33,9 @@ import NavbarSearchBarWrapper from './NavbarSearchBarWrapper.vue';
|
||||
</ul>
|
||||
</nav>
|
||||
<div class="navbar-search-bar-wrapper-small-screens-wrapper">
|
||||
<NavbarSearchBarWrapper class="navbar-search-bar-wrapper-small-screens" />
|
||||
<NavbarSearchBarWrapper
|
||||
class="navbar-search-bar-wrapper-small-screens"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -60,7 +62,7 @@ import NavbarSearchBarWrapper from './NavbarSearchBarWrapper.vue';
|
||||
color: var(--dark);
|
||||
}
|
||||
|
||||
.global-nav .right-links a:hover{
|
||||
.global-nav .right-links a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
@@ -15,27 +15,33 @@ limitations under the License.
|
||||
-->
|
||||
|
||||
<script setup>
|
||||
import Searchbar from '@/features/search/components/Searchbar.vue';
|
||||
|
||||
import SearchBar from '@/features/search/components/SearchBar.vue';
|
||||
import { watch } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
const route = useRoute();
|
||||
const searchQueryModel = defineModel();
|
||||
|
||||
watch(() => route.name, name => {
|
||||
searchQueryModel.value = name === 'searchResults' ? route.query.q || '' : '';
|
||||
});
|
||||
watch(
|
||||
() => route.name,
|
||||
(name) => {
|
||||
searchQueryModel.value =
|
||||
name === 'searchResults' ? route.query.q || '' : '';
|
||||
},
|
||||
);
|
||||
|
||||
watch(() => route.query.q, q => {
|
||||
watch(
|
||||
() => route.query.q,
|
||||
(q) => {
|
||||
if (route.name === 'searchResults') {
|
||||
searchQueryModel.value = q || '';
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Searchbar
|
||||
<SearchBar
|
||||
v-if="route.name === 'searchResults'"
|
||||
v-model="searchQueryModel"
|
||||
auto-submit
|
||||
|
||||
+21
-13
@@ -15,26 +15,25 @@ limitations under the License.
|
||||
-->
|
||||
|
||||
<script setup>
|
||||
import Icon from '@/features/icons/components/Icon.vue';
|
||||
import UIIcon from '@/features/icons/components/UIIcon.vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
const searchQuery = defineModel();
|
||||
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const router = useRouter()
|
||||
const props = defineProps(['autoSubmit'])
|
||||
const router = useRouter();
|
||||
const props = defineProps(['autoSubmit']);
|
||||
|
||||
const submitSearch = function () {
|
||||
const submitSearch = function submitSearch() {
|
||||
if (props.autoSubmit !== undefined) {
|
||||
router.push({
|
||||
name: 'searchResults',
|
||||
query: { q: searchQuery.value }
|
||||
query: { q: searchQuery.value },
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -53,7 +52,11 @@ const submitSearch = function () {
|
||||
:title="t('search.searchBar.submit')"
|
||||
:aria-label="t('search.searchBar.submit')"
|
||||
>
|
||||
<Icon class="search-icon" name="magnifying-glass" size="1.1em" />
|
||||
<UIIcon
|
||||
class="search-icon"
|
||||
name="magnifying-glass"
|
||||
size="1.1em"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -70,7 +73,10 @@ const submitSearch = function () {
|
||||
align-items: center;
|
||||
border: 1.5px solid var(--border);
|
||||
box-shadow: 0 0px 32px var(--blue-box-shadow);
|
||||
border-radius: calc(var(--content-height) * 0.5 + var(--submit-button-padding-y) + var(--padding));
|
||||
border-radius: calc(
|
||||
var(--content-height) * 0.5 + var(--submit-button-padding-y) +
|
||||
var(--padding)
|
||||
);
|
||||
padding: var(--padding);
|
||||
padding-left: var(--padding-left);
|
||||
width: 100%;
|
||||
@@ -91,7 +97,9 @@ const submitSearch = function () {
|
||||
font-size: 1rem;
|
||||
height: calc(var(--content-height) + 2 * var(--submit-button-padding-y));
|
||||
width: calc(var(--content-height) + 2 * var(--submit-button-padding-y));
|
||||
border-radius: calc(var(--content-height) * 0.5 + var(--submit-button-padding-y));
|
||||
border-radius: calc(
|
||||
var(--content-height) * 0.5 + var(--submit-button-padding-y)
|
||||
);
|
||||
border: none;
|
||||
padding: var(--submit-button-padding-y) 20px;
|
||||
background: var(--primary-color);
|
||||
@@ -28,8 +28,7 @@ const { t } = useI18n();
|
||||
<p>{{ t('error.tryAgainToAnotherTime') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="search-results-container">
|
||||
</div>
|
||||
<div class="search-results-container"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -15,20 +15,14 @@ limitations under the License.
|
||||
-->
|
||||
|
||||
<script setup>
|
||||
import Searchbar from '../features/search/components/Searchbar.vue';
|
||||
import { ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import logo from '@/assets/images/logo.svg';
|
||||
import SearchBar from '@/features/search/components/SearchBar.vue';
|
||||
import { ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
const router = useRouter();
|
||||
|
||||
const searchQuery = ref('');
|
||||
|
||||
const submitSearch = function () {
|
||||
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -41,7 +35,12 @@ const submitSearch = function () {
|
||||
</header>
|
||||
|
||||
<div class="search-container">
|
||||
<Searchbar v-model="searchQuery" ref="searchbar" class="search-bar" auto-submit />
|
||||
<SearchBar
|
||||
v-model="searchQuery"
|
||||
ref="searchbar"
|
||||
class="search-bar"
|
||||
auto-submit
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -15,10 +15,8 @@ limitations under the License.
|
||||
-->
|
||||
|
||||
<script setup>
|
||||
import Icon from '@/features/icons/components/Icon.vue';
|
||||
|
||||
import { useSettingsStore } from '../stores/settingsStore';
|
||||
|
||||
import UIIcon from '@/features/icons/components/UIIcon.vue';
|
||||
import { computed, ref, useId, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
@@ -26,11 +24,11 @@ const { t } = useI18n();
|
||||
|
||||
const props = defineProps({
|
||||
setting: {
|
||||
required: true
|
||||
required: true,
|
||||
},
|
||||
path: {
|
||||
required: true
|
||||
}
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const store = useSettingsStore();
|
||||
@@ -43,29 +41,29 @@ const doneButtonInitial = ref(true);
|
||||
const inputId = useId();
|
||||
const inputLabelId = useId();
|
||||
|
||||
const inputType = computed(() => ({
|
||||
const inputType = computed(
|
||||
() =>
|
||||
({
|
||||
number: 'number',
|
||||
string: 'text'
|
||||
}[props.setting.type]
|
||||
));
|
||||
|
||||
const hasChanged = computed(() =>
|
||||
inputModel.value !== store.get(props.path)
|
||||
string: 'text',
|
||||
})[props.setting.type],
|
||||
);
|
||||
|
||||
const hasChanged = computed(() => inputModel.value !== store.get(props.path));
|
||||
|
||||
watch(store, (newValue) => {
|
||||
const newInputValue = newValue.get(props.path);
|
||||
if (inputModel.value === oldInputValue) {
|
||||
inputModel.value = newInputValue;
|
||||
oldInputValue = newInputValue;
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
watch(inputModel, () => {
|
||||
doneButtonInitial.value = false;
|
||||
});
|
||||
|
||||
const apply = function apply () {
|
||||
const apply = function apply() {
|
||||
store.set(props.path, inputModel.value);
|
||||
};
|
||||
</script>
|
||||
@@ -95,7 +93,7 @@ const apply = function apply () {
|
||||
:title="t('settings.input.done.title')"
|
||||
:aria-label="t('settings.input.done.ariaLabel')"
|
||||
>
|
||||
<Icon name="check-mark" />
|
||||
<UIIcon name="check-mark" />
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
@@ -131,7 +129,10 @@ const apply = function apply () {
|
||||
}
|
||||
|
||||
.input-container.done-button-shown .input {
|
||||
width: calc(100% - 1.5 * var(--input-padding) - 26px - 0.75 * var(--input-height) + 1.5px);
|
||||
width: calc(
|
||||
100% - 1.5 * var(--input-padding) - 26px - 0.75 * var(--input-height) +
|
||||
1.5px
|
||||
);
|
||||
}
|
||||
|
||||
.done-button {
|
||||
|
||||
@@ -15,22 +15,21 @@ limitations under the License.
|
||||
-->
|
||||
|
||||
<script setup>
|
||||
import Switch from './Switch.vue';
|
||||
import SettingsSelection from './SettingsSelection.vue';
|
||||
import SettingsInput from './SettingsInput.vue';
|
||||
import Selection from './Selection.vue';
|
||||
import SettingsPageLink from './SettingsPageLink.vue';
|
||||
|
||||
import SettingsSwitch from './SettingsSwitch.vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = defineProps({
|
||||
setting: {
|
||||
required: true
|
||||
required: true,
|
||||
},
|
||||
path: {
|
||||
required: true
|
||||
}
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -44,7 +43,7 @@ const props = defineProps({
|
||||
:key="setting"
|
||||
class="setting"
|
||||
>
|
||||
<Switch
|
||||
<SettingsSwitch
|
||||
v-if="setting.type === 'bool'"
|
||||
:setting="setting"
|
||||
:path="`${props.path}.${setting.name}`"
|
||||
@@ -54,7 +53,7 @@ const props = defineProps({
|
||||
:setting="setting"
|
||||
:path="`${props.path}.${setting.name}`"
|
||||
/>
|
||||
<Selection
|
||||
<SettingsSelection
|
||||
v-if="setting.type === 'selection'"
|
||||
:setting="setting"
|
||||
:path="`${props.path}.${setting.name}`"
|
||||
@@ -79,15 +78,15 @@ const props = defineProps({
|
||||
margin-bottom: calc(var(--size) - 0.2em);
|
||||
}
|
||||
|
||||
.section-heading[aria-level="2"] {
|
||||
.section-heading[aria-level='2'] {
|
||||
--size: 1.8em;
|
||||
}
|
||||
|
||||
.section-heading[aria-level="3"] {
|
||||
.section-heading[aria-level='3'] {
|
||||
--size: 1.6em;
|
||||
}
|
||||
|
||||
.section-heading[aria-level="4"] {
|
||||
.section-heading[aria-level='4'] {
|
||||
--size: 1.4em;
|
||||
}
|
||||
</style>
|
||||
@@ -15,8 +15,8 @@ limitations under the License.
|
||||
-->
|
||||
|
||||
<script setup>
|
||||
import Icon from '@/features/icons/components/Icon.vue';
|
||||
import { useSettingsPage } from '../composables/useSettingsPage';
|
||||
import UIIcon from '@/features/icons/components/UIIcon.vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
@@ -25,15 +25,15 @@ const { goToSettingsPage } = useSettingsPage();
|
||||
const props = defineProps({
|
||||
target: {
|
||||
required: true,
|
||||
type: String
|
||||
type: String,
|
||||
},
|
||||
displayName: {
|
||||
required: true,
|
||||
type: String
|
||||
}
|
||||
type: String,
|
||||
},
|
||||
});
|
||||
|
||||
const followLink = function followLink () {
|
||||
const followLink = function followLink() {
|
||||
goToSettingsPage(props.target);
|
||||
};
|
||||
</script>
|
||||
@@ -42,11 +42,13 @@ const followLink = function followLink () {
|
||||
<button
|
||||
class="settings-button button settings-page-link"
|
||||
:title="props.displayName"
|
||||
:aria-label="t('settings.pages.link.ariaLabel', { page: props.displayName })"
|
||||
:aria-label="
|
||||
t('settings.pages.link.ariaLabel', { page: props.displayName })
|
||||
"
|
||||
@click="followLink"
|
||||
>
|
||||
{{ props.displayName }}
|
||||
<Icon name="chevron-right" size="1em" />
|
||||
<UIIcon name="chevron-right" size="1em" />
|
||||
</button>
|
||||
</template>
|
||||
|
||||
|
||||
+23
-12
@@ -15,14 +15,14 @@ limitations under the License.
|
||||
-->
|
||||
|
||||
<script setup>
|
||||
import Icon from '@/features/icons/components/Icon.vue';
|
||||
import { useSettingsStore } from '../stores/settingsStore';
|
||||
import { computed, ref, useId, watch } from 'vue';
|
||||
import UIIcon from '@/features/icons/components/UIIcon.vue';
|
||||
import { computed, useId } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const normalizeSelectedValue = function normalizeSelectedValue (value) {
|
||||
const normalizeSelectedValue = function normalizeSelectedValue(value) {
|
||||
if (props.setting.allowMultiple) {
|
||||
if (Array.isArray(value)) {
|
||||
return value;
|
||||
@@ -44,19 +44,22 @@ const normalizeSelectedValue = function normalizeSelectedValue (value) {
|
||||
|
||||
const props = defineProps({
|
||||
setting: {
|
||||
required: true
|
||||
required: true,
|
||||
},
|
||||
path: {
|
||||
required: true
|
||||
}
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const store = useSettingsStore();
|
||||
|
||||
const optionType = computed(() => props.setting.allowMultiple ? 'checkbox' : 'radio');
|
||||
const optionType = computed(() =>
|
||||
props.setting.allowMultiple ? 'checkbox' : 'radio',
|
||||
);
|
||||
const selected = computed({
|
||||
get: () => normalizeSelectedValue(store.get(props.path) ?? props.setting.default),
|
||||
set: (value) => store.set(props.path, value)
|
||||
get: () =>
|
||||
normalizeSelectedValue(store.get(props.path) ?? props.setting.default),
|
||||
set: (value) => store.set(props.path, value),
|
||||
});
|
||||
|
||||
const labelId = useId();
|
||||
@@ -72,7 +75,11 @@ const labelId = useId();
|
||||
<label
|
||||
class="settings-button"
|
||||
:title="t(option.i18n)"
|
||||
:aria-label="t('settings.selection.option.ariaLabel', { option: t(option.i18n) })"
|
||||
:aria-label="
|
||||
t('settings.selection.option.ariaLabel', {
|
||||
option: t(option.i18n),
|
||||
})
|
||||
"
|
||||
>
|
||||
<input
|
||||
:type="optionType"
|
||||
@@ -80,9 +87,13 @@ const labelId = useId();
|
||||
:value="option.name"
|
||||
class="option-input"
|
||||
v-model="selected"
|
||||
>
|
||||
/>
|
||||
<div class="option-check-mark-wrapper">
|
||||
<Icon class="option-check-mark" name="check-mark" size="1em" />
|
||||
<UIIcon
|
||||
class="option-check-mark"
|
||||
name="check-mark"
|
||||
size="1em"
|
||||
/>
|
||||
</div>
|
||||
<div class="input-label">
|
||||
{{ t(option.i18n) }}
|
||||
+4
-4
@@ -23,11 +23,11 @@ const { t } = useI18n();
|
||||
|
||||
const props = defineProps({
|
||||
setting: {
|
||||
required: true
|
||||
required: true,
|
||||
},
|
||||
path: {
|
||||
required: true
|
||||
}
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const labelId = useId();
|
||||
@@ -42,7 +42,7 @@ watch(store, (newStore) => {
|
||||
// set value after registering watcher to avoid immediate value change
|
||||
enabled.value = store.get(props.path) ?? props.setting.default;
|
||||
|
||||
const toggle = function toggle () {
|
||||
const toggle = function toggle() {
|
||||
enabled.value = !enabled.value;
|
||||
store.set(props.path, enabled.value);
|
||||
};
|
||||
@@ -14,25 +14,36 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { useSettingsStore } from '../../stores/settingsStore.js';
|
||||
import SettingsInput from '../SettingsInput.vue';
|
||||
import { mountComponent } from '@/test-utils/mountComponent';
|
||||
import { useSettingsStore } from '../../stores/settingsStore.js';
|
||||
import { describe, expect, test } from 'vitest';
|
||||
import { nextTick } from 'vue';
|
||||
|
||||
const getWrapper = function getWrapper({ type = 'string', i18n = 'input', defaultValue = undefined, piniaOptions = {} } = {}) {
|
||||
return mountComponent(SettingsInput, {
|
||||
const getWrapper = function getWrapper({
|
||||
type = 'string',
|
||||
i18n = 'input',
|
||||
defaultValue = undefined,
|
||||
piniaOptions = {},
|
||||
} = {}) {
|
||||
return mountComponent(
|
||||
SettingsInput,
|
||||
{
|
||||
attrs: {
|
||||
setting: {
|
||||
name: 'input',
|
||||
default: defaultValue,
|
||||
i18n,
|
||||
type
|
||||
type,
|
||||
},
|
||||
path: 'input'
|
||||
}
|
||||
}, {}, [], piniaOptions);
|
||||
}
|
||||
path: 'input',
|
||||
},
|
||||
},
|
||||
{},
|
||||
[],
|
||||
piniaOptions,
|
||||
);
|
||||
};
|
||||
|
||||
describe('SettingsInput', () => {
|
||||
test('uses correct input type', () => {
|
||||
@@ -63,8 +74,8 @@ describe('SettingsInput', () => {
|
||||
setupStores: () => {
|
||||
const store = useSettingsStore();
|
||||
store.settings.input = value;
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
const inputElement = stringSettingWrapper.find('input');
|
||||
|
||||
@@ -76,7 +87,7 @@ describe('SettingsInput', () => {
|
||||
const wrapper = getWrapper({
|
||||
type: 'string',
|
||||
i18n: 'input',
|
||||
defaultValue: value
|
||||
defaultValue: value,
|
||||
});
|
||||
const inputElement = wrapper.find('input');
|
||||
|
||||
@@ -89,13 +100,13 @@ describe('SettingsInput', () => {
|
||||
const wrapper = getWrapper({
|
||||
type: 'string',
|
||||
i18n: 'input',
|
||||
defaultValue: defaultValue,
|
||||
defaultValue,
|
||||
piniaOptions: {
|
||||
setupStores: () => {
|
||||
const store = useSettingsStore();
|
||||
store.settings.input = storeValue;
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
const inputElement = wrapper.find('input');
|
||||
|
||||
@@ -191,18 +202,22 @@ describe('SettingsInput', () => {
|
||||
test('shows correct translation', () => {
|
||||
const translation = 'Text input';
|
||||
|
||||
const wrapper = mountComponent(SettingsInput, {
|
||||
const wrapper = mountComponent(
|
||||
SettingsInput,
|
||||
{
|
||||
attrs: {
|
||||
setting: {
|
||||
name: 'input',
|
||||
i18n: 'input',
|
||||
type: 'string',
|
||||
},
|
||||
path: 'input'
|
||||
}
|
||||
}, {
|
||||
input: translation
|
||||
});
|
||||
path: 'input',
|
||||
},
|
||||
},
|
||||
{
|
||||
input: translation,
|
||||
},
|
||||
);
|
||||
|
||||
const label = wrapper.find('label');
|
||||
expect(label.text()).toBe(translation);
|
||||
|
||||
@@ -14,44 +14,53 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import Selection from '../Selection.vue';
|
||||
import SettingsSelection from '../SettingsSelection.vue';
|
||||
import SettingsInput from '../SettingsInput.vue';
|
||||
import SettingsPage from '../SettingsPage.vue';
|
||||
import SettingsPageLink from '../SettingsPageLink.vue';
|
||||
import Switch from '../Switch.vue';
|
||||
import SettingsSwitch from '../SettingsSwitch.vue';
|
||||
import { mountComponent } from '@/test-utils/mountComponent.js';
|
||||
import { describe, expect, test } from 'vitest';
|
||||
|
||||
|
||||
const getWrapper = function getWrapper({ content = [], translations = {}, piniaOptions = {} }) {
|
||||
return mountComponent(SettingsPage, {
|
||||
const getWrapper = function getWrapper({
|
||||
content = [],
|
||||
translations = {},
|
||||
piniaOptions = {},
|
||||
}) {
|
||||
return mountComponent(
|
||||
SettingsPage,
|
||||
{
|
||||
attrs: {
|
||||
setting: {
|
||||
name: 'section',
|
||||
type: 'section',
|
||||
i18n: 'section',
|
||||
content: content
|
||||
content,
|
||||
},
|
||||
path: 'section'
|
||||
}
|
||||
}, translations, [], piniaOptions);
|
||||
path: 'section',
|
||||
},
|
||||
},
|
||||
translations,
|
||||
[],
|
||||
piniaOptions,
|
||||
);
|
||||
};
|
||||
|
||||
const exampleSettings = [
|
||||
{
|
||||
type: 'bool',
|
||||
name: 'b0',
|
||||
i18n: 'settings.section.b0'
|
||||
i18n: 'settings.section.b0',
|
||||
},
|
||||
{
|
||||
type: 'number',
|
||||
name: 'n0',
|
||||
i18n: 'settings.section.n0'
|
||||
i18n: 'settings.section.n0',
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'st0',
|
||||
i18n: 'settings.section1.st0'
|
||||
i18n: 'settings.section1.st0',
|
||||
},
|
||||
{
|
||||
type: 'selection',
|
||||
@@ -61,14 +70,14 @@ const exampleSettings = [
|
||||
{ name: 'o0', i18n: 'settings.section.se0.o0' },
|
||||
{ name: 'o1', i18n: 'settings.section.se0.o1' },
|
||||
{ name: 'o2', i18n: 'settings.section.se0.o2' },
|
||||
{ name: 'o3', i18n: 'settings.section.se0.o3' }
|
||||
]
|
||||
{ name: 'o3', i18n: 'settings.section.se0.o3' },
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
name: 's0',
|
||||
i18n: 'settings.section.s0',
|
||||
content: []
|
||||
content: [],
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
@@ -79,17 +88,17 @@ const exampleSettings = [
|
||||
type: 'bool',
|
||||
name: 'b0',
|
||||
i18n: 'settings.section.s1.b0',
|
||||
default: true
|
||||
}
|
||||
]
|
||||
}
|
||||
default: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
describe('SettingsPage', () => {
|
||||
test('shows correct heading', () => {
|
||||
const translation = 'Example section';
|
||||
const wrapper = getWrapper({
|
||||
translations: { section: translation }
|
||||
translations: { section: translation },
|
||||
});
|
||||
|
||||
const heading = wrapper.find('.section-heading');
|
||||
@@ -98,20 +107,21 @@ describe('SettingsPage', () => {
|
||||
|
||||
test('shows settings correctly', () => {
|
||||
const wrapper = getWrapper({
|
||||
content: exampleSettings
|
||||
content: exampleSettings,
|
||||
});
|
||||
|
||||
|
||||
const settingsWrappers = wrapper.findAll('.setting');
|
||||
expect(settingsWrappers.length).toBe(exampleSettings.length);
|
||||
|
||||
exampleSettings.forEach((expectedSetting, index) => {
|
||||
const settingWrapper = settingsWrappers[index];
|
||||
|
||||
const switchSetting = settingWrapper.findComponent(Switch);
|
||||
const switchSetting = settingWrapper.findComponent(SettingsSwitch);
|
||||
const inputSetting = settingWrapper.findComponent(SettingsInput);
|
||||
const selectionSetting = settingWrapper.findComponent(Selection);
|
||||
const settingsPageLink = settingWrapper.findComponent(SettingsPageLink);
|
||||
const selectionSetting =
|
||||
settingWrapper.findComponent(SettingsSelection);
|
||||
const settingsPageLink =
|
||||
settingWrapper.findComponent(SettingsPageLink);
|
||||
|
||||
let setting;
|
||||
if (expectedSetting.type === 'bool') {
|
||||
@@ -121,7 +131,10 @@ describe('SettingsPage', () => {
|
||||
expect(settingsPageLink.exists()).toBe(false);
|
||||
|
||||
setting = switchSetting;
|
||||
} else if (expectedSetting.type === 'string' || expectedSetting.type === 'number') {
|
||||
} else if (
|
||||
expectedSetting.type === 'string' ||
|
||||
expectedSetting.type === 'number'
|
||||
) {
|
||||
expect(switchSetting.exists()).toBe(false);
|
||||
expect(inputSetting.exists()).toBe(true);
|
||||
expect(selectionSetting.exists()).toBe(false);
|
||||
@@ -150,10 +163,14 @@ describe('SettingsPage', () => {
|
||||
expectedSetting.type === 'selection'
|
||||
) {
|
||||
expect(setting.props('setting')).toMatchObject(expectedSetting);
|
||||
expect(setting.props('path')).toBe(`section.${expectedSetting.name}`);
|
||||
expect(setting.props('path')).toBe(
|
||||
`section.${expectedSetting.name}`,
|
||||
);
|
||||
} else {
|
||||
expect(setting.props('displayName')).toBe(expectedSetting.i18n);
|
||||
expect(setting.props('target')).toBe(`section.${expectedSetting.name}`);
|
||||
expect(setting.props('target')).toBe(
|
||||
`section.${expectedSetting.name}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -22,21 +22,19 @@ import { createI18n } from 'vue-i18n';
|
||||
const goToSettingsPage = vi.fn();
|
||||
vi.mock('../../composables/useSettingsPage.js', () => ({
|
||||
useSettingsPage: () => ({
|
||||
goToSettingsPage
|
||||
})
|
||||
goToSettingsPage,
|
||||
}),
|
||||
}));
|
||||
|
||||
const getWrapper = function getWrapper({ target, displayName }) {
|
||||
return mount(SettingsPageLink, {
|
||||
attrs: {
|
||||
target,
|
||||
displayName
|
||||
displayName,
|
||||
},
|
||||
global: {
|
||||
plugins: [
|
||||
createI18n()
|
||||
]
|
||||
}
|
||||
plugins: [createI18n()],
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
+41
-32
@@ -14,21 +14,23 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import Selection from '../Selection.vue';
|
||||
import { mountComponent } from '@/test-utils/mountComponent.js';
|
||||
import { useSettingsStore } from '../../stores/settingsStore.js';
|
||||
import SettingsSelection from '../SettingsSelection.vue';
|
||||
import { mountComponent } from '@/test-utils/mountComponent.js';
|
||||
import { expect, describe, test } from 'vitest';
|
||||
import { nextTick } from 'vue';
|
||||
|
||||
const getWrapper = function getWrapper ({
|
||||
const getWrapper = function getWrapper({
|
||||
i18n = 'selection',
|
||||
options = [],
|
||||
allowMultiple = false,
|
||||
defaultValue = undefined,
|
||||
translations = {},
|
||||
piniaOptions = {}
|
||||
piniaOptions = {},
|
||||
} = {}) {
|
||||
return mountComponent(Selection, {
|
||||
return mountComponent(
|
||||
SettingsSelection,
|
||||
{
|
||||
attrs: {
|
||||
setting: {
|
||||
name: 'selection',
|
||||
@@ -36,24 +38,28 @@ const getWrapper = function getWrapper ({
|
||||
i18n,
|
||||
options,
|
||||
allowMultiple,
|
||||
default: defaultValue
|
||||
default: defaultValue,
|
||||
},
|
||||
path: 'selection'
|
||||
}
|
||||
}, translations, [], piniaOptions);
|
||||
path: 'selection',
|
||||
},
|
||||
},
|
||||
translations,
|
||||
[],
|
||||
piniaOptions,
|
||||
);
|
||||
};
|
||||
|
||||
const getChecked = function getChecked (inputs) {
|
||||
const getChecked = function getChecked(inputs) {
|
||||
return inputs.map((input) => input.element.matches(':checked'));
|
||||
};
|
||||
|
||||
const exampleOptions = [
|
||||
{ name: 'o0', i18n: 'o0' },
|
||||
{ name: 'o1', i18n: 'o1' },
|
||||
{ name: 'o2', i18n: 'o2' }
|
||||
{ name: 'o2', i18n: 'o2' },
|
||||
];
|
||||
|
||||
describe('Selection', () => {
|
||||
describe('SettingsSelection', () => {
|
||||
test('displays options correctly', () => {
|
||||
const wrapper = getWrapper({ options: exampleOptions });
|
||||
|
||||
@@ -71,7 +77,7 @@ describe('Selection', () => {
|
||||
expect(input.attributes('name')).toBe('selection');
|
||||
|
||||
expect(input.element.matches(':checked')).toBe(false);
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
test('shows value from store', () => {
|
||||
@@ -81,8 +87,8 @@ describe('Selection', () => {
|
||||
setupStores: () => {
|
||||
const store = useSettingsStore();
|
||||
store.set('selection', 'o1');
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const inputs = wrapper.findAll('input');
|
||||
@@ -97,8 +103,8 @@ describe('Selection', () => {
|
||||
setupStores: () => {
|
||||
const store = useSettingsStore();
|
||||
store.set('selection', 'o1');
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
const settings = useSettingsStore();
|
||||
|
||||
@@ -123,9 +129,9 @@ describe('Selection', () => {
|
||||
setupStores: () => {
|
||||
const store = useSettingsStore();
|
||||
store.set('selection', ['o1', 'o2']);
|
||||
}
|
||||
},
|
||||
allowMultiple: true
|
||||
},
|
||||
allowMultiple: true,
|
||||
});
|
||||
|
||||
const inputs = wrapper.findAll('input');
|
||||
@@ -136,7 +142,7 @@ describe('Selection', () => {
|
||||
test('shows default value', () => {
|
||||
const wrapper = getWrapper({
|
||||
options: exampleOptions,
|
||||
defaultValue: 'o2'
|
||||
defaultValue: 'o2',
|
||||
});
|
||||
|
||||
const inputs = wrapper.findAll('input');
|
||||
@@ -148,7 +154,7 @@ describe('Selection', () => {
|
||||
const wrapper = getWrapper({
|
||||
options: exampleOptions,
|
||||
defaultValue: ['o0', 'o1'],
|
||||
allowMultiple: true
|
||||
allowMultiple: true,
|
||||
});
|
||||
|
||||
const inputs = wrapper.findAll('input');
|
||||
@@ -164,8 +170,8 @@ describe('Selection', () => {
|
||||
setupStores: () => {
|
||||
const store = useSettingsStore();
|
||||
store.set('selection', 'o1');
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const inputs = wrapper.findAll('input');
|
||||
@@ -199,33 +205,36 @@ describe('Selection', () => {
|
||||
});
|
||||
|
||||
test('stores multiple values', async () => {
|
||||
const setValue = async function setValue (input, value) {
|
||||
const setValue = async function setValue(input, value) {
|
||||
input.setValue(value);
|
||||
await nextTick();
|
||||
};
|
||||
|
||||
const check = function check (input) {
|
||||
const check = function check(input) {
|
||||
return setValue(input, true);
|
||||
};
|
||||
|
||||
const uncheck = function uncheck (input) {
|
||||
const uncheck = function uncheck(input) {
|
||||
return setValue(input, false);
|
||||
};
|
||||
|
||||
const expectSelectionValue = function expectSelectionValue (value, options = exampleOptions) {
|
||||
const expectSelectionValue = function expectSelectionValue(
|
||||
value,
|
||||
options = exampleOptions,
|
||||
) {
|
||||
value.sort();
|
||||
expect(store.get('selection').sort()).toStrictEqual(value);
|
||||
const checkedInputs = getChecked(inputs);
|
||||
expect(
|
||||
options
|
||||
.map((option) => option.name)
|
||||
.filter((entry, i) => checkedInputs[i])
|
||||
.filter((_, i) => checkedInputs[i]),
|
||||
).toStrictEqual(value);
|
||||
};
|
||||
|
||||
const wrapper = getWrapper({
|
||||
options: exampleOptions,
|
||||
allowMultiple: true
|
||||
allowMultiple: true,
|
||||
});
|
||||
|
||||
const store = useSettingsStore();
|
||||
@@ -237,7 +246,7 @@ describe('Selection', () => {
|
||||
const inputO2 = inputs[2];
|
||||
|
||||
await check(inputO0);
|
||||
expectSelectionValue(['o0'])
|
||||
expectSelectionValue(['o0']);
|
||||
|
||||
await uncheck(inputO0);
|
||||
expectSelectionValue([]);
|
||||
@@ -283,8 +292,8 @@ describe('Selection', () => {
|
||||
const translation = 'Selection setting';
|
||||
const wrapper = getWrapper({
|
||||
translations: {
|
||||
selection: translation
|
||||
}
|
||||
selection: translation,
|
||||
},
|
||||
});
|
||||
|
||||
const label = wrapper.find('.label');
|
||||
+22
-16
@@ -14,32 +14,38 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import Switch from '../Switch.vue';
|
||||
import { mountComponent } from '@/test-utils/mountComponent.js';
|
||||
import { useSettingsStore } from '../../stores/settingsStore.js';
|
||||
import { expect, describe, test } from 'vitest';
|
||||
import SettingsSwitch from '../SettingsSwitch.vue';
|
||||
import { mountComponent } from '@/test-utils/mountComponent.js';
|
||||
import { describe, expect, test } from 'vitest';
|
||||
import { nextTick } from 'vue';
|
||||
|
||||
const getWrapper = function getWrapper ({
|
||||
const getWrapper = function getWrapper({
|
||||
i18n = 'switch1',
|
||||
defaultValue = undefined,
|
||||
translations = {},
|
||||
piniaOptions = {}
|
||||
piniaOptions = {},
|
||||
} = {}) {
|
||||
return mountComponent(Switch, {
|
||||
return mountComponent(
|
||||
SettingsSwitch,
|
||||
{
|
||||
attrs: {
|
||||
setting: {
|
||||
type: 'bool',
|
||||
name: 'switch',
|
||||
i18n,
|
||||
default: defaultValue
|
||||
default: defaultValue,
|
||||
},
|
||||
path: 'switch'
|
||||
}
|
||||
}, translations, [], piniaOptions);
|
||||
path: 'switch',
|
||||
},
|
||||
},
|
||||
translations,
|
||||
[],
|
||||
piniaOptions,
|
||||
);
|
||||
};
|
||||
|
||||
describe('Switch', () => {
|
||||
describe('SettingsSwitch', () => {
|
||||
test('shows value from store', async () => {
|
||||
const wrapper = getWrapper();
|
||||
|
||||
@@ -61,7 +67,7 @@ describe('Switch', () => {
|
||||
|
||||
test('shows default value', () => {
|
||||
const wrapper = getWrapper({
|
||||
defaultValue: true
|
||||
defaultValue: true,
|
||||
});
|
||||
|
||||
const switchElement = wrapper.find('.switch-wrapper');
|
||||
@@ -75,8 +81,8 @@ describe('Switch', () => {
|
||||
setupStores: () => {
|
||||
const store = useSettingsStore();
|
||||
store.set('switch', false);
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const switchElement = wrapper.find('.switch-wrapper');
|
||||
@@ -116,8 +122,8 @@ describe('Switch', () => {
|
||||
const translation = 'Switch';
|
||||
|
||||
const wrapper = getWrapper({
|
||||
translations: { switch1: translation }
|
||||
})
|
||||
translations: { switch1: translation },
|
||||
});
|
||||
|
||||
const label = wrapper.find('label');
|
||||
expect(label.text()).toBe(translation);
|
||||
@@ -33,17 +33,17 @@ vi.mock('../../config.js', () => ({
|
||||
type: 'number',
|
||||
i18n: '',
|
||||
name: 'number',
|
||||
default: settingDefaultValue
|
||||
default: settingDefaultValue,
|
||||
},
|
||||
{
|
||||
type: 'bool',
|
||||
i18n: '',
|
||||
name: 'bool'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
name: 'bool',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
}));
|
||||
|
||||
describe('useSettings', () => {
|
||||
@@ -59,7 +59,7 @@ describe('useSettings', () => {
|
||||
});
|
||||
|
||||
test('returns default value', () => {
|
||||
const { getSetting, setSetting } = useSettings();
|
||||
const { getSetting } = useSettings();
|
||||
expect(getSetting(settingPath)).toBe(settingDefaultValue);
|
||||
});
|
||||
|
||||
@@ -85,7 +85,7 @@ beforeEach(() => {
|
||||
setActivePinia(
|
||||
createTestingPinia({
|
||||
createSpy: vi.fn,
|
||||
stubActions: false
|
||||
})
|
||||
stubActions: false,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -20,8 +20,8 @@ import { describe, expect, test, vi } from 'vitest';
|
||||
const pushRoute = vi.fn();
|
||||
vi.mock('vue-router', () => ({
|
||||
useRouter: () => ({
|
||||
push: pushRoute
|
||||
})
|
||||
push: pushRoute,
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('useSettingsPage', () => {
|
||||
@@ -32,8 +32,8 @@ describe('useSettingsPage', () => {
|
||||
expect(pushRoute).toHaveBeenCalledWith({
|
||||
name: 'settings',
|
||||
params: {
|
||||
rest: ['a', 'b', 'c', 'd', 'e']
|
||||
}
|
||||
rest: ['a', 'b', 'c', 'd', 'e'],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,20 +21,18 @@ import { getSettingRecursively } from '../utils/getSetting';
|
||||
/**
|
||||
* Provides access to the stored settings.
|
||||
*/
|
||||
export const useSettings = function useSettings () {
|
||||
export const useSettings = function useSettings() {
|
||||
/**
|
||||
* Returns the value of a specific setting.
|
||||
* @param {string[]} key - The settings key.
|
||||
* @return The value of the setting.
|
||||
*/
|
||||
const getSetting = function getSetting (key) {
|
||||
const getSetting = function getSetting(key) {
|
||||
const settingsStore = useSettingsStore();
|
||||
|
||||
return (
|
||||
settingsStore.get(key.join('.')) ??
|
||||
getSettingRecursively(
|
||||
key, settings.contents ?? []
|
||||
)?.default
|
||||
getSettingRecursively(key, settings.contents ?? [])?.default
|
||||
);
|
||||
};
|
||||
|
||||
@@ -43,7 +41,7 @@ export const useSettings = function useSettings () {
|
||||
* @param {string[]} key - The setting key.
|
||||
* @param value - The new value for the setting.
|
||||
*/
|
||||
const setSetting = function setSetting (key, value) {
|
||||
const setSetting = function setSetting(key, value) {
|
||||
const settingsStore = useSettingsStore();
|
||||
settingsStore.set(key.join('.'), value);
|
||||
};
|
||||
|
||||
@@ -17,15 +17,15 @@ limitations under the License.
|
||||
import { getSettingsPagePathSegments } from '../utils/settingsPage';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
export const useSettingsPage = function useSettingsPage () {
|
||||
export const useSettingsPage = function useSettingsPage() {
|
||||
const router = useRouter();
|
||||
|
||||
const goToSettingsPage = function goToSettingsPage (pagePath) {
|
||||
const goToSettingsPage = function goToSettingsPage(pagePath) {
|
||||
return router.push({
|
||||
name: 'settings',
|
||||
params: {
|
||||
rest: getSettingsPagePathSegments(pagePath)
|
||||
}
|
||||
rest: getSettingsPagePathSegments(pagePath),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -40,10 +40,10 @@ export default {
|
||||
default: defaultLanguage,
|
||||
options: SUPPORTED_LANGUAGES.map((language) => ({
|
||||
name: language,
|
||||
i18n: `preferences.locale.languages.${language}`
|
||||
}))
|
||||
}
|
||||
]
|
||||
i18n: `preferences.locale.languages.${language}`,
|
||||
})),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'appearance',
|
||||
@@ -57,19 +57,19 @@ export default {
|
||||
options: [
|
||||
{
|
||||
name: 'auto',
|
||||
i18n: 'settings.settings.appearance.contents.colorScheme.options.auto'
|
||||
i18n: 'settings.settings.appearance.contents.colorScheme.options.auto',
|
||||
},
|
||||
{
|
||||
name: 'light',
|
||||
i18n: 'settings.settings.appearance.contents.colorScheme.options.light'
|
||||
i18n: 'settings.settings.appearance.contents.colorScheme.options.light',
|
||||
},
|
||||
{
|
||||
name: 'dark',
|
||||
i18n: 'settings.settings.appearance.contents.colorScheme.options.dark'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
i18n: 'settings.settings.appearance.contents.colorScheme.options.dark',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -17,26 +17,30 @@ limitations under the License.
|
||||
import { defineStore } from 'pinia';
|
||||
import { reactive } from 'vue';
|
||||
|
||||
export const useSettingsStore = defineStore('settings', () => {
|
||||
export const useSettingsStore = defineStore(
|
||||
'settings',
|
||||
() => {
|
||||
const settings = reactive({});
|
||||
|
||||
const set = function set (key, value) {
|
||||
const set = function set(key, value) {
|
||||
settings[key] = value;
|
||||
};
|
||||
|
||||
const get = function get (key) {
|
||||
const get = function get(key) {
|
||||
return settings[key];
|
||||
};
|
||||
|
||||
const remove = function remove (key) {
|
||||
const remove = function remove(key) {
|
||||
delete settings[key];
|
||||
};
|
||||
|
||||
const clear = function clear () {
|
||||
Object.keys(settings).forEach(key => delete settings[key]);
|
||||
const clear = function clear() {
|
||||
Object.keys(settings).forEach((key) => delete settings[key]);
|
||||
};
|
||||
|
||||
return { set, get, remove, clear, settings };
|
||||
}, {
|
||||
persist: true
|
||||
});
|
||||
},
|
||||
{
|
||||
persist: true,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -15,31 +15,36 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { getSetting, getSettingRecursively } from '../getSetting.js';
|
||||
import { test, expect } from 'vitest';
|
||||
import { describe, test, expect } from 'vitest';
|
||||
|
||||
test.for([
|
||||
({ settings: [], name: '', expected: undefined }),
|
||||
({ settings: [], name: 'a', expected: undefined }),
|
||||
({ settings: [
|
||||
describe('getSetting', () => {
|
||||
test.for([
|
||||
{ settings: [], name: '', expected: undefined },
|
||||
{ settings: [], name: 'a', expected: undefined },
|
||||
{
|
||||
settings: [
|
||||
{
|
||||
type: 'bool',
|
||||
name: 'a',
|
||||
i18n: 'a'
|
||||
}
|
||||
], name: 'a', expected: {
|
||||
i18n: 'a',
|
||||
},
|
||||
],
|
||||
name: 'a',
|
||||
expected: {
|
||||
type: 'bool',
|
||||
name: 'a',
|
||||
i18n: 'a'
|
||||
} })
|
||||
])('returns $expected', ({ settings, name, expected }) => {
|
||||
i18n: 'a',
|
||||
},
|
||||
},
|
||||
])('returns $expected', ({ settings, name, expected }) => {
|
||||
expect(getSetting(name, settings)).toStrictEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
const veryNestedSettingsList = [
|
||||
const veryNestedSettingsList = [
|
||||
{
|
||||
type: 'bool',
|
||||
name: '1a',
|
||||
i18n: '1a'
|
||||
i18n: '1a',
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
@@ -49,7 +54,7 @@ const veryNestedSettingsList = [
|
||||
{
|
||||
type: 'bool',
|
||||
name: '2a',
|
||||
i18n: '2a'
|
||||
i18n: '2a',
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
@@ -59,7 +64,7 @@ const veryNestedSettingsList = [
|
||||
{
|
||||
type: 'bool',
|
||||
name: '3a',
|
||||
i18n: '3a'
|
||||
i18n: '3a',
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
@@ -69,7 +74,7 @@ const veryNestedSettingsList = [
|
||||
{
|
||||
type: 'bool',
|
||||
name: '4a',
|
||||
i18n: '4a'
|
||||
i18n: '4a',
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
@@ -79,7 +84,7 @@ const veryNestedSettingsList = [
|
||||
{
|
||||
type: 'bool',
|
||||
name: '5a',
|
||||
i18n: '5a'
|
||||
i18n: '5a',
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
@@ -89,7 +94,7 @@ const veryNestedSettingsList = [
|
||||
{
|
||||
type: 'bool',
|
||||
name: '6a',
|
||||
i18n: '6a'
|
||||
i18n: '6a',
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
@@ -99,40 +104,45 @@ const veryNestedSettingsList = [
|
||||
{
|
||||
type: 'bool',
|
||||
name: '7a',
|
||||
i18n: '7a'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
i18n: '7a',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
test.for([
|
||||
({ settings: [], path: ['a'], expected: undefined }),
|
||||
({ settings: [
|
||||
test.for([
|
||||
{ settings: [], path: ['a'], expected: undefined },
|
||||
{
|
||||
settings: [
|
||||
{
|
||||
type: 'bool',
|
||||
name: '1a',
|
||||
i18n: '1a'
|
||||
}
|
||||
], path: ['1a'], expected: {
|
||||
i18n: '1a',
|
||||
},
|
||||
],
|
||||
path: ['1a'],
|
||||
expected: {
|
||||
type: 'bool',
|
||||
name: '1a',
|
||||
i18n: '1a'
|
||||
} }),
|
||||
({ settings: [
|
||||
i18n: '1a',
|
||||
},
|
||||
},
|
||||
{
|
||||
settings: [
|
||||
{
|
||||
type: 'bool',
|
||||
name: '1a',
|
||||
i18n: '1a'
|
||||
i18n: '1a',
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
@@ -142,20 +152,24 @@ test.for([
|
||||
{
|
||||
type: 'bool',
|
||||
name: '2a',
|
||||
i18n: '2a'
|
||||
}
|
||||
]
|
||||
}
|
||||
], path: ['1a'], expected: {
|
||||
i18n: '2a',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
path: ['1a'],
|
||||
expected: {
|
||||
type: 'bool',
|
||||
name: '1a',
|
||||
i18n: '1a'
|
||||
} }),
|
||||
({ settings: [
|
||||
i18n: '1a',
|
||||
},
|
||||
},
|
||||
{
|
||||
settings: [
|
||||
{
|
||||
type: 'bool',
|
||||
name: '1a',
|
||||
i18n: '1a'
|
||||
i18n: '1a',
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
@@ -165,20 +179,24 @@ test.for([
|
||||
{
|
||||
type: 'bool',
|
||||
name: '2a',
|
||||
i18n: '2a'
|
||||
}
|
||||
]
|
||||
}
|
||||
], path: ['1b', '2a'], expected: {
|
||||
i18n: '2a',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
path: ['1b', '2a'],
|
||||
expected: {
|
||||
type: 'bool',
|
||||
name: '2a',
|
||||
i18n: '2a'
|
||||
} }),
|
||||
({ settings: [
|
||||
i18n: '2a',
|
||||
},
|
||||
},
|
||||
{
|
||||
settings: [
|
||||
{
|
||||
type: 'bool',
|
||||
name: '1a',
|
||||
i18n: '1a'
|
||||
i18n: '1a',
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
@@ -188,27 +206,31 @@ test.for([
|
||||
{
|
||||
type: 'bool',
|
||||
name: '2a',
|
||||
i18n: '2a'
|
||||
i18n: '2a',
|
||||
},
|
||||
{
|
||||
type: 'number',
|
||||
name: '2b',
|
||||
i18n: '2b',
|
||||
default: 42
|
||||
}
|
||||
]
|
||||
}
|
||||
], path: ['1b', '2b'], expected: {
|
||||
default: 42,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
path: ['1b', '2b'],
|
||||
expected: {
|
||||
type: 'number',
|
||||
name: '2b',
|
||||
i18n: '2b',
|
||||
default: 42
|
||||
} }),
|
||||
({ settings: [
|
||||
default: 42,
|
||||
},
|
||||
},
|
||||
{
|
||||
settings: [
|
||||
{
|
||||
type: 'bool',
|
||||
name: '1a',
|
||||
i18n: '1a'
|
||||
i18n: '1a',
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
@@ -218,7 +240,7 @@ test.for([
|
||||
{
|
||||
type: 'bool',
|
||||
name: '2a',
|
||||
i18n: '2a'
|
||||
i18n: '2a',
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
@@ -228,22 +250,26 @@ test.for([
|
||||
{
|
||||
type: 'string',
|
||||
name: '3a',
|
||||
i18n: '3a'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
], path: ['1b', '2b', '3a'], expected: {
|
||||
i18n: '3a',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
path: ['1b', '2b', '3a'],
|
||||
expected: {
|
||||
type: 'string',
|
||||
name: '3a',
|
||||
i18n: '3a'
|
||||
} }),
|
||||
({ settings: [
|
||||
i18n: '3a',
|
||||
},
|
||||
},
|
||||
{
|
||||
settings: [
|
||||
{
|
||||
type: 'bool',
|
||||
name: '1a',
|
||||
i18n: '1a'
|
||||
i18n: '1a',
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
@@ -253,7 +279,7 @@ test.for([
|
||||
{
|
||||
type: 'bool',
|
||||
name: '2a',
|
||||
i18n: '2a'
|
||||
i18n: '2a',
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
@@ -263,13 +289,15 @@ test.for([
|
||||
{
|
||||
type: 'string',
|
||||
name: '3a',
|
||||
i18n: '3a'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
], path: ['1b'], expected: {
|
||||
i18n: '3a',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
path: ['1b'],
|
||||
expected: {
|
||||
type: 'section',
|
||||
name: '1b',
|
||||
i18n: '1b',
|
||||
@@ -277,7 +305,7 @@ test.for([
|
||||
{
|
||||
type: 'bool',
|
||||
name: '2a',
|
||||
i18n: '2a'
|
||||
i18n: '2a',
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
@@ -287,17 +315,19 @@ test.for([
|
||||
{
|
||||
type: 'string',
|
||||
name: '3a',
|
||||
i18n: '3a'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
} }),
|
||||
({ settings: [
|
||||
i18n: '3a',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
settings: [
|
||||
{
|
||||
type: 'bool',
|
||||
name: '1a',
|
||||
i18n: '1a'
|
||||
i18n: '1a',
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
@@ -307,7 +337,7 @@ test.for([
|
||||
{
|
||||
type: 'bool',
|
||||
name: '2a',
|
||||
i18n: '2a'
|
||||
i18n: '2a',
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
@@ -317,13 +347,15 @@ test.for([
|
||||
{
|
||||
type: 'string',
|
||||
name: '3a',
|
||||
i18n: '3a'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
], path: ['1b', '2b'], expected: {
|
||||
i18n: '3a',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
path: ['1b', '2b'],
|
||||
expected: {
|
||||
type: 'section',
|
||||
name: '2b',
|
||||
i18n: '2b',
|
||||
@@ -331,16 +363,18 @@ test.for([
|
||||
{
|
||||
type: 'string',
|
||||
name: '3a',
|
||||
i18n: '3a'
|
||||
}
|
||||
]
|
||||
} }),
|
||||
i18n: '3a',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
({ settings: [
|
||||
{
|
||||
settings: [
|
||||
{
|
||||
type: 'bool',
|
||||
name: '1a',
|
||||
i18n: '1a'
|
||||
i18n: '1a',
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
@@ -350,7 +384,7 @@ test.for([
|
||||
{
|
||||
type: 'bool',
|
||||
name: '2a',
|
||||
i18n: '2a'
|
||||
i18n: '2a',
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
@@ -360,18 +394,22 @@ test.for([
|
||||
{
|
||||
type: 'string',
|
||||
name: '3a',
|
||||
i18n: '3a'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
], path: ['1b', '2b', '3b'], expected: undefined }),
|
||||
({ settings: [
|
||||
i18n: '3a',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
path: ['1b', '2b', '3b'],
|
||||
expected: undefined,
|
||||
},
|
||||
{
|
||||
settings: [
|
||||
{
|
||||
type: 'bool',
|
||||
name: '1a',
|
||||
i18n: '1a'
|
||||
i18n: '1a',
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
@@ -381,7 +419,7 @@ test.for([
|
||||
{
|
||||
type: 'bool',
|
||||
name: '2a',
|
||||
i18n: '2a'
|
||||
i18n: '2a',
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
@@ -391,18 +429,22 @@ test.for([
|
||||
{
|
||||
type: 'string',
|
||||
name: '3a',
|
||||
i18n: '3a'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
], path: ['1b', '2a', '3a'], expected: undefined }),
|
||||
({ settings: [
|
||||
i18n: '3a',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
path: ['1b', '2a', '3a'],
|
||||
expected: undefined,
|
||||
},
|
||||
{
|
||||
settings: [
|
||||
{
|
||||
type: 'bool',
|
||||
name: '1a',
|
||||
i18n: '1a'
|
||||
i18n: '1a',
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
@@ -412,7 +454,7 @@ test.for([
|
||||
{
|
||||
type: 'bool',
|
||||
name: '2a',
|
||||
i18n: '2a'
|
||||
i18n: '2a',
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
@@ -422,18 +464,22 @@ test.for([
|
||||
{
|
||||
type: 'string',
|
||||
name: '3a',
|
||||
i18n: '3a'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
], path: ['1b', '2c', '3a'], expected: undefined }),
|
||||
({ settings: [
|
||||
i18n: '3a',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
path: ['1b', '2c', '3a'],
|
||||
expected: undefined,
|
||||
},
|
||||
{
|
||||
settings: [
|
||||
{
|
||||
type: 'bool',
|
||||
name: '1a',
|
||||
i18n: '1a'
|
||||
i18n: '1a',
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
@@ -443,7 +489,7 @@ test.for([
|
||||
{
|
||||
type: 'bool',
|
||||
name: '2a',
|
||||
i18n: '2a'
|
||||
i18n: '2a',
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
@@ -453,18 +499,22 @@ test.for([
|
||||
{
|
||||
type: 'string',
|
||||
name: '3a',
|
||||
i18n: '3a'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
], path: ['1a', '2b', '3a'], expected: undefined }),
|
||||
({ settings: [
|
||||
i18n: '3a',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
path: ['1a', '2b', '3a'],
|
||||
expected: undefined,
|
||||
},
|
||||
{
|
||||
settings: [
|
||||
{
|
||||
type: 'bool',
|
||||
name: '1a',
|
||||
i18n: '1a'
|
||||
i18n: '1a',
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
@@ -474,7 +524,7 @@ test.for([
|
||||
{
|
||||
type: 'bool',
|
||||
name: '2a',
|
||||
i18n: '2a'
|
||||
i18n: '2a',
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
@@ -484,19 +534,29 @@ test.for([
|
||||
{
|
||||
type: 'string',
|
||||
name: '3a',
|
||||
i18n: '3a'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
], path: ['1c', '2b', '3a'], expected: undefined }),
|
||||
({ settings: veryNestedSettingsList, path: ['1b', '2b', '3b', '4b', '5b', '6b', '7a'], expected: {
|
||||
i18n: '3a',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
path: ['1c', '2b', '3a'],
|
||||
expected: undefined,
|
||||
},
|
||||
{
|
||||
settings: veryNestedSettingsList,
|
||||
path: ['1b', '2b', '3b', '4b', '5b', '6b', '7a'],
|
||||
expected: {
|
||||
type: 'bool',
|
||||
name: '7a',
|
||||
i18n: '7a'
|
||||
} }),
|
||||
({ settings: veryNestedSettingsList, path: ['1b', '2b', '3b', '4b', '5b'], expected: {
|
||||
i18n: '7a',
|
||||
},
|
||||
},
|
||||
{
|
||||
settings: veryNestedSettingsList,
|
||||
path: ['1b', '2b', '3b', '4b', '5b'],
|
||||
expected: {
|
||||
type: 'section',
|
||||
name: '5b',
|
||||
i18n: '5b',
|
||||
@@ -504,7 +564,7 @@ test.for([
|
||||
{
|
||||
type: 'bool',
|
||||
name: '6a',
|
||||
i18n: '6a'
|
||||
i18n: '6a',
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
@@ -514,13 +574,19 @@ test.for([
|
||||
{
|
||||
type: 'bool',
|
||||
name: '7a',
|
||||
i18n: '7a'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
} }),
|
||||
({ settings: veryNestedSettingsList, path: ['1b', '2b', '3b', '4ba', '5b', '6b', '7a'], expected: undefined })
|
||||
])('returns $expected', ({ settings, path, expected }) => {
|
||||
i18n: '7a',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
settings: veryNestedSettingsList,
|
||||
path: ['1b', '2b', '3b', '4ba', '5b', '6b', '7a'],
|
||||
expected: undefined,
|
||||
},
|
||||
])('returns $expected', ({ settings, path, expected }) => {
|
||||
expect(getSettingRecursively(path, settings)).toStrictEqual(expected);
|
||||
});
|
||||
});
|
||||
@@ -47,17 +47,26 @@ describe('settingsPage', () => {
|
||||
{ path: 'a1.#b.c/', expected: ['a1', 'b', 'c'] },
|
||||
{ path: 'a-1.#b.c/', expected: ['a-1', 'b', 'c'] },
|
||||
{ path: 'a-1.b@.c', expected: ['a-1', 'b', 'c'] },
|
||||
{ path: '....@a/#...)!&§[b.#§c..d....', expected: ['a', 'b', 'c', 'd'] },
|
||||
{ path: '....@a/#...)!&§[b.#§c..dä....', expected: ['a', 'b', 'c', 'd'] },
|
||||
{ path: '..,...~..@a/#.+..)!&§[b.#§c..dä..)..', expected: ['a', 'b', 'c', 'd'] },
|
||||
{
|
||||
path: '....@a/#...)!&§[b.#§c..d....',
|
||||
expected: ['a', 'b', 'c', 'd'],
|
||||
},
|
||||
{
|
||||
path: '....@a/#...)!&§[b.#§c..dä....',
|
||||
expected: ['a', 'b', 'c', 'd'],
|
||||
},
|
||||
{
|
||||
path: '..,...~..@a/#.+..)!&§[b.#§c..dä..)..',
|
||||
expected: ['a', 'b', 'c', 'd'],
|
||||
},
|
||||
{ path: 'a.@.b', expected: ['a', 'b'] },
|
||||
{ path: 1, expected: [] },
|
||||
{ path: false, expected: [] },
|
||||
{ path: true, expected: [] },
|
||||
{ path: null, expected: [] },
|
||||
{ path: undefined, expected: [] }
|
||||
])('returns $expected', async ({ path, expected }) => {
|
||||
{ path: undefined, expected: [] },
|
||||
])('returns $expected', ({ path, expected }) => {
|
||||
expect(getSettingsPagePathSegments(path)).toStrictEqual(expected);
|
||||
});
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -14,12 +14,15 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
export const getSetting = function getSetting (name, settings) {
|
||||
export const getSetting = function getSetting(name, settings) {
|
||||
return settings.filter((setting) => setting.name === name)[0];
|
||||
};
|
||||
|
||||
export const getSettingRecursively = function getSettingRecursively (sectionPath, settings) {
|
||||
const oneSettingsLevel = function oneSettingsLevel (cursor, acc) {
|
||||
export const getSettingRecursively = function getSettingRecursively(
|
||||
sectionPath,
|
||||
settings,
|
||||
) {
|
||||
const oneSettingsLevel = function oneSettingsLevel(cursor, acc) {
|
||||
const setting = getSetting(cursor[0], acc.content);
|
||||
if (!setting) return;
|
||||
|
||||
@@ -28,15 +31,18 @@ export const getSettingRecursively = function getSettingRecursively (sectionPath
|
||||
const newCursor = cursor.slice(1, cursor.length);
|
||||
|
||||
if (newCursor.length !== 0) {
|
||||
return oneSettingsLevel(cursor.slice(1, cursor.length), setting);
|
||||
return oneSettingsLevel(
|
||||
cursor.slice(1, cursor.length),
|
||||
setting,
|
||||
);
|
||||
} else {
|
||||
return setting;
|
||||
};
|
||||
}
|
||||
} else if (cursor.length <= 1) {
|
||||
return setting;
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
}
|
||||
};
|
||||
return oneSettingsLevel(sectionPath, { content: settings });
|
||||
};
|
||||
@@ -19,13 +19,13 @@ limitations under the License.
|
||||
* @param {string} path - The settings page path.
|
||||
* @returns {Array} The settings page path segments.
|
||||
*/
|
||||
export const getSettingsPagePathSegments = function getSettingsPagePathSegments (path) {
|
||||
export const getSettingsPagePathSegments = function getSettingsPagePathSegments(
|
||||
path,
|
||||
) {
|
||||
if (typeof path !== 'string') return [];
|
||||
return path
|
||||
.replace(/^\.+|\.+$/g, '')
|
||||
.split('.')
|
||||
.map(
|
||||
(segment) => segment.replace(/[^a-zA-Z0-9-]/g, '')
|
||||
)
|
||||
.map((segment) => segment.replace(/[^a-zA-Z0-9-]/g, ''))
|
||||
.filter(Boolean);
|
||||
};
|
||||
@@ -16,31 +16,36 @@ limitations under the License.
|
||||
|
||||
const VALID_TYPES = ['bool', 'number', 'string', 'selection', 'section'];
|
||||
|
||||
export const assertString = function assertString (value, path) {
|
||||
export const assertString = function assertString(value, path) {
|
||||
if (typeof value !== 'string' || value.trim() === '') {
|
||||
throw new Error(`[settings] "${path}" must be a non-empty string`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const assertType = function assertType (value, path) {
|
||||
export const assertType = function assertType(value, path) {
|
||||
if (!VALID_TYPES.includes(value)) {
|
||||
throw new Error(
|
||||
`[settings] "${path}" has invalid type "${value}". Must be one of: ${VALID_TYPES.join(', ')}`
|
||||
`[settings] "${path}" has invalid type "${value}". Must be one of: ${VALID_TYPES.join(', ')}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const validateSelectionOptions = function validateSelectionOptions (options, path) {
|
||||
export const validateSelectionOptions = function validateSelectionOptions(
|
||||
options,
|
||||
path,
|
||||
) {
|
||||
if (!Array.isArray(options) || options.length === 0) {
|
||||
throw new Error(`[settings] "${path}.options" must be a non-empty array`);
|
||||
};
|
||||
throw new Error(
|
||||
`[settings] "${path}.options" must be a non-empty array`,
|
||||
);
|
||||
}
|
||||
options.forEach((opt, i) => {
|
||||
assertString(opt.name, `${path}.options[${i}].name`);
|
||||
assertString(opt.i18n, `${path}.options[${i}].i18n`);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const validateEntry = function validateEntry (entry, path) {
|
||||
export const validateEntry = function validateEntry(entry, path) {
|
||||
assertType(entry.type, `${path}.type`);
|
||||
|
||||
assertString(entry.name, `${path}.name`);
|
||||
@@ -51,7 +56,7 @@ export const validateEntry = function validateEntry (entry, path) {
|
||||
throw new Error(`[settings] "${path}.content" must be an array`);
|
||||
}
|
||||
entry.content.forEach((child, i) =>
|
||||
validateEntry(child, `${path}.content[${i}]`)
|
||||
validateEntry(child, `${path}.content[${i}]`),
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -59,9 +64,11 @@ export const validateEntry = function validateEntry (entry, path) {
|
||||
if (entry.type === 'selection') {
|
||||
validateSelectionOptions(entry.options, path);
|
||||
if (typeof entry.allowMultiple !== 'boolean' && entry.allowMultiple) {
|
||||
throw new Error(`[settings] "${path}.allowMultiple" must be a boolean`);
|
||||
throw new Error(
|
||||
`[settings] "${path}.allowMultiple" must be a boolean`,
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
if (entry.default !== undefined) {
|
||||
if (entry.type === 'bool' && typeof entry.default !== 'boolean') {
|
||||
throw new Error(`[settings] "${path}.default" must be a boolean`);
|
||||
@@ -75,39 +82,56 @@ export const validateEntry = function validateEntry (entry, path) {
|
||||
if (entry.type === 'selection') {
|
||||
if (entry.allowMultiple) {
|
||||
if (!Array.isArray(entry.default)) {
|
||||
throw new Error(`[settings] "${path}.default" must be an array`);
|
||||
throw new Error(
|
||||
`[settings] "${path}.default" must be an array`,
|
||||
);
|
||||
}
|
||||
const allOptions = entry.options.map((option) => option.name);
|
||||
entry.default.forEach((defaultValue, index) => {
|
||||
if (typeof defaultValue !== 'string') {
|
||||
throw new Error(`[settings] "${path}.default[${index}]" must be a string`);
|
||||
throw new Error(
|
||||
`[settings] "${path}.default[${index}]" must be a string`,
|
||||
);
|
||||
}
|
||||
if (!allOptions.includes(defaultValue)) {
|
||||
throw new Error(`[settings] option "${path}.default[${index}]" does not exist`);
|
||||
throw new Error(
|
||||
`[settings] option "${path}.default[${index}]" does not exist`,
|
||||
);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if (typeof entry.default !== 'string') {
|
||||
throw new Error(`[settings] "${path}.default" must be a string`);
|
||||
};
|
||||
if (!entry.options.map((option) => option.name).includes(entry.default)) {
|
||||
throw new Error(`[settings] option "${path}.default" does not exist`);
|
||||
};
|
||||
throw new Error(
|
||||
`[settings] "${path}.default" must be a string`,
|
||||
);
|
||||
}
|
||||
};
|
||||
if (
|
||||
!entry.options
|
||||
.map((option) => option.name)
|
||||
.includes(entry.default)
|
||||
) {
|
||||
throw new Error(
|
||||
`[settings] option "${path}.default" does not exist`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const validateFirstLevelSection = function validateFirstLevelSection (section, path) {
|
||||
export const validateFirstLevelSection = function validateFirstLevelSection(
|
||||
section,
|
||||
path,
|
||||
) {
|
||||
assertString(section.name);
|
||||
assertString(section.i18n);
|
||||
|
||||
if (!Array.isArray(section.content)) {
|
||||
throw new Error(`[settings] "${path}.content" must be an array`);
|
||||
};
|
||||
}
|
||||
|
||||
section.content.forEach((entry, i) =>
|
||||
validateEntry(entry, `${path}.content[${i}]`)
|
||||
validateEntry(entry, `${path}.content[${i}]`),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -116,7 +140,7 @@ export const validateFirstLevelSection = function validateFirstLevelSection (sec
|
||||
* @param {unknown} raw
|
||||
* @returns {{ valid: true, config: import('../types/settingsConfig').SettingsConfig } | { valid: false, error: string }}
|
||||
*/
|
||||
export function validateSettingsConfig(raw) {
|
||||
export const validateSettingsConfig = function validateSettingsConfig(raw) {
|
||||
if (!raw || typeof raw !== 'object') {
|
||||
throw new Error('[settings] Config must be an object');
|
||||
}
|
||||
@@ -124,6 +148,6 @@ export function validateSettingsConfig(raw) {
|
||||
throw new Error('[settings] "contents" must be an array');
|
||||
}
|
||||
raw.contents.forEach((entry, i) =>
|
||||
validateFirstLevelSection(entry, `contents[${i}]`)
|
||||
validateFirstLevelSection(entry, `contents[${i}]`),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -15,11 +15,10 @@ limitations under the License.
|
||||
-->
|
||||
|
||||
<script setup>
|
||||
import LeftSidebarLayout from '@/layouts/LeftSidebarLayout.vue';
|
||||
import SettingsPage from '../components/SettingsPage.vue';
|
||||
|
||||
import settingsConfiguration from '../config.js';
|
||||
import { getSettingRecursively } from '../utils/getSetting.js';
|
||||
import LeftSidebarLayout from '@/layouts/LeftSidebarLayout.vue';
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
@@ -31,15 +30,13 @@ const router = useRouter();
|
||||
const settings = settingsConfiguration.contents;
|
||||
|
||||
const activePath = computed(() => {
|
||||
const segments = route.path
|
||||
.split('/')
|
||||
.filter(Boolean);
|
||||
const segments = route.path.split('/').filter(Boolean);
|
||||
|
||||
return segments.slice(1, segments.length);
|
||||
});
|
||||
|
||||
const activeSection = ref(null);
|
||||
const updateSettings = async function updateSettings () {
|
||||
const updateSettings = async function updateSettings() {
|
||||
const setting = getSettingRecursively(activePath.value, settings);
|
||||
if (setting) {
|
||||
activeSection.value = setting;
|
||||
@@ -50,7 +47,9 @@ const updateSettings = async function updateSettings () {
|
||||
watch(route, (newRoute) => updateSettings(newRoute));
|
||||
onMounted(updateSettings);
|
||||
|
||||
const toggleSidebarIfSmallScreen = function toggleSidebarIfSmallScreen (toggleSidebarFunction) {
|
||||
const toggleSidebarIfSmallScreen = function toggleSidebarIfSmallScreen(
|
||||
toggleSidebarFunction,
|
||||
) {
|
||||
if (matchMedia('(max-width: 48rem)').matches) toggleSidebarFunction();
|
||||
};
|
||||
</script>
|
||||
@@ -65,11 +64,15 @@ const toggleSidebarIfSmallScreen = function toggleSidebarIfSmallScreen (toggleSi
|
||||
<LeftSidebarLayout class="layout">
|
||||
<template #sidebar="{ toggleSidebar }">
|
||||
<ul class="sidebar-sections-list">
|
||||
<li v-for="section in settings">
|
||||
<li v-for="section in settings" :key="section">
|
||||
<RouterLink
|
||||
:to="`/settings/${section.name}`"
|
||||
class="button button-link link sidebar-section"
|
||||
:class="{ active: activeSection && activeSection.name === section.name }"
|
||||
:class="{
|
||||
active:
|
||||
activeSection &&
|
||||
activeSection.name === section.name,
|
||||
}"
|
||||
@click="toggleSidebarIfSmallScreen(toggleSidebar)"
|
||||
>
|
||||
{{ t(section.i18n) }}
|
||||
|
||||
@@ -23,7 +23,7 @@ const settingsHoisted = vi.hoisted(() => [
|
||||
{
|
||||
name: 's0',
|
||||
i18n: 'settings.s0',
|
||||
content: []
|
||||
content: [],
|
||||
},
|
||||
{
|
||||
name: 's1',
|
||||
@@ -32,17 +32,17 @@ const settingsHoisted = vi.hoisted(() => [
|
||||
{
|
||||
type: 'bool',
|
||||
name: 'b0',
|
||||
i18n: 'settings.s1.b0'
|
||||
i18n: 'settings.s1.b0',
|
||||
},
|
||||
{
|
||||
type: 'number',
|
||||
name: 'n0',
|
||||
i18n: 'settings.s1.n0'
|
||||
i18n: 'settings.s1.n0',
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'st0',
|
||||
i18n: 'settings.s1.st0'
|
||||
i18n: 'settings.s1.st0',
|
||||
},
|
||||
{
|
||||
type: 'selection',
|
||||
@@ -52,14 +52,14 @@ const settingsHoisted = vi.hoisted(() => [
|
||||
{ name: 'o0', i18n: 'settings.s1.se0.o0' },
|
||||
{ name: 'o1', i18n: 'settings.s1.se0.o1' },
|
||||
{ name: 'o2', i18n: 'settings.s1.se0.o2' },
|
||||
{ name: 'o3', i18n: 'settings.s1.se0.o3' }
|
||||
]
|
||||
{ name: 'o3', i18n: 'settings.s1.se0.o3' },
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
name: 's0',
|
||||
i18n: 'settings.s1.s0',
|
||||
content: []
|
||||
content: [],
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
@@ -70,11 +70,11 @@ const settingsHoisted = vi.hoisted(() => [
|
||||
type: 'bool',
|
||||
name: 'b0',
|
||||
i18n: 'settings.s1.s1.b0',
|
||||
default: true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
default: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 's2',
|
||||
@@ -89,11 +89,11 @@ const settingsHoisted = vi.hoisted(() => [
|
||||
type: 'bool',
|
||||
name: 'b0',
|
||||
i18n: 'settings.s2.s0.b0',
|
||||
default: true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
default: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 's3',
|
||||
@@ -103,9 +103,9 @@ const settingsHoisted = vi.hoisted(() => [
|
||||
type: 'section',
|
||||
name: 's0',
|
||||
i18n: 'settings.s3.s0',
|
||||
content: []
|
||||
}
|
||||
]
|
||||
content: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 's4',
|
||||
@@ -119,10 +119,10 @@ const settingsHoisted = vi.hoisted(() => [
|
||||
{ name: 'o0', i18n: 'settings.s4.se0.o0' },
|
||||
{ name: 'o1', i18n: 'settings.s4.se0.o1' },
|
||||
{ name: 'o2', i18n: 'settings.s4.se0.o2' },
|
||||
{ name: 'o3', i18n: 'settings.s4.se0.o3' }
|
||||
]
|
||||
}
|
||||
]
|
||||
{ name: 'o3', i18n: 'settings.s4.se0.o3' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 's5',
|
||||
@@ -131,9 +131,9 @@ const settingsHoisted = vi.hoisted(() => [
|
||||
{
|
||||
type: 'string',
|
||||
name: 'st0',
|
||||
i18n: 'settings.s5.st0'
|
||||
}
|
||||
]
|
||||
i18n: 'settings.s5.st0',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 's6',
|
||||
@@ -142,9 +142,9 @@ const settingsHoisted = vi.hoisted(() => [
|
||||
{
|
||||
type: 'number',
|
||||
name: 'n0',
|
||||
i18n: 'settings.s6.n0'
|
||||
}
|
||||
]
|
||||
i18n: 'settings.s6.n0',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 's7',
|
||||
@@ -153,9 +153,9 @@ const settingsHoisted = vi.hoisted(() => [
|
||||
{
|
||||
type: 'bool',
|
||||
name: 'b0',
|
||||
i18n: 'settings.s7.b0'
|
||||
}
|
||||
]
|
||||
i18n: 'settings.s7.b0',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 's8',
|
||||
@@ -181,16 +181,16 @@ const settingsHoisted = vi.hoisted(() => [
|
||||
name: 'se0',
|
||||
i18n: 'settings.s8.s0.s0.s0.se0',
|
||||
allowMultiple: true,
|
||||
options: []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
options: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
const settings = settingsHoisted.values();
|
||||
|
||||
@@ -198,7 +198,7 @@ const originalCurrentSection = 's0';
|
||||
let currentSection = originalCurrentSection;
|
||||
|
||||
vi.mock('../../config.js', () => ({
|
||||
default: { contents: settingsHoisted }
|
||||
default: { contents: settingsHoisted },
|
||||
}));
|
||||
|
||||
vi.mock('vue-router', async (importOriginal) => {
|
||||
@@ -206,12 +206,12 @@ vi.mock('vue-router', async (importOriginal) => {
|
||||
return {
|
||||
...actual,
|
||||
useRoute: () => ({
|
||||
path: `/settings/${currentSection}`
|
||||
})
|
||||
path: `/settings/${currentSection}`,
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
const getWrapper = function getWrapper ({ translations = {} } = {}) {
|
||||
const getWrapper = function getWrapper({ translations = {} } = {}) {
|
||||
return mountComponent(SettingsView, {}, translations, [], {});
|
||||
};
|
||||
|
||||
@@ -221,8 +221,8 @@ describe('SettingsView', () => {
|
||||
const translation = 'Settings';
|
||||
const wrapper = getWrapper({
|
||||
translations: {
|
||||
'preferences.settings': translation
|
||||
}
|
||||
'preferences.settings': translation,
|
||||
},
|
||||
});
|
||||
|
||||
const header = wrapper.find('header');
|
||||
@@ -242,8 +242,12 @@ describe('SettingsView', () => {
|
||||
expect(sidebarItem.text()).toBe(setting.i18n);
|
||||
|
||||
const link = sidebarItem.find('*');
|
||||
expect(link.attributes('href')).toBe(`/settings/${setting.name}`);
|
||||
expect(link.classes('active')).toBe(setting.name === currentSection);
|
||||
expect(link.attributes('href')).toBe(
|
||||
`/settings/${setting.name}`,
|
||||
);
|
||||
expect(link.classes('active')).toBe(
|
||||
setting.name === currentSection,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -257,7 +261,9 @@ describe('SettingsView', () => {
|
||||
await nextTick();
|
||||
|
||||
const mainContent = wrapper.find('.settings-main-content');
|
||||
const mainContentNoSection = wrapper.find('.main-content--no-section');
|
||||
const mainContentNoSection = wrapper.find(
|
||||
'.main-content--no-section',
|
||||
);
|
||||
|
||||
expect(mainContent.exists()).toBe(false);
|
||||
expect(mainContentNoSection.exists()).toBe(true);
|
||||
@@ -268,7 +274,9 @@ describe('SettingsView', () => {
|
||||
await nextTick();
|
||||
|
||||
const mainContent = wrapper.find('.settings-main-content');
|
||||
const mainContentNoSection = wrapper.find('.main-content--no-section');
|
||||
const mainContentNoSection = wrapper.find(
|
||||
'.main-content--no-section',
|
||||
);
|
||||
|
||||
expect(mainContent.exists()).toBe(true);
|
||||
expect(mainContentNoSection.exists()).toBe(false);
|
||||
|
||||
+8
-5
@@ -18,15 +18,15 @@ limitations under the License.
|
||||
import SidebarExpandButton from './SidebarExpandButton.vue';
|
||||
|
||||
const props = defineProps({
|
||||
'expanded': {
|
||||
expanded: {
|
||||
default: false,
|
||||
required: false,
|
||||
type: Boolean
|
||||
}
|
||||
type: Boolean,
|
||||
},
|
||||
});
|
||||
const emit = defineEmits(['toggleExpanded']);
|
||||
|
||||
const toggleSidebar = function toggleSidebar () {
|
||||
const toggleSidebar = function toggleSidebar() {
|
||||
emit('toggleExpanded');
|
||||
};
|
||||
</script>
|
||||
@@ -34,7 +34,10 @@ const toggleSidebar = function toggleSidebar () {
|
||||
<template>
|
||||
<nav class="sidebar" :class="{ expanded }">
|
||||
<div class="sidebar-controls">
|
||||
<SidebarExpandButton @click="toggleSidebar" class="sidebar-expand-button" />
|
||||
<SidebarExpandButton
|
||||
@click="toggleSidebar"
|
||||
class="sidebar-expand-button"
|
||||
/>
|
||||
</div>
|
||||
<div class="sidebar-content" v-show="props.expanded">
|
||||
<slot />
|
||||
@@ -15,18 +15,18 @@ limitations under the License.
|
||||
-->
|
||||
|
||||
<script setup>
|
||||
import Icon from '@/features/icons/components/Icon.vue';
|
||||
import UIIcon from '@/features/icons/components/UIIcon.vue';
|
||||
|
||||
const emit = defineEmits(['click']);
|
||||
|
||||
const click = function click (event) {
|
||||
const click = function click(event) {
|
||||
emit('click', event);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button class="button sidebar-expand-button">
|
||||
<Icon name="sidebar-expand" @click="click" />
|
||||
<UIIcon name="sidebar-expand" @click="click" />
|
||||
</button>
|
||||
</template>
|
||||
|
||||
|
||||
+10
-18
@@ -18,29 +18,19 @@ import { createI18n } from 'vue-i18n';
|
||||
|
||||
export const fallbackLocale = 'en';
|
||||
|
||||
|
||||
export const LANGUAGES_RTL = [
|
||||
'ar'
|
||||
];
|
||||
export const SUPPORTED_LANGUAGES = [
|
||||
'en',
|
||||
'de',
|
||||
'fr',
|
||||
'es',
|
||||
'pt',
|
||||
'it'
|
||||
];
|
||||
export const LANGUAGES_RTL = ['ar'];
|
||||
export const SUPPORTED_LANGUAGES = ['en', 'de', 'fr', 'es', 'pt', 'it'];
|
||||
|
||||
export const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: fallbackLocale,
|
||||
fallbackLocale: fallbackLocale,
|
||||
messages: {}
|
||||
fallbackLocale,
|
||||
messages: {},
|
||||
});
|
||||
|
||||
const loadedLanguages = new Set();
|
||||
|
||||
export async function loadLanguage (locale) {
|
||||
export const loadLanguage = async function loadLanguage(locale) {
|
||||
if (!SUPPORTED_LANGUAGES.includes(locale)) {
|
||||
locale = fallbackLocale;
|
||||
}
|
||||
@@ -48,7 +38,7 @@ export async function loadLanguage (locale) {
|
||||
if (loadedLanguages.has(locale)) {
|
||||
i18n.global.locale.value = locale;
|
||||
return;
|
||||
};
|
||||
}
|
||||
|
||||
const messages = (await import(`./locales/${locale}.json`)).default;
|
||||
|
||||
@@ -58,8 +48,10 @@ export async function loadLanguage (locale) {
|
||||
loadedLanguages.add(locale);
|
||||
};
|
||||
|
||||
export const setLocale = async function setLocale (locale) {
|
||||
export const setLocale = async function setLocale(locale) {
|
||||
await loadLanguage(locale);
|
||||
document.documentElement.lang = locale;
|
||||
document.documentElement.dir = LANGUAGES_RTL.includes(locale) ? 'rtl' : 'ltr';
|
||||
document.documentElement.dir = LANGUAGES_RTL.includes(locale)
|
||||
? 'rtl'
|
||||
: 'ltr';
|
||||
};
|
||||
|
||||
@@ -15,21 +15,21 @@ limitations under the License.
|
||||
-->
|
||||
|
||||
<script setup>
|
||||
import Sidebar from '@/features/sidebar/components/Sidebar.vue';
|
||||
import AppSidebar from '@/features/sidebar/components/AppSidebar.vue';
|
||||
import SidebarExpandButton from '@/features/sidebar/components/SidebarExpandButton.vue';
|
||||
import { ref } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
'expanded': {
|
||||
expanded: {
|
||||
default: false,
|
||||
required: false,
|
||||
type: Boolean
|
||||
}
|
||||
type: Boolean,
|
||||
},
|
||||
});
|
||||
|
||||
const expanded = ref(props.expanded);
|
||||
|
||||
const toggleSidebar = function toggleSidebar () {
|
||||
const toggleSidebar = function toggleSidebar() {
|
||||
expanded.value = !expanded.value;
|
||||
};
|
||||
</script>
|
||||
@@ -37,11 +37,18 @@ const toggleSidebar = function toggleSidebar () {
|
||||
<template>
|
||||
<div class="layout-container" :class="{ expanded }">
|
||||
<div class="sidebar-expand-button-container">
|
||||
<SidebarExpandButton class="sidebar-expand-button" @click="toggleSidebar" />
|
||||
<SidebarExpandButton
|
||||
class="sidebar-expand-button"
|
||||
@click="toggleSidebar"
|
||||
/>
|
||||
</div>
|
||||
<Sidebar @toggle-expanded="toggleSidebar" :expanded="expanded" class="sidebar">
|
||||
<AppSidebar
|
||||
@toggle-expanded="toggleSidebar"
|
||||
:expanded="expanded"
|
||||
class="sidebar"
|
||||
>
|
||||
<slot name="sidebar" :toggleSidebar="toggleSidebar" />
|
||||
</Sidebar>
|
||||
</AppSidebar>
|
||||
<main class="main-content">
|
||||
<slot :toggleSidebar="toggleSidebar" />
|
||||
</main>
|
||||
@@ -54,8 +61,8 @@ const toggleSidebar = function toggleSidebar () {
|
||||
grid-template-columns: calc(32px + 2em) 1fr;
|
||||
grid-template-rows: auto 1fr;
|
||||
grid-template-areas:
|
||||
"sidebar-expand sidebar-expand"
|
||||
"sidebar main-content";
|
||||
'sidebar-expand sidebar-expand'
|
||||
'sidebar main-content';
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
|
||||
+29
-25
@@ -14,40 +14,48 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { getAssetsUrl } from './config/env';
|
||||
import { createApp } from 'vue'
|
||||
import { i18n, loadLanguage, fallbackLocale } from './i18n';
|
||||
import router from './router';
|
||||
import { createPinia } from 'pinia';
|
||||
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';
|
||||
import App from './App.vue'
|
||||
import { i18n, loadLanguage, fallbackLocale } from './i18n';
|
||||
import router from './router'
|
||||
import './styles/common.css'
|
||||
import './styles/variables/colors.css'
|
||||
import { createApp } from 'vue';
|
||||
|
||||
import App from './App.vue';
|
||||
|
||||
import './styles/common.css';
|
||||
import rawFonts from './styles/fonts.json';
|
||||
import './styles/variables/colors.css';
|
||||
import './styles/variables/dimensions.css';
|
||||
|
||||
import rawFonts from './styles/fonts.json';
|
||||
import './styles/common.css';
|
||||
import './styles/variables/colors.css';
|
||||
|
||||
import settings from './features/settings/config.js';
|
||||
|
||||
import { getAssetsUrl } from './config/env';
|
||||
import { validateSettingsConfig } from './features/settings/utils/settingsValidator';
|
||||
|
||||
(async () => {
|
||||
const fonts = rawFonts.map((font) => new FontFace(
|
||||
const fonts = rawFonts.map(
|
||||
(font) =>
|
||||
new FontFace(
|
||||
font.family ?? '',
|
||||
Array.isArray(font.source)
|
||||
? font.source.map(
|
||||
(source) => `url('${source.url
|
||||
? new URL(source.url, getAssetsUrl()).href
|
||||
? font.source
|
||||
.map(
|
||||
(source) =>
|
||||
`url('${
|
||||
source.url
|
||||
? new URL(source.url, getAssetsUrl())
|
||||
.href
|
||||
: ''
|
||||
}') format('${source.format ?? ''}')`
|
||||
).join(',')
|
||||
}') format('${source.format ?? ''}')`,
|
||||
)
|
||||
.join(',')
|
||||
: '',
|
||||
{
|
||||
weight: font.weight ?? undefined,
|
||||
style: font.style ?? undefined
|
||||
}
|
||||
));
|
||||
style: font.style ?? undefined,
|
||||
},
|
||||
),
|
||||
);
|
||||
await Promise.all(fonts.map((font) => font.load()));
|
||||
fonts.forEach((font) => document.fonts.add(font));
|
||||
|
||||
@@ -58,9 +66,5 @@ import { validateSettingsConfig } from './features/settings/utils/settingsValida
|
||||
|
||||
validateSettingsConfig(settings);
|
||||
|
||||
createApp(App)
|
||||
.use(router)
|
||||
.use(i18n)
|
||||
.use(pinia)
|
||||
.mount('#app');
|
||||
createApp(App).use(router).use(i18n).use(pinia).mount('#app');
|
||||
})();
|
||||
|
||||
+20
-20
@@ -14,61 +14,61 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { createRouter, createWebHistory } from 'vue-router';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
import SearchView from '../views/SearchView.vue';
|
||||
import LegalNoticeView from '@/features/legal/views/LegalNoticeView.vue';
|
||||
import SearchResultsView from '@/features/search/views/SearchResultsView.vue';
|
||||
import SearchView from '@/features/search/views/SearchView.vue';
|
||||
import SettingsView from '@/features/settings/views/SettingsView.vue';
|
||||
import NotFound from '../views/NotFound.vue';
|
||||
import NotFound from '@/views/NotFound.vue';
|
||||
|
||||
import { i18n } from '@/i18n';
|
||||
import { createRouter, createWebHistory } from 'vue-router';
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'startPage',
|
||||
component: SearchView
|
||||
component: SearchView,
|
||||
},
|
||||
{
|
||||
path: '/search',
|
||||
name: 'searchResults',
|
||||
component: SearchResultsView,
|
||||
props: route => ({
|
||||
searchQuery: route.query.q
|
||||
props: (route) => ({
|
||||
searchQuery: route.query.q,
|
||||
}),
|
||||
meta: {
|
||||
title: (route) => route.query.q
|
||||
}
|
||||
title: (route) => route.query.q,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/legal/notice',
|
||||
name: 'legalNotice',
|
||||
component: LegalNoticeView,
|
||||
meta: {
|
||||
title: () => i18n.global.t('legal.notice.title')
|
||||
}
|
||||
title: () => i18n.global.t('legal.notice.title'),
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/settings/:rest(.*)*',
|
||||
name: 'settings',
|
||||
component: SettingsView,
|
||||
meta: {
|
||||
title: () => i18n.global.t('preferences.settings')
|
||||
}
|
||||
title: () => i18n.global.t('preferences.settings'),
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/:pathMatch(.*)*',
|
||||
name: 'notFound',
|
||||
component: NotFound
|
||||
component: NotFound,
|
||||
},
|
||||
];
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes
|
||||
routes,
|
||||
});
|
||||
|
||||
export const updatePageTitle = function updatePageTitle (route) {
|
||||
export const updatePageTitle = function updatePageTitle(route) {
|
||||
const title =
|
||||
typeof route.meta.title === 'function'
|
||||
? route.meta.title(route)
|
||||
@@ -78,7 +78,7 @@ export const updatePageTitle = function updatePageTitle (route) {
|
||||
document.title = `${title} - Seekra`;
|
||||
} else {
|
||||
document.title = 'Seekra';
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// remove trailing slash(es)
|
||||
@@ -88,9 +88,9 @@ router.beforeEach((route) => {
|
||||
path: route.path.replace(/\/+$/, ''),
|
||||
query: route.query,
|
||||
hash: route.hash,
|
||||
replace: true
|
||||
};
|
||||
replace: true,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
+15
-2
@@ -19,7 +19,19 @@ body {
|
||||
color: var(--dark);
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
font-family:
|
||||
'Inter',
|
||||
system-ui,
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
'Segoe UI',
|
||||
Roboto,
|
||||
Oxygen,
|
||||
Ubuntu,
|
||||
Cantarell,
|
||||
'Open Sans',
|
||||
'Helvetica Neue',
|
||||
sans-serif;
|
||||
}
|
||||
|
||||
.link {
|
||||
@@ -27,7 +39,8 @@ body {
|
||||
color: var(--dark);
|
||||
}
|
||||
|
||||
.link:hover:not(.button-link), .link:focus-visible:not(.button-link) {
|
||||
.link:hover:not(.button-link),
|
||||
.link:focus-visible:not(.button-link) {
|
||||
outline: none;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
@@ -102,7 +102,7 @@ body {
|
||||
--dark: var(--white);
|
||||
|
||||
--light: var(--black);
|
||||
--light-d-1: oklch(0.10 0 0);
|
||||
--light-d-1: oklch(0.1 0 0);
|
||||
--light-d-2: var(--black-l-2);
|
||||
--light-d-3: var(--black-l-3);
|
||||
--light-d-4: var(--black-l-4);
|
||||
@@ -136,7 +136,7 @@ body.dark {
|
||||
--dark: var(--white);
|
||||
|
||||
--light: var(--black);
|
||||
--light-d-1: oklch(0.10 0 0);
|
||||
--light-d-1: oklch(0.1 0 0);
|
||||
--light-d-2: var(--black-l-2);
|
||||
--light-d-3: var(--black-l-3);
|
||||
--light-d-4: var(--black-l-4);
|
||||
|
||||
@@ -15,34 +15,37 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import { vi } from 'vitest';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import { vi } from 'vitest';
|
||||
import { createI18n } from 'vue-i18n';
|
||||
import { createMemoryHistory, createRouter } from 'vue-router';
|
||||
|
||||
export const mountComponent = function mountComponent (component, options = {}, i18nMessages = {}, routes = [], piniaOptions = {}) {
|
||||
const {
|
||||
setupStores,
|
||||
...testingPiniaOptions
|
||||
} = piniaOptions;
|
||||
export const mountComponent = function mountComponent(
|
||||
component,
|
||||
options = {},
|
||||
i18nMessages = {},
|
||||
routes = [],
|
||||
piniaOptions = {},
|
||||
) {
|
||||
const { setupStores, ...testingPiniaOptions } = piniaOptions;
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: 'en',
|
||||
messages: {
|
||||
en: i18nMessages
|
||||
}
|
||||
en: i18nMessages,
|
||||
},
|
||||
});
|
||||
|
||||
const router = createRouter({
|
||||
history: createMemoryHistory(),
|
||||
routes
|
||||
routes,
|
||||
});
|
||||
|
||||
const testingPinia = createTestingPinia({
|
||||
...testingPiniaOptions,
|
||||
stubActions: false,
|
||||
createSpy: vi.fn
|
||||
createSpy: vi.fn,
|
||||
});
|
||||
|
||||
if (setupStores) {
|
||||
@@ -51,12 +54,8 @@ export const mountComponent = function mountComponent (component, options = {},
|
||||
|
||||
return mount(component, {
|
||||
global: {
|
||||
plugins: [
|
||||
i18n,
|
||||
router,
|
||||
testingPinia
|
||||
],
|
||||
plugins: [i18n, router, testingPinia],
|
||||
},
|
||||
...options
|
||||
...options,
|
||||
});
|
||||
};
|
||||
@@ -14,8 +14,8 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { describe, expect, test } from 'vitest';
|
||||
import { ensureUnit } from '../cssDimensions';
|
||||
import { describe, expect, test } from 'vitest';
|
||||
|
||||
describe('cssDimensions', () => {
|
||||
test.for([
|
||||
@@ -111,14 +111,26 @@ describe('cssDimensions', () => {
|
||||
{ dimension: 'calc(42vh)', expected: 'calc(42vh)' },
|
||||
{ dimension: 'min(42px, 1em)', expected: 'min(42px, 1em)' },
|
||||
{ dimension: 'max(42px, 5rem)', expected: 'max(42px, 5rem)' },
|
||||
{ dimension: 'clamp(42vh, 23vw, 13cap)', expected: 'clamp(42vh, 23vw, 13cap)' },
|
||||
{
|
||||
dimension: 'clamp(42vh, 23vw, 13cap)',
|
||||
expected: 'clamp(42vh, 23vw, 13cap)',
|
||||
},
|
||||
|
||||
{ dimension: 'calc(42px-1em)', expected: 'calc(42px-1em)' },
|
||||
{ dimension: 'min(42px,1em)', expected: 'min(42px,1em)' },
|
||||
{ dimension: 'max(42px,5rem)', expected: 'max(42px,5rem)' },
|
||||
{ dimension: 'clamp(42vh,23vw,13cap)', expected: 'clamp(42vh,23vw,13cap)' },
|
||||
{ dimension: 'clamp( 42vh,23vw,13cap )', expected: 'clamp( 42vh,23vw,13cap )' }
|
||||
])('ensureUnit returns $expected with input $dimension', ({ dimension, expected }) => {
|
||||
{
|
||||
dimension: 'clamp(42vh,23vw,13cap)',
|
||||
expected: 'clamp(42vh,23vw,13cap)',
|
||||
},
|
||||
{
|
||||
dimension: 'clamp( 42vh,23vw,13cap )',
|
||||
expected: 'clamp( 42vh,23vw,13cap )',
|
||||
},
|
||||
])(
|
||||
'ensureUnit returns $expected with input $dimension',
|
||||
({ dimension, expected }) => {
|
||||
expect(ensureUnit(dimension)).toBe(expected);
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
export const ensureUnit = function (d) {
|
||||
export const ensureUnit = function ensureUnit(d) {
|
||||
if (!d) d = 0;
|
||||
|
||||
// ensure that it is a string
|
||||
@@ -25,7 +25,7 @@ export const ensureUnit = function (d) {
|
||||
// if it ends with a number
|
||||
if (/\d$/.test(d)) {
|
||||
d += 'px';
|
||||
};
|
||||
}
|
||||
|
||||
return d;
|
||||
};
|
||||
@@ -40,7 +40,7 @@ const { t } = useI18n();
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
.error-message{
|
||||
.error-message {
|
||||
margin: 0;
|
||||
font-weight: 600;
|
||||
font-size: 1.75rem;
|
||||
|
||||
+10
-10
@@ -14,29 +14,29 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import vue from '@vitejs/plugin-vue';
|
||||
import path from 'path';
|
||||
import { defineConfig } from 'vite';
|
||||
import { createHtmlPlugin } from 'vite-plugin-html';
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
build: {
|
||||
target: 'es2020'
|
||||
target: 'es2020',
|
||||
},
|
||||
plugins: [
|
||||
vue(),
|
||||
createHtmlPlugin({
|
||||
minify: true
|
||||
})
|
||||
minify: true,
|
||||
}),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src')
|
||||
}
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
},
|
||||
},
|
||||
test: {
|
||||
globals: false,
|
||||
environment: 'jsdom'
|
||||
}
|
||||
})
|
||||
environment: 'jsdom',
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user