feat: page changes detection + side overlay component loader

parent f671d3b1
...@@ -145,7 +145,7 @@ module.exports = { ...@@ -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
})) { })) {
......
...@@ -31,7 +31,7 @@ extend type Query { ...@@ -31,7 +31,7 @@ extend type Query {
): [PageListItem!]! ): [PageListItem!]!
pageById( pageById(
id: Int! id: UUID!
): Page ): Page
pageByPath( pageByPath(
......
...@@ -2,10 +2,12 @@ import { boot } from 'quasar/wrappers' ...@@ -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)
}) })
...@@ -66,6 +66,11 @@ const isCopyright = computed(() => { ...@@ -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;
......
<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>
...@@ -273,11 +273,13 @@ q-card.page-properties-dialog ...@@ -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() ...@@ -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) => { ...@@ -335,6 +338,12 @@ watch(() => state.requirePassword, (newValue) => {
} }
}) })
pageStore.$subscribe(() => {
editorStore.$patch({
lastChangeTimestamp: DateTime.utc()
})
})
// METHODS // METHODS
function editScripts (mode) { function editScripts (mode) {
......
...@@ -73,15 +73,35 @@ q-page.column ...@@ -73,15 +73,35 @@ q-page.column
aria-label='Print' aria-label='Print'
) )
q-tooltip Print q-tooltip Print
q-btn.acrylic-btn( template(v-if='editorStore.hasPendingChanges')
flat q-btn.acrylic-btn.q-mr-sm(
icon='las la-edit' flat
color='deep-orange-9' icon='las la-times'
label='Edit' color='negative'
aria-label='Edit' label='Discard'
no-caps aria-label='Discard'
:href='editUrl' 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' ...@@ -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')), PageDataDialog: defineAsyncComponent({
PagePropertiesDialog: defineAsyncComponent(() => import('../components/PagePropertiesDialog.vue')) 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() ...@@ -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 () { ...@@ -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) { ...@@ -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">
......
...@@ -13,8 +13,14 @@ export const useEditorStore = defineStore('editor', { ...@@ -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: {}
}) })
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', { ...@@ -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: id ? gqlQueries.pageById : gqlQueries.pageByPath,
query loadPage ( variables: id ? { id } : { siteId: siteStore.id, path },
$siteId: UUID!
$path: String!
) {
pageByPath(
siteId: $siteId
path: $path
) {
id
title
description
path
locale
updatedAt
render
toc
}
}
`,
variables: {
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}` : '' // Update page store
this.$patch({ this.$patch(pageData)
...pageData, // Update editor state timestamps
breadcrumbs: transform(pageData.path.split('/'), (result, value, key) => { const curDate = DateTime.utc()
result.push({ editorStore.$patch({
id: key, lastChangeTimestamp: curDate,
title: value, lastSaveTimestamp: curDate
icon: 'las la-file-alt',
locale: 'en',
path: (last(result)?.path || pathPrefix) + `/${value}`
})
}, [])
}) })
} catch (err) { } catch (err) {
console.warn(err) console.warn(err)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment