Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
W
wiki-js
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
1
Issues
1
List
Board
Labels
Milestones
Merge Requests
1
Merge Requests
1
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Registry
Registry
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Jacklull
wiki-js
Commits
acc3b736
Unverified
Commit
acc3b736
authored
Nov 01, 2022
by
Nicolas Giard
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: page TOC + refactor PageDataDialog to composition API
parent
85a74aa3
Hide whitespace changes
Inline
Side-by-side
Showing
10 changed files
with
308 additions
and
356 deletions
+308
-356
common.js
server/controllers/common.js
+13
-61
page.js
server/graph/resolvers/page.js
+4
-1
page.graphql
server/graph/schemas/page.graphql
+2
-1
pages.js
server/models/pages.js
+1
-0
render-page.js
server/tasks/workers/render-page.js
+2
-2
PageDataDialog.vue
ux/src/components/PageDataDialog.vue
+58
-49
PageDataTemplateDialog.vue
ux/src/components/PageDataTemplateDialog.vue
+199
-176
PagePropertiesDialog.vue
ux/src/components/PagePropertiesDialog.vue
+3
-2
Index.vue
ux/src/pages/Index.vue
+20
-63
page.js
ux/src/stores/page.js
+6
-1
No files found.
server/controllers/common.js
View file @
acc3b736
...
...
@@ -153,6 +153,11 @@ router.get(['/d', '/d/*'], async (req, res, next) => {
*/
router
.
get
([
'/_edit'
,
'/_edit/*'
],
async
(
req
,
res
,
next
)
=>
{
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
===
''
)
{
return
res
.
redirect
(
`/_edit/home`
)
...
...
@@ -175,10 +180,10 @@ router.get(['/_edit', '/_edit/*'], async (req, res, next) => {
// -> Get page data from DB
let
page
=
await
WIKI
.
db
.
pages
.
getPageFromDb
({
siteId
:
site
.
id
,
path
:
pageArgs
.
path
,
locale
:
pageArgs
.
locale
,
userId
:
req
.
user
.
id
,
isPrivate
:
false
userId
:
req
.
user
.
id
})
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
pageArgs
=
pageHelper
.
parsePath
(
req
.
path
,
{
stripExt
})
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 (WIKI.config.lang.namespacing && !pageArgs.explicitLocale) {
...
...
@@ -426,6 +436,7 @@ router.get('/*', async (req, res, next) => {
try
{
// -> Get Page from cache
const
page
=
await
WIKI
.
db
.
pages
.
getPage
({
siteId
:
site
.
id
,
path
:
pageArgs
.
path
,
locale
:
pageArgs
.
locale
,
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
res
.
sendFile
(
path
.
join
(
WIKI
.
ROOTPATH
,
'assets/index.html'
))
// res.render('page', {
// page,
// sidebar,
// injectCode,
// comments: commentTmpl,
// effectivePermissions
// })
}
else
if
(
pageArgs
.
path
===
'home'
)
{
res
.
redirect
(
'/_welcome'
)
}
else
{
...
...
server/graph/resolvers/page.js
View file @
acc3b736
...
...
@@ -166,7 +166,10 @@ module.exports = {
*/
async
pageByPath
(
obj
,
args
,
context
,
info
)
{
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
)
{
return
{
...
page
,
...
...
server/graph/schemas/page.graphql
View file @
acc3b736
...
...
@@ -35,6 +35,7 @@ extend type Query {
):
Page
pageByPath
(
siteId
:
UUID
!
path
:
String
!
):
Page
...
...
@@ -173,7 +174,7 @@ type Page {
tags
:
[
PageTag
]
content
:
String
render
:
String
toc
:
String
toc
:
[
JSON
]
contentType
:
String
createdAt
:
Date
updatedAt
:
Date
...
...
server/models/pages.js
View file @
acc3b736
...
...
@@ -1010,6 +1010,7 @@ module.exports = class Page extends Model {
.
where
(
queryModeID
?
{
'pages.id'
:
opts
}
:
{
'pages.siteId'
:
opts
.
siteId
,
'pages.path'
:
opts
.
path
,
'pages.localeCode'
:
opts
.
locale
})
...
...
server/tasks/workers/render-page.js
View file @
acc3b736
...
...
@@ -62,8 +62,8 @@ module.exports = async ({ payload }) => {
$
(
'.toc-anchor'
,
el
).
remove
()
_
.
get
(
toc
,
leafPath
).
push
({
title
:
_
.
trim
(
$
(
el
).
text
()),
anchor
:
leafSlug
,
label
:
_
.
trim
(
$
(
el
).
text
()),
key
:
leafSlug
.
substring
(
1
)
,
children
:
[]
})
})
...
...
ux/src/components/PageDataDialog.vue
View file @
acc3b736
<
template
lang=
"pug"
>
q-card.page-data-dialog(style='width: 750px;')
q-toolbar.bg-primary.text-white.flex
.text-subtitle2
{{
$
t
(
'editor.pageData.title'
)
}}
.text-subtitle2
{{
t
(
'editor.pageData.title'
)
}}
q-space
q-btn(
icon='las la-times'
...
...
@@ -10,13 +10,13 @@ q-card.page-data-dialog(style='width: 750px;')
v-close-popup
)
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
q-select(
dark
v-model='templateId'
:label='
$
t(`editor.pageData.template`)'
:aria-label='
$
t(`editor.pageData.template`)'
v-model='
state.
templateId'
:label='t(`editor.pageData.template`)'
:aria-label='t(`editor.pageData.template`)'
:options='templates'
option-value='id'
map-options
...
...
@@ -28,14 +28,14 @@ q-card.page-data-dialog(style='width: 750px;')
q-btn.acrylic-btn(
dark
icon='las la-pen'
:label='
$
t(`common.actions.manage`)'
:label='t(`common.actions.manage`)'
unelevated
no-caps
color='deep-orange-9'
@click='editTemplates'
)
q-tabs.alt-card(
v-model='mode'
v-model='
state.
mode'
inline-label
no-caps
)
...
...
@@ -48,11 +48,11 @@ q-card.page-data-dialog(style='width: 750px;')
label='YAML'
)
q-scroll-area(
:thumb-style='thumbStyle'
:bar-style='barStyle'
:thumb-style='
siteStore.
thumbStyle'
:bar-style='
siteStore.
barStyle'
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-input(
label='Attribute Text'
...
...
@@ -76,60 +76,69 @@ q-card.page-data-dialog(style='width: 750px;')
dense
size='lg'
)
q-no-ssr(v-else, :placeholder='
$
t(`common.loading`)')
q-no-ssr(v-else, :placeholder='t(`common.loading`)')
codemirror.admin-theme-cm(
ref='cmData'
v-model='content'
v-model='
state.
content'
:options='{ mode: `text/yaml` }'
)
q-dialog(
v-model='showDataTemplateDialog'
v-model='s
tate.s
howDataTemplateDialog'
)
page-data-template-dialog
</
template
>
<
script
>
import
{
get
}
from
'vuex-pathify'
<
script
setup
>
import
{
useI18n
}
from
'vue-i18n'
import
{
useQuasar
}
from
'quasar'
import
{
nextTick
,
onMounted
,
reactive
,
ref
,
watch
}
from
'vue'
import
PageDataTemplateDialog
from
'./PageDataTemplateDialog.vue'
export
default
{
components
:
{
PageDataTemplateDialog
},
data
()
{
return
{
showDataTemplateDialog
:
false
,
templateId
:
''
,
content
:
''
,
mode
:
'visual'
}
},
computed
:
{
thumbStyle
:
get
(
'site/thumbStyle'
,
false
),
barStyle
:
get
(
'site/barStyle'
,
false
),
templates
()
{
return
[
{
id
:
''
,
label
:
'None'
,
data
:
[]
}
,
...
this
.
$store
.
get
(
'site/pageDataTemplates'
),
{
id
:
'basic'
,
label
:
'Basic'
,
data
:
[]
}
]
}
import
{
usePageStore
}
from
'src/stores/page'
import
{
useSiteStore
}
from
'src/stores/site'
// QUASAR
const
$q
=
useQuasar
()
// STORES
const
pageStore
=
usePageStore
()
const
siteStore
=
useSiteStore
()
// I18N
const
{
t
}
=
useI18n
()
// DATA
const
state
=
reactive
({
showDataTemplateDialog
:
false
,
templateId
:
''
,
content
:
''
,
mode
:
'visual'
})
const
templates
=
[
{
id
:
''
,
label
:
'None'
,
data
:
[]
},
methods
:
{
editTemplates
()
{
this
.
showDataTemplateDialog
=
!
this
.
showDataTemplateDialog
}
...
siteStore
.
pageDataTemplates
,
{
id
:
'basic'
,
label
:
'Basic'
,
data
:
[]
}
]
// METHODS
function
editTemplates
()
{
state
.
showDataTemplateDialog
=
!
state
.
showDataTemplateDialog
}
</
script
>
...
...
ux/src/components/PageDataTemplateDialog.vue
View file @
acc3b736
<
template
lang=
"pug"
>
q-card.page-datatmpl-dialog(style='width: 1100px; max-width: 1100px;')
q-toolbar.bg-primary.text-white
.text-subtitle2
{{
$
t
(
'editor.pageData.manageTemplates'
)
}}
.text-subtitle2
{{
t
(
'editor.pageData.manageTemplates'
)
}}
q-space
q-btn(
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
.flex.q-gutter-md
q-select.col(
v-model='selectedTemplateId'
:options='
t
emplates'
v-model='s
tate.s
electedTemplateId'
:options='
siteStore.pageDataT
emplates'
standout
:label='
$
t(`editor.pageData.template`)'
:label='t(`editor.pageData.template`)'
dense
dark
option-value='id'
...
...
@@ -24,23 +24,23 @@ q-card.page-datatmpl-dialog(style='width: 1100px; max-width: 1100px;')
)
q-btn(
icon='las la-plus'
:label='
$
t(`common.actions.new`)'
:label='t(`common.actions.new`)'
unelevated
color='primary'
no-caps
@click='create'
)
.row(v-if='tmpl')
.row(v-if='
state.
tmpl')
.col-auto.page-datatmpl-sd
.q-pa-md
q-btn.acrylic-btn.full-width(
:label='
$
t(`common.actions.howItWorks`)'
:label='t(`common.actions.howItWorks`)'
icon='las la-question-circle'
flat
color='pink'
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
draggable(
class='q-list rounded-borders'
...
...
@@ -49,8 +49,8 @@ q-card.page-datatmpl-dialog(style='width: 1100px; max-width: 1100px;')
:clone='cloneFieldType'
:sort='false'
:animation='150'
@start='dragStarted = true'
@end='dragStarted = false'
@start='
state.
dragStarted = true'
@end='
state.
dragStarted = false'
item-key='id'
)
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-item-section
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
draggable(
class='q-list rounded-borders'
...
...
@@ -68,8 +68,8 @@ q-card.page-datatmpl-dialog(style='width: 1100px; max-width: 1100px;')
:clone='cloneFieldType'
:sort='false'
:animation='150'
@start='dragStarted = true'
@end='dragStarted = false'
@start='
state.
dragStarted = true'
@end='
state.
dragStarted = false'
item-key='id'
)
template(#item='{element}')
...
...
@@ -81,21 +81,21 @@ q-card.page-datatmpl-dialog(style='width: 1100px; max-width: 1100px;')
.col.page-datatmpl-content
q-scroll-area(
ref='scrollArea'
:thumb-style='thumbStyle'
:bar-style='barStyle'
:thumb-style='
siteStore.
thumbStyle'
:bar-style='
siteStore.
barStyle'
style='height: 100%;'
)
.col.page-datatmpl-meta.q-px-md.q-py-md.flex.q-gutter-md
q-input.col(
ref='tmplTitleIpt'
:label='
$
t(`editor.pageData.templateTitle`)'
:label='t(`editor.pageData.templateTitle`)'
outlined
dense
v-model='tmpl.label'
v-model='
state.
tmpl.label'
)
q-btn.acrylic-btn(
icon='las la-check'
:label='
$
t(`common.actions.commit`)'
:label='t(`common.actions.commit`)'
no-caps
flat
color='positive'
...
...
@@ -103,22 +103,22 @@ q-card.page-datatmpl-dialog(style='width: 1100px; max-width: 1100px;')
)
q-btn.acrylic-btn(
icon='las la-trash'
:aria-label='
$
t(`common.actions.delete`)'
:aria-label='t(`common.actions.delete`)'
flat
color='negative'
@click='remove'
)
q-item-label(header)
{{
$
t
(
'editor.pageData.templateStructure'
)
}}
q-item-label(header)
{{
t
(
'editor.pageData.templateStructure'
)
}}
.q-px-md.q-pb-md
div(:class='(
dragStarted ||
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'
)
}}
div(:class='(
state.dragStarted || state.
tmpl.data.length < 1 ? `page-datatmpl-box` : ``)')
.text-caption.text-primary.q-pa-md(v-if='
state.tmpl.data.length < 1 && !state.dragStarted'): em
{{
t
(
'editor.pageData.dragDropHint'
)
}}
draggable(
class='q-list rounded-borders'
:list='tmpl.data'
:list='
state.
tmpl.data'
group='shared'
:animation='150'
handle='.handle'
@end='dragStarted = false'
@end='
state.
dragStarted = false'
item-key='id'
)
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-item-section
q-input(
:label='
$
t(`editor.pageData.label`)'
:label='t(`editor.pageData.label`)'
v-model='element.label'
outlined
dense
)
q-item-section(v-if='element.type !== `header`')
q-input(
:label='
$
t(`editor.pageData.uniqueKey`)'
:label='t(`editor.pageData.uniqueKey`)'
v-model='element.key'
outlined
dense
...
...
@@ -144,7 +144,7 @@ q-card.page-datatmpl-dialog(style='width: 1100px; max-width: 1100px;')
q-item-section(side)
q-btn.acrylic-btn(
color='negative'
:aria-label='
$
t(`common.actions.delete`)'
:aria-label='t(`common.actions.delete`)'
padding='xs'
icon='las la-times'
flat
...
...
@@ -152,171 +152,194 @@ q-card.page-datatmpl-dialog(style='width: 1100px; max-width: 1100px;')
)
.page-datatmpl-scrollend(ref='scrollAreaEnd')
.q-pa-md.text-center(v-else-if='
t
emplates.length > 0')
em.text-grey-6
{{
$
t
(
'editor.pageData.selectTemplateAbove'
)
}}
.q-pa-md.text-center(v-else-if='
siteStore.pageDataT
emplates.length > 0')
em.text-grey-6
{{
t
(
'editor.pageData.selectTemplateAbove'
)
}}
.q-pa-md.text-center(v-else)
em.text-grey-6
{{
$
t
(
'editor.pageData.noTemplate'
)
}}
em.text-grey-6
{{
t
(
'editor.pageData.noTemplate'
)
}}
</
template
>
<
script
>
import
{
get
,
sync
}
from
'vuex-pathify'
<
script
setup
>
import
{
v4
as
uuid
}
from
'uuid'
import
{
cloneDeep
,
sortBy
}
from
'lodash-es'
import
draggable
from
'vuedraggable'
import
{
useI18n
}
from
'vue-i18n'
import
{
useQuasar
}
from
'quasar'
import
{
nextTick
,
onMounted
,
reactive
,
ref
,
watch
}
from
'vue'
export
default
{
props
:
{
editId
:
{
type
:
String
,
default
:
null
}
import
{
usePageStore
}
from
'src/stores/page'
import
{
useSiteStore
}
from
'src/stores/site'
// PROPS
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
{
selectedTemplateId
:
null
,
dragStarted
:
false
,
tmpl
:
null
}
{
key
:
'number'
,
label
:
t
(
'editor.pageData.fieldTypeNumber'
),
icon
:
'las la-infinity'
},
computed
:
{
templates
:
sync
(
'site/pageDataTemplates'
,
false
),
thumbStyle
:
get
(
'site/thumbStyle'
,
false
),
barStyle
:
get
(
'site/barStyle'
,
false
),
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'
}
]
}
{
key
:
'boolean'
,
label
:
t
(
'editor.pageData.fieldTypeBoolean'
),
icon
:
'las la-check-square'
},
watch
:
{
dragStarted
(
newValue
)
{
if
(
newValue
)
{
this
.
$nextTick
(()
=>
{
this
.
$refs
.
scrollAreaEnd
.
scrollIntoView
({
behavior
:
'smooth'
})
})
}
},
selectedTemplateId
(
newValue
)
{
this
.
tmpl
=
cloneDeep
(
this
.
templates
.
find
(
t
=>
t
.
id
===
this
.
selectedTemplateId
))
{
key
:
'link'
,
label
:
t
(
'editor.pageData.fieldTypeLink'
),
icon
:
'las la-link'
}
]
// REFS
const
scrollAreaEnd
=
ref
(
null
)
const
tmplTitleIpt
=
ref
(
null
)
// 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
()
{
if
(
this
.
templates
.
length
>
0
)
{
this
.
tmpl
=
this
.
templates
[
0
]
this
.
selectedTemplateId
=
this
.
tmpl
.
id
}
else
{
this
.
create
()
const
keys
=
state
.
tmpl
.
data
.
filter
(
f
=>
f
.
type
!==
'header'
).
map
(
f
=>
f
.
key
)
if
((
new
Set
(
keys
)).
size
!==
keys
.
length
)
{
throw
new
Error
(
t
(
'editor.pageData.duplicateTemplateKeys'
))
}
},
methods
:
{
cloneFieldType
(
tp
)
{
return
{
id
:
uuid
(),
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
})
if
(
siteStore
.
pageDataTemplates
.
some
(
t
=>
t
.
id
===
state
.
tmpl
.
id
))
{
siteStore
.
pageDataTemplates
=
sortBy
([...
siteStore
.
pageDataTemplates
.
filter
(
t
=>
t
.
id
!==
state
.
tmpl
.
id
),
cloneDeep
(
state
.
tmpl
)],
'label'
)
}
else
{
siteStore
.
pageDataTemplates
=
sortBy
([...
siteStore
.
pageDataTemplates
,
cloneDeep
(
state
.
tmpl
)],
'label'
)
}
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
>
<
style
lang=
"scss"
>
...
...
ux/src/components/PagePropertiesDialog.vue
View file @
acc3b736
...
...
@@ -270,8 +270,6 @@ q-card.page-properties-dialog
</
template
>
<
script
setup
>
import
{
usePageStore
}
from
'src/stores/page'
import
{
useSiteStore
}
from
'src/stores/site'
import
{
useI18n
}
from
'vue-i18n'
import
{
useQuasar
}
from
'quasar'
import
{
nextTick
,
onMounted
,
reactive
,
ref
,
watch
}
from
'vue'
...
...
@@ -280,6 +278,9 @@ import PageRelationDialog from './PageRelationDialog.vue'
import
PageScriptsDialog
from
'./PageScriptsDialog.vue'
import
PageTags
from
'./PageTags.vue'
import
{
usePageStore
}
from
'src/stores/page'
import
{
useSiteStore
}
from
'src/stores/site'
// QUASAR
const
$q
=
useQuasar
()
...
...
ux/src/pages/Index.vue
View file @
acc3b736
...
...
@@ -141,9 +141,11 @@ q-page.column
q-icon.q-mr-sm(name='las la-stream', color='grey')
.text-caption.text-grey-7 Contents
.q-px-md.q-pb-sm
q-tree(
:nodes='state.toc'
q-tree.page-toc(
:nodes='pageStore.toc'
icon='las la-caret-right'
node-key='key'
dense
v-model:expanded='state.tocExpanded'
v-model:selected='state.tocSelected'
)
...
...
@@ -287,6 +289,7 @@ q-page.column
transition-show='jump-left'
transition-hide='jump-right'
class='floating-sidepanel'
no-shake
)
component(:is='sideDialogs[state.sideDialogComponent]')
...
...
@@ -354,54 +357,6 @@ const state = reactive({
globalDialogComponent
:
null
,
showTagsEditBtn
:
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'
],
tocSelected
:
[],
currentRating
:
3
...
...
@@ -472,8 +427,8 @@ watch(() => route.path, async (newValue) => {
}
},
{
immediate
:
true
})
watch
(()
=>
state
.
toc
,
refreshTocExpanded
)
watch
(()
=>
pageStore
.
tocDepth
,
refreshTocExpanded
)
watch
(()
=>
pageStore
.
toc
,
()
=>
{
refreshTocExpanded
()
},
{
immediate
:
true
}
)
watch
(()
=>
pageStore
.
tocDepth
,
()
=>
{
refreshTocExpanded
()
}
)
// METHODS
...
...
@@ -492,20 +447,22 @@ function savePage () {
state
.
showGlobalDialog
=
true
}
function
refreshTocExpanded
(
baseToc
)
{
function
refreshTocExpanded
(
baseToc
,
lvl
)
{
console
.
info
(
pageStore
.
tocDepth
.
min
,
lvl
,
pageStore
.
tocDepth
.
max
)
const
toExpand
=
[]
let
isRootNode
=
false
if
(
!
baseToc
)
{
baseToc
=
stat
e
.
toc
baseToc
=
pageStor
e
.
toc
isRootNode
=
true
lvl
=
1
}
if
(
baseToc
.
length
>
0
)
{
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
)
}
if
(
node
.
children
?.
length
&&
node
.
key
<
`h
${
pageStore
.
tocDepth
.
max
}
`
)
{
toExpand
.
push
(...
refreshTocExpanded
(
node
.
children
))
if
(
node
.
children
?.
length
&&
lvl
<
pageStore
.
tocDepth
.
max
-
1
)
{
toExpand
.
push
(...
refreshTocExpanded
(
node
.
children
,
lvl
+
1
))
}
}
}
...
...
@@ -515,12 +472,6 @@ function refreshTocExpanded (baseToc) {
return
toExpand
}
}
// MOUNTED
onMounted
(()
=>
{
refreshTocExpanded
()
})
</
script
>
<
style
lang=
"scss"
>
...
...
@@ -691,4 +642,10 @@ onMounted(() => {
background-color
:
$dark-3
;
}
}
.page-toc
{
&
.q-tree--dense
.q-tree__node
{
padding-bottom
:
5px
;
}
}
</
style
>
ux/src/stores/page.js
View file @
acc3b736
...
...
@@ -80,7 +80,8 @@ export const usePageStore = defineStore('page', {
},
commentsCount
:
0
,
content
:
''
,
render
:
''
render
:
''
,
toc
:
[]
}),
getters
:
{},
actions
:
{
...
...
@@ -93,9 +94,11 @@ export const usePageStore = defineStore('page', {
const
resp
=
await
APOLLO_CLIENT
.
query
({
query
:
gql
`
query loadPage (
$siteId: UUID!
$path: String!
) {
pageByPath(
siteId: $siteId
path: $path
) {
id
...
...
@@ -105,10 +108,12 @@ export const usePageStore = defineStore('page', {
locale
updatedAt
render
toc
}
}
`
,
variables
:
{
siteId
:
siteStore
.
id
,
path
},
fetchPolicy
:
'network-only'
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment