소스 검색

feat: block-index feature complete + dark theme fixes

NGPixel 1 년 전
부모
커밋
d51fc36b56

+ 100 - 28
blocks/block-index/component.js

@@ -1,5 +1,5 @@
 import { LitElement, html, css } from 'lit'
-import treeQuery from './folderByPath.graphql'
+import treeQuery from './tree.graphql'
 
 /**
  * Block Index
@@ -11,9 +11,6 @@ export class BlockIndexElement extends LitElement {
         display: block;
         margin-bottom: 16px;
       }
-      :host-context(body.body--dark) {
-        background-color: #F00;
-      }
 
       ul {
         padding: 0;
@@ -23,29 +20,48 @@ export class BlockIndexElement extends LitElement {
 
       li {
         background-color: #fafafa;
-        background-image: linear-gradient(180deg,#fff,#fafafa);
-        border-right: 1px solid #eee;
-        border-bottom: 1px solid #eee;
-        border-left: 5px solid #e0e0e0;
+        background-image: linear-gradient(to bottom,#fff,#fafafa);
+        border-right: 1px solid rgba(0,0,0,.05);
+        border-bottom: 1px solid rgba(0,0,0,.05);
+        border-left: 5px solid rgba(0,0,0,.1);
         box-shadow: 0 3px 8px 0 rgba(116,129,141,.1);
         padding: 0;
         border-radius: 5px;
         font-weight: 500;
       }
+      :host-context(body.body--dark) li {
+        background-color: #222;
+        background-image: linear-gradient(to bottom,#161b22, #0d1117);
+        border-right: 1px solid rgba(0,0,0,.5);
+        border-bottom: 1px solid rgba(0,0,0,.5);
+        border-left: 5px solid rgba(255,255,255,.2);
+        box-shadow: 0 3px 8px 0 rgba(0,0,0,.25);
+      }
       li:hover {
-        background-image: linear-gradient(180deg,#fff,#f6fbfe);
-        border-left-color: #2196f3;
+        background-color: var(--q-primary);
+        background-image: linear-gradient(to bottom,#fff,rgba(255,255,255,.95));
+        border-left-color: var(--q-primary);
         cursor: pointer;
       }
+      :host-context(body.body--dark) li:hover {
+        background-image: linear-gradient(to bottom,#1e232a, #161b22);
+        border-left-color: var(--q-primary);
+      }
       li + li {
         margin-top: .5rem;
       }
       li a {
         display: block;
-        color: #1976d2;
+        color: var(--q-primary);
         padding: 1rem;
         text-decoration: none;
       }
+      .no-links {
+        color: var(--q-negative);
+        border: 1px dashed color-mix(in srgb, currentColor 50%, transparent);
+        border-radius: 5px;
+        padding: 1rem;
+      }
     `
   }
 
@@ -55,52 +71,108 @@ export class BlockIndexElement extends LitElement {
        * The base path to fetch pages from
        * @type {string}
        */
-      path: {type: String},
+      path: { type: String },
 
       /**
        * A comma-separated list of tags to filter with
        * @type {string}
        */
-      tags: {type: String},
+      tags: { type: String },
 
       /**
        * The maximum number of items to fetch
        * @type {number}
        */
