فهرست منبع

feat: page changes detection + side overlay component loader

Nicolas Giard 2 سال پیش
والد
کامیت
274f3f4a0a

+ 1 - 1
server/graph/resolvers/page.js

@@ -145,7 +145,7 @@ module.exports = {
     async pageById (obj, args, context, info) {
     async pageById (obj, args, context, info) {
       let page = await WIKI.db.pages.getPageFromDb(args.id)
       let page = await WIKI.db.pages.getPageFromDb(args.id)
       if (page) {
       if (page) {
-        if (WIKI.auth.checkAccess(context.req.user, ['manage:pages', 'delete:pages'], {
+        if (WIKI.auth.checkAccess(context.req.user, ['read:pages'], {
           path: page.path,
           path: page.path,
           locale: page.localeCode
           locale: page.localeCode
         })) {
         })) {

+ 1 - 1
server/graph/schemas/page.graphql

@@ -31,7 +31,7 @@ extend type Query {
   ): [PageListItem!]!
   ): [PageListItem!]!
 
 
   pageById(
   pageById(
-    id: Int!
+    id: UUID!
   ): Page
   ): Page
 
 
   pageByPath(
   pageByPath(

+ 2 - 0
ux/src/boot/components.js

@@ -2,10 +2,12 @@ import { boot } from 'quasar/wrappers'
 
 
 import BlueprintIcon from '../components/BlueprintIcon.vue'
 import BlueprintIcon from '../components/BlueprintIcon.vue'
 import StatusLight from '../components/StatusLight.vue'
 import StatusLight from '../components/StatusLight.vue'
+import LoadingGeneric from '../components/LoadingGeneric.vue'
 import VNetworkGraph from 'v-network-graph'
 import VNetworkGraph from 'v-network-graph'
 
 
 export default boot(({ app }) => {
 export default boot(({ app }) => {
   app.component('BlueprintIcon', BlueprintIcon)
   app.component('BlueprintIcon', BlueprintIcon)
+  app.component('LoadingGeneric', LoadingGeneric)
   app.component('StatusLight', StatusLight)
   app.component('StatusLight', StatusLight)
   app.use(VNetworkGraph)
   app.use(VNetworkGraph)
 })
 })

+ 5 - 0
ux/src/components/FooterNav.vue

@@ -66,6 +66,11 @@ const isCopyright = computed(() => {
   padding: 4px 12px;
   padding: 4px 12px;
   font-size: 11px;
   font-size: 11px;
 
 
+  @at-root .body--dark & {
+    background-color: $dark-4;
+    color: rgba(255,255,255,.4);
+  }
+
   &-line {
   &-line {
     text-align: center;
     text-align: center;
 
 

+ 39 - 0
ux/src/components/LoadingGeneric.vue

@@ -0,0 +1,39 @@
+<template lang="pug">
+.loader-generic
+  div
+</template>
+
+<style lang="scss">
+.loader-generic {
+  box-shadow: none !important;
+  padding-top: 64px;
+
+  > div {
+    background-color: rgba(0,0,0,.75);
+    width: 64px;
+    height: 64px;
+    border-radius: 5px !important;
+    position: relative;
+
+    &:before {
+      content: '';
+      box-sizing: border-box;
+      position: absolute;
+      top: 50%;
+      left: 50%;
+      width: 24px;
+      height: 24px;
+      margin-top: -12px;
+      margin-left: -12px;
+      border-radius: 50%;
+      border-top: 2px solid #FFF;
+      border-right: 2px solid transparent;
+      animation: loadergenericspinner .6s linear infinite;
+    }
+  }
+}
+
+@keyframes loadergenericspinner {
+  to { transform: rotate(360deg); }
+}
+</style>

+ 9 - 0
ux/src/components/PagePropertiesDialog.vue

@@ -273,11 +273,13 @@ q-card.page-properties-dialog
 import { useI18n } from 'vue-i18n'
 import { useI18n } from 'vue-i18n'
 import { useQuasar } from 'quasar'
 import { useQuasar } from 'quasar'
 import { nextTick, onMounted, reactive, ref, watch } from 'vue'
 import { nextTick, onMounted, reactive, ref, watch } from 'vue'
+import { DateTime } from 'luxon'
 
 
 import PageRelationDialog from './PageRelationDialog.vue'
 import PageRelationDialog from './PageRelationDialog.vue'
 import PageScriptsDialog from './PageScriptsDialog.vue'
 import PageScriptsDialog from './PageScriptsDialog.vue'
 import PageTags from './PageTags.vue'
 import PageTags from './PageTags.vue'
 
 
+import { useEditorStore } from 'src/stores/editor'
 import { usePageStore } from 'src/stores/page'
 import { usePageStore } from 'src/stores/page'
 import { useSiteStore } from 'src/stores/site'
 import { useSiteStore } from 'src/stores/site'
 
 
@@ -287,6 +289,7 @@ const $q = useQuasar()
 
 
 // STORES
 // STORES
 
 
+const editorStore = useEditorStore()
 const pageStore = usePageStore()
 const pageStore = usePageStore()
 const siteStore = useSiteStore()
 const siteStore = useSiteStore()
 
 
@@ -335,6 +338,12 @@ watch(() => state.requirePassword, (newValue) => {
   }
   }
 })
 })
 
 
+pageStore.$subscribe(() => {
+  editorStore.$patch({
+    lastChangeTimestamp: DateTime.utc()
+  })
+})
+
 // METHODS
 // METHODS
 
 
 function editScripts (mode) {
 function editScripts (mode) {

+ 61 - 12
ux/src/pages/Index.vue

@@ -73,15 +73,35 @@ q-page.column
         aria-label='Print'
         aria-label='Print'
         )
         )
         q-tooltip Print
         q-tooltip Print
-      q-btn.acrylic-btn(
-        flat
-        icon='las la-edit'
-        color='deep-orange-9'
-        label='Edit'
-        aria-label='Edit'
-        no-caps
-        :href='editUrl'
-      )
+      template(v-if='editorStore.hasPendingChanges')
+        q-btn.acrylic-btn.q-mr-sm(
+          flat
+          icon='las la-times'
+          color='negative'
+          label='Discard'
+          aria-label='Discard'
+          no-caps
+          @click='discardChanges'
+        )
+        q-btn.acrylic-btn(
+          flat
+          icon='las la-check'
+          color='positive'
+          label='Save Changes'
+          aria-label='Save Changes'
+          no-caps
+          @click='saveChanges'
+        )
+      template(v-else)
+        q-btn.acrylic-btn(
+          flat
+          icon='las la-edit'
+          color='deep-orange-9'
+          label='Edit'
+          aria-label='Edit'
+          no-caps
+          :href='editUrl'
+        )
   .page-container.row.no-wrap.items-stretch(style='flex: 1 1 100%;')
   .page-container.row.no-wrap.items-stretch(style='flex: 1 1 100%;')
     .col(style='order: 1;')
     .col(style='order: 1;')
       q-scroll-area(
       q-scroll-area(
@@ -308,17 +328,25 @@ import { useRouter, useRoute } from 'vue-router'
 import { useI18n } from 'vue-i18n'
 import { useI18n } from 'vue-i18n'
 import { DateTime } from 'luxon'
 import { DateTime } from 'luxon'
 
 
+import { useEditorStore } from 'src/stores/editor'
 import { usePageStore } from 'src/stores/page'
 import { usePageStore } from 'src/stores/page'
 import { useSiteStore } from 'src/stores/site'
 import { useSiteStore } from 'src/stores/site'
 
 
 // COMPONENTS
 // COMPONENTS
 
 
 import SocialSharingMenu from '../components/SocialSharingMenu.vue'
 import SocialSharingMenu from '../components/SocialSharingMenu.vue'
+import LoadingGeneric from 'src/components/LoadingGeneric.vue'
 import PageTags from '../components/PageTags.vue'
 import PageTags from '../components/PageTags.vue'
 
 
 const sideDialogs = {
 const sideDialogs = {
-  PageDataDialog: defineAsyncComponent(() => import('../components/PageDataDialog.vue')),
-  PagePropertiesDialog: defineAsyncComponent(() => import('../components/PagePropertiesDialog.vue'))
+  PageDataDialog: defineAsyncComponent({
+    loader: () => import('../components/PageDataDialog.vue'),
+    loadingComponent: LoadingGeneric
+  }),
+  PagePropertiesDialog: defineAsyncComponent({
+    loader: () => import('../components/PagePropertiesDialog.vue'),
+    loadingComponent: LoadingGeneric
+  })
 }
 }
 const globalDialogs = {
 const globalDialogs = {
   PageSaveDialog: defineAsyncComponent(() => import('../components/PageSaveDialog.vue'))
   PageSaveDialog: defineAsyncComponent(() => import('../components/PageSaveDialog.vue'))
@@ -330,6 +358,7 @@ const $q = useQuasar()
 
 
 // STORES
 // STORES
 
 
+const editorStore = useEditorStore()
 const pageStore = usePageStore()
 const pageStore = usePageStore()
 const siteStore = useSiteStore()
 const siteStore = useSiteStore()
 
 
@@ -448,7 +477,6 @@ function savePage () {
 }
 }
 
 
 function refreshTocExpanded (baseToc, lvl) {
 function refreshTocExpanded (baseToc, lvl) {
-  console.info(pageStore.tocDepth.min, lvl, pageStore.tocDepth.max)
   const toExpand = []
   const toExpand = []
   let isRootNode = false
   let isRootNode = false
   if (!baseToc) {
   if (!baseToc) {
@@ -472,6 +500,27 @@ function refreshTocExpanded (baseToc, lvl) {
     return toExpand
     return toExpand
   }
   }
 }
 }
+
+async function discardChanges () {
+  $q.loading.show()
+  try {
+    await pageStore.pageLoad({ id: pageStore.id })
+    $q.notify({
+      type: 'positive',
+      message: 'Page has been reverted to the last saved state.'
+    })
+  } catch (err) {
+    $q.notify({
+      type: 'negative',
+      message: 'Failed to reload page state.'
+    })
+  }
+  $q.loading.hide()
+}
+
+async function saveChanges () {
+
+}
 </script>
 </script>
 
 
 <style lang="scss">
 <style lang="scss">

+ 7 - 1
ux/src/stores/editor.js

@@ -13,8 +13,14 @@ export const useEditorStore = defineStore('editor', {
       currentFileId: null
       currentFileId: null
     },
     },
     checkoutDateActive: '',
     checkoutDateActive: '',
+    lastSaveTimestamp: null,
+    lastChangeTimestamp: null,
     editors: {}
     editors: {}
   }),
   }),
-  getters: {},
+  getters: {
+    hasPendingChanges: (state) => {
+      return state.lastSaveTimestamp && state.lastSaveTimestamp !== state.lastChangeTimestamp
+    }
+  },
   actions: {}
   actions: {}
 })
 })

+ 69 - 77
ux/src/stores/page.js

@@ -1,8 +1,51 @@
 import { defineStore } from 'pinia'
 import { defineStore } from 'pinia'
 import gql from 'graphql-tag'
 import gql from 'graphql-tag'
 import { cloneDeep, last, transform } from 'lodash-es'
 import { cloneDeep, last, transform } from 'lodash-es'
+import { DateTime } from 'luxon'
 
 
 import { useSiteStore } from './site'
 import { useSiteStore } from './site'
+import { useEditorStore } from './editor'
+
+const gqlQueries = {
+  pageById: gql`
+    query loadPage (
+      $id: UUID!
+    ) {
+      pageById(
+        id: $id
+      ) {
+        id
+        title
+        description
+        path
+        locale
+        updatedAt
+        render
+        toc
+      }
+    }
+  `,
+  pageByPath: gql`
+    query loadPage (
+      $siteId: UUID!
+      $path: String!
+    ) {
+      pageByPath(
+        siteId: $siteId
+        path: $path
+      ) {
+        id
+        title
+        description
+        path
+        locale
+        updatedAt
+        render
+        toc
+      }
+    }
+  `
+}
 
 
 export const usePageStore = defineStore('page', {
 export const usePageStore = defineStore('page', {
   state: () => ({
   state: () => ({
@@ -39,101 +82,50 @@ export const usePageStore = defineStore('page', {
       min: 1,
       min: 1,
       max: 2
       max: 2
     },
     },
-    breadcrumbs: [
-      // {
-      //   id: 1,
-      //   title: 'Installation',
-      //   icon: 'las la-file-alt',
-      //   locale: 'en',
-      //   path: 'installation'
-      // },
-      // {
-      //   id: 2,
-      //   title: 'Ubuntu',
-      //   icon: 'lab la-ubuntu',
-      //   locale: 'en',
-      //   path: 'installation/ubuntu'
-      // }
-    ],
-    effectivePermissions: {
-      comments: {
-        read: false,
-        write: false,
-        manage: false
-      },
-      history: {
-        read: false
-      },
-      source: {
-        read: false
-      },
-      pages: {
-        write: false,
-        manage: false,
-        delete: false,
-        script: false,
-        style: false
-      },
-      system: {
-        manage: false
-      }
-    },
     commentsCount: 0,
     commentsCount: 0,
     content: '',
     content: '',
     render: '',
     render: '',
     toc: []
     toc: []
   }),
   }),
-  getters: {},
+  getters: {
+    breadcrumbs: (state) => {
+      const siteStore = useSiteStore()
+      const pathPrefix = siteStore.useLocales ? `/${state.locale}` : ''
+      return transform(state.path.split('/'), (result, value, key) => {
+        result.push({
+          id: key,
+          title: value,
+          icon: 'las la-file-alt',
+          locale: 'en',
+          path: (last(result)?.path || pathPrefix) + `/${value}`
+        })
+      }, [])
+    }
+  },
   actions: {
   actions: {
     /**
     /**
      * PAGE - LOAD
      * PAGE - LOAD
      */
      */
     async pageLoad ({ path, id }) {
     async pageLoad ({ path, id }) {
+      const editorStore = useEditorStore()
       const siteStore = useSiteStore()
       const siteStore = useSiteStore()
       try {
       try {
         const resp = await APOLLO_CLIENT.query({
         const resp = await APOLLO_CLIENT.query({
-          query: gql`
-            query loadPage (
-              $siteId: UUID!
-              $path: String!
-            ) {
-              pageByPath(
-                siteId: $siteId
-                path: $path
-              ) {
-                id
-                title
-                description
-                path
-                locale
-                updatedAt
-                render
-                toc
-              }
-            }
-          `,
-          variables: {
-            siteId: siteStore.id,
-            path
-          },
+          query: id ? gqlQueries.pageById : gqlQueries.pageByPath,
+          variables: id ? { id } : { siteId: siteStore.id, path },
           fetchPolicy: 'network-only'
           fetchPolicy: 'network-only'
         })
         })
-        const pageData = cloneDeep(resp?.data?.pageByPath ?? {})
+        const pageData = cloneDeep((id ? resp?.data?.pageById : resp?.data?.pageByPath) ?? {})
         if (!pageData?.id) {
         if (!pageData?.id) {
           throw new Error('ERR_PAGE_NOT_FOUND')
           throw new Error('ERR_PAGE_NOT_FOUND')
         }
         }
-        const pathPrefix = siteStore.useLocales ? `/${pageData.locale}` : ''
-        this.$patch({
-          ...pageData,
-          breadcrumbs: transform(pageData.path.split('/'), (result, value, key) => {
-            result.push({
-              id: key,
-              title: value,
-              icon: 'las la-file-alt',
-              locale: 'en',
-              path: (last(result)?.path || pathPrefix) + `/${value}`
-            })
-          }, [])
+        // Update page store
+        this.$patch(pageData)
+        // Update editor state timestamps
+        const curDate = DateTime.utc()
+        editorStore.$patch({
+          lastChangeTimestamp: curDate,
+          lastSaveTimestamp: curDate
         })
         })
       } catch (err) {
       } catch (err) {
         console.warn(err)
         console.warn(err)