admin-groups-edit-rules.vue 12.1 KB
Newer Older
1
<template lang="pug">
2
  v-card(flat)
3
    v-card-text(v-if='group.id === 1')
4
      v-alert.radius-7.mb-0(
5
        :class='$vuetify.theme.dark ? "grey darken-4" : "orange lighten-5"'
6
        color='orange darken-2'
7 8
        outlined
        icon='mdi-lock-outline'
9 10
        ) This group has access to everything.
    template(v-else)
11 12 13
      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`'
14
          color='grey'
15 16
          outlined
          icon='mdi-information'
17 18
          ) You must enable global content permissions (under Permissions tab) for page rules to have any effect.
        v-spacer
19 20
        v-btn.mx-2(depressed, color='primary', @click='addRule')
          v-icon(left) mdi-plus
21 22 23 24 25 26
          | Add Rule
        v-menu(
          right
          offset-y
          nudge-left='115'
          )
27 28 29
          template(v-slot:activator='{ on }')
            v-btn.is-icon(v-on='on', outlined, color='primary')
              v-icon mdi-dots-horizontal
30
          v-list(dense)
31 32
            v-list-item(@click='comingSoon')
              v-list-item-avatar
33
                v-icon mdi-application-import
34
              v-list-item-title Load Preset
35
            v-divider
36 37
            v-list-item(@click='comingSoon')
              v-list-item-avatar
38
                v-icon mdi-application-export
39
              v-list-item-title Save As Preset
40
            v-divider
41 42
            v-list-item(@click='comingSoon')
              v-list-item-avatar
43
                v-icon mdi-cloud-upload
44
              v-list-item-title Import Rules
45
            v-divider
46 47
            v-list-item(@click='comingSoon')
              v-list-item-avatar
48
                v-icon mdi-cloud-download
49
              v-list-item-title Export Rules
50
      v-card-text(:class='$vuetify.theme.dark ? `grey darken-4-l5` : `white`')
51 52
        .rules
          .caption(v-if='group.pageRules.length === 0')
53
            em(:class='$vuetify.theme.dark ? `grey--text` : `blue-grey--text`') This group has no page rules yet.
54
          .rule(v-for='rule of group.pageRules', :key='rule.id')
55
            v-btn.ma-0.radius-4.rule-deny-btn(
56 57 58 59
              solo
              :color='rule.deny ? "red" : "green"'
              dark
              @click='rule.deny = !rule.deny'
60
              height='48'
61
              )
62 63
              v-icon(v-if='rule.deny') mdi-cancel
              v-icon(v-else) mdi-check-circle
64 65 66 67 68 69 70 71 72 73 74
            //- Roles
            v-select.ml-1(
              solo
              :items='roles'
              v-model='rule.roles'
              placeholder='Select Role(s)...'
              hide-details
              multiple
              chips
              deletable-chips
              small-chips
75
              height='48px'
76 77 78 79 80 81
              style='flex: 0 1 440px;'
              :menu-props='{ "maxHeight": 500 }'
              clearable
              dense
              )
              template(slot='selection', slot-scope='{ item, index }')
82 83
                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
84
              template(slot='item', slot-scope='props')
85
                v-list-item-action(style='min-width: 30px;')
86
                  v-checkbox(
87
                    v-model='props.attrs.inputValue'
88 89 90 91
                    hide-details
                    color='primary'
                  )
                v-icon.mr-2(:color='rule.deny ? `red` : `green`') {{props.item.icon}}
92 93
                v-list-item-content
                  v-list-item-title.body-2 {{props.item.text}}
94
                v-chip.mr-2.grey--text(label, small, :color='$vuetify.theme.dark ? `grey darken-4` : `grey lighten-4`').caption {{props.item.value}}
95 96 97 98 99 100 101 102

            //- Match
            v-select.ml-1.mr-1(
              solo
              :items='matches'
              v-model='rule.match'
              placeholder='Match...'
              hide-details
103
              height='48px'
104 105 106 107
              style='flex: 0 1 250px;'
              dense
              )
              template(slot='selection', slot-scope='{ item, index }')
108
                .body-2 {{item.text}}
109
              template(slot='item', slot-scope='data')
110
                v-list-item-avatar
111
                  v-avatar.white--text.radius-4(color='blue', size='30', tile) {{ data.item.icon }}
112 113
                v-list-item-content
                  v-list-item-title(v-html='data.item.text')
114 115
            //- Locales
            v-select.mr-1(
116
              :background-color='$vuetify.theme.dark ? `grey darken-3-d5` : `blue-grey lighten-5`'
117 118 119 120
              solo
              :items='locales'
              v-model='rule.locales'
              placeholder='Any Locale'
121 122
              item-value='code'
              item-text='name'
123 124
              multiple
              hide-details
125
              height='48px'
126 127 128 129 130
              dense
              :menu-props='{ "minWidth": 250 }'
              style='flex: 0 1 150px;'
              )
              template(slot='selection', slot-scope='{ item, index }')
131
                v-chip.white--text.ml-0(v-if='rule.locales.length === 1', small, label, :color='rule.deny ? `red` : `green`').caption {{ item.code.toUpperCase() }}
132
                v-chip.white--text.ml-0(v-else-if='index === 0', small, label, :color='rule.deny ? `red` : `green`').caption {{ rule.locales.length }} locales
133 134
              v-list-item(slot='prepend-item', @click='rule.locales = []')
                v-list-item-action(style='min-width: 30px;')
135 136 137 138 139 140
                  v-checkbox(
                    :input-value='rule.locales.length === 0'
                    hide-details
                    color='primary'
                    readonly
                  )
141
                v-icon.mr-2(:color='rule.deny ? `red` : `green`') mdi-earth
142 143
                v-list-item-content
                  v-list-item-title.body-2 Any Locale
144 145
              v-divider(slot='prepend-item')
              template(slot='item', slot-scope='props')
146
                v-list-item-action(style='min-width: 30px;')
147
                  v-checkbox(
148
                    v-model='props.attrs.inputValue'
149 150 151
                    hide-details
                    color='primary'
                  )
152
                v-icon.mr-2(:color='rule.deny ? `red` : `green`') mdi-web
153
                v-list-item-content
154 155
                  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()}}
156 157 158 159 160 161

            //- Path
            v-text-field(
              solo
              v-model='rule.path'
              label='Path'
162 163
              :prefix='(rule.match !== `END` && rule.match !== `TAG`) ? `/` : null'
              :placeholder='rule.match === `REGEX` ? `Regular Expression` : rule.match === `TAG` ? `Tag` : `Path`'
164 165
              :suffix='rule.match === `REGEX` ? `/` : null'
              hide-details
166
              :color='$vuetify.theme.dark ? `grey` : `blue-grey`'
167 168
              )

169
            v-btn.ml-2(icon, @click='removeRule(rule.id)', small)
170
              v-icon(:color='$vuetify.theme.dark ? `grey` : `blue-grey`') mdi-close
171 172

        v-divider.mt-3
173 174 175 176 177
        .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
178 179 180 181 182 183 184 185
          ul
            li
              strong Path Starts With...
              em.caption.pl-1 (lowest)
            li
              strong Path Ends With...
            li
              strong Path Matches Regex...
186 187
            li
              strong Tag Matches...
188 189 190
            li
              strong Path Is Exactly...
              em.caption.pl-1 (highest)
191
        .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.
192
        v-divider.mt-3
193
        .overline.py-3 Regular Expressions
194 195
        span Expressions that are deemed unsafe or could result in exponential time processing will be rejected upon saving.

196 197 198 199
</template>

<script>
import _ from 'lodash'
200 201
import { customAlphabet } from 'nanoid/non-secure'

202 203
/* global siteLangs */

204
const nanoid = customAlphabet('1234567890abcdef', 10)
205 206 207 208

export default {
  props: {
    value: {
209 210
      type: Object,
      default: () => ({})
211 212 213 214 215
    }
  },
  data() {
    return {
      roles: [
216 217
        { text: 'Read Pages', value: 'read:pages', icon: 'mdi-file-eye-outline' },
        { text: 'Create Pages', value: 'write:pages', icon: 'mdi-file-plus-outline' },
218
        { text: 'Edit + Move Pages', value: 'manage:pages', icon: 'mdi-file-document-edit-outline' },
219
        { text: 'Delete Pages', value: 'delete:pages', icon: 'mdi-file-remove-outline' },
220 221 222 223 224 225 226 227
        { 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: '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' }
228 229 230 231 232
      ],
      matches: [
        { text: 'Path Starts With...', value: 'START', icon: '/...' },
        { text: 'Path is Exactly...', value: 'EXACT', icon: '=' },
        { text: 'Path Ends With...', value: 'END', icon: '.../' },
233 234
        { text: 'Path Matches Regex...', value: 'REGEX', icon: '$.*' },
        { text: 'Tag Matches...', value: 'TAG', icon: 'T' }
235 236 237 238 239 240 241
      ]
    }
  },
  computed: {
    group: {
      get() { return this.value },
      set(val) { this.$set('input', val) }
242 243
    },
    locales() { return siteLangs }
244 245 246 247
  },
  methods: {
    addRule(group) {
      this.group.pageRules.push({
248
        id: nanoid(),
249 250 251 252 253 254 255
        path: '',
        roles: [],
        match: 'START',
        deny: false,
        locales: []
      })
    },
256 257
    removeRule(ruleId) {
      this.group.pageRules.splice(_.findIndex(this.group.pageRules, ['id', ruleId]), 1)
258 259 260 261 262 263 264
    },
    comingSoon() {
      this.$store.commit('showNotification', {
        style: 'indigo',
        message: `Coming soon...`,
        icon: 'directions_boat'
      })
265 266 267
    },
    dude (stuff) {
      console.info(stuff)
268 269 270 271 272 273 274 275 276 277 278 279
    }
  }
}
</script>

<style lang="scss">
.rules {
  background-color: mc('blue-grey', '50');
  border-radius: 4px;
  padding: 1rem;
  position: relative;

280
  @at-root .v-application.theme--dark & {
281 282 283 284 285 286 287 288 289
    background-color: mc('grey', '800');
  }
}

.rule {
  display: flex;
  background-color: mc('blue-grey', '100');
  border-radius: 4px;
  padding: .5rem;
290
  align-items: center;
291 292 293 294 295 296 297 298

  &-enter-active, &-leave-active {
    transition: all .5s ease;
  }
  &-enter, &-leave-to {
    opacity: 0;
  }

299
  @at-root .v-application.theme--dark & {
300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322
    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;

323
      @at-root .v-application.theme--dark & {
324 325 326 327 328 329 330 331 332 333 334
        background-color: mc('grey', '800');
        color: mc('grey', '600');
      }
    }
  }

  .input-group + * {
    margin-left: .5rem;
  }
}
</style>