admin-groups-edit-rules.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336
  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. outlined
  8. icon='mdi-lock-outline'
  9. ) This group has access to everything.
  10. template(v-else)
  11. v-card-title(:class='$vuetify.theme.dark ? `grey darken-3-d5` : ``')
  12. v-alert.radius-7.caption(
  13. :class='$vuetify.theme.dark ? `grey darken-3-d3` : `grey lighten-4`'
  14. color='grey'
  15. outlined
  16. icon='mdi-information'
  17. ) You must enable global content permissions (under Permissions tab) for page rules to have any effect.
  18. v-spacer
  19. v-btn.mx-2(depressed, color='primary', @click='addRule')
  20. v-icon(left) mdi-plus
  21. | Add Rule
  22. v-menu(
  23. right
  24. offset-y
  25. nudge-left='115'
  26. )
  27. template(v-slot:activator='{ on }')
  28. v-btn.is-icon(v-on='on', outlined, color='primary')
  29. v-icon mdi-dots-horizontal
  30. v-list(dense)
  31. v-list-item(@click='comingSoon')
  32. v-list-item-avatar
  33. v-icon mdi-application-import
  34. v-list-item-title Load Preset
  35. v-divider
  36. v-list-item(@click='comingSoon')
  37. v-list-item-avatar
  38. v-icon mdi-application-export
  39. v-list-item-title Save As Preset
  40. v-divider
  41. v-list-item(@click='comingSoon')
  42. v-list-item-avatar
  43. v-icon mdi-cloud-upload
  44. v-list-item-title Import Rules
  45. v-divider
  46. v-list-item(@click='comingSoon')
  47. 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. .rules
  52. .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. solo
  57. :color='rule.deny ? "red" : "green"'
  58. dark
  59. @click='rule.deny = !rule.deny'
  60. height='48'
  61. )
  62. v-icon(v-if='rule.deny') mdi-cancel
  63. v-icon(v-else) mdi-check-circle
  64. //- Roles
  65. v-select.ml-1(
  66. solo
  67. :items='roles'
  68. v-model='rule.roles'
  69. placeholder='Select Role(s)...'
  70. hide-details
  71. multiple
  72. chips
  73. deletable-chips
  74. small-chips
  75. height='48px'
  76. style='flex: 0 1 440px;'
  77. :menu-props='{ "maxHeight": 500 }'
  78. clearable
  79. dense
  80. )
  81. template(slot='selection', slot-scope='{ item, index }')
  82. v-chip.white--text.ml-0(v-if='index <= 1', small, label, :color='rule.deny ? `red` : `green`').caption {{ item.value }}
  83. 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. hide-details
  89. color='primary'
  90. )
  91. v-icon.mr-2(:color='rule.deny ? `red` : `green`') {{props.item.icon}}
  92. v-list-item-content
  93. 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. //- Match
  96. v-select.ml-1.mr-1(
  97. solo
  98. :items='matches'
  99. v-model='rule.match'
  100. placeholder='Match...'
  101. hide-details
  102. height='48px'
  103. style='flex: 0 1 250px;'
  104. dense
  105. )
  106. template(slot='selection', slot-scope='{ item, index }')
  107. .body-2 {{item.text}}
  108. template(slot='item', slot-scope='data')
  109. v-list-item-avatar
  110. v-avatar.white--text.radius-4(color='blue', size='30', tile) {{ data.item.icon }}
  111. v-list-item-content
  112. v-list-item-title(v-html='data.item.text')
  113. //- Locales
  114. v-select.mr-1(
  115. :background-color='$vuetify.theme.dark ? `grey darken-3-d5` : `blue-grey lighten-5`'
  116. solo
  117. :items='locales'
  118. v-model='rule.locales'
  119. placeholder='Any Locale'
  120. item-value='code'
  121. item-text='name'
  122. multiple
  123. hide-details
  124. height='48px'
  125. dense
  126. :menu-props='{ "minWidth": 250 }'
  127. style='flex: 0 1 150px;'
  128. )
  129. template(slot='selection', slot-scope='{ item, index }')
  130. v-chip.white--text.ml-0(v-if='rule.locales.length === 1', small, label, :color='rule.deny ? `red` : `green`').caption {{ item.code.toUpperCase() }}
  131. v-chip.white--text.ml-0(v-else-if='index === 0', small, label, :color='rule.deny ? `red` : `green`').caption {{ rule.locales.length }} locales
  132. v-list-item(slot='prepend-item', @click='rule.locales = []')
  133. v-list-item-action(style='min-width: 30px;')
  134. v-checkbox(
  135. :input-value='rule.locales.length === 0'
  136. hide-details
  137. color='primary'
  138. readonly
  139. )
  140. v-icon.mr-2(:color='rule.deny ? `red` : `green`') mdi-earth
  141. v-list-item-content
  142. v-list-item-title.body-2 Any Locale
  143. v-divider(slot='prepend-item')
  144. template(slot='item', slot-scope='props')
  145. v-list-item-action(style='min-width: 30px;')
  146. v-checkbox(
  147. v-model='props.attrs.inputValue'
  148. hide-details
  149. color='primary'
  150. )
  151. v-icon.mr-2(:color='rule.deny ? `red` : `green`') mdi-web
  152. v-list-item-content
  153. v-list-item-title.body-2 {{props.item.name}}
  154. v-chip.mr-2.grey--text(label, small, :color='$vuetify.theme.dark ? `grey darken-4` : `grey lighten-4`').caption {{props.item.code.toUpperCase()}}
  155. //- Path
  156. v-text-field(
  157. solo
  158. v-model='rule.path'
  159. label='Path'
  160. :prefix='(rule.match !== `END` && rule.match !== `TAG`) ? `/` : null'
  161. :placeholder='rule.match === `REGEX` ? `Regular Expression` : rule.match === `TAG` ? `Tag` : `Path`'
  162. :suffix='rule.match === `REGEX` ? `/` : null'
  163. hide-details
  164. :color='$vuetify.theme.dark ? `grey` : `blue-grey`'
  165. )
  166. v-btn.ml-2(icon, @click='removeRule(rule.id)', small)
  167. v-icon(:color='$vuetify.theme.dark ? `grey` : `blue-grey`') mdi-close
  168. v-divider.mt-3
  169. .overline.py-3 Rules Order
  170. .body-2.pl-3 Rules are applied in order of path specificity. A more precise path will always override a less defined path.
  171. .body-2.pl-5 For example, #[span.teal--text /geography/countries] will override #[span.teal--text /geography].
  172. .body-2.pl-3.pt-2 When 2 rules have the same specificity, the priority is given from lowest to highest as follows:
  173. .body-2.pl-3.pt-1
  174. ul
  175. li
  176. strong Path Starts With...
  177. em.caption.pl-1 (lowest)
  178. li
  179. strong Path Ends With...
  180. li
  181. strong Path Matches Regex...
  182. li
  183. strong Tag Matches...
  184. li
  185. strong Path Is Exactly...
  186. em.caption.pl-1 (highest)
  187. .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.
  188. v-divider.mt-3
  189. .overline.py-3 Regular Expressions
  190. span Expressions that are deemed unsafe or could result in exponential time processing will be rejected upon saving.
  191. </template>
  192. <script>
  193. import _ from 'lodash'
  194. import { customAlphabet } from 'nanoid/non-secure'
  195. /* global siteLangs */
  196. const nanoid = customAlphabet('1234567890abcdef', 10)
  197. export default {
  198. props: {
  199. value: {
  200. type: Object,
  201. default: () => ({})
  202. }
  203. },
  204. data() {
  205. return {
  206. roles: [
  207. { text: 'Read Pages', value: 'read:pages', icon: 'mdi-file-eye-outline' },
  208. { text: 'Create + Edit Pages', value: 'write:pages', icon: 'mdi-file-plus-outline' },
  209. { text: 'Rename / Move Pages', value: 'manage:pages', icon: 'mdi-file-document-edit-outline' },
  210. { text: 'Delete Pages', value: 'delete:pages', icon: 'mdi-file-remove-outline' },
  211. { text: 'View Pages Source', value: 'read:source', icon: 'mdi-code-tags' },
  212. { text: 'View Pages History', value: 'read:history', icon: 'mdi-history' },
  213. { text: 'Read / Use Assets', value: 'read:assets', icon: 'mdi-image-search-outline' },
  214. { text: 'Upload Assets', value: 'write:assets', icon: 'mdi-image-plus' },
  215. { text: 'Edit + Delete Assets', value: 'manage:assets', icon: 'mdi-image-size-select-large' },
  216. { text: 'Edit Scripts', value: 'write:scripts', icon: 'mdi-language-javascript' },
  217. { text: 'Edit Styles', value: 'write:styles', icon: 'mdi-language-css3' },
  218. { text: 'Read Comments', value: 'read:comments', icon: 'mdi-comment-search-outline' },
  219. { text: 'Create Comments', value: 'write:comments', icon: 'mdi-comment-plus-outline' },
  220. { text: 'Edit + Delete Comments', value: 'manage:comments', icon: 'mdi-comment-remove-outline' }
  221. ],
  222. matches: [
  223. { text: 'Path Starts With...', value: 'START', icon: '/...' },
  224. { text: 'Path is Exactly...', value: 'EXACT', icon: '=' },
  225. { text: 'Path Ends With...', value: 'END', icon: '.../' },
  226. { text: 'Path Matches Regex...', value: 'REGEX', icon: '$.*' },
  227. { text: 'Tag Matches...', value: 'TAG', icon: 'T' }
  228. ]
  229. }
  230. },
  231. computed: {
  232. group: {
  233. get() { return this.value },
  234. set(val) { this.$set('input', val) }
  235. },
  236. locales() { return siteLangs }
  237. },
  238. methods: {
  239. addRule(group) {
  240. this.group.pageRules.push({
  241. id: nanoid(),
  242. path: '',
  243. roles: [],
  244. match: 'START',
  245. deny: false,
  246. locales: []
  247. })
  248. },
  249. removeRule(ruleId) {
  250. this.group.pageRules.splice(_.findIndex(this.group.pageRules, ['id', ruleId]), 1)
  251. },
  252. comingSoon() {
  253. this.$store.commit('showNotification', {
  254. style: 'indigo',
  255. message: `Coming soon...`,
  256. icon: 'directions_boat'
  257. })
  258. },
  259. dude (stuff) {
  260. console.info(stuff)
  261. }
  262. }
  263. }
  264. </script>
  265. <style lang="scss">
  266. .rules {
  267. background-color: mc('blue-grey', '50');
  268. border-radius: 4px;
  269. padding: 1rem;
  270. position: relative;
  271. @at-root .v-application.theme--dark & {
  272. background-color: mc('grey', '800');
  273. }
  274. }
  275. .rule {
  276. display: flex;
  277. background-color: mc('blue-grey', '100');
  278. border-radius: 4px;
  279. padding: .5rem;
  280. align-items: center;
  281. &-enter-active, &-leave-active {
  282. transition: all .5s ease;
  283. }
  284. &-enter, &-leave-to {
  285. opacity: 0;
  286. }
  287. @at-root .v-application.theme--dark & {
  288. background-color: mc('grey', '700');
  289. }
  290. & + .rule {
  291. margin-top: .5rem;
  292. position: relative;
  293. &::before {
  294. content: '+';
  295. position: absolute;
  296. width: 2rem;
  297. height: 2rem;
  298. border-radius: 50%;
  299. display: flex;
  300. justify-content: center;
  301. align-items: center;
  302. font-weight: 600;
  303. color: mc('blue-grey', '700');
  304. font-size: 1.25rem;
  305. background-color: mc('blue-grey', '50');
  306. left: -2rem;
  307. top: -1.3rem;
  308. @at-root .v-application.theme--dark & {
  309. background-color: mc('grey', '800');
  310. color: mc('grey', '600');
  311. }
  312. }
  313. }
  314. .input-group + * {
  315. margin-left: .5rem;
  316. }
  317. }
  318. </style>