mirror of
https://github.com/YunoHost/yunohost-admin.git
synced 2024-09-03 20:06:15 +02:00
Merge pull request #437 from YunoHost/enh-configpanels
Enh configpanels
This commit is contained in:
commit
895bab5e26
11 changed files with 288 additions and 166 deletions
|
@ -40,9 +40,9 @@
|
||||||
<!-- The `key` on router-view make sure that if a link points to a page that
|
<!-- The `key` on router-view make sure that if a link points to a page that
|
||||||
use the same component as the previous one, it will be refreshed -->
|
use the same component as the previous one, it will be refreshed -->
|
||||||
<transition v-if="transitions" :name="transitionName">
|
<transition v-if="transitions" :name="transitionName">
|
||||||
<router-view class="animated" :key="$route.fullPath" />
|
<router-view class="animated" :key="routerKey" />
|
||||||
</transition>
|
</transition>
|
||||||
<router-view v-else class="static" :key="$route.fullPath" />
|
<router-view v-else class="static" :key="routerKey" />
|
||||||
</main>
|
</main>
|
||||||
</view-lock-overlay>
|
</view-lock-overlay>
|
||||||
|
|
||||||
|
@ -86,25 +86,15 @@ export default {
|
||||||
ViewLockOverlay
|
ViewLockOverlay
|
||||||
},
|
},
|
||||||
|
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
transitionName: null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters(['connected', 'yunohost', 'transitions', 'waiting'])
|
...mapGetters([
|
||||||
},
|
'connected',
|
||||||
|
'yunohost',
|
||||||
watch: {
|
'routerKey',
|
||||||
// Set the css class to animate the components transition
|
'transitions',
|
||||||
'$route' (to, from) {
|
'transitionName',
|
||||||
if (!this.transitions || from.name === null) return
|
'waiting'
|
||||||
// Use the breadcrumb array length as a direction indicator
|
])
|
||||||
const toDepth = to.meta.breadcrumb.length
|
|
||||||
const fromDepth = from.meta.breadcrumb.length
|
|
||||||
this.transitionName = toDepth < fromDepth ? 'slide-right' : 'slide-left'
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
|
70
app/src/components/ConfigPanel.vue
Normal file
70
app/src/components/ConfigPanel.vue
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
<template>
|
||||||
|
<abstract-form
|
||||||
|
v-bind="{ id: panel.id + '-form', validation, serverError: panel.serverError }"
|
||||||
|
@submit.prevent.stop="$emit('submit', panel.id)"
|
||||||
|
>
|
||||||
|
<slot name="tab-top" />
|
||||||
|
|
||||||
|
<template v-if="panel.help" #disclaimer>
|
||||||
|
<div class="alert alert-info" v-html="help" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<slot name="tab-before" />
|
||||||
|
|
||||||
|
<template v-for="section in panel.sections">
|
||||||
|
<div v-if="isVisible(section.visible, section)" :key="section.id" class="mb-5">
|
||||||
|
<b-card-title v-if="section.name" title-tag="h3">
|
||||||
|
{{ section.name }} <small v-if="section.help">{{ section.help }}</small>
|
||||||
|
</b-card-title>
|
||||||
|
|
||||||
|
<template v-for="(field, fname) in section.fields">
|
||||||
|
<form-field
|
||||||
|
v-if="isVisible(field.visible, field)" :key="fname"
|
||||||
|
v-model="forms[panel.id][fname]" v-bind="field" :validation="validation[fname]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<slot name="tab-after" />
|
||||||
|
</abstract-form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { configPanelsFieldIsVisible } from '@/helpers/yunohostArguments'
|
||||||
|
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ConfigPanel',
|
||||||
|
|
||||||
|
props: {
|
||||||
|
tabId: { type: String, required: true },
|
||||||
|
panels: { type: Array, default: undefined },
|
||||||
|
forms: { type: Object, default: undefined },
|
||||||
|
v: { type: Object, default: undefined }
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
panel () {
|
||||||
|
return this.panels.find(panel => panel.id === this.tabId)
|
||||||
|
},
|
||||||
|
|
||||||
|
validation () {
|
||||||
|
return this.v.forms[this.panel.id]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
isVisible (expression, field) {
|
||||||
|
return configPanelsFieldIsVisible(expression, field, this.forms)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.card-title {
|
||||||
|
margin-bottom: 1em;
|
||||||
|
border-bottom: solid 1px #aaa;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,67 +1,50 @@
|
||||||
<template>
|
<template>
|
||||||
<b-card no-body>
|
<routable-tabs
|
||||||
<b-tabs fill pills card>
|
:routes="routes_"
|
||||||
<slot name="before" />
|
v-bind="{ panels, forms, v: $v }"
|
||||||
|
v-on="$listeners"
|
||||||
<tab-form
|
/>
|
||||||
v-for="{ name, id, sections, help, serverError } in panels" :key="id"
|
|
||||||
v-bind="{ name, id: id + '-form', validation: $v.forms[id], serverError }"
|
|
||||||
@submit.prevent.stop="$emit('submit', id)"
|
|
||||||
>
|
|
||||||
<template v-if="help" #disclaimer>
|
|
||||||
<div class="alert alert-info" v-html="help" />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<slot :name="id + '-tab-before'" />
|
|
||||||
|
|
||||||
<template v-for="section in sections">
|
|
||||||
<div v-if="isVisible(section.visible, section)" :key="section.id" class="mb-5">
|
|
||||||
<b-card-title v-if="section.name" title-tag="h3">
|
|
||||||
{{ section.name }} <small v-if="section.help">{{ section.help }}</small>
|
|
||||||
</b-card-title>
|
|
||||||
|
|
||||||
<template v-for="(field, fname) in section.fields">
|
|
||||||
<form-field
|
|
||||||
v-if="isVisible(field.visible, field)" :key="fname"
|
|
||||||
v-model="forms[id][fname]" v-bind="field" :validation="$v.forms[id][fname]"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<slot :name="id + '-tab-after'" />
|
|
||||||
</tab-form>
|
|
||||||
|
|
||||||
<slot name="default" />
|
|
||||||
</b-tabs>
|
|
||||||
</b-card>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { validationMixin } from 'vuelidate'
|
import { validationMixin } from 'vuelidate'
|
||||||
|
|
||||||
import { configPanelsFieldIsVisible } from '@/helpers/yunohostArguments'
|
|
||||||
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ConfigPanels',
|
name: 'ConfigPanels',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
RoutableTabs: () => import('@/components/RoutableTabs.vue')
|
||||||
|
},
|
||||||
|
|
||||||
mixins: [validationMixin],
|
mixins: [validationMixin],
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
panels: { type: Array, default: undefined },
|
panels: { type: Array, default: undefined },
|
||||||
forms: { type: Object, default: undefined },
|
forms: { type: Object, default: undefined },
|
||||||
validations: { type: Object, default: undefined }
|
validations: { type: Object, default: undefined },
|
||||||
|
errors: { type: Object, default: undefined }, // never used
|
||||||
|
routes: { type: Array, default: null },
|
||||||
|
noRedirect: { type: Boolean, default: false }
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
routes_ () {
|
||||||
|
if (this.routes) return this.routes
|
||||||
|
return this.panels.map(panel => ({
|
||||||
|
to: { params: { tabId: panel.id } },
|
||||||
|
text: panel.name,
|
||||||
|
icon: panel.icon || 'wrench'
|
||||||
|
}))
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
validations () {
|
validations () {
|
||||||
const v = this.validations
|
return { forms: this.validations }
|
||||||
return v ? { forms: v } : null
|
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
created () {
|
||||||
isVisible (expression, field) {
|
if (!this.noRedirect && !this.$route.params.tabId) {
|
||||||
return configPanelsFieldIsVisible(expression, field, this.forms)
|
this.$router.replace({ params: { tabId: this.panels[0].id } })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
32
app/src/components/RoutableTabs.vue
Normal file
32
app/src/components/RoutableTabs.vue
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
<template>
|
||||||
|
<b-card no-body>
|
||||||
|
<b-card-header header-tag="nav">
|
||||||
|
<b-nav card-header fill pills>
|
||||||
|
<b-nav-item
|
||||||
|
v-for="route in routes" :key="route.text"
|
||||||
|
:to="route.to" exact exact-active-class="active"
|
||||||
|
>
|
||||||
|
<icon v-if="route.icon" :iname="route.icon" />
|
||||||
|
{{ route.text }}
|
||||||
|
</b-nav-item>
|
||||||
|
</b-nav>
|
||||||
|
</b-card-header>
|
||||||
|
|
||||||
|
<!-- Bind extra props to the child view and forward child events to parent -->
|
||||||
|
<router-view v-bind="$attrs" v-on="$listeners" />
|
||||||
|
</b-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'RoutableTabs',
|
||||||
|
|
||||||
|
// Thanks to `v-bind="$attrs"` and `inheritAttrs: false`, this component can forward
|
||||||
|
// arbitrary attributes (props) directly to its children.
|
||||||
|
inheritAttrs: false,
|
||||||
|
|
||||||
|
props: {
|
||||||
|
routes: { type: Array, required: true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -1,40 +1,37 @@
|
||||||
<template>
|
<template>
|
||||||
<b-tab no-body>
|
<div>
|
||||||
<template #title>
|
|
||||||
<icon :iname="icon" /> {{ name }}
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<b-card-body>
|
<b-card-body>
|
||||||
<slot name="disclaimer" />
|
<slot name="disclaimer" />
|
||||||
|
|
||||||
|
|
||||||
<b-form
|
<b-form
|
||||||
:id="id" :inline="inline" :class="formClasses"
|
:id="id" :inline="inline" :class="formClasses"
|
||||||
@submit.prevent="onSubmit" novalidate
|
@submit.prevent="onSubmit" novalidate
|
||||||
>
|
>
|
||||||
<slot name="default" />
|
<slot name="default" />
|
||||||
|
|
||||||
<slot name="server-error">
|
<slot name="server-error" v-bind="{ errorFeedback }">
|
||||||
<b-alert
|
<b-alert
|
||||||
|
v-if="errorFeedback"
|
||||||
variant="danger" class="my-3" icon="ban"
|
variant="danger" class="my-3" icon="ban"
|
||||||
:show="errorFeedback !== ''" v-html="errorFeedback"
|
v-html="errorFeedback"
|
||||||
/>
|
/>
|
||||||
</slot>
|
</slot>
|
||||||
</b-form>
|
</b-form>
|
||||||
</b-card-body>
|
</b-card-body>
|
||||||
|
|
||||||
<b-card-footer>
|
<b-card-footer v-if="!noFooter">
|
||||||
<b-button type="submit" variant="success" :form="id">
|
<slot name="footer">
|
||||||
{{ submitText ? submitText : $t('save') }}
|
<b-button type="submit" variant="success" :form="id">
|
||||||
</b-button>
|
{{ submitText || $t('save') }}
|
||||||
|
</b-button>
|
||||||
|
</slot>
|
||||||
</b-card-footer>
|
</b-card-footer>
|
||||||
</b-tab>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'TabForm',
|
name: 'AbstractForm',
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
id: { type: String, default: 'ynh-form' },
|
id: { type: String, default: 'ynh-form' },
|
||||||
|
@ -43,8 +40,7 @@ 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 },
|
||||||
name: { type: String, required: true },
|
noFooter: { type: Boolean, default: false }
|
||||||
icon: { type: String, default: 'wrench' }
|
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
|
@ -1,13 +1,13 @@
|
||||||
<template>
|
<template>
|
||||||
<b-breadcrumb v-if="routesList">
|
<b-breadcrumb v-if="breadcrumb.length">
|
||||||
<b-breadcrumb-item to="/">
|
<b-breadcrumb-item to="/">
|
||||||
<span class="sr-only">{{ $t('home') }}</span>
|
<span class="sr-only">{{ $t('home') }}</span>
|
||||||
<icon iname="home" />
|
<icon iname="home" />
|
||||||
</b-breadcrumb-item>
|
</b-breadcrumb-item>
|
||||||
|
|
||||||
<b-breadcrumb-item
|
<b-breadcrumb-item
|
||||||
v-for="{ name, text } in breadcrumb" :key="name"
|
v-for="({ name, text }, i) in breadcrumb" :key="name"
|
||||||
:to="{ name }" :active="name === $route.name"
|
:to="{ name }" :active="i === breadcrumb.length - 1"
|
||||||
>
|
>
|
||||||
{{ text }}
|
{{ text }}
|
||||||
</b-breadcrumb-item>
|
</b-breadcrumb-item>
|
||||||
|
@ -15,41 +15,13 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { mapGetters } from 'vuex'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Breadcrumb',
|
name: 'Breadcrumb',
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
routesList () {
|
...mapGetters(['breadcrumb'])
|
||||||
const routesList = this.$route.meta.breadcrumb
|
|
||||||
return routesList && routesList.length ? routesList : null
|
|
||||||
},
|
|
||||||
|
|
||||||
breadcrumb () {
|
|
||||||
if (!this.routesList) return
|
|
||||||
// Get current params to pass it to potential previous routes
|
|
||||||
const currentParams = this.$route.params
|
|
||||||
return this.routesList.map(name => {
|
|
||||||
const { trad, param } = this.getRouteArgs(name)
|
|
||||||
let text = ''
|
|
||||||
// if a traduction key string has been given and we also need to pass
|
|
||||||
// the route param as a variable.
|
|
||||||
if (trad && param) {
|
|
||||||
text = this.$i18n.t(trad, { [param]: currentParams[param] })
|
|
||||||
} else if (trad) {
|
|
||||||
text = this.$i18n.t(trad)
|
|
||||||
} else {
|
|
||||||
text = currentParams[param]
|
|
||||||
}
|
|
||||||
return { name, text }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
getRouteArgs (routeName) {
|
|
||||||
const route = this.$router.options.routes.find(route => route.name === routeName)
|
|
||||||
return route ? route.meta.args : {}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div v-bind="$attr" :class="['custom-spinner', spinner]" />
|
<div :class="['custom-spinner', spinner]" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
|
@ -295,7 +295,7 @@ export function formatYunoHostConfigPanels (data) {
|
||||||
}
|
}
|
||||||
|
|
||||||
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: [] }
|
const panel = { id: panelId, sections: [], serverError: '' }
|
||||||
result.forms[panelId] = {}
|
result.forms[panelId] = {}
|
||||||
result.validations[panelId] = {}
|
result.validations[panelId] = {}
|
||||||
result.errors[panelId] = {}
|
result.errors[panelId] = {}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
import VueRouter from 'vue-router'
|
import VueRouter from 'vue-router'
|
||||||
import i18n from '@/i18n'
|
|
||||||
import routes from './routes'
|
import routes from './routes'
|
||||||
import store from '@/store'
|
import store from '@/store'
|
||||||
|
|
||||||
|
@ -29,6 +28,10 @@ const router = new VueRouter({
|
||||||
})
|
})
|
||||||
|
|
||||||
router.beforeEach((to, from, next) => {
|
router.beforeEach((to, from, next) => {
|
||||||
|
if (store.getters.transitions && from.name !== null) {
|
||||||
|
store.dispatch('UPDATE_TRANSITION_NAME', { to, from })
|
||||||
|
}
|
||||||
|
|
||||||
if (store.getters.error) {
|
if (store.getters.error) {
|
||||||
store.dispatch('DISMISS_ERROR', true)
|
store.dispatch('DISMISS_ERROR', true)
|
||||||
}
|
}
|
||||||
|
@ -41,29 +44,8 @@ router.beforeEach((to, from, next) => {
|
||||||
})
|
})
|
||||||
|
|
||||||
router.afterEach((to, from) => {
|
router.afterEach((to, from) => {
|
||||||
// Display a simplified breadcrumb as the document title.
|
store.dispatch('UPDATE_ROUTER_KEY', { to, from })
|
||||||
const routeParams = to.params
|
store.dispatch('UPDATE_BREADCRUMB', { to, from })
|
||||||
let breadcrumb = to.meta.breadcrumb
|
|
||||||
if (breadcrumb.length === 0) {
|
|
||||||
breadcrumb = [to.name]
|
|
||||||
} else if (breadcrumb.length > 2) {
|
|
||||||
breadcrumb = breadcrumb.slice(breadcrumb.length - 2)
|
|
||||||
}
|
|
||||||
|
|
||||||
const title = breadcrumb.map(name => {
|
|
||||||
const route = routes.find(route => route.name === name)
|
|
||||||
const { trad, param } = route ? route.meta.args : {}
|
|
||||||
// if a traduction key string has been given and we also need to pass
|
|
||||||
// the route param as a variable.
|
|
||||||
if (trad && param) {
|
|
||||||
return i18n.t(trad, { [param]: routeParams[param] })
|
|
||||||
} else if (trad) {
|
|
||||||
return i18n.t(trad)
|
|
||||||
}
|
|
||||||
return routeParams[param]
|
|
||||||
}).reverse().join(' / ')
|
|
||||||
|
|
||||||
document.title = `${title} | ${i18n.t('yunohost_admin')}`
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export default router
|
export default router
|
||||||
|
|
|
@ -17,10 +17,8 @@ const routes = [
|
||||||
name: 'home',
|
name: 'home',
|
||||||
path: '/',
|
path: '/',
|
||||||
component: Home,
|
component: Home,
|
||||||
// Leave the empty breadcrumb as it is used by the animated transition to know which way to go
|
|
||||||
meta: {
|
meta: {
|
||||||
args: { trad: 'home' },
|
args: { trad: 'home' }
|
||||||
breadcrumb: []
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -30,8 +28,7 @@ const routes = [
|
||||||
component: Login,
|
component: Login,
|
||||||
meta: {
|
meta: {
|
||||||
noAuth: true,
|
noAuth: true,
|
||||||
args: { trad: 'login' },
|
args: { trad: 'login' }
|
||||||
breadcrumb: []
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -42,12 +39,10 @@ const routes = [
|
||||||
name: 'post-install',
|
name: 'post-install',
|
||||||
path: '/postinstall',
|
path: '/postinstall',
|
||||||
component: () => import(/* webpackChunkName: "views/post-install" */ '@/views/PostInstall'),
|
component: () => import(/* webpackChunkName: "views/post-install" */ '@/views/PostInstall'),
|
||||||
// Leave the breadcrumb
|
|
||||||
meta: {
|
meta: {
|
||||||
noAuth: true,
|
noAuth: true,
|
||||||
args: { trad: 'postinstall.title' },
|
args: { trad: 'postinstall.title' }
|
||||||
breadcrumb: []
|
}
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/* ───────╮
|
/* ───────╮
|
||||||
|
@ -77,7 +72,7 @@ const routes = [
|
||||||
component: () => import(/* webpackChunkName: "views/user/import" */ '@/views/user/UserImport'),
|
component: () => import(/* webpackChunkName: "views/user/import" */ '@/views/user/UserImport'),
|
||||||
props: true,
|
props: true,
|
||||||
meta: {
|
meta: {
|
||||||
args: { param: 'name' },
|
args: { trad: 'users_import' },
|
||||||
breadcrumb: ['user-list', 'user-import']
|
breadcrumb: ['user-list', 'user-import']
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -156,14 +151,23 @@ const routes = [
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'domain-config',
|
// no need for name here, only children are visited
|
||||||
path: '/domains/:name/config',
|
path: '/domains/:name/config',
|
||||||
component: () => import(/* webpackChunkName: "views/domain/dns" */ '@/views/domain/DomainConfig'),
|
component: () => import(/* webpackChunkName: "views/domain/config" */ '@/views/domain/DomainConfig'),
|
||||||
props: true,
|
props: true,
|
||||||
meta: {
|
children: [
|
||||||
args: { trad: 'config' },
|
{
|
||||||
breadcrumb: ['domain-list', 'domain-info', 'domain-config']
|
name: 'domain-config',
|
||||||
}
|
path: ':tabId?',
|
||||||
|
component: () => import(/* webpackChunkName: "components/configPanel" */ '@/components/ConfigPanel'),
|
||||||
|
props: true,
|
||||||
|
meta: {
|
||||||
|
routerParams: ['name'], // Override router key params to avoid view recreation at tab change.
|
||||||
|
args: { trad: 'config' },
|
||||||
|
breadcrumb: ['domain-list', 'domain-info', 'domain-config']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'domain-dns',
|
name: 'domain-dns',
|
||||||
|
@ -248,14 +252,23 @@ const routes = [
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'app-config-panel',
|
// no need for name here, only children are visited
|
||||||
path: '/apps/:id/config-panel',
|
path: '/apps/:id/config-panel',
|
||||||
component: () => import(/* webpackChunkName: "views/apps/config" */ '@/views/app/AppConfigPanel'),
|
component: () => import(/* webpackChunkName: "views/apps/config" */ '@/views/app/AppConfigPanel'),
|
||||||
props: true,
|
props: true,
|
||||||
meta: {
|
children: [
|
||||||
args: { trad: 'app_config_panel' },
|
{
|
||||||
breadcrumb: ['app-list', 'app-info', 'app-config-panel']
|
name: 'app-config-panel',
|
||||||
}
|
path: ':tabId?',
|
||||||
|
component: () => import(/* webpackChunkName: "components/configPanel" */ '@/components/ConfigPanel'),
|
||||||
|
props: true,
|
||||||
|
meta: {
|
||||||
|
routerParams: ['id'],
|
||||||
|
args: { trad: 'app_config_panel' },
|
||||||
|
breadcrumb: ['app-list', 'app-info', 'app-config-panel']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
/* ────────────────╮
|
/* ────────────────╮
|
||||||
|
|
|
@ -2,7 +2,7 @@ import Vue from 'vue'
|
||||||
import router from '@/router'
|
import router from '@/router'
|
||||||
import i18n from '@/i18n'
|
import i18n from '@/i18n'
|
||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
import { timeout, isObjectLiteral } from '@/helpers/commons'
|
import { timeout, isEmptyValue, isObjectLiteral } from '@/helpers/commons'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
state: {
|
state: {
|
||||||
|
@ -15,7 +15,10 @@ export default {
|
||||||
requests: [], // Array of `request`
|
requests: [], // Array of `request`
|
||||||
error: null, // null || request
|
error: null, // null || request
|
||||||
historyTimer: null, // null || setTimeout id
|
historyTimer: null, // null || setTimeout id
|
||||||
tempMessages: [] // array of messages
|
tempMessages: [], // Array of messages
|
||||||
|
routerKey: undefined, // String if current route has params
|
||||||
|
breadcrumb: [], // Array of routes
|
||||||
|
transitionName: null // String of CSS class if transitions are enabled
|
||||||
},
|
},
|
||||||
|
|
||||||
mutations: {
|
mutations: {
|
||||||
|
@ -87,6 +90,18 @@ export default {
|
||||||
} else {
|
} else {
|
||||||
state.error = null
|
state.error = null
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
'SET_ROUTER_KEY' (state, key) {
|
||||||
|
state.routerKey = key
|
||||||
|
},
|
||||||
|
|
||||||
|
'SET_BREADCRUMB' (state, breadcrumb) {
|
||||||
|
state.breadcrumb = breadcrumb
|
||||||
|
},
|
||||||
|
|
||||||
|
'SET_TRANSITION_NAME' (state, transitionName) {
|
||||||
|
state.transitionName = transitionName
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -264,6 +279,72 @@ export default {
|
||||||
'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 }) {
|
||||||
|
if (isEmptyValue(to.params)) {
|
||||||
|
commit('SET_ROUTER_KEY', undefined)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// If the next route uses the same component as the previous one, Vue will not
|
||||||
|
// recreate an instance of that component, so hooks like `created()` will not be
|
||||||
|
// triggered and data will not be fetched.
|
||||||
|
// For routes with params, we create a unique key to force the recreation of a view.
|
||||||
|
// Params can be declared in route `meta` to stricly define which params should be
|
||||||
|
// taken into account.
|
||||||
|
const params = to.meta.routerParams
|
||||||
|
? to.meta.routerParams.map(key => to.params[key])
|
||||||
|
: Object.values(to.params)
|
||||||
|
|
||||||
|
commit('SET_ROUTER_KEY', `${to.name}-${params.join('-')}`)
|
||||||
|
},
|
||||||
|
|
||||||
|
'UPDATE_BREADCRUMB' ({ commit }, { to, from }) {
|
||||||
|
function getRouteNames (route) {
|
||||||
|
if (route.meta.breadcrumb) return route.meta.breadcrumb
|
||||||
|
const parentRoute = route.matched.slice().reverse().find(route => route.meta.breadcrumb)
|
||||||
|
if (parentRoute) return parentRoute.meta.breadcrumb
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatRoute (route) {
|
||||||
|
const { trad, param } = route.meta.args || {}
|
||||||
|
let text = ''
|
||||||
|
// if a traduction key string has been given and we also need to pass
|
||||||
|
// the route param as a variable.
|
||||||
|
if (trad && param) {
|
||||||
|
text = i18n.t(trad, { [param]: to.params[param] })
|
||||||
|
} else if (trad) {
|
||||||
|
text = i18n.t(trad)
|
||||||
|
} else {
|
||||||
|
text = to.params[param]
|
||||||
|
}
|
||||||
|
return { name: route.name, text }
|
||||||
|
}
|
||||||
|
|
||||||
|
const routeNames = getRouteNames(to)
|
||||||
|
const allRoutes = router.getRoutes()
|
||||||
|
const breadcrumb = routeNames.map(name => {
|
||||||
|
const route = allRoutes.find(route => route.name === name)
|
||||||
|
return formatRoute(route)
|
||||||
|
})
|
||||||
|
|
||||||
|
commit('SET_BREADCRUMB', breadcrumb)
|
||||||
|
|
||||||
|
function getTitle (breadcrumb) {
|
||||||
|
if (breadcrumb.length === 0) return formatRoute(to).text
|
||||||
|
return (breadcrumb.length > 2 ? breadcrumb.slice(-2) : breadcrumb).map(route => route.text).reverse().join(' / ')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display a simplified breadcrumb as the document title.
|
||||||
|
document.title = `${getTitle(breadcrumb)} | ${i18n.t('yunohost_admin')}`
|
||||||
|
},
|
||||||
|
|
||||||
|
'UPDATE_TRANSITION_NAME' ({ state, commit }, { to, from }) {
|
||||||
|
// Use the breadcrumb array length as a direction indicator
|
||||||
|
const toDepth = (to.meta.breadcrumb || []).length
|
||||||
|
const fromDepth = (from.meta.breadcrumb || []).length
|
||||||
|
commit('SET_TRANSITION_NAME', toDepth < fromDepth ? 'slide-right' : 'slide-left')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -279,6 +360,9 @@ export default {
|
||||||
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,
|
||||||
|
breadcrumb: state => state.breadcrumb,
|
||||||
|
transitionName: state => state.transitionName
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue