feat: edit page properties + update dependencies

parent 274f3f4a
...@@ -295,28 +295,28 @@ export default { ...@@ -295,28 +295,28 @@ export default {
$content: String! $content: String!
$description: String! $description: String!
$editor: String! $editor: String!
$isPublished: Boolean! $publishState: PagePublishState!
$locale: String! $locale: String!
$path: String! $path: String!
$publishEndDate: Date $publishEndDate: Date
$publishStartDate: Date $publishStartDate: Date
$scriptCss: String $scriptCss: String
$scriptJs: String $scriptJsLoad: String
$siteId: UUID! $siteId: UUID!
$tags: [String]! $tags: [String!]
$title: String! $title: String!
) { ) {
createPage( createPage(
content: $content content: $content
description: $description description: $description
editor: $editor editor: $editor
isPublished: $isPublished publishState: $publishState
locale: $locale locale: $locale
path: $path path: $path
publishEndDate: $publishEndDate publishEndDate: $publishEndDate
publishStartDate: $publishStartDate publishStartDate: $publishStartDate
scriptCss: $scriptCss scriptCss: $scriptCss
scriptJs: $scriptJs scriptJsLoad: $scriptJsLoad
siteId: $siteId siteId: $siteId
tags: $tags tags: $tags
title: $title title: $title
...@@ -337,12 +337,12 @@ export default { ...@@ -337,12 +337,12 @@ export default {
description: this.$store.get('page/description'), description: this.$store.get('page/description'),
editor: this.$store.get('editor/editorKey'), editor: this.$store.get('editor/editorKey'),
locale: this.$store.get('page/locale'), locale: this.$store.get('page/locale'),
isPublished: this.$store.get('page/isPublished'), publishState: this.$store.get('page/isPublished') ? 'published' : 'draft',
path: this.$store.get('page/path'), path: this.$store.get('page/path'),
publishEndDate: this.$store.get('page/publishEndDate') || '', publishEndDate: this.$store.get('page/publishEndDate') || '',
publishStartDate: this.$store.get('page/publishStartDate') || '', publishStartDate: this.$store.get('page/publishStartDate') || '',
scriptCss: this.$store.get('page/scriptCss'), scriptCss: this.$store.get('page/scriptCss'),
scriptJs: this.$store.get('page/scriptJs'), scriptJsLoad: this.$store.get('page/scriptJs'),
siteId: this.$store.get('site/id'), siteId: this.$store.get('site/id'),
tags: this.$store.get('page/tags'), tags: this.$store.get('page/tags'),
title: this.$store.get('page/title') title: this.$store.get('page/title')
...@@ -392,33 +392,11 @@ export default { ...@@ -392,33 +392,11 @@ export default {
mutation: gql` mutation: gql`
mutation ( mutation (
$id: UUID! $id: UUID!
$content: String $patch: PageUpdateInput!
$description: String
$editor: String
$isPublished: Boolean
$locale: String
$path: String
$publishEndDate: Date
$publishStartDate: Date
$scriptCss: String
$scriptJs: String
$tags: [String]
$title: String
) { ) {
updatePage( updatePage(
id: $id id: $id
content: $content patch: $patch
description: $description
editor: $editor
isPublished: $isPublished
locale: $locale
path: $path
publishEndDate: $publishEndDate
publishStartDate: $publishStartDate
scriptCss: $scriptCss
scriptJs: $scriptJs
tags: $tags
title: $title
) { ) {
operation { operation {
succeeded succeeded
...@@ -432,19 +410,20 @@ export default { ...@@ -432,19 +410,20 @@ export default {
`, `,
variables: { variables: {
id: this.$store.get('page/id'), id: this.$store.get('page/id'),
patch: {
content: this.$store.get('editor/content'), content: this.$store.get('editor/content'),
description: this.$store.get('page/description'), description: this.$store.get('page/description'),
editor: this.$store.get('editor/editorKey'),
locale: this.$store.get('page/locale'), locale: this.$store.get('page/locale'),
isPublished: this.$store.get('page/isPublished'), publishState: this.$store.get('page/isPublished') ? 'published' : 'draft',
path: this.$store.get('page/path'), path: this.$store.get('page/path'),
publishEndDate: this.$store.get('page/publishEndDate') || '', publishEndDate: this.$store.get('page/publishEndDate') || '',
publishStartDate: this.$store.get('page/publishStartDate') || '', publishStartDate: this.$store.get('page/publishStartDate') || '',
scriptCss: this.$store.get('page/scriptCss'), scriptCss: this.$store.get('page/scriptCss'),
scriptJs: this.$store.get('page/scriptJs'), scriptJsLoad: this.$store.get('page/scriptJs'),
tags: this.$store.get('page/tags'), tags: this.$store.get('page/tags'),
title: this.$store.get('page/title') title: this.$store.get('page/title')
} }
}
}) })
resp = _.get(resp, 'data.updatePage', {}) resp = _.get(resp, 'data.updatePage', {})
if (_.get(resp, 'operation.succeeded')) { if (_.get(resp, 'operation.succeeded')) {
......
...@@ -10,6 +10,7 @@ exports.up = async knex => { ...@@ -10,6 +10,7 @@ exports.up = async knex => {
// ===================================== // =====================================
// PG EXTENSIONS // PG EXTENSIONS
// ===================================== // =====================================
await knex.raw('CREATE EXTENSION IF NOT EXISTS ltree;')
await knex.raw('CREATE EXTENSION IF NOT EXISTS pgcrypto;') await knex.raw('CREATE EXTENSION IF NOT EXISTS pgcrypto;')
await knex.schema await knex.schema
...@@ -187,21 +188,27 @@ exports.up = async knex => { ...@@ -187,21 +188,27 @@ exports.up = async knex => {
.createTable('pageHistory', table => { .createTable('pageHistory', table => {
table.uuid('id').notNullable().primary().defaultTo(knex.raw('gen_random_uuid()')) table.uuid('id').notNullable().primary().defaultTo(knex.raw('gen_random_uuid()'))
table.uuid('pageId').notNullable().index() table.uuid('pageId').notNullable().index()
table.string('action').defaultTo('updated')
table.jsonb('affectedFields').notNullable().defaultTo('[]')
table.string('path').notNullable() table.string('path').notNullable()
table.string('hash').notNullable() table.string('hash').notNullable()
table.string('alias')
table.string('title').notNullable() table.string('title').notNullable()
table.string('description') table.string('description')
table.string('icon')
table.enu('publishState', ['draft', 'published', 'scheduled']).notNullable().defaultTo('draft') table.enu('publishState', ['draft', 'published', 'scheduled']).notNullable().defaultTo('draft')
table.timestamp('publishStartDate') table.timestamp('publishStartDate')
table.timestamp('publishEndDate') table.timestamp('publishEndDate')
table.string('action').defaultTo('updated') table.jsonb('config').notNullable().defaultTo('{}')
table.jsonb('relations').notNullable().defaultTo('[]')
table.text('content') table.text('content')
table.text('render')
table.jsonb('toc')
table.string('editor').notNullable() table.string('editor').notNullable()
table.string('contentType').notNullable() table.string('contentType').notNullable()
table.jsonb('extra').notNullable().defaultTo('{}') table.jsonb('scripts').notNullable().defaultTo('{}')
table.jsonb('tags').defaultTo('[]')
table.timestamp('versionDate').notNullable().defaultTo(knex.fn.now())
table.timestamp('createdAt').notNullable().defaultTo(knex.fn.now()) table.timestamp('createdAt').notNullable().defaultTo(knex.fn.now())
table.timestamp('versionDate').notNullable().defaultTo(knex.fn.now())
}) })
// PAGE LINKS -------------------------- // PAGE LINKS --------------------------
.createTable('pageLinks', table => { .createTable('pageLinks', table => {
...@@ -212,32 +219,32 @@ exports.up = async knex => { ...@@ -212,32 +219,32 @@ exports.up = async knex => {
// PAGES ------------------------------- // PAGES -------------------------------
.createTable('pages', table => { .createTable('pages', table => {
table.uuid('id').notNullable().primary().defaultTo(knex.raw('gen_random_uuid()')) table.uuid('id').notNullable().primary().defaultTo(knex.raw('gen_random_uuid()'))
table.string('slug')
table.string('path').notNullable() table.string('path').notNullable()
table.specificType('dotPath', 'ltree').notNullable().index()
table.string('hash').notNullable() table.string('hash').notNullable()
table.string('alias')
table.string('title').notNullable() table.string('title').notNullable()
table.string('description') table.string('description')
table.string('icon')
table.enu('publishState', ['draft', 'published', 'scheduled']).notNullable().defaultTo('draft') table.enu('publishState', ['draft', 'published', 'scheduled']).notNullable().defaultTo('draft')
table.timestamp('publishStartDate') table.timestamp('publishStartDate')
table.timestamp('publishEndDate') table.timestamp('publishEndDate')
table.jsonb('config').notNullable().defaultTo('{}')
table.jsonb('relations').notNullable().defaultTo('[]')
table.text('content') table.text('content')
table.text('render') table.text('render')
table.jsonb('toc') table.jsonb('toc')
table.string('editor').notNullable() table.string('editor').notNullable()
table.string('contentType').notNullable() table.string('contentType').notNullable()
table.jsonb('extra').notNullable().defaultTo('{}') table.boolean('isBrowsable').notNullable().defaultTo(true)
table.string('password')
table.integer('ratingScore').notNullable().defaultTo(0)
table.integer('ratingCount').notNullable().defaultTo(0)
table.jsonb('scripts').notNullable().defaultTo('{}')
table.jsonb('historyData').notNullable().defaultTo('{}')
table.timestamp('createdAt').notNullable().defaultTo(knex.fn.now()) table.timestamp('createdAt').notNullable().defaultTo(knex.fn.now())
table.timestamp('updatedAt').notNullable().defaultTo(knex.fn.now()) table.timestamp('updatedAt').notNullable().defaultTo(knex.fn.now())
}) })
// PAGE TREE ---------------------------
.createTable('pageTree', table => {
table.integer('id').unsigned().primary()
table.string('path').notNullable()
table.integer('depth').unsigned().notNullable()
table.string('title').notNullable()
table.boolean('isFolder').notNullable().defaultTo(false)
table.jsonb('ancestors')
})
// RENDERERS --------------------------- // RENDERERS ---------------------------
.createTable('renderers', table => { .createTable('renderers', table => {
table.uuid('id').notNullable().primary().defaultTo(knex.raw('gen_random_uuid()')) table.uuid('id').notNullable().primary().defaultTo(knex.raw('gen_random_uuid()'))
...@@ -365,11 +372,6 @@ exports.up = async knex => { ...@@ -365,11 +372,6 @@ exports.up = async knex => {
table.uuid('creatorId').notNullable().references('id').inTable('users').index() table.uuid('creatorId').notNullable().references('id').inTable('users').index()
table.uuid('siteId').notNullable().references('id').inTable('sites').index() table.uuid('siteId').notNullable().references('id').inTable('sites').index()
}) })
.table('pageTree', table => {
table.integer('parent').unsigned().references('id').inTable('pageTree').onDelete('CASCADE')
table.uuid('pageId').notNullable().references('id').inTable('pages').onDelete('CASCADE')
table.string('localeCode', 5).references('code').inTable('locales')
})
.table('storage', table => { .table('storage', table => {
table.uuid('siteId').notNullable().references('id').inTable('sites') table.uuid('siteId').notNullable().references('id').inTable('sites')
}) })
...@@ -507,7 +509,11 @@ exports.up = async knex => { ...@@ -507,7 +509,11 @@ exports.up = async knex => {
defaults: { defaults: {
timezone: 'America/New_York', timezone: 'America/New_York',
dateFormat: 'YYYY-MM-DD', dateFormat: 'YYYY-MM-DD',
timeFormat: '12h' timeFormat: '12h',
tocDepth: {
min: 1,
max: 2
}
}, },
features: { features: {
ratings: false, ratings: false,
......
...@@ -143,7 +143,7 @@ module.exports = { ...@@ -143,7 +143,7 @@ module.exports = {
* FETCH SINGLE PAGE BY ID * FETCH SINGLE PAGE BY ID
*/ */
async pageById (obj, args, context, info) { async pageById (obj, args, context, info) {
let page = await WIKI.db.pages.getPageFromDb(args.id) const page = await WIKI.db.pages.getPageFromDb(args.id)
if (page) { if (page) {
if (WIKI.auth.checkAccess(context.req.user, ['read:pages'], { if (WIKI.auth.checkAccess(context.req.user, ['read:pages'], {
path: page.path, path: page.path,
...@@ -151,30 +151,37 @@ module.exports = { ...@@ -151,30 +151,37 @@ module.exports = {
})) { })) {
return { return {
...page, ...page,
locale: page.localeCode, ...page.config,
editor: page.editorKey scriptCss: page.scripts?.css,
scriptJsLoad: page.scripts?.jsLoad,
scriptJsUnload: page.scripts?.jsUnload,
locale: page.localeCode
} }
} else { } else {
throw new WIKI.Error.PageViewForbidden() throw new Error('ERR_FORBIDDEN')
} }
} else { } else {
throw new WIKI.Error.PageNotFound() throw new Error('ERR_PAGE_NOT_FOUND')
} }
}, },
/** /**
* FETCH SINGLE PAGE BY PATH * FETCH SINGLE PAGE BY PATH
*/ */
async pageByPath (obj, args, context, info) { async pageByPath (obj, args, context, info) {
// console.info(info)
const pageArgs = pageHelper.parsePath(args.path) const pageArgs = pageHelper.parsePath(args.path)
let page = await WIKI.db.pages.getPageFromDb({ const page = await WIKI.db.pages.getPageFromDb({
...pageArgs, ...pageArgs,
siteId: args.siteId siteId: args.siteId
}) })
if (page) { if (page) {
return { return {
...page, ...page,
locale: page.localeCode, ...page.config,
editor: page.editorKey scriptCss: page.scripts?.css,
scriptJsLoad: page.scripts?.jsLoad,
scriptJsUnload: page.scripts?.jsUnload,
locale: page.localeCode
} }
} else { } else {
throw new Error('ERR_PAGE_NOT_FOUND') throw new Error('ERR_PAGE_NOT_FOUND')
...@@ -607,8 +614,20 @@ module.exports = { ...@@ -607,8 +614,20 @@ module.exports = {
} }
}, },
Page: { Page: {
icon (obj) {
return obj.icon || 'las la-file-alt'
},
password (obj) {
return obj ? '********' : ''
},
async tags (obj) { async tags (obj) {
return WIKI.db.pages.relatedQuery('tags').for(obj.id) return WIKI.db.pages.relatedQuery('tags').for(obj.id)
},
tocDepth (obj) {
return {
min: obj.extra?.tocDepth?.min ?? 1,
max: obj.extra?.tocDepth?.max ?? 2
}
} }
// comments(pg) { // comments(pg) {
// return pg.$relatedQuery('comments') // return pg.$relatedQuery('comments')
......
...@@ -32,11 +32,13 @@ extend type Query { ...@@ -32,11 +32,13 @@ extend type Query {
pageById( pageById(
id: UUID! id: UUID!
password: String
): Page ): Page
pageByPath( pageByPath(
siteId: UUID! siteId: UUID!
path: String! path: String!
password: String
): Page ): Page
tags: [PageTag]! tags: [PageTag]!
...@@ -69,35 +71,35 @@ extend type Query { ...@@ -69,35 +71,35 @@ extend type Query {
extend type Mutation { extend type Mutation {
createPage( createPage(
siteId: UUID! allowComments: Boolean
allowContributions: Boolean
allowRatings: Boolean
content: String! content: String!
description: String! description: String!
editor: String! editor: String!
isPublished: Boolean! icon: String
isBrowsable: Boolean
locale: String! locale: String!
path: String! path: String!
publishState: PagePublishState!
publishEndDate: Date publishEndDate: Date
publishStartDate: Date publishStartDate: Date
relations: [PageRelationInput!]
scriptCss: String scriptCss: String
scriptJs: String scriptJsLoad: String
tags: [String]! scriptJsUnload: String
showSidebar: Boolean
showTags: Boolean
showToc: Boolean
siteId: UUID!
tags: [String!]
title: String! title: String!
tocDepth: PageTocDepthInput
): PageResponse ): PageResponse
updatePage( updatePage(
id: UUID! id: UUID!
content: String patch: PageUpdateInput!
description: String
editor: String
isPublished: Boolean
locale: String
path: String
publishEndDate: Date
publishStartDate: Date
scriptCss: String
scriptJs: String
tags: [String]
title: String
): PageResponse ): PageResponse
convertPage( convertPage(
...@@ -163,31 +165,40 @@ type PageMigrationResponse { ...@@ -163,31 +165,40 @@ type PageMigrationResponse {
} }
type Page { type Page {
id: UUID allowComments: Boolean
path: String allowContributions: Boolean
hash: String allowRatings: Boolean
title: String author: User
description: String
isPublished: Boolean
publishStartDate: Date
publishEndDate: Date
tags: [PageTag]
content: String content: String
render: String
toc: [JSON]
contentType: String contentType: String
createdAt: Date createdAt: Date
updatedAt: Date creator: User
description: String
editor: String editor: String
hash: String
icon: String
id: UUID
isBrowsable: Boolean
locale: String locale: String
password: String
path: String
publishEndDate: Date
publishStartDate: Date
publishState: PagePublishState
relations: [PageRelation]
render: String
scriptJsLoad: String
scriptJsUnload: String
scriptCss: String scriptCss: String
scriptJs: String showSidebar: Boolean
authorId: Int showTags: Boolean
authorName: String showToc: Boolean
authorEmail: String siteId: UUID
creatorId: Int tags: [PageTag]
creatorName: String title: String
creatorEmail: String toc: [JSON]
tocDepth: PageTocDepth
updatedAt: Date
} }
type PageTag { type PageTag {
...@@ -299,6 +310,59 @@ type PageConflictLatest { ...@@ -299,6 +310,59 @@ type PageConflictLatest {
updatedAt: Date updatedAt: Date
} }
type PageRelation {
id: UUID
position: PageRelationPosition
label: String
caption: String
icon: String
target: String
}
input PageRelationInput {
id: UUID!
position: PageRelationPosition!
label: String!
caption: String
icon: String
target: String!
}
input PageUpdateInput {
allowComments: Boolean
allowContributions: Boolean
allowRatings: Boolean
content: String
description: String
icon: String
isBrowsable: Boolean
locale: String
password: String
path: String
publishEndDate: Date
publishStartDate: Date
publishState: PagePublishState
relations: [PageRelationInput!]
scriptJsLoad: String
scriptJsUnload: String
scriptCss: String
showSidebar: Boolean
showTags: Boolean
showToc: Boolean
tags: [String!]
title: String
tocDepth: PageTocDepthInput
}
type PageTocDepth {
min: Int
max: Int
}
input PageTocDepthInput {
min: Int!
max: Int!
}
enum PageOrderBy { enum PageOrderBy {
CREATED CREATED
ID ID
...@@ -317,3 +381,15 @@ enum PageTreeMode { ...@@ -317,3 +381,15 @@ enum PageTreeMode {
PAGES PAGES
ALL ALL
} }
enum PagePublishState {
draft
published
scheduled
}
enum PageRelationPosition {
left
center
right
}
...@@ -89,6 +89,7 @@ type SiteDefaults { ...@@ -89,6 +89,7 @@ type SiteDefaults {
timezone: String timezone: String
dateFormat: String dateFormat: String
timeFormat: String timeFormat: String
tocDepth: PageTocDepth
} }
type SiteLocale { type SiteLocale {
...@@ -174,6 +175,7 @@ input SiteDefaultsInput { ...@@ -174,6 +175,7 @@ input SiteDefaultsInput {
timezone: String timezone: String
dateFormat: String dateFormat: String
timeFormat: String timeFormat: String
tocDepth: PageTocDepthInput
} }
input SiteThemeInput { input SiteThemeInput {
......
...@@ -13,6 +13,8 @@ const TurndownService = require('turndown') ...@@ -13,6 +13,8 @@ const TurndownService = require('turndown')
const turndownPluginGfm = require('@joplin/turndown-plugin-gfm').gfm const turndownPluginGfm = require('@joplin/turndown-plugin-gfm').gfm
const cheerio = require('cheerio') const cheerio = require('cheerio')
const pageRegex = /^[a-zA0-90-9-_/]*$/
const frontmatterRegex = { const frontmatterRegex = {
html: /^(<!-{2}(?:\n|\r)([\w\W]+?)(?:\n|\r)-{2}>)?(?:\n|\r)*([\w\W]*)*/, html: /^(<!-{2}(?:\n|\r)([\w\W]+?)(?:\n|\r)-{2}>)?(?:\n|\r)*([\w\W]*)*/,
legacy: /^(<!-- TITLE: ?([\w\W]+?) ?-{2}>)?(?:\n|\r)?(<!-- SUBTITLE: ?([\w\W]+?) ?-{2}>)?(?:\n|\r)*([\w\W]*)*/i, legacy: /^(<!-- TITLE: ?([\w\W]+?) ?-{2}>)?(?:\n|\r)?(<!-- SUBTITLE: ?([\w\W]+?) ?-{2}>)?(?:\n|\r)*([\w\W]*)*/i,
...@@ -52,7 +54,7 @@ module.exports = class Page extends Model { ...@@ -52,7 +54,7 @@ module.exports = class Page extends Model {
} }
static get jsonAttributes() { static get jsonAttributes() {
return ['extra'] return ['config', 'historyData', 'relations', 'scripts', 'toc']
} }
static get relationMappings() { static get relationMappings() {
...@@ -231,11 +233,6 @@ module.exports = class Page extends Model { ...@@ -231,11 +233,6 @@ module.exports = class Page extends Model {
throw new WIKI.Error.Custom('InvalidSiteId', 'Site ID is invalid.') throw new WIKI.Error.Custom('InvalidSiteId', 'Site ID is invalid.')
} }
// -> Validate path
if (opts.path.includes('.') || opts.path.includes(' ') || opts.path.includes('\\') || opts.path.includes('//')) {
throw new WIKI.Error.PageIllegalPath()
}
// -> Remove trailing slash // -> Remove trailing slash
if (opts.path.endsWith('/')) { if (opts.path.endsWith('/')) {
opts.path = opts.path.slice(0, -1) opts.path = opts.path.slice(0, -1)
...@@ -246,6 +243,14 @@ module.exports = class Page extends Model { ...@@ -246,6 +243,14 @@ module.exports = class Page extends Model {
opts.path = opts.path.slice(1) opts.path = opts.path.slice(1)
} }
// -> Validate path
if (!pageRegex.test(opts.path)) {
throw new Error('ERR_INVALID_PATH')
}
opts.path = opts.path.toLowerCase()
const dotPath = opts.path.replaceAll('/', '.').replaceAll('-', '_')
// -> Check for page access // -> Check for page access
if (!WIKI.auth.checkAccess(opts.user, ['write:pages'], { if (!WIKI.auth.checkAccess(opts.user, ['write:pages'], {
locale: opts.locale, locale: opts.locale,
...@@ -279,41 +284,52 @@ module.exports = class Page extends Model { ...@@ -279,41 +284,52 @@ module.exports = class Page extends Model {
} }
// -> Format JS Scripts // -> Format JS Scripts
let scriptJs = '' let scriptJsLoad = ''
let scriptJsUnload = ''
if (WIKI.auth.checkAccess(opts.user, ['write:scripts'], { if (WIKI.auth.checkAccess(opts.user, ['write:scripts'], {
locale: opts.locale, locale: opts.locale,
path: opts.path path: opts.path
})) { })) {
scriptJs = opts.scriptJs || '' scriptJsLoad = opts.scriptJsLoad || ''
scriptJsUnload = opts.scriptJsUnload || ''
} }
// -> Create page // -> Create page
await WIKI.db.pages.query().insert({ const page = await WIKI.db.pages.query().insert({
authorId: opts.user.id, authorId: opts.user.id,
content: opts.content, content: opts.content,
creatorId: opts.user.id, creatorId: opts.user.id,
contentType: _.get(WIKI.data.editors[opts.editor], 'contentType', 'text'), config: {
allowComments: opts.allowComments ?? true,
allowContributions: opts.allowContributions ?? true,
allowRatings: opts.allowRatings ?? true,
showSidebar: opts.showSidebar ?? true,
showTags: opts.showTags ?? true,
showToc: opts.showToc ?? true,
tocDepth: opts.tocDepth ?? WIKI.sites[opts.siteId].config?.defaults.tocDepth
},
contentType: WIKI.data.editors[opts.editor]?.contentType ?? 'text',
description: opts.description, description: opts.description,
dotPath: dotPath,
editor: opts.editor, editor: opts.editor,
hash: pageHelper.generateHash({ path: opts.path, locale: opts.locale }), hash: pageHelper.generateHash({ path: opts.path, locale: opts.locale }),
publishState: opts.publishState, icon: opts.icon,
isBrowsable: opts.isBrowsable ?? true,
localeCode: opts.locale, localeCode: opts.locale,
path: opts.path, path: opts.path,
publishState: opts.publishState,
publishEndDate: opts.publishEndDate?.toISO(), publishEndDate: opts.publishEndDate?.toISO(),
publishStartDate: opts.publishStartDate?.toISO(), publishStartDate: opts.publishStartDate?.toISO(),
relations: opts.relations ?? [],
siteId: opts.siteId, siteId: opts.siteId,
title: opts.title, title: opts.title,
toc: '[]', toc: '[]',
extra: JSON.stringify({ scripts: JSON.stringify({
js: scriptJs, jsLoad: scriptJsLoad,
jsUnload: scriptJsUnload,
css: scriptCss css: scriptCss
}) })
}) }).returning('*')
const page = await WIKI.db.pages.getPageFromDb({
path: opts.path,
locale: opts.locale,
userId: opts.user.id
})
// -> Save Tags // -> Save Tags
if (opts.tags && opts.tags.length > 0) { if (opts.tags && opts.tags.length > 0) {
...@@ -365,7 +381,7 @@ module.exports = class Page extends Model { ...@@ -365,7 +381,7 @@ module.exports = class Page extends Model {
// -> Fetch original page // -> Fetch original page
const ogPage = await WIKI.db.pages.query().findById(opts.id) const ogPage = await WIKI.db.pages.query().findById(opts.id)
if (!ogPage) { if (!ogPage) {
throw new Error('Invalid Page Id') throw new Error('ERR_PAGE_NOT_FOUND')
} }
// -> Check for page access // -> Check for page access
...@@ -373,70 +389,205 @@ module.exports = class Page extends Model { ...@@ -373,70 +389,205 @@ module.exports = class Page extends Model {
locale: ogPage.localeCode, locale: ogPage.localeCode,
path: ogPage.path path: ogPage.path
})) { })) {
throw new WIKI.Error.PageUpdateForbidden() throw new Error('ERR_PAGE_UPDATE_FORBIDDEN')
} }
// -> Check for empty content const patch = {}
if (!opts.content || _.trim(opts.content).length < 1) { const historyData = {
throw new WIKI.Error.PageEmptyContent() action: 'updated',
affectedFields: []
} }
// -> Create version snapshot // -> Create version snapshot
await WIKI.db.pageHistory.addVersion({ await WIKI.db.pageHistory.addVersion(ogPage)
...ogPage,
action: opts.action ? opts.action : 'updated',
versionDate: ogPage.updatedAt
})
// -> Format Extra Properties // -> Basic fields
if (!_.isPlainObject(ogPage.extra)) { if ('title' in opts.patch) {
ogPage.extra = {} patch.title = opts.patch.title.trim()
historyData.affectedFields.push('title')
if (patch.title.length < 1) {
throw new Error('ERR_PAGE_TITLE_MISSING')
}
}
if ('description' in opts.patch) {
patch.description = opts.patch.description.trim()
historyData.affectedFields.push('description')
}
if ('icon' in opts.patch) {
patch.icon = opts.patch.icon.trim()
historyData.affectedFields.push('icon')
}
if ('content' in opts.patch) {
patch.content = opts.patch.content
historyData.affectedFields.push('content')
}
// -> Publish State
if (opts.patch.publishState) {
patch.publishState = opts.patch.publishState
historyData.affectedFields.push('publishState')
if (patch.publishState === 'scheduled' && (!opts.patch.publishStartDate || !opts.patch.publishEndDate)) {
throw new Error('ERR_PAGE_MISSING_SCHEDULED_DATES')
}
}
if (opts.patch.publishStartDate) {
patch.publishStartDate = opts.patch.publishStartDate
historyData.affectedFields.push('publishStartDate')
}
if (opts.patch.publishEndDate) {
patch.publishEndDate = opts.patch.publishEndDate
historyData.affectedFields.push('publishEndDate')
}
// -> Page Config
if ('isBrowsable' in opts.patch) {
patch.config = {
...patch.config ?? ogPage.config ?? {},
isBrowsable: opts.patch.isBrowsable
}
historyData.affectedFields.push('isBrowsable')
}
if ('allowComments' in opts.patch) {
patch.config = {
...patch.config ?? ogPage.config ?? {},
allowComments: opts.patch.allowComments
}
historyData.affectedFields.push('allowComments')
}
if ('allowContributions' in opts.patch) {
patch.config = {
...patch.config ?? ogPage.config ?? {},
allowContributions: opts.patch.allowContributions
}
historyData.affectedFields.push('allowContributions')
}
if ('allowRatings' in opts.patch) {
patch.config = {
...patch.config ?? ogPage.config ?? {},
allowRatings: opts.patch.allowRatings
}
historyData.affectedFields.push('allowRatings')
}
if ('showSidebar' in opts.patch) {
patch.config = {
...patch.config ?? ogPage.config ?? {},
showSidebar: opts.patch.showSidebar
}
historyData.affectedFields.push('showSidebar')
}
if ('showTags' in opts.patch) {
patch.config = {
...patch.config ?? ogPage.config ?? {},
showTags: opts.patch.showTags
}
historyData.affectedFields.push('showTags')
}
if ('showToc' in opts.patch) {
patch.config = {
...patch.config ?? ogPage.config ?? {},
showToc: opts.patch.showToc
}
historyData.affectedFields.push('showToc')
}
if ('tocDepth' in opts.patch) {
patch.config = {
...patch.config ?? ogPage.config ?? {},
tocDepth: opts.patch.tocDepth
}
historyData.affectedFields.push('tocDepth')
if (patch.config.tocDepth?.min < 1 || patch.config.tocDepth?.min > 6) {
throw new Error('ERR_PAGE_INVALID_TOC_DEPTH')
}
if (patch.config.tocDepth?.max < 1 || patch.config.tocDepth?.max > 6) {
throw new Error('ERR_PAGE_INVALID_TOC_DEPTH')
}
}
// -> Relations
if ('relations' in opts.patch) {
patch.relations = opts.patch.relations.map(r => {
if (r.label.length < 1) {
throw new Error('ERR_PAGE_RELATION_LABEL_MISSING')
} else if (r.label.length > 255) {
throw new Error('ERR_PAGE_RELATION_LABEL_TOOLONG')
} else if (r.icon.length > 255) {
throw new Error('ERR_PAGE_RELATION_ICON_INVALID')
} else if (r.target.length > 1024) {
throw new Error('ERR_PAGE_RELATION_TARGET_INVALID')
}
return r
})
historyData.affectedFields.push('relations')
} }
// -> Format CSS Scripts // -> Format CSS Scripts
let scriptCss = _.get(ogPage, 'extra.css', '') if (opts.patch.scriptCss) {
if (WIKI.auth.checkAccess(opts.user, ['write:styles'], { if (WIKI.auth.checkAccess(opts.user, ['write:styles'], {
locale: opts.locale, locale: ogPage.localeCode,
path: opts.path path: ogPage.path
})) { })) {
if (!_.isEmpty(opts.scriptCss)) { patch.scripts = {
scriptCss = new CleanCSS({ inline: false }).minify(opts.scriptCss).styles ...patch.scripts ?? ogPage.scripts ?? {},
} else { css: !_.isEmpty(opts.patch.scriptCss) ? new CleanCSS({ inline: false }).minify(opts.patch.scriptCss).styles : ''
scriptCss = '' }
historyData.affectedFields.push('scripts.css')
} }
} }
// -> Format JS Scripts // -> Format JS Scripts
let scriptJs = _.get(ogPage, 'extra.js', '') if (opts.patch.scriptJsLoad) {
if (WIKI.auth.checkAccess(opts.user, ['write:scripts'], { if (WIKI.auth.checkAccess(opts.user, ['write:scripts'], {
locale: opts.locale, locale: ogPage.localeCode,
path: opts.path path: ogPage.path
})) { })) {
scriptJs = opts.scriptJs || '' patch.scripts = {
...patch.scripts ?? ogPage.scripts ?? {},
jsLoad: opts.patch.scriptJsLoad ?? ''
}
historyData.affectedFields.push('scripts.jsLoad')
}
}
if (opts.patch.scriptJsUnload) {
if (WIKI.auth.checkAccess(opts.user, ['write:scripts'], {
locale: ogPage.localeCode,
path: ogPage.path
})) {
patch.scripts = {
...patch.scripts ?? ogPage.scripts ?? {},
jsUnload: opts.patch.scriptJsUnload ?? ''
}
historyData.affectedFields.push('scripts.jsUnload')
}
}
// -> Tags
if ('tags' in opts.patch) {
historyData.affectedFields.push('tags')
} }
// -> Update page // -> Update page
await WIKI.db.pages.query().patch({ await WIKI.db.pages.query().patch({
...patch,
authorId: opts.user.id, authorId: opts.user.id,
content: opts.content, historyData
description: opts.description,
publishState: opts.publishState,
publishEndDate: opts.publishEndDate?.toISO(),
publishStartDate: opts.publishStartDate?.toISO(),
title: opts.title,
extra: JSON.stringify({
...ogPage.extra,
js: scriptJs,
css: scriptCss
})
}).where('id', ogPage.id) }).where('id', ogPage.id)
let page = await WIKI.db.pages.getPageFromDb(ogPage.id) let page = await WIKI.db.pages.getPageFromDb(ogPage.id)
// -> Save Tags // -> Save Tags
await WIKI.db.tags.associateTags({ tags: opts.tags, page }) if (opts.patch.tags) {
await WIKI.db.tags.associateTags({ tags: opts.patch.tags, page })
}
// -> Render page to HTML // -> Render page to HTML
if (opts.patch.content) {
await WIKI.db.pages.renderPage(page) await WIKI.db.pages.renderPage(page)
}
WIKI.events.outbound.emit('deletePageFromCache', page.hash) WIKI.events.outbound.emit('deletePageFromCache', page.hash)
// // -> Update Search Index // // -> Update Search Index
...@@ -468,11 +619,6 @@ module.exports = class Page extends Model { ...@@ -468,11 +619,6 @@ module.exports = class Page extends Model {
destinationPath: opts.path, destinationPath: opts.path,
user: opts.user user: opts.user
}) })
} else {
// -> Update title of page tree entry
await WIKI.db.knex.table('pageTree').where({
pageId: page.id
}).update('title', page.title)
} }
// -> Get latest updatedAt // -> Get latest updatedAt
...@@ -944,6 +1090,8 @@ module.exports = class Page extends Model { ...@@ -944,6 +1090,8 @@ module.exports = class Page extends Model {
* @returns {Promise} Promise of the Page Model Instance * @returns {Promise} Promise of the Page Model Instance
*/ */
static async getPage(opts) { static async getPage(opts) {
return WIKI.db.pages.getPageFromDb(opts)
// -> Get from cache first // -> Get from cache first
let page = await WIKI.db.pages.getPageFromCache(opts) let page = await WIKI.db.pages.getPageFromCache(opts)
if (!page) { if (!page) {
...@@ -974,26 +1122,7 @@ module.exports = class Page extends Model { ...@@ -974,26 +1122,7 @@ module.exports = class Page extends Model {
try { try {
return WIKI.db.pages.query() return WIKI.db.pages.query()
.column([ .column([
'pages.id', 'pages.*',
'pages.path',
'pages.hash',
'pages.title',
'pages.description',
'pages.publishState',
'pages.publishStartDate',
'pages.publishEndDate',
'pages.content',
'pages.render',
'pages.toc',
'pages.contentType',
'pages.createdAt',
'pages.updatedAt',
'pages.editor',
'pages.localeCode',
'pages.authorId',
'pages.creatorId',
'pages.siteId',
'pages.extra',
{ {
authorName: 'author.name', authorName: 'author.name',
authorEmail: 'author.email', authorEmail: 'author.email',
......
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
"lint": "eslint --ext .js,.vue ./" "lint": "eslint --ext .js,.vue ./"
}, },
"dependencies": { "dependencies": {
"@apollo/client": "3.6.9", "@apollo/client": "3.7.1",
"@codemirror/autocomplete": "6.0.2", "@codemirror/autocomplete": "6.0.2",
"@codemirror/basic-setup": "0.20.0", "@codemirror/basic-setup": "0.20.0",
"@codemirror/closebrackets": "0.19.2", "@codemirror/closebrackets": "0.19.2",
...@@ -31,8 +31,8 @@ ...@@ -31,8 +31,8 @@
"@codemirror/state": "6.0.1", "@codemirror/state": "6.0.1",
"@codemirror/tooltip": "0.19.16", "@codemirror/tooltip": "0.19.16",
"@codemirror/view": "6.0.2", "@codemirror/view": "6.0.2",
"@lezer/common": "1.0.0", "@lezer/common": "1.0.1",
"@quasar/extras": "1.15.1", "@quasar/extras": "1.15.5",
"@tiptap/core": "2.0.0-beta.176", "@tiptap/core": "2.0.0-beta.176",
"@tiptap/extension-code-block": "2.0.0-beta.37", "@tiptap/extension-code-block": "2.0.0-beta.37",
"@tiptap/extension-code-block-lowlight": "2.0.0-beta.68", "@tiptap/extension-code-block-lowlight": "2.0.0-beta.68",
...@@ -58,45 +58,45 @@ ...@@ -58,45 +58,45 @@
"@tiptap/starter-kit": "2.0.0-beta.185", "@tiptap/starter-kit": "2.0.0-beta.185",
"@tiptap/vue-3": "2.0.0-beta.91", "@tiptap/vue-3": "2.0.0-beta.91",
"apollo-upload-client": "17.0.0", "apollo-upload-client": "17.0.0",
"browser-fs-access": "0.31.0", "browser-fs-access": "0.31.1",
"clipboard": "2.0.11", "clipboard": "2.0.11",
"codemirror": "6.0.1", "codemirror": "6.0.1",
"filesize": "9.0.11", "filesize": "10.0.5",
"filesize-parser": "1.5.0", "filesize-parser": "1.5.0",
"graphql": "16.6.0", "graphql": "16.6.0",
"graphql-tag": "2.12.6", "graphql-tag": "2.12.6",
"js-cookie": "3.0.1", "js-cookie": "3.0.1",
"jwt-decode": "3.1.2", "jwt-decode": "3.1.2",
"lodash-es": "4.17.21", "lodash-es": "4.17.21",
"luxon": "3.0.1", "luxon": "3.1.0",
"pinia": "2.0.20", "pinia": "2.0.23",
"pug": "3.0.2", "pug": "3.0.2",
"quasar": "2.7.7", "quasar": "2.10.1",
"socket.io-client": "4.5.2", "socket.io-client": "4.5.3",
"tippy.js": "6.3.7", "tippy.js": "6.3.7",
"uuid": "8.3.2", "uuid": "9.0.0",
"v-network-graph": "0.6.6", "v-network-graph": "0.6.10",
"vue": "3.2.37", "vue": "3.2.41",
"vue-codemirror": "6.0.2", "vue-codemirror": "6.1.1",
"vue-i18n": "9.2.2", "vue-i18n": "9.2.2",
"vue-router": "4.1.3", "vue-router": "4.1.6",
"vue3-otp-input": "0.3.6", "vue3-otp-input": "0.3.6",
"vuedraggable": "4.1.0", "vuedraggable": "4.1.0",
"xterm": "4.19.0", "xterm": "5.0.0",
"zxcvbn": "4.4.2" "zxcvbn": "4.4.2"
}, },
"devDependencies": { "devDependencies": {
"@intlify/vite-plugin-vue-i18n": "6.0.1", "@intlify/vite-plugin-vue-i18n": "6.0.3",
"@quasar/app-vite": "1.0.6", "@quasar/app-vite": "1.1.3",
"@types/lodash": "4.14.184", "@types/lodash": "4.14.188",
"@volar/vue-language-plugin-pug": "1.0.1", "@volar/vue-language-plugin-pug": "1.0.9",
"browserlist": "latest", "browserlist": "latest",
"eslint": "8.22.0", "eslint": "8.27.0",
"eslint-config-standard": "17.0.0", "eslint-config-standard": "17.0.0",
"eslint-plugin-import": "2.26.0", "eslint-plugin-import": "2.26.0",
"eslint-plugin-n": "15.2.4", "eslint-plugin-n": "15.5.0",
"eslint-plugin-promise": "6.0.0", "eslint-plugin-promise": "6.1.1",
"eslint-plugin-vue": "9.3.0" "eslint-plugin-vue": "9.7.0"
}, },
"engines": { "engines": {
"node": "^18 || ^16", "node": "^18 || ^16",
......
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40" width="80px" height="80px"><path fill="#b6dcfe" d="M10.5 1.5H28.5V4.5H10.5z"/><path fill="#4788c7" d="M28,2v2H11V2H28 M29,1H10v4h19V1L29,1z"/><path fill="#b6dcfe" d="M1.5 34.5H37.5V37.5H1.5z"/><path fill="#4788c7" d="M37 35v2H2v-2H37M38 34H1v4h37V34L38 34zM37.5 32A.5.5 0 1 0 37.5 33 .5.5 0 1 0 37.5 32zM36.808 30A.5.5 0 1 0 36.808 31 .5.5 0 1 0 36.808 30zM36.115 28A.5.5 0 1 0 36.115 29 .5.5 0 1 0 36.115 28zM35.423 26A.5.5 0 1 0 35.423 27 .5.5 0 1 0 35.423 26zM34.731 24A.5.5 0 1 0 34.731 25 .5.5 0 1 0 34.731 24zM34.038 22A.5.5 0 1 0 34.038 23 .5.5 0 1 0 34.038 22zM33.346 20A.5.5 0 1 0 33.346 21 .5.5 0 1 0 33.346 20zM32.654 18A.5.5 0 1 0 32.654 19 .5.5 0 1 0 32.654 18zM31.962 16A.5.5 0 1 0 31.962 17 .5.5 0 1 0 31.962 16zM31.269 14A.5.5 0 1 0 31.269 15 .5.5 0 1 0 31.269 14zM30.577 12A.5.5 0 1 0 30.577 13 .5.5 0 1 0 30.577 12zM29.885 10A.5.5 0 1 0 29.885 11 .5.5 0 1 0 29.885 10zM29.192 8A.5.5 0 1 0 29.192 9 .5.5 0 1 0 29.192 8zM28.5 6A.5.5 0 1 0 28.5 7 .5.5 0 1 0 28.5 6z"/><g><path fill="#4788c7" d="M1.5 32A.5.5 0 1 0 1.5 33 .5.5 0 1 0 1.5 32zM2.192 30A.5.5 0 1 0 2.192 31 .5.5 0 1 0 2.192 30zM2.885 28A.5.5 0 1 0 2.885 29 .5.5 0 1 0 2.885 28zM3.577 26A.5.5 0 1 0 3.577 27 .5.5 0 1 0 3.577 26zM4.269 24A.5.5 0 1 0 4.269 25 .5.5 0 1 0 4.269 24zM4.962 22A.5.5 0 1 0 4.962 23 .5.5 0 1 0 4.962 22zM5.654 20A.5.5 0 1 0 5.654 21 .5.5 0 1 0 5.654 20zM6.346 18A.5.5 0 1 0 6.346 19 .5.5 0 1 0 6.346 18zM7.038 16A.5.5 0 1 0 7.038 17 .5.5 0 1 0 7.038 16zM7.731 14A.5.5 0 1 0 7.731 15 .5.5 0 1 0 7.731 14zM8.423 12A.5.5 0 1 0 8.423 13 .5.5 0 1 0 8.423 12zM9.115 10A.5.5 0 1 0 9.115 11 .5.5 0 1 0 9.115 10zM9.808 8A.5.5 0 1 0 9.808 9 .5.5 0 1 0 9.808 8zM10.5 6A.5.5 0 1 0 10.5 7 .5.5 0 1 0 10.5 6z"/></g><g><path fill="#4788c7" d="M21 27L21 19 18 19 18 27 13 27 19.5 34 26 27z"/></g><g><path fill="#4788c7" d="M18 12L18 20 21 20 21 12 26 12 19.5 5 13 12z"/></g></svg>
\ No newline at end of file
...@@ -81,7 +81,7 @@ q-header.bg-header.text-white.site-header( ...@@ -81,7 +81,7 @@ q-header.bg-header.text-white.site-header(
round round
dense dense
icon='las la-tools' icon='las la-tools'
color='secondary' color='positive'
to='/_admin' to='/_admin'
aria-label='Administration' aria-label='Administration'
) )
......
...@@ -41,28 +41,39 @@ q-card.page-properties-dialog ...@@ -41,28 +41,39 @@ q-card.page-properties-dialog
outlined outlined
dense dense
) )
q-input(
v-model='pageStore.icon'
:label='t(`editor.props.icon`)'
outlined
dense
)
template(#append)
q-icon.cursor-pointer(
name='las la-icons'
color='primary'
)
q-card-section.alt-card(id='refCardPublishState') q-card-section.alt-card(id='refCardPublishState')
.text-overline.q-pb-xs.items-center.flex #[q-icon.q-mr-sm(name='las la-power-off', size='xs')] {{t('editor.props.publishState')}} .text-overline.q-pb-xs.items-center.flex #[q-icon.q-mr-sm(name='las la-power-off', size='xs')] {{t('editor.props.publishState')}}
q-form.q-gutter-md q-form.q-gutter-md
div div
q-btn-toggle( q-btn-toggle(
v-model='pageStore.isPublished' v-model='pageStore.publishState'
push push
glossy glossy
no-caps no-caps
toggle-color='primary' toggle-color='primary'
:options=`[ :options=`[
{ label: t('editor.props.draft'), value: false }, { label: t('editor.props.draft'), value: 'draft' },
{ label: t('editor.props.published'), value: true }, { label: t('editor.props.published'), value: 'published' },
{ label: t('editor.props.dateRange'), value: null } { label: t('editor.props.dateRange'), value: 'scheduled' }
]` ]`
) )
.text-caption(v-if='pageStore.isPublished'): em {{t('editor.props.publishedHint')}} .text-caption(v-if='pageStore.publishState === `published`'): em {{t('editor.props.publishedHint')}}
.text-caption(v-else-if='pageStore.isPublished === false'): em {{t('editor.props.draftHint')}} .text-caption(v-else-if='pageStore.publishState === `draft`'): em {{t('editor.props.draftHint')}}
template(v-else-if='pageStore.isPublished === null') template(v-else-if='pageStore.publishState === `scheduled`')
.text-caption: em {{t('editor.props.dateRangeHint')}} .text-caption: em {{t('editor.props.dateRangeHint')}}
q-date( q-date(
v-model='pageStore.publishingRange' v-model='publishingRange'
range range
flat flat
bordered bordered
...@@ -230,7 +241,7 @@ q-card.page-properties-dialog ...@@ -230,7 +241,7 @@ q-card.page-properties-dialog
q-form.q-gutter-md.q-pt-sm q-form.q-gutter-md.q-pt-sm
div div
q-toggle( q-toggle(
v-model='pageStore.showInTree' v-model='pageStore.isBrowsable'
dense dense
:label='$t(`editor.props.showInTree`)' :label='$t(`editor.props.showInTree`)'
color='primary' color='primary'
...@@ -240,6 +251,7 @@ q-card.page-properties-dialog ...@@ -240,6 +251,7 @@ q-card.page-properties-dialog
div div
q-toggle( q-toggle(
v-model='state.requirePassword' v-model='state.requirePassword'
@update:model-value='toggleRequirePassword'
dense dense
:label='$t(`editor.props.requirePassword`)' :label='$t(`editor.props.requirePassword`)'
color='primary' color='primary'
...@@ -252,7 +264,7 @@ q-card.page-properties-dialog ...@@ -252,7 +264,7 @@ q-card.page-properties-dialog
) )
q-input( q-input(
ref='iptPagePassword' ref='iptPagePassword'
v-model='state.password' v-model='pageStore.password'
:label='t(`editor.props.password`)' :label='t(`editor.props.password`)'
:hint='t(`editor.props.passwordHint`)' :hint='t(`editor.props.passwordHint`)'
outlined outlined
...@@ -272,7 +284,7 @@ q-card.page-properties-dialog ...@@ -272,7 +284,7 @@ q-card.page-properties-dialog
<script setup> <script setup>
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 { computed, nextTick, onMounted, reactive, ref, watch } from 'vue'
import { DateTime } from 'luxon' import { DateTime } from 'luxon'
import PageRelationDialog from './PageRelationDialog.vue' import PageRelationDialog from './PageRelationDialog.vue'
...@@ -302,9 +314,7 @@ const { t } = useI18n() ...@@ -302,9 +314,7 @@ const { t } = useI18n()
const state = reactive({ const state = reactive({
showRelationDialog: false, showRelationDialog: false,
showScriptsDialog: false, showScriptsDialog: false,
publishingRange: {},
requirePassword: false, requirePassword: false,
password: '',
editRelationId: null, editRelationId: null,
pageScriptsMode: 'jsLoad', pageScriptsMode: 'jsLoad',
showQuickAccess: true showQuickAccess: true
...@@ -325,19 +335,23 @@ const quickaccess = [ ...@@ -325,19 +335,23 @@ const quickaccess = [
const iptPagePassword = ref(null) const iptPagePassword = ref(null)
// WATCHERS // COMPUTED
watch(() => state.requirePassword, (newValue) => { const publishingRange = computed({
if (newValue) { get () {
nextTick(() => { return {
iptPagePassword.value.focus() from: pageStore.publishStartDate,
iptPagePassword.value.$el.scrollIntoView({ to: pageStore.publishEndDate
behavior: 'smooth' }
}) },
}) set (newValue) {
pageStore.publishStartDate = newValue?.from
pageStore.publishEndDate = newValue?.to
} }
}) })
// WATCHERS
pageStore.$subscribe(() => { pageStore.$subscribe(() => {
editorStore.$patch({ editorStore.$patch({
lastChangeTimestamp: DateTime.utc() lastChangeTimestamp: DateTime.utc()
...@@ -366,10 +380,24 @@ function jumpToSection (id) { ...@@ -366,10 +380,24 @@ function jumpToSection (id) {
behavior: 'smooth' behavior: 'smooth'
}) })
} }
function toggleRequirePassword (newValue) {
if (newValue) {
nextTick(() => {
iptPagePassword.value.focus()
iptPagePassword.value.$el.scrollIntoView({
behavior: 'smooth'
})
})
} else {
pageStore.password = ''
}
}
// MOUNTED // MOUNTED
onMounted(() => { onMounted(() => {
state.requirePassword = pageStore.password?.length > 0
setTimeout(() => { setTimeout(() => {
state.showQuickAccess = true state.showQuickAccess = true
}, 300) }, 300)
......
...@@ -1552,5 +1552,8 @@ ...@@ -1552,5 +1552,8 @@
"profile.avatarUploadSuccess": "Profile picture uploaded successfully.", "profile.avatarUploadSuccess": "Profile picture uploaded successfully.",
"profile.avatarUploadFailed": "Failed to upload user profile picture.", "profile.avatarUploadFailed": "Failed to upload user profile picture.",
"profile.avatarClearSuccess": "Profile picture cleared successfully.", "profile.avatarClearSuccess": "Profile picture cleared successfully.",
"profile.avatarClearFailed": "Failed to clear profile picture." "profile.avatarClearFailed": "Failed to clear profile picture.",
"admin.general.defaultTocDepth": "Default ToC Depth",
"admin.general.defaultTocDepthHint": "The default minimum and maximum header levels to show in the table of contents.",
"editor.props.icon": "Icon"
} }
...@@ -371,6 +371,25 @@ q-page.admin-general ...@@ -371,6 +371,25 @@ q-page.admin-general
toggle-color='primary' toggle-color='primary'
:options='timeFormats' :options='timeFormats'
) )
q-separator.q-my-sm(inset)
q-item
blueprint-icon(icon='depth')
q-item-section
q-item-label {{t(`admin.general.defaultTocDepth`)}}
q-item-label(caption) {{t(`admin.general.defaultTocDepthHint`)}}
q-item-section.col-auto.q-pl-sm(style='min-width: 180px;')
.text-caption {{t('editor.props.tocMinMaxDepth')}} #[strong (H{{state.config.defaults.tocDepth.min}} &rarr; H{{state.config.defaults.tocDepth.max}})]
q-range(
v-model='state.config.defaults.tocDepth'
:min='1'
:max='6'
color='primary'
:left-label-value='`H` + state.config.defaults.tocDepth.min'
:right-label-value='`H` + state.config.defaults.tocDepth.max'
snap
label
markers
)
//- ----------------------- //- -----------------------
//- SEO //- SEO
...@@ -479,7 +498,11 @@ const state = reactive({ ...@@ -479,7 +498,11 @@ const state = reactive({
defaults: { defaults: {
timezone: '', timezone: '',
dateFormat: '', dateFormat: '',
timeFormat: '' timeFormat: '',
tocDepth: {
min: 1,
max: 2
}
}, },
robots: { robots: {
index: false, index: false,
...@@ -573,6 +596,10 @@ async function load () { ...@@ -573,6 +596,10 @@ async function load () {
timezone timezone
dateFormat dateFormat
timeFormat timeFormat
tocDepth {
min
max
}
} }
} }
} }
...@@ -635,7 +662,11 @@ async function save () { ...@@ -635,7 +662,11 @@ async function save () {
defaults: { defaults: {
timezone: state.config.defaults?.timezone ?? 'America/New_York', timezone: state.config.defaults?.timezone ?? 'America/New_York',
dateFormat: state.config.defaults?.dateFormat ?? 'YYYY-MM-DD', dateFormat: state.config.defaults?.dateFormat ?? 'YYYY-MM-DD',
timeFormat: state.config.defaults?.timeFormat ?? '12h' timeFormat: state.config.defaults?.timeFormat ?? '12h',
tocDepth: {
min: state.config.defaults?.tocDepth?.min ?? 1,
max: state.config.defaults?.tocDepth?.max ?? 2
}
} }
} }
} }
......
...@@ -325,7 +325,7 @@ q-page.admin-mail ...@@ -325,7 +325,7 @@ q-page.admin-mail
<script setup> <script setup>
import { cloneDeep } from 'lodash-es' import { cloneDeep } from 'lodash-es'
import gql from 'graphql-tag' import gql from 'graphql-tag'
import filesize from 'filesize' import { filesize } from 'filesize'
import filesizeParser from 'filesize-parser' import filesizeParser from 'filesize-parser'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
......
...@@ -21,7 +21,7 @@ q-page.column ...@@ -21,7 +21,7 @@ q-page.column
:to='brd.path' :to='brd.path'
) )
.col-auto.flex.items-center.justify-end .col-auto.flex.items-center.justify-end
template(v-if='!pageStore.isPublished') template(v-if='!pageStore.publishState === `draft`')
.text-caption.text-accent: strong Unpublished .text-caption.text-accent: strong Unpublished
q-separator.q-mx-sm(vertical) q-separator.q-mx-sm(vertical)
.text-caption.text-grey-6 Last modified on #[strong {{lastModified}}] .text-caption.text-grey-6 Last modified on #[strong {{lastModified}}]
...@@ -233,6 +233,7 @@ q-page.column ...@@ -233,6 +233,7 @@ q-page.column
color='deep-orange-9' color='deep-orange-9'
aria-label='Page Data' aria-label='Page Data'
@click='togglePageData' @click='togglePageData'
disable
) )
q-tooltip(anchor='center left' self='center right') Page Data q-tooltip(anchor='center left' self='center right') Page Data
q-separator.q-my-sm(inset) q-separator.q-my-sm(inset)
...@@ -519,7 +520,20 @@ async function discardChanges () { ...@@ -519,7 +520,20 @@ async function discardChanges () {
} }
async function saveChanges () { async function saveChanges () {
$q.loading.show()
try {
await pageStore.pageSave()
$q.notify({
type: 'positive',
message: 'Page saved successfully.'
})
} catch (err) {
$q.notify({
type: 'negative',
message: 'Failed to save page changes.'
})
}
$q.loading.hide()
} }
</script> </script>
......
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, pick, transform } from 'lodash-es'
import { DateTime } from 'luxon' import { DateTime } from 'luxon'
import { useSiteStore } from './site' import { useSiteStore } from './site'
import { useEditorStore } from './editor' import { useEditorStore } from './editor'
const pagePropsFragment = gql`
fragment PageRead on Page {
allowComments
allowContributions
allowRatings
contentType
createdAt
description
editor
icon
id
isBrowsable
locale
password
path
publishEndDate
publishStartDate
publishState
relations {
id
position
label
caption
icon
target
}
render
scriptJsLoad
scriptJsUnload
scriptCss
showSidebar
showTags
showToc
tags {
tag
title
}
title
toc
tocDepth {
min
max
}
updatedAt
}
`
const gqlQueries = { const gqlQueries = {
pageById: gql` pageById: gql`
query loadPage ( query loadPage (
...@@ -14,16 +60,10 @@ const gqlQueries = { ...@@ -14,16 +60,10 @@ const gqlQueries = {
pageById( pageById(
id: $id id: $id
) { ) {
id ...PageRead
title
description
path
locale
updatedAt
render
toc
} }
} }
${pagePropsFragment}
`, `,
pageByPath: gql` pageByPath: gql`
query loadPage ( query loadPage (
...@@ -34,58 +74,49 @@ const gqlQueries = { ...@@ -34,58 +74,49 @@ const gqlQueries = {
siteId: $siteId siteId: $siteId
path: $path path: $path
) { ) {
id ...PageRead
title
description
path
locale
updatedAt
render
toc
} }
} }
${pagePropsFragment}
` `
} }
export const usePageStore = defineStore('page', { export const usePageStore = defineStore('page', {
state: () => ({ state: () => ({
isLoading: true, allowComments: false,
mode: 'view', allowContributions: true,
editor: 'wysiwyg', allowRatings: true,
editorMode: 'edit',
id: 0,
authorId: 0, authorId: 0,
authorName: '', authorName: '',
commentsCount: 0,
content: '',
createdAt: '', createdAt: '',
description: '', description: '',
isPublished: true, icon: 'las la-file-alt',
showInTree: true, id: '',
isBrowsable: true,
locale: 'en', locale: 'en',
password: '',
path: '', path: '',
publishEndDate: '', publishEndDate: '',
publishStartDate: '', publishStartDate: '',
tags: [], publishState: '',
title: '',
icon: 'las la-file-alt',
updatedAt: '',
relations: [], relations: [],
render: '',
scriptJsLoad: '', scriptJsLoad: '',
scriptJsUnload: '', scriptJsUnload: '',
scriptStyles: '', scriptCss: '',
allowComments: false,
allowContributions: true,
allowRatings: true,
showSidebar: true, showSidebar: true,
showToc: true,
showTags: true, showTags: true,
showToc: true,
tags: [],
title: '',
toc: [],
tocDepth: { tocDepth: {
min: 1, min: 1,
max: 2 max: 2
}, },
commentsCount: 0, updatedAt: ''
content: '',
render: '',
toc: []
}), }),
getters: { getters: {
breadcrumbs: (state) => { breadcrumbs: (state) => {
...@@ -120,7 +151,11 @@ export const usePageStore = defineStore('page', { ...@@ -120,7 +151,11 @@ export const usePageStore = defineStore('page', {
throw new Error('ERR_PAGE_NOT_FOUND') throw new Error('ERR_PAGE_NOT_FOUND')
} }
// Update page store // Update page store
this.$patch(pageData) this.$patch({
...pageData,
relations: pageData.relations.map(r => pick(r, ['id', 'position', 'label', 'caption', 'icon', 'target'])),
tocDepth: pick(pageData.tocDepth, ['min', 'max'])
})
// Update editor state timestamps // Update editor state timestamps
const curDate = DateTime.utc() const curDate = DateTime.utc()
editorStore.$patch({ editorStore.$patch({
...@@ -174,6 +209,73 @@ export const usePageStore = defineStore('page', { ...@@ -174,6 +209,73 @@ export const usePageStore = defineStore('page', {
// -> View Mode // -> View Mode
this.mode = 'edit' this.mode = 'edit'
}, },
/**
* PAGE SAVE
*/
async pageSave () {
const editorStore = useEditorStore()
try {
const resp = await APOLLO_CLIENT.mutate({
mutation: gql`
mutation savePage (
$id: UUID!
$patch: PageUpdateInput!
) {
updatePage (
id: $id
patch: $patch
) {
operation {
succeeded
message
}
}
}
`,
variables: {
id: this.id,
patch: pick(this, [
'allowComments',
'allowContributions',
'allowRatings',
// 'content',
'description',
'icon',
'isBrowsable',
'locale',
'password',
'path',
'publishEndDate',
'publishStartDate',
'publishState',
'relations',
'scriptJsLoad',
'scriptJsUnload',
'scriptCss',
'showSidebar',
'showTags',
'showToc',
'tags',
'title',
'tocDepth'
])
}
})
const result = resp?.data?.updatePage?.operation ?? {}
if (!result.succeeded) {
throw new Error(result.message)
}
// Update editor state timestamps
const curDate = DateTime.utc()
editorStore.$patch({
lastChangeTimestamp: curDate,
lastSaveTimestamp: curDate
})
} catch (err) {
console.warn(err)
throw err
}
},
generateToc () { generateToc () {
} }
......
This diff was suppressed by a .gitattributes entry.
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