Sfoglia il codice sorgente

feat: list pages by tags + fix search permissions

Nick 5 anni fa
parent
commit
b6fd070b0b

+ 1 - 1
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')
 </template>
 

+ 101 - 8
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
+      }
     }
   }
 }

+ 14 - 0
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 - 0
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>

+ 62 - 26
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)

+ 4 - 1
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 {