From b6fd070b0b66199d45f768059b7e363b17812ad5 Mon Sep 17 00:00:00 2001 From: Nick Date: Sun, 8 Sep 2019 21:31:22 -0400 Subject: [PATCH] feat: list pages by tags + fix search permissions --- client/components/admin/admin-pages.vue | 2 +- client/components/tags.vue | 109 ++++++++++++++++-- .../graph/common/common-pages-query-list.gql | 14 +++ client/static/svg/icon-info.svg | 1 + server/graph/resolvers/page.js | 88 +++++++++----- server/graph/schemas/page.graphql | 5 +- 6 files changed, 183 insertions(+), 36 deletions(-) create mode 100644 client/graph/common/common-pages-query-list.gql create mode 100644 client/static/svg/icon-info.svg diff --git a/client/components/admin/admin-pages.vue b/client/components/admin/admin-pages.vue index b5507c52..3b2907a1 100644 --- a/client/components/admin/admin-pages.vue +++ b/client/components/admin/admin-pages.vue @@ -65,7 +65,7 @@ td {{ props.item.updatedAt | moment('calendar') }} template(slot='no-data') 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') diff --git a/client/components/tags.vue b/client/components/tags.vue index 255f15e7..e0d0a20f 100644 --- a/client/components/tags.vue +++ b/client/components/tags.vue @@ -15,7 +15,7 @@ v-icon(v-if='isSelected(tag.tag)', color='primary') mdi-checkbox-intermediate v-icon(v-else) mdi-checkbox-blank-outline 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') template(v-if='selection.length > 0') .overline.mr-3.animated.fadeInLeft Current Selection @@ -41,6 +41,7 @@ .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-text-field.tags-search( + v-model='innerSearch' label='Search within results...' solo hide-details @@ -50,6 +51,7 @@ height='40' prepend-icon='mdi-file-document-box-search-outline' append-icon='mdi-arrow-right' + clearable ) template(v-if='locales.length > 1') 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-down v-divider - .text-center.pt-10 + .text-center.pt-10(v-if='selection.length < 1') img(src='/svg/icon-price-tag.svg') .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 notify search-results @@ -100,6 +155,7 @@ import VueRouter from 'vue-router' import _ from 'lodash' import tagsQuery from 'gql/common/common-pages-query-tags.gql' +import pagesQuery from 'gql/common/common-pages-query-list.gql' /* global siteLangs */ @@ -113,17 +169,27 @@ export default { return { tags: [], selection: [], + innerSearch: '', locale: 'any', locales: [], - orderBy: 'TITLE', + orderBy: 'title', 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, + pagination: { + page: 1, + itemsPerPage: 12, + mustSort: true, + sortBy: ['title'], + sortDesc: [false] + }, + pages: [], + isLoading: true, scrollStyle: { vuescroll: {}, scrollPanel: { @@ -154,6 +220,9 @@ export default { }, tagsSelected () { return _.filter(this.tags, t => _.includes(this.selection, t.tag)) + }, + pageTotal () { + return Math.ceil(this.pages.length / this.pagination.itemsPerPage) } }, watch: { @@ -162,9 +231,11 @@ export default { }, orderBy (newValue, oldValue) { this.rebuildURL() + this.pagination.sortBy = [newValue] }, orderByDirection (newValue, oldValue) { this.rebuildURL() + this.pagination.sortDesc = [newValue === 1] } }, router, @@ -186,6 +257,7 @@ export default { this.selection.push(tag) } this.rebuildURL() + console.info(this.$refs.dude) }, isSelected (tag) { return _.includes(this.selection, tag) @@ -204,6 +276,9 @@ export default { _.set(urlObj, 'query.dir', this.orderByDirection === 0 ? `asc` : `desc`) } this.$router.push(urlObj) + }, + goTo (page) { + window.location.assign(`/${page.locale}/${page.path}`) } }, apollo: { @@ -214,6 +289,24 @@ export default { watchLoading (isLoading) { 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 + } } } } diff --git a/client/graph/common/common-pages-query-list.gql b/client/graph/common/common-pages-query-list.gql new file mode 100644 index 00000000..e4d71756 --- /dev/null +++ b/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 + } + } +} diff --git a/client/static/svg/icon-info.svg b/client/static/svg/icon-info.svg new file mode 100644 index 00000000..d6849ed0 --- /dev/null +++ b/client/static/svg/icon-info.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/server/graph/resolvers/page.js b/server/graph/resolvers/page.js index e85779c6..f017a68d 100644 --- a/server/graph/resolvers/page.js +++ b/server/graph/resolvers/page.js @@ -1,3 +1,4 @@ +const _ = require('lodash') const graphHelper = require('../../helpers/graph') /* global WIKI */ @@ -19,7 +20,16 @@ module.exports = { }, async search (obj, args, context) { 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 { return { results: [], @@ -29,8 +39,8 @@ module.exports = { } }, async list (obj, args, context, info) { - return WIKI.models.pages.query().column([ - 'id', + let results = await WIKI.models.pages.query().column([ + 'pages.id', 'path', { locale: 'localeCode' }, 'title', @@ -41,29 +51,55 @@ module.exports = { 'contentType', 'createdAt', '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) { let page = await WIKI.models.pages.getPageFromDb(args.id) diff --git a/server/graph/schemas/page.graphql b/server/graph/schemas/page.graphql index f0b90280..078209d8 100644 --- a/server/graph/schemas/page.graphql +++ b/server/graph/schemas/page.graphql @@ -31,7 +31,9 @@ type PageQuery { limit: Int orderBy: PageOrderBy orderByDirection: PageOrderByDirection - ): [PageListItem!]! @auth(requires: ["manage:system"]) + tags: [String!] + locale: String + ): [PageListItem!]! @auth(requires: ["manage:system", "read:pages"]) single( id: Int! @@ -177,6 +179,7 @@ type PageListItem { privateNS: String createdAt: Date! updatedAt: Date! + tags: [String] } enum PageOrderBy {