feat: page changes detection + side overlay component loader

pull/6078/head
Nicolas Giard 2 years ago
parent f671d3b1ec
commit 274f3f4a0a
No known key found for this signature in database
GPG Key ID: 85061B8F9D55B7C8

@ -145,7 +145,7 @@ module.exports = {
async pageById (obj, args, context, info) {
let page = await WIKI.db.pages.getPageFromDb(args.id)
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,
locale: page.localeCode
})) {

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

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

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

@ -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>

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

@ -73,15 +73,35 @@ q-page.column
aria-label='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%;')
.col(style='order: 1;')
q-scroll-area(
@ -308,17 +328,25 @@ import { useRouter, useRoute } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { DateTime } from 'luxon'
import { useEditorStore } from 'src/stores/editor'
import { usePageStore } from 'src/stores/page'
import { useSiteStore } from 'src/stores/site'
// COMPONENTS
import SocialSharingMenu from '../components/SocialSharingMenu.vue'
import LoadingGeneric from 'src/components/LoadingGeneric.vue'
import PageTags from '../components/PageTags.vue'
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 = {
PageSaveDialog: defineAsyncComponent(() => import('../components/PageSaveDialog.vue'))
@ -330,6 +358,7 @@ const $q = useQuasar()
// STORES
const editorStore = useEditorStore()
const pageStore = usePageStore()
const siteStore = useSiteStore()
@ -448,7 +477,6 @@ function savePage () {
}
function refreshTocExpanded (baseToc, lvl) {
console.info(pageStore.tocDepth.min, lvl, pageStore.tocDepth.max)
const toExpand = []
let isRootNode = false
if (!baseToc) {
@ -472,6 +500,27 @@ function refreshTocExpanded (baseToc, lvl) {
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>
<style lang="scss">

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

@ -1,8 +1,51 @@
import { defineStore } from 'pinia'
import gql from 'graphql-tag'
import { cloneDeep, last, transform } from 'lodash-es'
import { DateTime } from 'luxon'
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', {
state: () => ({
@ -39,101 +82,50 @@ export const usePageStore = defineStore('page', {
min: 1,
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,
content: '',
render: '',
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: {
/**
* PAGE - LOAD
*/
async pageLoad ({ path, id }) {
const editorStore = useEditorStore()
const siteStore = useSiteStore()
try {
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'
})
const pageData = cloneDeep(resp?.data?.pageByPath ?? {})
const pageData = cloneDeep((id ? resp?.data?.pageById : resp?.data?.pageByPath) ?? {})
if (!pageData?.id) {
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) {
console.warn(err)

Loading…
Cancel
Save