feat: browse nav + pageTree ancestors

This commit is contained in:
NGPixel 2020-04-12 18:05:48 -04:00 committed by Nicolas Giard
parent 3ca72ccc1e
commit 1c80faa94d
11 changed files with 210 additions and 62 deletions

View File

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

View File

@ -38,7 +38,7 @@
v-list-item-title {{ item.title }} 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(v-else, :href='`/` + item.path', :key='`childpage-` + item.id', :input-value='path === item.path')
v-list-item-avatar(size='24') v-list-item-avatar(size='24')
v-icon mdi-file-document-box v-icon mdi-text-box
v-list-item-title {{ item.title }} v-list-item-title {{ item.title }}
</template> </template>
@ -74,7 +74,8 @@ export default {
id: 0, id: 0,
title: '/ (root)' title: '/ (root)'
}, },
all: [] parents: [],
loadedCache: []
} }
}, },
computed: { computed: {
@ -84,25 +85,40 @@ export default {
methods: { methods: {
switchMode (mode) { switchMode (mode) {
this.currentMode = mode this.currentMode = mode
if (mode === `browse`) { if (mode === `browse` && this.loadedCache.length < 1) {
this.fetchBrowseItems() this.loadFromCurrentPath()
} }
}, },
async fetchBrowseItems (item) { async fetchBrowseItems (item) {
this.$store.commit(`loadingStart`, 'browse-load') this.$store.commit(`loadingStart`, 'browse-load')
if (!item) { if (!item) {
item = this.currentParent item = this.currentParent
}
if (this.loadedCache.indexOf(item.id) < 0) {
this.currentItems = []
}
if (item.id === 0) {
this.parents = []
} else { } 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.parents.push(this.currentParent)
} }
this.currentParent = item this.parents.push(item)
} }
this.currentParent = item
const resp = await this.$apollo.query({ const resp = await this.$apollo.query({
query: gql` query: gql`
query ($parent: Int!, $locale: String!) { query ($parent: Int, $locale: String!) {
pages { pages {
tree(parent: $parent, mode: ALL, locale: $locale, includeParents: true) { tree(parent: $parent, mode: ALL, locale: $locale) {
id id
path path
title title
@ -119,15 +135,63 @@ export default {
locale: this.locale locale: this.locale
} }
}) })
this.loadedCache = _.union(this.loadedCache, [item.id])
this.currentItems = _.get(resp, 'data.pages.tree', []) 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') this.$store.commit(`loadingStop`, 'browse-load')
} }
}, },
mounted () { mounted () {
this.currentMode = this.mode this.currentMode = this.mode
if (this.mode === 'browse') { if (this.mode === 'browse') {
this.fetchBrowseItems() this.loadFromCurrentPath()
} }
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -4,18 +4,21 @@ const graphHelper = require('../../helpers/graph')
module.exports = { module.exports = {
Query: { Query: {
async navigation() { return {} } async navigation () { return {} }
}, },
Mutation: { Mutation: {
async navigation() { return {} } async navigation () { return {} }
}, },
NavigationQuery: { NavigationQuery: {
async tree(obj, args, context, info) { async tree (obj, args, context, info) {
return WIKI.models.navigation.getTree({ cache: false, locale: 'all' }) return WIKI.models.navigation.getTree({ cache: false, locale: 'all' })
},
config (obj, args, context, info) {
return WIKI.config.nav
} }
}, },
NavigationMutation: { NavigationMutation: {
async updateTree(obj, args, context) { async updateTree (obj, args, context) {
try { try {
await WIKI.models.navigation.query().patch({ await WIKI.models.navigation.query().patch({
config: args.tree config: args.tree
@ -28,6 +31,20 @@ module.exports = {
} catch (err) { } catch (err) {
return graphHelper.generateError(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)
}
} }
} }
} }

View File

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

View File

@ -16,6 +16,7 @@ extend type Mutation {
type NavigationQuery { type NavigationQuery {
tree: [NavigationTree]! tree: [NavigationTree]!
config: NavigationConfig!
} }
# ----------------------------------------------- # -----------------------------------------------
@ -26,6 +27,9 @@ type NavigationMutation {
updateTree( updateTree(
tree: [NavigationTreeInput]! tree: [NavigationTreeInput]!
): DefaultResponse @auth(requires: ["manage:navigation", "manage:system"]) ): 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 targetType: String
target: String target: String
} }
type NavigationConfig {
mode: NavigationMode!
}
enum NavigationMode {
NONE
TREE
MIXED
}

View File

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

View File

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