feat: admin - manage groups + permissions + page rules

pull/760/head
Nicolas Giard 6 years ago
parent 10940ca230
commit edb97b832d

@ -33,6 +33,11 @@ docker-dev-rebuild: ## Rebuild dockerized dev image
rm -rf ./node_modules
docker-compose -f ./dev/docker/docker-compose.yml -p wiki --project-directory . build --no-cache --force-rm
docker-dev-clean: ## Clean DB, redis and data folders
rm -rf ./data
docker-compose -f ./dev/docker/docker-compose.yml -p wiki --project-directory . exec db psql --dbname=wiki --username=postgres --command='DROP SCHEMA IF EXISTS public CASCADE; CREATE SCHEMA public'
docker-compose -f ./dev/docker/docker-compose.yml -p wiki --project-directory . exec redis redis-cli flushall
docker-build: ## Run assets generation build in docker
docker-compose -f ./dev/docker/docker-compose.yml -p wiki --project-directory . run wiki yarn build
docker-compose -f ./dev/docker/docker-compose.yml -p wiki --project-directory . down

@ -9,13 +9,13 @@
v-list-tile-title {{ $t('admin:dashboard.title') }}
v-divider.my-2
v-subheader.pl-4 {{ $t('admin:nav.site') }}
v-list-tile(to='/general')
v-list-tile(to='/general', v-if='hasPermission(`manage:system`)')
v-list-tile-avatar: v-icon widgets
v-list-tile-title {{ $t('admin:general.title') }}
v-list-tile(to='/locale')
v-list-tile(to='/locale', v-if='hasPermission(`manage:system`)')
v-list-tile-avatar: v-icon language
v-list-tile-title {{ $t('admin:locale.title') }}
v-list-tile(to='/navigation')
v-list-tile(to='/navigation', v-if='hasPermission([`manage:system`, `manage:navigation`])')
v-list-tile-avatar: v-icon near_me
v-list-tile-title {{ $t('admin:navigation.title') }}
v-list-tile(to='/pages')
@ -24,7 +24,7 @@
v-list-tile-action
v-chip(small, disabled, :color='darkMode ? `grey darken-3-d4` : `grey lighten-4`')
.caption.grey--text {{ info.pagesTotal }}
v-list-tile(to='/theme')
v-list-tile(to='/theme', v-if='hasPermission([`manage:system`, `manage:theme`])')
v-list-tile-avatar: v-icon palette
v-list-tile-title {{ $t('admin:theme.title') }}
v-divider.my-2
@ -41,44 +41,46 @@
v-list-tile-action
v-chip(small, disabled, :color='darkMode ? `grey darken-3-d4` : `grey lighten-4`')
.caption.grey--text {{ info.usersTotal }}
v-divider.my-2
v-subheader.pl-4 {{ $t('admin:nav.modules') }}
v-list-tile(to='/auth')
v-list-tile-avatar: v-icon lock_outline
v-list-tile-title {{ $t('admin:auth.title') }}
v-list-tile(to='/editor')
v-list-tile-avatar: v-icon transform
v-list-tile-title {{ $t('admin:editor.title') }}
v-list-tile(to='/logging')
v-list-tile-avatar: v-icon graphic_eq
v-list-tile-title {{ $t('admin:logging.title') }}
v-list-tile(to='/rendering')
v-list-tile-avatar: v-icon system_update_alt
v-list-tile-title {{ $t('admin:rendering.title') }}
v-list-tile(to='/search')
v-list-tile-avatar: v-icon search
v-list-tile-title {{ $t('admin:search.title') }}
v-list-tile(to='/storage')
v-list-tile-avatar: v-icon storage
v-list-tile-title {{ $t('admin:storage.title') }}
v-divider.my-2
v-subheader.pl-4 {{ $t('admin:nav.system') }}
v-list-tile(to='/api')
v-list-tile-avatar: v-icon call_split
v-list-tile-title {{ $t('admin:api.title') }}
v-list-tile(to='/mail')
v-list-tile-avatar: v-icon email
v-list-tile-title {{ $t('admin:mail.title') }}
v-list-tile(to='/system')
v-list-tile-avatar: v-icon tune
v-list-tile-title {{ $t('admin:system.title') }}
v-list-tile(to='/utilities')
v-list-tile-avatar: v-icon build
v-list-tile-title {{ $t('admin:utilities.title') }}
v-list-tile(to='/dev')
v-list-tile-avatar: v-icon weekend
v-list-tile-title {{ $t('admin:dev.title') }}
v-divider.my-2
template(v-if='hasPermission(`manage:system`)')
v-divider.my-2
v-subheader.pl-4 {{ $t('admin:nav.modules') }}
v-list-tile(to='/auth')
v-list-tile-avatar: v-icon lock_outline
v-list-tile-title {{ $t('admin:auth.title') }}
v-list-tile(to='/editor')
v-list-tile-avatar: v-icon transform
v-list-tile-title {{ $t('admin:editor.title') }}
v-list-tile(to='/logging')
v-list-tile-avatar: v-icon graphic_eq
v-list-tile-title {{ $t('admin:logging.title') }}
v-list-tile(to='/rendering')
v-list-tile-avatar: v-icon system_update_alt
v-list-tile-title {{ $t('admin:rendering.title') }}
v-list-tile(to='/search')
v-list-tile-avatar: v-icon search
v-list-tile-title {{ $t('admin:search.title') }}
v-list-tile(to='/storage')
v-list-tile-avatar: v-icon storage
v-list-tile-title {{ $t('admin:storage.title') }}
v-divider.my-2
template(v-if='hasPermission([`manage:system`, `manage:api`])')
v-subheader.pl-4 {{ $t('admin:nav.system') }}
v-list-tile(to='/api', v-if='hasPermission([`manage:system`, `manage:api`])')
v-list-tile-avatar: v-icon call_split
v-list-tile-title {{ $t('admin:api.title') }}
v-list-tile(to='/mail', v-if='hasPermission(`manage:system`)')
v-list-tile-avatar: v-icon email
v-list-tile-title {{ $t('admin:mail.title') }}
v-list-tile(to='/system', v-if='hasPermission(`manage:system`)')
v-list-tile-avatar: v-icon tune
v-list-tile-title {{ $t('admin:system.title') }}
v-list-tile(to='/utilities', v-if='hasPermission(`manage:system`)')
v-list-tile-avatar: v-icon build
v-list-tile-title {{ $t('admin:utilities.title') }}
v-list-tile(to='/dev', v-if='hasPermission([`manage:system`, `manage:api`])')
v-list-tile-avatar: v-icon weekend
v-list-tile-title {{ $t('admin:dev.title') }}
v-divider.my-2
v-list-tile(to='/contribute')
v-list-tile-avatar: v-icon favorite
v-list-tile-title {{ $t('admin:contribute.title') }}
@ -91,6 +93,7 @@
</template>
<script>
import _ from 'lodash'
import VueRouter from 'vue-router'
import { get, sync } from 'vuex-pathify'
@ -161,12 +164,24 @@ export default {
},
computed: {
darkMode: get('site/dark'),
info: sync('admin/info')
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,

@ -130,6 +130,7 @@
outline
background-color='grey lighten-2'
persistent-hint
small-chips
deletable-chips
clearable
multiple
@ -145,6 +146,7 @@
v-model='strategy.autoEnrollGroups'
prepend-icon='people'
hint='Automatically assign new users to these groups.'
small-chips
persistent-hint
deletable-chips
clearable

@ -8,38 +8,53 @@
.headline.primary--text {{ $t('admin:dashboard.title') }}
.subheading.grey--text {{ $t('admin:dashboard.subtitle') }}
v-flex(xs12 md6 lg4 xl3 d-flex)
v-card.primary.dashboard-card(dark)
v-card-text
v-icon.dashboard-icon insert_drive_file
.subheading Pages
animated-number.display-1(
:value='info.pagesTotal'
:duration='2000'
:formatValue='round'
easing='easeOutQuint'
)
v-hover
v-card.primary.dashboard-card(
dark
slot-scope='{ hover }'
:class='hover ? `elevation-10` : `elevation-2`'
)
v-card-text
v-icon.dashboard-icon insert_drive_file
.subheading Pages
animated-number.display-1(
:value='info.pagesTotal'
:duration='2000'
:formatValue='round'
easing='easeOutQuint'
)
v-flex(xs12 md6 lg4 xl3 d-flex)
v-card.indigo.lighten-1.dashboard-card(dark)
v-card-text
v-icon.dashboard-icon person
.subheading Users
animated-number.display-1(
:value='info.usersTotal'
:duration='2000'
:formatValue='round'
easing='easeOutQuint'
)
v-hover
v-card.indigo.lighten-1.dashboard-card(
dark
slot-scope='{ hover }'
:class='hover ? `elevation-10` : `elevation-2`'
)
v-card-text
v-icon.dashboard-icon person
.subheading Users
animated-number.display-1(
:value='info.usersTotal'
:duration='2000'
:formatValue='round'
easing='easeOutQuint'
)
v-flex(xs12 md6 lg4 xl3 d-flex)
v-card.indigo.lighten-2.dashboard-card(dark)
v-card-text
v-icon.dashboard-icon people
.subheading Groups
animated-number.display-1(
:value='info.groupsTotal'
:duration='2000'
:formatValue='round'
easing='easeOutQuint'
)
v-hover
v-card.indigo.lighten-2.dashboard-card(
dark
slot-scope='{ hover }'
:class='hover ? `elevation-10` : `elevation-2`'
)
v-card-text
v-icon.dashboard-icon people
.subheading 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(
:class='isLatestVersion ? "teal lighten-2" : "red lighten-2"'

@ -0,0 +1,203 @@
<template lang="pug">
v-card.wiki-form(flat)
v-card-text
v-text-field(
outline
background-color='grey lighten-3'
v-model='group.name'
label='Group Name'
counter='255'
prepend-icon='people'
)
v-alert.radius-7(
v-if='group.isSystem'
color='orange darken-2'
:class='$vuetify.dark ? "grey darken-4" : "orange lighten-5"'
outline
:value='true'
icon='lock_outline'
) This is a system group. Some permissions cannot be modified.
v-container.px-3.pb-3.pt-0(fluid, grid-list-md)
v-layout(row, wrap)
v-flex(xs12, md6, lg4, v-for='pmGroup in permissions')
v-card.md2(flat, :class='$vuetify.dark ? "grey darken-3-d5" : "white"')
v-subheader {{pmGroup.category}}
v-card-text.pt-0
template(v-for='(pm, idx) in pmGroup.items')
v-checkbox.pt-0(
:key='pm.permission'
:label='pm.permission'
:hint='pm.hint'
persistent-hint
color='primary'
v-model='group.permissions'
:value='pm.permission'
:append-icon='pm.warning ? "warning" : 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
}
},
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 view and create new pages, as specified in the Page Rules',
warning: false,
restrictedForSystem: false,
disabled: false
},
{
permission: 'manage:pages',
hint: 'Can view, create, edit and move existing pages as specified in the Page Rules',
warning: false,
restrictedForSystem: false,
disabled: false
},
{
permission: 'delete:pages',
hint: 'Can delete existing pages, 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: false,
disabled: false
},
{
permission: 'manage:assets',
hint: 'Can edit and delete assets (such as images and files), as specified in the Page Rules',
warning: false,
restrictedForSystem: false,
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 comments, as specified in the Page Rules',
warning: false,
restrictedForSystem: false,
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>

@ -0,0 +1,302 @@
<template lang="pug">
v-card.wiki-form
v-card-text(v-if='group.id === 1')
v-alert.radius-7(
:class='$vuetify.dark ? "grey darken-4" : "orange lighten-5"'
color='orange darken-2'
outline
:value='true'
icon='lock_outline'
) This group has access to everything.
template(v-else)
v-card-title(:class='$vuetify.dark ? `grey darken-3-d5` : `grey lighten-5`')
v-alert.radius-7(
:class='$vuetify.dark ? `grey darken-3-d3` : `white`'
:value='true'
color='grey'
outline
icon='info'
) You must enable global content permissions (under Permissions tab) for page rules to have any effect.
v-spacer
v-btn(depressed, color='primary', @click='addRule')
v-icon(left) add
| Add Rule
v-menu(
right
offset-y
nudge-left='115'
)
v-btn.is-icon(slot='activator', flat, outline, color='primary')
v-icon more_horiz
v-list(dense)
v-list-tile(@click='comingSoon')
v-list-tile-avatar
v-icon keyboard_capslock
v-list-tile-title Load Preset
v-divider
v-list-tile(@click='comingSoon')
v-list-tile-avatar
v-icon publish
v-list-tile-title Save As Preset
v-divider
v-list-tile(@click='comingSoon')
v-list-tile-avatar
v-icon cloud_upload
v-list-tile-title Import Rules
v-divider
v-list-tile(@click='comingSoon')
v-list-tile-avatar
v-icon cloud_download
v-list-tile-title Export Rules
v-card-text(:class='$vuetify.dark ? `grey darken-4-l5` : `white`')
.rules
.caption(v-if='group.pageRules.length === 0')
em(:class='$vuetify.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.rule-deny-btn(
solo
:color='rule.deny ? "red" : "green"'
dark
@click='rule.deny = !rule.deny'
)
v-icon(v-if='rule.deny') block
v-icon(v-else) 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
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 <= 2', small, label, :color='rule.deny ? `red` : `green`').caption {{ item.value }}
v-chip.white--text.ml-0(v-if='index === 3', small, label, :color='rule.deny ? `red lighten-2` : `green lighten-2`').caption + {{ rule.roles.length - 3 }} more
template(slot='item', slot-scope='props')
v-list-tile-action(style='min-width: 30px;')
v-checkbox(
v-model='props.tile.props.value'
hide-details
color='primary'
)
v-icon.mr-2(:color='rule.deny ? `red` : `green`') {{props.item.icon}}
v-list-tile-content
v-list-tile-title.body-2 {{props.item.text}}
v-chip.mr-2.grey--text(label, small, :color='$vuetify.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
style='flex: 0 1 250px;'
dense
)
template(slot='selection', slot-scope='{ item, index }')
.body-1 {{item.text}}
template(slot='item', slot-scope='data')
v-list-tile-avatar
v-avatar.white--text.radius-4(color='blue', size='30', tile) {{ data.item.icon }}
v-list-tile-content
v-list-tile-title(v-html='data.item.text')
//- Locales
v-select.mr-1(
:background-color='$vuetify.dark ? `grey darken-3-d5` : `blue-grey lighten-5`'
solo
:items='locales'
v-model='rule.locales'
placeholder='Any Locale'
multiple
hide-details
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.value.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-tile(slot='prepend-item', @click='rule.locales = []')
v-list-tile-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`') public
v-list-tile-content
v-list-tile-title.body-2 Any Locale
v-divider(slot='prepend-item')
template(slot='item', slot-scope='props')
v-list-tile-action(style='min-width: 30px;')
v-checkbox(
v-model='props.tile.props.value'
hide-details
color='primary'
)
v-icon.mr-2(:color='rule.deny ? `red` : `green`') language
v-list-tile-content
v-list-tile-title.body-2 {{props.item.text}}
v-chip.mr-2.grey--text(label, small, :color='$vuetify.dark ? `grey darken-4` : `grey lighten-4`').caption {{props.item.value.toUpperCase()}}
//- Path
v-text-field(
solo
v-model='rule.path'
label='Path'
:prefix='rule.match !== `END` ? `/` : null'
:placeholder='rule.match === `REGEX` ? `Regular Expression` : `Path`'
:suffix='rule.match === `REGEX` ? `/` : null'
hide-details
:color='$vuetify.dark ? `grey` : `blue-grey`'
)
v-btn(icon, @click='removeRule(rule.id)')
v-icon(:color='$vuetify.dark ? `grey` : `blue-grey`') clear
</template>
<script>
import _ from 'lodash'
import nanoid from 'nanoid/non-secure/generate'
export default {
props: {
value: {
type: Object
}
},
data() {
return {
roles: [
{ text: 'Read Pages', value: 'READ', icon: 'insert_drive_file' },
{ text: 'Create Pages', value: 'WRITE', icon: 'insert_drive_file' },
{ text: 'Edit + Move Pages', value: 'MANAGE', icon: 'insert_drive_file' },
{ text: 'Delete Pages', value: 'DELETE', icon: 'insert_drive_file' },
{ text: 'Read / Use Assets', value: 'AS_READ', icon: 'camera' },
{ text: 'Upload Assets', value: 'AS_WRITE', icon: 'camera' },
{ text: 'Edit + Delete Assets', value: 'AS_MANAGE', icon: 'camera' },
{ text: 'Read Comments', value: 'CM_READ', icon: 'insert_comment' },
{ text: 'Create Comments', value: 'CM_WRITE', icon: 'insert_comment' },
{ text: 'Edit + Delete Comments', value: 'CM_MANAGE', icon: 'insert_comment' }
],
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: '$.*' }
],
locales: [
{ text: 'English', value: 'en' },
{ text: 'Français', value: 'fr' },
]
}
},
computed: {
group: {
get() { return this.value },
set(val) { this.$set('input', val) }
}
},
methods: {
addRule(group) {
this.group.pageRules.push({
id: nanoid('1234567890abcdef', 10),
path: '',
roles: [],
match: 'START',
deny: false,
locales: []
})
},
removeRule(rule) {
this.group.pageRules.splice(_.findIndex(this.group.pageRules, ['id', rule.id]), 1)
},
comingSoon() {
this.$store.commit('showNotification', {
style: 'indigo',
message: `Coming soon...`,
icon: 'directions_boat'
})
}
}
}
</script>
<style lang="scss">
.rules {
background-color: mc('blue-grey', '50');
border-radius: 4px;
padding: 1rem;
position: relative;
@at-root .theme--dark & {
background-color: mc('grey', '800');
}
}
.rule {
display: flex;
background-color: mc('blue-grey', '100');
border-radius: 4px;
padding: .5rem;
&-enter-active, &-leave-active {
transition: all .5s ease;
}
&-enter, &-leave-to {
opacity: 0;
}
@at-root .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 .theme--dark & {
background-color: mc('grey', '800');
color: mc('grey', '600');
}
}
}
.input-group + * {
margin-left: .5rem;
}
&-deny-btn {
height: 48px;
border-radius: 2px 0 0 2px;
min-width: 0;
}
}
</style>

@ -0,0 +1,147 @@
<template lang="pug">
v-card.wiki-form
v-card-title(:class='$vuetify.dark ? `grey darken-3-d3` : `grey lighten-5`')
v-text-field(
outline
flat
prepend-inner-icon='search'
v-model='search'
label='Search Group Users...'
hide-details
)
v-spacer
v-btn(color='primary', depressed, @click='searchUserDialog = true', :disabled='group.id === 2')
v-icon(left) assignment_ind
| Assign User
v-data-table(
:items='group.users',
:headers='headers',
:search='search'
:pagination.sync='pagination',
:rows-per-page-items='[15]'
hide-actions
)
template(slot='items', slot-scope='props')
tr(:active='props.selected')
td.text-xs-right {{ props.item.id }}
td {{ props.item.name }}
td {{ props.item.email }}
td
v-menu(bottom, right, min-width='200')
v-btn(icon, slot='activator'): v-icon.grey--text.text--darken-1 more_horiz
v-list
v-list-tile(:to='`/users/` + props.item.id')
v-list-tile-action: v-icon(color='primary') person
v-list-tile-content
v-list-tile-title View User Profile
template(v-if='props.item.id !== 2')
v-divider
v-list-tile(@click='unassignUser(props.item.id)')
v-list-tile-action: v-icon(color='orange') highlight_off
v-list-tile-content
v-list-tile-title Unassign
template(slot='no-data')
v-alert.ma-3(icon='warning', :value='true', outline) No users to display.
.text-xs-center.py-2(v-if='group.users.length > 15')
v-pagination(v-model='pagination.page', :length='pages')
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
}
},
components: {
UserSearch
},
data() {
return {
headers: [
{ text: 'ID', value: 'id', width: 50, align: 'right' },
{ text: 'Name', value: 'name' },
{ text: 'Email', value: 'email' },
{ text: '', value: 'actions', sortable: false, width: 50 }
],
searchUserDialog: false,
pagination: {},
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) {
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>

@ -6,130 +6,57 @@
img(src='/svg/icon-social-group.svg', alt='Edit Group', style='width: 80px;')
.admin-header-title
.headline.blue--text.text--darken-2 Edit Group
.subheading.grey--text {{name}}
.subheading.grey--text {{group.name}}
v-spacer
.caption.grey--text ID #[strong {{group.id}}]
v-divider.mx-3(vertical)
v-btn(color='indigo', large, outline, to='/groups')
v-btn(color='grey', large, outline, to='/groups')
v-icon arrow_back
v-dialog(v-model='deleteGroupDialog', max-width='500', v-if='!group.isSystem')
v-btn(color='red', large, outline, slot='activator')
v-icon(color='red') delete
v-card
.dialog-header.is-red Delete Group?
v-card-text Are you sure you want to delete group #[strong {{ name }}]? All users will be unassigned from this group.
v-card-text 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(flat, @click='deleteGroupDialog = false') Cancel
v-btn(color='red', dark, @click='deleteGroup') Delete
v-btn(color='primary', large, depressed, @click='updateGroup')
v-btn(color='success', large, depressed, @click='updateGroup')
v-icon(left) check
span Update Group
v-card.mt-3
v-tabs(v-model='tab', :color='$vuetify.dark ? "primary" : "grey darken-2"', fixed-tabs, slider-color='white', show-arrows, dark)
v-tab(key='properties') Properties
v-tab(key='permissions') Permissions
v-tab(key='rules') Page Rules
v-tab(key='users') Users
v-tab-item(key='properties', :transition='false', :reverse-transition='false')
v-card
v-card-text
v-text-field(
outline
background-color='grey lighten-3'
v-model='name'
label='Group Name'
counter='255'
prepend-icon='people'
)
v-tab-item(key='permissions', :transition='false', :reverse-transition='false')
v-container.pa-3(fluid, grid-list-md)
v-layout(row, wrap)
v-flex(xs12, md6, lg4, v-for='pmGroup in permissions')
v-card.md2.grey(flat, :class='$vuetify.dark ? "darken-4" : "lighten-5"')
v-subheader {{pmGroup.category}}
v-card-text.pt-0
template(v-for='(pm, idx) in pmGroup.items')
v-checkbox.pt-0(
:key='pm.permission'
:label='pm.permission'
:hint='pm.hint'
persistent-hint
color='primary'
v-model='group.permissions'
:value='pm.permission'
:append-icon='pm.warning ? "warning" : null',
:disabled='(group.isSystem && pm.restrictedForSystem) || group.id === 1 || pm.disabled'
)
v-divider.mt-3(v-if='idx < pmGroup.items.length - 1')
group-permissions(v-model='group', @refresh='refresh')
v-tab-item(key='rules', :transition='false', :reverse-transition='false')
v-card
v-card-title.pb-0
v-spacer
v-btn(flat, outline)
v-icon(left) arrow_drop_down
| Load Preset
v-btn(flat, outline)
v-icon(left) vertical_align_bottom
| Import Rules
.pa-3.pl-4
criterias
group-rules(v-model='group', @refresh='refresh')
v-tab-item(key='users', :transition='false', :reverse-transition='false')
v-card
v-card-title.pb-0
v-spacer
v-btn(color='primary', outline, flat, @click='searchUserDialog = true')
v-icon(left) assignment_ind
| Assign User
v-data-table(
:items='group.users',
:headers='headers',
:search='search',
:pagination.sync='pagination',
:rows-per-page-items='[15]'
hide-actions
)
template(slot='items', slot-scope='props')
tr(:active='props.selected')
td.text-xs-right {{ props.item.id }}
td {{ props.item.name }}
td {{ props.item.email }}
td
v-menu(bottom, right, min-width='200')
v-btn(icon, slot='activator'): v-icon.grey--text.text--darken-1 more_horiz
v-list
v-list-tile(@click='unassignUser(props.item.id)')
v-list-tile-action: v-icon(color='orange') highlight_off
v-list-tile-content
v-list-tile-title Unassign
template(slot='no-data')
v-alert.ma-3(icon='warning', :value='true', outline) No users to display.
.text-xs-center.py-2(v-if='users.length > 15')
v-pagination(v-model='pagination.page', :length='pages')
user-search(v-model='searchUserDialog', @select='assignUser')
group-users(v-model='group', @refresh='refresh')
</template>
<script>
import _ from 'lodash'
import Criterias from '../common/criterias.vue'
import UserSearch from '../common/user-search.vue'
import GroupPermissions from './admin-groups-edit-permissions.vue'
import GroupRules from './admin-groups-edit-rules.vue'
import GroupUsers from './admin-groups-edit-users.vue'
import groupQuery from 'gql/admin/groups/groups-query-single.gql'
import assignUserMutation from 'gql/admin/groups/groups-mutation-assign.gql'
import deleteGroupMutation from 'gql/admin/groups/groups-mutation-delete.gql'
import unassignUserMutation from 'gql/admin/groups/groups-mutation-unassign.gql'
import updateGroupMutation from 'gql/admin/groups/groups-mutation-update.gql'
export default {
components: {
Criterias,
UserSearch
GroupPermissions,
GroupRules,
GroupUsers
},
data() {
return {
@ -141,158 +68,10 @@ export default {
pageRules: [],
users: []
},
name: '',
deleteGroupDialog: false,
searchUserDialog: false,
pagination: {},
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 view and create new pages, as specified in the Page Rules',
warning: false,
restrictedForSystem: false,
disabled: false
},
{
permission: 'manage:pages',
hint: 'Can view, create, edit and move existing pages as specified in the Page Rules',
warning: false,
restrictedForSystem: false,
disabled: false
},
{
permission: 'delete:pages',
hint: 'Can delete existing pages, as specified in the Page Rules',
warning: false,
restrictedForSystem: false,
disabled: false
},
{
permission: 'write:assets',
hint: 'Can upload assets (such as images and files), as specified in the Page Rules',
warning: false,
restrictedForSystem: false,
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
}
]
},
{
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
}
]
}
],
users: [],
headers: [
{ text: 'ID', value: 'id', width: 50, align: 'right' },
{ text: 'Name', value: 'name' },
{ text: 'Email', value: 'email' },
{ text: '', value: 'actions', sortable: false, width: 50 }
],
search: '',
tab: '1'
}
},
computed: {
pages () {
if (this.pagination.rowsPerPage == null || this.pagination.totalItems == null) {
return 0
}
return Math.ceil(this.pagination.totalItems / this.pagination.rowsPerPage)
}
},
watch: {
group(newValue, oldValue) {
this.name = newValue.name
}
},
methods: {
async updateGroup() {
try {
@ -300,7 +79,9 @@ export default {
mutation: updateGroupMutation,
variables: {
id: this.group.id,
name: this.name
name: this.group.name,
permissions: this.group.permissions,
pageRules: this.group.pageRules
},
watchLoading (isLoading) {
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-groups-update')
@ -345,57 +126,8 @@ export default {
})
}
},
async assignUser(id) {
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.$apollo.queries.group.refetch()
} 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.$apollo.queries.group.refetch()
} catch (err) {
this.$store.commit('showNotification', {
style: 'red',
message: err.message,
icon: 'warning'
})
}
async refresh() {
return this.$apollo.queries.group.refetch()
}
},
apollo: {
@ -407,7 +139,7 @@ export default {
}
},
fetchPolicy: 'network-only',
update: (data) => data.groups.single,
update: (data) => _.cloneDeep(data.groups.single),
watchLoading (isLoading) {
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-groups-refresh')
}

@ -14,19 +14,18 @@
v-btn(color='primary', depressed, slot='activator', large)
v-icon(left) add
span New Group
v-card
v-card.wiki-form
.dialog-header.is-short New Group
v-card-text
v-text-field.md2(
solo,
flat,
background-color='grey lighten-4'
outline
background-color='grey lighten-3'
prepend-icon='people'
v-model='newGroupName'
label='Group Name'
counter='255'
@keyup.enter='createGroup'
ref='groupNameInput'
ref='groupNameIpt'
)
v-card-chin
v-spacer
@ -44,7 +43,7 @@
template(slot='items', slot-scope='props')
tr.is-clickable(:active='props.selected', @click='$router.push("/groups/" + props.item.id)')
td.text-xs-right {{ props.item.id }}
td {{ props.item.name }}
td: strong {{ props.item.name }}
td {{ props.item.userCount }}
td {{ props.item.createdAt | moment('calendar') }}
td {{ props.item.updatedAt | moment('calendar') }}
@ -93,6 +92,15 @@ export default {
return Math.ceil(this.pagination.totalItems / this.pagination.rowsPerPage)
}
},
watch: {
newGroupDialog(newValue, oldValue) {
if (newValue) {
this.$nextTick(() => {
this.$refs.groupNameIpt.focus()
})
}
}
},
methods: {
async refresh() {
await this.$apollo.queries.groups.refetch()
@ -103,6 +111,14 @@ export default {
})
},
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({
@ -138,36 +154,6 @@ export default {
icon: 'warning'
})
}
},
async deleteGroupConfirm(group) {
this.deleteGroupDialog = true
this.selectedGroup = group
},
async deleteGroup() {
this.deleteGroupDialog = false
try {
await this.$apollo.mutate({
mutation: deleteGroupMutation,
variables: {
id: this.selectedGroup.id
},
watchLoading (isLoading) {
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-groups-delete')
}
})
await this.$apollo.queries.groups.refetch()
this.$store.commit('showNotification', {
style: 'success',
message: `Group ${this.selectedGroup.name} has been deleted.`,
icon: 'delete'
})
} catch (err) {
this.$store.commit('showNotification', {
style: 'red',
message: err.message,
icon: 'warning'
})
}
}
},
apollo: {

@ -33,10 +33,11 @@
v-subheader {{ $t('admin:system.hostInfo') }}
v-list-tile(avatar)
v-list-tile-avatar
v-icon.blue-grey.white--text bubble_chart
v-avatar.blue-grey(size='40')
img(:src='`/svg/icon-` + platformLogo + `-logo.svg`', alt='Platform', style='width: 24px;')
v-list-tile-content
v-list-tile-title {{ $t('admin:system.os') }}
v-list-tile-sub-title {{ info.operatingSystem }}
v-list-tile-sub-title {{ (info.platform === 'docker') ? 'Docker Container (Linux)' : info.operatingSystem }}
v-list-tile(avatar)
v-list-tile-avatar
v-icon.blue-grey.white--text computer
@ -127,6 +128,20 @@ export default {
computed: {
dbVersion() {
return _.get(this.info, 'dbVersion', '').replace(/(?:\r\n|\r|\n)/g, '<br />')
},
platformLogo() {
switch (this.info.platform) {
case 'docker':
return 'docker'
case 'darwin':
return 'apple'
case 'linux':
return 'linux'
case 'win32':
return 'windows'
default:
return ''
}
}
},
methods: {

@ -1,29 +1,23 @@
<template lang="pug">
v-dialog(v-model='isShown', max-width='550')
v-card
v-card.wiki-form
.dialog-header.is-short New Local User
v-card-text
v-text-field.md2(
solo
flat
background-color='grey lighten-4'
outline
prepend-icon='email'
v-model='email'
label='Email Address'
ref='emailInput'
)
v-text-field.md2(
solo
flat
background-color='grey lighten-4'
outline
prepend-icon='person'
v-model='name'
label='Name'
)
v-text-field.md2(
solo
flat
background-color='grey lighten-4'
outline
prepend-icon='lock'
append-icon='casino'
v-model='password'
@ -31,32 +25,10 @@
counter='255'
@click:append='generatePwd'
)
v-text-field.md2(
solo
flat
background-color='grey lighten-4'
prepend-icon='title'
v-model='jobTitle'
label='Job Title'
counter='255'
hint='Optional'
persistent-hint
)
v-text-field.md2(
solo
flat
background-color='grey lighten-4'
prepend-icon='public'
v-model='location'
label='Location'
counter='255'
hint='Optional'
persistent-hint
)
v-card-chin
v-spacer
v-btn(flat, @click='isShown = false') Cancel
v-btn(color='primary', @click='createUser') Create
v-btn(color='primary', @click='createUser') Create User
</template>
<script>

@ -1,211 +0,0 @@
<template lang="pug">
.criterias-item
//- Type
v-select(
solo
:items='filteredCriteriaTypes'
v-model='item.type'
placeholder='Rule Type'
ref='typeSelect'
hide-details
)
template(slot='item', slot-scope='data')
v-list-tile-avatar
v-avatar(:color='data.item.color', size='40', tile): v-icon(color='white') {{ data.item.icon }}
v-list-tile-content
v-list-tile-title(v-html='data.item.text')
v-list-tile-sub-title.caption(v-html='data.item.description')
//- Operator
v-select(
solo
:items='filteredCriteriaOperators'
v-model='item.operator'
placeholder='Operator'
:disabled='!item.type'
:class='!item.type ? "blue-grey lighten-4" : ""'
hide-details
)
template(slot='item', slot-scope='data')
v-list-tile-avatar
v-avatar.white--text(color='blue', size='30', tile) {{ data.item.icon }}
v-list-tile-content
v-list-tile-title(v-html='data.item.text')
//- Value
v-select(
v-if='item.type === "country"'
solo
:items='countries'
v-model='item.value'
placeholder='Countries...'
multiple
item-text='name'
item-value='code'
hide-details
)
v-text-field(
v-else-if='item.type === "path"'
solo
v-model='item.value'
label='Path (e.g. /section)'
hide-details
)
v-text-field(
v-else-if='item.type === "date"'
solo
@click.native.stop='dateActivator = true'
v-model='item.value'
label='YYYY-MM-DD'
readonly
hide-details
)
v-text-field(
v-else-if='item.type === "time"'
solo
@click.native.stop='timeActivator = true'
v-model='item.value'
label='HH:MM'
readonly
hide-details
)
v-select(
v-else-if='item.type === "group"'
solo
:items='groups'
v-model='item.value'
placeholder='Group...'
item-text='name'
item-value='id'
hide-details
)
v-text-field.blue-grey.lighten-4(
v-else
solo
disabled
hide-details
)
v-dialog(lazy, v-model='dateActivator', width='290px', ref='dateDialog')
v-date-picker(v-model='item.value', scrollable, color='primary')
v-btn(flat, color='primary' @click='$refs.dateDialog.save(date)', block) ok
v-dialog(lazy, v-model='timeActivator', width='300px', ref='timeDialog')
v-time-picker(v-model='item.value', scrollable, color='primary')
v-btn(flat, color='primary' @click='$refs.timeDialog.save(time)', block) ok
v-btn(icon, @click='remove'): v-icon(color='blue-grey') clear
</template>
<script>
import _ from 'lodash'
// import countriesQuery from 'gql/upsells-query-countries.gql'
export default {
inject: ['allowedCriteriaTypes'],
props: {
value: {
type: Object,
default() { return {} }
},
groupIndex: {
type: Number,
default() { return 0 }
},
itemIndex: {
type: Number,
default() { return 0 }
}
},
data() {
return {
item: {
operator: '',
type: '',
value: ''
},
dateActivator: false,
dateDialog: false,
timeActivator: false,
timeDialog: false,
countries: [],
groups: [],
criteriaTypes: [
{ text: 'Path', value: 'path', icon: 'space_bar', color: 'blue', description: 'Match the path of the document being viewed.' },
{ text: 'Date', value: 'date', icon: 'date_range', color: 'blue', description: 'Match the current calendar day.' },
{ text: 'Time', value: 'time', icon: 'access_time', color: 'blue', description: 'Match the current time of day.' },
{ text: 'User Country', value: 'country', icon: 'public', color: 'red', description: `Match the user's country.` },
{ text: 'User Group', value: 'group', icon: 'group', color: 'orange', description: 'Match the user group assignments.' }
],
criteriaOperators: {
country: [
{ text: 'In', value: 'in', icon: '[...]' },
{ text: 'Not In', value: 'notIn', icon: '[ x ]' }
],
path: [
{ text: 'Matches Exactly', value: 'eq', icon: '=' },
{ text: 'NOT Matches Exactly', value: 'ne', icon: '!=' },
{ text: 'Starts With', value: 'sw', icon: 'x...' },
{ text: 'NOT Starts With', value: 'nsw', icon: '!x...' },
{ text: 'Ends With', value: 'ew', icon: '...x' },
{ text: 'NOT Ends With', value: 'new', icon: '!...x' },
{ text: 'Matches Regex', value: 'regexp', icon: '^x$' }
],
date: [
{ text: 'On or After', value: 'gte', icon: '>=' },
{ text: 'On or Before', value: 'lte', icon: '<=' }
],
time: [
{ text: 'At or Later Than', value: 'gte', icon: '>=' },
{ text: 'At or Before', value: 'lte', icon: '<=' }
],
group: [
{ text: 'Is Part Of', value: 'in', icon: '[...]' },
{ text: 'Is Not Part Of', value: 'notIn', icon: '[ x ]' }
]
}
}
},
computed: {
filteredCriteriaOperators() {
return _.get(this.criteriaOperators, this.item.type, [])
},
filteredCriteriaTypes() {
console.info(this.allowedCriteriaTypes)
return _.filter(this.criteriaTypes, c => _.includes(this.allowedCriteriaTypes, c.value))
},
itemType() {
return this.item.type
}
},
watch: {
itemType(newValue, oldValue) {
this.item.operator = _.head(this.criteriaOperators[newValue]).value
this.item.value = ''
},
item: {
handler(newValue, oldValue) {
this.$emit('update', this.groupIndex, this.itemIndex, this.item)
},
deep: true
}
},
mounted() {
if (!this.item.type) {
this.$refs.typeSelect.showMenu()
}
},
methods: {
remove() {
this.$emit('remove', this.groupIndex, this.itemIndex)
}
}
// apollo: {
// countries: {
// query: countriesQuery,
// update: (data) => data.location.countries
// }
// }
}
</script>

