Browse Source

feat: new nav UI (wip)

NGPixel 5 years ago
parent
commit
3ca72ccc1e

+ 274 - 138
client/components/admin/admin-navigation.vue

@@ -14,173 +14,266 @@
             v-icon(left) mdi-check
             span {{$t('common:actions.apply')}}
         v-container.pa-0.mt-3(fluid, grid-list-lg)
-          v-layout(row)
-            v-flex(style='flex: 0 0 350px;')
+          v-row(dense)
+            v-col(cols='3')
               v-card.animated.fadeInUp
-                v-list.py-2(dense, nav, dark, :class='navTree.length < 1 ? "grey lighten-4" : "primary"')
-                  v-list-item(v-if='navTree.length < 1')
-                    v-list-item-avatar(size='24'): v-icon(color='grey') explore_off
-                    v-list-item-content
-                      .caption.grey--text {{$t('navigation.emptyList')}}
-                  draggable(v-model='navTree')
-                    template(v-for='navItem in navTree')
-                      v-list-item(
-                        v-if='navItem.kind === "link"'
-                        :key='navItem.id'
-                        :class='(navItem === current) ? "blue" : ""'
-                        @click='selectItem(navItem)'
-                        )
-                        v-list-item-avatar(size='24'): v-icon {{navItem.icon}}
-                        v-list-item-title {{navItem.label}}
-                      .py-2.clickable(
-                        v-else-if='navItem.kind === "divider"'
-                        :key='navItem.id'
-                        :class='(navItem === current) ? "blue" : ""'
-                        @click='selectItem(navItem)'
+                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-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-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-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-card.animated.fadeInUp.wait-p2s
+                v-row(no-gutters, align='stretch')
+                  v-col(style='flex: 0 0 350px;')
+                    v-card.grey(flat, style='height: 100%; border-radius: 4px 0 0 4px;', :class='$vuetify.theme.dark ? `darken-4-l5` : `lighten-3`')
+                      .teal.pa-2(style='margin-bottom: 1px; height: 56px;')
+                        v-select(
+                          v-if='locales.length > 0'
+                          label='Locale'
+                          hide-details
+                          solo
+                          flat
+                          background-color='teal darken-2'
+                          dark
+                          dense
+                          v-model='currentLang'
+                          :items='locales'
+                          item-text='name'
+                          item-value='code'
                         )
+                      v-list.py-2(dense, nav, dark, class='blue darken-2', style='border-radius: 0;')
+                        v-list-item(v-if='navTree.length < 1')
+                          v-list-item-avatar(size='24'): v-icon(color='blue lighten-3') mdi-alert
+                          v-list-item-content
+                            em.caption.blue--text.text--lighten-4 {{$t('navigation.emptyList')}}
+                        draggable(v-model='navTree')
+                          template(v-for='navItem in navTree')
+                            v-list-item(
+                              v-if='navItem.kind === "link"'
+                              :key='navItem.id'
+                              :class='(navItem === current) ? "blue" : ""'
+                              @click='selectItem(navItem)'
+                              )
+                              v-list-item-avatar(size='24'): v-icon {{navItem.icon}}
+                              v-list-item-title {{navItem.label}}
+                            .py-2.clickable(
+                              v-else-if='navItem.kind === "divider"'
+                              :key='navItem.id'
+                              :class='(navItem === current) ? "blue" : ""'
+                              @click='selectItem(navItem)'
+                              )
+                              v-divider
+                            v-subheader.pl-4.clickable(
+                              v-else-if='navItem.kind === "header"'
+                              :key='navItem.id'
+                              :class='(navItem === current) ? "blue" : ""'
+                              @click='selectItem(navItem)'
+                              ) {{navItem.label}}
+                      v-card-chin
+                        v-menu(offset-y, bottom, min-width='200px', style='flex: 1 1;')
+                          template(v-slot:activator='{ on }')
+                            v-btn(v-on='on', color='primary', depressed, block)
+                              v-icon(left) mdi-plus
+                              span {{$t('common:actions.add')}}
+                          v-list
+                            v-list-item(@click='addItem("link")')
+                              v-list-item-avatar(size='24'): v-icon mdi-link
+                              v-list-item-title {{$t('navigation.link')}}
+                            v-list-item(@click='addItem("header")')
+                              v-list-item-avatar(size='24'): v-icon mdi-format-title
+                              v-list-item-title {{$t('navigation.header')}}
+                            v-list-item(@click='addItem("divider")')
+                              v-list-item-avatar(size='24'): v-icon mdi-minus
+                              v-list-item-title {{$t('navigation.divider')}}
+                  v-col
+                    v-card(flat, style='border-radius: 0 4px 4px 0;')
+                      template(v-if='current.kind === "link"')
+                        v-toolbar(height='56', color='teal lighten-1', flat, dark)
+                          .subtitle-1 {{$t('navigation.edit', { kind: $t('navigation.link') })}}
+                          v-spacer
+                          v-btn.px-5(color='white', outlined, @click='deleteItem(current)')
+                            v-icon(left) mdi-delete
+                            span {{$t('navigation.delete', { kind: $t('navigation.link') })}}
+                        v-card-text
+                          v-text-field(
+                            outlined
+                            :label='$t("navigation.label")'
+                            prepend-icon='mdi-format-title'
+                            v-model='current.label'
+                            counter='255'
+                          )
+                          v-text-field(
+                            outlined
+                            :label='$t("navigation.icon")'
+                            prepend-icon='mdi-dice-5'
+                            v-model='current.icon'
+                            hide-details
+                          )
+                          .caption.pt-3.pl-5 The default icon set is #[strong Material Design Icons]. In order to use another icon set, you must first select it in the Theme administration section.
+                          .caption.pt-3.pl-5: strong Material Design Icons
+                          .caption.pl-5 Refer to the #[a(href='https://materialdesignicons.com/', target='_blank') Material Design Icons Reference] for the list of all possible values. You must prefix all values with #[code mdi-], e.g. #[code mdi-home]
+                          .caption.pt-3.pl-5: strong Font Awesome 5
+                          .caption.pl-5 Refer to the #[a(href='https://fontawesome.com/icons?d=gallery&m=free', target='_blank') Font Awesome 5 Reference] for the list of all possible values. You must prefix all values with #[code fas fa-], e.g. #[code fas fa-home]. Note that some icons use different prefixes (e.g. #[code fab], #[code fad], #[code fal], #[code far]).
+                          .caption.pt-3.pl-5: strong Font Awesome 4
+                          .caption.pl-5 Refer to the #[a(href='https://fontawesome.com/v4.7.0/icons/', target='_blank') Font Awesome 4 Reference] for the list of all possible values. You must prefix all values with #[code fa fa-], e.g. #[code fa fa-home]
+                        v-divider
+                        v-card-text
+                          v-select(
+                            outlined
+                            :label='$t("navigation.targetType")'
+                            prepend-icon='mdi-near-me'
+                            :items='navTypes'
+                            v-model='current.targetType'
+                            hide-details
+                          )
+                          v-text-field.mt-4(
+                            v-if='current.targetType === `external`'
+                            outlined
+                            :label='$t("navigation.target")'
+                            prepend-icon='mdi-near-me'
+                            v-model='current.target'
+                            hide-details
+                          )
+                          .d-flex.align-center.mt-4(v-else-if='current.targetType === "page"')
+                            v-btn.ml-8(
+                              color='primary'
+                              dark
+                              @click='selectPage'
+                              )
+                              v-icon(left) mdi-magnify
+                              span {{$t('admin:navigation.selectPageButton')}}
+                            .caption.ml-4.primary--text {{current.target}}
+                          v-text-field(
+                            v-else-if='current.targetType === `search`'
+                            outlined
+                            :label='$t("navigation.navType.searchQuery")'
+                            prepend-icon='search'
+                            v-model='current.target'
+                          )
+                        v-divider
+
+                      template(v-else-if='current.kind === "header"')
+                        v-toolbar(height='56', color='teal lighten-1', flat, dark)
+                          .subtitle-1 {{$t('navigation.edit', { kind: $t('navigation.header') })}}
+                          v-spacer
+                          v-btn.px-5(color='white', outlined, @click='deleteItem(current)')
+                            v-icon(left) mdi-delete
+                            span {{$t('navigation.delete', { kind: $t('navigation.header') })}}
+                        v-card-text
+                          v-text-field(
+                            outlined
+                            :label='$t("navigation.label")'
+                            prepend-icon='mdi-format-title'
+                            v-model='current.label'
+                          )
                         v-divider
-                      v-subheader.pl-4.clickable(
-                        v-else-if='navItem.kind === "header"'
-                        :key='navItem.id'
-                        :class='(navItem === current) ? "blue" : ""'
-                        @click='selectItem(navItem)'
-                        ) {{navItem.label}}
-                v-card-chin
-                  v-menu(offset-y, bottom, min-width='200px', style='flex: 1 1;')
-                    template(v-slot:activator='{ on }')
-                      v-btn(v-on='on', color='primary', depressed, block)
-                        v-icon(left) mdi-plus
-                        span {{$t('common:actions.add')}}
-                    v-list
-                      v-list-item(@click='addItem("link")')
-                        v-list-item-avatar(size='24'): v-icon mdi-link
-                        v-list-item-title {{$t('navigation.link')}}
-                      v-list-item(@click='addItem("header")')
-                        v-list-item-avatar(size='24'): v-icon mdi-format-title
-                        v-list-item-title {{$t('navigation.header')}}
-                      v-list-item(@click='addItem("divider")')
-                        v-list-item-avatar(size='24'): v-icon mdi-minus
-                        v-list-item-title {{$t('navigation.divider')}}
-            v-flex.animated.fadeInUp.wait-p2s
-              v-card.wiki-form(v-if='current.kind === "link"')
-                v-toolbar(dense, color='blue', flat, dark).subtitle-1 {{$t('navigation.edit', { kind: $t('navigation.link') })}}
-                v-card-text
-                  v-text-field(
-                    outlined
-                    :label='$t("navigation.label")'
-                    prepend-icon='mdi-format-title'
-                    v-model='current.label'
-                    counter='255'
-                  )
-                  v-text-field(
-                    outlined
-                    :label='$t("navigation.icon")'
-                    prepend-icon='mdi-dice-5'
-                    v-model='current.icon'
-                    hide-details
-                  )
-                  .caption.pt-3.pl-5 The default icon set is #[strong Material Design Icons]. In order to use another icon set, you must first select it in the Theme administration section.
-                  .caption.pt-3.pl-5: strong Material Design Icons
-                  .caption.pl-5 Refer to the #[a(href='https://materialdesignicons.com/', target='_blank') Material Design Icons Reference] for the list of all possible values. You must prefix all values with #[code mdi-], e.g. #[code mdi-home]
-                  .caption.pt-3.pl-5: strong Font Awesome 5
-                  .caption.pl-5 Refer to the #[a(href='https://fontawesome.com/icons?d=gallery&m=free', target='_blank') Font Awesome 5 Reference] for the list of all possible values. You must prefix all values with #[code fas fa-], e.g. #[code fas fa-home]. Note that some icons use different prefixes (e.g. #[code fab], #[code fad], #[code fal], #[code far]).
-                  .caption.pt-3.pl-5: strong Font Awesome 4
-                  .caption.pl-5 Refer to the #[a(href='https://fontawesome.com/v4.7.0/icons/', target='_blank') Font Awesome 4 Reference] for the list of all possible values. You must prefix all values with #[code fa fa-], e.g. #[code fa fa-home]
-                  v-select.mt-4(
-                    outlined
-                    :label='$t("navigation.targetType")'
-                    prepend-icon='mdi-near-me'
-                    :items='navTypes'
-                    v-model='current.targetType'
-                  )
-                  v-text-field(
-                    v-if='current.targetType === `external`'
-                    outlined
-                    :label='$t("navigation.target")'
-                    prepend-icon='mdi-near-me'
-                    v-model='current.target'
-                  )
-                  v-btn(
-                    v-else-if='current.targetType === "page"'
-                    color='indigo'
-                    :dark='false'
-                    disabled
-                    @click='selectPage'
-                    )
-                    v-icon(left) mdi-search
-                    span Select Page...
-                  v-text-field(
-                    v-else-if='current.targetType === `search`'
-                    outlined
-                    :label='$t("navigation.navType.searchQuery")'
-                    prepend-icon='search'
-                    v-model='current.target'
-                  )
 
-                v-card-chin
-                  v-spacer
-                  v-btn.px-5(color='red', outlined, @click='deleteItem(current)')
-                    v-icon(left) mdi-delete
-                    span {{$t('navigation.delete', { kind: $t('navigation.link') })}}
-              v-card(v-else-if='current.kind === "header"')
-                v-toolbar(dense, color='blue', flat, dark)
-                  .subtitle-1 {{$t('navigation.edit', { kind: $t('navigation.header') })}}
-                v-card-text
-                  v-text-field(
-                    outlined
-                    :label='$t("navigation.label")'
-                    prepend-icon='mdi-format-title'
-                    v-model='current.label'
-                  )
-                v-card-chin
-                  v-spacer
-                  v-btn.px-5(color='red', outlined, @click='deleteItem(current)')
-                    v-icon(left) mdi-delete
-                    span {{$t('navigation.delete', { kind: $t('navigation.header') })}}
-              div(v-else-if='current.kind === "divider"')
-                v-btn.mt-0.px-5(color='red', outlined, @click='deleteItem(current)')
-                  v-icon(left) mdi-delete
-                  span {{$t('navigation.delete', { kind: $t('navigation.divider') })}}
-              v-card(v-else)
-                v-card-text.grey--text(v-if='navTree.length > 0') {{$t('navigation.noSelectionText')}}
-                v-card-text.grey--text(v-else) {{$t('navigation.noItemsText')}}
+                      div(v-else-if='current.kind === "divider"')
+                        v-toolbar(height='56', color='teal lighten-1', flat, dark)
+                          .subtitle-1 {{$t('navigation.edit', { kind: $t('navigation.divider') })}}
+                          v-spacer
+                          v-btn.px-5(color='white', outlined, @click='deleteItem(current)')
+                            v-icon(left) mdi-delete
+                            span {{$t('navigation.delete', { kind: $t('navigation.divider') })}}
+
+                      v-card-text(v-if='current.kind')
+                        v-radio-group.pl-8(v-model='current.visibilityMode', mandatory, hide-details)
+                          v-radio(:label='$t("admin:navigation.visibilityMode.all")', value='all', color='primary')
+                          v-radio.mt-3(:label='$t("admin:navigation.visibilityMode.restricted")', value='restricted', color='primary')
+                        .pl-8
+                          v-select.pl-8.mt-3(
+                            item-text='name'
+                            item-value='id'
+                            outlined
+                            prepend-icon='mdi-account-group'
+                            label='Groups'
+                            :disabled='current.visibilityMode !== `restricted`'
+                            v-model='current.visibilityGroups'
+                            :items='groups'
+                            persistent-hint
+                            clearable
+                            multiple
+                          )
+                      template(v-else)
+                        v-toolbar(height='56', color='teal lighten-1', flat, dark)
+                        v-card-text.grey--text(v-if='navTree.length > 0') {{$t('navigation.noSelectionText')}}
+                        v-card-text.grey--text(v-else) {{$t('navigation.noItemsText')}}
+
+    page-selector(mode='select', v-model='selectPageModal', :open-handler='selectPageHandle', path='home', :locale='currentLang')
 </template>
 
 <script>
 import _ from 'lodash'
-import { v4 as uuid } from 'uuid'
+import gql from 'graphql-tag'
+import uuid from 'uuid/v4'
 
-import treeSaveMutation from 'gql/admin/navigation/navigation-mutation-save-tree.gql'
-import treeQuery from 'gql/admin/navigation/navigation-query-tree.gql'
+import groupsQuery from 'gql/admin/users/users-query-groups.gql'
 
 import draggable from 'vuedraggable'
 
+/* global siteConfig, siteLangs */
+
 export default {
   components: {
     draggable
   },
   data() {
     return {
+      selectPageModal: false,
+      navMode: 'custom',
       navTree: [],
-      current: {}
+      current: {},
+      currentLang: 'en',
+      groups: []
     }
   },
   computed: {
-    navTypes() {
+    navTypes () {
       return [
-        // { text: this.$t('navigation.navType.external'), value: 'external' },
+        { text: this.$t('navigation.navType.external'), value: 'external' },
         { text: this.$t('navigation.navType.home'), value: 'home' },
-        { text: 'Internal Path / External Link', value: 'external' }
-        // { text: this.$t('navigation.navType.page'), value: 'page' }
+        { text: this.$t('navigation.navType.page'), value: 'page' }
         // { text: this.$t('navigation.navType.searchQuery'), value: 'search' }
       ]
+    },
+    locales () {
+      return siteLangs
     }
   },
   methods: {
     addItem(kind) {
       let newItem = {
         id: uuid(),
-        kind
+        kind,
+        visibilityMode: 'all',
+        visibilityGroups: []
       }
       switch (kind) {
         case 'link':
@@ -207,13 +300,29 @@ export default {
       this.current = item
     },
     selectPage() {
-      window.alert(`Coming soon. Use External Link for now (you can still specify internal links).`)
+      this.selectPageModal = true
+    },
+    selectPageHandle ({ path, locale }) {
+      this.current.target = `/${locale}/${path}`
     },
     async save() {
       this.$store.commit(`loadingStart`, 'admin-navigation-save')
       try {
         const resp = await this.$apollo.mutate({
-          mutation: treeSaveMutation,
+          mutation: gql`
+            mutation ($tree: [NavigationTreeInput]!) {
+              navigation{
+                updateTree(tree: $tree) {
+                  responseResult {
+                    succeeded
+                    errorCode
+                    slug
+                    message
+                  }
+                }
+              }
+            }
+          `,
           variables: {
             tree: this.navTree
           }
@@ -242,14 +351,41 @@ export default {
       })
     }
   },
+  mounted () {
+    this.currentLang = siteConfig.lang
+  },
   apollo: {
     navTree: {
-      query: treeQuery,
+      query: gql`
+        {
+          navigation {
+            tree {
+              locale
+              items {
+                id
+                kind
+                label
+                icon
+                targetType
+                target
+              }
+            }
+          }
+        }
+      `,
       fetchPolicy: 'network-only',
       update: (data) => _.cloneDeep(data.navigation.tree),
       watchLoading (isLoading) {
         this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-navigation-tree')
       }
+    },
+    groups: {
+      query: groupsQuery,
+      fetchPolicy: 'network-only',
+      update: (data) => data.groups.list,
+      watchLoading (isLoading) {
+        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-navigation-groups')
+      }
     }
   }
 }

+ 1 - 1
client/components/common/page-selector.vue

@@ -10,7 +10,7 @@
         v-icon.mr-3(color='white') mdi-page-next-outline
         .body-1(v-if='mode === `create`') Select New Page Location
         .body-1(v-else-if='mode === `move`') Move / Rename Page Location
-        .body-1(v-else-if='mode === `select`') Select Page
+        .body-1(v-else-if='mode === `select`') Select a Page
         v-spacer
         v-progress-circular(
           indeterminate

+ 0 - 12
client/graph/admin/navigation/navigation-mutation-save-tree.gql

@@ -1,12 +0,0 @@
-mutation ($tree: [NavigationItemInput]!) {
-  navigation{
-    updateTree(tree: $tree) {
-      responseResult {
-        succeeded
-        errorCode
-        slug
-        message
-      }
-    }
-  }
-}

+ 0 - 12
client/graph/admin/navigation/navigation-query-tree.gql

@@ -1,12 +0,0 @@
-{
-  navigation {
-    tree {
-      id
-      kind
-      label
-      icon
-      targetType
-      target
-    }
-  }
-}

+ 100 - 9
client/themes/default/components/nav-sidebar.vue

@@ -1,13 +1,17 @@
 <template lang="pug">
   div
-    //- .blue.darken-3.pa-3.d-flex
-    //-   v-btn(depressed, color='blue darken-2', style='min-width:0;', href='/')
-    //-     v-icon(size='20') mdi-home
-    //-   v-btn.ml-3(depressed, color='blue darken-2', style='flex: 1 1 100%;')
-    //-     v-icon(left) mdi-file-tree
-    //-     .body-2.text-none Browse
-    //- v-divider
-    v-list.py-2(dense, :class='color', :dark='dark')
+    .blue.darken-3.pa-3.d-flex
+      v-btn(depressed, color='blue darken-2', style='min-width:0;', href='/')
+        v-icon(size='20') mdi-home
+      v-btn.ml-3(v-if='currentMode === `custom`', depressed, color='blue darken-2', style='flex: 1 1 100%;', @click='switchMode(`browse`)')
+        v-icon(left) mdi-file-tree
+        .body-2.text-none Browse
+      v-btn.ml-3(v-else-if='currentMode === `browse`', depressed, color='blue darken-2', style='flex: 1 1 100%;', @click='switchMode(`custom`)')
+        v-icon(left) mdi-navigation
+        .body-2.text-none Main Menu
+    v-divider
+    //-> Custom Navigation
+    v-list.py-2(v-if='currentMode === `custom`', dense, :class='color', :dark='dark')
       template(v-for='item of items')
         v-list-item(
           v-if='item.kind === `link`'
@@ -18,9 +22,31 @@
           v-list-item-title {{ item.label }}
         v-divider.my-2(v-else-if='item.kind === `divider`')
         v-subheader.pl-4(v-else-if='item.kind === `header`') {{ item.label }}
+    //-> Browse
+    v-list.py-2(v-else-if='currentMode === `browse`', dense, :class='color', :dark='dark')
+      template(v-if='currentParent.id > 0')
+        v-list-item(v-for='(item, idx) of parents', :key='`parent-` + item.id', @click='fetchBrowseItems(item)', style='min-height: 30px;')
+          v-list-item-avatar(size='18', :style='`padding-left: ` + (idx * 8) + `px; width: auto; margin: 0 5px 0 0;`')
+            v-icon(small) mdi-folder-open
+          v-list-item-title {{ item.title }}
+        v-divider.mt-2
+        v-subheader.pl-4 Current Directory
+      template(v-for='item of currentItems')
+        v-list-item(v-if='item.isFolder', :key='`childfolder-` + item.id', @click='fetchBrowseItems(item)')
+          v-list-item-avatar(size='24')
+            v-icon mdi-folder
+          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-list-item-title {{ item.title }}
 </template>
 
 <script>
+import _ from 'lodash'
+import gql from 'graphql-tag'
+import { get } from 'vuex-pathify'
+
 export default {
   props: {
     color: {
@@ -34,10 +60,75 @@ export default {
     items: {
       type: Array,
       default: () => []
+    },
+    mode: {
+      type: String,
+      default: 'browse'
     }
   },
   data() {
-    return {}
+    return {
+      currentMode: 'browse',
+      currentItems: [],
+      currentParent: {
+        id: 0,
+        title: '/ (root)'
+      },
+      all: []
+    }
+  },
+  computed: {
+    path: get('page/path'),
+    locale: get('page/locale')
+  },
+  methods: {
+    switchMode (mode) {
+      this.currentMode = mode
+      if (mode === `browse`) {
+        this.fetchBrowseItems()
+      }
+    },
+    async fetchBrowseItems (item) {
+      this.$store.commit(`loadingStart`, 'browse-load')
+      if (!item) {
+        item = this.currentParent
+      } else {
+        if (!_.some(this.parents, ['id', item.id])) {
+          this.parents.push(this.currentParent)
+        }
+        this.currentParent = item
+      }
+      const resp = await this.$apollo.query({
+        query: gql`
+          query ($parent: Int!, $locale: String!) {
+            pages {
+              tree(parent: $parent, mode: ALL, locale: $locale, includeParents: true) {
+                id
+                path
+                title
+                isFolder
+                pageId
+                parent
+              }
+            }
+          }
+        `,
+        fetchPolicy: 'cache-first',
+        variables: {
+          parent: item.id,
+          locale: this.locale
+        }
+      })
+      this.currentItems = _.get(resp, 'data.pages.tree', [])
+      this.all.push(...this.currentItems)
+      this.$store.commit(`loadingStop`, 'browse-load')
+    }
+  },
+  mounted () {
+    this.currentMode = this.mode
+    if (this.mode === 'browse') {
+      this.fetchBrowseItems()
+    }
   }
 }
 </script>

+ 1 - 1
server/graph/resolvers/navigation.js

@@ -11,7 +11,7 @@ module.exports = {
   },
   NavigationQuery: {
     async tree(obj, args, context, info) {
-      return WIKI.models.navigation.getTree()
+      return WIKI.models.navigation.getTree({ cache: false, locale: 'all' })
     }
   },
   NavigationMutation: {

+ 12 - 2
server/graph/schemas/navigation.graphql

@@ -15,7 +15,7 @@ extend type Mutation {
 # -----------------------------------------------
 
 type NavigationQuery {
-  tree: [NavigationItem]!
+  tree: [NavigationTree]!
 }
 
 # -----------------------------------------------
@@ -24,7 +24,7 @@ type NavigationQuery {
 
 type NavigationMutation {
   updateTree(
-    tree: [NavigationItemInput]!
+    tree: [NavigationTreeInput]!
   ): DefaultResponse @auth(requires: ["manage:navigation", "manage:system"])
 }
 
@@ -32,6 +32,16 @@ type NavigationMutation {
 # TYPES
 # -----------------------------------------------
 
+type NavigationTree {
+  locale: String!
+  items: [NavigationItem]!
+}
+
+input NavigationTreeInput {
+  locale: String!
+  items: [NavigationItemInput]!
+}
+
 type NavigationItem {
   id: String!
   kind: String!

+ 16 - 5
server/models/navigation.js

@@ -1,4 +1,5 @@
 const Model = require('objection').Model
+const _ = require('lodash')
 
 /* global WIKI */
 
@@ -21,19 +22,29 @@ module.exports = class Navigation extends Model {
     }
   }
 
-  static async getTree({ cache = false } = {}) {
+  static async getTree({ cache = false, locale = 'en' } = {}) {
     if (cache) {
-      const navTreeCached = await WIKI.cache.get('nav:sidebar')
+      const navTreeCached = await WIKI.cache.get(`nav:sidebar:${locale}`)
       if (navTreeCached) {
         return navTreeCached
       }
     }
     const navTree = await WIKI.models.navigation.query().findOne('key', 'site')
     if (navTree) {
-      if (cache) {
-        await WIKI.cache.set('nav:sidebar', navTree.config, 300)
+      // Check for pre-2.1 format
+      if (_.has(navTree.config[0], 'kind')) {
+        navTree.config = [{
+          locale: 'en',
+          items: navTree.config
+        }]
       }
-      return navTree.config
+
+      for (const tree of navTree.config) {
+        if (cache) {
+          await WIKI.cache.set(`nav:sidebar:${tree.locale}`, tree.items, 300)
+        }
+      }
+      return locale === 'all' ? navTree.config : WIKI.cache.get(`nav:sidebar:${locale}`)
     } else {
       WIKI.logger.warn('Site Navigation is missing or corrupted.')
       return []