mirror of
https://github.com/YunoHost/yunohost-admin.git
synced 2024-09-03 20:06:15 +02:00
Merge pull request #553 from YunoHost/pre-vue3-prettier-yarn
[2. pre-vue3] prettier + yarn to match yunohost-portal stack
This commit is contained in:
commit
d0cca4d423
116 changed files with 5775 additions and 5466 deletions
6
.github/workflows/eslint.yml
vendored
6
.github/workflows/eslint.yml
vendored
|
@ -11,7 +11,7 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Install npm dependencies
|
- name: Install yarn dependencies
|
||||||
run: cd app && npm ci
|
run: cd app && yarn install --frozen-lockfile
|
||||||
- name: Run linter
|
- name: Run linter
|
||||||
run: cd app && npm run lint
|
run: cd app && yarn lint
|
||||||
|
|
|
@ -2,37 +2,17 @@ module.exports = {
|
||||||
root: true,
|
root: true,
|
||||||
env: {
|
env: {
|
||||||
es2021: true,
|
es2021: true,
|
||||||
node: true
|
node: true,
|
||||||
},
|
},
|
||||||
extends: [
|
extends: [
|
||||||
'plugin:vue/strongly-recommended',
|
'plugin:vue/strongly-recommended',
|
||||||
'eslint:recommended',
|
'eslint:recommended',
|
||||||
'standard'
|
'plugin:prettier/recommended',
|
||||||
],
|
],
|
||||||
rules: {
|
rules: {
|
||||||
'vue/max-attributes-per-line': [
|
|
||||||
'error',
|
|
||||||
{
|
|
||||||
singleline: 3,
|
|
||||||
multiline: 3
|
|
||||||
}
|
|
||||||
],
|
|
||||||
'vue/multi-word-component-names': 'off', // FIXME this should be adressed at some point
|
|
||||||
'no-console': 'warn',
|
|
||||||
'template-curly-spacing': 'off',
|
|
||||||
camelcase: 'warn',
|
|
||||||
indent: 'off',
|
|
||||||
'no-irregular-whitespace': 'off',
|
|
||||||
'no-unused-vars': [
|
'no-unused-vars': [
|
||||||
'warn',
|
'warn',
|
||||||
{ varsIgnorePattern: '^_', destructuredArrayIgnorePattern: '^_' }
|
{ varsIgnorePattern: '^_', destructuredArrayIgnorePattern: '^_' },
|
||||||
],
|
],
|
||||||
quotes: 'warn',
|
},
|
||||||
'no-multiple-empty-lines': [
|
|
||||||
'error',
|
|
||||||
{
|
|
||||||
max: 2
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
1
app/.prettierignore
Normal file
1
app/.prettierignore
Normal file
|
@ -0,0 +1 @@
|
||||||
|
dist/
|
19
app/.prettierrc
Normal file
19
app/.prettierrc
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"semi": false,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"singleQuote": true,
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": "**/*.json",
|
||||||
|
"options": {
|
||||||
|
"tabWidth": 4
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"files": "./*.json",
|
||||||
|
"options": {
|
||||||
|
"tabWidth": 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -1,23 +1,26 @@
|
||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||||
|
<meta
|
||||||
|
name="viewport"
|
||||||
|
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=yes"
|
||||||
|
/>
|
||||||
|
<meta name="format-detection" content="telephone=no" />
|
||||||
|
<meta name="robots" content="noindex, nofollow" />
|
||||||
|
<link rel="icon" href="/favicon.png" />
|
||||||
|
<title>YunoHost Admin</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
<head>
|
<body>
|
||||||
<meta charset="utf-8">
|
<noscript>
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<strong>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=yes">
|
We're sorry but YunoHost Admin doesn't work properly without JavaScript
|
||||||
<meta name="format-detection" content="telephone=no" />
|
enabled. Please enable it to continue.
|
||||||
<meta name="robots" content="noindex, nofollow">
|
</strong>
|
||||||
<link rel="icon" href="/favicon.png">
|
</noscript>
|
||||||
<title>YunoHost Admin</title>
|
<div id="app"></div>
|
||||||
</head>
|
<script type="module" src="/src/main.js"></script>
|
||||||
|
</body>
|
||||||
<body>
|
</html>
|
||||||
<noscript>
|
|
||||||
<strong>We're sorry but YunoHost Admin doesn't work properly without JavaScript enabled. Please enable it to
|
|
||||||
continue.</strong>
|
|
||||||
</noscript>
|
|
||||||
<div id="app"></div>
|
|
||||||
<script type="module" src="/src/main.js"></script>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
|
|
2703
app/package-lock.json
generated
2703
app/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -7,8 +7,10 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"lint": "eslint --ext .js,.vue src",
|
"lint:js": "eslint --ext \".ts,.vue,.cjs,.js\" --ignore-path ../.gitignore .",
|
||||||
"lint-fix": "lint --fix"
|
"lint:prettier": "prettier --check .",
|
||||||
|
"lint": "yarn lint:js && yarn lint:prettier",
|
||||||
|
"lintfix": "prettier --write --list-different . && yarn lint:js --fix"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource/fira-code": "^4.5.13",
|
"@fontsource/fira-code": "^4.5.13",
|
||||||
|
@ -28,11 +30,13 @@
|
||||||
"@vitejs/plugin-vue2": "^2.2.0",
|
"@vitejs/plugin-vue2": "^2.2.0",
|
||||||
"bootstrap": "^4.6.0",
|
"bootstrap": "^4.6.0",
|
||||||
"eslint": "^8.36.0",
|
"eslint": "^8.36.0",
|
||||||
|
"eslint-config-prettier": "^9.1.0",
|
||||||
|
"eslint-plugin-prettier": "^5.1.3",
|
||||||
"eslint-plugin-vue": "^9.10.0",
|
"eslint-plugin-vue": "^9.10.0",
|
||||||
"popper.js": "^1.16.0",
|
"popper.js": "^1.16.0",
|
||||||
"portal-vue": "^2.1.7",
|
"portal-vue": "^2.1.7",
|
||||||
|
"prettier": "^3.2.5",
|
||||||
"sass": "^1.60.0",
|
"sass": "^1.60.0",
|
||||||
"standard": "^17.0.0",
|
|
||||||
"vite": "^4.2.1"
|
"vite": "^4.2.1"
|
||||||
},
|
},
|
||||||
"browserslist": [
|
"browserslist": [
|
||||||
|
|
|
@ -4,23 +4,22 @@
|
||||||
<header>
|
<header>
|
||||||
<BNavbar>
|
<BNavbar>
|
||||||
<BNavbarBrand
|
<BNavbarBrand
|
||||||
:to="{ name: 'home' }" :disabled="waiting"
|
:to="{ name: 'home' }"
|
||||||
exact exact-active-class="active"
|
:disabled="waiting"
|
||||||
|
exact
|
||||||
|
exact-active-class="active"
|
||||||
>
|
>
|
||||||
<span v-if="theme">
|
<span v-if="theme">
|
||||||
<img alt="YunoHost logo" src="./assets/logo_light.png" width="40">
|
<img alt="YunoHost logo" src="./assets/logo_light.png" width="40" />
|
||||||
</span>
|
</span>
|
||||||
<span v-else>
|
<span v-else>
|
||||||
<img alt="YunoHost logo" src="./assets/logo_dark.png" width="40">
|
<img alt="YunoHost logo" src="./assets/logo_dark.png" width="40" />
|
||||||
</span>
|
</span>
|
||||||
</BNavbarBrand>
|
</BNavbarBrand>
|
||||||
|
|
||||||
<BNavbarNav class="ml-auto">
|
<BNavbarNav class="ml-auto">
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<BButton
|
<BButton :href="ssoLink" variant="primary" size="sm" block>
|
||||||
:href="ssoLink"
|
|
||||||
variant="primary" size="sm" block
|
|
||||||
>
|
|
||||||
{{ $t('user_interface_link') }} <YIcon iname="user" />
|
{{ $t('user_interface_link') }} <YIcon iname="user" />
|
||||||
</BButton>
|
</BButton>
|
||||||
</li>
|
</li>
|
||||||
|
@ -28,7 +27,9 @@
|
||||||
<li class="nav-item" v-show="connected">
|
<li class="nav-item" v-show="connected">
|
||||||
<BButton
|
<BButton
|
||||||
@click.prevent="logout"
|
@click.prevent="logout"
|
||||||
variant="outline-dark" block size="sm"
|
variant="outline-dark"
|
||||||
|
block
|
||||||
|
size="sm"
|
||||||
>
|
>
|
||||||
{{ $t('logout') }} <YIcon iname="sign-out" />
|
{{ $t('logout') }} <YIcon iname="sign-out" />
|
||||||
</BButton>
|
</BButton>
|
||||||
|
@ -58,18 +59,32 @@
|
||||||
<footer class="py-3 mt-auto">
|
<footer class="py-3 mt-auto">
|
||||||
<nav>
|
<nav>
|
||||||
<BNav class="justify-content-center">
|
<BNav class="justify-content-center">
|
||||||
<BNavItem href="https://yunohost.org/docs" target="_blank" link-classes="text-secondary">
|
<BNavItem
|
||||||
|
href="https://yunohost.org/docs"
|
||||||
|
target="_blank"
|
||||||
|
link-classes="text-secondary"
|
||||||
|
>
|
||||||
<YIcon iname="book" /> {{ $t('footer.documentation') }}
|
<YIcon iname="book" /> {{ $t('footer.documentation') }}
|
||||||
</BNavItem>
|
</BNavItem>
|
||||||
<BNavItem href="https://yunohost.org/help" target="_blank" link-classes="text-secondary">
|
<BNavItem
|
||||||
|
href="https://yunohost.org/help"
|
||||||
|
target="_blank"
|
||||||
|
link-classes="text-secondary"
|
||||||
|
>
|
||||||
<YIcon iname="life-ring" /> {{ $t('footer.help') }}
|
<YIcon iname="life-ring" /> {{ $t('footer.help') }}
|
||||||
</BNavItem>
|
</BNavItem>
|
||||||
<BNavItem href="https://donate.yunohost.org/" target="_blank" link-classes="text-secondary">
|
<BNavItem
|
||||||
|
href="https://donate.yunohost.org/"
|
||||||
|
target="_blank"
|
||||||
|
link-classes="text-secondary"
|
||||||
|
>
|
||||||
<YIcon iname="heart" /> {{ $t('footer.donate') }}
|
<YIcon iname="heart" /> {{ $t('footer.donate') }}
|
||||||
</BNavItem>
|
</BNavItem>
|
||||||
|
|
||||||
<BNavText
|
<BNavText
|
||||||
v-if="yunohost" id="yunohost-version" class="ml-md-auto text-center"
|
v-if="yunohost"
|
||||||
|
id="yunohost-version"
|
||||||
|
class="ml-md-auto text-center"
|
||||||
>
|
>
|
||||||
<span v-html="$t('footer_version', yunohost)" />
|
<span v-html="$t('footer_version', yunohost)" />
|
||||||
</BNavText>
|
</BNavText>
|
||||||
|
@ -89,7 +104,7 @@ export default {
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
HistoryConsole,
|
HistoryConsole,
|
||||||
ViewLockOverlay
|
ViewLockOverlay,
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
|
@ -101,29 +116,31 @@ export default {
|
||||||
'transitionName',
|
'transitionName',
|
||||||
'waiting',
|
'waiting',
|
||||||
'theme',
|
'theme',
|
||||||
'ssoLink'
|
'ssoLink',
|
||||||
])
|
]),
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
async logout () {
|
async logout() {
|
||||||
this.$store.dispatch('LOGOUT')
|
this.$store.dispatch('LOGOUT')
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
// This hook is only triggered at page first load
|
// This hook is only triggered at page first load
|
||||||
created () {
|
created() {
|
||||||
this.$store.dispatch('ON_APP_CREATED')
|
this.$store.dispatch('ON_APP_CREATED')
|
||||||
},
|
},
|
||||||
|
|
||||||
mounted () {
|
mounted() {
|
||||||
// Unlock copypasta on log view
|
// Unlock copypasta on log view
|
||||||
const copypastaCode = ['ArrowDown', 'ArrowDown', 'ArrowUp', 'ArrowUp']
|
const copypastaCode = ['ArrowDown', 'ArrowDown', 'ArrowUp', 'ArrowUp']
|
||||||
let copypastastep = 0
|
let copypastastep = 0
|
||||||
document.addEventListener('keydown', ({ key }) => {
|
document.addEventListener('keydown', ({ key }) => {
|
||||||
if (key === copypastaCode[copypastastep++]) {
|
if (key === copypastaCode[copypastastep++]) {
|
||||||
if (copypastastep === copypastaCode.length) {
|
if (copypastastep === copypastaCode.length) {
|
||||||
document.getElementsByClassName('unselectable').forEach((element) => element.classList.remove('unselectable'))
|
document
|
||||||
|
.getElementsByClassName('unselectable')
|
||||||
|
.forEach((element) => element.classList.remove('unselectable'))
|
||||||
copypastastep = 0
|
copypastastep = 0
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -132,7 +149,18 @@ export default {
|
||||||
})
|
})
|
||||||
|
|
||||||
// Konamicode ;P
|
// Konamicode ;P
|
||||||
const konamiCode = ['ArrowUp', 'ArrowUp', 'ArrowDown', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'ArrowLeft', 'ArrowRight', 'b', 'a']
|
const konamiCode = [
|
||||||
|
'ArrowUp',
|
||||||
|
'ArrowUp',
|
||||||
|
'ArrowDown',
|
||||||
|
'ArrowDown',
|
||||||
|
'ArrowLeft',
|
||||||
|
'ArrowRight',
|
||||||
|
'ArrowLeft',
|
||||||
|
'ArrowRight',
|
||||||
|
'b',
|
||||||
|
'a',
|
||||||
|
]
|
||||||
let konamistep = 0
|
let konamistep = 0
|
||||||
document.addEventListener('keydown', ({ key }) => {
|
document.addEventListener('keydown', ({ key }) => {
|
||||||
if (key === konamiCode[konamistep++]) {
|
if (key === konamiCode[konamistep++]) {
|
||||||
|
@ -157,7 +185,7 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
document.documentElement.setAttribute('dark-theme', this.theme) // updates the data-theme attribute
|
document.documentElement.setAttribute('dark-theme', this.theme) // updates the data-theme attribute
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -177,14 +205,14 @@ header {
|
||||||
padding: 1rem 0;
|
padding: 1rem 0;
|
||||||
|
|
||||||
img {
|
img {
|
||||||
width: 70px;
|
width: 70px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-nav {
|
.navbar-nav {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
li {
|
li {
|
||||||
margin: .2rem 0;
|
margin: 0.2rem 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -195,15 +223,17 @@ main {
|
||||||
|
|
||||||
// Routes transition
|
// Routes transition
|
||||||
.animated {
|
.animated {
|
||||||
transition: all .15s ease-in-out;
|
transition: all 0.15s ease-in-out;
|
||||||
}
|
}
|
||||||
.slide-left-enter, .slide-right-leave-active {
|
.slide-left-enter,
|
||||||
|
.slide-right-leave-active {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
top: 0;
|
top: 0;
|
||||||
transform: translate(100vw, 0);
|
transform: translate(100vw, 0);
|
||||||
}
|
}
|
||||||
.slide-left-leave-active, .slide-right-enter {
|
.slide-left-leave-active,
|
||||||
|
.slide-right-enter {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
@ -229,7 +259,7 @@ footer {
|
||||||
|
|
||||||
.nav-item {
|
.nav-item {
|
||||||
& + .nav-item a::before {
|
& + .nav-item a::before {
|
||||||
content: "•";
|
content: '•';
|
||||||
width: 1rem;
|
width: 1rem;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-left: -1.15rem;
|
margin-left: -1.15rem;
|
||||||
|
|
|
@ -6,7 +6,6 @@
|
||||||
import store from '@/store'
|
import store from '@/store'
|
||||||
import { openWebSocket, getResponseData, handleError } from './handlers'
|
import { openWebSocket, getResponseData, handleError } from './handlers'
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Options available for an API call.
|
* Options available for an API call.
|
||||||
*
|
*
|
||||||
|
@ -17,7 +16,6 @@ import { openWebSocket, getResponseData, handleError } from './handlers'
|
||||||
* @property {Boolean} asFormData - if `true`, will send the data with a body encoded as `"multipart/form-data"` instead of `"x-www-form-urlencoded"`).
|
* @property {Boolean} asFormData - if `true`, will send the data with a body encoded as `"multipart/form-data"` instead of `"x-www-form-urlencoded"`).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Representation of an API call for `api.fetchAll`
|
* Representation of an API call for `api.fetchAll`
|
||||||
*
|
*
|
||||||
|
@ -26,8 +24,7 @@ import { openWebSocket, getResponseData, handleError } from './handlers'
|
||||||
* @property {String|Object} 1 - "uri", uri to call as string or as an object for cached uris.
|
* @property {String|Object} 1 - "uri", uri to call as string or as an object for cached uris.
|
||||||
* @property {Object|null} 2 - "data"
|
* @property {Object|null} 2 - "data"
|
||||||
* @property {Options} 3 - "options"
|
* @property {Options} 3 - "options"
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts an object literal into an `URLSearchParams` that can be turned into a
|
* Converts an object literal into an `URLSearchParams` that can be turned into a
|
||||||
|
@ -38,11 +35,15 @@ import { openWebSocket, getResponseData, handleError } from './handlers'
|
||||||
* @param {Boolean} [options.addLocale=false] - Option to append the locale to the query string.
|
* @param {Boolean} [options.addLocale=false] - Option to append the locale to the query string.
|
||||||
* @return {URLSearchParams}
|
* @return {URLSearchParams}
|
||||||
*/
|
*/
|
||||||
export function objectToParams (obj, { addLocale = false } = {}, formData = false) {
|
export function objectToParams(
|
||||||
const urlParams = (formData) ? new FormData() : new URLSearchParams()
|
obj,
|
||||||
|
{ addLocale = false } = {},
|
||||||
|
formData = false,
|
||||||
|
) {
|
||||||
|
const urlParams = formData ? new FormData() : new URLSearchParams()
|
||||||
for (const [key, value] of Object.entries(obj)) {
|
for (const [key, value] of Object.entries(obj)) {
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
value.forEach(v => urlParams.append(key, v))
|
value.forEach((v) => urlParams.append(key, v))
|
||||||
} else {
|
} else {
|
||||||
urlParams.append(key, value)
|
urlParams.append(key, value)
|
||||||
}
|
}
|
||||||
|
@ -53,7 +54,6 @@ export function objectToParams (obj, { addLocale = false } = {}, formData = fals
|
||||||
return urlParams
|
return urlParams
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
options: {
|
options: {
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
|
@ -64,11 +64,10 @@ export default {
|
||||||
// Auto header is :
|
// Auto header is :
|
||||||
// "Accept": "*/*",
|
// "Accept": "*/*",
|
||||||
|
|
||||||
'X-Requested-With': 'XMLHttpRequest'
|
'X-Requested-With': 'XMLHttpRequest',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generic method to fetch the api without automatic response handling.
|
* Generic method to fetch the api without automatic response handling.
|
||||||
*
|
*
|
||||||
|
@ -78,15 +77,22 @@ export default {
|
||||||
* @param {Options} [options={ wait = true, websocket = true, initial = false, asFormData = false }]
|
* @param {Options} [options={ wait = true, websocket = true, initial = false, asFormData = false }]
|
||||||
* @return {Promise<Object|Error>} Promise that resolve the api response data or an error.
|
* @return {Promise<Object|Error>} Promise that resolve the api response data or an error.
|
||||||
*/
|
*/
|
||||||
async fetch (
|
async fetch(
|
||||||
method,
|
method,
|
||||||
uri,
|
uri,
|
||||||
data = {},
|
data = {},
|
||||||
humanKey = null,
|
humanKey = null,
|
||||||
{ wait = true, websocket = true, initial = false, asFormData = false } = {}
|
{ wait = true, websocket = true, initial = false, asFormData = false } = {},
|
||||||
) {
|
) {
|
||||||
// `await` because Vuex actions returns promises by default.
|
// `await` because Vuex actions returns promises by default.
|
||||||
const request = await store.dispatch('INIT_REQUEST', { method, uri, humanKey, initial, wait, websocket })
|
const request = await store.dispatch('INIT_REQUEST', {
|
||||||
|
method,
|
||||||
|
uri,
|
||||||
|
humanKey,
|
||||||
|
initial,
|
||||||
|
wait,
|
||||||
|
websocket,
|
||||||
|
})
|
||||||
|
|
||||||
if (websocket) {
|
if (websocket) {
|
||||||
await openWebSocket(request)
|
await openWebSocket(request)
|
||||||
|
@ -96,17 +102,22 @@ export default {
|
||||||
if (method === 'GET') {
|
if (method === 'GET') {
|
||||||
uri += `${uri.includes('?') ? '&' : '?'}locale=${store.getters.locale}`
|
uri += `${uri.includes('?') ? '&' : '?'}locale=${store.getters.locale}`
|
||||||
} else {
|
} else {
|
||||||
options = { ...options, method, body: objectToParams(data, { addLocale: true }, true) }
|
options = {
|
||||||
|
...options,
|
||||||
|
method,
|
||||||
|
body: objectToParams(data, { addLocale: true }, true),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch('/yunohost/api/' + uri, options)
|
const response = await fetch('/yunohost/api/' + uri, options)
|
||||||
const responseData = await getResponseData(response)
|
const responseData = await getResponseData(response)
|
||||||
store.dispatch('END_REQUEST', { request, success: response.ok, wait })
|
store.dispatch('END_REQUEST', { request, success: response.ok, wait })
|
||||||
|
|
||||||
return response.ok ? responseData : handleError(request, response, responseData)
|
return response.ok
|
||||||
|
? responseData
|
||||||
|
: handleError(request, response, responseData)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Api multiple queries helper.
|
* Api multiple queries helper.
|
||||||
* Those calls will act as one (declare optional waiting for one but still create history entries for each)
|
* Those calls will act as one (declare optional waiting for one but still create history entries for each)
|
||||||
|
@ -117,14 +128,16 @@ export default {
|
||||||
* @param {Boolean}
|
* @param {Boolean}
|
||||||
* @return {Promise<Array|Error>} Promise that resolve the api responses data or an error.
|
* @return {Promise<Array|Error>} Promise that resolve the api responses data or an error.
|
||||||
*/
|
*/
|
||||||
async fetchAll (queries, { wait, initial } = {}) {
|
async fetchAll(queries, { wait, initial } = {}) {
|
||||||
const results = []
|
const results = []
|
||||||
if (wait) store.commit('SET_WAITING', true)
|
if (wait) store.commit('SET_WAITING', true)
|
||||||
try {
|
try {
|
||||||
for (const [method, uri, data, humanKey, options = {}] of queries) {
|
for (const [method, uri, data, humanKey, options = {}] of queries) {
|
||||||
if (wait) options.wait = false
|
if (wait) options.wait = false
|
||||||
if (initial) options.initial = true
|
if (initial) options.initial = true
|
||||||
results.push(await this[method.toLowerCase()](uri, data, humanKey, options))
|
results.push(
|
||||||
|
await this[method.toLowerCase()](uri, data, humanKey, options),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
// Stop waiting even if there is an error.
|
// Stop waiting even if there is an error.
|
||||||
|
@ -134,7 +147,6 @@ export default {
|
||||||
return results
|
return results
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Api get helper function.
|
* Api get helper function.
|
||||||
*
|
*
|
||||||
|
@ -143,13 +155,13 @@ export default {
|
||||||
* @param {Options} [options={}] - options to apply to the call (default is `{ websocket: false, wait: false }`)
|
* @param {Options} [options={}] - options to apply to the call (default is `{ websocket: false, wait: false }`)
|
||||||
* @return {Promise<Object|Error>} Promise that resolve the api response data or an error.
|
* @return {Promise<Object|Error>} Promise that resolve the api response data or an error.
|
||||||
*/
|
*/
|
||||||
get (uri, data = null, humanKey = null, options = {}) {
|
get(uri, data = null, humanKey = null, options = {}) {
|
||||||
options = { websocket: false, wait: false, ...options }
|
options = { websocket: false, wait: false, ...options }
|
||||||
if (typeof uri === 'string') return this.fetch('GET', uri, null, humanKey, options)
|
if (typeof uri === 'string')
|
||||||
|
return this.fetch('GET', uri, null, humanKey, options)
|
||||||
return store.dispatch('GET', { ...uri, humanKey, options })
|
return store.dispatch('GET', { ...uri, humanKey, options })
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Api post helper function.
|
* Api post helper function.
|
||||||
*
|
*
|
||||||
|
@ -158,12 +170,12 @@ export default {
|
||||||
* @param {Options} [options={}] - options to apply to the call
|
* @param {Options} [options={}] - options to apply to the call
|
||||||
* @return {Promise<Object|Error>} Promise that resolve the api response data or an error.
|
* @return {Promise<Object|Error>} Promise that resolve the api response data or an error.
|
||||||
*/
|
*/
|
||||||
post (uri, data = {}, humanKey = null, options = {}) {
|
post(uri, data = {}, humanKey = null, options = {}) {
|
||||||
if (typeof uri === 'string') return this.fetch('POST', uri, data, humanKey, options)
|
if (typeof uri === 'string')
|
||||||
|
return this.fetch('POST', uri, data, humanKey, options)
|
||||||
return store.dispatch('POST', { ...uri, data, humanKey, options })
|
return store.dispatch('POST', { ...uri, data, humanKey, options })
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Api put helper function.
|
* Api put helper function.
|
||||||
*
|
*
|
||||||
|
@ -172,12 +184,12 @@ export default {
|
||||||
* @param {Options} [options={}] - options to apply to the call
|
* @param {Options} [options={}] - options to apply to the call
|
||||||
* @return {Promise<Object|Error>} Promise that resolve the api response data or an error.
|
* @return {Promise<Object|Error>} Promise that resolve the api response data or an error.
|
||||||
*/
|
*/
|
||||||
put (uri, data = {}, humanKey = null, options = {}) {
|
put(uri, data = {}, humanKey = null, options = {}) {
|
||||||
if (typeof uri === 'string') return this.fetch('PUT', uri, data, humanKey, options)
|
if (typeof uri === 'string')
|
||||||
|
return this.fetch('PUT', uri, data, humanKey, options)
|
||||||
return store.dispatch('PUT', { ...uri, data, humanKey, options })
|
return store.dispatch('PUT', { ...uri, data, humanKey, options })
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Api delete helper function.
|
* Api delete helper function.
|
||||||
*
|
*
|
||||||
|
@ -186,8 +198,9 @@ export default {
|
||||||
* @param {Options} [options={}] - options to apply to the call (default is `{ websocket: false, wait: false }`)
|
* @param {Options} [options={}] - options to apply to the call (default is `{ websocket: false, wait: false }`)
|
||||||
* @return {Promise<Object|Error>} Promise that resolve the api response data or an error.
|
* @return {Promise<Object|Error>} Promise that resolve the api response data or an error.
|
||||||
*/
|
*/
|
||||||
delete (uri, data = {}, humanKey = null, options = {}) {
|
delete(uri, data = {}, humanKey = null, options = {}) {
|
||||||
if (typeof uri === 'string') return this.fetch('DELETE', uri, data, humanKey, options)
|
if (typeof uri === 'string')
|
||||||
|
return this.fetch('DELETE', uri, data, humanKey, options)
|
||||||
return store.dispatch('DELETE', { ...uri, data, humanKey, options })
|
return store.dispatch('DELETE', { ...uri, data, humanKey, options })
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -199,24 +212,27 @@ export default {
|
||||||
* @param {Number} initialDelay - delay before calling the API for the first time in ms.
|
* @param {Number} initialDelay - delay before calling the API for the first time in ms.
|
||||||
* @return {Promise<undefined|Error>}
|
* @return {Promise<undefined|Error>}
|
||||||
*/
|
*/
|
||||||
tryToReconnect ({ attemps = 5, delay = 2000, initialDelay = 0 } = {}) {
|
tryToReconnect({ attemps = 5, delay = 2000, initialDelay = 0 } = {}) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const api = this
|
const api = this
|
||||||
|
|
||||||
function reconnect (n) {
|
function reconnect(n) {
|
||||||
api.get('logout', {}, { key: 'reconnecting' }).then(resolve).catch(err => {
|
api
|
||||||
if (err.name === 'APIUnauthorizedError') {
|
.get('logout', {}, { key: 'reconnecting' })
|
||||||
resolve()
|
.then(resolve)
|
||||||
} else if (n < 1) {
|
.catch((err) => {
|
||||||
reject(err)
|
if (err.name === 'APIUnauthorizedError') {
|
||||||
} else {
|
resolve()
|
||||||
setTimeout(() => reconnect(n - 1), delay)
|
} else if (n < 1) {
|
||||||
}
|
reject(err)
|
||||||
})
|
} else {
|
||||||
|
setTimeout(() => reconnect(n - 1), delay)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (initialDelay > 0) setTimeout(() => reconnect(attemps), initialDelay)
|
if (initialDelay > 0) setTimeout(() => reconnect(attemps), initialDelay)
|
||||||
else reconnect(attemps)
|
else reconnect(attemps)
|
||||||
})
|
})
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,10 +5,13 @@
|
||||||
|
|
||||||
import i18n from '@/i18n'
|
import i18n from '@/i18n'
|
||||||
|
|
||||||
|
|
||||||
class APIError extends Error {
|
class APIError extends Error {
|
||||||
constructor (request, { url, status, statusText }, { error }) {
|
constructor(request, { url, status, statusText }, { error }) {
|
||||||
super(error ? error.replaceAll('\n', '<br>') : i18n.t('error_server_unexpected'))
|
super(
|
||||||
|
error
|
||||||
|
? error.replaceAll('\n', '<br>')
|
||||||
|
: i18n.t('error_server_unexpected'),
|
||||||
|
)
|
||||||
const urlObj = new URL(url)
|
const urlObj = new URL(url)
|
||||||
this.name = 'APIError'
|
this.name = 'APIError'
|
||||||
this.code = status
|
this.code = status
|
||||||
|
@ -18,7 +21,7 @@ class APIError extends Error {
|
||||||
this.path = urlObj.pathname + urlObj.search
|
this.path = urlObj.pathname + urlObj.search
|
||||||
}
|
}
|
||||||
|
|
||||||
log () {
|
log() {
|
||||||
/* eslint-disable-next-line */
|
/* eslint-disable-next-line */
|
||||||
console.error(`${this.name} (${this.code}): ${this.uri}\n${this.message}`)
|
console.error(`${this.name} (${this.code}): ${this.uri}\n${this.message}`)
|
||||||
}
|
}
|
||||||
|
@ -26,26 +29,24 @@ class APIError extends Error {
|
||||||
|
|
||||||
// Log (Special error to trigger a redirect to a log page)
|
// Log (Special error to trigger a redirect to a log page)
|
||||||
class APIErrorLog extends APIError {
|
class APIErrorLog extends APIError {
|
||||||
constructor (method, response, errorData) {
|
constructor(method, response, errorData) {
|
||||||
super(method, response, errorData)
|
super(method, response, errorData)
|
||||||
this.logRef = errorData.log_ref
|
this.logRef = errorData.log_ref
|
||||||
this.name = 'APIErrorLog'
|
this.name = 'APIErrorLog'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// 0 — (means "the connexion has been closed" apparently)
|
// 0 — (means "the connexion has been closed" apparently)
|
||||||
class APIConnexionError extends APIError {
|
class APIConnexionError extends APIError {
|
||||||
constructor (method, response) {
|
constructor(method, response) {
|
||||||
super(method, response, { error: i18n.t('error_connection_interrupted') })
|
super(method, response, { error: i18n.t('error_connection_interrupted') })
|
||||||
this.name = 'APIConnexionError'
|
this.name = 'APIConnexionError'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// 400 — Bad Request
|
// 400 — Bad Request
|
||||||
class APIBadRequestError extends APIError {
|
class APIBadRequestError extends APIError {
|
||||||
constructor (method, response, errorData) {
|
constructor(method, response, errorData) {
|
||||||
super(method, response, errorData)
|
super(method, response, errorData)
|
||||||
this.name = 'APIBadRequestError'
|
this.name = 'APIBadRequestError'
|
||||||
this.key = errorData.error_key
|
this.key = errorData.error_key
|
||||||
|
@ -53,45 +54,40 @@ class APIBadRequestError extends APIError {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// 401 — Unauthorized
|
// 401 — Unauthorized
|
||||||
class APIUnauthorizedError extends APIError {
|
class APIUnauthorizedError extends APIError {
|
||||||
constructor (method, response, errorData) {
|
constructor(method, response, errorData) {
|
||||||
super(method, response, { error: i18n.t('unauthorized') })
|
super(method, response, { error: i18n.t('unauthorized') })
|
||||||
this.name = 'APIUnauthorizedError'
|
this.name = 'APIUnauthorizedError'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// 404 — Not Found
|
// 404 — Not Found
|
||||||
class APINotFoundError extends APIError {
|
class APINotFoundError extends APIError {
|
||||||
constructor (method, response, errorData) {
|
constructor(method, response, errorData) {
|
||||||
errorData.error = i18n.t('api_not_found')
|
errorData.error = i18n.t('api_not_found')
|
||||||
super(method, response, errorData)
|
super(method, response, errorData)
|
||||||
this.name = 'APINotFoundError'
|
this.name = 'APINotFoundError'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// 500 — Server Internal Error
|
// 500 — Server Internal Error
|
||||||
class APIInternalError extends APIError {
|
class APIInternalError extends APIError {
|
||||||
constructor (method, response, errorData) {
|
constructor(method, response, errorData) {
|
||||||
super(method, response, errorData)
|
super(method, response, errorData)
|
||||||
this.traceback = errorData.traceback || null
|
this.traceback = errorData.traceback || null
|
||||||
this.name = 'APIInternalError'
|
this.name = 'APIInternalError'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// 502 — Bad gateway (means API is down)
|
// 502 — Bad gateway (means API is down)
|
||||||
class APINotRespondingError extends APIError {
|
class APINotRespondingError extends APIError {
|
||||||
constructor (method, response) {
|
constructor(method, response) {
|
||||||
super(method, response, { error: i18n.t('api_not_responding') })
|
super(method, response, { error: i18n.t('api_not_responding') })
|
||||||
this.name = 'APINotRespondingError'
|
this.name = 'APINotRespondingError'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Temp factory
|
// Temp factory
|
||||||
const errors = {
|
const errors = {
|
||||||
[undefined]: APIError,
|
[undefined]: APIError,
|
||||||
|
@ -101,10 +97,9 @@ const errors = {
|
||||||
401: APIUnauthorizedError,
|
401: APIUnauthorizedError,
|
||||||
404: APINotFoundError,
|
404: APINotFoundError,
|
||||||
500: APIInternalError,
|
500: APIInternalError,
|
||||||
502: APINotRespondingError
|
502: APINotRespondingError,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
errors as default,
|
errors as default,
|
||||||
APIError,
|
APIError,
|
||||||
|
@ -114,5 +109,5 @@ export {
|
||||||
APIInternalError,
|
APIInternalError,
|
||||||
APINotFoundError,
|
APINotFoundError,
|
||||||
APINotRespondingError,
|
APINotRespondingError,
|
||||||
APIUnauthorizedError
|
APIUnauthorizedError,
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,14 +6,13 @@
|
||||||
import store from '@/store'
|
import store from '@/store'
|
||||||
import errors, { APIError } from './errors'
|
import errors, { APIError } from './errors'
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Try to get response content as json and if it's not as text.
|
* Try to get response content as json and if it's not as text.
|
||||||
*
|
*
|
||||||
* @param {Response} response - A fetch `Response` object.
|
* @param {Response} response - A fetch `Response` object.
|
||||||
* @return {(Object|String)} Parsed response's json or response's text.
|
* @return {(Object|String)} Parsed response's json or response's text.
|
||||||
*/
|
*/
|
||||||
export async function getResponseData (response) {
|
export async function getResponseData(response) {
|
||||||
// FIXME the api should always return json as response
|
// FIXME the api should always return json as response
|
||||||
const responseText = await response.text()
|
const responseText = await response.text()
|
||||||
try {
|
try {
|
||||||
|
@ -23,7 +22,6 @@ export async function getResponseData (response) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opens a WebSocket connection to the server in case it sends messages.
|
* Opens a WebSocket connection to the server in case it sends messages.
|
||||||
* Currently, the connection is closed by the server right after an API call so
|
* Currently, the connection is closed by the server right after an API call so
|
||||||
|
@ -33,11 +31,16 @@ export async function getResponseData (response) {
|
||||||
* @param {Object} request - Request info data.
|
* @param {Object} request - Request info data.
|
||||||
* @return {Promise<Event>} Promise that resolve on websocket 'open' or 'error' event.
|
* @return {Promise<Event>} Promise that resolve on websocket 'open' or 'error' event.
|
||||||
*/
|
*/
|
||||||
export function openWebSocket (request) {
|
export function openWebSocket(request) {
|
||||||
return new Promise(resolve => {
|
return new Promise((resolve) => {
|
||||||
const ws = new WebSocket(`wss://${store.getters.host}/yunohost/api/messages`)
|
const ws = new WebSocket(
|
||||||
|
`wss://${store.getters.host}/yunohost/api/messages`,
|
||||||
|
)
|
||||||
ws.onmessage = ({ data }) => {
|
ws.onmessage = ({ data }) => {
|
||||||
store.dispatch('DISPATCH_MESSAGE', { request, messages: JSON.parse(data) })
|
store.dispatch('DISPATCH_MESSAGE', {
|
||||||
|
request,
|
||||||
|
messages: JSON.parse(data),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
// ws.onclose = (e) => {}
|
// ws.onclose = (e) => {}
|
||||||
ws.onopen = resolve
|
ws.onopen = resolve
|
||||||
|
@ -46,7 +49,6 @@ export function openWebSocket (request) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handler for API errors.
|
* Handler for API errors.
|
||||||
*
|
*
|
||||||
|
@ -55,7 +57,7 @@ export function openWebSocket (request) {
|
||||||
* @param {Object|String} errorData - The response parsed json/text.
|
* @param {Object|String} errorData - The response parsed json/text.
|
||||||
* @throws Will throw a `APIError` with request and response data.
|
* @throws Will throw a `APIError` with request and response data.
|
||||||
*/
|
*/
|
||||||
export async function handleError (request, response, errorData) {
|
export async function handleError(request, response, errorData) {
|
||||||
let errorCode = response.status in errors ? response.status : undefined
|
let errorCode = response.status in errors ? response.status : undefined
|
||||||
if (typeof errorData === 'string') {
|
if (typeof errorData === 'string') {
|
||||||
// FIXME API: Patching errors that are plain text or html.
|
// FIXME API: Patching errors that are plain text or html.
|
||||||
|
@ -70,26 +72,24 @@ export async function handleError (request, response, errorData) {
|
||||||
throw new errors[errorCode](request, response, errorData)
|
throw new errors[errorCode](request, response, errorData)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If an APIError is not catched by a view it will be dispatched to the store so the
|
* If an APIError is not catched by a view it will be dispatched to the store so the
|
||||||
* error can be displayed in the error modal.
|
* error can be displayed in the error modal.
|
||||||
*
|
*
|
||||||
* @param {APIError} error
|
* @param {APIError} error
|
||||||
*/
|
*/
|
||||||
export function onUnhandledAPIError (error) {
|
export function onUnhandledAPIError(error) {
|
||||||
error.log()
|
error.log()
|
||||||
store.dispatch('HANDLE_ERROR', error)
|
store.dispatch('HANDLE_ERROR', error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Global catching of unhandled promise's rejections.
|
* Global catching of unhandled promise's rejections.
|
||||||
* Those errors (thrown or rejected from inside a promise) can't be catched by
|
* Those errors (thrown or rejected from inside a promise) can't be catched by
|
||||||
* `window.onerror`.
|
* `window.onerror`.
|
||||||
*/
|
*/
|
||||||
export function registerGlobalErrorHandlers () {
|
export function registerGlobalErrorHandlers() {
|
||||||
window.addEventListener('unhandledrejection', e => {
|
window.addEventListener('unhandledrejection', (e) => {
|
||||||
const error = e.reason
|
const error = e.reason
|
||||||
if (error instanceof APIError) {
|
if (error instanceof APIError) {
|
||||||
onUnhandledAPIError(error)
|
onUnhandledAPIError(error)
|
||||||
|
|
|
@ -24,8 +24,16 @@
|
||||||
/>
|
/>
|
||||||
</BInputGroupAppend>
|
</BInputGroupAppend>
|
||||||
|
|
||||||
<span class="sr-only" :id="id + 'local-part-desc'" v-t="'address.local_part_description.' + type" />
|
<span
|
||||||
<span class="sr-only" :id="id + 'domain-desc'" v-t="'address.domain_description.' + type" />
|
class="sr-only"
|
||||||
|
:id="id + 'local-part-desc'"
|
||||||
|
v-t="'address.local_part_description.' + type"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
class="sr-only"
|
||||||
|
:id="id + 'domain-desc'"
|
||||||
|
v-t="'address.domain_description.' + type"
|
||||||
|
/>
|
||||||
</BInputGroup>
|
</BInputGroup>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -42,17 +50,17 @@ export default {
|
||||||
placeholder: { type: String, default: null },
|
placeholder: { type: String, default: null },
|
||||||
id: { type: String, default: null },
|
id: { type: String, default: null },
|
||||||
state: { type: null, default: null },
|
state: { type: null, default: null },
|
||||||
type: { type: String, default: 'email' }
|
type: { type: String, default: 'email' },
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
onInput (key, value) {
|
onInput(key, value) {
|
||||||
this.$emit('input', {
|
this.$emit('input', {
|
||||||
...this.value,
|
...this.value,
|
||||||
[key]: value
|
[key]: value,
|
||||||
})
|
})
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
<template>
|
<template>
|
||||||
<BCard
|
<BCard v-bind="$attrs" no-body :class="_class">
|
||||||
v-bind="$attrs"
|
|
||||||
no-body :class="_class"
|
|
||||||
>
|
|
||||||
<template #header>
|
<template #header>
|
||||||
<slot name="header">
|
<slot name="header">
|
||||||
<h2>
|
<h2>
|
||||||
<BButton v-b-toggle="id" :variant="variant" class="card-collapse-button">
|
<BButton
|
||||||
|
v-b-toggle="id"
|
||||||
|
:variant="variant"
|
||||||
|
class="card-collapse-button"
|
||||||
|
>
|
||||||
{{ title }}
|
{{ title }}
|
||||||
<YIcon class="ml-auto" iname="chevron-right" />
|
<YIcon class="ml-auto" iname="chevron-right" />
|
||||||
</BButton>
|
</BButton>
|
||||||
|
@ -29,21 +30,21 @@ export default {
|
||||||
title: { type: String, required: true },
|
title: { type: String, required: true },
|
||||||
variant: { type: String, default: 'white' },
|
variant: { type: String, default: 'white' },
|
||||||
visible: { type: Boolean, default: false },
|
visible: { type: Boolean, default: false },
|
||||||
flush: { type: Boolean, default: false }
|
flush: { type: Boolean, default: false },
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
_class () {
|
_class() {
|
||||||
const baseClass = 'card-collapse'
|
const baseClass = 'card-collapse'
|
||||||
return [
|
return [
|
||||||
baseClass,
|
baseClass,
|
||||||
{
|
{
|
||||||
[`${baseClass}-flush`]: this.flush,
|
[`${baseClass}-flush`]: this.flush,
|
||||||
[`${baseClass}-${this.variant}`]: this.variant
|
[`${baseClass}-${this.variant}`]: this.variant,
|
||||||
}
|
},
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -57,10 +58,10 @@ export default {
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
padding-top: $spacer * .5;
|
padding-top: $spacer * 0.5;
|
||||||
padding-bottom: $spacer * .5;
|
padding-bottom: $spacer * 0.5;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
font: inherit
|
font: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-flush {
|
&-flush {
|
||||||
|
|
|
@ -6,25 +6,30 @@ export default {
|
||||||
name: 'CardDeckFeed',
|
name: 'CardDeckFeed',
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
stacks: { type: Number, default: 21 }
|
stacks: { type: Number, default: 21 },
|
||||||
},
|
},
|
||||||
|
|
||||||
data () {
|
data() {
|
||||||
return {
|
return {
|
||||||
busy: false,
|
busy: false,
|
||||||
range: this.stacks,
|
range: this.stacks,
|
||||||
childrenCount: this.$slots.default.length
|
childrenCount: this.$slots.default.length,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
getTopParent (prev) {
|
getTopParent(prev) {
|
||||||
return prev.parentElement === this.$refs.feed ? prev : this.getTopParent(prev.parentElement)
|
return prev.parentElement === this.$refs.feed
|
||||||
|
? prev
|
||||||
|
: this.getTopParent(prev.parentElement)
|
||||||
},
|
},
|
||||||
|
|
||||||
onScroll () {
|
onScroll() {
|
||||||
const elem = this.$refs.feed
|
const elem = this.$refs.feed
|
||||||
if (window.innerHeight > elem.clientHeight + elem.getBoundingClientRect().top - 200) {
|
if (
|
||||||
|
window.innerHeight >
|
||||||
|
elem.clientHeight + elem.getBoundingClientRect().top - 200
|
||||||
|
) {
|
||||||
this.busy = true
|
this.busy = true
|
||||||
this.range = Math.min(this.range + this.stacks, this.childrenCount)
|
this.range = Math.min(this.range + this.stacks, this.childrenCount)
|
||||||
this.$nextTick().then(() => {
|
this.$nextTick().then(() => {
|
||||||
|
@ -33,7 +38,7 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onKeydown (e) {
|
onKeydown(e) {
|
||||||
if (['PageUp', 'PageDown'].includes(e.code)) {
|
if (['PageUp', 'PageDown'].includes(e.code)) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
const key = e.code === 'PageUp' ? 'previous' : 'next'
|
const key = e.code === 'PageUp' ? 'previous' : 'next'
|
||||||
|
@ -44,16 +49,16 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// FIXME Add `Home` and `End` shorcuts
|
// FIXME Add `Home` and `End` shorcuts
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
mounted () {
|
mounted() {
|
||||||
window.addEventListener('scroll', this.onScroll)
|
window.addEventListener('scroll', this.onScroll)
|
||||||
this.$refs.feed.addEventListener('keydown', this.onKeydown)
|
this.$refs.feed.addEventListener('keydown', this.onKeydown)
|
||||||
this.onScroll()
|
this.onScroll()
|
||||||
},
|
},
|
||||||
|
|
||||||
beforeUpdate () {
|
beforeUpdate() {
|
||||||
const slots = this.$slots.default
|
const slots = this.$slots.default
|
||||||
if (this.childrenCount !== slots.length) {
|
if (this.childrenCount !== slots.length) {
|
||||||
this.range = this.stacks
|
this.range = this.stacks
|
||||||
|
@ -61,21 +66,21 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
render (h) {
|
render(h) {
|
||||||
return h(
|
return h(
|
||||||
'BCardGroup',
|
'BCardGroup',
|
||||||
{
|
{
|
||||||
attrs: { role: 'feed', 'aria-busy': this.busy.toString() },
|
attrs: { role: 'feed', 'aria-busy': this.busy.toString() },
|
||||||
props: { deck: true },
|
props: { deck: true },
|
||||||
ref: 'feed'
|
ref: 'feed',
|
||||||
},
|
},
|
||||||
this.$slots.default.slice(0, this.range)
|
this.$slots.default.slice(0, this.range),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
||||||
beforeDestroy () {
|
beforeDestroy() {
|
||||||
window.removeEventListener('scroll', this.onScroll)
|
window.removeEventListener('scroll', this.onScroll)
|
||||||
this.$refs.feed.removeEventListener('keydown', this.onKeydown)
|
this.$refs.feed.removeEventListener('keydown', this.onKeydown)
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
<template>
|
<template>
|
||||||
<AbstractForm
|
<AbstractForm
|
||||||
v-bind="{ id: panel.id + '-form', validation, serverError: panel.serverError }"
|
v-bind="{
|
||||||
|
id: panel.id + '-form',
|
||||||
|
validation,
|
||||||
|
serverError: panel.serverError,
|
||||||
|
}"
|
||||||
@submit.prevent.stop="onApply"
|
@submit.prevent.stop="onApply"
|
||||||
:no-footer="!panel.hasApplyButton"
|
:no-footer="!panel.hasApplyButton"
|
||||||
>
|
>
|
||||||
|
@ -20,15 +24,20 @@
|
||||||
class="panel-section"
|
class="panel-section"
|
||||||
>
|
>
|
||||||
<BCardTitle v-if="section.name" title-tag="h3">
|
<BCardTitle v-if="section.name" title-tag="h3">
|
||||||
{{ section.name }} <small v-if="section.help">{{ section.help }}</small>
|
{{ section.name }}
|
||||||
|
<small v-if="section.help">{{ section.help }}</small>
|
||||||
</BCardTitle>
|
</BCardTitle>
|
||||||
|
|
||||||
<template v-for="(field, fname) in section.fields">
|
<template v-for="(field, fname) in section.fields">
|
||||||
<!-- FIXME rework the whole component chain to avoid direct mutation of the `forms` props -->
|
<!-- FIXME rework the whole component chain to avoid direct mutation of the `forms` props -->
|
||||||
<!-- eslint-disable -->
|
<!-- eslint-disable -->
|
||||||
<Component
|
<Component
|
||||||
v-if="field.visible" :is="field.is" v-bind="field.props"
|
v-if="field.visible"
|
||||||
v-model="forms[panel.id][fname]" :validation="validation[fname]" :key="fname"
|
:is="field.is"
|
||||||
|
v-bind="field.props"
|
||||||
|
v-model="forms[panel.id][fname]"
|
||||||
|
:validation="validation[fname]"
|
||||||
|
:key="fname"
|
||||||
@action.stop="onAction(section.id, fname, section.fields)"
|
@action.stop="onAction(section.id, fname, section.fields)"
|
||||||
/>
|
/>
|
||||||
<!-- eslint-enable -->
|
<!-- eslint-enable -->
|
||||||
|
@ -43,7 +52,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { filterObject } from '@/helpers/commons'
|
import { filterObject } from '@/helpers/commons'
|
||||||
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ConfigPanel',
|
name: 'ConfigPanel',
|
||||||
|
|
||||||
|
@ -51,41 +59,43 @@ export default {
|
||||||
tabId: { type: String, required: true },
|
tabId: { type: String, required: true },
|
||||||
panels: { type: Array, default: undefined },
|
panels: { type: Array, default: undefined },
|
||||||
forms: { type: Object, default: undefined },
|
forms: { type: Object, default: undefined },
|
||||||
v: { type: Object, default: undefined }
|
v: { type: Object, default: undefined },
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
panel () {
|
panel() {
|
||||||
return this.panels.find(panel => panel.id === this.tabId)
|
return this.panels.find((panel) => panel.id === this.tabId)
|
||||||
},
|
},
|
||||||
|
|
||||||
validation () {
|
validation() {
|
||||||
return this.v.forms[this.panel.id]
|
return this.v.forms[this.panel.id]
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
onApply () {
|
onApply() {
|
||||||
const panelId = this.panel.id
|
const panelId = this.panel.id
|
||||||
|
|
||||||
this.$emit('submit', {
|
this.$emit('submit', {
|
||||||
id: panelId,
|
id: panelId,
|
||||||
form: this.forms[panelId]
|
form: this.forms[panelId],
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
onAction (sectionId, actionId, actionFields) {
|
onAction(sectionId, actionId, actionFields) {
|
||||||
const panelId = this.panel.id
|
const panelId = this.panel.id
|
||||||
const actionFieldsKeys = Object.keys(actionFields)
|
const actionFieldsKeys = Object.keys(actionFields)
|
||||||
|
|
||||||
this.$emit('submit', {
|
this.$emit('submit', {
|
||||||
id: panelId,
|
id: panelId,
|
||||||
form: filterObject(this.forms[panelId], ([key]) => actionFieldsKeys.includes(key)),
|
form: filterObject(this.forms[panelId], ([key]) =>
|
||||||
|
actionFieldsKeys.includes(key),
|
||||||
|
),
|
||||||
action: [panelId, sectionId, actionId].join('.'),
|
action: [panelId, sectionId, actionId].join('.'),
|
||||||
name: actionId
|
name: actionId,
|
||||||
})
|
})
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -32,7 +32,7 @@ export default {
|
||||||
name: 'ConfigPanels',
|
name: 'ConfigPanels',
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
RoutableTabs: () => import('@/components/RoutableTabs.vue')
|
RoutableTabs: () => import('@/components/RoutableTabs.vue'),
|
||||||
},
|
},
|
||||||
|
|
||||||
mixins: [validationMixin],
|
mixins: [validationMixin],
|
||||||
|
@ -43,28 +43,28 @@ export default {
|
||||||
validations: { type: Object, default: undefined },
|
validations: { type: Object, default: undefined },
|
||||||
errors: { type: Object, default: undefined }, // never used
|
errors: { type: Object, default: undefined }, // never used
|
||||||
routes: { type: Array, default: null },
|
routes: { type: Array, default: null },
|
||||||
noRedirect: { type: Boolean, default: false }
|
noRedirect: { type: Boolean, default: false },
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
routes_ () {
|
routes_() {
|
||||||
if (this.routes) return this.routes
|
if (this.routes) return this.routes
|
||||||
return this.panels.map(panel => ({
|
return this.panels.map((panel) => ({
|
||||||
to: { params: { tabId: panel.id } },
|
to: { params: { tabId: panel.id } },
|
||||||
text: panel.name,
|
text: panel.name,
|
||||||
icon: panel.icon || 'wrench'
|
icon: panel.icon || 'wrench',
|
||||||
}))
|
}))
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
validations () {
|
validations() {
|
||||||
return { forms: this.validations }
|
return { forms: this.validations }
|
||||||
},
|
},
|
||||||
|
|
||||||
created () {
|
created() {
|
||||||
if (!this.noRedirect && !this.$route.params.tabId) {
|
if (!this.noRedirect && !this.$route.params.tabId) {
|
||||||
this.$router.replace({ params: { tabId: this.panels[0].id } })
|
this.$router.replace({ params: { tabId: this.panels[0].id } })
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -13,58 +13,64 @@ export default {
|
||||||
minHeight: { type: Number, default: 0 },
|
minHeight: { type: Number, default: 0 },
|
||||||
renderDelay: { type: Number, default: 100 },
|
renderDelay: { type: Number, default: 100 },
|
||||||
unrenderDelay: { type: Number, default: 2000 },
|
unrenderDelay: { type: Number, default: 2000 },
|
||||||
rootMargin: { type: String, default: '300px' }
|
rootMargin: { type: String, default: '300px' },
|
||||||
},
|
},
|
||||||
|
|
||||||
data () {
|
data() {
|
||||||
return {
|
return {
|
||||||
observer: null,
|
observer: null,
|
||||||
render: false,
|
render: false,
|
||||||
fixedMinHeight: this.minHeight
|
fixedMinHeight: this.minHeight,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
mounted () {
|
mounted() {
|
||||||
let unrenderTimer
|
let unrenderTimer
|
||||||
let renderTimer
|
let renderTimer
|
||||||
|
|
||||||
this.observer = new IntersectionObserver(entries => {
|
this.observer = new IntersectionObserver(
|
||||||
let intersecting = entries[0].isIntersecting
|
(entries) => {
|
||||||
|
let intersecting = entries[0].isIntersecting
|
||||||
|
|
||||||
// Fix for weird bug when typing fast in app search or on slow client.
|
// Fix for weird bug when typing fast in app search or on slow client.
|
||||||
// Intersection is triggered but even if the element is indeed in the viewport,
|
// Intersection is triggered but even if the element is indeed in the viewport,
|
||||||
// isIntersecting is `false`, so we have to manually check this…
|
// isIntersecting is `false`, so we have to manually check this…
|
||||||
// FIXME Would be great to find out why this is happening
|
// FIXME Would be great to find out why this is happening
|
||||||
if (!intersecting && this.$el.offsetTop < window.innerHeight) {
|
if (!intersecting && this.$el.offsetTop < window.innerHeight) {
|
||||||
intersecting = true
|
intersecting = true
|
||||||
}
|
|
||||||
|
|
||||||
if (intersecting) {
|
|
||||||
clearTimeout(unrenderTimer)
|
|
||||||
// Show the component after a delay (to avoid rendering while scrolling fast)
|
|
||||||
renderTimer = setTimeout(() => {
|
|
||||||
this.render = true
|
|
||||||
}, this.unrender ? this.renderDelay : 0)
|
|
||||||
|
|
||||||
if (!this.unrender) {
|
|
||||||
// Stop listening to intersections after first appearance if unrendering is not activated
|
|
||||||
this.observer.disconnect()
|
|
||||||
}
|
}
|
||||||
} else if (this.unrender) {
|
|
||||||
clearTimeout(renderTimer)
|
if (intersecting) {
|
||||||
// Hide the component after a delay if it's no longer in the viewport
|
clearTimeout(unrenderTimer)
|
||||||
unrenderTimer = setTimeout(() => {
|
// Show the component after a delay (to avoid rendering while scrolling fast)
|
||||||
this.fixedMinHeight = this.$el.clientHeight
|
renderTimer = setTimeout(
|
||||||
this.render = false
|
() => {
|
||||||
}, this.unrenderDelay)
|
this.render = true
|
||||||
}
|
},
|
||||||
}, { rootMargin: this.rootMargin })
|
this.unrender ? this.renderDelay : 0,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!this.unrender) {
|
||||||
|
// Stop listening to intersections after first appearance if unrendering is not activated
|
||||||
|
this.observer.disconnect()
|
||||||
|
}
|
||||||
|
} else if (this.unrender) {
|
||||||
|
clearTimeout(renderTimer)
|
||||||
|
// Hide the component after a delay if it's no longer in the viewport
|
||||||
|
unrenderTimer = setTimeout(() => {
|
||||||
|
this.fixedMinHeight = this.$el.clientHeight
|
||||||
|
this.render = false
|
||||||
|
}, this.unrenderDelay)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ rootMargin: this.rootMargin },
|
||||||
|
)
|
||||||
|
|
||||||
this.observer.observe(this.$el)
|
this.observer.observe(this.$el)
|
||||||
},
|
},
|
||||||
|
|
||||||
beforeDestroy () {
|
beforeDestroy() {
|
||||||
this.observer.disconnect()
|
this.observer.disconnect()
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,17 +1,21 @@
|
||||||
<template>
|
<template>
|
||||||
<BListGroup
|
<BListGroup
|
||||||
v-bind="$attrs" flush
|
v-bind="$attrs"
|
||||||
:class="{ 'fixed-height': fixedHeight, 'bordered': bordered }"
|
flush
|
||||||
|
:class="{ 'fixed-height': fixedHeight, bordered: bordered }"
|
||||||
@scroll="onScroll"
|
@scroll="onScroll"
|
||||||
>
|
>
|
||||||
<YListGroupItem
|
<YListGroupItem
|
||||||
v-if="limit && messages.length > limit"
|
v-if="limit && messages.length > limit"
|
||||||
variant="info" v-t="'api.partial_logs'"
|
variant="info"
|
||||||
|
v-t="'api.partial_logs'"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<YListGroupItem
|
<YListGroupItem
|
||||||
v-for="({ color, text }, i) in reducedMessages" :key="i"
|
v-for="({ color, text }, i) in reducedMessages"
|
||||||
:variant="color" size="xs"
|
:key="i"
|
||||||
|
:variant="color"
|
||||||
|
size="xs"
|
||||||
>
|
>
|
||||||
<span v-html="text" />
|
<span v-html="text" />
|
||||||
</YListGroupItem>
|
</YListGroupItem>
|
||||||
|
@ -27,43 +31,43 @@ export default {
|
||||||
fixedHeight: { type: Boolean, default: false },
|
fixedHeight: { type: Boolean, default: false },
|
||||||
bordered: { type: Boolean, default: false },
|
bordered: { type: Boolean, default: false },
|
||||||
autoScroll: { type: Boolean, default: false },
|
autoScroll: { type: Boolean, default: false },
|
||||||
limit: { type: Number, default: null }
|
limit: { type: Number, default: null },
|
||||||
},
|
},
|
||||||
|
|
||||||
data () {
|
data() {
|
||||||
return {
|
return {
|
||||||
auto: true
|
auto: true,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
reducedMessages () {
|
reducedMessages() {
|
||||||
const len = this.messages.length
|
const len = this.messages.length
|
||||||
if (!this.limit || len <= this.limit) {
|
if (!this.limit || len <= this.limit) {
|
||||||
return this.messages
|
return this.messages
|
||||||
}
|
}
|
||||||
return this.messages.slice(len - this.limit)
|
return this.messages.slice(len - this.limit)
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
scrollToEnd () {
|
scrollToEnd() {
|
||||||
if (!this.auto) return
|
if (!this.auto) return
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.$el.scrollTo(0, this.$el.lastElementChild.offsetTop)
|
this.$el.scrollTo(0, this.$el.lastElementChild.offsetTop)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
onScroll ({ target }) {
|
onScroll({ target }) {
|
||||||
this.auto = target.scrollHeight === target.scrollTop + target.clientHeight
|
this.auto = target.scrollHeight === target.scrollTop + target.clientHeight
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
created () {
|
created() {
|
||||||
if (this.autoScroll) {
|
if (this.autoScroll) {
|
||||||
this.$watch('messages', this.scrollToEnd)
|
this.$watch('messages', this.scrollToEnd)
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="query-header w-100" v-on="$listeners" v-bind="$attrs">
|
<div class="query-header w-100" v-on="$listeners" v-bind="$attrs">
|
||||||
<!-- STATUS -->
|
<!-- STATUS -->
|
||||||
<span class="status" :class="['bg-' + color, statusSize]" :aria-label="$t('api.query_status.' + request.status)" />
|
<span
|
||||||
|
class="status"
|
||||||
|
:class="['bg-' + color, statusSize]"
|
||||||
|
:aria-label="$t('api.query_status.' + request.status)"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- REQUEST DESCRIPTION -->
|
<!-- REQUEST DESCRIPTION -->
|
||||||
<strong class="request-desc">
|
<strong class="request-desc">
|
||||||
|
@ -15,14 +19,16 @@
|
||||||
</span>
|
</span>
|
||||||
<!-- WEBSOCKET WARNINGS COUNT -->
|
<!-- WEBSOCKET WARNINGS COUNT -->
|
||||||
<span class="count" v-if="request.warnings">
|
<span class="count" v-if="request.warnings">
|
||||||
{{ request.warnings }}<YIcon iname="warning" class="text-warning ml-1" />
|
{{ request.warnings
|
||||||
|
}}<YIcon iname="warning" class="text-warning ml-1" />
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- VIEW ERROR BUTTON -->
|
<!-- VIEW ERROR BUTTON -->
|
||||||
<BButton
|
<BButton
|
||||||
v-if="showError && request.error"
|
v-if="showError && request.error"
|
||||||
size="sm" pill
|
size="sm"
|
||||||
|
pill
|
||||||
class="error-btn ml-auto py-0"
|
class="error-btn ml-auto py-0"
|
||||||
variant="danger"
|
variant="danger"
|
||||||
@click="reviewError"
|
@click="reviewError"
|
||||||
|
@ -31,7 +37,11 @@
|
||||||
</BButton>
|
</BButton>
|
||||||
|
|
||||||
<!-- TIME DISPLAY -->
|
<!-- TIME DISPLAY -->
|
||||||
<time v-if="showTime" :datetime="hour(request.date)" :class="request.error ? 'ml-2' : 'ml-auto'">
|
<time
|
||||||
|
v-if="showTime"
|
||||||
|
:datetime="hour(request.date)"
|
||||||
|
:class="request.error ? 'ml-2' : 'ml-auto'"
|
||||||
|
>
|
||||||
{{ hour(request.date) }}
|
{{ hour(request.date) }}
|
||||||
</time>
|
</time>
|
||||||
</div>
|
</div>
|
||||||
|
@ -45,38 +55,40 @@ export default {
|
||||||
request: { type: Object, required: true },
|
request: { type: Object, required: true },
|
||||||
statusSize: { type: String, default: '' },
|
statusSize: { type: String, default: '' },
|
||||||
showTime: { type: Boolean, default: false },
|
showTime: { type: Boolean, default: false },
|
||||||
showError: { type: Boolean, default: false }
|
showError: { type: Boolean, default: false },
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
color () {
|
color() {
|
||||||
const statuses = {
|
const statuses = {
|
||||||
pending: 'primary',
|
pending: 'primary',
|
||||||
success: 'success',
|
success: 'success',
|
||||||
warning: 'warning',
|
warning: 'warning',
|
||||||
error: 'danger'
|
error: 'danger',
|
||||||
}
|
}
|
||||||
return statuses[this.request.status]
|
return statuses[this.request.status]
|
||||||
},
|
},
|
||||||
|
|
||||||
errorsCount () {
|
errorsCount() {
|
||||||
return this.request.messages.filter(({ type }) => type === 'danger').length
|
return this.request.messages.filter(({ type }) => type === 'danger')
|
||||||
|
.length
|
||||||
},
|
},
|
||||||
|
|
||||||
warningsCount () {
|
warningsCount() {
|
||||||
return this.request.messages.filter(({ type }) => type === 'warning').length
|
return this.request.messages.filter(({ type }) => type === 'warning')
|
||||||
}
|
.length
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
reviewError () {
|
reviewError() {
|
||||||
this.$store.dispatch('REVIEW_ERROR', this.request)
|
this.$store.dispatch('REVIEW_ERROR', this.request)
|
||||||
},
|
},
|
||||||
|
|
||||||
hour (date) {
|
hour(date) {
|
||||||
return new Date(date).toLocaleTimeString()
|
return new Date(date).toLocaleTimeString()
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -98,15 +110,15 @@ div {
|
||||||
.status {
|
.status {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
width: .75rem;
|
width: 0.75rem;
|
||||||
min-width: .75rem;
|
min-width: 0.75rem;
|
||||||
height: .75rem;
|
height: 0.75rem;
|
||||||
margin-right: .25rem;
|
margin-right: 0.25rem;
|
||||||
|
|
||||||
&.lg {
|
&.lg {
|
||||||
width: 1rem;
|
width: 1rem;
|
||||||
height: 1rem;
|
height: 1rem;
|
||||||
margin-right: .5rem;
|
margin-right: 0.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -118,7 +130,7 @@ time {
|
||||||
.count {
|
.count {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-left: .5rem;
|
margin-left: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@include media-breakpoint-down(xs) {
|
@include media-breakpoint-down(xs) {
|
||||||
|
@ -126,5 +138,4 @@ time {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -3,16 +3,20 @@
|
||||||
<template v-for="(node, i) in tree.children">
|
<template v-for="(node, i) in tree.children">
|
||||||
<BListGroupItem
|
<BListGroupItem
|
||||||
:key="node.id"
|
:key="node.id"
|
||||||
class="list-group-item-action" :class="getClasses(node, i)"
|
class="list-group-item-action"
|
||||||
|
:class="getClasses(node, i)"
|
||||||
@click="$router.push(node.data.to)"
|
@click="$router.push(node.data.to)"
|
||||||
>
|
>
|
||||||
<slot name="default" v-bind="node" />
|
<slot name="default" v-bind="node" />
|
||||||
|
|
||||||
<BButton
|
<BButton
|
||||||
v-if="node.children"
|
v-if="node.children"
|
||||||
size="xs" variant="outline-secondary"
|
size="xs"
|
||||||
:aria-expanded="node.data.opened ? 'true' : 'false'" :aria-controls="'collapse-' + node.id"
|
variant="outline-secondary"
|
||||||
:class="node.data.opened ? 'not-collapsed' : 'collapsed'" class="ml-2"
|
:aria-expanded="node.data.opened ? 'true' : 'false'"
|
||||||
|
:aria-controls="'collapse-' + node.id"
|
||||||
|
:class="node.data.opened ? 'not-collapsed' : 'collapsed'"
|
||||||
|
class="ml-2"
|
||||||
@click.stop="node.data.opened = !node.data.opened"
|
@click.stop="node.data.opened = !node.data.opened"
|
||||||
>
|
>
|
||||||
<span class="sr-only">{{ toggleText }}</span>
|
<span class="sr-only">{{ toggleText }}</span>
|
||||||
|
@ -21,12 +25,15 @@
|
||||||
</BListGroupItem>
|
</BListGroupItem>
|
||||||
|
|
||||||
<BCollapse
|
<BCollapse
|
||||||
v-if="node.children" :key="'collapse-' + node.id"
|
v-if="node.children"
|
||||||
v-model="node.data.opened" :id="'collapse-' + node.id"
|
:key="'collapse-' + node.id"
|
||||||
|
v-model="node.data.opened"
|
||||||
|
:id="'collapse-' + node.id"
|
||||||
>
|
>
|
||||||
<RecursiveListGroup
|
<RecursiveListGroup
|
||||||
:tree="node"
|
:tree="node"
|
||||||
:last="last !== undefined ? last : i === tree.children.length - 1" flush
|
:last="last !== undefined ? last : i === tree.children.length - 1"
|
||||||
|
flush
|
||||||
>
|
>
|
||||||
<!-- PASS THE DEFAULT SLOT WITH SCOPE TO NEXT NESTED COMPONENT -->
|
<!-- PASS THE DEFAULT SLOT WITH SCOPE TO NEXT NESTED COMPONENT -->
|
||||||
<template slot="default" slot-scope="scope">
|
<template slot="default" slot-scope="scope">
|
||||||
|
@ -46,17 +53,20 @@ export default {
|
||||||
tree: { type: Object, required: true },
|
tree: { type: Object, required: true },
|
||||||
flush: { type: Boolean, default: false },
|
flush: { type: Boolean, default: false },
|
||||||
last: { type: Boolean, default: undefined },
|
last: { type: Boolean, default: undefined },
|
||||||
toggleText: { type: String, default: null }
|
toggleText: { type: String, default: null },
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
getClasses (node, i) {
|
getClasses(node, i) {
|
||||||
const children = node.height > 0
|
const children = node.height > 0
|
||||||
const opened = children && node.data.opened
|
const opened = children && node.data.opened
|
||||||
const last = this.last !== false && (!children || !opened) && i === this.tree.children.length - 1
|
const last =
|
||||||
|
this.last !== false &&
|
||||||
|
(!children || !opened) &&
|
||||||
|
i === this.tree.children.length - 1
|
||||||
return { collapsible: children, uncollapsible: !children, opened, last }
|
return { collapsible: children, uncollapsible: !children, opened, last }
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -3,8 +3,11 @@
|
||||||
<BCardHeader header-tag="nav">
|
<BCardHeader header-tag="nav">
|
||||||
<BNav card-header fill pills>
|
<BNav card-header fill pills>
|
||||||
<BNavItem
|
<BNavItem
|
||||||
v-for="route in routes" :key="route.text"
|
v-for="route in routes"
|
||||||
:to="route.to" exact exact-active-class="active"
|
:key="route.text"
|
||||||
|
:to="route.to"
|
||||||
|
exact
|
||||||
|
exact-active-class="active"
|
||||||
>
|
>
|
||||||
<YIcon v-if="route.icon" :iname="route.icon" />
|
<YIcon v-if="route.icon" :iname="route.icon" />
|
||||||
{{ route.text }}
|
{{ route.text }}
|
||||||
|
@ -36,7 +39,7 @@ export default {
|
||||||
inheritAttrs: false,
|
inheritAttrs: false,
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
routes: { type: Array, required: true }
|
routes: { type: Array, required: true },
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -4,16 +4,16 @@
|
||||||
<slot name="disclaimer" />
|
<slot name="disclaimer" />
|
||||||
|
|
||||||
<BForm
|
<BForm
|
||||||
:id="id" :inline="inline" :class="formClasses"
|
:id="id"
|
||||||
@submit.prevent="onSubmit" novalidate
|
:inline="inline"
|
||||||
|
:class="formClasses"
|
||||||
|
@submit.prevent="onSubmit"
|
||||||
|
novalidate
|
||||||
>
|
>
|
||||||
<slot name="default" />
|
<slot name="default" />
|
||||||
|
|
||||||
<slot name="server-error" v-bind="{ errorFeedback }">
|
<slot name="server-error" v-bind="{ errorFeedback }">
|
||||||
<BAlert
|
<BAlert v-if="errorFeedback" variant="danger" class="my-3" icon="ban">
|
||||||
v-if="errorFeedback"
|
|
||||||
variant="danger" class="my-3" icon="ban"
|
|
||||||
>
|
|
||||||
<div v-html="errorFeedback" />
|
<div v-html="errorFeedback" />
|
||||||
</BAlert>
|
</BAlert>
|
||||||
</slot>
|
</slot>
|
||||||
|
@ -41,28 +41,28 @@ export default {
|
||||||
serverError: { type: String, default: '' },
|
serverError: { type: String, default: '' },
|
||||||
inline: { type: Boolean, default: false },
|
inline: { type: Boolean, default: false },
|
||||||
formClasses: { type: [Array, String, Object], default: null },
|
formClasses: { type: [Array, String, Object], default: null },
|
||||||
noFooter: { type: Boolean, default: false }
|
noFooter: { type: Boolean, default: false },
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
errorFeedback () {
|
errorFeedback() {
|
||||||
if (this.serverError) return this.serverError
|
if (this.serverError) return this.serverError
|
||||||
else if (this.validation && this.validation.$anyError) {
|
else if (this.validation && this.validation.$anyError) {
|
||||||
return this.$i18n.t('form_errors.invalid_form')
|
return this.$i18n.t('form_errors.invalid_form')
|
||||||
} else return ''
|
} else return ''
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
onSubmit (e) {
|
onSubmit(e) {
|
||||||
const v = this.validation
|
const v = this.validation
|
||||||
if (v) {
|
if (v) {
|
||||||
v.$touch()
|
v.$touch()
|
||||||
if (v.$pending || v.$invalid) return
|
if (v.$pending || v.$invalid) return
|
||||||
}
|
}
|
||||||
this.$emit('submit', e)
|
this.$emit('submit', e)
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -73,7 +73,7 @@ export default {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
& > *:not(:first-child) {
|
& > *:not(:first-child) {
|
||||||
margin-left: .5rem;
|
margin-left: 0.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -4,14 +4,19 @@
|
||||||
<slot name="disclaimer" />
|
<slot name="disclaimer" />
|
||||||
|
|
||||||
<BForm
|
<BForm
|
||||||
:id="id" :inline="inline" :class="formClasses"
|
:id="id"
|
||||||
@submit.prevent="onSubmit" novalidate
|
:inline="inline"
|
||||||
|
:class="formClasses"
|
||||||
|
@submit.prevent="onSubmit"
|
||||||
|
novalidate
|
||||||
>
|
>
|
||||||
<slot name="default" />
|
<slot name="default" />
|
||||||
|
|
||||||
<slot name="server-error">
|
<slot name="server-error">
|
||||||
<BAlert
|
<BAlert
|
||||||
variant="danger" class="my-3" icon="ban"
|
variant="danger"
|
||||||
|
class="my-3"
|
||||||
|
icon="ban"
|
||||||
:show="errorFeedback !== ''"
|
:show="errorFeedback !== ''"
|
||||||
>
|
>
|
||||||
<div v-html="errorFeedback" />
|
<div v-html="errorFeedback" />
|
||||||
|
@ -41,30 +46,29 @@ export default {
|
||||||
serverError: { type: String, default: '' },
|
serverError: { type: String, default: '' },
|
||||||
inline: { type: Boolean, default: false },
|
inline: { type: Boolean, default: false },
|
||||||
formClasses: { type: [Array, String, Object], default: null },
|
formClasses: { type: [Array, String, Object], default: null },
|
||||||
noFooter: { type: Boolean, default: false }
|
noFooter: { type: Boolean, default: false },
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
errorFeedback () {
|
errorFeedback() {
|
||||||
if (this.serverError) return this.serverError
|
if (this.serverError) return this.serverError
|
||||||
else if (this.validation && this.validation.$anyError) {
|
else if (this.validation && this.validation.$anyError) {
|
||||||
return this.$i18n.t('form_errors.invalid_form')
|
return this.$i18n.t('form_errors.invalid_form')
|
||||||
} else return ''
|
} else return ''
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
onSubmit (e) {
|
onSubmit(e) {
|
||||||
const v = this.validation
|
const v = this.validation
|
||||||
if (v) {
|
if (v) {
|
||||||
v.$touch()
|
v.$touch()
|
||||||
if (v.$pending || v.$invalid) return
|
if (v.$pending || v.$invalid) return
|
||||||
}
|
}
|
||||||
this.$emit('submit', e)
|
this.$emit('submit', e)
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss"></style>
|
||||||
</style>
|
|
||||||
|
|
|
@ -21,21 +21,21 @@ export default {
|
||||||
props: {
|
props: {
|
||||||
term: { type: String, default: null },
|
term: { type: String, default: null },
|
||||||
details: { type: String, default: null },
|
details: { type: String, default: null },
|
||||||
cols: { type: Object, default: () => ({ md: 4, xl: 3 }) }
|
cols: { type: Object, default: () => ({ md: 4, xl: 3 }) },
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
cols_ () {
|
cols_() {
|
||||||
return Object.assign({ md: 4, xl: 3 }, this.cols)
|
return Object.assign({ md: 4, xl: 3 }, this.cols)
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.description-row {
|
.description-row {
|
||||||
@include media-breakpoint-up(md) {
|
@include media-breakpoint-up(md) {
|
||||||
margin: .25rem 0;
|
margin: 0.25rem 0;
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: rgba($black, 0.05);
|
background-color: rgba($black, 0.05);
|
||||||
border-radius: 0.2rem;
|
border-radius: 0.2rem;
|
||||||
|
|
|
@ -2,17 +2,19 @@
|
||||||
<span class="explain-what">
|
<span class="explain-what">
|
||||||
<slot name="default" />
|
<slot name="default" />
|
||||||
<span class="explain-what-popover-container">
|
<span class="explain-what-popover-container">
|
||||||
<BButton
|
<BButton :id="id" href="#" variant="light">
|
||||||
:id="id" href="#"
|
|
||||||
variant="light"
|
|
||||||
>
|
|
||||||
<YIcon iname="question" />
|
<YIcon iname="question" />
|
||||||
<span class="sr-only">{{ $t('details_about', { subject: title }) }}</span>
|
<span class="sr-only">
|
||||||
|
{{ $t('details_about', { subject: title }) }}
|
||||||
|
</span>
|
||||||
</BButton>
|
</BButton>
|
||||||
<BPopover
|
<BPopover
|
||||||
placement="auto"
|
placement="auto"
|
||||||
:target="id" triggers="focus" custom-class="explain-what-popover"
|
:target="id"
|
||||||
:variant="variant" :title="title"
|
triggers="focus"
|
||||||
|
custom-class="explain-what-popover"
|
||||||
|
:variant="variant"
|
||||||
|
:title="title"
|
||||||
>
|
>
|
||||||
<span v-html="content" />
|
<span v-html="content" />
|
||||||
</BPopover>
|
</BPopover>
|
||||||
|
@ -28,14 +30,14 @@ export default {
|
||||||
id: { type: String, required: true },
|
id: { type: String, required: true },
|
||||||
title: { type: String, required: true },
|
title: { type: String, required: true },
|
||||||
content: { type: String, required: true },
|
content: { type: String, required: true },
|
||||||
variant: { type: String, default: 'info' }
|
variant: { type: String, default: 'info' },
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
cols_ () {
|
cols_() {
|
||||||
return Object.assign({ md: 4, xl: 3 }, this.cols)
|
return Object.assign({ md: 4, xl: 3 }, this.cols)
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -45,7 +47,7 @@ export default {
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin-left: .1rem;
|
margin-left: 0.1rem;
|
||||||
border-radius: 50rem;
|
border-radius: 50rem;
|
||||||
line-height: inherit;
|
line-height: inherit;
|
||||||
font-size: inherit;
|
font-size: inherit;
|
||||||
|
|
|
@ -28,18 +28,18 @@
|
||||||
<!-- Render description -->
|
<!-- Render description -->
|
||||||
<template v-if="description || link">
|
<template v-if="description || link">
|
||||||
<div class="d-flex">
|
<div class="d-flex">
|
||||||
<BLink
|
<BLink v-if="link" :to="link" :href="link.href" class="ml-auto">
|
||||||
v-if="link"
|
|
||||||
:to="link" :href="link.href" class="ml-auto"
|
|
||||||
>
|
|
||||||
{{ link.text }}
|
{{ link.text }}
|
||||||
</BLink>
|
</BLink>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<VueShowdown
|
<VueShowdown
|
||||||
v-if="description"
|
v-if="description"
|
||||||
:markdown="description" flavor="github"
|
:markdown="description"
|
||||||
:class="{ ['alert p-1 px-2 alert-' + descriptionVariant]: descriptionVariant }"
|
flavor="github"
|
||||||
|
:class="{
|
||||||
|
['alert p-1 px-2 alert-' + descriptionVariant]: descriptionVariant,
|
||||||
|
}"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<!-- Slot available to overwrite the one above -->
|
<!-- Slot available to overwrite the one above -->
|
||||||
|
@ -64,23 +64,23 @@ export default {
|
||||||
component: { type: String, default: 'InputItem' },
|
component: { type: String, default: 'InputItem' },
|
||||||
value: { type: null, default: null },
|
value: { type: null, default: null },
|
||||||
props: { type: Object, default: () => ({}) },
|
props: { type: Object, default: () => ({}) },
|
||||||
validation: { type: Object, default: null }
|
validation: { type: Object, default: null },
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
_id () {
|
_id() {
|
||||||
if (this.id) return this.id
|
if (this.id) return this.id
|
||||||
const childId = this.props.id || this.$attrs['label-for']
|
const childId = this.props.id || this.$attrs['label-for']
|
||||||
return childId ? childId + '_group' : null
|
return childId ? childId + '_group' : null
|
||||||
},
|
},
|
||||||
|
|
||||||
attrs () {
|
attrs() {
|
||||||
const attrs = { ...this.$attrs }
|
const attrs = { ...this.$attrs }
|
||||||
if ('label' in attrs) {
|
if ('label' in attrs) {
|
||||||
const defaultAttrs = {
|
const defaultAttrs = {
|
||||||
'label-cols-md': 4,
|
'label-cols-md': 4,
|
||||||
'label-cols-lg': 3,
|
'label-cols-lg': 3,
|
||||||
'label-class': ['font-weight-bold', 'py-0']
|
'label-class': ['font-weight-bold', 'py-0'],
|
||||||
}
|
}
|
||||||
if (!('label-cols' in attrs)) {
|
if (!('label-cols' in attrs)) {
|
||||||
for (const attr in defaultAttrs) {
|
for (const attr in defaultAttrs) {
|
||||||
|
@ -93,7 +93,7 @@ export default {
|
||||||
return attrs
|
return attrs
|
||||||
},
|
},
|
||||||
|
|
||||||
state () {
|
state() {
|
||||||
// Need to set state as null if no error, else component turn green
|
// Need to set state as null if no error, else component turn green
|
||||||
if (this.validation) {
|
if (this.validation) {
|
||||||
return this.validation.$anyError === true ? false : null
|
return this.validation.$anyError === true ? false : null
|
||||||
|
@ -101,18 +101,18 @@ export default {
|
||||||
return null
|
return null
|
||||||
},
|
},
|
||||||
|
|
||||||
errorMessage () {
|
errorMessage() {
|
||||||
const validation = this.validation
|
const validation = this.validation
|
||||||
if (validation && validation.$anyError) {
|
if (validation && validation.$anyError) {
|
||||||
const [type, errData] = this.findError(validation.$params, validation)
|
const [type, errData] = this.findError(validation.$params, validation)
|
||||||
return this.$i18n.t('form_errors.' + type, errData)
|
return this.$i18n.t('form_errors.' + type, errData)
|
||||||
}
|
}
|
||||||
return ''
|
return ''
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
touch (name) {
|
touch(name) {
|
||||||
if (this.validation) {
|
if (this.validation) {
|
||||||
// For fields that have multiple elements
|
// For fields that have multiple elements
|
||||||
if (name) {
|
if (name) {
|
||||||
|
@ -123,7 +123,7 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
findError (params, obj, parent = obj) {
|
findError(params, obj, parent = obj) {
|
||||||
for (const key in params) {
|
for (const key in params) {
|
||||||
if (!obj[key]) {
|
if (!obj[key]) {
|
||||||
return [key, obj.$params[key]]
|
return [key, obj.$params[key]]
|
||||||
|
@ -132,8 +132,8 @@ export default {
|
||||||
return this.findError(obj[key].$params, obj[key], parent)
|
return this.findError(obj[key].$params, obj[key], parent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -21,32 +21,35 @@ export default {
|
||||||
label: { type: String, required: true },
|
label: { type: String, required: true },
|
||||||
component: { type: String, default: 'InputItem' },
|
component: { type: String, default: 'InputItem' },
|
||||||
value: { type: null, default: null },
|
value: { type: null, default: null },
|
||||||
cols: { type: Object, default: () => ({ md: 4, lg: 3 }) }
|
cols: { type: Object, default: () => ({ md: 4, lg: 3 }) },
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
cols_ () {
|
cols_() {
|
||||||
return Object.assign({ md: 4, lg: 3 }, this.cols)
|
return Object.assign({ md: 4, lg: 3 }, this.cols)
|
||||||
},
|
},
|
||||||
|
|
||||||
text () {
|
text() {
|
||||||
return this.parseValue(this.value)
|
return this.parseValue(this.value)
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
parseValue (value) {
|
parseValue(value) {
|
||||||
const item = this.component
|
const item = this.component
|
||||||
if (item === 'FileItem') value = value.file ? value.file.name : null
|
if (item === 'FileItem') value = value.file ? value.file.name : null
|
||||||
if (item === 'CheckboxItem') value = this.$i18n.t(value ? 'yes' : 'no')
|
if (item === 'CheckboxItem') value = this.$i18n.t(value ? 'yes' : 'no')
|
||||||
if (item === 'TextAreaItem') value = value.replaceAll('\n', '<br>')
|
if (item === 'TextAreaItem') value = value.replaceAll('\n', '<br>')
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
value = value.length ? value.join(this.$i18n.t('words.separator')) : null
|
value = value.length
|
||||||
|
? value.join(this.$i18n.t('words.separator'))
|
||||||
|
: null
|
||||||
}
|
}
|
||||||
if ([null, undefined, ''].includes(this.value)) value = this.$i18n.t('words.none')
|
if ([null, undefined, ''].includes(this.value))
|
||||||
|
value = this.$i18n.t('words.none')
|
||||||
return value
|
return value
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -23,25 +23,25 @@ export default {
|
||||||
button: {
|
button: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: null,
|
default: null,
|
||||||
validator (value) {
|
validator(value) {
|
||||||
return ['text', 'to'].every(prop => (prop in value))
|
return ['text', 'to'].every((prop) => prop in value)
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
data () {
|
data() {
|
||||||
return {
|
return {
|
||||||
hasLeftSlot: null,
|
hasLeftSlot: null,
|
||||||
hasRightSlot: null
|
hasRightSlot: null,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
created () {
|
created() {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.hasLeftSlot = 'group-left' in this.$slots
|
this.hasLeftSlot = 'group-left' in this.$slots
|
||||||
this.hasRightSlot = 'group-right' in this.$slots
|
this.hasRightSlot = 'group-right' in this.$slots
|
||||||
})
|
})
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -65,10 +65,10 @@ export default {
|
||||||
flex-direction: column-reverse;
|
flex-direction: column-reverse;
|
||||||
|
|
||||||
#top-bar-right {
|
#top-bar-right {
|
||||||
margin-bottom: .75rem;
|
margin-bottom: 0.75rem;
|
||||||
|
|
||||||
::v-deep > * {
|
::v-deep > * {
|
||||||
margin-bottom: .25rem;
|
margin-bottom: 0.25rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -89,7 +89,7 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
::v-deep .btn {
|
::v-deep .btn {
|
||||||
margin-left: .5rem;
|
margin-left: 0.5rem;
|
||||||
&.dropdown-toggle-split {
|
&.dropdown-toggle-split {
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,44 +40,46 @@ export default {
|
||||||
queriesWait: { type: Boolean, default: false },
|
queriesWait: { type: Boolean, default: false },
|
||||||
skeleton: { type: [String, Array], default: null },
|
skeleton: { type: [String, Array], default: null },
|
||||||
// Optional prop to take control of the loading value
|
// Optional prop to take control of the loading value
|
||||||
loading: { type: Boolean, default: null }
|
loading: { type: Boolean, default: null },
|
||||||
},
|
},
|
||||||
|
|
||||||
data () {
|
data() {
|
||||||
return {
|
return {
|
||||||
fallback_loading: this.loading === null && this.queries !== null ? true : null
|
fallback_loading:
|
||||||
|
this.loading === null && this.queries !== null ? true : null,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
isLoading () {
|
isLoading() {
|
||||||
if (this.loading !== null) return this.loading
|
if (this.loading !== null) return this.loading
|
||||||
return this.fallback_loading
|
return this.fallback_loading
|
||||||
},
|
},
|
||||||
|
|
||||||
hasTopBar () {
|
hasTopBar() {
|
||||||
return ['top-bar-group-left', 'top-bar-group-right'].some(slotName => (slotName in this.$slots))
|
return ['top-bar-group-left', 'top-bar-group-right'].some(
|
||||||
}
|
(slotName) => slotName in this.$slots,
|
||||||
|
)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
fetchQueries ({ triggerLoading = false } = {}) {
|
fetchQueries({ triggerLoading = false } = {}) {
|
||||||
if (triggerLoading) {
|
if (triggerLoading) {
|
||||||
this.fallback_loading = true
|
this.fallback_loading = true
|
||||||
}
|
}
|
||||||
|
|
||||||
api.fetchAll(
|
api
|
||||||
this.queries,
|
.fetchAll(this.queries, { wait: this.queriesWait, initial: true })
|
||||||
{ wait: this.queriesWait, initial: true }
|
.then((responses) => {
|
||||||
).then(responses => {
|
this.$emit('queries-response', ...responses)
|
||||||
this.$emit('queries-response', ...responses)
|
this.fallback_loading = false
|
||||||
this.fallback_loading = false
|
})
|
||||||
})
|
},
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
created () {
|
created() {
|
||||||
if (this.queries) this.fetchQueries()
|
if (this.queries) this.fetchQueries()
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -11,8 +11,11 @@
|
||||||
|
|
||||||
<BFormInput
|
<BFormInput
|
||||||
id="top-bar-search"
|
id="top-bar-search"
|
||||||
:value="search" @input="$emit('update:search', $event)"
|
:value="search"
|
||||||
:placeholder="$t('search.for', { items: $tc('items.' + itemsName, 2) })"
|
@input="$emit('update:search', $event)"
|
||||||
|
:placeholder="
|
||||||
|
$t('search.for', { items: $tc('items.' + itemsName, 2) })
|
||||||
|
"
|
||||||
:disabled="!items"
|
:disabled="!items"
|
||||||
/>
|
/>
|
||||||
</BInputGroup>
|
</BInputGroup>
|
||||||
|
@ -29,7 +32,13 @@
|
||||||
<BAlert v-if="items === null || filteredItems === null" variant="warning">
|
<BAlert v-if="items === null || filteredItems === null" variant="warning">
|
||||||
<slot name="alert-message">
|
<slot name="alert-message">
|
||||||
<YIcon iname="exclamation-triangle" />
|
<YIcon iname="exclamation-triangle" />
|
||||||
{{ $tc(items === null ? 'items_verbose_count': 'search.not_found', 0, { items: $tc('items.' + itemsName, 0) }) }}
|
{{
|
||||||
|
$tc(
|
||||||
|
items === null ? 'items_verbose_count' : 'search.not_found',
|
||||||
|
0,
|
||||||
|
{ items: $tc('items.' + itemsName, 0) },
|
||||||
|
)
|
||||||
|
}}
|
||||||
</slot>
|
</slot>
|
||||||
</BAlert>
|
</BAlert>
|
||||||
|
|
||||||
|
@ -55,13 +64,13 @@ export default {
|
||||||
itemsName: { type: String, required: true },
|
itemsName: { type: String, required: true },
|
||||||
filteredItems: { type: null, required: true },
|
filteredItems: { type: null, required: true },
|
||||||
search: { type: String, default: null },
|
search: { type: String, default: null },
|
||||||
skeleton: { type: String, default: 'ListGroupSkeleton' }
|
skeleton: { type: String, default: 'ListGroupSkeleton' },
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
hasCustomTopBar () {
|
hasCustomTopBar() {
|
||||||
return 'top-bar' in this.$slots
|
return 'top-bar' in this.$slots
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -23,14 +23,14 @@ export default {
|
||||||
props: {
|
props: {
|
||||||
alert: { type: Boolean, default: false },
|
alert: { type: Boolean, default: false },
|
||||||
variant: { type: String, default: 'info' },
|
variant: { type: String, default: 'info' },
|
||||||
icon: { type: String, default: null }
|
icon: { type: String, default: null },
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
_icon () {
|
_icon() {
|
||||||
if (this.icon) return this.icon
|
if (this.icon) return this.icon
|
||||||
return DEFAULT_STATUS_ICON[this.variant]
|
return DEFAULT_STATUS_ICON[this.variant]
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -6,8 +6,10 @@
|
||||||
</BBreadcrumbItem>
|
</BBreadcrumbItem>
|
||||||
|
|
||||||
<BBreadcrumbItem
|
<BBreadcrumbItem
|
||||||
v-for="({ name, text }, i) in breadcrumb" :key="name"
|
v-for="({ name, text }, i) in breadcrumb"
|
||||||
:to="{ name }" :active="i === breadcrumb.length - 1"
|
:key="name"
|
||||||
|
:to="{ name }"
|
||||||
|
:active="i === breadcrumb.length - 1"
|
||||||
>
|
>
|
||||||
{{ text }}
|
{{ text }}
|
||||||
</BBreadcrumbItem>
|
</BBreadcrumbItem>
|
||||||
|
@ -21,8 +23,8 @@ export default {
|
||||||
name: 'YBreadcrumb',
|
name: 'YBreadcrumb',
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters(['breadcrumb'])
|
...mapGetters(['breadcrumb']),
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -9,15 +9,29 @@
|
||||||
<slot name="header-next" />
|
<slot name="header-next" />
|
||||||
</slot>
|
</slot>
|
||||||
|
|
||||||
<div v-if="hasButtons" class="mt-2 w-100 custom-header-buttons" :class="{ [`ml-${buttonUnbreak}-auto mt-${buttonUnbreak}-0 w-${buttonUnbreak}-auto`]: buttonUnbreak }">
|
<div
|
||||||
|
v-if="hasButtons"
|
||||||
|
class="mt-2 w-100 custom-header-buttons"
|
||||||
|
:class="{
|
||||||
|
[`ml-${buttonUnbreak}-auto mt-${buttonUnbreak}-0 w-${buttonUnbreak}-auto`]:
|
||||||
|
buttonUnbreak,
|
||||||
|
}"
|
||||||
|
>
|
||||||
<slot name="header-buttons" />
|
<slot name="header-buttons" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<BButton
|
<BButton
|
||||||
v-if="collapsable" @click="visible = !visible"
|
v-if="collapsable"
|
||||||
size="sm" variant="outline-secondary"
|
@click="visible = !visible"
|
||||||
class="align-self-center ml-auto" :class="{ 'not-collapsed': visible, 'collapsed': !visible, [`ml-${buttonUnbreak}-2`]: buttonUnbreak }"
|
size="sm"
|
||||||
|
variant="outline-secondary"
|
||||||
|
class="align-self-center ml-auto"
|
||||||
|
:class="{
|
||||||
|
'not-collapsed': visible,
|
||||||
|
collapsed: !visible,
|
||||||
|
[`ml-${buttonUnbreak}-2`]: buttonUnbreak,
|
||||||
|
}"
|
||||||
>
|
>
|
||||||
<YIcon iname="chevron-right" />
|
<YIcon iname="chevron-right" />
|
||||||
<span class="sr-only">{{ $t('words.collapse') }}</span>
|
<span class="sr-only">{{ $t('words.collapse') }}</span>
|
||||||
|
@ -25,7 +39,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<BCollapse v-if="collapsable" :visible="visible">
|
<BCollapse v-if="collapsable" :visible="visible">
|
||||||
<slot v-if="('no-body' in $attrs)" name="default" />
|
<slot v-if="'no-body' in $attrs" name="default" />
|
||||||
<BCardBody v-else>
|
<BCardBody v-else>
|
||||||
<slot name="default" />
|
<slot name="default" />
|
||||||
</BCardBody>
|
</BCardBody>
|
||||||
|
@ -41,7 +55,6 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'YCard',
|
name: 'YCard',
|
||||||
|
|
||||||
|
@ -52,20 +65,20 @@ export default {
|
||||||
icon: { type: String, default: null },
|
icon: { type: String, default: null },
|
||||||
collapsable: { type: Boolean, default: false },
|
collapsable: { type: Boolean, default: false },
|
||||||
collapsed: { type: Boolean, default: false },
|
collapsed: { type: Boolean, default: false },
|
||||||
buttonUnbreak: { type: String, default: 'md' }
|
buttonUnbreak: { type: String, default: 'md' },
|
||||||
},
|
},
|
||||||
|
|
||||||
data () {
|
data() {
|
||||||
return {
|
return {
|
||||||
visible: !this.collapsed
|
visible: !this.collapsed,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
hasButtons () {
|
hasButtons() {
|
||||||
return 'header-buttons' in this.$slots
|
return 'header-buttons' in this.$slots
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -79,7 +92,7 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn + .btn {
|
.btn + .btn {
|
||||||
margin-left: .5rem;
|
margin-left: 0.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -90,7 +103,7 @@ export default {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
& > *:not(:first-child) {
|
& > *:not(:first-child) {
|
||||||
margin-left: .5rem;
|
margin-left: 0.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.collapse:not(.show) + .card-footer {
|
.collapse:not(.show) + .card-footer {
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
<template>
|
<template>
|
||||||
<span :class="['icon fa fa-' + iname, variant ? 'variant ' + variant : '']" aria-hidden="true" />
|
<span
|
||||||
|
:class="['icon fa fa-' + iname, variant ? 'variant ' + variant : '']"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
@ -7,8 +10,8 @@ export default {
|
||||||
name: 'YIcon',
|
name: 'YIcon',
|
||||||
props: {
|
props: {
|
||||||
iname: { type: String, required: true },
|
iname: { type: String, required: true },
|
||||||
variant: { type: String, default: null }
|
variant: { type: String, default: null },
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -35,7 +38,7 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
&.variant {
|
&.variant {
|
||||||
font-size: .8rem;
|
font-size: 0.8rem;
|
||||||
width: 1.35rem;
|
width: 1.35rem;
|
||||||
min-width: 1.35rem;
|
min-width: 1.35rem;
|
||||||
height: 1.35rem;
|
height: 1.35rem;
|
||||||
|
|
|
@ -1,13 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<BListGroupItem
|
<BListGroupItem class="yuno-list-group-item" :class="_class" v-bind="$attrs">
|
||||||
class="yuno-list-group-item" :class="_class"
|
|
||||||
v-bind="$attrs"
|
|
||||||
>
|
|
||||||
<div v-if="!noStatus" class="yuno-list-group-item-status">
|
<div v-if="!noStatus" class="yuno-list-group-item-status">
|
||||||
<YIcon
|
<YIcon v-if="_icon" :iname="_icon" :class="['icon-' + variant]" />
|
||||||
v-if="_icon" :iname="_icon"
|
|
||||||
:class="['icon-' + variant]"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="yuno-list-group-item-content">
|
<div class="yuno-list-group-item-content">
|
||||||
|
@ -28,28 +22,27 @@ export default {
|
||||||
noIcon: { type: Boolean, default: false },
|
noIcon: { type: Boolean, default: false },
|
||||||
noStatus: { type: Boolean, default: false },
|
noStatus: { type: Boolean, default: false },
|
||||||
size: { type: String, default: 'md' },
|
size: { type: String, default: 'md' },
|
||||||
faded: { type: Boolean, default: false }
|
faded: { type: Boolean, default: false },
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
_icon () {
|
_icon() {
|
||||||
return this.noIcon ? null : this.icon || DEFAULT_STATUS_ICON[this.variant]
|
return this.noIcon ? null : this.icon || DEFAULT_STATUS_ICON[this.variant]
|
||||||
},
|
},
|
||||||
|
|
||||||
_class () {
|
_class() {
|
||||||
const baseClass = 'yuno-list-group-item-'
|
const baseClass = 'yuno-list-group-item-'
|
||||||
return [
|
return [
|
||||||
baseClass + this.size,
|
baseClass + this.size,
|
||||||
baseClass + this.variant,
|
baseClass + this.variant,
|
||||||
{ [baseClass + 'faded']: this.faded }
|
{ [baseClass + 'faded']: this.faded },
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
||||||
.yuno-list-group-item {
|
.yuno-list-group-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
@ -70,7 +63,7 @@ export default {
|
||||||
&-#{$color} {
|
&-#{$color} {
|
||||||
color: theme-color-level($color, 6);
|
color: theme-color-level($color, 6);
|
||||||
|
|
||||||
[dark-theme="true"] & {
|
[dark-theme='true'] & {
|
||||||
color: theme-color-level($color, -6);
|
color: theme-color-level($color, -6);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,7 +89,7 @@ export default {
|
||||||
|
|
||||||
&-xs {
|
&-xs {
|
||||||
.yuno-list-group-item-status {
|
.yuno-list-group-item-status {
|
||||||
width: .4rem;
|
width: 0.4rem;
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
display: none;
|
display: none;
|
||||||
|
@ -109,7 +102,7 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
&-faded > * {
|
&-faded > * {
|
||||||
opacity: .5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -5,13 +5,12 @@
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex'
|
import { mapGetters } from 'vuex'
|
||||||
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'YSpinner',
|
name: 'YSpinner',
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters(['spinner'])
|
...mapGetters(['spinner']),
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -26,15 +25,28 @@ export default {
|
||||||
background-image: url('../../assets/spinners/pacman_dark.gif');
|
background-image: url('../../assets/spinners/pacman_dark.gif');
|
||||||
animation-name: back-and-forth-pacman;
|
animation-name: back-and-forth-pacman;
|
||||||
|
|
||||||
[dark-theme="true"] & {
|
[dark-theme='true'] & {
|
||||||
background-image: url('../../assets/spinners/pacman_light.gif');
|
background-image: url('../../assets/spinners/pacman_light.gif');
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes back-and-forth-pacman {
|
@keyframes back-and-forth-pacman {
|
||||||
0%, 100% { transform: scale(1); margin-left: 0; }
|
0%,
|
||||||
49% { transform: scale(1); margin-left: calc(100% - 24px);}
|
100% {
|
||||||
50% { transform: scale(-1); margin-left: calc(100% - 24px);}
|
transform: scale(1);
|
||||||
99% { transform: scale(-1); margin-left: 0;}
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
49% {
|
||||||
|
transform: scale(1);
|
||||||
|
margin-left: calc(100% - 24px);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(-1);
|
||||||
|
margin-left: calc(100% - 24px);
|
||||||
|
}
|
||||||
|
99% {
|
||||||
|
transform: scale(-1);
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,10 +57,23 @@ export default {
|
||||||
animation-name: back-and-forth-magikarp;
|
animation-name: back-and-forth-magikarp;
|
||||||
|
|
||||||
@keyframes back-and-forth-magikarp {
|
@keyframes back-and-forth-magikarp {
|
||||||
0%, 100% { transform: scale(1, 1); margin-left: 0; }
|
0%,
|
||||||
49% { transform: scale(1, 1); margin-left: calc(100% - 32px);}
|
100% {
|
||||||
50% { transform: scale(-1, 1); margin-left: calc(100% - 32px);}
|
transform: scale(1, 1);
|
||||||
99% { transform: scale(-1, 1); margin-left: 0;}
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
49% {
|
||||||
|
transform: scale(1, 1);
|
||||||
|
margin-left: calc(100% - 32px);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(-1, 1);
|
||||||
|
margin-left: calc(100% - 32px);
|
||||||
|
}
|
||||||
|
99% {
|
||||||
|
transform: scale(-1, 1);
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -59,10 +84,23 @@ export default {
|
||||||
animation-name: back-and-forth-nyancat;
|
animation-name: back-and-forth-nyancat;
|
||||||
|
|
||||||
@keyframes back-and-forth-nyancat {
|
@keyframes back-and-forth-nyancat {
|
||||||
0%, 100% { transform: scale(1, 1); margin-left: 0; }
|
0%,
|
||||||
49% { transform: scale(1, 1); margin-left: calc(100% - 100px);}
|
100% {
|
||||||
50% { transform: scale(-1, 1); margin-left: calc(100% - 100px);}
|
transform: scale(1, 1);
|
||||||
99% { transform: scale(-1, 1); margin-left: 0;}
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
49% {
|
||||||
|
transform: scale(1, 1);
|
||||||
|
margin-left: calc(100% - 100px);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(-1, 1);
|
||||||
|
margin-left: calc(100% - 100px);
|
||||||
|
}
|
||||||
|
99% {
|
||||||
|
transform: scale(-1, 1);
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -73,10 +111,23 @@ export default {
|
||||||
animation-name: back-and-forth-spookycat;
|
animation-name: back-and-forth-spookycat;
|
||||||
|
|
||||||
@keyframes back-and-forth-spookycat {
|
@keyframes back-and-forth-spookycat {
|
||||||
0%, 100% { transform: scale(1, 1); margin-left: 0; }
|
0%,
|
||||||
49% { transform: scale(1, 1); margin-left: calc(100% - 100px);}
|
100% {
|
||||||
50% { transform: scale(-1, 1); margin-left: calc(100% - 100px);}
|
transform: scale(1, 1);
|
||||||
99% { transform: scale(-1, 1); margin-left: 0;}
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
49% {
|
||||||
|
transform: scale(1, 1);
|
||||||
|
margin-left: calc(100% - 100px);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(-1, 1);
|
||||||
|
margin-left: calc(100% - 100px);
|
||||||
|
}
|
||||||
|
99% {
|
||||||
|
transform: scale(-1, 1);
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,6 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ButtonItem',
|
name: 'ButtonItem',
|
||||||
|
|
||||||
|
@ -21,20 +20,20 @@ export default {
|
||||||
id: { type: String, default: null },
|
id: { type: String, default: null },
|
||||||
type: { type: String, default: 'success' },
|
type: { type: String, default: 'success' },
|
||||||
icon: { type: String, default: null },
|
icon: { type: String, default: null },
|
||||||
enabled: { type: [Boolean, String], default: true }
|
enabled: { type: [Boolean, String], default: true },
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
icon_ () {
|
icon_() {
|
||||||
const icons = {
|
const icons = {
|
||||||
success: 'thumbs-up',
|
success: 'thumbs-up',
|
||||||
info: 'info',
|
info: 'info',
|
||||||
warning: 'exclamation',
|
warning: 'exclamation',
|
||||||
danger: 'times'
|
danger: 'times',
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.icon || icons[this.type]
|
return this.icon || icons[this.type]
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -18,13 +18,13 @@ export default {
|
||||||
value: { type: Boolean, required: true },
|
value: { type: Boolean, required: true },
|
||||||
id: { type: String, default: null },
|
id: { type: String, default: null },
|
||||||
label: { type: String, default: null },
|
label: { type: String, default: null },
|
||||||
labels: { type: Object, default: () => ({ true: 'yes', false: 'no' }) }
|
labels: { type: Object, default: () => ({ true: 'yes', false: 'no' }) },
|
||||||
},
|
},
|
||||||
|
|
||||||
data () {
|
data() {
|
||||||
return {
|
return {
|
||||||
checked: this.value
|
checked: this.value,
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -10,7 +10,7 @@ export default {
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
id: { type: String, default: null },
|
id: { type: String, default: null },
|
||||||
label: { type: String, default: null }
|
label: { type: String, default: null },
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -2,7 +2,8 @@
|
||||||
<BButtonGroup class="w-100">
|
<BButtonGroup class="w-100">
|
||||||
<BButton
|
<BButton
|
||||||
v-if="!this.required && this.value.file !== null"
|
v-if="!this.required && this.value.file !== null"
|
||||||
@click="clearFiles" variant="danger"
|
@click="clearFiles"
|
||||||
|
variant="danger"
|
||||||
>
|
>
|
||||||
<span class="sr-only">{{ $t('delete') }}</span>
|
<span class="sr-only">{{ $t('delete') }}</span>
|
||||||
<YIcon iname="trash" />
|
<YIcon iname="trash" />
|
||||||
|
@ -39,42 +40,42 @@ export default {
|
||||||
accept: { type: String, default: null },
|
accept: { type: String, default: null },
|
||||||
state: { type: Boolean, default: null },
|
state: { type: Boolean, default: null },
|
||||||
required: { type: Boolean, default: false },
|
required: { type: Boolean, default: false },
|
||||||
name: { type: String, default: null }
|
name: { type: String, default: null },
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
_placeholder: function () {
|
_placeholder: function () {
|
||||||
return this.value.file === null ? this.placeholder : this.value.file.name
|
return this.value.file === null ? this.placeholder : this.value.file.name
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
onInput (file) {
|
onInput(file) {
|
||||||
const value = {
|
const value = {
|
||||||
file,
|
file,
|
||||||
content: '',
|
content: '',
|
||||||
current: false,
|
current: false,
|
||||||
removed: false
|
removed: false,
|
||||||
}
|
}
|
||||||
// Update the value with the new File and an empty content for now
|
// Update the value with the new File and an empty content for now
|
||||||
this.$emit('input', value)
|
this.$emit('input', value)
|
||||||
|
|
||||||
// Asynchronously load the File content and update the value again
|
// Asynchronously load the File content and update the value again
|
||||||
getFileContent(file).then(content => {
|
getFileContent(file).then((content) => {
|
||||||
this.$emit('input', { ...value, content })
|
this.$emit('input', { ...value, content })
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
clearFiles () {
|
clearFiles() {
|
||||||
this.$refs['input-file'].reset()
|
this.$refs['input-file'].reset()
|
||||||
this.$emit('input', {
|
this.$emit('input', {
|
||||||
file: null,
|
file: null,
|
||||||
content: '',
|
content: '',
|
||||||
current: false,
|
current: false,
|
||||||
removed: true
|
removed: true,
|
||||||
})
|
})
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,6 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'InputItem',
|
name: 'InputItem',
|
||||||
|
|
||||||
|
@ -34,13 +33,17 @@ export default {
|
||||||
trim: { type: Boolean, default: true },
|
trim: { type: Boolean, default: true },
|
||||||
autocomplete: { type: String, default: null },
|
autocomplete: { type: String, default: null },
|
||||||
pattern: { type: Object, default: null },
|
pattern: { type: Object, default: null },
|
||||||
name: { type: String, default: null }
|
name: { type: String, default: null },
|
||||||
},
|
},
|
||||||
|
|
||||||
data () {
|
data() {
|
||||||
return {
|
return {
|
||||||
autocomplete_: (this.autocomplete) ? this.autocomplete : (this.type === 'password') ? 'new-password' : null
|
autocomplete_: this.autocomplete
|
||||||
|
? this.autocomplete
|
||||||
|
: this.type === 'password'
|
||||||
|
? 'new-password'
|
||||||
|
: null,
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -8,7 +8,7 @@ export default {
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
id: { type: String, default: null },
|
id: { type: String, default: null },
|
||||||
label: { type: String, default: null }
|
label: { type: String, default: null },
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,10 +1,16 @@
|
||||||
<template>
|
<template>
|
||||||
<BAlert class="d-flex flex-column flex-md-row align-items-center" :variant="type" show>
|
<BAlert
|
||||||
|
class="d-flex flex-column flex-md-row align-items-center"
|
||||||
|
:variant="type"
|
||||||
|
show
|
||||||
|
>
|
||||||
<YIcon :iname="icon_" class="mr-md-3 mb-md-0 mb-2" :variant="type" />
|
<YIcon :iname="icon_" class="mr-md-3 mb-md-0 mb-2" :variant="type" />
|
||||||
|
|
||||||
<VueShowdown
|
<VueShowdown
|
||||||
:markdown="label" flavor="github"
|
:markdown="label"
|
||||||
tag="span" class="markdown"
|
flavor="github"
|
||||||
|
tag="span"
|
||||||
|
class="markdown"
|
||||||
/>
|
/>
|
||||||
</BAlert>
|
</BAlert>
|
||||||
</template>
|
</template>
|
||||||
|
@ -17,19 +23,19 @@ export default {
|
||||||
id: { type: String, default: null },
|
id: { type: String, default: null },
|
||||||
label: { type: String, default: null },
|
label: { type: String, default: null },
|
||||||
type: { type: String, default: null },
|
type: { type: String, default: null },
|
||||||
icon: { type: String, default: null }
|
icon: { type: String, default: null },
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
icon_ () {
|
icon_() {
|
||||||
const icons = {
|
const icons = {
|
||||||
success: 'thumbs-up',
|
success: 'thumbs-up',
|
||||||
info: 'info',
|
info: 'info',
|
||||||
warning: 'exclamation',
|
warning: 'exclamation',
|
||||||
danger: 'times'
|
danger: 'times',
|
||||||
}
|
}
|
||||||
return this.icon || icons[this.type]
|
return this.icon || icons[this.type]
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -18,7 +18,7 @@ export default {
|
||||||
id: { type: String, default: null },
|
id: { type: String, default: null },
|
||||||
choices: { type: [Array, Object], required: true },
|
choices: { type: [Array, Object], required: true },
|
||||||
required: { type: Boolean, default: false },
|
required: { type: Boolean, default: false },
|
||||||
name: { type: String, default: null }
|
name: { type: String, default: null },
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -17,9 +17,9 @@
|
||||||
export default {
|
export default {
|
||||||
name: 'TagsItem',
|
name: 'TagsItem',
|
||||||
|
|
||||||
data () {
|
data() {
|
||||||
return {
|
return {
|
||||||
tags: this.value
|
tags: this.value,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
|
@ -29,8 +29,7 @@ export default {
|
||||||
limit: { type: Number, default: null },
|
limit: { type: Number, default: null },
|
||||||
required: { type: Boolean, default: false },
|
required: { type: Boolean, default: false },
|
||||||
state: { type: Boolean, default: null },
|
state: { type: Boolean, default: null },
|
||||||
name: { type: String, default: null }
|
name: { type: String, default: null },
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,24 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="tags-selectize">
|
<div class="tags-selectize">
|
||||||
<BFormTags
|
<BFormTags
|
||||||
v-bind="$attrs" v-on="$listeners"
|
v-bind="$attrs"
|
||||||
:value="value" :id="id"
|
v-on="$listeners"
|
||||||
size="lg" class="p-0 border-0" no-outer-focus
|
:value="value"
|
||||||
|
:id="id"
|
||||||
|
size="lg"
|
||||||
|
class="p-0 border-0"
|
||||||
|
no-outer-focus
|
||||||
>
|
>
|
||||||
<template #default="{ tags, disabled, addTag, removeTag }">
|
<template #default="{ tags, disabled, addTag, removeTag }">
|
||||||
<ul v-if="!noTags && tags.length > 0" class="list-inline d-inline-block mb-2">
|
<ul
|
||||||
<li v-for="tag in tags" :key="id + '-' + tag" class="list-inline-item">
|
v-if="!noTags && tags.length > 0"
|
||||||
|
class="list-inline d-inline-block mb-2"
|
||||||
|
>
|
||||||
|
<li
|
||||||
|
v-for="tag in tags"
|
||||||
|
:key="id + '-' + tag"
|
||||||
|
class="list-inline-item"
|
||||||
|
>
|
||||||
<BFormTag
|
<BFormTag
|
||||||
@remove="onRemoveTag({ option: tag, removeTag })"
|
@remove="onRemoveTag({ option: tag, removeTag })"
|
||||||
:title="tag"
|
:title="tag"
|
||||||
|
@ -21,7 +32,9 @@
|
||||||
|
|
||||||
<BDropdown
|
<BDropdown
|
||||||
ref="dropdown"
|
ref="dropdown"
|
||||||
variant="outline-dark" block menu-class="w-100"
|
variant="outline-dark"
|
||||||
|
block
|
||||||
|
menu-class="w-100"
|
||||||
@keydown.native="onDropdownKeydown"
|
@keydown.native="onDropdownKeydown"
|
||||||
>
|
>
|
||||||
<template #button-content>
|
<template #button-content>
|
||||||
|
@ -32,15 +45,25 @@
|
||||||
<BDropdownForm @submit.stop.prevent="() => {}">
|
<BDropdownForm @submit.stop.prevent="() => {}">
|
||||||
<BFormGroup
|
<BFormGroup
|
||||||
:label="$t('search.for', { items: itemsName })"
|
:label="$t('search.for', { items: itemsName })"
|
||||||
label-cols-md="auto" label-size="sm" :label-for="id + '-search-input'"
|
label-cols-md="auto"
|
||||||
:invalid-feedback="$tc('search.not_found', 0, { items: $tc('items.' + itemsName, 0) })"
|
label-size="sm"
|
||||||
:state="searchState" :disabled="disabled"
|
:label-for="id + '-search-input'"
|
||||||
|
:invalid-feedback="
|
||||||
|
$tc('search.not_found', 0, {
|
||||||
|
items: $tc('items.' + itemsName, 0),
|
||||||
|
})
|
||||||
|
"
|
||||||
|
:state="searchState"
|
||||||
|
:disabled="disabled"
|
||||||
class="mb-0"
|
class="mb-0"
|
||||||
>
|
>
|
||||||
<BFormInput
|
<BFormInput
|
||||||
ref="search-input" v-model="search"
|
ref="search-input"
|
||||||
|
v-model="search"
|
||||||
:id="id + '-search-input'"
|
:id="id + '-search-input'"
|
||||||
type="search" size="sm" autocomplete="off"
|
type="search"
|
||||||
|
size="sm"
|
||||||
|
autocomplete="off"
|
||||||
/>
|
/>
|
||||||
</BFormGroup>
|
</BFormGroup>
|
||||||
</BDropdownForm>
|
</BDropdownForm>
|
||||||
|
@ -56,7 +79,11 @@
|
||||||
</BDropdownItemButton>
|
</BDropdownItemButton>
|
||||||
<BDropdownText v-if="!criteria && availableOptions.length === 0">
|
<BDropdownText v-if="!criteria && availableOptions.length === 0">
|
||||||
<YIcon iname="exclamation-triangle" />
|
<YIcon iname="exclamation-triangle" />
|
||||||
{{ $tc('items_verbose_items_left', 0, { items: $tc('items.' + itemsName, 0) }) }}
|
{{
|
||||||
|
$tc('items_verbose_items_left', 0, {
|
||||||
|
items: $tc('items.' + itemsName, 0),
|
||||||
|
})
|
||||||
|
}}
|
||||||
</BDropdownText>
|
</BDropdownText>
|
||||||
</BDropdown>
|
</BDropdown>
|
||||||
</template>
|
</template>
|
||||||
|
@ -76,43 +103,45 @@ export default {
|
||||||
limit: { type: Number, default: null },
|
limit: { type: Number, default: null },
|
||||||
name: { type: String, default: null },
|
name: { type: String, default: null },
|
||||||
itemsName: { type: String, required: true },
|
itemsName: { type: String, required: true },
|
||||||
disabledItems: { type: Array, default: () => ([]) },
|
disabledItems: { type: Array, default: () => [] },
|
||||||
// By default `addTag` and `removeTag` have to be executed manually by listening to 'tag-update'.
|
// By default `addTag` and `removeTag` have to be executed manually by listening to 'tag-update'.
|
||||||
auto: { type: Boolean, default: false },
|
auto: { type: Boolean, default: false },
|
||||||
noTags: { type: Boolean, default: false },
|
noTags: { type: Boolean, default: false },
|
||||||
label: { type: String, default: null },
|
label: { type: String, default: null },
|
||||||
tagIcon: { type: String, default: null }
|
tagIcon: { type: String, default: null },
|
||||||
},
|
},
|
||||||
|
|
||||||
data () {
|
data() {
|
||||||
return {
|
return {
|
||||||
search: ''
|
search: '',
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
criteria () {
|
criteria() {
|
||||||
return this.search.trim().toLowerCase()
|
return this.search.trim().toLowerCase()
|
||||||
},
|
},
|
||||||
|
|
||||||
availableOptions () {
|
availableOptions() {
|
||||||
const criteria = this.criteria
|
const criteria = this.criteria
|
||||||
const options = this.options.filter(opt => {
|
const options = this.options.filter((opt) => {
|
||||||
return this.value.indexOf(opt) === -1 && !this.disabledItems.includes(opt)
|
return (
|
||||||
|
this.value.indexOf(opt) === -1 && !this.disabledItems.includes(opt)
|
||||||
|
)
|
||||||
})
|
})
|
||||||
if (criteria) {
|
if (criteria) {
|
||||||
return options.filter(opt => opt.toLowerCase().indexOf(criteria) > -1)
|
return options.filter((opt) => opt.toLowerCase().indexOf(criteria) > -1)
|
||||||
}
|
}
|
||||||
return options
|
return options
|
||||||
},
|
},
|
||||||
|
|
||||||
searchState () {
|
searchState() {
|
||||||
return this.criteria && this.availableOptions.length === 0 ? false : null
|
return this.criteria && this.availableOptions.length === 0 ? false : null
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
onAddTag ({ option, addTag }) {
|
onAddTag({ option, addTag }) {
|
||||||
this.$emit('tag-update', { action: 'add', option, applyMethod: addTag })
|
this.$emit('tag-update', { action: 'add', option, applyMethod: addTag })
|
||||||
this.search = ''
|
this.search = ''
|
||||||
if (this.auto) {
|
if (this.auto) {
|
||||||
|
@ -120,14 +149,18 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onRemoveTag ({ option, removeTag }) {
|
onRemoveTag({ option, removeTag }) {
|
||||||
this.$emit('tag-update', { action: 'remove', option, applyMethod: removeTag })
|
this.$emit('tag-update', {
|
||||||
|
action: 'remove',
|
||||||
|
option,
|
||||||
|
applyMethod: removeTag,
|
||||||
|
})
|
||||||
if (this.auto) {
|
if (this.auto) {
|
||||||
removeTag(option)
|
removeTag(option)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onDropdownKeydown (e) {
|
onDropdownKeydown(e) {
|
||||||
// Allow to start searching after dropdown opening
|
// Allow to start searching after dropdown opening
|
||||||
if (
|
if (
|
||||||
!['Tab', 'Space'].includes(e.code) &&
|
!['Tab', 'Space'].includes(e.code) &&
|
||||||
|
@ -135,8 +168,8 @@ export default {
|
||||||
) {
|
) {
|
||||||
this.$refs['search-input'].focus()
|
this.$refs['search-input'].focus()
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -147,7 +180,7 @@ export default {
|
||||||
padding-top: 0;
|
padding-top: 0;
|
||||||
|
|
||||||
.search-group {
|
.search-group {
|
||||||
padding-top: .5rem;
|
padding-top: 0.5rem;
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
background-color: $white;
|
background-color: $white;
|
||||||
|
|
|
@ -22,7 +22,7 @@ export default {
|
||||||
type: { type: String, default: 'text' },
|
type: { type: String, default: 'text' },
|
||||||
required: { type: Boolean, default: false },
|
required: { type: Boolean, default: false },
|
||||||
state: { type: Boolean, default: null },
|
state: { type: Boolean, default: null },
|
||||||
name: { type: String, default: null }
|
name: { type: String, default: null },
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
<BSkeleton v-else :width="randint(45, 100) + '%'" height="24px" />
|
<BSkeleton v-else :width="randint(45, 100) + '%'" height="24px" />
|
||||||
|
|
||||||
<BSkeleton :width="randint(20, 30) + '%'" height="38px" class="mt-3" />
|
<BSkeleton :width="randint(20, 30) + '%'" height="38px" class="mt-3" />
|
||||||
<hr>
|
<hr />
|
||||||
</div>
|
</div>
|
||||||
</BCard>
|
</BCard>
|
||||||
</template>
|
</template>
|
||||||
|
@ -24,9 +24,9 @@ export default {
|
||||||
name: 'CardButtonsSkeleton',
|
name: 'CardButtonsSkeleton',
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
itemCount: { type: Number, default: 5 }
|
itemCount: { type: Number, default: 5 },
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: { randint }
|
methods: { randint },
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -8,12 +8,19 @@
|
||||||
<BRow :key="count" :class="{ 'd-block': cols === null }">
|
<BRow :key="count" :class="{ 'd-block': cols === null }">
|
||||||
<BCol v-bind="cols">
|
<BCol v-bind="cols">
|
||||||
<div style="height: 38px" class="d-flex align-items-center">
|
<div style="height: 38px" class="d-flex align-items-center">
|
||||||
<BSkeleton class="m-0" :width="randint(45, 100) + '%'" height="24px" />
|
<BSkeleton
|
||||||
|
class="m-0"
|
||||||
|
:width="randint(45, 100) + '%'"
|
||||||
|
height="24px"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</BCol>
|
</BCol>
|
||||||
|
|
||||||
<BCol>
|
<BCol>
|
||||||
<div class="w100 d-flex justify-content-between" v-if="count % 2 === 0">
|
<div
|
||||||
|
class="w100 d-flex justify-content-between"
|
||||||
|
v-if="count % 2 === 0"
|
||||||
|
>
|
||||||
<BSkeleton width="100%" height="38px" />
|
<BSkeleton width="100%" height="38px" />
|
||||||
|
|
||||||
<BSkeleton width="38px" height="38px" class="ml-2" />
|
<BSkeleton width="38px" height="38px" class="ml-2" />
|
||||||
|
@ -25,7 +32,7 @@
|
||||||
</BCol>
|
</BCol>
|
||||||
</BRow>
|
</BRow>
|
||||||
|
|
||||||
<hr :key="count + '-hr'">
|
<hr :key="count + '-hr'" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
|
@ -44,9 +51,14 @@ export default {
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
itemCount: { type: Number, default: 5 },
|
itemCount: { type: Number, default: 5 },
|
||||||
cols: { type: [Object, null], default () { return { md: 4, lg: 2 } } }
|
cols: {
|
||||||
|
type: [Object, null],
|
||||||
|
default() {
|
||||||
|
return { md: 4, lg: 2 }
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: { randint }
|
methods: { randint },
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -22,9 +22,9 @@ export default {
|
||||||
name: 'CardInfoSkeleton',
|
name: 'CardInfoSkeleton',
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
itemCount: { type: Number, default: 5 }
|
itemCount: { type: Number, default: 5 },
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: { randint }
|
methods: { randint },
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -6,8 +6,12 @@
|
||||||
|
|
||||||
<BListGroup flush>
|
<BListGroup flush>
|
||||||
<BListGroupItem v-for="count in itemCount" :key="count" class="d-flex">
|
<BListGroupItem v-for="count in itemCount" :key="count" class="d-flex">
|
||||||
<div style="width: 20%;">
|
<div style="width: 20%">
|
||||||
<BSkeleton :width="randint(50, 100) + '%'" height="24px" class="mr-3" />
|
<BSkeleton
|
||||||
|
:width="randint(50, 100) + '%'"
|
||||||
|
height="24px"
|
||||||
|
class="mr-3"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<BSkeleton :width="randint(30, 80) + '%'" height="24px" class="m-0" />
|
<BSkeleton :width="randint(30, 80) + '%'" height="24px" class="m-0" />
|
||||||
</BListGroupItem>
|
</BListGroupItem>
|
||||||
|
@ -22,9 +26,9 @@ export default {
|
||||||
name: 'CardListSkeleton',
|
name: 'CardListSkeleton',
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
itemCount: { type: Number, default: 5 }
|
itemCount: { type: Number, default: 5 },
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: { randint }
|
methods: { randint },
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -14,9 +14,9 @@ export default {
|
||||||
name: 'ListGroupSkeleton',
|
name: 'ListGroupSkeleton',
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
itemCount: { type: Number, default: 5 }
|
itemCount: { type: Number, default: 5 },
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: { randint }
|
methods: { randint },
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
* @param {Number} delay - delay after which the promise is rejected
|
* @param {Number} delay - delay after which the promise is rejected
|
||||||
* @return {Promise}
|
* @return {Promise}
|
||||||
*/
|
*/
|
||||||
export function timeout (promise, delay) {
|
export function timeout(promise, delay) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
// FIXME reject(new Error('api_not_responding')) for post-install
|
// FIXME reject(new Error('api_not_responding')) for post-install
|
||||||
setTimeout(() => reject, delay)
|
setTimeout(() => reject, delay)
|
||||||
|
@ -15,18 +15,20 @@ export function timeout (promise, delay) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if passed value is an object literal.
|
* Check if passed value is an object literal.
|
||||||
*
|
*
|
||||||
* @param {*} value - Anything.
|
* @param {*} value - Anything.
|
||||||
* @return {Boolean}
|
* @return {Boolean}
|
||||||
*/
|
*/
|
||||||
export function isObjectLiteral (value) {
|
export function isObjectLiteral(value) {
|
||||||
return value !== null && value !== undefined && Object.is(value.constructor, Object)
|
return (
|
||||||
|
value !== null &&
|
||||||
|
value !== undefined &&
|
||||||
|
Object.is(value.constructor, Object)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if value is "empty" (`null`, `undefined`, `''`, `[]`, '{}').
|
* Check if value is "empty" (`null`, `undefined`, `''`, `[]`, '{}').
|
||||||
* Note: `0` is not considered "empty" in that helper.
|
* Note: `0` is not considered "empty" in that helper.
|
||||||
|
@ -34,12 +36,11 @@ export function isObjectLiteral (value) {
|
||||||
* @param {*} value - Anything.
|
* @param {*} value - Anything.
|
||||||
* @return {Boolean}
|
* @return {Boolean}
|
||||||
*/
|
*/
|
||||||
export function isEmptyValue (value) {
|
export function isEmptyValue(value) {
|
||||||
if (typeof value === 'number') return false
|
if (typeof value === 'number') return false
|
||||||
return !value || value.length === 0 || Object.keys(value).length === 0
|
return !value || value.length === 0 || Object.keys(value).length === 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an flattened object literal, with all keys at first level and removing nested ones.
|
* Returns an flattened object literal, with all keys at first level and removing nested ones.
|
||||||
*
|
*
|
||||||
|
@ -47,8 +48,8 @@ export function isEmptyValue (value) {
|
||||||
* @param {Object} [flattened={}] - An object literal to add passed obj keys/values.
|
* @param {Object} [flattened={}] - An object literal to add passed obj keys/values.
|
||||||
* @return {Object}
|
* @return {Object}
|
||||||
*/
|
*/
|
||||||
export function flattenObjectLiteral (obj, flattened = {}) {
|
export function flattenObjectLiteral(obj, flattened = {}) {
|
||||||
function flatten (objLit) {
|
function flatten(objLit) {
|
||||||
for (const key in objLit) {
|
for (const key in objLit) {
|
||||||
const value = objLit[key]
|
const value = objLit[key]
|
||||||
if (isObjectLiteral(value)) {
|
if (isObjectLiteral(value)) {
|
||||||
|
@ -62,7 +63,6 @@ export function flattenObjectLiteral (obj, flattened = {}) {
|
||||||
return flattened
|
return flattened
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an new Object filtered with passed filter function.
|
* Returns an new Object filtered with passed filter function.
|
||||||
* Each entry `[key, value]` will be forwarded to the `filter` function.
|
* Each entry `[key, value]` will be forwarded to the `filter` function.
|
||||||
|
@ -71,11 +71,12 @@ export function flattenObjectLiteral (obj, flattened = {}) {
|
||||||
* @param {Function} filter - the filter function to call for each entry.
|
* @param {Function} filter - the filter function to call for each entry.
|
||||||
* @return {Object}
|
* @return {Object}
|
||||||
*/
|
*/
|
||||||
export function filterObject (obj, filter) {
|
export function filterObject(obj, filter) {
|
||||||
return Object.fromEntries(Object.entries(obj).filter((...args) => filter(...args)))
|
return Object.fromEntries(
|
||||||
|
Object.entries(obj).filter((...args) => filter(...args)),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an new array containing items that are in first array but not in the other.
|
* Returns an new array containing items that are in first array but not in the other.
|
||||||
*
|
*
|
||||||
|
@ -83,18 +84,17 @@ export function filterObject (obj, filter) {
|
||||||
* @param {Array} [arr2=[]]
|
* @param {Array} [arr2=[]]
|
||||||
* @return {Array}
|
* @return {Array}
|
||||||
*/
|
*/
|
||||||
export function arrayDiff (arr1 = [], arr2 = []) {
|
export function arrayDiff(arr1 = [], arr2 = []) {
|
||||||
return arr1.filter(item => !arr2.includes(item))
|
return arr1.filter((item) => !arr2.includes(item))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a new string with escaped HTML (`&<>"'` replaced by entities).
|
* Returns a new string with escaped HTML (`&<>"'` replaced by entities).
|
||||||
*
|
*
|
||||||
* @param {String} unsafe
|
* @param {String} unsafe
|
||||||
* @return {String}
|
* @return {String}
|
||||||
*/
|
*/
|
||||||
export function escapeHtml (unsafe) {
|
export function escapeHtml(unsafe) {
|
||||||
return unsafe
|
return unsafe
|
||||||
.replace(/&/g, '&')
|
.replace(/&/g, '&')
|
||||||
.replace(/</g, '<')
|
.replace(/</g, '<')
|
||||||
|
@ -110,11 +110,10 @@ export function escapeHtml (unsafe) {
|
||||||
* @param {Number} max
|
* @param {Number} max
|
||||||
* @return {Number}
|
* @return {Number}
|
||||||
*/
|
*/
|
||||||
export function randint (min, max) {
|
export function randint(min, max) {
|
||||||
return Math.floor(Math.random() * (max - min + 1)) + min
|
return Math.floor(Math.random() * (max - min + 1)) + min
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a File content.
|
* Returns a File content.
|
||||||
*
|
*
|
||||||
|
@ -123,7 +122,7 @@ export function randint (min, max) {
|
||||||
* @param {Boolean} [extraParams.base64] - returns a base64 representation of the file.
|
* @param {Boolean} [extraParams.base64] - returns a base64 representation of the file.
|
||||||
* @return {Promise<String>}
|
* @return {Promise<String>}
|
||||||
*/
|
*/
|
||||||
export function getFileContent (file, { base64 = false } = {}) {
|
export function getFileContent(file, { base64 = false } = {}) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const reader = new FileReader()
|
const reader = new FileReader()
|
||||||
reader.onerror = reject
|
reader.onerror = reject
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
* A Node that can have a parent and children.
|
* A Node that can have a parent and children.
|
||||||
*/
|
*/
|
||||||
export class Node {
|
export class Node {
|
||||||
constructor (data) {
|
constructor(data) {
|
||||||
this.data = data
|
this.data = data
|
||||||
this.depth = 0
|
this.depth = 0
|
||||||
this.height = 0
|
this.height = 0
|
||||||
|
@ -22,7 +22,7 @@ export class Node {
|
||||||
* @param {function} callback
|
* @param {function} callback
|
||||||
* @return {Object}
|
* @return {Object}
|
||||||
*/
|
*/
|
||||||
eachBefore (callback) {
|
eachBefore(callback) {
|
||||||
const nodes = []
|
const nodes = []
|
||||||
let index = -1
|
let index = -1
|
||||||
let node = this
|
let node = this
|
||||||
|
@ -49,7 +49,7 @@ export class Node {
|
||||||
* @param {function} callback
|
* @param {function} callback
|
||||||
* @return {Object}
|
* @return {Object}
|
||||||
*/
|
*/
|
||||||
eachAfter (callback) {
|
eachAfter(callback) {
|
||||||
const nodes = []
|
const nodes = []
|
||||||
const next = []
|
const next = []
|
||||||
let node = this
|
let node = this
|
||||||
|
@ -81,7 +81,7 @@ export class Node {
|
||||||
* @param {String} [args.parentIdKey='name'] - the key name where we can find the parent identity.
|
* @param {String} [args.parentIdKey='name'] - the key name where we can find the parent identity.
|
||||||
* @return {Node}
|
* @return {Node}
|
||||||
*/
|
*/
|
||||||
filter (callback) {
|
filter(callback) {
|
||||||
// Duplicates this tree and iter on nodes from leaves to root (post-order traversal)
|
// Duplicates this tree and iter on nodes from leaves to root (post-order traversal)
|
||||||
return hierarchy(this).eachAfter((node, i) => {
|
return hierarchy(this).eachAfter((node, i) => {
|
||||||
// Since we create a new hierarchy from another, nodes's `data` contains the
|
// Since we create a new hierarchy from another, nodes's `data` contains the
|
||||||
|
@ -90,7 +90,7 @@ export class Node {
|
||||||
|
|
||||||
if (node.children) {
|
if (node.children) {
|
||||||
// Removed flagged children
|
// Removed flagged children
|
||||||
node.children = node.children.filter(child => !child.remove)
|
node.children = node.children.filter((child) => !child.remove)
|
||||||
if (!node.children.length) delete node.children
|
if (!node.children.length) delete node.children
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -104,7 +104,6 @@ export class Node {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a new hierarchy from the specified tabular `dataset`.
|
* Generates a new hierarchy from the specified tabular `dataset`.
|
||||||
* The specified `dataset` must be an array of objects that contains at least a
|
* The specified `dataset` must be an array of objects that contains at least a
|
||||||
|
@ -117,13 +116,16 @@ export class Node {
|
||||||
* @param {String} [args.parentIdKey='name'] - the key name where we can find the parent identity.
|
* @param {String} [args.parentIdKey='name'] - the key name where we can find the parent identity.
|
||||||
* @return {Node}
|
* @return {Node}
|
||||||
*/
|
*/
|
||||||
export function stratify (dataset, { idKey = 'name', parentIdKey = 'parent' } = {}) {
|
export function stratify(
|
||||||
|
dataset,
|
||||||
|
{ idKey = 'name', parentIdKey = 'parent' } = {},
|
||||||
|
) {
|
||||||
const root = new Node(null, true)
|
const root = new Node(null, true)
|
||||||
root.children = []
|
root.children = []
|
||||||
const nodesMap = new Map()
|
const nodesMap = new Map()
|
||||||
|
|
||||||
// Creates all nodes that will be arranged in a hierarchy
|
// Creates all nodes that will be arranged in a hierarchy
|
||||||
const nodes = dataset.map(d => {
|
const nodes = dataset.map((d) => {
|
||||||
const node = new Node(d)
|
const node = new Node(d)
|
||||||
node.id = d[idKey]
|
node.id = d[idKey]
|
||||||
nodesMap.set(node.id, node)
|
nodesMap.set(node.id, node)
|
||||||
|
@ -148,7 +150,7 @@ export function stratify (dataset, { idKey = 'name', parentIdKey = 'parent' } =
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
root.eachBefore(node => {
|
root.eachBefore((node) => {
|
||||||
// Compute node depth
|
// Compute node depth
|
||||||
if (node.parent) {
|
if (node.parent) {
|
||||||
node.depth = node.parent.depth + 1
|
node.depth = node.parent.depth + 1
|
||||||
|
@ -160,7 +162,6 @@ export function stratify (dataset, { idKey = 'name', parentIdKey = 'parent' } =
|
||||||
return root
|
return root
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs a root node from the specified hierarchical `data`.
|
* Constructs a root node from the specified hierarchical `data`.
|
||||||
* The specified `data` must be an object representing the root node and its children.
|
* The specified `data` must be an object representing the root node and its children.
|
||||||
|
@ -170,14 +171,14 @@ export function stratify (dataset, { idKey = 'name', parentIdKey = 'parent' } =
|
||||||
* @param {Node|Object} data - object representing a root node (a simple { id, children } object or a `Node`)
|
* @param {Node|Object} data - object representing a root node (a simple { id, children } object or a `Node`)
|
||||||
* @return {Node}
|
* @return {Node}
|
||||||
*/
|
*/
|
||||||
export function hierarchy (data) {
|
export function hierarchy(data) {
|
||||||
const root = new Node(data)
|
const root = new Node(data)
|
||||||
const nodes = []
|
const nodes = []
|
||||||
let node = root
|
let node = root
|
||||||
|
|
||||||
while (node) {
|
while (node) {
|
||||||
if (node.data.children) {
|
if (node.data.children) {
|
||||||
node.children = node.data.children.map(child_ => {
|
node.children = node.data.children.map((child_) => {
|
||||||
const child = new Node(child_)
|
const child = new Node(child_)
|
||||||
child.id = child_.id
|
child.id = child_.id
|
||||||
child.parent = node === root ? null : node
|
child.parent = node === root ? null : node
|
||||||
|
@ -193,14 +194,13 @@ export function hierarchy (data) {
|
||||||
return root
|
return root
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compute the node height by iterating on parents
|
* Compute the node height by iterating on parents
|
||||||
* Code taken from d3.js https://github.com/d3/d3-hierarchy/blob/main/src/hierarchy/index.js#L62.
|
* Code taken from d3.js https://github.com/d3/d3-hierarchy/blob/main/src/hierarchy/index.js#L62.
|
||||||
*
|
*
|
||||||
* @param {Node} node
|
* @param {Node} node
|
||||||
*/
|
*/
|
||||||
function computeNodeHeight (node) {
|
function computeNodeHeight(node) {
|
||||||
let height = 0
|
let height = 0
|
||||||
do {
|
do {
|
||||||
node.height = height
|
node.height = height
|
||||||
|
|
|
@ -3,17 +3,13 @@ import format from 'date-fns/format'
|
||||||
|
|
||||||
import { dateFnsLocale as locale } from '@/i18n/helpers'
|
import { dateFnsLocale as locale } from '@/i18n/helpers'
|
||||||
|
|
||||||
export function distanceToNow (date, addSuffix = true, isTimestamp = false) {
|
export function distanceToNow(date, addSuffix = true, isTimestamp = false) {
|
||||||
return formatDistanceToNow(
|
return formatDistanceToNow(new Date(isTimestamp ? date * 1000 : date), {
|
||||||
new Date(isTimestamp ? date * 1000 : date),
|
addSuffix,
|
||||||
{ addSuffix, locale }
|
locale,
|
||||||
)
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function readableDate (date, isTimestamp = false) {
|
export function readableDate(date, isTimestamp = false) {
|
||||||
return format(
|
return format(new Date(isTimestamp ? date * 1000 : date), 'PPPpp', { locale })
|
||||||
new Date(isTimestamp ? date * 1000 : date),
|
|
||||||
'PPPpp',
|
|
||||||
{ locale }
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
export function humanSize (bytes) {
|
export function humanSize(bytes) {
|
||||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']
|
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']
|
||||||
if (bytes === 0) return 'n/a'
|
if (bytes === 0) return 'n/a'
|
||||||
const i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)))
|
const i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)))
|
||||||
return Math.round(bytes / Math.pow(1024, i), 2) + ' ' + sizes[i]
|
return Math.round(bytes / Math.pow(1024, i), 2) + ' ' + sizes[i]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function humanPermissionName(text) {
|
||||||
export function humanPermissionName (text) {
|
return text
|
||||||
return text.split('.')[1].replace('_', ' ').replace(/\w\S*/g, part => {
|
.split('.')[1]
|
||||||
return part.charAt(0).toUpperCase() + part.substr(1).toLowerCase()
|
.replace('_', ' ')
|
||||||
})
|
.replace(/\w\S*/g, (part) => {
|
||||||
|
return part.charAt(0).toUpperCase() + part.substr(1).toLowerCase()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,52 +1,70 @@
|
||||||
import { helpers } from 'vuelidate/lib/validators'
|
import { helpers } from 'vuelidate/lib/validators'
|
||||||
|
|
||||||
|
|
||||||
// Unicode ranges are taken from https://stackoverflow.com/a/37668315
|
// Unicode ranges are taken from https://stackoverflow.com/a/37668315
|
||||||
const nonAsciiWordCharacters = '\u00AA\u00B5\u00BA\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u052F\u0531-\u0556\u0559\u0561-\u0587\u05D0-\u05EA\u05F0-\u05F2\u0620-\u064A\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u08A0-\u08B4\u0904-\u0939\u093D\u0950\u0958-\u0961\u0971-\u0980\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0AF9\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D\u0C58-\u0C5A\u0C60\u0C61\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDE\u0CE0\u0CE1\u0CF1\u0CF2\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D5F-\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16F1-\u16F8\u1700-\u170C\u170E-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u1820-\u1877\u1880-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191E\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u1A00-\u1A16\u1A20-\u1A54\u1AA7\u1B05-\u1B33\u1B45-\u1B4B\u1B83-\u1BA0\u1BAE\u1BAF\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C7D\u1CE9-\u1CEC\u1CEE-\u1CF1\u1CF5\u1CF6\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2183\u2184\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2E2F\u3005\u3006\u3031-\u3035\u303B\u303C\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312D\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FD5\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA61F\uA62A\uA62B\uA640-\uA66E\uA67F-\uA69D\uA6A0-\uA6E5\uA717-\uA71F\uA722-\uA788\uA78B-\uA7AD\uA7B0-\uA7B7\uA7F7-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB\uA8FD\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF\uA9E0-\uA9E4\uA9E6-\uA9EF\uA9FA-\uA9FE\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA76\uAA7A\uAA7E-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB65\uAB70-\uABE2\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC'
|
const nonAsciiWordCharacters =
|
||||||
|
'\u00AA\u00B5\u00BA\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u052F\u0531-\u0556\u0559\u0561-\u0587\u05D0-\u05EA\u05F0-\u05F2\u0620-\u064A\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u08A0-\u08B4\u0904-\u0939\u093D\u0950\u0958-\u0961\u0971-\u0980\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0AF9\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D\u0C58-\u0C5A\u0C60\u0C61\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDE\u0CE0\u0CE1\u0CF1\u0CF2\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D5F-\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16F1-\u16F8\u1700-\u170C\u170E-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u1820-\u1877\u1880-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191E\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u1A00-\u1A16\u1A20-\u1A54\u1AA7\u1B05-\u1B33\u1B45-\u1B4B\u1B83-\u1BA0\u1BAE\u1BAF\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C7D\u1CE9-\u1CEC\u1CEE-\u1CF1\u1CF5\u1CF6\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2183\u2184\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2E2F\u3005\u3006\u3031-\u3035\u303B\u303C\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312D\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FD5\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA61F\uA62A\uA62B\uA640-\uA66E\uA67F-\uA69D\uA6A0-\uA6E5\uA717-\uA71F\uA722-\uA788\uA78B-\uA7AD\uA7B0-\uA7B7\uA7F7-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB\uA8FD\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF\uA9E0-\uA9E4\uA9E6-\uA9EF\uA9FA-\uA9FE\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA76\uAA7A\uAA7E-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB65\uAB70-\uABE2\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC'
|
||||||
|
|
||||||
const alphalownumdot_ = helpers.regex('alphalownumdot_', /^[a-z0-9_.]+$/)
|
const alphalownumdot_ = helpers.regex('alphalownumdot_', /^[a-z0-9_.]+$/)
|
||||||
|
|
||||||
const domain = helpers.regex('domain', new RegExp(`^(?:[\\da-z${nonAsciiWordCharacters}]+(?:-*[\\da-z${nonAsciiWordCharacters}]+)*\\.)+(?:(?:xn--)?[\\da-z${nonAsciiWordCharacters}]{2,})$`))
|
const domain = helpers.regex(
|
||||||
|
'domain',
|
||||||
|
new RegExp(
|
||||||
|
`^(?:[\\da-z${nonAsciiWordCharacters}]+(?:-*[\\da-z${nonAsciiWordCharacters}]+)*\\.)+(?:(?:xn--)?[\\da-z${nonAsciiWordCharacters}]{2,})$`,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
const dynDomain = helpers.regex('dynDomain', new RegExp(`^(?:xn--)?[\\da-z-${nonAsciiWordCharacters}]+$`))
|
const dynDomain = helpers.regex(
|
||||||
|
'dynDomain',
|
||||||
|
new RegExp(`^(?:xn--)?[\\da-z-${nonAsciiWordCharacters}]+$`),
|
||||||
|
)
|
||||||
|
|
||||||
const emailLocalPart = helpers.regex('emailLocalPart', /^[\w.-]+$/)
|
const emailLocalPart = helpers.regex('emailLocalPart', /^[\w.-]+$/)
|
||||||
|
|
||||||
const emailForwardLocalPart = helpers.regex('emailForwardLocalPart', /^[\w+.-]+$/)
|
const emailForwardLocalPart = helpers.regex(
|
||||||
|
'emailForwardLocalPart',
|
||||||
|
/^[\w+.-]+$/,
|
||||||
|
)
|
||||||
|
|
||||||
const email = value => helpers.withParams(
|
const email = (value) =>
|
||||||
{ type: 'email', value },
|
helpers.withParams({ type: 'email', value }, (value) => {
|
||||||
value => {
|
|
||||||
const [localPart, domainPart] = value.split('@')
|
const [localPart, domainPart] = value.split('@')
|
||||||
if (!domainPart) return !helpers.req(value) || false
|
if (!domainPart) return !helpers.req(value) || false
|
||||||
return !helpers.req(value) || (emailLocalPart(localPart) && domain(domainPart))
|
return (
|
||||||
}
|
!helpers.req(value) || (emailLocalPart(localPart) && domain(domainPart))
|
||||||
)(value)
|
)
|
||||||
|
})(value)
|
||||||
|
|
||||||
// Same as email but with `+` allowed.
|
// Same as email but with `+` allowed.
|
||||||
const emailForward = value => helpers.withParams(
|
const emailForward = (value) =>
|
||||||
{ type: 'emailForward', value },
|
helpers.withParams({ type: 'emailForward', value }, (value) => {
|
||||||
value => {
|
|
||||||
const [localPart, domainPart] = value.split('@')
|
const [localPart, domainPart] = value.split('@')
|
||||||
if (!domainPart) return !helpers.req(value) || false
|
if (!domainPart) return !helpers.req(value) || false
|
||||||
return !helpers.req(value) || (emailForwardLocalPart(localPart) && domain(domainPart))
|
return (
|
||||||
}
|
!helpers.req(value) ||
|
||||||
)(value)
|
(emailForwardLocalPart(localPart) && domain(domainPart))
|
||||||
|
)
|
||||||
|
})(value)
|
||||||
|
|
||||||
const appRepoUrl = helpers.regex('appRepoUrl', /^https:\/\/[a-zA-Z0-9-_.]+\/[a-zA-Z0-9-_./~]+\/[a-zA-Z0-9-_.]+_ynh(\/?(-\/)?tree\/[a-zA-Z0-9-_.]+)?(\.git)?\/?$/)
|
const appRepoUrl = helpers.regex(
|
||||||
|
'appRepoUrl',
|
||||||
|
/^https:\/\/[a-zA-Z0-9-_.]+\/[a-zA-Z0-9-_./~]+\/[a-zA-Z0-9-_.]+_ynh(\/?(-\/)?tree\/[a-zA-Z0-9-_.]+)?(\.git)?\/?$/,
|
||||||
|
)
|
||||||
|
|
||||||
const includes = items => item => helpers.withParams(
|
const includes = (items) => (item) =>
|
||||||
{ type: 'includes', value: item },
|
helpers.withParams(
|
||||||
item => !helpers.req(item) || (items ? items.includes(item) : false)
|
{ type: 'includes', value: item },
|
||||||
)(item)
|
(item) => !helpers.req(item) || (items ? items.includes(item) : false),
|
||||||
|
)(item)
|
||||||
|
|
||||||
const name = helpers.regex('name', new RegExp(`^(?:[A-Za-z${nonAsciiWordCharacters}]{1,30}[ ,.'-]{0,3})+$`))
|
const name = helpers.regex(
|
||||||
|
'name',
|
||||||
|
new RegExp(`^(?:[A-Za-z${nonAsciiWordCharacters}]{1,30}[ ,.'-]{0,3})+$`),
|
||||||
|
)
|
||||||
|
|
||||||
const unique = items => item => helpers.withParams(
|
const unique = (items) => (item) =>
|
||||||
{ type: 'unique', arg: items, value: item },
|
helpers.withParams({ type: 'unique', arg: items, value: item }, (item) =>
|
||||||
item => items ? !helpers.req(item) || !items.includes(item) : true
|
items ? !helpers.req(item) || !items.includes(item) : true,
|
||||||
)(item)
|
)(item)
|
||||||
|
|
||||||
export {
|
export {
|
||||||
alphalownumdot_,
|
alphalownumdot_,
|
||||||
|
@ -59,5 +77,5 @@ export {
|
||||||
appRepoUrl,
|
appRepoUrl,
|
||||||
includes,
|
includes,
|
||||||
name,
|
name,
|
||||||
unique
|
unique,
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,5 +8,5 @@ export {
|
||||||
minLength,
|
minLength,
|
||||||
minValue,
|
minValue,
|
||||||
required,
|
required,
|
||||||
sameAs
|
sameAs,
|
||||||
} from 'vuelidate/lib/validators'
|
} from 'vuelidate/lib/validators'
|
||||||
|
|
|
@ -6,16 +6,15 @@ import {
|
||||||
isObjectLiteral,
|
isObjectLiteral,
|
||||||
isEmptyValue,
|
isEmptyValue,
|
||||||
flattenObjectLiteral,
|
flattenObjectLiteral,
|
||||||
getFileContent
|
getFileContent,
|
||||||
} from '@/helpers/commons'
|
} from '@/helpers/commons'
|
||||||
|
|
||||||
|
|
||||||
const NO_VALUE_FIELDS = [
|
const NO_VALUE_FIELDS = [
|
||||||
'ReadOnlyField',
|
'ReadOnlyField',
|
||||||
'ReadOnlyAlertItem',
|
'ReadOnlyAlertItem',
|
||||||
'MarkdownItem',
|
'MarkdownItem',
|
||||||
'DisplayTextItem',
|
'DisplayTextItem',
|
||||||
'ButtonItem'
|
'ButtonItem',
|
||||||
]
|
]
|
||||||
|
|
||||||
export const DEFAULT_STATUS_ICON = {
|
export const DEFAULT_STATUS_ICON = {
|
||||||
|
@ -24,7 +23,7 @@ export const DEFAULT_STATUS_ICON = {
|
||||||
error: 'times',
|
error: 'times',
|
||||||
info: 'info',
|
info: 'info',
|
||||||
success: 'check',
|
success: 'check',
|
||||||
warning: 'warning'
|
warning: 'warning',
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -34,20 +33,19 @@ export const DEFAULT_STATUS_ICON = {
|
||||||
* @param {(Object|String|undefined)} field - A field value containing a translation object or string
|
* @param {(Object|String|undefined)} field - A field value containing a translation object or string
|
||||||
* @return {String}
|
* @return {String}
|
||||||
*/
|
*/
|
||||||
export function formatI18nField (field) {
|
export function formatI18nField(field) {
|
||||||
if (typeof field === 'string') return field
|
if (typeof field === 'string') return field
|
||||||
const { locale, fallbackLocale } = store.state
|
const { locale, fallbackLocale } = store.state
|
||||||
return field ? field[locale] || field[fallbackLocale] || field.en : ''
|
return field ? field[locale] || field[fallbackLocale] || field.en : ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a string size declaration to a M value.
|
* Returns a string size declaration to a M value.
|
||||||
*
|
*
|
||||||
* @param {String} sizeStr - A size declared like '500M' or '56k'
|
* @param {String} sizeStr - A size declared like '500M' or '56k'
|
||||||
* @return {Number}
|
* @return {Number}
|
||||||
*/
|
*/
|
||||||
export function sizeToM (sizeStr) {
|
export function sizeToM(sizeStr) {
|
||||||
const unit = sizeStr.slice(-1)
|
const unit = sizeStr.slice(-1)
|
||||||
const value = sizeStr.slice(0, -1)
|
const value = sizeStr.slice(0, -1)
|
||||||
if (unit === 'M') return parseInt(value)
|
if (unit === 'M') return parseInt(value)
|
||||||
|
@ -57,20 +55,18 @@ export function sizeToM (sizeStr) {
|
||||||
if (unit === 'T') return Math.ceil(value * 1024 * 1024)
|
if (unit === 'T') return Math.ceil(value * 1024 * 1024)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a formatted address element to be used by AdressInputSelect component.
|
* Returns a formatted address element to be used by AdressInputSelect component.
|
||||||
*
|
*
|
||||||
* @param {String} address - A string representing an adress (subdomain or email)
|
* @param {String} address - A string representing an adress (subdomain or email)
|
||||||
* @return {Object} - `{ localPart, separator, domain }`.
|
* @return {Object} - `{ localPart, separator, domain }`.
|
||||||
*/
|
*/
|
||||||
export function adressToFormValue (address) {
|
export function adressToFormValue(address) {
|
||||||
const separator = address.includes('@') ? '@' : '.'
|
const separator = address.includes('@') ? '@' : '.'
|
||||||
const [localPart, domain] = address.split(separator)
|
const [localPart, domain] = address.split(separator)
|
||||||
return { localPart, separator, domain }
|
return { localPart, separator, domain }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Evaluate config panel string expression that can contain regular expressions.
|
* Evaluate config panel string expression that can contain regular expressions.
|
||||||
* Expression are evaluated with the config panel form as context.
|
* Expression are evaluated with the config panel form as context.
|
||||||
|
@ -79,7 +75,7 @@ export function adressToFormValue (address) {
|
||||||
* @param {Object} forms - A nested form used in config panels.
|
* @param {Object} forms - A nested form used in config panels.
|
||||||
* @return {Boolean} - expression evaluation result.
|
* @return {Boolean} - expression evaluation result.
|
||||||
*/
|
*/
|
||||||
export function evaluateExpression (expression, form, nested = true) {
|
export function evaluateExpression(expression, form, nested = true) {
|
||||||
if (!expression) return true
|
if (!expression) return true
|
||||||
if (expression === '"false"') return false
|
if (expression === '"false"') return false
|
||||||
|
|
||||||
|
@ -110,13 +106,12 @@ export function evaluateExpression (expression, form, nested = true) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Adds a property to an Object that will dynamically returns a expression evaluation result.
|
// Adds a property to an Object that will dynamically returns a expression evaluation result.
|
||||||
function addEvaluationGetter (prop, obj, expr, ctx, nested) {
|
function addEvaluationGetter(prop, obj, expr, ctx, nested) {
|
||||||
Object.defineProperty(obj, prop, {
|
Object.defineProperty(obj, prop, {
|
||||||
get: () => evaluateExpression(expr, ctx, nested)
|
get: () => evaluateExpression(expr, ctx, nested),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format app install, actions and config panel argument into a data structure that
|
* Format app install, actions and config panel argument into a data structure that
|
||||||
* will be automaticly transformed into a component on screen.
|
* will be automaticly transformed into a component on screen.
|
||||||
|
@ -124,8 +119,13 @@ function addEvaluationGetter (prop, obj, expr, ctx, nested) {
|
||||||
* @param {Object} arg - a yunohost arg options written by a packager.
|
* @param {Object} arg - a yunohost arg options written by a packager.
|
||||||
* @return {Object} an formated argument containing formItem props, validation and base value.
|
* @return {Object} an formated argument containing formItem props, validation and base value.
|
||||||
*/
|
*/
|
||||||
export function formatYunoHostArgument (arg) {
|
export function formatYunoHostArgument(arg) {
|
||||||
let value = (arg.value !== undefined) ? arg.value : (arg.current_value !== undefined) ? arg.current_value : null
|
let value =
|
||||||
|
arg.value !== undefined
|
||||||
|
? arg.value
|
||||||
|
: arg.current_value !== undefined
|
||||||
|
? arg.current_value
|
||||||
|
: null
|
||||||
const validation = {}
|
const validation = {}
|
||||||
const error = { message: null }
|
const error = { message: null }
|
||||||
arg.ask = formatI18nField(arg.ask)
|
arg.ask = formatI18nField(arg.ask)
|
||||||
|
@ -135,8 +135,8 @@ export function formatYunoHostArgument (arg) {
|
||||||
props: {
|
props: {
|
||||||
label: arg.ask,
|
label: arg.ask,
|
||||||
component: undefined,
|
component: undefined,
|
||||||
props: {}
|
props: {},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultProps = ['id', 'placeholder:example']
|
const defaultProps = ['id', 'placeholder:example']
|
||||||
|
@ -144,12 +144,12 @@ export function formatYunoHostArgument (arg) {
|
||||||
{
|
{
|
||||||
types: ['string', 'path'],
|
types: ['string', 'path'],
|
||||||
name: 'InputItem',
|
name: 'InputItem',
|
||||||
props: defaultProps.concat(['autocomplete', 'trim', 'choices'])
|
props: defaultProps.concat(['autocomplete', 'trim', 'choices']),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
types: ['email', 'url', 'date', 'time', 'color'],
|
types: ['email', 'url', 'date', 'time', 'color'],
|
||||||
name: 'InputItem',
|
name: 'InputItem',
|
||||||
props: defaultProps.concat(['type', 'trim'])
|
props: defaultProps.concat(['type', 'trim']),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
types: ['password'],
|
types: ['password'],
|
||||||
|
@ -161,7 +161,7 @@ export function formatYunoHostArgument (arg) {
|
||||||
}
|
}
|
||||||
arg.example = '••••••••••••'
|
arg.example = '••••••••••••'
|
||||||
validation.passwordLenght = validators.minLength(8)
|
validation.passwordLenght = validators.minLength(8)
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
types: ['number', 'range'],
|
types: ['number', 'range'],
|
||||||
|
@ -175,7 +175,7 @@ export function formatYunoHostArgument (arg) {
|
||||||
validation.maxValue = validators.maxValue(parseInt(arg.max))
|
validation.maxValue = validators.maxValue(parseInt(arg.max))
|
||||||
}
|
}
|
||||||
validation.numValue = validators.integer
|
validation.numValue = validators.integer
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
types: ['select', 'user', 'domain', 'app', 'group'],
|
types: ['select', 'user', 'domain', 'app', 'group'],
|
||||||
|
@ -183,9 +183,12 @@ export function formatYunoHostArgument (arg) {
|
||||||
props: ['id', 'choices'],
|
props: ['id', 'choices'],
|
||||||
callback: function () {
|
callback: function () {
|
||||||
if (arg.type !== 'select') {
|
if (arg.type !== 'select') {
|
||||||
field.props.link = { name: arg.type + '-list', text: i18n.t(`manage_${arg.type}s`) }
|
field.props.link = {
|
||||||
|
name: arg.type + '-list',
|
||||||
|
text: i18n.t(`manage_${arg.type}s`),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
types: ['file'],
|
types: ['file'],
|
||||||
|
@ -197,26 +200,31 @@ export function formatYunoHostArgument (arg) {
|
||||||
file: value ? new File([''], value) : null,
|
file: value ? new File([''], value) : null,
|
||||||
content: '',
|
content: '',
|
||||||
current: !!value,
|
current: !!value,
|
||||||
removed: false
|
removed: false,
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
types: ['text'],
|
types: ['text'],
|
||||||
name: 'TextAreaItem',
|
name: 'TextAreaItem',
|
||||||
props: defaultProps
|
props: defaultProps,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
types: ['tags'],
|
types: ['tags'],
|
||||||
name: 'TagsItem',
|
name: 'TagsItem',
|
||||||
props: defaultProps.concat(['limit', 'placeholder', 'options:choices', 'tagIcon:icon']),
|
props: defaultProps.concat([
|
||||||
|
'limit',
|
||||||
|
'placeholder',
|
||||||
|
'options:choices',
|
||||||
|
'tagIcon:icon',
|
||||||
|
]),
|
||||||
callback: function () {
|
callback: function () {
|
||||||
if (arg.choices && arg.choices.length) {
|
if (arg.choices && arg.choices.length) {
|
||||||
this.name = 'TagsSelectizeItem'
|
this.name = 'TagsSelectizeItem'
|
||||||
Object.assign(field.props.props, {
|
Object.assign(field.props.props, {
|
||||||
auto: true,
|
auto: true,
|
||||||
itemsName: '',
|
itemsName: '',
|
||||||
label: arg.placeholder
|
label: arg.placeholder,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (typeof value === 'string') {
|
if (typeof value === 'string') {
|
||||||
|
@ -224,7 +232,7 @@ export function formatYunoHostArgument (arg) {
|
||||||
} else if (!value) {
|
} else if (!value) {
|
||||||
value = []
|
value = []
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
types: ['boolean'],
|
types: ['boolean'],
|
||||||
|
@ -232,36 +240,40 @@ export function formatYunoHostArgument (arg) {
|
||||||
props: ['id', 'choices'],
|
props: ['id', 'choices'],
|
||||||
callback: function () {
|
callback: function () {
|
||||||
if (value !== null && value !== undefined) {
|
if (value !== null && value !== undefined) {
|
||||||
value = ['1', 'yes', 'y', 'true'].includes(String(value).toLowerCase())
|
value = ['1', 'yes', 'y', 'true'].includes(
|
||||||
|
String(value).toLowerCase(),
|
||||||
|
)
|
||||||
} else if (arg.default !== null && arg.default !== undefined) {
|
} else if (arg.default !== null && arg.default !== undefined) {
|
||||||
value = ['1', 'yes', 'y', 'true'].includes(String(arg.default).toLowerCase())
|
value = ['1', 'yes', 'y', 'true'].includes(
|
||||||
|
String(arg.default).toLowerCase(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
types: ['alert'],
|
types: ['alert'],
|
||||||
name: 'ReadOnlyAlertItem',
|
name: 'ReadOnlyAlertItem',
|
||||||
props: ['type:style', 'label:ask', 'icon'],
|
props: ['type:style', 'label:ask', 'icon'],
|
||||||
renderSelf: true
|
renderSelf: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
types: ['markdown'],
|
types: ['markdown'],
|
||||||
name: 'MarkdownItem',
|
name: 'MarkdownItem',
|
||||||
props: ['label:ask'],
|
props: ['label:ask'],
|
||||||
renderSelf: true
|
renderSelf: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
types: ['display_text'],
|
types: ['display_text'],
|
||||||
name: 'DisplayTextItem',
|
name: 'DisplayTextItem',
|
||||||
props: ['label:ask'],
|
props: ['label:ask'],
|
||||||
renderSelf: true
|
renderSelf: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
types: ['button'],
|
types: ['button'],
|
||||||
name: 'ButtonItem',
|
name: 'ButtonItem',
|
||||||
props: ['type:style', 'label:ask', 'icon', 'enabled'],
|
props: ['type:style', 'label:ask', 'icon', 'enabled'],
|
||||||
renderSelf: true
|
renderSelf: true,
|
||||||
}
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
// Default type management if no one is filled
|
// Default type management if no one is filled
|
||||||
|
@ -273,7 +285,9 @@ export function formatYunoHostArgument (arg) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Search the component bind to the type
|
// Search the component bind to the type
|
||||||
const component = components.find(element => element.types.includes(arg.type))
|
const component = components.find((element) =>
|
||||||
|
element.types.includes(arg.type),
|
||||||
|
)
|
||||||
if (component === undefined) throw new TypeError('Unknown type: ' + arg.type)
|
if (component === undefined) throw new TypeError('Unknown type: ' + arg.type)
|
||||||
|
|
||||||
// Callback use for specific behaviour
|
// Callback use for specific behaviour
|
||||||
|
@ -290,11 +304,18 @@ export function formatYunoHostArgument (arg) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Required (no need for checkbox its value can't be null)
|
// Required (no need for checkbox its value can't be null)
|
||||||
if (!component.renderSelf && arg.type !== 'boolean' && arg.optional !== true) {
|
if (
|
||||||
|
!component.renderSelf &&
|
||||||
|
arg.type !== 'boolean' &&
|
||||||
|
arg.optional !== true
|
||||||
|
) {
|
||||||
validation.required = validators.required
|
validation.required = validators.required
|
||||||
}
|
}
|
||||||
if (arg.pattern && arg.type !== 'tags') {
|
if (arg.pattern && arg.type !== 'tags') {
|
||||||
validation.pattern = validators.helpers.regex(formatI18nField(arg.pattern.error), new RegExp(arg.pattern.regexp))
|
validation.pattern = validators.helpers.regex(
|
||||||
|
formatI18nField(arg.pattern.error),
|
||||||
|
new RegExp(arg.pattern.regexp),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!component.renderSelf && !arg.readonly) {
|
if (!component.renderSelf && !arg.readonly) {
|
||||||
|
@ -321,7 +342,10 @@ export function formatYunoHostArgument (arg) {
|
||||||
|
|
||||||
// Help message
|
// Help message
|
||||||
if (arg.helpLink) {
|
if (arg.helpLink) {
|
||||||
field.props.link = { href: arg.helpLink.href, text: i18n.t(arg.helpLink.text) }
|
field.props.link = {
|
||||||
|
href: arg.helpLink.href,
|
||||||
|
text: i18n.t(arg.helpLink.text),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (component.renderSelf) {
|
if (component.renderSelf) {
|
||||||
|
@ -334,11 +358,10 @@ export function formatYunoHostArgument (arg) {
|
||||||
field,
|
field,
|
||||||
// Return null instead of empty object if there's no validation
|
// Return null instead of empty object if there's no validation
|
||||||
validation: Object.keys(validation).length === 0 ? null : validation,
|
validation: Object.keys(validation).length === 0 ? null : validation,
|
||||||
error
|
error,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format app install, actions and config panel manifest args into a form that can be used
|
* Format app install, actions and config panel manifest args into a form that can be used
|
||||||
* as v-model values, fields that can be passed to a FormField component and validations.
|
* as v-model values, fields that can be passed to a FormField component and validations.
|
||||||
|
@ -347,7 +370,7 @@ export function formatYunoHostArgument (arg) {
|
||||||
* @param {Object|null} forms - nested form used as the expression evualuations context.
|
* @param {Object|null} forms - nested form used as the expression evualuations context.
|
||||||
* @return {Object} an object containing all parsed values to be used in vue views.
|
* @return {Object} an object containing all parsed values to be used in vue views.
|
||||||
*/
|
*/
|
||||||
export function formatYunoHostArguments (args, forms) {
|
export function formatYunoHostArguments(args, forms) {
|
||||||
const form = {}
|
const form = {}
|
||||||
const fields = {}
|
const fields = {}
|
||||||
const validations = {}
|
const validations = {}
|
||||||
|
@ -361,28 +384,44 @@ export function formatYunoHostArguments (args, forms) {
|
||||||
errors[arg.id] = error
|
errors[arg.id] = error
|
||||||
|
|
||||||
if ('visible' in arg && typeof arg.visible === 'string') {
|
if ('visible' in arg && typeof arg.visible === 'string') {
|
||||||
addEvaluationGetter('visible', field, arg.visible, forms || form, forms !== undefined)
|
addEvaluationGetter(
|
||||||
|
'visible',
|
||||||
|
field,
|
||||||
|
arg.visible,
|
||||||
|
forms || form,
|
||||||
|
forms !== undefined,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('enabled' in arg && typeof arg.enabled === 'string') {
|
if ('enabled' in arg && typeof arg.enabled === 'string') {
|
||||||
addEvaluationGetter('enabled', field.props, arg.enabled, forms || form, forms !== undefined)
|
addEvaluationGetter(
|
||||||
|
'enabled',
|
||||||
|
field.props,
|
||||||
|
arg.enabled,
|
||||||
|
forms || form,
|
||||||
|
forms !== undefined,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { form, fields, validations, errors }
|
return { form, fields, validations, errors }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function formatYunoHostConfigPanels(data) {
|
||||||
export function formatYunoHostConfigPanels (data) {
|
|
||||||
const result = {
|
const result = {
|
||||||
panels: [],
|
panels: [],
|
||||||
forms: {},
|
forms: {},
|
||||||
validations: {},
|
validations: {},
|
||||||
errors: {}
|
errors: {},
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const { id: panelId, name, help, sections } of data.panels) {
|
for (const { id: panelId, name, help, sections } of data.panels) {
|
||||||
const panel = { id: panelId, sections: [], serverError: '', hasApplyButton: false }
|
const panel = {
|
||||||
|
id: panelId,
|
||||||
|
sections: [],
|
||||||
|
serverError: '',
|
||||||
|
hasApplyButton: false,
|
||||||
|
}
|
||||||
result.forms[panelId] = {}
|
result.forms[panelId] = {}
|
||||||
result.validations[panelId] = {}
|
result.validations[panelId] = {}
|
||||||
result.errors[panelId] = {}
|
result.errors[panelId] = {}
|
||||||
|
@ -394,7 +433,7 @@ export function formatYunoHostConfigPanels (data) {
|
||||||
const section = {
|
const section = {
|
||||||
id: _section.id,
|
id: _section.id,
|
||||||
isActionSection: _section.is_action_section,
|
isActionSection: _section.is_action_section,
|
||||||
visible: [undefined, true, '"true"'].includes(_section.visible)
|
visible: [undefined, true, '"true"'].includes(_section.visible),
|
||||||
}
|
}
|
||||||
if (_section.help) section.help = formatI18nField(_section.help)
|
if (_section.help) section.help = formatI18nField(_section.help)
|
||||||
if (_section.name) section.name = formatI18nField(_section.name)
|
if (_section.name) section.name = formatI18nField(_section.name)
|
||||||
|
@ -402,12 +441,10 @@ export function formatYunoHostConfigPanels (data) {
|
||||||
addEvaluationGetter('visible', section, _section.visible, result.forms)
|
addEvaluationGetter('visible', section, _section.visible, result.forms)
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const { form, fields, validations, errors } = formatYunoHostArguments(
|
||||||
form,
|
_section.options,
|
||||||
fields,
|
result.forms,
|
||||||
validations,
|
)
|
||||||
errors
|
|
||||||
} = formatYunoHostArguments(_section.options, result.forms)
|
|
||||||
// Merge all sections forms to the panel to get a unique form
|
// Merge all sections forms to the panel to get a unique form
|
||||||
Object.assign(result.forms[panelId], form)
|
Object.assign(result.forms[panelId], form)
|
||||||
Object.assign(result.validations[panelId], validations)
|
Object.assign(result.validations[panelId], validations)
|
||||||
|
@ -415,7 +452,12 @@ export function formatYunoHostConfigPanels (data) {
|
||||||
section.fields = fields
|
section.fields = fields
|
||||||
panel.sections.push(section)
|
panel.sections.push(section)
|
||||||
|
|
||||||
if (!section.isActionSection && Object.values(fields).some((field) => !NO_VALUE_FIELDS.includes(field.is))) {
|
if (
|
||||||
|
!section.isActionSection &&
|
||||||
|
Object.values(fields).some(
|
||||||
|
(field) => !NO_VALUE_FIELDS.includes(field.is),
|
||||||
|
)
|
||||||
|
) {
|
||||||
panel.hasApplyButton = true
|
panel.hasApplyButton = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -426,7 +468,6 @@ export function formatYunoHostConfigPanels (data) {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse a front-end value to its API equivalent. This function returns a Promise or an
|
* Parse a front-end value to its API equivalent. This function returns a Promise or an
|
||||||
* Object `{ key: Promise }` if `key` is supplied. When parsing a form, all those
|
* Object `{ key: Promise }` if `key` is supplied. When parsing a form, all those
|
||||||
|
@ -439,11 +480,11 @@ export function formatYunoHostConfigPanels (data) {
|
||||||
* @param {*} value
|
* @param {*} value
|
||||||
* @return {*}
|
* @return {*}
|
||||||
*/
|
*/
|
||||||
export function formatFormDataValue (value, key = null) {
|
export function formatFormDataValue(value, key = null) {
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
return Promise.all(
|
return Promise.all(value.map((value_) => formatFormDataValue(value_))).then(
|
||||||
value.map(value_ => formatFormDataValue(value_))
|
(resolvedValues) => ({ [key]: resolvedValues }),
|
||||||
).then(resolvedValues => ({ [key]: resolvedValues }))
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
let result = value
|
let result = value
|
||||||
|
@ -454,10 +495,10 @@ export function formatFormDataValue (value, key = null) {
|
||||||
// File has not changed (will not be sent)
|
// File has not changed (will not be sent)
|
||||||
else if (value.current || value.file === null) result = null
|
else if (value.current || value.file === null) result = null
|
||||||
else {
|
else {
|
||||||
return getFileContent(value.file, { base64: true }).then(content => {
|
return getFileContent(value.file, { base64: true }).then((content) => {
|
||||||
return {
|
return {
|
||||||
[key]: content.replace(/data:[^;]*;base64,/, ''),
|
[key]: content.replace(/data:[^;]*;base64,/, ''),
|
||||||
[key + '[name]']: value.file.name
|
[key + '[name]']: value.file.name,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -469,7 +510,6 @@ export function formatFormDataValue (value, key = null) {
|
||||||
return Promise.resolve(key ? { [key]: result } : result)
|
return Promise.resolve(key ? { [key]: result } : result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convinient helper to properly parse a front-end form to its API equivalent.
|
* Convinient helper to properly parse a front-end form to its API equivalent.
|
||||||
* This parse each values asynchronously, allow to inject keys into the final form and
|
* This parse each values asynchronously, allow to inject keys into the final form and
|
||||||
|
@ -478,17 +518,16 @@ export function formatFormDataValue (value, key = null) {
|
||||||
* @param {Object} formData
|
* @param {Object} formData
|
||||||
* @return {Object}
|
* @return {Object}
|
||||||
*/
|
*/
|
||||||
function formatFormDataValues (formData) {
|
function formatFormDataValues(formData) {
|
||||||
const promisedValues = Object.entries(formData).map(([key, value]) => {
|
const promisedValues = Object.entries(formData).map(([key, value]) => {
|
||||||
return formatFormDataValue(value, key)
|
return formatFormDataValue(value, key)
|
||||||
})
|
})
|
||||||
|
|
||||||
return Promise.all(promisedValues).then(resolvedValues => {
|
return Promise.all(promisedValues).then((resolvedValues) => {
|
||||||
return resolvedValues.reduce((form, obj) => ({ ...form, ...obj }), {})
|
return resolvedValues.reduce((form, obj) => ({ ...form, ...obj }), {})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format a form produced by a vue view to be sent to the server.
|
* Format a form produced by a vue view to be sent to the server.
|
||||||
*
|
*
|
||||||
|
@ -499,13 +538,18 @@ function formatFormDataValues (formData) {
|
||||||
* @param {Boolean} [extraParams.removeEmpty=true] - Removes "empty" values from the object.
|
* @param {Boolean} [extraParams.removeEmpty=true] - Removes "empty" values from the object.
|
||||||
* @return {Object} the parsed data to be sent to the server, with extracted values if specified.
|
* @return {Object} the parsed data to be sent to the server, with extracted values if specified.
|
||||||
*/
|
*/
|
||||||
export async function formatFormData (
|
export async function formatFormData(
|
||||||
formData,
|
formData,
|
||||||
{ extract = null, flatten = false, removeEmpty = true, removeNull = false } = {}
|
{
|
||||||
|
extract = null,
|
||||||
|
flatten = false,
|
||||||
|
removeEmpty = true,
|
||||||
|
removeNull = false,
|
||||||
|
} = {},
|
||||||
) {
|
) {
|
||||||
const output = {
|
const output = {
|
||||||
data: {},
|
data: {},
|
||||||
extracted: {}
|
extracted: {},
|
||||||
}
|
}
|
||||||
|
|
||||||
const values = await formatFormDataValues(formData)
|
const values = await formatFormDataValues(formData)
|
||||||
|
|
|
@ -11,7 +11,7 @@ const loadedLanguages = []
|
||||||
*
|
*
|
||||||
* @return {string[]}
|
* @return {string[]}
|
||||||
*/
|
*/
|
||||||
function getDefaultLocales () {
|
function getDefaultLocales() {
|
||||||
const locale = store.getters.locale
|
const locale = store.getters.locale
|
||||||
const fallbackLocale = store.getters.fallbackLocale
|
const fallbackLocale = store.getters.fallbackLocale
|
||||||
if (locale && fallbackLocale) return [locale, fallbackLocale]
|
if (locale && fallbackLocale) return [locale, fallbackLocale]
|
||||||
|
@ -34,7 +34,7 @@ function getDefaultLocales () {
|
||||||
return defaultLocales
|
return defaultLocales
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateDocumentLocale (locale) {
|
function updateDocumentLocale(locale) {
|
||||||
document.documentElement.lang = locale
|
document.documentElement.lang = locale
|
||||||
// FIXME can't currently change document direction easily since bootstrap still doesn't handle rtl.
|
// FIXME can't currently change document direction easily since bootstrap still doesn't handle rtl.
|
||||||
// document.dir = locale === 'ar' ? 'rtl' : 'ltr'
|
// document.dir = locale === 'ar' ? 'rtl' : 'ltr'
|
||||||
|
@ -45,11 +45,11 @@ function updateDocumentLocale (locale) {
|
||||||
*
|
*
|
||||||
* @return {Promise<string>} Promise that resolve the given locale string
|
* @return {Promise<string>} Promise that resolve the given locale string
|
||||||
*/
|
*/
|
||||||
function loadLocaleMessages (locale) {
|
function loadLocaleMessages(locale) {
|
||||||
if (loadedLanguages.includes(locale)) {
|
if (loadedLanguages.includes(locale)) {
|
||||||
return Promise.resolve(locale)
|
return Promise.resolve(locale)
|
||||||
}
|
}
|
||||||
return import(`@/i18n/locales/${locale}.json`).then(messages => {
|
return import(`@/i18n/locales/${locale}.json`).then((messages) => {
|
||||||
i18n.setLocaleMessage(locale, messages.default)
|
i18n.setLocaleMessage(locale, messages.default)
|
||||||
loadedLanguages.push(locale)
|
loadedLanguages.push(locale)
|
||||||
return locale
|
return locale
|
||||||
|
@ -59,17 +59,19 @@ function loadLocaleMessages (locale) {
|
||||||
/**
|
/**
|
||||||
* Loads a date-fns locale object
|
* Loads a date-fns locale object
|
||||||
*/
|
*/
|
||||||
async function loadDateFnsLocale (locale) {
|
async function loadDateFnsLocale(locale) {
|
||||||
const dateFnsLocaleName = supportedLocales[locale].dateFnsLocale || locale
|
const dateFnsLocaleName = supportedLocales[locale].dateFnsLocale || locale
|
||||||
dateFnsLocale = (await import(
|
dateFnsLocale = (
|
||||||
`../../node_modules/date-fns/esm/locale/${dateFnsLocaleName}/index.js`
|
await import(
|
||||||
)).default
|
`../../node_modules/date-fns/esm/locale/${dateFnsLocaleName}/index.js`
|
||||||
|
)
|
||||||
|
).default
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize all locales
|
* Initialize all locales
|
||||||
*/
|
*/
|
||||||
function initDefaultLocales () {
|
function initDefaultLocales() {
|
||||||
// Get defined locales from `localStorage` or `navigator`
|
// Get defined locales from `localStorage` or `navigator`
|
||||||
const [locale, fallbackLocale] = getDefaultLocales()
|
const [locale, fallbackLocale] = getDefaultLocales()
|
||||||
|
|
||||||
|
@ -83,5 +85,5 @@ export {
|
||||||
updateDocumentLocale,
|
updateDocumentLocale,
|
||||||
loadLocaleMessages,
|
loadLocaleMessages,
|
||||||
loadDateFnsLocale,
|
loadDateFnsLocale,
|
||||||
dateFnsLocale
|
dateFnsLocale,
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,135 +6,135 @@
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
ar: {
|
ar: {
|
||||||
name: 'عربي'
|
name: 'عربي',
|
||||||
},
|
},
|
||||||
bn_BD: {
|
bn_BD: {
|
||||||
name: 'বাংলা',
|
name: 'বাংলা',
|
||||||
dateFnsLocale: 'bn'
|
dateFnsLocale: 'bn',
|
||||||
},
|
},
|
||||||
br: {
|
br: {
|
||||||
name: 'Brezhoneg',
|
name: 'Brezhoneg',
|
||||||
dateFnsLocale: 'fr'
|
dateFnsLocale: 'fr',
|
||||||
},
|
},
|
||||||
ca: {
|
ca: {
|
||||||
name: 'Català'
|
name: 'Català',
|
||||||
},
|
},
|
||||||
ckb: {
|
ckb: {
|
||||||
name: 'کوردی',
|
name: 'کوردی',
|
||||||
dateFnsLocale: 'fa-IR'
|
dateFnsLocale: 'fa-IR',
|
||||||
// FIXME fallback to Farsi (`fa-IR`) is arbitrary, some would probably prefer Arabic (`ar`)...
|
// FIXME fallback to Farsi (`fa-IR`) is arbitrary, some would probably prefer Arabic (`ar`)...
|
||||||
},
|
},
|
||||||
cs: {
|
cs: {
|
||||||
name: 'Čeština'
|
name: 'Čeština',
|
||||||
},
|
},
|
||||||
da: {
|
da: {
|
||||||
name: 'Dansk'
|
name: 'Dansk',
|
||||||
},
|
},
|
||||||
de: {
|
de: {
|
||||||
name: 'Deutsch'
|
name: 'Deutsch',
|
||||||
},
|
},
|
||||||
el: {
|
el: {
|
||||||
name: 'Eλληνικά'
|
name: 'Eλληνικά',
|
||||||
},
|
},
|
||||||
en: {
|
en: {
|
||||||
name: 'English',
|
name: 'English',
|
||||||
dateFnsLocale: 'en-GB'
|
dateFnsLocale: 'en-GB',
|
||||||
},
|
},
|
||||||
eo: {
|
eo: {
|
||||||
name: 'Esperanto'
|
name: 'Esperanto',
|
||||||
},
|
},
|
||||||
es: {
|
es: {
|
||||||
name: 'Español'
|
name: 'Español',
|
||||||
},
|
},
|
||||||
eu: {
|
eu: {
|
||||||
name: 'Euskara'
|
name: 'Euskara',
|
||||||
},
|
},
|
||||||
fa: {
|
fa: {
|
||||||
name: 'فارسی',
|
name: 'فارسی',
|
||||||
dateFnsLocale: 'fa-IR'
|
dateFnsLocale: 'fa-IR',
|
||||||
},
|
},
|
||||||
fi: {
|
fi: {
|
||||||
name: 'Suomi'
|
name: 'Suomi',
|
||||||
},
|
},
|
||||||
fr: {
|
fr: {
|
||||||
name: 'Français'
|
name: 'Français',
|
||||||
},
|
},
|
||||||
gl: {
|
gl: {
|
||||||
name: 'Galego'
|
name: 'Galego',
|
||||||
},
|
},
|
||||||
he: {
|
he: {
|
||||||
name: 'עברית'
|
name: 'עברית',
|
||||||
},
|
},
|
||||||
hi: {
|
hi: {
|
||||||
name: 'हिन्दी'
|
name: 'हिन्दी',
|
||||||
},
|
},
|
||||||
hu: {
|
hu: {
|
||||||
name: 'Magyar'
|
name: 'Magyar',
|
||||||
},
|
},
|
||||||
id: {
|
id: {
|
||||||
name: 'Bahasa Indonesia'
|
name: 'Bahasa Indonesia',
|
||||||
},
|
},
|
||||||
it: {
|
it: {
|
||||||
name: 'Italiano'
|
name: 'Italiano',
|
||||||
},
|
},
|
||||||
kab: {
|
kab: {
|
||||||
name: 'Taqbaylit',
|
name: 'Taqbaylit',
|
||||||
dateFnsLocale: 'ar-DZ'
|
dateFnsLocale: 'ar-DZ',
|
||||||
},
|
},
|
||||||
lt: {
|
lt: {
|
||||||
name: 'Lietuvių'
|
name: 'Lietuvių',
|
||||||
},
|
},
|
||||||
mk: {
|
mk: {
|
||||||
name: 'македонски'
|
name: 'македонски',
|
||||||
},
|
},
|
||||||
nb_NO: {
|
nb_NO: {
|
||||||
name: 'Norsk bokmål',
|
name: 'Norsk bokmål',
|
||||||
dateFnsLocale: 'nb'
|
dateFnsLocale: 'nb',
|
||||||
},
|
},
|
||||||
ne: {
|
ne: {
|
||||||
name: 'नेपाली',
|
name: 'नेपाली',
|
||||||
dateFnsLocale: 'en-GB'
|
dateFnsLocale: 'en-GB',
|
||||||
},
|
},
|
||||||
nl: {
|
nl: {
|
||||||
name: 'Nederlands'
|
name: 'Nederlands',
|
||||||
},
|
},
|
||||||
oc: {
|
oc: {
|
||||||
name: 'Occitan',
|
name: 'Occitan',
|
||||||
dateFnsLocale: 'ca'
|
dateFnsLocale: 'ca',
|
||||||
},
|
},
|
||||||
pl: {
|
pl: {
|
||||||
name: 'Polski'
|
name: 'Polski',
|
||||||
},
|
},
|
||||||
pt: {
|
pt: {
|
||||||
name: 'Português'
|
name: 'Português',
|
||||||
},
|
},
|
||||||
pt_BR: {
|
pt_BR: {
|
||||||
name: 'Português brasileiro',
|
name: 'Português brasileiro',
|
||||||
dateFnsLocale: 'pt-BR'
|
dateFnsLocale: 'pt-BR',
|
||||||
},
|
},
|
||||||
ru: {
|
ru: {
|
||||||
name: 'Русский'
|
name: 'Русский',
|
||||||
},
|
},
|
||||||
sk: {
|
sk: {
|
||||||
name: 'Slovak'
|
name: 'Slovak',
|
||||||
},
|
},
|
||||||
sl: {
|
sl: {
|
||||||
name: 'Slovenščina'
|
name: 'Slovenščina',
|
||||||
},
|
},
|
||||||
sv: {
|
sv: {
|
||||||
name: 'Svenska'
|
name: 'Svenska',
|
||||||
},
|
},
|
||||||
te: {
|
te: {
|
||||||
name: 'Telugu'
|
name: 'Telugu',
|
||||||
},
|
},
|
||||||
tr: {
|
tr: {
|
||||||
name: 'Türkçe'
|
name: 'Türkçe',
|
||||||
},
|
},
|
||||||
uk: {
|
uk: {
|
||||||
name: 'Українська'
|
name: 'Українська',
|
||||||
},
|
},
|
||||||
zh_Hans: {
|
zh_Hans: {
|
||||||
name: '简化字',
|
name: '简化字',
|
||||||
dateFnsLocale: 'zh-CN'
|
dateFnsLocale: 'zh-CN',
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,20 +10,19 @@ import i18n from './i18n'
|
||||||
import { registerGlobalErrorHandlers } from './api'
|
import { registerGlobalErrorHandlers } from './api'
|
||||||
import { initDefaultLocales } from './i18n/helpers'
|
import { initDefaultLocales } from './i18n/helpers'
|
||||||
|
|
||||||
|
|
||||||
Vue.config.productionTip = false
|
Vue.config.productionTip = false
|
||||||
|
|
||||||
// Styles are imported in `src/App.vue` <style>
|
// Styles are imported in `src/App.vue` <style>
|
||||||
Vue.use(BootstrapVue, {
|
Vue.use(BootstrapVue, {
|
||||||
BSkeleton: { animation: 'none' },
|
BSkeleton: { animation: 'none' },
|
||||||
BAlert: { show: true },
|
BAlert: { show: true },
|
||||||
BBadge: { pill: true }
|
BBadge: { pill: true },
|
||||||
})
|
})
|
||||||
|
|
||||||
Vue.use(VueShowdown, {
|
Vue.use(VueShowdown, {
|
||||||
options: {
|
options: {
|
||||||
emoji: true
|
emoji: true,
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
// Ugly wrapper for `$bvModal.msgBoxConfirm` to set default i18n button titles
|
// Ugly wrapper for `$bvModal.msgBoxConfirm` to set default i18n button titles
|
||||||
|
@ -34,14 +33,18 @@ Vue.prototype.$askConfirmation = function (message, props) {
|
||||||
cancelTitle: this.$i18n.t('cancel'),
|
cancelTitle: this.$i18n.t('cancel'),
|
||||||
bodyBgVariant: 'warning',
|
bodyBgVariant: 'warning',
|
||||||
centered: true,
|
centered: true,
|
||||||
bodyClass: ['font-weight-bold', 'rounded-top', store.state.theme ? 'text-white' : 'text-black'],
|
bodyClass: [
|
||||||
...props
|
'font-weight-bold',
|
||||||
|
'rounded-top',
|
||||||
|
store.state.theme ? 'text-white' : 'text-black',
|
||||||
|
],
|
||||||
|
...props,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
Vue.prototype.$askMdConfirmation = function (markdown, props, ok = false) {
|
Vue.prototype.$askMdConfirmation = function (markdown, props, ok = false) {
|
||||||
const content = this.$createElement('vue-showdown', {
|
const content = this.$createElement('vue-showdown', {
|
||||||
props: { markdown, flavor: 'github', options: { headerLevelStart: 4 } }
|
props: { markdown, flavor: 'github', options: { headerLevelStart: 4 } },
|
||||||
})
|
})
|
||||||
return this.$bvModal['msgBox' + (ok ? 'Ok' : 'Confirm')](content, {
|
return this.$bvModal['msgBox' + (ok ? 'Ok' : 'Confirm')](content, {
|
||||||
okTitle: this.$i18n.t('yes'),
|
okTitle: this.$i18n.t('yes'),
|
||||||
|
@ -49,15 +52,15 @@ Vue.prototype.$askMdConfirmation = function (markdown, props, ok = false) {
|
||||||
headerBgVariant: 'warning',
|
headerBgVariant: 'warning',
|
||||||
headerClass: store.state.theme ? 'text-white' : 'text-black',
|
headerClass: store.state.theme ? 'text-white' : 'text-black',
|
||||||
centered: true,
|
centered: true,
|
||||||
...props
|
...props,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register global components
|
// Register global components
|
||||||
const globalComponentsModules = import.meta.glob([
|
const globalComponentsModules = import.meta.glob(
|
||||||
'@/components/globals/*.vue',
|
['@/components/globals/*.vue', '@/components/globals/*/*.vue'],
|
||||||
'@/components/globals/*/*.vue'
|
{ eager: true },
|
||||||
], { eager: true })
|
)
|
||||||
Object.values(globalComponentsModules).forEach((module) => {
|
Object.values(globalComponentsModules).forEach((module) => {
|
||||||
const component = module.default
|
const component = module.default
|
||||||
Vue.component(component.name, component)
|
Vue.component(component.name, component)
|
||||||
|
@ -71,7 +74,7 @@ initDefaultLocales().then(() => {
|
||||||
store,
|
store,
|
||||||
router,
|
router,
|
||||||
i18n,
|
i18n,
|
||||||
render: h => h(App)
|
render: (h) => h(App),
|
||||||
})
|
})
|
||||||
|
|
||||||
app.$mount('#app')
|
app.$mount('#app')
|
||||||
|
|
|
@ -10,7 +10,7 @@ const router = new VueRouter({
|
||||||
base: import.meta.env.BASE_URL,
|
base: import.meta.env.BASE_URL,
|
||||||
routes,
|
routes,
|
||||||
|
|
||||||
scrollBehavior (to, from, savedPosition) {
|
scrollBehavior(to, from, savedPosition) {
|
||||||
// Mimics the native scroll behavior of the browser.
|
// Mimics the native scroll behavior of the browser.
|
||||||
// This allows the user to find his way back to the scroll level of the previous/next route.
|
// This allows the user to find his way back to the scroll level of the previous/next route.
|
||||||
|
|
||||||
|
@ -18,13 +18,13 @@ const router = new VueRouter({
|
||||||
// scroll state because the component probably hasn't updated the window height yet.
|
// scroll state because the component probably hasn't updated the window height yet.
|
||||||
// Note: this will only work with routes that use stored data or that has static content
|
// Note: this will only work with routes that use stored data or that has static content
|
||||||
if (store.getters.transitions && savedPosition) {
|
if (store.getters.transitions && savedPosition) {
|
||||||
return new Promise(resolve => {
|
return new Promise((resolve) => {
|
||||||
setTimeout(() => resolve(savedPosition), 0)
|
setTimeout(() => resolve(savedPosition), 0)
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
return savedPosition || { x: 0, y: 0 }
|
return savedPosition || { x: 0, y: 0 }
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
router.beforeEach((to, from, next) => {
|
router.beforeEach((to, from, next) => {
|
||||||
|
|
|
@ -18,8 +18,8 @@ const routes = [
|
||||||
path: '/',
|
path: '/',
|
||||||
component: HomeView,
|
component: HomeView,
|
||||||
meta: {
|
meta: {
|
||||||
args: { trad: 'home' }
|
args: { trad: 'home' },
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
@ -28,8 +28,8 @@ const routes = [
|
||||||
component: LoginView,
|
component: LoginView,
|
||||||
meta: {
|
meta: {
|
||||||
noAuth: true,
|
noAuth: true,
|
||||||
args: { trad: 'login' }
|
args: { trad: 'login' },
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
/* ───────────────╮
|
/* ───────────────╮
|
||||||
|
@ -41,8 +41,8 @@ const routes = [
|
||||||
component: () => import('@/views/PostInstall.vue'),
|
component: () => import('@/views/PostInstall.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
noAuth: true,
|
noAuth: true,
|
||||||
args: { trad: 'postinstall.title' }
|
args: { trad: 'postinstall.title' },
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
/* ───────╮
|
/* ───────╮
|
||||||
|
@ -54,8 +54,8 @@ const routes = [
|
||||||
component: () => import('@/views/user/UserList.vue'),
|
component: () => import('@/views/user/UserList.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
args: { trad: 'users' },
|
args: { trad: 'users' },
|
||||||
breadcrumb: ['user-list']
|
breadcrumb: ['user-list'],
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'user-create',
|
name: 'user-create',
|
||||||
|
@ -63,8 +63,8 @@ const routes = [
|
||||||
component: () => import('@/views/user/UserCreate.vue'),
|
component: () => import('@/views/user/UserCreate.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
args: { trad: 'users_new' },
|
args: { trad: 'users_new' },
|
||||||
breadcrumb: ['user-list', 'user-create']
|
breadcrumb: ['user-list', 'user-create'],
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'user-import',
|
name: 'user-import',
|
||||||
|
@ -73,8 +73,8 @@ const routes = [
|
||||||
props: true,
|
props: true,
|
||||||
meta: {
|
meta: {
|
||||||
args: { trad: 'users_import' },
|
args: { trad: 'users_import' },
|
||||||
breadcrumb: ['user-list', 'user-import']
|
breadcrumb: ['user-list', 'user-import'],
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'user-info',
|
name: 'user-info',
|
||||||
|
@ -83,8 +83,8 @@ const routes = [
|
||||||
props: true,
|
props: true,
|
||||||
meta: {
|
meta: {
|
||||||
args: { param: 'name' },
|
args: { param: 'name' },
|
||||||
breadcrumb: ['user-list', 'user-info']
|
breadcrumb: ['user-list', 'user-info'],
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'user-edit',
|
name: 'user-edit',
|
||||||
|
@ -93,8 +93,8 @@ const routes = [
|
||||||
props: true,
|
props: true,
|
||||||
meta: {
|
meta: {
|
||||||
args: { param: 'name', trad: 'user_username_edit' },
|
args: { param: 'name', trad: 'user_username_edit' },
|
||||||
breadcrumb: ['user-list', 'user-info', 'user-edit']
|
breadcrumb: ['user-list', 'user-info', 'user-edit'],
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
/* ────────╮
|
/* ────────╮
|
||||||
|
@ -106,8 +106,8 @@ const routes = [
|
||||||
component: () => import('@/views/group/GroupList.vue'),
|
component: () => import('@/views/group/GroupList.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
args: { trad: 'groups_and_permissions' },
|
args: { trad: 'groups_and_permissions' },
|
||||||
breadcrumb: ['user-list', 'group-list']
|
breadcrumb: ['user-list', 'group-list'],
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'group-create',
|
name: 'group-create',
|
||||||
|
@ -115,8 +115,8 @@ const routes = [
|
||||||
component: () => import('@/views/group/GroupCreate.vue'),
|
component: () => import('@/views/group/GroupCreate.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
args: { trad: 'group_new' },
|
args: { trad: 'group_new' },
|
||||||
breadcrumb: ['user-list', 'group-list', 'group-create']
|
breadcrumb: ['user-list', 'group-list', 'group-create'],
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
/* ─────────╮
|
/* ─────────╮
|
||||||
|
@ -128,8 +128,8 @@ const routes = [
|
||||||
component: () => import('@/views/domain/DomainList.vue'),
|
component: () => import('@/views/domain/DomainList.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
args: { trad: 'domains' },
|
args: { trad: 'domains' },
|
||||||
breadcrumb: ['domain-list']
|
breadcrumb: ['domain-list'],
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'domain-add',
|
name: 'domain-add',
|
||||||
|
@ -137,8 +137,8 @@ const routes = [
|
||||||
component: () => import('@/views/domain/DomainAdd.vue'),
|
component: () => import('@/views/domain/DomainAdd.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
args: { trad: 'domain_add' },
|
args: { trad: 'domain_add' },
|
||||||
breadcrumb: ['domain-list', 'domain-add']
|
breadcrumb: ['domain-list', 'domain-add'],
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/domains/:name',
|
path: '/domains/:name',
|
||||||
|
@ -153,10 +153,10 @@ const routes = [
|
||||||
meta: {
|
meta: {
|
||||||
routerParams: ['name'], // Override router key params to avoid view recreation at tab change.
|
routerParams: ['name'], // Override router key params to avoid view recreation at tab change.
|
||||||
args: { param: 'name' },
|
args: { param: 'name' },
|
||||||
breadcrumb: ['domain-list', 'domain-info']
|
breadcrumb: ['domain-list', 'domain-info'],
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
/* ───────╮
|
/* ───────╮
|
||||||
|
@ -168,18 +168,18 @@ const routes = [
|
||||||
component: () => import('@/views/app/AppList.vue'),
|
component: () => import('@/views/app/AppList.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
args: { trad: 'applications' },
|
args: { trad: 'applications' },
|
||||||
breadcrumb: ['app-list']
|
breadcrumb: ['app-list'],
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'app-catalog',
|
name: 'app-catalog',
|
||||||
path: '/apps/catalog',
|
path: '/apps/catalog',
|
||||||
component: () => import('@/views/app/AppCatalog.vue'),
|
component: () => import('@/views/app/AppCatalog.vue'),
|
||||||
props: route => route.query,
|
props: (route) => route.query,
|
||||||
meta: {
|
meta: {
|
||||||
args: { trad: 'catalog' },
|
args: { trad: 'catalog' },
|
||||||
breadcrumb: ['app-list', 'app-catalog']
|
breadcrumb: ['app-list', 'app-catalog'],
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'app-install',
|
name: 'app-install',
|
||||||
|
@ -188,8 +188,8 @@ const routes = [
|
||||||
props: true,
|
props: true,
|
||||||
meta: {
|
meta: {
|
||||||
args: { trad: 'install_name', param: 'id' },
|
args: { trad: 'install_name', param: 'id' },
|
||||||
breadcrumb: ['app-list', 'app-catalog', 'app-install']
|
breadcrumb: ['app-list', 'app-catalog', 'app-install'],
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'app-install-custom',
|
name: 'app-install-custom',
|
||||||
|
@ -198,8 +198,8 @@ const routes = [
|
||||||
props: true,
|
props: true,
|
||||||
meta: {
|
meta: {
|
||||||
args: { trad: 'install_name', param: 'id' },
|
args: { trad: 'install_name', param: 'id' },
|
||||||
breadcrumb: ['app-list', 'app-catalog', 'app-install-custom']
|
breadcrumb: ['app-list', 'app-catalog', 'app-install-custom'],
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/apps/:id',
|
path: '/apps/:id',
|
||||||
|
@ -214,10 +214,10 @@ const routes = [
|
||||||
meta: {
|
meta: {
|
||||||
routerParams: ['id'], // Override router key params to avoid view recreation at tab change.
|
routerParams: ['id'], // Override router key params to avoid view recreation at tab change.
|
||||||
args: { param: 'id' },
|
args: { param: 'id' },
|
||||||
breadcrumb: ['app-list', 'app-info']
|
breadcrumb: ['app-list', 'app-info'],
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
/* ────────────────╮
|
/* ────────────────╮
|
||||||
|
@ -229,8 +229,8 @@ const routes = [
|
||||||
component: () => import('@/views/update/SystemUpdate.vue'),
|
component: () => import('@/views/update/SystemUpdate.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
args: { trad: 'system_update' },
|
args: { trad: 'system_update' },
|
||||||
breadcrumb: ['update']
|
breadcrumb: ['update'],
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
/* ──────────╮
|
/* ──────────╮
|
||||||
|
@ -242,8 +242,8 @@ const routes = [
|
||||||
component: () => import('@/views/service/ServiceList.vue'),
|
component: () => import('@/views/service/ServiceList.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
args: { trad: 'services' },
|
args: { trad: 'services' },
|
||||||
breadcrumb: ['tool-list', 'service-list']
|
breadcrumb: ['tool-list', 'service-list'],
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'service-info',
|
name: 'service-info',
|
||||||
|
@ -252,8 +252,8 @@ const routes = [
|
||||||
props: true,
|
props: true,
|
||||||
meta: {
|
meta: {
|
||||||
args: { param: 'name' },
|
args: { param: 'name' },
|
||||||
breadcrumb: ['tool-list', 'service-list', 'service-info']
|
breadcrumb: ['tool-list', 'service-list', 'service-info'],
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
/* ────────╮
|
/* ────────╮
|
||||||
|
@ -265,8 +265,8 @@ const routes = [
|
||||||
component: ToolList,
|
component: ToolList,
|
||||||
meta: {
|
meta: {
|
||||||
args: { trad: 'tools' },
|
args: { trad: 'tools' },
|
||||||
breadcrumb: ['tool-list']
|
breadcrumb: ['tool-list'],
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'tool-logs',
|
name: 'tool-logs',
|
||||||
|
@ -274,8 +274,8 @@ const routes = [
|
||||||
component: () => import('@/views/tool/ToolLogs.vue'),
|
component: () => import('@/views/tool/ToolLogs.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
args: { trad: 'logs' },
|
args: { trad: 'logs' },
|
||||||
breadcrumb: ['tool-list', 'tool-logs']
|
breadcrumb: ['tool-list', 'tool-logs'],
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'tool-log',
|
name: 'tool-log',
|
||||||
|
@ -284,8 +284,8 @@ const routes = [
|
||||||
props: true,
|
props: true,
|
||||||
meta: {
|
meta: {
|
||||||
args: { param: 'name' },
|
args: { param: 'name' },
|
||||||
breadcrumb: ['tool-list', 'tool-logs', 'tool-log']
|
breadcrumb: ['tool-list', 'tool-logs', 'tool-log'],
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'tool-migrations',
|
name: 'tool-migrations',
|
||||||
|
@ -293,8 +293,8 @@ const routes = [
|
||||||
component: () => import('@/views/tool/ToolMigrations.vue'),
|
component: () => import('@/views/tool/ToolMigrations.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
args: { trad: 'migrations' },
|
args: { trad: 'migrations' },
|
||||||
breadcrumb: ['tool-list', 'tool-migrations']
|
breadcrumb: ['tool-list', 'tool-migrations'],
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'tool-firewall',
|
name: 'tool-firewall',
|
||||||
|
@ -302,8 +302,8 @@ const routes = [
|
||||||
component: () => import('@/views/tool/ToolFirewall.vue'),
|
component: () => import('@/views/tool/ToolFirewall.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
args: { trad: 'firewall' },
|
args: { trad: 'firewall' },
|
||||||
breadcrumb: ['tool-list', 'tool-firewall']
|
breadcrumb: ['tool-list', 'tool-firewall'],
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'tool-webadmin',
|
name: 'tool-webadmin',
|
||||||
|
@ -311,8 +311,8 @@ const routes = [
|
||||||
component: () => import('@/views/tool/ToolWebadmin.vue'),
|
component: () => import('@/views/tool/ToolWebadmin.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
args: { trad: 'tools_webadmin_settings' },
|
args: { trad: 'tools_webadmin_settings' },
|
||||||
breadcrumb: ['tool-list', 'tool-webadmin']
|
breadcrumb: ['tool-list', 'tool-webadmin'],
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/tools/settings',
|
path: '/tools/settings',
|
||||||
|
@ -326,10 +326,10 @@ const routes = [
|
||||||
meta: {
|
meta: {
|
||||||
routerParams: [],
|
routerParams: [],
|
||||||
args: { trad: 'tools_yunohost_settings' },
|
args: { trad: 'tools_yunohost_settings' },
|
||||||
breadcrumb: ['tool-list', 'tool-settings']
|
breadcrumb: ['tool-list', 'tool-settings'],
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'tool-power',
|
name: 'tool-power',
|
||||||
|
@ -337,8 +337,8 @@ const routes = [
|
||||||
component: () => import('@/views/tool/ToolPower.vue'),
|
component: () => import('@/views/tool/ToolPower.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
args: { trad: 'tools_shutdown_reboot' },
|
args: { trad: 'tools_shutdown_reboot' },
|
||||||
breadcrumb: ['tool-list', 'tool-power']
|
breadcrumb: ['tool-list', 'tool-power'],
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
/* ────────────╮
|
/* ────────────╮
|
||||||
|
@ -350,8 +350,8 @@ const routes = [
|
||||||
component: () => import('@/views/diagnosis/DiagnosisView.vue'),
|
component: () => import('@/views/diagnosis/DiagnosisView.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
args: { trad: 'diagnosis' },
|
args: { trad: 'diagnosis' },
|
||||||
breadcrumb: ['diagnosis']
|
breadcrumb: ['diagnosis'],
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
/* ─────────╮
|
/* ─────────╮
|
||||||
|
@ -363,8 +363,8 @@ const routes = [
|
||||||
component: () => import('@/views/backup/BackupView.vue'),
|
component: () => import('@/views/backup/BackupView.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
args: { trad: 'backup' },
|
args: { trad: 'backup' },
|
||||||
breadcrumb: ['backup']
|
breadcrumb: ['backup'],
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'backup-list',
|
name: 'backup-list',
|
||||||
|
@ -373,8 +373,8 @@ const routes = [
|
||||||
props: true,
|
props: true,
|
||||||
meta: {
|
meta: {
|
||||||
args: { param: 'id' },
|
args: { param: 'id' },
|
||||||
breadcrumb: ['backup', 'backup-list']
|
breadcrumb: ['backup', 'backup-list'],
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'backup-info',
|
name: 'backup-info',
|
||||||
|
@ -383,8 +383,8 @@ const routes = [
|
||||||
props: true,
|
props: true,
|
||||||
meta: {
|
meta: {
|
||||||
args: { param: 'name' },
|
args: { param: 'name' },
|
||||||
breadcrumb: ['backup', 'backup-list', 'backup-info']
|
breadcrumb: ['backup', 'backup-list', 'backup-info'],
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'backup-create',
|
name: 'backup-create',
|
||||||
|
@ -393,9 +393,9 @@ const routes = [
|
||||||
props: true,
|
props: true,
|
||||||
meta: {
|
meta: {
|
||||||
args: { trad: 'backup_create' },
|
args: { trad: 'backup_create' },
|
||||||
breadcrumb: ['backup', 'backup-list', 'backup-create']
|
breadcrumb: ['backup', 'backup-list', 'backup-create'],
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
export default routes
|
export default routes
|
||||||
|
|
|
@ -34,11 +34,13 @@
|
||||||
0: h,
|
0: h,
|
||||||
1: s,
|
1: s,
|
||||||
2: l,
|
2: l,
|
||||||
3: a
|
3: a,
|
||||||
);
|
);
|
||||||
// find end of part
|
// find end of part
|
||||||
$end: str-index($color, ',');
|
$end: str-index($color, ',');
|
||||||
@while ($end and not str-is-between(str-slice($color, 0, $end - 1), '(', ')')) {
|
@while (
|
||||||
|
$end and not str-is-between(str-slice($color, 0, $end - 1), '(', ')')
|
||||||
|
) {
|
||||||
$newEnd: str-index(str-slice($color, $end + 1), ',');
|
$newEnd: str-index(str-slice($color, $end + 1), ',');
|
||||||
@if (not $newEnd) {
|
@if (not $newEnd) {
|
||||||
$newEnd: 0;
|
$newEnd: 0;
|
||||||
|
@ -49,7 +51,7 @@
|
||||||
$part: str-slice($color, 0, $end - 1);
|
$part: str-slice($color, 0, $end - 1);
|
||||||
$value: map-merge(
|
$value: map-merge(
|
||||||
(
|
(
|
||||||
map-get($indices, $index): $part
|
map-get($indices, $index): $part,
|
||||||
),
|
),
|
||||||
recursive-color(str-slice($color, $end + 1), $index + 1)
|
recursive-color(str-slice($color, $end + 1), $index + 1)
|
||||||
);
|
);
|
||||||
|
@ -220,15 +222,14 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Taken from https://gist.github.com/johanlef/518a511b2b2f6b96c4f429b3af2f169a?permalink_comment_id=4053335#gistcomment-4053335
|
// Taken from https://gist.github.com/johanlef/518a511b2b2f6b96c4f429b3af2f169a?permalink_comment_id=4053335#gistcomment-4053335
|
||||||
@function theme-color-level($color-name: "primary", $level: 0) {
|
@function theme-color-level($color-name: 'primary', $level: 0) {
|
||||||
$color: theme-color($color-name);
|
$color: theme-color($color-name);
|
||||||
@if ($level == 0) {
|
@if ($level == 0) {
|
||||||
@return $color;
|
@return $color;
|
||||||
}
|
}
|
||||||
|
|
||||||
$amount: math.div($theme-color-interval * abs($level) , 100%);
|
$amount: math.div($theme-color-interval * abs($level), 100%);
|
||||||
$c: to-hsl($color);
|
$c: to-hsl($color);
|
||||||
$h: map-get($c, h);
|
$h: map-get($c, h);
|
||||||
$s: map-get($c, s);
|
$s: map-get($c, s);
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
|
|
||||||
// ╭─────────────────────────────────────────────────────────────────╮
|
// ╭─────────────────────────────────────────────────────────────────╮
|
||||||
// │ │
|
// │ │
|
||||||
// │ /!\ DO NOT IMPORT OR DEFINE ACTUAL RULES INTO THIS FILE /!\ │
|
// │ /!\ DO NOT IMPORT OR DEFINE ACTUAL RULES INTO THIS FILE /!\ │
|
||||||
|
@ -12,7 +11,6 @@
|
||||||
// But if some rules are defined here, they will be copied into the final build as many
|
// But if some rules are defined here, they will be copied into the final build as many
|
||||||
// times as there are components…
|
// times as there are components…
|
||||||
|
|
||||||
|
|
||||||
// ╭─────────────────────────────╮
|
// ╭─────────────────────────────╮
|
||||||
// │ ┌─╮╭─╮╭─╮╶┬╴╭─╴╶┬╴┌─╮╭─┐┌─╮ │
|
// │ ┌─╮╭─╮╭─╮╶┬╴╭─╴╶┬╴┌─╮╭─┐┌─╮ │
|
||||||
// │ │╶┤│ ││ │ │ ╰─╮ │ ├┬╯├─┤├─╯ │
|
// │ │╶┤│ ││ │ │ ╰─╮ │ ├┬╯├─┤├─╯ │
|
||||||
|
@ -27,10 +25,10 @@
|
||||||
// For exemple, turning rounding of elements off, the bases colors, etc.
|
// For exemple, turning rounding of elements off, the bases colors, etc.
|
||||||
// $enable-rounded: false;
|
// $enable-rounded: false;
|
||||||
|
|
||||||
$font-size-base: .9rem;
|
$font-size-base: 0.9rem;
|
||||||
$font-weight-bold: 500;
|
$font-weight-bold: 500;
|
||||||
|
|
||||||
$white: var(--white);
|
$white: var(--white);
|
||||||
$gray-100: var(--gray-100);
|
$gray-100: var(--gray-100);
|
||||||
$gray-200: var(--gray-200);
|
$gray-200: var(--gray-200);
|
||||||
$gray-300: var(--gray-300);
|
$gray-300: var(--gray-300);
|
||||||
|
@ -40,18 +38,18 @@ $gray-600: var(--gray-600);
|
||||||
$gray-700: var(--gray-700);
|
$gray-700: var(--gray-700);
|
||||||
$gray-800: var(--gray-800);
|
$gray-800: var(--gray-800);
|
||||||
$gray-900: var(--gray-900);
|
$gray-900: var(--gray-900);
|
||||||
$black: var(--black);
|
$black: var(--black);
|
||||||
|
|
||||||
$blue: var(--blue);
|
$blue: var(--blue);
|
||||||
$indigo: var(--indigo);
|
$indigo: var(--indigo);
|
||||||
$purple: var(--purple);
|
$purple: var(--purple);
|
||||||
$pink: var(--pink);
|
$pink: var(--pink);
|
||||||
$red: var(--red);
|
$red: var(--red);
|
||||||
$orange: var(--orange);
|
$orange: var(--orange);
|
||||||
$yellow: var(--yellow);
|
$yellow: var(--yellow);
|
||||||
$green: var(--green);
|
$green: var(--green);
|
||||||
$teal: var(--teal);
|
$teal: var(--teal);
|
||||||
$cyan: var(--cyan);
|
$cyan: var(--cyan);
|
||||||
|
|
||||||
$theme-colors: (
|
$theme-colors: (
|
||||||
'best': $purple,
|
'best': $purple,
|
||||||
|
@ -59,12 +57,12 @@ $theme-colors: (
|
||||||
|
|
||||||
$yiq-contrasted-threshold: var(--yiq-contrasted-threshold);
|
$yiq-contrasted-threshold: var(--yiq-contrasted-threshold);
|
||||||
|
|
||||||
$alert-bg-level: -10;
|
$alert-bg-level: -10;
|
||||||
$alert-border-level: -9;
|
$alert-border-level: -9;
|
||||||
$alert-color-level: 5;
|
$alert-color-level: 5;
|
||||||
|
|
||||||
$list-group-item-bg-level: -11;
|
$list-group-item-bg-level: -11;
|
||||||
$list-group-item-color-level: 6;
|
$list-group-item-color-level: 6;
|
||||||
|
|
||||||
$code-color: $black;
|
$code-color: $black;
|
||||||
|
|
||||||
|
@ -77,8 +75,23 @@ $display4-weight: 200;
|
||||||
$lead-font-weight: 200;
|
$lead-font-weight: 200;
|
||||||
|
|
||||||
// Set fonts
|
// Set fonts
|
||||||
$font-family-sans-serif: 'FiraGO', 'Fira Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji' !default;
|
$font-family-sans-serif:
|
||||||
$font-family-monospace: 'Fira Code', SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace !default;
|
'FiraGO',
|
||||||
|
'Fira Sans',
|
||||||
|
-apple-system,
|
||||||
|
BlinkMacSystemFont,
|
||||||
|
'Segoe UI',
|
||||||
|
Roboto,
|
||||||
|
'Helvetica Neue',
|
||||||
|
Arial,
|
||||||
|
'Noto Sans',
|
||||||
|
sans-serif,
|
||||||
|
'Apple Color Emoji',
|
||||||
|
'Segoe UI Emoji',
|
||||||
|
'Segoe UI Symbol',
|
||||||
|
'Noto Color Emoji' !default;
|
||||||
|
$font-family-monospace: 'Fira Code', SFMono-Regular, Menlo, Monaco, Consolas,
|
||||||
|
'Liberation Mono', 'Courier New', monospace !default;
|
||||||
|
|
||||||
$h2-font-size: $font-size-base * 1.5;
|
$h2-font-size: $font-size-base * 1.5;
|
||||||
$h3-font-size: $font-size-base * 1.4;
|
$h3-font-size: $font-size-base * 1.4;
|
||||||
|
@ -87,7 +100,7 @@ $h5-font-size: $font-size-base * 1.1;
|
||||||
|
|
||||||
$alert-padding-x: 1rem;
|
$alert-padding-x: 1rem;
|
||||||
|
|
||||||
$card-spacer-y: .6rem;
|
$card-spacer-y: 0.6rem;
|
||||||
$card-spacer-x: 1rem;
|
$card-spacer-x: 1rem;
|
||||||
|
|
||||||
$list-group-item-padding-x: 1rem;
|
$list-group-item-padding-x: 1rem;
|
||||||
|
@ -105,7 +118,9 @@ $b-toast-background-opacity: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
$custom-checkbox-indicator-icon-checked: get-checkbox-icon-checked('%23fff');
|
$custom-checkbox-indicator-icon-checked: get-checkbox-icon-checked('%23fff');
|
||||||
$custom-checkbox-indicator-icon-indeterminate: get-checkbox-icon-indeterminate('%23fff');
|
$custom-checkbox-indicator-icon-indeterminate: get-checkbox-icon-indeterminate(
|
||||||
|
'%23fff'
|
||||||
|
);
|
||||||
|
|
||||||
// Import default variables after the above setup to compute all other variables.
|
// Import default variables after the above setup to compute all other variables.
|
||||||
@import '~bootstrap/scss/functions.scss';
|
@import '~bootstrap/scss/functions.scss';
|
||||||
|
@ -114,7 +129,6 @@ $custom-checkbox-indicator-icon-indeterminate: get-checkbox-icon-indeterminate('
|
||||||
@import '~bootstrap/scss/mixins.scss';
|
@import '~bootstrap/scss/mixins.scss';
|
||||||
@import '~bootstrap-vue/src/variables';
|
@import '~bootstrap-vue/src/variables';
|
||||||
|
|
||||||
|
|
||||||
$hr-border-color: $gray-200;
|
$hr-border-color: $gray-200;
|
||||||
|
|
||||||
$list-group-action-color: $gray-800;
|
$list-group-action-color: $gray-800;
|
||||||
|
@ -133,7 +147,6 @@ $fa-font-size-base: $font-size-base;
|
||||||
|
|
||||||
@import '~fork-awesome/scss/variables';
|
@import '~fork-awesome/scss/variables';
|
||||||
|
|
||||||
|
|
||||||
// ╭────────────────────╮
|
// ╭────────────────────╮
|
||||||
// │ ╭─╴╷ ╷╭─╴╶┬╴╭─╮╭╮╮ │
|
// │ ╭─╴╷ ╷╭─╴╶┬╴╭─╮╭╮╮ │
|
||||||
// │ │ │ │╰─╮ │ │ ││││ │
|
// │ │ │ │╰─╮ │ │ ││││ │
|
||||||
|
@ -142,6 +155,6 @@ $fa-font-size-base: $font-size-base;
|
||||||
|
|
||||||
$thin-border: $hr-border-width solid $hr-border-color;
|
$thin-border: $hr-border-width solid $hr-border-color;
|
||||||
|
|
||||||
$btn-padding-y-xs: .25rem;
|
$btn-padding-y-xs: 0.25rem;
|
||||||
$btn-padding-x-xs: .35rem;
|
$btn-padding-x-xs: 0.35rem;
|
||||||
$btn-line-height-xs: 1.5;
|
$btn-line-height-xs: 1.5;
|
||||||
|
|
|
@ -108,7 +108,8 @@
|
||||||
src:
|
src:
|
||||||
local('Fira Code Regular'),
|
local('Fira Code Regular'),
|
||||||
// url('~@fontsource/fira-code/files/fira-code-all-400-normal.woff2') format('woff2'),
|
// url('~@fontsource/fira-code/files/fira-code-all-400-normal.woff2') format('woff2'),
|
||||||
url('~@fontsource/fira-code/files/fira-code-all-400-normal.woff') format('woff');
|
url('~@fontsource/fira-code/files/fira-code-all-400-normal.woff')
|
||||||
|
format('woff');
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,42 +5,41 @@
|
||||||
// Variables overrides are defined before actual SCSS imports
|
// Variables overrides are defined before actual SCSS imports
|
||||||
@import 'variables';
|
@import 'variables';
|
||||||
|
|
||||||
|
|
||||||
// Dependencies SCSS imports
|
// Dependencies SCSS imports
|
||||||
// `~` allow to import a node_modules folder (resolved by Webpack)
|
// `~` allow to import a node_modules folder (resolved by Webpack)
|
||||||
// @import "~bootstrap/scss/root";
|
// @import "~bootstrap/scss/root";
|
||||||
@import "~bootstrap/scss/reboot";
|
@import '~bootstrap/scss/reboot';
|
||||||
@import "~bootstrap/scss/type";
|
@import '~bootstrap/scss/type';
|
||||||
@import "~bootstrap/scss/images";
|
@import '~bootstrap/scss/images';
|
||||||
@import "~bootstrap/scss/code";
|
@import '~bootstrap/scss/code';
|
||||||
@import "~bootstrap/scss/grid";
|
@import '~bootstrap/scss/grid';
|
||||||
@import "~bootstrap/scss/tables";
|
@import '~bootstrap/scss/tables';
|
||||||
@import "~bootstrap/scss/forms";
|
@import '~bootstrap/scss/forms';
|
||||||
@import "~bootstrap/scss/buttons";
|
@import '~bootstrap/scss/buttons';
|
||||||
@import "~bootstrap/scss/transitions";
|
@import '~bootstrap/scss/transitions';
|
||||||
@import "~bootstrap/scss/dropdown";
|
@import '~bootstrap/scss/dropdown';
|
||||||
@import "~bootstrap/scss/button-group";
|
@import '~bootstrap/scss/button-group';
|
||||||
@import "~bootstrap/scss/input-group";
|
@import '~bootstrap/scss/input-group';
|
||||||
@import "~bootstrap/scss/custom-forms";
|
@import '~bootstrap/scss/custom-forms';
|
||||||
@import "~bootstrap/scss/nav";
|
@import '~bootstrap/scss/nav';
|
||||||
@import "~bootstrap/scss/navbar";
|
@import '~bootstrap/scss/navbar';
|
||||||
@import "~bootstrap/scss/card";
|
@import '~bootstrap/scss/card';
|
||||||
@import "~bootstrap/scss/breadcrumb";
|
@import '~bootstrap/scss/breadcrumb';
|
||||||
// @import "~bootstrap/scss/pagination";
|
// @import "~bootstrap/scss/pagination";
|
||||||
@import "~bootstrap/scss/badge";
|
@import '~bootstrap/scss/badge';
|
||||||
// @import "~bootstrap/scss/jumbotron";
|
// @import "~bootstrap/scss/jumbotron";
|
||||||
@import "~bootstrap/scss/alert";
|
@import '~bootstrap/scss/alert';
|
||||||
@import "~bootstrap/scss/progress";
|
@import '~bootstrap/scss/progress';
|
||||||
// @import "~bootstrap/scss/media";
|
// @import "~bootstrap/scss/media";
|
||||||
@import "~bootstrap/scss/list-group";
|
@import '~bootstrap/scss/list-group';
|
||||||
@import "~bootstrap/scss/close";
|
@import '~bootstrap/scss/close';
|
||||||
// @import "~bootstrap/scss/toasts";
|
// @import "~bootstrap/scss/toasts";
|
||||||
@import "~bootstrap/scss/modal";
|
@import '~bootstrap/scss/modal';
|
||||||
@import "~bootstrap/scss/tooltip";
|
@import '~bootstrap/scss/tooltip';
|
||||||
@import "~bootstrap/scss/popover";
|
@import '~bootstrap/scss/popover';
|
||||||
// @import "~bootstrap/scss/carousel";
|
// @import "~bootstrap/scss/carousel";
|
||||||
@import "~bootstrap/scss/spinners";
|
@import '~bootstrap/scss/spinners';
|
||||||
@import "~bootstrap/scss/utilities";
|
@import '~bootstrap/scss/utilities';
|
||||||
// @import "~bootstrap/scss/print";
|
// @import "~bootstrap/scss/print";
|
||||||
|
|
||||||
@import '~bootstrap-vue/src/index.scss';
|
@import '~bootstrap-vue/src/index.scss';
|
||||||
|
@ -87,18 +86,22 @@
|
||||||
|
|
||||||
// Overwrite list-group-item variants to lighter ones (used in diagnosis for example)
|
// Overwrite list-group-item variants to lighter ones (used in diagnosis for example)
|
||||||
@each $color, $value in $theme-colors {
|
@each $color, $value in $theme-colors {
|
||||||
@include list-group-item-variant($color, theme-color-level($color, $list-group-item-bg-level), theme-color-level($color, $list-group-item-color-level));
|
@include list-group-item-variant(
|
||||||
|
$color,
|
||||||
|
theme-color-level($color, $list-group-item-bg-level),
|
||||||
|
theme-color-level($color, $list-group-item-color-level)
|
||||||
|
);
|
||||||
|
|
||||||
.btn-#{$color} {
|
.btn-#{$color} {
|
||||||
&:focus,
|
&:focus,
|
||||||
&.focus {
|
&.focus {
|
||||||
box-shadow: 0 0 0 $btn-focus-width rgba($value, .3);
|
box-shadow: 0 0 0 $btn-focus-width rgba($value, 0.3);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[dark-theme="true"] {
|
[dark-theme='true'] {
|
||||||
color-scheme: dark; // Ask browser to use dark mode native styling
|
color-scheme: dark; // Ask browser to use dark mode native styling
|
||||||
|
|
||||||
--yiq-contrasted-threshold: 120;
|
--yiq-contrasted-threshold: 120;
|
||||||
|
@ -122,10 +125,18 @@
|
||||||
@include hsl-color('gray-100', 256, 0%, 15%);
|
@include hsl-color('gray-100', 256, 0%, 15%);
|
||||||
|
|
||||||
@each $color, $value in $theme-colors {
|
@each $color, $value in $theme-colors {
|
||||||
@include list-group-item-variant($color, theme-color-level($color, -6), theme-color-level($color, 2));
|
@include list-group-item-variant(
|
||||||
|
$color,
|
||||||
|
theme-color-level($color, -6),
|
||||||
|
theme-color-level($color, 2)
|
||||||
|
);
|
||||||
|
|
||||||
.alert-#{$color} {
|
.alert-#{$color} {
|
||||||
@include alert-variant(theme-color-level($color, -6), theme-color-level($color, -5), theme-color-level($color, 2));
|
@include alert-variant(
|
||||||
|
theme-color-level($color, -6),
|
||||||
|
theme-color-level($color, -5),
|
||||||
|
theme-color-level($color, 2)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -164,7 +175,6 @@ body {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Add breakpoints for w-*
|
// Add breakpoints for w-*
|
||||||
@each $breakpoint in map-keys($grid-breakpoints) {
|
@each $breakpoint in map-keys($grid-breakpoints) {
|
||||||
@each $size, $length in $sizes {
|
@each $size, $length in $sizes {
|
||||||
|
@ -178,7 +188,13 @@ body {
|
||||||
|
|
||||||
// Add xs sized btn
|
// Add xs sized btn
|
||||||
.btn-xs {
|
.btn-xs {
|
||||||
@include button-size($btn-padding-y-xs, $btn-padding-x-xs, $btn-font-size-sm, $btn-line-height-xs, $btn-border-radius-sm);
|
@include button-size(
|
||||||
|
$btn-padding-y-xs,
|
||||||
|
$btn-padding-x-xs,
|
||||||
|
$btn-font-size-sm,
|
||||||
|
$btn-line-height-xs,
|
||||||
|
$btn-border-radius-sm
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allow state of input group to be displayed under the group
|
// Allow state of input group to be displayed under the group
|
||||||
|
@ -186,8 +202,9 @@ body {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tooltip {
|
||||||
.tooltip { top: 0; }
|
top: 0;
|
||||||
|
}
|
||||||
// Descriptive list (<b-row /> elems with <b-col> inside)
|
// Descriptive list (<b-row /> elems with <b-col> inside)
|
||||||
// FIXME REMOVE when every infos switch to `DescriptionRow`
|
// FIXME REMOVE when every infos switch to `DescriptionRow`
|
||||||
.row-line {
|
.row-line {
|
||||||
|
@ -199,31 +216,45 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
@include media-breakpoint-down(sm) {
|
@include media-breakpoint-down(sm) {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
&:not(:last-of-type) {
|
&:not(:last-of-type) {
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
padding-bottom: 0.5rem;
|
padding-bottom: 0.5rem;
|
||||||
border-bottom: $border-width solid $card-border-color;
|
border-bottom: $border-width solid $card-border-color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.card + .card, .card + .config-panel, .config-panel + .card {
|
.card + .card,
|
||||||
|
.card + .config-panel,
|
||||||
|
.config-panel + .card {
|
||||||
margin-top: 2rem;
|
margin-top: 2rem;
|
||||||
}
|
}
|
||||||
.card-deck .card + .card {
|
.card-deck .card + .card {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-header, .list-group-item {
|
.card-header,
|
||||||
h1, h2, h3, h4, h5, h6 {
|
.list-group-item {
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-header, .list-group-item {
|
.card-header,
|
||||||
h1, h2, h3, h4, h5, h6 {
|
.list-group-item {
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
font-weight: $font-weight-normal;
|
font-weight: $font-weight-normal;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -275,14 +306,14 @@ h3.card-title {
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
margin-bottom: .5rem;
|
margin-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@include media-breakpoint-up(md) {
|
@include media-breakpoint-up(md) {
|
||||||
.btn ~ .btn {
|
.btn ~ .btn {
|
||||||
margin-left: .5rem;
|
margin-left: 0.5rem;
|
||||||
}
|
}
|
||||||
.btn ~ .dropdown-toggle-split {
|
.btn ~ .dropdown-toggle-split {
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
|
@ -302,7 +333,7 @@ h3.card-title {
|
||||||
|
|
||||||
code {
|
code {
|
||||||
background: $gray-300;
|
background: $gray-300;
|
||||||
padding: .15rem .25rem;
|
padding: 0.15rem 0.25rem;
|
||||||
border-radius: $border-radius;
|
border-radius: $border-radius;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,8 +4,7 @@ import api from '@/api'
|
||||||
import { isEmptyValue } from '@/helpers/commons'
|
import { isEmptyValue } from '@/helpers/commons'
|
||||||
import { stratify } from '@/helpers/data/tree'
|
import { stratify } from '@/helpers/data/tree'
|
||||||
|
|
||||||
|
export function getParentDomain(domain, domains, highest = false) {
|
||||||
export function getParentDomain (domain, domains, highest = false) {
|
|
||||||
const method = highest ? 'lastIndexOf' : 'indexOf'
|
const method = highest ? 'lastIndexOf' : 'indexOf'
|
||||||
let i = domain[method]('.')
|
let i = domain[method]('.')
|
||||||
while (i !== -1) {
|
while (i !== -1) {
|
||||||
|
@ -17,7 +16,6 @@ export function getParentDomain (domain, domains, highest = false) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
main_domain: undefined,
|
main_domain: undefined,
|
||||||
|
@ -26,36 +24,36 @@ export default {
|
||||||
users: undefined, // basic user data: Object {username: {data}}
|
users: undefined, // basic user data: Object {username: {data}}
|
||||||
users_details: {}, // precise user data: Object {username: {data}}
|
users_details: {}, // precise user data: Object {username: {data}}
|
||||||
groups: undefined,
|
groups: undefined,
|
||||||
permissions: undefined
|
permissions: undefined,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
mutations: {
|
mutations: {
|
||||||
'SET_DOMAINS' (state, [{ domains, main }]) {
|
SET_DOMAINS(state, [{ domains, main }]) {
|
||||||
state.domains = domains
|
state.domains = domains
|
||||||
state.main_domain = main
|
state.main_domain = main
|
||||||
},
|
},
|
||||||
|
|
||||||
'SET_DOMAINS_DETAILS' (state, [name, details]) {
|
SET_DOMAINS_DETAILS(state, [name, details]) {
|
||||||
Vue.set(state.domains_details, name, details)
|
Vue.set(state.domains_details, name, details)
|
||||||
},
|
},
|
||||||
|
|
||||||
'UPDATE_DOMAINS_DETAILS' (state, payload) {
|
UPDATE_DOMAINS_DETAILS(state, payload) {
|
||||||
// FIXME use a common function to execute the same code ?
|
// FIXME use a common function to execute the same code ?
|
||||||
this.commit('SET_DOMAINS_DETAILS', payload)
|
this.commit('SET_DOMAINS_DETAILS', payload)
|
||||||
},
|
},
|
||||||
|
|
||||||
'DEL_DOMAINS_DETAILS' (state, [name]) {
|
DEL_DOMAINS_DETAILS(state, [name]) {
|
||||||
Vue.delete(state.domains_details, name)
|
Vue.delete(state.domains_details, name)
|
||||||
if (state.domains) {
|
if (state.domains) {
|
||||||
Vue.delete(state.domains, name)
|
Vue.delete(state.domains, name)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
'ADD_DOMAINS' (state, [{ domain }]) {
|
ADD_DOMAINS(state, [{ domain }]) {
|
||||||
state.domains.push(domain)
|
state.domains.push(domain)
|
||||||
},
|
},
|
||||||
|
|
||||||
'DEL_DOMAINS' (state, [domain]) {
|
DEL_DOMAINS(state, [domain]) {
|
||||||
state.domains.splice(state.domains.indexOf(domain), 1)
|
state.domains.splice(state.domains.indexOf(domain), 1)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -64,20 +62,20 @@ export default {
|
||||||
// state.main_domain = response.current_main_domain
|
// state.main_domain = response.current_main_domain
|
||||||
// },
|
// },
|
||||||
|
|
||||||
'UPDATE_MAIN_DOMAIN' (state, [domain]) {
|
UPDATE_MAIN_DOMAIN(state, [domain]) {
|
||||||
state.main_domain = domain
|
state.main_domain = domain
|
||||||
},
|
},
|
||||||
|
|
||||||
'SET_USERS' (state, [users]) {
|
SET_USERS(state, [users]) {
|
||||||
state.users = users || null
|
state.users = users || null
|
||||||
},
|
},
|
||||||
|
|
||||||
'ADD_USERS' (state, [user]) {
|
ADD_USERS(state, [user]) {
|
||||||
if (!state.users) state.users = {}
|
if (!state.users) state.users = {}
|
||||||
Vue.set(state.users, user.username, user)
|
Vue.set(state.users, user.username, user)
|
||||||
},
|
},
|
||||||
|
|
||||||
'SET_USERS_DETAILS' (state, [username, userData]) {
|
SET_USERS_DETAILS(state, [username, userData]) {
|
||||||
Vue.set(state.users_details, username, userData)
|
Vue.set(state.users_details, username, userData)
|
||||||
if (!state.users) return
|
if (!state.users) return
|
||||||
const user = state.users[username]
|
const user = state.users[username]
|
||||||
|
@ -88,12 +86,12 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
'UPDATE_USERS_DETAILS' (state, payload) {
|
UPDATE_USERS_DETAILS(state, payload) {
|
||||||
// FIXME use a common function to execute the same code ?
|
// FIXME use a common function to execute the same code ?
|
||||||
this.commit('SET_USERS_DETAILS', payload)
|
this.commit('SET_USERS_DETAILS', payload)
|
||||||
},
|
},
|
||||||
|
|
||||||
'DEL_USERS_DETAILS' (state, [username]) {
|
DEL_USERS_DETAILS(state, [username]) {
|
||||||
Vue.delete(state.users_details, username)
|
Vue.delete(state.users_details, username)
|
||||||
if (state.users) {
|
if (state.users) {
|
||||||
Vue.delete(state.users, username)
|
Vue.delete(state.users, username)
|
||||||
|
@ -103,29 +101,29 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
'SET_GROUPS' (state, [groups]) {
|
SET_GROUPS(state, [groups]) {
|
||||||
state.groups = groups
|
state.groups = groups
|
||||||
},
|
},
|
||||||
|
|
||||||
'ADD_GROUPS' (state, [{ name }]) {
|
ADD_GROUPS(state, [{ name }]) {
|
||||||
if (state.groups !== undefined) {
|
if (state.groups !== undefined) {
|
||||||
Vue.set(state.groups, name, { members: [], permissions: [] })
|
Vue.set(state.groups, name, { members: [], permissions: [] })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
'UPDATE_GROUPS' (state, [data, { groupName }]) {
|
UPDATE_GROUPS(state, [data, { groupName }]) {
|
||||||
Vue.set(state.groups, groupName, data)
|
Vue.set(state.groups, groupName, data)
|
||||||
},
|
},
|
||||||
|
|
||||||
'DEL_GROUPS' (state, [groupname]) {
|
DEL_GROUPS(state, [groupname]) {
|
||||||
Vue.delete(state.groups, groupname)
|
Vue.delete(state.groups, groupname)
|
||||||
},
|
},
|
||||||
|
|
||||||
'SET_PERMISSIONS' (state, [permissions]) {
|
SET_PERMISSIONS(state, [permissions]) {
|
||||||
state.permissions = permissions
|
state.permissions = permissions
|
||||||
},
|
},
|
||||||
|
|
||||||
'UPDATE_PERMISSIONS' (state, [_, { groupName, action, permId }]) {
|
UPDATE_PERMISSIONS(state, [_, { groupName, action, permId }]) {
|
||||||
// FIXME hacky way to update the store
|
// FIXME hacky way to update the store
|
||||||
const permissions = state.groups[groupName].permissions
|
const permissions = state.groups[groupName].permissions
|
||||||
if (action === 'add') {
|
if (action === 'add') {
|
||||||
|
@ -134,56 +132,103 @@ export default {
|
||||||
const index = permissions.indexOf(permId)
|
const index = permissions.indexOf(permId)
|
||||||
if (index > -1) permissions.splice(index, 1)
|
if (index > -1) permissions.splice(index, 1)
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
'GET' (
|
GET(
|
||||||
{ state, commit, rootState },
|
{ state, commit, rootState },
|
||||||
{ uri, param, storeKey = uri, humanKey, noCache, options, ...extraParams }
|
{
|
||||||
|
uri,
|
||||||
|
param,
|
||||||
|
storeKey = uri,
|
||||||
|
humanKey,
|
||||||
|
noCache,
|
||||||
|
options,
|
||||||
|
...extraParams
|
||||||
|
},
|
||||||
) {
|
) {
|
||||||
const currentState = param ? state[storeKey][param] : state[storeKey]
|
const currentState = param ? state[storeKey][param] : state[storeKey]
|
||||||
// if data has already been queried, simply return
|
// if data has already been queried, simply return
|
||||||
const ignoreCache = !rootState.cache || noCache || false
|
const ignoreCache = !rootState.cache || noCache || false
|
||||||
if (currentState !== undefined && !ignoreCache) return currentState
|
if (currentState !== undefined && !ignoreCache) return currentState
|
||||||
return api.fetch('GET', param ? `${uri}/${param}` : uri, null, humanKey, options).then(responseData => {
|
return api
|
||||||
// FIXME here's an ugly fix to be able to also cache the main domain when querying domains
|
.fetch('GET', param ? `${uri}/${param}` : uri, null, humanKey, options)
|
||||||
const data = storeKey === 'domains'
|
.then((responseData) => {
|
||||||
? responseData
|
// FIXME here's an ugly fix to be able to also cache the main domain when querying domains
|
||||||
: responseData[storeKey] ? responseData[storeKey] : responseData
|
const data =
|
||||||
commit(
|
storeKey === 'domains'
|
||||||
'SET_' + storeKey.toUpperCase(),
|
? responseData
|
||||||
[param, data, extraParams].filter(item => !isEmptyValue(item))
|
: responseData[storeKey]
|
||||||
|
? responseData[storeKey]
|
||||||
|
: responseData
|
||||||
|
commit(
|
||||||
|
'SET_' + storeKey.toUpperCase(),
|
||||||
|
[param, data, extraParams].filter((item) => !isEmptyValue(item)),
|
||||||
|
)
|
||||||
|
return param ? state[storeKey][param] : state[storeKey]
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
POST(
|
||||||
|
{ state, commit },
|
||||||
|
{ uri, storeKey = uri, data, humanKey, options, ...extraParams },
|
||||||
|
) {
|
||||||
|
return api
|
||||||
|
.fetch('POST', uri, data, humanKey, options)
|
||||||
|
.then((responseData) => {
|
||||||
|
// FIXME api/domains returns null
|
||||||
|
if (responseData === null) responseData = data
|
||||||
|
responseData = responseData[storeKey]
|
||||||
|
? responseData[storeKey]
|
||||||
|
: responseData
|
||||||
|
commit(
|
||||||
|
'ADD_' + storeKey.toUpperCase(),
|
||||||
|
[responseData, extraParams].filter((item) => !isEmptyValue(item)),
|
||||||
|
)
|
||||||
|
return state[storeKey]
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
PUT(
|
||||||
|
{ state, commit },
|
||||||
|
{ uri, param, storeKey = uri, data, humanKey, options, ...extraParams },
|
||||||
|
) {
|
||||||
|
return api
|
||||||
|
.fetch('PUT', param ? `${uri}/${param}` : uri, data, humanKey, options)
|
||||||
|
.then((responseData) => {
|
||||||
|
const data = responseData[storeKey]
|
||||||
|
? responseData[storeKey]
|
||||||
|
: responseData
|
||||||
|
commit(
|
||||||
|
'UPDATE_' + storeKey.toUpperCase(),
|
||||||
|
[param, data, extraParams].filter((item) => !isEmptyValue(item)),
|
||||||
|
)
|
||||||
|
return param ? state[storeKey][param] : state[storeKey]
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
DELETE(
|
||||||
|
{ commit },
|
||||||
|
{ uri, param, storeKey = uri, data, humanKey, options, ...extraParams },
|
||||||
|
) {
|
||||||
|
return api
|
||||||
|
.fetch(
|
||||||
|
'DELETE',
|
||||||
|
param ? `${uri}/${param}` : uri,
|
||||||
|
data,
|
||||||
|
humanKey,
|
||||||
|
options,
|
||||||
)
|
)
|
||||||
return param ? state[storeKey][param] : state[storeKey]
|
.then(() => {
|
||||||
})
|
commit(
|
||||||
|
'DEL_' + storeKey.toUpperCase(),
|
||||||
|
[param, extraParams].filter((item) => !isEmptyValue(item)),
|
||||||
|
)
|
||||||
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
'POST' ({ state, commit }, { uri, storeKey = uri, data, humanKey, options, ...extraParams }) {
|
RESET_CACHE_DATA({ state }, keys = Object.keys(state)) {
|
||||||
return api.fetch('POST', uri, data, humanKey, options).then(responseData => {
|
|
||||||
// FIXME api/domains returns null
|
|
||||||
if (responseData === null) responseData = data
|
|
||||||
responseData = responseData[storeKey] ? responseData[storeKey] : responseData
|
|
||||||
commit('ADD_' + storeKey.toUpperCase(), [responseData, extraParams].filter(item => !isEmptyValue(item)))
|
|
||||||
return state[storeKey]
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
'PUT' ({ state, commit }, { uri, param, storeKey = uri, data, humanKey, options, ...extraParams }) {
|
|
||||||
return api.fetch('PUT', param ? `${uri}/${param}` : uri, data, humanKey, options).then(responseData => {
|
|
||||||
const data = responseData[storeKey] ? responseData[storeKey] : responseData
|
|
||||||
commit('UPDATE_' + storeKey.toUpperCase(), [param, data, extraParams].filter(item => !isEmptyValue(item)))
|
|
||||||
return param ? state[storeKey][param] : state[storeKey]
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
'DELETE' ({ commit }, { uri, param, storeKey = uri, data, humanKey, options, ...extraParams }) {
|
|
||||||
return api.fetch('DELETE', param ? `${uri}/${param}` : uri, data, humanKey, options).then(() => {
|
|
||||||
commit('DEL_' + storeKey.toUpperCase(), [param, extraParams].filter(item => !isEmptyValue(item)))
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
'RESET_CACHE_DATA' ({ state }, keys = Object.keys(state)) {
|
|
||||||
for (const key of keys) {
|
for (const key of keys) {
|
||||||
if (key === 'users_details') {
|
if (key === 'users_details') {
|
||||||
state[key] = {}
|
state[key] = {}
|
||||||
|
@ -191,36 +236,40 @@ export default {
|
||||||
state[key] = undefined
|
state[key] = undefined
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
getters: {
|
getters: {
|
||||||
users: state => {
|
users: (state) => {
|
||||||
if (state.users) return Object.values(state.users)
|
if (state.users) return Object.values(state.users)
|
||||||
return state.users
|
return state.users
|
||||||
},
|
},
|
||||||
|
|
||||||
userNames: state => {
|
userNames: (state) => {
|
||||||
if (state.users) return Object.keys(state.users)
|
if (state.users) return Object.keys(state.users)
|
||||||
return []
|
return []
|
||||||
},
|
},
|
||||||
|
|
||||||
user: state => name => state.users_details[name], // not cached
|
user: (state) => (name) => state.users_details[name], // not cached
|
||||||
|
|
||||||
domains: state => state.domains,
|
domains: (state) => state.domains,
|
||||||
|
|
||||||
orderedDomains: state => {
|
orderedDomains: (state) => {
|
||||||
if (!state.domains) return
|
if (!state.domains) return
|
||||||
|
|
||||||
const splittedDomains = Object.fromEntries(state.domains.map(domain => {
|
const splittedDomains = Object.fromEntries(
|
||||||
// Keep the main part of the domain and the extension together
|
state.domains.map((domain) => {
|
||||||
// eg: this.is.an.example.com -> ['example.com', 'an', 'is', 'this']
|
// Keep the main part of the domain and the extension together
|
||||||
domain = domain.split('.')
|
// eg: this.is.an.example.com -> ['example.com', 'an', 'is', 'this']
|
||||||
domain.push(domain.pop() + domain.pop())
|
domain = domain.split('.')
|
||||||
return [domain, domain.reverse()]
|
domain.push(domain.pop() + domain.pop())
|
||||||
}))
|
return [domain, domain.reverse()]
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
return state.domains.sort((a, b) => splittedDomains[a] > splittedDomains[b])
|
return state.domains.sort(
|
||||||
|
(a, b) => splittedDomains[a] > splittedDomains[b],
|
||||||
|
)
|
||||||
},
|
},
|
||||||
|
|
||||||
domainsTree: (state, getters) => {
|
domainsTree: (state, getters) => {
|
||||||
|
@ -230,30 +279,33 @@ export default {
|
||||||
// action when state.domain change)
|
// action when state.domain change)
|
||||||
const domains = getters.orderedDomains
|
const domains = getters.orderedDomains
|
||||||
if (!domains) return
|
if (!domains) return
|
||||||
const dataset = domains.map(name => ({
|
const dataset = domains.map((name) => ({
|
||||||
// data to build a hierarchy
|
// data to build a hierarchy
|
||||||
name,
|
name,
|
||||||
parent: getParentDomain(name, domains),
|
parent: getParentDomain(name, domains),
|
||||||
// utility data that will be used by `RecursiveListGroup` component
|
// utility data that will be used by `RecursiveListGroup` component
|
||||||
to: { name: 'domain-info', params: { name } },
|
to: { name: 'domain-info', params: { name } },
|
||||||
opened: true
|
opened: true,
|
||||||
}))
|
}))
|
||||||
return stratify(dataset)
|
return stratify(dataset)
|
||||||
},
|
},
|
||||||
|
|
||||||
domain: state => name => state.domains_details[name],
|
domain: (state) => (name) => state.domains_details[name],
|
||||||
|
|
||||||
highestDomainParentName: (state, getters) => name => {
|
highestDomainParentName: (state, getters) => (name) => {
|
||||||
return getParentDomain(name, getters.orderedDomains, true)
|
return getParentDomain(name, getters.orderedDomains, true)
|
||||||
},
|
},
|
||||||
|
|
||||||
mainDomain: state => state.main_domain,
|
mainDomain: (state) => state.main_domain,
|
||||||
|
|
||||||
domainsAsChoices: state => {
|
domainsAsChoices: (state) => {
|
||||||
const mainDomain = state.main_domain
|
const mainDomain = state.main_domain
|
||||||
return state.domains.map(domain => {
|
return state.domains.map((domain) => {
|
||||||
return { value: domain, text: domain === mainDomain ? domain + ' ★' : domain }
|
return {
|
||||||
|
value: domain,
|
||||||
|
text: domain === mainDomain ? domain + ' ★' : domain,
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,6 @@ export default new Vuex.Store({
|
||||||
getters: settings.getters,
|
getters: settings.getters,
|
||||||
modules: {
|
modules: {
|
||||||
info,
|
info,
|
||||||
data
|
data,
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -19,32 +19,32 @@ export default {
|
||||||
tempMessages: [], // Array of messages
|
tempMessages: [], // Array of messages
|
||||||
routerKey: undefined, // String if current route has params
|
routerKey: undefined, // String if current route has params
|
||||||
breadcrumb: [], // Array of routes
|
breadcrumb: [], // Array of routes
|
||||||
transitionName: null // String of CSS class if transitions are enabled
|
transitionName: null, // String of CSS class if transitions are enabled
|
||||||
},
|
},
|
||||||
|
|
||||||
mutations: {
|
mutations: {
|
||||||
'SET_INSTALLED' (state, boolean) {
|
SET_INSTALLED(state, boolean) {
|
||||||
state.installed = boolean
|
state.installed = boolean
|
||||||
},
|
},
|
||||||
|
|
||||||
'SET_CONNECTED' (state, boolean) {
|
SET_CONNECTED(state, boolean) {
|
||||||
localStorage.setItem('connected', boolean)
|
localStorage.setItem('connected', boolean)
|
||||||
state.connected = boolean
|
state.connected = boolean
|
||||||
},
|
},
|
||||||
|
|
||||||
'SET_YUNOHOST_INFOS' (state, yunohost) {
|
SET_YUNOHOST_INFOS(state, yunohost) {
|
||||||
state.yunohost = yunohost
|
state.yunohost = yunohost
|
||||||
},
|
},
|
||||||
|
|
||||||
'SET_WAITING' (state, boolean) {
|
SET_WAITING(state, boolean) {
|
||||||
state.waiting = boolean
|
state.waiting = boolean
|
||||||
},
|
},
|
||||||
|
|
||||||
'SET_RECONNECTING' (state, args) {
|
SET_RECONNECTING(state, args) {
|
||||||
state.reconnecting = args
|
state.reconnecting = args
|
||||||
},
|
},
|
||||||
|
|
||||||
'ADD_REQUEST' (state, request) {
|
ADD_REQUEST(state, request) {
|
||||||
if (state.requests.length > 10) {
|
if (state.requests.length > 10) {
|
||||||
// We do not remove requests right after it resolves since an error might bring
|
// We do not remove requests right after it resolves since an error might bring
|
||||||
// one back to life but we can safely remove some here.
|
// one back to life but we can safely remove some here.
|
||||||
|
@ -53,35 +53,38 @@ export default {
|
||||||
state.requests.push(request)
|
state.requests.push(request)
|
||||||
},
|
},
|
||||||
|
|
||||||
'UPDATE_REQUEST' (state, { request, key, value }) {
|
UPDATE_REQUEST(state, { request, key, value }) {
|
||||||
// This rely on data persistance and reactivity.
|
// This rely on data persistance and reactivity.
|
||||||
Vue.set(request, key, value)
|
Vue.set(request, key, value)
|
||||||
},
|
},
|
||||||
|
|
||||||
'REMOVE_REQUEST' (state, request) {
|
REMOVE_REQUEST(state, request) {
|
||||||
const index = state.requests.lastIndexOf(request)
|
const index = state.requests.lastIndexOf(request)
|
||||||
state.requests.splice(index, 1)
|
state.requests.splice(index, 1)
|
||||||
},
|
},
|
||||||
|
|
||||||
'ADD_HISTORY_ACTION' (state, request) {
|
ADD_HISTORY_ACTION(state, request) {
|
||||||
state.history.push(request)
|
state.history.push(request)
|
||||||
},
|
},
|
||||||
|
|
||||||
'ADD_TEMP_MESSAGE' (state, { request, message, type }) {
|
ADD_TEMP_MESSAGE(state, { request, message, type }) {
|
||||||
state.tempMessages.push([message, type])
|
state.tempMessages.push([message, type])
|
||||||
},
|
},
|
||||||
|
|
||||||
'UPDATE_DISPLAYED_MESSAGES' (state, { request }) {
|
UPDATE_DISPLAYED_MESSAGES(state, { request }) {
|
||||||
if (!state.tempMessages.length) {
|
if (!state.tempMessages.length) {
|
||||||
state.historyTimer = null
|
state.historyTimer = null
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const { messages, warnings, errors } = state.tempMessages.reduce((acc, [message, type]) => {
|
const { messages, warnings, errors } = state.tempMessages.reduce(
|
||||||
acc.messages.push(message)
|
(acc, [message, type]) => {
|
||||||
if (['error', 'warning'].includes(type)) acc[type + 's']++
|
acc.messages.push(message)
|
||||||
return acc
|
if (['error', 'warning'].includes(type)) acc[type + 's']++
|
||||||
}, { messages: [], warnings: 0, errors: 0 })
|
return acc
|
||||||
|
},
|
||||||
|
{ messages: [], warnings: 0, errors: 0 },
|
||||||
|
)
|
||||||
state.tempMessages = []
|
state.tempMessages = []
|
||||||
state.historyTimer = null
|
state.historyTimer = null
|
||||||
request.messages = request.messages.concat(messages)
|
request.messages = request.messages.concat(messages)
|
||||||
|
@ -89,7 +92,7 @@ export default {
|
||||||
request.errors += errors
|
request.errors += errors
|
||||||
},
|
},
|
||||||
|
|
||||||
'SET_ERROR' (state, request) {
|
SET_ERROR(state, request) {
|
||||||
if (request) {
|
if (request) {
|
||||||
state.error = request
|
state.error = request
|
||||||
} else {
|
} else {
|
||||||
|
@ -97,21 +100,21 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
'SET_ROUTER_KEY' (state, key) {
|
SET_ROUTER_KEY(state, key) {
|
||||||
state.routerKey = key
|
state.routerKey = key
|
||||||
},
|
},
|
||||||
|
|
||||||
'SET_BREADCRUMB' (state, breadcrumb) {
|
SET_BREADCRUMB(state, breadcrumb) {
|
||||||
state.breadcrumb = breadcrumb
|
state.breadcrumb = breadcrumb
|
||||||
},
|
},
|
||||||
|
|
||||||
'SET_TRANSITION_NAME' (state, transitionName) {
|
SET_TRANSITION_NAME(state, transitionName) {
|
||||||
state.transitionName = transitionName
|
state.transitionName = transitionName
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
async 'ON_APP_CREATED' ({ dispatch, state }) {
|
async ON_APP_CREATED({ dispatch, state }) {
|
||||||
await dispatch('CHECK_INSTALL')
|
await dispatch('CHECK_INSTALL')
|
||||||
|
|
||||||
if (!state.installed) {
|
if (!state.installed) {
|
||||||
|
@ -121,7 +124,7 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async 'CHECK_INSTALL' ({ dispatch, commit }, retry = 2) {
|
async CHECK_INSTALL({ dispatch, commit }, retry = 2) {
|
||||||
// this action will try to query the `/installed` route 3 times every 5 s with
|
// this action will try to query the `/installed` route 3 times every 5 s with
|
||||||
// a timeout of the same delay.
|
// a timeout of the same delay.
|
||||||
// FIXME need testing with api not responding
|
// FIXME need testing with api not responding
|
||||||
|
@ -137,7 +140,7 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async 'CONNECT' ({ commit, dispatch }) {
|
async CONNECT({ commit, dispatch }) {
|
||||||
// If the user is not connected, the first action will throw
|
// If the user is not connected, the first action will throw
|
||||||
// and login prompt will be shown automaticly
|
// and login prompt will be shown automaticly
|
||||||
await dispatch('GET_YUNOHOST_INFOS')
|
await dispatch('GET_YUNOHOST_INFOS')
|
||||||
|
@ -145,54 +148,77 @@ export default {
|
||||||
await dispatch('GET', { uri: 'domains', storeKey: 'domains' })
|
await dispatch('GET', { uri: 'domains', storeKey: 'domains' })
|
||||||
},
|
},
|
||||||
|
|
||||||
'RESET_CONNECTED' ({ commit }) {
|
RESET_CONNECTED({ commit }) {
|
||||||
commit('SET_CONNECTED', false)
|
commit('SET_CONNECTED', false)
|
||||||
commit('SET_YUNOHOST_INFOS', null)
|
commit('SET_YUNOHOST_INFOS', null)
|
||||||
},
|
},
|
||||||
|
|
||||||
'DISCONNECT' ({ dispatch }, route = router.currentRoute) {
|
DISCONNECT({ dispatch }, route = router.currentRoute) {
|
||||||
dispatch('RESET_CONNECTED')
|
dispatch('RESET_CONNECTED')
|
||||||
if (router.currentRoute.name === 'login') return
|
if (router.currentRoute.name === 'login') return
|
||||||
router.push({
|
router.push({
|
||||||
name: 'login',
|
name: 'login',
|
||||||
// Add a redirect query if next route is not unknown (like `logout`) or `login`
|
// Add a redirect query if next route is not unknown (like `logout`) or `login`
|
||||||
query: route && !['login', null].includes(route.name)
|
query:
|
||||||
? { redirect: route.path }
|
route && !['login', null].includes(route.name)
|
||||||
: {}
|
? { redirect: route.path }
|
||||||
|
: {},
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
'LOGIN' ({ dispatch }, credentials) {
|
LOGIN({ dispatch }, credentials) {
|
||||||
return api.post('login', { credentials }, null, { websocket: false }).then(() => {
|
return api
|
||||||
dispatch('CONNECT')
|
.post('login', { credentials }, null, { websocket: false })
|
||||||
})
|
.then(() => {
|
||||||
|
dispatch('CONNECT')
|
||||||
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
'LOGOUT' ({ dispatch }) {
|
LOGOUT({ dispatch }) {
|
||||||
dispatch('DISCONNECT')
|
dispatch('DISCONNECT')
|
||||||
return api.get('logout')
|
return api.get('logout')
|
||||||
},
|
},
|
||||||
|
|
||||||
'TRY_TO_RECONNECT' ({ commit, dispatch }, args = {}) {
|
TRY_TO_RECONNECT({ commit, dispatch }, args = {}) {
|
||||||
// FIXME This is very ugly arguments forwarding, will use proper component way of doing this when switching to Vue 3 (teleport)
|
// FIXME This is very ugly arguments forwarding, will use proper component way of doing this when switching to Vue 3 (teleport)
|
||||||
commit('SET_RECONNECTING', args)
|
commit('SET_RECONNECTING', args)
|
||||||
dispatch('RESET_CONNECTED')
|
dispatch('RESET_CONNECTED')
|
||||||
},
|
},
|
||||||
|
|
||||||
'GET_YUNOHOST_INFOS' ({ commit }) {
|
GET_YUNOHOST_INFOS({ commit }) {
|
||||||
return api.get('versions').then(versions => {
|
return api.get('versions').then((versions) => {
|
||||||
commit('SET_YUNOHOST_INFOS', versions.yunohost)
|
commit('SET_YUNOHOST_INFOS', versions.yunohost)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
'INIT_REQUEST' ({ commit }, { method, uri, humanKey, initial, wait, websocket }) {
|
INIT_REQUEST(
|
||||||
|
{ commit },
|
||||||
|
{ method, uri, humanKey, initial, wait, websocket },
|
||||||
|
) {
|
||||||
// Try to find a description for an API route to display in history and modals
|
// Try to find a description for an API route to display in history and modals
|
||||||
const { key, ...args } = isObjectLiteral(humanKey) ? humanKey : { key: humanKey }
|
const { key, ...args } = isObjectLiteral(humanKey)
|
||||||
const humanRoute = key ? i18n.t('human_routes.' + key, args) : `[${method}] /${uri}`
|
? humanKey
|
||||||
|
: { key: humanKey }
|
||||||
|
const humanRoute = key
|
||||||
|
? i18n.t('human_routes.' + key, args)
|
||||||
|
: `[${method}] /${uri}`
|
||||||
|
|
||||||
let request = { method, uri, humanRouteKey: key, humanRoute, initial, status: 'pending' }
|
let request = {
|
||||||
|
method,
|
||||||
|
uri,
|
||||||
|
humanRouteKey: key,
|
||||||
|
humanRoute,
|
||||||
|
initial,
|
||||||
|
status: 'pending',
|
||||||
|
}
|
||||||
if (websocket) {
|
if (websocket) {
|
||||||
request = { ...request, messages: [], date: Date.now(), warnings: 0, errors: 0 }
|
request = {
|
||||||
|
...request,
|
||||||
|
messages: [],
|
||||||
|
date: Date.now(),
|
||||||
|
warnings: 0,
|
||||||
|
errors: 0,
|
||||||
|
}
|
||||||
commit('ADD_HISTORY_ACTION', request)
|
commit('ADD_HISTORY_ACTION', request)
|
||||||
}
|
}
|
||||||
commit('ADD_REQUEST', request)
|
commit('ADD_REQUEST', request)
|
||||||
|
@ -208,7 +234,7 @@ export default {
|
||||||
return request
|
return request
|
||||||
},
|
},
|
||||||
|
|
||||||
'END_REQUEST' ({ state, commit }, { request, success, wait }) {
|
END_REQUEST({ state, commit }, { request, success, wait }) {
|
||||||
// Update last messages before finishing this request
|
// Update last messages before finishing this request
|
||||||
clearTimeout(state.historyTimer)
|
clearTimeout(state.historyTimer)
|
||||||
commit('UPDATE_DISPLAYED_MESSAGES', { request })
|
commit('UPDATE_DISPLAYED_MESSAGES', { request })
|
||||||
|
@ -216,7 +242,10 @@ export default {
|
||||||
let status = success ? 'success' : 'error'
|
let status = success ? 'success' : 'error'
|
||||||
if (success && (request.warnings || request.errors)) {
|
if (success && (request.warnings || request.errors)) {
|
||||||
const messages = request.messages
|
const messages = request.messages
|
||||||
if (messages.length && messages[messages.length - 1].color === 'warning') {
|
if (
|
||||||
|
messages.length &&
|
||||||
|
messages[messages.length - 1].color === 'warning'
|
||||||
|
) {
|
||||||
request.showWarningMessage = true
|
request.showWarningMessage = true
|
||||||
}
|
}
|
||||||
status = 'warning'
|
status = 'warning'
|
||||||
|
@ -231,11 +260,11 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
'DISPATCH_MESSAGE' ({ state, commit, dispatch }, { request, messages }) {
|
DISPATCH_MESSAGE({ state, commit, dispatch }, { request, messages }) {
|
||||||
for (const type in messages) {
|
for (const type in messages) {
|
||||||
const message = {
|
const message = {
|
||||||
text: messages[type].replaceAll('\n', '<br>'),
|
text: messages[type].replaceAll('\n', '<br>'),
|
||||||
color: type === 'error' ? 'danger' : type
|
color: type === 'error' ? 'danger' : type,
|
||||||
}
|
}
|
||||||
let progressBar = message.text.match(/^\[#*\+*\.*\] > /)
|
let progressBar = message.text.match(/^\[#*\+*\.*\] > /)
|
||||||
if (progressBar) {
|
if (progressBar) {
|
||||||
|
@ -245,7 +274,11 @@ export default {
|
||||||
for (const char of progressBar) {
|
for (const char of progressBar) {
|
||||||
if (char in progress) progress[char] += 1
|
if (char in progress) progress[char] += 1
|
||||||
}
|
}
|
||||||
commit('UPDATE_REQUEST', { request, key: 'progress', value: Object.values(progress) })
|
commit('UPDATE_REQUEST', {
|
||||||
|
request,
|
||||||
|
key: 'progress',
|
||||||
|
value: Object.values(progress),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
if (message.text) {
|
if (message.text) {
|
||||||
// To avoid rendering lag issues, limit the flow of websocket messages to batches of 50ms.
|
// To avoid rendering lag issues, limit the flow of websocket messages to batches of 50ms.
|
||||||
|
@ -259,7 +292,7 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
'HANDLE_ERROR' ({ commit, dispatch }, error) {
|
HANDLE_ERROR({ commit, dispatch }, error) {
|
||||||
if (error.code === 401) {
|
if (error.code === 401) {
|
||||||
// Unauthorized
|
// Unauthorized
|
||||||
dispatch('DISCONNECT')
|
dispatch('DISCONNECT')
|
||||||
|
@ -277,12 +310,12 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
'REVIEW_ERROR' ({ commit }, request) {
|
REVIEW_ERROR({ commit }, request) {
|
||||||
request.review = true
|
request.review = true
|
||||||
commit('SET_ERROR', request)
|
commit('SET_ERROR', request)
|
||||||
},
|
},
|
||||||
|
|
||||||
'DISMISS_ERROR' ({ commit, state }, { initial, review = false }) {
|
DISMISS_ERROR({ commit, state }, { initial, review = false }) {
|
||||||
if (initial && !review) {
|
if (initial && !review) {
|
||||||
// In case of an initial request (data that is needed by a view to render itself),
|
// In case of an initial request (data that is needed by a view to render itself),
|
||||||
// try to go back so the user doesn't get stuck at a never ending skeleton view.
|
// try to go back so the user doesn't get stuck at a never ending skeleton view.
|
||||||
|
@ -296,12 +329,12 @@ export default {
|
||||||
commit('SET_ERROR', null)
|
commit('SET_ERROR', null)
|
||||||
},
|
},
|
||||||
|
|
||||||
'DISMISS_WARNING' ({ commit, state }, request) {
|
DISMISS_WARNING({ commit, state }, request) {
|
||||||
commit('SET_WAITING', false)
|
commit('SET_WAITING', false)
|
||||||
Vue.delete(request, 'showWarningMessage')
|
Vue.delete(request, 'showWarningMessage')
|
||||||
},
|
},
|
||||||
|
|
||||||
'UPDATE_ROUTER_KEY' ({ commit }, { to, from }) {
|
UPDATE_ROUTER_KEY({ commit }, { to, from }) {
|
||||||
if (isEmptyValue(to.params)) {
|
if (isEmptyValue(to.params)) {
|
||||||
commit('SET_ROUTER_KEY', undefined)
|
commit('SET_ROUTER_KEY', undefined)
|
||||||
return
|
return
|
||||||
|
@ -313,21 +346,24 @@ export default {
|
||||||
// Params can be declared in route `meta` to stricly define which params should be
|
// Params can be declared in route `meta` to stricly define which params should be
|
||||||
// taken into account.
|
// taken into account.
|
||||||
const params = to.meta.routerParams
|
const params = to.meta.routerParams
|
||||||
? to.meta.routerParams.map(key => to.params[key])
|
? to.meta.routerParams.map((key) => to.params[key])
|
||||||
: Object.values(to.params)
|
: Object.values(to.params)
|
||||||
|
|
||||||
commit('SET_ROUTER_KEY', `${to.name}-${params.join('-')}`)
|
commit('SET_ROUTER_KEY', `${to.name}-${params.join('-')}`)
|
||||||
},
|
},
|
||||||
|
|
||||||
'UPDATE_BREADCRUMB' ({ commit }, { to, from }) {
|
UPDATE_BREADCRUMB({ commit }, { to, from }) {
|
||||||
function getRouteNames (route) {
|
function getRouteNames(route) {
|
||||||
if (route.meta.breadcrumb) return route.meta.breadcrumb
|
if (route.meta.breadcrumb) return route.meta.breadcrumb
|
||||||
const parentRoute = route.matched.slice().reverse().find(route => route.meta.breadcrumb)
|
const parentRoute = route.matched
|
||||||
|
.slice()
|
||||||
|
.reverse()
|
||||||
|
.find((route) => route.meta.breadcrumb)
|
||||||
if (parentRoute) return parentRoute.meta.breadcrumb
|
if (parentRoute) return parentRoute.meta.breadcrumb
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatRoute (route) {
|
function formatRoute(route) {
|
||||||
const { trad, param } = route.meta.args || {}
|
const { trad, param } = route.meta.args || {}
|
||||||
let text = ''
|
let text = ''
|
||||||
// if a traduction key string has been given and we also need to pass
|
// if a traduction key string has been given and we also need to pass
|
||||||
|
@ -344,49 +380,55 @@ export default {
|
||||||
|
|
||||||
const routeNames = getRouteNames(to)
|
const routeNames = getRouteNames(to)
|
||||||
const allRoutes = router.getRoutes()
|
const allRoutes = router.getRoutes()
|
||||||
const breadcrumb = routeNames.map(name => {
|
const breadcrumb = routeNames.map((name) => {
|
||||||
const route = allRoutes.find(route => route.name === name)
|
const route = allRoutes.find((route) => route.name === name)
|
||||||
return formatRoute(route)
|
return formatRoute(route)
|
||||||
})
|
})
|
||||||
|
|
||||||
commit('SET_BREADCRUMB', breadcrumb)
|
commit('SET_BREADCRUMB', breadcrumb)
|
||||||
|
|
||||||
function getTitle (breadcrumb) {
|
function getTitle(breadcrumb) {
|
||||||
if (breadcrumb.length === 0) return formatRoute(to).text
|
if (breadcrumb.length === 0) return formatRoute(to).text
|
||||||
return (breadcrumb.length > 2 ? breadcrumb.slice(-2) : breadcrumb).map(route => route.text).reverse().join(' / ')
|
return (breadcrumb.length > 2 ? breadcrumb.slice(-2) : breadcrumb)
|
||||||
|
.map((route) => route.text)
|
||||||
|
.reverse()
|
||||||
|
.join(' / ')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Display a simplified breadcrumb as the document title.
|
// Display a simplified breadcrumb as the document title.
|
||||||
document.title = `${getTitle(breadcrumb)} | ${i18n.t('yunohost_admin')}`
|
document.title = `${getTitle(breadcrumb)} | ${i18n.t('yunohost_admin')}`
|
||||||
},
|
},
|
||||||
|
|
||||||
'UPDATE_TRANSITION_NAME' ({ state, commit }, { to, from }) {
|
UPDATE_TRANSITION_NAME({ state, commit }, { to, from }) {
|
||||||
// Use the breadcrumb array length as a direction indicator
|
// Use the breadcrumb array length as a direction indicator
|
||||||
const toDepth = (to.meta.breadcrumb || []).length
|
const toDepth = (to.meta.breadcrumb || []).length
|
||||||
const fromDepth = (from.meta.breadcrumb || []).length
|
const fromDepth = (from.meta.breadcrumb || []).length
|
||||||
commit('SET_TRANSITION_NAME', toDepth < fromDepth ? 'slide-right' : 'slide-left')
|
commit(
|
||||||
}
|
'SET_TRANSITION_NAME',
|
||||||
|
toDepth < fromDepth ? 'slide-right' : 'slide-left',
|
||||||
|
)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
getters: {
|
getters: {
|
||||||
host: state => state.host,
|
host: (state) => state.host,
|
||||||
installed: state => state.installed,
|
installed: (state) => state.installed,
|
||||||
connected: state => state.connected,
|
connected: (state) => state.connected,
|
||||||
yunohost: state => state.yunohost,
|
yunohost: (state) => state.yunohost,
|
||||||
error: state => state.error,
|
error: (state) => state.error,
|
||||||
waiting: state => state.waiting,
|
waiting: (state) => state.waiting,
|
||||||
reconnecting: state => state.reconnecting,
|
reconnecting: (state) => state.reconnecting,
|
||||||
history: state => state.history,
|
history: (state) => state.history,
|
||||||
lastAction: state => state.history[state.history.length - 1],
|
lastAction: (state) => state.history[state.history.length - 1],
|
||||||
currentRequest: state => {
|
currentRequest: (state) => {
|
||||||
const request = state.requests.find(({ status }) => status === 'pending')
|
const request = state.requests.find(({ status }) => status === 'pending')
|
||||||
return request || state.requests[state.requests.length - 1]
|
return request || state.requests[state.requests.length - 1]
|
||||||
},
|
},
|
||||||
routerKey: state => state.routerKey,
|
routerKey: (state) => state.routerKey,
|
||||||
breadcrumb: state => state.breadcrumb,
|
breadcrumb: (state) => state.breadcrumb,
|
||||||
transitionName: state => state.transitionName,
|
transitionName: (state) => state.transitionName,
|
||||||
ssoLink: (state, getters) => {
|
ssoLink: (state, getters) => {
|
||||||
return `//${getters.mainDomain ?? state.host}/yunohost/sso`
|
return `//${getters.mainDomain ?? state.host}/yunohost/sso`
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,11 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import i18n from '@/i18n'
|
import i18n from '@/i18n'
|
||||||
import { loadLocaleMessages, updateDocumentLocale, loadDateFnsLocale } from '@/i18n/helpers'
|
import {
|
||||||
|
loadLocaleMessages,
|
||||||
|
updateDocumentLocale,
|
||||||
|
loadDateFnsLocale,
|
||||||
|
} from '@/i18n/helpers'
|
||||||
import supportedLocales from '@/i18n/supportedLocales'
|
import supportedLocales from '@/i18n/supportedLocales'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
@ -16,48 +20,48 @@ export default {
|
||||||
theme: localStorage.getItem('theme') === 'true',
|
theme: localStorage.getItem('theme') === 'true',
|
||||||
experimental: localStorage.getItem('experimental') === 'true',
|
experimental: localStorage.getItem('experimental') === 'true',
|
||||||
spinner: 'pacman',
|
spinner: 'pacman',
|
||||||
supportedLocales
|
supportedLocales,
|
||||||
},
|
},
|
||||||
|
|
||||||
mutations: {
|
mutations: {
|
||||||
'SET_LOCALE' (state, locale) {
|
SET_LOCALE(state, locale) {
|
||||||
localStorage.setItem('locale', locale)
|
localStorage.setItem('locale', locale)
|
||||||
state.locale = locale
|
state.locale = locale
|
||||||
},
|
},
|
||||||
|
|
||||||
'SET_FALLBACKLOCALE' (state, locale) {
|
SET_FALLBACKLOCALE(state, locale) {
|
||||||
localStorage.setItem('fallbackLocale', locale)
|
localStorage.setItem('fallbackLocale', locale)
|
||||||
state.fallbackLocale = locale
|
state.fallbackLocale = locale
|
||||||
},
|
},
|
||||||
|
|
||||||
'SET_CACHE' (state, boolean) {
|
SET_CACHE(state, boolean) {
|
||||||
localStorage.setItem('cache', boolean)
|
localStorage.setItem('cache', boolean)
|
||||||
state.cache = boolean
|
state.cache = boolean
|
||||||
},
|
},
|
||||||
|
|
||||||
'SET_TRANSITIONS' (state, boolean) {
|
SET_TRANSITIONS(state, boolean) {
|
||||||
localStorage.setItem('transitions', boolean)
|
localStorage.setItem('transitions', boolean)
|
||||||
state.transitions = boolean
|
state.transitions = boolean
|
||||||
},
|
},
|
||||||
|
|
||||||
'SET_EXPERIMENTAL' (state, boolean) {
|
SET_EXPERIMENTAL(state, boolean) {
|
||||||
localStorage.setItem('experimental', boolean)
|
localStorage.setItem('experimental', boolean)
|
||||||
state.experimental = boolean
|
state.experimental = boolean
|
||||||
},
|
},
|
||||||
|
|
||||||
'SET_SPINNER' (state, spinner) {
|
SET_SPINNER(state, spinner) {
|
||||||
state.spinner = spinner
|
state.spinner = spinner
|
||||||
},
|
},
|
||||||
|
|
||||||
'SET_THEME' (state, boolean) {
|
SET_THEME(state, boolean) {
|
||||||
localStorage.setItem('theme', boolean)
|
localStorage.setItem('theme', boolean)
|
||||||
state.theme = boolean
|
state.theme = boolean
|
||||||
document.documentElement.setAttribute('dark-theme', boolean)
|
document.documentElement.setAttribute('dark-theme', boolean)
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
'UPDATE_LOCALE' ({ commit }, locale) {
|
UPDATE_LOCALE({ commit }, locale) {
|
||||||
loadLocaleMessages(locale).then(() => {
|
loadLocaleMessages(locale).then(() => {
|
||||||
updateDocumentLocale(locale)
|
updateDocumentLocale(locale)
|
||||||
commit('SET_LOCALE', locale)
|
commit('SET_LOCALE', locale)
|
||||||
|
@ -67,31 +71,33 @@ export default {
|
||||||
loadDateFnsLocale(locale)
|
loadDateFnsLocale(locale)
|
||||||
},
|
},
|
||||||
|
|
||||||
'UPDATE_FALLBACKLOCALE' ({ commit }, locale) {
|
UPDATE_FALLBACKLOCALE({ commit }, locale) {
|
||||||
loadLocaleMessages(locale).then(() => {
|
loadLocaleMessages(locale).then(() => {
|
||||||
commit('SET_FALLBACKLOCALE', locale)
|
commit('SET_FALLBACKLOCALE', locale)
|
||||||
i18n.fallbackLocale = [locale, 'en']
|
i18n.fallbackLocale = [locale, 'en']
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
'UPDATE_THEME' ({ commit }, theme) {
|
UPDATE_THEME({ commit }, theme) {
|
||||||
commit('SET_THEME', theme)
|
commit('SET_THEME', theme)
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
getters: {
|
getters: {
|
||||||
locale: state => (state.locale),
|
locale: (state) => state.locale,
|
||||||
fallbackLocale: state => (state.fallbackLocale),
|
fallbackLocale: (state) => state.fallbackLocale,
|
||||||
cache: state => (state.cache),
|
cache: (state) => state.cache,
|
||||||
transitions: state => (state.transitions),
|
transitions: (state) => state.transitions,
|
||||||
theme: state => (state.theme),
|
theme: (state) => state.theme,
|
||||||
experimental: state => state.experimental,
|
experimental: (state) => state.experimental,
|
||||||
spinner: state => state.spinner,
|
spinner: (state) => state.spinner,
|
||||||
|
|
||||||
availableLocales: state => {
|
availableLocales: (state) => {
|
||||||
return Object.entries(state.supportedLocales).map(([locale, { name }]) => {
|
return Object.entries(state.supportedLocales).map(
|
||||||
return { value: locale, text: name }
|
([locale, { name }]) => {
|
||||||
})
|
return { value: locale, text: name }
|
||||||
}
|
},
|
||||||
}
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
export default {
|
export default {
|
||||||
name: 'HomeView',
|
name: 'HomeView',
|
||||||
|
|
||||||
data () {
|
data() {
|
||||||
return {
|
return {
|
||||||
menu: [
|
menu: [
|
||||||
{ routeName: 'user-list', icon: 'users', translation: 'users' },
|
{ routeName: 'user-list', icon: 'users', translation: 'users' },
|
||||||
|
@ -26,11 +26,15 @@ export default {
|
||||||
{ routeName: 'app-list', icon: 'cubes', translation: 'applications' },
|
{ routeName: 'app-list', icon: 'cubes', translation: 'applications' },
|
||||||
{ routeName: 'update', icon: 'refresh', translation: 'system_update' },
|
{ routeName: 'update', icon: 'refresh', translation: 'system_update' },
|
||||||
{ routeName: 'tool-list', icon: 'wrench', translation: 'tools' },
|
{ routeName: 'tool-list', icon: 'wrench', translation: 'tools' },
|
||||||
{ routeName: 'diagnosis', icon: 'stethoscope', translation: 'diagnosis' },
|
{
|
||||||
{ routeName: 'backup', icon: 'archive', translation: 'backup' }
|
routeName: 'diagnosis',
|
||||||
]
|
icon: 'stethoscope',
|
||||||
|
translation: 'diagnosis',
|
||||||
|
},
|
||||||
|
{ routeName: 'backup', icon: 'archive', translation: 'backup' },
|
||||||
|
],
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -1,19 +1,31 @@
|
||||||
<template>
|
<template>
|
||||||
<CardForm
|
<CardForm
|
||||||
:title="$t('login')" icon="lock"
|
:title="$t('login')"
|
||||||
:validation="$v" :server-error="serverError"
|
icon="lock"
|
||||||
|
:validation="$v"
|
||||||
|
:server-error="serverError"
|
||||||
@submit.prevent="login"
|
@submit.prevent="login"
|
||||||
>
|
>
|
||||||
<!-- ADMIN USERNAME -->
|
<!-- ADMIN USERNAME -->
|
||||||
<FormField v-bind="fields.username" v-model="form.username" :validation="$v.form.username" />
|
<FormField
|
||||||
|
v-bind="fields.username"
|
||||||
|
v-model="form.username"
|
||||||
|
:validation="$v.form.username"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- ADMIN PASSWORD -->
|
<!-- ADMIN PASSWORD -->
|
||||||
<FormField v-bind="fields.password" v-model="form.password" :validation="$v.form.password" />
|
<FormField
|
||||||
|
v-bind="fields.password"
|
||||||
|
v-model="form.password"
|
||||||
|
:validation="$v.form.password"
|
||||||
|
/>
|
||||||
|
|
||||||
<template #buttons>
|
<template #buttons>
|
||||||
<BButton
|
<BButton
|
||||||
type="submit" variant="success"
|
type="submit"
|
||||||
:disabled="!installed" form="ynh-form"
|
variant="success"
|
||||||
|
:disabled="!installed"
|
||||||
|
form="ynh-form"
|
||||||
>
|
>
|
||||||
{{ $t('login') }}
|
{{ $t('login') }}
|
||||||
</BButton>
|
</BButton>
|
||||||
|
@ -32,63 +44,68 @@ export default {
|
||||||
mixins: [validationMixin],
|
mixins: [validationMixin],
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
forceReload: { type: Boolean, default: false }
|
forceReload: { type: Boolean, default: false },
|
||||||
},
|
},
|
||||||
|
|
||||||
data () {
|
data() {
|
||||||
return {
|
return {
|
||||||
serverError: '',
|
serverError: '',
|
||||||
form: {
|
form: {
|
||||||
username: '',
|
username: '',
|
||||||
password: ''
|
password: '',
|
||||||
},
|
},
|
||||||
fields: {
|
fields: {
|
||||||
username: {
|
username: {
|
||||||
label: this.$i18n.t('user_username'),
|
label: this.$i18n.t('user_username'),
|
||||||
props: {
|
props: {
|
||||||
id: 'username',
|
id: 'username',
|
||||||
autocomplete: 'username'
|
autocomplete: 'username',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
password: {
|
password: {
|
||||||
label: this.$i18n.t('password'),
|
label: this.$i18n.t('password'),
|
||||||
props: {
|
props: {
|
||||||
id: 'password',
|
id: 'password',
|
||||||
type: 'password',
|
type: 'password',
|
||||||
autocomplete: 'current-password'
|
autocomplete: 'current-password',
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters(['installed'])
|
...mapGetters(['installed']),
|
||||||
},
|
},
|
||||||
|
|
||||||
validations () {
|
validations() {
|
||||||
return {
|
return {
|
||||||
form: {
|
form: {
|
||||||
username: { required, alphalownumdot_ },
|
username: { required, alphalownumdot_ },
|
||||||
password: { required, passwordLenght: minLength(4) }
|
password: { required, passwordLenght: minLength(4) },
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
login () {
|
login() {
|
||||||
const credentials = [this.form.username, this.form.password].join(':')
|
const credentials = [this.form.username, this.form.password].join(':')
|
||||||
this.$store.dispatch('LOGIN', credentials).then(() => {
|
this.$store
|
||||||
if (this.forceReload) {
|
.dispatch('LOGIN', credentials)
|
||||||
window.location.href = '/yunohost/admin/'
|
.then(() => {
|
||||||
} else {
|
if (this.forceReload) {
|
||||||
this.$router.push(this.$router.currentRoute.query.redirect || { name: 'home' })
|
window.location.href = '/yunohost/admin/'
|
||||||
}
|
} else {
|
||||||
}).catch(err => {
|
this.$router.push(
|
||||||
if (err.name !== 'APIUnauthorizedError') throw err
|
this.$router.currentRoute.query.redirect || { name: 'home' },
|
||||||
this.serverError = this.$i18n.t('wrong_password_or_username')
|
)
|
||||||
})
|
}
|
||||||
}
|
})
|
||||||
}
|
.catch((err) => {
|
||||||
|
if (err.name !== 'APIUnauthorizedError') throw err
|
||||||
|
this.serverError = this.$i18n.t('wrong_password_or_username')
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
<p class="alert alert-info">
|
<p class="alert alert-info">
|
||||||
<span v-t="'postinstall_intro_2'" />
|
<span v-t="'postinstall_intro_2'" />
|
||||||
<br>
|
<br />
|
||||||
<span v-html="$t('postinstall_intro_3')" />
|
<span v-html="$t('postinstall_intro_3')" />
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
@ -20,7 +20,9 @@
|
||||||
<!-- DOMAIN SETUP STEP -->
|
<!-- DOMAIN SETUP STEP -->
|
||||||
<template v-else-if="step === 'domain'">
|
<template v-else-if="step === 'domain'">
|
||||||
<DomainForm
|
<DomainForm
|
||||||
:title="$t('postinstall_set_domain')" :submit-text="$t('next')" :server-error="serverError"
|
:title="$t('postinstall_set_domain')"
|
||||||
|
:submit-text="$t('next')"
|
||||||
|
:server-error="serverError"
|
||||||
@submit="setDomain"
|
@submit="setDomain"
|
||||||
>
|
>
|
||||||
<template #disclaimer>
|
<template #disclaimer>
|
||||||
|
@ -36,9 +38,12 @@
|
||||||
<!-- FIRST USER SETUP STEP -->
|
<!-- FIRST USER SETUP STEP -->
|
||||||
<template v-else-if="step === 'user'">
|
<template v-else-if="step === 'user'">
|
||||||
<CardForm
|
<CardForm
|
||||||
:title="$t('postinstall.user.title')" icon="user-plus"
|
:title="$t('postinstall.user.title')"
|
||||||
:validation="$v" :server-error="serverError"
|
icon="user-plus"
|
||||||
:submit-text="$t('next')" @submit.prevent="setUser"
|
:validation="$v"
|
||||||
|
:server-error="serverError"
|
||||||
|
:submit-text="$t('next')"
|
||||||
|
@submit.prevent="setUser"
|
||||||
>
|
>
|
||||||
<ReadOnlyAlertItem
|
<ReadOnlyAlertItem
|
||||||
:label="$t('postinstall.user.first_user_help')"
|
:label="$t('postinstall.user.first_user_help')"
|
||||||
|
@ -46,8 +51,11 @@
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
v-for="(field, name) in fields" :key="name"
|
v-for="(field, name) in fields"
|
||||||
v-bind="field" v-model="user[name]" :validation="$v.user[name]"
|
:key="name"
|
||||||
|
v-bind="field"
|
||||||
|
v-model="user[name]"
|
||||||
|
:validation="$v.user[name]"
|
||||||
/>
|
/>
|
||||||
</CardForm>
|
</CardForm>
|
||||||
|
|
||||||
|
@ -87,7 +95,13 @@ import api from '@/api'
|
||||||
import { DomainForm } from '@/views/_partials'
|
import { DomainForm } from '@/views/_partials'
|
||||||
import LoginView from '@/views/LoginView.vue'
|
import LoginView from '@/views/LoginView.vue'
|
||||||
import { formatFormData } from '@/helpers/yunohostArguments'
|
import { formatFormData } from '@/helpers/yunohostArguments'
|
||||||
import { alphalownumdot_, required, minLength, name, sameAs } from '@/helpers/validators'
|
import {
|
||||||
|
alphalownumdot_,
|
||||||
|
required,
|
||||||
|
minLength,
|
||||||
|
name,
|
||||||
|
sameAs,
|
||||||
|
} from '@/helpers/validators'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'PostInstall',
|
name: 'PostInstall',
|
||||||
|
@ -96,10 +110,10 @@ export default {
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
DomainForm,
|
DomainForm,
|
||||||
LoginView
|
LoginView,
|
||||||
},
|
},
|
||||||
|
|
||||||
data () {
|
data() {
|
||||||
return {
|
return {
|
||||||
step: 'start',
|
step: 'start',
|
||||||
serverError: '',
|
serverError: '',
|
||||||
|
@ -109,98 +123,110 @@ export default {
|
||||||
username: '',
|
username: '',
|
||||||
fullname: '',
|
fullname: '',
|
||||||
password: '',
|
password: '',
|
||||||
confirmation: ''
|
confirmation: '',
|
||||||
},
|
},
|
||||||
|
|
||||||
fields: {
|
fields: {
|
||||||
username: {
|
username: {
|
||||||
label: this.$i18n.t('user_username'),
|
label: this.$i18n.t('user_username'),
|
||||||
props: { id: 'username', placeholder: this.$i18n.t('placeholder.username') }
|
props: {
|
||||||
|
id: 'username',
|
||||||
|
placeholder: this.$i18n.t('placeholder.username'),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
fullname: {
|
fullname: {
|
||||||
label: this.$i18n.t('user_fullname'),
|
label: this.$i18n.t('user_fullname'),
|
||||||
props: { id: 'fullname', placeholder: this.$i18n.t('placeholder.fullname') }
|
props: {
|
||||||
|
id: 'fullname',
|
||||||
|
placeholder: this.$i18n.t('placeholder.fullname'),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
password: {
|
password: {
|
||||||
label: this.$i18n.t('password'),
|
label: this.$i18n.t('password'),
|
||||||
description: this.$i18n.t('good_practices_about_admin_password'),
|
description: this.$i18n.t('good_practices_about_admin_password'),
|
||||||
descriptionVariant: 'warning',
|
descriptionVariant: 'warning',
|
||||||
props: { id: 'password', placeholder: '••••••••', type: 'password' }
|
props: { id: 'password', placeholder: '••••••••', type: 'password' },
|
||||||
},
|
},
|
||||||
|
|
||||||
confirmation: {
|
confirmation: {
|
||||||
label: this.$i18n.t('password_confirmation'),
|
label: this.$i18n.t('password_confirmation'),
|
||||||
props: { id: 'confirmation', placeholder: '••••••••', type: 'password' }
|
props: {
|
||||||
}
|
id: 'confirmation',
|
||||||
}
|
placeholder: '••••••••',
|
||||||
|
type: 'password',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
goToStep (step) {
|
goToStep(step) {
|
||||||
this.serverError = ''
|
this.serverError = ''
|
||||||
this.step = step
|
this.step = step
|
||||||
},
|
},
|
||||||
|
|
||||||
setDomain ({ domain, dyndns_recovery_password }) {
|
setDomain({ domain, dyndns_recovery_password }) {
|
||||||
this.domain = domain
|
this.domain = domain
|
||||||
this.dyndns_recovery_password = dyndns_recovery_password
|
this.dyndns_recovery_password = dyndns_recovery_password
|
||||||
this.goToStep('user')
|
this.goToStep('user')
|
||||||
},
|
},
|
||||||
|
|
||||||
async setUser () {
|
async setUser() {
|
||||||
const confirmed = await this.$askConfirmation(
|
const confirmed = await this.$askConfirmation(
|
||||||
this.$i18n.t('confirm_postinstall', { domain: this.domain })
|
this.$i18n.t('confirm_postinstall', { domain: this.domain }),
|
||||||
)
|
)
|
||||||
if (!confirmed) return
|
if (!confirmed) return
|
||||||
this.performPostInstall()
|
this.performPostInstall()
|
||||||
},
|
},
|
||||||
|
|
||||||
async performPostInstall (force = false) {
|
async performPostInstall(force = false) {
|
||||||
const data = await formatFormData({
|
const data = await formatFormData({
|
||||||
domain: this.domain,
|
domain: this.domain,
|
||||||
dyndns_recovery_password: this.dyndns_recovery_password,
|
dyndns_recovery_password: this.dyndns_recovery_password,
|
||||||
username: this.user.username,
|
username: this.user.username,
|
||||||
fullname: this.user.fullname,
|
fullname: this.user.fullname,
|
||||||
password: this.user.password
|
password: this.user.password,
|
||||||
})
|
})
|
||||||
|
|
||||||
// FIXME does the api will throw an error for bad passwords ?
|
// FIXME does the api will throw an error for bad passwords ?
|
||||||
api.post(
|
api
|
||||||
'postinstall' + (force ? '?force_diskspace' : ''),
|
.post('postinstall' + (force ? '?force_diskspace' : ''), data, {
|
||||||
data,
|
key: 'postinstall',
|
||||||
{ key: 'postinstall' }
|
})
|
||||||
).then(() => {
|
.then(() => {
|
||||||
// Display success message and allow the user to login
|
// Display success message and allow the user to login
|
||||||
this.goToStep('login')
|
this.goToStep('login')
|
||||||
}).catch(err => {
|
})
|
||||||
const hasWordsInError = (words) => words.some((word) => (err.key || err.message).includes(word))
|
.catch((err) => {
|
||||||
if (err.name !== 'APIBadRequestError') throw err
|
const hasWordsInError = (words) =>
|
||||||
if (err.key === 'postinstall_low_rootfsspace') {
|
words.some((word) => (err.key || err.message).includes(word))
|
||||||
this.step = 'rootfsspace-error'
|
if (err.name !== 'APIBadRequestError') throw err
|
||||||
} else if (hasWordsInError(['domain', 'dyndns'])) {
|
if (err.key === 'postinstall_low_rootfsspace') {
|
||||||
this.step = 'domain'
|
this.step = 'rootfsspace-error'
|
||||||
} else if (hasWordsInError(['password', 'user'])) {
|
} else if (hasWordsInError(['domain', 'dyndns'])) {
|
||||||
this.step = 'user'
|
this.step = 'domain'
|
||||||
} else {
|
} else if (hasWordsInError(['password', 'user'])) {
|
||||||
throw err
|
this.step = 'user'
|
||||||
}
|
} else {
|
||||||
this.serverError = err.message
|
throw err
|
||||||
})
|
}
|
||||||
}
|
this.serverError = err.message
|
||||||
|
})
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
validations () {
|
validations() {
|
||||||
return {
|
return {
|
||||||
user: {
|
user: {
|
||||||
username: { required, alphalownumdot_ },
|
username: { required, alphalownumdot_ },
|
||||||
fullname: { required, name },
|
fullname: { required, name },
|
||||||
password: { required, passwordLenght: minLength(8) },
|
password: { required, passwordLenght: minLength(8) },
|
||||||
confirmation: { required, passwordMatch: sameAs('password') }
|
confirmation: { required, passwordMatch: sameAs('password') },
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
<template>
|
<template>
|
||||||
<CardForm
|
<CardForm
|
||||||
:title="title" icon="globe" :submit-text="submitText"
|
:title="title"
|
||||||
:validation="$v" :server-error="serverError"
|
icon="globe"
|
||||||
|
:submit-text="submitText"
|
||||||
|
:validation="$v"
|
||||||
|
:server-error="serverError"
|
||||||
@submit.prevent="onSubmit"
|
@submit.prevent="onSubmit"
|
||||||
>
|
>
|
||||||
<template #disclaimer>
|
<template #disclaimer>
|
||||||
|
@ -9,7 +12,9 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<BFormRadio
|
<BFormRadio
|
||||||
v-model="selected" name="domain-type" value="domain"
|
v-model="selected"
|
||||||
|
name="domain-type"
|
||||||
|
value="domain"
|
||||||
:class="domainIsVisible ? null : 'collapsed'"
|
:class="domainIsVisible ? null : 'collapsed'"
|
||||||
:aria-expanded="domainIsVisible ? 'true' : 'false'"
|
:aria-expanded="domainIsVisible ? 'true' : 'false'"
|
||||||
aria-controls="collapse-domain"
|
aria-controls="collapse-domain"
|
||||||
|
@ -25,13 +30,17 @@
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
v-bind="fields.domain" v-model="form.domain"
|
v-bind="fields.domain"
|
||||||
:validation="$v.form.domain" class="mt-3"
|
v-model="form.domain"
|
||||||
|
:validation="$v.form.domain"
|
||||||
|
class="mt-3"
|
||||||
/>
|
/>
|
||||||
</BCollapse>
|
</BCollapse>
|
||||||
|
|
||||||
<BFormRadio
|
<BFormRadio
|
||||||
v-model="selected" name="domain-type" value="dynDomain"
|
v-model="selected"
|
||||||
|
name="domain-type"
|
||||||
|
value="dynDomain"
|
||||||
:disabled="dynDnsForbiden"
|
:disabled="dynDnsForbiden"
|
||||||
:class="dynDomainIsVisible ? null : 'collapsed'"
|
:class="dynDomainIsVisible ? null : 'collapsed'"
|
||||||
:aria-expanded="dynDomainIsVisible ? 'true' : 'false'"
|
:aria-expanded="dynDomainIsVisible ? 'true' : 'false'"
|
||||||
|
@ -47,7 +56,11 @@
|
||||||
<span class="pl-1" v-html="$t('domain.add.from_yunohost_desc')" />
|
<span class="pl-1" v-html="$t('domain.add.from_yunohost_desc')" />
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<FormField v-bind="fields.dynDomain" :validation="$v.form.dynDomain" class="mt-3">
|
<FormField
|
||||||
|
v-bind="fields.dynDomain"
|
||||||
|
:validation="$v.form.dynDomain"
|
||||||
|
class="mt-3"
|
||||||
|
>
|
||||||
<template #default="{ self }">
|
<template #default="{ self }">
|
||||||
<AdressInputSelect v-bind="self" v-model="form.dynDomain" />
|
<AdressInputSelect v-bind="self" v-model="form.dynDomain" />
|
||||||
</template>
|
</template>
|
||||||
|
@ -65,10 +78,16 @@
|
||||||
v-model="form.dynDomainPasswordConfirmation"
|
v-model="form.dynDomainPasswordConfirmation"
|
||||||
/>
|
/>
|
||||||
</BCollapse>
|
</BCollapse>
|
||||||
<div v-if="dynDnsForbiden" class="alert alert-warning mt-2" v-html="$t('domain_add_dyndns_forbidden')" />
|
<div
|
||||||
|
v-if="dynDnsForbiden"
|
||||||
|
class="alert alert-warning mt-2"
|
||||||
|
v-html="$t('domain_add_dyndns_forbidden')"
|
||||||
|
/>
|
||||||
|
|
||||||
<BFormRadio
|
<BFormRadio
|
||||||
v-model="selected" name="domain-type" value="localDomain"
|
v-model="selected"
|
||||||
|
name="domain-type"
|
||||||
|
value="localDomain"
|
||||||
:class="localDomainIsVisible ? null : 'collapsed'"
|
:class="localDomainIsVisible ? null : 'collapsed'"
|
||||||
:aria-expanded="localDomainIsVisible ? 'true' : 'false'"
|
:aria-expanded="localDomainIsVisible ? 'true' : 'false'"
|
||||||
aria-controls="collapse-localDomain"
|
aria-controls="collapse-localDomain"
|
||||||
|
@ -82,7 +101,11 @@
|
||||||
<span class="pl-1" v-html="$t('domain.add.from_local_desc')" />
|
<span class="pl-1" v-html="$t('domain.add.from_local_desc')" />
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<FormField v-bind="fields.localDomain" :validation="$v.form.localDomain" class="mt-3">
|
<FormField
|
||||||
|
v-bind="fields.localDomain"
|
||||||
|
:validation="$v.form.localDomain"
|
||||||
|
class="mt-3"
|
||||||
|
>
|
||||||
<template #default="{ self }">
|
<template #default="{ self }">
|
||||||
<AdressInputSelect v-bind="self" v-model="form.localDomain" />
|
<AdressInputSelect v-bind="self" v-model="form.localDomain" />
|
||||||
</template>
|
</template>
|
||||||
|
@ -97,7 +120,13 @@ import { validationMixin } from 'vuelidate'
|
||||||
|
|
||||||
import AdressInputSelect from '@/components/AdressInputSelect.vue'
|
import AdressInputSelect from '@/components/AdressInputSelect.vue'
|
||||||
import { formatFormData } from '@/helpers/yunohostArguments'
|
import { formatFormData } from '@/helpers/yunohostArguments'
|
||||||
import { required, domain, dynDomain, minLength, sameAs } from '@/helpers/validators'
|
import {
|
||||||
|
required,
|
||||||
|
domain,
|
||||||
|
dynDomain,
|
||||||
|
minLength,
|
||||||
|
sameAs,
|
||||||
|
} from '@/helpers/validators'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'DomainForm',
|
name: 'DomainForm',
|
||||||
|
@ -105,10 +134,10 @@ export default {
|
||||||
props: {
|
props: {
|
||||||
title: { type: String, required: true },
|
title: { type: String, required: true },
|
||||||
submitText: { type: String, default: null },
|
submitText: { type: String, default: null },
|
||||||
serverError: { type: String, default: '' }
|
serverError: { type: String, default: '' },
|
||||||
},
|
},
|
||||||
|
|
||||||
data () {
|
data() {
|
||||||
return {
|
return {
|
||||||
selected: '',
|
selected: '',
|
||||||
|
|
||||||
|
@ -117,7 +146,7 @@ export default {
|
||||||
dynDomain: { localPart: '', separator: '.', domain: 'nohost.me' },
|
dynDomain: { localPart: '', separator: '.', domain: 'nohost.me' },
|
||||||
dynDomainPassword: '',
|
dynDomainPassword: '',
|
||||||
dynDomainPasswordConfirmation: '',
|
dynDomainPasswordConfirmation: '',
|
||||||
localDomain: { localPart: '', separator: '.', domain: 'local' }
|
localDomain: { localPart: '', separator: '.', domain: 'local' },
|
||||||
},
|
},
|
||||||
|
|
||||||
fields: {
|
fields: {
|
||||||
|
@ -125,8 +154,8 @@ export default {
|
||||||
label: this.$i18n.t('domain_name'),
|
label: this.$i18n.t('domain_name'),
|
||||||
props: {
|
props: {
|
||||||
id: 'domain',
|
id: 'domain',
|
||||||
placeholder: this.$i18n.t('placeholder.domain')
|
placeholder: this.$i18n.t('placeholder.domain'),
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
dynDomain: {
|
dynDomain: {
|
||||||
|
@ -135,8 +164,8 @@ export default {
|
||||||
id: 'dyn-domain',
|
id: 'dyn-domain',
|
||||||
placeholder: this.$i18n.t('placeholder.domain').split('.')[0],
|
placeholder: this.$i18n.t('placeholder.domain').split('.')[0],
|
||||||
type: 'domain',
|
type: 'domain',
|
||||||
choices: ['nohost.me', 'noho.st', 'ynh.fr']
|
choices: ['nohost.me', 'noho.st', 'ynh.fr'],
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
dynDomainPassword: {
|
dynDomainPassword: {
|
||||||
|
@ -145,8 +174,8 @@ export default {
|
||||||
props: {
|
props: {
|
||||||
id: 'dyn-dns-password',
|
id: 'dyn-dns-password',
|
||||||
placeholder: '••••••••',
|
placeholder: '••••••••',
|
||||||
type: 'password'
|
type: 'password',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
dynDomainPasswordConfirmation: {
|
dynDomainPasswordConfirmation: {
|
||||||
|
@ -154,8 +183,8 @@ export default {
|
||||||
props: {
|
props: {
|
||||||
id: 'dyn-dns-password-confirmation',
|
id: 'dyn-dns-password-confirmation',
|
||||||
placeholder: '••••••••',
|
placeholder: '••••••••',
|
||||||
type: 'password'
|
type: 'password',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
localDomain: {
|
localDomain: {
|
||||||
|
@ -164,68 +193,70 @@ export default {
|
||||||
id: 'dyn-domain',
|
id: 'dyn-domain',
|
||||||
placeholder: this.$i18n.t('placeholder.domain').split('.')[0],
|
placeholder: this.$i18n.t('placeholder.domain').split('.')[0],
|
||||||
type: 'domain',
|
type: 'domain',
|
||||||
choices: ['local', 'test']
|
choices: ['local', 'test'],
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters(['domains']),
|
...mapGetters(['domains']),
|
||||||
|
|
||||||
dynDnsForbiden () {
|
dynDnsForbiden() {
|
||||||
if (!this.domains) return false
|
if (!this.domains) return false
|
||||||
const dynDomains = this.fields.dynDomain.props.choices
|
const dynDomains = this.fields.dynDomain.props.choices
|
||||||
return this.domains.some(domain => {
|
return this.domains.some((domain) => {
|
||||||
return dynDomains.some(dynDomain => domain.includes(dynDomain))
|
return dynDomains.some((dynDomain) => domain.includes(dynDomain))
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
domainIsVisible () {
|
domainIsVisible() {
|
||||||
return this.selected === 'domain'
|
return this.selected === 'domain'
|
||||||
},
|
},
|
||||||
|
|
||||||
dynDomainIsVisible () {
|
dynDomainIsVisible() {
|
||||||
return this.selected === 'dynDomain'
|
return this.selected === 'dynDomain'
|
||||||
},
|
},
|
||||||
|
|
||||||
localDomainIsVisible () {
|
localDomainIsVisible() {
|
||||||
return this.selected === 'localDomain'
|
return this.selected === 'localDomain'
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
validations () {
|
validations() {
|
||||||
return {
|
return {
|
||||||
selected: { required },
|
selected: { required },
|
||||||
form: ['domain', 'localDomain'].includes(this.selected)
|
form: ['domain', 'localDomain'].includes(this.selected)
|
||||||
? {
|
? {
|
||||||
[this.selected]: this.selected === 'domain'
|
[this.selected]:
|
||||||
? { required, domain }
|
this.selected === 'domain'
|
||||||
: { localPart: { required, dynDomain } }
|
? { required, domain }
|
||||||
}
|
: { localPart: { required, dynDomain } },
|
||||||
|
}
|
||||||
: {
|
: {
|
||||||
dynDomain: { localPart: { required, dynDomain } },
|
dynDomain: { localPart: { required, dynDomain } },
|
||||||
dynDomainPassword: { passwordLenght: minLength(8) },
|
dynDomainPassword: { passwordLenght: minLength(8) },
|
||||||
dynDomainPasswordConfirmation: { passwordMatch: sameAs('dynDomainPassword') }
|
dynDomainPasswordConfirmation: {
|
||||||
}
|
passwordMatch: sameAs('dynDomainPassword'),
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
async onSubmit () {
|
async onSubmit() {
|
||||||
const domainType = this.selected
|
const domainType = this.selected
|
||||||
const form = await formatFormData({
|
const form = await formatFormData({
|
||||||
domain: this.form[domainType],
|
domain: this.form[domainType],
|
||||||
dyndns_recovery_password: domainType === 'dynDomain'
|
dyndns_recovery_password:
|
||||||
? this.form.dynDomainPassword
|
domainType === 'dynDomain' ? this.form.dynDomainPassword : '',
|
||||||
: ''
|
|
||||||
})
|
})
|
||||||
this.$emit('submit', form)
|
this.$emit('submit', form)
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
created () {
|
created() {
|
||||||
if (this.dynDnsForbiden) {
|
if (this.dynDnsForbiden) {
|
||||||
this.selected = 'domain'
|
this.selected = 'domain'
|
||||||
}
|
}
|
||||||
|
@ -234,7 +265,7 @@ export default {
|
||||||
mixins: [validationMixin],
|
mixins: [validationMixin],
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
AdressInputSelect
|
AdressInputSelect,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -8,15 +8,17 @@
|
||||||
|
|
||||||
<div class="alert alert-info my-3">
|
<div class="alert alert-info my-3">
|
||||||
<span v-html="$t('api_error.help')" />
|
<span v-html="$t('api_error.help')" />
|
||||||
<br>{{ $t('api_error.info') }}
|
<br />{{ $t('api_error.info') }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- FIXME USE DD DL DT -->
|
<!-- FIXME USE DD DL DT -->
|
||||||
<p class="m-0">
|
<p class="m-0">
|
||||||
<strong v-t="'error'" />: <code>"{{ error.code }}" {{ error.status }}</code>
|
<strong v-t="'error'" />:
|
||||||
|
<code>"{{ error.code }}" {{ error.status }}</code>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<strong v-t="'action'" />: <code>"{{ error.method }}" {{ error.path }}</code>
|
<strong v-t="'action'" />:
|
||||||
|
<code>"{{ error.method }}" {{ error.path }}</code>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
|
@ -43,10 +45,7 @@
|
||||||
|
|
||||||
<BCardFooter footer-bg-variant="danger">
|
<BCardFooter footer-bg-variant="danger">
|
||||||
<!-- TODO add copy error ? -->
|
<!-- TODO add copy error ? -->
|
||||||
<BButton
|
<BButton variant="light" size="sm" v-t="'ok'" @click="dismiss" />
|
||||||
variant="light" size="sm"
|
|
||||||
v-t="'ok'" @click="dismiss"
|
|
||||||
/>
|
|
||||||
</BCardFooter>
|
</BCardFooter>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -58,35 +57,36 @@ export default {
|
||||||
name: 'ErrorDisplay',
|
name: 'ErrorDisplay',
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
MessageListGroup
|
MessageListGroup,
|
||||||
},
|
},
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
request: { type: [Object, null], default: null }
|
request: { type: [Object, null], default: null },
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
error () {
|
error() {
|
||||||
return this.request.error
|
return this.request.error
|
||||||
},
|
},
|
||||||
|
|
||||||
messages () {
|
messages() {
|
||||||
const messages = this.request.messages
|
const messages = this.request.messages
|
||||||
if (messages && messages.length > 0) return messages
|
if (messages && messages.length > 0) return messages
|
||||||
return null
|
return null
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
dismiss () {
|
dismiss() {
|
||||||
this.$store.dispatch('DISMISS_ERROR', this.request)
|
this.$store.dispatch('DISMISS_ERROR', this.request)
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
code, pre code {
|
code,
|
||||||
|
pre code {
|
||||||
color: $black;
|
color: $black;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -2,22 +2,29 @@
|
||||||
<BCard no-body id="console">
|
<BCard no-body id="console">
|
||||||
<!-- HISTORY BAR -->
|
<!-- HISTORY BAR -->
|
||||||
<BCardHeader
|
<BCardHeader
|
||||||
role="button" tabindex="0"
|
role="button"
|
||||||
:aria-expanded="open ? 'true' : 'false'" aria-controls="console-collapse"
|
tabindex="0"
|
||||||
header-tag="header" :header-bg-variant="open ? 'best' : 'white'"
|
:aria-expanded="open ? 'true' : 'false'"
|
||||||
|
aria-controls="console-collapse"
|
||||||
|
header-tag="header"
|
||||||
|
:header-bg-variant="open ? 'best' : 'white'"
|
||||||
:class="{ 'text-white': open }"
|
:class="{ 'text-white': open }"
|
||||||
class="d-flex align-items-center"
|
class="d-flex align-items-center"
|
||||||
@mousedown.left.prevent="onHistoryBarClick"
|
@mousedown.left.prevent="onHistoryBarClick"
|
||||||
@keyup.space.enter.prevent="onHistoryBarKey"
|
@keyup.space.enter.prevent="onHistoryBarKey"
|
||||||
>
|
>
|
||||||
<h5 class="m-0">
|
<h5 class="m-0">
|
||||||
<YIcon iname="history" /> <span class="d-none d-sm-inline font-weight-bold">{{ $t('history.title') }}</span>
|
<YIcon iname="history" />
|
||||||
|
<span class="d-none d-sm-inline font-weight-bold">
|
||||||
|
{{ $t('history.title') }}
|
||||||
|
</span>
|
||||||
</h5>
|
</h5>
|
||||||
|
|
||||||
<!-- CURRENT/LAST ACTION -->
|
<!-- CURRENT/LAST ACTION -->
|
||||||
<BButton
|
<BButton
|
||||||
v-if="lastAction"
|
v-if="lastAction"
|
||||||
size="sm" pill
|
size="sm"
|
||||||
|
pill
|
||||||
class="ml-auto py-0"
|
class="ml-auto py-0"
|
||||||
:variant="open ? 'light' : 'best'"
|
:variant="open ? 'light' : 'best'"
|
||||||
@click.prevent="onLastActionClick"
|
@click.prevent="onLastActionClick"
|
||||||
|
@ -25,36 +32,49 @@
|
||||||
>
|
>
|
||||||
<small>{{ $t('history.last_action') }}</small>
|
<small>{{ $t('history.last_action') }}</small>
|
||||||
</BButton>
|
</BButton>
|
||||||
<QueryHeader v-if="lastAction" :request="lastAction" class="w-auto ml-2 xs-hide" />
|
<QueryHeader
|
||||||
|
v-if="lastAction"
|
||||||
|
:request="lastAction"
|
||||||
|
class="w-auto ml-2 xs-hide"
|
||||||
|
/>
|
||||||
</BCardHeader>
|
</BCardHeader>
|
||||||
|
|
||||||
<BCollapse id="console-collapse" v-model="open">
|
<BCollapse id="console-collapse" v-model="open">
|
||||||
<div
|
<div class="accordion" role="tablist" id="history" ref="history">
|
||||||
class="accordion" role="tablist"
|
|
||||||
id="history" ref="history"
|
|
||||||
>
|
|
||||||
<p v-if="history.length === 0" class="alert m-0 px-2 py-1">
|
<p v-if="history.length === 0" class="alert m-0 px-2 py-1">
|
||||||
{{ $t('history.is_empty') }}
|
{{ $t('history.is_empty') }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<!-- ACTION LIST -->
|
<!-- ACTION LIST -->
|
||||||
<BCard
|
<BCard
|
||||||
v-for="(action, i) in history" :key="i"
|
v-for="(action, i) in history"
|
||||||
no-body class="rounded-0 rounded-top border-left-0 border-right-0"
|
:key="i"
|
||||||
|
no-body
|
||||||
|
class="rounded-0 rounded-top border-left-0 border-right-0"
|
||||||
>
|
>
|
||||||
<!-- ACTION -->
|
<!-- ACTION -->
|
||||||
<BCardHeader header-tag="header" header-bg-variant="white" class="sticky-top d-flex">
|
<BCardHeader
|
||||||
|
header-tag="header"
|
||||||
|
header-bg-variant="white"
|
||||||
|
class="sticky-top d-flex"
|
||||||
|
>
|
||||||
<!-- ACTION DESC -->
|
<!-- ACTION DESC -->
|
||||||
<QueryHeader
|
<QueryHeader
|
||||||
role="tab" v-b-toggle="action.messages.length ? 'messages-collapse-' + i : false"
|
role="tab"
|
||||||
:request="action" show-time show-error
|
v-b-toggle="
|
||||||
|
action.messages.length ? 'messages-collapse-' + i : false
|
||||||
|
"
|
||||||
|
:request="action"
|
||||||
|
show-time
|
||||||
|
show-error
|
||||||
/>
|
/>
|
||||||
</BCardHeader>
|
</BCardHeader>
|
||||||
|
|
||||||
<!-- ACTION MESSAGES -->
|
<!-- ACTION MESSAGES -->
|
||||||
<BCollapse
|
<BCollapse
|
||||||
v-if="action.messages.length"
|
v-if="action.messages.length"
|
||||||
:id="'messages-collapse-' + i" accordion="my-accordion"
|
:id="'messages-collapse-' + i"
|
||||||
|
accordion="my-accordion"
|
||||||
role="tabpanel"
|
role="tabpanel"
|
||||||
@shown="scrollToAction(i)"
|
@shown="scrollToAction(i)"
|
||||||
@hide="scrollToAction(i)"
|
@hide="scrollToAction(i)"
|
||||||
|
@ -78,33 +98,35 @@ export default {
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
QueryHeader,
|
QueryHeader,
|
||||||
MessageListGroup
|
MessageListGroup,
|
||||||
},
|
},
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
value: { type: Boolean, default: false },
|
value: { type: Boolean, default: false },
|
||||||
height: { type: [Number, String], default: 30 }
|
height: { type: [Number, String], default: 30 },
|
||||||
},
|
},
|
||||||
|
|
||||||
data () {
|
data() {
|
||||||
return {
|
return {
|
||||||
open: false
|
open: false,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters(['history', 'lastAction', 'waiting', 'error'])
|
...mapGetters(['history', 'lastAction', 'waiting', 'error']),
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
scrollToAction (actionIndex) {
|
scrollToAction(actionIndex) {
|
||||||
const actionCard = this.$el.querySelector('#messages-collapse-' + actionIndex).parentElement
|
const actionCard = this.$el.querySelector(
|
||||||
|
'#messages-collapse-' + actionIndex,
|
||||||
|
).parentElement
|
||||||
const headerOffset = actionCard.firstElementChild.offsetHeight
|
const headerOffset = actionCard.firstElementChild.offsetHeight
|
||||||
// Can't use `scrollIntoView()` here since it will also scroll in the main content.
|
// Can't use `scrollIntoView()` here since it will also scroll in the main content.
|
||||||
this.$refs.history.scrollTop = actionCard.offsetTop - headerOffset
|
this.$refs.history.scrollTop = actionCard.offsetTop - headerOffset
|
||||||
},
|
},
|
||||||
|
|
||||||
async onLastActionClick () {
|
async onLastActionClick() {
|
||||||
if (!this.open) {
|
if (!this.open) {
|
||||||
this.open = true
|
this.open = true
|
||||||
await this.$nextTick()
|
await this.$nextTick()
|
||||||
|
@ -122,15 +144,23 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onHistoryBarKey (e) {
|
onHistoryBarKey(e) {
|
||||||
// FIXME interactive element in another is not valid, need to find another way.
|
// FIXME interactive element in another is not valid, need to find another way.
|
||||||
if (e.target.nodeName === 'BUTTON' || e.target.parentElement.nodeName === 'BUTTON') return
|
if (
|
||||||
|
e.target.nodeName === 'BUTTON' ||
|
||||||
|
e.target.parentElement.nodeName === 'BUTTON'
|
||||||
|
)
|
||||||
|
return
|
||||||
this.open = !this.open
|
this.open = !this.open
|
||||||
},
|
},
|
||||||
|
|
||||||
onHistoryBarClick (e) {
|
onHistoryBarClick(e) {
|
||||||
// FIXME interactive element in another is not valid, need to find another way.
|
// FIXME interactive element in another is not valid, need to find another way.
|
||||||
if (e.target.nodeName === 'BUTTON' || e.target.parentElement.nodeName === 'BUTTON') return
|
if (
|
||||||
|
e.target.nodeName === 'BUTTON' ||
|
||||||
|
e.target.parentElement.nodeName === 'BUTTON'
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
const historyElem = this.$refs.history
|
const historyElem = this.$refs.history
|
||||||
let mousePos = e.clientY
|
let mousePos = e.clientY
|
||||||
|
@ -180,8 +210,8 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('mouseup', onMouseUp)
|
window.addEventListener('mouseup', onMouseUp)
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -207,7 +237,6 @@ export default {
|
||||||
border-bottom-left-radius: 0;
|
border-bottom-left-radius: 0;
|
||||||
font-size: $font-size-sm;
|
font-size: $font-size-sm;
|
||||||
|
|
||||||
|
|
||||||
& > header {
|
& > header {
|
||||||
cursor: ns-resize;
|
cursor: ns-resize;
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,9 @@
|
||||||
|
|
||||||
<div class="d-flex justify-content-end">
|
<div class="d-flex justify-content-end">
|
||||||
<BButton
|
<BButton
|
||||||
variant="success" v-t="'retry'" class="ml-auto"
|
variant="success"
|
||||||
|
v-t="'retry'"
|
||||||
|
class="ml-auto"
|
||||||
@click="tryToReconnect()"
|
@click="tryToReconnect()"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -40,39 +42,41 @@ import { mapGetters } from 'vuex'
|
||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
import LoginView from '@/views/LoginView.vue'
|
import LoginView from '@/views/LoginView.vue'
|
||||||
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ReconnectingDisplay',
|
name: 'ReconnectingDisplay',
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
LoginView
|
LoginView,
|
||||||
},
|
},
|
||||||
|
|
||||||
data () {
|
data() {
|
||||||
return {
|
return {
|
||||||
status: 'reconnecting',
|
status: 'reconnecting',
|
||||||
origin: undefined
|
origin: undefined,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters(['reconnecting'])
|
...mapGetters(['reconnecting']),
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
tryToReconnect (initialDelay = 0) {
|
tryToReconnect(initialDelay = 0) {
|
||||||
this.status = 'reconnecting'
|
this.status = 'reconnecting'
|
||||||
api.tryToReconnect({ ...this.reconnecting, initialDelay }).then(() => {
|
api
|
||||||
this.status = 'success'
|
.tryToReconnect({ ...this.reconnecting, initialDelay })
|
||||||
}).catch(() => {
|
.then(() => {
|
||||||
this.status = 'failed'
|
this.status = 'success'
|
||||||
})
|
})
|
||||||
}
|
.catch(() => {
|
||||||
|
this.status = 'failed'
|
||||||
|
})
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
created () {
|
created() {
|
||||||
this.origin = this.reconnecting.origin || 'unknown'
|
this.origin = this.reconnecting.origin || 'unknown'
|
||||||
this.tryToReconnect(this.reconnecting.initialDelay)
|
this.tryToReconnect(this.reconnecting.initialDelay)
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<BOverlay
|
<BOverlay
|
||||||
variant="white" opacity="0.75"
|
variant="white"
|
||||||
|
opacity="0.75"
|
||||||
no-center
|
no-center
|
||||||
:show="waiting || reconnecting || error !== null"
|
:show="waiting || reconnecting || error !== null"
|
||||||
>
|
>
|
||||||
|
@ -20,7 +21,12 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex'
|
import { mapGetters } from 'vuex'
|
||||||
import { ErrorDisplay, WarningDisplay, WaitingDisplay, ReconnectingDisplay } from '@/views/_partials'
|
import {
|
||||||
|
ErrorDisplay,
|
||||||
|
WarningDisplay,
|
||||||
|
WaitingDisplay,
|
||||||
|
ReconnectingDisplay,
|
||||||
|
} from '@/views/_partials'
|
||||||
import QueryHeader from '@/components/QueryHeader.vue'
|
import QueryHeader from '@/components/QueryHeader.vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
@ -31,13 +37,13 @@ export default {
|
||||||
WarningDisplay,
|
WarningDisplay,
|
||||||
WaitingDisplay,
|
WaitingDisplay,
|
||||||
ReconnectingDisplay,
|
ReconnectingDisplay,
|
||||||
QueryHeader
|
QueryHeader,
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters(['waiting', 'reconnecting', 'error', 'currentRequest']),
|
...mapGetters(['waiting', 'reconnecting', 'error', 'currentRequest']),
|
||||||
|
|
||||||
component () {
|
component() {
|
||||||
const { error, reconnecting, currentRequest: request } = this
|
const { error, reconnecting, currentRequest: request } = this
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
|
@ -49,8 +55,8 @@ export default {
|
||||||
} else {
|
} else {
|
||||||
return { name: 'WaitingDisplay', request }
|
return { name: 'WaitingDisplay', request }
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -81,14 +87,14 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-footer {
|
.card-footer {
|
||||||
padding: .5rem .75rem;
|
padding: 0.5rem 0.75rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-header {
|
.card-header {
|
||||||
padding: .5rem .75rem;
|
padding: 0.5rem 0.75rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
<template>
|
<template>
|
||||||
<!-- This card receives style from `ViewLockOverlay` if used inside it -->
|
<!-- This card receives style from `ViewLockOverlay` if used inside it -->
|
||||||
<BCardBody>
|
<BCardBody>
|
||||||
<BCardTitle class="text-center mt-4" v-t="hasMessages ? 'api.processing' : 'api_waiting'" />
|
<BCardTitle
|
||||||
|
class="text-center mt-4"
|
||||||
|
v-t="hasMessages ? 'api.processing' : 'api_waiting'"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- PROGRESS BAR -->
|
<!-- PROGRESS BAR -->
|
||||||
<BProgress
|
<BProgress v-if="progress" class="my-4" :max="progress.max" height=".5rem">
|
||||||
v-if="progress" class="my-4"
|
|
||||||
:max="progress.max" height=".5rem"
|
|
||||||
>
|
|
||||||
<BProgressBar variant="success" :value="progress.values[0]" />
|
<BProgressBar variant="success" :value="progress.values[0]" />
|
||||||
<BProgressBar variant="warning" :value="progress.values[1]" animated />
|
<BProgressBar variant="warning" :value="progress.values[1]" animated />
|
||||||
<BProgressBar variant="secondary" :value="progress.values[2]" striped />
|
<BProgressBar variant="secondary" :value="progress.values[2]" striped />
|
||||||
|
@ -16,8 +16,11 @@
|
||||||
<YSpinner v-else class="my-4" />
|
<YSpinner v-else class="my-4" />
|
||||||
|
|
||||||
<MessageListGroup
|
<MessageListGroup
|
||||||
v-if="hasMessages" :messages="request.messages"
|
v-if="hasMessages"
|
||||||
bordered fixed-height auto-scroll
|
:messages="request.messages"
|
||||||
|
bordered
|
||||||
|
fixed-height
|
||||||
|
auto-scroll
|
||||||
:limit="100"
|
:limit="100"
|
||||||
/>
|
/>
|
||||||
</BCardBody>
|
</BCardBody>
|
||||||
|
@ -30,26 +33,26 @@ export default {
|
||||||
name: 'WaitingDisplay',
|
name: 'WaitingDisplay',
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
MessageListGroup
|
MessageListGroup,
|
||||||
},
|
},
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
request: { type: Object, required: true }
|
request: { type: Object, required: true },
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
hasMessages () {
|
hasMessages() {
|
||||||
return this.request.messages && this.request.messages.length > 0
|
return this.request.messages && this.request.messages.length > 0
|
||||||
},
|
},
|
||||||
|
|
||||||
progress () {
|
progress() {
|
||||||
const progress = this.request.progress
|
const progress = this.request.progress
|
||||||
if (!progress) return null
|
if (!progress) return null
|
||||||
return {
|
return {
|
||||||
values: progress,
|
values: progress,
|
||||||
max: progress.reduce((sum, value) => (sum + value), 0)
|
max: progress.reduce((sum, value) => sum + value, 0),
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -6,10 +6,7 @@
|
||||||
</BCardBody>
|
</BCardBody>
|
||||||
|
|
||||||
<BCardFooter footer-bg-variant="warning">
|
<BCardFooter footer-bg-variant="warning">
|
||||||
<BButton
|
<BButton variant="light" size="sm" v-t="'ok'" @click="dismiss" />
|
||||||
variant="light" size="sm"
|
|
||||||
v-t="'ok'" @click="dismiss"
|
|
||||||
/>
|
|
||||||
</BCardFooter>
|
</BCardFooter>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -19,27 +16,27 @@ export default {
|
||||||
name: 'WarningDisplay',
|
name: 'WarningDisplay',
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
request: { type: Object, required: true }
|
request: { type: Object, required: true },
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
warning () {
|
warning() {
|
||||||
const messages = this.request.messages
|
const messages = this.request.messages
|
||||||
return messages[messages.length - 1]
|
return messages[messages.length - 1]
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
dismiss () {
|
dismiss() {
|
||||||
this.$store.dispatch('DISMISS_WARNING', this.request)
|
this.$store.dispatch('DISMISS_WARNING', this.request)
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.card-body {
|
.card-body {
|
||||||
padding-bottom: 1.5rem !important;
|
padding-bottom: 1.5rem !important;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
<template>
|
<template>
|
||||||
<ViewSearch
|
<ViewSearch
|
||||||
:items="apps" :filtered-items="filteredApps" items-name="apps"
|
:items="apps"
|
||||||
:queries="queries" @queries-response="onQueriesResponse"
|
:filtered-items="filteredApps"
|
||||||
|
items-name="apps"
|
||||||
|
:queries="queries"
|
||||||
|
@queries-response="onQueriesResponse"
|
||||||
>
|
>
|
||||||
<template #top-bar>
|
<template #top-bar>
|
||||||
<div id="view-top-bar">
|
<div id="view-top-bar">
|
||||||
|
@ -11,11 +14,17 @@
|
||||||
<YIcon iname="search" />
|
<YIcon iname="search" />
|
||||||
</BInputGroupPrepend>
|
</BInputGroupPrepend>
|
||||||
<BFormInput
|
<BFormInput
|
||||||
id="search-input" :placeholder="$t('search.for', { items: $tc('items.apps', 2) })"
|
id="search-input"
|
||||||
:value="search" @input="updateQuery('search', $event)"
|
:placeholder="$t('search.for', { items: $tc('items.apps', 2) })"
|
||||||
|
:value="search"
|
||||||
|
@input="updateQuery('search', $event)"
|
||||||
/>
|
/>
|
||||||
<BInputGroupAppend>
|
<BInputGroupAppend>
|
||||||
<BFormSelect :value="quality" :options="qualityOptions" @change="updateQuery('quality', $event)" />
|
<BFormSelect
|
||||||
|
:value="quality"
|
||||||
|
:options="qualityOptions"
|
||||||
|
@change="updateQuery('quality', $event)"
|
||||||
|
/>
|
||||||
</BInputGroupAppend>
|
</BInputGroupAppend>
|
||||||
</BInputGroup>
|
</BInputGroup>
|
||||||
|
|
||||||
|
@ -24,9 +33,17 @@
|
||||||
<BInputGroupPrepend is-text>
|
<BInputGroupPrepend is-text>
|
||||||
<YIcon iname="filter" />
|
<YIcon iname="filter" />
|
||||||
</BInputGroupPrepend>
|
</BInputGroupPrepend>
|
||||||
<BFormSelect :value="category" :options="categories" @change="updateQuery('category', $event)" />
|
<BFormSelect
|
||||||
|
:value="category"
|
||||||
|
:options="categories"
|
||||||
|
@change="updateQuery('category', $event)"
|
||||||
|
/>
|
||||||
<BInputGroupAppend>
|
<BInputGroupAppend>
|
||||||
<BButton variant="primary" :disabled="category === null" @click="updateQuery('category', null)">
|
<BButton
|
||||||
|
variant="primary"
|
||||||
|
:disabled="category === null"
|
||||||
|
@click="updateQuery('category', null)"
|
||||||
|
>
|
||||||
{{ $t('app_show_categories') }}
|
{{ $t('app_show_categories') }}
|
||||||
</BButton>
|
</BButton>
|
||||||
</BInputGroupAppend>
|
</BInputGroupAppend>
|
||||||
|
@ -34,16 +51,20 @@
|
||||||
|
|
||||||
<!-- CATEGORIES SUBTAGS -->
|
<!-- CATEGORIES SUBTAGS -->
|
||||||
<BInputGroup v-if="subtags" class="mt-3 subtags">
|
<BInputGroup v-if="subtags" class="mt-3 subtags">
|
||||||
<BInputGroupPrepend is-text>
|
<BInputGroupPrepend is-text> Subtags </BInputGroupPrepend>
|
||||||
Subtags
|
|
||||||
</BInputGroupPrepend>
|
|
||||||
<BFormRadioGroup
|
<BFormRadioGroup
|
||||||
id="subtags-radio" name="subtags"
|
id="subtags-radio"
|
||||||
:checked="subtag" :options="subtags" @change="updateQuery('subtag', $event)"
|
name="subtags"
|
||||||
buttons button-variant="outline-secondary"
|
:checked="subtag"
|
||||||
|
:options="subtags"
|
||||||
|
@change="updateQuery('subtag', $event)"
|
||||||
|
buttons
|
||||||
|
button-variant="outline-secondary"
|
||||||
/>
|
/>
|
||||||
<BFormSelect
|
<BFormSelect
|
||||||
id="subtags-select" :value="subtag" :options="subtags"
|
id="subtags-select"
|
||||||
|
:value="subtag"
|
||||||
|
:options="subtags"
|
||||||
@change="updateQuery('subtag', $event)"
|
@change="updateQuery('subtag', $event)"
|
||||||
/>
|
/>
|
||||||
</BInputGroup>
|
</BInputGroup>
|
||||||
|
@ -53,8 +74,10 @@
|
||||||
<!-- CATEGORIES CARDS -->
|
<!-- CATEGORIES CARDS -->
|
||||||
<BCardGroup v-if="category === null" deck tag="ul">
|
<BCardGroup v-if="category === null" deck tag="ul">
|
||||||
<BCard
|
<BCard
|
||||||
v-for="cat in categories.slice(1)" :key="cat.value"
|
v-for="cat in categories.slice(1)"
|
||||||
tag="li" class="category-card"
|
:key="cat.value"
|
||||||
|
tag="li"
|
||||||
|
class="category-card"
|
||||||
>
|
>
|
||||||
<BCardTitle>
|
<BCardTitle>
|
||||||
<BLink @click="updateQuery('category', cat.value)" class="card-link">
|
<BLink @click="updateQuery('category', cat.value)" class="card-link">
|
||||||
|
@ -68,33 +91,55 @@
|
||||||
<!-- APPS CARDS -->
|
<!-- APPS CARDS -->
|
||||||
<CardDeckFeed v-else>
|
<CardDeckFeed v-else>
|
||||||
<BCard
|
<BCard
|
||||||
v-for="(app, i) in filteredApps" :key="app.id"
|
v-for="(app, i) in filteredApps"
|
||||||
tag="article" :aria-labelledby="`${app.id}-title`" :aria-describedby="`${app.id}-desc`"
|
:key="app.id"
|
||||||
tabindex="0" :aria-posinset="i + 1" :aria-setsize="filteredApps.length"
|
tag="article"
|
||||||
no-body class="app-card"
|
:aria-labelledby="`${app.id}-title`"
|
||||||
|
:aria-describedby="`${app.id}-desc`"
|
||||||
|
tabindex="0"
|
||||||
|
:aria-posinset="i + 1"
|
||||||
|
:aria-setsize="filteredApps.length"
|
||||||
|
no-body
|
||||||
|
class="app-card"
|
||||||
>
|
>
|
||||||
<BCardBody class="d-flex">
|
<BCardBody class="d-flex">
|
||||||
<BImg v-if="app.logo_hash" class="app-logo rounded" :src="`./applogos/${app.logo_hash}.png`" />
|
<BImg
|
||||||
|
v-if="app.logo_hash"
|
||||||
|
class="app-logo rounded"
|
||||||
|
:src="`./applogos/${app.logo_hash}.png`"
|
||||||
|
/>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<BCardTitle :id="`${app.id}-title`" class="d-flex mb-2">
|
<BCardTitle :id="`${app.id}-title`" class="d-flex mb-2">
|
||||||
<BLink :to="{ name: 'app-install', params: { id: app.id }}" class="card-link">
|
<BLink
|
||||||
|
:to="{ name: 'app-install', params: { id: app.id } }"
|
||||||
|
class="card-link"
|
||||||
|
>
|
||||||
{{ app.manifest.name }}
|
{{ app.manifest.name }}
|
||||||
</BLink>
|
</BLink>
|
||||||
|
|
||||||
<small v-if="app.state !== 'working' || app.high_quality" class="d-flex align-items-center ml-2 position-relative">
|
<small
|
||||||
|
v-if="app.state !== 'working' || app.high_quality"
|
||||||
|
class="d-flex align-items-center ml-2 position-relative"
|
||||||
|
>
|
||||||
<BBadge
|
<BBadge
|
||||||
v-if="app.state !== 'working'"
|
v-if="app.state !== 'working'"
|
||||||
:variant="app.color"
|
:variant="app.color"
|
||||||
v-b-popover.hover.bottom="$t(`app_state_${app.state}_explanation`)"
|
v-b-popover.hover.bottom="
|
||||||
|
$t(`app_state_${app.state}_explanation`)
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<!-- app.state can be 'lowquality' or 'inprogress' -->
|
<!-- app.state can be 'lowquality' or 'inprogress' -->
|
||||||
{{ $t('app_state_' + app.state) }}
|
{{ $t('app_state_' + app.state) }}
|
||||||
</BBadge>
|
</BBadge>
|
||||||
|
|
||||||
<YIcon
|
<YIcon
|
||||||
v-if="app.high_quality" iname="star" class="star"
|
v-if="app.high_quality"
|
||||||
v-b-popover.hover.bottom="$t(`app_state_highquality_explanation`)"
|
iname="star"
|
||||||
|
class="star"
|
||||||
|
v-b-popover.hover.bottom="
|
||||||
|
$t(`app_state_highquality_explanation`)
|
||||||
|
"
|
||||||
/>
|
/>
|
||||||
</small>
|
</small>
|
||||||
</BCardTitle>
|
</BCardTitle>
|
||||||
|
@ -103,8 +148,14 @@
|
||||||
{{ app.manifest.description }}
|
{{ app.manifest.description }}
|
||||||
</BCardText>
|
</BCardText>
|
||||||
|
|
||||||
<BCardText v-if="!app.maintained" class="align-self-end position-relative mt-auto">
|
<BCardText
|
||||||
<span class="alert-warning p-1" v-b-popover.hover.top="$t('orphaned_details')">
|
v-if="!app.maintained"
|
||||||
|
class="align-self-end position-relative mt-auto"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="alert-warning p-1"
|
||||||
|
v-b-popover.hover.top="$t('orphaned_details')"
|
||||||
|
>
|
||||||
<YIcon iname="warning" /> {{ $t('orphaned') }}
|
<YIcon iname="warning" /> {{ $t('orphaned') }}
|
||||||
</span>
|
</span>
|
||||||
</BCardText>
|
</BCardText>
|
||||||
|
@ -125,37 +176,52 @@
|
||||||
<template #bot>
|
<template #bot>
|
||||||
<!-- INSTALL CUSTOM APP -->
|
<!-- INSTALL CUSTOM APP -->
|
||||||
<CardForm
|
<CardForm
|
||||||
:title="$t('custom_app_install')" icon="download"
|
:title="$t('custom_app_install')"
|
||||||
@submit.prevent="onCustomInstallClick" :submit-text="$t('install')"
|
icon="download"
|
||||||
:validation="$v" class="mt-5"
|
@submit.prevent="onCustomInstallClick"
|
||||||
|
:submit-text="$t('install')"
|
||||||
|
:validation="$v"
|
||||||
|
class="mt-5"
|
||||||
>
|
>
|
||||||
<template #disclaimer>
|
<template #disclaimer>
|
||||||
<div class="alert alert-warning">
|
<div class="alert alert-warning">
|
||||||
<YIcon iname="exclamation-triangle" /> {{ $t('confirm_install_custom_app') }}
|
<YIcon iname="exclamation-triangle" />
|
||||||
|
{{ $t('confirm_install_custom_app') }}
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- URL -->
|
<!-- URL -->
|
||||||
<FormField v-bind="customInstall.field" v-model="customInstall.url" :validation="$v.customInstall.url" />
|
<FormField
|
||||||
|
v-bind="customInstall.field"
|
||||||
|
v-model="customInstall.url"
|
||||||
|
:validation="$v.customInstall.url"
|
||||||
|
/>
|
||||||
</CardForm>
|
</CardForm>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- CUSTOM SKELETON -->
|
<!-- CUSTOM SKELETON -->
|
||||||
<template #skeleton>
|
<template #skeleton>
|
||||||
<BCardGroup deck>
|
<BCardGroup deck>
|
||||||
<BCard
|
<BCard v-for="i in 15" :key="i" no-body style="min-height: 10rem">
|
||||||
v-for="i in 15" :key="i"
|
|
||||||
no-body style="min-height: 10rem;"
|
|
||||||
>
|
|
||||||
<div class="d-flex w-100 mt-auto">
|
<div class="d-flex w-100 mt-auto">
|
||||||
<BSkeleton width="30px" height="30px" class="mr-2 ml-auto" />
|
<BSkeleton width="30px" height="30px" class="mr-2 ml-auto" />
|
||||||
<BSkeleton :width="randint(30, 70) + '%'" height="30px" class="mr-auto" />
|
<BSkeleton
|
||||||
|
:width="randint(30, 70) + '%'"
|
||||||
|
height="30px"
|
||||||
|
class="mr-auto"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<BSkeleton
|
<BSkeleton
|
||||||
v-if="randint(0, 1)"
|
v-if="randint(0, 1)"
|
||||||
:width="randint(30, 85) + '%'" height="24px" class="mx-auto"
|
:width="randint(30, 85) + '%'"
|
||||||
|
height="24px"
|
||||||
|
class="mx-auto"
|
||||||
|
/>
|
||||||
|
<BSkeleton
|
||||||
|
:width="randint(30, 85) + '%'"
|
||||||
|
height="24px"
|
||||||
|
class="mx-auto mb-auto"
|
||||||
/>
|
/>
|
||||||
<BSkeleton :width="randint(30, 85) + '%'" height="24px" class="mx-auto mb-auto" />
|
|
||||||
</BCard>
|
</BCard>
|
||||||
</BCardGroup>
|
</BCardGroup>
|
||||||
</template>
|
</template>
|
||||||
|
@ -173,21 +239,19 @@ export default {
|
||||||
name: 'AppCatalog',
|
name: 'AppCatalog',
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
CardDeckFeed
|
CardDeckFeed,
|
||||||
},
|
},
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
search: { type: String, default: '' },
|
search: { type: String, default: '' },
|
||||||
quality: { type: String, default: 'decent_quality' },
|
quality: { type: String, default: 'decent_quality' },
|
||||||
category: { type: String, default: null },
|
category: { type: String, default: null },
|
||||||
subtag: { type: String, default: 'all' }
|
subtag: { type: String, default: 'all' },
|
||||||
},
|
},
|
||||||
|
|
||||||
data () {
|
data() {
|
||||||
return {
|
return {
|
||||||
queries: [
|
queries: [['GET', 'apps/catalog?full&with_categories&with_antifeatures']],
|
||||||
['GET', 'apps/catalog?full&with_categories&with_antifeatures']
|
|
||||||
],
|
|
||||||
|
|
||||||
// Data
|
// Data
|
||||||
apps: undefined,
|
apps: undefined,
|
||||||
|
@ -197,13 +261,16 @@ export default {
|
||||||
// Filtering options
|
// Filtering options
|
||||||
qualityOptions: [
|
qualityOptions: [
|
||||||
{ value: 'high_quality', text: this.$i18n.t('only_highquality_apps') },
|
{ value: 'high_quality', text: this.$i18n.t('only_highquality_apps') },
|
||||||
{ value: 'decent_quality', text: this.$i18n.t('only_decent_quality_apps') },
|
{
|
||||||
|
value: 'decent_quality',
|
||||||
|
text: this.$i18n.t('only_decent_quality_apps'),
|
||||||
|
},
|
||||||
{ value: 'working', text: this.$i18n.t('only_working_apps') },
|
{ value: 'working', text: this.$i18n.t('only_working_apps') },
|
||||||
{ value: 'all', text: this.$i18n.t('all_apps') }
|
{ value: 'all', text: this.$i18n.t('all_apps') },
|
||||||
],
|
],
|
||||||
categories: [
|
categories: [
|
||||||
{ text: this.$i18n.t('app_choose_category'), value: null },
|
{ text: this.$i18n.t('app_choose_category'), value: null },
|
||||||
{ text: this.$i18n.t('all_apps'), value: 'all', icon: 'search' }
|
{ text: this.$i18n.t('all_apps'), value: 'all', icon: 'search' },
|
||||||
// The rest is filled from api data
|
// The rest is filled from api data
|
||||||
],
|
],
|
||||||
|
|
||||||
|
@ -213,31 +280,33 @@ export default {
|
||||||
label: this.$i18n.t('url'),
|
label: this.$i18n.t('url'),
|
||||||
props: {
|
props: {
|
||||||
id: 'custom-install',
|
id: 'custom-install',
|
||||||
placeholder: 'https://some.git.forge.tld/USER/REPOSITORY'
|
placeholder: 'https://some.git.forge.tld/USER/REPOSITORY',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
url: ''
|
url: '',
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
filteredApps () {
|
filteredApps() {
|
||||||
if (!this.apps || this.category === null) return
|
if (!this.apps || this.category === null) return
|
||||||
const search = this.search.toLowerCase()
|
const search = this.search.toLowerCase()
|
||||||
|
|
||||||
if (this.quality === 'all' && this.category === 'all' && search === '') {
|
if (this.quality === 'all' && this.category === 'all' && search === '') {
|
||||||
return this.apps
|
return this.apps
|
||||||
}
|
}
|
||||||
const filtered = this.apps.filter(app => {
|
const filtered = this.apps.filter((app) => {
|
||||||
// app doesn't match quality filter
|
// app doesn't match quality filter
|
||||||
if (this.quality !== 'all' && !app[this.quality]) return false
|
if (this.quality !== 'all' && !app[this.quality]) return false
|
||||||
// app doesn't match category filter
|
// app doesn't match category filter
|
||||||
if (this.category !== 'all' && app.category !== this.category) return false
|
if (this.category !== 'all' && app.category !== this.category)
|
||||||
|
return false
|
||||||
if (this.subtag !== 'all') {
|
if (this.subtag !== 'all') {
|
||||||
const appMatchSubtag = this.subtag === 'others'
|
const appMatchSubtag =
|
||||||
? app.subtags.length === 0
|
this.subtag === 'others'
|
||||||
: app.subtags.includes(this.subtag)
|
? app.subtags.length === 0
|
||||||
|
: app.subtags.includes(this.subtag)
|
||||||
// app doesn't match subtag filter
|
// app doesn't match subtag filter
|
||||||
if (!appMatchSubtag) return false
|
if (!appMatchSubtag) return false
|
||||||
}
|
}
|
||||||
|
@ -248,13 +317,15 @@ export default {
|
||||||
return filtered.length ? filtered : null
|
return filtered.length ? filtered : null
|
||||||
},
|
},
|
||||||
|
|
||||||
subtags () {
|
subtags() {
|
||||||
// build an options array for subtags v-model/options
|
// build an options array for subtags v-model/options
|
||||||
if (this.category && this.categories.length > 2) {
|
if (this.category && this.categories.length > 2) {
|
||||||
const category = this.categories.find(cat => cat.value === this.category)
|
const category = this.categories.find(
|
||||||
|
(cat) => cat.value === this.category,
|
||||||
|
)
|
||||||
if (category.subtags) {
|
if (category.subtags) {
|
||||||
const subtags = [{ text: this.$i18n.t('all'), value: 'all' }]
|
const subtags = [{ text: this.$i18n.t('all'), value: 'all' }]
|
||||||
category.subtags.forEach(subtag => {
|
category.subtags.forEach((subtag) => {
|
||||||
subtags.push({ text: subtag.title, value: subtag.id })
|
subtags.push({ text: subtag.title, value: subtag.id })
|
||||||
})
|
})
|
||||||
subtags.push({ text: this.$i18n.t('others'), value: 'others' })
|
subtags.push({ text: this.$i18n.t('others'), value: 'others' })
|
||||||
|
@ -262,21 +333,22 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
validations: {
|
validations: {
|
||||||
customInstall: {
|
customInstall: {
|
||||||
url: { required, appRepoUrl }
|
url: { required, appRepoUrl },
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
onQueriesResponse (data) {
|
onQueriesResponse(data) {
|
||||||
const apps = []
|
const apps = []
|
||||||
for (const key in data.apps) {
|
for (const key in data.apps) {
|
||||||
const app = data.apps[key]
|
const app = data.apps[key]
|
||||||
app.isInstallable = !app.installed || app.manifest.integration.multi_instance
|
app.isInstallable =
|
||||||
|
!app.installed || app.manifest.integration.multi_instance
|
||||||
app.working = app.state === 'working'
|
app.working = app.state === 'working'
|
||||||
app.decent_quality = app.working && app.level > 4
|
app.decent_quality = app.working && app.level > 4
|
||||||
app.high_quality = app.working && app.level >= 8
|
app.high_quality = app.working && app.level >= 8
|
||||||
|
@ -295,57 +367,71 @@ export default {
|
||||||
app.state,
|
app.state,
|
||||||
app.manifest.name,
|
app.manifest.name,
|
||||||
app.manifest.description,
|
app.manifest.description,
|
||||||
app.potential_alternative_to.join(' ')
|
app.potential_alternative_to.join(' '),
|
||||||
].join(' ').toLowerCase()
|
]
|
||||||
|
.join(' ')
|
||||||
|
.toLowerCase()
|
||||||
apps.push(app)
|
apps.push(app)
|
||||||
}
|
}
|
||||||
this.apps = apps.sort((a, b) => a.id > b.id ? 1 : -1)
|
this.apps = apps.sort((a, b) => (a.id > b.id ? 1 : -1))
|
||||||
|
|
||||||
// CATEGORIES
|
// CATEGORIES
|
||||||
data.categories.forEach(({ title, id, icon, subtags, description }) => {
|
data.categories.forEach(({ title, id, icon, subtags, description }) => {
|
||||||
this.categories.push({ text: title, value: id, icon, subtags, description })
|
this.categories.push({
|
||||||
|
text: title,
|
||||||
|
value: id,
|
||||||
|
icon,
|
||||||
|
subtags,
|
||||||
|
description,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
this.antifeatures = Object.fromEntries(data.antifeatures.map((af) => ([af.id, af])))
|
this.antifeatures = Object.fromEntries(
|
||||||
|
data.antifeatures.map((af) => [af.id, af]),
|
||||||
|
)
|
||||||
},
|
},
|
||||||
|
|
||||||
updateQuery (key, value) {
|
updateQuery(key, value) {
|
||||||
// Update the query string without reloading the page
|
// Update the query string without reloading the page
|
||||||
this.$router.replace({
|
this.$router.replace({
|
||||||
query: {
|
query: {
|
||||||
...this.$route.query,
|
...this.$route.query,
|
||||||
// allow search without selecting a category
|
// allow search without selecting a category
|
||||||
category: this.$route.query.category || 'all',
|
category: this.$route.query.category || 'all',
|
||||||
[key]: value
|
[key]: value,
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
// INSTALL APP
|
// INSTALL APP
|
||||||
async onInstallClick (appId) {
|
async onInstallClick(appId) {
|
||||||
const app = this.apps.find((app) => app.id === appId)
|
const app = this.apps.find((app) => app.id === appId)
|
||||||
if (!app.decent_quality) {
|
if (!app.decent_quality) {
|
||||||
const confirmed = await this.$askConfirmation(this.$i18n.t('confirm_install_app_' + app.state))
|
const confirmed = await this.$askConfirmation(
|
||||||
|
this.$i18n.t('confirm_install_app_' + app.state),
|
||||||
|
)
|
||||||
if (!confirmed) return
|
if (!confirmed) return
|
||||||
}
|
}
|
||||||
this.$router.push({ name: 'app-install', params: { id: app.id } })
|
this.$router.push({ name: 'app-install', params: { id: app.id } })
|
||||||
},
|
},
|
||||||
|
|
||||||
// INSTALL CUSTOM APP
|
// INSTALL CUSTOM APP
|
||||||
async onCustomInstallClick () {
|
async onCustomInstallClick() {
|
||||||
const confirmed = await this.$askConfirmation(this.$i18n.t('confirm_install_custom_app'))
|
const confirmed = await this.$askConfirmation(
|
||||||
|
this.$i18n.t('confirm_install_custom_app'),
|
||||||
|
)
|
||||||
if (!confirmed) return
|
if (!confirmed) return
|
||||||
|
|
||||||
const url = this.customInstall.url
|
const url = this.customInstall.url
|
||||||
this.$router.push({
|
this.$router.push({
|
||||||
name: 'app-install-custom',
|
name: 'app-install-custom',
|
||||||
params: { id: url.endsWith('/') ? url : url + '/' }
|
params: { id: url.endsWith('/') ? url : url + '/' },
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
randint
|
randint,
|
||||||
},
|
},
|
||||||
|
|
||||||
mixins: [validationMixin]
|
mixins: [validationMixin],
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -364,7 +450,7 @@ export default {
|
||||||
|
|
||||||
.subtags {
|
.subtags {
|
||||||
#subtags-radio {
|
#subtags-radio {
|
||||||
display: none
|
display: none;
|
||||||
}
|
}
|
||||||
@include media-breakpoint-up(md) {
|
@include media-breakpoint-up(md) {
|
||||||
#subtags-radio {
|
#subtags-radio {
|
||||||
|
@ -418,7 +504,7 @@ export default {
|
||||||
|
|
||||||
// not maintained info
|
// not maintained info
|
||||||
.alert-warning {
|
.alert-warning {
|
||||||
font-size: .75em;
|
font-size: 0.75em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.star {
|
.star {
|
||||||
|
@ -450,7 +536,7 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
&:focus::after {
|
&:focus::after {
|
||||||
box-shadow: 0 0 0 $btn-focus-width rgba($dark, .5);
|
box-shadow: 0 0 0 $btn-focus-width rgba($dark, 0.5);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,20 @@
|
||||||
<template>
|
<template>
|
||||||
<ViewBase
|
<ViewBase
|
||||||
:queries="queries" @queries-response="onQueriesResponse" :loading="loading"
|
:queries="queries"
|
||||||
|
@queries-response="onQueriesResponse"
|
||||||
|
:loading="loading"
|
||||||
ref="view"
|
ref="view"
|
||||||
>
|
>
|
||||||
<YAlert v-if="app && app.doc && app.doc.notifications && app.doc.notifications.postInstall.length" variant="info" class="my-4">
|
<YAlert
|
||||||
|
v-if="
|
||||||
|
app &&
|
||||||
|
app.doc &&
|
||||||
|
app.doc.notifications &&
|
||||||
|
app.doc.notifications.postInstall.length
|
||||||
|
"
|
||||||
|
variant="info"
|
||||||
|
class="my-4"
|
||||||
|
>
|
||||||
<div class="d-md-flex align-items-center mb-3">
|
<div class="d-md-flex align-items-center mb-3">
|
||||||
<h2 v-t="'app.doc.notifications.post_install'" class="md-m-0" />
|
<h2 v-t="'app.doc.notifications.post_install'" class="md-m-0" />
|
||||||
<BButton
|
<BButton
|
||||||
|
@ -18,12 +29,24 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<VueShowdown
|
<VueShowdown
|
||||||
v-for="[name, notif] in app.doc.notifications.postInstall" :key="name"
|
v-for="[name, notif] in app.doc.notifications.postInstall"
|
||||||
:markdown="notif" flavor="github" :options="{ headerLevelStart: 4 }"
|
:key="name"
|
||||||
|
:markdown="notif"
|
||||||
|
flavor="github"
|
||||||
|
:options="{ headerLevelStart: 4 }"
|
||||||
/>
|
/>
|
||||||
</YAlert>
|
</YAlert>
|
||||||
|
|
||||||
<YAlert v-if="app && app.doc && app.doc.notifications && app.doc.notifications.postUpgrade.length" variant="info" class="my-4">
|
<YAlert
|
||||||
|
v-if="
|
||||||
|
app &&
|
||||||
|
app.doc &&
|
||||||
|
app.doc.notifications &&
|
||||||
|
app.doc.notifications.postUpgrade.length
|
||||||
|
"
|
||||||
|
variant="info"
|
||||||
|
class="my-4"
|
||||||
|
>
|
||||||
<div class="d-md-flex align-items-center mb-3">
|
<div class="d-md-flex align-items-center mb-3">
|
||||||
<h2 v-t="'app.doc.notifications.post_upgrade'" class="md-m-0" />
|
<h2 v-t="'app.doc.notifications.post_upgrade'" class="md-m-0" />
|
||||||
<BButton
|
<BButton
|
||||||
|
@ -38,8 +61,11 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<VueShowdown
|
<VueShowdown
|
||||||
v-for="[name, notif] in app.doc.notifications.postUpgrade" :key="name"
|
v-for="[name, notif] in app.doc.notifications.postUpgrade"
|
||||||
:markdown="notif" flavor="github" :options="{ headerLevelStart: 4 }"
|
:key="name"
|
||||||
|
:markdown="notif"
|
||||||
|
flavor="github"
|
||||||
|
:options="{ headerLevelStart: 4 }"
|
||||||
/>
|
/>
|
||||||
</YAlert>
|
</YAlert>
|
||||||
|
|
||||||
|
@ -56,8 +82,10 @@
|
||||||
|
|
||||||
<BButton
|
<BButton
|
||||||
v-if="app.url"
|
v-if="app.url"
|
||||||
:href="app.url" target="_blank"
|
:href="app.url"
|
||||||
variant="success" class="ml-auto mr-2"
|
target="_blank"
|
||||||
|
variant="success"
|
||||||
|
class="ml-auto mr-2"
|
||||||
>
|
>
|
||||||
<YIcon iname="external-link" />
|
<YIcon iname="external-link" />
|
||||||
{{ $t('app.open_this_app') }}
|
{{ $t('app.open_this_app') }}
|
||||||
|
@ -75,10 +103,11 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="text-secondary">
|
<p class="text-secondary">
|
||||||
<strong v-t="'app.installed_version'" /> {{ app.version }}<br>
|
<strong v-t="'app.installed_version'" /> {{ app.version }}<br />
|
||||||
|
|
||||||
<template v-if="app.alternativeTo">
|
<template v-if="app.alternativeTo">
|
||||||
<strong v-t="'app.potential_alternative_to'" /> {{ app.alternativeTo }}
|
<strong v-t="'app.potential_alternative_to'" />
|
||||||
|
{{ app.alternativeTo }}
|
||||||
</template>
|
</template>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
@ -92,10 +121,7 @@
|
||||||
<VueShowdown :markdown="app.description" flavor="github" />
|
<VueShowdown :markdown="app.description" flavor="github" />
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<YAlert
|
<YAlert v-if="config_panel_err" class="mb-4" variant="danger" icon="bug">
|
||||||
v-if="config_panel_err"
|
|
||||||
class="mb-4" variant="danger" icon="bug"
|
|
||||||
>
|
|
||||||
<p>{{ $t('app.info.config_panel_error') }}</p>
|
<p>{{ $t('app.info.config_panel_error') }}</p>
|
||||||
<p>{{ config_panel_err }}</p>
|
<p>{{ config_panel_err }}</p>
|
||||||
<p>{{ $t('app.info.config_panel_error_please_report') }}</p>
|
<p>{{ $t('app.info.config_panel_error_please_report') }}</p>
|
||||||
|
@ -106,25 +132,38 @@
|
||||||
<!-- OPERATIONS TAB -->
|
<!-- OPERATIONS TAB -->
|
||||||
<template v-if="currentTab === 'operations'" #tab-top>
|
<template v-if="currentTab === 'operations'" #tab-top>
|
||||||
<!-- CHANGE PERMISSIONS LABEL -->
|
<!-- CHANGE PERMISSIONS LABEL -->
|
||||||
<BFormGroup :label="$t('app_manage_label_and_tiles')" label-class="font-weight-bold">
|
<BFormGroup
|
||||||
|
:label="$t('app_manage_label_and_tiles')"
|
||||||
|
label-class="font-weight-bold"
|
||||||
|
>
|
||||||
<FormField
|
<FormField
|
||||||
v-for="(perm, i) in app.permissions" :key="i"
|
v-for="(perm, i) in app.permissions"
|
||||||
:label="perm.title" :label-for="'perm-' + i"
|
:key="i"
|
||||||
label-cols="0" label-class="" class="m-0"
|
:label="perm.title"
|
||||||
:validation="$v.form.labels.$each[i] "
|
:label-for="'perm-' + i"
|
||||||
|
label-cols="0"
|
||||||
|
label-class=""
|
||||||
|
class="m-0"
|
||||||
|
:validation="$v.form.labels.$each[i]"
|
||||||
>
|
>
|
||||||
<template #default="{ self }">
|
<template #default="{ self }">
|
||||||
<BInputGroup>
|
<BInputGroup>
|
||||||
<InputItem
|
<InputItem
|
||||||
:state="self.state" v-model="form.labels[i].label"
|
:state="self.state"
|
||||||
:id="'perm' + i" :aria-describedby="'perm-' + i + '_group__BV_description_'"
|
v-model="form.labels[i].label"
|
||||||
|
:id="'perm' + i"
|
||||||
|
:aria-describedby="'perm-' + i + '_group__BV_description_'"
|
||||||
/>
|
/>
|
||||||
<BInputGroupAppend v-if="perm.tileAvailable" is-text>
|
<BInputGroupAppend v-if="perm.tileAvailable" is-text>
|
||||||
<CheckboxItem v-model="form.labels[i].show_tile" :label="$t('permission_show_tile_enabled')" />
|
<CheckboxItem
|
||||||
|
v-model="form.labels[i].show_tile"
|
||||||
|
:label="$t('permission_show_tile_enabled')"
|
||||||
|
/>
|
||||||
</BInputGroupAppend>
|
</BInputGroupAppend>
|
||||||
<BInputGroupAppend>
|
<BInputGroupAppend>
|
||||||
<BButton
|
<BButton
|
||||||
variant="info" v-t="'save'"
|
variant="info"
|
||||||
|
v-t="'save'"
|
||||||
@click="changeLabel(perm.name, form.labels[i])"
|
@click="changeLabel(perm.name, form.labels[i])"
|
||||||
/>
|
/>
|
||||||
</BInputGroupAppend>
|
</BInputGroupAppend>
|
||||||
|
@ -139,43 +178,52 @@
|
||||||
</template>
|
</template>
|
||||||
</FormField>
|
</FormField>
|
||||||
</BFormGroup>
|
</BFormGroup>
|
||||||
<hr>
|
<hr />
|
||||||
|
|
||||||
<!-- PERMISSIONS -->
|
<!-- PERMISSIONS -->
|
||||||
<BFormGroup
|
<BFormGroup
|
||||||
:label="$t('app_info_access_desc')" label-for="permissions"
|
:label="$t('app_info_access_desc')"
|
||||||
label-class="font-weight-bold" label-cols-lg="0"
|
label-for="permissions"
|
||||||
|
label-class="font-weight-bold"
|
||||||
|
label-cols-lg="0"
|
||||||
>
|
>
|
||||||
{{ allowedGroups.length > 0 ? allowedGroups.join(', ') : $t('nobody') }}
|
{{
|
||||||
|
allowedGroups.length > 0 ? allowedGroups.join(', ') : $t('nobody')
|
||||||
|
}}
|
||||||
<BButton
|
<BButton
|
||||||
size="sm" :to="{ name: 'group-list'}" variant="info"
|
size="sm"
|
||||||
|
:to="{ name: 'group-list' }"
|
||||||
|
variant="info"
|
||||||
class="ml-2"
|
class="ml-2"
|
||||||
>
|
>
|
||||||
<YIcon iname="key-modern" /> {{ $t('groups_and_permissions_manage') }}
|
<YIcon iname="key-modern" />
|
||||||
|
{{ $t('groups_and_permissions_manage') }}
|
||||||
</BButton>
|
</BButton>
|
||||||
</BFormGroup>
|
</BFormGroup>
|
||||||
<hr>
|
<hr />
|
||||||
|
|
||||||
<!-- CHANGE URL -->
|
<!-- CHANGE URL -->
|
||||||
<BFormGroup
|
<BFormGroup
|
||||||
:label="$t('app_info_changeurl_desc')" label-for="input-url"
|
:label="$t('app_info_changeurl_desc')"
|
||||||
:label-cols-lg="app.supports_change_url ? 0 : 0" label-class="font-weight-bold"
|
label-for="input-url"
|
||||||
|
:label-cols-lg="app.supports_change_url ? 0 : 0"
|
||||||
|
label-class="font-weight-bold"
|
||||||
v-if="app.is_webapp"
|
v-if="app.is_webapp"
|
||||||
>
|
>
|
||||||
<BInputGroup v-if="app.supports_change_url">
|
<BInputGroup v-if="app.supports_change_url">
|
||||||
<BInputGroupPrepend is-text>
|
<BInputGroupPrepend is-text> https:// </BInputGroupPrepend>
|
||||||
https://
|
|
||||||
</BInputGroupPrepend>
|
|
||||||
|
|
||||||
<BInputGroupPrepend class="flex-grow-1">
|
<BInputGroupPrepend class="flex-grow-1">
|
||||||
<BFormSelect v-model="form.url.domain" :options="domains" />
|
<BFormSelect v-model="form.url.domain" :options="domains" />
|
||||||
</BInputGroupPrepend>
|
</BInputGroupPrepend>
|
||||||
|
|
||||||
<BInputGroupPrepend is-text>
|
<BInputGroupPrepend is-text> / </BInputGroupPrepend>
|
||||||
/
|
|
||||||
</BInputGroupPrepend>
|
|
||||||
|
|
||||||
<BFormInput id="input-url" v-model="form.url.path" class="flex-grow-3" />
|
<BFormInput
|
||||||
|
id="input-url"
|
||||||
|
v-model="form.url.path"
|
||||||
|
class="flex-grow-3"
|
||||||
|
/>
|
||||||
|
|
||||||
<BInputGroupAppend>
|
<BInputGroupAppend>
|
||||||
<BButton @click="changeUrl" variant="info" v-t="'save'" />
|
<BButton @click="changeUrl" variant="info" v-t="'save'" />
|
||||||
|
@ -183,25 +231,36 @@
|
||||||
</BInputGroup>
|
</BInputGroup>
|
||||||
|
|
||||||
<div v-else class="alert alert-warning">
|
<div v-else class="alert alert-warning">
|
||||||
<YIcon iname="exclamation" /> {{ $t('app_info_change_url_disabled_tooltip') }}
|
<YIcon iname="exclamation" />
|
||||||
|
{{ $t('app_info_change_url_disabled_tooltip') }}
|
||||||
</div>
|
</div>
|
||||||
</BFormGroup>
|
</BFormGroup>
|
||||||
<hr v-if="app.is_webapp">
|
<hr v-if="app.is_webapp" />
|
||||||
|
|
||||||
<!-- MAKE DEFAULT -->
|
<!-- MAKE DEFAULT -->
|
||||||
<BFormGroup
|
<BFormGroup
|
||||||
:label="$t('app_info_default_desc', { domain: app.domain })" label-for="main-domain"
|
:label="$t('app_info_default_desc', { domain: app.domain })"
|
||||||
label-class="font-weight-bold" label-cols-md="4"
|
label-for="main-domain"
|
||||||
|
label-class="font-weight-bold"
|
||||||
|
label-cols-md="4"
|
||||||
v-if="app.is_webapp"
|
v-if="app.is_webapp"
|
||||||
>
|
>
|
||||||
<template v-if="!app.is_default">
|
<template v-if="!app.is_default">
|
||||||
<BButton @click="setAsDefaultDomain(false)" id="main-domain" variant="success">
|
<BButton
|
||||||
|
@click="setAsDefaultDomain(false)"
|
||||||
|
id="main-domain"
|
||||||
|
variant="success"
|
||||||
|
>
|
||||||
<YIcon iname="star" /> {{ $t('app_make_default') }}
|
<YIcon iname="star" /> {{ $t('app_make_default') }}
|
||||||
</BButton>
|
</BButton>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<BButton @click="setAsDefaultDomain(true)" id="main-domain" variant="warning">
|
<BButton
|
||||||
|
@click="setAsDefaultDomain(true)"
|
||||||
|
id="main-domain"
|
||||||
|
variant="warning"
|
||||||
|
>
|
||||||
<YIcon iname="star" /> {{ $t('app_make_not_default') }}
|
<YIcon iname="star" /> {{ $t('app_make_not_default') }}
|
||||||
</BButton>
|
</BButton>
|
||||||
</template>
|
</template>
|
||||||
|
@ -211,12 +270,10 @@
|
||||||
|
|
||||||
<BCard v-if="app && app.doc.admin.length" no-body>
|
<BCard v-if="app && app.doc.admin.length" no-body>
|
||||||
<BTabs card fill pills>
|
<BTabs card fill pills>
|
||||||
<BTab
|
<BTab v-for="[name, content] in app.doc.admin" :key="name">
|
||||||
v-for="[name, content] in app.doc.admin" :key="name"
|
|
||||||
>
|
|
||||||
<template #title>
|
<template #title>
|
||||||
<YIcon iname="book" class="mr-2" />
|
<YIcon iname="book" class="mr-2" />
|
||||||
{{ name === "admin" ? $t('app.doc.admin.title') : name }}
|
{{ name === 'admin' ? $t('app.doc.admin.title') : name }}
|
||||||
</template>
|
</template>
|
||||||
<VueShowdown :markdown="content" flavor="github" />
|
<VueShowdown :markdown="content" flavor="github" />
|
||||||
</BTab>
|
</BTab>
|
||||||
|
@ -225,21 +282,34 @@
|
||||||
|
|
||||||
<YCard
|
<YCard
|
||||||
v-if="app && app.integration"
|
v-if="app && app.integration"
|
||||||
id="app-integration" :title="$t('app.integration.title')"
|
id="app-integration"
|
||||||
collapsable collapsed no-body
|
:title="$t('app.integration.title')"
|
||||||
|
collapsable
|
||||||
|
collapsed
|
||||||
|
no-body
|
||||||
>
|
>
|
||||||
<BListGroup flush>
|
<BListGroup flush>
|
||||||
<YListGroupItem variant="info">
|
<YListGroupItem variant="info">
|
||||||
{{ $t('app.integration.archs') }} {{ app.integration.archs }}
|
{{ $t('app.integration.archs') }} {{ app.integration.archs }}
|
||||||
</YListGroupItem>
|
</YListGroupItem>
|
||||||
<YListGroupItem v-if="app.integration.ldap" :variant="app.integration.ldap === true ? 'success' : 'warning'">
|
<YListGroupItem
|
||||||
|
v-if="app.integration.ldap"
|
||||||
|
:variant="app.integration.ldap === true ? 'success' : 'warning'"
|
||||||
|
>
|
||||||
{{ $t(`app.integration.ldap.${app.integration.ldap}`) }}
|
{{ $t(`app.integration.ldap.${app.integration.ldap}`) }}
|
||||||
</YListGroupItem>
|
</YListGroupItem>
|
||||||
<YListGroupItem v-if="app.integration.sso" :variant="app.integration.sso === true ? 'success' : 'warning'">
|
<YListGroupItem
|
||||||
|
v-if="app.integration.sso"
|
||||||
|
:variant="app.integration.sso === true ? 'success' : 'warning'"
|
||||||
|
>
|
||||||
{{ $t(`app.integration.sso.${app.integration.sso}`) }}
|
{{ $t(`app.integration.sso.${app.integration.sso}`) }}
|
||||||
</YListGroupItem>
|
</YListGroupItem>
|
||||||
<YListGroupItem variant="info">
|
<YListGroupItem variant="info">
|
||||||
{{ $t(`app.integration.multi_instance.${app.integration.multi_instance}`) }}
|
{{
|
||||||
|
$t(
|
||||||
|
`app.integration.multi_instance.${app.integration.multi_instance}`,
|
||||||
|
)
|
||||||
|
}}
|
||||||
</YListGroupItem>
|
</YListGroupItem>
|
||||||
<YListGroupItem variant="info">
|
<YListGroupItem variant="info">
|
||||||
{{ $t('app.integration.resources', app.integration.resources) }}
|
{{ $t('app.integration.resources', app.integration.resources) }}
|
||||||
|
@ -249,8 +319,12 @@
|
||||||
|
|
||||||
<YCard
|
<YCard
|
||||||
v-if="app"
|
v-if="app"
|
||||||
id="app-links" icon="link" :title="$t('app.links.title')"
|
id="app-links"
|
||||||
collapsable collapsed no-body
|
icon="link"
|
||||||
|
:title="$t('app.links.title')"
|
||||||
|
collapsable
|
||||||
|
collapsed
|
||||||
|
no-body
|
||||||
>
|
>
|
||||||
<BListGroup flush>
|
<BListGroup flush>
|
||||||
<YListGroupItem v-for="[key, link] in app.links" :key="key" no-status>
|
<YListGroupItem v-for="[key, link] in app.links" :key="key" no-status>
|
||||||
|
@ -264,8 +338,11 @@
|
||||||
|
|
||||||
<BModal
|
<BModal
|
||||||
v-if="app"
|
v-if="app"
|
||||||
id="uninstall-modal" :title="$t('confirm_uninstall', { name: id })"
|
id="uninstall-modal"
|
||||||
header-bg-variant="warning" :body-class="{ 'd-none': !app.supports_purge }" body-bg-variant=""
|
:title="$t('confirm_uninstall', { name: id })"
|
||||||
|
header-bg-variant="warning"
|
||||||
|
:body-class="{ 'd-none': !app.supports_purge }"
|
||||||
|
body-bg-variant=""
|
||||||
@ok="uninstall"
|
@ok="uninstall"
|
||||||
>
|
>
|
||||||
<BFormGroup v-if="app.supports_purge">
|
<BFormGroup v-if="app.supports_purge">
|
||||||
|
@ -293,7 +370,7 @@ import { isEmptyValue } from '@/helpers/commons'
|
||||||
import {
|
import {
|
||||||
formatFormData,
|
formatFormData,
|
||||||
formatI18nField,
|
formatI18nField,
|
||||||
formatYunoHostConfigPanels
|
formatYunoHostConfigPanels,
|
||||||
} from '@/helpers/yunohostArguments'
|
} from '@/helpers/yunohostArguments'
|
||||||
import ConfigPanels from '@/components/ConfigPanels.vue'
|
import ConfigPanels from '@/components/ConfigPanels.vue'
|
||||||
|
|
||||||
|
@ -301,19 +378,19 @@ export default {
|
||||||
name: 'AppInfo',
|
name: 'AppInfo',
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
ConfigPanels
|
ConfigPanels,
|
||||||
},
|
},
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
id: { type: String, required: true }
|
id: { type: String, required: true },
|
||||||
},
|
},
|
||||||
|
|
||||||
data () {
|
data() {
|
||||||
return {
|
return {
|
||||||
queries: [
|
queries: [
|
||||||
['GET', `apps/${this.id}?full`],
|
['GET', `apps/${this.id}?full`],
|
||||||
['GET', { uri: 'users/permissions?full', storeKey: 'permissions' }],
|
['GET', { uri: 'users/permissions?full', storeKey: 'permissions' }],
|
||||||
['GET', { uri: 'domains' }]
|
['GET', { uri: 'domains' }],
|
||||||
],
|
],
|
||||||
loading: true,
|
loading: true,
|
||||||
app: undefined,
|
app: undefined,
|
||||||
|
@ -326,62 +403,66 @@ export default {
|
||||||
{
|
{
|
||||||
hasApplyButton: false,
|
hasApplyButton: false,
|
||||||
id: 'operations',
|
id: 'operations',
|
||||||
name: this.$i18n.t('operations')
|
name: this.$i18n.t('operations'),
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
validations: {}
|
validations: {},
|
||||||
},
|
},
|
||||||
doc: undefined
|
doc: undefined,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters(['domains']),
|
...mapGetters(['domains']),
|
||||||
|
|
||||||
currentTab () {
|
currentTab() {
|
||||||
return this.$route.params.tabId
|
return this.$route.params.tabId
|
||||||
},
|
},
|
||||||
|
|
||||||
allowedGroups () {
|
allowedGroups() {
|
||||||
if (!this.app) return
|
if (!this.app) return
|
||||||
return this.app.permissions[0].allowed
|
return this.app.permissions[0].allowed
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
validations () {
|
validations() {
|
||||||
return {
|
return {
|
||||||
form: {
|
form: {
|
||||||
labels: {
|
labels: {
|
||||||
$each: { label: { required } }
|
$each: { label: { required } },
|
||||||
},
|
},
|
||||||
url: { path: { required } }
|
url: { path: { required } },
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
appLinksIcons (linkType) {
|
appLinksIcons(linkType) {
|
||||||
const linksIcons = {
|
const linksIcons = {
|
||||||
license: 'institution',
|
license: 'institution',
|
||||||
website: 'globe',
|
website: 'globe',
|
||||||
admindoc: 'book',
|
admindoc: 'book',
|
||||||
userdoc: 'book',
|
userdoc: 'book',
|
||||||
code: 'code',
|
code: 'code',
|
||||||
package: 'code',
|
package: 'code',
|
||||||
package_license: 'institution',
|
package_license: 'institution',
|
||||||
forum: 'comments'
|
forum: 'comments',
|
||||||
}
|
}
|
||||||
return linksIcons[linkType]
|
return linksIcons[linkType]
|
||||||
},
|
},
|
||||||
|
|
||||||
async onQueriesResponse (app) {
|
async onQueriesResponse(app) {
|
||||||
const form = { labels: [] }
|
const form = { labels: [] }
|
||||||
|
|
||||||
const mainPermission = app.permissions[this.id + '.main']
|
const mainPermission = app.permissions[this.id + '.main']
|
||||||
mainPermission.name = this.id + '.main'
|
mainPermission.name = this.id + '.main'
|
||||||
mainPermission.title = this.$i18n.t('permission_main')
|
mainPermission.title = this.$i18n.t('permission_main')
|
||||||
mainPermission.tileAvailable = mainPermission.url !== null && !mainPermission.url.startsWith('re:')
|
mainPermission.tileAvailable =
|
||||||
form.labels.push({ label: mainPermission.label, show_tile: mainPermission.show_tile })
|
mainPermission.url !== null && !mainPermission.url.startsWith('re:')
|
||||||
|
form.labels.push({
|
||||||
|
label: mainPermission.label,
|
||||||
|
show_tile: mainPermission.show_tile,
|
||||||
|
})
|
||||||
|
|
||||||
const permissions = [mainPermission]
|
const permissions = [mainPermission]
|
||||||
for (const [name, perm] of Object.entries(app.permissions)) {
|
for (const [name, perm] of Object.entries(app.permissions)) {
|
||||||
|
@ -391,7 +472,7 @@ export default {
|
||||||
name,
|
name,
|
||||||
label: perm.sublabel,
|
label: perm.sublabel,
|
||||||
title: humanPermissionName(name),
|
title: humanPermissionName(name),
|
||||||
tileAvailable: perm.url !== null && !perm.url.startsWith('re:')
|
tileAvailable: perm.url !== null && !perm.url.startsWith('re:'),
|
||||||
})
|
})
|
||||||
form.labels.push({ label: perm.sublabel, show_tile: perm.show_tile })
|
form.labels.push({ label: perm.sublabel, show_tile: perm.show_tile })
|
||||||
}
|
}
|
||||||
|
@ -400,148 +481,214 @@ export default {
|
||||||
|
|
||||||
const { DESCRIPTION, ADMIN, ...doc } = app.manifest.doc
|
const { DESCRIPTION, ADMIN, ...doc } = app.manifest.doc
|
||||||
const notifs = app.manifest.notifications
|
const notifs = app.manifest.notifications
|
||||||
const { ldap, sso, multi_instance, ram, disk, architectures: archs } = app.manifest.integration
|
const {
|
||||||
|
ldap,
|
||||||
|
sso,
|
||||||
|
multi_instance,
|
||||||
|
ram,
|
||||||
|
disk,
|
||||||
|
architectures: archs,
|
||||||
|
} = app.manifest.integration
|
||||||
this.app = {
|
this.app = {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
version: app.version,
|
version: app.version,
|
||||||
label: mainPermission.label,
|
label: mainPermission.label,
|
||||||
domain: app.settings.domain,
|
domain: app.settings.domain,
|
||||||
alternativeTo: app.from_catalog.potential_alternative_to?.length
|
alternativeTo: app.from_catalog.potential_alternative_to?.length
|
||||||
? app.from_catalog.potential_alternative_to.join(this.$i18n.t('words.separator'))
|
? app.from_catalog.potential_alternative_to.join(
|
||||||
: null,
|
this.$i18n.t('words.separator'),
|
||||||
description: DESCRIPTION ? formatI18nField(DESCRIPTION) : app.description,
|
)
|
||||||
integration: app.manifest.packaging_format >= 2
|
|
||||||
? {
|
|
||||||
archs: Array.isArray(archs) ? archs.join(this.$i18n.t('words.separator')) : archs,
|
|
||||||
ldap: ldap === 'not_relevant' ? null : ldap,
|
|
||||||
sso: sso === 'not_relevant' ? null : sso,
|
|
||||||
multi_instance,
|
|
||||||
resources: { ram: ram.runtime, disk }
|
|
||||||
}
|
|
||||||
: null,
|
: null,
|
||||||
|
description: DESCRIPTION
|
||||||
|
? formatI18nField(DESCRIPTION)
|
||||||
|
: app.description,
|
||||||
|
integration:
|
||||||
|
app.manifest.packaging_format >= 2
|
||||||
|
? {
|
||||||
|
archs: Array.isArray(archs)
|
||||||
|
? archs.join(this.$i18n.t('words.separator'))
|
||||||
|
: archs,
|
||||||
|
ldap: ldap === 'not_relevant' ? null : ldap,
|
||||||
|
sso: sso === 'not_relevant' ? null : sso,
|
||||||
|
multi_instance,
|
||||||
|
resources: { ram: ram.runtime, disk },
|
||||||
|
}
|
||||||
|
: null,
|
||||||
links: [
|
links: [
|
||||||
['license', `https://spdx.org/licenses/${app.manifest.upstream.license}`],
|
[
|
||||||
...['website', 'admindoc', 'userdoc', 'code'].map((key) => ([key, app.manifest.upstream[key]])),
|
'license',
|
||||||
|
`https://spdx.org/licenses/${app.manifest.upstream.license}`,
|
||||||
|
],
|
||||||
|
...['website', 'admindoc', 'userdoc', 'code'].map((key) => {
|
||||||
|
return [key, app.manifest.upstream[key]]
|
||||||
|
}),
|
||||||
['package', app.from_catalog.git?.url],
|
['package', app.from_catalog.git?.url],
|
||||||
['package_license', app.from_catalog.git?.url + '/blob/master/LICENSE'],
|
[
|
||||||
['forum', `https://forum.yunohost.org/tag/${app.manifest.id}`]
|
'package_license',
|
||||||
|
app.from_catalog.git?.url + '/blob/master/LICENSE',
|
||||||
|
],
|
||||||
|
['forum', `https://forum.yunohost.org/tag/${app.manifest.id}`],
|
||||||
].filter(([key, val]) => !!val),
|
].filter(([key, val]) => !!val),
|
||||||
doc: {
|
doc: {
|
||||||
notifications: {
|
notifications: {
|
||||||
postInstall: notifs.POST_INSTALL && notifs.POST_INSTALL.main ? [['main', formatI18nField(notifs.POST_INSTALL.main)]] : [],
|
postInstall:
|
||||||
|
notifs.POST_INSTALL && notifs.POST_INSTALL.main
|
||||||
|
? [['main', formatI18nField(notifs.POST_INSTALL.main)]]
|
||||||
|
: [],
|
||||||
postUpgrade: notifs.POST_UPGRADE
|
postUpgrade: notifs.POST_UPGRADE
|
||||||
? Object.entries(notifs.POST_UPGRADE).map(([key, content]) => {
|
? Object.entries(notifs.POST_UPGRADE).map(([key, content]) => {
|
||||||
return [key, formatI18nField(content)]
|
return [key, formatI18nField(content)]
|
||||||
})
|
})
|
||||||
: []
|
: [],
|
||||||
},
|
},
|
||||||
admin: [
|
admin: [
|
||||||
['admin', formatI18nField(ADMIN)],
|
['admin', formatI18nField(ADMIN)],
|
||||||
...Object.keys(doc).sort().map((key) => [key.charAt(0) + key.slice(1).toLowerCase(), formatI18nField(doc[key])])
|
...Object.keys(doc)
|
||||||
].filter((doc) => doc[1])
|
.sort()
|
||||||
|
.map((key) => [
|
||||||
|
key.charAt(0) + key.slice(1).toLowerCase(),
|
||||||
|
formatI18nField(doc[key]),
|
||||||
|
]),
|
||||||
|
].filter((doc) => doc[1]),
|
||||||
},
|
},
|
||||||
is_webapp: app.is_webapp,
|
is_webapp: app.is_webapp,
|
||||||
is_default: app.is_default,
|
is_default: app.is_default,
|
||||||
supports_change_url: app.supports_change_url,
|
supports_change_url: app.supports_change_url,
|
||||||
supports_config_panel: app.supports_config_panel,
|
supports_config_panel: app.supports_config_panel,
|
||||||
supports_purge: app.supports_purge,
|
supports_purge: app.supports_purge,
|
||||||
permissions
|
permissions,
|
||||||
}
|
}
|
||||||
if (app.settings.domain && app.settings.path) {
|
if (app.settings.domain && app.settings.path) {
|
||||||
this.app.url = 'https://' + app.settings.domain + app.settings.path
|
this.app.url = 'https://' + app.settings.domain + app.settings.path
|
||||||
form.url = {
|
form.url = {
|
||||||
domain: app.settings.domain,
|
domain: app.settings.domain,
|
||||||
path: app.settings.path.slice(1)
|
path: app.settings.path.slice(1),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Object.values(this.app.doc.notifications).some((notif) => notif.length)) {
|
if (
|
||||||
|
!Object.values(this.app.doc.notifications).some((notif) => notif.length)
|
||||||
|
) {
|
||||||
this.app.doc.notifications = null
|
this.app.doc.notifications = null
|
||||||
}
|
}
|
||||||
|
|
||||||
if (app.supports_config_panel) {
|
if (app.supports_config_panel) {
|
||||||
await api.get(`apps/${this.id}/config?full`).then((config) => {
|
await api
|
||||||
const config_ = formatYunoHostConfigPanels(config)
|
.get(`apps/${this.id}/config?full`)
|
||||||
// reinject 'operations' fake config tab
|
.then((config) => {
|
||||||
config_.panels.unshift(this.config.panels[0])
|
const config_ = formatYunoHostConfigPanels(config)
|
||||||
this.config = config_
|
// reinject 'operations' fake config tab
|
||||||
}).catch((err) => {
|
config_.panels.unshift(this.config.panels[0])
|
||||||
this.config_panel_err = err.message
|
this.config = config_
|
||||||
})
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
this.config_panel_err = err.message
|
||||||
|
})
|
||||||
}
|
}
|
||||||
this.loading = false
|
this.loading = false
|
||||||
},
|
},
|
||||||
|
|
||||||
async onConfigSubmit ({ id, form, action, name }) {
|
async onConfigSubmit({ id, form, action, name }) {
|
||||||
const args = await formatFormData(form, { removeEmpty: false, removeNull: true })
|
const args = await formatFormData(form, {
|
||||||
|
removeEmpty: false,
|
||||||
api.put(
|
removeNull: true,
|
||||||
action
|
|
||||||
? `apps/${this.id}/actions/${action}`
|
|
||||||
: `apps/${this.id}/config/${id}`,
|
|
||||||
isEmptyValue(args) ? {} : { args: objectToParams(args) },
|
|
||||||
{ key: `apps.${action ? 'action' : 'update'}_config`, id, name: this.id }
|
|
||||||
).then(() => {
|
|
||||||
this.loading = true
|
|
||||||
this.$refs.view.fetchQueries()
|
|
||||||
}).catch(err => {
|
|
||||||
if (err.name !== 'APIBadRequestError') throw err
|
|
||||||
const panel = this.config.panels.find(panel => panel.id === id)
|
|
||||||
if (err.data.name) {
|
|
||||||
this.config.errors[id][err.data.name].message = err.message
|
|
||||||
} else this.$set(panel, 'serverError', err.message)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
api
|
||||||
|
.put(
|
||||||
|
action
|
||||||
|
? `apps/${this.id}/actions/${action}`
|
||||||
|
: `apps/${this.id}/config/${id}`,
|
||||||
|
isEmptyValue(args) ? {} : { args: objectToParams(args) },
|
||||||
|
{
|
||||||
|
key: `apps.${action ? 'action' : 'update'}_config`,
|
||||||
|
id,
|
||||||
|
name: this.id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.then(() => {
|
||||||
|
this.loading = true
|
||||||
|
this.$refs.view.fetchQueries()
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
if (err.name !== 'APIBadRequestError') throw err
|
||||||
|
const panel = this.config.panels.find((panel) => panel.id === id)
|
||||||
|
if (err.data.name) {
|
||||||
|
this.config.errors[id][err.data.name].message = err.message
|
||||||
|
} else this.$set(panel, 'serverError', err.message)
|
||||||
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
changeLabel (permName, data) {
|
changeLabel(permName, data) {
|
||||||
data.show_tile = data.show_tile ? 'True' : 'False'
|
data.show_tile = data.show_tile ? 'True' : 'False'
|
||||||
api.put(
|
api
|
||||||
'users/permissions/' + permName,
|
.put('users/permissions/' + permName, data, {
|
||||||
data,
|
key: 'apps.change_label',
|
||||||
{ key: 'apps.change_label', prevName: this.app.label, nextName: data.label }
|
prevName: this.app.label,
|
||||||
).then(this.$refs.view.fetchQueries)
|
nextName: data.label,
|
||||||
|
})
|
||||||
|
.then(this.$refs.view.fetchQueries)
|
||||||
},
|
},
|
||||||
|
|
||||||
async changeUrl () {
|
async changeUrl() {
|
||||||
const confirmed = await this.$askConfirmation(this.$i18n.t('confirm_app_change_url'))
|
const confirmed = await this.$askConfirmation(
|
||||||
|
this.$i18n.t('confirm_app_change_url'),
|
||||||
|
)
|
||||||
if (!confirmed) return
|
if (!confirmed) return
|
||||||
|
|
||||||
const { domain, path } = this.form.url
|
const { domain, path } = this.form.url
|
||||||
api.put(
|
api
|
||||||
`apps/${this.id}/changeurl`,
|
.put(
|
||||||
{ domain, path: '/' + path },
|
`apps/${this.id}/changeurl`,
|
||||||
{ key: 'apps.change_url', name: this.app.label }
|
{ domain, path: '/' + path },
|
||||||
).then(this.$refs.view.fetchQueries)
|
{ key: 'apps.change_url', name: this.app.label },
|
||||||
|
)
|
||||||
|
.then(this.$refs.view.fetchQueries)
|
||||||
},
|
},
|
||||||
|
|
||||||
async setAsDefaultDomain (undo = false) {
|
async setAsDefaultDomain(undo = false) {
|
||||||
const confirmed = await this.$askConfirmation(this.$i18n.t('confirm_app_default'))
|
const confirmed = await this.$askConfirmation(
|
||||||
|
this.$i18n.t('confirm_app_default'),
|
||||||
|
)
|
||||||
if (!confirmed) return
|
if (!confirmed) return
|
||||||
|
|
||||||
api.put(
|
api
|
||||||
`apps/${this.id}/default${undo ? '?undo' : ''}`,
|
.put(
|
||||||
{},
|
`apps/${this.id}/default${undo ? '?undo' : ''}`,
|
||||||
{ key: 'apps.set_default', name: this.app.label, domain: this.app.domain }
|
{},
|
||||||
).then(this.$refs.view.fetchQueries)
|
{
|
||||||
|
key: 'apps.set_default',
|
||||||
|
name: this.app.label,
|
||||||
|
domain: this.app.domain,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.then(this.$refs.view.fetchQueries)
|
||||||
},
|
},
|
||||||
|
|
||||||
async dismissNotification (name) {
|
async dismissNotification(name) {
|
||||||
api.put(
|
api
|
||||||
`apps/${this.id}/dismiss_notification/${name}`,
|
.put(
|
||||||
{},
|
`apps/${this.id}/dismiss_notification/${name}`,
|
||||||
{ key: 'apps.dismiss_notification', name: this.app.label }
|
{},
|
||||||
).then(this.$refs.view.fetchQueries)
|
{ key: 'apps.dismiss_notification', name: this.app.label },
|
||||||
|
)
|
||||||
|
.then(this.$refs.view.fetchQueries)
|
||||||
},
|
},
|
||||||
|
|
||||||
async uninstall () {
|
async uninstall() {
|
||||||
const data = this.purge === true ? { purge: 1 } : {}
|
const data = this.purge === true ? { purge: 1 } : {}
|
||||||
api.delete('apps/' + this.id, data, { key: 'apps.uninstall', name: this.app.label }).then(() => {
|
api
|
||||||
this.$router.push({ name: 'app-list' })
|
.delete('apps/' + this.id, data, {
|
||||||
})
|
key: 'apps.uninstall',
|
||||||
}
|
name: this.app.label,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
this.$router.push({ name: 'app-list' })
|
||||||
|
})
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
mixins: [validationMixin]
|
mixins: [validationMixin],
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -9,8 +9,10 @@
|
||||||
|
|
||||||
<BButton
|
<BButton
|
||||||
v-if="app.demo"
|
v-if="app.demo"
|
||||||
:href="app.demo" target="_blank"
|
:href="app.demo"
|
||||||
variant="primary" class="ml-auto"
|
target="_blank"
|
||||||
|
variant="primary"
|
||||||
|
class="ml-auto"
|
||||||
>
|
>
|
||||||
<YIcon iname="external-link" />
|
<YIcon iname="external-link" />
|
||||||
{{ $t('app.install.try_demo') }}
|
{{ $t('app.install.try_demo') }}
|
||||||
|
@ -18,7 +20,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="text-secondary">
|
<p class="text-secondary">
|
||||||
{{ $t('app.install.version', { version: app.version }) }}<br>
|
{{ $t('app.install.version', { version: app.version }) }}<br />
|
||||||
|
|
||||||
<template v-if="app.alternativeTo">
|
<template v-if="app.alternativeTo">
|
||||||
{{ $t('app.potential_alternative_to') }} {{ app.alternativeTo }}
|
{{ $t('app.potential_alternative_to') }} {{ app.alternativeTo }}
|
||||||
|
@ -30,27 +32,42 @@
|
||||||
<BImg
|
<BImg
|
||||||
v-if="app.screenshot"
|
v-if="app.screenshot"
|
||||||
:src="app.screenshot"
|
:src="app.screenshot"
|
||||||
aria-hidden="true" class="d-block" fluid
|
aria-hidden="true"
|
||||||
|
class="d-block"
|
||||||
|
fluid
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<YCard
|
<YCard
|
||||||
v-if="app.integration"
|
v-if="app.integration"
|
||||||
id="app-integration" :title="$t('app.integration.title')"
|
id="app-integration"
|
||||||
collapsable collapsed no-body
|
:title="$t('app.integration.title')"
|
||||||
|
collapsable
|
||||||
|
collapsed
|
||||||
|
no-body
|
||||||
>
|
>
|
||||||
<BListGroup flush>
|
<BListGroup flush>
|
||||||
<YListGroupItem variant="info">
|
<YListGroupItem variant="info">
|
||||||
{{ $t('app.integration.archs') }} {{ app.integration.archs }}
|
{{ $t('app.integration.archs') }} {{ app.integration.archs }}
|
||||||
</YListGroupItem>
|
</YListGroupItem>
|
||||||
<YListGroupItem v-if="app.integration.ldap" :variant="app.integration.ldap === true ? 'success' : 'warning'">
|
<YListGroupItem
|
||||||
|
v-if="app.integration.ldap"
|
||||||
|
:variant="app.integration.ldap === true ? 'success' : 'warning'"
|
||||||
|
>
|
||||||
{{ $t(`app.integration.ldap.${app.integration.ldap}`) }}
|
{{ $t(`app.integration.ldap.${app.integration.ldap}`) }}
|
||||||
</YListGroupItem>
|
</YListGroupItem>
|
||||||
<YListGroupItem v-if="app.integration.sso" :variant="app.integration.sso === true ? 'success' : 'warning'">
|
<YListGroupItem
|
||||||
|
v-if="app.integration.sso"
|
||||||
|
:variant="app.integration.sso === true ? 'success' : 'warning'"
|
||||||
|
>
|
||||||
{{ $t(`app.integration.sso.${app.integration.sso}`) }}
|
{{ $t(`app.integration.sso.${app.integration.sso}`) }}
|
||||||
</YListGroupItem>
|
</YListGroupItem>
|
||||||
<YListGroupItem variant="info">
|
<YListGroupItem variant="info">
|
||||||
{{ $t(`app.integration.multi_instance.${app.integration.multi_instance}`) }}
|
{{
|
||||||
|
$t(
|
||||||
|
`app.integration.multi_instance.${app.integration.multi_instance}`,
|
||||||
|
)
|
||||||
|
}}
|
||||||
</YListGroupItem>
|
</YListGroupItem>
|
||||||
<YListGroupItem variant="info">
|
<YListGroupItem variant="info">
|
||||||
{{ $t('app.integration.resources', app.integration.resources) }}
|
{{ $t('app.integration.resources', app.integration.resources) }}
|
||||||
|
@ -59,8 +76,12 @@
|
||||||
</YCard>
|
</YCard>
|
||||||
|
|
||||||
<YCard
|
<YCard
|
||||||
id="app-links" icon="link" :title="$t('app.links.title')"
|
id="app-links"
|
||||||
collapsable collapsed no-body
|
icon="link"
|
||||||
|
:title="$t('app.links.title')"
|
||||||
|
collapsable
|
||||||
|
collapsed
|
||||||
|
no-body
|
||||||
>
|
>
|
||||||
<template #header>
|
<template #header>
|
||||||
<h2><YIcon iname="link" /> {{ $t('app.links.title') }}</h2>
|
<h2><YIcon iname="link" /> {{ $t('app.links.title') }}</h2>
|
||||||
|
@ -94,14 +115,23 @@
|
||||||
</dl>
|
</dl>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<p v-if="app.quality.state === 'lowquality'" v-t="'app.install.problems.lowquality'" />
|
<p
|
||||||
|
v-if="app.quality.state === 'lowquality'"
|
||||||
|
v-t="'app.install.problems.lowquality'"
|
||||||
|
/>
|
||||||
|
|
||||||
<VueShowdown v-if="app.preInstall" :markdown="app.preInstall" flavor="github" />
|
<VueShowdown
|
||||||
|
v-if="app.preInstall"
|
||||||
|
:markdown="app.preInstall"
|
||||||
|
flavor="github"
|
||||||
|
/>
|
||||||
</YAlert>
|
</YAlert>
|
||||||
|
|
||||||
<YAlert
|
<YAlert
|
||||||
v-if="!app.hasSupport"
|
v-if="!app.hasSupport"
|
||||||
variant="danger" icon="warning" class="my-4"
|
variant="danger"
|
||||||
|
icon="warning"
|
||||||
|
class="my-4"
|
||||||
>
|
>
|
||||||
<h2>{{ $t('app.install.notifs.pre.critical') }}</h2>
|
<h2>{{ $t('app.install.notifs.pre.critical') }}</h2>
|
||||||
|
|
||||||
|
@ -109,35 +139,58 @@
|
||||||
{{ $t('app.install.problems.arch', app.requirements.arch.values) }}
|
{{ $t('app.install.problems.arch', app.requirements.arch.values) }}
|
||||||
</p>
|
</p>
|
||||||
<p v-if="!app.requirements.install.pass">
|
<p v-if="!app.requirements.install.pass">
|
||||||
{{ $t('app.install.problems.install', app.requirements.install.values) }}
|
{{
|
||||||
|
$t('app.install.problems.install', app.requirements.install.values)
|
||||||
|
}}
|
||||||
</p>
|
</p>
|
||||||
<p v-if="!app.requirements.required_yunohost_version.pass">
|
<p v-if="!app.requirements.required_yunohost_version.pass">
|
||||||
{{ $t('app.install.problems.version', app.requirements.required_yunohost_version.values) }}
|
{{
|
||||||
|
$t(
|
||||||
|
'app.install.problems.version',
|
||||||
|
app.requirements.required_yunohost_version.values,
|
||||||
|
)
|
||||||
|
}}
|
||||||
</p>
|
</p>
|
||||||
</YAlert>
|
</YAlert>
|
||||||
|
|
||||||
<YAlert v-else-if="app.hasDanger" variant="danger" class="my-4">
|
<YAlert v-else-if="app.hasDanger" variant="danger" class="my-4">
|
||||||
<h2>{{ $t('app.install.notifs.pre.danger') }}</h2>
|
<h2>{{ $t('app.install.notifs.pre.danger') }}</h2>
|
||||||
|
|
||||||
<p v-if="['inprogress', 'broken', 'thirdparty'].includes(app.quality.state)" v-t="'app.install.problems.' + app.quality.state" />
|
<p
|
||||||
|
v-if="
|
||||||
|
['inprogress', 'broken', 'thirdparty'].includes(app.quality.state)
|
||||||
|
"
|
||||||
|
v-t="'app.install.problems.' + app.quality.state"
|
||||||
|
/>
|
||||||
<p v-if="!app.requirements.ram.pass">
|
<p v-if="!app.requirements.ram.pass">
|
||||||
{{ $t('app.install.problems.ram', app.requirements.ram.values) }}
|
{{ $t('app.install.problems.ram', app.requirements.ram.values) }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<CheckboxItem v-model="force" id="force-install" :label="$t('app.install.problems.ignore')" />
|
<CheckboxItem
|
||||||
|
v-model="force"
|
||||||
|
id="force-install"
|
||||||
|
:label="$t('app.install.problems.ignore')"
|
||||||
|
/>
|
||||||
</YAlert>
|
</YAlert>
|
||||||
|
|
||||||
<!-- INSTALL FORM -->
|
<!-- INSTALL FORM -->
|
||||||
<CardForm
|
<CardForm
|
||||||
v-if="app.canInstall || force"
|
v-if="app.canInstall || force"
|
||||||
:title="$t('app_install_parameters')" icon="cog" :submit-text="$t('install')"
|
:title="$t('app_install_parameters')"
|
||||||
:validation="$v" :server-error="serverError"
|
icon="cog"
|
||||||
|
:submit-text="$t('install')"
|
||||||
|
:validation="$v"
|
||||||
|
:server-error="serverError"
|
||||||
@submit.prevent="performInstall"
|
@submit.prevent="performInstall"
|
||||||
>
|
>
|
||||||
<template v-for="(field, fname) in fields">
|
<template v-for="(field, fname) in fields">
|
||||||
<Component
|
<Component
|
||||||
v-if="field.visible" :is="field.is" v-bind="field.props"
|
v-if="field.visible"
|
||||||
v-model="form[fname]" :validation="$v.form[fname]" :key="fname"
|
:is="field.is"
|
||||||
|
v-bind="field.props"
|
||||||
|
v-model="form[fname]"
|
||||||
|
:validation="$v.form[fname]"
|
||||||
|
:key="fname"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</CardForm>
|
</CardForm>
|
||||||
|
@ -145,7 +198,8 @@
|
||||||
|
|
||||||
<!-- In case of a custom url with no manifest found -->
|
<!-- In case of a custom url with no manifest found -->
|
||||||
<BAlert v-else-if="app === null" variant="warning">
|
<BAlert v-else-if="app === null" variant="warning">
|
||||||
<YIcon iname="exclamation-triangle" /> {{ $t('app_install_custom_no_manifest') }}
|
<YIcon iname="exclamation-triangle" />
|
||||||
|
{{ $t('app_install_custom_no_manifest') }}
|
||||||
</BAlert>
|
</BAlert>
|
||||||
|
|
||||||
<template #skeleton>
|
<template #skeleton>
|
||||||
|
@ -162,7 +216,7 @@ import api, { objectToParams } from '@/api'
|
||||||
import {
|
import {
|
||||||
formatYunoHostArguments,
|
formatYunoHostArguments,
|
||||||
formatI18nField,
|
formatI18nField,
|
||||||
formatFormData
|
formatFormData,
|
||||||
} from '@/helpers/yunohostArguments'
|
} from '@/helpers/yunohostArguments'
|
||||||
import CardCollapse from '@/components/CardCollapse.vue'
|
import CardCollapse from '@/components/CardCollapse.vue'
|
||||||
|
|
||||||
|
@ -172,18 +226,18 @@ export default {
|
||||||
mixins: [validationMixin],
|
mixins: [validationMixin],
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
CardCollapse
|
CardCollapse,
|
||||||
},
|
},
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
id: { type: String, required: true }
|
id: { type: String, required: true },
|
||||||
},
|
},
|
||||||
|
|
||||||
data () {
|
data() {
|
||||||
return {
|
return {
|
||||||
queries: [
|
queries: [
|
||||||
['GET', 'apps/catalog?full&with_categories&with_antifeatures'],
|
['GET', 'apps/catalog?full&with_categories&with_antifeatures'],
|
||||||
['GET', `apps/manifest?app=${this.id}&with_screenshot`]
|
['GET', `apps/manifest?app=${this.id}&with_screenshot`],
|
||||||
],
|
],
|
||||||
app: undefined,
|
app: undefined,
|
||||||
name: undefined,
|
name: undefined,
|
||||||
|
@ -192,16 +246,16 @@ export default {
|
||||||
validations: null,
|
validations: null,
|
||||||
errors: undefined,
|
errors: undefined,
|
||||||
serverError: '',
|
serverError: '',
|
||||||
force: false
|
force: false,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
validations () {
|
validations() {
|
||||||
return this.validations
|
return this.validations
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
appLinksIcons (linkType) {
|
appLinksIcons(linkType) {
|
||||||
const linksIcons = {
|
const linksIcons = {
|
||||||
license: 'institution',
|
license: 'institution',
|
||||||
website: 'globe',
|
website: 'globe',
|
||||||
|
@ -210,16 +264,25 @@ export default {
|
||||||
code: 'code',
|
code: 'code',
|
||||||
package: 'code',
|
package: 'code',
|
||||||
package_license: 'institution',
|
package_license: 'institution',
|
||||||
forum: 'comments'
|
forum: 'comments',
|
||||||
}
|
}
|
||||||
return linksIcons[linkType]
|
return linksIcons[linkType]
|
||||||
},
|
},
|
||||||
|
|
||||||
onQueriesResponse (catalog, _app) {
|
onQueriesResponse(catalog, _app) {
|
||||||
const antifeaturesList = Object.fromEntries(catalog.antifeatures.map((af) => ([af.id, af])))
|
const antifeaturesList = Object.fromEntries(
|
||||||
|
catalog.antifeatures.map((af) => [af.id, af]),
|
||||||
|
)
|
||||||
|
|
||||||
const { id, name, version, requirements } = _app
|
const { id, name, version, requirements } = _app
|
||||||
const { ldap, sso, multi_instance, ram, disk, architectures: archs } = _app.integration
|
const {
|
||||||
|
ldap,
|
||||||
|
sso,
|
||||||
|
multi_instance,
|
||||||
|
ram,
|
||||||
|
disk,
|
||||||
|
architectures: archs,
|
||||||
|
} = _app.integration
|
||||||
|
|
||||||
const quality = { state: _app.quality.state, variant: 'danger' }
|
const quality = { state: _app.quality.state, variant: 'danger' }
|
||||||
if (quality.state === 'working') {
|
if (quality.state === 'working') {
|
||||||
|
@ -230,7 +293,8 @@ export default {
|
||||||
quality.variant = 'warning'
|
quality.variant = 'warning'
|
||||||
} else {
|
} else {
|
||||||
quality.variant = 'success'
|
quality.variant = 'success'
|
||||||
quality.state = _app.quality.level >= 8 ? 'highquality' : 'goodquality'
|
quality.state =
|
||||||
|
_app.quality.level >= 8 ? 'highquality' : 'goodquality'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const preInstall = formatI18nField(_app.notifications.PRE_INSTALL.main)
|
const preInstall = formatI18nField(_app.notifications.PRE_INSTALL.main)
|
||||||
|
@ -247,38 +311,47 @@ export default {
|
||||||
const app = {
|
const app = {
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
alternativeTo: _app.potential_alternative_to && _app.potential_alternative_to.length
|
alternativeTo:
|
||||||
? _app.potential_alternative_to.join(this.$i18n.t('words.separator'))
|
_app.potential_alternative_to && _app.potential_alternative_to.length
|
||||||
: null,
|
? _app.potential_alternative_to.join(
|
||||||
|
this.$i18n.t('words.separator'),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
description: formatI18nField(_app.doc.DESCRIPTION || _app.description),
|
description: formatI18nField(_app.doc.DESCRIPTION || _app.description),
|
||||||
screenshot: _app.screenshot,
|
screenshot: _app.screenshot,
|
||||||
demo: _app.upstream.demo,
|
demo: _app.upstream.demo,
|
||||||
version,
|
version,
|
||||||
license: _app.upstream.license,
|
license: _app.upstream.license,
|
||||||
integration: _app.packaging_format >= 2
|
integration:
|
||||||
? {
|
_app.packaging_format >= 2
|
||||||
archs: Array.isArray(archs) ? archs.join(this.$i18n.t('words.separator')) : archs,
|
? {
|
||||||
ldap: ldap === 'not_relevant' ? null : ldap,
|
archs: Array.isArray(archs)
|
||||||
sso: sso === 'not_relevant' ? null : sso,
|
? archs.join(this.$i18n.t('words.separator'))
|
||||||
multi_instance,
|
: archs,
|
||||||
resources: { ram: ram.runtime, disk }
|
ldap: ldap === 'not_relevant' ? null : ldap,
|
||||||
}
|
sso: sso === 'not_relevant' ? null : sso,
|
||||||
: null,
|
multi_instance,
|
||||||
|
resources: { ram: ram.runtime, disk },
|
||||||
|
}
|
||||||
|
: null,
|
||||||
links: [
|
links: [
|
||||||
['license', `https://spdx.org/licenses/${_app.upstream.license}`],
|
['license', `https://spdx.org/licenses/${_app.upstream.license}`],
|
||||||
...['website', 'admindoc', 'userdoc', 'code'].map((key) => ([key, _app.upstream[key]])),
|
...['website', 'admindoc', 'userdoc', 'code'].map((key) => {
|
||||||
|
return [key, _app.upstream[key]]
|
||||||
|
}),
|
||||||
['package', _app.remote.url],
|
['package', _app.remote.url],
|
||||||
['package_license', _app.remote.url + '/blob/master/LICENSE'],
|
['package_license', _app.remote.url + '/blob/master/LICENSE'],
|
||||||
['forum', `https://forum.yunohost.org/tag/${id}`]
|
['forum', `https://forum.yunohost.org/tag/${id}`],
|
||||||
].filter(([key, val]) => !!val),
|
].filter(([key, val]) => !!val),
|
||||||
preInstall,
|
preInstall,
|
||||||
antifeatures,
|
antifeatures,
|
||||||
quality,
|
quality,
|
||||||
requirements,
|
requirements,
|
||||||
hasWarning: !!preInstall || antifeatures || quality.variant === 'warning',
|
hasWarning:
|
||||||
|
!!preInstall || antifeatures || quality.variant === 'warning',
|
||||||
hasDanger,
|
hasDanger,
|
||||||
hasSupport,
|
hasSupport,
|
||||||
canInstall: hasSupport && !hasDanger
|
canInstall: hasSupport && !hasDanger,
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME yunohost should add the label field by default
|
// FIXME yunohost should add the label field by default
|
||||||
|
@ -286,15 +359,12 @@ export default {
|
||||||
ask: this.$t('label_for_manifestname', { name }),
|
ask: this.$t('label_for_manifestname', { name }),
|
||||||
default: name,
|
default: name,
|
||||||
name: 'label',
|
name: 'label',
|
||||||
help: this.$t('label_for_manifestname_help')
|
help: this.$t('label_for_manifestname_help'),
|
||||||
})
|
})
|
||||||
|
|
||||||
const {
|
const { form, fields, validations, errors } = formatYunoHostArguments(
|
||||||
form,
|
_app.install,
|
||||||
fields,
|
)
|
||||||
validations,
|
|
||||||
errors
|
|
||||||
} = formatYunoHostArguments(_app.install)
|
|
||||||
|
|
||||||
this.app = app
|
this.app = app
|
||||||
this.fields = fields
|
this.fields = fields
|
||||||
|
@ -303,51 +373,70 @@ export default {
|
||||||
this.errors = errors
|
this.errors = errors
|
||||||
},
|
},
|
||||||
|
|
||||||
formatAppNotifs (notifs) {
|
formatAppNotifs(notifs) {
|
||||||
return Object.keys(notifs).reduce((acc, key) => {
|
return Object.keys(notifs).reduce((acc, key) => {
|
||||||
return acc + '\n\n' + notifs[key]
|
return acc + '\n\n' + notifs[key]
|
||||||
}, '')
|
}, '')
|
||||||
},
|
},
|
||||||
|
|
||||||
async performInstall () {
|
async performInstall() {
|
||||||
if ('path' in this.form && this.form.path === '/') {
|
if ('path' in this.form && this.form.path === '/') {
|
||||||
const confirmed = await this.$askConfirmation(
|
const confirmed = await this.$askConfirmation(
|
||||||
this.$i18n.t('confirm_install_domain_root', { domain: this.form.domain })
|
this.$i18n.t('confirm_install_domain_root', {
|
||||||
|
domain: this.form.domain,
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
if (!confirmed) return
|
if (!confirmed) return
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data: args, label } = await formatFormData(
|
const { data: args, label } = await formatFormData(this.form, {
|
||||||
this.form,
|
extract: ['label'],
|
||||||
{ extract: ['label'], removeEmpty: false, removeNull: true }
|
removeEmpty: false,
|
||||||
)
|
removeNull: true,
|
||||||
const data = { app: this.id, label, args: Object.entries(args).length ? objectToParams(args) : undefined }
|
|
||||||
|
|
||||||
api.post('apps', data, { key: 'apps.install', name: this.app.name }).then(async ({ notifications }) => {
|
|
||||||
const postInstall = this.formatAppNotifs(notifications)
|
|
||||||
if (postInstall) {
|
|
||||||
const message = this.$i18n.t('app.install.notifs.post.alert') + '\n\n' + postInstall
|
|
||||||
await this.$askMdConfirmation(message, {
|
|
||||||
title: this.$i18n.t('app.install.notifs.post.title', { name: this.app.name }),
|
|
||||||
okTitle: this.$i18n.t('ok')
|
|
||||||
}, true)
|
|
||||||
}
|
|
||||||
this.$router.push({ name: 'app-list' })
|
|
||||||
}).catch(err => {
|
|
||||||
if (err.name !== 'APIBadRequestError') throw err
|
|
||||||
if (err.data.name) {
|
|
||||||
this.errors[err.data.name].message = err.message
|
|
||||||
} else this.serverError = err.message
|
|
||||||
})
|
})
|
||||||
}
|
const data = {
|
||||||
}
|
app: this.id,
|
||||||
|
label,
|
||||||
|
args: Object.entries(args).length ? objectToParams(args) : undefined,
|
||||||
|
}
|
||||||
|
|
||||||
|
api
|
||||||
|
.post('apps', data, { key: 'apps.install', name: this.app.name })
|
||||||
|
.then(async ({ notifications }) => {
|
||||||
|
const postInstall = this.formatAppNotifs(notifications)
|
||||||
|
if (postInstall) {
|
||||||
|
const message =
|
||||||
|
this.$i18n.t('app.install.notifs.post.alert') +
|
||||||
|
'\n\n' +
|
||||||
|
postInstall
|
||||||
|
await this.$askMdConfirmation(
|
||||||
|
message,
|
||||||
|
{
|
||||||
|
title: this.$i18n.t('app.install.notifs.post.title', {
|
||||||
|
name: this.app.name,
|
||||||
|
}),
|
||||||
|
okTitle: this.$i18n.t('ok'),
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
this.$router.push({ name: 'app-list' })
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
if (err.name !== 'APIBadRequestError') throw err
|
||||||
|
if (err.data.name) {
|
||||||
|
this.errors[err.data.name].message = err.message
|
||||||
|
} else this.serverError = err.message
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.antifeatures {
|
.antifeatures {
|
||||||
dt::before {
|
dt::before {
|
||||||
content: "• ";
|
content: '• ';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -16,8 +16,9 @@
|
||||||
|
|
||||||
<BListGroup>
|
<BListGroup>
|
||||||
<BListGroupItem
|
<BListGroupItem
|
||||||
v-for="{ id, description, label } in filteredApps" :key="id"
|
v-for="{ id, description, label } in filteredApps"
|
||||||
:to="{ name: 'app-info', params: { id }}"
|
:key="id"
|
||||||
|
:to="{ name: 'app-info', params: { id } }"
|
||||||
class="d-flex justify-content-between align-items-center pr-0"
|
class="d-flex justify-content-between align-items-center pr-0"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
|
@ -40,40 +41,40 @@
|
||||||
export default {
|
export default {
|
||||||
name: 'AppList',
|
name: 'AppList',
|
||||||
|
|
||||||
data () {
|
data() {
|
||||||
return {
|
return {
|
||||||
queries: [
|
queries: [['GET', 'apps?full']],
|
||||||
['GET', 'apps?full']
|
|
||||||
],
|
|
||||||
search: '',
|
search: '',
|
||||||
apps: undefined
|
apps: undefined,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
filteredApps () {
|
filteredApps() {
|
||||||
if (!this.apps) return
|
if (!this.apps) return
|
||||||
const search = this.search.toLowerCase()
|
const search = this.search.toLowerCase()
|
||||||
const match = (item) => item && item.toLowerCase().includes(search)
|
const match = (item) => item && item.toLowerCase().includes(search)
|
||||||
// Check if any value in apps (label, id, name, description) match the search query.
|
// Check if any value in apps (label, id, name, description) match the search query.
|
||||||
const filtered = this.apps.filter(app => Object.values(app).some(match))
|
const filtered = this.apps.filter((app) => Object.values(app).some(match))
|
||||||
return filtered.length ? filtered : null
|
return filtered.length ? filtered : null
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
onQueriesResponse ({ apps }) {
|
onQueriesResponse({ apps }) {
|
||||||
if (apps.length === 0) {
|
if (apps.length === 0) {
|
||||||
this.apps = null
|
this.apps = null
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
this.apps = apps.map(({ id, name, description, manifest }) => {
|
this.apps = apps
|
||||||
return { id, name: manifest.name, label: name, description }
|
.map(({ id, name, description, manifest }) => {
|
||||||
}).sort((prev, app) => {
|
return { id, name: manifest.name, label: name, description }
|
||||||
return prev.label > app.label ? 1 : -1
|
})
|
||||||
})
|
.sort((prev, app) => {
|
||||||
}
|
return prev.label > app.label ? 1 : -1
|
||||||
}
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,34 +1,46 @@
|
||||||
<template>
|
<template>
|
||||||
<ViewBase :queries="queries" @queries-response="onQueriesResponse" skeleton="CardListSkeleton">
|
<ViewBase
|
||||||
|
:queries="queries"
|
||||||
|
@queries-response="onQueriesResponse"
|
||||||
|
skeleton="CardListSkeleton"
|
||||||
|
>
|
||||||
<!-- FIXME switch to <CardForm> ? -->
|
<!-- FIXME switch to <CardForm> ? -->
|
||||||
<YCard :title="$t('backup_create')" icon="archive" no-body>
|
<YCard :title="$t('backup_create')" icon="archive" no-body>
|
||||||
<BFormCheckboxGroup
|
<BFormCheckboxGroup
|
||||||
v-model="selected"
|
v-model="selected"
|
||||||
id="backup-select" name="backup-select" size="lg"
|
id="backup-select"
|
||||||
|
name="backup-select"
|
||||||
|
size="lg"
|
||||||
>
|
>
|
||||||
<BListGroup flush>
|
<BListGroup flush>
|
||||||
<!-- SYSTEM HEADER -->
|
<!-- SYSTEM HEADER -->
|
||||||
<BListGroupItem class="d-flex align-items-sm-center flex-column flex-sm-row text-primary">
|
<BListGroupItem
|
||||||
<h4 class="m-0">
|
class="d-flex align-items-sm-center flex-column flex-sm-row text-primary"
|
||||||
<YIcon iname="cube" /> {{ $t('system') }}
|
>
|
||||||
</h4>
|
<h4 class="m-0"><YIcon iname="cube" /> {{ $t('system') }}</h4>
|
||||||
|
|
||||||
<div class="ml-sm-auto mt-2 mt-sm-0">
|
<div class="ml-sm-auto mt-2 mt-sm-0">
|
||||||
<BButton
|
<BButton
|
||||||
@click="toggleSelected(true, 'system')" v-t="'select_all'"
|
@click="toggleSelected(true, 'system')"
|
||||||
size="sm" variant="outline-dark"
|
v-t="'select_all'"
|
||||||
|
size="sm"
|
||||||
|
variant="outline-dark"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<BButton
|
<BButton
|
||||||
@click="toggleSelected(false, 'system')" v-t="'select_none'"
|
@click="toggleSelected(false, 'system')"
|
||||||
size="sm" variant="outline-dark" class="ml-2"
|
v-t="'select_none'"
|
||||||
|
size="sm"
|
||||||
|
variant="outline-dark"
|
||||||
|
class="ml-2"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</BListGroupItem>
|
</BListGroupItem>
|
||||||
|
|
||||||
<!-- SYSTEM ITEMS -->
|
<!-- SYSTEM ITEMS -->
|
||||||
<BListGroupItem
|
<BListGroupItem
|
||||||
v-for="(item, partName) in system" :key="partName"
|
v-for="(item, partName) in system"
|
||||||
|
:key="partName"
|
||||||
class="d-flex justify-content-between align-items-center pr-0"
|
class="d-flex justify-content-between align-items-center pr-0"
|
||||||
>
|
>
|
||||||
<div class="mr-2">
|
<div class="mr-2">
|
||||||
|
@ -40,43 +52,60 @@
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<BFormCheckbox :value="partName" :aria-label="$t('check')" class="d-inline" />
|
<BFormCheckbox
|
||||||
|
:value="partName"
|
||||||
|
:aria-label="$t('check')"
|
||||||
|
class="d-inline"
|
||||||
|
/>
|
||||||
</BListGroupItem>
|
</BListGroupItem>
|
||||||
|
|
||||||
<!-- APPS HEADER -->
|
<!-- APPS HEADER -->
|
||||||
<BListGroupItem class="d-flex align-items-sm-center flex-column flex-sm-row text-primary">
|
<BListGroupItem
|
||||||
|
class="d-flex align-items-sm-center flex-column flex-sm-row text-primary"
|
||||||
|
>
|
||||||
<h4 class="m-0">
|
<h4 class="m-0">
|
||||||
<YIcon iname="cubes" /> {{ $t('applications') }}
|
<YIcon iname="cubes" /> {{ $t('applications') }}
|
||||||
</h4>
|
</h4>
|
||||||
|
|
||||||
<div class="ml-sm-auto mt-2 mt-sm-0">
|
<div class="ml-sm-auto mt-2 mt-sm-0">
|
||||||
<BButton
|
<BButton
|
||||||
@click="toggleSelected(true, 'apps')" v-t="'select_all'"
|
@click="toggleSelected(true, 'apps')"
|
||||||
size="sm" variant="outline-dark"
|
v-t="'select_all'"
|
||||||
|
size="sm"
|
||||||
|
variant="outline-dark"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<BButton
|
<BButton
|
||||||
@click="toggleSelected(false, 'apps')" v-t="'select_none'"
|
@click="toggleSelected(false, 'apps')"
|
||||||
size="sm" variant="outline-dark" class="ml-2"
|
v-t="'select_none'"
|
||||||
|
size="sm"
|
||||||
|
variant="outline-dark"
|
||||||
|
class="ml-2"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</BListGroupItem>
|
</BListGroupItem>
|
||||||
|
|
||||||
<!-- APPS ITEMS -->
|
<!-- APPS ITEMS -->
|
||||||
<BListGroupItem
|
<BListGroupItem
|
||||||
v-for="(item, appName) in apps" :key="appName"
|
v-for="(item, appName) in apps"
|
||||||
|
:key="appName"
|
||||||
class="d-flex justify-content-between align-items-center pr-0"
|
class="d-flex justify-content-between align-items-center pr-0"
|
||||||
>
|
>
|
||||||
<div class="mr-2">
|
<div class="mr-2">
|
||||||
<h5 class="font-weight-bold">
|
<h5 class="font-weight-bold">
|
||||||
{{ item.name }} <small class="text-secondary">{{ item.id }}</small>
|
{{ item.name }}
|
||||||
|
<small class="text-secondary">{{ item.id }}</small>
|
||||||
</h5>
|
</h5>
|
||||||
<p class="m-0">
|
<p class="m-0">
|
||||||
{{ item.description }}
|
{{ item.description }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<BFormCheckbox :value="appName" :aria-label="$t('check')" class="d-inline" />
|
<BFormCheckbox
|
||||||
|
:value="appName"
|
||||||
|
:aria-label="$t('check')"
|
||||||
|
class="d-inline"
|
||||||
|
/>
|
||||||
</BListGroupItem>
|
</BListGroupItem>
|
||||||
</BListGroup>
|
</BListGroup>
|
||||||
</BFormCheckboxGroup>
|
</BFormCheckboxGroup>
|
||||||
|
@ -84,8 +113,10 @@
|
||||||
<!-- SUBMIT -->
|
<!-- SUBMIT -->
|
||||||
<template #buttons>
|
<template #buttons>
|
||||||
<BButton
|
<BButton
|
||||||
@click="createBackup" v-t="'backup_action'"
|
@click="createBackup"
|
||||||
variant="success" :disabled="selected.length === 0"
|
v-t="'backup_action'"
|
||||||
|
variant="success"
|
||||||
|
:disabled="selected.length === 0"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</YCard>
|
</YCard>
|
||||||
|
@ -99,27 +130,29 @@ export default {
|
||||||
name: 'BackupCreate',
|
name: 'BackupCreate',
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
id: { type: String, required: true }
|
id: { type: String, required: true },
|
||||||
},
|
},
|
||||||
|
|
||||||
data () {
|
data() {
|
||||||
return {
|
return {
|
||||||
queries: [
|
queries: [
|
||||||
['GET', 'hooks/backup'],
|
['GET', 'hooks/backup'],
|
||||||
['GET', 'apps?with_backup']
|
['GET', 'apps?with_backup'],
|
||||||
],
|
],
|
||||||
selected: [],
|
selected: [],
|
||||||
// api data
|
// api data
|
||||||
system: undefined,
|
system: undefined,
|
||||||
apps: undefined
|
apps: undefined,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
formatHooks (hooks) {
|
formatHooks(hooks) {
|
||||||
const data = {}
|
const data = {}
|
||||||
hooks.forEach(hook => {
|
hooks.forEach((hook) => {
|
||||||
const groupId = hook.startsWith('conf_') ? 'adminjs_group_configuration' : hook
|
const groupId = hook.startsWith('conf_')
|
||||||
|
? 'adminjs_group_configuration'
|
||||||
|
: hook
|
||||||
if (groupId in data) {
|
if (groupId in data) {
|
||||||
data[groupId].value.push(hook)
|
data[groupId].value.push(hook)
|
||||||
data[groupId].description += ', ' + this.$i18n.t('hook_' + hook)
|
data[groupId].description += ', ' + this.$i18n.t('hook_' + hook)
|
||||||
|
@ -127,14 +160,16 @@ export default {
|
||||||
data[groupId] = {
|
data[groupId] = {
|
||||||
name: this.$i18n.t('hook_' + groupId),
|
name: this.$i18n.t('hook_' + groupId),
|
||||||
value: [hook],
|
value: [hook],
|
||||||
description: this.$i18n.t(groupId === hook ? `hook_${hook}_desc` : 'hook_' + hook)
|
description: this.$i18n.t(
|
||||||
|
groupId === hook ? `hook_${hook}_desc` : 'hook_' + hook,
|
||||||
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return data
|
return data
|
||||||
},
|
},
|
||||||
|
|
||||||
onQueriesResponse ({ hooks }, { apps }) {
|
onQueriesResponse({ hooks }, { apps }) {
|
||||||
this.system = this.formatHooks(hooks)
|
this.system = this.formatHooks(hooks)
|
||||||
// transform app array into literal object to match hooks data structure
|
// transform app array into literal object to match hooks data structure
|
||||||
this.apps = apps.reduce((obj, app) => {
|
this.apps = apps.reduce((obj, app) => {
|
||||||
|
@ -144,17 +179,21 @@ export default {
|
||||||
this.selected = [...Object.keys(this.system), ...Object.keys(this.apps)]
|
this.selected = [...Object.keys(this.system), ...Object.keys(this.apps)]
|
||||||
},
|
},
|
||||||
|
|
||||||
toggleSelected (select, type) {
|
toggleSelected(select, type) {
|
||||||
if (select) {
|
if (select) {
|
||||||
const toSelect = Object.keys(this[type]).filter(item => !this.selected.includes(item))
|
const toSelect = Object.keys(this[type]).filter(
|
||||||
|
(item) => !this.selected.includes(item),
|
||||||
|
)
|
||||||
this.selected = [...this.selected, ...toSelect]
|
this.selected = [...this.selected, ...toSelect]
|
||||||
} else {
|
} else {
|
||||||
const toUnselect = Object.keys(this[type])
|
const toUnselect = Object.keys(this[type])
|
||||||
this.selected = this.selected.filter(selected => !toUnselect.includes(selected))
|
this.selected = this.selected.filter(
|
||||||
|
(selected) => !toUnselect.includes(selected),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
createBackup () {
|
createBackup() {
|
||||||
const data = { apps: [], system: [] }
|
const data = { apps: [], system: [] }
|
||||||
for (const item of this.selected) {
|
for (const item of this.selected) {
|
||||||
if (item in this.system) {
|
if (item in this.system) {
|
||||||
|
@ -167,7 +206,7 @@ export default {
|
||||||
api.post('backups', data, 'backups.create').then(() => {
|
api.post('backups', data, 'backups.create').then(() => {
|
||||||
this.$router.push({ name: 'backup-list', params: { id: this.id } })
|
this.$router.push({ name: 'backup-list', params: { id: this.id } })
|
||||||
})
|
})
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -15,8 +15,10 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<BRow
|
<BRow
|
||||||
v-for="(value, prop) in infos" :key="prop"
|
v-for="(value, prop) in infos"
|
||||||
no-gutters class="row-line"
|
:key="prop"
|
||||||
|
no-gutters
|
||||||
|
class="row-line"
|
||||||
>
|
>
|
||||||
<BCol md="3" xl="2">
|
<BCol md="3" xl="2">
|
||||||
<strong>{{ $t(prop === 'name' ? 'id' : prop) }}</strong>
|
<strong>{{ $t(prop === 'name' ? 'id' : prop) }}</strong>
|
||||||
|
@ -32,35 +34,48 @@
|
||||||
<!-- BACKUP CONTENT -->
|
<!-- BACKUP CONTENT -->
|
||||||
<!-- FIXME switch to <CardForm> ? -->
|
<!-- FIXME switch to <CardForm> ? -->
|
||||||
<YCard
|
<YCard
|
||||||
:title="$t('backup_content')" icon="archive"
|
:title="$t('backup_content')"
|
||||||
no-body button-unbreak="sm"
|
icon="archive"
|
||||||
|
no-body
|
||||||
|
button-unbreak="sm"
|
||||||
>
|
>
|
||||||
<template #header-buttons>
|
<template #header-buttons>
|
||||||
<BButton
|
<BButton
|
||||||
size="sm" variant="outline-secondary"
|
size="sm"
|
||||||
@click="toggleSelected()" v-t="'select_all'"
|
variant="outline-secondary"
|
||||||
|
@click="toggleSelected()"
|
||||||
|
v-t="'select_all'"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<BButton
|
<BButton
|
||||||
size="sm" variant="outline-secondary"
|
size="sm"
|
||||||
@click="toggleSelected(false)" v-t="'select_none'"
|
variant="outline-secondary"
|
||||||
|
@click="toggleSelected(false)"
|
||||||
|
v-t="'select_none'"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<BFormCheckboxGroup
|
<BFormCheckboxGroup
|
||||||
v-if="hasBackupData" v-model="selected"
|
v-if="hasBackupData"
|
||||||
id="backup-select" name="backup-select" size="lg"
|
v-model="selected"
|
||||||
|
id="backup-select"
|
||||||
|
name="backup-select"
|
||||||
|
size="lg"
|
||||||
aria-describedby="backup-restore-feedback"
|
aria-describedby="backup-restore-feedback"
|
||||||
>
|
>
|
||||||
<BListGroup flush>
|
<BListGroup flush>
|
||||||
<!-- SYSTEM PARTS -->
|
<!-- SYSTEM PARTS -->
|
||||||
<BListGroupItem
|
<BListGroupItem
|
||||||
v-for="(item, partName) in system" :key="partName"
|
v-for="(item, partName) in system"
|
||||||
|
:key="partName"
|
||||||
class="d-flex justify-content-between align-items-center pr-0"
|
class="d-flex justify-content-between align-items-center pr-0"
|
||||||
>
|
>
|
||||||
<div class="mr-2">
|
<div class="mr-2">
|
||||||
<h5 class="font-weight-bold">
|
<h5 class="font-weight-bold">
|
||||||
{{ item.name }} <small class="text-secondary" v-if="item.size">({{ humanSize(item.size) }})</small>
|
{{ item.name }}
|
||||||
|
<small class="text-secondary" v-if="item.size">
|
||||||
|
({{ humanSize(item.size) }})
|
||||||
|
</small>
|
||||||
</h5>
|
</h5>
|
||||||
<p class="m-0">
|
<p class="m-0">
|
||||||
{{ item.description }}
|
{{ item.description }}
|
||||||
|
@ -72,16 +87,18 @@
|
||||||
|
|
||||||
<!-- APPS -->
|
<!-- APPS -->
|
||||||
<BListGroupItem
|
<BListGroupItem
|
||||||
v-for="(item, appName) in apps" :key="appName"
|
v-for="(item, appName) in apps"
|
||||||
|
:key="appName"
|
||||||
class="d-flex justify-content-between align-items-center pr-0"
|
class="d-flex justify-content-between align-items-center pr-0"
|
||||||
>
|
>
|
||||||
<div class="mr-2">
|
<div class="mr-2">
|
||||||
<h5 class="font-weight-bold">
|
<h5 class="font-weight-bold">
|
||||||
{{ item.name }} <small class="text-secondary">{{ appName }} ({{ humanSize(item.size) }})</small>
|
{{ item.name }}
|
||||||
|
<small class="text-secondary">
|
||||||
|
{{ appName }} ({{ humanSize(item.size) }})
|
||||||
|
</small>
|
||||||
</h5>
|
</h5>
|
||||||
<p class="m-0">
|
<p class="m-0">{{ $t('version') }} {{ item.version }}</p>
|
||||||
{{ $t('version') }} {{ item.version }}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<BFormCheckbox :value="appName" :aria-label="$t('check')" />
|
<BFormCheckbox :value="appName" :aria-label="$t('check')" />
|
||||||
|
@ -102,8 +119,11 @@
|
||||||
<!-- SUBMIT -->
|
<!-- SUBMIT -->
|
||||||
<template v-if="hasBackupData" #buttons>
|
<template v-if="hasBackupData" #buttons>
|
||||||
<BButton
|
<BButton
|
||||||
@click="restoreBackup" form="backup-restore" variant="success"
|
@click="restoreBackup"
|
||||||
v-t="'restore'" :disabled="selected.length === 0"
|
form="backup-restore"
|
||||||
|
variant="success"
|
||||||
|
v-t="'restore'"
|
||||||
|
:disabled="selected.length === 0"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</YCard>
|
</YCard>
|
||||||
|
@ -126,35 +146,35 @@ export default {
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
id: { type: String, required: true },
|
id: { type: String, required: true },
|
||||||
name: { type: String, required: true }
|
name: { type: String, required: true },
|
||||||
},
|
},
|
||||||
|
|
||||||
data () {
|
data() {
|
||||||
return {
|
return {
|
||||||
queries: [
|
queries: [['GET', `backups/${this.name}?with_details`]],
|
||||||
['GET', `backups/${this.name}?with_details`]
|
|
||||||
],
|
|
||||||
selected: [],
|
selected: [],
|
||||||
error: '',
|
error: '',
|
||||||
isValid: null,
|
isValid: null,
|
||||||
// api data
|
// api data
|
||||||
infos: undefined,
|
infos: undefined,
|
||||||
apps: undefined,
|
apps: undefined,
|
||||||
system: undefined
|
system: undefined,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
hasBackupData () {
|
hasBackupData() {
|
||||||
return !isEmptyValue(this.system) || !isEmptyValue(this.apps)
|
return !isEmptyValue(this.system) || !isEmptyValue(this.apps)
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
formatHooks (hooks) {
|
formatHooks(hooks) {
|
||||||
const data = {}
|
const data = {}
|
||||||
Object.entries(hooks).forEach(([hook, { size }]) => {
|
Object.entries(hooks).forEach(([hook, { size }]) => {
|
||||||
const groupId = hook.startsWith('conf_') ? 'adminjs_group_configuration' : hook
|
const groupId = hook.startsWith('conf_')
|
||||||
|
? 'adminjs_group_configuration'
|
||||||
|
: hook
|
||||||
if (groupId in data) {
|
if (groupId in data) {
|
||||||
data[groupId].value.push(hook)
|
data[groupId].value.push(hook)
|
||||||
data[groupId].description += ', ' + this.$i18n.t('hook_' + hook)
|
data[groupId].description += ', ' + this.$i18n.t('hook_' + hook)
|
||||||
|
@ -163,20 +183,22 @@ export default {
|
||||||
data[groupId] = {
|
data[groupId] = {
|
||||||
name: this.$i18n.t('hook_' + groupId),
|
name: this.$i18n.t('hook_' + groupId),
|
||||||
value: [hook],
|
value: [hook],
|
||||||
description: this.$i18n.t(groupId === hook ? `hook_${hook}_desc` : 'hook_' + hook),
|
description: this.$i18n.t(
|
||||||
size
|
groupId === hook ? `hook_${hook}_desc` : 'hook_' + hook,
|
||||||
|
),
|
||||||
|
size,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return data
|
return data
|
||||||
},
|
},
|
||||||
|
|
||||||
onQueriesResponse (data) {
|
onQueriesResponse(data) {
|
||||||
this.infos = {
|
this.infos = {
|
||||||
name: this.name,
|
name: this.name,
|
||||||
created_at: data.created_at,
|
created_at: data.created_at,
|
||||||
size: data.size,
|
size: data.size,
|
||||||
path: data.path
|
path: data.path,
|
||||||
}
|
}
|
||||||
this.system = this.formatHooks(data.system)
|
this.system = this.formatHooks(data.system)
|
||||||
this.apps = data.apps
|
this.apps = data.apps
|
||||||
|
@ -184,20 +206,17 @@ export default {
|
||||||
this.toggleSelected()
|
this.toggleSelected()
|
||||||
},
|
},
|
||||||
|
|
||||||
toggleSelected (select = true) {
|
toggleSelected(select = true) {
|
||||||
if (select) {
|
if (select) {
|
||||||
this.selected = [
|
this.selected = [...Object.keys(this.apps), ...Object.keys(this.system)]
|
||||||
...Object.keys(this.apps),
|
|
||||||
...Object.keys(this.system)
|
|
||||||
]
|
|
||||||
} else {
|
} else {
|
||||||
this.selected = []
|
this.selected = []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async restoreBackup () {
|
async restoreBackup() {
|
||||||
const confirmed = await this.$askConfirmation(
|
const confirmed = await this.$askConfirmation(
|
||||||
this.$i18n.t('confirm_restore', { name: this.name })
|
this.$i18n.t('confirm_restore', { name: this.name }),
|
||||||
)
|
)
|
||||||
if (!confirmed) return
|
if (!confirmed) return
|
||||||
|
|
||||||
|
@ -210,35 +229,48 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
api.put(
|
api
|
||||||
`backups/${this.name}/restore`, data, { key: 'backups.restore', name: this.name }
|
.put(`backups/${this.name}/restore`, data, {
|
||||||
).then(() => {
|
key: 'backups.restore',
|
||||||
this.isValid = null
|
name: this.name,
|
||||||
}).catch(err => {
|
})
|
||||||
if (err.name !== 'APIBadRequestError') throw err
|
.then(() => {
|
||||||
this.error = err.message
|
this.isValid = null
|
||||||
this.isValid = false
|
})
|
||||||
})
|
.catch((err) => {
|
||||||
|
if (err.name !== 'APIBadRequestError') throw err
|
||||||
|
this.error = err.message
|
||||||
|
this.isValid = false
|
||||||
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
async deleteBackup () {
|
async deleteBackup() {
|
||||||
const confirmed = await this.$askConfirmation(this.$i18n.t('confirm_delete', { name: this.name }))
|
const confirmed = await this.$askConfirmation(
|
||||||
|
this.$i18n.t('confirm_delete', { name: this.name }),
|
||||||
|
)
|
||||||
if (!confirmed) return
|
if (!confirmed) return
|
||||||
|
|
||||||
api.delete(
|
api
|
||||||
'backups/' + this.name, {}, { key: 'backups.delete', name: this.name }
|
.delete(
|
||||||
).then(() => {
|
'backups/' + this.name,
|
||||||
this.$router.push({ name: 'backup-list', params: { id: this.id } })
|
{},
|
||||||
})
|
{ key: 'backups.delete', name: this.name },
|
||||||
|
)
|
||||||
|
.then(() => {
|
||||||
|
this.$router.push({ name: 'backup-list', params: { id: this.id } })
|
||||||
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
downloadBackup () {
|
downloadBackup() {
|
||||||
const host = this.$store.getters.host
|
const host = this.$store.getters.host
|
||||||
window.open(`https://${host}/yunohost/api/backups/${this.name}/download`, '_blank')
|
window.open(
|
||||||
|
`https://${host}/yunohost/api/backups/${this.name}/download`,
|
||||||
|
'_blank',
|
||||||
|
)
|
||||||
},
|
},
|
||||||
|
|
||||||
readableDate,
|
readableDate,
|
||||||
humanSize
|
humanSize,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,7 +1,17 @@
|
||||||
<template>
|
<template>
|
||||||
<ViewBase :queries="queries" @queries-response="onQueriesResponse" skeleton="ListGroupSkeleton">
|
<ViewBase
|
||||||
|
:queries="queries"
|
||||||
|
@queries-response="onQueriesResponse"
|
||||||
|
skeleton="ListGroupSkeleton"
|
||||||
|
>
|
||||||
<template #top>
|
<template #top>
|
||||||
<TopBar :button="{ text: $t('backup_new'), icon: 'plus', to: { name: 'backup-create' } }" />
|
<TopBar
|
||||||
|
:button="{
|
||||||
|
text: $t('backup_new'),
|
||||||
|
icon: 'plus',
|
||||||
|
to: { name: 'backup-create' },
|
||||||
|
}"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<BAlert v-if="!archives" variant="warning">
|
<BAlert v-if="!archives" variant="warning">
|
||||||
|
@ -11,15 +21,18 @@
|
||||||
|
|
||||||
<BListGroup v-else>
|
<BListGroup v-else>
|
||||||
<BListGroupItem
|
<BListGroupItem
|
||||||
v-for="{ name, created_at, path, size } in archives" :key="name"
|
v-for="{ name, created_at, path, size } in archives"
|
||||||
:to="{ name: 'backup-info', params: { name, id }}"
|
:key="name"
|
||||||
|
:to="{ name: 'backup-info', params: { name, id } }"
|
||||||
:title="readableDate(created_at)"
|
:title="readableDate(created_at)"
|
||||||
class="d-flex justify-content-between align-items-center pr-0"
|
class="d-flex justify-content-between align-items-center pr-0"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<h5 class="font-weight-bold">
|
<h5 class="font-weight-bold">
|
||||||
{{ distanceToNow(created_at) }}
|
{{ distanceToNow(created_at) }}
|
||||||
<small class="text-secondary">{{ name }} ({{ humanSize(size) }})</small>
|
<small class="text-secondary"
|
||||||
|
>{{ name }} ({{ humanSize(size) }})</small
|
||||||
|
>
|
||||||
</h5>
|
</h5>
|
||||||
<p class="mb-0">
|
<p class="mb-0">
|
||||||
{{ path }}
|
{{ path }}
|
||||||
|
@ -39,26 +52,26 @@ export default {
|
||||||
name: 'BackupList',
|
name: 'BackupList',
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
id: { type: String, required: true }
|
id: { type: String, required: true },
|
||||||
},
|
},
|
||||||
|
|
||||||
data () {
|
data() {
|
||||||
return {
|
return {
|
||||||
queries: [
|
queries: [['GET', 'backups?with_info']],
|
||||||
['GET', 'backups?with_info']
|
archives: undefined,
|
||||||
],
|
|
||||||
archives: undefined
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
onQueriesResponse (data) {
|
onQueriesResponse(data) {
|
||||||
const archives = Object.entries(data.archives)
|
const archives = Object.entries(data.archives)
|
||||||
if (archives.length) {
|
if (archives.length) {
|
||||||
this.archives = archives.map(([name, infos]) => {
|
this.archives = archives
|
||||||
infos.name = name
|
.map(([name, infos]) => {
|
||||||
return infos
|
infos.name = name
|
||||||
}).reverse()
|
return infos
|
||||||
|
})
|
||||||
|
.reverse()
|
||||||
} else {
|
} else {
|
||||||
this.archives = null
|
this.archives = null
|
||||||
}
|
}
|
||||||
|
@ -66,7 +79,7 @@ export default {
|
||||||
|
|
||||||
distanceToNow,
|
distanceToNow,
|
||||||
readableDate,
|
readableDate,
|
||||||
humanSize
|
humanSize,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -2,8 +2,9 @@
|
||||||
<div>
|
<div>
|
||||||
<BListGroup>
|
<BListGroup>
|
||||||
<BListGroupItem
|
<BListGroupItem
|
||||||
v-for="{ id, name, uri } in storages" :key="id"
|
v-for="{ id, name, uri } in storages"
|
||||||
:to="{ name: 'backup-list', params: { id }}"
|
:key="id"
|
||||||
|
:to="{ name: 'backup-list', params: { id } }"
|
||||||
class="d-flex justify-content-between align-items-center pr-0"
|
class="d-flex justify-content-between align-items-center pr-0"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
|
@ -25,16 +26,16 @@
|
||||||
export default {
|
export default {
|
||||||
name: 'BackupView',
|
name: 'BackupView',
|
||||||
|
|
||||||
data () {
|
data() {
|
||||||
return {
|
return {
|
||||||
storages: [
|
storages: [
|
||||||
{
|
{
|
||||||
id: 'local',
|
id: 'local',
|
||||||
name: this.$i18n.t('local_archives'),
|
name: this.$i18n.t('local_archives'),
|
||||||
uri: '/home/yunohost.backup/'
|
uri: '/home/yunohost.backup/',
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
<template>
|
<template>
|
||||||
<ViewBase
|
<ViewBase
|
||||||
:queries="queries" @queries-response="onQueriesResponse" queries-wait
|
:queries="queries"
|
||||||
|
@queries-response="onQueriesResponse"
|
||||||
|
queries-wait
|
||||||
ref="view"
|
ref="view"
|
||||||
>
|
>
|
||||||
<template #top-bar-group-right>
|
<template #top-bar-group-right>
|
||||||
|
@ -13,7 +15,9 @@
|
||||||
<div class="alert alert-info">
|
<div class="alert alert-info">
|
||||||
{{ $t(reports ? 'diagnosis_explanation' : 'diagnosis_first_run') }}
|
{{ $t(reports ? 'diagnosis_explanation' : 'diagnosis_first_run') }}
|
||||||
<BButton
|
<BButton
|
||||||
v-if="reports === null" class="d-block mt-2" variant="info"
|
v-if="reports === null"
|
||||||
|
class="d-block mt-2"
|
||||||
|
variant="info"
|
||||||
@click="runDiagnosis()"
|
@click="runDiagnosis()"
|
||||||
>
|
>
|
||||||
<YIcon iname="stethoscope" /> {{ $t('run_first_diagnosis') }}
|
<YIcon iname="stethoscope" /> {{ $t('run_first_diagnosis') }}
|
||||||
|
@ -23,24 +27,46 @@
|
||||||
|
|
||||||
<!-- REPORT CARD -->
|
<!-- REPORT CARD -->
|
||||||
<YCard
|
<YCard
|
||||||
v-for="report in reports" :key="report.id"
|
v-for="report in reports"
|
||||||
collapsable :collapsed="report.noIssues"
|
:key="report.id"
|
||||||
no-body button-unbreak="lg"
|
collapsable
|
||||||
|
:collapsed="report.noIssues"
|
||||||
|
no-body
|
||||||
|
button-unbreak="lg"
|
||||||
>
|
>
|
||||||
<!-- REPORT HEADER -->
|
<!-- REPORT HEADER -->
|
||||||
<template #header>
|
<template #header>
|
||||||
<h2>{{ report.description }}</h2>
|
<h2>{{ report.description }}</h2>
|
||||||
|
|
||||||
<div class="">
|
<div class="">
|
||||||
<BBadge v-if="report.noIssues" variant="success" v-t="'everything_good'" />
|
<BBadge
|
||||||
<BBadge v-if="report.errors" variant="danger" v-t="{ path: 'issues', args: { count: report.errors } }" />
|
v-if="report.noIssues"
|
||||||
<BBadge v-if="report.warnings" variant="warning" v-t="{ path: 'warnings', args: { count: report.warnings } }" />
|
variant="success"
|
||||||
<BBadge v-if="report.ignoreds" v-t="{ path: 'ignored', args: { count: report.ignoreds } }" />
|
v-t="'everything_good'"
|
||||||
|
/>
|
||||||
|
<BBadge
|
||||||
|
v-if="report.errors"
|
||||||
|
variant="danger"
|
||||||
|
v-t="{ path: 'issues', args: { count: report.errors } }"
|
||||||
|
/>
|
||||||
|
<BBadge
|
||||||
|
v-if="report.warnings"
|
||||||
|
variant="warning"
|
||||||
|
v-t="{ path: 'warnings', args: { count: report.warnings } }"
|
||||||
|
/>
|
||||||
|
<BBadge
|
||||||
|
v-if="report.ignoreds"
|
||||||
|
v-t="{ path: 'ignored', args: { count: report.ignoreds } }"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #header-buttons>
|
<template #header-buttons>
|
||||||
<BButton size="sm" :variant="report.items ? 'info' : 'success'" @click="runDiagnosis(report)">
|
<BButton
|
||||||
|
size="sm"
|
||||||
|
:variant="report.items ? 'info' : 'success'"
|
||||||
|
@click="runDiagnosis(report)"
|
||||||
|
>
|
||||||
<YIcon iname="refresh" /> {{ $t('rerun_diagnosis') }}
|
<YIcon iname="refresh" /> {{ $t('rerun_diagnosis') }}
|
||||||
</BButton>
|
</BButton>
|
||||||
</template>
|
</template>
|
||||||
|
@ -53,21 +79,27 @@
|
||||||
<BListGroup flush>
|
<BListGroup flush>
|
||||||
<!-- REPORT ITEM -->
|
<!-- REPORT ITEM -->
|
||||||
<YListGroupItem
|
<YListGroupItem
|
||||||
v-for="(item, i) in report.items" :key="i"
|
v-for="(item, i) in report.items"
|
||||||
:variant="item.variant" :icon="item.Icon" :faded="item.ignored"
|
:key="i"
|
||||||
|
:variant="item.variant"
|
||||||
|
:icon="item.Icon"
|
||||||
|
:faded="item.ignored"
|
||||||
>
|
>
|
||||||
<div class="item-button d-flex align-items-center">
|
<div class="item-button d-flex align-items-center">
|
||||||
<p class="mb-0 mr-2" v-html="item.summary" />
|
<p class="mb-0 mr-2" v-html="item.summary" />
|
||||||
|
|
||||||
<div class="d-flex flex-column flex-lg-row ml-auto">
|
<div class="d-flex flex-column flex-lg-row ml-auto">
|
||||||
<BButton
|
<BButton
|
||||||
v-if="item.ignored" size="sm"
|
v-if="item.ignored"
|
||||||
|
size="sm"
|
||||||
@click="toggleIgnoreIssue('unignore', report, item)"
|
@click="toggleIgnoreIssue('unignore', report, item)"
|
||||||
>
|
>
|
||||||
<YIcon iname="bell" /> {{ $t('unignore') }}
|
<YIcon iname="bell" /> {{ $t('unignore') }}
|
||||||
</BButton>
|
</BButton>
|
||||||
<BButton
|
<BButton
|
||||||
v-else-if="item.issue" variant="warning" size="sm"
|
v-else-if="item.issue"
|
||||||
|
variant="warning"
|
||||||
|
size="sm"
|
||||||
@click="toggleIgnoreIssue('ignore', report, item)"
|
@click="toggleIgnoreIssue('ignore', report, item)"
|
||||||
>
|
>
|
||||||
<YIcon iname="bell-slash" /> {{ $t('ignore') }}
|
<YIcon iname="bell-slash" /> {{ $t('ignore') }}
|
||||||
|
@ -75,7 +107,9 @@
|
||||||
|
|
||||||
<BButton
|
<BButton
|
||||||
v-if="item.details"
|
v-if="item.details"
|
||||||
size="sm" variant="outline-dark" class="ml-lg-2 mt-2 mt-lg-0"
|
size="sm"
|
||||||
|
variant="outline-dark"
|
||||||
|
class="ml-lg-2 mt-2 mt-lg-0"
|
||||||
v-b-toggle="`collapse-${report.id}-item-${i}`"
|
v-b-toggle="`collapse-${report.id}-item-${i}`"
|
||||||
>
|
>
|
||||||
<YIcon iname="level-down" /> {{ $t('details') }}
|
<YIcon iname="level-down" /> {{ $t('details') }}
|
||||||
|
@ -83,9 +117,16 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<BCollapse v-if="item.details" :id="`collapse-${report.id}-item-${i}`">
|
<BCollapse
|
||||||
|
v-if="item.details"
|
||||||
|
:id="`collapse-${report.id}-item-${i}`"
|
||||||
|
>
|
||||||
<ul class="mt-2 pl-4">
|
<ul class="mt-2 pl-4">
|
||||||
<li v-for="(detail, index) in item.details" :key="index" v-html="detail" />
|
<li
|
||||||
|
v-for="(detail, index) in item.details"
|
||||||
|
:key="index"
|
||||||
|
v-html="detail"
|
||||||
|
/>
|
||||||
</ul>
|
</ul>
|
||||||
</BCollapse>
|
</BCollapse>
|
||||||
</YListGroupItem>
|
</YListGroupItem>
|
||||||
|
@ -114,22 +155,22 @@ import { DEFAULT_STATUS_ICON } from '@/helpers/yunohostArguments'
|
||||||
export default {
|
export default {
|
||||||
name: 'DiagnosisView',
|
name: 'DiagnosisView',
|
||||||
|
|
||||||
data () {
|
data() {
|
||||||
return {
|
return {
|
||||||
queries: [
|
queries: [
|
||||||
['PUT', 'diagnosis/run?except_if_never_ran_yet', {}, 'diagnosis.run'],
|
['PUT', 'diagnosis/run?except_if_never_ran_yet', {}, 'diagnosis.run'],
|
||||||
['GET', 'diagnosis?full']
|
['GET', 'diagnosis?full'],
|
||||||
],
|
],
|
||||||
reports: undefined
|
reports: undefined,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters(['theme'])
|
...mapGetters(['theme']),
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
onQueriesResponse (_, reportsData) {
|
onQueriesResponse(_, reportsData) {
|
||||||
if (reportsData === null) {
|
if (reportsData === null) {
|
||||||
this.reports = null
|
this.reports = null
|
||||||
return
|
return
|
||||||
|
@ -142,7 +183,7 @@ export default {
|
||||||
report.ignoreds = 0
|
report.ignoreds = 0
|
||||||
|
|
||||||
for (const item of report.items) {
|
for (const item of report.items) {
|
||||||
const status = item.variant = item.status.toLowerCase()
|
const status = (item.variant = item.status.toLowerCase())
|
||||||
item.icon = DEFAULT_STATUS_ICON[status]
|
item.icon = DEFAULT_STATUS_ICON[status]
|
||||||
item.issue = false
|
item.issue = false
|
||||||
|
|
||||||
|
@ -164,53 +205,58 @@ export default {
|
||||||
this.reports = reports
|
this.reports = reports
|
||||||
},
|
},
|
||||||
|
|
||||||
runDiagnosis ({ id = null, description } = {}) {
|
runDiagnosis({ id = null, description } = {}) {
|
||||||
const param = id !== null ? '?force' : ''
|
const param = id !== null ? '?force' : ''
|
||||||
const data = id !== null ? { categories: [id] } : {}
|
const data = id !== null ? { categories: [id] } : {}
|
||||||
|
|
||||||
api.put(
|
api
|
||||||
'diagnosis/run' + param,
|
.put('diagnosis/run' + param, data, {
|
||||||
data,
|
key: 'diagnosis.run' + (id !== null ? '_specific' : ''),
|
||||||
{ key: 'diagnosis.run' + (id !== null ? '_specific' : ''), description }
|
description,
|
||||||
).then(this.$refs.view.fetchQueries)
|
})
|
||||||
|
.then(this.$refs.view.fetchQueries)
|
||||||
},
|
},
|
||||||
|
|
||||||
toggleIgnoreIssue (action, report, item) {
|
toggleIgnoreIssue(action, report, item) {
|
||||||
const filterArgs = [report.id].concat(Object.entries(item.meta).map(entries => entries.join('=')))
|
const filterArgs = [report.id].concat(
|
||||||
|
Object.entries(item.meta).map((entries) => entries.join('=')),
|
||||||
|
)
|
||||||
|
|
||||||
api.put(
|
api
|
||||||
'diagnosis/' + action,
|
.put(
|
||||||
{ filter: filterArgs },
|
'diagnosis/' + action,
|
||||||
`diagnosis.${action}.${item.status.toLowerCase()}`
|
{ filter: filterArgs },
|
||||||
).then(() => {
|
`diagnosis.${action}.${item.status.toLowerCase()}`,
|
||||||
item.ignored = action === 'ignore'
|
)
|
||||||
if (item.ignored) {
|
.then(() => {
|
||||||
report[item.status.toLowerCase() + 's']--
|
item.ignored = action === 'ignore'
|
||||||
} else {
|
if (item.ignored) {
|
||||||
report.ignoreds--
|
report[item.status.toLowerCase() + 's']--
|
||||||
}
|
} else {
|
||||||
this.formatReportItem(report, item)
|
report.ignoreds--
|
||||||
})
|
}
|
||||||
|
this.formatReportItem(report, item)
|
||||||
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
shareLogs () {
|
shareLogs() {
|
||||||
api.get('diagnosis?share').then(({ url }) => {
|
api.get('diagnosis?share').then(({ url }) => {
|
||||||
window.open(url, '_blank')
|
window.open(url, '_blank')
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
distanceToNow
|
distanceToNow,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.badge + .badge {
|
.badge + .badge {
|
||||||
margin-left: .5rem
|
margin-left: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
p.last-time-run {
|
p.last-time-run {
|
||||||
margin: .75rem 1rem;
|
margin: 0.75rem 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-group {
|
.list-group {
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
<template>
|
<template>
|
||||||
<ViewBase :queries="queries" skeleton="CardFormSkeleton">
|
<ViewBase :queries="queries" skeleton="CardFormSkeleton">
|
||||||
<DomainForm
|
<DomainForm
|
||||||
:title="$t('domain_add')" :server-error="serverError"
|
:title="$t('domain_add')"
|
||||||
@submit="onSubmit" :submit-text="$t('add')"
|
:server-error="serverError"
|
||||||
|
@submit="onSubmit"
|
||||||
|
:submit-text="$t('add')"
|
||||||
/>
|
/>
|
||||||
</ViewBase>
|
</ViewBase>
|
||||||
</template>
|
</template>
|
||||||
|
@ -14,29 +16,28 @@ import { DomainForm } from '@/views/_partials'
|
||||||
export default {
|
export default {
|
||||||
name: 'DomainAdd',
|
name: 'DomainAdd',
|
||||||
|
|
||||||
data () {
|
data() {
|
||||||
return {
|
return {
|
||||||
queries: [
|
queries: [['GET', { uri: 'domains' }]],
|
||||||
['GET', { uri: 'domains' }]
|
serverError: '',
|
||||||
],
|
|
||||||
serverError: ''
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
onSubmit (data) {
|
onSubmit(data) {
|
||||||
api.post(
|
api
|
||||||
'domains', data, { key: 'domains.add', name: data.domain }
|
.post('domains', data, { key: 'domains.add', name: data.domain })
|
||||||
).then(() => {
|
.then(() => {
|
||||||
this.$store.dispatch('RESET_CACHE_DATA', ['domains'])
|
this.$store.dispatch('RESET_CACHE_DATA', ['domains'])
|
||||||
this.$router.push({ name: 'domain-list' })
|
this.$router.push({ name: 'domain-list' })
|
||||||
}).catch(err => {
|
})
|
||||||
if (err.name !== 'APIBadRequestError') throw err
|
.catch((err) => {
|
||||||
this.serverError = err.message
|
if (err.name !== 'APIBadRequestError') throw err
|
||||||
})
|
this.serverError = err.message
|
||||||
}
|
})
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
components: { DomainForm }
|
components: { DomainForm },
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
<template>
|
<template>
|
||||||
<ViewBase
|
<ViewBase
|
||||||
:queries="queries" @queries-response="onQueriesResponse" :loading="loading"
|
:queries="queries"
|
||||||
|
@queries-response="onQueriesResponse"
|
||||||
|
:loading="loading"
|
||||||
skeleton="CardInfoSkeleton"
|
skeleton="CardInfoSkeleton"
|
||||||
>
|
>
|
||||||
<section v-if="showAutoConfigCard" class="panel-section">
|
<section v-if="showAutoConfigCard" class="panel-section">
|
||||||
|
@ -16,22 +18,49 @@
|
||||||
|
|
||||||
<!-- AUTO CONFIG CHANGES -->
|
<!-- AUTO CONFIG CHANGES -->
|
||||||
<template v-if="dnsChanges">
|
<template v-if="dnsChanges">
|
||||||
<div class="mb-3" v-for="{ action, records, icon, variant} in dnsChanges" :key="icon">
|
<div
|
||||||
|
class="mb-3"
|
||||||
|
v-for="{ action, records, icon, variant } in dnsChanges"
|
||||||
|
:key="icon"
|
||||||
|
>
|
||||||
<h4 class="mt-4 mb-2">
|
<h4 class="mt-4 mb-2">
|
||||||
{{ action }}
|
{{ action }}
|
||||||
</h4>
|
</h4>
|
||||||
|
|
||||||
<div class="log">
|
<div class="log">
|
||||||
<div
|
<div
|
||||||
v-for="({ name: record, spaces, old_content, content, type, managed_by_yunohost }, i) in records" :key="i"
|
v-for="(
|
||||||
class="records px-2" :class="{ 'ignored': managed_by_yunohost === false && force !== true }"
|
{
|
||||||
:title="managed_by_yunohost === false && force !== true ? $t('domain.dns.auto_config_ignored') : null"
|
name: record,
|
||||||
|
spaces,
|
||||||
|
old_content,
|
||||||
|
content,
|
||||||
|
type,
|
||||||
|
managed_by_yunohost,
|
||||||
|
},
|
||||||
|
i
|
||||||
|
) in records"
|
||||||
|
:key="i"
|
||||||
|
class="records px-2"
|
||||||
|
:class="{
|
||||||
|
ignored: managed_by_yunohost === false && force !== true,
|
||||||
|
}"
|
||||||
|
:title="
|
||||||
|
managed_by_yunohost === false && force !== true
|
||||||
|
? $t('domain.dns.auto_config_ignored')
|
||||||
|
: null
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<YIcon :iname="icon" :class="'text-' + variant" />
|
<YIcon :iname="icon" :class="'text-' + variant" />
|
||||||
{{ record }}
|
{{ record }}
|
||||||
<span class="bg-dark text-light px-1 rounded">{{ type }}</span>{{ spaces }}
|
<span class="bg-dark text-light px-1 rounded">{{ type }}</span
|
||||||
<span v-if="old_content"><span class="text-danger">{{ old_content }}</span> --> </span>
|
>{{ spaces }}
|
||||||
<span :class="{ 'text-success': old_content }">{{ content }}</span>
|
<span v-if="old_content"
|
||||||
|
><span class="text-danger">{{ old_content }}</span> -->
|
||||||
|
</span>
|
||||||
|
<span :class="{ 'text-success': old_content }">{{
|
||||||
|
content
|
||||||
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -48,7 +77,8 @@
|
||||||
<!-- CONFIG ERROR ALERT -->
|
<!-- CONFIG ERROR ALERT -->
|
||||||
<template v-if="dnsErrors && dnsErrors.length">
|
<template v-if="dnsErrors && dnsErrors.length">
|
||||||
<ReadOnlyAlertItem
|
<ReadOnlyAlertItem
|
||||||
v-for="({ variant, icon, message }, i) in dnsErrors" :key="i"
|
v-for="({ variant, icon, message }, i) in dnsErrors"
|
||||||
|
:key="i"
|
||||||
:label="message"
|
:label="message"
|
||||||
:type="variant"
|
:type="variant"
|
||||||
:icon="icon"
|
:icon="icon"
|
||||||
|
@ -75,15 +105,23 @@
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- CURRENT DNS ZONE -->
|
<!-- CURRENT DNS ZONE -->
|
||||||
<section v-if="showAutoConfigCard && dnsZone && dnsZone.length" class="panel-section">
|
<section
|
||||||
|
v-if="showAutoConfigCard && dnsZone && dnsZone.length"
|
||||||
|
class="panel-section"
|
||||||
|
>
|
||||||
<BCardTitle title-tag="h3">
|
<BCardTitle title-tag="h3">
|
||||||
{{ $t('domain.dns.auto_config_zone') }}
|
{{ $t('domain.dns.auto_config_zone') }}
|
||||||
</BCardTitle>
|
</BCardTitle>
|
||||||
|
|
||||||
<div class="log">
|
<div class="log">
|
||||||
<div v-for="({ name: record, spaces, content, type }, i) in dnsZone" :key="'zone-' + i" class="records">
|
<div
|
||||||
|
v-for="({ name: record, spaces, content, type }, i) in dnsZone"
|
||||||
|
:key="'zone-' + i"
|
||||||
|
class="records"
|
||||||
|
>
|
||||||
{{ record }}
|
{{ record }}
|
||||||
<span class="bg-dark text-light px-1 rounded">{{ type }}</span>{{ spaces }}
|
<span class="bg-dark text-light px-1 rounded">{{ type }}</span
|
||||||
|
>{{ spaces }}
|
||||||
<span>{{ content }}</span>
|
<span>{{ content }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -113,14 +151,12 @@ export default {
|
||||||
name: 'DomainDns',
|
name: 'DomainDns',
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
name: { type: String, required: true }
|
name: { type: String, required: true },
|
||||||
},
|
},
|
||||||
|
|
||||||
data () {
|
data() {
|
||||||
return {
|
return {
|
||||||
queries: [
|
queries: [['GET', `domains/${this.name}/dns/suggest`]],
|
||||||
['GET', `domains/${this.name}/dns/suggest`]
|
|
||||||
],
|
|
||||||
loading: true,
|
loading: true,
|
||||||
showAutoConfigCard: true,
|
showAutoConfigCard: true,
|
||||||
showManualConfigCard: false,
|
showManualConfigCard: false,
|
||||||
|
@ -128,108 +164,121 @@ export default {
|
||||||
dnsChanges: undefined,
|
dnsChanges: undefined,
|
||||||
dnsErrors: undefined,
|
dnsErrors: undefined,
|
||||||
dnsZone: undefined,
|
dnsZone: undefined,
|
||||||
force: null
|
force: null,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
onQueriesResponse (suggestedConfig) {
|
onQueriesResponse(suggestedConfig) {
|
||||||
this.dnsConfig = suggestedConfig
|
this.dnsConfig = suggestedConfig
|
||||||
},
|
},
|
||||||
|
|
||||||
getDnsChanges () {
|
getDnsChanges() {
|
||||||
this.loading = true
|
this.loading = true
|
||||||
|
|
||||||
return api.post(
|
return api
|
||||||
`domains/${this.name}/dns/push?dry_run`, {}, null, { wait: false, websocket: false }
|
.post(`domains/${this.name}/dns/push?dry_run`, {}, null, {
|
||||||
).then(dnsChanges => {
|
wait: false,
|
||||||
function getLongest (arr, key) {
|
websocket: false,
|
||||||
return arr.reduce((acc, obj) => {
|
|
||||||
if (obj[key].length > acc) return obj[key].length
|
|
||||||
return acc
|
|
||||||
}, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
const changes = []
|
|
||||||
let canForce = false
|
|
||||||
const categories = [
|
|
||||||
{ action: 'create', icon: 'plus', variant: 'success' },
|
|
||||||
{ action: 'update', icon: 'exchange', variant: 'warning' },
|
|
||||||
{ action: 'delete', icon: 'minus', variant: 'danger' }
|
|
||||||
]
|
|
||||||
categories.forEach(category => {
|
|
||||||
const records = dnsChanges[category.action]
|
|
||||||
if (records && records.length > 0) {
|
|
||||||
const longestName = getLongest(records, 'name')
|
|
||||||
const longestType = getLongest(records, 'type')
|
|
||||||
records.forEach(record => {
|
|
||||||
record.name = record.name + ' '.repeat(longestName - record.name.length + 1)
|
|
||||||
record.spaces = ' '.repeat(longestType - record.type.length + 1)
|
|
||||||
if (record.managed_by_yunohost === false) canForce = true
|
|
||||||
})
|
|
||||||
changes.push({ ...category, records })
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
.then((dnsChanges) => {
|
||||||
|
function getLongest(arr, key) {
|
||||||
|
return arr.reduce((acc, obj) => {
|
||||||
|
if (obj[key].length > acc) return obj[key].length
|
||||||
|
return acc
|
||||||
|
}, 0)
|
||||||
|
}
|
||||||
|
|
||||||
const unchanged = dnsChanges.unchanged
|
const changes = []
|
||||||
if (unchanged) {
|
let canForce = false
|
||||||
const longestName = getLongest(unchanged, 'name')
|
const categories = [
|
||||||
const longestType = getLongest(unchanged, 'type')
|
{ action: 'create', icon: 'plus', variant: 'success' },
|
||||||
unchanged.forEach(record => {
|
{ action: 'update', icon: 'exchange', variant: 'warning' },
|
||||||
record.name = record.name + ' '.repeat(longestName - record.name.length + 1)
|
{ action: 'delete', icon: 'minus', variant: 'danger' },
|
||||||
record.spaces = ' '.repeat(longestType - record.type.length + 1)
|
]
|
||||||
|
categories.forEach((category) => {
|
||||||
|
const records = dnsChanges[category.action]
|
||||||
|
if (records && records.length > 0) {
|
||||||
|
const longestName = getLongest(records, 'name')
|
||||||
|
const longestType = getLongest(records, 'type')
|
||||||
|
records.forEach((record) => {
|
||||||
|
record.name =
|
||||||
|
record.name + ' '.repeat(longestName - record.name.length + 1)
|
||||||
|
record.spaces = ' '.repeat(longestType - record.type.length + 1)
|
||||||
|
if (record.managed_by_yunohost === false) canForce = true
|
||||||
|
})
|
||||||
|
changes.push({ ...category, records })
|
||||||
|
}
|
||||||
})
|
})
|
||||||
this.dnsZone = unchanged
|
|
||||||
}
|
|
||||||
|
|
||||||
this.dnsChanges = changes.length > 0 ? changes : null
|
const unchanged = dnsChanges.unchanged
|
||||||
this.force = canForce ? false : null
|
if (unchanged) {
|
||||||
this.loading = false
|
const longestName = getLongest(unchanged, 'name')
|
||||||
}).catch(err => {
|
const longestType = getLongest(unchanged, 'type')
|
||||||
if (err.name !== 'APIBadRequestError') throw err
|
unchanged.forEach((record) => {
|
||||||
const key = err.data.error_key
|
record.name =
|
||||||
if (key === 'domain_dns_push_managed_in_parent_domain') {
|
record.name + ' '.repeat(longestName - record.name.length + 1)
|
||||||
const message = this.$t(key, err.data)
|
record.spaces = ' '.repeat(longestType - record.type.length + 1)
|
||||||
this.dnsErrors = [{ icon: 'info', variant: 'info', message }]
|
})
|
||||||
} else if (key === 'domain_dns_push_failed_to_authenticate') {
|
this.dnsZone = unchanged
|
||||||
const message = this.$t(key, err.data)
|
}
|
||||||
this.dnsErrors = [{ icon: 'ban', variant: 'danger', message }]
|
|
||||||
} else {
|
this.dnsChanges = changes.length > 0 ? changes : null
|
||||||
this.showManualConfigCard = true
|
this.force = canForce ? false : null
|
||||||
this.showAutoConfigCard = false
|
this.loading = false
|
||||||
}
|
})
|
||||||
this.loading = false
|
.catch((err) => {
|
||||||
})
|
if (err.name !== 'APIBadRequestError') throw err
|
||||||
|
const key = err.data.error_key
|
||||||
|
if (key === 'domain_dns_push_managed_in_parent_domain') {
|
||||||
|
const message = this.$t(key, err.data)
|
||||||
|
this.dnsErrors = [{ icon: 'info', variant: 'info', message }]
|
||||||
|
} else if (key === 'domain_dns_push_failed_to_authenticate') {
|
||||||
|
const message = this.$t(key, err.data)
|
||||||
|
this.dnsErrors = [{ icon: 'ban', variant: 'danger', message }]
|
||||||
|
} else {
|
||||||
|
this.showManualConfigCard = true
|
||||||
|
this.showAutoConfigCard = false
|
||||||
|
}
|
||||||
|
this.loading = false
|
||||||
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
async pushDnsChanges () {
|
async pushDnsChanges() {
|
||||||
if (this.force) {
|
if (this.force) {
|
||||||
const confirmed = await this.$askConfirmation(this.$i18n.t('domain.dns.push_force_confirm'))
|
const confirmed = await this.$askConfirmation(
|
||||||
|
this.$i18n.t('domain.dns.push_force_confirm'),
|
||||||
|
)
|
||||||
if (!confirmed) return
|
if (!confirmed) return
|
||||||
}
|
}
|
||||||
|
|
||||||
api.post(
|
api
|
||||||
`domains/${this.name}/dns/push${this.force ? '?force' : ''}`,
|
.post(
|
||||||
{},
|
`domains/${this.name}/dns/push${this.force ? '?force' : ''}`,
|
||||||
{ key: 'domains.push_dns_changes', name: this.name }
|
{},
|
||||||
).then(async responseData => {
|
{ key: 'domains.push_dns_changes', name: this.name },
|
||||||
await this.getDnsChanges()
|
)
|
||||||
if (!isEmptyValue(responseData)) {
|
.then(async (responseData) => {
|
||||||
this.dnsErrors = Object.keys(responseData).reduce((acc, key) => {
|
await this.getDnsChanges()
|
||||||
const args = key === 'warnings'
|
if (!isEmptyValue(responseData)) {
|
||||||
? { icon: 'warning', variant: 'warning' }
|
this.dnsErrors = Object.keys(responseData).reduce((acc, key) => {
|
||||||
: { icon: 'ban', variant: 'danger' }
|
const args =
|
||||||
responseData[key].forEach(message => acc.push({ ...args, message }))
|
key === 'warnings'
|
||||||
return acc
|
? { icon: 'warning', variant: 'warning' }
|
||||||
}, [])
|
: { icon: 'ban', variant: 'danger' }
|
||||||
}
|
responseData[key].forEach((message) =>
|
||||||
})
|
acc.push({ ...args, message }),
|
||||||
}
|
)
|
||||||
|
return acc
|
||||||
|
}, [])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
created () {
|
created() {
|
||||||
this.getDnsChanges()
|
this.getDnsChanges()
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
<template>
|
<template>
|
||||||
<ViewBase
|
<ViewBase
|
||||||
:queries="queries" @queries-response="onQueriesResponse"
|
:queries="queries"
|
||||||
ref="view" skeleton="CardListSkeleton"
|
@queries-response="onQueriesResponse"
|
||||||
|
ref="view"
|
||||||
|
skeleton="CardListSkeleton"
|
||||||
>
|
>
|
||||||
<!-- INFO CARD -->
|
<!-- INFO CARD -->
|
||||||
<YCard v-if="domain" :title="name" icon="globe">
|
<YCard v-if="domain" :title="name" icon="globe">
|
||||||
|
@ -19,12 +21,20 @@
|
||||||
|
|
||||||
<template #header-buttons>
|
<template #header-buttons>
|
||||||
<!-- DEFAULT DOMAIN -->
|
<!-- DEFAULT DOMAIN -->
|
||||||
<BButton v-if="!isMainDomain" @click="setAsDefaultDomain" variant="info">
|
<BButton
|
||||||
|
v-if="!isMainDomain"
|
||||||
|
@click="setAsDefaultDomain"
|
||||||
|
variant="info"
|
||||||
|
>
|
||||||
<YIcon iname="star" /> {{ $t('set_default') }}
|
<YIcon iname="star" /> {{ $t('set_default') }}
|
||||||
</BButton>
|
</BButton>
|
||||||
|
|
||||||
<!-- DELETE DOMAIN -->
|
<!-- DELETE DOMAIN -->
|
||||||
<BButton v-b-modal.delete-modal :disabled="isMainDomain" variant="danger">
|
<BButton
|
||||||
|
v-b-modal.delete-modal
|
||||||
|
:disabled="isMainDomain"
|
||||||
|
variant="danger"
|
||||||
|
>
|
||||||
<YIcon iname="trash-o" /> {{ $t('delete') }}
|
<YIcon iname="trash-o" /> {{ $t('delete') }}
|
||||||
</BButton>
|
</BButton>
|
||||||
</template>
|
</template>
|
||||||
|
@ -40,13 +50,24 @@
|
||||||
<DescriptionRow :term="$t('domain.info.certificate_authority')">
|
<DescriptionRow :term="$t('domain.info.certificate_authority')">
|
||||||
<YIcon :iname="cert.icon" :variant="cert.variant" class="mr-1" />
|
<YIcon :iname="cert.icon" :variant="cert.variant" class="mr-1" />
|
||||||
{{ $t('domain.cert.types.' + cert.authority) }}
|
{{ $t('domain.cert.types.' + cert.authority) }}
|
||||||
<span class="text-secondary px-2">({{ $t('domain.cert.valid_for', { days: $tc('day_validity', cert.validity) }) }})</span>
|
<span class="text-secondary px-2">
|
||||||
|
({{
|
||||||
|
$t('domain.cert.valid_for', {
|
||||||
|
days: $tc('day_validity', cert.validity),
|
||||||
|
})
|
||||||
|
}})
|
||||||
|
</span>
|
||||||
</DescriptionRow>
|
</DescriptionRow>
|
||||||
|
|
||||||
<!-- DOMAIN REGISTRAR -->
|
<!-- DOMAIN REGISTRAR -->
|
||||||
<DescriptionRow v-if="domain.registrar" :term="$t('domain.info.registrar')">
|
<DescriptionRow
|
||||||
|
v-if="domain.registrar"
|
||||||
|
:term="$t('domain.info.registrar')"
|
||||||
|
>
|
||||||
<template v-if="domain.registrar === 'parent_domain'">
|
<template v-if="domain.registrar === 'parent_domain'">
|
||||||
{{ $t('domain.see_parent_domain') }} <BLink :href="`#/domains/${domain.topest_parent}/dns`">
|
{{ $t('domain.see_parent_domain') }} <BLink
|
||||||
|
:href="`#/domains/${domain.topest_parent}/dns`"
|
||||||
|
>
|
||||||
{{ domain.topest_parent }}
|
{{ domain.topest_parent }}
|
||||||
</BLink>
|
</BLink>
|
||||||
</template>
|
</template>
|
||||||
|
@ -59,15 +80,23 @@
|
||||||
<DescriptionRow :term="$t('domain.info.apps_on_domain')">
|
<DescriptionRow :term="$t('domain.info.apps_on_domain')">
|
||||||
<div>
|
<div>
|
||||||
<BButton-group
|
<BButton-group
|
||||||
v-for="app in domain.apps" :key="app.id"
|
v-for="app in domain.apps"
|
||||||
size="sm" class="mr-2 mb-2"
|
:key="app.id"
|
||||||
|
size="sm"
|
||||||
|
class="mr-2 mb-2"
|
||||||
>
|
>
|
||||||
<BButton class="py-0 font-weight-bold" variant="outline-dark" :to="{ name: 'app-info', params: { id: app.id }}">
|
<BButton
|
||||||
|
class="py-0 font-weight-bold"
|
||||||
|
variant="outline-dark"
|
||||||
|
:to="{ name: 'app-info', params: { id: app.id } }"
|
||||||
|
>
|
||||||
{{ app.name }}
|
{{ app.name }}
|
||||||
</BButton>
|
</BButton>
|
||||||
<BButton
|
<BButton
|
||||||
variant="outline-dark" class="py-0 px-1"
|
variant="outline-dark"
|
||||||
:href="'https://' + name + app.path" target="_blank"
|
class="py-0 px-1"
|
||||||
|
:href="'https://' + name + app.path"
|
||||||
|
target="_blank"
|
||||||
>
|
>
|
||||||
<span class="sr-only">{{ $t('app.visit_app') }}</span>
|
<span class="sr-only">{{ $t('app.visit_app') }}</span>
|
||||||
<YIcon iname="external-link" />
|
<YIcon iname="external-link" />
|
||||||
|
@ -87,8 +116,12 @@
|
||||||
|
|
||||||
<BModal
|
<BModal
|
||||||
v-if="domain"
|
v-if="domain"
|
||||||
id="delete-modal" :title="$t('confirm_delete', { name: this.name })" @ok="deleteDomain"
|
id="delete-modal"
|
||||||
header-bg-variant="warning" body-class="" body-bg-variant=""
|
:title="$t('confirm_delete', { name: this.name })"
|
||||||
|
@ok="deleteDomain"
|
||||||
|
header-bg-variant="warning"
|
||||||
|
body-class=""
|
||||||
|
body-bg-variant=""
|
||||||
>
|
>
|
||||||
<BFormGroup v-if="isMainDynDomain">
|
<BFormGroup v-if="isMainDynDomain">
|
||||||
<BFormCheckbox v-model="unsubscribeDomainFromDyndns">
|
<BFormCheckbox v-model="unsubscribeDomainFromDyndns">
|
||||||
|
@ -105,52 +138,54 @@ import { mapGetters } from 'vuex'
|
||||||
import api, { objectToParams } from '@/api'
|
import api, { objectToParams } from '@/api'
|
||||||
import {
|
import {
|
||||||
formatFormData,
|
formatFormData,
|
||||||
formatYunoHostConfigPanels
|
formatYunoHostConfigPanels,
|
||||||
} from '@/helpers/yunohostArguments'
|
} from '@/helpers/yunohostArguments'
|
||||||
import ConfigPanels from '@/components/ConfigPanels.vue'
|
import ConfigPanels from '@/components/ConfigPanels.vue'
|
||||||
import DomainDns from './DomainDns.vue'
|
import DomainDns from './DomainDns.vue'
|
||||||
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'DomainInfo',
|
name: 'DomainInfo',
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
ConfigPanels,
|
ConfigPanels,
|
||||||
DomainDns
|
DomainDns,
|
||||||
},
|
},
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
name: { type: String, required: true }
|
name: { type: String, required: true },
|
||||||
},
|
},
|
||||||
|
|
||||||
data () {
|
data() {
|
||||||
return {
|
return {
|
||||||
queries: [
|
queries: [
|
||||||
['GET', { uri: 'domains', storeKey: 'domains' }],
|
['GET', { uri: 'domains', storeKey: 'domains' }],
|
||||||
['GET', { uri: 'domains', storeKey: 'domains_details', param: this.name }],
|
[
|
||||||
['GET', `domains/${this.name}/config?full`]
|
'GET',
|
||||||
|
{ uri: 'domains', storeKey: 'domains_details', param: this.name },
|
||||||
|
],
|
||||||
|
['GET', `domains/${this.name}/config?full`],
|
||||||
],
|
],
|
||||||
config: {},
|
config: {},
|
||||||
unsubscribeDomainFromDyndns: false
|
unsubscribeDomainFromDyndns: false,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters(['mainDomain']),
|
...mapGetters(['mainDomain']),
|
||||||
|
|
||||||
currentTab () {
|
currentTab() {
|
||||||
return this.$route.params.tabId
|
return this.$route.params.tabId
|
||||||
},
|
},
|
||||||
|
|
||||||
domain () {
|
domain() {
|
||||||
return this.$store.getters.domain(this.name)
|
return this.$store.getters.domain(this.name)
|
||||||
},
|
},
|
||||||
|
|
||||||
parentName () {
|
parentName() {
|
||||||
return this.$store.getters.highestDomainParentName(this.name)
|
return this.$store.getters.highestDomainParentName(this.name)
|
||||||
},
|
},
|
||||||
|
|
||||||
cert () {
|
cert() {
|
||||||
const { CA_type: authority, validity } = this.domain.certificate
|
const { CA_type: authority, validity } = this.domain.certificate
|
||||||
const baseInfos = { authority, validity }
|
const baseInfos = { authority, validity }
|
||||||
if (validity <= 0) {
|
if (validity <= 0) {
|
||||||
|
@ -165,77 +200,98 @@ export default {
|
||||||
return { icon: 'exclamation', variant: 'warning', ...baseInfos }
|
return { icon: 'exclamation', variant: 'warning', ...baseInfos }
|
||||||
},
|
},
|
||||||
|
|
||||||
dns () {
|
dns() {
|
||||||
return this.domain.dns
|
return this.domain.dns
|
||||||
},
|
},
|
||||||
|
|
||||||
isMainDomain () {
|
isMainDomain() {
|
||||||
if (!this.mainDomain) return
|
if (!this.mainDomain) return
|
||||||
return this.name === this.mainDomain
|
return this.name === this.mainDomain
|
||||||
},
|
},
|
||||||
|
|
||||||
isMainDynDomain () {
|
isMainDynDomain() {
|
||||||
return this.domain.registrar === 'yunohost' && this.name.split('.').length === 3
|
return (
|
||||||
}
|
this.domain.registrar === 'yunohost' &&
|
||||||
|
this.name.split('.').length === 3
|
||||||
|
)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
onQueriesResponse (domains, domain, config) {
|
onQueriesResponse(domains, domain, config) {
|
||||||
this.config = formatYunoHostConfigPanels(config)
|
this.config = formatYunoHostConfigPanels(config)
|
||||||
},
|
},
|
||||||
|
|
||||||
async onConfigSubmit ({ id, form, action, name }) {
|
async onConfigSubmit({ id, form, action, name }) {
|
||||||
const args = await formatFormData(form, { removeEmpty: false, removeNull: true })
|
const args = await formatFormData(form, {
|
||||||
|
removeEmpty: false,
|
||||||
api.put(
|
removeNull: true,
|
||||||
action
|
|
||||||
? `domain/${this.name}/actions/${action}`
|
|
||||||
: `domains/${this.name}/config/${id}`,
|
|
||||||
{ args: objectToParams(args) },
|
|
||||||
{ key: `domains.${action ? 'action' : 'update'}_config`, id, name: this.name }
|
|
||||||
).then(() => {
|
|
||||||
this.$refs.view.fetchQueries({ triggerLoading: true })
|
|
||||||
}).catch(err => {
|
|
||||||
if (err.name !== 'APIBadRequestError') throw err
|
|
||||||
const panel = this.config.panels.find(panel => panel.id === id)
|
|
||||||
if (err.data.name) {
|
|
||||||
this.config.errors[id][err.data.name].message = err.message
|
|
||||||
} else this.$set(panel, 'serverError', err.message)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
api
|
||||||
|
.put(
|
||||||
|
action
|
||||||
|
? `domain/${this.name}/actions/${action}`
|
||||||
|
: `domains/${this.name}/config/${id}`,
|
||||||
|
{ args: objectToParams(args) },
|
||||||
|
{
|
||||||
|
key: `domains.${action ? 'action' : 'update'}_config`,
|
||||||
|
id,
|
||||||
|
name: this.name,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.then(() => {
|
||||||
|
this.$refs.view.fetchQueries({ triggerLoading: true })
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
if (err.name !== 'APIBadRequestError') throw err
|
||||||
|
const panel = this.config.panels.find((panel) => panel.id === id)
|
||||||
|
if (err.data.name) {
|
||||||
|
this.config.errors[id][err.data.name].message = err.message
|
||||||
|
} else this.$set(panel, 'serverError', err.message)
|
||||||
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
async deleteDomain () {
|
async deleteDomain() {
|
||||||
const data = this.isMainDynDomain && !this.unsubscribeDomainFromDyndns
|
const data =
|
||||||
? { ignore_dyndns: 1 }
|
this.isMainDynDomain && !this.unsubscribeDomainFromDyndns
|
||||||
: {}
|
? { ignore_dyndns: 1 }
|
||||||
|
: {}
|
||||||
|
|
||||||
api.delete(
|
api
|
||||||
{ uri: 'domains', param: this.name }, data, { key: 'domains.delete', name: this.name }
|
.delete({ uri: 'domains', param: this.name }, data, {
|
||||||
).then(() => {
|
key: 'domains.delete',
|
||||||
this.$router.push({ name: 'domain-list' })
|
name: this.name,
|
||||||
})
|
})
|
||||||
|
.then(() => {
|
||||||
|
this.$router.push({ name: 'domain-list' })
|
||||||
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
async setAsDefaultDomain () {
|
async setAsDefaultDomain() {
|
||||||
const confirmed = await this.$askConfirmation(this.$i18n.t('confirm_change_maindomain'))
|
const confirmed = await this.$askConfirmation(
|
||||||
|
this.$i18n.t('confirm_change_maindomain'),
|
||||||
|
)
|
||||||
if (!confirmed) return
|
if (!confirmed) return
|
||||||
|
|
||||||
api.put(
|
api
|
||||||
{ uri: `domains/${this.name}/main`, storeKey: 'main_domain' },
|
.put(
|
||||||
{},
|
{ uri: `domains/${this.name}/main`, storeKey: 'main_domain' },
|
||||||
{ key: 'domains.set_default', name: this.name }
|
{},
|
||||||
).then(() => {
|
{ key: 'domains.set_default', name: this.name },
|
||||||
// FIXME Have to commit by hand here since the response is empty (should return the given name)
|
)
|
||||||
this.$store.commit('UPDATE_MAIN_DOMAIN', this.name)
|
.then(() => {
|
||||||
})
|
// FIXME Have to commit by hand here since the response is empty (should return the given name)
|
||||||
}
|
this.$store.commit('UPDATE_MAIN_DOMAIN', this.name)
|
||||||
}
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.main-domain-badge {
|
.main-domain-badge {
|
||||||
font-size: .75rem;
|
font-size: 0.75rem;
|
||||||
padding-right: .2em;
|
padding-right: 0.2em;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -15,18 +15,27 @@
|
||||||
</BButton>
|
</BButton>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<RecursiveListGroup :tree="tree" :toggle-text="$t('domain.toggle_subdomains')" class="mb-5">
|
<RecursiveListGroup
|
||||||
|
:tree="tree"
|
||||||
|
:toggle-text="$t('domain.toggle_subdomains')"
|
||||||
|
class="mb-5"
|
||||||
|
>
|
||||||
<template #default="{ data, parent }">
|
<template #default="{ data, parent }">
|
||||||
<div class="w-100 d-flex justify-content-between align-items-center">
|
<div class="w-100 d-flex justify-content-between align-items-center">
|
||||||
<h5 class="mr-3">
|
<h5 class="mr-3">
|
||||||
<BLink :to="data.to" class="text-body text-decoration-none">
|
<BLink :to="data.to" class="text-body text-decoration-none">
|
||||||
<span class="font-weight-bold">{{ data.name.replace(parent ? parent.data.name : null, '') }}</span>
|
<span class="font-weight-bold">
|
||||||
<span v-if="parent" class="text-secondary">{{ parent.data.name }}</span>
|
{{ data.name.replace(parent ? parent.data.name : null, '') }}
|
||||||
|
</span>
|
||||||
|
<span v-if="parent" class="text-secondary">
|
||||||
|
{{ parent.data.name }}
|
||||||
|
</span>
|
||||||
</BLink>
|
</BLink>
|
||||||
|
|
||||||
<small
|
<small
|
||||||
v-if="data.name === mainDomain"
|
v-if="data.name === mainDomain"
|
||||||
:title="$t('domain.types.main_domain')" class="ml-1"
|
:title="$t('domain.types.main_domain')"
|
||||||
|
class="ml-1"
|
||||||
v-b-tooltip.hover
|
v-b-tooltip.hover
|
||||||
>
|
>
|
||||||
<YIcon iname="star" />
|
<YIcon iname="star" />
|
||||||
|
@ -43,47 +52,46 @@ import { mapGetters } from 'vuex'
|
||||||
|
|
||||||
import RecursiveListGroup from '@/components/RecursiveListGroup.vue'
|
import RecursiveListGroup from '@/components/RecursiveListGroup.vue'
|
||||||
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'DomainList',
|
name: 'DomainList',
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
RecursiveListGroup
|
RecursiveListGroup,
|
||||||
},
|
},
|
||||||
|
|
||||||
data () {
|
data() {
|
||||||
return {
|
return {
|
||||||
queries: [
|
queries: [['GET', { uri: 'domains', storeKey: 'domains' }]],
|
||||||
['GET', { uri: 'domains', storeKey: 'domains' }]
|
|
||||||
],
|
|
||||||
search: '',
|
search: '',
|
||||||
domainsTree: undefined
|
domainsTree: undefined,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters(['domains', 'mainDomain']),
|
...mapGetters(['domains', 'mainDomain']),
|
||||||
|
|
||||||
tree () {
|
tree() {
|
||||||
if (!this.domainsTree) return
|
if (!this.domainsTree) return
|
||||||
if (this.search) {
|
if (this.search) {
|
||||||
const search = this.search.toLowerCase()
|
const search = this.search.toLowerCase()
|
||||||
return this.domainsTree.filter(node => node.data.name.includes(search))
|
return this.domainsTree.filter((node) =>
|
||||||
|
node.data.name.includes(search),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return this.domainsTree
|
return this.domainsTree
|
||||||
},
|
},
|
||||||
|
|
||||||
hasFilteredItems () {
|
hasFilteredItems() {
|
||||||
if (!this.tree) return
|
if (!this.tree) return
|
||||||
return this.tree.children || null
|
return this.tree.children || null
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
onQueriesResponse () {
|
onQueriesResponse() {
|
||||||
// Add the tree to `data` to make it reactive
|
// Add the tree to `data` to make it reactive
|
||||||
this.domainsTree = this.$store.getters.domainsTree
|
this.domainsTree = this.$store.getters.domainsTree
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,11 +1,17 @@
|
||||||
<template>
|
<template>
|
||||||
<CardForm
|
<CardForm
|
||||||
:title="$t('group_new')" icon="users"
|
:title="$t('group_new')"
|
||||||
:validation="$v" :server-error="serverError"
|
icon="users"
|
||||||
|
:validation="$v"
|
||||||
|
:server-error="serverError"
|
||||||
@submit.prevent="onSubmit"
|
@submit.prevent="onSubmit"
|
||||||
>
|
>
|
||||||
<!-- GROUP NAME -->
|
<!-- GROUP NAME -->
|
||||||
<FormField v-bind="groupname" v-model="form.groupname" :validation="$v.form.groupname" />
|
<FormField
|
||||||
|
v-bind="groupname"
|
||||||
|
v-model="form.groupname"
|
||||||
|
:validation="$v.form.groupname"
|
||||||
|
/>
|
||||||
</CardForm>
|
</CardForm>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -18,10 +24,10 @@ import { required, alphalownumdot_ } from '@/helpers/validators'
|
||||||
export default {
|
export default {
|
||||||
name: 'GroupCreate',
|
name: 'GroupCreate',
|
||||||
|
|
||||||
data () {
|
data() {
|
||||||
return {
|
return {
|
||||||
form: {
|
form: {
|
||||||
groupname: ''
|
groupname: '',
|
||||||
},
|
},
|
||||||
serverError: '',
|
serverError: '',
|
||||||
groupname: {
|
groupname: {
|
||||||
|
@ -29,33 +35,35 @@ export default {
|
||||||
description: this.$i18n.t('group_format_name_help'),
|
description: this.$i18n.t('group_format_name_help'),
|
||||||
props: {
|
props: {
|
||||||
id: 'groupname',
|
id: 'groupname',
|
||||||
placeholder: this.$i18n.t('placeholder.groupname')
|
placeholder: this.$i18n.t('placeholder.groupname'),
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
validations: {
|
validations: {
|
||||||
form: {
|
form: {
|
||||||
groupname: { required, alphalownumdot_ }
|
groupname: { required, alphalownumdot_ },
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
onSubmit () {
|
onSubmit() {
|
||||||
api.post(
|
api
|
||||||
{ uri: 'users/groups', storeKey: 'groups' },
|
.post({ uri: 'users/groups', storeKey: 'groups' }, this.form, {
|
||||||
this.form,
|
key: 'groups.create',
|
||||||
{ key: 'groups.create', name: this.form.groupname }
|
name: this.form.groupname,
|
||||||
).then(() => {
|
})
|
||||||
this.$router.push({ name: 'group-list' })
|
.then(() => {
|
||||||
}).catch(err => {
|
this.$router.push({ name: 'group-list' })
|
||||||
if (err.name !== 'APIBadRequestError') throw err
|
})
|
||||||
this.serverError = err.message
|
.catch((err) => {
|
||||||
})
|
if (err.name !== 'APIBadRequestError') throw err
|
||||||
}
|
this.serverError = err.message
|
||||||
|
})
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
mixins: [validationMixin]
|
mixins: [validationMixin],
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -16,14 +16,23 @@
|
||||||
|
|
||||||
<!-- PRIMARY GROUPS CARDS -->
|
<!-- PRIMARY GROUPS CARDS -->
|
||||||
<YCard
|
<YCard
|
||||||
v-for="(group, groupName) in filteredGroups" :key="groupName" collapsable
|
v-for="(group, groupName) in filteredGroups"
|
||||||
:title="group.isSpecial ? $t('group_' + groupName) : `${$t('group')} '${groupName}'`" icon="group"
|
:key="groupName"
|
||||||
|
collapsable
|
||||||
|
:title="
|
||||||
|
group.isSpecial
|
||||||
|
? $t('group_' + groupName)
|
||||||
|
: `${$t('group')} '${groupName}'`
|
||||||
|
"
|
||||||
|
icon="group"
|
||||||
>
|
>
|
||||||
<template #header-buttons>
|
<template #header-buttons>
|
||||||
<!-- DELETE GROUP -->
|
<!-- DELETE GROUP -->
|
||||||
<BButton
|
<BButton
|
||||||
v-if="!group.isSpecial" @click="deleteGroup(groupName)"
|
v-if="!group.isSpecial"
|
||||||
size="sm" variant="danger"
|
@click="deleteGroup(groupName)"
|
||||||
|
size="sm"
|
||||||
|
variant="danger"
|
||||||
>
|
>
|
||||||
<YIcon iname="trash-o" /> {{ $t('delete') }}
|
<YIcon iname="trash-o" /> {{ $t('delete') }}
|
||||||
</BButton>
|
</BButton>
|
||||||
|
@ -37,23 +46,29 @@
|
||||||
<BCol>
|
<BCol>
|
||||||
<template v-if="group.isSpecial">
|
<template v-if="group.isSpecial">
|
||||||
<p class="text-primary">
|
<p class="text-primary">
|
||||||
<YIcon iname="info-circle" /> {{ $t('group_explain_' + groupName) }}
|
<YIcon iname="info-circle" />
|
||||||
|
{{ $t('group_explain_' + groupName) }}
|
||||||
</p>
|
</p>
|
||||||
<p class="text-primary" v-if="groupName === 'visitors'">
|
<p class="text-primary" v-if="groupName === 'visitors'">
|
||||||
<em>{{ $t('group_explain_visitors_needed_for_external_client') }}</em>
|
<em>{{
|
||||||
|
$t('group_explain_visitors_needed_for_external_client')
|
||||||
|
}}</em>
|
||||||
</p>
|
</p>
|
||||||
</template>
|
</template>
|
||||||
<template v-if="groupName == 'admins' || !group.isSpecial">
|
<template v-if="groupName == 'admins' || !group.isSpecial">
|
||||||
<TagsSelectizeItem
|
<TagsSelectizeItem
|
||||||
v-model="group.members" :options="usersOptions"
|
v-model="group.members"
|
||||||
:id="groupName + '-users'" :label="$t('group_add_member')"
|
:options="usersOptions"
|
||||||
tag-icon="user" items-name="users"
|
:id="groupName + '-users'"
|
||||||
|
:label="$t('group_add_member')"
|
||||||
|
tag-icon="user"
|
||||||
|
items-name="users"
|
||||||
@tag-update="onUserChanged({ ...$event, groupName })"
|
@tag-update="onUserChanged({ ...$event, groupName })"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</BCol>
|
</BCol>
|
||||||
</BRow>
|
</BRow>
|
||||||
<hr>
|
<hr />
|
||||||
|
|
||||||
<BRow>
|
<BRow>
|
||||||
<BCol md="3" lg="2">
|
<BCol md="3" lg="2">
|
||||||
|
@ -61,9 +76,12 @@
|
||||||
</BCol>
|
</BCol>
|
||||||
<BCol>
|
<BCol>
|
||||||
<TagsSelectizeItem
|
<TagsSelectizeItem
|
||||||
v-model="group.permissions" :options="permissionsOptions"
|
v-model="group.permissions"
|
||||||
:id="groupName + '-perms'" :label="$t('group_add_permission')"
|
:options="permissionsOptions"
|
||||||
tag-icon="key-modern" items-name="permissions"
|
:id="groupName + '-perms'"
|
||||||
|
:label="$t('group_add_permission')"
|
||||||
|
tag-icon="key-modern"
|
||||||
|
items-name="permissions"
|
||||||
@tag-update="onPermissionChanged({ ...$event, groupName })"
|
@tag-update="onPermissionChanged({ ...$event, groupName })"
|
||||||
:disabled-items="group.disabledItems"
|
:disabled-items="group.disabledItems"
|
||||||
/>
|
/>
|
||||||
|
@ -73,8 +91,10 @@
|
||||||
|
|
||||||
<!-- USER GROUPS CARD -->
|
<!-- USER GROUPS CARD -->
|
||||||
<YCard
|
<YCard
|
||||||
v-if="userGroups" collapsable
|
v-if="userGroups"
|
||||||
:title="$t('group_specific_permissions')" icon="group"
|
collapsable
|
||||||
|
:title="$t('group_specific_permissions')"
|
||||||
|
icon="group"
|
||||||
>
|
>
|
||||||
<template v-for="(userName, index) in activeUserGroups">
|
<template v-for="(userName, index) in activeUserGroups">
|
||||||
<BRow :key="userName">
|
<BRow :key="userName">
|
||||||
|
@ -84,20 +104,28 @@
|
||||||
|
|
||||||
<BCol>
|
<BCol>
|
||||||
<TagsSelectizeItem
|
<TagsSelectizeItem
|
||||||
v-model="userGroups[userName].permissions" :options="permissionsOptions"
|
v-model="userGroups[userName].permissions"
|
||||||
:id="userName + '-perms'" :label="$t('group_add_permission')"
|
:options="permissionsOptions"
|
||||||
tag-icon="key-modern" items-name="permissions"
|
:id="userName + '-perms'"
|
||||||
@tag-update="onPermissionChanged({ ...$event, groupName: userName })"
|
:label="$t('group_add_permission')"
|
||||||
|
tag-icon="key-modern"
|
||||||
|
items-name="permissions"
|
||||||
|
@tag-update="
|
||||||
|
onPermissionChanged({ ...$event, groupName: userName })
|
||||||
|
"
|
||||||
/>
|
/>
|
||||||
</BCol>
|
</BCol>
|
||||||
</BRow>
|
</BRow>
|
||||||
<hr :key="index">
|
<hr :key="index" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<TagsSelectizeItem
|
<TagsSelectizeItem
|
||||||
v-model="activeUserGroups" :options="usersOptions"
|
v-model="activeUserGroups"
|
||||||
id="user-groups" :label="$t('group_add_member')"
|
:options="usersOptions"
|
||||||
no-tags items-name="users"
|
id="user-groups"
|
||||||
|
:label="$t('group_add_member')"
|
||||||
|
no-tags
|
||||||
|
items-name="users"
|
||||||
@tag-update="onSpecificUserAdded"
|
@tag-update="onSpecificUserAdded"
|
||||||
/>
|
/>
|
||||||
</YCard>
|
</YCard>
|
||||||
|
@ -117,15 +145,21 @@ export default {
|
||||||
name: 'GroupList',
|
name: 'GroupList',
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
TagsSelectizeItem
|
TagsSelectizeItem,
|
||||||
},
|
},
|
||||||
|
|
||||||
data () {
|
data() {
|
||||||
return {
|
return {
|
||||||
queries: [
|
queries: [
|
||||||
['GET', { uri: 'users' }],
|
['GET', { uri: 'users' }],
|
||||||
['GET', { uri: 'users/groups?full&include_primary_groups', storeKey: 'groups' }],
|
[
|
||||||
['GET', { uri: 'users/permissions?full', storeKey: 'permissions' }]
|
'GET',
|
||||||
|
{
|
||||||
|
uri: 'users/groups?full&include_primary_groups',
|
||||||
|
storeKey: 'groups',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
['GET', { uri: 'users/permissions?full', storeKey: 'permissions' }],
|
||||||
],
|
],
|
||||||
search: '',
|
search: '',
|
||||||
permissions: undefined,
|
permissions: undefined,
|
||||||
|
@ -133,12 +167,12 @@ export default {
|
||||||
primaryGroups: undefined,
|
primaryGroups: undefined,
|
||||||
userGroups: undefined,
|
userGroups: undefined,
|
||||||
usersOptions: undefined,
|
usersOptions: undefined,
|
||||||
activeUserGroups: undefined
|
activeUserGroups: undefined,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
filteredGroups () {
|
filteredGroups() {
|
||||||
const groups = this.primaryGroups
|
const groups = this.primaryGroups
|
||||||
if (!groups) return
|
if (!groups) return
|
||||||
const search = this.search.toLowerCase()
|
const search = this.search.toLowerCase()
|
||||||
|
@ -149,14 +183,17 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return isEmptyValue(filtered) ? null : filtered
|
return isEmptyValue(filtered) ? null : filtered
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
onQueriesResponse (users, allGroups, permsDict) {
|
onQueriesResponse(users, allGroups, permsDict) {
|
||||||
// Do not use computed properties to get values from the store here to avoid auto
|
// Do not use computed properties to get values from the store here to avoid auto
|
||||||
// updates while modifying values.
|
// updates while modifying values.
|
||||||
const permissions = Object.entries(permsDict).map(([id, value]) => ({ id, ...value }))
|
const permissions = Object.entries(permsDict).map(([id, value]) => ({
|
||||||
|
id,
|
||||||
|
...value,
|
||||||
|
}))
|
||||||
const userNames = users ? Object.keys(users) : []
|
const userNames = users ? Object.keys(users) : []
|
||||||
const primaryGroups = {}
|
const primaryGroups = {}
|
||||||
const userGroups = {}
|
const userGroups = {}
|
||||||
|
@ -173,90 +210,124 @@ export default {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
group.isSpecial = ['visitors', 'all_users', 'admins'].includes(groupName)
|
group.isSpecial = ['visitors', 'all_users', 'admins'].includes(
|
||||||
|
groupName,
|
||||||
|
)
|
||||||
|
|
||||||
if (groupName === 'visitors') {
|
if (groupName === 'visitors') {
|
||||||
// Forbid to add or remove a protected permission on group `visitors`
|
// Forbid to add or remove a protected permission on group `visitors`
|
||||||
group.disabledItems = permissions.filter(({ id }) => {
|
group.disabledItems = permissions
|
||||||
return ['mail.main', 'xmpp.main'].includes(id) || permsDict[id].protected
|
.filter(({ id }) => {
|
||||||
}).map(({ id }) => permsDict[id].label)
|
return (
|
||||||
|
['mail.main', 'xmpp.main'].includes(id) ||
|
||||||
|
permsDict[id].protected
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.map(({ id }) => permsDict[id].label)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (groupName === 'all_users') {
|
if (groupName === 'all_users') {
|
||||||
// Forbid to add ssh and sftp permission on group `all_users`
|
// Forbid to add ssh and sftp permission on group `all_users`
|
||||||
group.disabledItems = permissions.filter(({ id }) => {
|
group.disabledItems = permissions
|
||||||
return ['ssh.main', 'sftp.main'].includes(id)
|
.filter(({ id }) => {
|
||||||
}).map(({ id }) => permsDict[id].label)
|
return ['ssh.main', 'sftp.main'].includes(id)
|
||||||
|
})
|
||||||
|
.map(({ id }) => permsDict[id].label)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (groupName === 'admins') {
|
if (groupName === 'admins') {
|
||||||
// Forbid to add ssh and sftp permission on group `admins`
|
// Forbid to add ssh and sftp permission on group `admins`
|
||||||
group.disabledItems = permissions.filter(({ id }) => {
|
group.disabledItems = permissions
|
||||||
return ['ssh.main', 'sftp.main'].includes(id)
|
.filter(({ id }) => {
|
||||||
}).map(({ id }) => permsDict[id].label)
|
return ['ssh.main', 'sftp.main'].includes(id)
|
||||||
|
})
|
||||||
|
.map(({ id }) => permsDict[id].label)
|
||||||
}
|
}
|
||||||
|
|
||||||
primaryGroups[groupName] = group
|
primaryGroups[groupName] = group
|
||||||
}
|
}
|
||||||
|
|
||||||
const activeUserGroups = Object.entries(userGroups).filter(([_, group]) => {
|
const activeUserGroups = Object.entries(userGroups)
|
||||||
return group.permissions.length > 0
|
.filter(([_, group]) => {
|
||||||
}).map(([name]) => name)
|
return group.permissions.length > 0
|
||||||
|
})
|
||||||
|
.map(([name]) => name)
|
||||||
|
|
||||||
Object.assign(this, {
|
Object.assign(this, {
|
||||||
permissions,
|
permissions,
|
||||||
permissionsOptions: permissions.map(perm => perm.label),
|
permissionsOptions: permissions.map((perm) => perm.label),
|
||||||
primaryGroups,
|
primaryGroups,
|
||||||
userGroups: isEmptyValue(userGroups) ? null : userGroups,
|
userGroups: isEmptyValue(userGroups) ? null : userGroups,
|
||||||
usersOptions: userNames,
|
usersOptions: userNames,
|
||||||
activeUserGroups
|
activeUserGroups,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
async onPermissionChanged ({ option, groupName, action, applyMethod }) {
|
async onPermissionChanged({ option, groupName, action, applyMethod }) {
|
||||||
const permId = this.permissions.find(perm => perm.label === option).id
|
const permId = this.permissions.find((perm) => perm.label === option).id
|
||||||
if (action === 'add' && ['sftp.main', 'ssh.main'].includes(permId)) {
|
if (action === 'add' && ['sftp.main', 'ssh.main'].includes(permId)) {
|
||||||
const confirmed = await this.$askConfirmation(
|
const confirmed = await this.$askConfirmation(
|
||||||
this.$i18n.t('confirm_group_add_access_permission', { name: groupName, perm: option })
|
this.$i18n.t('confirm_group_add_access_permission', {
|
||||||
|
name: groupName,
|
||||||
|
perm: option,
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
if (!confirmed) return
|
if (!confirmed) return
|
||||||
}
|
}
|
||||||
api.put(
|
api
|
||||||
// FIXME hacky way to update the store
|
.put(
|
||||||
{ uri: `users/permissions/${permId}/${action}/${groupName}`, storeKey: 'permissions', groupName, action, permId },
|
// FIXME hacky way to update the store
|
||||||
{},
|
{
|
||||||
{ key: 'permissions.' + action, perm: option, name: groupName }
|
uri: `users/permissions/${permId}/${action}/${groupName}`,
|
||||||
).then(() => applyMethod(option))
|
storeKey: 'permissions',
|
||||||
|
groupName,
|
||||||
|
action,
|
||||||
|
permId,
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
{ key: 'permissions.' + action, perm: option, name: groupName },
|
||||||
|
)
|
||||||
|
.then(() => applyMethod(option))
|
||||||
},
|
},
|
||||||
|
|
||||||
onUserChanged ({ option, groupName, action, applyMethod }) {
|
onUserChanged({ option, groupName, action, applyMethod }) {
|
||||||
api.put(
|
api
|
||||||
{ uri: `users/groups/${groupName}/${action}/${option}`, storeKey: 'groups', groupName },
|
.put(
|
||||||
{},
|
{
|
||||||
{ key: 'groups.' + action, user: option, name: groupName }
|
uri: `users/groups/${groupName}/${action}/${option}`,
|
||||||
).then(() => applyMethod(option))
|
storeKey: 'groups',
|
||||||
|
groupName,
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
{ key: 'groups.' + action, user: option, name: groupName },
|
||||||
|
)
|
||||||
|
.then(() => applyMethod(option))
|
||||||
},
|
},
|
||||||
|
|
||||||
onSpecificUserAdded ({ option: userName, action, applyMethod }) {
|
onSpecificUserAdded({ option: userName, action, applyMethod }) {
|
||||||
if (action === 'add') {
|
if (action === 'add') {
|
||||||
this.userGroups[userName].permissions = []
|
this.userGroups[userName].permissions = []
|
||||||
applyMethod(userName)
|
applyMethod(userName)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async deleteGroup (groupName) {
|
async deleteGroup(groupName) {
|
||||||
const confirmed = await this.$askConfirmation(this.$i18n.t('confirm_delete', { name: groupName }))
|
const confirmed = await this.$askConfirmation(
|
||||||
|
this.$i18n.t('confirm_delete', { name: groupName }),
|
||||||
|
)
|
||||||
if (!confirmed) return
|
if (!confirmed) return
|
||||||
|
|
||||||
api.delete(
|
api
|
||||||
{ uri: 'users/groups', param: groupName, storeKey: 'groups' },
|
.delete(
|
||||||
{},
|
{ uri: 'users/groups', param: groupName, storeKey: 'groups' },
|
||||||
{ key: 'groups.delete', name: groupName }
|
{},
|
||||||
).then(() => {
|
{ key: 'groups.delete', name: groupName },
|
||||||
Vue.delete(this.primaryGroups, groupName)
|
)
|
||||||
})
|
.then(() => {
|
||||||
}
|
Vue.delete(this.primaryGroups, groupName)
|
||||||
}
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
<template>
|
<template>
|
||||||
<ViewBase
|
<ViewBase
|
||||||
:queries="queries" @queries-response="onQueriesResponse"
|
:queries="queries"
|
||||||
ref="view" skeleton="CardInfoSkeleton"
|
@queries-response="onQueriesResponse"
|
||||||
|
ref="view"
|
||||||
|
skeleton="CardInfoSkeleton"
|
||||||
>
|
>
|
||||||
<!-- INFO CARD -->
|
<!-- INFO CARD -->
|
||||||
<YCard :title="name" icon="info-circle" button-unbreak="sm">
|
<YCard :title="name" icon="info-circle" button-unbreak="sm">
|
||||||
|
@ -13,7 +15,11 @@
|
||||||
</BButton>
|
</BButton>
|
||||||
|
|
||||||
<!-- STOP SERVICE -->
|
<!-- STOP SERVICE -->
|
||||||
<BButton v-if="!isCritical" @click="updateService('stop')" variant="danger">
|
<BButton
|
||||||
|
v-if="!isCritical"
|
||||||
|
@click="updateService('stop')"
|
||||||
|
variant="danger"
|
||||||
|
>
|
||||||
<YIcon iname="warning" /> {{ $t('stop') }}
|
<YIcon iname="warning" /> {{ $t('stop') }}
|
||||||
</BButton>
|
</BButton>
|
||||||
</template>
|
</template>
|
||||||
|
@ -25,11 +31,15 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<BRow
|
<BRow
|
||||||
v-for="(value, key) in infos" :key="key"
|
v-for="(value, key) in infos"
|
||||||
no-gutters class="row-line"
|
:key="key"
|
||||||
|
no-gutters
|
||||||
|
class="row-line"
|
||||||
>
|
>
|
||||||
<BCol md="3" xl="2">
|
<BCol md="3" xl="2">
|
||||||
<strong>{{ $t(key === 'start_on_boot' ? 'service_' + key : key) }}</strong>
|
<strong>
|
||||||
|
{{ $t(key === 'start_on_boot' ? 'service_' + key : key) }}
|
||||||
|
</strong>
|
||||||
</BCol>
|
</BCol>
|
||||||
<BCol>
|
<BCol>
|
||||||
<template v-if="key === 'status'">
|
<template v-if="key === 'status'">
|
||||||
|
@ -37,10 +47,13 @@
|
||||||
<YIcon :iname="value === 'running' ? 'check-circle' : 'times'" />
|
<YIcon :iname="value === 'running' ? 'check-circle' : 'times'" />
|
||||||
{{ $t(value) }}
|
{{ $t(value) }}
|
||||||
</span>
|
</span>
|
||||||
{{ $t('since') }} {{ distanceToNow(uptime ) }}
|
{{ $t('since') }} {{ distanceToNow(uptime) }}
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<span v-else-if="key === 'start_on_boot'" :class="value === 'enabled' ? 'text-success' : 'text-danger'">
|
<span
|
||||||
|
v-else-if="key === 'start_on_boot'"
|
||||||
|
:class="value === 'enabled' ? 'text-success' : 'text-danger'"
|
||||||
|
>
|
||||||
{{ $t(value) }}
|
{{ $t(value) }}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
@ -76,14 +89,14 @@ export default {
|
||||||
name: 'ServiceInfo',
|
name: 'ServiceInfo',
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
name: { type: String, required: true }
|
name: { type: String, required: true },
|
||||||
},
|
},
|
||||||
|
|
||||||
data () {
|
data() {
|
||||||
return {
|
return {
|
||||||
queries: [
|
queries: [
|
||||||
['GET', 'services/' + this.name],
|
['GET', 'services/' + this.name],
|
||||||
['GET', `services/${this.name}/log?number=50`]
|
['GET', `services/${this.name}/log?number=50`],
|
||||||
],
|
],
|
||||||
// Service data
|
// Service data
|
||||||
infos: undefined,
|
infos: undefined,
|
||||||
|
@ -91,62 +104,71 @@ export default {
|
||||||
isCritical: undefined,
|
isCritical: undefined,
|
||||||
logs: undefined,
|
logs: undefined,
|
||||||
// Modal action
|
// Modal action
|
||||||
action: undefined
|
action: undefined,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
onQueriesResponse (
|
onQueriesResponse(
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
{ status, description, start_on_boot, last_state_change, configuration },
|
{ status, description, start_on_boot, last_state_change, configuration },
|
||||||
logs
|
logs,
|
||||||
) {
|
) {
|
||||||
this.isCritical = ['nginx', 'ssh', 'slapd', 'yunohost-api'].includes(this.name)
|
this.isCritical = ['nginx', 'ssh', 'slapd', 'yunohost-api'].includes(
|
||||||
|
this.name,
|
||||||
|
)
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
this.uptime = last_state_change === 'unknown' ? 0 : last_state_change
|
this.uptime = last_state_change === 'unknown' ? 0 : last_state_change
|
||||||
this.infos = { description, status, start_on_boot, configuration }
|
this.infos = { description, status, start_on_boot, configuration }
|
||||||
|
|
||||||
this.logs = Object.keys(logs).sort((prev, curr) => {
|
this.logs = Object.keys(logs)
|
||||||
if (prev === 'journalctl') return -1
|
.sort((prev, curr) => {
|
||||||
else if (curr === 'journalctl') return 1
|
if (prev === 'journalctl') return -1
|
||||||
else if (prev < curr) return -1
|
else if (curr === 'journalctl') return 1
|
||||||
else return 1
|
else if (prev < curr) return -1
|
||||||
}).map(filename => ({ content: logs[filename].join('\n'), filename }))
|
else return 1
|
||||||
|
})
|
||||||
|
.map((filename) => ({ content: logs[filename].join('\n'), filename }))
|
||||||
},
|
},
|
||||||
|
|
||||||
async updateService (action) {
|
async updateService(action) {
|
||||||
const confirmed = await this.$askConfirmation(
|
const confirmed = await this.$askConfirmation(
|
||||||
this.$i18n.t('confirm_service_' + action, { name: this.name })
|
this.$i18n.t('confirm_service_' + action, { name: this.name }),
|
||||||
)
|
)
|
||||||
if (!confirmed) return
|
if (!confirmed) return
|
||||||
|
|
||||||
api.put(
|
api
|
||||||
`services/${this.name}/${action}`,
|
.put(
|
||||||
{},
|
`services/${this.name}/${action}`,
|
||||||
{ key: 'services.' + action, name: this.name }
|
{},
|
||||||
).then(this.$refs.view.fetchQueries)
|
{ key: 'services.' + action, name: this.name },
|
||||||
|
)
|
||||||
|
.then(this.$refs.view.fetchQueries)
|
||||||
},
|
},
|
||||||
|
|
||||||
shareLogs () {
|
shareLogs() {
|
||||||
const logs = this.logs.map(({ filename, content }) => {
|
const logs = this.logs
|
||||||
return `LOGFILE: ${filename}\n${content}`
|
.map(({ filename, content }) => {
|
||||||
}).join('\n\n')
|
return `LOGFILE: ${filename}\n${content}`
|
||||||
|
})
|
||||||
|
.join('\n\n')
|
||||||
|
|
||||||
fetch('https://paste.yunohost.org/documents', {
|
fetch('https://paste.yunohost.org/documents', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: logs
|
body: logs,
|
||||||
}).then(response => {
|
|
||||||
if (response.ok) return response.json()
|
|
||||||
// FIXME flash error
|
|
||||||
/* eslint-disable-next-line */
|
|
||||||
else console.log('error', response)
|
|
||||||
}).then(({ key }) => {
|
|
||||||
window.open('https://paste.yunohost.org/' + key, '_blank')
|
|
||||||
})
|
})
|
||||||
|
.then((response) => {
|
||||||
|
if (response.ok) return response.json()
|
||||||
|
// FIXME flash error
|
||||||
|
/* eslint-disable-next-line */ else console.log('error', response)
|
||||||
|
})
|
||||||
|
.then(({ key }) => {
|
||||||
|
window.open('https://paste.yunohost.org/' + key, '_blank')
|
||||||
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
distanceToNow
|
distanceToNow,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -10,8 +10,14 @@
|
||||||
>
|
>
|
||||||
<BListGroup>
|
<BListGroup>
|
||||||
<BListGroupItem
|
<BListGroupItem
|
||||||
v-for="{ name, description, status, last_state_change } in filteredServices" :key="name"
|
v-for="{
|
||||||
:to="{ name: 'service-info', params: { name }}"
|
name,
|
||||||
|
description,
|
||||||
|
status,
|
||||||
|
last_state_change,
|
||||||
|
} in filteredServices"
|
||||||
|
:key="name"
|
||||||
|
:to="{ name: 'service-info', params: { name } }"
|
||||||
class="d-flex justify-content-between align-items-center pr-0"
|
class="d-flex justify-content-between align-items-center pr-0"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
|
@ -20,7 +26,9 @@
|
||||||
<small class="text-secondary">{{ description }}</small>
|
<small class="text-secondary">{{ description }}</small>
|
||||||
</h5>
|
</h5>
|
||||||
<p class="m-0">
|
<p class="m-0">
|
||||||
<span :class="status === 'running' ? 'text-success' : 'text-danger'">
|
<span
|
||||||
|
:class="status === 'running' ? 'text-success' : 'text-danger'"
|
||||||
|
>
|
||||||
<YIcon :iname="status === 'running' ? 'check-circle' : 'times'" />
|
<YIcon :iname="status === 'running' ? 'check-circle' : 'times'" />
|
||||||
{{ $t(status) }}
|
{{ $t(status) }}
|
||||||
</span>
|
</span>
|
||||||
|
@ -40,48 +48,48 @@ import { distanceToNow } from '@/helpers/filters/date'
|
||||||
export default {
|
export default {
|
||||||
name: 'ServiceList',
|
name: 'ServiceList',
|
||||||
|
|
||||||
data () {
|
data() {
|
||||||
return {
|
return {
|
||||||
queries: [
|
queries: [['GET', 'services']],
|
||||||
['GET', 'services']
|
|
||||||
],
|
|
||||||
search: '',
|
search: '',
|
||||||
services: undefined
|
services: undefined,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
filteredServices () {
|
filteredServices() {
|
||||||
if (!this.services) return
|
if (!this.services) return
|
||||||
const search = this.search.toLowerCase()
|
const search = this.search.toLowerCase()
|
||||||
const services = this.services.filter(({ name }) => {
|
const services = this.services.filter(({ name }) => {
|
||||||
return name.toLowerCase().includes(search)
|
return name.toLowerCase().includes(search)
|
||||||
})
|
})
|
||||||
return services.length ? services : null
|
return services.length ? services : null
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
onQueriesResponse (services) {
|
onQueriesResponse(services) {
|
||||||
this.services = Object.keys(services).sort().map(name => {
|
this.services = Object.keys(services)
|
||||||
const service = services[name]
|
.sort()
|
||||||
if (service.last_state_change === 'unknown') {
|
.map((name) => {
|
||||||
service.last_state_change = 0
|
const service = services[name]
|
||||||
}
|
if (service.last_state_change === 'unknown') {
|
||||||
return { ...service, name }
|
service.last_state_change = 0
|
||||||
})
|
}
|
||||||
|
return { ...service, name }
|
||||||
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
distanceToNow
|
distanceToNow,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@include media-breakpoint-down(md) {
|
@include media-breakpoint-down(md) {
|
||||||
h5 small {
|
h5 small {
|
||||||
display: block;
|
display: block;
|
||||||
margin-top: .25rem;
|
margin-top: 0.25rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,17 +1,16 @@
|
||||||
<template>
|
<template>
|
||||||
<ViewBase
|
<ViewBase
|
||||||
:queries="queries" @queries-response="onQueriesResponse"
|
:queries="queries"
|
||||||
ref="view" skeleton="CardFormSkeleton"
|
@queries-response="onQueriesResponse"
|
||||||
|
ref="view"
|
||||||
|
skeleton="CardFormSkeleton"
|
||||||
>
|
>
|
||||||
<!-- PORTS -->
|
<!-- PORTS -->
|
||||||
<YCard :title="$t('ports')" icon="shield">
|
<YCard :title="$t('ports')" icon="shield">
|
||||||
<div v-for="(items, protocol) in protocols" :key="protocol">
|
<div v-for="(items, protocol) in protocols" :key="protocol">
|
||||||
<h5>{{ $t(protocol) }}</h5>
|
<h5>{{ $t(protocol) }}</h5>
|
||||||
|
|
||||||
<BTable
|
<BTable :fields="fields" :items="items" small striped responsive>
|
||||||
:fields="fields" :items="items"
|
|
||||||
small striped responsive
|
|
||||||
>
|
|
||||||
<!-- PORT CELL -->
|
<!-- PORT CELL -->
|
||||||
<template #cell(port)="data">
|
<template #cell(port)="data">
|
||||||
{{ data.value }}
|
{{ data.value }}
|
||||||
|
@ -24,9 +23,21 @@
|
||||||
class="on-off-switch"
|
class="on-off-switch"
|
||||||
v-model="data.value"
|
v-model="data.value"
|
||||||
switch
|
switch
|
||||||
@change="onTablePortToggling(data.item.port, protocol, data.field.key, data.index, $event)"
|
@change="
|
||||||
|
onTablePortToggling(
|
||||||
|
data.item.port,
|
||||||
|
protocol,
|
||||||
|
data.field.key,
|
||||||
|
data.index,
|
||||||
|
$event,
|
||||||
|
)
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<span :class="'btn btn-sm py-0 btn-' + (data.value ? 'danger' : 'success')">
|
<span
|
||||||
|
:class="
|
||||||
|
'btn btn-sm py-0 btn-' + (data.value ? 'danger' : 'success')
|
||||||
|
"
|
||||||
|
>
|
||||||
{{ $t(data.value ? 'close' : 'open') }}
|
{{ $t(data.value ? 'close' : 'open') }}
|
||||||
</span>
|
</span>
|
||||||
</BFormCheckbox>
|
</BFormCheckbox>
|
||||||
|
@ -43,10 +54,13 @@
|
||||||
|
|
||||||
<!-- OPERATIONS -->
|
<!-- OPERATIONS -->
|
||||||
<CardForm
|
<CardForm
|
||||||
:title="$t('operations')" icon="cogs"
|
:title="$t('operations')"
|
||||||
:validation="$v" :server-error="serverError"
|
icon="cogs"
|
||||||
|
:validation="$v"
|
||||||
|
:server-error="serverError"
|
||||||
@submit.prevent="onFormPortToggling"
|
@submit.prevent="onFormPortToggling"
|
||||||
inline form-classes="d-flex justify-content-between align-items-start"
|
inline
|
||||||
|
form-classes="d-flex justify-content-between align-items-start"
|
||||||
>
|
>
|
||||||
<BInputGroup :prepend="$t('action')">
|
<BInputGroup :prepend="$t('action')">
|
||||||
<BFormSelect v-model="form.action" :options="actionChoices" />
|
<BFormSelect v-model="form.action" :options="actionChoices" />
|
||||||
|
@ -55,32 +69,49 @@
|
||||||
<FormField :validation="$v.form.port">
|
<FormField :validation="$v.form.port">
|
||||||
<BInputGroup :prepend="$t('port')">
|
<BInputGroup :prepend="$t('port')">
|
||||||
<InputItem
|
<InputItem
|
||||||
id="input-port" placeholder="0" type="number"
|
id="input-port"
|
||||||
|
placeholder="0"
|
||||||
|
type="number"
|
||||||
v-model="form.port"
|
v-model="form.port"
|
||||||
/>
|
/>
|
||||||
</BInputGroup>
|
</BInputGroup>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
<BInputGroup :prepend="$t('connection')">
|
<BInputGroup :prepend="$t('connection')">
|
||||||
<BFormSelect v-model="form.connection" :options="connectionChoices" id="input-connection" />
|
<BFormSelect
|
||||||
|
v-model="form.connection"
|
||||||
|
:options="connectionChoices"
|
||||||
|
id="input-connection"
|
||||||
|
/>
|
||||||
</BInputGroup>
|
</BInputGroup>
|
||||||
|
|
||||||
<BInputGroup :prepend="$t('protocol')">
|
<BInputGroup :prepend="$t('protocol')">
|
||||||
<BFormSelect v-model="form.protocol" :options="protocolChoices" id="input-protocol" />
|
<BFormSelect
|
||||||
|
v-model="form.protocol"
|
||||||
|
:options="protocolChoices"
|
||||||
|
id="input-protocol"
|
||||||
|
/>
|
||||||
</BInputGroup>
|
</BInputGroup>
|
||||||
</CardForm>
|
</CardForm>
|
||||||
|
|
||||||
<!-- UPnP -->
|
<!-- UPnP -->
|
||||||
<YCard :title="$t('upnp')" icon="exchange" :body-text-variant="upnpEnabled ? 'success' : 'danger'">
|
<YCard
|
||||||
{{ $t(upnpEnabled ? 'upnp_enabled' : 'upnp_disabled' ) }}
|
:title="$t('upnp')"
|
||||||
|
icon="exchange"
|
||||||
|
:body-text-variant="upnpEnabled ? 'success' : 'danger'"
|
||||||
|
>
|
||||||
|
{{ $t(upnpEnabled ? 'upnp_enabled' : 'upnp_disabled') }}
|
||||||
|
|
||||||
<BFormInvalidFeedback :state="upnpError !== '' ? false : null">
|
<BFormInvalidFeedback :state="upnpError !== '' ? false : null">
|
||||||
{{ upnpError }}
|
{{ upnpError }}
|
||||||
</BFormInvalidFeedback>
|
</BFormInvalidFeedback>
|
||||||
|
|
||||||
<template #buttons>
|
<template #buttons>
|
||||||
<BButton @click="toggleUpnp" :variant="!upnpEnabled ? 'success' : 'danger'">
|
<BButton
|
||||||
{{ $t(!upnpEnabled ? 'enable' : 'disable' ) }}
|
@click="toggleUpnp"
|
||||||
|
:variant="!upnpEnabled ? 'success' : 'danger'"
|
||||||
|
>
|
||||||
|
{{ $t(!upnpEnabled ? 'enable' : 'disable') }}
|
||||||
</BButton>
|
</BButton>
|
||||||
</template>
|
</template>
|
||||||
</YCard>
|
</YCard>
|
||||||
|
@ -96,11 +127,9 @@ import { required, integer, between } from '@/helpers/validators'
|
||||||
export default {
|
export default {
|
||||||
name: 'ToolFirewall',
|
name: 'ToolFirewall',
|
||||||
|
|
||||||
data () {
|
data() {
|
||||||
return {
|
return {
|
||||||
queries: [
|
queries: [['GET', '/firewall?raw']],
|
||||||
['GET', '/firewall?raw']
|
|
||||||
],
|
|
||||||
serverError: '',
|
serverError: '',
|
||||||
|
|
||||||
// Ports tables data
|
// Ports tables data
|
||||||
|
@ -108,7 +137,7 @@ export default {
|
||||||
{ key: 'port', label: this.$i18n.t('port') },
|
{ key: 'port', label: this.$i18n.t('port') },
|
||||||
{ key: 'ipv4', label: this.$i18n.t('ipv4') },
|
{ key: 'ipv4', label: this.$i18n.t('ipv4') },
|
||||||
{ key: 'ipv6', label: this.$i18n.t('ipv6') },
|
{ key: 'ipv6', label: this.$i18n.t('ipv6') },
|
||||||
{ key: 'uPnP', label: this.$i18n.t('upnp') }
|
{ key: 'uPnP', label: this.$i18n.t('upnp') },
|
||||||
],
|
],
|
||||||
protocols: undefined,
|
protocols: undefined,
|
||||||
portToToggle: undefined,
|
portToToggle: undefined,
|
||||||
|
@ -116,50 +145,53 @@ export default {
|
||||||
// Ports form data
|
// Ports form data
|
||||||
actionChoices: [
|
actionChoices: [
|
||||||
{ value: 'allow', text: this.$i18n.t('open') },
|
{ value: 'allow', text: this.$i18n.t('open') },
|
||||||
{ value: 'disallow', text: this.$i18n.t('close') }
|
{ value: 'disallow', text: this.$i18n.t('close') },
|
||||||
],
|
],
|
||||||
connectionChoices: [
|
connectionChoices: [
|
||||||
{ value: 'ipv4', text: this.$i18n.t('ipv4') },
|
{ value: 'ipv4', text: this.$i18n.t('ipv4') },
|
||||||
{ value: 'ipv6', text: this.$i18n.t('ipv6') }
|
{ value: 'ipv6', text: this.$i18n.t('ipv6') },
|
||||||
],
|
],
|
||||||
protocolChoices: [
|
protocolChoices: [
|
||||||
{ value: 'TCP', text: this.$i18n.t('tcp') },
|
{ value: 'TCP', text: this.$i18n.t('tcp') },
|
||||||
{ value: 'UDP', text: this.$i18n.t('udp') },
|
{ value: 'UDP', text: this.$i18n.t('udp') },
|
||||||
{ value: 'Both', text: this.$i18n.t('both') }
|
{ value: 'Both', text: this.$i18n.t('both') },
|
||||||
],
|
],
|
||||||
form: {
|
form: {
|
||||||
action: 'allow',
|
action: 'allow',
|
||||||
port: undefined,
|
port: undefined,
|
||||||
connection: 'ipv4',
|
connection: 'ipv4',
|
||||||
protocol: 'TCP'
|
protocol: 'TCP',
|
||||||
},
|
},
|
||||||
|
|
||||||
// uPnP
|
// uPnP
|
||||||
upnpEnabled: undefined,
|
upnpEnabled: undefined,
|
||||||
upnpError: ''
|
upnpError: '',
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
validations: {
|
validations: {
|
||||||
form: {
|
form: {
|
||||||
port: { number: required, integer, between: between(0, 65535) }
|
port: { number: required, integer, between: between(0, 65535) },
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
onQueriesResponse (data) {
|
onQueriesResponse(data) {
|
||||||
const ports = Object.values(data).reduce((ports, protocols) => {
|
const ports = Object.values(data).reduce(
|
||||||
for (const type of ['TCP', 'UDP']) {
|
(ports, protocols) => {
|
||||||
for (const port of protocols[type]) {
|
for (const type of ['TCP', 'UDP']) {
|
||||||
ports[type].add(port)
|
for (const port of protocols[type]) {
|
||||||
|
ports[type].add(port)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
return ports
|
||||||
return ports
|
},
|
||||||
}, { TCP: new Set(), UDP: new Set() })
|
{ TCP: new Set(), UDP: new Set() },
|
||||||
|
)
|
||||||
|
|
||||||
const tables = {
|
const tables = {
|
||||||
TCP: [],
|
TCP: [],
|
||||||
UDP: []
|
UDP: [],
|
||||||
}
|
}
|
||||||
for (const protocol of ['TCP', 'UDP']) {
|
for (const protocol of ['TCP', 'UDP']) {
|
||||||
for (const port of ports[protocol]) {
|
for (const port of ports[protocol]) {
|
||||||
|
@ -169,89 +201,111 @@ export default {
|
||||||
}
|
}
|
||||||
tables[protocol].push(row)
|
tables[protocol].push(row)
|
||||||
}
|
}
|
||||||
tables[protocol].sort((a, b) => a.port < b.port ? -1 : 1)
|
tables[protocol].sort((a, b) => (a.port < b.port ? -1 : 1))
|
||||||
}
|
}
|
||||||
|
|
||||||
this.protocols = tables
|
this.protocols = tables
|
||||||
this.upnpEnabled = data.uPnP.enabled
|
this.upnpEnabled = data.uPnP.enabled
|
||||||
},
|
},
|
||||||
|
|
||||||
async togglePort ({ action, port, protocol, connection }) {
|
async togglePort({ action, port, protocol, connection }) {
|
||||||
const confirmed = await this.$askConfirmation(
|
const confirmed = await this.$askConfirmation(
|
||||||
this.$i18n.t('confirm_firewall_' + action, { port, protocol, connection })
|
this.$i18n.t('confirm_firewall_' + action, {
|
||||||
|
port,
|
||||||
|
protocol,
|
||||||
|
connection,
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
if (!confirmed) {
|
if (!confirmed) {
|
||||||
return Promise.resolve(confirmed)
|
return Promise.resolve(confirmed)
|
||||||
}
|
}
|
||||||
|
|
||||||
const actionTrad = this.$i18n.t({ allow: 'open', disallow: 'close' }[action])
|
const actionTrad = this.$i18n.t(
|
||||||
return api.put(
|
{ allow: 'open', disallow: 'close' }[action],
|
||||||
`firewall/${protocol}/${action}/${port}?${connection}_only`,
|
)
|
||||||
{},
|
return api
|
||||||
{ key: 'firewall.ports', protocol, action: actionTrad, port, connection },
|
.put(
|
||||||
{ wait: false }
|
`firewall/${protocol}/${action}/${port}?${connection}_only`,
|
||||||
).then(() => confirmed)
|
{},
|
||||||
|
{
|
||||||
|
key: 'firewall.ports',
|
||||||
|
protocol,
|
||||||
|
action: actionTrad,
|
||||||
|
port,
|
||||||
|
connection,
|
||||||
|
},
|
||||||
|
{ wait: false },
|
||||||
|
)
|
||||||
|
.then(() => confirmed)
|
||||||
},
|
},
|
||||||
|
|
||||||
async toggleUpnp (value) {
|
async toggleUpnp(value) {
|
||||||
const action = this.upnpEnabled ? 'disable' : 'enable'
|
const action = this.upnpEnabled ? 'disable' : 'enable'
|
||||||
const confirmed = await this.$askConfirmation(this.$i18n.t('confirm_upnp_' + action))
|
const confirmed = await this.$askConfirmation(
|
||||||
|
this.$i18n.t('confirm_upnp_' + action),
|
||||||
|
)
|
||||||
if (!confirmed) return
|
if (!confirmed) return
|
||||||
|
|
||||||
api.put(
|
api
|
||||||
'firewall/upnp/' + action,
|
.put(
|
||||||
{},
|
'firewall/upnp/' + action,
|
||||||
{ key: 'firewall.upnp', action: this.$i18n.t(action) }
|
{},
|
||||||
).then(() => {
|
{ key: 'firewall.upnp', action: this.$i18n.t(action) },
|
||||||
// FIXME Couldn't test when it works.
|
)
|
||||||
this.$refs.view.fetchQueries()
|
.then(() => {
|
||||||
}).catch(err => {
|
// FIXME Couldn't test when it works.
|
||||||
if (err.name !== 'APIBadRequestError') throw err
|
this.$refs.view.fetchQueries()
|
||||||
this.upnpError = err.message
|
})
|
||||||
})
|
.catch((err) => {
|
||||||
|
if (err.name !== 'APIBadRequestError') throw err
|
||||||
|
this.upnpError = err.message
|
||||||
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
onTablePortToggling (port, protocol, connection, index, value) {
|
onTablePortToggling(port, protocol, connection, index, value) {
|
||||||
this.$set(this.protocols[protocol][index], connection, value)
|
this.$set(this.protocols[protocol][index], connection, value)
|
||||||
const action = value ? 'allow' : 'disallow'
|
const action = value ? 'allow' : 'disallow'
|
||||||
this.togglePort({ action, port, protocol, connection }).then(toggled => {
|
this.togglePort({ action, port, protocol, connection }).then(
|
||||||
// Revert change on cancel
|
(toggled) => {
|
||||||
if (!toggled) {
|
// Revert change on cancel
|
||||||
this.$set(this.protocols[protocol][index], connection, !value)
|
if (!toggled) {
|
||||||
}
|
this.$set(this.protocols[protocol][index], connection, !value)
|
||||||
})
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
},
|
},
|
||||||
|
|
||||||
onFormPortToggling (e) {
|
onFormPortToggling(e) {
|
||||||
this.togglePort(this.form).then(toggled => {
|
this.togglePort(this.form).then((toggled) => {
|
||||||
if (toggled) this.$refs.view.fetchQueries()
|
if (toggled) this.$refs.view.fetchQueries()
|
||||||
})
|
})
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
mixins: [validationMixin]
|
mixins: [validationMixin],
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
::v-deep .on-off-switch {
|
::v-deep .on-off-switch {
|
||||||
.custom-control-input {
|
.custom-control-input {
|
||||||
&:checked ~ .custom-control-label::before {
|
&:checked ~ .custom-control-label::before {
|
||||||
border-color: $success;
|
border-color: $success;
|
||||||
background-color: $success;
|
background-color: $success;
|
||||||
|
}
|
||||||
|
&:not(:checked) ~ .custom-control-label {
|
||||||
|
&::before {
|
||||||
|
border-color: $danger;
|
||||||
|
background-color: $danger;
|
||||||
}
|
}
|
||||||
&:not(:checked) ~ .custom-control-label {
|
&::after {
|
||||||
&::before {
|
background-color: $white;
|
||||||
border-color: $danger;
|
|
||||||
background-color: $danger;
|
|
||||||
}
|
|
||||||
&::after {
|
|
||||||
background-color: $white;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
input:focus ~ .custom-control-label, &:hover {
|
input:focus ~ .custom-control-label,
|
||||||
|
&:hover {
|
||||||
span {
|
span {
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
}
|
}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue