wikijs-fork/client/components/admin/admin-groups-edit-rules.vue
scienceasdf cfbd3dca00
Fix: in group edit rules, write scripts permission and write styles permission can be configured. (#2829)
At present, the user that are not in the administrator group have no access to edit scripts and styles in page editor panel. This commit add configuration in group rule manage webpage so that users' permission on writing styles and writing scripts can be modified.
2021-01-29 14:17:30 -05:00

337 lines
12 KiB
Vue

<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>