Browse Source

feat: list pages by tags + fix search permissions

pull/1066/head
Nick 5 years ago
parent
commit
b6fd070b0b
6 changed files with 183 additions and 36 deletions
  1. 2
      client/components/admin/admin-pages.vue
  2. 109
      client/components/tags.vue
  3. 14
      client/graph/common/common-pages-query-list.gql
  4. 1
      client/static/svg/icon-info.svg
  5. 88
      server/graph/resolvers/page.js
  6. 5
      server/graph/schemas/page.graphql

2
client/components/admin/admin-pages.vue

@ -65,7 +65,7 @@
td {{ props.item.updatedAt | moment('calendar') }} td {{ props.item.updatedAt | moment('calendar') }}
template(slot='no-data') template(slot='no-data')
v-alert.ma-3(icon='mdi-alert', :value='true', outlined) No pages to display. v-alert.ma-3(icon='mdi-alert', :value='true', outlined) No pages to display.
.text-xs-center.py-2.animated.fadeInDown(v-if='this.pageTotal > 1')
.text-center.py-2.animated.fadeInDown(v-if='this.pageTotal > 1')
v-pagination(v-model='pagination', :length='pageTotal') v-pagination(v-model='pagination', :length='pageTotal')
</template> </template>

109
client/components/tags.vue

@ -15,7 +15,7 @@
v-icon(v-if='isSelected(tag.tag)', color='primary') mdi-checkbox-intermediate v-icon(v-if='isSelected(tag.tag)', color='primary') mdi-checkbox-intermediate
v-icon(v-else) mdi-checkbox-blank-outline v-icon(v-else) mdi-checkbox-blank-outline
v-list-item-title {{tag.title}} v-list-item-title {{tag.title}}
v-content
v-content.grey(:class='$vuetify.theme.dark ? `darken-4-d5` : `lighten-3`')
v-toolbar(color='primary', dark, flat, height='58') v-toolbar(color='primary', dark, flat, height='58')
template(v-if='selection.length > 0') template(v-if='selection.length > 0')
.overline.mr-3.animated.fadeInLeft Current Selection .overline.mr-3.animated.fadeInLeft Current Selection
@ -41,6 +41,7 @@
.overline.animated.fadeInRight Select one or more tags .overline.animated.fadeInRight Select one or more tags
v-toolbar(:color='$vuetify.theme.dark ? `grey darken-4-l5` : `grey lighten-4`', flat, height='58') v-toolbar(:color='$vuetify.theme.dark ? `grey darken-4-l5` : `grey lighten-4`', flat, height='58')
v-text-field.tags-search( v-text-field.tags-search(
v-model='innerSearch'
label='Search within results...' label='Search within results...'
solo solo
hide-details hide-details
@ -50,6 +51,7 @@
height='40' height='40'
prepend-icon='mdi-file-document-box-search-outline' prepend-icon='mdi-file-document-box-search-outline'
append-icon='mdi-arrow-right' append-icon='mdi-arrow-right'
clearable
) )
template(v-if='locales.length > 1') template(v-if='locales.length > 1')
v-divider.mx-3(vertical) v-divider.mx-3(vertical)
@ -86,9 +88,62 @@
v-btn(text, height='40'): v-icon(size='20') mdi-chevron-double-up v-btn(text, height='40'): v-icon(size='20') mdi-chevron-double-up
v-btn(text, height='40'): v-icon(size='20') mdi-chevron-double-down v-btn(text, height='40'): v-icon(size='20') mdi-chevron-double-down
v-divider v-divider
.text-center.pt-10
.text-center.pt-10(v-if='selection.length < 1')
img(src='/svg/icon-price-tag.svg') img(src='/svg/icon-price-tag.svg')
.subtitle-2.grey--text Select one or more tags on the left. .subtitle-2.grey--text Select one or more tags on the left.
.px-5.py-2(v-else)
v-data-iterator(
:items='pages'
:items-per-page='4'
:search='innerSearch'
:loading='isLoading'
:options.sync='pagination'
hide-default-footer
ref='dude'
)
template(v-slot:loading)
.text-center.pt-10
v-progress-circular(
indeterminate
color='primary'
size='96'
width='2'
)
.subtitle-2.grey--text.mt-5 Retrieving page results...
template(v-slot:no-data)
.text-center.pt-10
img(src='/svg/icon-info.svg')
.subtitle-2.grey--text Couldn't find any page with the selected tags.
template(v-slot:no-results)
.text-center.pt-10
img(src='/svg/icon-info.svg')
.subtitle-2.grey--text Couldn't find any page matching the current filtering options.
template(v-slot:default='props')
v-row(align='stretch')
v-col(
v-for='item of props.items'
:key='`page-` + item.id'
cols='12'
lg='6'
)
v-card.radius-7(
@click='goTo(item)'
style='height:100%;'
:class='$vuetify.theme.dark ? `grey darken-4` : ``'
)
v-card-text
.d-flex.flex-row.align-center
.body-1: strong.primary--text {{item.title}}
v-spacer
.caption Last updated {{item.updatedAt | moment('from')}}
.body-2.grey--text {{item.description || '---'}}
v-divider.my-2
.d-flex.flex-row.align-center
v-chip(small, label, :color='$vuetify.theme.dark ? `grey darken-3-l5` : `grey lighten-4`').overline {{item.locale}}
.caption.ml-1 / {{item.path}}
.text-center.py-2.animated.fadeInDown(v-if='this.pageTotal > 1')
v-pagination(v-model='pagination.page', :length='pageTotal')
nav-footer nav-footer
notify notify
search-results search-results
@ -100,6 +155,7 @@ import VueRouter from 'vue-router'
import _ from 'lodash' import _ from 'lodash'
import tagsQuery from 'gql/common/common-pages-query-tags.gql' import tagsQuery from 'gql/common/common-pages-query-tags.gql'
import pagesQuery from 'gql/common/common-pages-query-list.gql'
/* global siteLangs */ /* global siteLangs */
@ -113,17 +169,27 @@ export default {
return { return {
tags: [], tags: [],
selection: [], selection: [],
innerSearch: '',
locale: 'any', locale: 'any',
locales: [], locales: [],
orderBy: 'TITLE',
orderBy: 'title',
orderByItems: [ orderByItems: [
{ text: 'Creation Date', value: 'CREATED' },
{ text: 'ID', value: 'ID' },
{ text: 'Last Modified', value: 'UPDATED' },
{ text: 'Path', value: 'PATH' },
{ text: 'Title', value: 'TITLE' }
{ text: 'Creation Date', value: 'createdAt' },
{ text: 'ID', value: 'id' },
{ text: 'Last Modified', value: 'updatedAt' },
{ text: 'Path', value: 'path' },
{ text: 'Title', value: 'title' }
], ],
orderByDirection: 0, orderByDirection: 0,
pagination: {
page: 1,
itemsPerPage: 12,
mustSort: true,
sortBy: ['title'],
sortDesc: [false]
},
pages: [],
isLoading: true,
scrollStyle: { scrollStyle: {
vuescroll: {}, vuescroll: {},
scrollPanel: { scrollPanel: {
@ -154,6 +220,9 @@ export default {
}, },
tagsSelected () { tagsSelected () {
return _.filter(this.tags, t => _.includes(this.selection, t.tag)) return _.filter(this.tags, t => _.includes(this.selection, t.tag))
},
pageTotal () {
return Math.ceil(this.pages.length / this.pagination.itemsPerPage)
} }
}, },
watch: { watch: {
@ -162,9 +231,11 @@ export default {
}, },
orderBy (newValue, oldValue) { orderBy (newValue, oldValue) {
this.rebuildURL() this.rebuildURL()
this.pagination.sortBy = [newValue]
}, },
orderByDirection (newValue, oldValue) { orderByDirection (newValue, oldValue) {
this.rebuildURL() this.rebuildURL()
this.pagination.sortDesc = [newValue === 1]
} }
}, },
router, router,
@ -186,6 +257,7 @@ export default {
this.selection.push(tag) this.selection.push(tag)
} }
this.rebuildURL() this.rebuildURL()
console.info(this.$refs.dude)
}, },
isSelected (tag) { isSelected (tag) {
return _.includes(this.selection, tag) return _.includes(this.selection, tag)
@ -204,6 +276,9 @@ export default {
_.set(urlObj, 'query.dir', this.orderByDirection === 0 ? `asc` : `desc`) _.set(urlObj, 'query.dir', this.orderByDirection === 0 ? `asc` : `desc`)
} }
this.$router.push(urlObj) this.$router.push(urlObj)
},
goTo (page) {
window.location.assign(`/${page.locale}/${page.path}`)
} }
}, },
apollo: { apollo: {
@ -214,6 +289,24 @@ export default {
watchLoading (isLoading) { watchLoading (isLoading) {
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'tags-refresh') this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'tags-refresh')
} }
},
pages: {
query: pagesQuery,
fetchPolicy: 'cache-and-network',
update: (data) => _.cloneDeep(data.pages.list),
watchLoading (isLoading) {
this.isLoading = isLoading
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'pages-refresh')
},
variables () {
return {
locale: this.locale === 'any' ? null : this.locale,
tags: this.selection
}
},
skip () {
return this.selection.length < 1
}
} }
} }
} }