@ -1,173 +0,0 @@
<template lang="pug">
.criterias
transition-group(name='criterias-group', tag='div')
.criterias-group(v-for='(group, g) in groups', :key='g')
transition-group(name='criterias-item', tag='div')
criterias-item(v-for='(item, i) in group', :key='i', :item='item', :group-index='g', :item-index='i', @update='updateItem', @remove='removeItem')
.criterias-item-more
v-btn.ml-0(@click='addItem(group)', small, color='blue-grey lighten-2', dark, depressed)
v-icon(color='white', left) add
| Add condition
.criterias-group-more
v-btn(@click='addGroup', small, color='blue-grey lighten-1', dark, depressed)
v-icon(color='white', left) add
| Add condition group
</template>
<script>
import CriteriasItem from './criterias-item.vue'
export default {
components: {
CriteriasItem
},
props: {
value: {
type: Array,
default() { return [] }
},
types: {
type: Array,
default() {
return ['country', 'path', 'date', 'time', 'group']
}
}
},
provide () {
return {
allowedCriteriaTypes: this.types
}
},
data() {
return {
dataGroups: this.value || []
}
},
computed: {
groups: {
get() { return this.dataGroups },
set(grp) {
this.dataGroups = grp
}
}
},
watch: {
dataGroups(newValue, oldValue) {
if (newValue !== oldValue) {
this.$emit('input', newValue)
}
}
},
methods: {
addGroup() {
this.dataGroups.push([{}])
},
addItem(group) {
group.push({})
},
updateItem(groupIndex, itemIndex, item) {
console.info(item)
this.$set(this.dataGroups[groupIndex], itemIndex, item)
},
removeItem(groupIndex, itemIndex) {
this.dataGroups[groupIndex].splice(itemIndex, 1)
if (this.dataGroups[groupIndex].length < 1) {
this.dataGroups.splice(groupIndex, 1)
}
}
}
}
</script>
<style lang="scss">
.criterias {
&-group {
background-color: mc('blue-grey', '100');
border-radius: 4px;
padding: 1rem;
position: relative;
&-enter-active, &-leave-active {
transition: all .5s ease;
}
&-enter, &-leave-to {
opacity: 0;
}
& + .criterias-group {
margin-top: 1rem;
&::before {
content: 'OR';
position: absolute;
display: inline-flex;
padding: 0 2rem;
top: -1.25rem;
left: 2rem;
background-color: mc('blue-grey', '100');
color: mc('blue-grey', '700');
font-weight: 600;
font-size: .9rem;
}
}
&-more {
margin: .5rem 0 0 .4rem;
}
}
&-item {
display: flex;
background-color: mc('blue-grey', '200');
border-radius: 4px;
padding: .5rem;
&-enter-active, &-leave-active {
transition: all .5s ease;
}
&-enter, &-leave-to {
opacity: 0;
}
& + .criterias-item {
margin-top: .5rem;
position: relative;
&::before {
content: 'AND';
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: .7rem;
background-color: mc('blue-grey', '100');
left: -2rem;
top: -1.3rem;
}
}
.input-group {
&:nth-child(1) {
flex: 0 1 350px;
}
&:nth-child(2) {
flex: 0 1 250px;
}
& + * {
margin-left: .5rem;
}
}
&-more {
margin-top: .15rem;
}
}
}
</style>

