mirror of https://github.com/requarks/wiki
parent
05797652a0
commit
3ebbbc8b5e
@ -1,335 +0,0 @@
|
||||
<template lang='pug'>
|
||||
v-app.admin
|
||||
nav-header(hide-search)
|
||||
template(slot='mid')
|
||||
v-spacer
|
||||
.overline.grey--text {{$t('admin:adminArea')}}
|
||||
v-spacer
|
||||
v-navigation-drawer.pb-0.admin-sidebar(v-model='adminDrawerShown', app, fixed, clipped, :right='$vuetify.rtl', permanent, width='300', :class='$vuetify.theme.dark ? `grey darken-4` : ``')
|
||||
vue-scroll(:ops='scrollStyle')
|
||||
v-list.radius-0(dense, nav)
|
||||
v-list-item(to='/dashboard', color='primary')
|
||||
v-list-item-avatar(size='24', tile): v-icon mdi-view-dashboard-variant
|
||||
v-list-item-title {{ $t('admin:dashboard.title') }}
|
||||
template(v-if='hasPermission([`manage:system`, `manage:navigation`, `write:pages`, `manage:pages`, `delete:pages`])')
|
||||
v-divider.my-2
|
||||
v-subheader.pl-4 {{ $t('admin:nav.site') }}
|
||||
v-list-item(to='/general', color='primary', v-if='hasPermission(`manage:system`)')
|
||||
v-list-item-avatar(size='24', tile): v-icon mdi-widgets
|
||||
v-list-item-title {{ $t('admin:general.title') }}
|
||||
v-list-item(to='/locale', color='primary', v-if='hasPermission(`manage:system`)')
|
||||
v-list-item-avatar(size='24', tile): v-icon mdi-web
|
||||
v-list-item-title {{ $t('admin:locale.title') }}
|
||||
v-list-item(to='/navigation', color='primary', v-if='hasPermission([`manage:system`, `manage:navigation`])')
|
||||
v-list-item-avatar(size='24', tile): v-icon mdi-near-me
|
||||
v-list-item-title {{ $t('admin:navigation.title') }}
|
||||
v-list-item(to='/pages', color='primary', v-if='hasPermission([`manage:system`, `write:pages`, `manage:pages`, `delete:pages`])')
|
||||
v-list-item-avatar(size='24', tile): v-icon mdi-file-document-outline
|
||||
v-list-item-title {{ $t('admin:pages.title') }}
|
||||
v-list-item-action(style='min-width:auto;')
|
||||
v-chip(x-small, :color='$vuetify.theme.dark ? `grey darken-3-d4` : `grey lighten-5`')
|
||||
.caption.grey--text {{ info.pagesTotal }}
|
||||
v-list-item(to='/tags', v-if='hasPermission([`manage:system`])')
|
||||
v-list-item-avatar(size='24', tile): v-icon mdi-tag-multiple
|
||||
v-list-item-title {{ $t('admin:tags.title') }}
|
||||
v-list-item-action(style='min-width:auto;')
|
||||
v-chip(x-small, :color='$vuetify.theme.dark ? `grey darken-3-d4` : `grey lighten-5`')
|
||||
.caption.grey--text {{ info.tagsTotal }}
|
||||
v-list-item(to='/theme', color='primary', v-if='hasPermission([`manage:system`, `manage:theme`])')
|
||||
v-list-item-avatar(size='24', tile): v-icon mdi-palette-outline
|
||||
v-list-item-title {{ $t('admin:theme.title') }}
|
||||
template(v-if='hasPermission([`manage:system`, `manage:groups`, `write:groups`, `manage:users`, `write:users`])')
|
||||
v-divider.my-2
|
||||
v-subheader.pl-4 {{ $t('admin:nav.users') }}
|
||||
v-list-item(to='/groups', color='primary', v-if='hasPermission([`manage:system`, `manage:groups`, `write:groups`])')
|
||||
v-list-item-avatar(size='24', tile): v-icon mdi-account-group
|
||||
v-list-item-title {{ $t('admin:groups.title') }}
|
||||
v-list-item-action(style='min-width:auto;')
|
||||
v-chip(x-small, :color='$vuetify.theme.dark ? `grey darken-3-d4` : `grey lighten-4`')
|
||||
.caption.grey--text {{ info.groupsTotal }}
|
||||
v-list-item(to='/users', color='primary', v-if='hasPermission([`manage:system`, `manage:groups`, `write:groups`, `manage:users`, `write:users`])')
|
||||
v-list-item-avatar(size='24', tile): v-icon mdi-account-box
|
||||
v-list-item-title {{ $t('admin:users.title') }}
|
||||
v-list-item-action(style='min-width:auto;')
|
||||
v-chip(x-small, :color='$vuetify.theme.dark ? `grey darken-3-d4` : `grey lighten-4`')
|
||||
.caption.grey--text {{ info.usersTotal }}
|
||||
template(v-if='hasPermission(`manage:system`)')
|
||||
v-divider.my-2
|
||||
v-subheader.pl-4 {{ $t('admin:nav.modules') }}
|
||||
v-list-item(to='/analytics', color='primary')
|
||||
v-list-item-avatar(size='24', tile): v-icon mdi-chart-timeline-variant
|
||||
v-list-item-title {{ $t('admin:analytics.title') }}
|
||||
v-list-item(to='/auth', color='primary')
|
||||
v-list-item-avatar(size='24', tile): v-icon mdi-lock-outline
|
||||
v-list-item-title {{ $t('admin:auth.title') }}
|
||||
v-list-item(to='/comments')
|
||||
v-list-item-avatar(size='24', tile): v-icon mdi-comment-text-outline
|
||||
v-list-item-title {{ $t('admin:comments.title') }}
|
||||
v-list-item(to='/editor', disabled)
|
||||
v-list-item-avatar(size='24', tile): v-icon(color='grey lighten-2') mdi-playlist-edit
|
||||
v-list-item-title {{ $t('admin:editor.title') }}
|
||||
v-list-item(to='/extensions')
|
||||
v-list-item-avatar(size='24', tile): v-icon mdi-chip
|
||||
v-list-item-title {{ $t('admin:extensions.title') }}
|
||||
v-list-item(to='/logging', disabled)
|
||||
v-list-item-avatar(size='24', tile): v-icon(color='grey lighten-2') mdi-script-text-outline
|
||||
v-list-item-title {{ $t('admin:logging.title') }}
|
||||
v-list-item(to='/rendering', color='primary')
|
||||
v-list-item-avatar(size='24', tile): v-icon mdi-cogs
|
||||
v-list-item-title {{ $t('admin:rendering.title') }}
|
||||
v-list-item(to='/search', color='primary')
|
||||
v-list-item-avatar(size='24', tile): v-icon mdi-cloud-search-outline
|
||||
v-list-item-title {{ $t('admin:search.title') }}
|
||||
v-list-item(to='/storage', color='primary')
|
||||
v-list-item-avatar(size='24', tile): v-icon mdi-harddisk
|
||||
v-list-item-title {{ $t('admin:storage.title') }}
|
||||
template(v-if='hasPermission([`manage:system`, `manage:api`])')
|
||||
v-divider.my-2
|
||||
v-subheader.pl-4 {{ $t('admin:nav.system') }}
|
||||
v-list-item(to='/api', v-if='hasPermission([`manage:system`, `manage:api`])')
|
||||
v-list-item-avatar(size='24', tile): v-icon mdi-call-split
|
||||
v-list-item-title {{ $t('admin:api.title') }}
|
||||
v-list-item(to='/mail', color='primary', v-if='hasPermission(`manage:system`)')
|
||||
v-list-item-avatar(size='24', tile): v-icon mdi-email-multiple-outline
|
||||
v-list-item-title {{ $t('admin:mail.title') }}
|
||||
v-list-item(to='/security', v-if='hasPermission(`manage:system`)')
|
||||
v-list-item-avatar(size='24', tile): v-icon mdi-lock-check
|
||||
v-list-item-title {{ $t('admin:security.title') }}
|
||||
v-list-item(to='/ssl', v-if='hasPermission(`manage:system`)')
|
||||
v-list-item-avatar(size='24', tile): v-icon mdi-cloud-lock-outline
|
||||
v-list-item-title {{ $t('admin:ssl.title') }}
|
||||
v-list-item(to='/system', color='primary', v-if='hasPermission(`manage:system`)')
|
||||
v-list-item-avatar(size='24', tile): v-icon mdi-tune
|
||||
v-list-item-title {{ $t('admin:system.title') }}
|
||||
v-list-item(to='/utilities', color='primary', v-if='hasPermission(`manage:system`)')
|
||||
v-list-item-avatar(size='24', tile): v-icon mdi-wrench-outline
|
||||
v-list-item-title {{ $t('admin:utilities.title') }}
|
||||
v-list-item(to='/webhooks', v-if='hasPermission(`manage:system`)', disabled)
|
||||
v-list-item-avatar(size='24', tile): v-icon(color='grey lighten-2') mdi-webhook
|
||||
v-list-item-title {{ $t('admin:webhooks.title') }}
|
||||
v-list-group(
|
||||
to='/dev'
|
||||
no-action
|
||||
v-if='hasPermission([`manage:system`, `manage:api`])'
|
||||
)
|
||||
v-list-item(slot='activator')
|
||||
v-list-item-avatar(size='24', tile): v-icon mdi-dev-to
|
||||
v-list-item-title {{ $t('admin:dev.title') }}
|
||||
|
||||
v-list-item(to='/dev-flags', color='primary')
|
||||
v-list-item-title {{ $t('admin:dev.flags.title') }}
|
||||
v-list-item(href='/graphql', color='primary')
|
||||
v-list-item-title GraphQL
|
||||
//- v-list-item(to='/dev-graphiql')
|
||||
//- v-list-item-title {{ $t('admin:dev.graphiql.title') }}
|
||||
//- v-list-item(to='/dev-voyager')
|
||||
//- v-list-item-title {{ $t('admin:dev.voyager.title') }}
|
||||
v-divider.my-2
|
||||
v-list-item(to='/contribute', color='primary')
|
||||
v-list-item-avatar(size='24', tile): v-icon mdi-heart-outline
|
||||
v-list-item-title {{ $t('admin:contribute.title') }}
|
||||
|
||||
v-main(:class='$vuetify.theme.dark ? "grey darken-5" : "grey lighten-5"')
|
||||
transition(name='admin-router')
|
||||
router-view
|
||||
|
||||
nav-footer
|
||||
notify
|
||||
search-results
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import VueRouter from 'vue-router'
|
||||
import { get, sync } from 'vuex-pathify'
|
||||
|
||||
import statsQuery from 'gql/admin/dashboard/dashboard-query-stats.gql'
|
||||
|
||||
import adminStore from '../store/admin'
|
||||
|
||||
/* global WIKI */
|
||||
|
||||
WIKI.$store.registerModule('admin', adminStore)
|
||||
|
||||
const router = new VueRouter({
|
||||
mode: 'history',
|
||||
base: '/a',
|
||||
routes: [
|
||||
{ path: '/', redirect: '/dashboard' },
|
||||
{ path: '/dashboard', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-dashboard.vue') },
|
||||
{ path: '/general', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-general.vue') },
|
||||
{ path: '/locale', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-locale.vue') },
|
||||
{ path: '/navigation', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-navigation.vue') },
|
||||
{ path: '/pages', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-pages.vue') },
|
||||
{ path: '/pages/:id(\\d+)', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-pages-edit.vue') },
|
||||
{ path: '/pages/visualize', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-pages-visualize.vue') },
|
||||
{ path: '/tags', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-tags.vue') },
|
||||
{ path: '/theme', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-theme.vue') },
|
||||
{ path: '/groups', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-groups.vue') },
|
||||
{ path: '/groups/:id(\\d+)', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-groups-edit.vue') },
|
||||
{ path: '/users', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-users.vue') },
|
||||
{ path: '/users/:id(\\d+)', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-users-edit.vue') },
|
||||
{ path: '/analytics', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-analytics.vue') },
|
||||
{ path: '/auth', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-auth.vue') },
|
||||
{ path: '/comments', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-comments.vue') },
|
||||
{ path: '/rendering', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-rendering.vue') },
|
||||
{ path: '/editor', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-editor.vue') },
|
||||
{ path: '/extensions', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-extensions.vue') },
|
||||
{ path: '/logging', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-logging.vue') },
|
||||
{ path: '/search', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-search.vue') },
|
||||
{ path: '/storage', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-storage.vue') },
|
||||
{ path: '/api', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-api.vue') },
|
||||
{ path: '/mail', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-mail.vue') },
|
||||
{ path: '/security', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-security.vue') },
|
||||
{ path: '/ssl', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-ssl.vue') },
|
||||
{ path: '/system', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-system.vue') },
|
||||
{ path: '/utilities', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-utilities.vue') },
|
||||
{ path: '/webhooks', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-webhooks.vue') },
|
||||
{ path: '/dev-flags', component: () => import(/* webpackChunkName: "admin-dev" */ './admin/admin-dev-flags.vue') },
|
||||
{ path: '/contribute', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-contribute.vue') }
|
||||
]
|
||||
})
|
||||
|
||||
export default {
|
||||
i18nOptions: { namespaces: 'admin' },
|
||||
data() {
|
||||
return {
|
||||
adminDrawerShown: true,
|
||||
scrollStyle: {
|
||||
vuescroll: {},
|
||||
scrollPanel: {
|
||||
initialScrollY: 0,
|
||||
initialScrollX: 0,
|
||||
scrollingX: false,
|
||||
easing: 'easeOutQuad',
|
||||
speed: 1000,
|
||||
verticalNativeBarPos: this.$vuetify.rtl ? `left` : `right`
|
||||
},
|
||||
rail: {
|
||||
gutterOfEnds: '2px'
|
||||
},
|
||||
bar: {
|
||||
onlyShowBarOnScroll: false,
|
||||
background: '#CCC',
|
||||
hoverStyle: {
|
||||
background: '#999'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
info: sync('admin/info'),
|
||||
permissions: get('user/permissions')
|
||||
},
|
||||
router,
|
||||
created() {
|
||||
this.$store.commit('page/SET_MODE', 'admin')
|
||||
},
|
||||
methods: {
|
||||
hasPermission(prm) {
|
||||
if (_.isArray(prm)) {
|
||||
return _.some(prm, p => {
|
||||
return _.includes(this.permissions, p)
|
||||
})
|
||||
} else {
|
||||
return _.includes(this.permissions, prm)
|
||||
}
|
||||
}
|
||||
},
|
||||
apollo: {
|
||||
info: {
|
||||
query: statsQuery,
|
||||
fetchPolicy: 'network-only',
|
||||
manual: true,
|
||||
result({ data, loading, networkStatus }) {
|
||||
this.info = data.system.info
|
||||
},
|
||||
watchLoading (isLoading) {
|
||||
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-stats-refresh')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang='scss'>
|
||||
|
||||
.admin {
|
||||
&.theme--light .application--wrap {
|
||||
background-color: lighten(mc('grey', '200'), 2%);
|
||||
}
|
||||
}
|
||||
|
||||
.admin-router {
|
||||
&-enter-active, &-leave-active {
|
||||
transition: opacity .25s ease;
|
||||
opacity: 1;
|
||||
}
|
||||
&-enter-active {
|
||||
transition-delay: .25s;
|
||||
}
|
||||
&-enter, &-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.admin-sidebar {
|
||||
.v-list__tile--active {
|
||||
background-color: rgba(mc('theme', 'primary'), .1);
|
||||
|
||||
.v-icon {
|
||||
color: mc('theme', 'primary');
|
||||
}
|
||||
}
|
||||
|
||||
.v-list-group > .v-list-item {
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.theme--dark {
|
||||
.admin-sidebar .v-list__tile--active {
|
||||
background-color: rgba(0,0,0, .2);
|
||||
color: mc('blue', '500') !important;
|
||||
|
||||
.v-icon {
|
||||
color: mc('blue', '500');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.admin-header {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
|
||||
&-title {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.admin-providerlogo {
|
||||
width: 250px;
|
||||
height: 50px;
|
||||
float: right;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
margin-left: 16px;
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
max-height: 50px;
|
||||
}
|
||||
}
|
||||
|
||||
.v-application.admin {
|
||||
code {
|
||||
box-shadow: none;
|
||||
font-family: 'Roboto Mono', monospace;
|
||||
color: mc('pink', '500');
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
@ -1,181 +0,0 @@
|
||||
<template lang='pug'>
|
||||
v-container(fluid, grid-list-lg)
|
||||
v-layout(row, wrap)
|
||||
v-flex(xs12)
|
||||
.admin-header
|
||||
img.animated.fadeInUp(src='/_assets/svg/icon-line-chart.svg', alt='Analytics', style='width: 80px;')
|
||||
.admin-header-title
|
||||
.headline.primary--text.animated.fadeInLeft {{ $t('admin:analytics.title') }}
|
||||
.subtitle-1.grey--text.animated.fadeInLeft.wait-p4s {{ $t('admin:analytics.subtitle') }}
|
||||
v-spacer
|
||||
v-btn.animated.fadeInDown.wait-p2s.mr-3(icon, outlined, color='grey', @click='refresh')
|
||||
v-icon mdi-refresh
|
||||
v-btn.animated.fadeInDown(color='success', @click='save', depressed, large)
|
||||
v-icon(left) mdi-check
|
||||
span {{$t('common:actions.apply')}}
|
||||
|
||||
v-flex(lg3, xs12)
|
||||
v-card.animated.fadeInUp
|
||||
v-toolbar(flat, color='primary', dark, dense)
|
||||
.subtitle-1 {{$t('admin:analytics.providers')}}
|
||||
v-list(two-line, dense).py-0
|
||||
template(v-for='(str, idx) in providers')
|
||||
v-list-item(:key='str.key', @click='selectedProvider = str.key', :disabled='!str.isAvailable')
|
||||
v-list-item-avatar(size='24')
|
||||
v-icon(color='grey', v-if='!str.isAvailable') mdi-minus-box-outline
|
||||
v-icon(color='primary', v-else-if='str.isEnabled', v-ripple, @click='str.isEnabled = false') mdi-checkbox-marked-outline
|
||||
v-icon(color='grey', v-else, v-ripple, @click='str.isEnabled = true') mdi-checkbox-blank-outline
|
||||
v-list-item-content
|
||||
v-list-item-title.body-2(:class='!str.isAvailable ? `grey--text` : (selectedProvider === str.key ? `primary--text` : ``)') {{ str.title }}
|
||||
v-list-item-subtitle: .caption(:class='!str.isAvailable ? `grey--text text--lighten-1` : (selectedProvider === str.key ? `blue--text ` : ``)') {{ str.description }}
|
||||
v-list-item-avatar(v-if='selectedProvider === str.key', size='24')
|
||||
v-icon.animated.fadeInLeft(color='primary', large) mdi-chevron-right
|
||||
v-divider(v-if='idx < providers.length - 1')
|
||||
|
||||
v-flex(xs12, lg9)
|
||||
|
||||
v-card.animated.fadeInUp.wait-p2s
|
||||
v-toolbar(color='primary', dense, flat, dark)
|
||||
.subtitle-1 {{provider.title}}
|
||||
v-spacer
|
||||
v-switch(
|
||||
dark
|
||||
color='blue lighten-5'
|
||||
label='Active'
|
||||
v-model='provider.isEnabled'
|
||||
hide-details
|
||||
inset
|
||||
)
|
||||
v-card-info(color='blue')
|
||||
div
|
||||
div {{provider.description}}
|
||||
span.caption: a(:href='provider.website') {{provider.website}}
|
||||
v-spacer
|
||||
.admin-providerlogo
|
||||
img(:src='provider.logo', :alt='provider.title')
|
||||
v-card-text
|
||||
v-form
|
||||
.overline.pb-5 {{$t('admin:analytics.providerConfiguration')}}
|
||||
.body-1.ml-3(v-if='!provider.config || provider.config.length < 1'): em {{$t('admin:analytics.providerNoConfiguration')}}
|
||||
template(v-else, v-for='cfg in provider.config')
|
||||
v-select(
|
||||
v-if='cfg.value.type === "string" && cfg.value.enum'
|
||||
outlined
|
||||
:items='cfg.value.enum'
|
||||
:key='cfg.key'
|
||||
:label='cfg.value.title'
|
||||
v-model='cfg.value.value'
|
||||
prepend-icon='mdi-cog-box'
|
||||
:hint='cfg.value.hint ? cfg.value.hint : ""'
|
||||
persistent-hint
|
||||
:class='cfg.value.hint ? "mb-2" : ""'
|
||||
)
|
||||
v-switch.mb-3(
|
||||
v-else-if='cfg.value.type === "boolean"'
|
||||
:key='cfg.key'
|
||||
:label='cfg.value.title'
|
||||
v-model='cfg.value.value'
|
||||
color='primary'
|
||||
prepend-icon='mdi-cog-box'
|
||||
:hint='cfg.value.hint ? cfg.value.hint : ""'
|
||||
persistent-hint
|
||||
inset
|
||||
)
|
||||
v-textarea(
|
||||
v-else-if='cfg.value.type === "string" && cfg.value.multiline'
|
||||
outlined
|
||||
:key='cfg.key'
|
||||
:label='cfg.value.title'
|
||||
v-model='cfg.value.value'
|
||||
prepend-icon='mdi-cog-box'
|
||||
:hint='cfg.value.hint ? cfg.value.hint : ""'
|
||||
persistent-hint
|
||||
:class='cfg.value.hint ? "mb-2" : ""'
|
||||
)
|
||||
v-text-field(
|
||||
v-else
|
||||
outlined
|
||||
:key='cfg.key'
|
||||
:label='cfg.value.title'
|
||||
v-model='cfg.value.value'
|
||||
prepend-icon='mdi-cog-box'
|
||||
:hint='cfg.value.hint ? cfg.value.hint : ""'
|
||||
persistent-hint
|
||||
:class='cfg.value.hint ? "mb-2" : ""'
|
||||
)
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
|
||||
import providersQuery from 'gql/admin/analytics/analytics-query-providers.gql'
|
||||
import providersSaveMutation from 'gql/admin/analytics/analytics-mutation-save-providers.gql'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
providers: [],
|
||||
selectedProvider: '',
|
||||
provider: {}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
selectedProvider(newValue, oldValue) {
|
||||
this.provider = _.find(this.providers, ['key', newValue]) || {}
|
||||
},
|
||||
providers(newValue, oldValue) {
|
||||
this.selectedProvider = 'google'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async refresh() {
|
||||
await this.$apollo.queries.providers.refetch()
|
||||
this.$store.commit('showNotification', {
|
||||
message: this.$t('admin:analytics.refreshSuccess'),
|
||||
style: 'success',
|
||||
icon: 'cached'
|
||||
})
|
||||
},
|
||||
async save() {
|
||||
this.$store.commit(`loadingStart`, 'admin-analytics-saveproviders')
|
||||
try {
|
||||
await this.$apollo.mutate({
|
||||
mutation: providersSaveMutation,
|
||||
variables: {
|
||||
providers: this.providers.map(str => _.pick(str, [
|
||||
'isEnabled',
|
||||
'key',
|
||||
'config'
|
||||
])).map(str => ({...str, config: str.config.map(cfg => ({...cfg, value: JSON.stringify({ v: cfg.value.value })}))}))
|
||||
}
|
||||
})
|
||||
this.$store.commit('showNotification', {
|
||||
message: this.$t('admin:analytics.saveSuccess'),
|
||||
style: 'success',
|
||||
icon: 'check'
|
||||
})
|
||||
} catch (err) {
|
||||
this.$store.commit('pushGraphError', err)
|
||||
}
|
||||
this.$store.commit(`loadingStop`, 'admin-analytics-saveproviders')
|
||||
}
|
||||
},
|
||||
apollo: {
|
||||
providers: {
|
||||
query: providersQuery,
|
||||
fetchPolicy: 'network-only',
|
||||
update: (data) => _.cloneDeep(data.analytics.providers).map(str => ({
|
||||
...str,
|
||||
config: _.sortBy(str.config.map(cfg => ({
|
||||
...cfg,
|
||||
value: JSON.parse(cfg.value)
|
||||
})), [t => t.value.order])
|
||||
})),
|
||||
watchLoading (isLoading) {
|
||||
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-analytics-refresh')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
@ -1,236 +0,0 @@
|
||||
<template lang="pug">
|
||||
div
|
||||
v-dialog(v-model='isShown', max-width='650', persistent)
|
||||
v-card
|
||||
.dialog-header.is-short
|
||||
v-icon.mr-3(color='white') mdi-plus
|
||||
span {{$t('admin:api.newKeyTitle')}}
|
||||
v-card-text.pt-5
|
||||
v-text-field(
|
||||
outlined
|
||||
prepend-icon='mdi-format-title'
|
||||
v-model='name'
|
||||
:label='$t(`admin:api.newKeyName`)'
|
||||
persistent-hint
|
||||
ref='keyNameInput'
|
||||
:hint='$t(`admin:api.newKeyNameHint`)'
|
||||
counter='255'
|
||||
)
|
||||
v-select.mt-3(
|
||||
:items='expirations'
|
||||
outlined
|
||||
prepend-icon='mdi-clock'
|
||||
v-model='expiration'
|
||||
:label='$t(`admin:api.newKeyExpiration`)'
|
||||
:hint='$t(`admin:api.newKeyExpirationHint`)'
|
||||
persistent-hint
|
||||
)
|
||||
v-divider.mt-4
|
||||
v-subheader.pl-2: strong.indigo--text {{$t('admin:api.newKeyPermissionScopes')}}
|
||||
v-list.pl-8(nav)
|
||||
v-list-item-group(v-model='fullAccess')
|
||||
v-list-item(
|
||||
:value='true'
|
||||
active-class='indigo--text'
|
||||
)
|
||||
template(v-slot:default='{ active, toggle }')
|
||||
v-list-item-action
|
||||
v-checkbox(
|
||||
:input-value='active'
|
||||
:true-value='true'
|
||||
color='indigo'
|
||||
@click='toggle'
|
||||
)
|
||||
v-list-item-content
|
||||
v-list-item-title {{$t('admin:api.newKeyFullAccess')}}
|
||||
v-divider.mt-3
|
||||
v-subheader.caption.indigo--text {{$t('admin:api.newKeyGroupPermissions')}}
|
||||
v-list-item
|
||||
v-select(
|
||||
:disabled='fullAccess'
|
||||
:items='groups'
|
||||
item-text='name'
|
||||
item-value='id'
|
||||
outlined
|
||||
color='indigo'
|
||||
v-model='group'
|
||||
:label='$t(`admin:api.newKeyGroup`)'
|
||||
:hint='$t(`admin:api.newKeyGroupHint`)'
|
||||
persistent-hint
|
||||
)
|
||||
v-card-chin
|
||||
v-spacer
|
||||
v-btn(text, @click='isShown = false', :disabled='loading') {{$t('common:actions.cancel')}}
|
||||
v-btn.px-3(depressed, color='primary', @click='generate', :loading='loading')
|
||||
v-icon(left) mdi-chevron-right
|
||||
span {{$t('common:actions.generate')}}
|
||||
|
||||
v-dialog(
|
||||
v-model='isCopyKeyDialogShown'
|
||||
max-width='750'
|
||||
persistent
|
||||
overlay-color='blue darken-5'
|
||||
overlay-opacity='.9'
|
||||
)
|
||||
v-card
|
||||
v-toolbar(dense, flat, color='primary', dark) {{$t('admin:api.newKeyTitle')}}
|
||||
v-card-text.pt-5
|
||||
.body-2.text-center
|
||||
i18next(tag='span', path='admin:api.newKeyCopyWarn')
|
||||
strong(place='bold') {{$t('admin:api.newKeyCopyWarnBold')}}
|
||||
v-textarea.mt-3(
|
||||
ref='keyContentsIpt'
|
||||
filled
|
||||
no-resize
|
||||
readonly
|
||||
v-model='key'
|
||||
:rows='10'
|
||||
hide-details
|
||||
)
|
||||
v-card-chin
|
||||
v-spacer
|
||||
v-btn.px-3(depressed, dark, color='primary', @click='isCopyKeyDialogShown = false') {{$t('common:actions.close')}}
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
import groupsQuery from 'gql/admin/users/users-query-groups.gql'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
value: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
name: '',
|
||||
expiration: '1y',
|
||||
fullAccess: true,
|
||||
groups: [],
|
||||
group: null,
|
||||
isCopyKeyDialogShown: false,
|
||||
key: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isShown: {
|
||||
get() { return this.value },
|
||||
set(val) { this.$emit('input', val) }
|
||||
},
|
||||
expirations() {
|
||||
return [
|
||||
{ value: '30d', text: this.$t('admin:api.expiration30d') },
|
||||
{ value: '90d', text: this.$t('admin:api.expiration90d') },
|
||||
{ value: '180d', text: this.$t('admin:api.expiration180d') },
|
||||
{ value: '1y', text: this.$t('admin:api.expiration1y') },
|
||||
{ value: '3y', text: this.$t('admin:api.expiration3y') }
|
||||
]
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value (newValue, oldValue) {
|
||||
if (newValue) {
|
||||
setTimeout(() => {
|
||||
this.$refs.keyNameInput.focus()
|
||||
}, 400)
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async generate () {
|
||||
try {
|
||||
if (_.trim(this.name).length < 2 || this.name.length > 255) {
|
||||
throw new Error(this.$t('admin:api.newKeyNameError'))
|
||||
} else if (!this.fullAccess && !this.group) {
|
||||
throw new Error(this.$t('admin:api.newKeyGroupError'))
|
||||
} else if (!this.fullAccess && this.group === 2) {
|
||||
throw new Error(this.$t('admin:api.newKeyGuestGroupError'))
|
||||
}
|
||||
} catch (err) {
|
||||
return this.$store.commit('showNotification', {
|
||||
style: 'red',
|
||||
message: err,
|
||||
icon: 'alert'
|
||||
})
|
||||
}
|
||||
|
||||
this.loading = true
|
||||
|
||||
try {
|
||||
const resp = await this.$apollo.mutate({
|
||||
mutation: gql`
|
||||
mutation ($name: String!, $expiration: String!, $fullAccess: Boolean!, $group: Int) {
|
||||
authentication {
|
||||
createApiKey (name: $name, expiration: $expiration, fullAccess: $fullAccess, group: $group) {
|
||||
key
|
||||
responseResult {
|
||||
succeeded
|
||||
errorCode
|
||||
slug
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
name: this.name,
|
||||
expiration: this.expiration,
|
||||
fullAccess: (this.fullAccess === true),
|
||||
group: this.group
|
||||
},
|
||||
watchLoading (isLoading) {
|
||||
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-api-create')
|
||||
}
|
||||
})
|
||||
if (_.get(resp, 'data.authentication.createApiKey.responseResult.succeeded', false)) {
|
||||
this.$store.commit('showNotification', {
|
||||
style: 'success',
|
||||
message: this.$t('admin:api.newKeySuccess'),
|
||||
icon: 'check'
|
||||
})
|
||||
|
||||
this.name = ''
|
||||
this.expiration = '1y'
|
||||
this.fullAccess = true
|
||||
this.group = null
|
||||
this.isShown = false
|
||||
this.$emit('refresh')
|
||||
|
||||
this.key = _.get(resp, 'data.authentication.createApiKey.key', '???')
|
||||
this.isCopyKeyDialogShown = true
|
||||
|
||||
setTimeout(() => {
|
||||
this.$refs.keyContentsIpt.$refs.input.select()
|
||||
}, 400)
|
||||
} else {
|
||||
this.$store.commit('showNotification', {
|
||||
style: 'red',
|
||||
message: _.get(resp, 'data.authentication.createApiKey.responseResult.message', 'An unexpected error occurred.'),
|
||||
icon: 'alert'
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
this.$store.commit('pushGraphError', err)
|
||||
}
|
||||
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
apollo: {
|
||||
groups: {
|
||||
query: groupsQuery,
|
||||
fetchPolicy: 'network-only',
|
||||
update: (data) => data.groups.list,
|
||||
watchLoading (isLoading) {
|
||||
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-api-groups-refresh')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
@ -1,239 +0,0 @@
|
||||
<template lang='pug'>
|
||||
v-container(fluid, grid-list-lg)
|
||||
v-layout(row, wrap)
|
||||
v-flex(xs12)
|
||||
.admin-header
|
||||
img.animated.fadeInUp(src='/_assets/svg/icon-rest-api.svg', alt='API', style='width: 80px;')
|
||||
.admin-header-title
|
||||
.headline.primary--text.animated.fadeInLeft {{$t('admin:api.title')}}
|
||||
.subtitle-1.grey--text.animated.fadeInLeft {{$t('admin:api.subtitle')}}
|
||||
v-spacer
|
||||
template(v-if='enabled')
|
||||
status-indicator.mr-3(positive, pulse)
|
||||
.caption.green--text.animated.fadeInLeft {{$t('admin:api.enabled')}}
|
||||
template(v-else)
|
||||
status-indicator.mr-3(negative, pulse)
|
||||
.caption.red--text.animated.fadeInLeft {{$t('admin:api.disabled')}}
|
||||
v-spacer
|
||||
v-btn.mr-3.animated.fadeInDown.wait-p2s(outlined, color='grey', icon, @click='refresh')
|
||||
v-icon mdi-refresh
|
||||
v-btn.mr-3.animated.fadeInDown.wait-p1s(:color='enabled ? `red` : `green`', depressed, @click='globalSwitch', dark, :loading='isToggleLoading')
|
||||
v-icon(left) mdi-power
|
||||
span(v-if='!enabled') {{$t('admin:api.enableButton')}}
|
||||
span(v-else) {{$t('admin:api.disableButton')}}
|
||||
v-btn.animated.fadeInDown(color='primary', depressed, large, @click='newKey', dark)
|
||||
v-icon(left) mdi-plus
|
||||
span {{$t('admin:api.newKeyButton')}}
|
||||
v-card.mt-3.animated.fadeInUp
|
||||
v-simple-table(v-if='keys && keys.length > 0')
|
||||
template(v-slot:default)
|
||||
thead
|
||||
tr.grey(:class='$vuetify.theme.dark ? `darken-4-d5` : `lighten-5`')
|
||||
th {{$t('admin:api.headerName')}}
|
||||
th {{$t('admin:api.headerKeyEnding')}}
|
||||
th {{$t('admin:api.headerExpiration')}}
|
||||
th {{$t('admin:api.headerCreated')}}
|
||||
th {{$t('admin:api.headerLastUpdated')}}
|
||||
th(width='100') {{$t('admin:api.headerRevoke')}}
|
||||
tbody
|
||||
tr(v-for='key of keys', :key='`key-` + key.id')
|
||||
td
|
||||
strong(:class='key.isRevoked ? `red--text` : ``') {{ key.name }}
|
||||
em.caption.ml-1.red--text(v-if='key.isRevoked') (revoked)
|
||||
td.caption {{ key.keyShort }}
|
||||
td(:style='key.isRevoked ? `text-decoration: line-through;` : ``') {{ key.expiration | moment('LL') }}
|
||||
td {{ key.createdAt | moment('calendar') }}
|
||||
td {{ key.updatedAt | moment('calendar') }}
|
||||
td: v-btn(icon, @click='revoke(key)', :disabled='key.isRevoked'): v-icon(color='error') mdi-cancel
|
||||
v-card-text(v-else)
|
||||
v-alert.mb-0(icon='mdi-information', :value='true', outlined, color='info') {{$t('admin:api.noKeyInfo')}}
|
||||
|
||||
create-api-key(v-model='isCreateDialogShown', @refresh='refresh(false)')
|
||||
|
||||
v-dialog(v-model='isRevokeConfirmDialogShown', max-width='500', persistent)
|
||||
v-card
|
||||
.dialog-header.is-red {{$t('admin:api.revokeConfirm')}}
|
||||
v-card-text.pa-4
|
||||
i18next(tag='span', path='admin:api.revokeConfirmText')
|
||||
strong(place='name') {{ current.name }}
|
||||
v-card-actions
|
||||
v-spacer
|
||||
v-btn(text, @click='isRevokeConfirmDialogShown = false', :disabled='revokeLoading') {{$t('common:actions.cancel')}}
|
||||
v-btn(color='red', dark, @click='revokeConfirm', :loading='revokeLoading') {{$t('admin:api.revoke')}}
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import gql from 'graphql-tag'
|
||||
import { StatusIndicator } from 'vue-status-indicator'
|
||||
|
||||
import CreateApiKey from './admin-api-create.vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
StatusIndicator,
|
||||
CreateApiKey
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
enabled: false,
|
||||
isToggleLoading: false,
|
||||
keys: [],
|
||||
isCreateDialogShown: false,
|
||||
isRevokeConfirmDialogShown: false,
|
||||
revokeLoading: false,
|
||||
current: {}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async refresh (notify = true) {
|
||||
this.$apollo.queries.keys.refetch()
|
||||
if (notify) {
|
||||
this.$store.commit('showNotification', {
|
||||
message: this.$t('admin:api.refreshSuccess'),
|
||||
style: 'success',
|
||||
icon: 'cached'
|
||||
})
|
||||
}
|
||||
},
|
||||
async globalSwitch () {
|
||||
this.isToggleLoading = true
|
||||
try {
|
||||
const resp = await this.$apollo.mutate({
|
||||
mutation: gql`
|
||||
mutation ($enabled: Boolean!) {
|
||||
authentication {
|
||||
setApiState (enabled: $enabled) {
|
||||
responseResult {
|
||||
succeeded
|
||||
errorCode
|
||||
slug
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
enabled: !this.enabled
|
||||
},
|
||||
watchLoading (isLoading) {
|
||||
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-api-toggle')
|
||||
}
|
||||
})
|
||||
if (_.get(resp, 'data.authentication.setApiState.responseResult.succeeded', false)) {
|
||||
this.$store.commit('showNotification', {
|
||||
style: 'success',
|
||||
message: this.enabled ? this.$t('admin:api.toggleStateDisabledSuccess') : this.$t('admin:api.toggleStateEnabledSuccess'),
|
||||
icon: 'check'
|
||||
})
|
||||
await this.$apollo.queries.enabled.refetch()
|
||||
} else {
|
||||
this.$store.commit('showNotification', {
|
||||
style: 'red',
|
||||
message: _.get(resp, 'data.authentication.setApiState.responseResult.message', 'An unexpected error occurred.'),
|
||||
icon: 'alert'
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
this.$store.commit('pushGraphError', err)
|
||||
}
|
||||
this.isToggleLoading = false
|
||||
},
|
||||
async newKey () {
|
||||
this.isCreateDialogShown = true
|
||||
},
|
||||
revoke (key) {
|
||||
this.current = key
|
||||
this.isRevokeConfirmDialogShown = true
|
||||
},
|
||||
async revokeConfirm () {
|
||||
this.revokeLoading = true
|
||||
try {
|
||||
const resp = await this.$apollo.mutate({
|
||||
mutation: gql`
|
||||
mutation ($id: Int!) {
|
||||
authentication {
|
||||
revokeApiKey (id: $id) {
|
||||
responseResult {
|
||||
succeeded
|
||||
errorCode
|
||||
slug
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
id: this.current.id
|
||||
},
|
||||
watchLoading (isLoading) {
|
||||
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-api-revoke')
|
||||
}
|
||||
})
|
||||
if (_.get(resp, 'data.authentication.revokeApiKey.responseResult.succeeded', false)) {
|
||||
this.$store.commit('showNotification', {
|
||||
style: 'success',
|
||||
message: this.$t('admin:api.revokeSuccess'),
|
||||
icon: 'check'
|
||||
})
|
||||
this.refresh(false)
|
||||
} else {
|
||||
this.$store.commit('showNotification', {
|
||||
style: 'red',
|
||||
message: _.get(resp, 'data.authentication.revokeApiKey.responseResult.message', 'An unexpected error occurred.'),
|
||||
icon: 'alert'
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
this.$store.commit('pushGraphError', err)
|
||||
}
|
||||
this.isRevokeConfirmDialogShown = false
|
||||
this.revokeLoading = false
|
||||
}
|
||||
},
|
||||
apollo: {
|
||||
enabled: {
|
||||
query: gql`
|
||||
{
|
||||
authentication {
|
||||
apiState
|
||||
}
|
||||
}
|
||||
`,
|
||||
fetchPolicy: 'network-only',
|
||||
update: (data) => data.authentication.apiState,
|
||||
watchLoading (isLoading) {
|
||||
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-api-state-refresh')
|
||||
}
|
||||
},
|
||||
keys: {
|
||||
query: gql`
|
||||
{
|
||||
authentication {
|
||||
apiKeys {
|
||||
id
|
||||
name
|
||||
keyShort
|
||||
expiration
|
||||
isRevoked
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
fetchPolicy: 'network-only',
|
||||
update: (data) => data.authentication.apiKeys,
|
||||
watchLoading (isLoading) {
|
||||
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-api-keys-refresh')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang='scss'>
|
||||
|
||||
</style>
|
@ -1,433 +0,0 @@
|
||||
<template lang='pug'>
|
||||
v-container(fluid, grid-list-lg)
|
||||
v-layout(row, wrap)
|
||||
v-flex(xs12)
|
||||
.admin-header
|
||||
img.animated.fadeInUp(src='/_assets/svg/icon-unlock.svg', alt='Authentication', style='width: 80px;')
|
||||
.admin-header-title
|
||||
.headline.primary--text.animated.fadeInLeft {{ $t('admin:auth.title') }}
|
||||
.subtitle-1.grey--text.animated.fadeInLeft.wait-p4s {{ $t('admin:auth.subtitle') }}
|
||||
v-spacer
|
||||
v-btn.animated.fadeInDown.wait-p3s(icon, outlined, color='grey', href='https://docs.requarks.io/auth', target='_blank')
|
||||
v-icon mdi-help-circle
|
||||
v-btn.animated.fadeInDown.wait-p2s.mx-3(icon, outlined, color='grey', @click='refresh')
|
||||
v-icon mdi-refresh
|
||||
v-btn.animated.fadeInDown(color='success', @click='save', depressed, large)
|
||||
v-icon(left) mdi-check
|
||||
span {{$t('common:actions.apply')}}
|
||||
|
||||
v-flex(lg3, xs12)
|
||||
v-card.animated.fadeInUp
|
||||
v-toolbar(flat, color='teal', dark, dense)
|
||||
.subtitle-1 {{$t('admin:auth.activeStrategies')}}
|
||||
v-list(two-line, dense).py-0
|
||||
draggable(
|
||||
v-model='activeStrategies'
|
||||
handle='.is-handle'
|
||||
direction='vertical'
|
||||
)
|
||||
transition-group
|
||||
v-list-item(
|
||||
v-for='(str, idx) in activeStrategies'
|
||||
:key='str.key'
|
||||
@click='selectedStrategy = str.key'
|
||||
:class='selectedStrategy === str.key ? ($vuetify.theme.dark ? `grey darken-5` : `teal lighten-5`) : ``'
|
||||
)
|
||||
v-list-item-avatar.is-handle(size='24')
|
||||
v-icon(:color='selectedStrategy === str.key ? `teal` : `grey`') mdi-drag-horizontal
|
||||
v-list-item-content
|
||||
v-list-item-title.body-2(:class='selectedStrategy === str.key ? `teal--text` : ``') {{ str.displayName }}
|
||||
v-list-item-subtitle: .caption(:class='selectedStrategy === str.key ? `teal--text ` : ``') {{ str.strategy.title }}
|
||||
v-list-item-avatar(v-if='selectedStrategy === str.key', size='24')
|
||||
v-icon.animated.fadeInLeft(color='teal', large) mdi-chevron-right
|
||||
v-card-chin
|
||||
v-menu(offset-y, bottom, min-width='250px', max-width='550px', max-height='50vh', style='flex: 1 1;', center)
|
||||
template(v-slot:activator='{ on }')
|
||||
v-btn(v-on='on', color='primary', depressed, block)
|
||||
v-icon(left) mdi-plus
|
||||
span {{$t('admin:auth.addStrategy')}}
|
||||
v-list(dense)
|
||||
template(v-for='(str, idx) of strategies')
|
||||
v-list-item(
|
||||
:key='str.key'
|
||||
:disabled='str.isDisabled'
|
||||
@click='addStrategy(str)'
|
||||
)
|
||||
v-list-item-avatar(height='24', width='48', tile)
|
||||
v-img(:src='str.logo', width='48px', height='24px', contain, :style='str.isDisabled ? `opacity: .25;` : ``')
|
||||
v-list-item-content
|
||||
v-list-item-title {{str.title}}
|
||||
v-list-item-subtitle: .caption(:style='str.isDisabled ? `opacity: .4;` : ``') {{str.description}}
|
||||
v-divider(v-if='idx < strategies.length - 1')
|
||||
|
||||
v-flex(xs12, lg9)
|
||||
v-card.animated.fadeInUp.wait-p2s
|
||||
v-toolbar(color='primary', dense, flat, dark)
|
||||
.subtitle-1 {{strategy.displayName}} #[em ({{strategy.strategy.title}})]
|
||||
v-spacer
|
||||
v-btn(small, outlined, dark, color='white', :disabled='strategy.key === `local`', @click='deleteStrategy()')
|
||||
v-icon(left) mdi-close
|
||||
span {{$t('common:actions.delete')}}
|
||||
v-card-info(color='blue')
|
||||
div
|
||||
span {{strategy.strategy.description}}
|
||||
.caption: a(:href='strategy.strategy.website') {{strategy.strategy.website}}
|
||||
v-spacer
|
||||
.admin-providerlogo
|
||||
img(:src='strategy.strategy.logo', :alt='strategy.strategy.title')
|
||||
v-card-text
|
||||
.row
|
||||
.col-8
|
||||
v-text-field(
|
||||
outlined
|
||||
:label='$t(`admin:auth.displayName`)'
|
||||
v-model='strategy.displayName'
|
||||
prepend-icon='mdi-format-title'
|
||||
:hint='$t(`admin:auth.displayNameHint`)'
|
||||
persistent-hint
|
||||
)
|
||||
.col-4
|
||||
v-switch.mt-1(
|
||||
:label='$t(`admin:auth.strategyIsEnabled`)'
|
||||
v-model='strategy.isEnabled'
|
||||
color='primary'
|
||||
prepend-icon='mdi-power'
|
||||
:hint='$t(`admin:auth.strategyIsEnabledHint`)'
|
||||
persistent-hint
|
||||
inset
|
||||
:disabled='strategy.key === `local`'
|
||||
)
|
||||
template(v-if='strategy.config && Object.keys(strategy.config).length > 0')
|
||||
v-divider
|
||||
.overline.my-5 {{$t('admin:auth.strategyConfiguration')}}
|
||||
.pr-3
|
||||
template(v-for='cfg in strategy.config')
|
||||
v-select.mb-3(
|
||||
v-if='cfg.value.type === "string" && cfg.value.enum'
|
||||
outlined
|
||||
:items='cfg.value.enum'
|
||||
:key='cfg.key'
|
||||
:label='cfg.value.title'
|
||||
v-model='cfg.value.value'
|
||||
prepend-icon='mdi-cog-box'
|
||||
:hint='cfg.value.hint ? cfg.value.hint : ""'
|
||||
persistent-hint
|
||||
:class='cfg.value.hint ? "mb-2" : ""'
|
||||
:style='cfg.value.maxWidth > 0 ? `max-width:` + cfg.value.maxWidth + `px;` : ``'
|
||||
)
|
||||
v-switch.mb-6(
|
||||
v-else-if='cfg.value.type === "boolean"'
|
||||
:key='cfg.key'
|
||||
:label='cfg.value.title'
|
||||
v-model='cfg.value.value'
|
||||
color='primary'
|
||||
prepend-icon='mdi-cog-box'
|
||||
:hint='cfg.value.hint ? cfg.value.hint : ""'
|
||||
persistent-hint
|
||||
inset
|
||||
)
|
||||
v-textarea.mb-3(
|
||||
v-else-if='cfg.value.type === "string" && cfg.value.multiline'
|
||||
outlined
|
||||
:key='cfg.key'
|
||||
:label='cfg.value.title'
|
||||
v-model='cfg.value.value'
|
||||
prepend-icon='mdi-cog-box'
|
||||
:hint='cfg.value.hint ? cfg.value.hint : ""'
|
||||
persistent-hint
|
||||
:class='cfg.value.hint ? "mb-2" : ""'
|
||||
)
|
||||
v-text-field.mb-3(
|
||||
v-else
|
||||
outlined
|
||||
:key='cfg.key'
|
||||
:label='cfg.value.title'
|
||||
v-model='cfg.value.value'
|
||||
prepend-icon='mdi-cog-box'
|
||||
:hint='cfg.value.hint ? cfg.value.hint : ""'
|
||||
persistent-hint
|
||||
:class='cfg.value.hint ? "mb-2" : ""'
|
||||
:style='cfg.value.maxWidth > 0 ? `max-width:` + cfg.value.maxWidth + `px;` : ``'
|
||||
)
|
||||
v-divider
|
||||
.overline.my-5 {{$t('admin:auth.registration')}}
|
||||
.pr-3
|
||||
v-switch.ml-3(
|
||||
v-model='strategy.selfRegistration'
|
||||
:label='$t(`admin:auth.selfRegistration`)'
|
||||
color='primary'
|
||||
:hint='$t(`admin:auth.selfRegistrationHint`)'
|
||||
persistent-hint
|
||||
inset
|
||||
)
|
||||
v-combobox.ml-3.mt-5(
|
||||
:label='$t(`admin:auth.domainsWhitelist`)'
|
||||
v-model='strategy.domainWhitelist'
|
||||
prepend-icon='mdi-email-check-outline'
|
||||
outlined
|
||||
:disabled='!strategy.selfRegistration'
|
||||
:hint='$t(`admin:auth.domainsWhitelistHint`)'
|
||||
persistent-hint
|
||||
small-chips
|
||||
deletable-chips
|
||||
clearable
|
||||
multiple
|
||||
chips
|
||||
)
|
||||
v-autocomplete.mt-3.ml-3(
|
||||
outlined
|
||||
:disabled='!strategy.selfRegistration'
|
||||
:items='groups'
|
||||
item-text='name'
|
||||
item-value='id'
|
||||
:label='$t(`admin:auth.autoEnrollGroups`)'
|
||||
v-model='strategy.autoEnrollGroups'
|
||||
prepend-icon='mdi-account-group'
|
||||
:hint='$t(`admin:auth.autoEnrollGroupsHint`)'
|
||||
small-chips
|
||||
persistent-hint
|
||||
deletable-chips
|
||||
clearable
|
||||
multiple
|
||||
chips
|
||||
)
|
||||
|
||||
v-card.mt-4.wiki-form.animated.fadeInUp.wait-p4s(v-if='selectedStrategy !== `local`')
|
||||
v-toolbar(color='primary', dense, flat, dark)
|
||||
.subtitle-1 {{$t('admin:auth.configReference')}}
|
||||
v-card-text
|
||||
.body-2 {{$t('admin:auth.configReferenceSubtitle')}}
|
||||
v-alert.mt-3.radius-7(v-if='host.length < 8', color='red', outlined, :value='true', icon='mdi-alert')
|
||||
i18next(path='admin:auth.siteUrlNotSetup', tag='span')
|
||||
strong(place='siteUrl') {{$t('admin:general.siteUrl')}}
|
||||
strong(place='general') {{$t('admin:general.title')}}
|
||||
.pa-3.mt-3.radius-7.grey(v-else, :class='$vuetify.theme.dark ? `darken-3-d5` : `lighten-3`')
|
||||
.body-2: strong {{$t('admin:auth.allowedWebOrigins')}}
|
||||
.body-2 {{host}}
|
||||
v-divider.my-3
|
||||
.body-2: strong {{$t('admin:auth.callbackUrl')}}
|
||||
.body-2 {{host}}/login/{{strategy.key}}/callback
|
||||
v-divider.my-3
|
||||
.body-2: strong {{$t('admin:auth.loginUrl')}}
|
||||
.body-2 {{host}}/login
|
||||
v-divider.my-3
|
||||
.body-2: strong {{$t('admin:auth.logoutUrl')}}
|
||||
.body-2 {{host}}
|
||||
v-divider.my-3
|
||||
.body-2: strong {{$t('admin:auth.tokenEndpointAuthMethod')}}
|
||||
.body-2 HTTP-POST
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import gql from 'graphql-tag'
|
||||
import { v4 as uuid } from 'uuid'
|
||||
|
||||
import groupsQuery from 'gql/admin/auth/auth-query-groups.gql'
|
||||
import hostQuery from 'gql/admin/auth/auth-query-host.gql'
|
||||
|
||||
import draggable from 'vuedraggable'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
draggable
|
||||
},
|
||||
filters: {
|
||||
startCase(val) { return _.startCase(val) }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
groups: [],
|
||||
strategies: [],
|
||||
activeStrategies: [],
|
||||
selectedStrategy: '',
|
||||
host: '',
|
||||
strategy: {
|
||||
strategy: {}
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
selectedStrategy(newValue, oldValue) {
|
||||
this.strategy = _.find(this.activeStrategies, ['key', newValue]) || {}
|
||||
},
|
||||
activeStrategies(newValue, oldValue) {
|
||||
this.selectedStrategy = 'local'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async refresh() {
|
||||
await this.$apollo.queries.strategies.refetch()
|
||||
await this.$apollo.queries.activeStrategies.refetch()
|
||||
this.$store.commit('showNotification', {
|
||||
message: this.$t('admin:auth.refreshSuccess'),
|
||||
style: 'success',
|
||||
icon: 'cached'
|
||||
})
|
||||
},
|
||||
addStrategy (str) {
|
||||
const newStr = {
|
||||
key: uuid(),
|
||||
strategy: str,
|
||||
config: str.props.map(c => ({
|
||||
key: c.key,
|
||||
value: {
|
||||
...c,
|
||||
value: c.default
|
||||
}
|
||||
})),
|
||||
order: this.activeStrategies.length,
|
||||
isEnabled: true,
|
||||
displayName: str.title,
|
||||
selfRegistration: false,
|
||||
domainWhitelist: [],
|
||||
autoEnrollGroups: []
|
||||
}
|
||||
this.activeStrategies = [...this.activeStrategies, newStr]
|
||||
this.$nextTick(() => {
|
||||
this.selectedStrategy = newStr.key
|
||||
})
|
||||
},
|
||||
deleteStrategy () {
|
||||
this.activeStrategies = _.reject(this.activeStrategies, ['key', this.strategy.key])
|
||||
},
|
||||
async save() {
|
||||
this.$store.commit(`loadingStart`, 'admin-auth-savestrategies')
|
||||
try {
|
||||
const resp = await this.$apollo.mutate({
|
||||
mutation: gql`
|
||||
mutation($strategies: [AuthenticationStrategyInput]!) {
|
||||
authentication {
|
||||
updateStrategies(strategies: $strategies) {
|
||||
responseResult {
|
||||
succeeded
|
||||
errorCode
|
||||
slug
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
strategies: this.activeStrategies.map((str, idx) => ({
|
||||
key: str.key,
|
||||
strategyKey: str.strategy.key,
|
||||
displayName: str.displayName,
|
||||
order: idx,
|
||||
isEnabled: str.isEnabled,
|
||||
config: str.config.map(cfg => ({...cfg, value: JSON.stringify({ v: cfg.value.value })})),
|
||||
selfRegistration: str.selfRegistration,
|
||||
domainWhitelist: str.domainWhitelist,
|
||||
autoEnrollGroups: str.autoEnrollGroups
|
||||
}))
|
||||
}
|
||||
})
|
||||
if (_.get(resp, 'data.authentication.updateStrategies.responseResult.succeeded', false)) {
|
||||
this.$store.commit('showNotification', {
|
||||
message: this.$t('admin:auth.saveSuccess'),
|
||||
style: 'success',
|
||||
icon: 'check'
|
||||
})
|
||||
} else {
|
||||
throw new Error(_.get(resp, 'data.authentication.updateStrategies.responseResult.message', this.$t('common:error.unexpected')))
|
||||
}
|
||||
} catch (err) {
|
||||
this.$store.commit('pushGraphError', err)
|
||||
}
|
||||
this.$store.commit(`loadingStop`, 'admin-auth-savestrategies')
|
||||
}
|
||||
},
|
||||
apollo: {
|
||||
strategies: {
|
||||
query: gql`
|
||||
query {
|
||||
authentication {
|
||||
strategies {
|
||||
key
|
||||
title
|
||||
description
|
||||
isAvailable
|
||||
useForm
|
||||
logo
|
||||
website
|
||||
props {
|
||||
key
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
fetchPolicy: 'network-only',
|
||||
update: (data) => _.get(data, 'authentication.strategies', []).map(str => ({
|
||||
...str,
|
||||
isDisabled: !str.isAvailable || str.key === `local`,
|
||||
props: _.sortBy(str.props.map(cfg => ({
|
||||
key: cfg.key,
|
||||
...JSON.parse(cfg.value)
|
||||
})), [t => t.order])
|
||||
})),
|
||||
watchLoading (isLoading) {
|
||||
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-auth-strategies-refresh')
|
||||
}
|
||||
},
|
||||
activeStrategies: {
|
||||
query: gql`
|
||||
query {
|
||||
authentication {
|
||||
activeStrategies {
|
||||
key
|
||||
strategy {
|
||||
key
|
||||
title
|
||||
description
|
||||
useForm
|
||||
logo
|
||||
website
|
||||
}
|
||||
config {
|
||||
key
|
||||
value
|
||||
}
|
||||
order
|
||||
isEnabled
|
||||
displayName
|
||||
selfRegistration
|
||||
domainWhitelist
|
||||
autoEnrollGroups
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
fetchPolicy: 'network-only',
|
||||
update: (data) => _.sortBy(_.get(data, 'authentication.activeStrategies', []).map(str => ({
|
||||
...str,
|
||||
config: _.sortBy(str.config.map(cfg => ({
|
||||
...cfg,
|
||||
value: JSON.parse(cfg.value)
|
||||
})), [t => t.value.order])
|
||||
})), ['order']),
|
||||
watchLoading (isLoading) {
|
||||
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-auth-activestrategies-refresh')
|
||||
}
|
||||
},
|
||||
groups: {
|
||||
query: groupsQuery,
|
||||
fetchPolicy: 'network-only',
|
||||
update: (data) => data.groups.list,
|
||||
watchLoading (isLoading) {
|
||||
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-auth-groups-refresh')
|
||||
}
|
||||
},
|
||||
host: {
|
||||
query: hostQuery,
|
||||
fetchPolicy: 'network-only',
|
||||
update: (data) => _.cloneDeep(data.site.config.host),
|
||||
watchLoading (isLoading) {
|
||||
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-auth-host-refresh')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
@ -1,206 +0,0 @@
|
||||
<template lang='pug'>
|
||||
v-container(fluid, grid-list-lg)
|
||||
v-layout(row, wrap)
|
||||
v-flex(xs12)
|
||||
.admin-header
|
||||
img.animated.fadeInUp(src='/_assets/svg/icon-chat-bubble.svg', alt='Comments', style='width: 80px;')
|
||||
.admin-header-title
|
||||
.headline.primary--text.animated.fadeInLeft {{$t('admin:comments.title')}}
|
||||
.subtitle-1.grey--text.animated.fadeInLeft.wait-p2s {{$t('admin:comments.subtitle')}}
|
||||
v-spacer
|
||||
v-btn.animated.fadeInDown.wait-p3s(icon, outlined, color='grey', href='https://docs.requarks.io/comments', target='_blank')
|
||||
v-icon mdi-help-circle
|
||||
v-btn.mx-3.animated.fadeInDown.wait-p2s(icon, outlined, color='grey', @click='refresh')
|
||||
v-icon mdi-refresh
|
||||
v-btn.animated.fadeInDown(color='success', @click='save', depressed, large)
|
||||
v-icon(left) mdi-check
|
||||
span {{$t('common:actions.apply')}}
|
||||
|
||||
v-flex(lg3, xs12)
|
||||
v-card.animated.fadeInUp
|
||||
v-toolbar(flat, color='primary', dark, dense)
|
||||
.subtitle-1 {{$t('admin:comments.provider')}}
|
||||
v-list.py-0(two-line, dense)
|
||||
template(v-for='(provider, idx) in providers')
|
||||
v-list-item(:key='provider.key', @click='selectedProvider = provider.key', :disabled='!provider.isAvailable')
|
||||
v-list-item-avatar(size='24')
|
||||
v-icon(color='grey', v-if='!provider.isAvailable') mdi-minus-box-outline
|
||||
v-icon(color='primary', v-else-if='provider.key === selectedProvider') mdi-checkbox-marked-circle-outline
|
||||
v-icon(color='grey', v-else) mdi-checkbox-blank-circle-outline
|
||||
v-list-item-content
|
||||
v-list-item-title.body-2(:class='!provider.isAvailable ? `grey--text` : (selectedProvider === provider.key ? `primary--text` : ``)') {{ provider.title }}
|
||||
v-list-item-subtitle: .caption(:class='!provider.isAvailable ? `grey--text text--lighten-1` : (selectedProvider === provider.key ? `blue--text ` : ``)') {{ provider.description }}
|
||||
v-list-item-avatar(v-if='selectedProvider === provider.key', size='24')
|
||||
v-icon.animated.fadeInLeft(color='primary', large) mdi-chevron-right
|
||||
v-divider(v-if='idx < providers.length - 1')
|
||||
|
||||
v-flex(lg9, xs12)
|
||||
v-card.animated.fadeInUp.wait-p2s
|
||||
v-toolbar(color='primary', dense, flat, dark)
|
||||
.subtitle-1 {{provider.title}}
|
||||
v-card-info(color='blue')
|
||||
div
|
||||
div {{provider.description}}
|
||||
span.caption: a(:href='provider.website') {{provider.website}}
|
||||
v-spacer
|
||||
.admin-providerlogo
|
||||
img(:src='provider.logo', :alt='provider.title')
|
||||
v-card-text
|
||||
.overline.my-5 {{$t('admin:comments.providerConfig')}}
|
||||
.body-2.ml-3(v-if='!provider.config || provider.config.length < 1'): em {{$t('admin:comments.providerNoConfig')}}
|
||||
template(v-else, v-for='cfg in provider.config')
|
||||
v-select.mb-3(
|
||||
v-if='cfg.value.type === "string" && cfg.value.enum'
|
||||
outlined
|
||||
:items='cfg.value.enum'
|
||||
:key='cfg.key'
|
||||
:label='cfg.value.title'
|
||||
v-model='cfg.value.value'
|
||||
prepend-icon='mdi-cog-box'
|
||||
:hint='cfg.value.hint ? cfg.value.hint : ""'
|
||||
persistent-hint
|
||||
:class='cfg.value.hint ? "mb-2" : ""'
|
||||
:style='cfg.value.maxWidth > 0 ? `max-width:` + cfg.value.maxWidth + `px;` : ``'
|
||||
)
|
||||
v-switch.mb-6(
|
||||
v-else-if='cfg.value.type === "boolean"'
|
||||
:key='cfg.key'
|
||||
:label='cfg.value.title'
|
||||
v-model='cfg.value.value'
|
||||
color='primary'
|
||||
prepend-icon='mdi-cog-box'
|
||||
:hint='cfg.value.hint ? cfg.value.hint : ""'
|
||||
persistent-hint
|
||||
inset
|
||||
)
|
||||
v-textarea.mb-3(
|
||||
v-else-if='cfg.value.type === "string" && cfg.value.multiline'
|
||||
outlined
|
||||
:key='cfg.key'
|
||||
:label='cfg.value.title'
|
||||
v-model='cfg.value.value'
|
||||
prepend-icon='mdi-cog-box'
|
||||
:hint='cfg.value.hint ? cfg.value.hint : ""'
|
||||
persistent-hint
|
||||
:class='cfg.value.hint ? "mb-2" : ""'
|
||||
)
|
||||
v-text-field.mb-3(
|
||||
v-else
|
||||
outlined
|
||||
:key='cfg.key'
|
||||
:label='cfg.value.title'
|
||||
v-model='cfg.value.value'
|
||||
prepend-icon='mdi-cog-box'
|
||||
:hint='cfg.value.hint ? cfg.value.hint : ""'
|
||||
persistent-hint
|
||||
:class='cfg.value.hint ? "mb-2" : ""'
|
||||
:style='cfg.value.maxWidth > 0 ? `max-width:` + cfg.value.maxWidth + `px;` : ``'
|
||||
)
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
providers: [],
|
||||
selectedProvider: '',
|
||||
provider: {}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
selectedProvider(newValue, oldValue) {
|
||||
this.provider = _.find(this.providers, ['key', newValue]) || {}
|
||||
},
|
||||
providers(newValue, oldValue) {
|
||||
this.selectedProvider = _.get(_.find(this.providers, 'isEnabled'), 'key', 'db')
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async refresh() {
|
||||
await this.$apollo.queries.providers.refetch()
|
||||
this.$store.commit('showNotification', {
|
||||
message: this.$t('admin:comments.listRefreshSuccess'),
|
||||
style: 'success',
|
||||
icon: 'cached'
|
||||
})
|
||||
},
|
||||
async save() {
|
||||
this.$store.commit(`loadingStart`, 'admin-comments-saveproviders')
|
||||
try {
|
||||
const resp = await this.$apollo.mutate({
|
||||
mutation: gql`
|
||||
mutation($providers: [CommentProviderInput]!) {
|
||||
comments {
|
||||
updateProviders(providers: $providers) {
|
||||
responseResult {
|
||||
succeeded
|
||||
errorCode
|
||||
slug
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
providers: this.providers.map(tgt => ({
|
||||
isEnabled: tgt.key === this.selectedProvider,
|
||||
key: tgt.key,
|
||||
config: tgt.config.map(cfg => ({...cfg, value: JSON.stringify({ v: cfg.value.value })}))
|
||||
}))
|
||||
}
|
||||
})
|
||||
if (_.get(resp, 'data.comments.updateProviders.responseResult.succeeded', false)) {
|
||||
this.$store.commit('showNotification', {
|
||||
message: this.$t('admin:comments.configSaveSuccess'),
|
||||
style: 'success',
|
||||
icon: 'check'
|
||||
})
|
||||
} else {
|
||||
throw new Error(_.get(resp, 'data.comments.updateProviders.responseResult.message', this.$t('common:error.unexpected')))
|
||||
}
|
||||
} catch (err) {
|
||||
this.$store.commit('pushGraphError', err)
|
||||
}
|
||||
this.$store.commit(`loadingStop`, 'admin-comments-saveproviders')
|
||||
}
|
||||
},
|
||||
apollo: {
|
||||
providers: {
|
||||
query: gql`
|
||||
query {
|
||||
comments {
|
||||
providers {
|
||||
isEnabled
|
||||
key
|
||||
title
|
||||
description
|
||||
logo
|
||||
website
|
||||
isAvailable
|
||||
config {
|
||||
key
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
fetchPolicy: 'network-only',
|
||||
update: (data) => _.cloneDeep(data.comments.providers).map(str => ({
|
||||
...str,
|
||||
config: _.sortBy(str.config.map(cfg => ({
|
||||
...cfg,
|
||||
value: JSON.parse(cfg.value)
|
||||
})), [t => t.value.order])
|
||||
})),
|
||||
watchLoading (isLoading) {
|
||||
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-comments-refresh')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
@ -1,256 +0,0 @@
|
||||
<template lang='pug'>
|
||||
v-container.admin-contribute(fluid, grid-list-lg)
|
||||
v-layout(row, wrap)
|
||||
v-flex(xs12)
|
||||
.admin-header
|
||||
img.animated.fadeInUp(src='/_assets/svg/icon-heart-health.svg', alt='Contribute', style='width: 80px;')
|
||||
.admin-header-title
|
||||
.headline.primary--text.animated.fadeInLeft {{ $t('admin:contribute.title') }}
|
||||
.subtitle-1.grey--text.animated.fadeInLeft.wait-p4s {{ $t('admin:contribute.subtitle') }}
|
||||
v-card.mt-3.animated.fadeInUp
|
||||
v-card-text
|
||||
i18next.body-2.pl-3(path='admin:contribute.openSource', tag='div')
|
||||
v-icon(color='red') mdi-heart
|
||||
a(href='https://requarks.io', target='_blank') requarks.io
|
||||
a(href='https://github.com/Requarks/wiki/graphs/contributors', target='_blank') {{ $t('admin:contribute.openSourceContributors') }}
|
||||
.body-2.pt-3.pl-3 {{ $t('admin:contribute.needYourHelp') }}
|
||||
v-divider.mt-3
|
||||
v-subheader.subtitle-2 {{ $t('admin:contribute.fundOurWork') }}
|
||||
v-tabs.mx-3.radius-7.admin-contribute-tabs(
|
||||
centered
|
||||
fixed-tabs
|
||||
background-color='primary'
|
||||
color='white'
|
||||
dark
|
||||
slider-color='#FFF'
|
||||
icons-and-text
|
||||
)
|
||||
v-tab
|
||||
span GitHub
|
||||
v-icon.my-1(size='24') mdi-github
|
||||
v-tab
|
||||
span Patreon
|
||||
img.my-1(src='/_assets/svg/icon-patreon.svg', style='height: 24px;')
|
||||
v-tab
|
||||
span OpenCollective
|
||||
img.my-1(src='/_assets/svg/icon-opencollective.svg', style='height: 24px;')
|
||||
v-tab
|
||||
span PayPal
|
||||
img.my-1(src='/_assets/svg/icon-paypal.svg', style='height: 24px;')
|
||||
v-tab
|
||||
span Ethereum
|
||||
img.my-1(src='/_assets/svg/icon-ethereum.svg', style='height: 24px;')
|
||||
v-tab
|
||||
span T-Shirts
|
||||
img.my-1(src='/_assets/svg/icon-t-shirt.svg', style='height: 24px;')
|
||||
v-tab-item(:transition='false', :reverse-transition='false')
|
||||
.body-2.pa-3 {{ $t('admin:contribute.github') }}
|
||||
a.ml-3(href='https://github.com/users/NGPixel/sponsorship', :title='$t(`admin:contribute.becomeASponsor`)')
|
||||
img(src='/_assets/img/donate_github.svg', :alt='$t(`admin:contribute.becomeASponsor`)' style='width:200px;')
|
||||
v-tab-item(:transition='false', :reverse-transition='false')
|
||||
.body-2.pa-3 {{ $t('admin:contribute.patreon') }}
|
||||
a.ml-3(href='https://www.patreon.com/bePatron?u=16744039', :title='$t(`admin:contribute.becomeAPatron`)')
|
||||
img(src='/_assets/img/donate_patreon.png', :alt='$t(`admin:contribute.becomeAPatron`)' style='width:200px;')
|
||||
v-tab-item(:transition='false', :reverse-transition='false')
|
||||
.body-2.pa-3 {{ $t('admin:contribute.openCollective') }}
|
||||
a.ml-3(href='https://opencollective.com/wikijs/donate', :title='$t(`admin:contribute.makeADonation`)')
|
||||
img(src='/_assets/img/donate_opencollective.png', :alt='$t(`admin:contribute.makeADonation`)' style='width:300px;')
|
||||
v-tab-item(:transition='false', :reverse-transition='false')
|
||||
.body-2.pa-3 {{ $t('admin:contribute.paypal') }}
|
||||
.ml-3
|
||||
form(action='https://www.paypal.com/cgi-bin/webscr', method='post', target='_top')
|
||||
input(type='hidden', name='cmd', value='_s-xclick')
|
||||
input(type='hidden', name='hosted_button_id', value='FLV5X255Z9CJU')
|
||||
input(type='image', src='/_assets/img/donate_paypal.png', border='0', name='submit', title='PayPal - The safer, easier way to pay online!', alt='Donate with PayPal button')
|
||||
img(alt='', border='0', src='https://www.paypal.com/en_CA/i/scr/pixel.gif', width='1', height='1')
|
||||
v-tab-item(:transition='false', :reverse-transition='false')
|
||||
.body-2.pa-3 {{ $t('admin:contribute.ethereum') }}
|
||||
.ml-3
|
||||
.admin-contribute-ethaddress
|
||||
strong Ethereum Address
|
||||
span 0xE1d55C19aE86f6Bcbfb17e7f06aCe96BdBb22Cb5
|
||||
div: img(src='/_assets/img/donate_eth_qr.png')
|
||||
v-tab-item(:transition='false', :reverse-transition='false')
|
||||
.body-2.pa-3 {{ $t('admin:contribute.tshirts') }}
|
||||
v-card-actions.ml-2
|
||||
v-btn(outlined, :color='$vuetify.theme.dark ? `blue lighten-1` : `primary`', href='https://wikijs.threadless.com', large)
|
||||
v-icon(left) mdi-tshirt-crew
|
||||
span {{ $t('admin:contribute.shop') }}
|
||||
v-divider.mt-3
|
||||
v-subheader.subtitle-2 {{ $t('admin:contribute.contribute') }}
|
||||
.body-2.pl-3
|
||||
ul
|
||||
i18next(path='admin:contribute.submitAnIdea', tag='li')
|
||||
a(href='https://requests.requarks.io/wiki', target='_blank') {{ $t('admin:contribute.submitAnIdeaLink') }}
|
||||
i18next(path='admin:contribute.foundABug', tag='li')
|
||||
a(href='https://github.com/Requarks/wiki/issues', target='_blank') Github
|
||||
i18next(path='admin:contribute.helpTranslate', tag='li')
|
||||
a(href='https://wiki.requarks.io/slack', target='_blank') Slack
|
||||
v-divider.mt-3
|
||||
v-subheader.subtitle-2 {{ $t('admin:contribute.spreadTheWord') }}
|
||||
.body-2.pl-3
|
||||
ul
|
||||
li {{ $t('admin:contribute.talkToFriends') }}
|
||||
i18next(path='admin:contribute.followUsOnTwitter', tag='li')
|
||||
a(href='https://twitter.com/requarks', target='_blank') Twitter
|
||||
v-toolbar(color='indigo', dense, dark)
|
||||
.subtitle-1 Sponsors & Backers
|
||||
v-container.pa-5.grey(fluid, :class='$vuetify.theme.dark ? `darken-3` : `lighten-4`')
|
||||
v-progress-circular(indeterminate, color='indigo', size='24', width='2', v-if='backers.length < 1')
|
||||
v-row(dense)
|
||||
v-col(cols='12', lg='6', xl='4', v-for='(backer, idx) in backers', :key='backer.id')
|
||||
v-card.grey(flat, :class='$vuetify.theme.dark ? `darken-4` : `lighten-2`')
|
||||
v-list-item
|
||||
v-list-item-avatar
|
||||
img(v-if='backer.avatar', :src='backer.avatar')
|
||||
v-avatar(v-else, color='blue-grey', size='40')
|
||||
span.white--text.subtitle-1 {{backer.name[0].toUpperCase()}}
|
||||
v-list-item-content
|
||||
v-list-item-title {{backer.name}}
|
||||
v-list-item-subtitle: .caption Since {{backer.joined | moment('MMMM DD, YYYY')}} on {{backer.source}}
|
||||
v-list-item-action(v-if='backer.twitter')
|
||||
v-btn(icon, :href='backer.twitter', target='_blank')
|
||||
v-icon(color='grey') mdi-twitter
|
||||
v-list-item-action(v-if='backer.website')
|
||||
v-btn(icon, :href='backer.website', target='_blank')
|
||||
v-icon(color='grey') mdi-earth
|
||||
v-toolbar(color='primary', dense, dark)
|
||||
.subtitle-1 Special Thanks
|
||||
v-list(two-line)
|
||||
v-list-item
|
||||
v-list-item-avatar
|
||||
img(src='https://static.requarks.io/logo/algolia.svg', alt='Algolia')
|
||||
v-list-item-content
|
||||
v-list-item-title Algolia
|
||||
v-list-item-subtitle Algolia is a powerful search-as-a-service solution, made easy to use with API clients, UI libraries, and pre-built integrations.
|
||||
v-list-item-action
|
||||
v-btn(icon, href='https://www.algolia.com/', target='_blank')
|
||||
v-icon(color='grey') mdi-earth
|
||||
v-divider
|
||||
v-list-item
|
||||
v-list-item-avatar
|
||||
img(src='https://static.requarks.io/logo/browserstack.svg', alt='Browserstack')
|
||||
v-list-item-content
|
||||
v-list-item-title BrowserStack
|
||||
v-list-item-subtitle BrowserStack is a cloud web and mobile testing platform that enables developers to test their websites and mobile applications.
|
||||
v-list-item-action
|
||||
v-btn(icon, href='https://www.browserstack.com/', target='_blank')
|
||||
v-icon(color='grey') mdi-earth
|
||||
v-divider
|
||||
v-list-item
|
||||
v-list-item-avatar
|
||||
img(src='https://static.requarks.io/logo/cloudflare.svg', alt='Cloudflare')
|
||||
v-list-item-content
|
||||
v-list-item-title Cloudflare
|
||||
v-list-item-subtitle Providing content delivery network services, DDoS mitigation, Internet security and distributed domain name server services.
|
||||
v-list-item-action
|
||||
v-btn(icon, href='https://www.cloudflare.com/', target='_blank')
|
||||
v-icon(color='grey') mdi-earth
|
||||
v-divider
|
||||
v-list-item
|
||||
v-list-item-avatar
|
||||
img(src='https://static.requarks.io/logo/digitalocean.svg', alt='DigitalOcean')
|
||||
v-list-item-content
|
||||
v-list-item-title DigitalOcean
|
||||
v-list-item-subtitle Providing developers and businesses a reliable, easy-to-use cloud computing platform of virtual servers (Droplets), object storage (Spaces), and more.
|
||||
v-list-item-action
|
||||
v-btn(icon, href='https://m.do.co/c/5f7445bfa4d0', target='_blank')
|
||||
v-icon(color='grey') mdi-earth
|
||||
v-divider
|
||||
v-list-item
|
||||
v-list-item-avatar(tile)
|
||||
img(src='/_assets/svg/logo-icons8.svg', alt='Icons8')
|
||||
v-list-item-content
|
||||
v-list-item-title Icons8
|
||||
v-list-item-subtitle All the Icons You Need. Guaranteed.
|
||||
v-list-item-action
|
||||
v-btn(icon, href='https://icons8.com', target='_blank')
|
||||
v-icon(color='grey') mdi-earth
|
||||
v-divider
|
||||
v-list-item
|
||||
v-list-item-avatar(tile)
|
||||
img(src='https://static.requarks.io/logo/lokalise.png', alt='Lokalise')
|
||||
v-list-item-content
|
||||
v-list-item-title Lokalise
|
||||
v-list-item-subtitle Lokalise is a translation management system built for agile teams who want to automate their localization process.
|
||||
v-list-item-action
|
||||
v-btn(icon, href='https://lokalise.co', target='_blank')
|
||||
v-icon(color='grey') mdi-earth
|
||||
v-divider
|
||||
v-list-item
|
||||
v-list-item-avatar(tile)
|
||||
img(src='https://static.requarks.io/logo/netlify.svg', alt='Netlify')
|
||||
v-list-item-content
|
||||
v-list-item-title Netlify
|
||||
v-list-item-subtitle Deploy modern static websites with Netlify. Get CDN, Continuous deployment, 1-click HTTPS, and all the services you need.
|
||||
v-list-item-action
|
||||
v-btn(icon, href='https://wwwnetlify.com', target='_blank')
|
||||
v-icon(color='grey') mdi-earth
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
backers: []
|
||||
}
|
||||
},
|
||||
apollo: {
|
||||
backers: {
|
||||
query: gql`
|
||||
{
|
||||
contribute {
|
||||
contributors {
|
||||
id
|
||||
source
|
||||
name
|
||||
joined
|
||||
website
|
||||
twitter
|
||||
avatar
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
fetchPolicy: 'network-only',
|
||||
update: (data) => data.contribute.contributors,
|
||||
watchLoading (isLoading) {
|
||||
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-contribute-refresh')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang='scss'>
|
||||
.admin-contribute {
|
||||
|
||||
&-tabs {
|
||||
.v-tabs__item img {
|
||||
height: 24px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
&-ethaddress {
|
||||
display: inline-block;
|
||||
margin-bottom: 12px;
|
||||
border-radius: 7px;
|
||||
background-color: mc('grey', '100');
|
||||
color: mc('grey', '700');
|
||||
padding: 12px;
|
||||
|
||||
strong {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
margin-left: 1rem;
|
||||
list-style-type: square;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,256 +0,0 @@
|
||||
<template lang='pug'>
|
||||
v-container(fluid, grid-list-lg)
|
||||
v-layout(row, wrap)
|
||||
v-flex(xs12)
|
||||
.admin-header
|
||||
img.animated.fadeInUp(src='/_assets/svg/icon-browse-page.svg', alt='Dashboard', style='width: 80px;')
|
||||
.admin-header-title
|
||||
.headline.primary--text.animated.fadeInLeft {{ $t('admin:dashboard.title') }}
|
||||
.subtitle-1.grey--text.animated.fadeInLeft.wait-p2s {{ $t('admin:dashboard.subtitle') }}
|
||||
v-flex(xs12 md6 lg4 xl3 d-flex)
|
||||
v-card.primary.dashboard-card.animated.fadeInUp(dark)
|
||||
v-card-text
|
||||
v-icon.dashboard-icon mdi-file-document-outline
|
||||
.overline {{$t('admin:dashboard.pages')}}
|
||||
animated-number.display-1(
|
||||
:value='info.pagesTotal'
|
||||
:duration='2000'
|
||||
:formatValue='round'
|
||||
easing='easeOutQuint'
|
||||
)
|
||||
v-flex(xs12 md6 lg4 xl3 d-flex)
|
||||
v-card.blue.darken-3.dashboard-card.animated.fadeInUp.wait-p2s(dark)
|
||||
v-card-text
|
||||
v-icon.dashboard-icon mdi-account
|
||||
.overline {{$t('admin:dashboard.users')}}
|
||||
animated-number.display-1(
|
||||
:value='info.usersTotal'
|
||||
:duration='2000'
|
||||
:formatValue='round'
|
||||
easing='easeOutQuint'
|
||||
)
|
||||
v-flex(xs12 md6 lg4 xl3 d-flex)
|
||||
v-card.blue.darken-4.dashboard-card.animated.fadeInUp.wait-p4s(dark)
|
||||
v-card-text
|
||||
v-icon.dashboard-icon mdi-account-group
|
||||
.overline {{$t('admin:dashboard.groups')}}
|
||||
animated-number.display-1(
|
||||
:value='info.groupsTotal'
|
||||
:duration='2000'
|
||||
:formatValue='round'
|
||||
easing='easeOutQuint'
|
||||
)
|
||||
v-flex(xs12 md6 lg12 xl3 d-flex)
|
||||
v-card.dashboard-card.animated.fadeInUp.wait-p6s(
|
||||
:class='isLatestVersion ? "green" : "red lighten-2"'
|
||||
dark
|
||||
)
|
||||
v-btn.btn-animate-wrench(fab, absolute, :right='!$vuetify.rtl', :left='$vuetify.rtl', top, small, light, to='system', v-if='hasPermission(`manage:system`)')
|
||||
v-icon(:color='isLatestVersion ? `green` : `red darken-4`', small) mdi-wrench
|
||||
v-card-text
|
||||
v-icon.dashboard-icon mdi-blur
|
||||
.subtitle-1 Wiki.js {{info.currentVersion}}
|
||||
.body-2(v-if='isLatestVersion') {{$t('admin:dashboard.versionLatest')}}
|
||||
.body-2(v-else) {{$t('admin:dashboard.versionNew', { version: info.latestVersion })}}
|
||||
v-flex(xs12, xl6)
|
||||
v-card.radius-7.animated.fadeInUp.wait-p2s
|
||||
v-toolbar(:color='$vuetify.theme.dark ? `grey darken-2` : `grey lighten-5`', dense, flat)
|
||||
v-spacer
|
||||
.overline {{$t('admin:dashboard.recentPages')}}
|
||||
v-spacer
|
||||
v-data-table.pb-2(
|
||||
:items='recentPages'
|
||||
:headers='recentPagesHeaders'
|
||||
:loading='recentPagesLoading'
|
||||
hide-default-footer
|
||||
hide-default-header
|
||||
)
|
||||
template(slot='item', slot-scope='props')
|
||||
tr.is-clickable(:active='props.selected', @click='$router.push(`/pages/` + props.item.id)')
|
||||
td
|
||||
.body-2: strong {{ props.item.title }}
|
||||
td.admin-pages-path
|
||||
v-chip(label, small, :color='$vuetify.theme.dark ? `grey darken-4` : `grey lighten-4`') {{ props.item.locale }}
|
||||
span.ml-2.grey--text(:class='$vuetify.theme.dark ? `text--lighten-1` : `text--darken-2`') / {{ props.item.path }}
|
||||
td.text-right.caption(width='250') {{ props.item.updatedAt | moment('calendar') }}
|
||||
v-flex(xs12, xl6)
|
||||
v-card.radius-7.animated.fadeInUp.wait-p4s
|
||||
v-toolbar(:color='$vuetify.theme.dark ? `grey darken-2` : `grey lighten-5`', dense, flat)
|
||||
v-spacer
|
||||
.overline {{$t('admin:dashboard.lastLogins')}}
|
||||
v-spacer
|
||||
v-data-table.pb-2(
|
||||
:items='lastLogins'
|
||||
:headers='lastLoginsHeaders'
|
||||
:loading='lastLoginsLoading'
|
||||
hide-default-footer
|
||||
hide-default-header
|
||||
)
|
||||
template(slot='item', slot-scope='props')
|
||||
tr.is-clickable(:active='props.selected', @click='$router.push(`/users/` + props.item.id)')
|
||||
td
|
||||
.body-2: strong {{ props.item.name }}
|
||||
td.text-right.caption(width='250') {{ props.item.lastLoginAt | moment('calendar') }}
|
||||
|
||||
v-flex(xs12)
|
||||
v-card.dashboard-contribute.animated.fadeInUp.wait-p4s
|
||||
v-card-text
|
||||
img(src='/_assets/svg/icon-heart-health.svg', alt='Contribute', style='height: 80px;')
|
||||
.pl-5
|
||||
.subtitle-1 {{$t('admin:contribute.title')}}
|
||||
.body-2.mt-3: strong {{$t('admin:dashboard.contributeSubtitle')}}
|
||||
.body-2 {{$t('admin:dashboard.contributeHelp')}}
|
||||
v-btn.mx-0.mt-4(:color='$vuetify.theme.dark ? `indigo lighten-3` : `indigo`', outlined, small, to='/contribute')
|
||||
.caption: strong {{$t('admin:dashboard.contributeLearnMore')}}
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import AnimatedNumber from 'animated-number-vue'
|
||||
import { get } from 'vuex-pathify'
|
||||
import gql from 'graphql-tag'
|
||||
import semverLte from 'semver/functions/lte'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
AnimatedNumber
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
recentPages: [],
|
||||
recentPagesLoading: false,
|
||||
recentPagesHeaders: [
|
||||
{ text: 'Title', value: 'title' },
|
||||
{ text: 'Path', value: 'path' },
|
||||
{ text: 'Last Updated', value: 'updatedAt', width: 250 }
|
||||
],
|
||||
lastLogins: [],
|
||||
lastLoginsLoading: false,
|
||||
lastLoginsHeaders: [
|
||||
{ text: 'User', value: 'displayName' },
|
||||
{ text: 'Last Login', value: 'lastLoginAt', width: 250 }
|
||||
]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isLatestVersion() {
|
||||
if (this.info.latestVersion === 'n/a' || this.info.currentVersion === 'n/a') {
|
||||
return true
|
||||
} else {
|
||||
return semverLte(this.info.latestVersion, this.info.currentVersion)
|
||||
}
|
||||
},
|
||||
info: get('admin/info'),
|
||||
permissions: get('user/permissions')
|
||||
},
|
||||
methods: {
|
||||
round(val) { return Math.round(val) },
|
||||
hasPermission(prm) {
|
||||
if (_.isArray(prm)) {
|
||||
return _.some(prm, p => {
|
||||
return _.includes(this.permissions, p)
|
||||
})
|
||||
} else {
|
||||
return _.includes(this.permissions, prm)
|
||||
}
|
||||
}
|
||||
},
|
||||
apollo: {
|
||||
recentPages: {
|
||||
query: gql`
|
||||
query {
|
||||
pages {
|
||||
list(limit: 10, orderBy: UPDATED, orderByDirection: DESC) {
|
||||
id
|
||||
locale
|
||||
path
|
||||
title
|
||||
description
|
||||
contentType
|
||||
isPublished
|
||||
isPrivate
|
||||
privateNS
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
update: (data) => data.pages.list,
|
||||
watchLoading (isLoading) {
|
||||
this.recentPagesLoading = isLoading
|
||||
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-dashboard-recentpages')
|
||||
}
|
||||
},
|
||||
lastLogins: {
|
||||
query: gql`
|
||||
query {
|
||||
users {
|
||||
lastLogins {
|
||||
id
|
||||
name
|
||||
lastLoginAt
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
fetchPolicy: 'network-only',
|
||||
update: (data) => data.users.lastLogins,
|
||||
watchLoading (isLoading) {
|
||||
this.lastLoginsLoading = isLoading
|
||||
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-dashboard-lastlogins')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang='scss'>
|
||||
|
||||
.dashboard-card {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
border-radius: 7px;
|
||||
|
||||
.v-card__text {
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
.dashboard-contribute {
|
||||
background-color: #FFF;
|
||||
background-image: linear-gradient(to bottom, #FFF 0%, lighten(mc('indigo', '50'), 3%) 100%);
|
||||
border-radius: 7px;
|
||||
|
||||
@at-root .theme--dark & {
|
||||
background-color: mc('grey', '800');
|
||||
background-image: linear-gradient(to bottom, mc('grey', '800') 0%, darken(mc('grey', '800'), 6%) 100%);
|
||||
}
|
||||
|
||||
.v-card__text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: mc('indigo', '500') !important;
|
||||
|
||||
@at-root .theme--dark & {
|
||||
color: mc('grey', '300') !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.v-icon.dashboard-icon {
|
||||
position: absolute !important;
|
||||
right: 0;
|
||||
top: 12px;
|
||||
font-size: 100px !important;
|
||||
opacity: .25;
|
||||
|
||||
@at-root .v-application--is-rtl & {
|
||||
left: 0;
|
||||
right: initial;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
@ -1,94 +0,0 @@
|
||||
<template lang='pug'>
|
||||
v-container(fluid, grid-list-lg)
|
||||
v-layout(row, wrap)
|
||||
v-flex(xs12)
|
||||
.admin-header
|
||||
img(src='/_assets/svg/icon-console.svg', alt='Developer Tools', style='width: 80px;')
|
||||
.admin-header-title
|
||||
.headline.primary--text Developer Tools
|
||||
.subtitle-1.grey--text Flags
|
||||
v-spacer
|
||||
v-btn(color='success', depressed, @click='save', large)
|
||||
v-icon(left) mdi-check
|
||||
span {{$t('common:actions.apply')}}
|
||||
|
||||
v-card.mt-3(:class='$vuetify.theme.dark ? `grey darken-3-d5` : `white grey--text text--darken-3`')
|
||||
v-alert(color='red', :value='true', icon='mdi-alert', dark, prominent)
|
||||
span Do NOT enable these flags unless you know what you're doing!
|
||||
.caption Doing so may result in data loss or broken installation!
|
||||
v-card-text
|
||||
v-switch.mt-3(
|
||||
color='primary'
|
||||
hint='Log detailed debug info on LDAP/AD login attempts.'
|
||||
persistent-hint
|
||||
label='LDAP Debug'
|
||||
v-model='flags.ldapdebug'
|
||||
inset
|
||||
)
|
||||
v-divider.mt-3
|
||||
v-switch.mt-3(
|
||||
color='red'
|
||||
hint='Log all queries made to the database to console.'
|
||||
persistent-hint
|
||||
label='SQL Query Logging'
|
||||
v-model='flags.sqllog'
|
||||
inset
|
||||
)
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
|
||||
import flagsQuery from 'gql/admin/dev/dev-query-flags.gql'
|
||||
import flagsMutation from 'gql/admin/dev/dev-mutation-save-flags.gql'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
flags: {
|
||||
sqllog: false
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async save() {
|
||||
try {
|
||||
await this.$apollo.mutate({
|
||||
mutation: flagsMutation,
|
||||
variables: {
|
||||
flags: _.transform(this.flags, (result, value, key) => {
|
||||
result.push({ key, value })
|
||||
}, [])
|
||||
},
|
||||
watchLoading (isLoading) {
|
||||
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-dev-flags-update')
|
||||
}
|
||||
})
|
||||
this.$store.commit('showNotification', {
|
||||
style: 'success',
|
||||
message: 'Flags applied successfully.',
|
||||
icon: 'check'
|
||||
})
|
||||
} catch (err) {
|
||||
this.$store.commit('pushGraphError', err)
|
||||
}
|
||||
}
|
||||
},
|
||||
apollo: {
|
||||
flags: {
|
||||
query: flagsQuery,
|
||||
fetchPolicy: 'network-only',
|
||||
update: (data) => _.transform(data.system.flags, (result, row) => {
|
||||
_.set(result, row.key, row.value)
|
||||
}, {}),
|
||||
watchLoading (isLoading) {
|
||||
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-dev-flags-refresh')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang='scss'>
|
||||
|
||||
</style>
|
@ -1,66 +0,0 @@
|
||||
<template lang='pug'>
|
||||
v-container(fluid, grid-list-lg)
|
||||
v-layout(row, wrap)
|
||||
v-flex(xs12)
|
||||
.admin-header
|
||||
img(src='/_assets/svg/icon-web-design.svg', alt='Editor', style='width: 80px;')
|
||||
.admin-header-title
|
||||
.headline.primary--text Editor
|
||||
.subtitle-1.grey--text Configure the content editors #[v-chip(label, color='primary', small).white--text coming soon]
|
||||
v-spacer
|
||||
v-btn(outline, color='grey', @click='refresh', large)
|
||||
v-icon refresh
|
||||
v-btn(color='success', @click='save', depressed, large)
|
||||
v-icon(left) check
|
||||
span {{$t('common:actions.apply')}}
|
||||
|
||||
v-card.mt-3
|
||||
v-tabs(color='grey darken-2', fixed-tabs, slider-color='white', show-arrows, dark)
|
||||
v-tab(key='settings'): v-icon settings
|
||||
v-tab(key='code') Markdown
|
||||
|
||||
v-tab-item(key='settings', :transition='false', :reverse-transition='false')
|
||||
v-card.pa-3(flat, tile)
|
||||
.body-2.grey--text.text--darken-1 Select which editors to enable:
|
||||
.caption.grey--text.pb-2 Some editors require additional configuration in their dedicated tab (when selected).
|
||||
v-form
|
||||
v-checkbox.my-0(
|
||||
v-for='editor in editors'
|
||||
v-model='editor.isEnabled'
|
||||
:key='editor.key'
|
||||
:label='editor.title'
|
||||
color='primary'
|
||||
disabled
|
||||
hide-details
|
||||
)
|
||||
v-tab-item(key='code', :transition='false', :reverse-transition='false')
|
||||
v-card.wiki-form.pa-3(flat, tile)
|
||||
v-form
|
||||
v-subheader Editor Configuration
|
||||
.body-1.ml-3 This editor has no configuration options you can modify.
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
editors: [
|
||||
{ title: 'API Docs', key: 'api', isEnabled: false },
|
||||
{ title: 'Code', key: 'code', isEnabled: true },
|
||||
{ title: 'Markdown', key: 'markdown', isEnabled: true },
|
||||
{ title: 'Tabular', key: 'tabular', isEnabled: false },
|
||||
{ title: 'Visual Builder', key: 'visual', isEnabled: false },
|
||||
{ title: 'WikiText', key: 'wikitext', isEnabled: false }
|
||||
]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
save() {},
|
||||
refresh() {}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang='scss'>
|
||||
|
||||
</style>
|
@ -1,119 +0,0 @@
|
||||
<template lang='pug'>
|
||||
v-container(fluid, grid-list-lg)
|
||||
v-layout(row wrap)
|
||||
v-flex(xs12)
|
||||
.admin-header
|
||||
img.animated.fadeInUp(src='/_assets/svg/icon-installing-updates.svg', alt='Extensions', style='width: 80px;')
|
||||
.admin-header-title
|
||||
.headline.primary--text.animated.fadeInLeft {{ $t('admin:extensions.title') }}
|
||||
.subtitle-1.grey--text.animated.fadeInLeft {{ $t('admin:extensions.subtitle') }}
|
||||
v-form.pt-3
|
||||
v-layout(row wrap)
|
||||
v-flex(xl6 lg8 xs12)
|
||||
v-alert.mb-4(outlined, color='error', icon='mdi-alert')
|
||||
span New extensions cannot be installed at the moment. This feature is coming in a future release.
|
||||
v-expansion-panels.admin-extensions-exp(hover, popout)
|
||||
v-expansion-panel(v-for='ext of extensions', :key='`ext-` + ext.key')
|
||||
v-expansion-panel-header(disable-icon-rotate)
|
||||
span {{ext.title}}
|
||||
template(v-slot:actions)
|
||||
v-chip(label, color='success', small, v-if='ext.isInstalled') Installed
|
||||
v-chip(label, color='warning', small, v-else) Not Installed
|
||||
v-expansion-panel-content.pa-0
|
||||
v-card(flat, :class='$vuetify.theme.dark ? `grey darken-3` : `grey lighten-5`', tile)
|
||||
v-card-text
|
||||
.body-2 {{ext.description}}
|
||||
v-divider.my-4
|
||||
.body-2
|
||||
strong.mr-2 This extension is
|
||||
v-chip.mr-2(v-if='ext.isCompatible', label, outlined, small, color='success') compatible
|
||||
v-chip.mr-2(v-else, label, small, color='error') not compatible
|
||||
strong with your host.
|
||||
v-card-chin
|
||||
v-spacer
|
||||
v-btn(disabled)
|
||||
v-icon(left) mdi-plus
|
||||
span Install
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
extensions: []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async save () {
|
||||
// try {
|
||||
// await this.$apollo.mutate({
|
||||
// mutation: gql`
|
||||
// mutation (
|
||||
// $host: String!
|
||||
// ) {
|
||||
// site {
|
||||
// updateConfig(
|
||||
// host: $host
|
||||
// ) {
|
||||
// responseResult {
|
||||
// succeeded
|
||||
// errorCode
|
||||
// slug
|
||||
// message
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// `,
|
||||
// variables: {
|
||||
// host: _.get(this.config, 'host', '')
|
||||
// },
|
||||
// watchLoading (isLoading) {
|
||||
// this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-extensions-update')
|
||||
// }
|
||||
// })
|
||||
// this.$store.commit('showNotification', {
|
||||
// style: 'success',
|
||||
// message: 'Configuration saved successfully.',
|
||||
// icon: 'check'
|
||||
// })
|
||||
// } catch (err) {
|
||||
// this.$store.commit('pushGraphError', err)
|
||||
// }
|
||||
}
|
||||
},
|
||||
apollo: {
|
||||
extensions: {
|
||||
query: gql`
|
||||
{
|
||||
system {
|
||||
extensions {
|
||||
key
|
||||
title
|
||||
description
|
||||
isInstalled
|
||||
isCompatible
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
fetchPolicy: 'network-only',
|
||||
update: (data) => _.cloneDeep(data.system.extensions),
|
||||
watchLoading (isLoading) {
|
||||
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-extensions-refresh')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang='scss'>
|
||||
.admin-extensions-exp {
|
||||
.v-expansion-panel-content__wrap {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,369 +0,0 @@
|
||||
<template lang='pug'>
|
||||
v-container(fluid, grid-list-lg)
|
||||
v-layout(row wrap)
|
||||
v-flex(xs12)
|
||||
.admin-header
|
||||
img.animated.fadeInUp(src='/_assets/svg/icon-categorize.svg', alt='General', style='width: 80px;')
|
||||
.admin-header-title
|
||||
.headline.primary--text.animated.fadeInLeft {{ $t('admin:general.title') }}
|
||||
.subtitle-1.grey--text.animated.fadeInLeft {{ $t('admin:general.subtitle') }}
|
||||
v-spacer
|
||||
v-btn.animated.fadeInDown(color='success', depressed, @click='save', large)
|
||||
v-icon(left) mdi-check
|
||||
span {{$t('common:actions.apply')}}
|
||||
v-form.pt-3
|
||||
v-layout(row wrap)
|
||||
v-flex(lg6 xs12)
|
||||
v-form
|
||||
v-card.animated.fadeInUp
|
||||
v-toolbar(color='primary', dark, dense, flat)
|
||||
v-toolbar-title.subtitle-1 {{ $t('admin:general.siteInfo') }}
|
||||
.overline.grey--text.pa-4 {{$t('admin:general.general')}}
|
||||
.px-3.pb-3
|
||||
v-text-field(
|
||||
outlined
|
||||
:label='$t(`admin:general.siteUrl`)'
|
||||
required
|
||||
:counter='255'
|
||||
v-model='config.host'
|
||||
prepend-icon='mdi-label-variant-outline'
|
||||
:hint='$t(`admin:general.siteUrlHint`)'
|
||||
persistent-hint
|
||||
)
|
||||
v-text-field.mt-3(
|
||||
outlined
|
||||
:label='$t(`admin:general.siteTitle`)'
|
||||
required
|
||||
:counter='50'
|
||||
v-model='config.title'
|
||||
prepend-icon='mdi-earth'
|
||||
:hint='$t(`admin:general.siteTitleHint`)'
|
||||
persistent-hint
|
||||
)
|
||||
v-divider
|
||||
.overline.grey--text.pa-4 {{$t('admin:general.logo')}}
|
||||
.pt-2.pb-7.pl-10.pr-3
|
||||
.d-flex.align-center
|
||||
v-avatar(size='100', tile)
|
||||
v-img(
|
||||
:src='config.logoUrl'
|
||||
lazy-src='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNcWQ8AAdcBKrJda2oAAAAASUVORK5CYII='
|
||||
aspect-ratio='1'
|
||||
)
|
||||
.ml-4(style='flex: 1 1 auto;')
|
||||
v-text-field(
|
||||
outlined
|
||||
:label='$t(`admin:general.logoUrl`)'
|
||||
v-model='config.logoUrl'
|
||||
:hint='$t(`admin:general.logoUrlHint`)'
|
||||
persistent-hint
|
||||
append-icon='mdi-folder-image'
|
||||
@click:append='browseLogo'
|
||||
@keyup.enter='refreshLogo'
|
||||
)
|
||||
v-divider
|
||||
.overline.grey--text.pa-4 {{$t('admin:general.footerCopyright')}}
|
||||
.px-3.pb-3
|
||||
v-text-field(
|
||||
outlined
|
||||
:label='$t(`admin:general.companyName`)'
|
||||
v-model='config.company'
|
||||
:counter='255'
|
||||
prepend-icon='mdi-domain'
|
||||
persistent-hint
|
||||
:hint='$t(`admin:general.companyNameHint`)'
|
||||
)
|
||||
v-select.mt-3(
|
||||
outlined
|
||||
:label='$t(`admin:general.contentLicense`)'
|
||||
:items='contentLicenses'
|
||||
v-model='config.contentLicense'
|
||||
prepend-icon='mdi-creative-commons'
|
||||
:return-object='false'
|
||||
:hint='$t(`admin:general.contentLicenseHint`)'
|
||||
persistent-hint
|
||||
)
|
||||
v-divider
|
||||
.overline.grey--text.pa-4 SEO
|
||||
.px-3.pb-3
|
||||
v-text-field(
|
||||
outlined
|
||||
:label='$t(`admin:general.siteDescription`)'
|
||||
:counter='255'
|
||||
v-model='config.description'
|
||||
prepend-icon='mdi-compass'
|
||||
:hint='$t(`admin:general.siteDescriptionHint`)'
|
||||
persistent-hint
|
||||
)
|
||||
v-select.mt-3(
|
||||
outlined
|
||||
:label='$t(`admin:general.metaRobots`)'
|
||||
multiple
|
||||
:items='metaRobots'
|
||||
v-model='config.robots'
|
||||
prepend-icon='mdi-compass'
|
||||
:return-object='false'
|
||||
:hint='$t(`admin:general.metaRobotsHint`)'
|
||||
persistent-hint
|
||||
)
|
||||
|
||||
v-flex(lg6 xs12)
|
||||
v-card.animated.fadeInUp.wait-p4s
|
||||
v-toolbar(color='indigo', dark, dense, flat)
|
||||
v-toolbar-title.subtitle-1 Features
|
||||
v-card-text
|
||||
//- v-switch(
|
||||
//- inset
|
||||
//- label='Asset Image Optimization'
|
||||
//- color='indigo'
|
||||
//- v-model='config.featureTinyPNG'
|
||||
//- persistent-hint
|
||||
//- hint='Image optimization tool to reduce filesize and bandwidth costs.'
|
||||
//- disabled
|
||||
//- )
|
||||
//- v-text-field.mt-3(
|
||||
//- outlined
|
||||
//- label='TinyPNG API Key'
|
||||
//- :counter='255'
|
||||
//- v-model='config.description'
|
||||
//- prepend-icon='mdi-subdirectory-arrow-right'
|
||||
//- hint='Get your API key at https://tinypng.com/developers'
|
||||
//- persistent-hint
|
||||
//- disabled
|
||||
//- )
|
||||
|
||||
//- v-divider.mt-3
|
||||
//- v-switch(
|
||||
//- inset
|
||||
//- label='Page Ratings'
|
||||
//- color='indigo'
|
||||
//- v-model='config.featurePageRatings'
|
||||
//- persistent-hint
|
||||
//- hint='Allow users to rate pages.'
|
||||
//- disabled
|
||||
//- )
|
||||
|
||||
//- v-divider.mt-3
|
||||
v-switch(
|
||||
inset
|
||||
label='Comments'
|
||||
color='indigo'
|
||||
v-model='config.featurePageComments'
|
||||
persistent-hint
|
||||
hint='Allow users to leave comments on pages.'
|
||||
)
|
||||
|
||||
//- v-divider.mt-3
|
||||
//- v-switch(
|
||||
//- inset
|
||||
//- label='Personal Wikis'
|
||||
//- color='indigo'
|
||||
//- v-model='config.featurePersonalWikis'
|
||||
//- persistent-hint
|
||||
//- hint='Allow users to have their own personal wiki.'
|
||||
//- disabled
|
||||
//- )
|
||||
|
||||
component(:is='activeModal')
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import { sync } from 'vuex-pathify'
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
import editorStore from '../../store/editor'
|
||||
|
||||
/* global WIKI */
|
||||
|
||||
const titleRegex = /[<>"]/i
|
||||
|
||||
WIKI.$store.registerModule('editor', editorStore)
|
||||
|
||||
export default {
|
||||
i18nOptions: { namespaces: 'editor' },
|
||||
components: {
|
||||
editorModalMedia: () => import(/* webpackChunkName: "editor", webpackMode: "lazy" */ '../editor/editor-modal-media.vue')
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
config: {
|
||||
host: '',
|
||||
title: '',
|
||||
description: '',
|
||||
robots: [],
|
||||
analyticsService: '',
|
||||
analyticsId: '',
|
||||
company: '',
|
||||
contentLicense: '',
|
||||
logoUrl: '',
|
||||
featureAnalytics: false,
|
||||
featurePageRatings: false,
|
||||
featurePageComments: false,
|
||||
featurePersonalWikis: false,
|
||||
featureTinyPNG: false
|
||||
},
|
||||
metaRobots: [
|
||||
{ text: 'Index', value: 'index' },
|
||||
{ text: 'Follow', value: 'follow' },
|
||||
{ text: 'No Index', value: 'noindex' },
|
||||
{ text: 'No Follow', value: 'nofollow' }
|
||||
]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
siteTitle: sync('site/title'),
|
||||
logoUrl: sync('site/logoUrl'),
|
||||
company: sync('site/company'),
|
||||
contentLicense: sync('site/contentLicense'),
|
||||
activeModal: sync('editor/activeModal'),
|
||||
contentLicenses () {
|
||||
return [
|
||||
{ value: '', text: this.$t('common:license.none') },
|
||||
{ value: 'alr', text: this.$t('common:license.alr') },
|
||||
{ value: 'cc0', text: this.$t('common:license.cc0') },
|
||||
{ value: 'ccby', text: this.$t('common:license.ccby') },
|
||||
{ value: 'ccbysa', text: this.$t('common:license.ccbysa') },
|
||||
{ value: 'ccbynd', text: this.$t('common:license.ccbynd') },
|
||||
{ value: 'ccbync', text: this.$t('common:license.ccbync') },
|
||||
{ value: 'ccbyncsa', text: this.$t('common:license.ccbyncsa') },
|
||||
{ value: 'ccbyncnd', text: this.$t('common:license.ccbyncnd') }
|
||||
]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async save () {
|
||||
const title = _.get(this.config, 'title', '')
|
||||
if (titleRegex.test(title)) {
|
||||
this.$store.commit('showNotification', {
|
||||
style: 'error',
|
||||
message: this.$t('admin:general.siteTitleInvalidChars'),
|
||||
icon: 'alert'
|
||||
})
|
||||
return
|
||||
}
|
||||
try {
|
||||
await this.$apollo.mutate({
|
||||
mutation: gql`
|
||||
mutation (
|
||||
$host: String!
|
||||
$title: String!
|
||||
$description: String!
|
||||
$robots: [String]!
|
||||
$analyticsService: String!
|
||||
$analyticsId: String!
|
||||
$company: String!
|
||||
$contentLicense: String!
|
||||
$logoUrl: String!
|
||||
$featurePageRatings: Boolean!
|
||||
$featurePageComments: Boolean!
|
||||
$featurePersonalWikis: Boolean!
|
||||
) {
|
||||
site {
|
||||
updateConfig(
|
||||
host: $host,
|
||||
title: $title,
|
||||
description: $description,
|
||||
robots: $robots,
|
||||
analyticsService: $analyticsService,
|
||||
analyticsId: $analyticsId,
|
||||
company: $company,
|
||||
contentLicense: $contentLicense,
|
||||
logoUrl: $logoUrl,
|
||||
featurePageRatings: $featurePageRatings,
|
||||
featurePageComments: $featurePageComments,
|
||||
featurePersonalWikis: $featurePersonalWikis
|
||||
) {
|
||||
responseResult {
|
||||
succeeded
|
||||
errorCode
|
||||
slug
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
host: _.get(this.config, 'host', ''),
|
||||
title: _.get(this.config, 'title', ''),
|
||||
description: _.get(this.config, 'description', ''),
|
||||
robots: _.get(this.config, 'robots', []),
|
||||
analyticsService: _.get(this.config, 'analyticsService', ''),
|
||||
analyticsId: _.get(this.config, 'analyticsId', ''),
|
||||
company: _.get(this.config, 'company', ''),
|
||||
contentLicense: _.get(this.config, 'contentLicense', ''),
|
||||
logoUrl: _.get(this.config, 'logoUrl', ''),
|
||||
featurePageRatings: _.get(this.config, 'featurePageRatings', false),
|
||||
featurePageComments: _.get(this.config, 'featurePageComments', false),
|
||||
featurePersonalWikis: _.get(this.config, 'featurePersonalWikis', false)
|
||||
},
|
||||
watchLoading (isLoading) {
|
||||
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-site-update')
|
||||
}
|
||||
})
|
||||
this.$store.commit('showNotification', {
|
||||
style: 'success',
|
||||
message: this.$t('admin:general.saveSuccess'),
|
||||
icon: 'check'
|
||||
})
|
||||
this.siteTitle = this.config.title
|
||||
this.company = this.config.company
|
||||
this.contentLicense = this.config.contentLicense
|
||||
this.logoUrl = this.config.logoUrl
|
||||
} catch (err) {
|
||||
this.$store.commit('pushGraphError', err)
|
||||
}
|
||||
},
|
||||
browseLogo () {
|
||||
this.$store.set('editor/editorKey', 'common')
|
||||
this.activeModal = 'editorModalMedia'
|
||||
},
|
||||
refreshLogo () {
|
||||
this.$forceUpdate()
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.$root.$on('editorInsert', opts => {
|
||||
this.config.logoUrl = opts.path
|
||||
})
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.$root.$off('editorInsert')
|
||||
},
|
||||
apollo: {
|
||||
config: {
|
||||
query: gql`
|
||||
{
|
||||
site {
|
||||
config {
|
||||
host
|
||||
title
|
||||
description
|
||||
robots
|
||||
analyticsService
|
||||
analyticsId
|
||||
company
|
||||
contentLicense
|
||||
logoUrl
|
||||
featurePageRatings
|
||||
featurePageComments
|
||||
featurePersonalWikis
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
fetchPolicy: 'network-only',
|
||||
update: (data) => _.cloneDeep(data.site.config),
|
||||
watchLoading (isLoading) {
|
||||
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-site-refresh')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang='scss'>
|
||||
|
||||
</style>
|
@ -1,224 +0,0 @@
|
||||
<template lang="pug">
|
||||
v-card(flat)
|
||||
v-container.px-3.pb-3.pt-3(fluid, grid-list-md)
|
||||
v-layout(row, wrap)
|
||||
v-flex(xs12, v-if='group.isSystem')
|
||||
v-alert.radius-7.mb-0(
|
||||
color='orange darken-2'
|
||||
:class='$vuetify.theme.dark ? "grey darken-4" : "orange lighten-5"'
|
||||
outlined
|
||||
:value='true'
|
||||
icon='mdi-lock-outline'
|
||||
) This is a system group. Some permissions cannot be modified.
|
||||
v-flex(xs12, md6, lg4, v-for='pmGroup in permissions', :key='pmGroup.category')
|
||||
v-card.md2(flat, :class='$vuetify.theme.dark ? "grey darken-3-d5" : "grey lighten-5"')
|
||||
.overline.px-5.pt-5.pb-3.grey--text.text--darken-2 {{pmGroup.category}}
|
||||
v-card-text.pt-0
|
||||
template(v-for='(pm, idx) in pmGroup.items')
|
||||
v-checkbox.pt-0(
|
||||
style='justify-content: space-between;'
|
||||
:key='pm.permission'
|
||||
:label='pm.permission'
|
||||
:hint='pm.hint'
|
||||
persistent-hint
|
||||
color='primary'
|
||||
v-model='group.permissions'
|
||||
:value='pm.permission'
|
||||
:append-icon='pm.warning ? "mdi-alert" : null',
|
||||
:disabled='(group.isSystem && pm.restrictedForSystem) || group.id === 1 || pm.disabled'
|
||||
)
|
||||
v-divider.mt-3(v-if='idx < pmGroup.items.length - 1')
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
permissions: [
|
||||
{
|
||||
category: 'Content',
|
||||
items: [
|
||||
{
|
||||
permission: 'read:pages',
|
||||
hint: 'Can view pages, as specified in the Page Rules',
|
||||
warning: false,
|
||||
restrictedForSystem: false,
|
||||
disabled: false
|
||||
},
|
||||
{
|
||||
permission: 'write:pages',
|
||||
hint: 'Can create / edit pages, as specified in the Page Rules',
|
||||
warning: false,
|
||||
restrictedForSystem: true,
|
||||
disabled: false
|
||||
},
|
||||
{
|
||||
permission: 'manage:pages',
|
||||
hint: 'Can move existing pages as specified in the Page Rules',
|
||||
warning: false,
|
||||
restrictedForSystem: true,
|
||||
disabled: false
|
||||
},
|
||||
{
|
||||
permission: 'delete:pages',
|
||||
hint: 'Can delete existing pages, as specified in the Page Rules',
|
||||
warning: false,
|
||||
restrictedForSystem: true,
|
||||
disabled: false
|
||||
},
|
||||
{
|
||||
permission: 'write:styles',
|
||||
hint: 'Can insert CSS styles in pages, as specified in the Page Rules',
|
||||
warning: false,
|
||||
restrictedForSystem: true,
|
||||
disabled: false
|
||||
},
|
||||
{
|
||||
permission: 'write:scripts',
|
||||
hint: 'Can insert JavaScript in pages, as specified in the Page Rules',
|
||||
warning: false,
|
||||
restrictedForSystem: true,
|
||||
disabled: false
|
||||
},
|
||||
{
|
||||
permission: 'read:source',
|
||||
hint: 'Can view pages source, as specified in the Page Rules',
|
||||
warning: false,
|
||||
restrictedForSystem: false,
|
||||
disabled: false
|
||||
},
|
||||
{
|
||||
permission: 'read:history',
|
||||
hint: 'Can view pages history, as specified in the Page Rules',
|
||||
warning: false,
|
||||
restrictedForSystem: false,
|
||||
disabled: false
|
||||
},
|
||||
{
|
||||
permission: 'read:assets',
|
||||
hint: 'Can view / use assets (such as images and files), as specified in the Page Rules',
|
||||
warning: false,
|
||||
restrictedForSystem: false,
|
||||
disabled: false
|
||||
},
|
||||
{
|
||||
permission: 'write:assets',
|
||||
hint: 'Can upload new assets (such as images and files), as specified in the Page Rules',
|
||||
warning: false,
|
||||
restrictedForSystem: true,
|
||||
disabled: false
|
||||
},
|
||||
{
|
||||
permission: 'manage:assets',
|
||||
hint: 'Can edit and delete existing assets (such as images and files), as specified in the Page Rules',
|
||||
warning: false,
|
||||
restrictedForSystem: true,
|
||||
disabled: false
|
||||
},
|
||||
{
|
||||
permission: 'read:comments',
|
||||
hint: 'Can view comments, as specified in the Page Rules',
|
||||
warning: false,
|
||||
restrictedForSystem: false,
|
||||
disabled: false
|
||||
},
|
||||
{
|
||||
permission: 'write:comments',
|
||||
hint: 'Can post new comments, as specified in the Page Rules',
|
||||
warning: false,
|
||||
restrictedForSystem: false,
|
||||
disabled: false
|
||||
},
|
||||
{
|
||||
permission: 'manage:comments',
|
||||
hint: 'Can edit and delete existing comments, as specified in the Page Rules',
|
||||
warning: false,
|
||||
restrictedForSystem: true,
|
||||
disabled: false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
category: 'Users',
|
||||
items: [
|
||||
{
|
||||
permission: 'write:users',
|
||||
hint: 'Can create or authorize new users, but not modify existing ones',
|
||||
warning: false,
|
||||
restrictedForSystem: true,
|
||||
disabled: false
|
||||
},
|
||||
{
|
||||
permission: 'manage:users',
|
||||
hint: 'Can manage all users (but not users with administrative permissions)',
|
||||
warning: false,
|
||||
restrictedForSystem: true,
|
||||
disabled: false
|
||||
},
|
||||
{
|
||||
permission: 'write:groups',
|
||||
hint: 'Can manage groups and assign CONTENT permissions / page rules',
|
||||
warning: false,
|
||||
restrictedForSystem: true,
|
||||
disabled: false
|
||||
},
|
||||
{
|
||||
permission: 'manage:groups',
|
||||
hint: 'Can manage groups and assign ANY permissions (but not manage:system) / page rules',
|
||||
warning: true,
|
||||
restrictedForSystem: true,
|
||||
disabled: false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
category: 'Administration',
|
||||
items: [
|
||||
{
|
||||
permission: 'manage:navigation',
|
||||
hint: 'Can manage the site navigation',
|
||||
warning: false,
|
||||
restrictedForSystem: true,
|
||||
disabled: false
|
||||
},
|
||||
{
|
||||
permission: 'manage:theme',
|
||||
hint: 'Can manage and modify themes',
|
||||
warning: false,
|
||||
restrictedForSystem: true,
|
||||
disabled: false
|
||||
},
|
||||
{
|
||||
permission: 'manage:api',
|
||||
hint: 'Can generate and revoke API keys',
|
||||
warning: true,
|
||||
restrictedForSystem: true,
|
||||
disabled: false
|
||||
},
|
||||
{
|
||||
permission: 'manage:system',
|
||||
hint: 'Can manage and access everything. Root administrator.',
|
||||
warning: true,
|
||||
restrictedForSystem: true,
|
||||
disabled: true
|
||||
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
group: {
|
||||
get() { return this.value },
|
||||
set(val) { this.$set('input', val) }
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
@ -1,336 +0,0 @@
|
||||
<template lang="pug">
|
||||
v-card(flat)
|
||||
v-card-text(v-if='group.id === 1')
|
||||
v-alert.radius-7.mb-0(
|
||||
:class='$vuetify.theme.dark ? "grey darken-4" : "orange lighten-5"'
|
||||
color='orange darken-2'
|
||||
outlined
|
||||
icon='mdi-lock-outline'
|
||||
) This group has access to everything.
|
||||
template(v-else)
|
||||
v-card-title(:class='$vuetify.theme.dark ? `grey darken-3-d5` : ``')
|
||||
v-alert.radius-7.caption(
|
||||
:class='$vuetify.theme.dark ? `grey darken-3-d3` : `grey lighten-4`'
|
||||
color='grey'
|
||||
outlined
|
||||
icon='mdi-information'
|
||||
) You must enable global content permissions (under Permissions tab) for page rules to have any effect.
|
||||
v-spacer
|
||||
v-btn.mx-2(depressed, color='primary', @click='addRule')
|
||||
v-icon(left) mdi-plus
|
||||
| Add Rule
|
||||
v-menu(
|
||||
right
|
||||
offset-y
|
||||
nudge-left='115'
|
||||
)
|
||||
template(v-slot:activator='{ on }')
|
||||
v-btn.is-icon(v-on='on', outlined, color='primary')
|
||||
v-icon mdi-dots-horizontal
|
||||
v-list(dense)
|
||||
v-list-item(@click='comingSoon')
|
||||
v-list-item-avatar
|
||||
v-icon mdi-application-import
|
||||
v-list-item-title Load Preset
|
||||
v-divider
|
||||
v-list-item(@click='comingSoon')
|
||||
v-list-item-avatar
|
||||
v-icon mdi-application-export
|
||||
v-list-item-title Save As Preset
|
||||
v-divider
|
||||
v-list-item(@click='comingSoon')
|
||||
v-list-item-avatar
|
||||
v-icon mdi-cloud-upload
|
||||
v-list-item-title Import Rules
|
||||
v-divider
|
||||
v-list-item(@click='comingSoon')
|
||||
v-list-item-avatar
|
||||
v-icon mdi-cloud-download
|
||||
v-list-item-title Export Rules
|
||||
v-card-text(:class='$vuetify.theme.dark ? `grey darken-4-l5` : `white`')
|
||||
.rules
|
||||
.caption(v-if='group.pageRules.length === 0')
|
||||
em(:class='$vuetify.theme.dark ? `grey--text` : `blue-grey--text`') This group has no page rules yet.
|
||||
.rule(v-for='rule of group.pageRules', :key='rule.id')
|
||||
v-btn.ma-0.radius-4.rule-deny-btn(
|
||||
solo
|
||||
:color='rule.deny ? "red" : "green"'
|
||||
dark
|
||||
@click='rule.deny = !rule.deny'
|
||||
height='48'
|
||||
)
|
||||
v-icon(v-if='rule.deny') mdi-cancel
|
||||
v-icon(v-else) mdi-check-circle
|
||||
//- Roles
|
||||
v-select.ml-1(
|
||||
solo
|
||||
:items='roles'
|
||||
v-model='rule.roles'
|
||||
placeholder='Select Role(s)...'
|
||||
hide-details
|
||||
multiple
|
||||
chips
|
||||
deletable-chips
|
||||
small-chips
|
||||
height='48px'
|
||||
style='flex: 0 1 440px;'
|
||||
:menu-props='{ "maxHeight": 500 }'
|
||||
clearable
|
||||
dense
|
||||
)
|
||||
template(slot='selection', slot-scope='{ item, index }')
|
||||
v-chip.white--text.ml-0(v-if='index <= 1', small, label, :color='rule.deny ? `red` : `green`').caption {{ item.value }}
|
||||
v-chip.white--text.ml-0(v-if='index === 2', small, label, :color='rule.deny ? `red lighten-2` : `green lighten-2`').caption + {{ rule.roles.length - 2 }} more
|
||||
template(slot='item', slot-scope='props')
|
||||
v-list-item-action(style='min-width: 30px;')
|
||||
v-checkbox(
|
||||
v-model='props.attrs.inputValue'
|
||||
hide-details
|
||||
color='primary'
|
||||
)
|
||||
v-icon.mr-2(:color='rule.deny ? `red` : `green`') {{props.item.icon}}
|
||||
v-list-item-content
|
||||
v-list-item-title.body-2 {{props.item.text}}
|
||||
v-chip.mr-2.grey--text(label, small, :color='$vuetify.theme.dark ? `grey darken-4` : `grey lighten-4`').caption {{props.item.value}}
|
||||
|
||||
//- Match
|
||||
v-select.ml-1.mr-1(
|
||||
solo
|
||||
:items='matches'
|
||||
v-model='rule.match'
|
||||
placeholder='Match...'
|
||||
hide-details
|
||||
height='48px'
|
||||
style='flex: 0 1 250px;'
|
||||
dense
|
||||
)
|
||||
template(slot='selection', slot-scope='{ item, index }')
|
||||
.body-2 {{item.text}}
|
||||
template(slot='item', slot-scope='data')
|
||||
v-list-item-avatar
|
||||
v-avatar.white--text.radius-4(color='blue', size='30', tile) {{ data.item.icon }}
|
||||
v-list-item-content
|
||||
v-list-item-title(v-html='data.item.text')
|
||||
//- Locales
|
||||
v-select.mr-1(
|
||||
:background-color='$vuetify.theme.dark ? `grey darken-3-d5` : `blue-grey lighten-5`'
|
||||
solo
|
||||
:items='locales'
|
||||
v-model='rule.locales'
|
||||
placeholder='Any Locale'
|
||||
item-value='code'
|
||||
item-text='name'
|
||||
multiple
|
||||
hide-details
|
||||
height='48px'
|
||||
dense
|
||||
:menu-props='{ "minWidth": 250 }'
|
||||
style='flex: 0 1 150px;'
|
||||
)
|
||||
template(slot='selection', slot-scope='{ item, index }')
|
||||
v-chip.white--text.ml-0(v-if='rule.locales.length === 1', small, label, :color='rule.deny ? `red` : `green`').caption {{ item.code.toUpperCase() }}
|
||||
v-chip.white--text.ml-0(v-else-if='index === 0', small, label, :color='rule.deny ? `red` : `green`').caption {{ rule.locales.length }} locales
|
||||
v-list-item(slot='prepend-item', @click='rule.locales = []')
|
||||
v-list-item-action(style='min-width: 30px;')
|
||||
v-checkbox(
|
||||
:input-value='rule.locales.length === 0'
|
||||
hide-details
|
||||
color='primary'
|
||||
readonly
|
||||
)
|
||||
v-icon.mr-2(:color='rule.deny ? `red` : `green`') mdi-earth
|
||||
v-list-item-content
|
||||
v-list-item-title.body-2 Any Locale
|
||||
v-divider(slot='prepend-item')
|
||||
template(slot='item', slot-scope='props')
|
||||
v-list-item-action(style='min-width: 30px;')
|
||||
v-checkbox(
|
||||
v-model='props.attrs.inputValue'
|
||||
hide-details
|
||||
color='primary'
|
||||
)
|
||||
v-icon.mr-2(:color='rule.deny ? `red` : `green`') mdi-web
|
||||
v-list-item-content
|
||||
v-list-item-title.body-2 {{props.item.name}}
|
||||
v-chip.mr-2.grey--text(label, small, :color='$vuetify.theme.dark ? `grey darken-4` : `grey lighten-4`').caption {{props.item.code.toUpperCase()}}
|
||||
|
||||
//- Path
|
||||
v-text-field(
|
||||
solo
|
||||
v-model='rule.path'
|
||||
label='Path'
|
||||
:prefix='(rule.match !== `END` && rule.match !== `TAG`) ? `/` : null'
|
||||
:placeholder='rule.match === `REGEX` ? `Regular Expression` : rule.match === `TAG` ? `Tag` : `Path`'
|
||||
:suffix='rule.match === `REGEX` ? `/` : null'
|
||||
hide-details
|
||||
:color='$vuetify.theme.dark ? `grey` : `blue-grey`'
|
||||
)
|
||||
|
||||
v-btn.ml-2(icon, @click='removeRule(rule.id)', small)
|
||||
v-icon(:color='$vuetify.theme.dark ? `grey` : `blue-grey`') mdi-close
|
||||
|
||||
v-divider.mt-3
|
||||
.overline.py-3 Rules Order
|
||||
.body-2.pl-3 Rules are applied in order of path specificity. A more precise path will always override a less defined path.
|
||||
.body-2.pl-5 For example, #[span.teal--text /geography/countries] will override #[span.teal--text /geography].
|
||||
.body-2.pl-3.pt-2 When 2 rules have the same specificity, the priority is given from lowest to highest as follows:
|
||||
.body-2.pl-3.pt-1
|
||||
ul
|
||||
li
|
||||
strong Path Starts With...
|
||||
em.caption.pl-1 (lowest)
|
||||
li
|
||||
strong Path Ends With...
|
||||
li
|
||||
strong Path Matches Regex...
|
||||
li
|
||||
strong Tag Matches...
|
||||
li
|
||||
strong Path Is Exactly...
|
||||
em.caption.pl-1 (highest)
|
||||
.body-2.pl-3.pt-2 When 2 rules have the same path specificity AND the same match type, #[strong.red--text DENY] will always override an #[strong.green--text ALLOW] rule.
|
||||
v-divider.mt-3
|
||||
.overline.py-3 Regular Expressions
|
||||
span Expressions that are deemed unsafe or could result in exponential time processing will be rejected upon saving.
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import { customAlphabet } from 'nanoid/non-secure'
|
||||
|
||||
/* global siteLangs */
|
||||
|
||||
const nanoid = customAlphabet('1234567890abcdef', 10)
|
||||
|
||||
export default {
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
roles: [
|
||||
{ text: 'Read Pages', value: 'read:pages', icon: 'mdi-file-eye-outline' },
|
||||
{ text: 'Create Pages', value: 'write:pages', icon: 'mdi-file-plus-outline' },
|
||||
{ text: 'Edit + Move Pages', value: 'manage:pages', icon: 'mdi-file-document-edit-outline' },
|
||||
{ text: 'Delete Pages', value: 'delete:pages', icon: 'mdi-file-remove-outline' },
|
||||
{ text: 'View Pages Source', value: 'read:source', icon: 'mdi-code-tags' },
|
||||
{ text: 'View Pages History', value: 'read:history', icon: 'mdi-history' },
|
||||
{ text: 'Read / Use Assets', value: 'read:assets', icon: 'mdi-image-search-outline' },
|
||||
{ text: 'Upload Assets', value: 'write:assets', icon: 'mdi-image-plus' },
|
||||
{ text: 'Edit + Delete Assets', value: 'manage:assets', icon: 'mdi-image-size-select-large' },
|
||||
{ text: 'Edit Scripts', value: 'write:scripts', icon: 'mdi-language-javascript' },
|
||||
{ text: 'Edit Styles', value: 'write:styles', icon: 'mdi-language-css3' },
|
||||
{ text: 'Read Comments', value: 'read:comments', icon: 'mdi-comment-search-outline' },
|
||||
{ text: 'Create Comments', value: 'write:comments', icon: 'mdi-comment-plus-outline' },
|
||||
{ text: 'Edit + Delete Comments', value: 'manage:comments', icon: 'mdi-comment-remove-outline' }
|
||||
],
|
||||
matches: [
|
||||
{ text: 'Path Starts With...', value: 'START', icon: '/...' },
|
||||
{ text: 'Path is Exactly...', value: 'EXACT', icon: '=' },
|
||||
{ text: 'Path Ends With...', value: 'END', icon: '.../' },
|
||||
{ text: 'Path Matches Regex...', value: 'REGEX', icon: '$.*' },
|
||||
{ text: 'Tag Matches...', value: 'TAG', icon: 'T' }
|
||||
]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
group: {
|
||||
get() { return this.value },
|
||||
set(val) { this.$set('input', val) }
|
||||
},
|
||||
locales() { return siteLangs }
|
||||
},
|
||||
methods: {
|
||||
addRule(group) {
|
||||
this.group.pageRules.push({
|
||||
id: nanoid(),
|
||||
path: '',
|
||||
roles: [],
|
||||
match: 'START',
|
||||
deny: false,
|
||||
locales: []
|
||||
})
|
||||
},
|
||||
removeRule(ruleId) {
|
||||
this.group.pageRules.splice(_.findIndex(this.group.pageRules, ['id', ruleId]), 1)
|
||||
},
|
||||
comingSoon() {
|
||||
this.$store.commit('showNotification', {
|
||||
style: 'indigo',
|
||||
message: `Coming soon...`,
|
||||
icon: 'directions_boat'
|
||||
})
|
||||
},
|
||||
dude (stuff) {
|
||||
console.info(stuff)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.rules {
|
||||
background-color: mc('blue-grey', '50');
|
||||
border-radius: 4px;
|
||||
padding: 1rem;
|
||||
position: relative;
|
||||
|
||||
@at-root .v-application.theme--dark & {
|
||||
background-color: mc('grey', '800');
|
||||
}
|
||||
}
|
||||
|
||||
.rule {
|
||||
display: flex;
|
||||
background-color: mc('blue-grey', '100');
|
||||
border-radius: 4px;
|
||||
padding: .5rem;
|
||||
align-items: center;
|
||||
|
||||
&-enter-active, &-leave-active {
|
||||
transition: all .5s ease;
|
||||
}
|
||||
&-enter, &-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@at-root .v-application.theme--dark & {
|
||||
background-color: mc('grey', '700');
|
||||
}
|
||||
|
||||
& + .rule {
|
||||
margin-top: .5rem;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '+';
|
||||
position: absolute;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-weight: 600;
|
||||
color: mc('blue-grey', '700');
|
||||
font-size: 1.25rem;
|
||||
background-color: mc('blue-grey', '50');
|
||||
left: -2rem;
|
||||
top: -1.3rem;
|
||||
|
||||
@at-root .v-application.theme--dark & {
|
||||
background-color: mc('grey', '800');
|
||||
color: mc('grey', '600');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.input-group + * {
|
||||
margin-left: .5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,149 +0,0 @@
|
||||
<template lang="pug">
|
||||
v-card(flat)
|
||||
v-card-title.pb-4(:class='$vuetify.theme.dark ? `grey darken-3-d3` : `grey lighten-5`')
|
||||
v-text-field(
|
||||
outlined
|
||||
flat
|
||||
prepend-inner-icon='mdi-magnify'
|
||||
v-model='search'
|
||||
label='Search Group Users...'
|
||||
hide-details
|
||||
dense
|
||||
style='max-width: 450px;'
|
||||
)
|
||||
v-spacer
|
||||
v-btn(color='primary', depressed, @click='searchUserDialog = true', :disabled='group.id === 2')
|
||||
v-icon(left) mdi-clipboard-account
|
||||
| Assign User
|
||||
v-data-table(
|
||||
:items='group.users',
|
||||
:headers='headers',
|
||||
:search='search'
|
||||
:page.sync='pagination'
|
||||
:items-per-page='15'
|
||||
@page-count='pageCount = $event'
|
||||
must-sort,
|
||||
hide-default-footer
|
||||
)
|
||||
template(v-slot:item.actions='{ item }')
|
||||
v-menu(bottom, right, min-width='200')
|
||||
template(v-slot:activator='{ on }')
|
||||
v-btn(icon, v-on='on', small)
|
||||
v-icon.grey--text.text--darken-1 mdi-dots-horizontal
|
||||
v-list(dense, nav)
|
||||
v-list-item(:to='`/users/` + item.id')
|
||||
v-list-item-action: v-icon(color='primary') mdi-account-outline
|
||||
v-list-item-content
|
||||
v-list-item-title View User Profile
|
||||
template(v-if='item.id !== 2')
|
||||
v-list-item(@click='unassignUser(item.id)')
|
||||
v-list-item-action: v-icon(color='orange') mdi-account-remove-outline
|
||||
v-list-item-content
|
||||
v-list-item-title Unassign
|
||||
template(slot='no-data')
|
||||
v-alert.ma-3(icon='mdi-alert', outlined) No users to display.
|
||||
.text-center.py-2(v-if='group.users.length > 15')
|
||||
v-pagination(v-model='pagination', :length='pageCount')
|
||||
|
||||
user-search(v-model='searchUserDialog', @select='assignUser')
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import UserSearch from '../common/user-search.vue'
|
||||
|
||||
import assignUserMutation from 'gql/admin/groups/groups-mutation-assign.gql'
|
||||
import unassignUserMutation from 'gql/admin/groups/groups-mutation-unassign.gql'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
components: {
|
||||
UserSearch
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
headers: [
|
||||
{ text: 'ID', value: 'id', width: 70 },
|
||||
{ text: 'Name', value: 'name' },
|
||||
{ text: 'Email', value: 'email' },
|
||||
{ text: 'Actions', value: 'actions', sortable: false, width: 50 }
|
||||
],
|
||||
searchUserDialog: false,
|
||||
pagination: 1,
|
||||
pageCount: 0,
|
||||
search: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
group: {
|
||||
get() { return this.value },
|
||||
set(val) { this.$set('input', val) }
|
||||
},
|
||||
pages () {
|
||||
if (this.pagination.rowsPerPage == null || this.pagination.totalItems == null) {
|
||||
return 0
|
||||
}
|
||||
|
||||
return Math.ceil(this.pagination.totalItems / this.pagination.rowsPerPage)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async assignUser({ id, email, name }) {
|
||||
try {
|
||||
await this.$apollo.mutate({
|
||||
mutation: assignUserMutation,
|
||||
variables: {
|
||||
groupId: this.group.id,
|
||||
userId: id
|
||||
},
|
||||
watchLoading (isLoading) {
|
||||
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-groups-assign')
|
||||
}
|
||||
})
|
||||
this.$store.commit('showNotification', {
|
||||
style: 'success',
|
||||
message: `User has been assigned to ${this.group.name}.`,
|
||||
icon: 'assignment_ind'
|
||||
})
|
||||
this.$emit('refresh')
|
||||
} catch (err) {
|
||||
this.$store.commit('showNotification', {
|
||||
style: 'red',
|
||||
message: err.message,
|
||||
icon: 'warning'
|
||||
})
|
||||
}
|
||||
},
|
||||
async unassignUser(id) {
|
||||
try {
|
||||
await this.$apollo.mutate({
|
||||
mutation: unassignUserMutation,
|
||||
variables: {
|
||||
groupId: this.group.id,
|
||||
userId: id
|
||||
},
|
||||
watchLoading (isLoading) {
|
||||
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-groups-unassign')
|
||||
}
|
||||
})
|
||||
this.$store.commit('showNotification', {
|
||||
style: 'success',
|
||||
message: `User has been unassigned from ${this.group.name}.`,
|
||||
icon: 'assignment_ind'
|
||||
})
|
||||
this.$emit('refresh')
|
||||
} catch (err) {
|
||||
this.$store.commit('showNotification', {
|
||||
style: 'red',
|
||||
message: err.message,
|
||||
icon: 'warning'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
@ -1,271 +0,0 @@
|
||||
<template lang='pug'>
|
||||
v-container(fluid, grid-list-lg)
|
||||
v-layout(row wrap)
|
||||
v-flex(xs12)
|
||||
.admin-header
|
||||
img(src='/_assets/svg/icon-social-group.svg', alt='Edit Group', style='width: 80px;')
|
||||
.admin-header-title
|
||||
.headline.blue--text.text--darken-2 Edit Group
|
||||
.subtitle-1.grey--text {{group.name}}
|
||||
v-spacer
|
||||
v-btn(color='grey', icon, outlined, to='/groups')
|
||||
v-icon mdi-arrow-left
|
||||
v-dialog(v-model='deleteGroupDialog', max-width='500', v-if='!group.isSystem')
|
||||
template(v-slot:activator='{ on }')
|
||||
v-btn.ml-3(color='red', icon, outlined, v-on='on')
|
||||
v-icon(color='red') mdi-trash-can-outline
|
||||
v-card
|
||||
.dialog-header.is-red Delete Group?
|
||||
v-card-text.pa-4 Are you sure you want to delete group #[strong {{ group.name }}]? All users will be unassigned from this group.
|
||||
v-card-actions
|
||||
v-spacer
|
||||
v-btn(text, @click='deleteGroupDialog = false') Cancel
|
||||
v-btn(color='red', dark, @click='deleteGroup') Delete
|
||||
v-btn.ml-3(color='success', large, depressed, @click='updateGroup')
|
||||
v-icon(left) mdi-check
|
||||
span Update Group
|
||||
v-card.mt-3
|
||||
v-tabs.grad-tabs(v-model='tab', :color='$vuetify.theme.dark ? `blue` : `primary`', fixed-tabs, show-arrows, icons-and-text)
|
||||
v-tab(key='settings')
|
||||
span Settings
|
||||
v-icon mdi-cog-box
|
||||
v-tab(key='permissions')
|
||||
span Permissions
|
||||
v-icon mdi-lock-pattern
|
||||
v-tab(key='rules')
|
||||
span Page Rules
|
||||
v-icon mdi-file-lock
|
||||
v-tab(key='users')
|
||||
span Users
|
||||
v-icon mdi-account-group
|
||||
|
||||
v-tab-item(key='settings', :transition='false', :reverse-transition='false')
|
||||
v-card(flat)
|
||||
template(v-if='group.id <= 2')
|
||||
v-card-text
|
||||
v-alert.radius-7.mb-0(
|
||||
color='orange darken-2'
|
||||
:class='$vuetify.theme.dark ? "grey darken-4" : "orange lighten-5"'
|
||||
outlined
|
||||
:value='true'
|
||||
icon='mdi-lock-outline'
|
||||
) This is a system group and its settings cannot be modified.
|
||||
v-divider
|
||||
v-card-text
|
||||
v-text-field(
|
||||
outlined
|
||||
v-model='group.name'
|
||||
label='Group Name'
|
||||
hide-details
|
||||
prepend-icon='mdi-account-group'
|
||||
style='max-width: 600px;'
|
||||
:disabled='group.id <= 2'
|
||||
)
|
||||
template(v-if='group.id !== 2')
|
||||
v-divider
|
||||
v-card-text
|
||||
v-text-field(
|
||||
outlined
|
||||
v-model='group.redirectOnLogin'
|
||||
label='Redirect on Login'
|
||||
persistent-hint
|
||||
hint='The path / URL where the user will be redirected upon successful login.'
|
||||
prepend-icon='mdi-arrow-top-left-thick'
|
||||
append-icon='mdi-folder-search'
|
||||
@click:append='selectPage'
|
||||
style='max-width: 850px;'
|
||||
:counter='255'
|
||||
)
|
||||
|
||||
v-tab-item(key='permissions', :transition='false', :reverse-transition='false')
|
||||
group-permissions(v-model='group', @refresh='refresh')
|
||||
|
||||
v-tab-item(key='rules', :transition='false', :reverse-transition='false')
|
||||
group-rules(v-model='group', @refresh='refresh')
|
||||
|
||||
v-tab-item(key='users', :transition='false', :reverse-transition='false')
|
||||
group-users(v-model='group', @refresh='refresh')
|
||||
|
||||
v-card-chin
|
||||
v-spacer
|
||||
.caption.grey--text.pr-2 Group ID #[strong {{group.id}}]
|
||||
|
||||
page-selector(mode='select', v-model='selectPageModal', :open-handler='selectPageHandle', path='home', :locale='currentLang')
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
import GroupPermissions from './admin-groups-edit-permissions.vue'
|
||||
import GroupRules from './admin-groups-edit-rules.vue'
|
||||
import GroupUsers from './admin-groups-edit-users.vue'
|
||||
|
||||
/* global siteConfig */
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GroupPermissions,
|
||||
GroupRules,
|
||||
GroupUsers
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
group: {
|
||||
id: 0,
|
||||
name: '',
|
||||
isSystem: false,
|
||||
permissions: [],
|
||||
pageRules: [],
|
||||
users: [],
|
||||
redirectOnLogin: '/'
|
||||
},
|
||||
deleteGroupDialog: false,
|
||||
tab: null,
|
||||
selectPageModal: false,
|
||||
currentLang: siteConfig.lang
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
selectPage () {
|
||||
this.selectPageModal = true
|
||||
},
|
||||
selectPageHandle ({ path, locale }) {
|
||||
this.group.redirectOnLogin = `/${locale}/${path}`
|
||||
},
|
||||
async updateGroup() {
|
||||
try {
|
||||
await this.$apollo.mutate({
|
||||
mutation: gql`
|
||||
mutation (
|
||||
$id: Int!
|
||||
$name: String!
|
||||
$redirectOnLogin: String!
|
||||
$permissions: [String]!
|
||||
$pageRules: [PageRuleInput]!
|
||||
) {
|
||||
groups {
|
||||
update(
|
||||
id: $id
|
||||
name: $name
|
||||
redirectOnLogin: $redirectOnLogin
|
||||
permissions: $permissions
|
||||
pageRules: $pageRules
|
||||
) {
|
||||
responseResult {
|
||||
succeeded
|
||||
errorCode
|
||||
slug
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
id: this.group.id,
|
||||
name: this.group.name,
|
||||
redirectOnLogin: this.group.redirectOnLogin,
|
||||
permissions: this.group.permissions,
|
||||
pageRules: this.group.pageRules
|
||||
},
|
||||
watchLoading (isLoading) {
|
||||
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-groups-update')
|
||||
}
|
||||
})
|
||||
this.$store.commit('showNotification', {
|
||||
style: 'success',
|
||||
message: `Group changes have been saved.`,
|
||||
icon: 'check'
|
||||
})
|
||||
} catch (err) {
|
||||
this.$store.commit('pushGraphError', err)
|
||||
}
|
||||
},
|
||||
async deleteGroup() {
|
||||
this.deleteGroupDialog = false
|
||||
try {
|
||||
await this.$apollo.mutate({
|
||||
mutation: gql`
|
||||
mutation ($id: Int!) {
|
||||
groups {
|
||||
delete(id: $id) {
|
||||
responseResult {
|
||||
succeeded
|
||||
errorCode
|
||||
slug
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
id: this.group.id
|
||||
},
|
||||
watchLoading (isLoading) {
|
||||
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-groups-delete')
|
||||
}
|
||||
})
|
||||
this.$store.commit('showNotification', {
|
||||
style: 'success',
|
||||
message: `Group ${this.group.name} has been deleted.`,
|
||||
icon: 'delete'
|
||||
})
|
||||
this.$router.replace('/groups')
|
||||
} catch (err) {
|
||||
this.$store.commit('pushGraphError', err)
|
||||
}
|
||||
},
|
||||
async refresh() {
|
||||
return this.$apollo.queries.group.refetch()
|
||||
}
|
||||
},
|
||||
apollo: {
|
||||
group: {
|
||||
query: gql`
|
||||
query ($id: Int!) {
|
||||
groups {
|
||||
single(id: $id) {
|
||||
id
|
||||
name
|
||||
redirectOnLogin
|
||||
isSystem
|
||||
permissions
|
||||
pageRules {
|
||||
id
|
||||
path
|
||||
roles
|
||||
match
|
||||
deny
|
||||
locales
|
||||
}
|
||||
users {
|
||||
id
|
||||
name
|
||||
email
|
||||
}
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables() {
|
||||
return {
|
||||
id: _.toSafeInteger(this.$route.params.id)
|
||||
}
|
||||
},
|
||||
fetchPolicy: 'network-only',
|
||||
update: (data) => _.cloneDeep(data.groups.single),
|
||||
watchLoading (isLoading) {
|
||||
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-groups-refresh')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang='scss'>
|
||||
|
||||
</style>
|
@ -1,170 +0,0 @@
|
||||
<template lang='pug'>
|
||||
v-container(fluid, grid-list-lg)
|
||||
v-layout(row wrap)
|
||||
v-flex(xs12)
|
||||
.admin-header
|
||||
img.animated.fadeInUp(src='/_assets/svg/icon-people.svg', alt='Groups', style='width: 80px;')
|
||||
.admin-header-title
|
||||
.headline.blue--text.text--darken-2.animated.fadeInLeft Groups
|
||||
.subtitle-1.grey--text.animated.fadeInLeft.wait-p4s Manage groups and their permissions
|
||||
v-spacer
|
||||
v-btn.animated.fadeInDown.wait-p3s(icon, outlined, color='grey', href='https://docs.requarks.io/groups', target='_blank')
|
||||
v-icon mdi-help-circle
|
||||
v-btn.animated.fadeInDown.wait-p2s.mx-3(color='grey', outlined, @click='refresh', icon)
|
||||
v-icon mdi-refresh
|
||||
v-dialog(v-model='newGroupDialog', max-width='500')
|
||||
template(v-slot:activator='{ on }')
|
||||
v-btn.animated.fadeInDown(color='primary', depressed, v-on='on', large)
|
||||
v-icon(left) mdi-plus
|
||||
span New Group
|
||||
v-card
|
||||
.dialog-header.is-short New Group
|
||||
v-card-text.pt-5
|
||||
v-text-field.md2(
|
||||
outlined
|
||||
prepend-icon='mdi-account-group'
|
||||
v-model='newGroupName'
|
||||
label='Group Name'
|
||||
counter='255'
|
||||
@keyup.enter='createGroup'
|
||||
@keyup.esc='newGroupDialog = false'
|
||||
ref='groupNameIpt'
|
||||
)
|
||||
v-card-chin
|
||||
v-spacer
|
||||
v-btn(text, @click='newGroupDialog = false') Cancel
|
||||
v-btn(color='primary', @click='createGroup') Create
|
||||
v-card.mt-3.animated.fadeInUp
|
||||
v-data-table(
|
||||
:items='groups'
|
||||
:headers='headers'
|
||||
:search='search'
|
||||
:page.sync='pagination'
|
||||
:items-per-page='15'
|
||||
:loading='loading'
|
||||
@page-count='pageCount = $event'
|
||||
must-sort,
|
||||
hide-default-footer
|
||||
)
|
||||
template(slot='item', slot-scope='props')
|
||||
tr.is-clickable(:active='props.selected', @click='$router.push("/groups/" + props.item.id)')
|
||||
td {{ props.item.id }}
|
||||
td: strong {{ props.item.name }}
|
||||
td {{ props.item.userCount }}
|
||||
td {{ props.item.createdAt | moment('calendar') }}
|
||||
td {{ props.item.updatedAt | moment('calendar') }}
|
||||
td
|
||||
v-tooltip(left, v-if='props.item.isSystem')
|
||||
template(v-slot:activator='{ on }')
|
||||
v-icon(v-on='on') mdi-lock-outline
|
||||
span System Group
|
||||
template(slot='no-data')
|
||||
v-alert.ma-3(icon='mdi-alert', :value='true', outline) No groups to display.
|
||||
.text-xs-center.py-2(v-if='pageCount > 1')
|
||||
v-pagination(v-model='pagination', :length='pageCount')
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
|
||||
import groupsQuery from 'gql/admin/groups/groups-query-list.gql'
|
||||
import createGroupMutation from 'gql/admin/groups/groups-mutation-create.gql'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
newGroupDialog: false,
|
||||
newGroupName: '',
|
||||
selectedGroup: {},
|
||||
pagination: 1,
|
||||
pageCount: 0,
|
||||
groups: [],
|
||||
headers: [
|
||||
{ text: 'ID', value: 'id', width: 80, sortable: true },
|
||||
{ text: 'Name', value: 'name' },
|
||||
{ text: 'Users', value: 'userCount', width: 200 },
|
||||
{ text: 'Created', value: 'createdAt', width: 250 },
|
||||
{ text: 'Last Updated', value: 'updatedAt', width: 250 },
|
||||
{ text: '', value: 'isSystem', width: 20, sortable: false }
|
||||
],
|
||||
search: '',
|
||||
loading: false
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
newGroupDialog(newValue, oldValue) {
|
||||
if (newValue) {
|
||||
this.$nextTick(() => {
|
||||
this.$refs.groupNameIpt.focus()
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async refresh() {
|
||||
await this.$apollo.queries.groups.refetch()
|
||||
this.$store.commit('showNotification', {
|
||||
message: 'Groups have been refreshed.',
|
||||
style: 'success',
|
||||
icon: 'cached'
|
||||
})
|
||||
},
|
||||
async createGroup() {
|
||||
if (_.trim(this.newGroupName).length < 1) {
|
||||
this.$store.commit('showNotification', {
|
||||
style: 'red',
|
||||
message: 'Enter a group name.',
|
||||
icon: 'warning'
|
||||
})
|
||||
return
|
||||
}
|
||||
this.newGroupDialog = false
|
||||
try {
|
||||
await this.$apollo.mutate({
|
||||
mutation: createGroupMutation,
|
||||
variables: {
|
||||
name: this.newGroupName
|
||||
},
|
||||
update (store, resp) {
|
||||
const data = _.get(resp, 'data.groups.create', { responseResult: {} })
|
||||
if (data.responseResult.succeeded === true) {
|
||||
const apolloData = store.readQuery({ query: groupsQuery })
|
||||
data.group.userCount = 0
|
||||
apolloData.groups.list.push(data.group)
|
||||
store.writeQuery({ query: groupsQuery, data: apolloData })
|
||||
} else {
|
||||
throw new Error(data.responseResult.message)
|
||||
}
|
||||
},
|
||||
watchLoading (isLoading) {
|
||||
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-groups-create')
|
||||
}
|
||||
})
|
||||
this.newGroupName = ''
|
||||
this.$store.commit('showNotification', {
|
||||
style: 'success',
|
||||
message: `Group has been created successfully.`,
|
||||
icon: 'check'
|
||||
})
|
||||
} catch (err) {
|
||||
this.$store.commit('pushGraphError', err)
|
||||
}
|
||||
}
|
||||
},
|
||||
apollo: {
|
||||
groups: {
|
||||
query: groupsQuery,
|
||||
fetchPolicy: 'network-only',
|
||||
update: (data) => data.groups.list,
|
||||
watchLoading (isLoading) {
|
||||
this.loading = isLoading
|
||||
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-groups-refresh')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang='scss'>
|
||||
|
||||
</style>
|
@ -1,302 +0,0 @@
|
||||
<template lang='pug'>
|
||||
v-container(fluid, grid-list-lg)
|
||||
v-layout(row, wrap)
|
||||
v-flex(xs12)
|
||||
.admin-header
|
||||
img.animated.fadeInUp(src='/_assets/svg/icon-globe-earth.svg', alt='Locale', style='width: 80px;')
|
||||
.admin-header-title
|
||||
.headline.primary--text.animated.fadeInLeft {{ $t('admin:locale.title') }}
|
||||
.subtitle-1.grey--text.animated.fadeInLeft.wait-p4s {{ $t('admin:locale.subtitle') }}
|
||||
v-spacer
|
||||
v-btn.animated.fadeInDown.wait-p3s(icon, outlined, color='grey', href='https://docs.requarks.io/locales', target='_blank')
|
||||
v-icon mdi-help-circle
|
||||
v-btn.animated.fadeInDown.ml-3(color='success', depressed, @click='save', large, :loading='loading')
|
||||
v-icon(left) mdi-check
|
||||
span {{$t('common:actions.apply')}}
|
||||
v-form.pt-3
|
||||
v-layout(row wrap)
|
||||
v-flex(xl6 lg5 xs12)
|
||||
v-card.wiki-form.animated.fadeInUp
|
||||
v-toolbar(color='primary', dark, dense, flat)
|
||||
v-toolbar-title.subtitle-1 {{ $t('admin:locale.settings') }}
|
||||
v-card-text
|
||||
v-select(
|
||||
outlined
|
||||
:items='installedLocales'
|
||||
prepend-icon='mdi-web'
|
||||
v-model='selectedLocale'
|
||||
item-value='code'
|
||||
item-text='nativeName'
|
||||
:label='namespacing ? $t("admin:locale.base.labelWithNS") : $t("admin:locale.base.label")'
|
||||
persistent-hint
|
||||
:hint='$t("admin:locale.base.hint")'
|
||||
)
|
||||
template(slot='item', slot-scope='data')
|
||||
template(v-if='typeof data.item !== "object"')
|
||||
v-list-item-content(v-text='data.item')
|
||||
template(v-else)
|
||||
v-list-item-avatar
|
||||
v-avatar.blue.white--text(tile, size='40', v-html='data.item.code.toUpperCase()')
|
||||
v-list-item-content
|
||||
v-list-item-title(v-html='data.item.name')
|
||||
v-list-item-subtitle(v-html='data.item.nativeName')
|
||||
v-divider.mt-3
|
||||
v-switch(
|
||||
inset
|
||||
v-model='autoUpdate'
|
||||
:label='$t("admin:locale.autoUpdate.label")'
|
||||
color='primary'
|
||||
persistent-hint
|
||||
:hint='namespacing ? $t("admin:locale.autoUpdate.hintWithNS") : $t("admin:locale.autoUpdate.hint")'
|
||||
)
|
||||
|
||||
v-card.wiki-form.mt-3.animated.fadeInUp.wait-p2s
|
||||
v-toolbar(color='primary', dark, dense, flat)
|
||||
v-toolbar-title.subtitle-1 {{ $t('admin:locale.namespacing') }}
|
||||
v-card-text
|
||||
v-switch(
|
||||
inset
|
||||
v-model='namespacing'
|
||||
:label='$t("admin:locale.namespaces.label")'
|
||||
color='primary'
|
||||
persistent-hint
|
||||
:hint='$t("admin:locale.namespaces.hint")'
|
||||
)
|
||||
v-alert.mt-3(
|
||||
outlined
|
||||
color='orange'
|
||||
:value='true'
|
||||
icon='mdi-alert'
|
||||
)
|
||||
span {{ $t('admin:locale.namespacingPrefixWarning.title', { langCode: selectedLocale }) }}
|
||||
.caption.grey--text {{ $t('admin:locale.namespacingPrefixWarning.subtitle') }}
|
||||
v-divider.mt-3.mb-4
|
||||
v-select(
|
||||
outlined
|
||||
:disabled='!namespacing'
|
||||
:items='installedLocales'
|
||||
prepend-icon='mdi-web'
|
||||
multiple
|
||||
chips
|
||||
deletable-chips
|
||||
v-model='namespaces'
|
||||
item-value='code'
|
||||
item-text='name'
|
||||
:label='$t("admin:locale.activeNamespaces.label")'
|
||||
persistent-hint
|
||||
small-chips
|
||||
:hint='$t("admin:locale.activeNamespaces.hint")'
|
||||
)
|
||||
template(slot='item', slot-scope='data')
|
||||
template(v-if='typeof data.item !== "object"')
|
||||
v-list-item-content(v-text='data.item')
|
||||
template(v-else)
|
||||
v-list-item-avatar
|
||||
v-avatar.blue.white--text(tile, size='40', v-html='data.item.code.toUpperCase()')
|
||||
v-list-item-content
|
||||
v-list-item-title(v-html='data.item.name')
|
||||
v-list-item-subtitle(v-html='data.item.nativeName')
|
||||
v-list-item-action
|
||||
v-checkbox(:input-value='data.attrs.inputValue', color='primary', value)
|
||||
v-flex(xl6 lg7 xs12)
|
||||
v-card.animated.fadeInUp.wait-p4s
|
||||
v-toolbar(color='teal', dark, dense, flat)
|
||||
v-toolbar-title.subtitle-1 {{ $t('admin:locale.downloadTitle') }}
|
||||
v-data-table(
|
||||
:headers='headers',
|
||||
:items='locales',
|
||||
hide-default-footer,
|
||||
item-key='code',
|
||||
:items-per-page='1000'
|
||||
)
|
||||
template(v-slot:item.code='{ item }')
|
||||
v-chip.white--text(label, color='teal', small) {{item.code}}
|
||||
template(v-slot:item.name='{ item }')
|
||||
strong {{item.name}}
|
||||
template(v-slot:item.isRTL='{ item }')
|
||||
v-icon(v-if='item.isRTL') mdi-check
|
||||
template(v-slot:item.availability='{ item }')
|
||||
.d-flex.align-center.pl-4
|
||||
v-progress-circular(:value='item.availability', width='2', size='20', :color='item.availability <= 33 ? `red` : (item.availability <= 66) ? `orange` : `green`')
|
||||
.caption.mx-2(:class='item.availability <= 33 ? `red--text` : (item.availability <= 66) ? `orange--text` : `green--text`') {{item.availability}}%
|
||||
template(v-slot:item.isInstalled='{ item }')
|
||||
v-progress-circular(v-if='item.isDownloading', indeterminate, color='blue', size='20', :width='2')
|
||||
v-btn(v-else-if='item.isInstalled && item.installDate < item.updatedAt', icon, small, @click='download(item)')
|
||||
v-icon.blue--text mdi-cached
|
||||
v-btn(v-else-if='item.isInstalled', icon, small, @click='download(item)')
|
||||
v-icon.green--text mdi-check-bold
|
||||
v-btn(v-else, icon, small, @click='download(item)')
|
||||
v-icon.grey--text mdi-cloud-download
|
||||
v-card.wiki-form.mt-3.animated.fadeInUp.wait-p5s
|
||||
v-toolbar(color='teal', dark, dense, flat)
|
||||
v-toolbar-title.subtitle-1 {{ $t('admin:locale.sideload') }}
|
||||
v-spacer
|
||||
v-chip(label, color='white', small).teal--text coming soon
|
||||
v-card-text
|
||||
div {{ $t('admin:locale.sideloadHelp') }}
|
||||
v-btn.ml-0.mt-3(color='teal', disabled) {{ $t('common:actions.browse') }}
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
|
||||
/* global WIKI */
|
||||
|
||||
import localesQuery from 'gql/admin/locale/locale-query-list.gql'
|
||||
import localesDownloadMutation from 'gql/admin/locale/locale-mutation-download.gql'
|
||||
import localesSaveMutation from 'gql/admin/locale/locale-mutation-save.gql'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
locales: [],
|
||||
selectedLocale: 'en',
|
||||
autoUpdate: false,
|
||||
namespacing: false,
|
||||
namespaces: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
installedLocales() {
|
||||
return _.filter(this.locales, ['isInstalled', true])
|
||||
},
|
||||
headers() {
|
||||
return [
|
||||
{
|
||||
text: this.$t('admin:locale.code'),
|
||||
align: 'left',
|
||||
value: 'code',
|
||||
width: 90
|
||||
},
|
||||
{
|
||||
text: this.$t('admin:locale.name'),
|
||||
align: 'left',
|
||||
value: 'name'
|
||||
},
|
||||
{
|
||||
text: this.$t('admin:locale.nativeName'),
|
||||
align: 'left',
|
||||
value: 'nativeName'
|
||||
},
|
||||
{
|
||||
text: this.$t('admin:locale.rtl'),
|
||||
align: 'center',
|
||||
value: 'isRTL',
|
||||
sortable: false,
|
||||
width: 10
|
||||
},
|
||||
{
|
||||
text: this.$t('admin:locale.availability'),
|
||||
align: 'center',
|
||||
value: 'availability',
|
||||
sortable: false,
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
text: this.$t('admin:locale.download'),
|
||||
align: 'center',
|
||||
value: 'isInstalled',
|
||||
sortable: false,
|
||||
width: 100
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async download(lc) {
|
||||
lc.isDownloading = true
|
||||
const respRaw = await this.$apollo.mutate({
|
||||
mutation: localesDownloadMutation,
|
||||
variables: {
|
||||
locale: lc.code
|
||||
}
|
||||
})
|
||||
const resp = _.get(respRaw, 'data.localization.downloadLocale.responseResult', {})
|
||||
if (resp.succeeded) {
|
||||
lc.isDownloading = false
|
||||
lc.isInstalled = true
|
||||
lc.updatedAt = new Date().toISOString()
|
||||
lc.installDate = lc.updatedAt
|
||||
this.$store.commit('showNotification', {
|
||||
message: `Locale ${lc.name} has been installed successfully.`,
|
||||
style: 'success',
|
||||
icon: 'get_app'
|
||||
})
|
||||
} else {
|
||||
this.$store.commit('showNotification', {
|
||||
message: `Error: ${resp.message}`,
|
||||
style: 'error',
|
||||
icon: 'warning'
|
||||
})
|
||||
}
|
||||
this.isDownloading = false
|
||||
},
|
||||
async save() {
|
||||
this.loading = true
|
||||
const respRaw = await this.$apollo.mutate({
|
||||
mutation: localesSaveMutation,
|
||||
variables: {
|
||||
locale: this.selectedLocale,
|
||||
autoUpdate: this.autoUpdate,
|
||||
namespacing: this.namespacing,
|
||||
namespaces: this.namespaces
|
||||
}
|
||||
})
|
||||
const resp = _.get(respRaw, 'data.localization.updateLocale.responseResult', {})
|
||||
if (resp.succeeded) {
|
||||
// Change UI language
|
||||
WIKI.$i18n.i18next.changeLanguage(this.selectedLocale)
|
||||
WIKI.$moment.locale(this.selectedLocale)
|
||||
|
||||
// Check for RTL
|
||||
const curLocale = _.find(this.locales, ['code', this.selectedLocale])
|
||||
this.$vuetify.rtl = curLocale && curLocale.isRTL
|
||||
|
||||
this.$store.commit('showNotification', {
|
||||
message: 'Locale settings updated successfully.',
|
||||
style: 'success',
|
||||
icon: 'check'
|
||||
})
|
||||
|
||||
_.delay(() => {
|
||||
window.location.reload(true)
|
||||
}, 1000)
|
||||
} else {
|
||||
this.$store.commit('showNotification', {
|
||||
message: `Error: ${resp.message}`,
|
||||
style: 'error',
|
||||
icon: 'warning'
|
||||
})
|
||||
}
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
apollo: {
|
||||
locales: {
|
||||
query: localesQuery,
|
||||
fetchPolicy: 'network-only',
|
||||
update: (data) => data.localization.locales.map(lc => ({ ...lc, isDownloading: false })),
|
||||
watchLoading (isLoading) {
|
||||
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-locale-refresh')
|
||||
}
|
||||
},
|
||||
selectedLocale: {
|
||||
query: localesQuery,
|
||||
update: (data) => data.localization.config.locale
|
||||
},
|
||||
autoUpdate: {
|
||||
query: localesQuery,
|
||||
update: (data) => data.localization.config.autoUpdate
|
||||
},
|
||||
namespacing: {
|
||||
query: localesQuery,
|
||||
update: (data) => data.localization.config.namespacing
|
||||
},
|
||||
namespaces: {
|
||||
query: localesQuery,
|
||||
update: (data) => data.localization.config.namespaces
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
@ -1,106 +0,0 @@
|
||||
<template lang='pug'>
|
||||
v-dialog(v-model='isShown', width='90vw', max-width='1200')
|
||||
.dialog-header
|
||||
span Live Console
|
||||
v-spacer
|
||||
.caption.blue--text.text--lighten-3.mr-3 Streaming...
|
||||
v-progress-circular(
|
||||
indeterminate
|
||||
color='blue lighten-3'
|
||||
:size='20'
|
||||
:width='2'
|
||||
)
|
||||
.consoleTerm(ref='consoleContainer')
|
||||
v-toolbar(flat, color='grey darken-3', dark)
|
||||
v-spacer
|
||||
v-btn(outline, @click='clear')
|
||||
v-icon(left) cancel_presentation
|
||||
span Clear
|
||||
v-btn(outline, @click='close')
|
||||
v-icon(left) close
|
||||
span Close
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
// import { Terminal } from 'xterm'
|
||||
// import * as fit from 'xterm/lib/addons/fit/fit'
|
||||
|
||||
import livetrailSubscription from 'gql/admin/logging/logging-subscription-livetrail.gql'
|
||||
|
||||
// Terminal.applyAddon(fit)
|
||||
|
||||
export default {
|
||||
term: null,
|
||||
props: {
|
||||
value: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isShown: {
|
||||
get() { return this.value },
|
||||
set(val) { this.$emit('input', val) }
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value(newValue, oldValue) {
|
||||
if (newValue) {
|
||||
_.delay(() => {
|
||||
// this.term = new Terminal()
|
||||
this.term.open(this.$refs.consoleContainer)
|
||||
this.term.writeln('Connecting to \x1B[1;3;31mconsole output\x1B[0m...')
|
||||
|
||||
this.attach()
|
||||
}, 100)
|
||||
} else {
|
||||
this.term.dispose()
|
||||
this.term = null
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
|
||||
},
|
||||
methods: {
|
||||
clear() {
|
||||
this.term.clear()
|
||||
},
|
||||
close() {
|
||||
this.isShown = false
|
||||
},
|
||||
attach() {
|
||||
const self = this
|
||||
const observer = this.$apollo.subscribe({
|
||||
query: livetrailSubscription
|
||||
})
|
||||
observer.subscribe({
|
||||
next(data) {
|
||||
const item = _.get(data, `data.loggingLiveTrail`, {})
|
||||
console.info(item)
|
||||
self.term.writeln(`${item.level}: ${item.output}`)
|
||||
},
|
||||
error(error) {
|
||||
self.$store.commit('showNotification', {
|
||||
style: 'red',
|
||||
message: error.message,
|
||||
icon: 'warning'
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang='scss'>
|
||||
|
||||
.consoleTerm {
|
||||
background-color: #000;
|
||||
padding: 16px;
|
||||
width: 100%;
|
||||
height: 415px;
|
||||
}
|
||||
|
||||
</style>
|
@ -1,194 +0,0 @@
|
||||
<template lang='pug'>
|
||||
v-container(fluid, grid-list-lg)
|
||||
v-layout(row, wrap)
|
||||
v-flex(xs12)
|
||||
.admin-header
|
||||
img(src='/_assets/svg/icon-registry-editor.svg', alt='Logging', style='width: 80px;')
|
||||
.admin-header-title
|
||||
.headline.primary--text Logging
|
||||
.subtitle-1.grey--text Configure the system logger(s) #[v-chip(label, color='primary', small).white--text coming soon]
|
||||
v-spacer
|
||||
v-btn(outline, color='grey', @click='refresh', large)
|
||||
v-icon refresh
|
||||
v-btn(color='black', disabled, depressed, @click='toggleConsole', large)
|
||||
v-icon check
|
||||
span Live Trail
|
||||
v-btn(color='success', @click='save', depressed, large)
|
||||
v-icon(left) check
|
||||
span {{$t('common:actions.apply')}}
|
||||
|
||||
v-card.mt-3
|
||||
v-tabs(color='grey darken-2', fixed-tabs, slider-color='white', show-arrows, dark)
|
||||
v-tab(key='settings'): v-icon settings
|
||||
v-tab(v-for='logger in activeLoggers', :key='logger.key') {{ logger.title }}
|
||||
|
||||
v-tab-item(key='settings', :transition='false', :reverse-transition='false')
|
||||
v-card.pa-3(flat, tile)
|
||||
.body-2.grey--text.text--darken-1 Select which logging service to enable:
|
||||
.caption.grey--text.pb-2 Some loggers require additional configuration in their dedicated tab (when selected).
|
||||
v-form
|
||||
v-checkbox.my-0(
|
||||
v-for='(logger, n) in loggers'
|
||||
v-model='logger.isEnabled'
|
||||
:key='logger.key'
|
||||
:label='logger.title'
|
||||
color='primary'
|
||||
hide-details
|
||||
disabled
|
||||
)
|
||||
|
||||
v-tab-item(v-for='(logger, n) in activeLoggers', :key='logger.key', :transition='false', :reverse-transition='false')
|
||||
v-card.wiki-form.pa-3(flat, tile)
|
||||
v-form
|
||||
.loggerlogo
|
||||
img(:src='logger.logo', :alt='logger.title')
|
||||
v-subheader.pl-0 {{logger.title}}
|
||||
.caption {{logger.description}}
|
||||
.caption: a(:href='logger.website') {{logger.website}}
|
||||
v-divider.mt-3
|
||||
v-subheader.pl-0 Logger Configuration
|
||||
.body-1.ml-3(v-if='!logger.config || logger.config.length < 1') This logger has no configuration options you can modify.
|
||||
template(v-else, v-for='cfg in logger.config')
|
||||
v-select(
|
||||
v-if='cfg.value.type === "string" && cfg.value.enum'
|
||||
outline
|
||||
background-color='grey lighten-2'
|
||||
:items='cfg.value.enum'
|
||||
:key='cfg.key'
|
||||
:label='cfg.value.title'
|
||||
v-model='cfg.value.value'
|
||||
prepend-icon='settings_applications'
|
||||
:hint='cfg.value.hint ? cfg.value.hint : ""'
|
||||
persistent-hint
|
||||
:class='cfg.value.hint ? "mb-2" : ""'
|
||||
)
|
||||
v-switch(
|
||||
v-else-if='cfg.value.type === "boolean"'
|
||||
:key='cfg.key'
|
||||
:label='cfg.value.title'
|
||||
v-model='cfg.value.value'
|
||||
color='primary'
|
||||
prepend-icon='settings_applications'
|
||||
:hint='cfg.value.hint ? cfg.value.hint : ""'
|
||||
persistent-hint
|
||||
)
|
||||
v-text-field(
|
||||
v-else
|
||||
outline
|
||||
background-color='grey lighten-2'
|
||||
:key='cfg.key'
|
||||
:label='cfg.value.title'
|
||||
v-model='cfg.value.value'
|
||||
prepend-icon='settings_applications'
|
||||
:hint='cfg.value.hint ? cfg.value.hint : ""'
|
||||
persistent-hint
|
||||
:class='cfg.value.hint ? "mb-2" : ""'
|
||||
)
|
||||
v-divider.mt-3
|
||||
v-subheader.pl-0 Log Level
|
||||
.body-1.ml-3 Select the minimum error level that will be reported to this logger.
|
||||
v-layout(row)
|
||||
v-flex(xs12, md6, lg4)
|
||||
.pt-3
|
||||
v-select(
|
||||
single-line
|
||||
outline
|
||||
background-color='grey lighten-2'
|
||||
:items='levels'
|
||||
label='Level'
|
||||
v-model='logger.level'
|
||||
prepend-icon='graphic_eq'
|
||||
hint='Default: warn'
|
||||
persistent-hint
|
||||
)
|
||||
|
||||
logging-console(v-model='showConsole')
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
|
||||
import LoggingConsole from './admin-logging-console.vue'
|
||||
|
||||
import loggersQuery from 'gql/admin/logging/logging-query-loggers.gql'
|
||||
import loggersSaveMutation from 'gql/admin/logging/logging-mutation-save-loggers.gql'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
LoggingConsole
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showConsole: false,
|
||||
loggers: [],
|
||||
levels: ['error', 'warn', 'info', 'debug', 'verbose']
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
activeLoggers() {
|
||||
return _.filter(this.loggers, 'isEnabled')
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async refresh() {
|
||||
await this.$apollo.queries.loggers.refetch()
|
||||
this.$store.commit('showNotification', {
|
||||
message: 'List of loggers has been refreshed.',
|
||||
style: 'success',
|
||||
icon: 'cached'
|
||||
})
|
||||
},
|
||||
async save() {
|
||||
this.$store.commit(`loadingStart`, 'admin-logging-saveloggers')
|
||||
await this.$apollo.mutate({
|
||||
mutation: loggersSaveMutation,
|
||||
variables: {
|
||||
loggers: this.loggers.map(tgt => _.pick(tgt, [
|
||||
'isEnabled',
|
||||
'key',
|
||||
'config',
|
||||
'level'
|
||||
])).map(str => ({...str, config: str.config.map(cfg => ({...cfg, value: JSON.stringify({ v: cfg.value.value })}))}))
|
||||
}
|
||||
})
|
||||
this.$store.commit('showNotification', {
|
||||
message: 'Logging configuration saved successfully.',
|
||||
style: 'success',
|
||||
icon: 'check'
|
||||
})
|
||||
this.$store.commit(`loadingStop`, 'admin-logging-saveloggers')
|
||||
},
|
||||
toggleConsole() {
|
||||
this.showConsole = !this.showConsole
|
||||
}
|
||||
},
|
||||
apollo: {
|
||||
loggers: {
|
||||
query: loggersQuery,
|
||||
fetchPolicy: 'network-only',
|
||||
update: (data) => _.cloneDeep(data.logging.loggers).map(str => ({...str, config: str.config.map(cfg => ({...cfg, value: JSON.parse(cfg.value)}))})),
|
||||
watchLoading (isLoading) {
|
||||
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-logging-refresh')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang='scss' scoped>
|
||||
|
||||
.loggerlogo {
|
||||
width: 250px;
|
||||
height: 85px;
|
||||
float:right;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
max-height: 50px;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
@ -1,258 +0,0 @@
|
||||
<template lang='pug'>
|
||||
v-container(fluid, grid-list-lg)
|
||||
v-layout(row, wrap)
|
||||
v-flex(xs12)
|
||||
.admin-header
|
||||
img.animated.fadeInUp(src='/_assets/svg/icon-new-post.svg', alt='Mail', style='width: 80px;')
|
||||
.admin-header-title
|
||||
.headline.primary--text.animated.fadeInLeft {{ $t('admin:mail.title') }}
|
||||
.subtitle-1.grey--text.animated.fadeInLeft.wait-p4s {{ $t('admin:mail.subtitle') }}
|
||||
v-spacer
|
||||
v-btn.animated.fadeInDown(color='success', depressed, @click='save', large)
|
||||
v-icon(left) mdi-check
|
||||
span {{$t('common:actions.apply')}}
|
||||
v-form.pt-3
|
||||
v-layout(row wrap)
|
||||
v-flex(lg6 xs12)
|
||||
v-form
|
||||
v-card.animated.fadeInUp
|
||||
v-toolbar(color='primary', dark, dense, flat)
|
||||
v-toolbar-title.subtitle-1 {{ $t('admin:mail.configuration') }}
|
||||
.overline.pa-4.grey--text {{ $t('admin:mail.sender') }}
|
||||
.px-4
|
||||
v-text-field(
|
||||
outlined
|
||||
v-model='config.senderName'
|
||||
:label='$t(`admin:mail.senderName`)'
|
||||
required
|
||||
:counter='255'
|
||||
prepend-icon='mdi-mailbox'
|
||||
)
|
||||
v-text-field(
|
||||
outlined
|
||||
v-model='config.senderEmail'
|
||||
:label='$t(`admin:mail.senderEmail`)'
|
||||
required
|
||||
:counter='255'
|
||||
prepend-icon='mdi-mailbox'
|
||||
)
|
||||
v-divider
|
||||
.overline.pa-4.grey--text {{ $t('admin:mail.smtp') }}
|
||||
.px-4
|
||||
v-text-field(
|
||||
outlined
|
||||
v-model='config.host'
|
||||
:label='$t(`admin:mail.smtpHost`)'
|
||||
required
|
||||
:counter='255'
|
||||
prepend-icon='mdi-memory'
|
||||
)
|
||||
v-text-field(
|
||||
outlined
|
||||
v-model='config.port'
|
||||
:label='$t(`admin:mail.smtpPort`)'
|
||||
required
|
||||
prepend-icon='mdi-serial-port'
|
||||
persistent-hint
|
||||
:hint='$t(`admin:mail.smtpPortHint`)'
|
||||
style='max-width: 300px;'
|
||||
)
|
||||
v-switch(
|
||||
v-model='config.secure'
|
||||
:label='$t(`admin:mail.smtpTLS`)'
|
||||
color='primary'
|
||||
persistent-hint
|
||||
:hint='$t(`admin:mail.smtpTLSHint`)'
|
||||
prepend-icon='mdi-security-network'
|
||||
inset
|
||||
)
|
||||
v-switch(
|
||||
v-model='config.verifySSL'
|
||||
:label='$t(`admin:mail.smtpVerifySSL`)'
|
||||
color='primary'
|
||||
persistent-hint
|
||||
:hint='$t(`admin:mail.smtpVerifySSLHint`)'
|
||||
prepend-icon='mdi-security-network'
|
||||
inset
|
||||
)
|
||||
v-text-field.mt-8(
|
||||
outlined
|
||||
v-model='config.user'
|
||||
:label='$t(`admin:mail.smtpUser`)'
|
||||
required
|
||||
:counter='255'
|
||||
prepend-icon='mdi-shield-account-outline'
|
||||
)
|
||||
v-text-field(
|
||||
outlined
|
||||
v-model='config.pass'
|
||||
:label='$t(`admin:mail.smtpPwd`)'
|
||||
required
|
||||
prepend-icon='mdi-form-textbox-password'
|
||||
type='password'
|
||||
)
|
||||
|
||||
v-flex(lg6 xs12)
|
||||
v-card.animated.fadeInUp.wait-p2s
|
||||
v-form
|
||||
v-toolbar(color='primary', dark, dense, flat)
|
||||
v-toolbar-title.subtitle-1 {{ $t('admin:mail.dkim') }}
|
||||
v-card-info
|
||||
span {{ $t('admin:mail.dkimHint') }}
|
||||
.pa-4
|
||||
v-switch(
|
||||
v-model='config.useDKIM'
|
||||
:label='$t(`admin:mail.dkimUse`)'
|
||||
color='primary'
|
||||
prepend-icon='mdi-key'
|
||||
inset
|
||||
)
|
||||
v-text-field(
|
||||
outlined
|
||||
v-model='config.dkimDomainName'
|
||||
:label='$t(`admin:mail.dkimDomainName`)'
|
||||
:counter='255'
|
||||
prepend-icon='mdi-key'
|
||||
:disabled='!config.useDKIM'
|
||||
)
|
||||
v-text-field(
|
||||
outlined
|
||||
v-model='config.dkimKeySelector'
|
||||
:label='$t(`admin:mail.dkimKeySelector`)'
|
||||
:counter='255'
|
||||
prepend-icon='mdi-key'
|
||||
:disabled='!config.useDKIM'
|
||||
)
|
||||
v-textarea(
|
||||
outlined
|
||||
v-model='config.dkimPrivateKey'
|
||||
:label='$t(`admin:mail.dkimPrivateKey`)'
|
||||
prepend-icon='mdi-key'
|
||||
persistent-hint
|
||||
:hint='$t(`admin:mail.dkimPrivateKeyHint`)'
|
||||
:disabled='!config.useDKIM'
|
||||
)
|
||||
|
||||
v-card.mt-3.animated.fadeInUp.wait-p3s
|
||||
v-form
|
||||
v-toolbar(color='teal', dark, dense, flat)
|
||||
v-toolbar-title.subtitle-1 {{ $t('admin:mail.test') }}
|
||||
.pa-4
|
||||
.body-2.grey--text.text--darken-2 {{ $t('admin:mail.testHint') }}
|
||||
v-text-field.mt-3(
|
||||
outlined
|
||||
v-model='testEmail'
|
||||
:label='$t(`admin:mail.testRecipient`)'
|
||||
:counter='255'
|
||||
prepend-icon='mdi-email-outline'
|
||||
:disabled='testLoading'
|
||||
)
|
||||
v-card-chin
|
||||
v-spacer
|
||||
v-btn.px-4(color='teal', dark, @click='sendTest', :loading='testLoading')
|
||||
v-icon(left) mdi-send
|
||||
span {{ $t('admin:mail.testSend') }}
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import mailConfigQuery from 'gql/admin/mail/mail-query-config.gql'
|
||||
import mailUpdateConfigMutation from 'gql/admin/mail/mail-mutation-save-config.gql'
|
||||
import mailTestMutation from 'gql/admin/mail/mail-mutation-sendtest.gql'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
config: {
|
||||
senderName: '',
|
||||
senderEmail: '',
|
||||
host: '',
|
||||
port: 0,
|
||||
secure: false,
|
||||
verifySSL: false,
|
||||
user: '',
|
||||
pass: '',
|
||||
useDKIM: false,
|
||||
dkimDomainName: '',
|
||||
dkimKeySelector: '',
|
||||
dkimPrivateKey: ''
|
||||
},
|
||||
testEmail: '',
|
||||
testLoading: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async save () {
|
||||
try {
|
||||
await this.$apollo.mutate({
|
||||
mutation: mailUpdateConfigMutation,
|
||||
variables: {
|
||||
senderName: this.config.senderName || '',
|
||||
senderEmail: this.config.senderEmail || '',
|
||||
host: this.config.host || '',
|
||||
port: _.toSafeInteger(this.config.port) || 0,
|
||||
secure: this.config.secure || false,
|
||||
verifySSL: this.config.verifySSL || false,
|
||||
user: this.config.user || '',
|
||||
pass: this.config.pass || '',
|
||||
useDKIM: this.config.useDKIM || false,
|
||||
dkimDomainName: this.config.dkimDomainName || '',
|
||||
dkimKeySelector: this.config.dkimKeySelector || '',
|
||||
dkimPrivateKey: this.config.dkimPrivateKey || ''
|
||||
},
|
||||
watchLoading (isLoading) {
|
||||
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-mail-update')
|
||||
}
|
||||
})
|
||||
this.$store.commit('showNotification', {
|
||||
style: 'success',
|
||||
message: this.$t('admin:mail.saveSuccess'),
|
||||
icon: 'check'
|
||||
})
|
||||
} catch (err) {
|
||||
this.$store.commit('pushGraphError', err)
|
||||
}
|
||||
},
|
||||
async sendTest () {
|
||||
try {
|
||||
const resp = await this.$apollo.mutate({
|
||||
mutation: mailTestMutation,
|
||||
variables: {
|
||||
recipientEmail: this.testEmail
|
||||
},
|
||||
watchLoading (isLoading) {
|
||||
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-mail-test')
|
||||
}
|
||||
})
|
||||
if (!_.get(resp, 'data.mail.sendTest.responseResult.succeeded', false)) {
|
||||
throw new Error(_.get(resp, 'data.mail.sendTest.responseResult.message', 'An unexpected error occurred.'))
|
||||
}
|
||||
|
||||
this.testEmail = ''
|
||||
this.$store.commit('showNotification', {
|
||||
style: 'success',
|
||||
message: this.$t('admin:mail.sendTestSuccess'),
|
||||
icon: 'check'
|
||||
})
|
||||
} catch (err) {
|
||||
this.$store.commit('pushGraphError', err)
|
||||
}
|
||||
}
|
||||
},
|
||||
apollo: {
|
||||
config: {
|
||||
query: mailConfigQuery,
|
||||
fetchPolicy: 'network-only',
|
||||
update: (data) => _.cloneDeep(data.mail.config),
|
||||
watchLoading (isLoading) {
|
||||
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-mail-refresh')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang='scss'>
|
||||
|
||||
</style>
|
@ -1,526 +0,0 @@
|
||||
<template lang='pug'>
|
||||
v-container(fluid, grid-list-lg)
|
||||
v-layout(row wrap)
|
||||
v-flex(xs12)
|
||||
.admin-header
|
||||
img.animated.fadeInUp(src='/_assets/svg/icon-triangle-arrow.svg', alt='Navigation', style='width: 80px;')
|
||||
.admin-header-title
|
||||
.headline.primary--text.animated.fadeInLeft {{$t('navigation.title')}}
|
||||
.subtitle-1.grey--text.animated.fadeInLeft.wait-p4s {{$t('navigation.subtitle')}}
|
||||
v-spacer
|
||||
v-btn.animated.fadeInDown.wait-p3s(icon, outlined, color='grey', href='https://docs.requarks.io/navigation', target='_blank')
|
||||
v-icon mdi-help-circle
|
||||
v-btn.mx-3.animated.fadeInDown.wait-p2s.mr-3(icon, outlined, color='grey', @click='refresh')
|
||||
v-icon mdi-refresh
|
||||
v-btn.animated.fadeInDown(color='success', depressed, @click='save', large)
|
||||
v-icon(left) mdi-check
|
||||
span {{$t('common:actions.apply')}}
|
||||
v-container.pa-0.mt-3(fluid, grid-list-lg)
|
||||
v-row(dense)
|
||||
v-col(cols='3')
|
||||
v-card.animated.fadeInUp
|
||||
v-toolbar(color='teal', dark, dense, flat, height='56')
|
||||
v-toolbar-title.subtitle-1 {{$t('admin:navigation.mode')}}
|
||||
v-list(nav, two-line)
|
||||
v-list-item-group(v-model='config.mode', mandatory, :color='$vuetify.theme.dark ? `teal lighten-3` : `teal`')
|
||||
v-list-item(value='TREE')
|
||||
v-list-item-avatar
|
||||
img(src='/_assets/svg/icon-tree-structure-dotted.svg', alt='Site Tree')
|
||||
v-list-item-content
|
||||
v-list-item-title {{$t('admin:navigation.modeSiteTree.title')}}
|
||||
v-list-item-subtitle {{$t('admin:navigation.modeSiteTree.description')}}
|
||||
v-list-item-avatar
|
||||
v-icon(v-if='$vuetify.theme.dark', :color='config.mode === `TREE` ? `teal lighten-3` : `grey darken-2`') mdi-check-circle
|
||||
v-icon(v-else, :color='config.mode === `TREE` ? `teal` : `grey lighten-3`') mdi-check-circle
|
||||
v-list-item(value='STATIC')
|
||||
v-list-item-avatar
|
||||
img(src='/_assets/svg/icon-features-list.svg', alt='Static Navigation')
|
||||
v-list-item-content
|
||||
v-list-item-title {{$t('admin:navigation.modeStatic.title')}}
|
||||
v-list-item-subtitle {{$t('admin:navigation.modeStatic.description')}}
|
||||
v-list-item-avatar
|
||||
v-icon(v-if='$vuetify.theme.dark', :color='config.mode === `STATIC` ? `teal lighten-3` : `grey darken-2`') mdi-check-circle
|
||||
v-icon(v-else, :color='config.mode === `STATIC` ? `teal` : `grey lighten-3`') mdi-check-circle
|
||||
v-list-item(value='MIXED')
|
||||
v-list-item-avatar
|
||||
img(src='/_assets/svg/icon-user-menu-male-dotted.svg', alt='Custom Navigation')
|
||||
v-list-item-content
|
||||
v-list-item-title {{$t('admin:navigation.modeCustom.title')}}
|
||||
v-list-item-subtitle {{$t('admin:navigation.modeCustom.description')}}
|
||||
v-list-item-avatar
|
||||
v-icon(v-if='$vuetify.theme.dark', :color='config.mode === `MIXED` ? `teal lighten-3` : `grey darken-2`') mdi-check-circle
|
||||
v-icon(v-else, :color='config.mode === `MIXED` ? `teal` : `grey lighten-3`') mdi-check-circle
|
||||
v-list-item(value='NONE')
|
||||
v-list-item-avatar
|
||||
img(src='/_assets/svg/icon-cancel-dotted.svg', alt='None')
|
||||
v-list-item-content
|
||||
v-list-item-title {{$t('admin:navigation.modeNone.title')}}
|
||||
v-list-item-subtitle {{$t('admin:navigation.modeNone.description')}}
|
||||
v-list-item-avatar
|
||||
v-icon(v-if='$vuetify.theme.dark', :color='config.mode === `none` ? `teal lighten-3` : `grey darken-2`') mdi-check-circle
|
||||
v-icon(v-else, :color='config.mode === `none` ? `teal` : `grey lighten-3`') mdi-check-circle
|
||||
v-col(cols='9', v-if='config.mode === `MIXED` || config.mode === `STATIC`')
|
||||
v-card.animated.fadeInUp.wait-p2s
|
||||
v-row(no-gutters, align='stretch')
|
||||
v-col(style='flex: 0 0 350px;')
|
||||
v-card.grey(flat, style='height: 100%; border-radius: 4px 0 0 4px;', :class='$vuetify.theme.dark ? `darken-4-l5` : `lighten-3`')
|
||||
.teal.lighten-1.pa-2.d-flex(style='margin-bottom: 1px; height:56px;')
|
||||
v-select(
|
||||
:disabled='locales.length < 2'
|
||||
label='Locale'
|
||||
hide-details
|
||||
solo
|
||||
flat
|
||||
background-color='teal darken-2'
|
||||
dark
|
||||
dense
|
||||
v-model='currentLang'
|
||||
:items='locales'
|
||||
item-text='nativeName'
|
||||
item-value='code'
|
||||
)
|
||||
v-tooltip(top)
|
||||
template(v-slot:activator='{ on }')
|
||||
v-btn.ml-2(icon, tile, color='white', v-on='on', @click='copyFromLocaleDialogIsShown = true')
|
||||
v-icon mdi-arrange-send-backward
|
||||
span {{$t('admin:navigation.copyFromLocale')}}
|
||||
v-list.py-2(dense, nav, dark, class='blue darken-2', style='border-radius: 0;')
|
||||
v-list-item(v-if='currentTree.length < 1')
|
||||
v-list-item-avatar(size='24'): v-icon(color='blue lighten-3') mdi-alert
|
||||
v-list-item-content
|
||||
em.caption.blue--text.text--lighten-4 {{$t('navigation.emptyList')}}
|
||||
draggable(v-model='currentTree')
|
||||
template(v-for='navItem in currentTree')
|
||||
v-list-item(
|
||||
v-if='navItem.kind === "link"'
|
||||
:key='navItem.id'
|
||||
:class='(navItem === current) ? "blue" : ""'
|
||||
@click='selectItem(navItem)'
|
||||
)
|
||||
v-list-item-avatar(size='24', tile)
|
||||
v-icon(v-if='navItem.icon.match(/fa[a-z] fa-/)', size='19') {{ navItem.icon }}
|
||||
v-icon(v-else) {{ navItem.icon }}
|
||||
v-list-item-title {{navItem.label}}
|
||||
.py-2.clickable(
|
||||
v-else-if='navItem.kind === "divider"'
|
||||
:key='navItem.id'
|
||||
:class='(navItem === current) ? "blue" : ""'
|
||||
@click='selectItem(navItem)'
|
||||
)
|
||||
v-divider
|
||||
v-subheader.pl-4.clickable(
|
||||
v-else-if='navItem.kind === "header"'
|
||||
:key='navItem.id'
|
||||
:class='(navItem === current) ? "blue" : ""'
|
||||
@click='selectItem(navItem)'
|
||||
) {{navItem.label}}
|
||||
v-card-chin
|
||||
v-menu(offset-y, bottom, min-width='200px', style='flex: 1 1;')
|
||||
template(v-slot:activator='{ on }')
|
||||
v-btn(v-on='on', color='primary', depressed, block)
|
||||
v-icon(left) mdi-plus
|
||||
span {{$t('common:actions.add')}}
|
||||
v-list
|
||||
v-list-item(@click='addItem("link")')
|
||||
v-list-item-avatar(size='24'): v-icon mdi-link
|
||||
v-list-item-title {{$t('navigation.link')}}
|
||||
v-list-item(@click='addItem("header")')
|
||||
v-list-item-avatar(size='24'): v-icon mdi-format-title
|
||||
v-list-item-title {{$t('navigation.header')}}
|
||||
v-list-item(@click='addItem("divider")')
|
||||
v-list-item-avatar(size='24'): v-icon mdi-minus
|
||||
v-list-item-title {{$t('navigation.divider')}}
|
||||
v-col
|
||||
v-card(flat, style='border-radius: 0 4px 4px 0;')
|
||||
template(v-if='current.kind === "link"')
|
||||
v-toolbar(height='56', color='teal lighten-1', flat, dark)
|
||||
.subtitle-1 {{$t('navigation.edit', { kind: $t('navigation.link') })}}
|
||||
v-spacer
|
||||
v-btn.px-5(color='white', outlined, @click='deleteItem(current)')
|
||||
v-icon(left) mdi-delete
|
||||
span {{$t('navigation.delete', { kind: $t('navigation.link') })}}
|
||||
v-card-text
|
||||
v-text-field(
|
||||
outlined
|
||||
:label='$t("navigation.label")'
|
||||
prepend-icon='mdi-format-title'
|
||||
v-model='current.label'
|
||||
counter='255'
|
||||
)
|
||||
v-text-field(
|
||||
outlined
|
||||
:label='$t("navigation.icon")'
|
||||
prepend-icon='mdi-dice-5'
|
||||
v-model='current.icon'
|
||||
hide-details
|
||||
)
|
||||
.caption.pt-3.pl-5 The default icon set is #[strong Material Design Icons]. In order to use another icon set, you must first select it in the Theme administration section.
|
||||
.caption.pt-3.pl-5: strong Material Design Icons
|
||||
.caption.pl-5 Refer to the #[a(href='https://materialdesignicons.com/', target='_blank') Material Design Icons Reference] for the list of all possible values. You must prefix all values with #[code mdi-], e.g. #[code mdi-home]
|
||||
.caption.pt-3.pl-5: strong Font Awesome 5
|
||||
.caption.pl-5 Refer to the #[a(href='https://fontawesome.com/icons?d=gallery&m=free', target='_blank') Font Awesome 5 Reference] for the list of all possible values. You must prefix all values with #[code fas fa-], e.g. #[code fas fa-home]. Note that some icons use different prefixes (e.g. #[code fab], #[code fad], #[code fal], #[code far]).
|
||||
.caption.pt-3.pl-5: strong Font Awesome 4
|
||||
.caption.pl-5 Refer to the #[a(href='https://fontawesome.com/v4.7.0/icons/', target='_blank') Font Awesome 4 Reference] for the list of all possible values. You must prefix all values with #[code fa fa-], e.g. #[code fa fa-home]
|
||||
v-divider
|
||||
v-card-text
|
||||
v-select(
|
||||
outlined
|
||||
:label='$t("navigation.targetType")'
|
||||
prepend-icon='mdi-near-me'
|
||||
:items='navTypes'
|
||||
v-model='current.targetType'
|
||||
hide-details
|
||||
)
|
||||
v-text-field.mt-4(
|
||||
v-if='current.targetType === `external` || current.targetType === `externalblank`'
|
||||
outlined
|
||||
:label='$t("navigation.target")'
|
||||
prepend-icon='mdi-near-me'
|
||||
v-model='current.target'
|
||||
hide-details
|
||||
)
|
||||
.d-flex.align-center.mt-4(v-else-if='current.targetType === "page"')
|
||||
v-btn.ml-8(
|
||||
color='primary'
|
||||
dark
|
||||
@click='selectPage'
|
||||
)
|
||||
v-icon(left) mdi-magnify
|
||||
span {{$t('admin:navigation.selectPageButton')}}
|
||||
.caption.ml-4.primary--text {{current.target}}
|
||||
v-text-field(
|
||||
v-else-if='current.targetType === `search`'
|
||||
outlined
|
||||
:label='$t("navigation.navType.searchQuery")'
|
||||
prepend-icon='search'
|
||||
v-model='current.target'
|
||||
)
|
||||
v-divider
|
||||
|
||||
template(v-else-if='current.kind === "header"')
|
||||
v-toolbar(height='56', color='teal lighten-1', flat, dark)
|
||||
.subtitle-1 {{$t('navigation.edit', { kind: $t('navigation.header') })}}
|
||||
v-spacer
|
||||
v-btn.px-5(color='white', outlined, @click='deleteItem(current)')
|
||||
v-icon(left) mdi-delete
|
||||
span {{$t('navigation.delete', { kind: $t('navigation.header') })}}
|
||||
v-card-text
|
||||
v-text-field(
|
||||
outlined
|
||||
:label='$t("navigation.label")'
|
||||
prepend-icon='mdi-format-title'
|
||||
v-model='current.label'
|
||||
)
|
||||
v-divider
|
||||
|
||||
div(v-else-if='current.kind === "divider"')
|
||||
v-toolbar(height='56', color='teal lighten-1', flat, dark)
|
||||
.subtitle-1 {{$t('navigation.edit', { kind: $t('navigation.divider') })}}
|
||||
v-spacer
|
||||
v-btn.px-5(color='white', outlined, @click='deleteItem(current)')
|
||||
v-icon(left) mdi-delete
|
||||
span {{$t('navigation.delete', { kind: $t('navigation.divider') })}}
|
||||
|
||||
v-card-text(v-if='current.kind')
|
||||
v-radio-group.pl-8(v-model='current.visibilityMode', mandatory, hide-details)
|
||||
v-radio(:label='$t("admin:navigation.visibilityMode.all")', value='all', color='primary')
|
||||
v-radio.mt-3(:label='$t("admin:navigation.visibilityMode.restricted")', value='restricted', color='primary')
|
||||
.pl-8
|
||||
v-select.pl-8.mt-3(
|
||||
item-text='name'
|
||||
item-value='id'
|
||||
outlined
|
||||
prepend-icon='mdi-account-group'
|
||||
label='Groups'
|
||||
:disabled='current.visibilityMode !== `restricted`'
|
||||
v-model='current.visibilityGroups'
|
||||
:items='groups'
|
||||
persistent-hint
|
||||
clearable
|
||||
multiple
|
||||
)
|
||||
template(v-else)
|
||||
v-toolbar(height='56', color='teal lighten-1', flat, dark)
|
||||
v-card-text.grey--text(v-if='currentTree.length > 0') {{$t('navigation.noSelectionText')}}
|
||||
v-card-text.grey--text(v-else) {{$t('navigation.noItemsText')}}
|
||||
|
||||
v-dialog(v-model='copyFromLocaleDialogIsShown', max-width='650', persistent)
|
||||
v-card
|
||||
.dialog-header.is-short.is-teal
|
||||
v-icon.mr-3(color='white') mdi-arrange-send-backward
|
||||
span {{$t('admin:navigation.copyFromLocale')}}
|
||||
v-card-text.pt-5
|
||||
.body-2 {{$t('admin:navigation.copyFromLocaleInfoText')}}
|
||||
v-select.mt-3(
|
||||
:items='locales'
|
||||
item-text='nativeName'
|
||||
item-value='code'
|
||||
outlined
|
||||
prepend-icon='mdi-web'
|
||||
v-model='copyFromLocaleCode'
|
||||
:label='$t(`admin:navigation.sourceLocale`)'
|
||||
:hint='$t(`admin:navigation.sourceLocaleHint`)'
|
||||
persistent-hint
|
||||
)
|
||||
v-card-chin
|
||||
v-spacer
|
||||
v-btn(text, @click='copyFromLocaleDialogIsShown = false') {{$t('common:actions.cancel')}}
|
||||
v-btn.px-3(depressed, color='primary', @click='copyFromLocale')
|
||||
v-icon(left) mdi-chevron-right
|
||||
span {{$t('common:actions.copy')}}
|
||||
|
||||
page-selector(mode='select', v-model='selectPageModal', :open-handler='selectPageHandle', path='home', :locale='currentLang')
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import gql from 'graphql-tag'
|
||||
import { v4 as uuid } from 'uuid'
|
||||
|
||||
import groupsQuery from 'gql/admin/users/users-query-groups.gql'
|
||||
|
||||
import draggable from 'vuedraggable'
|
||||
|
||||
/* global siteConfig, siteLangs */
|
||||
|
||||
export default {
|
||||
components: {
|
||||
draggable
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectPageModal: false,
|
||||
trees: [],
|
||||
current: {},
|
||||
currentLang: siteConfig.lang,
|
||||
groups: [],
|
||||
copyFromLocaleDialogIsShown: false,
|
||||
config: {
|
||||
mode: 'NONE'
|
||||
},
|
||||
allLocales: [],
|
||||
copyFromLocaleCode: 'en'
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
navTypes () {
|
||||
return [
|
||||
{ text: this.$t('navigation.navType.external'), value: 'external' },
|
||||
{ text: this.$t('navigation.navType.externalblank'), value: 'externalblank' },
|
||||
{ text: this.$t('navigation.navType.home'), value: 'home' },
|
||||
{ text: this.$t('navigation.navType.page'), value: 'page' }
|
||||
// { text: this.$t('navigation.navType.searchQuery'), value: 'search' }
|
||||
]
|
||||
},
|
||||
locales () {
|
||||
return _.intersectionBy(this.allLocales, _.unionBy(siteLangs, [{ code: 'en' }, { code: siteConfig.lang }], 'code'), 'code')
|
||||
},
|
||||
currentTree: {
|
||||
get () {
|
||||
return _.get(_.find(this.trees, ['locale', this.currentLang]), 'items', null) || []
|
||||
},
|
||||
set (val) {
|
||||
const tree = _.find(this.trees, ['locale', this.currentLang])
|
||||
if (tree) {
|
||||
tree.items = val
|
||||
} else {
|
||||
this.trees = [...this.trees, {
|
||||
locale: this.currentLang,
|
||||
items: val
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
currentLang (newValue, oldValue) {
|
||||
this.$nextTick(() => {
|
||||
if (this.currentTree.length > 0) {
|
||||
this.current = this.currentTree[0]
|
||||
} else {
|
||||
this.current = {}
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
addItem(kind) {
|
||||
let newItem = {
|
||||
id: uuid(),
|
||||
kind,
|
||||
visibilityMode: 'all',
|
||||
visibilityGroups: []
|
||||
}
|
||||
switch (kind) {
|
||||
case 'link':
|
||||
newItem = {
|
||||
...newItem,
|
||||
label: this.$t('navigation.untitled', { kind: this.$t(`navigation.link`) }),
|
||||
icon: 'mdi-chevron-right',
|
||||
targetType: 'home',
|
||||
target: ''
|
||||
}
|
||||
break
|
||||
case 'header':
|
||||
newItem.label = this.$t('navigation.untitled', { kind: this.$t(`navigation.header`) })
|
||||
break
|
||||
}
|
||||
this.currentTree = [...this.currentTree, newItem]
|
||||
this.current = newItem
|
||||
},
|
||||
deleteItem(item) {
|
||||
this.currentTree = _.pull(this.currentTree, item)
|
||||
this.current = {}
|
||||
},
|
||||
selectItem(item) {
|
||||
this.current = item
|
||||
},
|
||||
selectPage() {
|
||||
this.selectPageModal = true
|
||||
},
|
||||
selectPageHandle ({ path, locale }) {
|
||||
this.current.target = `/${locale}/${path}`
|
||||
},
|
||||
copyFromLocale () {
|
||||
this.copyFromLocaleDialogIsShown = false
|
||||
this.currentTree = [...this.currentTree, ..._.get(_.find(this.trees, ['locale', this.copyFromLocaleCode]), 'items', null) || []]
|
||||
},
|
||||
async save() {
|
||||
this.$store.commit(`loadingStart`, 'admin-navigation-save')
|
||||
try {
|
||||
const resp = await this.$apollo.mutate({
|
||||
mutation: gql`
|
||||
mutation ($tree: [NavigationTreeInput]!, $mode: NavigationMode!) {
|
||||
navigation{
|
||||
updateTree(tree: $tree) {
|
||||
responseResult {
|
||||
succeeded
|
||||
errorCode
|
||||
slug
|
||||
message
|
||||
}
|
||||
},
|
||||
updateConfig(mode: $mode) {
|
||||
responseResult {
|
||||
succeeded
|
||||
errorCode
|
||||
slug
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
tree: this.trees,
|
||||
mode: this.config.mode
|
||||
}
|
||||
})
|
||||
if (_.get(resp, 'data.navigation.updateTree.responseResult.succeeded', false) && _.get(resp, 'data.navigation.updateConfig.responseResult.succeeded', false)) {
|
||||
this.$store.commit('showNotification', {
|
||||
message: this.$t('navigation.saveSuccess'),
|
||||
style: 'success',
|
||||
icon: 'check'
|
||||
})
|
||||
} else {
|
||||
throw new Error(_.get(resp, 'data.navigation.updateTree.responseResult.message', 'An unexpected error occurred.'))
|
||||
}
|
||||
} catch (err) {
|
||||
this.$store.commit('pushGraphError', err)
|
||||
}
|
||||
this.$store.commit(`loadingStop`, 'admin-navigation-save')
|
||||
},
|
||||
async refresh() {
|
||||
await this.$apollo.queries.trees.refetch()
|
||||
this.current = {}
|
||||
this.$store.commit('showNotification', {
|
||||
message: 'Navigation has been refreshed.',
|
||||
style: 'success',
|
||||
icon: 'cached'
|
||||
})
|
||||
}
|
||||
},
|
||||
apollo: {
|
||||
config: {
|
||||
query: gql`
|
||||
{
|
||||
navigation {
|
||||
config {
|
||||
mode
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
fetchPolicy: 'network-only',
|
||||
update: (data) => _.cloneDeep(data.navigation.config),
|
||||
watchLoading (isLoading) {
|
||||
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-navigation-config')
|
||||
}
|
||||
},
|
||||
trees: {
|
||||
query: gql`
|
||||
{
|
||||
navigation {
|
||||
tree {
|
||||
locale
|
||||
items {
|
||||
id
|
||||
kind
|
||||
label
|
||||
icon
|
||||
targetType
|
||||
target
|
||||
visibilityMode
|
||||
visibilityGroups
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
fetchPolicy: 'network-only',
|
||||
update: (data) => _.cloneDeep(data.navigation.tree),
|
||||
watchLoading (isLoading) {
|
||||
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-navigation-tree')
|
||||
}
|
||||
},
|
||||
groups: {
|
||||
query: groupsQuery,
|
||||
fetchPolicy: 'network-only',
|
||||
update: (data) => data.groups.list,
|
||||
watchLoading (isLoading) {
|
||||
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-navigation-groups')
|
||||
}
|
||||
},
|
||||
allLocales: {
|
||||
query: gql`
|
||||
{
|
||||
localization {
|
||||
locales {
|
||||
code
|
||||
name
|
||||
nativeName
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
fetchPolicy: 'network-only',
|
||||
update: (data) => data.localization.locales,
|
||||
watchLoading (isLoading) {
|
||||
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-navigation-locales')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang='scss' scoped>
|
||||
|
||||
.clickable {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(mc('blue', '500'), .25);
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
@ -1,235 +0,0 @@
|
||||
<template lang='pug'>
|
||||
v-container(fluid, grid-list-lg)
|
||||
v-layout(row, wrap, v-if='page.id')
|
||||
v-flex(xs12)
|
||||
.admin-header
|
||||
img.animated.fadeInUp(src='/_assets/svg/icon-view-details.svg', alt='Edit Page', style='width: 80px;')
|
||||
.admin-header-title
|
||||
.headline.blue--text.text--darken-2.animated.fadeInLeft Page Details
|
||||
.subtitle-1.grey--text.animated.fadeInLeft.wait-p2s
|
||||
v-chip.ml-0.mr-2(label, small).caption ID {{page.id}}
|
||||
span /{{page.locale}}/{{page.path}}
|
||||
v-spacer
|
||||
template(v-if='page.isPublished')
|
||||
status-indicator.mr-3(positive, pulse)
|
||||
.caption.green--text {{$t('common:page.published')}}
|
||||
template(v-else)
|
||||
status-indicator.mr-3(negative, pulse)
|
||||
.caption.red--text {{$t('common:page.unpublished')}}
|
||||
template(v-if='page.isPrivate')
|
||||
status-indicator.mr-3.ml-4(intermediary, pulse)
|
||||
.caption.deep-orange--text {{$t('common:page.private')}}
|
||||
template(v-else)
|
||||
status-indicator.mr-3.ml-4(active, pulse)
|
||||
.caption.blue--text {{$t('common:page.global')}}
|
||||
v-spacer
|
||||
v-btn.animated.fadeInDown.wait-p3s(color='grey', icon, outlined, to='/pages')
|
||||
v-icon mdi-arrow-left
|
||||
v-menu(offset-y, origin='top right')
|
||||
template(v-slot:activator='{ on }')
|
||||
v-btn.mx-3.animated.fadeInDown.wait-p2s(color='black', v-on='on', depressed, dark)
|
||||
span Actions
|
||||
v-icon(right) mdi-chevron-down
|
||||
v-list(dense, nav)
|
||||
v-list-item(:href='`/` + page.locale + `/` + page.path')
|
||||
v-list-item-icon
|
||||
v-icon(color='indigo') mdi-text-subject
|
||||
v-list-item-title View
|
||||
v-list-item(:href='`/e/` + page.locale + `/` + page.path')
|
||||
v-list-item-icon
|
||||
v-icon(color='indigo') mdi-pencil
|
||||
v-list-item-title Edit
|
||||
v-list-item(@click='', disabled)
|
||||
v-list-item-icon
|
||||
v-icon(color='grey') mdi-cube-scan
|
||||
v-list-item-title Re-Render
|
||||
v-list-item(@click='', disabled)
|
||||
v-list-item-icon
|
||||
v-icon(color='grey') mdi-earth-remove
|
||||
v-list-item-title Unpublish
|
||||
v-list-item(:href='`/s/` + page.locale + `/` + page.path')
|
||||
v-list-item-icon
|
||||
v-icon(color='indigo') mdi-code-tags
|
||||
v-list-item-title View Source
|
||||
v-list-item(:href='`/h/` + page.locale + `/` + page.path')
|
||||
v-list-item-icon
|
||||
v-icon(color='indigo') mdi-history
|
||||
v-list-item-title View History
|
||||
v-list-item(@click='', disabled)
|
||||
v-list-item-icon
|
||||
v-icon(color='grey') mdi-content-duplicate
|
||||
v-list-item-title Duplicate
|
||||
v-list-item(@click='', disabled)
|
||||
v-list-item-icon
|
||||
v-icon(color='grey') mdi-content-save-move-outline
|
||||
v-list-item-title Move / Rename
|
||||
v-dialog(v-model='deletePageDialog', max-width='500')
|
||||
template(v-slot:activator='{ on }')
|
||||
v-list-item(v-on='on')
|
||||
v-list-item-icon
|
||||
v-icon(color='red') mdi-trash-can-outline
|
||||
v-list-item-title Delete
|
||||
v-card
|
||||
.dialog-header.is-short.is-red
|
||||
v-icon.mr-2(color='white') mdi-file-document-box-remove-outline
|
||||
span {{$t('common:page.delete')}}
|
||||
v-card-text.pt-5
|
||||
i18next.body-2(path='common:page.deleteTitle', tag='div')
|
||||
span.red--text.text--darken-2(place='title') {{page.title}}
|
||||
.caption {{$t('common:page.deleteSubtitle')}}
|
||||
v-chip.mt-3.ml-0.mr-1(label, color='red lighten-4', disabled, small)
|
||||
.caption.red--text.text--darken-2 {{page.locale.toUpperCase()}}
|
||||
v-chip.mt-3.mx-0(label, color='red lighten-5', disabled, small)
|
||||
span.red--text.text--darken-2 /{{page.path}}
|
||||
v-card-chin
|
||||
v-spacer
|
||||
v-btn(text, @click='deletePageDialog = false', :disabled='loading') {{$t('common:actions.cancel')}}
|
||||
v-btn(color='red darken-2', @click='deletePage', :loading='loading').white--text {{$t('common:actions.delete')}}
|
||||
v-btn.animated.fadeInDown(color='success', large, depressed, disabled)
|
||||
v-icon(left) mdi-check
|
||||
span Save Changes
|
||||
v-flex(xs12, lg6)
|
||||
v-card.animated.fadeInUp
|
||||
v-toolbar(color='primary', dense, dark, flat)
|
||||
v-icon.mr-2 mdi-text-subject
|
||||
span Properties
|
||||
v-list.py-0(two-line, dense)
|
||||
v-list-item
|
||||
v-list-item-content
|
||||
v-list-item-title: .overline.grey--text Title
|
||||
v-list-item-subtitle.body-2(:class='$vuetify.theme.dark ? `grey--text text--lighten-2` : `grey--text text--darken-3`') {{ page.title }}
|
||||
v-divider
|
||||
v-list-item
|
||||
v-list-item-content
|
||||
v-list-item-title: .overline.grey--text Description
|
||||
v-list-item-subtitle.body-2(:class='$vuetify.theme.dark ? `grey--text text--lighten-2` : `grey--text text--darken-3`') {{ page.description || '-' }}
|
||||
v-divider
|
||||
v-list-item
|
||||
v-list-item-content
|
||||
v-list-item-title: .overline.grey--text Locale
|
||||
v-list-item-subtitle.body-2(:class='$vuetify.theme.dark ? `grey--text text--lighten-2` : `grey--text text--darken-3`') {{ page.locale }}
|
||||
v-divider
|
||||
v-list-item
|
||||
v-list-item-content
|
||||
v-list-item-title: .overline.grey--text Path
|
||||
v-list-item-subtitle.body-2(:class='$vuetify.theme.dark ? `grey--text text--lighten-2` : `grey--text text--darken-3`') {{ page.path }}
|
||||
v-divider
|
||||
v-list-item
|
||||
v-list-item-content
|
||||
v-list-item-title: .overline.grey--text Editor
|
||||
v-list-item-subtitle.body-2(:class='$vuetify.theme.dark ? `grey--text text--lighten-2` : `grey--text text--darken-3`') {{ page.editor || '?' }}
|
||||
v-divider
|
||||
v-list-item
|
||||
v-list-item-content
|
||||
v-list-item-title: .overline.grey--text Content Type
|
||||
v-list-item-subtitle.body-2(:class='$vuetify.theme.dark ? `grey--text text--lighten-2` : `grey--text text--darken-3`') {{ page.contentType || '?' }}
|
||||
v-divider
|
||||
v-list-item
|
||||
v-list-item-content
|
||||
v-list-item-title: .overline.grey--text Page Hash
|
||||
v-list-item-subtitle.body-2(:class='$vuetify.theme.dark ? `grey--text text--lighten-2` : `grey--text text--darken-3`') {{ page.hash }}
|
||||
|
||||
v-flex(xs12, lg6)
|
||||
v-card.animated.fadeInUp.wait-p2s
|
||||
v-toolbar(color='primary', dense, dark, flat)
|
||||
v-icon.mr-2 mdi-account-multiple
|
||||
span Users
|
||||
v-list.py-0(two-line, dense)
|
||||
v-list-item
|
||||
v-list-item-avatar(size='24')
|
||||
v-btn(icon, :to='`/users/` + page.creatorId')
|
||||
v-icon(color='grey') mdi-account
|
||||
v-list-item-content
|
||||
v-list-item-title: .overline.grey--text Creator
|
||||
v-list-item-subtitle.body-2(:class='$vuetify.theme.dark ? `grey--text text--lighten-2` : `grey--text text--darken-3`') {{ page.creatorName }} #[em.caption ({{ page.creatorEmail }})]
|
||||
v-list-item-action
|
||||
v-list-item-action-text {{ page.createdAt | moment('calendar') }}
|
||||
v-divider
|
||||
v-list-item
|
||||
v-list-item-avatar(size='24')
|
||||
v-btn(icon, :to='`/users/` + page.authorId')
|
||||
v-icon(color='grey') mdi-account
|
||||
v-list-item-content
|
||||
v-list-item-title: .overline.grey--text Last Editor
|
||||
v-list-item-subtitle.body-2(:class='$vuetify.theme.dark ? `grey--text text--lighten-2` : `grey--text text--darken-3`') {{ page.authorName }} #[em.caption ({{ page.authorEmail }})]
|
||||
v-list-item-action
|
||||
v-list-item-action-text {{ page.updatedAt | moment('calendar') }}
|
||||
|
||||
v-layout(row, align-center, v-else)
|
||||
v-progress-circular(indeterminate, width='2', color='grey')
|
||||
.body-2.pl-3.grey--text {{ $t('common:page.loading') }}
|
||||
|
||||
</template>
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import { StatusIndicator } from 'vue-status-indicator'
|
||||
|
||||
import pageQuery from 'gql/admin/pages/pages-query-single.gql'
|
||||
import deletePageMutation from 'gql/common/common-pages-mutation-delete.gql'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
StatusIndicator
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
deletePageDialog: false,
|
||||
page: {},
|
||||
loading: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async deletePage() {
|
||||
this.loading = true
|
||||
this.$store.commit(`loadingStart`, 'page-delete')
|
||||
try {
|
||||
const resp = await this.$apollo.mutate({
|
||||
mutation: deletePageMutation,
|
||||
variables: {
|
||||
id: this.page.id
|
||||
}
|
||||
})
|
||||
if (_.get(resp, 'data.pages.delete.responseResult.succeeded', false)) {
|
||||
this.$store.commit('showNotification', {
|
||||
style: 'green',
|
||||
message: `Page deleted successfully.`,
|
||||
icon: 'check'
|
||||
})
|
||||
this.$router.replace('/pages')
|
||||
} else {
|
||||
throw new Error(_.get(resp, 'data.pages.delete.responseResult.message', this.$t('common:error.unexpected')))
|
||||
}
|
||||
} catch (err) {
|
||||
this.$store.commit('pushGraphError', err)
|
||||
}
|
||||
this.$store.commit(`loadingStop`, 'page-delete')
|
||||
},
|
||||
async rerenderPage() {
|
||||
this.$store.commit('showNotification', {
|
||||
style: 'indigo',
|
||||
message: `Coming soon...`,
|
||||
icon: 'directions_boat'
|
||||
})
|
||||
}
|
||||
},
|
||||
apollo: {
|
||||
page: {
|
||||
query: pageQuery,
|
||||
variables() {
|
||||
return {
|
||||
id: _.toSafeInteger(this.$route.params.id)
|
||||
}
|
||||
},
|
||||
fetchPolicy: 'network-only',
|
||||
update: (data) => data.pages.single,
|
||||
watchLoading (isLoading) {
|
||||
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-pages-refresh')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang='scss'>
|
||||
|
||||
</style>
|
@ -1,405 +0,0 @@
|
||||
<template lang='pug'>
|
||||
v-container(fluid, grid-list-lg)
|
||||
v-layout(row wrap)
|
||||
v-flex(xs12)
|
||||
.admin-header
|
||||
img.animated.fadeInUp(src='/_assets/svg/icon-venn-diagram.svg', alt='Visualize Pages', style='width: 80px;')
|
||||
.admin-header-title
|
||||
.headline.blue--text.text--darken-2.animated.fadeInLeft Visualize Pages
|
||||
.subtitle-1.grey--text.animated.fadeInLeft.wait-p2s Dendrogram representation of your pages
|
||||
v-spacer
|
||||
v-select.mx-5.animated.fadeInDown.wait-p1s(
|
||||
v-if='locales.length > 0'
|
||||
v-model='currentLocale'
|
||||
:items='locales'
|
||||
style='flex: 0 1 120px;'
|
||||
solo
|
||||
dense
|
||||
hide-details
|
||||
item-value='code'
|
||||
item-text='name'
|
||||
)
|
||||
v-btn-toggle.animated.fadeInDown(v-model='graphMode', color='primary', dense, rounded)
|
||||
v-btn.px-5(value='htree')
|
||||
v-icon(left, :color='graphMode === `htree` ? `primary` : `grey darken-3`') mdi-sitemap
|
||||
span.text-none Hierarchical Tree
|
||||
v-btn.px-5(value='hradial')
|
||||
v-icon(left, :color='graphMode === `hradial` ? `primary` : `grey darken-3`') mdi-chart-donut-variant
|
||||
span.text-none Hierarchical Radial
|
||||
v-btn.px-5(value='rradial')
|
||||
v-icon(left, :color='graphMode === `rradial` ? `primary` : `grey darken-3`') mdi-blur-radial
|
||||
span.text-none Relational Radial
|
||||
.admin-pages-visualize-svg(ref='svgContainer', v-show='pages.length >= 1')
|
||||
v-alert(v-if='pages.length < 1', outlined, type='warning', style='max-width: 650px; margin: 0 auto;') Looks like there's no data yet to graph!
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import * as d3 from 'd3'
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
/* global siteConfig, siteLangs */
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
graphMode: 'htree',
|
||||
width: 800,
|
||||
radius: 400,
|
||||
pages: [],
|
||||
locales: siteLangs,
|
||||
currentLocale: siteConfig.lang
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
pages () {
|
||||
this.redraw()
|
||||
},
|
||||
graphMode () {
|
||||
this.redraw()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
goToPage (d) {
|
||||
const id = d.data.id
|
||||
if (id) {
|
||||
if (d3.event.ctrlKey || d3.event.metaKey) {
|
||||
const { href } = this.$router.resolve(String(id))
|
||||
window.open(href, '_blank')
|
||||
} else {
|
||||
this.$router.push(String(id))
|
||||
}
|
||||
}
|
||||
},
|
||||
bilink (root) {
|
||||
const map = new Map(root.descendants().map(d => [d.data.path, d]))
|
||||
for (const d of root.descendants()) {
|
||||
d.incoming = []
|
||||
d.outgoing = []
|
||||
d.data.links.forEach(i => {
|
||||
const relNode = map.get(i)
|
||||
if (relNode) {
|
||||
d.outgoing.push([d, relNode])
|
||||
}
|
||||
})
|
||||
}
|
||||
for (const d of root.descendants()) {
|
||||
for (const o of d.outgoing) {
|
||||
if (o[1]) {
|
||||
o[1].incoming.push(o)
|
||||
}
|
||||
}
|
||||
}
|
||||
return root
|
||||
},
|
||||
hierarchy (pages) {
|
||||
const map = new Map(pages.map(p => [p.path, p]))
|
||||
const getPage = path => map.get(path) || {
|
||||
path: path,
|
||||
title: path.split('/').slice(-1)[0],
|
||||
links: []
|
||||
}
|
||||
|
||||
function recurse (depth, [parent, descendants]) {
|
||||
const truncatePath = path => _.take(path.split('/'), depth).join('/')
|
||||
const descendantsByChild =
|
||||
Object.entries(_.groupBy(descendants, page => truncatePath(page.path)))
|
||||
.map(([childPath, descendantsGroup]) => [getPage(childPath), descendantsGroup])
|
||||
.map(([child, descendantsGroup]) =>
|
||||
[child, _.filter(descendantsGroup, d => d.path !== child.path)])
|
||||
return {
|
||||
...parent,
|
||||
children: descendantsByChild.map(_.partial(recurse, depth + 1))
|
||||
}
|
||||
}
|
||||
const root = { path: this.currentLocale, title: this.currentLocale, links: [] }
|
||||
// start at depth=2 because we're taking {locale} as the root and
|
||||
// all paths start with {locale}/
|
||||
return recurse(2, [root, pages])
|
||||
},
|
||||
/**
|
||||
* Relational Radial
|
||||
*/
|
||||
drawRelations () {
|
||||
const data = this.hierarchy(this.pages)
|
||||
|
||||
const line = d3.lineRadial()
|
||||
.curve(d3.curveBundle.beta(0.85))
|
||||
.radius(d => d.y)
|
||||
.angle(d => d.x)
|
||||
|
||||
const tree = d3.cluster()
|
||||
.size([2 * Math.PI, this.radius - 100])
|
||||
|
||||
const root = tree(this.bilink(d3.hierarchy(data)
|
||||
.sort((a, b) => d3.ascending(a.height, b.height) || d3.ascending(a.data.path, b.data.path))))
|
||||
|
||||
const svg = d3.create('svg')
|
||||
.attr('viewBox', [-this.width / 2, -this.width / 2, this.width, this.width])
|
||||
|
||||
const g = svg.append('g')
|
||||
|
||||
svg.call(d3.zoom().on('zoom', function() {
|
||||
g.attr('transform', d3.event.transform)
|
||||
}))
|
||||
|
||||
const link = g.append('g')
|
||||
.attr('stroke', '#CCC')
|
||||
.attr('fill', 'none')
|
||||
.selectAll('path')
|
||||
.data(root.descendants().flatMap(leaf => leaf.outgoing))
|
||||
.join('path')
|
||||
.style('mix-blend-mode', 'multiply')
|
||||
.attr('d', ([i, o]) => line(i.path(o)))
|
||||
.each(function(d) { d.path = this })
|
||||
|
||||
g.append('g')
|
||||
.attr('font-family', 'sans-serif')
|
||||
.attr('font-size', 10)
|
||||
.selectAll('g')
|
||||
.data(root.descendants())
|
||||
.join('g')
|
||||
.attr('transform', d => `rotate(${d.x * 180 / Math.PI - 90}) translate(${d.y},0)`)
|
||||
.append('text')
|
||||
.attr('dy', '0.31em')
|
||||
.attr('x', d => d.x < Math.PI ? 6 : -6)
|
||||
.attr('text-anchor', d => d.x < Math.PI ? 'start' : 'end')
|
||||
.attr('transform', d => d.x >= Math.PI ? 'rotate(180)' : null)
|
||||
.attr('fill', this.$vuetify.theme.dark ? 'white' : '')
|
||||
.attr('cursor', 'pointer')
|
||||
.text(d => d.data.title)
|
||||
.each(function(d) { d.text = this })
|
||||
.on('mouseover', overed)
|
||||
.on('mouseout', outed)
|
||||
.on('click', d => this.goToPage(d))
|
||||
.call(text => text.append('title').text(d => `${d.data.path}
|
||||
${d.outgoing.length} outgoing
|
||||
${d.incoming.length} incoming`))
|
||||
.clone(true).lower()
|
||||
.attr('stroke', this.$vuetify.theme.dark ? '#222' : 'white')
|
||||
|
||||
function overed(d) {
|
||||
link.style('mix-blend-mode', null)
|
||||
d3.select(this).attr('font-weight', 'bold')
|
||||
d3.selectAll(d.incoming.map(d => d.path)).attr('stroke', '#2196F3').raise()
|
||||
d3.selectAll(d.incoming.map(([d]) => d.text)).attr('fill', '#2196F3').attr('font-weight', 'bold')
|
||||
d3.selectAll(d.outgoing.map(d => d.path)).attr('stroke', '#E91E63').raise()
|
||||
d3.selectAll(d.outgoing.map(([, d]) => d.text)).attr('fill', '#E91E63').attr('font-weight', 'bold')
|
||||
}
|
||||
|
||||
function outed(d) {
|
||||
link.style('mix-blend-mode', 'multiply')
|
||||
d3.select(this).attr('font-weight', null)
|
||||
d3.selectAll(d.incoming.map(d => d.path)).attr('stroke', null)
|
||||
d3.selectAll(d.incoming.map(([d]) => d.text)).attr('fill', null).attr('font-weight', null)
|
||||
d3.selectAll(d.outgoing.map(d => d.path)).attr('stroke', null)
|
||||
d3.selectAll(d.outgoing.map(([, d]) => d.text)).attr('fill', null).attr('font-weight', null)
|
||||
}
|
||||
|
||||
this.$refs.svgContainer.appendChild(svg.node())
|
||||
},
|
||||
/**
|
||||
* Hierarchical Tree
|
||||
*/
|
||||
drawTree () {
|
||||
const data = this.hierarchy(this.pages)
|
||||
|
||||
const treeRoot = d3.hierarchy(data)
|
||||
treeRoot.dx = 10
|
||||
treeRoot.dy = this.width / (treeRoot.height + 1)
|
||||
const root = d3.tree().nodeSize([treeRoot.dx, treeRoot.dy])(treeRoot)
|
||||
|
||||
let x0 = Infinity
|
||||
let x1 = -x0
|
||||
root.each(d => {
|
||||
if (d.x > x1) x1 = d.x
|
||||
if (d.x < x0) x0 = d.x
|
||||
})
|
||||
|
||||
const svg = d3.create('svg')
|
||||
.attr('viewBox', [0, 0, this.width, x1 - x0 + root.dx * 2])
|
||||
|
||||
// this extra level is necessary because the element that we
|
||||
// apply the zoom tranform to must be above the element where
|
||||
// we apply the translation (`g`), or else zoom is wonky
|
||||
const gZoom = svg.append('g')
|
||||
|
||||
svg.call(d3.zoom().on('zoom', function() {
|
||||
gZoom.attr('transform', d3.event.transform)
|
||||
}))
|
||||
|
||||
const g = gZoom.append('g')
|
||||
.attr('font-family', 'sans-serif')
|
||||
.attr('font-size', 10)
|
||||
.attr('transform', `translate(${root.dy / 3},${root.dx - x0})`)
|
||||
|
||||
g.append('g')
|
||||
.attr('fill', 'none')
|
||||
.attr('stroke', this.$vuetify.theme.dark ? '#999' : '#555')
|
||||
.attr('stroke-opacity', 0.4)
|
||||
.attr('stroke-width', 1.5)
|
||||
.selectAll('path')
|
||||
.data(root.links())
|
||||
.join('path')
|
||||
.attr('d', d3.linkHorizontal()
|
||||
.x(d => d.y)
|
||||
.y(d => d.x))
|
||||
|
||||
const node = g.append('g')
|
||||
.attr('stroke-linejoin', 'round')
|
||||
.attr('stroke-width', 3)
|
||||
.selectAll('g')
|
||||
.data(root.descendants())
|
||||
.join('g')
|
||||
.attr('transform', d => `translate(${d.y},${d.x})`)
|
||||
|
||||
node.append('circle')
|
||||
.attr('fill', d => d.children ? '#555' : '#999')
|
||||
.attr('r', 2.5)
|
||||
|
||||
node.append('text')
|
||||
.attr('dy', '0.31em')
|
||||
.attr('x', d => d.children ? -6 : 6)
|
||||
.attr('text-anchor', d => d.children ? 'end' : 'start')
|
||||
.attr('fill', this.$vuetify.theme.dark ? 'white' : '')
|
||||
.attr('cursor', 'pointer')
|
||||
.text(d => d.data.title)
|
||||
.on('click', d => this.goToPage(d))
|
||||
.clone(true).lower()
|
||||
.attr('stroke', this.$vuetify.theme.dark ? '#222' : 'white')
|
||||
|
||||
this.$refs.svgContainer.appendChild(svg.node())
|
||||
},
|
||||
/**
|
||||
* Hierarchical Radial
|
||||
*/
|
||||
drawRadialTree () {
|
||||
const data = this.hierarchy(this.pages)
|
||||
|
||||
const tree = d3.tree()
|
||||
.size([2 * Math.PI, this.radius])
|
||||
.separation((a, b) => (a.parent === b.parent ? 1 : 2) / a.depth)
|
||||
|
||||
const root = tree(d3.hierarchy(data)
|
||||
.sort((a, b) => d3.ascending(a.data.title, b.data.title)))
|
||||
|
||||
const svg = d3.create('svg')
|
||||
.style('font', '10px sans-serif')
|
||||
|
||||
const g = svg.append('g')
|
||||
|
||||
svg.call(d3.zoom().on('zoom', function () {
|
||||
g.attr('transform', d3.event.transform)
|
||||
}))
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const link = g.append('g')
|
||||
.attr('fill', 'none')
|
||||
.attr('stroke', this.$vuetify.theme.dark ? 'white' : '#555')
|
||||
.attr('stroke-opacity', 0.4)
|
||||
.attr('stroke-width', 1.5)
|
||||
.selectAll('path')
|
||||
.data(root.links())
|
||||
.join('path')
|
||||
.attr('d', d3.linkRadial()
|
||||
.angle(d => d.x)
|
||||
.radius(d => d.y))
|
||||
|
||||
const node = g.append('g')
|
||||
.attr('stroke-linejoin', 'round')
|
||||
.attr('stroke-width', 3)
|
||||
.selectAll('g')
|
||||
.data(root.descendants().reverse())
|
||||
.join('g')
|
||||
.attr('transform', d => `
|
||||
rotate(${d.x * 180 / Math.PI - 90})
|
||||
translate(${d.y},0)
|
||||
`)
|
||||
|
||||
node.append('circle')
|
||||
.attr('fill', d => d.children ? '#555' : '#999')
|
||||
.attr('r', 2.5)
|
||||
|
||||
node.append('text')
|
||||
.attr('dy', '0.31em')
|
||||
/* eslint-disable no-mixed-operators */
|
||||
.attr('x', d => d.x < Math.PI === !d.children ? 6 : -6)
|
||||
.attr('text-anchor', d => d.x < Math.PI === !d.children ? 'start' : 'end')
|
||||
.attr('transform', d => d.x >= Math.PI ? 'rotate(180)' : null)
|
||||
/* eslint-enable no-mixed-operators */
|
||||
.attr('fill', this.$vuetify.theme.dark ? 'white' : '')
|
||||
.attr('cursor', 'pointer')
|
||||
.text(d => d.data.title)
|
||||
.on('click', d => this.goToPage(d))
|
||||
.clone(true).lower()
|
||||
.attr('stroke', this.$vuetify.theme.dark ? '#222' : 'white')
|
||||
|
||||
this.$refs.svgContainer.appendChild(svg.node())
|
||||
|
||||
function autoBox() {
|
||||
const {x, y, width, height} = this.getBBox()
|
||||
return [x, y, width, height]
|
||||
}
|
||||
|
||||
svg.attr('viewBox', autoBox)
|
||||
},
|
||||
redraw () {
|
||||
while (this.$refs.svgContainer.firstChild) {
|
||||
this.$refs.svgContainer.firstChild.remove()
|
||||
}
|
||||
if (this.pages.length > 0) {
|
||||
switch (this.graphMode) {
|
||||
case 'rradial':
|
||||
this.drawRelations()
|
||||
break
|
||||
case 'htree':
|
||||
this.drawTree()
|
||||
break
|
||||
case 'hradial':
|
||||
this.drawRadialTree()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
apollo: {
|
||||
pages: {
|
||||
query: gql`
|
||||
query ($locale: String!) {
|
||||
pages {
|
||||
links(locale: $locale) {
|
||||
id
|
||||
path
|
||||
title
|
||||
links
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables () {
|
||||
return {
|
||||
locale: this.currentLocale
|
||||
}
|
||||
},
|
||||
fetchPolicy: 'network-only',
|
||||
update: (data) => data.pages.links,
|
||||
watchLoading (isLoading) {
|
||||
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-pages-refresh')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang='scss'>
|
||||
.admin-pages-visualize-svg {
|
||||
text-align: center;
|
||||
// 100vh - header - title section - footer - content padding
|
||||
height: calc(100vh - 64px - 92px - 32px - 16px);
|
||||
|
||||
> svg {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,169 +0,0 @@
|
||||
<template lang='pug'>
|
||||
v-container(fluid, grid-list-lg)
|
||||
v-layout(row wrap)
|
||||
v-flex(xs12)
|
||||
.admin-header
|
||||
img.animated.fadeInUp(src='/_assets/svg/icon-file.svg', alt='Page', style='width: 80px;')
|
||||
.admin-header-title
|
||||
.headline.blue--text.text--darken-2.animated.fadeInLeft Pages
|
||||
.subtitle-1.grey--text.animated.fadeInLeft.wait-p2s Manage pages
|
||||
v-spacer
|
||||
v-btn.animated.fadeInDown.wait-p1s(icon, color='grey', outlined, @click='refresh')
|
||||
v-icon.grey--text mdi-refresh
|
||||
v-btn.animated.fadeInDown.mx-3(color='primary', outlined, @click='recyclebin', disabled)
|
||||
v-icon(left) mdi-delete-outline
|
||||
span Recycle Bin
|
||||
v-btn.animated.fadeInDown(color='primary', depressed, large, to='pages/visualize')
|
||||
v-icon(left) mdi-graph
|
||||
span Visualize
|
||||
v-card.mt-3.animated.fadeInUp
|
||||
.pa-2.d-flex.align-center(:class='$vuetify.theme.dark ? `grey darken-3-d5` : `grey lighten-3`')
|
||||
v-text-field(
|
||||
solo
|
||||
flat
|
||||
v-model='search'
|
||||
prepend-inner-icon='mdi-file-search-outline'
|
||||
label='Search Pages...'
|
||||
hide-details
|
||||
dense
|
||||
style='max-width: 400px;'
|
||||
)
|
||||
v-spacer
|
||||
v-select.ml-2(
|
||||
solo
|
||||
flat
|
||||
hide-details
|
||||
dense
|
||||
label='Locale'
|
||||
:items='langs'
|
||||
v-model='selectedLang'
|
||||
style='max-width: 250px;'
|
||||
)
|
||||
v-select.ml-2(
|
||||
solo
|
||||
flat
|
||||
hide-details
|
||||
dense
|
||||
label='Publish State'
|
||||
:items='states'
|
||||
v-model='selectedState'
|
||||
style='max-width: 250px;'
|
||||
)
|
||||
v-divider
|
||||
v-data-table(
|
||||
:items='filteredPages'
|
||||
:headers='headers'
|
||||
:search='search'
|
||||
:page.sync='pagination'
|
||||
:items-per-page='15'
|
||||
:loading='loading'
|
||||
must-sort,
|
||||
sort-by='updatedAt',
|
||||
sort-desc,
|
||||
hide-default-footer
|
||||
@page-count="pageTotal = $event"
|
||||
)
|
||||
template(slot='item', slot-scope='props')
|
||||
tr.is-clickable(:active='props.selected', @click='$router.push(`/pages/` + props.item.id)')
|
||||
td.text-xs-right {{ props.item.id }}
|
||||
td
|
||||
.body-2: strong {{ props.item.title }}
|
||||
.caption {{ props.item.description }}
|
||||
td.admin-pages-path
|
||||
v-chip(label, small, :color='$vuetify.theme.dark ? `grey darken-4` : `grey lighten-4`') {{ props.item.locale }}
|
||||
span.ml-2.grey--text(:class='$vuetify.theme.dark ? `text--lighten-1` : `text--darken-2`') / {{ props.item.path }}
|
||||
td {{ props.item.createdAt | moment('calendar') }}
|
||||
td {{ props.item.updatedAt | moment('calendar') }}
|
||||
template(slot='no-data')
|
||||
v-alert.ma-3(icon='mdi-alert', :value='true', outlined) No pages to display.
|
||||
.text-center.py-2.animated.fadeInDown(v-if='this.pageTotal > 1')
|
||||
v-pagination(v-model='pagination', :length='pageTotal')
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import pagesQuery from 'gql/admin/pages/pages-query-list.gql'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
selectedPage: {},
|
||||
pagination: 1,
|
||||
pages: [],
|
||||
pageTotal: 0,
|
||||
headers: [
|
||||
{ text: 'ID', value: 'id', width: 80, sortable: true },
|
||||
{ text: 'Title', value: 'title' },
|
||||
{ text: 'Path', value: 'path' },
|
||||
{ text: 'Created', value: 'createdAt', width: 250 },
|
||||
{ text: 'Last Updated', value: 'updatedAt', width: 250 }
|
||||
],
|
||||
search: '',
|
||||
selectedLang: null,
|
||||
selectedState: null,
|
||||
states: [
|
||||
{ text: 'All Publishing States', value: null },
|
||||
{ text: 'Published', value: true },
|
||||
{ text: 'Not Published', value: false }
|
||||
],
|
||||
loading: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
filteredPages () {
|
||||
return _.filter(this.pages, pg => {
|
||||
if (this.selectedLang !== null && this.selectedLang !== pg.locale) {
|
||||
return false
|
||||
}
|
||||
if (this.selectedState !== null && this.selectedState !== pg.isPublished) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
},
|
||||
langs () {
|
||||
return _.concat({
|
||||
text: 'All Locales',
|
||||
value: null
|
||||
}, _.uniqBy(this.pages, 'locale').map(pg => ({
|
||||
text: pg.locale,
|
||||
value: pg.locale
|
||||
})))
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async refresh() {
|
||||
await this.$apollo.queries.pages.refetch()
|
||||
this.$store.commit('showNotification', {
|
||||
message: 'Page list has been refreshed.',
|
||||
style: 'success',
|
||||
icon: 'cached'
|
||||
})
|
||||
},
|
||||
newpage() {
|
||||
this.pageSelectorShown = true
|
||||
},
|
||||
recyclebin () { }
|
||||
},
|
||||
apollo: {
|
||||
pages: {
|
||||
query: pagesQuery,
|
||||
fetchPolicy: 'network-only',
|
||||
update: (data) => data.pages.list,
|
||||
watchLoading (isLoading) {
|
||||
this.loading = isLoading
|
||||
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-pages-refresh')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang='scss'>
|
||||
.admin-pages-path {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
font-family: 'Roboto Mono', monospace;
|
||||
}
|
||||
</style>
|
@ -1,261 +0,0 @@
|
||||
<template lang='pug'>
|
||||
v-container(fluid, grid-list-lg)
|
||||
v-layout(row, wrap)
|
||||
v-flex(xs12)
|
||||
.admin-header
|
||||
img.animated.fadeInUp(src='/_assets/svg/icon-process.svg', alt='Rendering', style='width: 80px;')
|
||||
.admin-header-title
|
||||
.headline.primary--text.animated.fadeInLeft {{ $t('admin:rendering.title') }}
|
||||
.subtitle-1.grey--text.animated.fadeInLeft.wait-p4s {{ $t('admin:rendering.subtitle') }}
|
||||
v-spacer
|
||||
v-btn.animated.fadeInDown.wait-p3s(icon, outlined, color='grey', href='https://docs.requarks.io/rendering', target='_blank')
|
||||
v-icon mdi-help-circle
|
||||
v-btn.mx-3.animated.fadeInDown.wait-p2s(icon, outlined, color='grey', @click='refresh')
|
||||
v-icon mdi-refresh
|
||||
v-btn.animated.fadeInDown(color='success', @click='save', depressed, large)
|
||||
v-icon(left) mdi-check
|
||||
span {{$t('common:actions.apply')}}
|
||||
|
||||
v-flex.animated.fadeInUp(lg3, xs12)
|
||||
v-toolbar(
|
||||
color='blue darken-2'
|
||||
dense
|
||||
flat
|
||||
dark
|
||||
)
|
||||
.subtitle-1 Pipeline
|
||||
v-expansion-panels.adm-rendering-pipeline(
|
||||
v-model='selectedCore'
|
||||
accordion
|
||||
mandatory
|
||||
)
|
||||
v-expansion-panel(
|
||||
v-for='core in renderers'
|
||||
:key='core.key'
|
||||
)
|
||||
v-expansion-panel-header(
|
||||
hide-actions
|
||||
ripple
|
||||
)
|
||||
v-toolbar(
|
||||
color='blue'
|
||||
dense
|
||||
dark
|
||||
flat
|
||||
)
|
||||
v-spacer
|
||||
.body-2 {{core.input}}
|
||||
v-icon.mx-2 mdi-arrow-right-circle
|
||||
.caption {{core.output}}
|
||||
v-spacer
|
||||
v-expansion-panel-content
|
||||
v-list.py-0(two-line, dense)
|
||||
template(v-for='(rdr, n) in core.children')
|
||||
v-list-item(
|
||||
:key='rdr.key'
|
||||
@click='selectRenderer(rdr.key)'
|
||||
:class='currentRenderer.key === rdr.key ? ($vuetify.theme.dark ? `grey darken-4-l4` : `blue lighten-5`) : ``'
|
||||
)
|
||||
v-list-item-avatar(size='24', tile)
|
||||
v-icon(:color='currentRenderer.key === rdr.key ? "primary" : "grey"') {{rdr.icon}}
|
||||
v-list-item-content
|
||||
v-list-item-title {{rdr.title}}
|
||||
v-list-item-subtitle: .caption {{rdr.description}}
|
||||
v-list-item-avatar(size='24')
|
||||
status-indicator(v-if='rdr.isEnabled', positive, pulse)
|
||||
status-indicator(v-else, negative, pulse)
|
||||
v-divider.my-0(v-if='n < core.children.length - 1')
|
||||
|
||||
v-flex(lg9, xs12)
|
||||
v-card.wiki-form.animated.fadeInUp
|
||||
v-toolbar(
|
||||
color='indigo'
|
||||
dark
|
||||
flat
|
||||
dense
|
||||
)
|
||||
v-icon.mr-2 {{currentRenderer.icon}}
|
||||
.subtitle-1 {{currentRenderer.title}}
|
||||
v-spacer
|
||||
v-switch(
|
||||
dark
|
||||
color='white'
|
||||
label='Enabled'
|
||||
v-model='currentRenderer.isEnabled'
|
||||
hide-details
|
||||
inset
|
||||
)
|
||||
v-card-info(color='blue')
|
||||
div
|
||||
div {{currentRenderer.description}}
|
||||
span.caption: a(href='https://docs.requarks.io/en/rendering', target='_blank') Documentation
|
||||
v-card-text.pb-4.pl-4
|
||||
.overline.mb-5 Rendering Module Configuration
|
||||
.body-2.ml-3(v-if='!currentRenderer.config || currentRenderer.config.length < 1'): em This rendering module has no configuration options you can modify.
|
||||
template(v-else, v-for='(cfg, idx) in currentRenderer.config')
|
||||
v-select(
|
||||
v-if='cfg.value.type === "string" && cfg.value.enum'
|
||||
outlined
|
||||
:items='cfg.value.enum'
|
||||
:key='cfg.key'
|
||||
:label='cfg.value.title'
|
||||
v-model='cfg.value.value'
|
||||
:hint='cfg.value.hint ? cfg.value.hint : ""'
|
||||
persistent-hint
|
||||
:class='cfg.value.hint ? "mb-2" : ""'
|
||||
color='indigo'
|
||||
)
|
||||
v-switch(
|
||||
v-else-if='cfg.value.type === "boolean"'
|
||||
:key='cfg.key'
|
||||
:label='cfg.value.title'
|
||||
v-model='cfg.value.value'
|
||||
color='indigo'
|
||||
:hint='cfg.value.hint ? cfg.value.hint : ""'
|
||||
persistent-hint
|
||||
inset
|
||||
)
|
||||
v-text-field(
|
||||
v-else
|
||||
outlined
|
||||
:key='cfg.key'
|
||||
:label='cfg.value.title'
|
||||
v-model='cfg.value.value'
|
||||
:hint='cfg.value.hint ? cfg.value.hint : ""'
|
||||
persistent-hint
|
||||
:class='cfg.value.hint ? "mb-2" : ""'
|
||||
color='indigo'
|
||||
)
|
||||
v-divider.my-5(v-if='idx < currentRenderer.config.length - 1')
|
||||
v-card-chin
|
||||
v-spacer
|
||||
.caption.pr-3.grey--text Module: {{ currentRenderer.key }}
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import { DepGraph } from 'dependency-graph'
|
||||
|
||||
import { StatusIndicator } from 'vue-status-indicator'
|
||||
|
||||
import renderersQuery from 'gql/admin/rendering/rendering-query-renderers.gql'
|
||||
import renderersSaveMutation from 'gql/admin/rendering/rendering-mutation-save-renderers.gql'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
StatusIndicator
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectedCore: -1,
|
||||
renderers: [],
|
||||
currentRenderer: {}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
renderers(newValue, oldValue) {
|
||||
_.delay(() => {
|
||||
this.selectedCore = _.findIndex(newValue, ['key', 'markdownCore'])
|
||||
this.selectRenderer('markdownCore')
|
||||
}, 500)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
selectRenderer (key) {
|
||||
this.renderers.map(rdr => {
|
||||
if (_.some(rdr.children, ['key', key])) {
|
||||
this.currentRenderer = _.find(rdr.children, ['key', key])
|
||||
}
|
||||
})
|
||||
},
|
||||
async refresh () {
|
||||
await this.$apollo.queries.renderers.refetch()
|
||||
this.$store.commit('showNotification', {
|
||||
message: 'Rendering active configuration has been reloaded.',
|
||||
style: 'success',
|
||||
icon: 'cached'
|
||||
})
|
||||
},
|
||||
async save () {
|
||||
this.$store.commit(`loadingStart`, 'admin-rendering-saverenderers')
|
||||
await this.$apollo.mutate({
|
||||
mutation: renderersSaveMutation,
|
||||
variables: {
|
||||
renderers: _.reduce(this.renderers, (result, core) => {
|
||||
result = _.concat(result, core.children.map(rd => ({
|
||||
key: rd.key,
|
||||
isEnabled: rd.isEnabled,
|
||||
config: rd.config.map(cfg => ({ key: cfg.key, value: JSON.stringify({ v: cfg.value.value }) }))
|
||||
})))
|
||||
return result
|
||||
}, [])
|
||||
}
|
||||
})
|
||||
this.$store.commit('showNotification', {
|
||||
message: 'Rendering configuration saved successfully.',
|
||||
style: 'success',
|
||||
icon: 'check'
|
||||
})
|
||||
this.$store.commit(`loadingStop`, 'admin-rendering-saverenderers')
|
||||
}
|
||||
},
|
||||
apollo: {
|
||||
renderers: {
|
||||
query: renderersQuery,
|
||||
fetchPolicy: 'network-only',
|
||||
update: (data) => {
|
||||
let renderers = _.cloneDeep(data.rendering.renderers).map(str => ({
|
||||
...str,
|
||||
config: _.sortBy(str.config.map(cfg => ({
|
||||
...cfg,
|
||||
value: JSON.parse(cfg.value)
|
||||
})), [t => t.value.order])
|
||||
}))
|
||||
// Build tree
|
||||
const graph = new DepGraph({ circular: true })
|
||||
const rawCores = _.filter(renderers, ['dependsOn', null]).map(core => {
|
||||
core.children = _.concat([_.cloneDeep(core)], _.filter(renderers, ['dependsOn', core.key]))
|
||||
return core
|
||||
})
|
||||
// Build dependency graph
|
||||
rawCores.map(core => { graph.addNode(core.key) })
|
||||
rawCores.map(core => {
|
||||
rawCores.map(coreTarget => {
|
||||
if (core.key !== coreTarget.key) {
|
||||
if (core.output === coreTarget.input) {
|
||||
graph.addDependency(core.key, coreTarget.key)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
// Reorder cores in reverse dependency order
|
||||
let orderedCores = []
|
||||
_.reverse(graph.overallOrder()).map(coreKey => {
|
||||
orderedCores.push(_.find(rawCores, ['key', coreKey]))
|
||||
})
|
||||
return orderedCores
|
||||
},
|
||||
watchLoading (isLoading) {
|
||||
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-rendering-refresh')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang='scss'>
|
||||
.adm-rendering-pipeline {
|
||||
.v-expansion-panel--active .v-expansion-panel-header {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.v-expansion-panel-header {
|
||||
padding: 0;
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
.v-expansion-panel-content__wrap {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,217 +0,0 @@
|
||||
<template lang='pug'>
|
||||
v-container(fluid, grid-list-lg)
|
||||
v-layout(row, wrap)
|
||||
v-flex(xs12)
|
||||
.admin-header
|
||||
img.animated.fadeInUp(src='/_assets/svg/icon-search.svg', alt='Search Engine', style='width: 80px;')
|
||||
.admin-header-title
|
||||
.headline.primary--text.animated.fadeInLeft {{$t('admin:search.title')}}
|
||||
.subtitle-1.grey--text.animated.fadeInLeft.wait-p2s {{$t('admin:search.subtitle')}}
|
||||
v-spacer
|
||||
v-btn.mr-3.animated.fadeInDown.wait-p3s(icon, outlined, color='grey', href='https://docs.requarks.io/search', target='_blank')
|
||||
v-icon mdi-help-circle
|
||||
v-btn.animated.fadeInDown.wait-p2s(icon, outlined, color='grey', @click='refresh')
|
||||
v-icon mdi-refresh
|
||||
v-btn.mx-3.animated.fadeInDown.wait-p1s(color='black', dark, depressed, @click='rebuild')
|
||||
v-icon(left) mdi-cached
|
||||
span {{$t('admin:search.rebuildIndex')}}
|
||||
v-btn.animated.fadeInDown(color='success', @click='save', depressed, large)
|
||||
v-icon(left) mdi-check
|
||||
span {{$t('common:actions.apply')}}
|
||||
|
||||
v-flex(lg3, xs12)
|
||||
v-card.animated.fadeInUp
|
||||
v-toolbar(flat, color='primary', dark, dense)
|
||||
.subtitle-1 {{$t('admin:search.searchEngine')}}
|
||||
v-list.py-0(two-line, dense)
|
||||
template(v-for='(eng, idx) in engines')
|
||||
v-list-item(:key='eng.key', @click='selectedEngine = eng.key', :disabled='!eng.isAvailable')
|
||||
v-list-item-avatar(size='24')
|
||||
v-icon(color='grey', v-if='!eng.isAvailable') mdi-minus-box-outline
|
||||
v-icon(color='primary', v-else-if='eng.key === selectedEngine') mdi-checkbox-marked-circle-outline
|
||||
v-icon(color='grey', v-else) mdi-checkbox-blank-circle-outline
|
||||
v-list-item-content
|
||||
v-list-item-title.body-2(:class='!eng.isAvailable ? `grey--text` : (selectedEngine === eng.key ? `primary--text` : ``)') {{ eng.title }}
|
||||
v-list-item-subtitle: .caption(:class='!eng.isAvailable ? `grey--text text--lighten-1` : (selectedEngine === eng.key ? `blue--text ` : ``)') {{ eng.description }}
|
||||
v-list-item-avatar(v-if='selectedEngine === eng.key', size='24')
|
||||
v-icon.animated.fadeInLeft(color='primary', large) mdi-chevron-right
|
||||
v-divider(v-if='idx < engines.length - 1')
|
||||
|
||||
v-flex(lg9, xs12)
|
||||
v-card.animated.fadeInUp.wait-p2s
|
||||
v-toolbar(color='primary', dense, flat, dark)
|
||||
.subtitle-1 {{engine.title}}
|
||||
v-card-info(color='blue')
|
||||
div
|
||||
div {{engine.description}}
|
||||
span.caption: a(:href='engine.website') {{engine.website}}
|
||||
v-spacer
|
||||
.admin-providerlogo
|
||||
img(:src='engine.logo', :alt='engine.title')
|
||||
v-card-text
|
||||
.overline.mb-5 {{$t('admin:search.engineConfig')}}
|
||||
.body-2.ml-3(v-if='!engine.config || engine.config.length < 1'): em {{$t('admin:search.engineNoConfig')}}
|
||||
template(v-else, v-for='cfg in engine.config')
|
||||
v-select(
|
||||
v-if='cfg.value.type === "string" && cfg.value.enum'
|
||||
outlined
|
||||
:items='cfg.value.enum'
|
||||
:key='cfg.key'
|
||||
:label='cfg.value.title'
|
||||
v-model='cfg.value.value'
|
||||
prepend-icon='mdi-cog-box'
|
||||
:hint='cfg.value.hint ? cfg.value.hint : ""'
|
||||
persistent-hint
|
||||
:class='cfg.value.hint ? "mb-2" : ""'
|
||||
)
|
||||
v-switch.mb-3(
|
||||
v-else-if='cfg.value.type === "boolean"'
|
||||
:key='cfg.key'
|
||||
:label='cfg.value.title'
|
||||
v-model='cfg.value.value'
|
||||
color='primary'
|
||||
prepend-icon='mdi-cog-box'
|
||||
:hint='cfg.value.hint ? cfg.value.hint : ""'
|
||||
persistent-hint
|
||||
inset
|
||||
)
|
||||
v-textarea(
|
||||
v-else-if='cfg.value.type === "string" && cfg.value.multiline'
|
||||
outlined
|
||||
:key='cfg.key'
|
||||
:label='cfg.value.title'
|
||||
v-model='cfg.value.value'
|
||||
prepend-icon='mdi-cog-box'
|
||||
:hint='cfg.value.hint ? cfg.value.hint : ""'
|
||||
persistent-hint
|
||||
:class='cfg.value.hint ? "mb-2" : ""'
|
||||
)
|
||||
v-text-field(
|
||||
v-else
|
||||
outlined
|
||||
:key='cfg.key'
|
||||
:label='cfg.value.title'
|
||||
v-model='cfg.value.value'
|
||||
prepend-icon='mdi-cog-box'
|
||||
:hint='cfg.value.hint ? cfg.value.hint : ""'
|
||||
persistent-hint
|
||||
:class='cfg.value.hint ? "mb-2" : ""'
|
||||
)
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
|
||||
import enginesQuery from 'gql/admin/search/search-query-engines.gql'
|
||||
import enginesSaveMutation from 'gql/admin/search/search-mutation-save-engines.gql'
|
||||
import enginesRebuildMutation from 'gql/admin/search/search-mutation-rebuild-index.gql'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
engines: [],
|
||||
selectedEngine: '',
|
||||
engine: {}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
selectedEngine(newValue, oldValue) {
|
||||
this.engine = _.find(this.engines, ['key', newValue]) || {}
|
||||
},
|
||||
engines(newValue, oldValue) {
|
||||
this.selectedEngine = _.get(_.find(this.engines, 'isEnabled'), 'key', 'db')
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async refresh() {
|
||||
await this.$apollo.queries.engines.refetch()
|
||||
this.$store.commit('showNotification', {
|
||||
message: this.$t('admin:search.listRefreshSuccess'),
|
||||
style: 'success',
|
||||
icon: 'cached'
|
||||
})
|
||||
},
|
||||
async save() {
|
||||
this.$store.commit(`loadingStart`, 'admin-search-saveengines')
|
||||
try {
|
||||
const resp = await this.$apollo.mutate({
|
||||
mutation: enginesSaveMutation,
|
||||
variables: {
|
||||
engines: this.engines.map(tgt => ({
|
||||
isEnabled: tgt.key === this.selectedEngine,
|
||||
key: tgt.key,
|
||||
config: tgt.config.map(cfg => ({...cfg, value: JSON.stringify({ v: cfg.value.value })}))
|
||||
}))
|
||||
}
|
||||
})
|
||||
if (_.get(resp, 'data.search.updateSearchEngines.responseResult.succeeded', false)) {
|
||||
this.$store.commit('showNotification', {
|
||||
message: this.$t('admin:search.configSaveSuccess'),
|
||||
style: 'success',
|
||||
icon: 'check'
|
||||
})
|
||||
} else {
|
||||
throw new Error(_.get(resp, 'data.search.updateSearchEngines.responseResult.message', this.$t('common:error.unexpected')))
|
||||
}
|
||||
} catch (err) {
|
||||
this.$store.commit('pushGraphError', err)
|
||||
}
|
||||
this.$store.commit(`loadingStop`, 'admin-search-saveengines')
|
||||
},
|
||||
async rebuild () {
|
||||
this.$store.commit(`loadingStart`, 'admin-search-rebuildindex')
|
||||
try {
|
||||
const resp = await this.$apollo.mutate({
|
||||
mutation: enginesRebuildMutation
|
||||
})
|
||||
if (_.get(resp, 'data.search.rebuildIndex.responseResult.succeeded', false)) {
|
||||
this.$store.commit('showNotification', {
|
||||
message: this.$t('admin:search.indexRebuildSuccess'),
|
||||
style: 'success',
|
||||
icon: 'check'
|
||||
})
|
||||
} else {
|
||||
throw new Error(_.get(resp, 'data.search.rebuildIndex.responseResult.message', this.$t('common:error.unexpected')))
|
||||
}
|
||||
} catch (err) {
|
||||
this.$store.commit('pushGraphError', err)
|
||||
}
|
||||
this.$store.commit(`loadingStop`, 'admin-search-rebuildindex')
|
||||
}
|
||||
},
|
||||
apollo: {
|
||||
engines: {
|
||||
query: enginesQuery,
|
||||
fetchPolicy: 'network-only',
|
||||
update: (data) => _.cloneDeep(data.search.searchEngines).map(str => ({
|
||||
...str,
|
||||
config: _.sortBy(str.config.map(cfg => ({
|
||||
...cfg,
|
||||
value: JSON.parse(cfg.value)
|
||||
})), [t => t.value.order])
|
||||
})),
|
||||
watchLoading (isLoading) {
|
||||
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-search-refresh')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang='scss' scoped>
|
||||
|
||||
.enginelogo {
|
||||
width: 250px;
|
||||
height: 85px;
|
||||
float:right;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
max-height: 50px;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
@ -1,444 +0,0 @@
|
||||
<template lang='pug'>
|
||||
v-container(fluid, grid-list-lg)
|
||||
v-layout(row wrap)
|
||||
v-flex(xs12)
|
||||
.admin-header
|
||||
img.animated.fadeInUp(src='/_assets/svg/icon-private.svg', alt='Security', style='width: 80px;')
|
||||
.admin-header-title
|
||||
.headline.primary--text.animated.fadeInLeft {{ $t('admin:security.title') }}
|
||||
.subtitle-1.grey--text.animated.fadeInLeft {{ $t('admin:security.subtitle') }}
|
||||
v-spacer
|
||||
v-btn.animated.fadeInDown(color='success', depressed, @click='save', large)
|
||||
v-icon(left) mdi-check
|
||||
span {{$t('common:actions.apply')}}
|
||||
v-form.pt-3
|
||||
v-layout(row wrap)
|
||||
v-flex(lg6 xs12)
|
||||
v-card.animated.fadeInUp
|
||||
v-toolbar(color='red darken-2', dark, dense, flat)
|
||||
v-toolbar-title.subtitle-1 Security
|
||||
v-card-info(color='red')
|
||||
span Make sure to understand the implications before turning on / off a security feature.
|
||||
v-card-text
|
||||
v-switch(
|
||||
inset
|
||||
label='Block Open Redirect'
|
||||
color='red darken-2'
|
||||
v-model='config.securityOpenRedirect'
|
||||
persistent-hint
|
||||
hint='Prevents user controlled URLs from directing to websites outside of your wiki. This provides Open Redirect protection.'
|
||||
)
|
||||
|
||||
v-divider.mt-3
|
||||
v-switch.mt-3(
|
||||
inset
|
||||
label='Block IFrame Embedding'
|
||||
color='red darken-2'
|
||||
v-model='config.securityIframe'
|
||||
persistent-hint
|
||||
hint='Prevents other websites from embedding your wiki in an iframe. This provides clickjacking protection.'
|
||||
)
|
||||
|
||||
v-divider.mt-3
|
||||
v-switch(
|
||||
inset
|
||||
label='Same Origin Referrer Policy'
|
||||
color='red darken-2'
|
||||
v-model='config.securityReferrerPolicy'
|
||||
persistent-hint
|
||||
hint='Limits the referrer header to same origin.'
|
||||
)
|
||||
|
||||
v-divider.mt-3
|
||||
v-switch(
|
||||
inset
|
||||
label='Trust X-Forwarded-* Proxy Headers'
|
||||
color='red darken-2'
|
||||
v-model='config.securityTrustProxy'
|
||||
persistent-hint
|
||||
hint='Should be enabled when using a reverse-proxy like nginx, apache, CloudFlare, etc in front of Wiki.js. Turn off otherwise.'
|
||||
)
|
||||
|
||||
//- v-divider.mt-3
|
||||
//- v-switch(
|
||||
//- inset
|
||||
//- label='Subresource Integrity (SRI)'
|
||||
//- color='red darken-2'
|
||||
//- v-model='config.securitySRI'
|
||||
//- persistent-hint
|
||||
//- hint='This ensure that resources such as CSS and JS files are not altered during delivery.'
|
||||
//- disabled
|
||||
//- )
|
||||
|
||||
v-divider.mt-3
|
||||
v-switch(
|
||||
inset
|
||||
label='Enforce HSTS'
|
||||
color='red darken-2'
|
||||
v-model='config.securityHSTS'
|
||||
persistent-hint
|
||||
hint='This ensures the connection cannot be established through an insecure HTTP connection.'
|
||||
)
|
||||
v-select.mt-5(
|
||||
outlined
|
||||
label='HSTS Max Age'
|
||||
:items='hstsDurations'
|
||||
v-model='config.securityHSTSDuration'
|
||||
prepend-icon='mdi-subdirectory-arrow-right'
|
||||
:disabled='!config.securityHSTS'
|
||||
hide-details
|
||||
style='max-width: 450px;'
|
||||
)
|
||||
.pl-11.mt-3
|
||||
.caption Defines the duration for which the server should only deliver content through HTTPS.
|
||||
.caption It's a good idea to start with small values and make sure that nothing breaks on your wiki before moving to longer values.
|
||||
|
||||
//- v-divider.mt-3
|
||||
//- v-switch(
|
||||
//- inset
|
||||
//- label='Enforce CSP'
|
||||
//- color='red darken-2'
|
||||
//- v-model='config.securityCSP'
|
||||
//- persistent-hint
|
||||
//- hint='Restricts scripts to pre-approved content sources.'
|
||||
//- disabled
|
||||
//- )
|
||||
//- v-textarea.mt-5(
|
||||
//- label='CSP Directives'
|
||||
//- outlined
|
||||
//- v-model='config.securityCSPDirectives'
|
||||
//- prepend-icon='mdi-subdirectory-arrow-right'
|
||||
//- persistent-hint
|
||||
//- hint='One directive per line.'
|
||||
//- disabled
|
||||
//- )
|
||||
|
||||
v-flex(lg6 xs12)
|
||||
v-card.animated.fadeInUp.wait-p2s
|
||||
v-toolbar(color='primary', dark, dense, flat)
|
||||
v-toolbar-title.subtitle-1 {{ $t('admin:security.uploads') }}
|
||||
v-card-info(color='blue')
|
||||
span {{$t('admin:security.uploadsInfo')}}
|
||||
v-card-text
|
||||
v-text-field.mt-3(
|
||||
outlined
|
||||
:label='$t(`admin:security.maxUploadSize`)'
|
||||
required
|
||||
v-model='config.uploadMaxFileSize'
|
||||
prepend-icon='mdi-progress-upload'
|
||||
:hint='$t(`admin:security.maxUploadSizeHint`)'
|
||||
persistent-hint
|
||||
:suffix='$t(`admin:security.maxUploadSizeSuffix`)'
|
||||
style='max-width: 450px;'
|
||||
)
|
||||
v-text-field.mt-3(
|
||||
outlined
|
||||
:label='$t(`admin:security.maxUploadBatch`)'
|
||||
required
|
||||
v-model='config.uploadMaxFiles'
|
||||
prepend-icon='mdi-upload-lock'
|
||||
:hint='$t(`admin:security.maxUploadBatchHint`)'
|
||||
persistent-hint
|
||||
:suffix='$t(`admin:security.maxUploadBatchSuffix`)'
|
||||
style='max-width: 450px;'
|
||||
)
|
||||
v-divider.mt-3
|
||||
v-switch(
|
||||
inset
|
||||
label='Scan and Sanitize SVG Uploads'
|
||||
color='primary'
|
||||
v-model='config.uploadScanSVG'
|
||||
persistent-hint
|
||||
hint='Should SVG uploads be scanned for vulnerabilities and stripped of any potentially unsafe content.'
|
||||
)
|
||||
v-divider.mt-3
|
||||
v-switch(
|
||||
inset
|
||||
label='Force Download of Unsafe Extensions'
|
||||
color='primary'
|
||||
v-model='config.uploadForceDownload'
|
||||
persistent-hint
|
||||
hint='Should non-image files be forced as downloads when accessed directly. This prevents potential XSS attacks via unsafe file extensions uploads.'
|
||||
)
|
||||
|
||||
v-card.mt-3.animated.fadeInUp.wait-p2s
|
||||
v-toolbar(flat, color='primary', dark, dense)
|
||||
.subtitle-1 {{$t('admin:security.login')}}
|
||||
//- v-card-info(color='blue')
|
||||
//- span {{$t('admin:security.loginInfo')}}
|
||||
.overline.grey--text.pa-4 {{$t('admin:security.loginScreen')}}
|
||||
.px-4.pb-3
|
||||
v-text-field(
|
||||
outlined
|
||||
:label='$t(`admin:security.loginBgUrl`)'
|
||||
v-model='config.authLoginBgUrl'
|
||||
:hint='$t(`admin:security.loginBgUrlHint`)'
|
||||
persistent-hint
|
||||
prepend-icon='mdi-image-area'
|
||||
append-icon='mdi-folder-image'
|
||||
@click:append='browseLoginBg'
|
||||
)
|
||||
v-switch(
|
||||
inset
|
||||
:label='$t(`admin:security.bypassLogin`)'
|
||||
color='primary'
|
||||
v-model='config.authAutoLogin'
|
||||
prepend-icon='mdi-fast-forward'
|
||||
persistent-hint
|
||||
:hint='$t(`admin:security.bypassLoginHint`)'
|
||||
)
|
||||
v-switch(
|
||||
inset
|
||||
:label='$t(`admin:security.hideLocalLogin`)'
|
||||
color='primary'
|
||||
v-model='config.authHideLocal'
|
||||
prepend-icon='mdi-eye-off-outline'
|
||||
persistent-hint
|
||||
:hint='$t(`admin:security.hideLocalLoginHint`)'
|
||||
)
|
||||
v-divider.mt-3
|
||||
.overline.grey--text.pa-4 {{$t('admin:security.loginSecurity')}}
|
||||
.px-4.pb-3
|
||||
v-switch.mt-0(
|
||||
inset
|
||||
:label='$t(`admin:security.enforce2fa`)'
|
||||
color='primary'
|
||||
v-model='config.authEnforce2FA'
|
||||
prepend-icon='mdi-two-factor-authentication'
|
||||
:hint='$t(`admin:security.enforce2faHint`)'
|
||||
persistent-hint
|
||||
)
|
||||
v-divider.mt-3
|
||||
.overline.grey--text.pa-4 {{$t('admin:security.jwt')}}
|
||||
.px-4.pb-3
|
||||
v-text-field(
|
||||
v-model='config.authJwtAudience'
|
||||
outlined
|
||||
prepend-icon='mdi-account-group-outline'
|
||||
:label='$t(`admin:auth.jwtAudience`)'
|
||||
:hint='$t(`admin:auth.jwtAudienceHint`)'
|
||||
persistent-hint
|
||||
)
|
||||
v-text-field.mt-3(
|
||||
v-model='config.authJwtExpiration'
|
||||
outlined
|
||||
prepend-icon='mdi-clock-outline'
|
||||
:label='$t(`admin:auth.tokenExpiration`)'
|
||||
:hint='$t(`admin:auth.tokenExpirationHint`)'
|
||||
persistent-hint
|
||||
)
|
||||
v-text-field.mt-3(
|
||||
v-model='config.authJwtRenewablePeriod'
|
||||
outlined
|
||||
prepend-icon='mdi-update'
|
||||
:label='$t(`admin:auth.tokenRenewalPeriod`)'
|
||||
:hint='$t(`admin:auth.tokenRenewalPeriodHint`)'
|
||||
persistent-hint
|
||||
)
|
||||
|
||||
component(:is='activeModal')
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import { sync } from 'vuex-pathify'
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
import editorStore from '../../store/editor'
|
||||
|
||||
/* global WIKI */
|
||||
|
||||
WIKI.$store.registerModule('editor', editorStore)
|
||||
|
||||
export default {
|
||||
i18nOptions: { namespaces: 'editor' },
|
||||
components: {
|
||||
editorModalMedia: () => import(/* webpackChunkName: "editor", webpackMode: "lazy" */ '../editor/editor-modal-media.vue')
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
config: {
|
||||
uploadMaxFileSize: 0,
|
||||
uploadMaxFiles: 0,
|
||||
uploadScanSVG: true,
|
||||
uploadForceDownload: true,
|
||||
securityOpenRedirect: true,
|
||||
securityIframe: true,
|
||||
securityReferrerPolicy: true,
|
||||
securityTrustProxy: true,
|
||||
securitySRI: true,
|
||||
securityHSTS: false,
|
||||
securityHSTSDuration: 0,
|
||||
securityCSP: false,
|
||||
securityCSPDirectives: '',
|
||||
authAutoLogin: false,
|
||||
authHideLocal: false,
|
||||
authLoginBgUrl: '',
|
||||
authJwtAudience: 'urn:wiki.js',
|
||||
authJwtExpiration: '30m',
|
||||
authJwtRenewablePeriod: '14d'
|
||||
},
|
||||
hstsDurations: [
|
||||
{ value: 300, text: '5 minutes' },
|
||||
{ value: 86400, text: '1 day' },
|
||||
{ value: 604800, text: '1 week' },
|
||||
{ value: 2592000, text: '1 month' },
|
||||
{ value: 31536000, text: '1 year' },
|
||||
{ value: 63072000, text: '2 years' }
|
||||
]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
activeModal: sync('editor/activeModal')
|
||||
},
|
||||
methods: {
|
||||
async save () {
|
||||
try {
|
||||
await this.$apollo.mutate({
|
||||
mutation: gql`
|
||||
mutation (
|
||||
$authAutoLogin: Boolean
|
||||
$authEnforce2FA: Boolean
|
||||
$authHideLocal: Boolean
|
||||
$authLoginBgUrl: String
|
||||
$authJwtAudience: String
|
||||
$authJwtExpiration: String
|
||||
$authJwtRenewablePeriod: String
|
||||
$uploadMaxFileSize: Int
|
||||
$uploadMaxFiles: Int
|
||||
$uploadScanSVG: Boolean
|
||||
$uploadForceDownload: Boolean
|
||||
$securityOpenRedirect: Boolean
|
||||
$securityIframe: Boolean
|
||||
$securityReferrerPolicy: Boolean
|
||||
$securityTrustProxy: Boolean
|
||||
$securitySRI: Boolean
|
||||
$securityHSTS: Boolean
|
||||
$securityHSTSDuration: Int
|
||||
$securityCSP: Boolean
|
||||
$securityCSPDirectives: String
|
||||
) {
|
||||
site {
|
||||
updateConfig(
|
||||
authAutoLogin: $authAutoLogin,
|
||||
authEnforce2FA: $authEnforce2FA,
|
||||
authHideLocal: $authHideLocal,
|
||||
authLoginBgUrl: $authLoginBgUrl,
|
||||
authJwtAudience: $authJwtAudience,
|
||||
authJwtExpiration: $authJwtExpiration,
|
||||
authJwtRenewablePeriod: $authJwtRenewablePeriod,
|
||||
uploadMaxFileSize: $uploadMaxFileSize,
|
||||
uploadMaxFiles: $uploadMaxFiles,
|
||||
uploadScanSVG: $uploadScanSVG
|
||||
uploadForceDownload: $uploadForceDownload,
|
||||
securityOpenRedirect: $securityOpenRedirect,
|
||||
securityIframe: $securityIframe,
|
||||
securityReferrerPolicy: $securityReferrerPolicy,
|
||||
securityTrustProxy: $securityTrustProxy,
|
||||
securitySRI: $securitySRI,
|
||||
securityHSTS: $securityHSTS,
|
||||
securityHSTSDuration: $securityHSTSDuration,
|
||||
securityCSP: $securityCSP,
|
||||
securityCSPDirectives: $securityCSPDirectives
|
||||
) {
|
||||
responseResult {
|
||||
succeeded
|
||||
errorCode
|
||||
slug
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
authAutoLogin: _.get(this.config, 'authAutoLogin', false),
|
||||
authEnforce2FA: _.get(this.config, 'authEnforce2FA', false),
|
||||
authHideLocal: _.get(this.config, 'authHideLocal', false),
|
||||
authLoginBgUrl: _.get(this.config, 'authLoginBgUrl', ''),
|
||||
authJwtAudience: _.get(this.config, 'authJwtAudience', ''),
|
||||
authJwtExpiration: _.get(this.config, 'authJwtExpiration', ''),
|
||||
authJwtRenewablePeriod: _.get(this.config, 'authJwtRenewablePeriod', ''),
|
||||
uploadMaxFileSize: _.toSafeInteger(_.get(this.config, 'uploadMaxFileSize', 0)),
|
||||
uploadMaxFiles: _.toSafeInteger(_.get(this.config, 'uploadMaxFiles', 0)),
|
||||
uploadScanSVG: _.get(this.config, 'uploadScanSVG', false),
|
||||
uploadForceDownload: _.get(this.config, 'uploadForceDownload', false),
|
||||
securityOpenRedirect: _.get(this.config, 'securityOpenRedirect', false),
|
||||
securityIframe: _.get(this.config, 'securityIframe', false),
|
||||
securityReferrerPolicy: _.get(this.config, 'securityReferrerPolicy', false),
|
||||
securityTrustProxy: _.get(this.config, 'securityTrustProxy', false),
|
||||
securitySRI: _.get(this.config, 'securitySRI', false),
|
||||
securityHSTS: _.get(this.config, 'securityHSTS', false),
|
||||
securityHSTSDuration: _.get(this.config, 'securityHSTSDuration', 0),
|
||||
securityCSP: _.get(this.config, 'securityCSP', false),
|
||||
securityCSPDirectives: _.get(this.config, 'securityCSPDirectives', '')
|
||||
},
|
||||
watchLoading (isLoading) {
|
||||
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-site-update')
|
||||
}
|
||||
})
|
||||
this.$store.commit('showNotification', {
|
||||
style: 'success',
|
||||
message: 'Configuration saved successfully.',
|
||||
icon: 'check'
|
||||
})
|
||||
} catch (err) {
|
||||
this.$store.commit('pushGraphError', err)
|
||||
}
|
||||
},
|
||||
browseLoginBg () {
|
||||
this.$store.set('editor/editorKey', 'common')
|
||||
this.activeModal = 'editorModalMedia'
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.$root.$on('editorInsert', opts => {
|
||||
this.config.authLoginBgUrl = opts.path
|
||||
})
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.$root.$off('editorInsert')
|
||||
},
|
||||
apollo: {
|
||||
config: {
|
||||
query: gql`
|
||||
{
|
||||
site {
|
||||
config {
|
||||
authAutoLogin
|
||||
authEnforce2FA
|
||||
authHideLocal
|
||||
authLoginBgUrl
|
||||
authJwtAudience
|
||||
authJwtExpiration
|
||||
authJwtRenewablePeriod
|
||||
uploadMaxFileSize
|
||||
uploadMaxFiles
|
||||
uploadScanSVG
|
||||
uploadForceDownload
|
||||
securityOpenRedirect
|
||||
securityIframe
|
||||
securityReferrerPolicy
|
||||
securityTrustProxy
|
||||
securitySRI
|
||||
securityHSTS
|
||||
securityHSTSDuration
|
||||
securityCSP
|
||||
securityCSPDirectives
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
fetchPolicy: 'network-only',
|
||||
update: (data) => _.cloneDeep(data.site.config),
|
||||
watchLoading (isLoading) {
|
||||
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-security-refresh')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang='scss'>
|
||||
|
||||
</style>
|
@ -1,269 +0,0 @@
|
||||
<template lang='pug'>
|
||||
v-container(fluid, grid-list-lg)
|
||||
v-layout(row wrap)
|
||||
v-flex(xs12)
|
||||
.admin-header
|
||||
img.animated.fadeInUp(src='/_assets/svg/icon-validation.svg', alt='SSL', style='width: 80px;')
|
||||
.admin-header-title
|
||||
.headline.primary--text.animated.fadeInLeft {{ $t('admin:ssl.title') }}
|
||||
.subtitle-1.grey--text.animated.fadeInLeft {{ $t('admin:ssl.subtitle') }}
|
||||
v-spacer
|
||||
v-btn.animated.fadeInDown(
|
||||
v-if='info.sslProvider === `letsencrypt` && info.httpsPort > 0'
|
||||
color='black'
|
||||
dark
|
||||
depressed
|
||||
@click='renewCertificate'
|
||||
large
|
||||
:loading='loadingRenew'
|
||||
)
|
||||
v-icon(left) mdi-cached
|
||||
span {{$t('admin:ssl.renewCertificate')}}
|
||||
v-form.pt-3
|
||||
v-layout(row wrap)
|
||||
v-flex(lg6 xs12)
|
||||
v-card.animated.fadeInUp
|
||||
v-subheader {{ $t('admin:ssl.currentState') }}
|
||||
v-list(two-line, dense)
|
||||
v-list-item
|
||||
v-list-item-avatar
|
||||
v-icon.indigo.white--text mdi-handshake
|
||||
v-list-item-content
|
||||
v-list-item-title {{ $t(`admin:ssl.provider`) }}
|
||||
v-list-item-subtitle {{ providerTitle }}
|
||||
template(v-if='info.sslProvider === `letsencrypt` && info.httpsPort > 0')
|
||||
v-list-item
|
||||
v-list-item-avatar
|
||||
v-icon.indigo.white--text mdi-application
|
||||
v-list-item-content
|
||||
v-list-item-title {{ $t(`admin:ssl.domain`) }}
|
||||
v-list-item-subtitle {{ info.sslDomain }}
|
||||
v-list-item
|
||||
v-list-item-avatar
|
||||
v-icon.indigo.white--text mdi-at
|
||||
v-list-item-content
|
||||
v-list-item-title {{ $t('admin:ssl.subscriberEmail') }}
|
||||
v-list-item-subtitle {{ info.sslSubscriberEmail }}
|
||||
v-list-item
|
||||
v-list-item-avatar
|
||||
v-icon.indigo.white--text mdi-calendar-remove-outline
|
||||
v-list-item-content
|
||||
v-list-item-title {{ $t('admin:ssl.expiration') }}
|
||||
v-list-item-subtitle {{ info.sslExpirationDate | moment('calendar') }}
|
||||
v-list-item
|
||||
v-list-item-avatar
|
||||
v-icon.indigo.white--text mdi-traffic-light
|
||||
v-list-item-content
|
||||
v-list-item-title {{ $t(`admin:ssl.status`) }}
|
||||
v-list-item-subtitle {{ info.sslStatus }}
|
||||
|
||||
v-flex(lg6 xs12)
|
||||
v-card.animated.fadeInUp.wait-p2s
|
||||
v-subheader {{ $t('admin:ssl.ports') }}
|
||||
v-list(two-line, dense)
|
||||
v-list-item
|
||||
v-list-item-avatar
|
||||
v-icon.blue.white--text mdi-lock-open-variant
|
||||
v-list-item-content
|
||||
v-list-item-title {{ $t(`admin:ssl.httpPort`) }}
|
||||
v-list-item-subtitle {{ info.httpPort }}
|
||||
template(v-if='info.httpsPort > 0')
|
||||
v-divider
|
||||
v-list-item
|
||||
v-list-item-avatar
|
||||
v-icon.green.white--text mdi-lock
|
||||
v-list-item-content
|
||||
v-list-item-title {{ $t(`admin:ssl.httpsPort`) }}
|
||||
v-list-item-subtitle {{ info.httpsPort }}
|
||||
v-divider
|
||||
v-list-item
|
||||
v-list-item-avatar
|
||||
v-icon.indigo.white--text mdi-sign-direction
|
||||
v-list-item-content
|
||||
v-list-item-title {{ $t(`admin:ssl.httpPortRedirect`) }}
|
||||
v-list-item-subtitle {{ info.httpRedirection }}
|
||||
v-list-item-action
|
||||
v-btn.red--text(
|
||||
v-if='info.httpRedirection'
|
||||
depressed
|
||||
:color='$vuetify.theme.dark ? `red darken-4` : `red lighten-5`'
|
||||
:class='$vuetify.theme.dark ? `text--lighten-5` : `text--darken-2`'
|
||||
@click='toggleRedir'
|
||||
:loading='loadingRedir'
|
||||
)
|
||||
v-icon(left) mdi-power
|
||||
span {{$t('admin:ssl.httpPortRedirectTurnOff')}}
|
||||
v-btn.green--text(
|
||||
v-else
|
||||
depressed
|
||||
:color='$vuetify.theme.dark ? `green darken-4` : `green lighten-5`'
|
||||
:class='$vuetify.theme.dark ? `text--lighten-5` : `text--darken-2`'
|
||||
@click='toggleRedir'
|
||||
:loading='loadingRedir'
|
||||
)
|
||||
v-icon(left) mdi-power
|
||||
span {{$t('admin:ssl.httpPortRedirectTurnOn')}}
|
||||
|
||||
v-dialog(
|
||||
v-model='loadingRenew'
|
||||
persistent
|
||||
max-width='450'
|
||||
)
|
||||
v-card(color='black', dark)
|
||||
v-card-text.pa-10.text-center
|
||||
semipolar-spinner.animated.fadeIn(
|
||||
:animation-duration='1500'
|
||||
:size='65'
|
||||
color='#FFF'
|
||||
style='margin: 0 auto;'
|
||||
)
|
||||
.mt-5.body-1.white--text {{$t('admin:ssl.renewCertificateLoadingTitle')}}
|
||||
.caption.mt-4 {{$t('admin:ssl.renewCertificateLoadingSubtitle')}}
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
import { SemipolarSpinner } from 'epic-spinners'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
SemipolarSpinner
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loadingRenew: false,
|
||||
loadingRedir: false,
|
||||
info: {
|
||||
sslDomain: '',
|
||||
sslProvider: '',
|
||||
sslSubscriberEmail: '',
|
||||
sslExpirationDate: false,
|
||||
sslStatus: '',
|
||||
httpPort: 0,
|
||||
httpRedirection: false,
|
||||
httpsPort: 0
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
providerTitle () {
|
||||
switch (this.info.sslProvider) {
|
||||
case 'custom':
|
||||
return this.$t('admin:ssl.providerCustomCertificate')
|
||||
case 'letsencrypt':
|
||||
return this.$t('admin:ssl.providerLetsEncrypt')
|
||||
default:
|
||||
return this.$t('admin:ssl.providerDisabled')
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async toggleRedir () {
|
||||
this.loadingRedir = true
|
||||
try {
|
||||
this.info.httpRedirection = !this.info.httpRedirection
|
||||
await this.$apollo.mutate({
|
||||
mutation: gql`
|
||||
mutation ($enabled: Boolean!) {
|
||||
system {
|
||||
setHTTPSRedirection(enabled: $enabled) {
|
||||
responseResult {
|
||||
succeeded
|
||||
errorCode
|
||||
slug
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
enabled: _.get(this.info, 'httpRedirection', false)
|
||||
},
|
||||
watchLoading (isLoading) {
|
||||
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-ssl-toggleRedirection')
|
||||
}
|
||||
})
|
||||
this.$store.commit('showNotification', {
|
||||
style: 'success',
|
||||
message: this.$t('admin:ssl.httpPortRedirectSaveSuccess'),
|
||||
icon: 'check'
|
||||
})
|
||||
} catch (err) {
|
||||
this.info.httpRedirection = !this.info.httpRedirection
|
||||
this.$store.commit('pushGraphError', err)
|
||||
}
|
||||
this.loadingRedir = false
|
||||
},
|
||||
async renewCertificate () {
|
||||
this.loadingRenew = true
|
||||
try {
|
||||
const respRaw = await this.$apollo.mutate({
|
||||
mutation: gql`
|
||||
mutation {
|
||||
system {
|
||||
renewHTTPSCertificate {
|
||||
responseResult {
|
||||
succeeded
|
||||
errorCode
|
||||
slug
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
watchLoading (isLoading) {
|
||||
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-ssl-renew')
|
||||
}
|
||||
})
|
||||
const resp = _.get(respRaw, 'data.system.renewHTTPSCertificate.responseResult', {})
|
||||
if (resp.succeeded) {
|
||||
this.$store.commit('showNotification', {
|
||||
style: 'success',
|
||||
message: this.$t('admin:ssl.renewCertificateSuccess'),
|
||||
icon: 'check'
|
||||
})
|
||||
} else {
|
||||
throw new Error(resp.message)
|
||||
}
|
||||
} catch (err) {
|
||||
this.$store.commit('pushGraphError', err)
|
||||
}
|
||||
this.loadingRenew = false
|
||||
}
|
||||
},
|
||||
apollo: {
|
||||
info: {
|
||||
query: gql`
|
||||
{
|
||||
system {
|
||||
info {
|
||||
httpPort
|
||||
httpRedirection
|
||||
httpsPort
|
||||
sslDomain
|
||||
sslExpirationDate
|
||||
sslProvider
|
||||
sslStatus
|
||||
sslSubscriberEmail
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
fetchPolicy: 'network-only',
|
||||
update: (data) => _.cloneDeep(data.system.info),
|
||||
watchLoading (isLoading) {
|
||||
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-ssl-refresh')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang='scss'>
|
||||
|
||||
</style>
|
@ -1,32 +0,0 @@
|
||||
<template lang='pug'>
|
||||
v-container(fluid, fill-height)
|
||||
v-layout(row wrap)
|
||||
v-flex(xs12)
|
||||
.admin-header-icon: v-icon(size='80', color='grey lighten-2') show_chart
|
||||
.headline.primary--text Statistics
|
||||
.subtitle-1.grey--text Useful information about your wiki
|
||||
.pa-3
|
||||
fingerprint-spinner(
|
||||
:animation-duration='1500'
|
||||
:size='128'
|
||||
color='#e91e63'
|
||||
)
|
||||
.caption.pink--text.mt-3 Compiling latest data...
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { FingerprintSpinner } from 'epic-spinners'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
FingerprintSpinner
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang='scss'>
|
||||
|
||||
</style>
|
@ -1,372 +0,0 @@
|
||||
<template lang='pug'>
|
||||
v-container(fluid, grid-list-lg)
|
||||
v-layout(row, wrap)
|
||||
v-flex(xs12)
|
||||
.admin-header
|
||||
img.animated.fadeInUp(src='/_assets/svg/icon-cloud-storage.svg', alt='Storage', style='width: 80px;')
|
||||
.admin-header-title
|
||||
.headline.primary--text.animated.fadeInLeft {{$t('admin:storage.title')}}
|
||||
.subtitle-1.grey--text.animated.fadeInLeft.wait-p4s {{$t('admin:storage.subtitle')}}
|
||||
v-spacer
|
||||
v-btn.animated.fadeInDown.wait-p3s(icon, outlined, color='grey', href='https://docs.requarks.io/storage', target='_blank')
|
||||
v-icon mdi-help-circle
|
||||
v-btn.mx-3.animated.fadeInDown.wait-p2s(icon, outlined, color='grey', @click='refresh')
|
||||
v-icon mdi-refresh
|
||||
v-btn.animated.fadeInDown(color='success', @click='save', depressed, large)
|
||||
v-icon(left) mdi-check
|
||||
span {{$t('common:actions.apply')}}
|
||||
|
||||
v-flex(lg3, xs12)
|
||||
v-card.animated.fadeInUp
|
||||
v-toolbar(flat, color='primary', dark, dense)
|
||||
.subtitle-1 {{$t('admin:storage.targets')}}
|
||||
v-list(two-line, dense).py-0
|
||||
template(v-for='(tgt, idx) in targets')
|
||||
v-list-item(:key='tgt.key', @click='selectedTarget = tgt.key', :disabled='!tgt.isAvailable')
|
||||
v-list-item-avatar(size='24')
|
||||
v-icon(color='grey', v-if='!tgt.isAvailable') mdi-minus-box-outline
|
||||
v-icon(color='primary', v-else-if='tgt.isEnabled', v-ripple, @click='tgt.key !== `local` && (tgt.isEnabled = false)') mdi-checkbox-marked-outline
|
||||
v-icon(color='grey', v-else, v-ripple, @click='tgt.isEnabled = true') mdi-checkbox-blank-outline
|
||||
v-list-item-content
|
||||
v-list-item-title.body-2(:class='!tgt.isAvailable ? `grey--text` : (selectedTarget === tgt.key ? `primary--text` : ``)') {{ tgt.title }}
|
||||
v-list-item-subtitle: .caption(:class='!tgt.isAvailable ? `grey--text text--lighten-1` : (selectedTarget === tgt.key ? `blue--text ` : ``)') {{ tgt.description }}
|
||||
v-list-item-avatar(v-if='selectedTarget === tgt.key', size='24')
|
||||
v-icon.animated.fadeInLeft(color='primary', large) mdi-chevron-right
|
||||
v-divider(v-if='idx < targets.length - 1')
|
||||
|
||||
v-card.mt-3.animated.fadeInUp.wait-p2s
|
||||
v-toolbar(flat, :color='$vuetify.theme.dark ? `grey darken-3-l5` : `grey darken-3`', dark, dense)
|
||||
.subtitle-1 {{$t('admin:storage.status')}}
|
||||
v-spacer
|
||||
looping-rhombuses-spinner(
|
||||
:animation-duration='5000'
|
||||
:rhombus-size='10'
|
||||
color='#FFF'
|
||||
)
|
||||
v-list.py-0(two-line, dense)
|
||||
template(v-for='(tgt, n) in status')
|
||||
v-list-item(:key='tgt.key')
|
||||
template(v-if='tgt.status === `pending`')
|
||||
v-list-item-avatar(color='purple')
|
||||
v-icon(color='white') mdi-clock-outline
|
||||
v-list-item-content
|
||||
v-list-item-title.body-2 {{tgt.title}}
|
||||
v-list-item-subtitle.purple--text.caption {{tgt.status}}
|
||||
v-list-item-action
|
||||
v-progress-circular(indeterminate, :size='20', :width='2', color='purple')
|
||||
template(v-else-if='tgt.status === `operational`')
|
||||
v-list-item-avatar(color='green')
|
||||
v-icon(color='white') mdi-check-circle
|
||||
v-list-item-content
|
||||
v-list-item-title.body-2 {{tgt.title}}
|
||||
v-list-item-subtitle.green--text.caption {{$t('admin:storage.lastSync', { time: $options.filters.moment(tgt.lastAttempt, 'from') })}}
|
||||
template(v-else)
|
||||
v-list-item-avatar(color='red')
|
||||
v-icon(color='white') mdi-close-circle-outline
|
||||
v-list-item-content
|
||||
v-list-item-title.body-2 {{tgt.title}}
|
||||
v-list-item-subtitle.red--text.caption {{$t('admin:storage.lastSyncAttempt', { time: $options.filters.moment(tgt.lastAttempt, 'from') })}}
|
||||
v-list-item-action
|
||||
v-menu
|
||||
template(v-slot:activator='{ on }')
|
||||
v-btn(icon, v-on='on')
|
||||
v-icon(color='red') mdi-information
|
||||
v-card(width='450')
|
||||
v-toolbar(flat, color='red', dark, dense) {{$t('admin:storage.errorMsg')}}
|
||||
v-card-text {{tgt.message}}
|
||||
|
||||
v-divider(v-if='n < status.length - 1')
|
||||
v-list-item(v-if='status.length < 1')
|
||||
em {{$t('admin:storage.noTarget')}}
|
||||
|
||||
v-flex(xs12, lg9)
|
||||
v-card.wiki-form.animated.fadeInUp.wait-p2s
|
||||
v-toolbar(color='primary', dense, flat, dark)
|
||||
.subtitle-1 {{target.title}}
|
||||
v-spacer
|
||||
v-switch(
|
||||
dark
|
||||
color='blue lighten-5'
|
||||
label='Active'
|
||||
v-model='target.isEnabled'
|
||||
hide-details
|
||||
inset
|
||||
)
|
||||
v-card-info(color='blue')
|
||||
div
|
||||
div {{target.description}}
|
||||
span.caption: a(:href='target.website') {{target.website}}
|
||||
v-spacer
|
||||
.admin-providerlogo
|
||||
img(:src='target.logo', :alt='target.title')
|
||||
v-card-text
|
||||
v-form
|
||||
i18next.body-2(path='admin:storage.targetState', tag='div', v-if='target.isEnabled')
|
||||
v-chip(color='green', small, dark, label, place='state') {{$t('admin:storage.targetStateActive')}}
|
||||
i18next.body-2(path='admin:storage.targetState', tag='div', v-else)
|
||||
v-chip(color='red', small, dark, label, place='state') {{$t('admin:storage.targetStateInactive')}}
|
||||
v-divider.mt-3
|
||||
.overline.my-5 {{$t('admin:storage.targetConfig')}}
|
||||
.body-2.ml-3(v-if='!target.config || target.config.length < 1'): em {{$t('admin:storage.noConfigOption')}}
|
||||
template(v-else, v-for='cfg in target.config')
|
||||
v-select(
|
||||
v-if='cfg.value.type === "string" && cfg.value.enum'
|
||||
outlined
|
||||
:items='cfg.value.enum'
|
||||
:key='cfg.key'
|
||||
:label='cfg.value.title'
|
||||
v-model='cfg.value.value'
|
||||
prepend-icon='mdi-cog-box'
|
||||
:hint='cfg.value.hint ? cfg.value.hint : ""'
|
||||
persistent-hint
|
||||
:class='cfg.value.hint ? "mb-2" : ""'
|
||||
)
|
||||
v-switch.mb-3(
|
||||
v-else-if='cfg.value.type === "boolean"'
|
||||
:key='cfg.key'
|
||||
:label='cfg.value.title'
|
||||
v-model='cfg.value.value'
|
||||
color='primary'
|
||||
prepend-icon='mdi-cog-box'
|
||||
:hint='cfg.value.hint ? cfg.value.hint : ""'
|
||||
persistent-hint
|
||||
inset
|
||||
)
|
||||
v-textarea(
|
||||
v-else-if='cfg.value.type === "string" && cfg.value.multiline'
|
||||
outlined
|
||||
:key='cfg.key'
|
||||
:label='cfg.value.title'
|
||||
v-model='cfg.value.value'
|
||||
prepend-icon='mdi-cog-box'
|
||||
:hint='cfg.value.hint ? cfg.value.hint : ""'
|
||||
persistent-hint
|
||||
:class='cfg.value.hint ? "mb-2" : ""'
|
||||
)
|
||||
v-text-field(
|
||||
v-else
|
||||
outlined
|
||||
:key='cfg.key'
|
||||
:label='cfg.value.title'
|
||||
v-model='cfg.value.value'
|
||||
prepend-icon='mdi-cog-box'
|
||||
:hint='cfg.value.hint ? cfg.value.hint : ""'
|
||||
persistent-hint
|
||||
:class='cfg.value.hint ? "mb-2" : ""'
|
||||
)
|
||||
v-divider.mt-3
|
||||
.overline.my-5 {{$t('admin:storage.syncDirection')}}
|
||||
.body-2.ml-3 {{$t('admin:storage.syncDirectionSubtitle')}}
|
||||
.pr-3.pt-3
|
||||
v-radio-group.ml-3.py-0(v-model='target.mode')
|
||||
v-radio(
|
||||
:label='$t(`admin:storage.syncDirBi`)'
|
||||
color='primary'
|
||||
value='sync'
|
||||
:disabled='target.supportedModes.indexOf(`sync`) < 0'
|
||||
)
|
||||
v-radio(
|
||||
:label='$t(`admin:storage.syncDirPush`)'
|
||||
color='primary'
|
||||
value='push'
|
||||
:disabled='target.supportedModes.indexOf(`push`) < 0'
|
||||
)
|
||||
v-radio(
|
||||
:label='$t(`admin:storage.syncDirPull`)'
|
||||
color='primary'
|
||||
value='pull'
|
||||
:disabled='target.supportedModes.indexOf(`pull`) < 0'
|
||||
)
|
||||
.body-2.ml-3
|
||||
strong {{$t('admin:storage.syncDirBi')}} #[em.red--text.text--lighten-2(v-if='target.supportedModes.indexOf(`sync`) < 0') {{$t('admin:storage.unsupported')}}]
|
||||
.pb-3 {{$t('admin:storage.syncDirBiHint')}}
|
||||
strong {{$t('admin:storage.syncDirPush')}} #[em.red--text.text--lighten-2(v-if='target.supportedModes.indexOf(`push`) < 0') {{$t('admin:storage.unsupported')}}]
|
||||
.pb-3 {{$t('admin:storage.syncDirPushHint')}}
|
||||
strong {{$t('admin:storage.syncDirPull')}} #[em.red--text.text--lighten-2(v-if='target.supportedModes.indexOf(`pull`) < 0') {{$t('admin:storage.unsupported')}}]
|
||||
.pb-3 {{$t('admin:storage.syncDirPullHint')}}
|
||||
|
||||
template(v-if='target.hasSchedule')
|
||||
v-divider.mt-3
|
||||
.overline.my-5 {{$t('admin:storage.syncSchedule')}}
|
||||
.body-2.ml-3 {{$t('admin:storage.syncScheduleHint')}}
|
||||
.pa-3
|
||||
duration-picker(v-model='target.syncInterval')
|
||||
i18next.caption.mt-3(path='admin:storage.syncScheduleCurrent', tag='div')
|
||||
strong(place='schedule') {{getDefaultSchedule(target.syncInterval)}}
|
||||
i18next.caption(path='admin:storage.syncScheduleDefault', tag='div')
|
||||
strong(place='schedule') {{getDefaultSchedule(target.syncIntervalDefault)}}
|
||||
|
||||
template(v-if='target.actions && target.actions.length > 0')
|
||||
v-divider.mt-3
|
||||
.overline.my-5 {{$t('admin:storage.actions')}}
|
||||
v-alert(outlined, :value='!target.isEnabled', color='red', icon='mdi-alert')
|
||||
.body-2 {{$t('admin:storage.actionsInactiveWarn')}}
|
||||
v-container.pt-0(grid-list-xl, fluid)
|
||||
v-layout(row, wrap, fill-height)
|
||||
v-flex(xs12, lg6, xl4, v-for='act of target.actions', :key='act.handler')
|
||||
v-card.radius-7.grey(flat, :class='$vuetify.theme.dark ? `darken-3-d5` : `lighten-3`', height='100%')
|
||||
v-card-text
|
||||
.subtitle-1(v-html='act.label')
|
||||
.body-2.mt-4(v-html='act.hint')
|
||||
v-btn.mx-0.mt-5(
|
||||
@click='executeAction(target.key, act.handler)'
|
||||
outlined
|
||||
:color='$vuetify.theme.dark ? `blue` : `primary`'
|
||||
:disabled='runningAction || !target.isEnabled'
|
||||
:loading='runningActionHandler === act.handler'
|
||||
) {{$t('admin:storage.actionRun')}}
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import moment from 'moment'
|
||||
import momentDurationFormatSetup from 'moment-duration-format'
|
||||
|
||||
import DurationPicker from '../common/duration-picker.vue'
|
||||
import { LoopingRhombusesSpinner } from 'epic-spinners'
|
||||
|
||||
import statusQuery from 'gql/admin/storage/storage-query-status.gql'
|
||||
import targetsQuery from 'gql/admin/storage/storage-query-targets.gql'
|
||||
import targetExecuteActionMutation from 'gql/admin/storage/storage-mutation-executeaction.gql'
|
||||
import targetsSaveMutation from 'gql/admin/storage/storage-mutation-save-targets.gql'
|
||||
|
||||
momentDurationFormatSetup(moment)
|
||||
|
||||
export default {
|
||||
components: {
|
||||
DurationPicker,
|
||||
LoopingRhombusesSpinner
|
||||
},
|
||||
filters: {
|
||||
startCase(val) { return _.startCase(val) }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
runningAction: false,
|
||||
runningActionHandler: '',
|
||||
selectedTarget: '',
|
||||
target: {
|
||||
supportedModes: []
|
||||
},
|
||||
targets: [],
|
||||
status: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
activeTargets() {
|
||||
return _.filter(this.targets, 'isEnabled')
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
selectedTarget(newValue, oldValue) {
|
||||
this.target = _.find(this.targets, ['key', newValue]) || {}
|
||||
},
|
||||
targets(newValue, oldValue) {
|
||||
this.selectedTarget = _.get(_.find(this.targets, ['isEnabled', true]), 'key', 'disk')
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async refresh() {
|
||||
await this.$apollo.queries.targets.refetch()
|
||||
this.$store.commit('showNotification', {
|
||||
message: 'List of storage targets has been refreshed.',
|
||||
style: 'success',
|
||||
icon: 'cached'
|
||||
})
|
||||
},
|
||||
async save() {
|
||||
this.$store.commit(`loadingStart`, 'admin-storage-savetargets')
|
||||
await this.$apollo.mutate({
|
||||
mutation: targetsSaveMutation,
|
||||
variables: {
|
||||
targets: this.targets.map(tgt => _.pick(tgt, [
|
||||
'isEnabled',
|
||||
'key',
|
||||
'config',
|
||||
'mode',
|
||||
'syncInterval'
|
||||
])).map(str => ({...str, config: str.config.map(cfg => ({...cfg, value: JSON.stringify({ v: cfg.value.value })}))}))
|
||||
}
|
||||
})
|
||||
this.$store.commit('showNotification', {
|
||||
message: 'Storage configuration saved successfully.',
|
||||
style: 'success',
|
||||
icon: 'check'
|
||||
})
|
||||
this.$store.commit(`loadingStop`, 'admin-storage-savetargets')
|
||||
},
|
||||
getDefaultSchedule(val) {
|
||||
if (!val) { return 'N/A' }
|
||||
return moment.duration(val).format('y [years], M [months], d [days], h [hours], m [minutes]')
|
||||
},
|
||||
async executeAction(targetKey, handler) {
|
||||
this.$store.commit(`loadingStart`, 'admin-storage-executeaction')
|
||||
this.runningAction = true
|
||||
this.runningActionHandler = handler
|
||||
try {
|
||||
await this.$apollo.mutate({
|
||||
mutation: targetExecuteActionMutation,
|
||||
variables: {
|
||||
targetKey,
|
||||
handler
|
||||
}
|
||||
})
|
||||
this.$store.commit('showNotification', {
|
||||
message: 'Action completed.',
|
||||
style: 'success',
|
||||
icon: 'check'
|
||||
})
|
||||
} catch (err) {
|
||||
console.warn(err)
|
||||
}
|
||||
this.runningAction = false
|
||||
this.runningActionHandler = ''
|
||||
this.$store.commit(`loadingStop`, 'admin-storage-executeaction')
|
||||
}
|
||||
},
|
||||
apollo: {
|
||||
targets: {
|
||||
query: targetsQuery,
|
||||
fetchPolicy: 'network-only',
|
||||
update: (data) => _.cloneDeep(data.storage.targets).map(str => ({
|
||||
...str,
|
||||
config: _.sortBy(str.config.map(cfg => ({
|
||||
...cfg,
|
||||
value: JSON.parse(cfg.value)
|
||||
})), [t => t.value.order])
|
||||
})),
|
||||
watchLoading (isLoading) {
|
||||
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-storage-targets-refresh')
|
||||
}
|
||||
},
|
||||
status: {
|
||||
query: statusQuery,
|
||||
fetchPolicy: 'network-only',
|
||||
update: (data) => data.storage.status,
|
||||
watchLoading (isLoading) {
|
||||
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-storage-status-refresh')
|
||||
},
|
||||
pollInterval: 3000
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang='scss' scoped>
|
||||
|
||||
.targetlogo {
|
||||
width: 250px;
|
||||
height: 85px;
|
||||
float:right;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
max-height: 50px;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
@ -1,238 +0,0 @@
|
||||
<template lang='pug'>
|
||||
v-container.admin-system(fluid, grid-list-lg)
|
||||
v-layout(row, wrap)
|
||||
v-flex(xs12)
|
||||
.admin-header
|
||||
img.animated.fadeInUp(src='/_assets/svg/icon-tune.svg', alt='System Info', style='width: 80px;')
|
||||
.admin-header-title
|
||||
.headline.primary--text.animated.fadeInLeft {{ $t('admin:system.title') }}
|
||||
.subtitle-1.grey--text.animated.fadeInLeft.wait-p2s {{ $t('admin:system.subtitle') }}
|
||||
v-layout.mt-3(row wrap)
|
||||
v-flex(lg6 xs12)
|
||||
v-card.animated.fadeInUp
|
||||
v-btn.animated.fadeInLeft.wait-p2s.btn-animate-rotate(fab, absolute, :right='!$vuetify.rtl', :left='$vuetify.rtl', top, small, light, @click='refresh'): v-icon(color='grey') mdi-refresh
|
||||
v-subheader Wiki.js
|
||||
v-list(two-line, dense)
|
||||
v-list-item
|
||||
v-list-item-avatar
|
||||
v-icon.blue.white--text mdi-application-export
|
||||
v-list-item-content
|
||||
v-list-item-title {{ $t('admin:system.currentVersion') }}
|
||||
v-list-item-subtitle {{ info.currentVersion }}
|
||||
v-list-item
|
||||
v-list-item-avatar
|
||||
v-icon.blue.white--text mdi-inbox-arrow-up
|
||||
v-list-item-content
|
||||
v-list-item-title {{ $t('admin:system.latestVersion') }}
|
||||
v-list-item-subtitle {{ info.latestVersion }}
|
||||
v-list-item-action
|
||||
v-list-item-action-text {{ $t('admin:system.published') }} {{ info.latestVersionReleaseDate | moment('from') }}
|
||||
v-card-actions(v-if='info.upgradeCapable && !isLatestVersion && info.platform === `docker`', :class='$vuetify.theme.dark ? `grey darken-3-d5` : `indigo lighten-5`')
|
||||
.caption.indigo--text.pl-3(:class='$vuetify.theme.dark ? `text--lighten-4` : ``') Wiki.js can perform the upgrade to the latest version for you.
|
||||
v-spacer
|
||||
v-btn.px-3(
|
||||
color='indigo'
|
||||
dark
|
||||
@click='performUpgrade'
|
||||
)
|
||||
v-icon(left) mdi-upload
|
||||
span Perform Upgrade
|
||||
|
||||
v-card.mt-4.animated.fadeInUp.wait-p2s
|
||||
v-subheader {{ $t('admin:system.hostInfo') }}
|
||||
v-list(two-line, dense)
|
||||
v-list-item
|
||||
v-list-item-avatar
|
||||
v-avatar.blue-grey(size='40')
|
||||
v-icon(color='white') {{platformLogo}}
|
||||
v-list-item-content
|
||||
v-list-item-title {{ $t('admin:system.os') }}
|
||||
v-list-item-subtitle {{ (info.platform === 'docker') ? 'Docker Container (Linux)' : info.operatingSystem }}
|
||||
v-list-item
|
||||
v-list-item-avatar
|
||||
v-icon.blue-grey.white--text mdi-desktop-classic
|
||||
v-list-item-content
|
||||
v-list-item-title {{ $t('admin:system.hostname') }}
|
||||
v-list-item-subtitle {{ info.hostname }}
|
||||
v-list-item
|
||||
v-list-item-avatar
|
||||
v-icon.blue-grey.white--text mdi-cpu-64-bit
|
||||
v-list-item-content
|
||||
v-list-item-title {{ $t('admin:system.cpuCores') }}
|
||||
v-list-item-subtitle {{ info.cpuCores }}
|
||||
v-list-item
|
||||
v-list-item-avatar
|
||||
v-icon.blue-grey.white--text mdi-memory
|
||||
v-list-item-content
|
||||
v-list-item-title {{ $t('admin:system.totalRAM') }}
|
||||
v-list-item-subtitle {{ info.ramTotal }}
|
||||
v-list-item
|
||||
v-list-item-avatar
|
||||
v-icon.blue-grey.white--text mdi-iframe-outline
|
||||
v-list-item-content
|
||||
v-list-item-title {{ $t('admin:system.workingDirectory') }}
|
||||
v-list-item-subtitle {{ info.workingDirectory }}
|
||||
v-list-item
|
||||
v-list-item-avatar
|
||||
v-icon.blue-grey.white--text mdi-card-bulleted-settings-outline
|
||||
v-list-item-content
|
||||
v-list-item-title {{ $t('admin:system.configFile') }}
|
||||
v-list-item-subtitle {{ info.configFile }}
|
||||
|
||||
v-flex(lg6 xs12)
|
||||
v-card.pb-3.animated.fadeInUp.wait-p4s
|
||||
v-subheader Node.js
|
||||
v-list(dense)
|
||||
v-list-item
|
||||
v-list-item-avatar
|
||||
v-avatar.light-green(size='40')
|
||||
v-icon(color='white') mdi-nodejs
|
||||
v-list-item-content
|
||||
v-list-item-title {{ info.nodeVersion }}
|
||||
|
||||
v-divider.mt-3
|
||||
v-subheader {{ info.dbType }}
|
||||
v-list(dense)
|
||||
v-list-item
|
||||
v-list-item-avatar
|
||||
v-avatar.indigo.darken-1(size='40')
|
||||
v-icon(color='white') mdi-database
|
||||
v-list-item-content
|
||||
v-list-item-title(v-html='dbVersion')
|
||||
v-list-item-subtitle {{ info.dbHost }}
|
||||
|
||||
v-alert.mt-3.mx-4(:value='isDbLimited', color='deep-orange darken-2', icon='mdi-alert', dark) {{ $t('admin:system.dbPartialSupport') }}
|
||||
|
||||
v-dialog(
|
||||
v-model='isUpgrading'
|
||||
persistent
|
||||
width='450'
|
||||
)
|
||||
v-card.blue.darken-5(dark)
|
||||
v-card-text.text-center.pa-10
|
||||
self-building-square-spinner(
|
||||
:animation-duration='4000'
|
||||
:size='40'
|
||||
color='#FFF'
|
||||
style='margin: 0 auto;'
|
||||
)
|
||||
.body-2.mt-5.blue--text.text--lighten-4 Your Wiki.js container is being upgraded...
|
||||
.caption.blue--text.text--lighten-2 Please wait
|
||||
v-progress-linear.mt-5(
|
||||
color='blue lighten-2'
|
||||
:value='upgradeProgress'
|
||||
:buffer-value='upgradeProgress'
|
||||
rounded
|
||||
:stream='isUpgradingStarted'
|
||||
query
|
||||
:indeterminate='!isUpgradingStarted'
|
||||
)
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
|
||||
import { SelfBuildingSquareSpinner } from 'epic-spinners'
|
||||
|
||||
import systemInfoQuery from 'gql/admin/system/system-query-info.gql'
|
||||
import performUpgradeMutation from 'gql/admin/system/system-mutation-upgrade.gql'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
SelfBuildingSquareSpinner
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
isUpgrading: false,
|
||||
isUpgradingStarted: false,
|
||||
upgradeProgress: 0,
|
||||
info: {}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
dbVersion () {
|
||||
return _.get(this.info, 'dbVersion', '').replace(/(?:\r\n|\r|\n)/g, '<br />')
|
||||
},
|
||||
platformLogo () {
|
||||
switch (this.info.platform) {
|
||||
case 'docker':
|
||||
return 'mdi-docker'
|
||||
case 'darwin':
|
||||
return 'mdi-apple'
|
||||
case 'linux':
|
||||
if (this.info.operatingSystem.indexOf('Ubuntu')) {
|
||||
return 'mdi-ubuntu'
|
||||
} else {
|
||||
return 'mdi-linux'
|
||||
}
|
||||
case 'win32':
|
||||
return 'mdi-microsoft-windows'
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
},
|
||||
isDbLimited () {
|
||||
return this.info.dbType === 'MySQL' && this.dbVersion.indexOf('5.') === 0
|
||||
},
|
||||
isLatestVersion () {
|
||||
return this.info.currentVersion === this.info.latestVersion
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async refresh () {
|
||||
await this.$apollo.queries.info.refetch()
|
||||
this.$store.commit('showNotification', {
|
||||
message: this.$t('admin:system.refreshSuccess'),
|
||||
style: 'success',
|
||||
icon: 'cached'
|
||||
})
|
||||
},
|
||||
async performUpgrade () {
|
||||
this.isUpgrading = true
|
||||
this.isUpgradingStarted = false
|
||||
this.upgradeProgress = 0
|
||||
this.$store.commit(`loadingStart`, 'admin-system-upgrade')
|
||||
try {
|
||||
const respRaw = await this.$apollo.mutate({
|
||||
mutation: performUpgradeMutation
|
||||
})
|
||||
const resp = _.get(respRaw, 'data.system.performUpgrade.responseResult', {})
|
||||
if (resp.succeeded) {
|
||||
this.isUpgradingStarted = true
|
||||
let progressInterval = setInterval(() => {
|
||||
this.upgradeProgress += 0.83
|
||||
}, 500)
|
||||
_.delay(() => {
|
||||
clearInterval(progressInterval)
|
||||
window.location.reload(true)
|
||||
}, 60000)
|
||||
} else {
|
||||
throw new Error(resp.message)
|
||||
}
|
||||
} catch (err) {
|
||||
this.$store.commit('pushGraphError', err)
|
||||
this.$store.commit(`loadingStop`, 'admin-system-upgrade')
|
||||
this.isUpgrading = false
|
||||
}
|
||||
}
|
||||
},
|
||||
apollo: {
|
||||
info: {
|
||||
query: systemInfoQuery,
|
||||
fetchPolicy: 'network-only',
|
||||
update: (data) => data.system.info,
|
||||
watchLoading (isLoading) {
|
||||
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-system-refresh')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang='scss'>
|
||||
.admin-system {
|
||||
.v-list-item-title, .v-list-item__subtitle {
|
||||
user-select: text;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,248 +0,0 @@
|
||||
<template lang='pug'>
|
||||
v-container(fluid, grid-list-lg)
|
||||
v-layout(row wrap)
|
||||
v-flex(xs12)
|
||||
.admin-header
|
||||
img.animated.fadeInUp(src='/_assets/svg/icon-tags.svg', alt='Tags', style='width: 80px;')
|
||||
.admin-header-title
|
||||
.headline.primary--text.animated.fadeInLeft {{$t('tags.title')}}
|
||||
.subtitle-1.grey--text.animated.fadeInLeft.wait-p4s {{$t('tags.subtitle')}}
|
||||
v-spacer
|
||||
v-btn.animated.fadeInDown(outlined, color='grey', @click='refresh', icon)
|
||||
v-icon mdi-refresh
|
||||
v-container.pa-0.mt-3(fluid, grid-list-lg)
|
||||
v-layout(row)
|
||||
v-flex(style='flex: 0 0 350px;')
|
||||
v-card.animated.fadeInUp
|
||||
v-toolbar(:color='$vuetify.theme.dark ? `grey darken-3-d5` : `grey lighten-4`', flat)
|
||||
v-text-field(
|
||||
v-model='filter'
|
||||
:label='$t(`admin:tags.filter`)'
|
||||
hide-details
|
||||
single-line
|
||||
solo
|
||||
flat
|
||||
dense
|
||||
color='teal'
|
||||
:background-color='$vuetify.theme.dark ? `grey darken-4` : `grey lighten-2`'
|
||||
prepend-inner-icon='mdi-magnify'
|
||||
)
|
||||
v-divider
|
||||
v-list.py-2(dense, nav)
|
||||
v-list-item(v-if='tags.length < 1')
|
||||
v-list-item-avatar(size='24'): v-icon(color='grey') mdi-compass-off
|
||||
v-list-item-content
|
||||
.caption.grey--text {{$t('tags.emptyList')}}
|
||||
v-list-item(
|
||||
v-for='tag of filteredTags'
|
||||
:key='tag.id'
|
||||
:class='(tag.id === current.id) ? "teal" : ""'
|
||||
@click='selectTag(tag)'
|
||||
)
|
||||
v-list-item-avatar(size='24', tile): v-icon(size='18', :color='tag.id === current.id ? `white` : `teal`') mdi-tag
|
||||
v-list-item-title(:class='tag.id === current.id ? `white--text` : ``') {{tag.tag}}
|
||||
v-flex.animated.fadeInUp.wait-p2s
|
||||
template(v-if='current.id')
|
||||
v-card
|
||||
v-toolbar(dense, color='teal', flat, dark)
|
||||
.subtitle-1 {{$t('tags.edit')}}
|
||||
v-spacer
|
||||
v-btn.pl-4(
|
||||
color='white'
|
||||
dark
|
||||
outlined
|
||||
small
|
||||
:href='`/t/` + current.tag'
|
||||
)
|
||||
span.text-none {{$t('admin:tags.viewLinkedPages')}}
|
||||
v-icon(right) mdi-chevron-right
|
||||
v-card-text
|
||||
v-text-field(
|
||||
outlined
|
||||
:label='$t("tags.tag")'
|
||||
prepend-icon='mdi-tag'
|
||||
v-model='current.tag'
|
||||
counter='255'
|
||||
)
|
||||
v-text-field(
|
||||
outlined
|
||||
:label='$t("tags.label")'
|
||||
prepend-icon='mdi-format-title'
|
||||
v-model='current.title'
|
||||
hide-details
|
||||
)
|
||||
v-card-chin
|
||||
i18next.caption.pl-3(path='admin:tags.date', tag='div')
|
||||
strong(place='created') {{current.createdAt | moment('from')}}
|
||||
strong(place='updated') {{current.updatedAt | moment('from')}}
|
||||
v-spacer
|
||||
v-dialog(v-model='deleteTagDialog', max-width='500')
|
||||
template(v-slot:activator='{ on }')
|
||||
v-btn(color='red', outlined, v-on='on')
|
||||
v-icon(color='red') mdi-trash-can-outline
|
||||
v-card
|
||||
.dialog-header.is-red {{$t('admin:tags.deleteConfirm')}}
|
||||
v-card-text.pa-4
|
||||
i18next(tag='span', path='admin:tags.deleteConfirmText')
|
||||
strong(place='tag') {{ current.tag }}
|
||||
v-card-actions
|
||||
v-spacer
|
||||
v-btn(text, @click='deleteTagDialog = false') {{$t('common:actions.cancel')}}
|
||||
v-btn(color='red', dark, @click='deleteTag(current)') {{$t('common:actions.delete')}}
|
||||
v-btn.px-5.mr-2(color='success', depressed, dark, @click='saveTag(current)')
|
||||
v-icon(left) mdi-content-save
|
||||
span {{$t('common:actions.save')}}
|
||||
v-card(v-else)
|
||||
v-card-text.grey--text(v-if='tags.length > 0') {{$t('tags.noSelectionText')}}
|
||||
v-card-text.grey--text(v-else) {{$t('tags.noItemsText')}}
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
tags: [],
|
||||
current: {},
|
||||
filter: '',
|
||||
deleteTagDialog: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
filteredTags () {
|
||||
if (this.filter.length > 0) {
|
||||
return _.filter(this.tags, t => t.tag.indexOf(this.filter) >= 0 || t.title.indexOf(this.filter) >= 0)
|
||||
} else {
|
||||
return this.tags
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
selectTag(tag) {
|
||||
this.current = tag
|
||||
},
|
||||
async deleteTag(tag) {
|
||||
this.$store.commit(`loadingStart`, 'admin-tags-delete')
|
||||
try {
|
||||
const resp = await this.$apollo.mutate({
|
||||
mutation: gql`
|
||||
mutation ($id: Int!) {
|
||||
pages {
|
||||
deleteTag (id: $id) {
|
||||
responseResult {
|
||||
succeeded
|
||||
errorCode
|
||||
slug
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
id: tag.id
|
||||
}
|
||||
})
|
||||
if (_.get(resp, 'data.pages.deleteTag.responseResult.succeeded', false)) {
|
||||
this.$store.commit('showNotification', {
|
||||
message: this.$t('tags.deleteSuccess'),
|
||||
style: 'success',
|
||||
icon: 'check'
|
||||
})
|
||||
this.refresh()
|
||||
} else {
|
||||
throw new Error(_.get(resp, 'data.pages.deleteTag.responseResult.message', 'An unexpected error occurred.'))
|
||||
}
|
||||
} catch (err) {
|
||||
this.$store.commit('pushGraphError', err)
|
||||
}
|
||||
this.deleteTagDialog = false
|
||||
this.$store.commit(`loadingStop`, 'admin-tags-delete')
|
||||
},
|
||||
async saveTag(tag) {
|
||||
this.$store.commit(`loadingStart`, 'admin-tags-save')
|
||||
try {
|
||||
const resp = await this.$apollo.mutate({
|
||||
mutation: gql`
|
||||
mutation ($id: Int!, $tag: String!, $title: String!) {
|
||||
pages {
|
||||
updateTag (id: $id, tag: $tag, title: $title) {
|
||||
responseResult {
|
||||
succeeded
|
||||
errorCode
|
||||
slug
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
id: tag.id,
|
||||
tag: tag.tag,
|
||||
title: tag.title
|
||||
}
|
||||
})
|
||||
if (_.get(resp, 'data.pages.updateTag.responseResult.succeeded', false)) {
|
||||
this.$store.commit('showNotification', {
|
||||
message: this.$t('tags.saveSuccess'),
|
||||
style: 'success',
|
||||
icon: 'check'
|
||||
})
|
||||
this.current.updatedAt = new Date()
|
||||
} else {
|
||||
throw new Error(_.get(resp, 'data.pages.updateTag.responseResult.message', 'An unexpected error occurred.'))
|
||||
}
|
||||
} catch (err) {
|
||||
this.$store.commit('pushGraphError', err)
|
||||
}
|
||||
this.$store.commit(`loadingStop`, 'admin-tags-save')
|
||||
},
|
||||
async refresh() {
|
||||
await this.$apollo.queries.tags.refetch()
|
||||
this.current = {}
|
||||
this.$store.commit('showNotification', {
|
||||
message: this.$t('tags.refreshSuccess'),
|
||||
style: 'success',
|
||||
icon: 'cached'
|
||||
})
|
||||
}
|
||||
},
|
||||
apollo: {
|
||||
tags: {
|
||||
query: gql`
|
||||
{
|
||||
pages {
|
||||
tags {
|
||||
id
|
||||
tag
|
||||
title
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
fetchPolicy: 'network-only',
|
||||
update: (data) => _.cloneDeep(data.pages.tags),
|
||||
watchLoading (isLoading) {
|
||||
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-tags-refresh')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang='scss' scoped>
|
||||
|
||||
.clickable {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(mc('blue', '500'), .25);
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
@ -1,255 +0,0 @@
|
||||
<template lang='pug'>
|
||||
v-container(fluid, grid-list-lg)
|
||||
v-layout(row wrap)
|
||||
v-flex(xs12)
|
||||
.admin-header
|
||||
img.animated.fadeInUp(src='/_assets/svg/icon-paint-palette.svg', alt='Theme', style='width: 80px;')
|
||||
.admin-header-title
|
||||
.headline.primary--text.animated.fadeInLeft {{$t('admin:theme.title')}}
|
||||
.subtitle-1.grey--text.animated.fadeInLeft.wait-p2s {{$t('admin:theme.subtitle')}}
|
||||
v-spacer
|
||||
v-btn.animated.fadeInRight(color='success', depressed, @click='save', large, :loading='loading')
|
||||
v-icon(left) mdi-check
|
||||
span {{$t('common:actions.apply')}}
|
||||
v-form.pt-3
|
||||
v-layout(row wrap)
|
||||
v-flex(lg6 xs12)
|
||||
v-card.animated.fadeInUp
|
||||
v-toolbar(color='primary', dark, dense, flat)
|
||||
v-toolbar-title.subtitle-1 {{$t('admin:theme.title')}}
|
||||
v-card-text
|
||||
v-select(
|
||||
:items='themes'
|
||||
outlined
|
||||
prepend-icon='mdi-palette'
|
||||
v-model='config.theme'
|
||||
:label='$t(`admin:theme.siteTheme`)'
|
||||
persistent-hint
|
||||
:hint='$t(`admin:theme.siteThemeHint`)'
|
||||
)
|
||||
template(slot='item', slot-scope='data')
|
||||
v-list-item-avatar
|
||||
v-icon.blue--text(dark) mdi-image-filter-frames
|
||||
v-list-item-content
|
||||
v-list-item-title(v-html='data.item.text')
|
||||
v-list-item-sub-title(v-html='data.item.author')
|
||||
v-select.mt-3(
|
||||
:items='iconsets'
|
||||
outlined
|
||||
prepend-icon='mdi-paw'
|
||||
v-model='config.iconset'
|
||||
:label='$t(`admin:theme.iconset`)'
|
||||
persistent-hint
|
||||
:hint='$t(`admin:theme.iconsetHint`)'
|
||||
)
|
||||
v-divider.mt-3
|
||||
v-switch(
|
||||
inset
|
||||
v-model='darkMode'
|
||||
:label='$t(`admin:theme.darkMode`)'
|
||||
color='primary'
|
||||
persistent-hint
|
||||
:hint='$t(`admin:theme.darkModeHint`)'
|
||||
)
|
||||
|
||||
v-card.mt-3.animated.fadeInUp.wait-p1s
|
||||
v-toolbar(color='primary', dark, dense, flat)
|
||||
v-toolbar-title.subtitle-1 {{$t(`admin:theme.options`)}}
|
||||
v-spacer
|
||||
v-chip(label, color='white', small).primary--text coming soon
|
||||
v-card-text
|
||||
v-select(
|
||||
:items='[]'
|
||||
outlined
|
||||
prepend-icon='mdi-border-vertical'
|
||||
v-model='config.iconset'
|
||||
label='Table of Contents Position'
|
||||
persistent-hint
|
||||
hint='Select whether the table of contents is shown on the left, right or not at all.'
|
||||
disabled
|
||||
)
|
||||
|
||||
v-flex(lg6 xs12)
|
||||
//- v-card.animated.fadeInUp.wait-p2s
|
||||
//- v-toolbar(color='teal', dark, dense, flat)
|
||||
//- v-toolbar-title.subtitle-1 {{$t('admin:theme.downloadThemes')}}
|
||||
//- v-spacer
|
||||
//- v-chip(label, color='white', small).teal--text coming soon
|
||||
//- v-data-table(
|
||||
//- :headers='headers',
|
||||
//- :items='themes',
|
||||
//- hide-default-footer,
|
||||
//- item-key='value',
|
||||
//- :items-per-page='1000'
|
||||
//- )
|
||||
//- template(v-slot:item='thm')
|
||||
//- td
|
||||
//- strong {{thm.item.text}}
|
||||
//- td
|
||||
//- span {{ thm.item.author }}
|
||||
//- td.text-xs-center
|
||||
//- v-progress-circular(v-if='thm.item.isDownloading', indeterminate, color='blue', size='20', :width='2')
|
||||
//- v-btn(v-else-if='thm.item.isInstalled && thm.item.installDate < thm.item.updatedAt', icon)
|
||||
//- v-icon.blue--text mdi-cached
|
||||
//- v-btn(v-else-if='thm.item.isInstalled', icon)
|
||||
//- v-icon.green--text mdi-check-bold
|
||||
//- v-btn(v-else, icon)
|
||||
//- v-icon.grey--text mdi-cloud-download
|
||||
|
||||
v-card.animated.fadeInUp.wait-p2s
|
||||
v-toolbar(color='primary', dark, dense, flat)
|
||||
v-toolbar-title.subtitle-1 {{$t(`admin:theme.codeInjection`)}}
|
||||
v-card-text
|
||||
v-textarea.is-monospaced(
|
||||
v-model='config.injectCSS'
|
||||
:label='$t(`admin:theme.cssOverride`)'
|
||||
outlined
|
||||
color='primary'
|
||||
persistent-hint
|
||||
:hint='$t(`admin:theme.cssOverrideHint`)'
|
||||
auto-grow
|
||||
)
|
||||
i18next.caption.pl-2.ml-1(path='admin:theme.cssOverrideWarning', tag='div')
|
||||
strong.red--text(place='caution') {{$t('admin:theme.cssOverrideWarningCaution')}}
|
||||
code(place='cssClass') .contents
|
||||
v-textarea.is-monospaced.mt-3(
|
||||
v-model='config.injectHead'
|
||||
:label='$t(`admin:theme.headHtmlInjection`)'
|
||||
outlined
|
||||
color='primary'
|
||||
persistent-hint
|
||||
:hint='$t(`admin:theme.headHtmlInjectionHint`)'
|
||||
auto-grow
|
||||
)
|
||||
v-textarea.is-monospaced.mt-2(
|
||||
v-model='config.injectBody'
|
||||
:label='$t(`admin:theme.bodyHtmlInjection`)'
|
||||
outlined
|
||||
color='primary'
|
||||
persistent-hint
|
||||
:hint='$t(`admin:theme.bodyHtmlInjectionHint`)'
|
||||
auto-grow
|
||||
)
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import { sync } from 'vuex-pathify'
|
||||
|
||||
import themeConfigQuery from 'gql/admin/theme/theme-query-config.gql'
|
||||
import themeSaveMutation from 'gql/admin/theme/theme-mutation-save.gql'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
themes: [
|
||||
{ text: 'Default', author: 'requarks.io', value: 'default', isInstalled: true, installDate: '', updatedAt: '' }
|
||||
],
|
||||
iconsets: [
|
||||
{ text: 'Material Design Icons (default)', value: 'mdi' },
|
||||
{ text: 'Font Awesome 5', value: 'fa' },
|
||||
{ text: 'Font Awesome 4', value: 'fa4' }
|
||||
],
|
||||
config: {
|
||||
theme: 'default',
|
||||
darkMode: false,
|
||||
iconset: '',
|
||||
injectCSS: '',
|
||||
injectHead: '',
|
||||
injectBody: ''
|
||||
},
|
||||
darkModeInitial: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
darkMode: sync('site/dark'),
|
||||
headers() {
|
||||
return [
|
||||
{
|
||||
text: this.$t('admin:theme.downloadName'),
|
||||
align: 'left',
|
||||
value: 'text'
|
||||
},
|
||||
{
|
||||
text: this.$t('admin:theme.downloadAuthor'),
|
||||
align: 'left',
|
||||
value: 'author'
|
||||
},
|
||||
{
|
||||
text: this.$t('admin:theme.downloadDownload'),
|
||||
align: 'center',
|
||||
value: 'value',
|
||||
sortable: false,
|
||||
width: 100
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'darkMode' (newValue, oldValue) {
|
||||
this.$vuetify.theme.dark = newValue
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.darkModeInitial = this.darkMode
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.darkMode = this.darkModeInitial
|
||||
this.$vuetify.theme.dark = this.darkModeInitial
|
||||
},
|
||||
methods: {
|
||||
async save () {
|
||||
this.loading = true
|
||||
this.$store.commit(`loadingStart`, 'admin-theme-save')
|
||||
try {
|
||||
const respRaw = await this.$apollo.mutate({
|
||||
mutation: themeSaveMutation,
|
||||
variables: {
|
||||
theme: this.config.theme,
|
||||
iconset: this.config.iconset,
|
||||
darkMode: this.darkMode,
|
||||
injectCSS: this.config.injectCSS,
|
||||
injectHead: this.config.injectHead,
|
||||
injectBody: this.config.injectBody
|
||||
}
|
||||
})
|
||||
const resp = _.get(respRaw, 'data.theming.setConfig.responseResult', {})
|
||||
if (resp.succeeded) {
|
||||
this.darkModeInitial = this.darkMode
|
||||
this.$store.commit('showNotification', {
|
||||
message: 'Theme settings updated successfully.',
|
||||
style: 'success',
|
||||
icon: 'check'
|
||||
})
|
||||
} else {
|
||||
throw new Error(resp.message)
|
||||
}
|
||||
} catch (err) {
|
||||
this.$store.commit('pushGraphError', err)
|
||||
}
|
||||
this.$store.commit(`loadingStop`, 'admin-theme-save')
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
apollo: {
|
||||
config: {
|
||||
query: themeConfigQuery,
|
||||
fetchPolicy: 'network-only',
|
||||
update: (data) => data.theming.config,
|
||||
watchLoading (isLoading) {
|
||||
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-theme-refresh')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang='scss'>
|
||||
.v-textarea.is-monospaced textarea {
|
||||
font-family: 'Roboto Mono', 'Courier New', Courier, monospace;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
}
|
||||
</style>
|
@ -1,256 +0,0 @@
|
||||
<template lang="pug">
|
||||
v-dialog(v-model='isShown', max-width='650', persistent)
|
||||
v-card
|
||||
.dialog-header.is-short
|
||||
v-icon.mr-3(color='white') mdi-plus
|
||||
span New User
|
||||
v-spacer
|
||||
v-btn.mx-0(color='white', outlined, disabled, dark)
|
||||
v-icon(left) mdi-database-import
|
||||
span Bulk Import
|
||||
v-card-text.pt-5
|
||||
v-select(
|
||||
:items='providers'
|
||||
item-text='displayName'
|
||||
item-value='key'
|
||||
outlined
|
||||
prepend-icon='mdi-domain'
|
||||
v-model='provider'
|
||||
label='Provider'
|
||||
)
|
||||
v-text-field(
|
||||
outlined
|
||||
prepend-icon='mdi-at'
|
||||
v-model='email'
|
||||
label='Email Address'
|
||||
key='newUserEmail'
|
||||
persistent-hint
|
||||
ref='emailInput'
|
||||
)
|
||||
v-text-field(
|
||||
v-if='provider === `local`'
|
||||
outlined
|
||||
prepend-icon='mdi-lock-outline'
|
||||
append-icon='mdi-dice-5'
|
||||
v-model='password'
|
||||
:label='mustChangePwd ? `Temporary Password` : `Password`'
|
||||
counter='255'
|
||||
@click:append='generatePwd'
|
||||
key='newUserPassword'
|
||||
persistent-hint
|
||||
)
|
||||
v-text-field(
|
||||
outlined
|
||||
prepend-icon='mdi-account-outline'
|
||||
v-model='name'
|
||||
label='Name'
|
||||
:hint='provider === `local` ? `Can be changed by the user.` : `May be overwritten by the provider during login.`'
|
||||
key='newUserName'
|
||||
persistent-hint
|
||||
)
|
||||
v-select.mt-2(
|
||||
:items='groups'
|
||||
item-text='name'
|
||||
item-value='id'
|
||||
item-disabled='isSystem'
|
||||
outlined
|
||||
prepend-icon='mdi-account-group'
|
||||
v-model='group'
|
||||
label='Assign to Group(s)...'
|
||||
hint='Note that you cannot assign users to the Administrators or Guests groups from this dialog.'
|
||||
persistent-hint
|
||||
clearable
|
||||
multiple
|
||||
)
|
||||
v-divider
|
||||
v-checkbox(
|
||||
color='primary'
|
||||
label='Require password change on first login'
|
||||
v-if='provider === `local`'
|
||||
v-model='mustChangePwd'
|
||||
hide-details
|
||||
)
|
||||
v-checkbox(
|
||||
color='primary'
|
||||
label='Send a welcome email'
|
||||
hide-details
|
||||
v-model='sendWelcomeEmail'
|
||||
disabled
|
||||
)
|
||||
v-card-chin
|
||||
v-spacer
|
||||
v-btn(text, @click='isShown = false') Cancel
|
||||
v-btn.px-3(depressed, color='primary', @click='newUser(false)')
|
||||
v-icon(left) mdi-chevron-right
|
||||
span Create
|
||||
v-btn.px-3(depressed, color='primary', @click='newUser(true)')
|
||||
v-icon(left) mdi-chevron-double-right
|
||||
span Create and Close
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import validate from 'validate.js'
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
import createUserMutation from 'gql/admin/users/users-mutation-create.gql'
|
||||
import groupsQuery from 'gql/admin/users/users-query-groups.gql'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
value: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
providers: [],
|
||||
provider: 'local',
|
||||
email: '',
|
||||
password: '',
|
||||
name: '',
|
||||
groups: [],
|
||||
group: [],
|
||||
mustChangePwd: false,
|
||||
sendWelcomeEmail: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isShown: {
|
||||
get() { return this.value },
|
||||
set(val) { this.$emit('input', val) }
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value(newValue, oldValue) {
|
||||
if (newValue) {
|
||||
this.$nextTick(() => {
|
||||
this.$refs.emailInput.focus()
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async newUser(close = false) {
|
||||
let rules = {
|
||||
email: {
|
||||
presence: {
|
||||
allowEmpty: false
|
||||
},
|
||||
email: true
|
||||
},
|
||||
name: {
|
||||
presence: {
|
||||
allowEmpty: false
|
||||
},
|
||||
length: {
|
||||
minimum: 2,
|
||||
maximum: 255
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this.provider === `local`) {
|
||||
rules.password = {
|
||||
presence: {
|
||||
allowEmpty: false
|
||||
},
|
||||
length: {
|
||||
minimum: 6,
|
||||
maximum: 255
|
||||
}
|
||||
}
|
||||
}
|
||||
const validationResults = validate({
|
||||
email: this.email,
|
||||
password: this.password,
|
||||
name: this.name
|
||||
}, rules, { format: 'flat' })
|
||||
|
||||
if (validationResults) {
|
||||
this.$store.commit('showNotification', {
|
||||
style: 'red',
|
||||
message: validationResults[0],
|
||||
icon: 'alert'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const resp = await this.$apollo.mutate({
|
||||
mutation: createUserMutation,
|
||||
variables: {
|
||||
providerKey: this.provider,
|
||||
email: this.email,
|
||||
passwordRaw: this.password,
|
||||
name: this.name,
|
||||
groups: this.group,
|
||||
mustChangePassword: this.mustChangePwd,
|
||||
sendWelcomeEmail: this.sendWelcomeEmail
|
||||
},
|
||||
watchLoading (isLoading) {
|
||||
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-users-create')
|
||||
}
|
||||
})
|
||||
if (_.get(resp, 'data.users.create.responseResult.succeeded', false)) {
|
||||
this.$store.commit('showNotification', {
|
||||
style: 'success',
|
||||
message: 'New user created successfully.',
|
||||
icon: 'check'
|
||||
})
|
||||
|
||||
this.email = ''
|
||||
this.password = ''
|
||||
this.name = ''
|
||||
|
||||
if (close) {
|
||||
this.isShown = false
|
||||
this.$emit('refresh')
|
||||
} else {
|
||||
this.$refs.emailInput.focus()
|
||||
}
|
||||
} else {
|
||||
this.$store.commit('showNotification', {
|
||||
style: 'red',
|
||||
message: _.get(resp, 'data.users.create.responseResult.message', 'An unexpected error occurred.'),
|
||||
icon: 'alert'
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
this.$store.commit('pushGraphError', err)
|
||||
}
|
||||
},
|
||||
generatePwd() {
|
||||
const pwdChars = 'abcdefghkmnpqrstuvwxyzABCDEFHJKLMNPQRSTUVWXYZ23456789_*=?#!()+'
|
||||
this.password = _.sampleSize(pwdChars, 12).join('')
|
||||
}
|
||||
},
|
||||
apollo: {
|
||||
providers: {
|
||||
query: gql`
|
||||
query {
|
||||
authentication {
|
||||
activeStrategies {
|
||||
key
|
||||
displayName
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
fetchPolicy: 'network-only',
|
||||
update: (data) => data.authentication.activeStrategies,
|
||||
watchLoading (isLoading) {
|
||||
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-users-strategies-refresh')
|
||||
}
|
||||
},
|
||||
groups: {
|
||||
query: groupsQuery,
|
||||
fetchPolicy: 'network-only',
|
||||
update: (data) => data.groups.list,
|
||||
watchLoading (isLoading) {
|
||||
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-auth-groups-refresh')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
File diff suppressed because it is too large
Load Diff
@ -1,192 +0,0 @@
|
||||
<template lang='pug'>
|
||||
v-container(fluid, grid-list-lg)
|
||||
v-layout(row, wrap)
|
||||
v-flex(xs12)
|
||||
.admin-header
|
||||
img.animated.fadeInUp(src='/_assets/svg/icon-customer.svg', alt='Users', style='width: 80px;')
|
||||
.admin-header-title
|
||||
.headline.blue--text.text--darken-2.animated.fadeInLeft Users
|
||||
.subtitle-1.grey--text.animated.fadeInLeft.wait-p2s Manage users
|
||||
v-spacer
|
||||
v-btn.animated.fadeInDown.wait-p2s.mr-3(outlined, color='grey', icon, @click='refresh')
|
||||
v-icon mdi-refresh
|
||||
v-btn.animated.fadeInDown(color='primary', large, depressed, @click='createUser')
|
||||
v-icon(left) mdi-plus
|
||||
span New User
|
||||
v-card.mt-3.animated.fadeInUp
|
||||
.pa-2.d-flex.align-center(:class='$vuetify.theme.dark ? `grey darken-3-d5` : `grey lighten-3`')
|
||||
v-text-field(
|
||||
solo
|
||||
flat
|
||||
v-model='search'
|
||||
prepend-inner-icon='mdi-account-search-outline'
|
||||
label='Search Users...'
|
||||
hide-details
|
||||
style='max-width: 400px;'
|
||||
dense
|
||||
)
|
||||
v-spacer
|
||||
v-select(
|
||||
solo
|
||||
flat
|
||||
hide-details
|
||||
label='Identity Provider'
|
||||
:items='strategies'
|
||||
v-model='filterStrategy'
|
||||
item-text='displayName'
|
||||
item-value='key'
|
||||
style='max-width: 300px;'
|
||||
dense
|
||||
)
|
||||
v-divider
|
||||
v-data-table(
|
||||
v-model='selected'
|
||||
:items='usersFiltered',
|
||||
:headers='headers',
|
||||
:search='search',
|
||||
:page.sync='pagination'
|
||||
:items-per-page='15'
|
||||
:loading='loading'
|
||||
@page-count='pageCount = $event'
|
||||
hide-default-footer
|
||||
)
|
||||
template(slot='item', slot-scope='props')
|
||||
tr.is-clickable(:active='props.selected', @click='$router.push("/users/" + props.item.id)')
|
||||
//- td
|
||||
v-checkbox(hide-details, :input-value='props.selected', color='blue darken-2', @click='props.selected = !props.selected')
|
||||
td {{ props.item.id }}
|
||||
td: strong {{ props.item.name }}
|
||||
td {{ props.item.email }}
|
||||
td {{ getStrategyName(props.item.providerKey) }}
|
||||
td {{ props.item.createdAt | moment('from') }}
|
||||
td
|
||||
span(v-if='props.item.lastLoginAt') {{ props.item.lastLoginAt | moment('from') }}
|
||||
em.grey--text(v-else) Never
|
||||
td.text-right
|
||||
v-icon.mr-3(v-if='props.item.isSystem') mdi-lock-outline
|
||||
status-indicator(positive, pulse, v-if='props.item.isActive')
|
||||
status-indicator(negative, pulse, v-else)
|
||||
template(slot='no-data')
|
||||
.pa-3
|
||||
v-alert.text-left(icon='mdi-alert', outlined, color='grey')
|
||||
em.body-2 No users to display!
|
||||
v-card-chin(v-if='pageCount > 1')
|
||||
v-spacer
|
||||
v-pagination(v-model='pagination', :length='pageCount')
|
||||
v-spacer
|
||||
|
||||
user-create(v-model='isCreateDialogShown', @refresh='refresh(false)')
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
import { StatusIndicator } from 'vue-status-indicator'
|
||||
import UserCreate from './admin-users-create.vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
StatusIndicator,
|
||||
UserCreate
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selected: [],
|
||||
pagination: 1,
|
||||
pageCount: 0,
|
||||
users: [],
|
||||
headers: [
|
||||
{ text: 'ID', value: 'id', width: 80, sortable: true },
|
||||
{ text: 'Name', value: 'name', sortable: true },
|
||||
{ text: 'Email', value: 'email', sortable: true },
|
||||
{ text: 'Provider', value: 'provider', sortable: true },
|
||||
{ text: 'Created', value: 'createdAt', sortable: true },
|
||||
{ text: 'Last Login', value: 'lastLoginAt', sortable: true },
|
||||
{ text: '', value: 'actions', sortable: false, width: 80 }
|
||||
],
|
||||
strategies: [],
|
||||
filterStrategy: 'all',
|
||||
search: '',
|
||||
loading: false,
|
||||
isCreateDialogShown: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
usersFiltered () {
|
||||
const all = this.filterStrategy === 'all' || this.filterStrategy === ''
|
||||
return _.filter(this.users, u => all || u.providerKey === this.filterStrategy)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
createUser() {
|
||||
this.isCreateDialogShown = true
|
||||
},
|
||||
async refresh(notify = true) {
|
||||
await this.$apollo.queries.users.refetch()
|
||||
if (notify) {
|
||||
this.$store.commit('showNotification', {
|
||||
message: 'Users list has been refreshed.',
|
||||
style: 'success',
|
||||
icon: 'cached'
|
||||
})
|
||||
}
|
||||
},
|
||||
getStrategyName(key) {
|
||||
return (_.find(this.strategies, ['key', key]) || {}).displayName || key
|
||||
}
|
||||
},
|
||||
apollo: {
|
||||
users: {
|
||||
query: gql`
|
||||
query {
|
||||
users {
|
||||
list {
|
||||
id
|
||||
name
|
||||
email
|
||||
providerKey
|
||||
isSystem
|
||||
isActive
|
||||
createdAt
|
||||
lastLoginAt
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
fetchPolicy: 'network-only',
|
||||
update: (data) => data.users.list,
|
||||
watchLoading (isLoading) {
|
||||
this.loading = isLoading
|
||||
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-users-refresh')
|
||||
}
|
||||
},
|
||||
strategies: {
|
||||
query: gql`
|
||||
query {
|
||||
authentication {
|
||||
activeStrategies {
|
||||
key
|
||||
displayName
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
fetchPolicy: 'network-only',
|
||||
update: (data) => {
|
||||
return _.concat({
|
||||
key: 'all',
|
||||
displayName: 'All Providers'
|
||||
}, data.authentication.activeStrategies)
|
||||
},
|
||||
watchLoading (isLoading) {
|
||||
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-users-strategies-refresh')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang='scss'>
|
||||
|
||||
</style>
|
@ -1,93 +0,0 @@
|
||||
<template lang='pug'>
|
||||
v-card
|
||||
v-toolbar(flat, color='primary', dark, dense)
|
||||
.subtitle-1 {{ $t('admin:utilities.authTitle') }}
|
||||
v-card-text
|
||||
.subtitle-1.pb-3.primary--text Generate New Authentication Public / Private Key Certificates
|
||||
.body-2 This will invalidate all current session tokens and cause all users to be logged out.
|
||||
.body-2.red--text You will need to log back in after the operation.
|
||||
v-btn(outlined, color='primary', @click='regenCerts', :disabled='loading').ml-0.mt-3
|
||||
v-icon(left) mdi-gesture-double-tap
|
||||
span Proceed
|
||||
v-divider.my-5
|
||||
.subtitle-1.pb-3.primary--text Reset Guest User
|
||||
.body-2 This will reset the guest user to its default parameters and permissions.
|
||||
v-btn(outlined, color='primary', @click='resetGuest', :disabled='loading').ml-0.mt-3
|
||||
v-icon(left) mdi-gesture-double-tap
|
||||
span Proceed
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import Cookies from 'js-cookie'
|
||||
import utilityAuthRegencertsMutation from 'gql/admin/utilities/utilities-mutation-auth-regencerts.gql'
|
||||
import utilityAuthResetguestMutation from 'gql/admin/utilities/utilities-mutation-auth-resetguest.gql'
|
||||
|
||||
export default {
|
||||
data: () => {
|
||||
return {
|
||||
loading: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async regenCerts() {
|
||||
this.loading = true
|
||||
this.$store.commit(`loadingStart`, 'admin-utilities-auth-regencerts')
|
||||
|
||||
try {
|
||||
const respRaw = await this.$apollo.mutate({
|
||||
mutation: utilityAuthRegencertsMutation
|
||||
})
|
||||
const resp = _.get(respRaw, 'data.authentication.regenerateCertificates.responseResult', {})
|
||||
if (resp.succeeded) {
|
||||
this.$store.commit('showNotification', {
|
||||
message: 'New Certificates generated successfully.',
|
||||
style: 'success',
|
||||
icon: 'check'
|
||||
})
|
||||
Cookies.remove('jwt')
|
||||
_.delay(() => {
|
||||
window.location.assign('/login')
|
||||
}, 1000)
|
||||
} else {
|
||||
throw new Error(resp.message)
|
||||
}
|
||||
} catch (err) {
|
||||
this.$store.commit('pushGraphError', err)
|
||||
}
|
||||
|
||||
this.$store.commit(`loadingStop`, 'admin-utilities-auth-regencerts')
|
||||
this.loading = false
|
||||
},
|
||||
async resetGuest() {
|
||||
this.loading = true
|
||||
this.$store.commit(`loadingStart`, 'admin-utilities-auth-resetguest')
|
||||
|
||||
try {
|
||||
const respRaw = await this.$apollo.mutate({
|
||||
mutation: utilityAuthResetguestMutation
|
||||
})
|
||||
const resp = _.get(respRaw, 'data.authentication.resetGuestUser.responseResult', {})
|
||||
if (resp.succeeded) {
|
||||
this.$store.commit('showNotification', {
|
||||
message: 'Guest user was reset successfully.',
|
||||
style: 'success',
|
||||
icon: 'check'
|
||||
})
|
||||
} else {
|
||||
throw new Error(resp.message)
|
||||
}
|
||||
} catch (err) {
|
||||
this.$store.commit('pushGraphError', err)
|
||||
}
|
||||
|
||||
this.$store.commit(`loadingStop`, 'admin-utilities-auth-resetguest')
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang='scss'>
|
||||
|
||||
</style>
|
@ -1,108 +0,0 @@
|
||||
<template lang='pug'>
|
||||
v-card
|
||||
v-toolbar(flat, color='primary', dark, dense)
|
||||
.subtitle-1 {{ $t('admin:utilities.cacheTitle') }}
|
||||
v-card-text
|
||||
.subtitle-1.pb-3.primary--text Flush Pages and Assets Cache
|
||||
.body-2 Pages and Assets are cached to disk for better performance. You can flush the cache to force all content to be fetched from the DB again.
|
||||
v-btn(outlined, color='primary', @click='flushCache', :disabled='loading').ml-0.mt-3
|
||||
v-icon(left) mdi-gesture-double-tap
|
||||
span Proceed
|
||||
v-divider.my-5
|
||||
.subtitle-1.pb-3.primary--text Flush Temporary Uploads
|
||||
.body-2 New uploads are temporarily saved to disk while they are being processed. They are automatically deleted after processing, but you can force an immediate cleanup using this tool.
|
||||
.body-2.red--text Note that performing this action while an upload is in progress can result in a failed upload.
|
||||
v-btn(outlined, color='primary', @click='flushUploads', :disabled='loading').ml-0.mt-3
|
||||
v-icon(left) mdi-gesture-double-tap
|
||||
span Proceed
|
||||
v-divider.my-5
|
||||
.subtitle-1.pb-3.primary--text Flush Client-Side Locale Cache
|
||||
.body-2 Locale strings are cached in the browser local storage for 24h. You can delete your current cache in order to fetch the latest data during the next page load.
|
||||
.body-2 Note that this affects only #[strong your own browser] and not everyone.
|
||||
v-btn(outlined, color='primary', @click='flushClientLocaleCache', :disabled='loading').ml-0.mt-3
|
||||
v-icon(left) mdi-gesture-double-tap
|
||||
span Proceed
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import utilityCacheFlushCacheMutation from 'gql/admin/utilities/utilities-mutation-cache-flushcache.gql'
|
||||
import utilityCacheFlushUploadsMutation from 'gql/admin/utilities/utilities-mutation-cache-flushuploads.gql'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
loading: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async flushCache() {
|
||||
this.loading = true
|
||||
this.$store.commit(`loadingStart`, 'admin-utilities-cache-flushCache')
|
||||
|
||||
try {
|
||||
const respRaw = await this.$apollo.mutate({
|
||||
mutation: utilityCacheFlushCacheMutation
|
||||
})
|
||||
const resp = _.get(respRaw, 'data.pages.flushCache.responseResult', {})
|
||||
if (resp.succeeded) {
|
||||
this.$store.commit('showNotification', {
|
||||
message: 'Cache flushed successfully.',
|
||||
style: 'success',
|
||||
icon: 'check'
|
||||
})
|
||||
} else {
|
||||
throw new Error(resp.message)
|
||||
}
|
||||
} catch (err) {
|
||||
this.$store.commit('pushGraphError', err)
|
||||
}
|
||||
|
||||
this.$store.commit(`loadingStop`, 'admin-utilities-cache-flushCache')
|
||||
this.loading = false
|
||||
},
|
||||
async flushUploads() {
|
||||
this.loading = true
|
||||
this.$store.commit(`loadingStart`, 'admin-utilities-cache-flushUploads')
|
||||
|
||||
try {
|
||||
const respRaw = await this.$apollo.mutate({
|
||||
mutation: utilityCacheFlushUploadsMutation
|
||||
})
|
||||
const resp = _.get(respRaw, 'data.assets.flushTempUploads.responseResult', {})
|
||||
if (resp.succeeded) {
|
||||
this.$store.commit('showNotification', {
|
||||
message: 'Temporary Uploads flushed successfully.',
|
||||
style: 'success',
|
||||
icon: 'check'
|
||||
})
|
||||
} else {
|
||||
throw new Error(resp.message)
|
||||
}
|
||||
} catch (err) {
|
||||
this.$store.commit('pushGraphError', err)
|
||||
}
|
||||
|
||||
this.$store.commit(`loadingStop`, 'admin-utilities-cache-flushUploads')
|
||||
this.loading = false
|
||||
},
|
||||
async flushClientLocaleCache () {
|
||||
for (let i = 0; i < window.localStorage.length; i++) {
|
||||
const lsKey = window.localStorage.key(i)
|
||||
if (_.startsWith(lsKey, 'i18next_res')) {
|
||||
window.localStorage.removeItem(lsKey)
|
||||
}
|
||||
}
|
||||
this.$store.commit('showNotification', {
|
||||
message: 'Locale Client-Side Cache flushed successfully.',
|
||||
style: 'success',
|
||||
icon: 'check'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang='scss'>
|
||||
|
||||
</style>
|
@ -1,318 +0,0 @@
|
||||
<template lang='pug'>
|
||||
v-card
|
||||
v-toolbar(flat, color='primary', dark, dense)
|
||||
.subtitle-1 {{ $t('admin:utilities.contentTitle') }}
|
||||
v-card-text
|
||||
.subtitle-1.pb-3.primary--text Rebuild Page Tree
|
||||
.body-2 The virtual structure of your wiki is automatically inferred from all page paths. You can trigger a full rebuild of the tree if some virtual folders are missing or not valid anymore.
|
||||
v-btn(outlined, color='primary', @click='rebuildTree', :disabled='loading').ml-0.mt-3
|
||||
v-icon(left) mdi-gesture-double-tap
|
||||
span Proceed
|
||||
|
||||
v-divider.my-5
|
||||
|
||||
.subtitle-1.pb-3.primary--text Rerender All Pages
|
||||
.body-2 All pages will be rendered again. Useful if internal links are broken or the rendering pipeline has changed.
|
||||
v-btn(outlined, color='primary', @click='rerenderPages', :disabled='loading', :loading='isRerendering').ml-0.mt-3
|
||||
v-icon(left) mdi-gesture-double-tap
|
||||
span Proceed
|
||||
v-dialog(
|
||||
v-model='isRerendering'
|
||||
persistent
|
||||
max-width='450'
|
||||
)
|
||||
v-card(color='blue darken-2', dark)
|
||||
v-card-text.pa-10.text-center
|
||||
semipolar-spinner.animated.fadeIn(
|
||||
:animation-duration='1500'
|
||||
:size='65'
|
||||
color='#FFF'
|
||||
style='margin: 0 auto;'
|
||||
)
|
||||
.mt-5.body-1.white--text Rendering all pages...
|
||||
.caption(v-if='renderIndex > 0') Rendering {{renderCurrentPath}}... ({{renderIndex}}/{{renderTotal}}, {{renderProgress}}%)
|
||||
.caption.mt-4 Do not leave this page.
|
||||
v-progress-linear.mt-5(
|
||||
color='white'
|
||||
:value='renderProgress'
|
||||
stream
|
||||
rounded
|
||||
:buffer-value='0'
|
||||
)
|
||||
|
||||
v-divider.my-5
|
||||
|
||||
.subtitle-1.pb-3.pl-0.primary--text Migrate all pages to target locale
|
||||
.body-2 If you created content before selecting a different locale and activating the namespacing capabilities, you may want to transfer all content to the base locale.
|
||||
.body-2.red--text: strong This operation is destructive and cannot be reversed! Make sure you have proper backups!
|
||||
v-toolbar.radius-7.mt-5(flat, :color='$vuetify.theme.dark ? `grey darken-3-d5` : `grey lighten-4`', height='80')
|
||||
v-select(
|
||||
label='Source Locale'
|
||||
outlined
|
||||
hide-details
|
||||
:items='locales'
|
||||
item-text='name'
|
||||
item-value='code'
|
||||
v-model='sourceLocale'
|
||||
)
|
||||
v-icon.mx-3(large) mdi-chevron-right-box-outline
|
||||
v-select(
|
||||
label='Target Locale'
|
||||
outlined
|
||||
hide-details
|
||||
:items='locales'
|
||||
item-text='name'
|
||||
item-value='code'
|
||||
v-model='targetLocale'
|
||||
)
|
||||
.body-2.mt-5 Pages that are already in the target locale will not be touched. If a page already exists at the target, the source page will not be modified as it would create a conflict. If you want to overwrite the target page, you must first delete it.
|
||||
v-btn(outlined, color='primary', @click='migrateToLocale', :disabled='loading').ml-0.mt-3
|
||||
v-icon(left) mdi-gesture-double-tap
|
||||
span Proceed
|
||||
|
||||
v-divider.my-5
|
||||
|
||||
.subtitle-1.pb-3.pl-0.primary--text Purge Page History
|
||||
.body-2 You may want to purge old history for pages to reduce database usage.
|
||||
.body-2 This operation only affects the database and not any history saved by a storage module (e.g. git version history)
|
||||
v-toolbar.radius-7.mt-5(flat, :color='$vuetify.theme.dark ? `grey darken-3-d5` : `grey lighten-4`', height='80')
|
||||
v-select(
|
||||
label='Delete history older than...'
|
||||
outlined
|
||||
hide-details
|
||||
:items='purgeHistoryOptions'
|
||||
item-text='title'
|
||||
item-value='key'
|
||||
v-model='purgeHistorySelection'
|
||||
)
|
||||
v-btn(outlined, color='primary', @click='purgeHistory', :disabled='loading').ml-0.mt-3
|
||||
v-icon(left) mdi-gesture-double-tap
|
||||
span Proceed
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import gql from 'graphql-tag'
|
||||
import utilityContentMigrateLocaleMutation from 'gql/admin/utilities/utilities-mutation-content-migratelocale.gql'
|
||||
import utilityContentRebuildTreeMutation from 'gql/admin/utilities/utilities-mutation-content-rebuildtree.gql'
|
||||
|
||||
import { SemipolarSpinner } from 'epic-spinners'
|
||||
|
||||
/* global siteLangs, siteConfig */
|
||||
|
||||
export default {
|
||||
components: {
|
||||
SemipolarSpinner
|
||||
},
|
||||
data: () => {
|
||||
return {
|
||||
isRerendering: false,
|
||||
loading: false,
|
||||
renderProgress: 0,
|
||||
renderIndex: 0,
|
||||
renderTotal: 0,
|
||||
renderCurrentPath: '',
|
||||
sourceLocale: '',
|
||||
targetLocale: '',
|
||||
purgeHistorySelection: 'P1Y',
|
||||
purgeHistoryOptions: [
|
||||
{ key: 'P1D', title: 'Today' },
|
||||
{ key: 'P1M', title: '1 month' },
|
||||
{ key: 'P3M', title: '3 months' },
|
||||
{ key: 'P6M', title: '6 months' },
|
||||
{ key: 'P1Y', title: '1 year' },
|
||||
{ key: 'P2Y', title: '2 years' },
|
||||
{ key: 'P3Y', title: '3 years' },
|
||||
{ key: 'P5Y', title: '5 years' }
|
||||
]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
currentLocale () {
|
||||
return siteConfig.lang
|
||||
},
|
||||
locales () {
|
||||
return siteLangs
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async rebuildTree () {
|
||||
this.loading = true
|
||||
this.$store.commit(`loadingStart`, 'admin-utilities-content-rebuildtree')
|
||||
|
||||
try {
|
||||
const respRaw = await this.$apollo.mutate({
|
||||
mutation: utilityContentRebuildTreeMutation
|
||||
})
|
||||
const resp = _.get(respRaw, 'data.pages.rebuildTree.responseResult', {})
|
||||
if (resp.succeeded) {
|
||||
this.$store.commit('showNotification', {
|
||||
message: 'Page Tree rebuilt successfully.',
|
||||
style: 'success',
|
||||
icon: 'check'
|
||||
})
|
||||
} else {
|
||||
throw new Error(resp.message)
|
||||
}
|
||||
} catch (err) {
|
||||
this.$store.commit('pushGraphError', err)
|
||||
}
|
||||
|
||||
this.$store.commit(`loadingStop`, 'admin-utilities-content-rebuildtree')
|
||||
this.loading = false
|
||||
},
|
||||
async rerenderPages () {
|
||||
this.loading = true
|
||||
this.isRerendering = true
|
||||
this.$store.commit(`loadingStart`, 'admin-utilities-content-rerender')
|
||||
|
||||
try {
|
||||
const pagesRaw = await this.$apollo.query({
|
||||
query: gql`
|
||||
{
|
||||
pages {
|
||||
list {
|
||||
id
|
||||
path
|
||||
locale
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
fetchPolicy: 'network-only'
|
||||
})
|
||||
if (_.get(pagesRaw, 'data.pages.list', []).length < 1) {
|
||||
throw new Error('Could not find any page to render!')
|
||||
}
|
||||
|
||||
this.renderIndex = 0
|
||||
this.renderTotal = pagesRaw.data.pages.list.length
|
||||
let failed = 0
|
||||
for (const page of pagesRaw.data.pages.list) {
|
||||
this.renderCurrentPath = `${page.locale}/${page.path}`
|
||||
this.renderIndex++
|
||||
this.renderProgress = Math.round(this.renderIndex / this.renderTotal * 100)
|
||||
const respRaw = await this.$apollo.mutate({
|
||||
mutation: gql`
|
||||
mutation($id: Int!) {
|
||||
pages {
|
||||
render(id: $id) {
|
||||
responseResult {
|
||||
succeeded
|
||||
errorCode
|
||||
slug
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
id: page.id
|
||||
}
|
||||
})
|
||||
const resp = _.get(respRaw, 'data.pages.render.responseResult', {})
|
||||
if (!resp.succeeded) {
|
||||
failed++
|
||||
}
|
||||
}
|
||||
if (failed > 0) {
|
||||
this.$store.commit('showNotification', {
|
||||
message: `Completed with ${failed} pages that failed to render. Check server logs for details.`,
|
||||
style: 'error',
|
||||
icon: 'alert'
|
||||
})
|
||||
} else {
|
||||
this.$store.commit('showNotification', {
|
||||
message: 'All pages have been rendered successfully.',
|
||||
style: 'success',
|
||||
icon: 'check'
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
this.$store.commit('pushGraphError', err)
|
||||
}
|
||||
|
||||
this.$store.commit(`loadingStop`, 'admin-utilities-content-rerender')
|
||||
this.isRerendering = false
|
||||
this.loading = false
|
||||
},
|
||||
async migrateToLocale () {
|
||||
this.loading = true
|
||||
this.$store.commit(`loadingStart`, 'admin-utilities-content-migratelocale')
|
||||
|
||||
try {
|
||||
const respRaw = await this.$apollo.mutate({
|
||||
mutation: utilityContentMigrateLocaleMutation,
|
||||
variables: {
|
||||
sourceLocale: this.sourceLocale,
|
||||
targetLocale: this.targetLocale
|
||||
}
|
||||
})
|
||||
const resp = _.get(respRaw, 'data.pages.migrateToLocale.responseResult', {})
|
||||
if (resp.succeeded) {
|
||||
this.$store.commit('showNotification', {
|
||||
message: `Migrated ${_.get(respRaw, 'data.pages.migrateToLocale.count', 0)} page(s) to target locale successfully.`,
|
||||
style: 'success',
|
||||
icon: 'check'
|
||||
})
|
||||
} else {
|
||||
throw new Error(resp.message)
|
||||
}
|
||||
} catch (err) {
|
||||
this.$store.commit('pushGraphError', err)
|
||||
}
|
||||
|
||||
this.$store.commit(`loadingStop`, 'admin-utilities-content-migratelocale')
|
||||
this.loading = false
|
||||
},
|
||||
async purgeHistory () {
|
||||
this.loading = true
|
||||
this.$store.commit(`loadingStart`, 'admin-utilities-content-purgehistory')
|
||||
|
||||
try {
|
||||
const respRaw = await this.$apollo.mutate({
|
||||
mutation: gql`
|
||||
mutation ($olderThan: String!) {
|
||||
pages {
|
||||
purgeHistory (
|
||||
olderThan: $olderThan
|
||||
) {
|
||||
responseResult {
|
||||
errorCode
|
||||
message
|
||||
slug
|
||||
succeeded
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
olderThan: this.purgeHistorySelection
|
||||
}
|
||||
})
|
||||
const resp = _.get(respRaw, 'data.pages.purgeHistory.responseResult', {})
|
||||
if (resp.succeeded) {
|
||||
this.$store.commit('showNotification', {
|
||||
message: `Purged history successfully.`,
|
||||
style: 'success',
|
||||
icon: 'check'
|
||||
})
|
||||
} else {
|
||||
throw new Error(resp.message)
|
||||
}
|
||||
} catch (err) {
|
||||
this.$store.commit('pushGraphError', err)
|
||||
}
|
||||
|
||||
this.$store.commit(`loadingStop`, 'admin-utilities-content-purgehistory')
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang='scss'>
|
||||
|
||||
</style>
|
@ -1,507 +0,0 @@
|
||||
<template lang='pug'>
|
||||
v-card
|
||||
v-toolbar(flat, color='primary', dark, dense)
|
||||
.subtitle-1 {{ $t('admin:utilities.importv1Title') }}
|
||||
v-card-text
|
||||
.text-center
|
||||
img.animated.fadeInUp.wait-p1s(src='/_assets/svg/icon-software.svg')
|
||||
.body-2 Import from Wiki.js 1.x
|
||||
v-divider.my-4
|
||||
.body-2 Data from a Wiki.js 1.x installation can easily be imported using this tool. What do you want to import?
|
||||
v-checkbox(
|
||||
label='Content + Uploads'
|
||||
value='content'
|
||||
color='deep-orange darken-2'
|
||||
v-model='importFilters'
|
||||
hide-details
|
||||
)
|
||||
template(v-slot:label)
|
||||
strong.deep-orange--text.text--darken-2 Content + Uploads
|
||||
.pl-8(v-if='wantContent')
|
||||
v-radio-group(v-model='contentMode', hide-details)
|
||||
v-radio(
|
||||
value='git'
|
||||
color='primary'
|
||||
)
|
||||
template(v-slot:label)
|
||||
div
|
||||
span Import from Git Connection
|
||||
.caption: em #[strong.primary--text Recommended] | The Git storage module will also be configured for you.
|
||||
.pl-8.mt-5(v-if='needGit')
|
||||
v-row
|
||||
v-col(cols='8')
|
||||
v-select(
|
||||
label='Authentication Mode'
|
||||
:items='gitAuthModes'
|
||||
v-model='gitAuthMode'
|
||||
outlined
|
||||
hide-details
|
||||
)
|
||||
v-col(cols='4')
|
||||
v-switch(
|
||||
label='Verify SSL Certificate'
|
||||
v-model='gitVerifySSL'
|
||||
hide-details
|
||||
color='primary'
|
||||
)
|
||||
v-col(cols='8')
|
||||
v-text-field(
|
||||
outlined
|
||||
label='Repository URL'
|
||||
:placeholder='(gitAuthMode === `ssh`) ? `e.g. git@github.com:orgname/repo.git` : `e.g. https://github.com/orgname/repo.git`'
|
||||
hide-details
|
||||
v-model='gitRepoUrl'
|
||||
)
|
||||
v-col(cols='4')
|
||||
v-text-field(
|
||||
label='Branch'
|
||||
placeholder='e.g. master'
|
||||
v-model='gitRepoBranch'
|
||||
outlined
|
||||
hide-details
|
||||
)
|
||||
v-col(v-if='gitAuthMode === `ssh`', cols='12')
|
||||
v-textarea(
|
||||
outlined
|
||||
label='Private Key Contents'
|
||||
placeholder='-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----'
|
||||
hide-details
|
||||
v-model='gitPrivKey'
|
||||
)
|
||||
template(v-else-if='gitAuthMode === `basic`')
|
||||
v-col(cols='6')
|
||||
v-text-field(
|
||||
label='Username'
|
||||
v-model='gitUsername'
|
||||
outlined
|
||||
hide-details
|
||||
)
|
||||
v-col(cols='6')
|
||||
v-text-field(
|
||||
type='password'
|
||||
label='Password / PAT'
|
||||
v-model='gitPassword'
|
||||
outlined
|
||||
hide-details
|
||||
)
|
||||
v-col(cols='6')
|
||||
v-text-field(
|
||||
label='Default Author Email'
|
||||
placeholder='e.g. name@company.com'
|
||||
v-model='gitUserEmail'
|
||||
outlined
|
||||
hide-details
|
||||
)
|
||||
v-col(cols='6')
|
||||
v-text-field(
|
||||
label='Default Author Name'
|
||||
placeholder='e.g. John Smith'
|
||||
v-model='gitUserName'
|
||||
outlined
|
||||
hide-details
|
||||
)
|
||||
v-col(cols='12')
|
||||
v-text-field(
|
||||
label='Local Repository Path'
|
||||
placeholder='e.g. ./data/repo'
|
||||
v-model='gitRepoPath'
|
||||
outlined
|
||||
hide-details
|
||||
)
|
||||
.caption.mt-2 This folder should be empty or not exist yet. #[strong.deep-orange--text.text--darken-2 DO NOT] point to your existing Wiki.js 1.x repository folder. In most cases, it should be left to the default value.
|
||||
v-alert(color='deep-orange', outlined, icon='mdi-alert', prominent)
|
||||
.body-2 - Note that if you already configured the git storage module, its configuration will be replaced with the above.
|
||||
.body-2 - Although both v1 and v2 installations can use the same remote git repository, you shouldn't make edits to the same pages simultaneously.
|
||||
v-radio-group(v-model='contentMode', hide-details)
|
||||
v-divider
|
||||
v-radio.mt-3(
|
||||
value='disk'
|
||||
color='primary'
|
||||
)
|
||||
template(v-slot:label)
|
||||
div
|
||||
span Import from local folder
|
||||
.caption: em Choose this option only if you didn't have git configured in your Wiki.js 1.x installation.
|
||||
.pl-8.mt-5(v-if='needDisk')
|
||||
v-text-field(
|
||||
outlined
|
||||
label='Content Repo Path'
|
||||
hint='The absolute path to where the Wiki.js 1.x content is stored on disk.'
|
||||
persistent-hint
|
||||
v-model='contentPath'
|
||||
)
|
||||
|
||||
v-checkbox(
|
||||
label='Users'
|
||||
value='users'
|
||||
color='deep-orange darken-2'
|
||||
v-model='importFilters'
|
||||
hide-details
|
||||
)
|
||||
template(v-slot:label)
|
||||
strong.deep-orange--text.text--darken-2 Users
|
||||
.pl-8.mt-5(v-if='wantUsers')
|
||||
v-text-field(
|
||||
outlined
|
||||
label='MongoDB Connection String'
|
||||
hint='The connection string to connect to the Wiki.js 1.x MongoDB database.'
|
||||
persistent-hint
|
||||
v-model='dbConnStr'
|
||||
)
|
||||
v-radio-group(v-model='groupMode', hide-details, mandatory)
|
||||
v-radio(
|
||||
value='MULTI'
|
||||
color='primary'
|
||||
)
|
||||
template(v-slot:label)
|
||||
div
|
||||
span Create groups for each unique user permissions configuration
|
||||
.caption: em #[strong.primary--text Recommended] | Users having identical permission sets will be assigned to the same group. Note that this can potentially result in a large amount of groups being created.
|
||||
v-divider
|
||||
v-radio.mt-3(
|
||||
value='SINGLE'
|
||||
color='primary'
|
||||
)
|
||||
template(v-slot:label)
|
||||
div
|
||||
span Create a single group with all imported users
|
||||
.caption: em The new group will have read permissions enabled by default.
|
||||
v-divider
|
||||
v-radio.mt-3(
|
||||
value='NONE'
|
||||
color='primary'
|
||||
)
|
||||
template(v-slot:label)
|
||||
div
|
||||
span Don't create any group
|
||||
.caption: em Users will not be able to access your wiki until they are assigned to a group.
|
||||
|
||||
v-alert.mt-5(color='deep-orange', outlined, icon='mdi-alert', prominent)
|
||||
.body-2 Note that any user that already exists in this installation will not be imported. A list of skipped users will be displayed upon completion.
|
||||
.caption.grey--text You must first delete from this installation any user you want to migrate over from the old installation.
|
||||
|
||||
v-card-chin
|
||||
v-btn.px-3(depressed, color='deep-orange darken-2', :disabled='!wantUsers && !wantContent', @click='startImport').ml-0
|
||||
v-icon(left, color='white') mdi-database-import
|
||||
span.white--text Start Import
|
||||
v-dialog(
|
||||
v-model='isLoading'
|
||||
persistent
|
||||
max-width='350'
|
||||
)
|
||||
v-card(color='deep-orange darken-2', dark)
|
||||
v-card-text.pa-10.text-center
|
||||
semipolar-spinner.animated.fadeIn(
|
||||
:animation-duration='1500'
|
||||
:size='65'
|
||||
color='#FFF'
|
||||
style='margin: 0 auto;'
|
||||
)
|
||||
.mt-5.body-1.white--text Importing from Wiki.js 1.x...
|
||||
.caption Please wait
|
||||
v-progress-linear.mt-5(
|
||||
color='white'
|
||||
:value='progress'
|
||||
stream
|
||||
rounded
|
||||
:buffer-value='0'
|
||||
)
|
||||
v-dialog(
|
||||
v-model='isSuccess'
|
||||
persistent
|
||||
max-width='350'
|
||||
)
|
||||
v-card(color='green darken-2', dark)
|
||||
v-card-text.pa-10.text-center
|
||||
v-icon(size='60') mdi-check-circle-outline
|
||||
.my-5.body-1.white--text Import completed
|
||||
template(v-if='wantUsers')
|
||||
.body-2
|
||||
span #[strong {{successUsers}}] users imported
|
||||
v-btn.text-none.ml-3(
|
||||
v-if='failedUsers.length > 0'
|
||||
text
|
||||
color='white'
|
||||
dark
|
||||
@click='showFailedUsers = true'
|
||||
)
|
||||
v-icon(left) mdi-alert
|
||||
span {{failedUsers.length}} failed
|
||||
.body-2 #[strong {{successGroups}}] groups created
|
||||
v-card-actions.green.darken-1
|
||||
v-spacer
|
||||
v-btn.px-5(
|
||||
color='white'
|
||||
outlined
|
||||
@click='isSuccess = false'
|
||||
) Close
|
||||
v-spacer
|
||||
v-dialog(
|
||||
v-model='showFailedUsers'
|
||||
persistent
|
||||
max-width='800'
|
||||
)
|
||||
v-card(color='red darken-2', dark)
|
||||
v-toolbar(color='red darken-2', dense)
|
||||
v-icon mdi-alert
|
||||
.body-2.pl-3 Failed User Imports
|
||||
v-spacer
|
||||
v-btn.px-5(
|
||||
color='white'
|
||||
text
|
||||
@click='showFailedUsers = false'
|
||||
) Close
|
||||
v-simple-table(dense, fixed-header, height='300px')
|
||||
template(v-slot:default)
|
||||
thead
|
||||
tr
|
||||
th Provider
|
||||
th Email
|
||||
th Error
|
||||
tbody
|
||||
tr(v-for='(fusr, idx) in failedUsers', :key='`fusr-` + idx')
|
||||
td {{fusr.provider}}
|
||||
td {{fusr.email}}
|
||||
td {{fusr.error}}
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
|
||||
import { SemipolarSpinner } from 'epic-spinners'
|
||||
|
||||
import utilityImportv1UsersMutation from 'gql/admin/utilities/utilities-mutation-importv1-users.gql'
|
||||
import storageTargetsQuery from 'gql/admin/storage/storage-query-targets.gql'
|
||||
import storageStatusQuery from 'gql/admin/storage/storage-query-status.gql'
|
||||
import targetExecuteActionMutation from 'gql/admin/storage/storage-mutation-executeaction.gql'
|
||||
import targetsSaveMutation from 'gql/admin/storage/storage-mutation-save-targets.gql'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
SemipolarSpinner
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
importFilters: ['content', 'users'],
|
||||
groupMode: 'MULTI',
|
||||
contentMode: 'git',
|
||||
dbConnStr: 'mongodb://',
|
||||
contentPath: '/wiki-v1/repo',
|
||||
isLoading: false,
|
||||
isSuccess: false,
|
||||
gitAuthMode: 'ssh',
|
||||
gitAuthModes: [
|
||||
{ text: 'SSH', value: 'ssh' },
|
||||
{ text: 'Basic', value: 'basic' }
|
||||
],
|
||||
gitVerifySSL: true,
|
||||
gitRepoUrl: '',
|
||||
gitRepoBranch: 'master',
|
||||
gitPrivKey: '',
|
||||
gitUsername: '',
|
||||
gitPassword: '',
|
||||
gitUserEmail: '',
|
||||
gitUserName: '',
|
||||
gitRepoPath: './data/repo',
|
||||
progress: 0,
|
||||
successGroups: 0,
|
||||
successUsers: 0,
|
||||
successPages: 0,
|
||||
showFailedUsers: false,
|
||||
failedUsers: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
wantContent () {
|
||||
return this.importFilters.indexOf('content') >= 0
|
||||
},
|
||||
wantUsers () {
|
||||
return this.importFilters.indexOf('users') >= 0
|
||||
},
|
||||
needDisk () {
|
||||
return this.contentMode === `disk`
|
||||
},
|
||||
needGit () {
|
||||
return this.contentMode === `git`
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async startImport () {
|
||||
this.isLoading = true
|
||||
this.progress = 0
|
||||
this.failedUsers = []
|
||||
|
||||
_.delay(async () => {
|
||||
// -> Import Users
|
||||
|
||||
if (this.wantUsers) {
|
||||
try {
|
||||
const resp = await this.$apollo.mutate({
|
||||
mutation: utilityImportv1UsersMutation,
|
||||
variables: {
|
||||
mongoDbConnString: this.dbConnStr,
|
||||
groupMode: this.groupMode
|
||||
}
|
||||
})
|
||||
const respObj = _.get(resp, 'data.system.importUsersFromV1', {})
|
||||
if (!_.get(respObj, 'responseResult.succeeded', false)) {
|
||||
throw new Error(_.get(respObj, 'responseResult.message', 'An unexpected error occurred'))
|
||||
}
|
||||
this.successUsers = _.get(respObj, 'usersCount', 0)
|
||||
this.successGroups = _.get(respObj, 'groupsCount', 0)
|
||||
this.failedUsers = _.get(respObj, 'failed', [])
|
||||
this.progress += 50
|
||||
} catch (err) {
|
||||
this.$store.commit('pushGraphError', err)
|
||||
this.isLoading = false
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// -> Import Content
|
||||
|
||||
if (this.wantContent) {
|
||||
try {
|
||||
const resp = await this.$apollo.query({
|
||||
query: storageTargetsQuery,
|
||||
fetchPolicy: 'network-only'
|
||||
})
|
||||
if (_.has(resp, 'data.storage.targets')) {
|
||||
this.progress += 10
|
||||
let targets = resp.data.storage.targets.map(str => {
|
||||
let nStr = {
|
||||
...str,
|
||||
config: _.sortBy(str.config.map(cfg => ({
|
||||
...cfg,
|
||||
value: JSON.parse(cfg.value)
|
||||
})), [t => t.value.order])
|
||||
}
|
||||
|
||||
// -> Setup Git Module
|
||||
|
||||
if (this.contentMode === 'git' && nStr.key === 'git') {
|
||||
nStr.isEnabled = true
|
||||
nStr.mode = 'sync'
|
||||
nStr.syncInterval = 'PT5M'
|
||||
nStr.config = [
|
||||
{ key: 'authType', value: { value: this.gitAuthMode } },
|
||||
{ key: 'repoUrl', value: { value: this.gitRepoUrl } },
|
||||
{ key: 'branch', value: { value: this.gitRepoBranch } },
|
||||
{ key: 'sshPrivateKeyMode', value: { value: 'contents' } },
|
||||
{ key: 'sshPrivateKeyPath', value: { value: '' } },
|
||||
{ key: 'sshPrivateKeyContent', value: { value: this.gitPrivKey } },
|
||||
{ key: 'verifySSL', value: { value: this.gitVerifySSL } },
|
||||
{ key: 'basicUsername', value: { value: this.gitUsername } },
|
||||
{ key: 'basicPassword', value: { value: this.gitPassword } },
|
||||
{ key: 'defaultEmail', value: { value: this.gitUserEmail } },
|
||||
{ key: 'defaultName', value: { value: this.gitUserName } },
|
||||
{ key: 'localRepoPath', value: { value: this.gitRepoPath } },
|
||||
{ key: 'gitBinaryPath', value: { value: '' } }
|
||||
]
|
||||
}
|
||||
|
||||
// -> Setup Disk Module
|
||||
if (this.contentMode === 'disk' && nStr.key === 'disk') {
|
||||
nStr.isEnabled = true
|
||||
nStr.mode = 'push'
|
||||
nStr.syncInterval = 'P0D'
|
||||
nStr.config = [
|
||||
{ key: 'path', value: { value: this.contentPath } },
|
||||
{ key: 'createDailyBackups', value: { value: false } }
|
||||
]
|
||||
}
|
||||
return nStr
|
||||
})
|
||||
|
||||
// -> Save storage modules configuration
|
||||
|
||||
const respSv = await this.$apollo.mutate({
|
||||
mutation: targetsSaveMutation,
|
||||
variables: {
|
||||
targets: targets.map(tgt => _.pick(tgt, [
|
||||
'isEnabled',
|
||||
'key',
|
||||
'config',
|
||||
'mode',
|
||||
'syncInterval'
|
||||
])).map(str => ({...str, config: str.config.map(cfg => ({...cfg, value: JSON.stringify({ v: cfg.value.value })}))}))
|
||||
}
|
||||
})
|
||||
const respObj = _.get(respSv, 'data.storage.updateTargets', {})
|
||||
if (!_.get(respObj, 'responseResult.succeeded', false)) {
|
||||
throw new Error(_.get(respObj, 'responseResult.message', 'An unexpected error occurred'))
|
||||
}
|
||||
|
||||
this.progress += 10
|
||||
|
||||
// -> Wait for success sync
|
||||
|
||||
let statusAttempts = 0
|
||||
while (statusAttempts < 10) {
|
||||
statusAttempts++
|
||||
const respStatus = await this.$apollo.query({
|
||||
query: storageStatusQuery,
|
||||
fetchPolicy: 'network-only'
|
||||
})
|
||||
if (_.has(respStatus, 'data.storage.status[0]')) {
|
||||
const st = _.find(respStatus.data.storage.status, ['key', this.contentMode])
|
||||
if (!st) {
|
||||
throw new Error('Storage target could not be configured.')
|
||||
}
|
||||
switch (st.status) {
|
||||
case 'pending':
|
||||
if (statusAttempts >= 10) {
|
||||
throw new Error('Storage target is stuck in pending state. Try again.')
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
case 'operational':
|
||||
statusAttempts = 10
|
||||
break
|
||||
case 'error':
|
||||
throw new Error(st.message)
|
||||
}
|
||||
} else {
|
||||
throw new Error('Failed to fetch storage sync status.')
|
||||
}
|
||||
}
|
||||
|
||||
this.progress += 15
|
||||
|
||||
// -> Perform import all
|
||||
|
||||
const respImport = await this.$apollo.mutate({
|
||||
mutation: targetExecuteActionMutation,
|
||||
variables: {
|
||||
targetKey: this.contentMode,
|
||||
handler: 'importAll'
|
||||
}
|
||||
})
|
||||
|
||||
const respImportObj = _.get(respImport, 'data.storage.executeAction', {})
|
||||
if (!_.get(respImportObj, 'responseResult.succeeded', false)) {
|
||||
throw new Error(_.get(respImportObj, 'responseResult.message', 'An unexpected error occurred'))
|
||||
}
|
||||
|
||||
this.progress += 15
|
||||
} else {
|
||||
throw new Error('Failed to fetch storage targets.')
|
||||
}
|
||||
} catch (err) {
|
||||
this.$store.commit('pushGraphError', err)
|
||||
this.isLoading = false
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
this.isLoading = false
|
||||
this.isSuccess = true
|
||||
}, 1500)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang='scss'>
|
||||
|
||||
</style>
|
@ -1,162 +0,0 @@
|
||||
<template lang='pug'>
|
||||
v-card
|
||||
v-toolbar(flat, color='primary', dark, dense)
|
||||
.subtitle-1 {{ $t('admin:utilities.telemetryTitle') }}
|
||||
v-form
|
||||
v-card-text
|
||||
.subtitle-2 What is telemetry?
|
||||
.body-2.mt-3 Telemetry allows the developers of Wiki.js to improve the software by collecting basic anonymized data about its usage and the host info. #[br] This is entirely optional and #[strong absolutely no] private data (such as content or personal data) is collected.
|
||||
.body-2.mt-3 For maximum privacy, a random client ID is generated during setup. This ID is used to group requests together while keeping complete anonymity. You can reset and generate a new one below at any time.
|
||||
v-divider.my-4
|
||||
.subtitle-2 What is collected?
|
||||
.body-2.mt-3 When telemetry is enabled, only the following data is transmitted:
|
||||
v-list
|
||||
v-list-item
|
||||
v-list-item-avatar: v-icon mdi-information-outline
|
||||
v-list-item-content
|
||||
v-list-item-title.body-2 Version of Wiki.js installed
|
||||
v-list-item-subtitle.caption: em e.g. v2.0.123
|
||||
v-list-item
|
||||
v-list-item-avatar: v-icon mdi-information-outline
|
||||
v-list-item-content
|
||||
v-list-item-title.body-2 Basic OS information
|
||||
v-list-item-subtitle.caption: em Platform (Linux, macOS or Windows), Total CPU cores and DB type (PostgreSQL, MySQL, MariaDB, SQLite or SQL Server)
|
||||
v-list-item
|
||||
v-list-item-avatar: v-icon mdi-information-outline
|
||||
v-list-item-content
|
||||
v-list-item-title.body-2 Crash debug data
|
||||
v-list-item-subtitle.caption: em Stack trace of the error
|
||||
v-list-item
|
||||
v-list-item-avatar: v-icon mdi-information-outline
|
||||
v-list-item-content
|
||||
v-list-item-title.body-2 Setup analytics
|
||||
v-list-item-subtitle.caption: em Installation checkpoint reached
|
||||
.body-2 Note that crash debug data is stored for a maximum of 30 days while analytics are stored for a maximum of 16 months, after which it is permanently deleted.
|
||||
v-divider.my-4
|
||||
.subtitle-2 What is it used for?
|
||||
.body-2.mt-3 Telemetry is used by developers to improve Wiki.js, mostly for the following reasons:
|
||||
v-list(dense)
|
||||
v-list-item
|
||||
v-list-item-avatar: v-icon mdi-chevron-right
|
||||
v-list-item-content: v-list-item-title: .body-2 Identify critical bugs more easily and fix them in a timely manner.
|
||||
v-list-item
|
||||
v-list-item-avatar: v-icon mdi-chevron-right
|
||||
v-list-item-content: v-list-item-title: .body-2 Understand the upgrade rate of current installations.
|
||||
v-list-item
|
||||
v-list-item-avatar: v-icon mdi-chevron-right
|
||||
v-list-item-content: v-list-item-title: .body-2 Optimize performance and testing scenarios based on most popular environments.
|
||||
.body-2 Only authorized developers have access to the data. It is not shared to any 3rd party nor is it used for any other application than improving Wiki.js.
|
||||
v-divider.my-4
|
||||
.subtitle-2 Settings
|
||||
.mt-3
|
||||
v-switch.mt-0(
|
||||
v-model='telemetry',
|
||||
label='Enable Telemetry',
|
||||
color='primary',
|
||||
hint='Allow Wiki.js to transmit telemetry data.',
|
||||
persistent-hint
|
||||
)
|
||||
v-divider.my-4
|
||||
.subtitle-2.mt-3.grey--text.text--darken-1 Client ID
|
||||
.body-2.mt-2 {{clientId}}
|
||||
v-card-chin
|
||||
v-btn.px-3(depressed, color='success', @click='updateTelemetry')
|
||||
v-icon(left) mdi-chevron-right
|
||||
| Save Changes
|
||||
v-spacer
|
||||
v-btn.px-3(outlined, color='grey', @click='resetClientId')
|
||||
v-icon(left) mdi-autorenew
|
||||
span Reset Client ID
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
|
||||
import utilityTelemetryResetIdMutation from 'gql/admin/utilities/utilities-mutation-telemetry-resetid.gql'
|
||||
import utilityTelemetrySetMutation from 'gql/admin/utilities/utilities-mutation-telemetry-set.gql'
|
||||
import utilityTelemetryQuery from 'gql/admin/utilities/utilities-query-telemetry.gql'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
telemetry: false,
|
||||
clientId: 'N/A'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async updateTelemetry() {
|
||||
this.loading = true
|
||||
this.$store.commit(`loadingStart`, 'admin-utilities-telemetry-set')
|
||||
|
||||
try {
|
||||
const respRaw = await this.$apollo.mutate({
|
||||
mutation: utilityTelemetrySetMutation,
|
||||
variables: {
|
||||
enabled: this.telemetry
|
||||
}
|
||||
})
|
||||
const resp = _.get(respRaw, 'data.system.setTelemetry.responseResult', {})
|
||||
if (resp.succeeded) {
|
||||
this.$store.commit('showNotification', {
|
||||
message: 'Telemetry updated successfully.',
|
||||
style: 'success',
|
||||
icon: 'check'
|
||||
})
|
||||
} else {
|
||||
throw new Error(resp.message)
|
||||
}
|
||||
} catch (err) {
|
||||
this.$store.commit('pushGraphError', err)
|
||||
}
|
||||
|
||||
this.$store.commit(`loadingStop`, 'admin-utilities-telemetry-set')
|
||||
this.loading = false
|
||||
},
|
||||
async resetClientId() {
|
||||
this.loading = true
|
||||
this.$store.commit(`loadingStart`, 'admin-utilities-telemetry-resetid')
|
||||
|
||||
try {
|
||||
const respRaw = await this.$apollo.mutate({
|
||||
mutation: utilityTelemetryResetIdMutation
|
||||
})
|
||||
const resp = _.get(respRaw, 'data.system.resetTelemetryClientId.responseResult', {})
|
||||
if (resp.succeeded) {
|
||||
this.$apollo.queries.telemetry.refetch()
|
||||
this.$store.commit('showNotification', {
|
||||
message: 'Telemetry Client ID reset successfully.',
|
||||
style: 'success',
|
||||
icon: 'check'
|
||||
})
|
||||
} else {
|
||||
throw new Error(resp.message)
|
||||
}
|
||||
} catch (err) {
|
||||
this.$store.commit('pushGraphError', err)
|
||||
}
|
||||
|
||||
this.$store.commit(`loadingStop`, 'admin-utilities-telemetry-resetid')
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
apollo: {
|
||||
telemetry: {
|
||||
query: utilityTelemetryQuery,
|
||||
fetchPolicy: 'network-only',
|
||||
manual: true,
|
||||
result ({ data }) {
|
||||
this.telemetry = _.get(data, 'system.info.telemetry', false)
|
||||
this.clientId = _.get(data, 'system.info.telemetryClientId', 'N/A')
|
||||
},
|
||||
watchLoading (isLoading) {
|
||||
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-utilities-telemetry-refresh')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang='scss'>
|
||||
|
||||
</style>
|
@ -1,91 +0,0 @@
|
||||
<template lang='pug'>
|
||||
v-container(fluid, grid-list-lg)
|
||||
v-layout(row, wrap)
|
||||
v-flex(xs12)
|
||||
.admin-header
|
||||
img(src='/_assets/svg/icon-maintenance.svg', alt='Utilities', style='width: 80px;')
|
||||
.admin-header-title
|
||||
.headline.primary--text {{$t('admin:utilities.title')}}
|
||||
.subtitle-1.grey--text {{$t('admin:utilities.subtitle')}}
|
||||
|
||||
v-flex(lg3, xs12)
|
||||
v-card.animated.fadeInUp
|
||||
v-toolbar(flat, color='primary', dark, dense)
|
||||
.subtitle-1 {{$t('admin:utilities.tools')}}
|
||||
v-list(two-line, dense).py-0
|
||||
template(v-for='(tool, idx) in tools')
|
||||
v-list-item(:key='tool.key', @click='selectedTool = tool.key', :disabled='!tool.isAvailable')
|
||||
v-list-item-avatar
|
||||
v-icon(:color='!tool.isAvailable ? `grey lighten-1` : (selectedTool === tool.key ? `blue ` : `grey darken-1`)') {{ tool.icon }}
|
||||
v-list-item-content
|
||||
v-list-item-title.body-2(:class='!tool.isAvailable ? `grey--text` : (selectedTool === tool.key ? `primary--text` : ``)') {{ $t('admin:utilities.' + tool.i18nKey + 'Title') }}
|
||||
v-list-item-subtitle: .caption(:class='!tool.isAvailable ? `grey--text text--lighten-1` : (selectedTool === tool.key ? `blue--text ` : ``)') {{ $t('admin:utilities.' + tool.i18nKey + 'Subtitle') }}
|
||||
v-list-item-avatar(v-if='selectedTool === tool.key')
|
||||
v-icon.animated.fadeInLeft(color='primary', large) mdi-chevron-right
|
||||
v-divider(v-if='idx < tools.length - 1')
|
||||
|
||||
v-flex.animated.fadeInUp.wait-p2s(xs12, lg9)
|
||||
transition(name='admin-router')
|
||||
component(:is='selectedTool')
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
components: {
|
||||
UtilityAuth: () => import(/* webpackChunkName: "admin" */ './admin-utilities-auth.vue'),
|
||||
UtilityContent: () => import(/* webpackChunkName: "admin" */ './admin-utilities-content.vue'),
|
||||
UtilityCache: () => import(/* webpackChunkName: "admin" */ './admin-utilities-cache.vue'),
|
||||
UtilityImportv1: () => import(/* webpackChunkName: "admin" */ './admin-utilities-importv1.vue'),
|
||||
UtilityTelemetry: () => import(/* webpackChunkName: "admin" */ './admin-utilities-telemetry.vue')
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectedTool: 'UtilityAuth',
|
||||
tools: [
|
||||
{
|
||||
key: 'UtilityAuth',
|
||||
icon: 'mdi-lock-open-outline',
|
||||
i18nKey: 'auth',
|
||||
isAvailable: true
|
||||
},
|
||||
{
|
||||
key: 'UtilityContent',
|
||||
icon: 'mdi-content-duplicate',
|
||||
i18nKey: 'content',
|
||||
isAvailable: true
|
||||
},
|
||||
{
|
||||
key: 'UtilityCache',
|
||||
icon: 'mdi-database-refresh',
|
||||
i18nKey: 'cache',
|
||||
isAvailable: true
|
||||
},
|
||||
// {
|
||||
// key: 'UtilityGraphEndpoint',
|
||||
// icon: 'mdi-graphql',
|
||||
// i18nKey: 'graphEndpoint',
|
||||
// isAvailable: false
|
||||
// },
|
||||
{
|
||||
key: 'UtilityImportv1',
|
||||
icon: 'mdi-database-import',
|
||||
i18nKey: 'importv1',
|
||||
isAvailable: true
|
||||
},
|
||||
{
|
||||
key: 'UtilityTelemetry',
|
||||
icon: 'mdi-math-compass',
|
||||
i18nKey: 'telemetry',
|
||||
isAvailable: true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang='scss'>
|
||||
|
||||
</style>
|
@ -1,116 +0,0 @@
|
||||
<template lang='pug'>
|
||||
v-container(fluid, grid-list-lg)
|
||||
v-layout(row, wrap)
|
||||
v-flex(xs12)
|
||||
.admin-header
|
||||
img.animated.fadeInUp(src='/_assets/svg/icon-winter.svg', alt='Mail', style='width: 80px;')
|
||||
.admin-header-title
|
||||
.headline.primary--text.animated.fadeInLeft {{ $t('admin:webhooks.title') }}
|
||||
.subtitle-1.grey--text.animated.fadeInLeft.wait-p4s {{ $t('admin:webhooks.subtitle') }}
|
||||
v-spacer
|
||||
v-btn.animated.fadeInDown(color='success', depressed, @click='save', large, disabled)
|
||||
v-icon(left) check
|
||||
span {{$t('common:actions.apply')}}
|
||||
|
||||
v-flex(lg3, xs12)
|
||||
v-card.animated.fadeInUp
|
||||
v-toolbar(flat, color='primary', dark, dense)
|
||||
.subtitle-1 Webhooks
|
||||
v-spacer
|
||||
v-btn(outline, small)
|
||||
v-icon.mr-2 add
|
||||
span New
|
||||
v-list(two-line, dense).py-0
|
||||
template(v-for='(str, idx) in hooks')
|
||||
v-list-item(:key='str.key', @click='selectedHook = str.key')
|
||||
v-list-item-avatar
|
||||
v-icon(color='primary', v-if='str.isEnabled', v-ripple, @click='str.isEnabled = false') check_box
|
||||
v-icon(color='grey', v-else, v-ripple, @click='str.isEnabled = true') check_box_outline_blank
|
||||
v-list-item-content
|
||||
v-list-item-title.body-2(:class='!str.isAvailable ? `grey--text` : (selectedHook === str.key ? `primary--text` : ``)') {{ str.title }}
|
||||
v-list-item-sub-title.caption(:class='!str.isAvailable ? `grey--text text--lighten-1` : (selectedHook === str.key ? `blue--text ` : ``)') {{ str.description }}
|
||||
v-list-item-avatar(v-if='selectedHook === str.key')
|
||||
v-icon.animated.fadeInLeft(color='primary') arrow_forward_ios
|
||||
v-divider(v-if='idx < hooks.length - 1')
|
||||
|
||||
v-flex(xs12, lg9)
|
||||
v-card.wiki-form.animated.fadeInUp.wait-p2s
|
||||
v-toolbar(color='primary', dense, flat, dark)
|
||||
.subtitle-1 {{hook.title}}
|
||||
v-card-text
|
||||
v-form
|
||||
.authlogo
|
||||
img(:src='hook.logo', :alt='hook.title')
|
||||
.caption.pt-3 {{hook.description}}
|
||||
.caption.pb-3: a(:href='hook.website') {{hook.website}}
|
||||
.body-2(v-if='hook.isEnabled')
|
||||
span This hook is
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
// import { get } from 'vuex-pathify'
|
||||
import mailConfigQuery from 'gql/admin/mail/mail-query-config.gql'
|
||||
import mailUpdateConfigMutation from 'gql/admin/mail/mail-mutation-save-config.gql'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
hooks: [],
|
||||
selectedHook: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
hook() {
|
||||
return _.find(this.hooks, ['id', this.selectedHook]) || {}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async save () {
|
||||
try {
|
||||
await this.$apollo.mutate({
|
||||
mutation: mailUpdateConfigMutation,
|
||||
variables: {
|
||||
senderName: this.config.senderName || '',
|
||||
senderEmail: this.config.senderEmail || '',
|
||||
host: this.config.host || '',
|
||||
port: _.toSafeInteger(this.config.port) || 0,
|
||||
secure: this.config.secure || false,
|
||||
user: this.config.user || '',
|
||||
pass: this.config.pass || '',
|
||||
useDKIM: this.config.useDKIM || false,
|
||||
dkimDomainName: this.config.dkimDomainName || '',
|
||||
dkimKeySelector: this.config.dkimKeySelector || '',
|
||||
dkimPrivateKey: this.config.dkimPrivateKey || ''
|
||||
},
|
||||
watchLoading (isLoading) {
|
||||
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-mail-update')
|
||||
}
|
||||
})
|
||||
this.$store.commit('showNotification', {
|
||||
style: 'success',
|
||||
message: 'Configuration saved successfully.',
|
||||
icon: 'check'
|
||||
})
|
||||
} catch (err) {
|
||||
this.$store.commit('pushGraphError', err)
|
||||
}
|
||||
}
|
||||
},
|
||||
apollo: {
|
||||
hooks: {
|
||||
query: mailConfigQuery,
|
||||
fetchPolicy: 'network-only',
|
||||
update: (data) => _.cloneDeep(data.mail.config),
|
||||
watchLoading (isLoading) {
|
||||
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-mail-refresh')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang='scss'>
|
||||
|
||||
</style>
|
@ -1,801 +0,0 @@
|
||||
<template lang="pug">
|
||||
v-app
|
||||
.login(:style='`background-image: url(` + bgUrl + `);`')
|
||||
.login-sd
|
||||
.d-flex.mb-5
|
||||
.login-logo
|
||||
v-avatar(tile, size='34')
|
||||
v-img(:src='logoUrl')
|
||||
.login-title
|
||||
.text-h6.grey--text.text--darken-4 {{ siteTitle }}
|
||||
v-alert.mb-0(
|
||||
v-model='errorShown'
|
||||
transition='slide-y-reverse-transition'
|
||||
color='red darken-2'
|
||||
tile
|
||||
dark
|
||||
dense
|
||||
icon='mdi-alert'
|
||||
)
|
||||
.body-2 {{errorMessage}}
|
||||
//-------------------------------------------------
|
||||
//- PROVIDERS LIST
|
||||
//-------------------------------------------------
|
||||
template(v-if='screen === `login` && strategies.length > 1')
|
||||
.login-subtitle
|
||||
.text-subtitle-1 {{$t('auth:selectAuthProvider')}}
|
||||
.login-list
|
||||
v-list.elevation-1.radius-7(nav, light)
|
||||
v-list-item-group(v-model='selectedStrategyKey')
|
||||
v-list-item(
|
||||
v-for='(stg, idx) of filteredStrategies'
|
||||
:key='stg.key'
|
||||
:value='stg.key'
|
||||
:color='stg.strategy.color'
|
||||
)
|
||||
v-avatar.mr-3(tile, size='24', v-html='stg.strategy.icon')
|
||||
span.text-none {{stg.displayName}}
|
||||
//-------------------------------------------------
|
||||
//- LOGIN FORM
|
||||
//-------------------------------------------------
|
||||
template(v-if='screen === `login` && selectedStrategy.strategy.useForm')
|
||||
.login-subtitle
|
||||
.text-subtitle-1 {{$t('auth:enterCredentials')}}
|
||||
.login-form
|
||||
v-text-field(
|
||||
solo
|
||||
flat
|
||||
prepend-inner-icon='mdi-clipboard-account'
|
||||
background-color='white'
|
||||
color='blue darken-2'
|
||||
hide-details
|
||||
ref='iptEmail'
|
||||
v-model='username'
|
||||
:placeholder='isUsernameEmail ? $t(`auth:fields.email`) : $t(`auth:fields.username`)'
|
||||
:type='isUsernameEmail ? `email` : `text`'
|
||||
:autocomplete='isUsernameEmail ? `email` : `username`'
|
||||
light
|
||||
)
|
||||
v-text-field.mt-2(
|
||||
solo
|
||||
flat
|
||||
prepend-inner-icon='mdi-form-textbox-password'
|
||||
background-color='white'
|
||||
color='blue darken-2'
|
||||
hide-details
|
||||
ref='iptPassword'
|
||||
v-model='password'
|
||||
:append-icon='hidePassword ? "mdi-eye-off" : "mdi-eye"'
|
||||
@click:append='() => (hidePassword = !hidePassword)'
|
||||
:type='hidePassword ? "password" : "text"'
|
||||
:placeholder='$t("auth:fields.password")'
|
||||
autocomplete='current-password'
|
||||
@keyup.enter='login'
|
||||
light
|
||||
)
|
||||
v-btn.mt-2.text-none(
|
||||
width='100%'
|
||||
large
|
||||
color='blue darken-2'
|
||||
dark
|
||||
@click='login'
|
||||
:loading='isLoading'
|
||||
) {{ $t('auth:actions.login') }}
|
||||
.text-center.mt-5
|
||||
v-btn.text-none(
|
||||
text
|
||||
rounded
|
||||
color='grey darken-3'
|
||||
@click.stop.prevent='forgotPassword'
|
||||
href='#forgot'
|
||||
): .caption {{ $t('auth:forgotPasswordLink') }}
|
||||
v-btn.text-none(
|
||||
v-if='selectedStrategyKey === `local` && selectedStrategy.selfRegistration'
|
||||
color='indigo darken-2'
|
||||
text
|
||||
rounded
|
||||
href='/register'
|
||||
): .caption {{ $t('auth:switchToRegister.link') }}
|
||||
//-------------------------------------------------
|
||||
//- FORGOT PASSWORD FORM
|
||||
//-------------------------------------------------
|
||||
template(v-if='screen === `forgot`')
|
||||
.login-subtitle
|
||||
.text-subtitle-1 {{$t('auth:forgotPasswordTitle')}}
|
||||
.login-info {{ $t('auth:forgotPasswordSubtitle') }}
|
||||
.login-form
|
||||
v-text-field(
|
||||
solo
|
||||
flat
|
||||
prepend-inner-icon='mdi-clipboard-account'
|
||||
background-color='white'
|
||||
color='blue darken-2'
|
||||
hide-details
|
||||
ref='iptForgotPwdEmail'
|
||||
v-model='username'
|
||||
:placeholder='$t(`auth:fields.email`)'
|
||||
type='email'
|
||||
autocomplete='email'
|
||||
light
|
||||
)
|
||||
v-btn.mt-2.text-none(
|
||||
width='100%'
|
||||
large
|
||||
color='blue darken-2'
|
||||
dark
|
||||
@click='forgotPasswordSubmit'
|
||||
:loading='isLoading'
|
||||
) {{ $t('auth:sendResetPassword') }}
|
||||
.text-center.mt-5
|
||||
v-btn.text-none(
|
||||
text
|
||||
rounded
|
||||
color='grey darken-3'
|
||||
@click.stop.prevent='screen = `login`'
|
||||
href='#forgot'
|
||||
): .caption {{ $t('auth:forgotPasswordCancel') }}
|
||||
//-------------------------------------------------
|
||||
//- CHANGE PASSWORD FORM
|
||||
//-------------------------------------------------
|
||||
template(v-if='screen === `changePwd`')
|
||||
.login-subtitle
|
||||
.text-subtitle-1 {{ $t('auth:changePwd.subtitle') }}
|
||||
.login-form
|
||||
v-text-field.mt-2(
|
||||
type='password'
|
||||
solo
|
||||
flat
|
||||
prepend-inner-icon='mdi-form-textbox-password'
|
||||
background-color='white'
|
||||
color='blue darken-2'
|
||||
hide-details
|
||||
ref='iptNewPassword'
|
||||
v-model='newPassword'
|
||||
:placeholder='$t(`auth:changePwd.newPasswordPlaceholder`)'
|
||||
autocomplete='new-password'
|
||||
light
|
||||
)
|
||||
password-strength(slot='progress', v-model='newPassword')
|
||||
v-text-field.mt-2(
|
||||
type='password'
|
||||
solo
|
||||
flat
|
||||
prepend-inner-icon='mdi-form-textbox-password'
|
||||
background-color='white'
|
||||
color='blue darken-2'
|
||||
hide-details
|
||||
v-model='newPasswordVerify'
|
||||
:placeholder='$t(`auth:changePwd.newPasswordVerifyPlaceholder`)'
|
||||
autocomplete='new-password'
|
||||
@keyup.enter='changePassword'
|
||||
light
|
||||
)
|
||||
v-btn.mt-2.text-none(
|
||||
width='100%'
|
||||
large
|
||||
color='blue darken-2'
|
||||
dark
|
||||
@click='changePassword'
|
||||
:loading='isLoading'
|
||||
) {{ $t('auth:changePwd.proceed') }}
|
||||
|
||||
//-------------------------------------------------
|
||||
//- TFA FORM
|
||||
//-------------------------------------------------
|
||||
v-dialog(v-model='isTFAShown', max-width='500', persistent)
|
||||
v-card
|
||||
.login-tfa.text-center.pa-5.grey--text.text--darken-3
|
||||
img(src='_assets/svg/icon-pin-pad.svg')
|
||||
.subtitle-2 {{$t('auth:tfaFormTitle')}}
|
||||
v-text-field.login-tfa-field.mt-2(
|
||||
solo
|
||||
flat
|
||||
background-color='white'
|
||||
color='blue darken-2'
|
||||
hide-details
|
||||
ref='iptTFA'
|
||||
v-model='securityCode'
|
||||
:placeholder='$t("auth:tfa.placeholder")'
|
||||
autocomplete='one-time-code'
|
||||
@keyup.enter='verifySecurityCode(false)'
|
||||
light
|
||||
)
|
||||
v-btn.mt-2.text-none(
|
||||
width='100%'
|
||||
large
|
||||
color='blue darken-2'
|
||||
dark
|
||||
@click='verifySecurityCode(false)'
|
||||
:loading='isLoading'
|
||||
) {{ $t('auth:tfa.verifyToken') }}
|
||||
|
||||
//-------------------------------------------------
|
||||
//- SETUP TFA FORM
|
||||
//-------------------------------------------------
|
||||
v-dialog(v-model='isTFASetupShown', max-width='600', persistent)
|
||||
v-card
|
||||
.login-tfa.text-center.pa-5.grey--text.text--darken-3
|
||||
.subtitle-1.primary--text {{$t('auth:tfaSetupTitle')}}
|
||||
v-divider.my-5
|
||||
.subtitle-2 {{$t('auth:tfaSetupInstrFirst')}}
|
||||
.caption (#[a(href='https://authy.com/', target='_blank', noopener) Authy], #[a(href='https://support.google.com/accounts/answer/1066447', target='_blank', noopener) Google Authenticator], #[a(href='https://www.microsoft.com/en-us/account/authenticator', target='_blank', noopener) Microsoft Authenticator], etc.)
|
||||
.login-tfa-qr.mt-5(v-if='isTFASetupShown', v-html='tfaQRImage')
|
||||
.subtitle-2.mt-5 {{$t('auth:tfaSetupInstrSecond')}}
|
||||
v-text-field.login-tfa-field.mt-2(
|
||||
solo
|
||||
flat
|
||||
background-color='white'
|
||||
color='blue darken-2'
|
||||
hide-details
|
||||
ref='iptTFASetup'
|
||||
v-model='securityCode'
|
||||
:placeholder='$t("auth:tfa.placeholder")'
|
||||
autocomplete='one-time-code'
|
||||
@keyup.enter='verifySecurityCode(true)'
|
||||
light
|
||||
)
|
||||
v-btn.mt-2.text-none(
|
||||
width='100%'
|
||||
large
|
||||
color='blue darken-2'
|
||||
dark
|
||||
@click='verifySecurityCode(true)'
|
||||
:loading='isLoading'
|
||||
) {{ $t('auth:tfa.verifyToken') }}
|
||||
|
||||
loader(v-model='isLoading', :color='loaderColor', :title='loaderTitle', :subtitle='$t(`auth:pleaseWait`)')
|
||||
notify(style='padding-top: 64px;')
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/* global siteConfig */
|
||||
|
||||
// <span>Photo by <a href="https://unsplash.com/@isaacquesada?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Isaac Quesada</a> on <a href="/t/textures-patterns?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a></span>
|
||||
|
||||
import _ from 'lodash'
|
||||
import Cookies from 'js-cookie'
|
||||
import gql from 'graphql-tag'
|
||||
import { sync } from 'vuex-pathify'
|
||||
|
||||
export default {
|
||||
i18nOptions: { namespaces: 'auth' },
|
||||
props: {
|
||||
bgUrl: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
hideLocal: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
changePwdContinuationToken: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
error: false,
|
||||
strategies: [],
|
||||
selectedStrategyKey: 'unselected',
|
||||
selectedStrategy: { key: 'unselected', strategy: { useForm: false, usernameType: 'email' } },
|
||||
screen: 'login',
|
||||
username: '',
|
||||
password: '',
|
||||
hidePassword: true,
|
||||
securityCode: '',
|
||||
continuationToken: '',
|
||||
isLoading: false,
|
||||
loaderColor: 'grey darken-4',
|
||||
loaderTitle: 'Working...',
|
||||
isShown: false,
|
||||
newPassword: '',
|
||||
newPasswordVerify: '',
|
||||
isTFAShown: false,
|
||||
isTFASetupShown: false,
|
||||
tfaQRImage: '',
|
||||
errorShown: false,
|
||||
errorMessage: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
activeModal: sync('editor/activeModal'),
|
||||
siteTitle () {
|
||||
return siteConfig.title
|
||||
},
|
||||
isSocialShown () {
|
||||
return this.strategies.length > 1
|
||||
},
|
||||
logoUrl () { return siteConfig.logoUrl },
|
||||
filteredStrategies () {
|
||||
const qParams = new URLSearchParams(window.location.search)
|
||||
if (this.hideLocal && !qParams.has('all')) {
|
||||
return _.reject(this.strategies, ['key', 'local'])
|
||||
} else {
|
||||
return this.strategies
|
||||
}
|
||||
},
|
||||
isUsernameEmail () {
|
||||
return this.selectedStrategy.strategy.usernameType === `email`
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
filteredStrategies (newValue, oldValue) {
|
||||
if (_.head(newValue).strategy.useForm) {
|
||||
this.selectedStrategyKey = _.head(newValue).key
|
||||
}
|
||||
},
|
||||
selectedStrategyKey (newValue, oldValue) {
|
||||
this.selectedStrategy = _.find(this.strategies, ['key', newValue])
|
||||
if (this.screen === 'changePwd') {
|
||||
return
|
||||
}
|
||||
this.screen = 'login'
|
||||
if (!this.selectedStrategy.strategy.useForm) {
|
||||
this.isLoading = true
|
||||
window.location.assign('/login/' + newValue)
|
||||
} else {
|
||||
this.$nextTick(() => {
|
||||
this.$refs.iptEmail.focus()
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.isShown = true
|
||||
if (this.changePwdContinuationToken) {
|
||||
this.screen = 'changePwd'
|
||||
this.continuationToken = this.changePwdContinuationToken
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* LOGIN
|
||||
*/
|
||||
async login () {
|
||||
this.errorShown = false
|
||||
if (this.username.length < 2) {
|
||||
this.errorMessage = this.$t('auth:invalidEmailUsername')
|
||||
this.errorShown = true
|
||||
this.$refs.iptEmail.focus()
|
||||
} else if (this.password.length < 2) {
|
||||
this.errorMessage = this.$t('auth:invalidPassword')
|
||||
this.errorShown = true
|
||||
this.$refs.iptPassword.focus()
|
||||
} else {
|
||||
this.loaderColor = 'grey darken-4'
|
||||
this.loaderTitle = this.$t('auth:signingIn')
|
||||
this.isLoading = true
|
||||
try {
|
||||
const resp = await this.$apollo.mutate({
|
||||
mutation: gql`
|
||||
mutation($username: String!, $password: String!, $strategy: String!) {
|
||||
authentication {
|
||||
login(username: $username, password: $password, strategy: $strategy) {
|
||||
responseResult {
|
||||
succeeded
|
||||
errorCode
|
||||
slug
|
||||
message
|
||||
}
|
||||
jwt
|
||||
mustChangePwd
|
||||
mustProvideTFA
|
||||
mustSetupTFA
|
||||
continuationToken
|
||||
redirect
|
||||
tfaQRImage
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
username: this.username,
|
||||
password: this.password,
|
||||
strategy: this.selectedStrategy.key
|
||||
}
|
||||
})
|
||||
if (_.has(resp, 'data.authentication.login')) {
|
||||
const respObj = _.get(resp, 'data.authentication.login', {})
|
||||
if (respObj.responseResult.succeeded === true) {
|
||||
this.handleLoginResponse(respObj)
|
||||
} else {
|
||||
throw new Error(respObj.responseResult.message)
|
||||
}
|
||||
} else {
|
||||
throw new Error(this.$t('auth:genericError'))
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
this.$store.commit('showNotification', {
|
||||
style: 'red',
|
||||
message: err.message,
|
||||
icon: 'alert'
|
||||
})
|
||||
this.isLoading = false
|
||||
}
|
||||
}
|
||||
},
|
||||
/**
|
||||
* VERIFY TFA CODE
|
||||
*/
|
||||
async verifySecurityCode (setup = false) {
|
||||
if (this.securityCode.length !== 6) {
|
||||
this.$store.commit('showNotification', {
|
||||
style: 'red',
|
||||
message: 'Enter a valid security code.',
|
||||
icon: 'alert'
|
||||
})
|
||||
if (setup) {
|
||||
this.$refs.iptTFASetup.focus()
|
||||
} else {
|
||||
this.$refs.iptTFA.focus()
|
||||
}
|
||||
} else {
|
||||
this.loaderColor = 'grey darken-4'
|
||||
this.loaderTitle = this.$t('auth:signingIn')
|
||||
this.isLoading = true
|
||||
try {
|
||||
const resp = await this.$apollo.mutate({
|
||||
mutation: gql`
|
||||
mutation(
|
||||
$continuationToken: String!
|
||||
$securityCode: String!
|
||||
$setup: Boolean
|
||||
) {
|
||||
authentication {
|
||||
loginTFA(
|
||||
continuationToken: $continuationToken
|
||||
securityCode: $securityCode
|
||||
setup: $setup
|
||||
) {
|
||||
responseResult {
|
||||
succeeded
|
||||
errorCode
|
||||
slug
|
||||
message
|
||||
}
|
||||
jwt
|
||||
mustChangePwd
|
||||
continuationToken
|
||||
redirect
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
continuationToken: this.continuationToken,
|
||||
securityCode: this.securityCode,
|
||||
setup
|
||||
}
|
||||
})
|
||||
if (_.has(resp, 'data.authentication.loginTFA')) {
|
||||
let respObj = _.get(resp, 'data.authentication.loginTFA', {})
|
||||
if (respObj.responseResult.succeeded === true) {
|
||||
this.handleLoginResponse(respObj)
|
||||
} else {
|
||||
if (!setup) {
|
||||
this.isTFAShown = false
|
||||
}
|
||||
throw new Error(respObj.responseResult.message)
|
||||
}
|
||||
} else {
|
||||
throw new Error(this.$t('auth:genericError'))
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
this.$store.commit('showNotification', {
|
||||
style: 'red',
|
||||
message: err.message,
|
||||
icon: 'alert'
|
||||
})
|
||||
this.isLoading = false
|
||||
}
|
||||
}
|
||||
},
|
||||
/**
|
||||
* CHANGE PASSWORD
|
||||
*/
|
||||
async changePassword () {
|
||||
this.loaderColor = 'grey darken-4'
|
||||
this.loaderTitle = this.$t('auth:changePwd.loading')
|
||||
this.isLoading = true
|
||||
try {
|
||||
const resp = await this.$apollo.mutate({
|
||||
mutation: gql`
|
||||
mutation (
|
||||
$continuationToken: String!
|
||||
$newPassword: String!
|
||||
) {
|
||||
authentication {
|
||||
loginChangePassword (
|
||||
continuationToken: $continuationToken
|
||||
newPassword: $newPassword
|
||||
) {
|
||||
responseResult {
|
||||
succeeded
|
||||
errorCode
|
||||
slug
|
||||
message
|
||||
}
|
||||
jwt
|
||||
continuationToken
|
||||
redirect
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
continuationToken: this.continuationToken,
|
||||
newPassword: this.newPassword
|
||||
}
|
||||
})
|
||||
if (_.has(resp, 'data.authentication.loginChangePassword')) {
|
||||
let respObj = _.get(resp, 'data.authentication.loginChangePassword', {})
|
||||
if (respObj.responseResult.succeeded === true) {
|
||||
this.handleLoginResponse(respObj)
|
||||
} else {
|
||||
throw new Error(respObj.responseResult.message)
|
||||
}
|
||||
} else {
|
||||
throw new Error(this.$t('auth:genericError'))
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
this.$store.commit('showNotification', {
|
||||
style: 'red',
|
||||
message: err.message,
|
||||
icon: 'alert'
|
||||
})
|
||||
this.isLoading = false
|
||||
}
|
||||
},
|
||||
/**
|
||||
* SWITCH TO FORGOT PASSWORD SCREEN
|
||||
*/
|
||||
forgotPassword () {
|
||||
this.screen = 'forgot'
|
||||
this.$nextTick(() => {
|
||||
this.$refs.iptForgotPwdEmail.focus()
|
||||
})
|
||||
},
|
||||
/**
|
||||
* FORGOT PASSWORD SUBMIT
|
||||
*/
|
||||
async forgotPasswordSubmit () {
|
||||
this.loaderColor = 'grey darken-4'
|
||||
this.loaderTitle = this.$t('auth:forgotPasswordLoading')
|
||||
this.isLoading = true
|
||||
try {
|
||||
const resp = await this.$apollo.mutate({
|
||||
mutation: gql`
|
||||
mutation (
|
||||
$email: String!
|
||||
) {
|
||||
authentication {
|
||||
forgotPassword (
|
||||
email: $email
|
||||
) {
|
||||
responseResult {
|
||||
succeeded
|
||||
errorCode
|
||||
slug
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
email: this.username
|
||||
}
|
||||
})
|
||||
if (_.has(resp, 'data.authentication.forgotPassword.responseResult')) {
|
||||
let respObj = _.get(resp, 'data.authentication.forgotPassword.responseResult', {})
|
||||
if (respObj.succeeded === true) {
|
||||
this.$store.commit('showNotification', {
|
||||
style: 'success',
|
||||
message: this.$t('auth:forgotPasswordSuccess'),
|
||||
icon: 'email'
|
||||
})
|
||||
this.screen = 'login'
|
||||
} else {
|
||||
throw new Error(respObj.message)
|
||||
}
|
||||
} else {
|
||||
throw new Error(this.$t('auth:genericError'))
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
this.$store.commit('showNotification', {
|
||||
style: 'red',
|
||||
message: err.message,
|
||||
icon: 'alert'
|
||||
})
|
||||
}
|
||||
this.isLoading = false
|
||||
},
|
||||
handleLoginResponse (respObj) {
|
||||
this.continuationToken = respObj.continuationToken
|
||||
if (respObj.mustChangePwd === true) {
|
||||
this.screen = 'changePwd'
|
||||
this.$nextTick(() => {
|
||||
this.$refs.iptNewPassword.focus()
|
||||
})
|
||||
this.isLoading = false
|
||||
} else if (respObj.mustProvideTFA === true) {
|
||||
this.securityCode = ''
|
||||
this.isTFAShown = true
|
||||
setTimeout(() => {
|
||||
this.$refs.iptTFA.focus()
|
||||
}, 500)
|
||||
this.isLoading = false
|
||||
} else if (respObj.mustSetupTFA === true) {
|
||||
this.securityCode = ''
|
||||
this.isTFASetupShown = true
|
||||
this.tfaQRImage = respObj.tfaQRImage
|
||||
setTimeout(() => {
|
||||
this.$refs.iptTFASetup.focus()
|
||||
}, 500)
|
||||
this.isLoading = false
|
||||
} else {
|
||||
this.loaderColor = 'green darken-1'
|
||||
this.loaderTitle = this.$t('auth:loginSuccess')
|
||||
Cookies.set('jwt', respObj.jwt, { expires: 365 })
|
||||
_.delay(() => {
|
||||
const loginRedirect = Cookies.get('loginRedirect')
|
||||
if (loginRedirect === '/' && respObj.redirect) {
|
||||
Cookies.remove('loginRedirect')
|
||||
window.location.replace(respObj.redirect)
|
||||
} else if (loginRedirect) {
|
||||
Cookies.remove('loginRedirect')
|
||||
window.location.replace(loginRedirect)
|
||||
} else if (respObj.redirect) {
|
||||
window.location.replace(respObj.redirect)
|
||||
} else {
|
||||
window.location.replace('/')
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
},
|
||||
apollo: {
|
||||
strategies: {
|
||||
query: gql`
|
||||
query loginFetchSiteStrategies(
|
||||
$siteId: UUID
|
||||
) {
|
||||
authStrategies(
|
||||
siteId: $siteId
|
||||
enabledOnly: true
|
||||
) {
|
||||
key
|
||||
strategy {
|
||||
key
|
||||
logo
|
||||
color
|
||||
icon
|
||||
useForm
|
||||
usernameType
|
||||
}
|
||||
displayName
|
||||
order
|
||||
selfRegistration
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables () {
|
||||
return {
|
||||
siteId: siteConfig.id
|
||||
}
|
||||
},
|
||||
update: (data) => _.sortBy(data.authStrategies, ['order']),
|
||||
watchLoading (isLoading) {
|
||||
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'login-strategies-refresh')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.login {
|
||||
// background-image: url('/_assets/img/splash/1.jpg');
|
||||
background-color: mc('grey', '900');
|
||||
background-size: cover;
|
||||
background-position: center center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
&-sd {
|
||||
background-color: rgba(255,255,255,.8);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
border-left: 1px solid rgba(255,255,255,.85);
|
||||
border-right: 1px solid rgba(255,255,255,.85);
|
||||
width: 450px;
|
||||
height: 100%;
|
||||
margin-left: 5vw;
|
||||
|
||||
@at-root .no-backdropfilter & {
|
||||
background-color: rgba(255,255,255,.95);
|
||||
}
|
||||
|
||||
@include until($tablet) {
|
||||
margin-left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&-logo {
|
||||
padding: 12px 0 0 12px;
|
||||
width: 58px;
|
||||
height: 58px;
|
||||
background-color: #222;
|
||||
margin-left: 12px;
|
||||
border-bottom-left-radius: 7px;
|
||||
border-bottom-right-radius: 7px;
|
||||
}
|
||||
|
||||
&-title {
|
||||
height: 58px;
|
||||
padding-left: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-shadow: .5px .5px #FFF;
|
||||
}
|
||||
|
||||
&-subtitle {
|
||||
padding: 24px 12px 12px 12px;
|
||||
color: #111;
|
||||
font-weight: 500;
|
||||
text-shadow: 1px 1px rgba(255,255,255,.5);
|
||||
background-image: linear-gradient(to bottom, rgba(0,0,0,0), rgba(0,0,0,.15));
|
||||
text-align: center;
|
||||
border-bottom: 1px solid rgba(0,0,0,.3);
|
||||
}
|
||||
|
||||
&-info {
|
||||
border-top: 1px solid rgba(255,255,255,.85);
|
||||
background-color: rgba(255,255,255,.15);
|
||||
border-bottom: 1px solid rgba(0,0,0,.15);
|
||||
padding: 12px;
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
color: mc('grey', '900');
|
||||
}
|
||||
|
||||
&-list {
|
||||
border-top: 1px solid rgba(255,255,255,.85);
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
&-form {
|
||||
padding: 12px;
|
||||
border-top: 1px solid rgba(255,255,255,.85);
|
||||
}
|
||||
|
||||
&-main {
|
||||
flex: 1 0 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
&-tfa {
|
||||
background-color: #EEE;
|
||||
border: 7px solid #FFF;
|
||||
|
||||
&-field input {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&-qr {
|
||||
background-color: #FFF;
|
||||
padding: 5px;
|
||||
border-radius: 5px;
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,98 +0,0 @@
|
||||
<template lang='pug'>
|
||||
v-app(:dark='$vuetify.theme.dark').profile
|
||||
nav-header
|
||||
v-navigation-drawer.pb-0(v-model='profileDrawerShown', app, fixed, clipped, left, permanent)
|
||||
v-list(dense, nav)
|
||||
v-list-item(to='/profile', color='primary')
|
||||
v-list-item-action: v-icon mdi-face-profile
|
||||
v-list-item-content
|
||||
v-list-item-title {{$t('profile:title')}}
|
||||
//- v-list-item(to='/preferences', disabled)
|
||||
//- v-list-item-action: v-icon(color='grey lighten-1') mdi-cog-outline
|
||||
//- v-list-item-content
|
||||
//- v-list-item-title Preferences
|
||||
//- v-list-item-subtitle.caption.grey--text.text--lighten-1 Coming soon
|
||||
v-list-item(to='/pages', color='primary')
|
||||
v-list-item-action: v-icon mdi-file-document-outline
|
||||
v-list-item-content
|
||||
v-list-item-title {{$t('profile:pages.title')}}
|
||||
//- v-list-item(to='/comments', disabled)
|
||||
//- v-list-item-action: v-icon(color='grey lighten-1') mdi-message-reply-text
|
||||
//- v-list-item-content
|
||||
//- v-list-item-title {{$t('profile:comments.title')}}
|
||||
//- v-list-item-subtitle.caption.grey--text.text--lighten-1 Coming soon
|
||||
|
||||
v-content(:class='$vuetify.theme.dark ? "grey darken-4" : "grey lighten-5"')
|
||||
transition(name='profile-router')
|
||||
router-view
|
||||
|
||||
nav-footer
|
||||
notify
|
||||
search-results
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import VueRouter from 'vue-router'
|
||||
|
||||
/* global WIKI */
|
||||
|
||||
const router = new VueRouter({
|
||||
mode: 'history',
|
||||
base: '/p',
|
||||
routes: [
|
||||
{ path: '/', redirect: '/profile' },
|
||||
{ path: '/profile', component: () => import(/* webpackChunkName: "profile" */ './profile/profile.vue') },
|
||||
{ path: '/pages', component: () => import(/* webpackChunkName: "profile" */ './profile/pages.vue') },
|
||||
{ path: '/comments', component: () => import(/* webpackChunkName: "profile" */ './profile/comments.vue') }
|
||||
]
|
||||
})
|
||||
|
||||
router.beforeEach((to, from, next) => {
|
||||
WIKI.$store.commit('loadingStart', 'profile')
|
||||
next()
|
||||
})
|
||||
|
||||
router.afterEach((to, from) => {
|
||||
WIKI.$store.commit('loadingStop', 'profile')
|
||||
})
|
||||
|
||||
export default {
|
||||
i18nOptions: { namespaces: 'profile' },
|
||||
data() {
|
||||
return {
|
||||
profileDrawerShown: true
|
||||
}
|
||||
},
|
||||
router,
|
||||
created() {
|
||||
this.$store.commit('page/SET_MODE', 'profile')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang='scss'>
|
||||
|
||||
.profile-router {
|
||||
&-enter-active, &-leave-active {
|
||||
transition: opacity .25s ease;
|
||||
opacity: 1;
|
||||
}
|
||||
&-enter-active {
|
||||
transition-delay: .25s;
|
||||
}
|
||||
&-enter, &-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.profile-header {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
|
||||
&-title {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
@ -1,20 +0,0 @@
|
||||
<template lang='pug'>
|
||||
v-container(fluid, fill-height, grid-list-lg)
|
||||
v-layout(row wrap)
|
||||
v-flex(xs12)
|
||||
.headline.primary--text Comments
|
||||
.subheading.grey--text List of comments I posted
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return { }
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang='scss'>
|
||||
|
||||
</style>
|
@ -1,121 +0,0 @@
|
||||
<template lang='pug'>
|
||||
v-container(fluid, grid-list-lg)
|
||||
v-layout(row wrap)
|
||||
v-flex(xs12)
|
||||
.profile-header
|
||||
img.animated.fadeInUp(src='/_assets/svg/icon-file.svg', alt='Users', style='width: 80px;')
|
||||
.profile-header-title
|
||||
.headline.primary--text.animated.fadeInLeft {{$t('profile:pages.title')}}
|
||||
.subheading.grey--text.animated.fadeInLeft {{$t('profile:pages.subtitle')}}
|
||||
v-spacer
|
||||
v-btn.animated.fadeInDown.wait-p1s(color='grey', outlined, @click='refresh', large)
|
||||
v-icon.grey--text mdi-refresh
|
||||
v-flex(xs12)
|
||||
v-card.animated.fadeInUp
|
||||
v-data-table(
|
||||
:items='pages'
|
||||
:headers='headers'
|
||||
:page.sync='pagination'
|
||||
:items-per-page='15'
|
||||
:loading='loading'
|
||||
must-sort,
|
||||
sort-by='updatedAt',
|
||||
sort-desc,
|
||||
hide-default-footer
|
||||
)
|
||||
template(slot='item', slot-scope='props')
|
||||
tr.is-clickable(:active='props.selected', @click='goToPage(props.item.id)')
|
||||
td
|
||||
.body-2: strong {{ props.item.title }}
|
||||
.caption {{ props.item.description }}
|
||||
td.admin-pages-path
|
||||
v-chip(label, small, :color='$vuetify.theme.dark ? `grey darken-4` : `grey lighten-4`') {{ props.item.locale }}
|
||||
span.ml-2.grey--text(:class='$vuetify.theme.dark ? `text--lighten-1` : `text--darken-2`') / {{ props.item.path }}
|
||||
td {{ props.item.createdAt | moment('calendar') }}
|
||||
td {{ props.item.updatedAt | moment('calendar') }}
|
||||
template(slot='no-data')
|
||||
v-alert.ma-3(icon='mdi-alert', :value='true', outlined, color='grey')
|
||||
em.caption {{$t('profile:pages.emptyList')}}
|
||||
.text-center.py-2.animated.fadeInDown(v-if='this.pageTotal > 1')
|
||||
v-pagination(v-model='pagination', :length='pageTotal')
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
selectedPage: {},
|
||||
pagination: 1,
|
||||
pages: [],
|
||||
loading: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
headers () {
|
||||
return [
|
||||
{ text: this.$t('profile:pages.headerTitle'), value: 'title' },
|
||||
{ text: this.$t('profile:pages.headerPath'), value: 'path' },
|
||||
{ text: this.$t('profile:pages.headerCreatedAt'), value: 'createdAt', width: 250 },
|
||||
{ text: this.$t('profile:pages.headerUpdatedAt'), value: 'updatedAt', width: 250 }
|
||||
]
|
||||
},
|
||||
pageTotal () {
|
||||
return Math.ceil(this.pages.length / 15)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async refresh() {
|
||||
await this.$apollo.queries.pages.refetch()
|
||||
this.$store.commit('showNotification', {
|
||||
message: this.$t('profile:pages.refreshSuccess'),
|
||||
style: 'success',
|
||||
icon: 'cached'
|
||||
})
|
||||
},
|
||||
goToPage(id) {
|
||||
window.location.assign(`/i/` + id)
|
||||
}
|
||||
},
|
||||
apollo: {
|
||||
pages: {
|
||||
query: gql`
|
||||
query($creatorId: Int, $authorId: Int) {
|
||||
pages {
|
||||
list(creatorId: $creatorId, authorId: $authorId) {
|
||||
id
|
||||
locale
|
||||
path
|
||||
title
|
||||
description
|
||||
contentType
|
||||
isPublished
|
||||
isPrivate
|
||||
privateNS
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables () {
|
||||
return {
|
||||
creatorId: this.$store.get('user/id'),
|
||||
authorId: this.$store.get('user/id')
|
||||
}
|
||||
},
|
||||
fetchPolicy: 'network-only',
|
||||
update: (data) => data.pages.list,
|
||||
watchLoading (isLoading) {
|
||||
this.loading = isLoading
|
||||
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'profile-pages-refresh')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang='scss'>
|
||||
|
||||
</style>
|
@ -1,923 +0,0 @@
|
||||
<template lang='pug'>
|
||||
v-container(fluid, grid-list-lg)
|
||||
v-layout(row wrap)
|
||||
v-flex(xs12)
|
||||
.profile-header
|
||||
img.animated.fadeInUp(src='/_assets/svg/icon-profile.svg', alt='Users', style='width: 80px;')
|
||||
.profile-header-title
|
||||
.headline.primary--text.animated.fadeInLeft {{$t('profile:title')}}
|
||||
.subheading.grey--text.animated.fadeInLeft {{$t('profile:subtitle')}}
|
||||
v-spacer
|
||||
v-btn.animated.fadeInDown(color='success', depressed, @click='saveProfile', :loading='saveLoading', large)
|
||||
v-icon(left) mdi-check
|
||||
span {{$t('common:actions.save')}}
|
||||
//- v-btn.animated.fadeInDown(outlined, color='primary', disabled).mr-0
|
||||
//- v-icon(left) mdi-earth
|
||||
//- span {{$t('profile:viewPublicProfile')}}
|
||||
v-flex(lg6 xs12)
|
||||
v-card.animated.fadeInUp
|
||||
v-toolbar(color='blue-grey', dark, dense, flat)
|
||||
v-toolbar-title.subtitle-1 {{$t('profile:myInfo')}}
|
||||
v-list(two-line, dense)
|
||||
v-list-item
|
||||
v-list-item-avatar(size='32')
|
||||
v-icon mdi-account
|
||||
v-list-item-content
|
||||
v-list-item-title {{$t('profile:displayName')}}
|
||||
v-list-item-subtitle {{ user.name }}
|
||||
v-list-item-action
|
||||
v-menu(
|
||||
v-model='editPop.name'
|
||||
:close-on-content-click='false'
|
||||
min-width='350'
|
||||
left
|
||||
)
|
||||
template(v-slot:activator='{ on }')
|
||||
v-btn(text, color='grey', small, v-on='on', @click='focusField(`iptDisplayName`)')
|
||||
v-icon(left) mdi-pencil
|
||||
span {{ $t('common:actions:edit') }}
|
||||
v-card
|
||||
v-text-field(
|
||||
ref='iptDisplayName'
|
||||
v-model='user.name'
|
||||
:label='$t(`profile:displayName`)'
|
||||
solo
|
||||
hide-details
|
||||
append-icon='mdi-check'
|
||||
@click:append='editPop.name = false'
|
||||
@keydown.enter='editPop.name = false'
|
||||
@keydown.esc='editPop.name = false'
|
||||
)
|
||||
v-divider
|
||||
v-list-item
|
||||
v-list-item-avatar(size='32')
|
||||
v-icon mdi-map-marker
|
||||
v-list-item-content
|
||||
v-list-item-title {{$t('profile:location')}}
|
||||
v-list-item-subtitle {{ user.location }}
|
||||
v-list-item-action
|
||||
v-menu(
|
||||
v-model='editPop.location'
|
||||
:close-on-content-click='false'
|
||||
min-width='350'
|
||||
left
|
||||
)
|
||||
template(v-slot:activator='{ on }')
|
||||
v-btn(text, color='grey', small, v-on='on', @click='focusField(`iptLocation`)')
|
||||
v-icon(left) mdi-pencil
|
||||
span {{ $t('common:actions:edit') }}
|
||||
v-card
|
||||
v-text-field(
|
||||
ref='iptLocation'
|
||||
v-model='user.location'
|
||||
:label='$t(`profile:location`)'
|
||||
solo
|
||||
hide-details
|
||||
append-icon='mdi-check'
|
||||
@click:append='editPop.location = false'
|
||||
@keydown.enter='editPop.location = false'
|
||||
@keydown.esc='editPop.location = false'
|
||||
)
|
||||
v-divider
|
||||
v-list-item
|
||||
v-list-item-avatar(size='32')
|
||||
v-icon mdi-briefcase
|
||||
v-list-item-content
|
||||
v-list-item-title {{$t('profile:jobTitle')}}
|
||||
v-list-item-subtitle {{ user.jobTitle }}
|
||||
v-list-item-action
|
||||
v-menu(
|
||||
v-model='editPop.jobTitle'
|
||||
:close-on-content-click='false'
|
||||
min-width='350'
|
||||
left
|
||||
)
|
||||
template(v-slot:activator='{ on }')
|
||||
v-btn(text, color='grey', small, v-on='on', @click='focusField(`iptJobTitle`)')
|
||||
v-icon(left) mdi-pencil
|
||||
span {{ $t('common:actions:edit') }}
|
||||
v-card
|
||||
v-text-field(
|
||||
ref='iptJobTitle'
|
||||
v-model='user.jobTitle'
|
||||
:label='$t(`profile:jobTitle`)'
|
||||
solo
|
||||
hide-details
|
||||
append-icon='mdi-check'
|
||||
@click:append='editPop.jobTitle = false'
|
||||
@keydown.enter='editPop.jobTitle = false'
|
||||
@keydown.esc='editPop.jobTitle = false'
|
||||
)
|
||||
|
||||
v-card.mt-3.animated.fadeInUp.wait-p2s
|
||||
v-toolbar(color='blue-grey', dark, dense, flat)
|
||||
v-toolbar-title
|
||||
.subtitle-1 {{$t('profile:auth.title')}}
|
||||
v-card-text.pt-0
|
||||
v-subheader.pl-0: span.subtitle-2 {{$t('profile:auth.provider')}}
|
||||
v-toolbar(
|
||||
flat
|
||||
:color='$vuetify.theme.dark ? "grey darken-2" : "purple lighten-5"'
|
||||
dense
|
||||
:class='$vuetify.theme.dark ? "grey--text text--lighten-1" : "purple--text text--darken-4"'
|
||||
)
|
||||
v-icon(:color='$vuetify.theme.dark ? "grey lighten-1" : "purple darken-4"') mdi-shield-lock
|
||||
.subheading.ml-3 {{ user.providerName }}
|
||||
//- v-divider.mt-3
|
||||
//- v-subheader.pl-0: span.subtitle-2 Two-Factor Authentication (2FA)
|
||||
//- .caption.mb-2 2FA adds an extra layer of security by requiring a unique code generated on your smartphone when signing in.
|
||||
//- v-btn(color='purple darken-4', disabled).ml-0 Enable 2FA
|
||||
//- v-btn(color='purple darken-4', dark, depressed, disabled).ml-0 Disable 2FA
|
||||
template(v-if='user.providerKey === `local`')
|
||||
v-divider.mt-3
|
||||
v-subheader.pl-0: span.subtitle-2 {{$t('profile:auth.changePassword')}}
|
||||
v-text-field(
|
||||
ref='iptCurrentPass'
|
||||
v-model='currentPass'
|
||||
outlined
|
||||
:label='$t(`profile:auth.currentPassword`)'
|
||||
type='password'
|
||||
prepend-inner-icon='mdi-form-textbox-password'
|
||||
)
|
||||
v-text-field(
|
||||
ref='iptNewPass'
|
||||
v-model='newPass'
|
||||
outlined
|
||||
:label='$t(`profile:auth.newPassword`)'
|
||||
type='password'
|
||||
prepend-inner-icon='mdi-form-textbox-password'
|
||||
autocomplete='off'
|
||||
counter='255'
|
||||
loading
|
||||
)
|
||||
password-strength(slot='progress', v-model='newPass')
|
||||
v-text-field(
|
||||
ref='iptVerifyPass'
|
||||
v-model='verifyPass'
|
||||
outlined
|
||||
:label='$t(`profile:auth.verifyPassword`)'
|
||||
type='password'
|
||||
prepend-inner-icon='mdi-form-textbox-password'
|
||||
autocomplete='off'
|
||||
hide-details
|
||||
)
|
||||
v-card-chin(v-if='user.providerKey === `local`')
|
||||
v-spacer
|
||||
v-btn.px-4(color='purple darken-4', dark, depressed, @click='changePassword', :loading='changePassLoading')
|
||||
v-icon(left) mdi-progress-check
|
||||
span {{$t('profile:auth.changePassword')}}
|
||||
v-flex(lg6 xs12)
|
||||
//- v-card
|
||||
//- v-toolbar(color='blue-grey', dark, dense, flat)
|
||||
//- v-toolbar-title
|
||||
//- .subtitle-1 Picture
|
||||
//- v-card-title
|
||||
//- v-avatar.blue(v-if='picture.kind === `initials`', :size='40')
|
||||
//- span.white--text.subheading {{picture.initials}}
|
||||
//- v-avatar(v-else-if='picture.kind === `image`', :size='40')
|
||||
//- v-img(:src='picture.url')
|
||||
//- v-btn(outlined).mx-4 Upload Picture
|
||||
//- v-btn(outlined, disabled) Remove Picture
|
||||
v-card.animated.fadeInUp.wait-p2s
|
||||
v-toolbar(color='blue-grey', dark, dense, flat)
|
||||
v-toolbar-title.subtitle-1 {{$t('profile:preferences')}}
|
||||
v-list(two-line, dense)
|
||||
v-list-item
|
||||
v-list-item-avatar(size='32')
|
||||
v-icon mdi-map-clock-outline
|
||||
v-list-item-content
|
||||
v-list-item-title {{$t('profile:timezone')}}
|
||||
v-list-item-subtitle {{ user.timezone }}
|
||||
v-list-item-action
|
||||
v-menu(
|
||||
v-model='editPop.timezone'
|
||||
:close-on-content-click='false'
|
||||
min-width='350'
|
||||
max-width='350'
|
||||
left
|
||||
)
|
||||
template(v-slot:activator='{ on }')
|
||||
v-btn(text, color='grey', small, v-on='on', @click='focusField(`iptTimezone`)')
|
||||
v-icon(left) mdi-pencil
|
||||
span {{ $t('common:actions:edit') }}
|
||||
v-card(flat)
|
||||
v-select(
|
||||
ref='iptTimezone'
|
||||
:items='timezones'
|
||||
v-model='user.timezone'
|
||||
:label='$t(`profile:timezone`)'
|
||||
solo
|
||||
flat
|
||||
dense
|
||||
hide-details
|
||||
@keydown.enter='editPop.timezone = false'
|
||||
@keydown.esc='editPop.timezone = false'
|
||||
style='height: 38px;'
|
||||
)
|
||||
v-card-chin
|
||||
v-spacer
|
||||
v-btn(
|
||||
small
|
||||
text
|
||||
color='primary'
|
||||
@click='editPop.timezone = false'
|
||||
)
|
||||
v-icon(left) mdi-check
|
||||
span {{$t('common:actions.ok')}}
|
||||
v-divider
|
||||
v-list-item
|
||||
v-list-item-avatar(size='32')
|
||||
v-icon mdi-calendar-month-outline
|
||||
v-list-item-content
|
||||
v-list-item-title {{$t('profile:dateFormat')}}
|
||||
v-list-item-subtitle {{ user.dateFormat && user.dateFormat.length > 0 ? user.dateFormat : $t('profile:localeDefault') }}
|
||||
v-list-item-action
|
||||
v-menu(
|
||||
v-model='editPop.dateFormat'
|
||||
:close-on-content-click='false'
|
||||
min-width='350'
|
||||
max-width='350'
|
||||
left
|
||||
)
|
||||
template(v-slot:activator='{ on }')
|
||||
v-btn(text, color='grey', small, v-on='on', @click='focusField(`iptDateFormat`)')
|
||||
v-icon(left) mdi-pencil
|
||||
span {{ $t('common:actions:edit') }}
|
||||
v-card(flat)
|
||||
v-select(
|
||||
ref='iptDateFormat'
|
||||
:items='dateFormats'
|
||||
v-model='user.dateFormat'
|
||||
:label='$t(`profile:dateFormat`)'
|
||||
solo
|
||||
flat
|
||||
dense
|
||||
hide-details
|
||||
@keydown.enter='editPop.dateFormat = false'
|
||||
@keydown.esc='editPop.dateFormat = false'
|
||||
style='height: 38px;'
|
||||
)
|
||||
v-card-chin
|
||||
v-spacer
|
||||
v-btn(
|
||||
small
|
||||
text
|
||||
color='primary'
|
||||
@click='editPop.dateFormat = false'
|
||||
)
|
||||
v-icon(left) mdi-check
|
||||
span {{$t('common:actions.ok')}}
|
||||
v-divider
|
||||
v-list-item
|
||||
v-list-item-avatar(size='32')
|
||||
v-icon mdi-palette
|
||||
v-list-item-content
|
||||
v-list-item-title {{$t('profile:appearance')}}
|
||||
v-list-item-subtitle {{ currentAppearance }}
|
||||
v-list-item-action
|
||||
v-menu(
|
||||
v-model='editPop.appearance'
|
||||
:close-on-content-click='false'
|
||||
min-width='350'
|
||||
max-width='350'
|
||||
left
|
||||
)
|
||||
template(v-slot:activator='{ on }')
|
||||
v-btn(text, color='grey', small, v-on='on', @click='focusField(`iptAppearance`)')
|
||||
v-icon(left) mdi-pencil
|
||||
span {{ $t('common:actions:edit') }}
|
||||
v-card(flat)
|
||||
v-select(
|
||||
ref='iptAppearance'
|
||||
:items='appearances'
|
||||
v-model='user.appearance'
|
||||
:label='$t(`profile:appearance`)'
|
||||
solo
|
||||
flat
|
||||
dense
|
||||
hide-details
|
||||
@keydown.enter='editPop.appearance = false'
|
||||
@keydown.esc='editPop.appearance = false'
|
||||
style='height: 38px;'
|
||||
)
|
||||
v-card-chin
|
||||
v-spacer
|
||||
v-btn(
|
||||
small
|
||||
text
|
||||
color='primary'
|
||||
@click='editPop.appearance = false'
|
||||
)
|
||||
v-icon(left) mdi-check
|
||||
span {{$t('common:actions.ok')}}
|
||||
|
||||
v-card.mt-3.animated.fadeInUp.wait-p3s
|
||||
v-toolbar(color='primary', dark, dense, flat)
|
||||
v-toolbar-title
|
||||
.subtitle-1 {{$t('profile:groups.title')}}
|
||||
v-list(dense)
|
||||
template(v-for='(grp, idx) of user.groups')
|
||||
v-list-item(:key='`grp-id-` + grp')
|
||||
v-list-item-avatar(size='32')
|
||||
v-icon mdi-account-group
|
||||
v-list-item-content
|
||||
v-list-item-title.body-2 {{grp}}
|
||||
v-divider(v-if='idx < user.groups.length - 1')
|
||||
|
||||
v-card.mt-3.animated.fadeInUp.wait-p4s
|
||||
v-toolbar(color='teal', dark, dense, flat)
|
||||
v-toolbar-title
|
||||
.subtitle-1 {{$t('profile:activity.title')}}
|
||||
v-card-text.grey--text.text--darken-2
|
||||
.caption.grey--text {{$t('profile:activity.joinedOn')}}
|
||||
.body-2: strong {{ user.createdAt | moment('LLLL') }}
|
||||
.caption.grey--text.mt-3 {{$t('profile:activity.lastUpdatedOn')}}
|
||||
.body-2: strong {{ user.updatedAt | moment('LLLL') }}
|
||||
.caption.grey--text.mt-3 {{$t('profile:activity.lastLoginOn')}}
|
||||
.body-2: strong {{ user.lastLoginAt | moment('LLLL') }}
|
||||
v-divider.mt-3
|
||||
.caption.grey--text.mt-3 {{$t('profile:activity.pagesCreated')}}
|
||||
.body-2: strong {{ user.pagesTotal }}
|
||||
.caption.grey--text.mt-3 {{$t('profile:activity.commentsPosted')}}
|
||||
.body-2: strong 0
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { get } from 'vuex-pathify'
|
||||
import gql from 'graphql-tag'
|
||||
import _ from 'lodash'
|
||||
import Cookies from 'js-cookie'
|
||||
import validate from 'validate.js'
|
||||
|
||||
import PasswordStrength from '../common/password-strength.vue'
|
||||
|
||||
/* global WIKI, siteConfig */
|
||||
|
||||
export default {
|
||||
i18nOptions: {
|
||||
namespaces: ['profile', 'auth']
|
||||
},
|
||||
components: {
|
||||
PasswordStrength
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
saveLoading: false,
|
||||
changePassLoading: false,
|
||||
user: {
|
||||
name: 'unknown',
|
||||
location: '',
|
||||
jobTitle: '',
|
||||
timezone: '',
|
||||
dateFormat: '',
|
||||
appearance: '',
|
||||
createdAt: '1970-01-01',
|
||||
updatedAt: '1970-01-01',
|
||||
lastLoginAt: '1970-01-01',
|
||||
groups: []
|
||||
},
|
||||
currentPass: '',
|
||||
newPass: '',
|
||||
verifyPass: '',
|
||||
editPop: {
|
||||
name: false,
|
||||
location: false,
|
||||
jobTitle: false,
|
||||
timezone: false,
|
||||
dateFormat: false,
|
||||
appearance: false
|
||||
},
|
||||
timezones: [
|
||||
{ text: '(GMT-11:00) Niue', value: 'Pacific/Niue' },
|
||||
{ text: '(GMT-11:00) Pago Pago', value: 'Pacific/Pago_Pago' },
|
||||
{ text: '(GMT-10:00) Hawaii Time', value: 'Pacific/Honolulu' },
|
||||
{ text: '(GMT-10:00) Rarotonga', value: 'Pacific/Rarotonga' },
|
||||
{ text: '(GMT-10:00) Tahiti', value: 'Pacific/Tahiti' },
|
||||
{ text: '(GMT-09:30) Marquesas', value: 'Pacific/Marquesas' },
|
||||
{ text: '(GMT-09:00) Alaska Time', value: 'America/Anchorage' },
|
||||
{ text: '(GMT-09:00) Gambier', value: 'Pacific/Gambier' },
|
||||
{ text: '(GMT-08:00) Pacific Time', value: 'America/Los_Angeles' },
|
||||
{ text: '(GMT-08:00) Pacific Time - Tijuana', value: 'America/Tijuana' },
|
||||
{ text: '(GMT-08:00) Pacific Time - Vancouver', value: 'America/Vancouver' },
|
||||
{ text: '(GMT-08:00) Pacific Time - Whitehorse', value: 'America/Whitehorse' },
|
||||
{ text: '(GMT-08:00) Pitcairn', value: 'Pacific/Pitcairn' },
|
||||
{ text: '(GMT-07:00) Mountain Time', value: 'America/Denver' },
|
||||
{ text: '(GMT-07:00) Mountain Time - Arizona', value: 'America/Phoenix' },
|
||||
{ text: '(GMT-07:00) Mountain Time - Chihuahua, Mazatlan', value: 'America/Mazatlan' },
|
||||
{ text: '(GMT-07:00) Mountain Time - Dawson Creek', value: 'America/Dawson_Creek' },
|
||||
{ text: '(GMT-07:00) Mountain Time - Edmonton', value: 'America/Edmonton' },
|
||||
{ text: '(GMT-07:00) Mountain Time - Hermosillo', value: 'America/Hermosillo' },
|
||||
{ text: '(GMT-07:00) Mountain Time - Yellowknife', value: 'America/Yellowknife' },
|
||||
{ text: '(GMT-06:00) Belize', value: 'America/Belize' },
|
||||
{ text: '(GMT-06:00) Central Time', value: 'America/Chicago' },
|
||||
{ text: '(GMT-06:00) Central Time - Mexico City', value: 'America/Mexico_City' },
|
||||
{ text: '(GMT-06:00) Central Time - Regina', value: 'America/Regina' },
|
||||
{ text: '(GMT-06:00) Central Time - Tegucigalpa', value: 'America/Tegucigalpa' },
|
||||
{ text: '(GMT-06:00) Central Time - Winnipeg', value: 'America/Winnipeg' },
|
||||
{ text: '(GMT-06:00) Costa Rica', value: 'America/Costa_Rica' },
|
||||
{ text: '(GMT-06:00) El Salvador', value: 'America/El_Salvador' },
|
||||
{ text: '(GMT-06:00) Galapagos', value: 'Pacific/Galapagos' },
|
||||
{ text: '(GMT-06:00) Guatemala', value: 'America/Guatemala' },
|
||||
{ text: '(GMT-06:00) Managua', value: 'America/Managua' },
|
||||
{ text: '(GMT-05:00) America Cancun', value: 'America/Cancun' },
|
||||
{ text: '(GMT-05:00) Bogota', value: 'America/Bogota' },
|
||||
{ text: '(GMT-05:00) Easter Island', value: 'Pacific/Easter' },
|
||||
{ text: '(GMT-05:00) Eastern Time', value: 'America/New_York' },
|
||||
{ text: '(GMT-05:00) Eastern Time - Iqaluit', value: 'America/Iqaluit' },
|
||||
{ text: '(GMT-05:00) Eastern Time - Toronto', value: 'America/Toronto' },
|
||||
{ text: '(GMT-05:00) Guayaquil', value: 'America/Guayaquil' },
|
||||
{ text: '(GMT-05:00) Havana', value: 'America/Havana' },
|
||||
{ text: '(GMT-05:00) Jamaica', value: 'America/Jamaica' },
|
||||
{ text: '(GMT-05:00) Lima', value: 'America/Lima' },
|
||||
{ text: '(GMT-05:00) Nassau', value: 'America/Nassau' },
|
||||
{ text: '(GMT-05:00) Panama', value: 'America/Panama' },
|
||||
{ text: '(GMT-05:00) Port-au-Prince', value: 'America/Port-au-Prince' },
|
||||
{ text: '(GMT-05:00) Rio Branco', value: 'America/Rio_Branco' },
|
||||
{ text: '(GMT-04:00) Atlantic Time - Halifax', value: 'America/Halifax' },
|
||||
{ text: '(GMT-04:00) Barbados', value: 'America/Barbados' },
|
||||
{ text: '(GMT-04:00) Bermuda', value: 'Atlantic/Bermuda' },
|
||||
{ text: '(GMT-04:00) Boa Vista', value: 'America/Boa_Vista' },
|
||||
{ text: '(GMT-04:00) Caracas', value: 'America/Caracas' },
|
||||
{ text: '(GMT-04:00) Curacao', value: 'America/Curacao' },
|
||||
{ text: '(GMT-04:00) Grand Turk', value: 'America/Grand_Turk' },
|
||||
{ text: '(GMT-04:00) Guyana', value: 'America/Guyana' },
|
||||
{ text: '(GMT-04:00) La Paz', value: 'America/La_Paz' },
|
||||
{ text: '(GMT-04:00) Manaus', value: 'America/Manaus' },
|
||||
{ text: '(GMT-04:00) Martinique', value: 'America/Martinique' },
|
||||
{ text: '(GMT-04:00) Port of Spain', value: 'America/Port_of_Spain' },
|
||||
{ text: '(GMT-04:00) Porto Velho', value: 'America/Porto_Velho' },
|
||||
{ text: '(GMT-04:00) Puerto Rico', value: 'America/Puerto_Rico' },
|
||||
{ text: '(GMT-04:00) Santo Domingo', value: 'America/Santo_Domingo' },
|
||||
{ text: '(GMT-04:00) Thule', value: 'America/Thule' },
|
||||
{ text: '(GMT-03:30) Newfoundland Time - St. Johns', value: 'America/St_Johns' },
|
||||
{ text: '(GMT-03:00) Araguaina', value: 'America/Araguaina' },
|
||||
{ text: '(GMT-03:00) Asuncion', value: 'America/Asuncion' },
|
||||
{ text: '(GMT-03:00) Belem', value: 'America/Belem' },
|
||||
{ text: '(GMT-03:00) Buenos Aires', value: 'America/Argentina/Buenos_Aires' },
|
||||
{ text: '(GMT-03:00) Campo Grande', value: 'America/Campo_Grande' },
|
||||
{ text: '(GMT-03:00) Cayenne', value: 'America/Cayenne' },
|
||||
{ text: '(GMT-03:00) Cuiaba', value: 'America/Cuiaba' },
|
||||
{ text: '(GMT-03:00) Fortaleza', value: 'America/Fortaleza' },
|
||||
{ text: '(GMT-03:00) Godthab', value: 'America/Godthab' },
|
||||
{ text: '(GMT-03:00) Maceio', value: 'America/Maceio' },
|
||||
{ text: '(GMT-03:00) Miquelon', value: 'America/Miquelon' },
|
||||
{ text: '(GMT-03:00) Montevideo', value: 'America/Montevideo' },
|
||||
{ text: '(GMT-03:00) Palmer', value: 'Antarctica/Palmer' },
|
||||
{ text: '(GMT-03:00) Paramaribo', value: 'America/Paramaribo' },
|
||||
{ text: '(GMT-03:00) Punta Arenas', value: 'America/Punta_Arenas' },
|
||||
{ text: '(GMT-03:00) Recife', value: 'America/Recife' },
|
||||
{ text: '(GMT-03:00) Rothera', value: 'Antarctica/Rothera' },
|
||||
{ text: '(GMT-03:00) Salvador', value: 'America/Bahia' },
|
||||
{ text: '(GMT-03:00) Santiago', value: 'America/Santiago' },
|
||||
{ text: '(GMT-03:00) Stanley', value: 'Atlantic/Stanley' },
|
||||
{ text: '(GMT-02:00) Noronha', value: 'America/Noronha' },
|
||||
{ text: '(GMT-02:00) Sao Paulo', value: 'America/Sao_Paulo' },
|
||||
{ text: '(GMT-02:00) South Georgia', value: 'Atlantic/South_Georgia' },
|
||||
{ text: '(GMT-01:00) Azores', value: 'Atlantic/Azores' },
|
||||
{ text: '(GMT-01:00) Cape Verde', value: 'Atlantic/Cape_Verde' },
|
||||
{ text: '(GMT-01:00) Scoresbysund', value: 'America/Scoresbysund' },
|
||||
{ text: '(GMT+00:00) Abidjan', value: 'Africa/Abidjan' },
|
||||
{ text: '(GMT+00:00) Accra', value: 'Africa/Accra' },
|
||||
{ text: '(GMT+00:00) Bissau', value: 'Africa/Bissau' },
|
||||
{ text: '(GMT+00:00) Canary Islands', value: 'Atlantic/Canary' },
|
||||
{ text: '(GMT+00:00) Casablanca', value: 'Africa/Casablanca' },
|
||||
{ text: '(GMT+00:00) Danmarkshavn', value: 'America/Danmarkshavn' },
|
||||
{ text: '(GMT+00:00) Dublin', value: 'Europe/Dublin' },
|
||||
{ text: '(GMT+00:00) El Aaiun', value: 'Africa/El_Aaiun' },
|
||||
{ text: '(GMT+00:00) Faeroe', value: 'Atlantic/Faroe' },
|
||||
{ text: '(GMT+00:00) GMT (no daylight saving)', value: 'Etc/GMT' },
|
||||
{ text: '(GMT+00:00) Lisbon', value: 'Europe/Lisbon' },
|
||||
{ text: '(GMT+00:00) London', value: 'Europe/London' },
|
||||
{ text: '(GMT+00:00) Monrovia', value: 'Africa/Monrovia' },
|
||||
{ text: '(GMT+00:00) Reykjavik', value: 'Atlantic/Reykjavik' },
|
||||
{ text: '(GMT+01:00) Algiers', value: 'Africa/Algiers' },
|
||||
{ text: '(GMT+01:00) Amsterdam', value: 'Europe/Amsterdam' },
|
||||
{ text: '(GMT+01:00) Andorra', value: 'Europe/Andorra' },
|
||||
{ text: '(GMT+01:00) Berlin', value: 'Europe/Berlin' },
|
||||
{ text: '(GMT+01:00) Brussels', value: 'Europe/Brussels' },
|
||||
{ text: '(GMT+01:00) Budapest', value: 'Europe/Budapest' },
|
||||
{ text: '(GMT+01:00) Central European Time - Belgrade', value: 'Europe/Belgrade' },
|
||||
{ text: '(GMT+01:00) Central European Time - Prague', value: 'Europe/Prague' },
|
||||
{ text: '(GMT+01:00) Ceuta', value: 'Africa/Ceuta' },
|
||||
{ text: '(GMT+01:00) Copenhagen', value: 'Europe/Copenhagen' },
|
||||
{ text: '(GMT+01:00) Gibraltar', value: 'Europe/Gibraltar' },
|
||||
{ text: '(GMT+01:00) Lagos', value: 'Africa/Lagos' },
|
||||
{ text: '(GMT+01:00) Luxembourg', value: 'Europe/Luxembourg' },
|
||||
{ text: '(GMT+01:00) Madrid', value: 'Europe/Madrid' },
|
||||
{ text: '(GMT+01:00) Malta', value: 'Europe/Malta' },
|
||||
{ text: '(GMT+01:00) Monaco', value: 'Europe/Monaco' },
|
||||
{ text: '(GMT+01:00) Ndjamena', value: 'Africa/Ndjamena' },
|
||||
{ text: '(GMT+01:00) Oslo', value: 'Europe/Oslo' },
|
||||
{ text: '(GMT+01:00) Paris', value: 'Europe/Paris' },
|
||||
{ text: '(GMT+01:00) Rome', value: 'Europe/Rome' },
|
||||
{ text: '(GMT+01:00) Stockholm', value: 'Europe/Stockholm' },
|
||||
{ text: '(GMT+01:00) Tirane', value: 'Europe/Tirane' },
|
||||
{ text: '(GMT+01:00) Tunis', value: 'Africa/Tunis' },
|
||||
{ text: '(GMT+01:00) Vienna', value: 'Europe/Vienna' },
|
||||
{ text: '(GMT+01:00) Warsaw', value: 'Europe/Warsaw' },
|
||||
{ text: '(GMT+01:00) Zurich', value: 'Europe/Zurich' },
|
||||
{ text: '(GMT+02:00) Amman', value: 'Asia/Amman' },
|
||||
{ text: '(GMT+02:00) Athens', value: 'Europe/Athens' },
|
||||
{ text: '(GMT+02:00) Beirut', value: 'Asia/Beirut' },
|
||||
{ text: '(GMT+02:00) Bucharest', value: 'Europe/Bucharest' },
|
||||
{ text: '(GMT+02:00) Cairo', value: 'Africa/Cairo' },
|
||||
{ text: '(GMT+02:00) Chisinau', value: 'Europe/Chisinau' },
|
||||
{ text: '(GMT+02:00) Damascus', value: 'Asia/Damascus' },
|
||||
{ text: '(GMT+02:00) Gaza', value: 'Asia/Gaza' },
|
||||
{ text: '(GMT+02:00) Helsinki', value: 'Europe/Helsinki' },
|
||||
{ text: '(GMT+02:00) Jerusalem', value: 'Asia/Jerusalem' },
|
||||
{ text: '(GMT+02:00) Johannesburg', value: 'Africa/Johannesburg' },
|
||||
{ text: '(GMT+02:00) Khartoum', value: 'Africa/Khartoum' },
|
||||
{ text: '(GMT+02:00) Kyiv', value: 'Europe/Kyiv' },
|
||||
{ text: '(GMT+02:00) Maputo', value: 'Africa/Maputo' },
|
||||
{ text: '(GMT+02:00) Moscow-01 - Kaliningrad', value: 'Europe/Kaliningrad' },
|
||||
{ text: '(GMT+02:00) Nicosia', value: 'Asia/Nicosia' },
|
||||
{ text: '(GMT+02:00) Riga', value: 'Europe/Riga' },
|
||||
{ text: '(GMT+02:00) Sofia', value: 'Europe/Sofia' },
|
||||
{ text: '(GMT+02:00) Tallinn', value: 'Europe/Tallinn' },
|
||||
{ text: '(GMT+02:00) Tripoli', value: 'Africa/Tripoli' },
|
||||
{ text: '(GMT+02:00) Vilnius', value: 'Europe/Vilnius' },
|
||||
{ text: '(GMT+02:00) Windhoek', value: 'Africa/Windhoek' },
|
||||
{ text: '(GMT+03:00) Baghdad', value: 'Asia/Baghdad' },
|
||||
{ text: '(GMT+03:00) Istanbul', value: 'Europe/Istanbul' },
|
||||
{ text: '(GMT+03:00) Minsk', value: 'Europe/Minsk' },
|
||||
{ text: '(GMT+03:00) Moscow+00 - Moscow', value: 'Europe/Moscow' },
|
||||
{ text: '(GMT+03:00) Nairobi', value: 'Africa/Nairobi' },
|
||||
{ text: '(GMT+03:00) Qatar', value: 'Asia/Qatar' },
|
||||
{ text: '(GMT+03:00) Riyadh', value: 'Asia/Riyadh' },
|
||||
{ text: '(GMT+03:00) Syowa', value: 'Antarctica/Syowa' },
|
||||
{ text: '(GMT+03:30) Tehran', value: 'Asia/Tehran' },
|
||||
{ text: '(GMT+04:00) Baku', value: 'Asia/Baku' },
|
||||
{ text: '(GMT+04:00) Dubai', value: 'Asia/Dubai' },
|
||||
{ text: '(GMT+04:00) Mahe', value: 'Indian/Mahe' },
|
||||
{ text: '(GMT+04:00) Mauritius', value: 'Indian/Mauritius' },
|
||||
{ text: '(GMT+04:00) Moscow+01 - Samara', value: 'Europe/Samara' },
|
||||
{ text: '(GMT+04:00) Reunion', value: 'Indian/Reunion' },
|
||||
{ text: '(GMT+04:00) Tbilisi', value: 'Asia/Tbilisi' },
|
||||
{ text: '(GMT+04:00) Yerevan', value: 'Asia/Yerevan' },
|
||||
{ text: '(GMT+04:30) Kabul', value: 'Asia/Kabul' },
|
||||
{ text: '(GMT+05:00) Aqtau', value: 'Asia/Aqtau' },
|
||||
{ text: '(GMT+05:00) Aqtobe', value: 'Asia/Aqtobe' },
|
||||
{ text: '(GMT+05:00) Ashgabat', value: 'Asia/Ashgabat' },
|
||||
{ text: '(GMT+05:00) Dushanbe', value: 'Asia/Dushanbe' },
|
||||
{ text: '(GMT+05:00) Karachi', value: 'Asia/Karachi' },
|
||||
{ text: '(GMT+05:00) Kerguelen', value: 'Indian/Kerguelen' },
|
||||
{ text: '(GMT+05:00) Maldives', value: 'Indian/Maldives' },
|
||||
{ text: '(GMT+05:00) Mawson', value: 'Antarctica/Mawson' },
|
||||
{ text: '(GMT+05:00) Moscow+02 - Yekaterinburg', value: 'Asia/Yekaterinburg' },
|
||||
{ text: '(GMT+05:00) Tashkent', value: 'Asia/Tashkent' },
|
||||
{ text: '(GMT+05:30) Colombo', value: 'Asia/Colombo' },
|
||||
{ text: '(GMT+05:30) India Standard Time', value: 'Asia/Kolkata' },
|
||||
{ text: '(GMT+05:45) Kathmandu', value: 'Asia/Kathmandu' },
|
||||
{ text: '(GMT+06:00) Almaty', value: 'Asia/Almaty' },
|
||||
{ text: '(GMT+06:00) Bishkek', value: 'Asia/Bishkek' },
|
||||
{ text: '(GMT+06:00) Chagos', value: 'Indian/Chagos' },
|
||||
{ text: '(GMT+06:00) Dhaka', value: 'Asia/Dhaka' },
|
||||
{ text: '(GMT+06:00) Moscow+03 - Omsk', value: 'Asia/Omsk' },
|
||||
{ text: '(GMT+06:00) Thimphu', value: 'Asia/Thimphu' },
|
||||
{ text: '(GMT+06:00) Vostok', value: 'Antarctica/Vostok' },
|
||||
{ text: '(GMT+06:30) Cocos', value: 'Indian/Cocos' },
|
||||
{ text: '(GMT+06:30) Rangoon', value: 'Asia/Yangon' },
|
||||
{ text: '(GMT+07:00) Bangkok', value: 'Asia/Bangkok' },
|
||||
{ text: '(GMT+07:00) Christmas', value: 'Indian/Christmas' },
|
||||
{ text: '(GMT+07:00) Davis', value: 'Antarctica/Davis' },
|
||||
{ text: '(GMT+07:00) Hanoi', value: 'Asia/Saigon' },
|
||||
{ text: '(GMT+07:00) Hovd', value: 'Asia/Hovd' },
|
||||
{ text: '(GMT+07:00) Jakarta', value: 'Asia/Jakarta' },
|
||||
{ text: '(GMT+07:00) Moscow+04 - Krasnoyarsk', value: 'Asia/Krasnoyarsk' },
|
||||
{ text: '(GMT+08:00) Brunei', value: 'Asia/Brunei' },
|
||||
{ text: '(GMT+08:00) China Time - Beijing', value: 'Asia/Shanghai' },
|
||||
{ text: '(GMT+08:00) Choibalsan', value: 'Asia/Choibalsan' },
|
||||
{ text: '(GMT+08:00) Hong Kong', value: 'Asia/Hong_Kong' },
|
||||
{ text: '(GMT+08:00) Kuala Lumpur', value: 'Asia/Kuala_Lumpur' },
|
||||
{ text: '(GMT+08:00) Macau', value: 'Asia/Macau' },
|
||||
{ text: '(GMT+08:00) Makassar', value: 'Asia/Makassar' },
|
||||
{ text: '(GMT+08:00) Manila', value: 'Asia/Manila' },
|
||||
{ text: '(GMT+08:00) Moscow+05 - Irkutsk', value: 'Asia/Irkutsk' },
|
||||
{ text: '(GMT+08:00) Singapore', value: 'Asia/Singapore' },
|
||||
{ text: '(GMT+08:00) Taipei', value: 'Asia/Taipei' },
|
||||
{ text: '(GMT+08:00) Ulaanbaatar', value: 'Asia/Ulaanbaatar' },
|
||||
{ text: '(GMT+08:00) Western Time - Perth', value: 'Australia/Perth' },
|
||||
{ text: '(GMT+08:30) Pyongyang', value: 'Asia/Pyongyang' },
|
||||
{ text: '(GMT+09:00) Dili', value: 'Asia/Dili' },
|
||||
{ text: '(GMT+09:00) Jayapura', value: 'Asia/Jayapura' },
|
||||
{ text: '(GMT+09:00) Moscow+06 - Yakutsk', value: 'Asia/Yakutsk' },
|
||||
{ text: '(GMT+09:00) Palau', value: 'Pacific/Palau' },
|
||||
{ text: '(GMT+09:00) Seoul', value: 'Asia/Seoul' },
|
||||
{ text: '(GMT+09:00) Tokyo', value: 'Asia/Tokyo' },
|
||||
{ text: '(GMT+09:30) Central Time - Darwin', value: 'Australia/Darwin' },
|
||||
{ text: '(GMT+10:00) Dumont D\'Urville', value: 'Antarctica/DumontDUrville' },
|
||||
{ text: '(GMT+10:00) Eastern Time - Brisbane', value: 'Australia/Brisbane' },
|
||||
{ text: '(GMT+10:00) Guam', value: 'Pacific/Guam' },
|
||||
{ text: '(GMT+10:00) Moscow+07 - Vladivostok', value: 'Asia/Vladivostok' },
|
||||
{ text: '(GMT+10:00) Port Moresby', value: 'Pacific/Port_Moresby' },
|
||||
{ text: '(GMT+10:00) Truk', value: 'Pacific/Chuuk' },
|
||||
{ text: '(GMT+10:30) Central Time - Adelaide', value: 'Australia/Adelaide' },
|
||||
{ text: '(GMT+11:00) Casey', value: 'Antarctica/Casey' },
|
||||
{ text: '(GMT+11:00) Eastern Time - Hobart', value: 'Australia/Hobart' },
|
||||
{ text: '(GMT+11:00) Eastern Time - Melbourne, Sydney', value: 'Australia/Sydney' },
|
||||
{ text: '(GMT+11:00) Efate', value: 'Pacific/Efate' },
|
||||
{ text: '(GMT+11:00) Guadalcanal', value: 'Pacific/Guadalcanal' },
|
||||
{ text: '(GMT+11:00) Kosrae', value: 'Pacific/Kosrae' },
|
||||
{ text: '(GMT+11:00) Moscow+08 - Magadan', value: 'Asia/Magadan' },
|
||||
{ text: '(GMT+11:00) Norfolk', value: 'Pacific/Norfolk' },
|
||||
{ text: '(GMT+11:00) Noumea', value: 'Pacific/Noumea' },
|
||||
{ text: '(GMT+11:00) Ponape', value: 'Pacific/Pohnpei' },
|
||||
{ text: '(GMT+12:00) Funafuti', value: 'Pacific/Funafuti' },
|
||||
{ text: '(GMT+12:00) Kwajalein', value: 'Pacific/Kwajalein' },
|
||||
{ text: '(GMT+12:00) Majuro', value: 'Pacific/Majuro' },
|
||||
{ text: '(GMT+12:00) Moscow+09 - Petropavlovsk-Kamchatskiy', value: 'Asia/Kamchatka' },
|
||||
{ text: '(GMT+12:00) Nauru', value: 'Pacific/Nauru' },
|
||||
{ text: '(GMT+12:00) Tarawa', value: 'Pacific/Tarawa' },
|
||||
{ text: '(GMT+12:00) Wake', value: 'Pacific/Wake' },
|
||||
{ text: '(GMT+12:00) Wallis', value: 'Pacific/Wallis' },
|
||||
{ text: '(GMT+13:00) Auckland', value: 'Pacific/Auckland' },
|
||||
{ text: '(GMT+13:00) Enderbury', value: 'Pacific/Enderbury' },
|
||||
{ text: '(GMT+13:00) Fakaofo', value: 'Pacific/Fakaofo' },
|
||||
{ text: '(GMT+13:00) Fiji', value: 'Pacific/Fiji' },
|
||||
{ text: '(GMT+13:00) Tongatapu', value: 'Pacific/Tongatapu' },
|
||||
{ text: '(GMT+14:00) Apia', value: 'Pacific/Apia' },
|
||||
{ text: '(GMT+14:00) Kiritimati', value: 'Pacific/Kiritimati' }
|
||||
]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
dateFormats () {
|
||||
return [
|
||||
{ text: this.$t('profile:localeDefault'), value: '' },
|
||||
{ text: 'DD/MM/YYYY', value: 'DD/MM/YYYY' },
|
||||
{ text: 'DD.MM.YYYY', value: 'DD.MM.YYYY' },
|
||||
{ text: 'MM/DD/YYYY', value: 'MM/DD/YYYY' },
|
||||
{ text: 'YYYY-MM-DD', value: 'YYYY-MM-DD' },
|
||||
{ text: 'YYYY/MM/DD', value: 'YYYY/MM/DD' }
|
||||
]
|
||||
},
|
||||
appearances () {
|
||||
return [
|
||||
{ text: this.$t('profile:appearanceDefault'), value: '' },
|
||||
{ text: this.$t('profile:appearanceLight'), value: 'light' },
|
||||
{ text: this.$t('profile:appearanceDark'), value: 'dark' }
|
||||
]
|
||||
},
|
||||
currentAppearance () {
|
||||
return _.get(_.find(this.appearances, ['value', this.user.appearance]), 'text', false) || this.$t('profile:appearanceDefault')
|
||||
},
|
||||
pictureUrl: get('user/pictureUrl'),
|
||||
picture () {
|
||||
if (this.pictureUrl && this.pictureUrl.length > 1) {
|
||||
return {
|
||||
kind: 'image',
|
||||
url: this.pictureUrl
|
||||
}
|
||||
} else {
|
||||
const nameParts = this.user.name.toUpperCase().split(' ')
|
||||
let initials = _.head(nameParts).charAt(0)
|
||||
if (nameParts.length > 1) {
|
||||
initials += _.last(nameParts).charAt(0)
|
||||
}
|
||||
return {
|
||||
kind: 'initials',
|
||||
initials
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'user.appearance': (newValue, oldValue) => {
|
||||
if (newValue === '') {
|
||||
WIKI.$vuetify.theme.dark = siteConfig.darkMode
|
||||
} else {
|
||||
WIKI.$vuetify.theme.dark = (newValue === 'dark')
|
||||
}
|
||||
},
|
||||
'user.dateFormat': (newValue, oldValue) => {
|
||||
if (newValue === '') {
|
||||
WIKI.$moment.updateLocale(WIKI.$moment.locale(), null)
|
||||
} else {
|
||||
WIKI.$moment.updateLocale(WIKI.$moment.locale(), {
|
||||
longDateFormat: {
|
||||
'L': newValue
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
'user.timezone': (newValue, oldValue) => {
|
||||
if (newValue === '') {
|
||||
WIKI.$moment.tz.setDefault()
|
||||
} else {
|
||||
WIKI.$moment.tz.setDefault(newValue)
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* Focus an input after delay
|
||||
*/
|
||||
focusField (ipt) {
|
||||
this.$nextTick(() => {
|
||||
_.delay(() => {
|
||||
this.$refs[ipt].focus()
|
||||
}, 200)
|
||||
})
|
||||
},
|
||||
/**
|
||||
* Save User Profile
|
||||
*/
|
||||
async saveProfile () {
|
||||
this.saveLoading = true
|
||||
this.$store.commit(`loadingStart`, 'profile-save')
|
||||
|
||||
try {
|
||||
const respRaw = await this.$apollo.mutate({
|
||||
mutation: gql`
|
||||
mutation ($name: String!, $location: String!, $jobTitle: String!, $timezone: String!, $dateFormat: String!, $appearance: String!) {
|
||||
users {
|
||||
updateProfile(name: $name, location: $location, jobTitle: $jobTitle, timezone: $timezone, dateFormat: $dateFormat, appearance: $appearance) {
|
||||
responseResult {
|
||||
succeeded
|
||||
errorCode
|
||||
slug
|
||||
message
|
||||
}
|
||||
jwt
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
name: this.user.name,
|
||||
location: this.user.location,
|
||||
jobTitle: this.user.jobTitle,
|
||||
timezone: this.user.timezone,
|
||||
dateFormat: this.user.dateFormat,
|
||||
appearance: this.user.appearance
|
||||
}
|
||||
})
|
||||
const resp = _.get(respRaw, 'data.users.updateProfile.responseResult', {})
|
||||
if (resp.succeeded) {
|
||||
Cookies.set('jwt', _.get(respRaw, 'data.users.updateProfile.jwt', ''), { expires: 365 })
|
||||
this.$store.set('user/name', this.user.name)
|
||||
this.$store.commit('showNotification', {
|
||||
message: this.$t('profile:save.success'),
|
||||
style: 'success',
|
||||
icon: 'check'
|
||||
})
|
||||
} else {
|
||||
throw new Error(resp.message)
|
||||
}
|
||||
} catch (err) {
|
||||
this.$store.commit('pushGraphError', err)
|
||||
}
|
||||
|
||||
this.$store.commit(`loadingStop`, 'profile-save')
|
||||
this.saveLoading = false
|
||||
},
|
||||
/**
|
||||
* Change Password
|
||||
*/
|
||||
async changePassword () {
|
||||
const validation = validate({
|
||||
current: this.currentPass,
|
||||
password: this.newPass,
|
||||
verifyPassword: this.verifyPass
|
||||
}, {
|
||||
current: {
|
||||
presence: {
|
||||
message: this.$t('auth:missingPassword'),
|
||||
allowEmpty: false
|
||||
},
|
||||
length: {
|
||||
minimum: 6,
|
||||
tooShort: this.$t('auth:passwordTooShort')
|
||||
}
|
||||
},
|
||||
password: {
|
||||
presence: {
|
||||
message: this.$t('auth:missingPassword'),
|
||||
allowEmpty: false
|
||||
},
|
||||
length: {
|
||||
minimum: 6,
|
||||
tooShort: this.$t('auth:passwordTooShort')
|
||||
}
|
||||
},
|
||||
verifyPassword: {
|
||||
equality: {
|
||||
attribute: 'password',
|
||||
message: this.$t('auth:passwordNotMatch')
|
||||
}
|
||||
}
|
||||
}, { fullMessages: false })
|
||||
|
||||
if (validation) {
|
||||
if (validation.current) {
|
||||
this.$store.commit('showNotification', {
|
||||
style: 'red',
|
||||
message: validation.current[0],
|
||||
icon: 'warning'
|
||||
})
|
||||
this.$refs.iptCurrentPass.focus()
|
||||
} else if (validation.password) {
|
||||
this.$store.commit('showNotification', {
|
||||
style: 'red',
|
||||
message: validation.password[0],
|
||||
icon: 'warning'
|
||||
})
|
||||
this.$refs.iptNewPass.focus()
|
||||
} else if (validation.verifyPassword) {
|
||||
this.$store.commit('showNotification', {
|
||||
style: 'red',
|
||||
message: validation.verifyPassword[0],
|
||||
icon: 'warning'
|
||||
})
|
||||
this.$refs.iptVerifyPass.focus()
|
||||
}
|
||||
} else {
|
||||
this.changePassLoading = true
|
||||
this.$store.commit(`loadingStart`, 'profile-changepassword')
|
||||
|
||||
try {
|
||||
const respRaw = await this.$apollo.mutate({
|
||||
mutation: gql`
|
||||
mutation ($current: String!, $new: String!) {
|
||||
users {
|
||||
changePassword(current: $current, new: $new) {
|
||||
responseResult {
|
||||
succeeded
|
||||
errorCode
|
||||
slug
|
||||
message
|
||||
}
|
||||
jwt
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
current: this.currentPass,
|
||||
new: this.newPass
|
||||
}
|
||||
})
|
||||
const resp = _.get(respRaw, 'data.users.changePassword.responseResult', {})
|
||||
if (resp.succeeded) {
|
||||
this.currentPass = ''
|
||||
this.newPass = ''
|
||||
this.verifyPass = ''
|
||||
Cookies.set('jwt', _.get(respRaw, 'data.users.changePassword.jwt', ''), { expires: 365 })
|
||||
this.$store.commit('showNotification', {
|
||||
message: this.$t('profile:auth.changePassSuccess'),
|
||||
style: 'success',
|
||||
icon: 'check'
|
||||
})
|
||||
} else {
|
||||
throw new Error(resp.message)
|
||||
}
|
||||
} catch (err) {
|
||||
this.$store.commit('pushGraphError', err)
|
||||
}
|
||||
|
||||
this.$store.commit(`loadingStop`, 'profile-changepassword')
|
||||
this.changePassLoading = false
|
||||
}
|
||||
}
|
||||
},
|
||||
apollo: {
|
||||
user: {
|
||||
query: gql`
|
||||
{
|
||||
users {
|
||||
profile {
|
||||
id
|
||||
name
|
||||
email
|
||||
providerKey
|
||||
providerName
|
||||
isSystem
|
||||
isVerified
|
||||
location
|
||||
jobTitle
|
||||
timezone
|
||||
dateFormat
|
||||
appearance
|
||||
createdAt
|
||||
updatedAt
|
||||
lastLoginAt
|
||||
groups
|
||||
pagesTotal
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
fetchPolicy: 'network-only',
|
||||
update: (data) => _.cloneDeep(data.users.profile),
|
||||
watchLoading (isLoading) {
|
||||
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'profile-refresh')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang='scss'>
|
||||
|
||||
</style>
|
@ -1,306 +0,0 @@
|
||||
<template lang="pug">
|
||||
v-app
|
||||
.register
|
||||
v-container(grid-list-lg)
|
||||
v-layout(row, wrap)
|
||||
v-flex(
|
||||
xs12
|
||||
offset-sm1, sm10
|
||||
offset-md2, md8
|
||||
offset-lg3, lg6
|
||||
offset-xl4, xl4
|
||||
)
|
||||
transition(name='fadeUp')
|
||||
v-card.elevation-5.md2(v-show='isShown')
|
||||
v-toolbar(color='indigo', flat, dense, dark)
|
||||
v-spacer
|
||||
.subheading {{ $t('auth:registerTitle') }}
|
||||
v-spacer
|
||||
v-card-text.text-center
|
||||
h1.display-1.indigo--text.py-2 {{ siteTitle }}
|
||||
.body-2 {{ $t('auth:registerSubTitle') }}
|
||||
v-text-field.md2.mt-3(
|
||||
solo
|
||||
flat
|
||||
prepend-icon='mdi-email'
|
||||
:background-color='$vuetify.theme.dark ? `grey darken-3` : `grey lighten-4`'
|
||||
hide-details
|
||||
ref='iptEmail'
|
||||
v-model='email'
|
||||
:placeholder='$t("auth:fields.email")'
|
||||
color='indigo'
|
||||
)
|
||||
v-text-field.md2.mt-2(
|
||||
solo
|
||||
flat
|
||||
prepend-icon='mdi-form-textbox-password'
|
||||
:background-color='$vuetify.theme.dark ? `grey darken-3` : `grey lighten-4`'
|
||||
ref='iptPassword'
|
||||
v-model='password'
|
||||
:append-icon='hidePassword ? "mdi-eye-off" : "mdi-eye"'
|
||||
@click:append='() => (hidePassword = !hidePassword)'
|
||||
:type='hidePassword ? "password" : "text"'
|
||||
:placeholder='$t("auth:fields.password")'
|
||||
color='indigo'
|
||||
loading
|
||||
counter='255'
|
||||
)
|
||||
password-strength(slot='progress', v-model='password')
|
||||
v-text-field.md2.mt-2(
|
||||
solo
|
||||
flat
|
||||
prepend-icon='mdi-form-textbox-password'
|
||||
:background-color='$vuetify.theme.dark ? `grey darken-3` : `grey lighten-4`'
|
||||
hide-details
|
||||
ref='iptVerifyPassword'
|
||||
v-model='verifyPassword'
|
||||
@click:append='() => (hidePassword = !hidePassword)'
|
||||
type='password'
|
||||
:placeholder='$t("auth:fields.verifyPassword")'
|
||||
color='indigo'
|
||||
)
|
||||
v-text-field.md2.mt-2(
|
||||
solo
|
||||
flat
|
||||
prepend-icon='mdi-account'
|
||||
:background-color='$vuetify.theme.dark ? `grey darken-3` : `grey lighten-4`'
|
||||
ref='iptName'
|
||||
v-model='name'
|
||||
:placeholder='$t("auth:fields.name")'
|
||||
@keyup.enter='register'
|
||||
color='indigo'
|
||||
counter='255'
|
||||
)
|
||||
v-card-actions.pb-4
|
||||
v-spacer
|
||||
v-btn.md2(
|
||||
width='100%'
|
||||
max-width='250px'
|
||||
large
|
||||
dark
|
||||
color='indigo'
|
||||
@click='register'
|
||||
rounded
|
||||
:loading='isLoading'
|
||||
) {{ $t('auth:actions.register') }}
|
||||
v-spacer
|
||||
v-divider
|
||||
v-card-actions.py-3.grey(:class='$vuetify.theme.dark ? `darken-4-l1` : `lighten-4`')
|
||||
v-spacer
|
||||
i18next.caption(path='auth:switchToLogin.text', tag='div')
|
||||
a.caption(href='/login', place='link') {{ $t('auth:switchToLogin.link') }}
|
||||
v-spacer
|
||||
|
||||
loader(v-model='isLoading', :mode='loaderMode', :icon='loaderIcon', :color='loaderColor', :title='loaderTitle', :subtitle='loaderSubtitle')
|
||||
nav-footer(color='grey darken-5', dark-color='grey darken-5')
|
||||
notify(style='padding-top: 64px;')
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/* global siteConfig */
|
||||
|
||||
import _ from 'lodash'
|
||||
import validate from 'validate.js'
|
||||
import PasswordStrength from './common/password-strength.vue'
|
||||
|
||||
import registerMutation from 'gql/register/register-mutation-create.gql'
|
||||
|
||||
export default {
|
||||
i18nOptions: { namespaces: 'auth' },
|
||||
components: {
|
||||
PasswordStrength
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
email: '',
|
||||
password: '',
|
||||
verifyPassword: '',
|
||||
name: '',
|
||||
hidePassword: true,
|
||||
isLoading: false,
|
||||
isShown: false,
|
||||
loaderColor: 'grey darken-4',
|
||||
loaderTitle: 'Working...',
|
||||
loaderSubtitle: 'Please wait',
|
||||
loaderMode: 'icon',
|
||||
loaderIcon: 'checkmark'
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
siteTitle () {
|
||||
return siteConfig.title
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.isShown = true
|
||||
this.$nextTick(() => {
|
||||
this.$refs.iptEmail.focus()
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* REGISTER
|
||||
*/
|
||||
async register () {
|
||||
const validation = validate({
|
||||
email: this.email,
|
||||
password: this.password,
|
||||
verifyPassword: this.verifyPassword,
|
||||
name: this.name
|
||||
}, {
|
||||
email: {
|
||||
presence: {
|
||||
message: this.$t('auth:missingEmail'),
|
||||
allowEmpty: false
|
||||
},
|
||||
email: {
|
||||
message: this.$t('auth:invalidEmail')
|
||||
}
|
||||
},
|
||||
password: {
|
||||
presence: {
|
||||
message: this.$t('auth:missingPassword'),
|
||||
allowEmpty: false
|
||||
},
|
||||
length: {
|
||||
minimum: 6,
|
||||
tooShort: this.$t('auth:passwordTooShort')
|
||||
}
|
||||
},
|
||||
verifyPassword: {
|
||||
equality: {
|
||||
attribute: 'password',
|
||||
message: this.$t('auth:passwordNotMatch')
|
||||
}
|
||||
},
|
||||
name: {
|
||||
presence: {
|
||||
message: this.$t('auth:missingName'),
|
||||
allowEmpty: false
|
||||
},
|
||||
length: {
|
||||
minimum: 2,
|
||||
maximum: 255,
|
||||
tooShort: this.$t('auth:nameTooShort'),
|
||||
tooLong: this.$t('auth:nameTooLong')
|
||||
}
|
||||
}
|
||||
}, { fullMessages: false })
|
||||
|
||||
if (validation) {
|
||||
if (validation.email) {
|
||||
this.$store.commit('showNotification', {
|
||||
style: 'red',
|
||||
message: validation.email[0],
|
||||
icon: 'warning'
|
||||
})
|
||||
this.$refs.iptEmail.focus()
|
||||
} else if (validation.password) {
|
||||
this.$store.commit('showNotification', {
|
||||
style: 'red',
|
||||
message: validation.password[0],
|
||||
icon: 'warning'
|
||||
})
|
||||
this.$refs.iptPassword.focus()
|
||||
} else if (validation.verifyPassword) {
|
||||
this.$store.commit('showNotification', {
|
||||
style: 'red',
|
||||
message: validation.verifyPassword[0],
|
||||
icon: 'warning'
|
||||
})
|
||||
this.$refs.iptVerifyPassword.focus()
|
||||
} else {
|
||||
this.$store.commit('showNotification', {
|
||||
style: 'red',
|
||||
message: validation.name[0],
|
||||
icon: 'warning'
|
||||
})
|
||||
this.$refs.iptName.focus()
|
||||
}
|
||||
} else {
|
||||
this.loaderColor = 'grey darken-4'
|
||||
this.loaderTitle = this.$t('auth:registering')
|
||||
this.loaderSubtitle = this.$t(`auth:pleaseWait`)
|
||||
this.loaderMode = 'loading'
|
||||
this.isLoading = true
|
||||
try {
|
||||
let resp = await this.$apollo.mutate({
|
||||
mutation: registerMutation,
|
||||
variables: {
|
||||
email: this.email,
|
||||
password: this.password,
|
||||
name: this.name
|
||||
}
|
||||
})
|
||||
if (_.has(resp, 'data.authentication.register')) {
|
||||
let respObj = _.get(resp, 'data.authentication.register', {})
|
||||
if (respObj.responseResult.succeeded === true) {
|
||||
this.loaderColor = 'grey darken-4'
|
||||
this.loaderTitle = this.$t('auth:registerSuccess')
|
||||
this.loaderSubtitle = this.$t(`auth:registerCheckEmail`)
|
||||
this.loaderMode = 'icon'
|
||||
this.isShown = false
|
||||
} else {
|
||||
throw new Error(respObj.responseResult.message)
|
||||
}
|
||||
} else {
|
||||
throw new Error(this.$t('auth:genericError'))
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
this.$store.commit('showNotification', {
|
||||
style: 'red',
|
||||
message: err.message,
|
||||
icon: 'warning'
|
||||
})
|
||||
this.isLoading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.register {
|
||||
background-color: mc('indigo', '900');
|
||||
background-image: url('../static/svg/motif-blocks.svg');
|
||||
background-repeat: repeat;
|
||||
background-size: 200px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
animation: loginBgReveal 20s linear infinite;
|
||||
|
||||
@include keyframes(loginBgReveal) {
|
||||
0% {
|
||||
background-position-x: 0;
|
||||
}
|
||||
100% {
|
||||
background-position-x: 800px;
|
||||
}
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
background-image: url('../static/svg/motif-overlay.svg');
|
||||
background-attachment: fixed;
|
||||
background-size: cover;
|
||||
opacity: .5;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
> .container {
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.v-text-field.centered input {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,273 +0,0 @@
|
||||
<template lang="pug">
|
||||
v-app.setup
|
||||
v-content
|
||||
v-container
|
||||
v-layout
|
||||
v-flex(xs12, lg6, offset-lg3)
|
||||
v-card.elevation-20.radius-7.animated.fadeInUp
|
||||
v-alert(v-if='isDevMode', tile, dark, color='red darken-3', icon='mdi-alert', prominent)
|
||||
.body-2 You are running an unstable, unreleased development version. This code base is #[strong NOT] for production use!
|
||||
.body-2.mt-3 Cloning the dev branch directly from GitHub is #[strong NOT] the proper way to install Wiki.js!
|
||||
.body-2 Read the #[a(href='https://docs.requarks.io/install', style='color: #FFF;') documentation] on correctly installing the latest stable version.
|
||||
.text-center
|
||||
img.setup-logo.animated.fadeInUp.wait-p2s(src='/_assets/svg/logo-wikijs-full.svg', alt='Wiki.js Logo')
|
||||
v-alert(v-model='error', type='error', icon='mdi-alert', tile, dismissible) {{ errorMessage }}
|
||||
v-alert(v-if='!error', tile, color='blue lighten-5', :value='true')
|
||||
v-icon.mr-3(color='blue') mdi-package-variant
|
||||
span.blue--text You are about to install Wiki.js #[strong {{wikiVersion}}].
|
||||
v-card-text
|
||||
.overline.pl-3 Administrator Account
|
||||
v-container.pa-3.mt-3(grid-list-xl)
|
||||
v-layout(row, wrap)
|
||||
v-flex(xs12)
|
||||
v-text-field(
|
||||
outlined
|
||||
v-model='conf.adminEmail',
|
||||
label='Administrator Email',
|
||||
hint='The email address of the administrator account.',
|
||||
persistent-hint
|
||||
required
|
||||
ref='adminEmailInput'
|
||||
)
|
||||
v-flex(xs6)
|
||||
v-text-field(
|
||||
outlined
|
||||
ref='adminPassword',
|
||||
counter='255'
|
||||
v-model='conf.adminPassword',
|
||||
label='Password',
|
||||
:append-icon="pwdMode ? 'mdi-eye-off' : 'mdi-eye'"
|
||||
@click:append="() => (pwdMode = !pwdMode)"
|
||||
:type="pwdMode ? 'password' : 'text'"
|
||||
hint='At least 8 characters long.',
|
||||
persistent-hint
|
||||
)
|
||||
v-flex(xs6)
|
||||
v-text-field(
|
||||
outlined
|
||||
ref='adminPasswordConfirm',
|
||||
counter='255'
|
||||
v-model='conf.adminPasswordConfirm',
|
||||
label='Confirm Password',
|
||||
:append-icon="pwdConfirmMode ? 'mdi-eye-off' : 'mdi-eye'"
|
||||
@click:append="() => (pwdConfirmMode = !pwdConfirmMode)"
|
||||
:type="pwdConfirmMode ? 'password' : 'text'"
|
||||
hint='Verify your password again.',
|
||||
persistent-hint
|
||||
)
|
||||
v-divider.mb-4
|
||||
.overline.pl-3.mb-5 Site URL
|
||||
v-text-field.mb-4.mx-3(
|
||||
outlined
|
||||
ref='adminSiteUrl',
|
||||
v-model='conf.siteUrl',
|
||||
label='Site URL',
|
||||
hint='Full URL to your wiki, without the trailing slash (e.g. https://wiki.example.com). This should be the public facing URL, not the internal one if using a reverse-proxy.',
|
||||
persistent-hint
|
||||
@keyup.enter='install'
|
||||
)
|
||||
v-divider.mb-4
|
||||
.overline.pl-3.mb-3 Telemetry
|
||||
v-switch.ml-3(
|
||||
inset
|
||||
color='primary',
|
||||
v-model='conf.telemetry',
|
||||
label='Allow Telemetry',
|
||||
persistent-hint,
|
||||
hint='Help Wiki.js developers improve this app with anonymized telemetry.'
|
||||
)
|
||||
a.pl-3(style='font-size: 12px; letter-spacing: initial;', href='https://docs.requarks.io/telemetry', target='_blank') Learn more
|
||||
v-divider.mt-2
|
||||
v-card-actions
|
||||
v-btn(color='primary', @click='install', :disabled='loading', x-large, depressed, block)
|
||||
v-icon(left) mdi-check
|
||||
span Install
|
||||
|
||||
v-dialog(v-model='loading', width='450', persistent)
|
||||
v-card(color='primary', dark).radius-7
|
||||
v-card-text.text-center.py-5
|
||||
.py-3(style='width: 64px; display:inline-block;')
|
||||
breeding-rhombus-spinner(
|
||||
:animation-duration='2000'
|
||||
:size='64'
|
||||
color='#FFF'
|
||||
)
|
||||
template(v-if='!success')
|
||||
.subtitle-1.white--text Finalizing your installation...
|
||||
.caption Just a moment
|
||||
template(v-else)
|
||||
.subtitle-1.white--text Installation complete!
|
||||
.caption Redirecting...
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import validate from 'validate.js'
|
||||
import { BreedingRhombusSpinner } from 'epic-spinners'
|
||||
import confetti from 'canvas-confetti'
|
||||
|
||||
/* global siteConfig */
|
||||
|
||||
export default {
|
||||
components: {
|
||||
BreedingRhombusSpinner
|
||||
},
|
||||
props: {
|
||||
wikiVersion: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
success: false,
|
||||
error: false,
|
||||
errorMessage: '',
|
||||
conf: {
|
||||
adminEmail: '',
|
||||
adminPassword: '',
|
||||
adminPasswordConfirm: '',
|
||||
siteUrl: 'https://wiki.yourdomain.com',
|
||||
telemetry: true
|
||||
},
|
||||
pwdMode: true,
|
||||
pwdConfirmMode: true,
|
||||
isDevMode: false
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
_.delay(() => {
|
||||
this.$refs.adminEmailInput.focus()
|
||||
}, 500)
|
||||
this.isDevMode = siteConfig.devMode === true
|
||||
},
|
||||
methods: {
|
||||
async install () {
|
||||
this.error = false
|
||||
|
||||
const validationResults = validate(this.conf, {
|
||||
adminEmail: {
|
||||
presence: {
|
||||
allowEmpty: false
|
||||
},
|
||||
email: true
|
||||
},
|
||||
adminPassword: {
|
||||
presence: {
|
||||
allowEmpty: false
|
||||
},
|
||||
length: {
|
||||
minimum: 6,
|
||||
maximum: 255
|
||||
}
|
||||
},
|
||||
adminPasswordConfirm: {
|
||||
equality: 'adminPassword'
|
||||
},
|
||||
siteUrl: {
|
||||
presence: {
|
||||
allowEmpty: false
|
||||
},
|
||||
url: {
|
||||
schemes: ['http', 'https'],
|
||||
allowLocal: true,
|
||||
allowDataUrl: false
|
||||
},
|
||||
format: {
|
||||
pattern: '^(?!.*/$).*$',
|
||||
flags: 'i',
|
||||
message: 'must not have a trailing slash'
|
||||
}
|
||||
}
|
||||
}, {
|
||||
format: 'flat'
|
||||
})
|
||||
if (validationResults) {
|
||||
this.error = true
|
||||
this.errorMessage = validationResults[0]
|
||||
this.$forceUpdate()
|
||||
return
|
||||
}
|
||||
|
||||
this.loading = true
|
||||
this.success = false
|
||||
this.$forceUpdate()
|
||||
|
||||
_.delay(async () => {
|
||||
try {
|
||||
const resp = await fetch('/finalize', {
|
||||
method: 'POST',
|
||||
cache: 'no-cache',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(this.conf)
|
||||
}).then(res => res.json())
|
||||
|
||||
if (resp.ok === true) {
|
||||
_.delay(() => {
|
||||
confetti({
|
||||
particleCount: 100,
|
||||
spread: 70,
|
||||
zIndex: 100000
|
||||
})
|
||||
this.success = true
|
||||
_.delay(() => {
|
||||
window.location.assign('/login')
|
||||
}, 3000)
|
||||
}, 10000)
|
||||
} else {
|
||||
this.error = true
|
||||
this.errorMessage = resp.error
|
||||
this.loading = false
|
||||
}
|
||||
} catch (err) {
|
||||
window.alert(err.message)
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang='scss'>
|
||||
.setup {
|
||||
.v-application--wrap {
|
||||
padding-top: 10vh;
|
||||
background-color: #111;
|
||||
background-image: linear-gradient(45deg, mc('blue', '100'), mc('blue', '700'), mc('indigo', '900'));
|
||||
background-blend-mode: exclusion;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
z-index: 0;
|
||||
background-color: transparent;
|
||||
background-image: url(../static/svg/motif-grid.svg) !important;
|
||||
background-size: 100px;
|
||||
background-repeat: repeat;
|
||||
animation: bg-anim 100s linear infinite;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes bg-anim {
|
||||
0% {
|
||||
background-position: 0 0;
|
||||
}
|
||||
100% {
|
||||
background-position: 100% 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&-logo {
|
||||
width: 400px;
|
||||
margin: 2rem 0 2rem 0;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,35 +0,0 @@
|
||||
<template lang='pug'>
|
||||
v-app
|
||||
.onboarding
|
||||
.onboarding-content
|
||||
img.animated.fadeIn(src='/_assets-legacy/svg/logo-wikijs.svg', alt='Wiki.js')
|
||||
.headline.animated.fadeInUp {{ $t('welcome.title') }}
|
||||
.subtitle-1.mt-3.animated.fadeInUp.wait-p1s {{ $t('welcome.subtitle') }}
|
||||
div
|
||||
v-btn.mt-5.mx-3.animated.fadeInUp.wait-p2s(color='primary', :href='`/e/` + locale + `/home`', x-large)
|
||||
v-icon(left) mdi-plus
|
||||
span {{ $t('welcome.createhome') }}
|
||||
v-btn.mt-5.mx-3.animated.fadeInUp.wait-p3s(color='primary', href='/a', x-large)
|
||||
v-icon(left) mdi-view-dashboard
|
||||
span {{ $t('welcome.goadmin') }}
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
props: {
|
||||
locale: {
|
||||
type: String,
|
||||
default: 'en'
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return { }
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang='scss'>
|
||||
|
||||
</style>
|
@ -1,12 +0,0 @@
|
||||
mutation($providers: [AnalyticsProviderInput]!) {
|
||||
analytics {
|
||||
updateProviders(providers: $providers) {
|
||||
responseResult {
|
||||
succeeded
|
||||
errorCode
|
||||
slug
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
query {
|
||||
analytics {
|
||||
providers {
|
||||
isEnabled
|
||||
key
|
||||
title
|
||||
description
|
||||
isAvailable
|
||||
logo
|
||||
website
|
||||
config {
|
||||
key
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
query {
|
||||
groups {
|
||||
list {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
{
|
||||
site {
|
||||
config {
|
||||
host
|
||||
}
|
||||
}
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
query {
|
||||
authentication {
|
||||
strategies {
|
||||
isEnabled
|
||||
key
|
||||
title
|
||||
description
|
||||
isAvailable
|
||||
useForm
|
||||
logo
|
||||
website
|
||||
config {
|
||||
key
|
||||
value
|
||||
}
|
||||
selfRegistration
|
||||
domainWhitelist
|
||||
autoEnrollGroups
|
||||
}
|
||||
}
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
query {
|
||||
contribute {
|
||||
contributors {
|
||||
company
|
||||
currency
|
||||
description
|
||||
id
|
||||
image
|
||||
name
|
||||
profile
|
||||
tier
|
||||
totalDonated
|
||||
twitter
|
||||
website
|
||||
}
|
||||
}
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
query {
|
||||
system {
|
||||
info {
|
||||
currentVersion
|
||||
latestVersion
|
||||
groupsTotal
|
||||
pagesTotal
|
||||
usersTotal
|
||||
tagsTotal
|
||||
}
|
||||
}
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
mutation (
|
||||
$flags: [SystemFlagInput]!
|
||||
) {
|
||||
system {
|
||||
updateFlags(
|
||||
flags: $flags
|
||||
) {
|
||||
responseResult {
|
||||
succeeded
|
||||
errorCode
|
||||
slug
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
{
|
||||
system {
|
||||
flags {
|
||||
key
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
mutation ($groupId: Int!, $userId: Int!) {
|
||||
groups {
|
||||
assignUser(groupId: $groupId, userId: $userId) {
|
||||
responseResult {
|
||||
succeeded
|
||||
errorCode
|
||||
slug
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
mutation ($name: String!) {
|
||||
groups {
|
||||
create(name: $name) {
|
||||
responseResult {
|
||||
succeeded
|
||||
errorCode
|
||||
slug
|
||||
message
|
||||
}
|
||||
group {
|
||||
id
|
||||
name
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
mutation ($groupId: Int!, $userId: Int!) {
|
||||
groups {
|
||||
unassignUser(groupId: $groupId, userId: $userId) {
|
||||
responseResult {
|
||||
succeeded
|
||||
errorCode
|
||||
slug
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
query {
|
||||
groups {
|
||||
list {
|
||||
id
|
||||
name
|
||||
isSystem
|
||||
userCount
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
mutation($locale: String!) {
|
||||
localization {
|
||||
downloadLocale(locale: $locale) {
|
||||
responseResult {
|
||||
succeeded
|
||||
errorCode
|
||||
slug
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
mutation($locale: String!, $autoUpdate: Boolean!, $namespacing: Boolean!, $namespaces: [String]!) {
|
||||
localization {
|
||||
updateLocale(locale: $locale, autoUpdate: $autoUpdate, namespacing: $namespacing, namespaces: $namespaces) {
|
||||
responseResult {
|
||||
succeeded
|
||||
errorCode
|
||||
slug
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
{
|
||||
localization {
|
||||
locales {
|
||||
availability
|
||||
code
|
||||
createdAt
|
||||
isInstalled
|
||||
installDate
|
||||
isRTL
|
||||
name
|
||||
nativeName
|
||||
updatedAt
|
||||
}
|
||||
config {
|
||||
locale
|
||||
autoUpdate
|
||||
namespacing
|
||||
namespaces
|
||||
}
|
||||
}
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
mutation($loggers: [LoggerInput]) {
|
||||
logging {
|
||||
updateLoggers(loggers: $loggers) {
|
||||
responseResult {
|
||||
succeeded
|
||||
errorCode
|
||||
slug
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
query {
|
||||
logging {
|
||||
loggers(orderBy: "title ASC") {
|
||||
isEnabled
|
||||
key
|
||||
title
|
||||
description
|
||||
logo
|
||||
website
|
||||
level
|
||||
config {
|
||||
key
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
subscription {
|
||||
loggingLiveTrail {
|
||||
level
|
||||
output
|
||||
timestamp
|
||||
}
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
mutation (
|
||||
$senderName: String!,
|
||||
$senderEmail: String!,
|
||||
$host: String!,
|
||||
$port: Int!,
|
||||
$secure: Boolean!,
|
||||
$verifySSL: Boolean!,
|
||||
$user: String!,
|
||||
$pass: String!,
|
||||
$useDKIM: Boolean!,
|
||||
$dkimDomainName: String!,
|
||||
$dkimKeySelector: String!,
|
||||
$dkimPrivateKey: String!
|
||||
) {
|
||||
mail {
|
||||
updateConfig(
|
||||
senderName: $senderName,
|
||||
senderEmail: $senderEmail,
|
||||
host: $host,
|
||||
port: $port,
|
||||
secure: $secure,
|
||||
verifySSL: $verifySSL,
|
||||
user: $user,
|
||||
pass: $pass,
|
||||
useDKIM: $useDKIM,
|
||||
dkimDomainName: $dkimDomainName,
|
||||
dkimKeySelector: $dkimKeySelector,
|
||||
dkimPrivateKey: $dkimPrivateKey
|
||||
) {
|
||||
responseResult {
|
||||
succeeded
|
||||
errorCode
|
||||
slug
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
mutation ($recipientEmail: String!) {
|
||||
mail {
|
||||
sendTest(recipientEmail: $recipientEmail) {
|
||||
responseResult {
|
||||
succeeded
|
||||
errorCode
|
||||
slug
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
{
|
||||
mail {
|
||||
config {
|
||||
senderName
|
||||
senderEmail
|
||||
host
|
||||
port
|
||||
secure
|
||||
verifySSL
|
||||
user
|
||||
pass
|
||||
useDKIM
|
||||
dkimDomainName
|
||||
dkimKeySelector
|
||||
dkimPrivateKey
|
||||
}
|
||||
}
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
query {
|
||||
pages {
|
||||
list {
|
||||
id
|
||||
locale
|
||||
path
|
||||
title
|
||||
description
|
||||
contentType
|
||||
isPublished
|
||||
isPrivate
|
||||
privateNS
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
query($id: Int!) {
|
||||
pages {
|
||||
single(id:$id) {
|
||||
id
|
||||
path
|
||||
hash
|
||||
title
|
||||
description
|
||||
isPrivate
|
||||
isPublished
|
||||
privateNS
|
||||
publishStartDate
|
||||
publishEndDate
|
||||
contentType
|
||||
createdAt
|
||||
updatedAt
|
||||
editor
|
||||
locale
|
||||
authorId
|
||||
authorName
|
||||
authorEmail
|
||||
creatorId
|
||||
creatorName
|
||||
creatorEmail
|
||||
}
|
||||
}
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
mutation($renderers: [RendererInput]) {
|
||||
rendering {
|
||||
updateRenderers(renderers: $renderers) {
|
||||
responseResult {
|
||||
succeeded
|
||||
errorCode
|
||||
slug
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
{
|
||||
rendering {
|
||||
renderers {
|
||||
isEnabled
|
||||
key
|
||||
title
|
||||
description
|
||||
icon
|
||||
dependsOn
|
||||
input
|
||||
output
|
||||
config {
|
||||
key
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
mutation {
|
||||
search {
|
||||
rebuildIndex {
|
||||
responseResult {
|
||||
succeeded
|
||||
errorCode
|
||||
slug
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
mutation($engines: [SearchEngineInput]) {
|
||||
search {
|
||||
updateSearchEngines(engines: $engines) {
|
||||
responseResult {
|
||||
succeeded
|
||||
errorCode
|
||||
slug
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
query {
|
||||
search {
|
||||
searchEngines(orderBy: "title") {
|
||||
isEnabled
|
||||
key
|
||||
title
|
||||
description
|
||||
logo
|
||||
website
|
||||
isAvailable
|
||||
config {
|
||||
key
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
mutation($targetKey: String!, $handler: String!) {
|
||||
storage {
|
||||
executeAction(targetKey: $targetKey, handler: $handler) {
|
||||
responseResult {
|
||||
succeeded
|
||||
errorCode
|
||||
slug
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
mutation($targets: [StorageTargetInput]!) {
|
||||
storage {
|
||||
updateTargets(targets: $targets) {
|
||||
responseResult {
|
||||
succeeded
|
||||
errorCode
|
||||
slug
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
query {
|
||||
storage {
|
||||
status {
|
||||
key
|
||||
title
|
||||
status
|
||||
message
|
||||
lastAttempt
|
||||
}
|
||||
}
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
query {
|
||||
storage {
|
||||
targets {
|
||||
isAvailable
|
||||
isEnabled
|
||||
key
|
||||
title
|
||||
description
|
||||
logo
|
||||
website
|
||||
supportedModes
|
||||
mode
|
||||
hasSchedule
|
||||
syncInterval
|
||||
syncIntervalDefault
|
||||
config {
|
||||
key
|
||||
value
|
||||
}
|
||||
actions {
|
||||
handler
|
||||
label
|
||||
hint
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
mutation {
|
||||
system {
|
||||
performUpgrade {
|
||||
responseResult {
|
||||
succeeded
|
||||
errorCode
|
||||
slug
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
query {
|
||||
system {
|
||||
info {
|
||||
configFile
|
||||
cpuCores
|
||||
currentVersion
|
||||
dbHost
|
||||
dbType
|
||||
dbVersion
|
||||
hostname
|
||||
latestVersion
|
||||
latestVersionReleaseDate
|
||||
nodeVersion
|
||||
operatingSystem
|
||||
platform
|
||||
ramTotal
|
||||
upgradeCapable
|
||||
workingDirectory
|
||||
}
|
||||
}
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
mutation($theme: String!, $iconset: String!, $darkMode: Boolean!, $injectCSS: String, $injectHead: String, $injectBody: String) {
|
||||
theming {
|
||||
setConfig(theme: $theme, iconset: $iconset, darkMode: $darkMode, injectCSS: $injectCSS, injectHead: $injectHead, injectBody: $injectBody) {
|
||||
responseResult {
|
||||
succeeded
|
||||
errorCode
|
||||
slug
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
query {
|
||||
theming {
|
||||
config {
|
||||
theme
|
||||
iconset
|
||||
darkMode
|
||||
injectCSS
|
||||
injectHead
|
||||
injectBody
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue