Browse Source

feat: browse nav + pageTree ancestors

NGPixel 5 years ago
parent
commit
1c80faa94d

+ 32 - 14
client/components/admin/admin-navigation.vue

@@ -20,35 +20,35 @@
                 v-toolbar(color='teal', dark, dense, flat, height='56')
                   v-toolbar-title.subtitle-1 {{$t('admin:navigation.mode')}}
                 v-list(nav, two-line)
-                  v-list-item-group(v-model='navMode', mandatory, :color='$vuetify.theme.dark ? `teal lighten-3` : `teal`')
-                    v-list-item(value='classic')
+                  v-list-item-group(v-model='config.mode', mandatory, :color='$vuetify.theme.dark ? `teal lighten-3` : `teal`')
+                    v-list-item(value='TREE')
                       v-list-item-avatar
                         img(src='/svg/icon-tree-structure-dotted.svg', alt='Site Tree')
                       v-list-item-content
                         v-list-item-title {{$t('admin:navigation.modeSiteTree.title')}}
                         v-list-item-subtitle {{$t('admin:navigation.modeSiteTree.description')}}
                       v-list-item-avatar
-                        v-icon(v-if='$vuetify.theme.dark', :color='navMode === `classic` ? `teal lighten-3` : `grey darken-2`') mdi-check-circle
-                        v-icon(v-else, :color='navMode === `classic` ? `teal` : `grey lighten-3`') mdi-check-circle
-                    v-list-item(value='custom')
+                        v-icon(v-if='$vuetify.theme.dark', :color='config.mode === `TREE` ? `teal lighten-3` : `grey darken-2`') mdi-check-circle
+                        v-icon(v-else, :color='config.mode === `TREE` ? `teal` : `grey lighten-3`') mdi-check-circle
+                    v-list-item(value='MIXED')
                       v-list-item-avatar
                         img(src='/svg/icon-user-menu-male-dotted.svg', alt='Custom Navigation')
                       v-list-item-content
                         v-list-item-title {{$t('admin:navigation.modeCustom.title')}}
                         v-list-item-subtitle {{$t('admin:navigation.modeCustom.description')}}
                       v-list-item-avatar
-                        v-icon(v-if='$vuetify.theme.dark', :color='navMode === `custom` ? `teal lighten-3` : `grey darken-2`') mdi-check-circle
-                        v-icon(v-else, :color='navMode === `custom` ? `teal` : `grey lighten-3`') mdi-check-circle
-                    v-list-item(value='none')
+                        v-icon(v-if='$vuetify.theme.dark', :color='config.mode === `MIXED` ? `teal lighten-3` : `grey darken-2`') mdi-check-circle
+                        v-icon(v-else, :color='config.mode === `MIXED` ? `teal` : `grey lighten-3`') mdi-check-circle
+                    v-list-item(value='NONE')
                       v-list-item-avatar
                         img(src='/svg/icon-cancel-dotted.svg', alt='None')
                       v-list-item-content
                         v-list-item-title {{$t('admin:navigation.modeNone.title')}}
                         v-list-item-subtitle {{$t('admin:navigation.modeNone.description')}}
                       v-list-item-avatar
-                        v-icon(v-if='$vuetify.theme.dark', :color='navMode === `none` ? `teal lighten-3` : `grey darken-2`') mdi-check-circle
-                        v-icon(v-else, :color='navMode === `none` ? `teal` : `grey lighten-3`') mdi-check-circle
-            v-col(cols='9', v-if='navMode === `custom`')
+                        v-icon(v-if='$vuetify.theme.dark', :color='config.mode === `none` ? `teal lighten-3` : `grey darken-2`') mdi-check-circle
+                        v-icon(v-else, :color='config.mode === `none` ? `teal` : `grey lighten-3`') mdi-check-circle
+            v-col(cols='9', v-if='config.mode === `MIXED`')
               v-card.animated.fadeInUp.wait-p2s
                 v-row(no-gutters, align='stretch')
                   v-col(style='flex: 0 0 350px;')
@@ -232,7 +232,7 @@
 <script>
 import _ from 'lodash'
 import gql from 'graphql-tag'
-import uuid from 'uuid/v4'
+import { v4 as uuid } from 'uuid'
 
 import groupsQuery from 'gql/admin/users/users-query-groups.gql'
 
