From 69d3f1ff330b497cec296643f73d1a8e19e1c2a9 Mon Sep 17 00:00:00 2001 From: Ruslan Semak Date: Mon, 21 Apr 2025 17:09:53 +0300 Subject: [PATCH] feat: Expand-tree view with bug (clicks works only at title) --- client/components/admin/admin-navigation.vue | 9 ++ .../themes/default/components/nav-sidebar.vue | 151 +++++++++++++++++- server/graph/schemas/navigation.graphql | 1 + 3 files changed, 160 insertions(+), 1 deletion(-) diff --git a/client/components/admin/admin-navigation.vue b/client/components/admin/admin-navigation.vue index 56e0889d..3145a247 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 fb0e98bc..6c45c2ec 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') @@ -230,6 +268,94 @@ export default { // Иначе открываем в текущем окне window.location.assign(url) } + }, + 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 () { @@ -238,12 +364,35 @@ 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() + } } } + + 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 }