14
client/graph/common/common-pages-query-list.gql

@ -0,0 +1,14 @@
query ($limit: Int, $orderBy: PageOrderBy, $orderByDirection: PageOrderByDirection, $tags: [String!], $locale: String) {
pages {
list(limit: $limit, orderBy: $orderBy, orderByDirection: $orderByDirection, tags: $tags, locale: $locale) {
id
locale
path
title
description
createdAt
updatedAt
tags
}
}
}

1
client/static/svg/icon-info.svg

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" width="256" height="256"><path fill="#fff" d="M64,14c27.6,0,50,22.4,50,50c0,27.6-22.4,50-50,50c-27.6,0-50-22.4-50-50C14,36.4,36.4,14,64,14"/><path fill="#e6e7e7" d="M64,14c-0.2,0-0.4,0-0.6,0c-1.5,0-3.1,0.1-4.6,0.3c-0.3,0-0.7,0.1-1,0.1 c24.6,3.1,43.7,24.1,43.7,49.6c0,25.5-19.1,46.5-43.7,49.6c0.5,0.1,1,0.1,1.6,0.2c1.2,0.1,2.5,0.2,3.7,0.2c0.3,0,0.6,0,0.9,0 c27.6,0,50-22.4,50-50C114,36.4,91.6,14,64,14"/><path fill="#454b54" d="M64,117c-29.2,0-53-23.8-53-53s23.8-53,53-53s53,23.8,53,53S93.2,117,64,117z M64,17 c-25.9,0-47,21.1-47,47s21.1,47,47,47s47-21.1,47-47S89.9,17,64,17z"/><path fill="#454b54" d="M64 42.7c-1.7 0-3 1.3-3 3s1.3 3 3 3c1.7 0 3-1.3 3-3S65.7 42.7 64 42.7zM64 93c-1.7 0-3-1.3-3-3V62.3c0-1.7 1.3-3 3-3 1.7 0 3 1.3 3 3V90C67 91.7 65.7 93 64 93z"/></svg>

88
server/graph/resolvers/page.js