@@ -247,11 +247,13 @@ export default {
   data() {
     return {
       selectPageModal: false,
-      navMode: 'custom',
       navTree: [],
       current: {},
       currentLang: 'en',
-      groups: []
+      groups: [],
+      config: {
+        mode: 'NONE'
+      }
     }
   },
   computed: {
@@ -355,6 +357,22 @@ export default {
     this.currentLang = siteConfig.lang
   },
   apollo: {
+    config: {
+      query: gql`
+        {
+          navigation {
+            config {
+              mode
+            }
+          }
+        }
+      `,
+      fetchPolicy: 'network-only',
+      update: (data) => _.cloneDeep(data.navigation.config),
+      watchLoading (isLoading) {
+        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-navigation-config')
+      }
+    },
     navTree: {
       query: gql`
         {

+ 74 - 10
client/themes/default/components/nav-sidebar.vue

@@ -38,7 +38,7 @@
           v-list-item-title {{ item.title }}
         v-list-item(v-else, :href='`/` + item.path', :key='`childpage-` + item.id', :input-value='path === item.path')
           v-list-item-avatar(size='24')
-            v-icon mdi-file-document-box
+            v-icon mdi-text-box
           v-list-item-title {{ item.title }}
 </template>
 
@@ -74,7 +74,8 @@ export default {
         id: 0,
         title: '/ (root)'
       },
-      all: []
+      parents: [],
+      loadedCache: []
     }
   },
   computed: {
@@ -84,25 +85,40 @@ export default {
   methods: {
     switchMode (mode) {
       this.currentMode = mode
-      if (mode === `browse`) {
-        this.fetchBrowseItems()
+      if (mode === `browse` && this.loadedCache.length < 1) {
+        this.loadFromCurrentPath()
       }
     },
     async fetchBrowseItems (item) {
       this.$store.commit(`loadingStart`, 'browse-load')
       if (!item) {
         item = this.currentParent
+      }
+
+      if (this.loadedCache.indexOf(item.id) < 0) {
+        this.currentItems = []
+      }
+
+      if (item.id === 0) {
+        this.parents = []
       } else {
-        if (!_.some(this.parents, ['id', item.id])) {
+        const flushRightIndex = _.findIndex(this.parents, ['id', item.id])
+        if (flushRightIndex >= 0) {
+          this.parents = _.take(this.parents, flushRightIndex)
+        }
+        if (this.parents.length < 1) {
           this.parents.push(this.currentParent)
         }
-        this.currentParent = item
+        this.parents.push(item)
       }
+
+      this.currentParent = item
+
       const resp = await this.$apollo.query({
         query: gql`
-          query ($parent: Int!, $locale: String!) {
+          query ($parent: Int, $locale: String!) {
             pages {
-              tree(parent: $parent, mode: ALL, locale: $locale, includeParents: true) {
+              tree(parent: $parent, mode: ALL, locale: $locale) {
                 id
                 path
                 title
@@ -119,15 +135,63 @@ export default {
           locale: this.locale
         }
       })
+      this.loadedCache = _.union(this.loadedCache, [item.id])
       this.currentItems = _.get(resp, 'data.pages.tree', [])
-      this.all.push(...this.currentItems)
+      this.$store.commit(`loadingStop`, 'browse-load')
+    },
+    async loadFromCurrentPath() {
+      this.$store.commit(`loadingStart`, 'browse-load')
+      const resp = await this.$apollo.query({
+        query: gql`
+          query ($path: String, $locale: String!) {
+            pages {
+              tree(path: $path, mode: ALL, locale: $locale, includeAncestors: true) {
+                id
+                path
+                title
+                isFolder
+                pageId
+                parent
+              }
+            }
+          }
+        `,
+        fetchPolicy: 'cache-first',
+        variables: {
+          path: this.path,
+          locale: this.locale
+        }
+      })
+      const items = _.get(resp, 'data.pages.tree', [])
+      const curPage = _.find(items, ['pageId', this.$store.get('page/id')])
+      if (!curPage) {
+        console.warn('Could not find current page in page tree listing!')
+        return
+      }
+
+      let curParentId = curPage.parent
+      let invertedAncestors = []
+      while (curParentId) {
+        const curParent = _.find(items, ['id', curParentId])
+        if (!curParent) {
+          break
+        }
+        invertedAncestors.push(curParent)
+        curParentId = curParent.parent
+      }
+
+      this.parents = [this.currentParent, ...invertedAncestors.reverse()]
+      this.currentParent = _.last(this.parents)
+
+      this.loadedCache = [curPage.parent]
+      this.currentItems = _.filter(items, ['parent', curPage.parent])
       this.$store.commit(`loadingStop`, 'browse-load')
     }
   },
   mounted () {
     this.currentMode = this.mode
     if (this.mode === 'browse') {
-      this.fetchBrowseItems()
+      this.loadFromCurrentPath()
     }
   }
 }

+ 12 - 12
client/themes/default/components/page.vue

@@ -409,19 +409,19 @@ export default {
     }
   },
   created() {
-    this.$store.commit('page/SET_AUTHOR_ID', this.authorId)
-    this.$store.commit('page/SET_AUTHOR_NAME', this.authorName)
-    this.$store.commit('page/SET_CREATED_AT', this.createdAt)
-    this.$store.commit('page/SET_DESCRIPTION', this.description)
-    this.$store.commit('page/SET_IS_PUBLISHED', this.isPublished)
-    this.$store.commit('page/SET_ID', this.pageId)
-    this.$store.commit('page/SET_LOCALE', this.locale)
-    this.$store.commit('page/SET_PATH', this.path)
-    this.$store.commit('page/SET_TAGS', this.tags)
-    this.$store.commit('page/SET_TITLE', this.title)
-    this.$store.commit('page/SET_UPDATED_AT', this.updatedAt)
+    this.$store.set('page/authorId', this.authorId)
+    this.$store.set('page/authorName', this.authorName)
+    this.$store.set('page/createdAt', this.createdAt)
+    this.$store.set('page/description', this.description)
+    this.$store.set('page/isPublished', this.isPublished)
+    this.$store.set('page/id', this.pageId)
+    this.$store.set('page/locale', this.locale)
+    this.$store.set('page/path', this.path)
+    this.$store.set('page/tags', this.tags)
+    this.$store.set('page/title', this.title)
+    this.$store.set('page/updatedAt', this.updatedAt)
 
-    this.$store.commit('page/SET_MODE', 'view')
+    this.$store.set('page/mode', 'view')
   },
   mounted () {
     // -> Check side navigation visibility

+ 2 - 0
server/app/data.yml

@@ -45,6 +45,8 @@ defaults:
     company: ''
     contentLicense: ''
     logoUrl: https://static.requarks.io/logo/wikijs-butterfly.svg
+    nav:
+      mode: 'MIXED'
     theming:
       theme: 'default'
       iconset: 'md'

+ 8 - 0
server/db/migrations-sqlite/2.3.23.js

@@ -0,0 +1,8 @@
+exports.up = knex => {
+  return knex.schema
+    .alterTable('pageTree', table => {
+      table.json('ancestors')
+    })
+}
+
+exports.down = knex => { }

+ 8 - 0
server/db/migrations/2.3.23.js

@@ -0,0 +1,8 @@
+exports.up = knex => {
+  return knex.schema
+    .alterTable('pageTree', table => {
+      table.json('ancestors')
+    })
+}
+
+exports.down = knex => { }

+ 21 - 4
server/graph/resolvers/navigation.js

@@ -4,18 +4,21 @@ const graphHelper = require('../../helpers/graph')
 
 module.exports = {
   Query: {
-    async navigation() { return {} }
+    async navigation () { return {} }
   },
   Mutation: {
-    async navigation() { return {} }
+    async navigation () { return {} }
   },
   NavigationQuery: {
-    async tree(obj, args, context, info) {
+    async tree (obj, args, context, info) {
       return WIKI.models.navigation.getTree({ cache: false, locale: 'all' })
+    },
+    config (obj, args, context, info) {
+      return WIKI.config.nav
     }
   },
   NavigationMutation: {
-    async updateTree(obj, args, context) {
+    async updateTree (obj, args, context) {
       try {
         await WIKI.models.navigation.query().patch({
           config: args.tree
@@ -28,6 +31,20 @@ module.exports = {
       } catch (err) {
         return graphHelper.generateError(err)
       }
+    },
+    async updateConfig (obj, args, context) {
+      try {
+        WIKI.config.nav = {
+          mode: args.mode
+        }
+        await WIKI.configSvc.saveToDb(['nav'])
+
+        return {
+          responseResult: graphHelper.generateSuccess('Navigation config updated successfully')
+        }
+      } catch (err) {
+        return graphHelper.generateError(err)
+      }
     }
   }
 }

+ 34 - 20
server/graph/resolvers/page.js

@@ -196,27 +196,41 @@ module.exports = {
      * FETCH PAGE TREE
      */
     async tree (obj, args, context, info) {
-      let results = []
-      let conds = {
-        localeCode: args.locale
-      }
-      if (args.parent) {
-        conds.parent = (args.parent < 1) ? null : args.parent
-      } else if (args.path) {
-        // conds.parent = (args.parent < 1) ? null : args.parent
-      }
-      switch (args.mode) {
-        case 'FOLDERS':
-          conds.isFolder = true
-          results = await WIKI.models.knex('pageTree').where(conds)
-          break
-        case 'PAGES':
-          await WIKI.models.knex('pageTree').where(conds).andWhereNotNull('pageId')
-          break
-        default:
-          results = await WIKI.models.knex('pageTree').where(conds)
-          break
+      let curPage = null
+
+      if (!args.locale) { args.locale = WIKI.config.lang.code }
+
+      if (args.path && !args.parent) {
+        curPage = await WIKI.models.knex('pageTree').first('parent', 'ancestors').where({
+          path: args.path,
+          localeCode: args.locale
+        })
+        if (curPage) {
+          args.parent = curPage.parent || 0
+        } else {
+          return []
+        }
       }
+
+      const results = await WIKI.models.knex('pageTree').where(builder => {
+        builder.where('localeCode', args.locale)
+        switch (args.mode) {
+          case 'FOLDERS':
+            builder.andWhere('isFolder', true)
+            break
+          case 'PAGES':
+            builder.andWhereNotNull('pageId')
+            break
+        }
+        if (!args.parent || args.parent < 1) {
+          builder.whereNull('parent')
+        } else {
+          builder.where('parent', args.parent)
+          if (args.includeAncestors && curPage && curPage.ancestors.length > 0) {
+            builder.orWhereIn('id', curPage.ancestors)
+          }
+        }
+      }).orderBy([{ column: 'isFolder', order: 'desc' }, 'title'])
       return results.filter(r => {
         return WIKI.auth.checkAccess(context.req.user, ['read:pages'], {
           path: r.path,

+ 14 - 0
server/graph/schemas/navigation.graphql

@@ -16,6 +16,7 @@ extend type Mutation {
 
 type NavigationQuery {
   tree: [NavigationTree]!
+  config: NavigationConfig!
 }
 
 # -----------------------------------------------
@@ -26,6 +27,9 @@ type NavigationMutation {
   updateTree(
     tree: [NavigationTreeInput]!
   ): DefaultResponse @auth(requires: ["manage:navigation", "manage:system"])
+  updateConfig(
+    mode: NavigationMode!
+  ): DefaultResponse @auth(requires: ["manage:navigation", "manage:system"])
 }
 
 # -----------------------------------------------
@@ -59,3 +63,13 @@ input NavigationItemInput {
   targetType: String
   target: String
 }
+
+type NavigationConfig {
+  mode: NavigationMode!
+}
+
+enum NavigationMode {
+  NONE
+  TREE
+  MIXED
+}

+ 1 - 1
server/graph/schemas/page.graphql

@@ -57,7 +57,7 @@ type PageQuery {
     parent: Int
     mode: PageTreeMode!
     locale: String!
-    includeParents: Boolean
+    includeAncestors: Boolean
   ): [PageTreeItem] @auth(requires: ["manage:system", "read:pages"])
 
   links(

+ 4 - 1
server/jobs/rebuild-tree.js

@@ -19,6 +19,7 @@ module.exports = async (pageId) => {
       let currentPath = ''
       let depth = 0
       let parentId = null
+      let ancestors = []
       for (const part of pagePaths) {
         depth++
         const isFolder = (depth < pagePaths.length)
@@ -39,7 +40,8 @@ module.exports = async (pageId) => {
             isPrivate: !isFolder && page.isPrivate,
             privateNS: !isFolder ? page.privateNS : null,
             parent: parentId,
-            pageId: isFolder ? null : page.id
+            pageId: isFolder ? null : page.id,
+            ancestors: JSON.stringify(ancestors)
           })
           parentId = pik
         } else if (isFolder && !found.isFolder) {
@@ -48,6 +50,7 @@ module.exports = async (pageId) => {
         } else {
           parentId = found.id
         }
+        ancestors.push(parentId)
       }
     }