linting…

This commit is contained in:
Axolotle 2020-07-15 16:39:24 +02:00
parent d69bb8b906
commit 6f8457381a
15 changed files with 577 additions and 546 deletions

View file

@ -1,48 +1,64 @@
<template> <template>
<div id="app" class="container"> <div id="app" class="container">
<header> <header>
<b-navbar> <b-navbar>
<b-navbar-brand to="/" exact exact-active-class="active"><img alt="Yunohost logo" src="./assets/logo.png"></b-navbar-brand> <b-navbar-brand to="/" exact exact-active-class="active">
<b-navbar-nav class="ml-auto"> <img alt="Yunohost logo" src="./assets/logo.png">
<li class="nav-item"> </b-navbar-brand>
<b-button href="/yunohost/sso" variant="primary" block size="sm"> <b-navbar-nav class="ml-auto">
{{ $t('user_interface_link') }} <icon iname="user"/> <li class="nav-item">
</b-button> <b-button href="/yunohost/sso"
</li> variant="primary" size="sm" block
<li class="nav-item" v-show="connected"> >
<b-button @click.prevent="logout" to="/logout" variant="outline-dark" block size="sm" > {{ $t('user_interface_link') }} <icon iname="user" />
{{ $t('logout') }} <icon iname="sign-out"/> </b-button>
</b-button> </li>
</li> <li class="nav-item" v-show="connected">
</b-navbar-nav> <b-button @click.prevent="logout" to="/logout"
</b-navbar> variant="outline-dark" block size="sm"
</header> >
{{ $t('logout') }} <icon iname="sign-out" />
</b-button>
</li>
</b-navbar-nav>
</b-navbar>
</header>
<main> <main>
<router-view v-if="isReady"/> <router-view v-if="isReady" />
</main> </main>
<footer> <footer>
<nav> <nav>
<b-nav> <b-nav>
<b-nav-item href="https://yunohost.org/docs" target="_blank" link-classes='text-secondary'> <b-nav-item href="https://yunohost.org/docs" target="_blank" link-classes="text-secondary">
<icon iname="book"/> Documentation <icon iname="book" /> Documentation
</b-nav-item> </b-nav-item>
<b-nav-item href="https://yunohost.org/help" target="_blank" link-classes='text-secondary'> <b-nav-item href="https://yunohost.org/help" target="_blank" link-classes="text-secondary">
<icon iname="life-ring"/> Need help? <icon iname="life-ring" /> Need help?
</b-nav-item> </b-nav-item>
<b-nav-item href="https://donate.yunohost.org/" target="_blank" link-classes='text-secondary'> <b-nav-item href="https://donate.yunohost.org/" target="_blank" link-classes="text-secondary">
<icon iname="heart"/> Donate <icon iname="heart" /> Donate
</b-nav-item> </b-nav-item>
<i18n v-if="yunohostInfos" path="footer_version" tag="b-nav-text" class="ml-auto" id="yunohost-version"> <i18n v-if="yunohostInfos" path="footer_version" tag="b-nav-text"
<template v-slot:ynh><b-link href="https://yunohost.org">YunoHost</b-link></template> id="yunohost-version" class="ml-auto"
<template v-slot:version>{{ yunohostInfos.version }}</template> >
<template v-slot:repo>{{ yunohostInfos.repo }}</template> <template v-slot:ynh>
</i18n> <b-link href="https://yunohost.org">
</b-nav> YunoHost
</nav> </b-link>
</footer> </template>
</div> <template v-slot:version>
{{ yunohostInfos.version }}
</template>
<template v-slot:repo>
{{ yunohostInfos.repo }}
</template>
</i18n>
</b-nav>
</nav>
</footer>
</div>
</template> </template>
<script> <script>
@ -50,50 +66,49 @@ import { mapState } from 'vuex'
import api from '@/helpers/api' import api from '@/helpers/api'
export default { export default {
name: 'App', name: 'App',
data: () => { data: () => {
return { return {
// isReady blocks the rendering of the rooter-view until we have a true info // isReady blocks the rendering of the rooter-view until we have a true info
// about the connected state of the user. // about the connected state of the user.
isReady: false, isReady: false
}
},
computed: {
...mapState(['connected', 'yunohostInfos']),
},
methods: {
async logout() {
await api.logout()
this.$store.commit('CONNECTED', false);
this.$router.push('/login')
},
},
// This hook is only triggered at page reload so the value of state.connected
// always come from the localStorage
async created() {
if (!this.$store.state.connected) {
// user is not connected: allow the login view to be rendered.
this.isReady = true
return
}
// localStorage 'connected' value may be true, but session may have expired.
// Try to get the yunohost version.
try {
const data = await api.getVersion()
this.$store.commit('YUNOHOST_INFOS', data.yunohost)
} catch (err) {
// Session expired, reset the 'connected' state and redirect with a query
// FIXME is there a case where the error may not be a 401 therefor requires
// better handling ?
this.$store.commit('CONNECTED', false);
this.$router.push({name: 'login', query: {redirect: this.$route.path}})
} finally {
// in any case allow the router-view to be rendered
this.isReady = true;
}
} }
},
computed: {
...mapState(['connected', 'yunohostInfos'])
},
methods: {
async logout () {
await api.logout()
this.$store.commit('CONNECTED', false)
this.$router.push('/login')
}
},
// This hook is only triggered at page reload so the value of state.connected
// always come from the localStorage
async created () {
if (!this.$store.state.connected) {
// user is not connected: allow the login view to be rendered.
this.isReady = true
return
}
// localStorage 'connected' value may be true, but session may have expired.
// Try to get the yunohost version.
try {
const data = await api.getVersion()
this.$store.commit('YUNOHOST_INFOS', data.yunohost)
} catch (err) {
// Session expired, reset the 'connected' state and redirect with a query
// FIXME is there a case where the error may not be a 401 therefor requires
// better handling ?
this.$store.commit('CONNECTED', false)
this.$router.push({ name: 'login', query: { redirect: this.$route.path } })
} finally {
// in any case allow the router-view to be rendered
this.isReady = true
}
}
} }
</script> </script>
@ -101,46 +116,46 @@ export default {
@import '@/scss/main.scss'; @import '@/scss/main.scss';
#app > header { #app > header {
border-bottom: 1px solid #eee; border-bottom: 1px solid #eee;
padding-top: 1rem; padding-top: 1rem;
margin-bottom: 1rem; margin-bottom: 1rem;
.navbar { .navbar {
padding: 1rem 0; padding: 1rem 0;
img { img {
width: 70px; width: 70px;
}
.navbar-nav {
flex-direction: column;
li {
margin: .2rem 0;
}
icon {
margin-left: .5rem;
}
}
} }
.navbar-nav {
flex-direction: column;
li {
margin: .2rem 0;
}
icon {
margin-left: .5rem;
}
}
}
} }
#app > footer { #app > footer {
padding: 1rem 0; padding: 1rem 0;
border-top: 1px solid #eee; border-top: 1px solid #eee;
font-size: 0.875rem; font-size: 0.875rem;
margin-top: 2rem; margin-top: 2rem;
.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;
}
&:first-child {
margin-left: -1rem;
}
} }
&:first-child {
margin-left: -1rem;
}
}
} }
</style> </style>

View file

@ -1,51 +1,51 @@
<template> <template>
<b-breadcrumb> <b-breadcrumb>
<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="(route, index) in breadcrumb" v-for="(route, index) in breadcrumb"
:key="index" :key="index"
:to="{name: route.name}" :to="{name: route.name}"
:active="index == lastIndex ? true : false" :active="index == lastIndex ? true : false"
> >
{{ route.text }} {{ route.text }}
</b-breadcrumb-item> </b-breadcrumb-item>
</b-breadcrumb> </b-breadcrumb>
</template> </template>
<script> <script>
export default { export default {
computed: { computed: {
params: function () { params: function () {
return this.$route.params return this.$route.params
},
breadcrumb: function () {
return this.$route.meta.breadcrumb.map(({name, trad, param}) => {
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]: this.params[param]})
} else if (trad) {
text = this.$i18n.t(trad)
} else {
text = this.params[param]
}
return {name, text}
})
},
lastIndex: function () {
return this.breadcrumb.length - 1
},
}, },
breadcrumb: function () {
return this.$route.meta.breadcrumb.map(({ name, trad, param }) => {
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]: this.params[param] })
} else if (trad) {
text = this.$i18n.t(trad)
} else {
text = this.params[param]
}
return { name, text }
})
},
lastIndex: function () {
return this.breadcrumb.length - 1
}
}
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.breadcrumb { .breadcrumb {
border: none; border: none;
background-color: transparent !important; background-color: transparent !important;
} }
</style> </style>

View file

@ -1,27 +1,29 @@
<template> <template>
<span :class="'icon fa fa-' + iname" aria-hidden="true"></span> <span :class="'icon fa fa-' + iname" aria-hidden="true" />
</template> </template>
<script> <script>
export default { export default {
name: 'icon', name: 'Icon',
props: ['iname'], props: {
iname: { type: String, required: true }
}
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.icon { .icon {
font-size: 1rem;
width: 1rem;
text-align: center;
&.lg {
width: 3rem;
font-size: 1.5rem;
}
&.fs-sm {
font-size: 1rem; font-size: 1rem;
width: 1rem; }
text-align: center;
&.lg {
width: 3rem;
font-size: 1.5rem;
}
&.fs-sm {
font-size: 1rem;
}
} }
</style> </style>

View file

@ -1 +1 @@
export {default as Icon} from './Icon' export { default as Icon } from './Icon'

View file

@ -1,63 +1,57 @@
function objectToParams (object) {
const urlParams = new URLSearchParams()
function objectToParams(object) { for (const [key, value] of Object.entries(object)) {
const urlParams = new URLSearchParams(); urlParams.append(key, value)
for (const [key, value] of Object.entries(object)) { }
urlParams.append(key, value) return urlParams
}
return urlParams
} }
function handleResponse (response, type = 'json') {
function handleResponse(response, type = 'json') { return response.ok ? response[type]() : handleErrors(response)
return response.ok ? response[type]() : handleErrors(response)
} }
function handleErrors (response) {
function handleErrors(response) { if (response.status === 401) {
if (response.status == 401) { throw new Error('Unauthorized')
throw new Error('Unauthorized'); }
}
} }
export default { export default {
options: { options: {
credentials: 'include', credentials: 'include',
mode: 'cors', mode: 'cors',
headers: { headers: {
// FIXME is it important to keep this previous `Accept` header ? // FIXME is it important to keep this previous `Accept` header ?
// 'Accept': 'application/json, text/javascript, */*; q=0.01', // 'Accept': 'application/json, text/javascript, */*; q=0.01',
// Auto header is : // Auto header is :
// "Accept": "*/*", // "Accept": "*/*",
// Also is this still important ? (needed by back-end) // Also is this still important ? (needed by back-end)
'X-Requested-With': 'XMLHttpRequest', 'X-Requested-With': 'XMLHttpRequest'
}
},
get(uri) {
return fetch('/api/' + uri, this.options)
.then(response => handleResponse(response))
.catch(err => {
console.log(err)
})
},
login(password) {
return fetch('/api/login', {
method: 'POST',
body: objectToParams({password}),
...this.options
}).then(response => (response.ok))
},
logout() {
return fetch('/api/logout', this.options).then(response => (response.ok))
},
getVersion() {
return fetch('/api/versions', this.options)
.then(response => handleResponse(response))
} }
},
get (uri) {
return fetch('/api/' + uri, this.options)
.then(response => handleResponse(response))
.catch(err => {
console.log(err)
})
},
login (password) {
return fetch('/api/login', {
method: 'POST',
body: objectToParams({ password }),
...this.options
}).then(response => (response.ok))
},
logout () {
return fetch('/api/logout', this.options).then(response => (response.ok))
},
getVersion () {
return fetch('/api/versions', this.options).then(response => handleResponse(response))
}
} }

View file

@ -7,17 +7,16 @@ import store from './plugins/store'
import * as globalsComponents from './components/globals' import * as globalsComponents from './components/globals'
Vue.config.productionTip = false Vue.config.productionTip = false
// Register global components // Register global components
for (let component of Object.values(globalsComponents)) { for (const component of Object.values(globalsComponents)) {
Vue.component(component.name, component) Vue.component(component.name, component)
} }
new Vue({ new Vue({
i18n, i18n,
router, router,
store, store,
render: h => h(App), render: h => h(App)
}).$mount('#app') }).$mount('#app')

View file

@ -1,35 +1,34 @@
import Vue from 'vue' import Vue from 'vue'
import VueI18n from 'vue-i18n' import VueI18n from 'vue-i18n'
Vue.use(VueI18n) Vue.use(VueI18n)
function loadLocaleMessages () { function loadLocaleMessages () {
const locales = require.context('../locales', true, /[A-Za-z0-9-_,\s]+\.json$/i) const locales = require.context('../locales', true, /[A-Za-z0-9-_,\s]+\.json$/i)
const messages = {} const messages = {}
locales.keys().forEach(key => { locales.keys().forEach(key => {
const matched = key.match(/([A-Za-z0-9-_]+)\./i) const matched = key.match(/([A-Za-z0-9-_]+)\./i)
if (matched && matched.length > 1) { if (matched && matched.length > 1) {
const locale = matched[1] const locale = matched[1]
messages[locale] = locales(key) messages[locale] = locales(key)
} }
}) })
return messages return messages
} }
function getBrowserLocale() { function getBrowserLocale () {
const navigatorLocale = navigator.languages !== undefined const navigatorLocale = navigator.languages !== undefined
? navigator.languages[0] ? navigator.languages[0]
: navigator.language : navigator.language
return !navigatorLocale return !navigatorLocale
? process.env.VUE_APP_I18N_LOCALE || 'en' ? process.env.VUE_APP_I18N_LOCALE || 'en'
: navigatorLocale : navigatorLocale
} }
export default new VueI18n({ export default new VueI18n({
locale: getBrowserLocale(), locale: getBrowserLocale(),
fallbackLocale: process.env.VUE_APP_I18N_FALLBACK_LOCALE || 'en', fallbackLocale: process.env.VUE_APP_I18N_FALLBACK_LOCALE || 'en',
// TODO : chunk locales json and lazy load them // TODO : chunk locales json and lazy load them
messages: loadLocaleMessages() messages: loadLocaleMessages()
}) })

View file

@ -3,24 +3,22 @@ import VueRouter from 'vue-router'
import routes from '../routes' import routes from '../routes'
import store from './store' import store from './store'
Vue.use(VueRouter) Vue.use(VueRouter)
const router = new VueRouter({ const router = new VueRouter({
// mode: 'history', // this allow all routes to be real ones (without '#') // mode: 'history', // this allow all routes to be real ones (without '#')
base: process.env.BASE_URL, base: process.env.BASE_URL,
routes routes
}) })
// Before each route request hook // Before each route request hook
router.beforeEach((to, from, next) => { router.beforeEach((to, from, next) => {
// Allow if connected or route is not protected // Allow if connected or route is not protected
if (store.state.connected || to.meta.noAuth) { if (store.state.connected || to.meta.noAuth) {
next() next()
} else { } else {
next({name: 'login', query: {redirect: to.path}}) next({ name: 'login', query: { redirect: to.path } })
} }
}) })
export default router
export default router;

View file

@ -1,31 +1,30 @@
import Vue from 'vue' import Vue from 'vue'
import Vuex from 'vuex' import Vuex from 'vuex'
Vue.use(Vuex) Vue.use(Vuex)
export default new Vuex.Store({ export default new Vuex.Store({
state: { state: {
connected: localStorage.getItem('connected') === 'true', connected: localStorage.getItem('connected') === 'true',
yunohostInfos: null yunohostInfos: null
},
// Mutations must be synchronous. They are used to change the store state.
mutations: {
'CONNECTED' (state, connected) {
localStorage.setItem('connected', connected)
state.connected = connected
if (!connected) {
state.yunohostInfos = null
}
}, },
// Mutations must be synchronous. They are used to change the store state. 'YUNOHOST_INFOS' (state, data) {
mutations: { console.log('version changed', data)
['CONNECTED'] (state, connected) { state.yunohostInfos = data
localStorage.setItem('connected', connected)
state.connected = connected
if (!connected) {
state.yunohostInfos = null
}
},
['YUNOHOST_INFOS'] (state, data) {
console.log('version changed', data);
state.yunohostInfos = data
}
},
// Actions may be asynchronous. They are used to commit mutations.
actions: {
},
modules: {
} }
},
// Actions may be asynchronous. They are used to commit mutations.
actions: {
},
modules: {
}
}) })

View file

@ -3,21 +3,29 @@ import Login from './views/Login'
import Users from './views/Users' import Users from './views/Users'
import User from './views/User' import User from './views/User'
const routes = [ const routes = [
{name: 'home', path: '/', component: Home}, { name: 'home', path: '/', component: Home },
{name: 'login', path: '/login', component: Login, meta: { { name: 'login', path: '/login', component: Login, meta: { noAuth: true } },
noAuth: true {
}}, name: 'users',
{name: 'users', path: '/users', component: Users, meta: { path: '/users',
breadcrumb: [{name: 'users', trad: 'users'}] component: Users,
}}, meta: {
{name: 'user', path: '/user/:name', component: User, props: true, meta: { breadcrumb: [{ name: 'users', trad: 'users' }]
breadcrumb: [ }
{name: 'users', trad: 'users'}, },
{name: 'user', param: 'name'} {
] name: 'user',
}}, path: '/user/:name',
component: User,
props: true,
meta: {
breadcrumb: [
{ name: 'users', trad: 'users' },
{ name: 'user', param: 'name' }
]
}
}
] ]
export default routes export default routes

View file

@ -18,20 +18,20 @@
// Bootstrap overrides // Bootstrap overrides
body { body {
margin: 0; margin: 0;
font-family: "Source Sans Pro", "Helvetica Neue", "Fira Sans", Helvetica, Arial, sans-serif; font-family: "Source Sans Pro", "Helvetica Neue", "Fira Sans", Helvetica, Arial, sans-serif;
} }
.list-group-item { .list-group-item {
padding: 0.75rem 1rem; padding: 0.75rem 1rem;
} }
.list-group-item-action { .list-group-item-action {
color: #333; color: #333;
} }
// Fork-awesome overrides // Fork-awesome overrides
.fa-fw { .fa-fw {
width: 1.25em !important; width: 1.25em !important;
} }

View file

@ -1,32 +1,32 @@
<template> <template>
<div class="home"> <div class="home">
<b-list-group> <b-list-group>
<b-list-group-item v-for="item in menu" :key="item.id" :to="item.uri"> <b-list-group-item v-for="item in menu" :key="item.id" :to="item.uri">
<icon :iname="item.icon" class="lg"/> <icon :iname="item.icon" class="lg" />
<h2>{{ $t(item.translation) }}</h2> <h2>{{ $t(item.translation) }}</h2>
<icon iname="chevron-right" class="lg fs-sm ml-auto"/> <icon iname="chevron-right" class="lg fs-sm ml-auto" />
</b-list-group-item> </b-list-group-item>
</b-list-group> </b-list-group>
</div> </div>
</template> </template>
<script> <script>
export default { export default {
name: 'Home', name: 'Home',
data: () => { data: () => {
return { return {
menu: [ menu: [
{id: 0, uri: '/users', icon: 'users', translation: 'users'}, { id: 0, uri: '/users', icon: 'users', translation: 'users' },
{id: 1, uri: '/domains', icon: 'globe', translation: 'domains'}, { id: 1, uri: '/domains', icon: 'globe', translation: 'domains' },
{id: 2, uri: '/apps', icon: 'cubes', translation: 'applications'}, { id: 2, uri: '/apps', icon: 'cubes', translation: 'applications' },
{id: 3, uri: '/update', icon: 'refresh', translation: 'system_update'}, { id: 3, uri: '/update', icon: 'refresh', translation: 'system_update' },
{id: 4, uri: '/services', icon: 'cog', translation: 'services'}, { id: 4, uri: '/services', icon: 'cog', translation: 'services' },
{id: 5, uri: '/tools', icon: 'wrench', translation: 'tools'}, { id: 5, uri: '/tools', icon: 'wrench', translation: 'tools' },
{id: 6, uri: '/diagnosis', icon: 'stethoscope', translation: 'diagnosis'}, { id: 6, uri: '/diagnosis', icon: 'stethoscope', translation: 'diagnosis' },
{id: 7, uri: '/backup', icon: 'archive', translation: 'backup'}, { id: 7, uri: '/backup', icon: 'archive', translation: 'backup' }
] ]
} }
}, }
} }
</script> </script>

View file

@ -1,58 +1,59 @@
<template> <template>
<b-form @submit.prevent="login"> <b-form @submit.prevent="login">
<!-- TODO add hidden domain input --> <!-- TODO add hidden domain input -->
<b-input-group> <b-input-group>
<template v-slot:prepend> <template v-slot:prepend>
<b-input-group-text> <b-input-group-text>
<label class="sr-only" for="input-password" >{{ $t('password') }}</label> <label class="sr-only" for="input-password">{{ $t('password') }}</label>
<icon iname="lock" class="sm"/> <icon iname="lock" class="sm" />
</b-input-group-text> </b-input-group-text>
</template> </template>
<b-form-input required id="input-password" <b-form-input
v-model="password" id="input-password"
type="password" required type="password"
:placeholder="$t('administration_password')" v-model="password"
:state="isValid" :placeholder="$t('administration_password')" :state="isValid"
></b-form-input> />
<template v-slot:append> <template v-slot:append>
<b-button type="submit" variant="success">{{ $t('login') }}</b-button> <b-button type="submit" variant="success">
</template> {{ $t('login') }}
</b-input-group> </b-button>
<b-form-invalid-feedback :state="isValid"> </template>
{{ $t('wrong_password') }} </b-input-group>
</b-form-invalid-feedback> <b-form-invalid-feedback :state="isValid">
</b-form> {{ $t('wrong_password') }}
</b-form-invalid-feedback>
</b-form>
</template> </template>
<script> <script>
import api from '@/helpers/api' import api from '@/helpers/api'
export default { export default {
name: 'Login', name: 'Login',
data: () => { data: () => {
return { return {
password: '', password: '',
isValid: null, isValid: null
} }
}, },
methods: { methods: {
async login() { async login () {
const connected = await api.login(this.password) const connected = await api.login(this.password)
if (connected) { if (connected) {
this.$store.commit('CONNECTED', true); this.$store.commit('CONNECTED', true)
this.$router.push(this.$route.query.redirect || '/') this.$router.push(this.$route.query.redirect || '/')
const infos = await api.getVersion(); const infos = await api.getVersion()
this.$store.commit('YUNOHOST_INFOS', infos.yunohost) this.$store.commit('YUNOHOST_INFOS', infos.yunohost)
} else { } else {
this.$store.commit('CONNECTED', false); this.$store.commit('CONNECTED', false)
this.isValid = false this.isValid = false
} }
} }
}, }
// TODO checkInstall // TODO checkInstall
// beforeRouteEnter (to, from, next) { // beforeRouteEnter (to, from, next) {
// }, // },
} }
</script> </script>

View file

@ -1,63 +1,73 @@
<template> <template>
<div class="user"> <div class="user">
<breadcrumb/> <breadcrumb />
<b-card :class="{skeleton: !user}"> <b-card :class="{skeleton: !user}">
<template v-slot:header> <template v-slot:header>
<h2>{{ user ? user.fullname : '' }}</h2> <h2>{{ user ? user.fullname : '' }}</h2>
</template> </template>
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<icon iname="user" class="fa-fw"></icon> <icon iname="user" class="fa-fw" />
<div class="w-100"> <div class="w-100">
<template v-if="user"> <template v-if="user">
<b-row> <b-row>
<b-col><strong>{{ $t('user_username') }}</strong></b-col> <b-col><strong>{{ $t('user_username') }}</strong></b-col>
<b-col>{{ user.username }}</b-col> <b-col>{{ user.username }}</b-col>
</b-row> </b-row>
<b-row> <b-row>
<b-col><strong>{{ $t('user_email') }}</strong></b-col> <b-col><strong>{{ $t('user_email') }}</strong></b-col>
<b-col class="font-italic">{{ user.mail }}</b-col> <b-col class="font-italic">
</b-row> {{ user.mail }}
<b-row> </b-col>
<b-col><strong>{{ $t('user_mailbox_quota') }}</strong></b-col> </b-row>
<b-col>{{ user['mailbox-quota'].limit }}</b-col> <b-row>
</b-row> <b-col><strong>{{ $t('user_mailbox_quota') }}</strong></b-col>
<b-row> <b-col>{{ user['mailbox-quota'].limit }}</b-col>
<b-col><strong>{{ $t('user_mailbox_use') }}</strong></b-col> </b-row>
<b-col>{{ user['mailbox-quota'].use }}</b-col> <b-row>
</b-row> <b-col><strong>{{ $t('user_mailbox_use') }}</strong></b-col>
<b-row v-for="(trad, mailType) in {'mail-aliases': 'user_emailaliases', 'mail-forward': 'user_emailforward'}" :key="mailType"> <b-col>{{ user['mailbox-quota'].use }}</b-col>
<b-col><strong>{{ $t(trad) }}</strong></b-col> </b-row>
<b-col> <b-row v-for="(trad, mailType) in {'mail-aliases': 'user_emailaliases', 'mail-forward': 'user_emailforward'}" :key="mailType">
<ul v-if="user[mailType] && user[mailType].length > 1"> <b-col><strong>{{ $t(trad) }}</strong></b-col>
<li v-for="(alias, index) in user[mailType]" :key="index"> <b-col>
{{ alias }} <ul v-if="user[mailType] && user[mailType].length > 1">
</li> <li v-for="(alias, index) in user[mailType]" :key="index">
</ul> {{ alias }}
<template v-else-if="user[mailType]"> </li>
{{ user[mailType] }} </ul>
</template> <template v-else-if="user[mailType]">
</b-col> {{ user[mailType] }}
</b-row> </template>
</template> </b-col>
<!-- skeleton --> </b-row>
<template v-else> </template>
<b-row v-for="(n, index) in 6" :key="index"> <!-- skeleton -->
<b-col><strong class="rounded"></strong></b-col> <template v-else>
<b-col><span v-if="n <= 4" class="rounded"></span></b-col> <b-row v-for="(n, index) in 6" :key="index">
</b-row> <b-col>
</template> <strong class="rounded" />
</div> </b-col>
</div> <b-col>
<template v-slot:footer > <span v-if="n <= 4" class="rounded" />
<div class="d-flex d-flex justify-content-end"> </b-col>
<b-button :to="user ? {name: 'user-edit', params: {user: user}} : null" :variant="user ? 'info' : 'dark'" > </b-row>
{{ user ? $t('user_username_edit', {name: user.username}) : '' }} </template>
</b-button> </div>
<b-button :variant="user ? 'danger' : 'dark'" class="ml-2">{{ user ? $t('delete') : '' }}</b-button> </div>
</div> <template v-slot:footer>
</template> <div class="d-flex d-flex justify-content-end">
</b-card> <b-button :to="user ? {name: 'user-edit', params: {user: user}} : null"
</div> :variant="user ? 'info' : 'dark'"
>
{{ user ? $t('user_username_edit', {name: user.username}) : '' }}
</b-button>
<b-button :variant="user ? 'danger' : 'dark'" class="ml-2">
{{ user ? $t('delete') : '' }}
</b-button>
</div>
</template>
</b-card>
</div>
</template> </template>
<script> <script>
@ -65,21 +75,26 @@ import api from '@/helpers/api'
import Breadcrumb from '@/components/Breadcrumb' import Breadcrumb from '@/components/Breadcrumb'
export default { export default {
name: 'User', name: 'User',
props: ['name'], props: {
data: function () { name: {
return { type: Object,
user: undefined, required: true
}
},
async created() {
const data = await api.get('users/' + this.name)
if (!data) return;
this.user = data
},
components: {
Breadcrumb
} }
},
data: function () {
return {
user: undefined
}
},
async created () {
const data = await api.get('users/' + this.name)
if (!data) return
this.user = data
},
components: {
Breadcrumb
}
} }
</script> </script>
@ -87,68 +102,67 @@ export default {
@import '@/scss/main.scss'; @import '@/scss/main.scss';
.card-body > div { .card-body > div {
flex-direction: column; flex-direction: column;
@include media-breakpoint-up(md) { @include media-breakpoint-up(md) {
flex-direction: row; flex-direction: row;
} }
} }
h2 { h2 {
margin: 0; margin: 0;
} }
.icon.fa-user { .icon.fa-user {
font-size: 10rem; font-size: 10rem;
padding-right: 3rem; padding-right: 3rem;
padding-left: 1.75rem; padding-left: 1.75rem;
} }
.row { .row {
+ .row { + .row {
border-top: 1px solid #eee; border-top: 1px solid #eee;
} }
padding: .5rem; padding: .5rem;
} }
.col { .col {
margin: 0; margin: 0;
} }
ul { ul {
margin: 0; margin: 0;
padding: 0; padding: 0;
li { li {
font-style: italic; font-style: italic;
list-style: none; list-style: none;
} }
} }
.skeleton { .skeleton {
opacity: 0.5; opacity: 0.5;
h2 { h2 {
height: #{2 * 1.2}rem; height: #{2 * 1.2}rem;
}
.col {
& > * {
display: block;
background-color: $skeleton-color;
height: 1.5rem;
max-width: 8rem;
} }
.col { strong {
& > * { max-width: 12rem;
display: block;
background-color: $skeleton-color;
height: 1.5rem;
max-width: 8rem;
}
strong {
max-width: 12rem;
}
} }
}
button { button {
height: calc(2.25rem + 2px); height: calc(2.25rem + 2px);
width: 7rem; width: 7rem;
} }
} }
</style> </style>

View file

@ -1,32 +1,34 @@
<template> <template>
<div class="users"> <div class="users">
<breadcrumb/> <breadcrumb />
<template v-if="users === null"> <template v-if="users === null">
<b-alert variant="warning" show> <b-alert variant="warning" show>
<icon iname="exclamation-triangle" class="fa-fw mr-1"/> <icon iname="exclamation-triangle" class="fa-fw mr-1" />
{{ $t('users_no') }} {{ $t('users_no') }}
</b-alert> </b-alert>
</template> </template>
<template v-else> <template v-else>
<b-list-group :class="{skeleton: !users}"> <b-list-group :class="{skeleton: !users}">
<b-list-group-item <b-list-group-item
class="d-flex justify-content-between align-items-center pr-0" v-for="(user, index) in (users ? users : 3)"
v-for="(user, index) in (users ? users : 3)" :key="index"
:key="index" :to="users ? { name: 'user', params: { name: user.username }} : null"
:to="users ? { name: 'user', params: { name: user.username }} : null" class="d-flex justify-content-between align-items-center pr-0"
> >
<div> <div>
<h5 :class="{rounded: !users}"> <h5 :class="{rounded: !users}">
{{ user.username }} {{ user.username }}
<small>({{ user.fullname }})</small> <small>({{ user.fullname }})</small>
</h5> </h5>
<p :class="{rounded: !users}">{{ user.mail }}</p> <p :class="{rounded: !users}">
</div> {{ user.mail }}
<icon iname="chevron-right" class="lg fs-sm ml-auto"/> </p>
</b-list-group-item> </div>
</b-list-group> <icon iname="chevron-right" class="lg fs-sm ml-auto" />
</template> </b-list-group-item>
</div> </b-list-group>
</template>
</div>
</template> </template>
<script> <script>
@ -34,25 +36,25 @@ import api from '@/helpers/api'
import Breadcrumb from '@/components/Breadcrumb' import Breadcrumb from '@/components/Breadcrumb'
export default { export default {
name: 'Users', name: 'Users',
data: function () { data: function () {
return { return {
users: undefined users: undefined
} }
}, },
computed: { computed: {
}, },
async created() { async created () {
const data = await api.get('users') const data = await api.get('users')
if (!data || Object.keys(data.users).length === 0) { if (!data || Object.keys(data.users).length === 0) {
this.users = null this.users = null
} else { } else {
this.users = data.users this.users = data.users
} }
}, },
components: { components: {
Breadcrumb Breadcrumb
}, }
} }
</script> </script>
@ -60,22 +62,22 @@ export default {
@import '@/scss/_variables.scss'; @import '@/scss/_variables.scss';
p { p {
margin: 0 margin: 0
} }
.skeleton { .skeleton {
@each $i, $opacity in 1 .75, 2 .5, 3 .25 { @each $i, $opacity in 1 .75, 2 .5, 3 .25 {
.list-group-item:nth-child(#{$i}) { opacity: $opacity; } .list-group-item:nth-child(#{$i}) { opacity: $opacity; }
} }
h5, p { h5, p {
background-color: $skeleton-color; background-color: $skeleton-color;
height: 1.5rem; height: 1.5rem;
width: 10rem; width: 10rem;
} }
small { small {
display: none; display: none;
} }
} }
</style> </style>