diff --git a/client/components/editor.vue b/client/components/editor.vue index eaa959a0..2e54a3ca 100644 --- a/client/components/editor.vue +++ b/client/components/editor.vue @@ -295,28 +295,28 @@ export default { $content: String! $description: String! $editor: String! - $isPublished: Boolean! + $publishState: PagePublishState! $locale: String! $path: String! $publishEndDate: Date $publishStartDate: Date $scriptCss: String - $scriptJs: String + $scriptJsLoad: String $siteId: UUID! - $tags: [String]! + $tags: [String!] $title: String! ) { createPage( content: $content description: $description editor: $editor - isPublished: $isPublished + publishState: $publishState locale: $locale path: $path publishEndDate: $publishEndDate publishStartDate: $publishStartDate scriptCss: $scriptCss - scriptJs: $scriptJs + scriptJsLoad: $scriptJsLoad siteId: $siteId tags: $tags title: $title @@ -337,12 +337,12 @@ export default { description: this.$store.get('page/description'), editor: this.$store.get('editor/editorKey'), 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'), publishEndDate: this.$store.get('page/publishEndDate') || '', publishStartDate: this.$store.get('page/publishStartDate') || '', scriptCss: this.$store.get('page/scriptCss'), - scriptJs: this.$store.get('page/scriptJs'), + scriptJsLoad: this.$store.get('page/scriptJs'), siteId: this.$store.get('site/id'), tags: this.$store.get('page/tags'), title: this.$store.get('page/title') @@ -392,33 +392,11 @@ export default { mutation: gql` mutation ( $id: UUID! - $content: String - $description: String - $editor: String - $isPublished: Boolean - $locale: String - $path: String - $publishEndDate: Date - $publishStartDate: Date - $scriptCss: String - $scriptJs: String - $tags: [String] - $title: String + $patch: PageUpdateInput! ) { updatePage( id: $id - content: $content - description: $description - editor: $editor - isPublished: $isPublished - locale: $locale - path: $path - publishEndDate: $publishEndDate - publishStartDate: $publishStartDate - scriptCss: $scriptCss - scriptJs: $scriptJs - tags: $tags - title: $title + patch: $patch ) { operation { succeeded @@ -432,18 +410,19 @@ export default { `, variables: { id: this.$store.get('page/id'), - content: this.$store.get('editor/content'), - description: this.$store.get('page/description'), - editor: this.$store.get('editor/editorKey'), - locale: this.$store.get('page/locale'), - isPublished: this.$store.get('page/isPublished'), - path: this.$store.get('page/path'), - publishEndDate: this.$store.get('page/publishEndDate') || '', - publishStartDate: this.$store.get('page/publishStartDate') || '', - scriptCss: this.$store.get('page/scriptCss'), - scriptJs: this.$store.get('page/scriptJs'), - tags: this.$store.get('page/tags'), - title: this.$store.get('page/title') + patch: { + content: this.$store.get('editor/content'), + description: this.$store.get('page/description'), + locale: this.$store.get('page/locale'), + publishState: this.$store.get('page/isPublished') ? 'published' : 'draft', + path: this.$store.get('page/path'), + publishEndDate: this.$store.get('page/publishEndDate') || '', + publishStartDate: this.$store.get('page/publishStartDate') || '', + scriptCss: this.$store.get('page/scriptCss'), + scriptJsLoad: this.$store.get('page/scriptJs'), + tags: this.$store.get('page/tags'), + title: this.$store.get('page/title') + } } }) resp = _.get(resp, 'data.updatePage', {}) diff --git a/server/db/migrations/3.0.0.js b/server/db/migrations/3.0.0.js index c717eb52..961fb32a 100644 --- a/server/db/migrations/3.0.0.js +++ b/server/db/migrations/3.0.0.js @@ -10,6 +10,7 @@ exports.up = async knex => { // ===================================== // PG EXTENSIONS // ===================================== + await knex.raw('CREATE EXTENSION IF NOT EXISTS ltree;') await knex.raw('CREATE EXTENSION IF NOT EXISTS pgcrypto;') await knex.schema @@ -187,21 +188,27 @@ exports.up = async knex => { .createTable('pageHistory', table => { table.uuid('id').notNullable().primary().defaultTo(knex.raw('gen_random_uuid()')) table.uuid('pageId').notNullable().index() + table.string('action').defaultTo('updated') + table.jsonb('affectedFields').notNullable().defaultTo('[]') table.string('path').notNullable() table.string('hash').notNullable() + table.string('alias') table.string('title').notNullable() table.string('description') + table.string('icon') table.enu('publishState', ['draft', 'published', 'scheduled']).notNullable().defaultTo('draft') table.timestamp('publishStartDate') table.timestamp('publishEndDate') - table.string('action').defaultTo('updated') + table.jsonb('config').notNullable().defaultTo('{}') + table.jsonb('relations').notNullable().defaultTo('[]') table.text('content') + table.text('render') + table.jsonb('toc') table.string('editor').notNullable() table.string('contentType').notNullable() - table.jsonb('extra').notNullable().defaultTo('{}') - table.jsonb('tags').defaultTo('[]') - table.timestamp('versionDate').notNullable().defaultTo(knex.fn.now()) + table.jsonb('scripts').notNullable().defaultTo('{}') table.timestamp('createdAt').notNullable().defaultTo(knex.fn.now()) + table.timestamp('versionDate').notNullable().defaultTo(knex.fn.now()) }) // PAGE LINKS -------------------------- .createTable('pageLinks', table => { @@ -212,32 +219,32 @@ exports.up = async knex => { // PAGES ------------------------------- .createTable('pages', table => { table.uuid('id').notNullable().primary().defaultTo(knex.raw('gen_random_uuid()')) - table.string('slug') table.string('path').notNullable() + table.specificType('dotPath', 'ltree').notNullable().index() table.string('hash').notNullable() + table.string('alias') table.string('title').notNullable() table.string('description') + table.string('icon') table.enu('publishState', ['draft', 'published', 'scheduled']).notNullable().defaultTo('draft') table.timestamp('publishStartDate') table.timestamp('publishEndDate') + table.jsonb('config').notNullable().defaultTo('{}') + table.jsonb('relations').notNullable().defaultTo('[]') table.text('content') table.text('render') table.jsonb('toc') table.string('editor').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('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 --------------------------- .createTable('renderers', table => { table.uuid('id').notNullable().primary().defaultTo(knex.raw('gen_random_uuid()')) @@ -365,11 +372,6 @@ exports.up = async knex => { table.uuid('creatorId').notNullable().references('id').inTable('users').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.uuid('siteId').notNullable().references('id').inTable('sites') }) @@ -507,7 +509,11 @@ exports.up = async knex => { defaults: { timezone: 'America/New_York', dateFormat: 'YYYY-MM-DD', - timeFormat: '12h' + timeFormat: '12h', + tocDepth: { + min: 1, + max: 2 + } }, features: { ratings: false, diff --git a/server/graph/resolvers/page.js b/server/graph/resolvers/page.js index 14040557..c94cdd26 100644 --- a/server/graph/resolvers/page.js +++ b/server/graph/resolvers/page.js @@ -143,7 +143,7 @@ module.exports = { * FETCH SINGLE PAGE BY ID */ 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 (WIKI.auth.checkAccess(context.req.user, ['read:pages'], { path: page.path, @@ -151,30 +151,37 @@ module.exports = { })) { return { ...page, - locale: page.localeCode, - editor: page.editorKey + ...page.config, + scriptCss: page.scripts?.css, + scriptJsLoad: page.scripts?.jsLoad, + scriptJsUnload: page.scripts?.jsUnload, + locale: page.localeCode } } else { - throw new WIKI.Error.PageViewForbidden() + throw new Error('ERR_FORBIDDEN') } } else { - throw new WIKI.Error.PageNotFound() + throw new Error('ERR_PAGE_NOT_FOUND') } }, /** * FETCH SINGLE PAGE BY PATH */ async pageByPath (obj, args, context, info) { + // console.info(info) const pageArgs = pageHelper.parsePath(args.path) - let page = await WIKI.db.pages.getPageFromDb({ + const page = await WIKI.db.pages.getPageFromDb({ ...pageArgs, siteId: args.siteId }) if (page) { return { ...page, - locale: page.localeCode, - editor: page.editorKey + ...page.config, + scriptCss: page.scripts?.css, + scriptJsLoad: page.scripts?.jsLoad, + scriptJsUnload: page.scripts?.jsUnload, + locale: page.localeCode } } else { throw new Error('ERR_PAGE_NOT_FOUND') @@ -607,8 +614,20 @@ module.exports = { } }, Page: { + icon (obj) { + return obj.icon || 'las la-file-alt' + }, + password (obj) { + return obj ? '********' : '' + }, async tags (obj) { 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) { // return pg.$relatedQuery('comments') diff --git a/server/graph/schemas/page.graphql b/server/graph/schemas/page.graphql index e25896c1..7548bc73 100644 --- a/server/graph/schemas/page.graphql +++ b/server/graph/schemas/page.graphql @@ -32,11 +32,13 @@ extend type Query { pageById( id: UUID! + password: String ): Page pageByPath( siteId: UUID! path: String! + password: String ): Page tags: [PageTag]! @@ -69,35 +71,35 @@ extend type Query { extend type Mutation { createPage( - siteId: UUID! + allowComments: Boolean + allowContributions: Boolean + allowRatings: Boolean content: String! description: String! editor: String! - isPublished: Boolean! + icon: String + isBrowsable: Boolean locale: String! path: String! + publishState: PagePublishState! publishEndDate: Date publishStartDate: Date + relations: [PageRelationInput!] scriptCss: String - scriptJs: String - tags: [String]! + scriptJsLoad: String + scriptJsUnload: String + showSidebar: Boolean + showTags: Boolean + showToc: Boolean + siteId: UUID! + tags: [String!] title: String! + tocDepth: PageTocDepthInput ): PageResponse updatePage( id: UUID! - content: String - description: String - editor: String - isPublished: Boolean - locale: String - path: String - publishEndDate: Date - publishStartDate: Date - scriptCss: String - scriptJs: String - tags: [String] - title: String + patch: PageUpdateInput! ): PageResponse convertPage( @@ -163,31 +165,40 @@ type PageMigrationResponse { } type Page { - id: UUID - path: String - hash: String - title: String - description: String - isPublished: Boolean - publishStartDate: Date - publishEndDate: Date - tags: [PageTag] + allowComments: Boolean + allowContributions: Boolean + allowRatings: Boolean + author: User content: String - render: String - toc: [JSON] contentType: String createdAt: Date - updatedAt: Date + creator: User + description: String editor: String + hash: String + icon: String + id: UUID + isBrowsable: Boolean locale: String + password: String + path: String + publishEndDate: Date + publishStartDate: Date + publishState: PagePublishState + relations: [PageRelation] + render: String + scriptJsLoad: String + scriptJsUnload: String scriptCss: String - scriptJs: String - authorId: Int - authorName: String - authorEmail: String - creatorId: Int - creatorName: String - creatorEmail: String + showSidebar: Boolean + showTags: Boolean + showToc: Boolean + siteId: UUID + tags: [PageTag] + title: String + toc: [JSON] + tocDepth: PageTocDepth + updatedAt: Date } type PageTag { @@ -299,6 +310,59 @@ type PageConflictLatest { 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 { CREATED ID @@ -317,3 +381,15 @@ enum PageTreeMode { PAGES ALL } + +enum PagePublishState { + draft + published + scheduled +} + +enum PageRelationPosition { + left + center + right +} diff --git a/server/graph/schemas/site.graphql b/server/graph/schemas/site.graphql index 9cdf8ddd..2cda97b9 100644 --- a/server/graph/schemas/site.graphql +++ b/server/graph/schemas/site.graphql @@ -89,6 +89,7 @@ type SiteDefaults { timezone: String dateFormat: String timeFormat: String + tocDepth: PageTocDepth } type SiteLocale { @@ -174,6 +175,7 @@ input SiteDefaultsInput { timezone: String dateFormat: String timeFormat: String + tocDepth: PageTocDepthInput } input SiteThemeInput { diff --git a/server/models/pages.js b/server/models/pages.js index cf3546a9..565d7394 100644 --- a/server/models/pages.js +++ b/server/models/pages.js @@ -13,6 +13,8 @@ const TurndownService = require('turndown') const turndownPluginGfm = require('@joplin/turndown-plugin-gfm').gfm const cheerio = require('cheerio') +const pageRegex = /^[a-zA0-90-9-_/]*$/ + const frontmatterRegex = { html: /^()?(?:\n|\r)*([\w\W]*)*/, legacy: /^(