@ -1,3 +1,4 @@
const _ = require('lodash')
const graphHelper = require('../../helpers/graph') const graphHelper = require('../../helpers/graph')
/* global WIKI */ /* global WIKI */
@ -19,7 +20,16 @@ module.exports = {
}, },
async search (obj, args, context) { async search (obj, args, context) {
if (WIKI.data.searchEngine) { if (WIKI.data.searchEngine) {
return WIKI.data.searchEngine.query(args.query, args)
const resp = await WIKI.data.searchEngine.query(args.query, args)
return {
...resp,
results: _.filter(resp.results, r => {
return WIKI.auth.checkAccess(context.req.user, ['read:pages'], {
path: r.path,
locale: r.locale
})
})
}
} else { } else {
return { return {
results: [], results: [],
@ -29,8 +39,8 @@ module.exports = {
} }
}, },
async list (obj, args, context, info) { async list (obj, args, context, info) {
return WIKI.models.pages.query().column([
'id',
let results = await WIKI.models.pages.query().column([
'pages.id',
'path', 'path',
{ locale: 'localeCode' }, { locale: 'localeCode' },
'title', 'title',
@ -41,29 +51,55 @@ module.exports = {
'contentType', 'contentType',
'createdAt', 'createdAt',
'updatedAt' 'updatedAt'
]).modify(queryBuilder => {
if (args.limit) {
queryBuilder.limit(args.limit)
}
const orderDir = args.orderByDirection === 'DESC' ? 'desc' : 'asc'
switch (args.orderBy) {
case 'CREATED':
queryBuilder.orderBy('createdAt', orderDir)
break
case 'PATH':
queryBuilder.orderBy('path', orderDir)
break
case 'TITLE':
queryBuilder.orderBy('title', orderDir)
break
case 'UPDATED':
queryBuilder.orderBy('updatedAt', orderDir)
break
default:
queryBuilder.orderBy('id', orderDir)
break
}
})
])
.eagerAlgorithm(WIKI.models.Objection.Model.JoinEagerAlgorithm)
.eager('tags(selectTags)', {
selectTags: builder => {
builder.select('tag')
}
})
.modify(queryBuilder => {
if (args.limit) {
queryBuilder.limit(args.limit)
}
if (args.locale) {
queryBuilder.where('localeCode', args.locale)
}
if (args.tags && args.tags.length > 0) {
queryBuilder.whereIn('tags.tag', args.tags)
}
const orderDir = args.orderByDirection === 'DESC' ? 'desc' : 'asc'
switch (args.orderBy) {
case 'CREATED':
queryBuilder.orderBy('createdAt', orderDir)
break
case 'PATH':
queryBuilder.orderBy('path', orderDir)
break
case 'TITLE':
queryBuilder.orderBy('title', orderDir)
break
case 'UPDATED':
queryBuilder.orderBy('updatedAt', orderDir)
break
default:
queryBuilder.orderBy('pages.id', orderDir)
break
}
})
results = _.filter(results, r => {
return WIKI.auth.checkAccess(context.req.user, ['read:pages'], {
path: r.path,
locale: r.locale
})
}).map(r => ({
...r,
tags: _.map(r.tags, 'tag')
}))
if (args.tags && args.tags.length > 0) {
results = _.filter(results, r => _.every(args.tags, t => _.includes(r.tags, t)))
}
return results
}, },
async single (obj, args, context, info) { async single (obj, args, context, info) {
let page = await WIKI.models.pages.getPageFromDb(args.id) let page = await WIKI.models.pages.getPageFromDb(args.id)

5
server/graph/schemas/page.graphql

@ -31,7 +31,9 @@ type PageQuery {
limit: Int limit: Int
orderBy: PageOrderBy orderBy: PageOrderBy
orderByDirection: PageOrderByDirection orderByDirection: PageOrderByDirection
): [PageListItem!]! @auth(requires: ["manage:system"])
tags: [String!]
locale: String
): [PageListItem!]! @auth(requires: ["manage:system", "read:pages"])
single( single(
id: Int! id: Int!
@ -177,6 +179,7 @@ type PageListItem {
privateNS: String privateNS: String
createdAt: Date! createdAt: Date!
updatedAt: Date! updatedAt: Date!
tags: [String]
} }
enum PageOrderBy { enum PageOrderBy {

Loading…
Cancel
Save