feat: page TOC + refactor PageDataDialog to composition API

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

@ -153,6 +153,11 @@ router.get(['/d', '/d/*'], async (req, res, next) => {
*/ */
router.get(['/_edit', '/_edit/*'], async (req, res, next) => { router.get(['/_edit', '/_edit/*'], async (req, res, next) => {
const pageArgs = pageHelper.parsePath(req.path, { stripExt: true }) const pageArgs = pageHelper.parsePath(req.path, { stripExt: true })
const site = await WIKI.db.sites.getSiteByHostname({ hostname: req.hostname })
if (!site) {
throw new Error('INVALID_SITE')
}
if (pageArgs.path === '') { if (pageArgs.path === '') {
return res.redirect(`/_edit/home`) return res.redirect(`/_edit/home`)
@ -175,10 +180,10 @@ router.get(['/_edit', '/_edit/*'], async (req, res, next) => {
// -> Get page data from DB // -> Get page data from DB
let page = await WIKI.db.pages.getPageFromDb({ let page = await WIKI.db.pages.getPageFromDb({
siteId: site.id,
path: pageArgs.path, path: pageArgs.path,
locale: pageArgs.locale, locale: pageArgs.locale,
userId: req.user.id, userId: req.user.id
isPrivate: false
}) })
pageArgs.tags = _.get(page, 'tags', []) pageArgs.tags = _.get(page, 'tags', [])
@ -415,6 +420,11 @@ router.get('/*', async (req, res, next) => {
const stripExt = _.some(WIKI.data.pageExtensions, ext => _.endsWith(req.path, `.${ext}`)) const stripExt = _.some(WIKI.data.pageExtensions, ext => _.endsWith(req.path, `.${ext}`))
const pageArgs = pageHelper.parsePath(req.path, { stripExt }) const pageArgs = pageHelper.parsePath(req.path, { stripExt })
const isPage = (stripExt || pageArgs.path.indexOf('.') === -1) const isPage = (stripExt || pageArgs.path.indexOf('.') === -1)
const site = await WIKI.db.sites.getSiteByHostname({ hostname: req.hostname })
if (!site) {
throw new Error('INVALID_SITE')
}
if (isPage) { if (isPage) {
// if (WIKI.config.lang.namespacing && !pageArgs.explicitLocale) { // if (WIKI.config.lang.namespacing && !pageArgs.explicitLocale) {
@ -426,6 +436,7 @@ router.get('/*', async (req, res, next) => {
try { try {
// -> Get Page from cache // -> Get Page from cache
const page = await WIKI.db.pages.getPage({ const page = await WIKI.db.pages.getPage({
siteId: site.id,
path: pageArgs.path, path: pageArgs.path,
locale: pageArgs.locale, locale: pageArgs.locale,
userId: req.user.id userId: req.user.id
@ -470,67 +481,8 @@ router.get('/*', async (req, res, next) => {
}) })
} }
// -> Build sidebar navigation
let sdi = 1
const sidebar = (await WIKI.db.navigation.getTree({ cache: true, locale: pageArgs.locale, groups: req.user.groups })).map(n => ({
i: `sdi-${sdi++}`,
k: n.kind,
l: n.label,
c: n.icon,
y: n.targetType,
t: n.target
}))
// -> Build theme code injection
const injectCode = {
css: '', // WIKI.config.theming.injectCSS,
head: '', // WIKI.config.theming.injectHead,
body: '' // WIKI.config.theming.injectBody
}
// Handle missing extra field
page.extra = page.extra || { css: '', js: '' }
if (!_.isEmpty(page.extra.css)) {
injectCode.css = `${injectCode.css}\n${page.extra.css}`
}
if (!_.isEmpty(page.extra.js)) {
injectCode.body = `${injectCode.body}\n${page.extra.js}`
}
// -> Convert page TOC
if (!_.isString(page.toc)) {
page.toc = JSON.stringify(page.toc)
}
// -> Inject comments variables
const commentTmpl = {
codeTemplate: '', // WIKI.data.commentProvider.codeTemplate,
head: '', // WIKI.data.commentProvider.head,
body: '', // WIKI.data.commentProvider.body,
main: '' // WIKI.data.commentProvider.main
}
if (false && WIKI.config.features.featurePageComments && WIKI.data.commentProvider.codeTemplate) {
[
{ key: 'pageUrl', value: `${WIKI.config.host}/i/${page.id}` },
{ key: 'pageId', value: page.id }
].forEach((cfg) => {
commentTmpl.head = _.replace(commentTmpl.head, new RegExp(`{{${cfg.key}}}`, 'g'), cfg.value)
commentTmpl.body = _.replace(commentTmpl.body, new RegExp(`{{${cfg.key}}}`, 'g'), cfg.value)
commentTmpl.main = _.replace(commentTmpl.main, new RegExp(`{{${cfg.key}}}`, 'g'), cfg.value)
})
}
// -> Render view // -> Render view
res.sendFile(path.join(WIKI.ROOTPATH, 'assets/index.html')) res.sendFile(path.join(WIKI.ROOTPATH, 'assets/index.html'))
// res.render('page', {
// page,
// sidebar,
// injectCode,
// comments: commentTmpl,
// effectivePermissions
// })
} else if (pageArgs.path === 'home') { } else if (pageArgs.path === 'home') {
res.redirect('/_welcome') res.redirect('/_welcome')
} else { } else {

@ -166,7 +166,10 @@ module.exports = {
*/ */
async pageByPath (obj, args, context, info) { async pageByPath (obj, args, context, info) {
const pageArgs = pageHelper.parsePath(args.path) const pageArgs = pageHelper.parsePath(args.path)
let page = await WIKI.db.pages.getPageFromDb(pageArgs) let page = await WIKI.db.pages.getPageFromDb({
...pageArgs,
siteId: args.siteId
})
if (page) { if (page) {
return { return {
...page, ...page,

@ -35,6 +35,7 @@ extend type Query {
): Page ): Page
pageByPath( pageByPath(
siteId: UUID!
path: String! path: String!
): Page ): Page
@ -173,7 +174,7 @@ type Page {
tags: [PageTag] tags: [PageTag]
content: String content: String
render: String render: String
toc: String toc: [JSON]
contentType: String contentType: String
createdAt: Date createdAt: Date
updatedAt: Date updatedAt: Date

@ -1010,6 +1010,7 @@ module.exports = class Page extends Model {
.where(queryModeID ? { .where(queryModeID ? {
'pages.id': opts 'pages.id': opts
} : { } : {
'pages.siteId': opts.siteId,
'pages.path': opts.path, 'pages.path': opts.path,
'pages.localeCode': opts.locale 'pages.localeCode': opts.locale
}) })

@ -62,8 +62,8 @@ module.exports = async ({ payload }) => {
$('.toc-anchor', el).remove() $('.toc-anchor', el).remove()
_.get(toc, leafPath).push({ _.get(toc, leafPath).push({
title: _.trim($(el).text()), label: _.trim($(el).text()),
anchor: leafSlug, key: leafSlug.substring(1),
children: [] children: []
}) })
}) })

@ -1,7 +1,7 @@
<template lang="pug"> <template lang="pug">
q-card.page-data-dialog(style='width: 750px;') q-card.page-data-dialog(style='width: 750px;')
q-toolbar.bg-primary.text-white.flex q-toolbar.bg-primary.text-white.flex
.text-subtitle2 {{$t('editor.pageData.title')}} .text-subtitle2 {{t('editor.pageData.title')}}
q-space q-space
q-btn( q-btn(
icon='las la-times' icon='las la-times'
@ -10,13 +10,13 @@ q-card.page-data-dialog(style='width: 750px;')
v-close-popup v-close-popup
) )
q-card-section.page-data-dialog-selector q-card-section.page-data-dialog-selector
//- .text-overline.text-white {{$t('editor.pageData.template')}} //- .text-overline.text-white {{t('editor.pageData.template')}}
.flex.q-gutter-sm .flex.q-gutter-sm
q-select( q-select(
dark dark
v-model='templateId' v-model='state.templateId'
:label='$t(`editor.pageData.template`)' :label='t(`editor.pageData.template`)'
:aria-label='$t(`editor.pageData.template`)' :aria-label='t(`editor.pageData.template`)'
:options='templates' :options='templates'
option-value='id' option-value='id'
map-options map-options
@ -28,14 +28,14 @@ q-card.page-data-dialog(style='width: 750px;')
q-btn.acrylic-btn( q-btn.acrylic-btn(
dark dark
icon='las la-pen' icon='las la-pen'
:label='$t(`common.actions.manage`)' :label='t(`common.actions.manage`)'
unelevated unelevated
no-caps no-caps
color='deep-orange-9' color='deep-orange-9'
@click='editTemplates' @click='editTemplates'
) )
q-tabs.alt-card( q-tabs.alt-card(
v-model='mode' v-model='state.mode'
inline-label inline-label
no-caps no-caps
) )
@ -48,11 +48,11 @@ q-card.page-data-dialog(style='width: 750px;')
label='YAML' label='YAML'
) )
q-scroll-area( q-scroll-area(
:thumb-style='thumbStyle' :thumb-style='siteStore.thumbStyle'
:bar-style='barStyle' :bar-style='siteStore.barStyle'
style='height: calc(100% - 50px - 75px - 48px);' style='height: calc(100% - 50px - 75px - 48px);'
) )
q-card-section(v-if='mode === `visual`') q-card-section(v-if='state.mode === `visual`')
.q-gutter-sm .q-gutter-sm
q-input( q-input(
label='Attribute Text' label='Attribute Text'
@ -76,60 +76,69 @@ q-card.page-data-dialog(style='width: 750px;')
dense dense
size='lg' size='lg'
) )
q-no-ssr(v-else, :placeholder='$t(`common.loading`)') q-no-ssr(v-else, :placeholder='t(`common.loading`)')
codemirror.admin-theme-cm( codemirror.admin-theme-cm(
ref='cmData' ref='cmData'
v-model='content' v-model='state.content'
:options='{ mode: `text/yaml` }' :options='{ mode: `text/yaml` }'
) )
q-dialog( q-dialog(
v-model='showDataTemplateDialog' v-model='state.showDataTemplateDialog'
) )
page-data-template-dialog page-data-template-dialog
</template> </template>
<script> <script setup>
import { get } from 'vuex-pathify' import { useI18n } from 'vue-i18n'
import { useQuasar } from 'quasar'
import { nextTick, onMounted, reactive, ref, watch } from 'vue'
import PageDataTemplateDialog from './PageDataTemplateDialog.vue' import PageDataTemplateDialog from './PageDataTemplateDialog.vue'
export default { import { usePageStore } from 'src/stores/page'
components: { import { useSiteStore } from 'src/stores/site'
PageDataTemplateDialog
}, // QUASAR
data () {
return { const $q = useQuasar()
showDataTemplateDialog: false,
templateId: '', // STORES
content: '',
mode: 'visual' const pageStore = usePageStore()
} const siteStore = useSiteStore()
},
computed: { // I18N
thumbStyle: get('site/thumbStyle', false),
barStyle: get('site/barStyle', false), const { t } = useI18n()
templates () {
return [ // DATA
{
id: '', const state = reactive({
label: 'None', showDataTemplateDialog: false,
data: [] templateId: '',
}, content: '',
...this.$store.get('site/pageDataTemplates'), mode: 'visual'
{ })
id: 'basic',
label: 'Basic', const templates = [
data: [] {
} id: '',
] label: 'None',
} data: []
}, },
methods: { ...siteStore.pageDataTemplates,
editTemplates () { {
this.showDataTemplateDialog = !this.showDataTemplateDialog id: 'basic',
} label: 'Basic',
data: []
} }
]
// METHODS
function editTemplates () {
state.showDataTemplateDialog = !state.showDataTemplateDialog
} }
</script> </script>

@ -1,7 +1,7 @@
<template lang="pug"> <template lang="pug">
q-card.page-datatmpl-dialog(style='width: 1100px; max-width: 1100px;') q-card.page-datatmpl-dialog(style='width: 1100px; max-width: 1100px;')
q-toolbar.bg-primary.text-white q-toolbar.bg-primary.text-white
.text-subtitle2 {{$t('editor.pageData.manageTemplates')}} .text-subtitle2 {{t('editor.pageData.manageTemplates')}}
q-space q-space
q-btn( q-btn(
icon='las la-times' icon='las la-times'
@ -12,10 +12,10 @@ q-card.page-datatmpl-dialog(style='width: 1100px; max-width: 1100px;')
q-card-section.page-datatmpl-selector q-card-section.page-datatmpl-selector
.flex.q-gutter-md .flex.q-gutter-md
q-select.col( q-select.col(
v-model='selectedTemplateId' v-model='state.selectedTemplateId'
:options='templates' :options='siteStore.pageDataTemplates'
standout standout
:label='$t(`editor.pageData.template`)' :label='t(`editor.pageData.template`)'
dense dense
dark dark
option-value='id' option-value='id'
@ -24,23 +24,23 @@ q-card.page-datatmpl-dialog(style='width: 1100px; max-width: 1100px;')
) )
q-btn( q-btn(
icon='las la-plus' icon='las la-plus'
:label='$t(`common.actions.new`)' :label='t(`common.actions.new`)'
unelevated unelevated
color='primary' color='primary'
no-caps no-caps
@click='create' @click='create'
) )
.row(v-if='tmpl') .row(v-if='state.tmpl')
.col-auto.page-datatmpl-sd .col-auto.page-datatmpl-sd
.q-pa-md .q-pa-md
q-btn.acrylic-btn.full-width( q-btn.acrylic-btn.full-width(
:label='$t(`common.actions.howItWorks`)' :label='t(`common.actions.howItWorks`)'
icon='las la-question-circle' icon='las la-question-circle'
flat flat
color='pink' color='pink'
no-caps no-caps
) )
q-item-label(header, style='margin-top: 2px;') {{$t('editor.pageData.templateFullRowTypes')}} q-item-label(header, style='margin-top: 2px;') {{t('editor.pageData.templateFullRowTypes')}}
.q-px-md .q-px-md
draggable( draggable(
class='q-list rounded-borders' class='q-list rounded-borders'
@ -49,8 +49,8 @@ q-card.page-datatmpl-dialog(style='width: 1100px; max-width: 1100px;')
:clone='cloneFieldType' :clone='cloneFieldType'
:sort='false' :sort='false'
:animation='150' :animation='150'
@start='dragStarted = true' @start='state.dragStarted = true'
@end='dragStarted = false' @end='state.dragStarted = false'
item-key='id' item-key='id'
) )
template(#item='{element}') template(#item='{element}')
@ -59,7 +59,7 @@ q-card.page-datatmpl-dialog(style='width: 1100px; max-width: 1100px;')
q-icon(:name='element.icon', color='primary') q-icon(:name='element.icon', color='primary')
q-item-section q-item-section
q-item-label {{element.label}} q-item-label {{element.label}}
q-item-label(header) {{$t('editor.pageData.templateKeyValueTypes')}} q-item-label(header) {{t('editor.pageData.templateKeyValueTypes')}}
.q-px-md.q-pb-md .q-px-md.q-pb-md
draggable( draggable(
class='q-list rounded-borders' class='q-list rounded-borders'
@ -68,8 +68,8 @@ q-card.page-datatmpl-dialog(style='width: 1100px; max-width: 1100px;')
:clone='cloneFieldType' :clone='cloneFieldType'
:sort='false' :sort='false'
:animation='150' :animation='150'
@start='dragStarted = true' @start='state.dragStarted = true'
@end='dragStarted = false' @end='state.dragStarted = false'
item-key='id' item-key='id'
) )
template(#item='{element}') template(#item='{element}')
@ -81,21 +81,21 @@ q-card.page-datatmpl-dialog(style='width: 1100px; max-width: 1100px;')
.col.page-datatmpl-content .col.page-datatmpl-content
q-scroll-area( q-scroll-area(
ref='scrollArea' ref='scrollArea'
:thumb-style='thumbStyle' :thumb-style='siteStore.thumbStyle'
:bar-style='barStyle' :bar-style='siteStore.barStyle'
style='height: 100%;' style='height: 100%;'
) )
.col.page-datatmpl-meta.q-px-md.q-py-md.flex.q-gutter-md .col.page-datatmpl-meta.q-px-md.q-py-md.flex.q-gutter-md
q-input.col( q-input.col(
ref='tmplTitleIpt' ref='tmplTitleIpt'
:label='$t(`editor.pageData.templateTitle`)' :label='t(`editor.pageData.templateTitle`)'
outlined outlined
dense dense
v-model='tmpl.label' v-model='state.tmpl.label'
) )
q-btn.acrylic-btn( q-btn.acrylic-btn(
icon='las la-check' icon='las la-check'
:label='$t(`common.actions.commit`)' :label='t(`common.actions.commit`)'
no-caps no-caps
flat flat
color='positive' color='positive'
@ -103,22 +103,22 @@ q-card.page-datatmpl-dialog(style='width: 1100px; max-width: 1100px;')
) )
q-btn.acrylic-btn( q-btn.acrylic-btn(
icon='las la-trash' icon='las la-trash'
:aria-label='$t(`common.actions.delete`)' :aria-label='t(`common.actions.delete`)'
flat flat
color='negative' color='negative'
@click='remove' @click='remove'
) )
q-item-label(header) {{$t('editor.pageData.templateStructure')}} q-item-label(header) {{t('editor.pageData.templateStructure')}}
.q-px-md.q-pb-md .q-px-md.q-pb-md
div(:class='(dragStarted || tmpl.data.length < 1 ? `page-datatmpl-box` : ``)') div(:class='(state.dragStarted || state.tmpl.data.length < 1 ? `page-datatmpl-box` : ``)')
.text-caption.text-primary.q-pa-md(v-if='tmpl.data.length < 1 && !dragStarted'): em {{$t('editor.pageData.dragDropHint')}} .text-caption.text-primary.q-pa-md(v-if='state.tmpl.data.length < 1 && !state.dragStarted'): em {{t('editor.pageData.dragDropHint')}}
draggable( draggable(
class='q-list rounded-borders' class='q-list rounded-borders'
:list='tmpl.data' :list='state.tmpl.data'
group='shared' group='shared'
:animation='150' :animation='150'
handle='.handle' handle='.handle'
@end='dragStarted = false' @end='state.dragStarted = false'
item-key='id' item-key='id'
) )
template(#item='{element}') template(#item='{element}')
@ -129,14 +129,14 @@ q-card.page-datatmpl-dialog(style='width: 1100px; max-width: 1100px;')
q-icon(:name='element.icon', color='primary') q-icon(:name='element.icon', color='primary')
q-item-section q-item-section
q-input( q-input(
:label='$t(`editor.pageData.label`)' :label='t(`editor.pageData.label`)'
v-model='element.label' v-model='element.label'
outlined outlined
dense dense
) )
q-item-section(v-if='element.type !== `header`') q-item-section(v-if='element.type !== `header`')
q-input( q-input(
:label='$t(`editor.pageData.uniqueKey`)' :label='t(`editor.pageData.uniqueKey`)'
v-model='element.key' v-model='element.key'
outlined outlined
dense dense
@ -144,7 +144,7 @@ q-card.page-datatmpl-dialog(style='width: 1100px; max-width: 1100px;')
q-item-section(side) q-item-section(side)
q-btn.acrylic-btn( q-btn.acrylic-btn(
color='negative' color='negative'
:aria-label='$t(`common.actions.delete`)' :aria-label='t(`common.actions.delete`)'
padding='xs' padding='xs'
icon='las la-times' icon='las la-times'
flat flat
@ -152,171 +152,194 @@ q-card.page-datatmpl-dialog(style='width: 1100px; max-width: 1100px;')
) )
.page-datatmpl-scrollend(ref='scrollAreaEnd') .page-datatmpl-scrollend(ref='scrollAreaEnd')
.q-pa-md.text-center(v-else-if='templates.length > 0') .q-pa-md.text-center(v-else-if='siteStore.pageDataTemplates.length > 0')
em.text-grey-6 {{$t('editor.pageData.selectTemplateAbove')}} em.text-grey-6 {{t('editor.pageData.selectTemplateAbove')}}
.q-pa-md.text-center(v-else) .q-pa-md.text-center(v-else)
em.text-grey-6 {{$t('editor.pageData.noTemplate')}} em.text-grey-6 {{t('editor.pageData.noTemplate')}}
</template> </template>
<script> <script setup>
import { get, sync } from 'vuex-pathify'
import { v4 as uuid } from 'uuid' import { v4 as uuid } from 'uuid'
import { cloneDeep, sortBy } from 'lodash-es' import { cloneDeep, sortBy } from 'lodash-es'
import draggable from 'vuedraggable' import draggable from 'vuedraggable'
import { useI18n } from 'vue-i18n'
import { useQuasar } from 'quasar'
import { nextTick, onMounted, reactive, ref, watch } from 'vue'
export default { import { usePageStore } from 'src/stores/page'
props: { import { useSiteStore } from 'src/stores/site'
editId: {
type: String, // PROPS
default: null
} const props = defineProps({
editId: {
type: String,
default: null
}
})
// QUASAR
const $q = useQuasar()
// STORES
const pageStore = usePageStore()
const siteStore = useSiteStore()
// I18N
const { t } = useI18n()
// DATA
const state = reactive({
selectedTemplateId: null,
dragStarted: false,
tmpl: null
})
const inventoryMisc = [
{
key: 'header',
label: t('editor.pageData.fieldTypeHeader'),
icon: 'las la-heading'
}, },
components: { {
draggable key: 'image',
label: t('editor.pageData.fieldTypeImage'),
icon: 'las la-image'
}
]
const inventoryKV = [
{
key: 'text',
label: t('editor.pageData.fieldTypeText'),
icon: 'las la-font'
}, },
data () { {
return { key: 'number',
selectedTemplateId: null, label: t('editor.pageData.fieldTypeNumber'),
dragStarted: false, icon: 'las la-infinity'
tmpl: null
}
}, },
computed: { {
templates: sync('site/pageDataTemplates', false), key: 'boolean',
thumbStyle: get('site/thumbStyle', false), label: t('editor.pageData.fieldTypeBoolean'),
barStyle: get('site/barStyle', false), icon: 'las la-check-square'
inventoryMisc () {
return [
{
key: 'header',
label: this.$t('editor.pageData.fieldTypeHeader'),
icon: 'las la-heading'
},
{
key: 'image',
label: this.$t('editor.pageData.fieldTypeImage'),
icon: 'las la-image'
}
]
},
inventoryKV () {
return [
{
key: 'text',
label: this.$t('editor.pageData.fieldTypeText'),
icon: 'las la-font'
},
{
key: 'number',
label: this.$t('editor.pageData.fieldTypeNumber'),
icon: 'las la-infinity'
},
{
key: 'boolean',
label: this.$t('editor.pageData.fieldTypeBoolean'),
icon: 'las la-check-square'
},
{
key: 'link',
label: this.$t('editor.pageData.fieldTypeLink'),
icon: 'las la-link'
}
]
}
}, },
watch: { {
dragStarted (newValue) { key: 'link',
if (newValue) { label: t('editor.pageData.fieldTypeLink'),
this.$nextTick(() => { icon: 'las la-link'
this.$refs.scrollAreaEnd.scrollIntoView({ }
behavior: 'smooth' ]
})
}) // REFS
}
}, const scrollAreaEnd = ref(null)
selectedTemplateId (newValue) { const tmplTitleIpt = ref(null)
this.tmpl = cloneDeep(this.templates.find(t => t.id === this.selectedTemplateId))
// WATCHERS
watch(() => state.dragStarted, (newValue) => {
if (newValue) {
nextTick(() => {
scrollAreaEnd.value.scrollIntoView({
behavior: 'smooth'
})
})
}
})
watch(() => state.selectedTemplateId, (newValue) => {
state.tmpl = cloneDeep(siteStore.pageDataTemplates.find(t => t.id === state.selectedTemplateId))
})
// METHODS
function cloneFieldType (tp) {
return {
id: uuid(),
type: tp.key,
label: '',
...(tp.key !== 'header' ? { key: '' } : {}),
icon: tp.icon
}
}
function removeItem (item) {
state.tmpl.data = state.tmpl.data.filter(i => i.id !== item.id)
}
function create () {
state.tmpl = {
id: uuid(),
label: t('editor.pageData.templateUntitled'),
data: []
}
nextTick(() => {
tmplTitleIpt.value.focus()
nextTick(() => {
document.execCommand('selectall')
})
})
}
function commit () {
try {
if (state.tmpl.label.length < 1) {
throw new Error(t('editor.pageData.invalidTemplateName'))
} else if (state.tmpl.data.length < 1) {
throw new Error(t('editor.pageData.emptyTemplateStructure'))
} else if (state.tmpl.data.some(f => f.label.length < 1)) {
throw new Error(t('editor.pageData.invalidTemplateLabels'))
} else if (state.tmpl.data.some(f => f.type !== 'header' && f.key.length < 1)) {
throw new Error(t('editor.pageData.invalidTemplateKeys'))
} }
},
mounted () { const keys = state.tmpl.data.filter(f => f.type !== 'header').map(f => f.key)
if (this.templates.length > 0) { if ((new Set(keys)).size !== keys.length) {
this.tmpl = this.templates[0] throw new Error(t('editor.pageData.duplicateTemplateKeys'))
this.selectedTemplateId = this.tmpl.id
} else {
this.create()
} }
},
methods: { if (siteStore.pageDataTemplates.some(t => t.id === state.tmpl.id)) {
cloneFieldType (tp) { siteStore.pageDataTemplates = sortBy([...siteStore.pageDataTemplates.filter(t => t.id !== state.tmpl.id), cloneDeep(state.tmpl)], 'label')
return { } else {
id: uuid(), siteStore.pageDataTemplates = sortBy([...siteStore.pageDataTemplates, cloneDeep(state.tmpl)], 'label')
type: tp.key,
label: '',
...(tp.key !== 'header' ? { key: '' } : {}),
icon: tp.icon
}
},
removeItem (item) {
this.tmpl.data = this.tmpl.data.filter(i => i.id !== item.id)
},
create () {
this.tmpl = {
id: uuid(),
label: this.$t('editor.pageData.templateUntitled'),
data: []
}
this.$nextTick(() => {
this.$refs.tmplTitleIpt.focus()
this.$nextTick(() => {
document.execCommand('selectall')
})
})
},
commit () {
try {
if (this.tmpl.label.length < 1) {
throw new Error(this.$t('editor.pageData.invalidTemplateName'))
} else if (this.tmpl.data.length < 1) {
throw new Error(this.$t('editor.pageData.emptyTemplateStructure'))
} else if (this.tmpl.data.some(f => f.label.length < 1)) {
throw new Error(this.$t('editor.pageData.invalidTemplateLabels'))
} else if (this.tmpl.data.some(f => f.type !== 'header' && f.key.length < 1)) {
throw new Error(this.$t('editor.pageData.invalidTemplateKeys'))
}
const keys = this.tmpl.data.filter(f => f.type !== 'header').map(f => f.key)
if ((new Set(keys)).size !== keys.length) {
throw new Error(this.$t('editor.pageData.duplicateTemplateKeys'))
}
if (this.templates.some(t => t.id === this.tmpl.id)) {
this.templates = sortBy([...this.templates.filter(t => t.id !== this.tmpl.id), cloneDeep(this.tmpl)], 'label')
} else {
this.templates = sortBy([...this.templates, cloneDeep(this.tmpl)], 'label')
}
this.selectedTemplateId = this.tmpl.id
} catch (err) {
this.$q.notify({
type: 'negative',
message: err.message
})
}
},
remove () {
this.$q.dialog({
title: this.$t('editor.pageData.templateDeleteConfirmTitle'),
message: this.$t('editor.pageData.templateDeleteConfirmText'),
cancel: true,
persistent: true,
color: 'negative'
}).onOk(() => {
this.templates = this.templates.filter(t => t.id !== this.selectedTemplateId)
this.selectedTemplateId = null
this.tmpl = null
})
} }
state.selectedTemplateId = state.tmpl.id
} catch (err) {
$q.notify({
type: 'negative',
message: err.message
})
} }
} }
function remove () {
$q.dialog({
title: t('editor.pageData.templateDeleteConfirmTitle'),
message: t('editor.pageData.templateDeleteConfirmText'),
cancel: true,
persistent: true,
color: 'negative'
}).onOk(() => {
siteStore.pageDataTemplates = siteStore.pageDataTemplates.filter(t => t.id !== state.selectedTemplateId)
state.selectedTemplateId = null
state.tmpl = null
})
}
// MOUNTED
onMounted(() => {
if (siteStore.pageDataTemplates.length > 0) {
state.tmpl = siteStore.pageDataTemplates[0]
state.selectedTemplateId = state.tmpl.id
} else {
create()
}
})
</script> </script>
<style lang="scss"> <style lang="scss">

@ -270,8 +270,6 @@ q-card.page-properties-dialog
</template> </template>
<script setup> <script setup>
import { usePageStore } from 'src/stores/page'
import { useSiteStore } from 'src/stores/site'
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'
@ -280,6 +278,9 @@ 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 { usePageStore } from 'src/stores/page'
import { useSiteStore } from 'src/stores/site'
// QUASAR // QUASAR
const $q = useQuasar() const $q = useQuasar()

@ -141,9 +141,11 @@ q-page.column
q-icon.q-mr-sm(name='las la-stream', color='grey') q-icon.q-mr-sm(name='las la-stream', color='grey')
.text-caption.text-grey-7 Contents .text-caption.text-grey-7 Contents
.q-px-md.q-pb-sm .q-px-md.q-pb-sm
q-tree( q-tree.page-toc(
:nodes='state.toc' :nodes='pageStore.toc'
icon='las la-caret-right'
node-key='key' node-key='key'
dense
v-model:expanded='state.tocExpanded' v-model:expanded='state.tocExpanded'
v-model:selected='state.tocSelected' v-model:selected='state.tocSelected'
) )
@ -287,6 +289,7 @@ q-page.column
transition-show='jump-left' transition-show='jump-left'
transition-hide='jump-right' transition-hide='jump-right'
class='floating-sidepanel' class='floating-sidepanel'
no-shake
) )
component(:is='sideDialogs[state.sideDialogComponent]') component(:is='sideDialogs[state.sideDialogComponent]')
@ -354,54 +357,6 @@ const state = reactive({
globalDialogComponent: null, globalDialogComponent: null,
showTagsEditBtn: false, showTagsEditBtn: false,
tagEditMode: false, tagEditMode: false,
toc: [
{
key: 'h1-0',
label: 'Introduction'
},
{
key: 'h1-1',
label: 'Planets',
children: [
{
key: 'h2-0',
label: 'Earth',
children: [
{
key: 'h3-0',
label: 'Countries',
children: [
{
key: 'h4-0',
label: 'Cities',
children: [
{
key: 'h5-0',
label: 'Montreal',
children: [
{
key: 'h6-0',
label: 'Districts'
}
]
}
]
}
]
}
]
},
{
key: 'h2-1',
label: 'Mars'
},
{
key: 'h2-2',
label: 'Jupiter'
}
]
}
],
tocExpanded: ['h1-0', 'h1-1'], tocExpanded: ['h1-0', 'h1-1'],
tocSelected: [], tocSelected: [],
currentRating: 3 currentRating: 3
@ -472,8 +427,8 @@ watch(() => route.path, async (newValue) => {
} }
}, { immediate: true }) }, { immediate: true })
watch(() => state.toc, refreshTocExpanded) watch(() => pageStore.toc, () => { refreshTocExpanded() }, { immediate: true })
watch(() => pageStore.tocDepth, refreshTocExpanded) watch(() => pageStore.tocDepth, () => { refreshTocExpanded() })
// METHODS // METHODS
@ -492,20 +447,22 @@ function savePage () {
state.showGlobalDialog = true state.showGlobalDialog = true
} }
function refreshTocExpanded (baseToc) { 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) {
baseToc = state.toc baseToc = pageStore.toc
isRootNode = true isRootNode = true
lvl = 1
} }
if (baseToc.length > 0) { if (baseToc.length > 0) {
for (const node of baseToc) { for (const node of baseToc) {
if (node.key >= `h${pageStore.tocDepth.min}` && node.key <= `h${pageStore.tocDepth.max}`) { if (lvl >= pageStore.tocDepth.min && lvl < pageStore.tocDepth.max) {
toExpand.push(node.key) toExpand.push(node.key)
} }
if (node.children?.length && node.key < `h${pageStore.tocDepth.max}`) { if (node.children?.length && lvl < pageStore.tocDepth.max - 1) {
toExpand.push(...refreshTocExpanded(node.children)) toExpand.push(...refreshTocExpanded(node.children, lvl + 1))
} }
} }
} }
@ -515,12 +472,6 @@ function refreshTocExpanded (baseToc) {
return toExpand return toExpand
} }
} }
// MOUNTED
onMounted(() => {
refreshTocExpanded()
})
</script> </script>
<style lang="scss"> <style lang="scss">
@ -691,4 +642,10 @@ onMounted(() => {
background-color: $dark-3; background-color: $dark-3;
} }
} }
.page-toc {
&.q-tree--dense .q-tree__node {
padding-bottom: 5px;
}
}
</style> </style>

@ -80,7 +80,8 @@ export const usePageStore = defineStore('page', {
}, },
commentsCount: 0, commentsCount: 0,
content: '', content: '',
render: '' render: '',
toc: []
}), }),
getters: {}, getters: {},
actions: { actions: {
@ -93,9 +94,11 @@ export const usePageStore = defineStore('page', {
const resp = await APOLLO_CLIENT.query({ const resp = await APOLLO_CLIENT.query({
query: gql` query: gql`
query loadPage ( query loadPage (
$siteId: UUID!
$path: String! $path: String!
) { ) {
pageByPath( pageByPath(
siteId: $siteId
path: $path path: $path
) { ) {
id id
@ -105,10 +108,12 @@ export const usePageStore = defineStore('page', {
locale locale
updatedAt updatedAt
render render
toc
} }
} }
`, `,
variables: { variables: {
siteId: siteStore.id,
path path
}, },
fetchPolicy: 'network-only' fetchPolicy: 'network-only'

Loading…
Cancel
Save