-      limit: {type: Number}
+      limit: { type: Number },
+
+      /**
+       * Ordering (createdAt, fileName, title, updatedAt)
+       * @type {string}
+       */
+      orderBy: { type: String },
+
+      /**
+       * Ordering direction (asc, desc)
+       * @type {string}
+       */
+      orderByDirection: { type: String },
+
+      /**
+       * Maximum folder depth to fetch
+       * @type {number}
+       */
+      depth: { type: Number },
+
+      /**
+       * A fallback message if no results are returned
+       * @type {string}
+       */
+      noResultMsg: { type: String },
+
+      // Internal Properties
+      _loading: { state: true },
+      _pages: { state: true }
     }
   }
 
   constructor() {
     super()
-    this.pages = []
+    this._loading = true
+    this._pages = []
+    this.path = ''
+    this.tags = ''
+    this.limit = 10
+    this.orderBy = 'title'
+    this.orderByDirection = 'asc'
+    this.depth = 0
+    this.noResultMsg = 'No pages matching your query.'
   }
 
   async connectedCallback() {
     super.connectedCallback()
-    const resp = await APOLLO_CLIENT.query({
-      query: treeQuery,
-      variables: {
-        siteId: WIKI_STORES.site.id,
-        locale: 'en',
-        parentPath: ''
-      }
-    })
-    this.pages = resp.data.tree
-    this.requestUpdate()
+    try {
+      const resp = await APOLLO_CLIENT.query({
+        query: treeQuery,
+        variables: {
+          siteId: WIKI_STATE.site.id,
+          locale: WIKI_STATE.page.locale,
+          parentPath: this.path,
+          limit: this.limit,
+          orderBy: this.orderBy,
+          orderByDirection: this.orderByDirection,
+          depth: this.depth,
+          ...this.tags && { tags: this.tags.split(',').map(t => t.trim()).filter(t => t) },
+        }
+      })
+      this._pages = resp.data.tree.map(p => ({
+        ...p,
+        href: p.folderPath ? `/${p.folderPath}/${p.fileName}` : `/${p.fileName}`
+      }))
+    } catch (err) {
+      console.warn(err)
+    }
+    this._loading = false
   }
 
   render() {
-    return html`
+    return this._pages.length > 0 || this._loading ? html`
       <ul>
-        ${this.pages.map(p =>
-          html`<li><a href="#">${p.title}</a></li>`
+        ${this._pages.map(p =>
+          html`<li><a href="${p.href}" @click="${this._navigate}">${p.title}</a></li>`
         )}
       </ul>
       <slot></slot>
+    ` : html`
+      <div class="no-links">${this.noResultMsg}</div>
+      <slot></slot>
     `
   }
 
+  _navigate (e) {
+    e.preventDefault()
+    WIKI_ROUTER.push(e.target.getAttribute('href'))
+  }
+
   // createRenderRoot() {
   //   return this;
   // }

+ 0 - 14
blocks/block-index/folderByPath.graphql

@@ -1,14 +0,0 @@
-query blockIndexFetchPages (
-  $siteId: UUID!
-  $locale: String!
-  $parentPath: String!
-  ) {
-  tree(
-    siteId: $siteId,
-    locale: $locale,
-    parentPath: $parentPath
-    ) {
-    id
-    title
-  }
-}

+ 27 - 0
blocks/block-index/tree.graphql

@@ -0,0 +1,27 @@
+query blockIndexFetchPages (
+  $siteId: UUID!
+  $locale: String
+  $parentPath: String
+  $tags: [String]
+  $limit: Int
+  $orderBy: TreeOrderBy
+  $orderByDirection: OrderByDirection
+  $depth: Int
+  ) {
+  tree(
+    siteId: $siteId
+    locale: $locale
+    parentPath: $parentPath
+    tags: $tags
+    limit: $limit
+    types: [page]
+    orderBy: $orderBy
+    orderByDirection: $orderByDirection
+    depth: $depth
+    ) {
+    id
+    folderPath
+    fileName
+    title
+  }
+}

+ 4 - 3
server/db/migrations/3.0.0.mjs

@@ -244,8 +244,8 @@ export async function up (knex) {
       table.text('content')
       table.text('render')
       table.text('searchContent')
-      table.specificType('ts', 'tsvector').index('ts_idx', { indexType: 'GIN' })
-      table.specificType('tags', 'text[]').index('tags_idx', { indexType: 'GIN' })
+      table.specificType('ts', 'tsvector').index('pages_ts_idx', { indexType: 'GIN' })
+      table.specificType('tags', 'text[]').index('pages_tags_idx', { indexType: 'GIN' })
       table.jsonb('toc')
       table.string('editor').notNullable()
       table.string('contentType').notNullable()
@@ -303,7 +303,7 @@ export async function up (knex) {
     // TREE --------------------------------
     .createTable('tree', table => {
       table.uuid('id').notNullable().primary().defaultTo(knex.raw('gen_random_uuid()'))
-      table.specificType('folderPath', 'ltree').index().index('tree_folderpath_gist_index', { indexType: 'GIST' })
+      table.specificType('folderPath', 'ltree').index().index('tree_folderpath_gist_idx', { indexType: 'GIST' })
       table.string('fileName').notNullable().index()
       table.string('hash').notNullable().index()
       table.enu('type', ['folder', 'page', 'asset']).notNullable().index()
@@ -311,6 +311,7 @@ export async function up (knex) {
       table.string('title').notNullable()
       table.enum('navigationMode', ['inherit', 'override', 'overrideExact', 'hide', 'hideExact']).notNullable().defaultTo('inherit').index()
       table.uuid('navigationId').index()
+      table.specificType('tags', 'text[]').index('tree_tags_idx', { indexType: 'GIN' })
       table.jsonb('meta').notNullable().defaultTo('{}')
       table.timestamp('createdAt').notNullable().defaultTo(knex.fn.now())
       table.timestamp('updatedAt').notNullable().defaultTo(knex.fn.now())

+ 6 - 0
server/graph/resolvers/tree.mjs

@@ -80,6 +80,11 @@ export default {
               type: 'folder'
             })
           }
+
+          // -> Filter by tags
+          if (args.tags && args.tags.length > 0) {
+            builder.where('tags', '@>', args.tags)
+          }
         })
         .andWhere(builder => {
           // -> Limit to specific types
@@ -101,6 +106,7 @@ export default {
         folderPath: decodeTreePath(decodeFolderPath(item.folderPath)),
         fileName: item.fileName,
         title: item.title,
+        tags: item.tags ?? [],
         createdAt: item.createdAt,
         updatedAt: item.updatedAt,
         ...(item.type === 'folder') && {

+ 1 - 0
server/graph/schemas/tree.graphql

@@ -13,6 +13,7 @@ extend type Query {
     parentPath: String
     locale: String
     types: [TreeItemType]
+    tags: [String]
     limit: Int
     offset: Int
     orderBy: TreeOrderBy

+ 2 - 2
server/locales/en.json

@@ -110,8 +110,8 @@
   "admin.auth.vendor": "Vendor",
   "admin.auth.vendorWebsite": "Website",
   "admin.blocks.add": "Add Block",
-  "admin.blocks.builtin": "Built-in component",
-  "admin.blocks.custom": "Custom component",
+  "admin.blocks.builtin": "Built-in",
+  "admin.blocks.custom": "Custom",
   "admin.blocks.isEnabled": "Enabled",
   "admin.blocks.saveSuccess": "Blocks state saved successfully.",
   "admin.blocks.subtitle": "Manage dynamic components available for use inside pages.",

+ 2 - 0
server/models/pages.mjs

@@ -366,6 +366,7 @@ export class Page extends Model {
       fileName: last(pathParts),
       locale: page.locale,
       title: page.title,
+      tags,
       meta: {
         authorId: page.authorId,
         contentType: page.contentType,
@@ -649,6 +650,7 @@ export class Page extends Model {
     // -> Update tree
     await WIKI.db.knex('tree').where('id', page.id).update({
       title: page.title,
+      tags: page.tags,
       meta: {
         authorId: page.authorId,
         contentType: page.contentType,

+ 6 - 2
server/models/tree.mjs

@@ -120,9 +120,10 @@ export class Tree extends Model {
    * @param {string} args.title - Title of the page to add
    * @param {string} args.locale - Locale code of the page to add
    * @param {string} args.siteId - UUID of the site in which the page will be added
+   * @param {string[]} [args.tags] - Tags of the assets
    * @param {Object} [args.meta] - Extra metadata
    */
-  static async addPage ({ id, parentId, parentPath, fileName, title, locale, siteId, meta = {} }) {
+  static async addPage ({ id, parentId, parentPath, fileName, title, locale, siteId, tags = [], meta = {} }) {
     const folder = (parentId || parentPath) ? await WIKI.db.tree.getFolder({
       id: parentId,
       path: parentPath,
@@ -147,6 +148,7 @@ export class Tree extends Model {
       hash: generateHash(fullPath),
       locale: locale,
       siteId,
+      tags,
       meta,
       navigationId: siteId,
     }).returning('*')
@@ -164,9 +166,10 @@ export class Tree extends Model {
    * @param {string} args.title - Title of the asset to add
    * @param {string} args.locale - Locale code of the asset to add
    * @param {string} args.siteId - UUID of the site in which the asset will be added
+   * @param {string[]} [args.tags] - Tags of the assets
    * @param {Object} [args.meta] - Extra metadata
    */
-  static async addAsset ({ id, parentId, parentPath, fileName, title, locale, siteId, meta = {} }) {
+  static async addAsset ({ id, parentId, parentPath, fileName, title, locale, siteId, tags = [], meta = {} }) {
     const folder = (parentId || parentPath) ? await WIKI.db.tree.getFolder({
       id: parentId,
       path: parentPath,
@@ -191,6 +194,7 @@ export class Tree extends Model {
       hash: generateHash(fullPath),
       locale: locale,
       siteId,
+      tags,
       meta
     }).returning('*')
 

+ 15 - 13
ux/quasar.config.js

@@ -90,22 +90,24 @@ module.exports = configure(function (ctx) {
       distDir: '../assets',
 
       extendViteConf (viteConf) {
-        viteConf.build.assetsDir = '_assets'
-        viteConf.build.rollupOptions = {
-          ...viteConf.build.rollupOptions ?? {},
-          output: {
-            manualChunks: {
-              lodash: ['lodash-es', 'lodash'],
-              quasar: ['quasar', 'quasar/src/components']
+        if (ctx.prod) {
+          viteConf.build.assetsDir = '_assets'
+          viteConf.build.rollupOptions = {
+            ...viteConf.build.rollupOptions ?? {},
+            output: {
+              manualChunks: {
+                lodash: ['lodash-es', 'lodash'],
+                quasar: ['quasar', 'quasar/src/components']
+              }
             }
           }
+          viteConf.optimizeDeps.include = [
+            'prosemirror-state',
+            'prosemirror-transform',
+            'prosemirror-model',
+            'prosemirror-view'
+          ]
         }
-        viteConf.optimizeDeps.include = [
-          'prosemirror-state',
-          'prosemirror-transform',
-          'prosemirror-model',
-          'prosemirror-view'
-        ]
       },
       // viteVuePluginOptions: {},
 

+ 8 - 3
ux/src/boot/externals.js

@@ -1,17 +1,22 @@
 import { boot } from 'quasar/wrappers'
+import { usePageStore } from 'src/stores/page'
 import { useSiteStore } from 'src/stores/site'
 import { useUserStore } from 'src/stores/user'
 
-export default boot(() => {
+export default boot(({ router }) => {
   if (import.meta.env.SSR) {
-    global.WIKI_STORES = {
+    global.WIKI_STATE = {
+      page: usePageStore(),
       site: useSiteStore(),
       user: useUserStore()
     }
+    global.WIKI_ROUTER = router
   } else {
-    window.WIKI_STORES = {
+    window.WIKI_STATE = {
+      page: usePageStore(),
       site: useSiteStore(),
       user: useUserStore()
     }
+    window.WIKI_ROUTER = router
   }
 })

+ 4 - 2
ux/src/components/AccountMenu.vue

@@ -1,5 +1,5 @@
 <template lang='pug'>
-q-btn.q-ml-md(flat, round, dense, color='grey')
+q-btn.account-avbtn.q-ml-md(flat, round, dense, color='custom-color')
   q-icon(
     v-if='!userStore.authenticated || !userStore.hasAvatar'
     name='las la-user-circle'
@@ -49,5 +49,7 @@ const { t } = useI18n()
 </script>
 
 <style lang="scss">
-
+.account-avbtn {
+  color: rgba(255,255,255,.6);
+}
 </style>

+ 6 - 2
ux/src/components/HeaderSearch.vue

@@ -28,10 +28,10 @@ q-toolbar(
         )
       q-icon(v-else, name='las la-search')
     template(v-slot:append)
-      q-badge.q-mr-sm(
+      q-badge.search-kbdbadge.q-mr-sm(
         v-if='!state.searchIsFocused'
         label='Ctrl+K'
-        color='grey-7'
+        color='custom-color'
         outline
         @click='searchField.focus()'
         )
@@ -221,4 +221,8 @@ onBeforeUnmount(() => {
     border-radius: 4px;
   }
 }
+
+.search-kbdbadge {
+  color: rgba(255,255,255,.5);
+}
 </style>

+ 2 - 1
ux/src/components/NavEditOverlay.vue

@@ -242,7 +242,8 @@ q-layout(view='hHh lpR fFf', container)
                     color='primary'
                     )
                     q-menu(content-class='shadow-7')
-                      icon-picker-dialog(v-model='state.current.icon')
+                      .q-pa-lg: em [ TODO: Icon Picker Dialog ]
+                      // icon-picker-dialog(v-model='pageStore.icon')
           q-separator.q-my-sm(inset)
           q-item
             blueprint-icon(icon='link')

+ 5 - 1
ux/src/components/NavSidebar.vue

@@ -9,7 +9,7 @@ q-scroll-area.sidebar-nav(
     dark
     )
     template(v-for='item of siteStore.nav.items', :key='item.id')
-      q-item-label.text-blue-2.text-caption.text-wordbreak-all(
+      q-item-label.sidebar-nav-header.text-caption.text-wordbreak-all(
         v-if='item.type === `header`'
         header
         ) {{ item.label }}
@@ -175,5 +175,9 @@ watch(() => pageStore.navigationId, (newValue) => {
       border-left: 10px solid rgba(255,255,255,.25);
     }
   }
+
+  &-header {
+    color: rgba(255,255,255,.75) !important;
+  }
 }
 </style>

+ 2 - 1
ux/src/components/PageHeader.vue

@@ -13,7 +13,8 @@
       q-badge(color='grey' floating rounded)
         q-icon(name='las la-pen', size='xs', padding='xs xs')
       q-menu(content-class='shadow-7')
-        icon-picker-dialog(v-model='pageStore.icon')
+        .q-pa-lg: em [ TODO: Icon Picker Dialog ]
+        // icon-picker-dialog(v-model='pageStore.icon')
     q-icon.rounded-borders(
       v-else
       :name='pageStore.icon'

+ 4 - 1
ux/src/components/PagePropertiesDialog.vue

@@ -61,7 +61,10 @@ q-card.page-properties-dialog
             q-icon.cursor-pointer(
               name='las la-icons'
               color='primary'
-            )
+              )
+              q-menu(content-class='shadow-7')
+                .q-pa-lg: em [ TODO: Icon Picker Dialog ]
+                // icon-picker-dialog(v-model='pageStore.icon')
         q-input(
           v-if='pageStore.path !== `home`'
           v-model='pageStore.alias'

+ 14 - 3
ux/src/layouts/MainLayout.vue

@@ -57,7 +57,7 @@ q-layout(view='hHh Lpr lff')
           dense
           icon='las la-globe'
           color='blue-7'
-          text-color='blue-2'
+          text-color='custom-color'
           :label='commonStore.locale'
           :aria-label='commonStore.locale'
           size='sm'
@@ -69,13 +69,16 @@ q-layout(view='hHh Lpr lff')
           dense
           icon='las la-sitemap'
           color='blue-7'
-          text-color='blue-2'
+          text-color='custom-color'
           label='Browse'
           aria-label='Browse'
           size='sm'
           )
       nav-sidebar
-      q-bar.bg-blue-9.text-white(dense, v-if='userStore.authenticated')
+      q-bar.sidebar-footerbtns.text-white(
+        v-if='userStore.authenticated'
+        dense
+        )
         q-btn.col(
           icon='las la-dharmachakra'
           label='Edit Nav'
@@ -186,12 +189,20 @@ const isSidebarMini = computed(() => {
   background: linear-gradient(to bottom, rgba(255,255,255,.1) 0%, rgba(0,0,0, .05) 100%);
   border-bottom: 1px solid rgba(0,0,0,.2);
   height: 38px;
+
+  .q-btn {
+    color: rgba(255,255,255,.8);
+  }
 }
 
 .sidebar-mini {
   height: 100%;
 }
 
+.sidebar-footerbtns {
+  background-color: rgba(255,255,255,.1);
+}
+
 body.body--dark {
   background-color: $dark-6;
 }