@ -16,117 +16,123 @@
:loading='searchIsLoading',
@keyup.enter='searchEnter'
)
v-menu(open-on-hover, offset-y, bottom, left, min-width='250')
v-toolbar-side-icon.btn-animate-app(slot='activator')
v-icon view_module
v-list(dense, :light='!$vuetify.dark', :dark='$vuetify.dark', :class='$vuetify.dark ? `grey darken-4` : ``').py-0
v-list-tile(avatar, href='/')
v-list-tile-avatar: v-icon(color='blue') home
v-list-tile-content Home
v-list-tile(avatar, @click='pageNew')
v-list-tile-avatar: v-icon(color='green') add_box
v-list-tile-content New Page
template(v-if='path && path.length')
v-divider.my-0
v-subheader Current Page
v-list-tile(avatar, @click='pageView', v-if='mode !== `view`')
v-list-tile-avatar: v-icon(color='indigo') subject
v-list-tile-content View
v-list-tile(avatar, @click='pageEdit', v-if='mode !== `edit`')
v-list-tile-avatar: v-icon(color='indigo') edit
v-list-tile-content Edit
v-list-tile(avatar, @click='pageHistory', v-if='mode !== `history`')
v-list-tile-avatar: v-icon(color='indigo') history
v-list-tile-content History
v-list-tile(avatar, @click='pageSource', v-if='mode !== `source`')
v-list-tile-avatar: v-icon(color='indigo') code
v-list-tile-content View Source
v-list-tile(avatar, @click='pageMove')
v-list-tile-avatar: v-icon(color='indigo') forward
v-list-tile-content Move / Rename
v-list-tile(avatar, @click='pageDelete')
v-list-tile-avatar: v-icon(color='red darken-2') delete
v-list-tile-content Delete
v-divider.my-0
v-subheader Assets
v-list-tile(avatar, @click='')
v-list-tile-avatar: v-icon(color='blue-grey') burst_mode
v-list-tile-content Images &amp; Files
v-toolbar-title(:class='{ "ml-2": $vuetify.breakpoint.mdAndUp, "ml-0": $vuetify.breakpoint.smAndDown }')
span.subheading {{title}}
v-spacer(v-if='searchIsShown && $vuetify.breakpoint.mdAndUp')
transition(name='navHeaderSearch')
v-text-field(
ref='searchField',
v-if='searchIsShown && $vuetify.breakpoint.mdAndUp',
v-model='search',
clearable,
color='white',
label='Search...',
single-line,
solo
flat
hide-details,
prepend-inner-icon='search',
:loading='searchIsLoading',
@keyup.enter='searchEnter'
)
v-progress-linear(
indeterminate,
slot='progress',
height='2',
color='blue'
)
v-spacer
.navHeaderLoading.mr-3
v-progress-circular(indeterminate, color='blue', :size='22', :width='2' v-show='isLoading')
slot(name='actions')
v-btn(
v-if='!hideSearch && $vuetify.breakpoint.smAndDown'
@click='searchToggle'
icon
)
v-icon(color='grey') search
v-tooltip(bottom, v-if='isAuthenticated && isAdmin')
v-btn.btn-animate-rotate(icon, href='/a', slot='activator')
v-icon(color='grey') settings
span Admin
v-menu(offset-y, min-width='300')
v-tooltip(bottom, slot='activator')
v-btn.btn-animate-grow(icon, slot='activator', outline, :color='isAuthenticated ? `blue` : `grey darken-3`')
v-icon(color='grey') account_circle
span Account
v-list.py-0
template(v-if='isAuthenticated')
v-list-tile.py-3.grey(avatar, :class='$vuetify.dark ? `darken-4-l5` : `lighten-5`')
v-list-tile-avatar
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-list-tile-content
v-list-tile-title {{name}}
v-list-tile-sub-title {{email}}
v-divider.my-0
v-list-tile(href='/w')
v-list-tile-action: v-icon(color='blue') web
v-list-tile-title My Wiki
v-divider.my-0
v-list-tile(href='/p')
v-list-tile-action: v-icon(color='blue') person
v-list-tile-title Profile
v-divider.my-0
v-list-tile(@click='logout')
v-list-tile-action: v-icon(color='red') exit_to_app
v-list-tile-title Logout
template(v-else)
v-list-tile(href='/login')
v-list-tile-action: v-icon(color='grey') person
v-list-tile-title Login
v-divider.my-0
v-list-tile(href='/register')
v-list-tile-action: v-icon(color='grey') person_add
v-list-tile-title Register
v-layout(row)
v-flex(xs6, :md4='searchIsShown', :md6='!searchIsShown')
v-toolbar.nav-header-inner(color='black', dark, flat)
v-menu(open-on-hover, offset-y, bottom, left, min-width='250')
v-toolbar-side-icon.btn-animate-app(slot='activator')
v-icon view_module
v-list(dense, :light='!$vuetify.dark', :dark='$vuetify.dark', :class='$vuetify.dark ? `grey darken-4` : ``').py-0
v-list-tile(avatar, href='/')
v-list-tile-avatar: v-icon(color='blue') home
v-list-tile-content Home
v-list-tile(avatar, @click='pageNew')
v-list-tile-avatar: v-icon(color='green') add_box
v-list-tile-content New Page
template(v-if='path && path.length')
v-divider.my-0
v-subheader Current Page
v-list-tile(avatar, @click='pageView', v-if='mode !== `view`')
v-list-tile-avatar: v-icon(color='indigo') subject
v-list-tile-content View
v-list-tile(avatar, @click='pageEdit', v-if='mode !== `edit`')
v-list-tile-avatar: v-icon(color='indigo') edit
v-list-tile-content Edit
v-list-tile(avatar, @click='pageHistory', v-if='mode !== `history`')
v-list-tile-avatar: v-icon(color='indigo') history
v-list-tile-content History
v-list-tile(avatar, @click='pageSource', v-if='mode !== `source`')
v-list-tile-avatar: v-icon(color='indigo') code
v-list-tile-content View Source
v-list-tile(avatar, @click='pageMove')
v-list-tile-avatar: v-icon(color='indigo') forward
v-list-tile-content Move / Rename
v-list-tile(avatar, @click='pageDelete')
v-list-tile-avatar: v-icon(color='red darken-2') delete
v-list-tile-content Delete
v-divider.my-0
v-subheader Assets
v-list-tile(avatar, @click='')
v-list-tile-avatar: v-icon(color='blue-grey') burst_mode
v-list-tile-content Images &amp; Files
v-toolbar-title(:class='{ "ml-2": $vuetify.breakpoint.mdAndUp, "ml-0": $vuetify.breakpoint.smAndDown }')
span.subheading {{title}}
v-flex(md4, v-if='searchIsShown && $vuetify.breakpoint.mdAndUp')
v-toolbar.nav-header-inner(color='black', dark, flat)
transition(name='navHeaderSearch')
v-text-field(
ref='searchField',
v-if='searchIsShown && $vuetify.breakpoint.mdAndUp',
v-model='search',
clearable,
color='white',
label='Search...',
single-line,
solo
flat
hide-details,
prepend-inner-icon='search',
:loading='searchIsLoading',
@keyup.enter='searchEnter'
)
v-progress-linear(
indeterminate,
slot='progress',
height='2',
color='blue'
)
v-flex(xs6, :md4='searchIsShown', :md6='!searchIsShown')
v-toolbar.nav-header-inner(color='black', dark, flat)
v-spacer
.navHeaderLoading.mr-3
v-progress-circular(indeterminate, color='blue', :size='22', :width='2' v-show='isLoading')
slot(name='actions')
v-btn(
v-if='!hideSearch && $vuetify.breakpoint.smAndDown'
@click='searchToggle'
icon
)
v-icon(color='grey') search
v-tooltip(bottom, v-if='isAuthenticated && isAdmin')
v-btn.btn-animate-rotate(icon, href='/a', slot='activator')
v-icon(color='grey') settings
span Admin
v-menu(offset-y, min-width='300')
v-tooltip(bottom, slot='activator')
v-btn.btn-animate-grow(icon, slot='activator', outline, :color='isAuthenticated ? `blue` : `grey darken-3`')
v-icon(color='grey') account_circle
span Account
v-list.py-0
template(v-if='isAuthenticated')
v-list-tile.py-3.grey(avatar, :class='$vuetify.dark ? `darken-4-l5` : `lighten-5`')
v-list-tile-avatar
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-list-tile-content
v-list-tile-title {{name}}
v-list-tile-sub-title {{email}}
v-divider.my-0
v-list-tile(href='/w')
v-list-tile-action: v-icon(color='blue') web
v-list-tile-title My Wiki
v-divider.my-0
v-list-tile(href='/p')
v-list-tile-action: v-icon(color='blue') person
v-list-tile-title Profile
v-divider.my-0
v-list-tile(@click='logout')
v-list-tile-action: v-icon(color='red') exit_to_app
v-list-tile-title Logout
template(v-else)
v-list-tile(href='/login')
v-list-tile-action: v-icon(color='grey') person
v-list-tile-title Login
v-divider.my-0
v-list-tile(href='/register')
v-list-tile-action: v-icon(color='grey') person_add
v-list-tile-title Register
page-selector(mode='create', v-model='newPageModal', :open-handler='pageNewCreate')
</template>
@ -251,6 +257,12 @@ export default {
padding-right: 14px;
}
}
&-inner {
.v-toolbar__content {
padding: 0;
}
}
}
.navHeaderSearch {

@ -3,7 +3,7 @@
v-model='dialogOpen'
max-width='650'
)
v-card
v-card.wiki-form
.dialog-header
span Search User
v-spacer
@ -16,23 +16,25 @@
)
v-card-text
v-text-field(
solo
flat
outline
label='Search Users...'
v-model='search'
prepend-icon='search'
:background-color='$vuetify.dark ? "grey darken-4" : "blue lighten-5"'
prepend-inner-icon='search'
color='primary'
ref='searchIpt'
hide-details
)
v-list(two-line)
v-list.grey.mt-3.py-0.radius-7(
:class='$vuetify.dark ? `darken-3-d5` : `lighten-3`'
two-line
dense
)
template(v-for='(usr, idx) in items')
v-list-tile(:key='usr.id', @click='setUser(usr.id)')
v-list-tile-avatar(size='40', color='primary')
span.body-1.white--text {{usr.name | initials}}
v-list-tile-content
v-list-tile-title {{usr.name}}
v-list-tile-title.body-2 {{usr.name}}
v-list-tile-sub-title {{usr.email}}
v-list-tile-action
v-icon(color='primary') arrow_forward

@ -243,6 +243,7 @@ export default {
})
this.$store.set('editor/id', _.get(resp, 'page.id'))
this.$store.set('editor/mode', 'update')
window.location.assign(`/${this.$store.get('page/path')}`)
} else {
throw new Error(_.get(resp, 'responseResult.message'))
}

@ -1,6 +1,6 @@
mutation ($id: Int!, $name: String!) {
mutation ($id: Int!, $name: String!, $permissions: [String]!, $pageRules: [PageRuleInput]!) {
groups {
update(id: $id, name: $name) {
update(id: $id, name: $name, permissions: $permissions, pageRules: $pageRules) {
responseResult {
succeeded
errorCode

@ -8,9 +8,10 @@ query ($id: Int!) {
pageRules {
id
path
role
exact
allow
roles
match
deny
locales
}
users {
id

@ -9,6 +9,7 @@ query {
latestVersion
latestVersionReleaseDate
operatingSystem
platform
hostname
cpuCores
ramTotal

@ -0,0 +1,4 @@
<?xml version="1.0"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24px" height="24px">
<path style="fill:#ffffff;" d="M 16.125 1 C 14.972 1.067 13.648328 1.7093438 12.861328 2.5273438 C 12.150328 3.2713438 11.589359 4.3763125 11.818359 5.4453125 C 13.071359 5.4783125 14.329031 4.8193281 15.082031 3.9863281 C 15.785031 3.2073281 16.318 2.12 16.125 1 z M 16.193359 5.4433594 C 14.384359 5.4433594 13.628 6.5546875 12.375 6.5546875 C 11.086 6.5546875 9.9076562 5.5136719 8.3476562 5.5136719 C 6.2256562 5.5146719 3 7.4803281 3 12.111328 C 3 16.324328 6.8176563 21 8.9726562 21 C 10.281656 21.013 10.599 20.176969 12.375 20.167969 C 14.153 20.154969 14.536656 21.011 15.847656 21 C 17.323656 20.989 18.476359 19.367031 19.318359 18.082031 C 19.922359 17.162031 20.170672 16.692344 20.638672 15.652344 C 17.165672 14.772344 16.474672 9.1716719 20.638672 8.0136719 C 19.852672 6.6726719 17.558359 5.4433594 16.193359 5.4433594 z"/>
</svg>

After

Width:  |  Height:  |  Size: 962 B

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;">
<g>
<path d="M3.929,43.929c3.902,-3.903 10.239,-3.903 14.142,0c3.902,3.902 3.902,10.239 0,14.142c-3.903,3.902 -10.24,3.902 -14.142,0c-3.903,-3.903 -3.903,-10.24 0,-14.142Z" style="fill:#ff5576;"/>
<path d="M7.5,56c-0.4,0 -0.75,-0.15 -1.05,-0.45c-0.6,-0.6 -0.6,-1.55 0,-2.1l7,-7c0.6,-0.6 1.55,-0.6 2.1,0c0.6,0.6 0.6,1.55 0,2.1l-7,7c-0.3,0.3 -0.65,0.45 -1.05,0.45Z" style="fill:#fff;fill-rule:nonzero;"/>
<path d="M14.5,56c-0.4,0 -0.75,-0.15 -1.05,-0.45l-7,-7c-0.6,-0.6 -0.6,-1.55 0,-2.1c0.6,-0.6 1.55,-0.6 2.1,0l7,7c0.6,0.6 0.6,1.55 0,2.1c-0.3,0.3 -0.65,0.45 -1.05,0.45Z" style="fill:#fff;fill-rule:nonzero;"/>
</g>
<g>
<path d="M53,19.5c-0.85,0 -1.5,0.65 -1.5,1.5l-0.05,33.5c0,1.95 -1.55,3.5 -3.5,3.5l-21.95,0c-0.85,0 -1.5,0.65 -1.5,1.5c0,0.85 0.65,1.5 1.5,1.5l21.95,0c3.6,0 6.5,-2.9 6.5,-6.5l0.05,-33.5c0,-0.85 -0.7,-1.5 -1.5,-1.5Z" style="fill:#fff;fill-rule:nonzero;"/>
<path d="M51.15,6.4c-2.2,-2.2 -5.05,-3.4 -8.15,-3.4l-27,0c-3.6,0 -6.5,2.9 -6.5,6.5l0,24c0,0.85 0.65,1.5 1.5,1.5c0.85,0 1.5,-0.65 1.5,-1.5l0,-24c0,-1.95 1.55,-3.5 3.5,-3.5l25.5,0l0,3.5c0,3.6 2.9,6.5 6.5,6.5l5,0c0.6,0 1.15,-0.4 1.4,-0.9c0.1,-0.2 0.15,-0.4 0.15,-0.6c0,-0.05 0,-0.1 0,-0.2c-0.1,-3 -1.3,-5.8 -3.4,-7.9Zm-6.65,3.1l0,-3.35c1.7,0.3 3.25,1.1 4.5,2.35c1.25,1.25 2.05,2.8 2.35,4.5l-3.35,0c-1.95,0 -3.5,-1.55 -3.5,-3.5Z" style="fill:#fff;fill-rule:nonzero;"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 24 24" version="1.1" width="24px" height="24px">
<path style="fill:#ffffff;" d="M 13 1 L 13 4 L 16 4 L 16 1 Z M 5 5 L 5 8 L 8 8 L 8 5 Z M 9 5 L 9 8 L 12 8 L 12 5 Z M 13 5 L 13 8 L 16 8 L 16 5 Z M 18.3125 7.84375 C 17.9375 7.878906 17.289063 8.507813 17 9.03125 C 16.269531 10.339844 16.828125 11.988281 16.96875 12.34375 C 16.644531 12.761719 15 13 15 13 L 0.96875 13 C 0.148438 13.027344 0 14.246094 0 14.53125 C -0.0351563 19.5 3.710938 21.988281 7.96875 22 C 14.132813 22.019531 17.871094 19.5625 20.03125 16 L 23 16 C 22.492188 15.871094 21.820313 15.660156 22 15 C 21.800781 15.292969 21.09375 15.582031 20.25 15.625 C 20.335938 15.476563 20.421875 15.339844 20.5 15.1875 C 21.824219 15.109375 22.585938 14.488281 23.15625 13.9375 C 23.808594 13.308594 23.960938 12.953125 24 12.59375 C 24.0625 12.050781 22.617188 11.113281 20.84375 10.90625 C 20.675781 9.261719 18.871094 7.789063 18.3125 7.84375 Z M 1 9 L 1 12 L 4 12 L 4 9 Z M 5 9 L 5 12 L 8 12 L 8 9 Z M 9 9 L 9 12 L 12 12 L 12 9 Z M 13 9 L 13 12 L 16 12 L 16 9 Z M 18.4375 9.9375 C 18.726563 10.261719 19.3125 10.992188 19.4375 11.65625 C 19.519531 12.089844 20.121094 12.429688 20.5625 12.46875 C 21.25 12.53125 21.671875 12.664063 22.0625 12.78125 C 21.933594 13.027344 21.054688 13.550781 19.78125 13.5 C 19.441406 13.484375 19.128906 13.6875 19 14 C 18.921875 14.199219 18.632813 14.632813 18.21875 15.15625 C 18.132813 15.105469 18.050781 15.058594 17.96875 15 C 17.515625 15.425781 15.355469 16.308594 13.96875 15 C 13.1875 15.917969 10.75 15.917969 9.96875 15 C 9.714844 15.703125 6.59375 15.902344 5.96875 15 C 5.792969 15.8125 2.992188 16.199219 1.96875 15 L 1.96875 15.5 C 1.949219 15.328125 1.945313 15.15625 1.96875 15 C 1.96875 15 4.289063 15 5.96875 15 C 7.648438 15 8.082031 15 9.96875 15 C 11.855469 15 13.339844 15.003906 13.96875 15 C 14.132813 15 14.984375 15 15 15 C 17.03125 14.875 18.011719 13.761719 18.34375 13.5 C 18.597656 13.300781 18.851563 12.785156 18.625 12.40625 C 18.609375 12.378906 18.003906 10.660156 18.4375 9.9375 Z M 2.0625 16 L 17.46875 16 C 15.738281 17.8125 12.570313 20.101563 7.96875 20 C 6.574219 19.96875 5.101563 19.753906 4.09375 18.96875 C 5.792969 18.910156 7.28125 18.59375 7.28125 18.59375 C 7.714844 18.511719 7.988281 18.089844 7.90625 17.65625 C 7.824219 17.222656 7.402344 16.917969 6.96875 17 C 6.933594 17.007813 4.667969 17.5625 2.59375 17.3125 C 2.476563 17.097656 2.21875 16.570313 2.0625 16 Z M 8.4375 17 C 8.179688 17 7.96875 17.210938 7.96875 17.46875 C 7.96875 17.726563 8.179688 17.9375 8.4375 17.9375 C 8.695313 17.9375 8.90625 17.726563 8.90625 17.46875 C 8.90625 17.40625 8.898438 17.367188 8.875 17.3125 C 8.84375 17.371094 8.757813 17.40625 8.6875 17.40625 C 8.582031 17.40625 8.53125 17.292969 8.53125 17.1875 C 8.53125 17.117188 8.535156 17.09375 8.59375 17.0625 C 8.542969 17.042969 8.496094 17 8.4375 17 Z "/>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 24 24" version="1.1" width="24px" height="24px">
<path style="fill:#ffffff;" d="M 10.4375 2 C 6.140625 2 3 4.824219 3 11 C 3 14.410156 4.773438 20.515625 2 22 L 4.3125 22 C 5.203125 20.640625 5.09375 18.6875 5.09375 18.6875 C 5.09375 18.6875 4.03125 17.234375 4.03125 16.46875 C 4.03125 15.0625 5.15625 14.3125 5.15625 14.3125 C 5.15625 14.3125 4.09375 13.304688 4.09375 11.5625 C 4.09375 10.054688 5.296875 9.375 5.90625 9.375 C 8.144531 9.375 8.25 12 8.25 12 L 10.375 12 C 10.375 12 10.238281 9.125 12.96875 9.125 C 13.890625 9.125 15.46875 9.933594 15.46875 12.0625 C 15.46875 13.261719 15.09375 14 15.09375 14 C 15.09375 14 16.46875 14.734375 16.46875 16.03125 C 16.46875 17.285156 15.53125 18.34375 15.53125 18.34375 L 17.09375 22 L 22 22 C 22 22 19 20.789063 19 11 C 19 10.253906 19.222656 2 10.4375 2 Z M 13.75 4.03125 C 14.003906 4.027344 14.289063 4.109375 14.5625 4.28125 C 15.109375 4.628906 15.386719 5.203125 15.15625 5.5625 C 14.925781 5.921875 14.296875 5.941406 13.75 5.59375 C 13.203125 5.246094 12.925781 4.671875 13.15625 4.3125 C 13.269531 4.132813 13.496094 4.035156 13.75 4.03125 Z M 6.125 11 C 5.617188 11 5.03125 11.367188 5.03125 12.46875 C 5.03125 13.453125 6.3125 14.34375 6.3125 14.34375 C 6.3125 14.34375 4.5625 15.175781 4.5625 16.5 C 4.5625 17.226563 5.316406 17.96875 6.0625 18.5625 C 6.800781 19.167969 7.738281 20.21875 9 20.21875 C 10.796875 20.21875 12.292969 18.859375 13.09375 18.375 C 13.894531 17.890625 15.5 17.109375 15.5 16 C 15.5 14.671875 13.03125 14.28125 13.03125 14.28125 C 13.03125 14.28125 14.25 13.890625 14.25 12.59375 C 14.25 11.679688 13.65625 11 12.90625 11 C 11.820313 11 11.53125 12.046875 11.53125 12.59375 C 11.53125 13.210938 11.78125 13.8125 11.78125 13.8125 C 11.78125 13.8125 10.46875 13.03125 9 13.03125 C 8.214844 13.03125 7.34375 13.5625 7.34375 13.5625 C 7.34375 13.5625 7.4375 13.128906 7.4375 12.65625 C 7.4375 12.175781 7.15625 11 6.125 11 Z M 8.28125 13.75 C 8.546875 13.703125 8.25 14.203125 7.9375 14.53125 C 7.816406 14.65625 7.40625 14.277344 7.4375 14.1875 C 7.480469 14.09375 7.878906 13.835938 8.28125 13.75 Z M 9.71875 13.75 C 9.917969 13.75 10.339844 13.851563 10.5625 13.96875 C 10.730469 14.058594 10.699219 14.539063 10.53125 14.625 C 10.3125 14.734375 9.625 13.96875 9.625 13.78125 C 9.625 13.75 9.652344 13.75 9.71875 13.75 Z M 14.5 16 C 14.5 16 11.46875 19 9 19 C 6.890625 19 5.0625 16.71875 5.0625 16.71875 C 5.0625 16.71875 7.140625 18 9 18 C 11.125 18 14.5 16 14.5 16 Z "/>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

@ -0,0 +1,4 @@
<?xml version="1.0"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24px" height="24px">
<path style="fill:#ffffff;" d="M 21 2 L 12 3.5 L 12 11 L 21 11 L 21 2 z M 10 3.8320312 L 3 5 L 3 11 L 10 11 L 10 3.8320312 z M 3 13 L 3 19 L 10 20.167969 L 10 13 L 3 13 z M 12 13 L 12 20.5 L 21 22 L 21 13 L 12 13 z"/>
</svg>

After

Width:  |  Height:  |  Size: 337 B

@ -6,16 +6,28 @@
dark
app
clipped
:mini-variant='$vuetify.breakpoint.md || $vuetify.breakpoint.sm'
mini-variant-width='80'
mobile-break-point='600'
:temporary='$vuetify.breakpoint.xs'
:temporary='$vuetify.breakpoint.mdAndDown'
v-model='navShown'
)
vue-scroll(:ops='scrollStyle')
nav-sidebar(:color='darkMode ? `grey darken-3` : `primary`')
slot(name='sidebar')
v-fab-transition
v-btn(
fab
color='primary'
fixed
bottom
left
small
@click='navShown = !navShown'
v-if='$vuetify.breakpoint.mdAndDown'
v-show='!navShown'
)
v-icon menu
v-content
template(v-if='path !== `home`')
v-toolbar(:color='darkMode ? `grey darken-4-d3` : `grey lighten-3`', flat, dense)
@ -167,7 +179,8 @@ export default {
},
data() {
return {
navOpen: false,
navShown: false,
navExpanded: false,
upBtnShown: false,
scrollOpts: {
duration: 1500,
@ -203,10 +216,6 @@ export default {
},
computed: {
darkMode: get('site/dark'),
navShown: {
get() { return this.navOpen || this.$vuetify.breakpoint.smAndUp },
set(val) { this.navOpen = val }
},
rating: {
get () {
return 3.5
@ -232,6 +241,7 @@ export default {
},
mounted () {
Prism.highlightAllUnder(this.$refs.container)
this.navShown = this.$vuetify.breakpoint.smAndUp
},
methods: {
toggleNavigation () {

@ -0,0 +1,35 @@
const { Client } = require('pg')
const fs = require('fs')
const path = require('path')
const yaml = require('js-yaml')
let config = {}
try {
conf = yaml.safeLoad(
cfgHelper.parseConfigValue(
fs.readFileSync(path.join(process.cwd(), 'dev/docker/config.yml'), 'utf8')
)
)
} catch (err) {
console.error(err.message)
process.exit(1)
}
const client = new Client({
user: config.db.username,
host: config.db.host,
database: config.db.database,
password: config.db.password,
port: config.db.port,
})
async function main () {
await client.connect()
await client.query('DROP SCHEMA public CASCADE;')
await client.query('CREATE SCHEMA public;')
await client.end()
console.info('Success.')
}
main()

@ -10,11 +10,7 @@
"dev": "node wiki dev",
"build": "webpack --profile --config dev/webpack/webpack.prod.js",
"watch": "webpack --config dev/webpack/webpack.dev.js",
"test": "eslint --format codeframe --ext .js,.vue . && pug-lint server/views && jest",
"docker:dev:up": "docker-compose -f ./dev/docker/docker-compose.yml -p wiki --project-directory . up -d && docker-compose -f ./dev/docker/docker-compose.yml -p wiki --project-directory . exec wiki yarn dev",
"docker:dev:down": "docker-compose -f ./dev/docker/docker-compose.yml -p wiki --project-directory . down",
"docker:dev:rebuild": "rmdir node_modules /s /q && docker-compose -f ./dev/docker/docker-compose.yml -p wiki --project-directory . build --no-cache --force-rm",
"docker:build": "docker-compose -f ./dev/docker/docker-compose.yml -p wiki --project-directory . run wiki yarn build && docker-compose -f ./dev/docker/docker-compose.yml -p wiki --project-directory . down"
"test": "eslint --format codeframe --ext .js,.vue . && pug-lint server/views && jest"
},
"bin": {
"wiki": "wiki.js"
@ -39,7 +35,7 @@
},
"homepage": "https://github.com/Requarks/wiki#readme",
"engines": {
"node": ">=10.10"
"node": ">=10.12"
},
"dependencies": {
"apollo-server": "2.2.2",

@ -56,6 +56,7 @@ exports.up = knex => {
table.increments('id').primary()
table.string('name').notNullable()
table.json('permissions').notNullable()
table.json('pageRules').notNullable()
table.boolean('isSystem').notNullable().defaultTo(false)
table.string('createdAt').notNullable()
table.string('updatedAt').notNullable()

@ -41,6 +41,7 @@ module.exports = {
const group = await WIKI.models.groups.query().insertAndFetch({
name: args.name,
permissions: JSON.stringify(WIKI.data.groups.defaultPermissions),
pageRules: JSON.stringify([]),
isSystem: false
})
return {
@ -69,7 +70,11 @@ module.exports = {
}
},
async update(obj, args) {
await WIKI.models.groups.query().patch({ name: args.name }).where('id', args.id)
await WIKI.models.groups.query().patch({
name: args.name,
permissions: JSON.stringify(args.permissions),
pageRules: JSON.stringify(args.pageRules)
}).where('id', args.id)
return {
responseResult: graphHelper.generateSuccess('Group has been updated.')
}

@ -77,11 +77,14 @@ module.exports = {
const osInfo = await getos()
osLabel = `${os.type()} - ${osInfo.dist} (${osInfo.codename || os.platform()}) ${osInfo.release || os.release()} ${os.arch()}`
}
return osLabel
},
async platform () {
const isDockerized = await fs.pathExists('/.dockerenv')
if (isDockerized) {
osLabel = `${osLabel} (Docker Container)`
return 'docker'
}
return osLabel
return os.platform()
},
hostname() {
return os.hostname()

@ -37,6 +37,8 @@ type GroupMutation {
update(
id: Int!
name: String!
permissions: [String]!
pageRules: [PageRuleInput]!
): DefaultResponse @auth(requires: ["write:groups", "manage:groups", "manage:system"])
delete(
@ -77,8 +79,46 @@ type Group {
name: String!
isSystem: Boolean!
permissions: [String]!
pageRules: [Right]
pageRules: [PageRule]
users: [UserMinimal]
createdAt: Date!
updatedAt: Date!
}
type PageRule {
id: String!
deny: Boolean!
match: PageRuleMatch!
roles: [PageRuleRole]!
path: String!
locales: [String]!
}
input PageRuleInput {
id: String!
deny: Boolean!
match: PageRuleMatch!
roles: [PageRuleRole]!
path: String!
locales: [String]!
}
enum PageRuleRole {
READ
WRITE
MANAGE
DELETE
AS_READ
AS_WRITE
AS_MANAGE
CM_READ
CM_WRITE
CM_MANAGE
}
enum PageRuleMatch {
START
EXACT
END
REGEX
}

@ -44,6 +44,7 @@ type SystemInfo {
nodeVersion: String
operatingSystem: String
pagesTotal: Int
platform: String
ramTotal: String
redisHost: String
redisTotalRAM: String

@ -11,7 +11,7 @@ module.exports = async (job) => {
WIKI.logger.info('Purging orphaned upload files...')
try {
const uplTempPath = path.resolve(process.cwd(), WIKI.config.paths.data, 'temp-upload')
const uplTempPath = path.resolve(process.cwd(), WIKI.config.paths.data, 'uploads')
const ls = await fs.readdirAsync(uplTempPath)
const fifteenAgo = moment().subtract(15, 'minutes')

@ -153,6 +153,7 @@ module.exports = async () => {
app.use((err, req, res, next) => {
res.status(err.status || 500)
res.locals.pageMeta.title = 'Error'
res.render('error', {
message: err.message,
error: WIKI.IS_DEBUG ? err : {}

@ -26,6 +26,7 @@ module.exports = () => {
const cfgHelper = require('./helpers/config')
const crypto = Promise.promisifyAll(require('crypto'))
const pem2jwk = require('pem-jwk').pem2jwk
const semver = require('semver')
// ----------------------------------------
// Define Express App
@ -83,6 +84,11 @@ module.exports = () => {
WIKI.telemetry.sendEvent('setup', 'finalize')
try {
// Basic checks
if (!semver.satisfies(process.version, '>=10.14')) {
throw new Error('Node.js 10.14.x or later required!')
}
// Upgrade from WIKI.js 1.x?
if (req.body.upgrade) {
await WIKI.system.upgradeFromMongo({
@ -205,11 +211,15 @@ module.exports = () => {
const adminGroup = await WIKI.models.groups.query().insert({
name: 'Administrators',
permissions: JSON.stringify(['manage:system']),
pageRules: [],
isSystem: true
})
const guestGroup = await WIKI.models.groups.query().insert({
name: 'Guests',
permissions: JSON.stringify(['read:pages']),
pageRules: [
{ id: 'guest', roles: ['READ', 'AS_READ', 'CM_READ'], match: 'START', deny: false, path: '', locales: [] }
],
isSystem: true
})

@ -5,7 +5,7 @@ block body
v-app
.newpage
.newpage-content
img.animated.fadeIn(src='/svg/icon-close-window.svg', alt='Henry')
img.animated.fadeIn(src='/svg/icon-delete-file.svg', alt='Not Found')
.headline= t('newpage.title')
.subheading.mt-3= t('newpage.subtitle')
v-btn.mt-5(href='/e' + pagePath, large)

Loading…
Cancel
Save