diff --git a/server/graph/resolvers/page.mjs b/server/graph/resolvers/page.mjs index 163a9118..80909ef1 100644 --- a/server/graph/resolvers/page.mjs +++ b/server/graph/resolvers/page.mjs @@ -86,7 +86,7 @@ export default { // -> Add Highlighting if enabled if (WIKI.config.search.termHighlighting && hasQuery) { - searchCols.push(WIKI.db.knex.raw(`ts_headline(?, "searchContent", query, 'MaxWords=5, MinWords=3, MaxFragments=5') AS highlight`, [dictName])) + searchCols.push(WIKI.db.knex.raw('ts_headline(?, "searchContent", query, \'MaxWords=5, MinWords=3, MaxFragments=5\') AS highlight', [dictName])) } const results = await WIKI.db.knex @@ -408,7 +408,7 @@ export default { * CHECK FOR EDITING CONFLICT */ async checkConflicts (obj, args, context, info) { - let page = await WIKI.db.pages.query().select('path', 'locale', 'updatedAt').findById(args.id) + const page = await WIKI.db.pages.query().select('path', 'locale', 'updatedAt').findById(args.id) if (page) { if (WIKI.auth.checkAccess(context.req.user, ['write:pages', 'manage:pages'], { path: page.path, @@ -426,7 +426,7 @@ export default { * FETCH LATEST VERSION FOR CONFLICT COMPARISON */ async checkConflictsLatest (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, ['write:pages', 'manage:pages'], { path: page.path, @@ -449,7 +449,7 @@ export default { /** * CREATE PAGE */ - async createPage(obj, args, context) { + async createPage (obj, args, context) { try { const page = await WIKI.db.pages.createPage({ ...args, @@ -466,7 +466,7 @@ export default { /** * UPDATE PAGE */ - async updatePage(obj, args, context) { + async updatePage (obj, args, context) { try { const page = await WIKI.db.pages.updatePage({ ...args, @@ -483,7 +483,7 @@ export default { /** * CONVERT PAGE */ - async convertPage(obj, args, context) { + async convertPage (obj, args, context) { try { await WIKI.db.pages.convertPage({ ...args, @@ -497,9 +497,9 @@ export default { } }, /** - * RENAME PAGE + * MOVE PAGE */ - async renamePage(obj, args, context) { + async movePage (obj, args, context) { try { await WIKI.db.pages.movePage({ ...args, @@ -515,7 +515,7 @@ export default { /** * DELETE PAGE */ - async deletePage(obj, args, context) { + async deletePage (obj, args, context) { try { await WIKI.db.pages.deletePage({ ...args, @@ -571,7 +571,7 @@ export default { /** * FLUSH PAGE CACHE */ - async flushCache(obj, args, context) { + async flushCache (obj, args, context) { try { await WIKI.db.pages.flushCache() WIKI.events.outbound.emit('flushCache') @@ -585,7 +585,7 @@ export default { /** * MIGRATE ALL PAGES FROM SOURCE LOCALE TO TARGET LOCALE */ - async migrateToLocale(obj, args, context) { + async migrateToLocale (obj, args, context) { try { const count = await WIKI.db.pages.migrateToLocale(args) return { @@ -599,7 +599,7 @@ export default { /** * REBUILD TREE */ - async rebuildPageTree(obj, args, context) { + async rebuildPageTree (obj, args, context) { try { await WIKI.db.pages.rebuildTree() return { diff --git a/server/graph/schemas/page.graphql b/server/graph/schemas/page.graphql index 981c01f4..47fe2a3a 100644 --- a/server/graph/schemas/page.graphql +++ b/server/graph/schemas/page.graphql @@ -162,10 +162,11 @@ extend type Mutation { editor: String! ): DefaultResponse - renamePage( - id: Int! - destinationPath: String! + movePage( + id: UUID! destinationLocale: String! + destinationPath: String! + title: String ): DefaultResponse deletePage( diff --git a/server/models/pages.mjs b/server/models/pages.mjs index 76a22df0..e0fc8ec1 100644 --- a/server/models/pages.mjs +++ b/server/models/pages.mjs @@ -1,5 +1,5 @@ import { Model } from 'objection' -import { find, get, has, initial, isEmpty, isString, last, pick } from 'lodash-es' +import { cloneDeep, find, get, has, initial, isEmpty, isString, last, pick } from 'lodash-es' import { Type as JSBinType } from 'js-binary' import { getDictNameFromLocale } from '../helpers/common.mjs' import { generateHash, getFileExtension, injectPageMetadata } from '../helpers/page.mjs' @@ -888,15 +888,10 @@ export class Page extends Model { * @returns {Promise} Promise with no value */ static async movePage (opts) { - let page - if (has(opts, 'id')) { - page = await WIKI.db.pages.query().findById(opts.id) - } else { - page = await WIKI.db.pages.query().findOne({ - path: opts.path, - locale: opts.locale - }) + if (!has(opts, 'id')) { + throw new Error('Missing page ID') } + const page = await WIKI.db.pages.query().findById(opts.id) if (!page) { throw new WIKI.Error.PageNotFound() } @@ -947,63 +942,83 @@ export class Page extends Model { versionDate: page.updatedAt }) - const destinationHash = generateHash({ path: opts.destinationPath, locale: opts.destinationLocale }) + // -> Update page object + const updatedPage = cloneDeep(page) + updatedPage.path = opts.destinationPath + updatedPage.locale = opts.destinationLocale + updatedPage.title = opts.title ?? page.title + updatedPage.hash = generateHash({ path: opts.destinationPath, locale: opts.destinationLocale }) + updatedPage.authorId = opts.user.id // -> Move page - const destinationTitle = (page.title === page.path ? opts.destinationPath : page.title) await WIKI.db.pages.query().patch({ - path: opts.destinationPath, - locale: opts.destinationLocale, - title: destinationTitle, - hash: destinationHash + path: updatedPage.path, + locale: updatedPage.locale, + title: updatedPage.title, + hash: updatedPage.hash, + authorId: updatedPage.authorId }).findById(page.id) await WIKI.db.pages.deletePageFromCache(page.hash) WIKI.events.outbound.emit('deletePageFromCache', page.hash) - // -> Rebuild page tree - await WIKI.db.pages.rebuildTree() + // -> Replace tree node + const pathParts = updatedPage.path.split('/') + await WIKI.db.knex('tree').where('id', page.id).del() + await WIKI.db.tree.addPage({ + id: page.id, + parentPath: initial(pathParts).join('/'), + fileName: last(pathParts), + locale: updatedPage.locale, + title: updatedPage.title, + tags: updatedPage.tags, + meta: { + authorId: updatedPage.authorId, + contentType: updatedPage.contentType, + creatorId: updatedPage.creatorId, + description: updatedPage.description, + isBrowsable: updatedPage.isBrowsable, + ownerId: updatedPage.ownerId, + publishState: updatedPage.publishState, + publishEndDate: updatedPage.publishEndDate, + publishStartDate: updatedPage.publishStartDate + }, + siteId: updatedPage.siteId + }) // -> Rename in Search Index - const pageContents = await WIKI.db.pages.query().findById(page.id).select('render') - page.safeContent = WIKI.db.pages.cleanHTML(pageContents.render) - await WIKI.data.searchEngine.renamed({ - ...page, - destinationPath: opts.destinationPath, - destinationLocale: opts.destinationLocale, - destinationHash - }) + WIKI.db.pages.updatePageSearchVector({ id: page.id }) // -> Rename in Storage if (!opts.skipStorage) { - await WIKI.db.storage.pageEvent({ - event: 'renamed', - page: { - ...page, - destinationPath: opts.destinationPath, - destinationLocale: opts.destinationLocale, - destinationHash, - moveAuthorId: opts.user.id, - moveAuthorName: opts.user.name, - moveAuthorEmail: opts.user.email - } - }) + // await WIKI.db.storage.pageEvent({ + // event: 'renamed', + // page: { + // ...page, + // destinationPath: updatedPage.path, + // destinationLocale: updatedPage.locale, + // destinationHash: updatedPage.hash, + // moveAuthorId: opts.user.id, + // moveAuthorName: opts.user.name, + // moveAuthorEmail: opts.user.email + // } + // }) } - // -> Reconnect Links : Changing old links to the new path - await WIKI.db.pages.reconnectLinks({ - sourceLocale: page.locale, - sourcePath: page.path, - locale: opts.destinationLocale, - path: opts.destinationPath, - mode: 'move' - }) + // // -> Reconnect Links : Changing old links to the new path + // await WIKI.db.pages.reconnectLinks({ + // sourceLocale: page.locale, + // sourcePath: page.path, + // locale: opts.destinationLocale, + // path: opts.destinationPath, + // mode: 'move' + // }) - // -> Reconnect Links : Validate invalid links to the new path - await WIKI.db.pages.reconnectLinks({ - locale: opts.destinationLocale, - path: opts.destinationPath, - mode: 'create' - }) + // // -> Reconnect Links : Validate invalid links to the new path + // await WIKI.db.pages.reconnectLinks({ + // locale: opts.destinationLocale, + // path: opts.destinationPath, + // mode: 'create' + // }) } /** diff --git a/server/models/tree.mjs b/server/models/tree.mjs index 4007c694..df0c51c9 100644 --- a/server/models/tree.mjs +++ b/server/models/tree.mjs @@ -17,7 +17,7 @@ const reTitle = /^[^<>"]+$/ * Tree model */ export class Tree extends Model { - static get tableName() { return 'tree' } + static get tableName () { return 'tree' } static get jsonSchema () { return { @@ -25,22 +25,22 @@ export class Tree extends Model { required: ['fileName'], properties: { - id: {type: 'string'}, - folderPath: {type: 'string'}, - fileName: {type: 'string'}, - type: {type: 'string'}, - title: {type: 'string'}, - createdAt: {type: 'string'}, - updatedAt: {type: 'string'} + id: { type: 'string' }, + folderPath: { type: 'string' }, + fileName: { type: 'string' }, + type: { type: 'string' }, + title: { type: 'string' }, + createdAt: { type: 'string' }, + updatedAt: { type: 'string' } } } } - static get jsonAttributes() { + static get jsonAttributes () { return ['meta'] } - static get relationMappings() { + static get relationMappings () { return { site: { relation: Model.BelongsToOneRelation, @@ -53,10 +53,11 @@ export class Tree extends Model { } } - $beforeUpdate() { + $beforeUpdate () { this.updatedAt = new Date().toISOString() } - $beforeInsert() { + + $beforeInsert () { this.createdAt = new Date().toISOString() this.updatedAt = new Date().toISOString() } @@ -90,7 +91,7 @@ export class Tree extends Model { const parent = await WIKI.db.knex('tree').where({ ...parentFilter, type: 'folder', - locale: locale, + locale, siteId }).first() if (parent) { @@ -123,16 +124,18 @@ export class Tree extends Model { * @param {Object} [args.meta] - Extra metadata */ static async addPage ({ id, parentId, parentPath, fileName, title, locale, siteId, tags = [], meta = {} }) { - const folder = (parentId || parentPath) ? await WIKI.db.tree.getFolder({ - id: parentId, - path: parentPath, - locale, - siteId, - createIfMissing: true - }) : { - folderPath: '', - fileName: '' - } + const folder = (parentId || parentPath) + ? await WIKI.db.tree.getFolder({ + id: parentId, + path: parentPath, + locale, + siteId, + createIfMissing: true + }) + : { + folderPath: '', + fileName: '' + } const folderPath = folder.folderPath ? `${folder.folderPath}.${folder.fileName}` : folder.fileName const fullPath = folderPath ? `${folderPath}/${fileName}` : fileName @@ -143,9 +146,9 @@ export class Tree extends Model { folderPath: encodeFolderPath(folderPath), fileName, type: 'page', - title: title, + title, hash: generateHash(fullPath), - locale: locale, + locale, siteId, tags, meta, @@ -169,16 +172,18 @@ export class Tree extends Model { * @param {Object} [args.meta] - Extra metadata */ static async addAsset ({ id, parentId, parentPath, fileName, title, locale, siteId, tags = [], meta = {} }) { - const folder = (parentId || parentPath) ? await WIKI.db.tree.getFolder({ - id: parentId, - path: parentPath, - locale, - siteId, - createIfMissing: true - }) : { - folderPath: '', - fileName: '' - } + const folder = (parentId || parentPath) + ? await WIKI.db.tree.getFolder({ + id: parentId, + path: parentPath, + locale, + siteId, + createIfMissing: true + }) + : { + folderPath: '', + fileName: '' + } const folderPath = folder.folderPath ? `${folder.folderPath}.${folder.fileName}` : folder.fileName const fullPath = folderPath ? `${folderPath}/${fileName}` : fileName @@ -189,9 +194,9 @@ export class Tree extends Model { folderPath: encodeFolderPath(folderPath), fileName, type: 'asset', - title: title, + title, hash: generateHash(fullPath), - locale: locale, + locale, siteId, tags, meta @@ -246,8 +251,8 @@ export class Tree extends Model { // Check for collision const existingFolder = await WIKI.db.knex('tree').select('id').where({ - siteId: siteId, - locale: locale, + siteId, + locale, folderPath: encodeFolderPath(parentPath), fileName: pathName, type: 'folder' @@ -281,8 +286,8 @@ export class Tree extends Model { type: 'folder', title: ancestor.fileName, hash: generateHash(newAncestorFullPath), - locale: locale, - siteId: siteId, + locale, + siteId, meta: { children: 1 } @@ -301,10 +306,10 @@ export class Tree extends Model { folderPath: encodeFolderPath(parentPath), fileName: pathName, type: 'folder', - title: title, + title, hash: generateHash(fullPath), - locale: locale, - siteId: siteId, + locale, + siteId, meta: { children: 0 } @@ -383,13 +388,13 @@ export class Tree extends Model { const fullPath = folder.folderPath ? `${decodeFolderPath(folder.folderPath)}/${pathName}` : pathName await WIKI.db.knex('tree').where('id', folder.id).update({ fileName: pathName, - title: title, + title, hash: generateHash(fullPath) }) } else { // Update the folder title only await WIKI.db.knex('tree').where('id', folder.id).update({ - title: title + title }) } diff --git a/ux/src/components/PageActionsCol.vue b/ux/src/components/PageActionsCol.vue index 82d6992b..bb7ea2fa 100644 --- a/ux/src/components/PageActionsCol.vue +++ b/ux/src/components/PageActionsCol.vue @@ -244,8 +244,27 @@ function renamePage () { itemTitle: pageStore.title, itemFileName: pageStore.path } - }).onOk(() => { - // TODO: change route to new location + }).onOk(async (renamedPageOpts) => { + try { + if (renamedPageOpts.path === pageStore.path) { + await pageStore.pageRename({ id: pageStore.id, title: renamedPageOpts.title }) + $q.notify({ + type: 'positive', + message: 'Page renamed successfully.' + }) + } else { + await pageStore.pageMove({ id: pageStore.id, path: renamedPageOpts.path, title: renamedPageOpts.title }) + $q.notify({ + type: 'positive', + message: 'Page moved successfully.' + }) + } + } catch (err) { + $q.notify({ + type: 'negative', + message: err.message + }) + } }) } diff --git a/ux/src/components/TreeBrowserDialog.vue b/ux/src/components/TreeBrowserDialog.vue index a80975b0..f4a22977 100644 --- a/ux/src/components/TreeBrowserDialog.vue +++ b/ux/src/components/TreeBrowserDialog.vue @@ -64,7 +64,9 @@ q-dialog(ref='dialogRef', @hide='onDialogHide') dense outlined @focus='state.pathDirty = true; state.currentFileId = null' - ) + ) + //- template(#append) + //- q-badge(outline, color='grey', label='valid') q-card-actions.card-actions.q-px-md q-btn.acrylic-btn( icon='las la-ellipsis-h' @@ -408,8 +410,17 @@ onMounted(() => { fName = last(fParts) } switch (props.mode) { - case 'pageSave': { + case 'savePage': { + state.typesToFetch = ['folder', 'page'] + break + } + case 'duplicatePage': { + state.typesToFetch = ['folder', 'page'] + break + } + case 'renamePage': { state.typesToFetch = ['folder', 'page'] + state.pathDirty = true break } } diff --git a/ux/src/stores/page.js b/ux/src/stores/page.js index 3d42d4e2..83e93a2e 100644 --- a/ux/src/stores/page.js +++ b/ux/src/stores/page.js @@ -452,6 +452,83 @@ export const usePageStore = defineStore('page', { editor: this.editor }) }, + /** + * PAGE - MOVE + */ + async pageMove ({ id, title, path } = {}) { + const resp = await APOLLO_CLIENT.mutate({ + mutation: gql` + mutation movePage ( + $id: UUID! + $destinationLocale: String! + $destinationPath: String! + $title: String + ) { + movePage ( + id: $id + destinationLocale: $destinationLocale + destinationPath: $destinationPath + title: $title + ) { + operation { + succeeded + message + } + } + } + `, + variables: { + id, + destinationLocale: this.locale, + destinationPath: path, + title + } + }) + const result = resp?.data?.movePage?.operation ?? {} + if (!result.succeeded) { + throw new Error(result.message) + } else { + this.router.replace(`/${path}`) + } + }, + /** + * PAGE - Rename + */ + async pageRename ({ id, title } = {}) { + const resp = await APOLLO_CLIENT.mutate({ + mutation: gql` + mutation renamePage ( + $id: UUID! + $patch: PageUpdateInput! + ) { + updatePage ( + id: $id + patch: $patch + ) { + operation { + succeeded + message + } + } + } + `, + variables: { + id: id, + patch: { + title + } + } + }) + const result = resp?.data?.updatePage?.operation ?? {} + if (!result.succeeded) { + throw new Error(result.message) + } + + // Update page store + if (id === this.id) { + this.$patch({ title }) + } + }, /** * PAGE SAVE */