From 40f875e085b7b155e50024697c62954af1a59dea Mon Sep 17 00:00:00 2001 From: myml Date: Mon, 30 May 2022 09:31:50 +0800 Subject: [PATCH] feat: New tree navigation replaces browse navigation --- client/components/admin/admin-navigation.vue | 9 + .../themes/default/components/nav-sidebar.vue | 155 +++++++++++++++++- server/graph/schemas/navigation.graphql | 1 + 3 files changed, 163 insertions(+), 2 deletions(-) diff --git a/client/components/admin/admin-navigation.vue b/client/components/admin/admin-navigation.vue index e4796c2f..8b187449 100644 --- a/client/components/admin/admin-navigation.vue +++ b/client/components/admin/admin-navigation.vue @@ -23,6 +23,15 @@ v-toolbar-title.subtitle-1 {{$t('admin:navigation.mode')}} v-list(nav, two-line) v-list-item-group(v-model='config.mode', mandatory, :color='$vuetify.theme.dark ? `teal lighten-3` : `teal`') + v-list-item(value='NEWTREE') + v-list-item-avatar + img(src='/_assets/svg/icon-tree-structure-dotted.svg', alt='Site Tree') + v-list-item-content + v-list-item-title {{$t('admin:navigation.modeNewSiteTree.title')}} + v-list-item-subtitle {{$t('admin:navigation.modeNewSiteTree.description')}} + v-list-item-avatar + v-icon(v-if='$vuetify.theme.dark', :color='config.mode === `NEWTREE` ? `teal lighten-3` : `grey darken-2`') mdi-check-circle + v-icon(v-else, :color='config.mode === `NEWTREE` ? `teal` : `grey lighten-3`') mdi-check-circle v-list-item(value='TREE') v-list-item-avatar img(src='/_assets/svg/icon-tree-structure-dotted.svg', alt='Site Tree') diff --git a/client/themes/default/components/nav-sidebar.vue b/client/themes/default/components/nav-sidebar.vue index fc2619e5..392eab1f 100644 --- a/client/themes/default/components/nav-sidebar.vue +++ b/client/themes/default/components/nav-sidebar.vue @@ -23,6 +23,15 @@ depressed :color='$vuetify.theme.dark ? `grey darken-4` : `blue darken-2`' style='flex: 1 1 100%;' + @click='switchMode(`tree`)' + ) + v-icon(left) mdi-file-tree + .body-2.text-none {{$t('common:sidebar.tree')}} + v-btn.ml-3( + v-else-if='currentMode === `tree`' + depressed + :color='$vuetify.theme.dark ? `grey darken-4` : `blue darken-2`' + style='flex: 1 1 100%;' @click='switchMode(`custom`)' ) v-icon(left) mdi-navigation @@ -43,6 +52,29 @@ v-list-item-title {{ item.l }} v-divider.my-2(v-else-if='item.k === `divider`') v-subheader.pl-4(v-else-if='item.k === `header`') {{ item.l }} + + //-> Tree Navigation + v-treeview( + v-else-if='currentMode === `tree`' + activatable + open-on-click + :color='"white"' + :active='treeDefaultActive' + :open='treeDefaultOpen' + :items='treeItems' + :load-children='fetchTreeChild' + @update:active='activeTreeItem' + ) + template(v-slot:prepend="{ item, open }") + v-icon(v-if="!item.children") mdi-text-box + v-icon(v-else-if="open") mdi-folder-open + v-icon(v-else) mdi-folder + template(v-slot:label="{ item }") + div(class='tree-item') + a(v-if="!item.children" :href="'/'+item.locale+'/'+item.path") + span {{item.name}} + span(v-else) {{item.name}} + //-> Browse v-list.py-2(v-else-if='currentMode === `browse`', dense, :class='color', :dark='dark') template(v-if='currentParent.id > 0') @@ -102,7 +134,10 @@ export default { title: '/ (root)' }, parents: [], - loadedCache: [] + loadedCache: [], + treeItems: [], + treeDefaultOpen: [], + treeDefaultActive: [], } }, computed: { @@ -116,6 +151,9 @@ export default { if (mode === `browse` && this.loadedCache.length < 1) { this.loadFromCurrentPath() } + if (mode === 'tree') { + this.fetchTreeRoot(); + } }, async fetchBrowseItems (item) { this.$store.commit(`loadingStart`, 'browse-load') @@ -219,7 +257,95 @@ export default { }, goHome () { window.location.assign(siteLangs.length > 0 ? `/${this.locale}/home` : '/') - } + }, + pageItem2TreeItem(item,level) { + if (item.isFolder) { + return { id: item.id, level: level, pageId: item.pageId, path: item.path, locale: item.locale, name: item.title, children: [] } + } else { + return { id: item.id, level: level, path: item.path, locale: item.locale, name: item.title } + } + }, + activeTreeItem(id) { + const find = (items) => { + for(const item of items) { + if(item.id == id) { + return item + } + if(item.children && item.children.length) { + const v = find(item.children) + if(v) { + return v + } + } + } + } + const item = find(this.treeItems) + if(item) { + if(!this.treeDefaultActive.includes(item.id)) { + location.href = `/${item.locale}/${item.path}` + } else { + setTimeout(() => { + const el = document.querySelector(".v-treeview-node--active") + el.scrollIntoViewIfNeeded() + }) + } + } + }, + async fetchTreeChild(parent) { + const items = await this.fetchPages(parent.id) + parent.children = [] + if(parent.pageId){ + parent.children.push({ + id: parent.pageId,level: parent.level+1, path: parent.path, locale: parent.locale, name: parent.name + }) + } + parent.children.push( + ...items.map(item => this.pageItem2TreeItem(item, parent.level+1)) + ) + this.checkTreeDefaultOpen(parent.children); + }, + async fetchTreeRoot(){ + const children = await this.fetchPages(0) + this.treeItems = children.map(item => this.pageItem2TreeItem(item, 0)) + this.checkTreeDefaultOpen(this.treeItems, 0); + }, + async checkTreeDefaultOpen(items){ + const item = items.find(item => item.children && this.path.startsWith(item.path)) + if(item) { + setTimeout(()=>{ + this.treeDefaultOpen.push(item.id) + }) + } + const active = items.find(item => item.path == this.path) + if(active) { + this.treeDefaultActive.push(active.id) + } + }, + async fetchPages(id) { + const resp = await this.$apollo.query({ + query: gql` + query($parent: Int, $locale: String!) { + pages { + tree(parent: $parent, mode: ALL, locale: $locale) { + id + path + title + isFolder + pageId + parent + locale + } + } + } + `, + fetchPolicy: 'cache-first', + variables: { + parent: id, + locale: this.locale + } + }) + return _.get(resp, 'data.pages.tree', []) + }, }, mounted () { this.currentParent.title = `/ ${this.$t('common:sidebar.root')}` @@ -227,12 +353,37 @@ export default { this.currentMode = 'browse' } else if (this.navMode === 'STATIC') { this.currentMode = 'custom' + } else if (this.navMode === 'NEWTREE') { + this.currentMode = 'tree' } else { this.currentMode = window.localStorage.getItem('navPref') || 'custom' } if (this.currentMode === 'browse') { this.loadFromCurrentPath() } + if (this.currentMode === "tree") { + this.fetchTreeRoot(); + } } } + + + \ No newline at end of file diff --git a/server/graph/schemas/navigation.graphql b/server/graph/schemas/navigation.graphql index 26ef9a9c..929973fa 100644 --- a/server/graph/schemas/navigation.graphql +++ b/server/graph/schemas/navigation.graphql @@ -75,6 +75,7 @@ type NavigationConfig { enum NavigationMode { NONE TREE + NEWTREE MIXED STATIC }