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>
<div id="app" class="container">
<header>
<b-navbar>
<b-navbar-brand to="/" exact exact-active-class="active"><img alt="Yunohost logo" src="./assets/logo.png"></b-navbar-brand>
<b-navbar-nav class="ml-auto">
<li class="nav-item">
<b-button href="/yunohost/sso" variant="primary" block size="sm">
{{ $t('user_interface_link') }} <icon iname="user"/>
</b-button>
</li>
<li class="nav-item" v-show="connected">
<b-button @click.prevent="logout" to="/logout" variant="outline-dark" block size="sm" >
{{ $t('logout') }} <icon iname="sign-out"/>
</b-button>
</li>
</b-navbar-nav>
</b-navbar>
</header>
<div id="app" class="container">
<header>
<b-navbar>
<b-navbar-brand to="/" exact exact-active-class="active">
<img alt="Yunohost logo" src="./assets/logo.png">
</b-navbar-brand>
<b-navbar-nav class="ml-auto">
<li class="nav-item">
<b-button href="/yunohost/sso"
variant="primary" size="sm" block
>
{{ $t('user_interface_link') }} <icon iname="user" />
</b-button>
</li>
<li class="nav-item" v-show="connected">
<b-button @click.prevent="logout" to="/logout"
variant="outline-dark" block size="sm"
>
{{ $t('logout') }} <icon iname="sign-out" />
</b-button>
</li>
</b-navbar-nav>
</b-navbar>
</header>
<main>
<router-view v-if="isReady"/>
</main>
<main>
<router-view v-if="isReady" />
</main>
<footer>
<nav>
<b-nav>
<b-nav-item href="https://yunohost.org/docs" target="_blank" link-classes='text-secondary'>
<icon iname="book"/> Documentation
</b-nav-item>
<b-nav-item href="https://yunohost.org/help" target="_blank" link-classes='text-secondary'>
<icon iname="life-ring"/> Need help?
</b-nav-item>
<b-nav-item href="https://donate.yunohost.org/" target="_blank" link-classes='text-secondary'>
<icon iname="heart"/> Donate
</b-nav-item>
<i18n v-if="yunohostInfos" path="footer_version" tag="b-nav-text" class="ml-auto" id="yunohost-version">
<template v-slot:ynh><b-link href="https://yunohost.org">YunoHost</b-link></template>
<template v-slot:version>{{ yunohostInfos.version }}</template>
<template v-slot:repo>{{ yunohostInfos.repo }}</template>
</i18n>
</b-nav>
</nav>
</footer>
</div>
<footer>
<nav>
<b-nav>
<b-nav-item href="https://yunohost.org/docs" target="_blank" link-classes="text-secondary">
<icon iname="book" /> Documentation
</b-nav-item>
<b-nav-item href="https://yunohost.org/help" target="_blank" link-classes="text-secondary">
<icon iname="life-ring" /> Need help?
</b-nav-item>
<b-nav-item href="https://donate.yunohost.org/" target="_blank" link-classes="text-secondary">
<icon iname="heart" /> Donate
</b-nav-item>
<i18n v-if="yunohostInfos" path="footer_version" tag="b-nav-text"
id="yunohost-version" class="ml-auto"
>
<template v-slot:ynh>
<b-link href="https://yunohost.org">
YunoHost
</b-link>
</template>
<template v-slot:version>
{{ yunohostInfos.version }}
</template>
<template v-slot:repo>
{{ yunohostInfos.repo }}
</template>
</i18n>
</b-nav>
</nav>
</footer>
</div>
</template>
<script>
@ -50,50 +66,49 @@ import { mapState } from 'vuex'
import api from '@/helpers/api'
export default {
name: 'App',
data: () => {
return {
// isReady blocks the rendering of the rooter-view until we have a true info
// about the connected state of the user.
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;
}
name: 'App',
data: () => {
return {
// isReady blocks the rendering of the rooter-view until we have a true info
// about the connected state of the user.
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
}
}
}
</script>
@ -101,46 +116,46 @@ export default {
@import '@/scss/main.scss';
#app > header {
border-bottom: 1px solid #eee;
padding-top: 1rem;
margin-bottom: 1rem;
border-bottom: 1px solid #eee;
padding-top: 1rem;
margin-bottom: 1rem;
.navbar {
padding: 1rem 0;
.navbar {
padding: 1rem 0;
img {
width: 70px;
}
.navbar-nav {
flex-direction: column;
li {
margin: .2rem 0;
}
icon {
margin-left: .5rem;
}
}
img {
width: 70px;
}
.navbar-nav {
flex-direction: column;
li {
margin: .2rem 0;
}
icon {
margin-left: .5rem;
}
}
}
}
#app > footer {
padding: 1rem 0;
border-top: 1px solid #eee;
font-size: 0.875rem;
margin-top: 2rem;
padding: 1rem 0;
border-top: 1px solid #eee;
font-size: 0.875rem;
margin-top: 2rem;
.nav-item {
& + .nav-item a::before {
content: "•";
width: 1rem;
display: inline-block;
margin-left: -1.15rem;
}
&:first-child {
margin-left: -1rem;
}
.nav-item {
& + .nav-item a::before {
content: "•";
width: 1rem;
display: inline-block;
margin-left: -1.15rem;
}
&:first-child {
margin-left: -1rem;
}
}
}
</style>

View file

@ -1,51 +1,51 @@
<template>
<b-breadcrumb>
<b-breadcrumb-item to="/">
<span class="sr-only">{{ $t('home') }}</span>
<icon iname="home"/>
</b-breadcrumb-item>
<b-breadcrumb-item
v-for="(route, index) in breadcrumb"
:key="index"
:to="{name: route.name}"
:active="index == lastIndex ? true : false"
>
{{ route.text }}
</b-breadcrumb-item>
</b-breadcrumb>
<b-breadcrumb>
<b-breadcrumb-item to="/">
<span class="sr-only">{{ $t('home') }}</span>
<icon iname="home" />
</b-breadcrumb-item>
<b-breadcrumb-item
v-for="(route, index) in breadcrumb"
:key="index"
:to="{name: route.name}"
:active="index == lastIndex ? true : false"
>
{{ route.text }}
</b-breadcrumb-item>
</b-breadcrumb>
</template>
<script>
export default {
computed: {
params: function () {
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
},
computed: {
params: function () {
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
}
}
}
</script>
<style lang="scss" scoped>
.breadcrumb {
border: none;
background-color: transparent !important;
border: none;
background-color: transparent !important;
}
</style>

View file

@ -1,27 +1,29 @@
<template>
<span :class="'icon fa fa-' + iname" aria-hidden="true"></span>
<span :class="'icon fa fa-' + iname" aria-hidden="true" />
</template>
<script>
export default {
name: 'icon',
props: ['iname'],
name: 'Icon',
props: {
iname: { type: String, required: true }
}
}
</script>
<style lang="scss" scoped>
.icon {
font-size: 1rem;
width: 1rem;
text-align: center;
&.lg {
width: 3rem;
font-size: 1.5rem;
}
&.fs-sm {
font-size: 1rem;
width: 1rem;
text-align: center;
&.lg {
width: 3rem;
font-size: 1.5rem;
}
&.fs-sm {
font-size: 1rem;
}
}
}
</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();
for (const [key, value] of Object.entries(object)) {
urlParams.append(key, value)
}
return urlParams
function objectToParams (object) {
const urlParams = new URLSearchParams()
for (const [key, value] of Object.entries(object)) {
urlParams.append(key, value)
}
return urlParams
}
function handleResponse(response, type = 'json') {
return response.ok ? response[type]() : handleErrors(response)
function handleResponse (response, type = 'json') {
return response.ok ? response[type]() : handleErrors(response)
}
function handleErrors(response) {
if (response.status == 401) {
throw new Error('Unauthorized');
}
function handleErrors (response) {
if (response.status === 401) {
throw new Error('Unauthorized')
}
}
export default {
options: {
credentials: 'include',
mode: 'cors',
headers: {
// FIXME is it important to keep this previous `Accept` header ?
// 'Accept': 'application/json, text/javascript, */*; q=0.01',
// Auto header is :
// "Accept": "*/*",
options: {
credentials: 'include',
mode: 'cors',
headers: {
// FIXME is it important to keep this previous `Accept` header ?
// 'Accept': 'application/json, text/javascript, */*; q=0.01',
// Auto header is :
// "Accept": "*/*",
// Also is this still important ? (needed by back-end)
'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))
// Also is this still important ? (needed by back-end)
'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))
}
}

View file

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

View file

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

View file

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

View file

@ -1,31 +1,30 @@
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
connected: localStorage.getItem('connected') === 'true',
yunohostInfos: null
state: {
connected: localStorage.getItem('connected') === 'true',
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.
mutations: {
['CONNECTED'] (state, connected) {
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: {
'YUNOHOST_INFOS' (state, data) {
console.log('version changed', data)
state.yunohostInfos = data
}
},
// 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 User from './views/User'
const routes = [
{name: 'home', path: '/', component: Home},
{name: 'login', path: '/login', component: Login, meta: {
noAuth: true
}},
{name: 'users', path: '/users', component: Users, meta: {
breadcrumb: [{name: 'users', trad: 'users'}]
}},
{name: 'user', path: '/user/:name', component: User, props: true, meta: {
breadcrumb: [
{name: 'users', trad: 'users'},
{name: 'user', param: 'name'}
]
}},
{ name: 'home', path: '/', component: Home },
{ name: 'login', path: '/login', component: Login, meta: { noAuth: true } },
{
name: 'users',
path: '/users',
component: Users,
meta: {
breadcrumb: [{ name: 'users', trad: 'users' }]
}
},
{
name: 'user',
path: '/user/:name',
component: User,
props: true,
meta: {
breadcrumb: [
{ name: 'users', trad: 'users' },
{ name: 'user', param: 'name' }
]
}
}
]
export default routes

View file

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

View file

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

View file

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

View file

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

View file

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