Pārlūkot izejas kodu

feat: redirect editor UI (wip)

NGPixel 5 gadi atpakaļ
vecāks
revīzija
7508d92f92

+ 28 - 9
client/components/common/page-selector.vue

@@ -20,7 +20,7 @@
           v-show='searchLoading'
           )
       .d-flex
-        v-flex.grey(xs5, :class='darkMode ? `darken-4` : `lighten-3`')
+        v-flex.grey(xs5, :class='$vuetify.theme.dark ? `darken-4` : `lighten-3`')
           v-toolbar(color='grey darken-3', dark, dense, flat)
             .body-2 Virtual Folders
             v-spacer
@@ -57,7 +57,7 @@
                   color='primary'
                   )
                   template(v-for='(page, idx) of currentPages')
-                    v-list-item(:key='`page-` + page.id', :value='page.path')
+                    v-list-item(:key='`page-` + page.id', :value='page')
                       v-list-item-icon: v-icon mdi-text-box
                       v-list-item-title {{page.title}}
                     v-divider(v-if='idx < pages.length - 1')
@@ -69,7 +69,7 @@
             icon='mdi-alert'
             )
             .body-2 This folder is empty.
-      v-card-actions.grey.pa-2(:class='darkMode ? `darken-2` : `lighten-1`')
+      v-card-actions.grey.pa-2(:class='$vuetify.theme.dark ? `darken-2` : `lighten-1`', v-if='!mustExist')
         v-select(
           solo
           dark
@@ -101,8 +101,7 @@
 
 <script>
 import _ from 'lodash'
-import { get } from 'vuex-pathify'
-import pageTreeQuery from 'gql/common/common-pages-query-tree.gql'
+import gql from 'graphql-tag'
 
 const localeSegmentRegex = /^[A-Z]{2}(-[A-Z]{2})?$/i
 
@@ -129,6 +128,10 @@ export default {
     openHandler: {
       type: Function,
       default: () => {}
+    },
+    mustExist: {
+      type: Boolean,
+      default: false
     }
   },
   data() {
@@ -172,7 +175,6 @@ export default {
     }
   },
   computed: {
-    darkMode: get('site/dark'),
     isShown: {
       get() { return this.value },
       set(val) { this.$emit('input', val) }
@@ -184,6 +186,9 @@ export default {
       if (!this.currentPath) {
         return false
       }
+      if (this.mustExist && !this.currentPage) {
+        return false
+      }
       const firstSection = _.head(this.currentPath.split('/'))
       if (firstSection.length <= 1) {
         return false
@@ -235,7 +240,7 @@ export default {
     },
     currentPage (newValue, oldValue) {
       if (!_.isEmpty(newValue)) {
-        this.currentPath = newValue
+        this.currentPath = newValue.path
       }
     },
     currentLocale (newValue, oldValue) {
@@ -262,7 +267,8 @@ export default {
     open() {
       const exit = this.openHandler({
         locale: this.currentLocale,
-        path: this.currentPath
+        path: this.currentPath,
+        id: (this.mustExist && this.currentPage) ? this.currentPage.pageId : 0
       })
       if (exit !== false) {
         this.close()
@@ -271,7 +277,20 @@ export default {
     async fetchFolders (item) {
       this.searchLoading = true
       const resp = await this.$apollo.query({
-        query: pageTreeQuery,
+        query: gql`
+          query ($parent: Int!, $mode: PageTreeMode!, $locale: String!) {
+            pages {
+              tree(parent: $parent, mode: $mode, locale: $locale) {
+                id
+                path
+                title
+                isFolder
+                pageId
+                parent
+              }
+            }
+          }
+        `,
         fetchPolicy: 'network-only',
         variables: {
           parent: item.id,

+ 2 - 2
client/components/editor.vue

@@ -1,5 +1,5 @@
 <template lang="pug">
-  v-app.editor(:dark='darkMode')
+  v-app.editor(:dark='$vuetify.theme.dark')
     nav-header(dense)
       template(slot='mid')
         v-text-field.editor-title-input(
@@ -81,6 +81,7 @@ export default {
     editorCode: () => import(/* webpackChunkName: "editor-code", webpackMode: "lazy" */ './editor/editor-code.vue'),
     editorCkeditor: () => import(/* webpackChunkName: "editor-ckeditor", webpackMode: "lazy" */ './editor/editor-ckeditor.vue'),
     editorMarkdown: () => import(/* webpackChunkName: "editor-markdown", webpackMode: "lazy" */ './editor/editor-markdown.vue'),
+    editorRedirect: () => import(/* webpackChunkName: "editor-redirect", webpackMode: "lazy" */ './editor/editor-redirect.vue'),
     editorModalEditorselect: () => import(/* webpackChunkName: "editor", webpackMode: "eager" */ './editor/editor-modal-editorselect.vue'),
     editorModalProperties: () => import(/* webpackChunkName: "editor", webpackMode: "eager" */ './editor/editor-modal-properties.vue'),
     editorModalUnsaved: () => import(/* webpackChunkName: "editor", webpackMode: "eager" */ './editor/editor-modal-unsaved.vue'),
@@ -148,7 +149,6 @@ export default {
   },
   computed: {
     currentEditor: sync('editor/editor'),
-    darkMode: get('site/dark'),
     activeModal: sync('editor/activeModal'),
     mode: get('editor/mode'),
     welcomeMode() { return this.mode === `create` && this.path === `home` },

+ 28 - 38
client/components/editor/editor-modal-blocks.vue

@@ -1,38 +1,22 @@
 <template lang='pug'>
   v-card.editor-modal-blocks.animated.fadeInLeft(flat, tile)
     v-container.pa-3(grid-list-lg, fluid)
-      v-layout(row, wrap)
-        v-flex(xs12, lg4, xl3)
-          v-card.radius-7(light)
+      v-row(dense)
+        v-col(
+          v-for='(item, idx) of blocks'
+          :key='`block-` + item.key'
+          cols='12'
+          lg='4'
+          xl='3'
+          )
+          v-card.radius-7(light, flat, @click='selectBlock(item)')
             v-card-text
-              .d-flex
-                v-toolbar.radius-7(color='teal lighten-5', dense, flat, height='44')
-                  .body-2.teal--text Blocks
-              v-list(two-line)
-                template(v-for='(item, idx) of blocks')
-                  v-list-item(@click='selectBlock(item)')
-                    v-list-item-avatar
-                      v-avatar.radius-7(color='teal')
-                        v-icon(dark) dashboard
-                    v-list-item-content
-                      v-list-item-title.body-2 {{item.title}}
-                      v-list-item-sub-title {{item.description}}
-                    v-list-item-avatar(v-if='block.key === item.key')
-                      v-icon.animated.fadeInLeft(color='teal') arrow_forward_ios
-                  v-divider(v-if='idx < blocks.length - 1')
-
-        v-flex(xs3)
-          v-card.radius-7.animated.fadeInLeft(light, v-if='block.key')
-            v-card-text
-              v-toolbar.radius-7(color='teal lighten-5', dense, flat)
-                v-icon.mr-3(color='teal') dashboard
-                .body-2.teal--text {{block.title}}
-              .d-flex.mt-3
-                v-toolbar.radius-7(flat, color='grey lighten-4', dense, height='44')
-                  .body-2 Coming soon
-                v-btn.ml-3.my-0.mr-0.radius-7(color='teal', large, @click='', disabled)
-                  v-icon(left) save_alt
-                  span Insert
+              .d-flex.align-center
+                v-avatar.radius-7(color='teal')
+                  v-icon(dark) {{item.icon}}
+                .pl-3
+                  .body-2: strong.teal--text {{item.title}}
+                  .caption.grey--text {{item.description}}
 </template>
 
 <script>
@@ -49,13 +33,19 @@ export default {
   data() {
     return {
       blocks: [
-        { key: 'hero', title: 'Hero', description: 'A large banner with a title.' },
-        { key: 'toc', title: 'Table of Contents', description: 'A list of children pages.' }
-        // { key: 'form', title: 'Form', description: '' }
-      ],
-      block: {
-        key: false
-      }
+        {
+          key: 'childlist',
+          title: 'List Children Pages',
+          description: 'Display a links list of all children of this page.',
+          icon: 'mdi-format-list-text'
+        },
+        {
+          key: 'tabs',
+          title: 'Tabs',
+          description: 'Organize content within tabs.',
+          icon: 'mdi-tab'
+        }
+      ]
     }
   },
   computed: {

+ 30 - 30
client/components/editor/editor-modal-editorselect.vue

@@ -106,43 +106,27 @@
             v-flex(xs4)
               v-hover
                 template(v-slot:default='{ hover }')
-                  v-card.radius-7.teal.animated.fadeInUp(
+                  v-card.radius-7.animated.fadeInUp(
                     hover
                     light
                     ripple
                     )
-                    v-card-text.text-center(@click='')
+                    v-card-text.text-center(@click='fromTemplate')
                       img(src='/svg/icon-cube.svg', alt='From Template', style='width: 42px; opacity: .5;')
-                      .body-2.mt-1.teal--text.text--lighten-2 From Template
-                      .caption.teal--text.text--lighten-1 Use an existing page / tree
-                    v-fade-transition
-                      v-overlay(
-                        v-if='hover'
-                        absolute
-                        color='teal'
-                        opacity='.8'
-                        )
-                        .body-2.mt-7 Coming Soon
+                      .body-2.mt-1.teal--text From Template
+                      .caption.grey--text Use an existing page...
             v-flex(xs4)
               v-hover
                 template(v-slot:default='{ hover }')
-                  v-card.radius-7.teal.animated.fadeInUp.wait-p1s(
+                  v-card.radius-7.animated.fadeInUp.wait-p1s(
                     hover
                     light
                     ripple
                     )
-                    v-card-text.text-center(@click='')
-                      img(src='/svg/icon-tree-structure.svg', alt='Tree View', style='width: 42px; opacity: .5;')
-                      .body-2.mt-1.teal--text.text--lighten-2 Tree View
-                      .caption.teal--text.text--lighten-1 List children pages
-                    v-fade-transition
-                      v-overlay(
-                        v-if='hover'
-                        absolute
-                        color='teal'
-                        opacity='.8'
-                        )
-                        .body-2.mt-7 Coming Soon
+                    v-card-text.text-center(@click='selectEditor("redirect")')
+                      img(src='/svg/icon-route.svg', alt='Redirection', style='width: 42px; opacity: .5;')
+                      .body-2.mt-1.teal--text Redirection
+                      .caption.grey--text Redirect the user to...
             v-flex(xs4)
               v-hover
                 template(v-slot:default='{ hover }')
@@ -190,11 +174,13 @@
               opacity='.8'
               )
               .body-2 Coming Soon
+
+    page-selector(mode='select', v-model='templateDialogIsShown', :open-handler='fromTemplateHandle', :path='path', :locale='locale', must-exist)
 </template>
 
 <script>
 import _ from 'lodash'
-import { sync } from 'vuex-pathify'
+import { sync, get } from 'vuex-pathify'
 
 export default {
   props: {
@@ -204,22 +190,36 @@ export default {
     }
   },
   data() {
-    return { }
+    return {
+      templateDialogIsShown: false
+    }
   },
   computed: {
     isShown: {
       get() { return this.value },
       set(val) { this.$emit('input', val) }
     },
-    currentEditor: sync('editor/editor')
+    currentEditor: sync('editor/editor'),
+    locale: get('page/locale'),
+    path: get('page/path')
   },
   methods: {
-    selectEditor(name) {
+    selectEditor (name) {
       this.currentEditor = `editor${_.startCase(name)}`
       this.isShown = false
     },
-    goBack() {
+    goBack () {
       window.history.go(-1)
+    },
+    fromTemplate () {
+      this.templateDialogIsShown = true
+    },
+    fromTemplateHandle ({ id }) {
+      this.templateDialogIsShown = false
+      this.isShown = false
+      this.$nextTick(() => {
+        window.location.assign(`/e/${this.locale}/${this.path}?from=${id}`)
+      })
     }
   }
 }

+ 224 - 0
client/components/editor/editor-redirect.vue

@@ -0,0 +1,224 @@
+<template lang='pug'>
+  .editor-redirect
+    .editor-redirect-main
+      .editor-redirect-editor
+        v-container.px-2.pt-1(fluid)
+          v-row(dense)
+            v-col(
+              cols='12'
+              lg='8'
+              offset-lg='2'
+              xl='6'
+              offset-xl='3'
+              )
+              v-card.pt-2
+                v-card-text
+                  .pb-1
+                    .subtitle-2.primary--text When a user reaches this page
+                    .caption.grey--text.text--darken-1 and matches one of these rules...
+                  v-timeline(dense)
+                    v-slide-x-reverse-transition(group, hide-on-leave)
+                      v-timeline-item(
+                        key='cond-add-new'
+                        hide-dot
+                        )
+                        v-btn(
+                          color='primary'
+                          @click=''
+                          )
+                          v-icon(left) mdi-plus
+                          span Add Conditional Rule
+                      v-timeline-item(
+                        key='cond-none'
+                        small
+                        color='grey'
+                        )
+                        v-card.grey.lighten-5(flat)
+                          v-card-text
+                            .body-2: strong No conditional rule
+                            em Add conditional rules to direct users to a different page based on their group.
+                      v-timeline-item(
+                        key='cond-rule-1'
+                        small
+                        color='primary'
+                        )
+                        v-card.blue-grey.lighten-5(flat)
+                          v-card-text
+                            .d-flex.align-center
+                              .body-2: strong User is a member of any of these groups:
+                              v-select.ml-3(
+                                color='primary'
+                                :items='groups'
+                                item-text='name'
+                                item-value='id'
+                                multiple
+                                solo
+                                flat
+                                hide-details
+                                dense
+                                chips
+                                small-chips
+                                )
+                            v-divider.my-3
+                            .d-flex.align-center
+                              .body-2.mr-3 then redirect to
+                              v-btn-toggle.mr-3(
+                                v-model='fallbackMode'
+                                mandatory
+                                color='primary'
+                                borderless
+                                dense
+                                )
+                                v-btn.text-none(value='page') Page
+                                v-btn.text-none(value='url') External URL
+                              v-btn.mr-3(
+                                v-if='fallbackMode === `page`'
+                                color='primary'
+                                )
+                                v-icon(left) mdi-magnify
+                                span Select Page...
+                              v-text-field(
+                                v-if='fallbackMode === `url`'
+                                label='External URL'
+                                outlined
+                                hint='Required - Title of the API'
+                                hide-details
+                                v-model='fallbackUrl'
+                                dense
+                                single-line
+                              )
+                  v-divider.mb-5
+                  .subtitle-2.primary--text Otherwise, redirect to...
+                  .caption.grey--text.text--darken-1.pb-2 This fallback rule is mandatory and used if none of the conditional rules above applies.
+                  .d-flex.align-center
+                    v-btn-toggle.mr-3(
+                      v-model='fallbackMode'
+                      mandatory
+                      color='primary'
+                      borderless
+                      dense
+                      )
+                      v-btn.text-none(value='page') Page
+                      v-btn.text-none(value='url') External URL
+                    v-btn.mr-3(
+                      v-if='fallbackMode === `page`'
+                      color='primary'
+                      )
+                      v-icon(left) mdi-magnify
+                      span Select Page...
+                    v-text-field(
+                      v-if='fallbackMode === `url`'
+                      label='External URL'
+                      outlined
+                      hint='Required - Title of the API'
+                      hide-details
+                      v-model='fallbackUrl'
+                      dense
+                      single-line
+                    )
+
+    v-system-bar.editor-redirect-sysbar(dark, status, color='grey darken-3')
+      .caption.editor-redirect-sysbar-locale {{locale.toUpperCase()}}
+      .caption.px-3 /{{path}}
+      template(v-if='$vuetify.breakpoint.mdAndUp')
+        v-spacer
+        .caption Redirect
+        v-spacer
+        .caption 0 rules
+</template>
+
+<script>
+import _ from 'lodash'
+import gql from 'graphql-tag'
+import { v4 as uuid } from 'uuid'
+import { get, sync } from 'vuex-pathify'
+
+export default {
+  data() {
+    return {
+      fallbackMode: 'page',
+      fallbackUrl: 'https://'
+    }
+  },
+  computed: {
+    isMobile() {
+      return this.$vuetify.breakpoint.smAndDown
+    },
+    locale: get('page/locale'),
+    path: get('page/path'),
+    mode: get('editor/mode'),
+    activeModal: sync('editor/activeModal')
+  },
+  methods: {
+  },
+  mounted() {
+    this.$store.set('editor/editorKey', 'redirect')
+
+    if (this.mode === 'create') {
+      this.$store.set('editor/content', '<h1>Title</h1>\n\n<p>Some text here</p>')
+    }
+  },
+  apollo: {
+    groups: {
+      query: gql`
+        {
+          groups {
+            list {
+              id
+              name
+            }
+          }
+        }
+      `,
+      fetchPolicy: 'network-only',
+      update: (data) => data.groups.list,
+      watchLoading (isLoading) {
+        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'editor-redirect-groups')
+      }
+    }
+  }
+}
+</script>
+
+<style lang='scss'>
+$editor-height: calc(100vh - 64px - 24px);
+$editor-height-mobile: calc(100vh - 56px - 16px);
+
+.editor-redirect {
+  &-main {
+    display: flex;
+    width: 100%;
+  }
+
+  &-editor {
+    background-color: darken(mc('grey', '100'), 4.5%);
+    flex: 1 1 50%;
+    display: block;
+    height: $editor-height;
+    position: relative;
+
+    @at-root .theme--dark & {
+      background-color: darken(mc('grey', '900'), 4.5%);
+    }
+  }
+
+  &-sidebar {
+    width: 200px;
+  }
+
+  &-sysbar {
+    padding-left: 0 !important;
+
+    &-locale {
+      background-color: rgba(255,255,255,.25);
+      display:inline-flex;
+      padding: 0 12px;
+      height: 24px;
+      width: 63px;
+      justify-content: center;
+      align-items: center;
+    }
+  }
+
+}
+</style>

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
client/static/svg/icon-route.svg


+ 6 - 0
server/modules/editor/redirect/definition.yml

@@ -0,0 +1,6 @@
+key: redirect
+title: Redirection
+description: Redirect the user
+contentType: redirect
+author: requarks.io
+props: {}

Daži faili netika attēloti, jo izmaiņu fails ir pārāk liels