feat: list pages by tags + fix search permissions
This commit is contained in:
		| @@ -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') | ||||
| </template> | ||||
|  | ||||
|   | ||||
| @@ -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 | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
							
								
								
									
										14
									
								
								client/graph/common/common-pages-query-list.gql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								client/graph/common/common-pages-query-list.gql
									
									
									
									
									
										Normal file
									
								
							| @@ -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
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								client/static/svg/icon-info.svg
									
									
									
									
									
										Normal file
									
								
							| @@ -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> | ||||
| After Width: | Height: | Size: 828 B | 
| @@ -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) | ||||
|   | ||||
| @@ -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 { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user