feat: move page

This commit is contained in:
NGPixel 2019-10-13 19:59:50 -04:00
parent daa8174d7f
commit 62d1d7a1df
23 changed files with 451 additions and 99 deletions

View File

@ -24,6 +24,7 @@
v-card-text.pt-0 v-card-text.pt-0
template(v-for='(pm, idx) in pmGroup.items') template(v-for='(pm, idx) in pmGroup.items')
v-checkbox.pt-0( v-checkbox.pt-0(
style='justify-content: space-between;'
:key='pm.permission' :key='pm.permission'
:label='pm.permission' :label='pm.permission'
:hint='pm.hint' :hint='pm.hint'
@ -60,14 +61,14 @@ export default {
}, },
{ {
permission: 'write:pages', permission: 'write:pages',
hint: 'Can create new pages, as specified in the Page Rules', hint: 'Can create / edit pages, as specified in the Page Rules',
warning: false, warning: false,
restrictedForSystem: false, restrictedForSystem: false,
disabled: false disabled: false
}, },
{ {
permission: 'manage:pages', permission: 'manage:pages',
hint: 'Can edit and move existing pages as specified in the Page Rules', hint: 'Can move existing pages as specified in the Page Rules',
warning: false, warning: false,
restrictedForSystem: false, restrictedForSystem: false,
disabled: false disabled: false

View File

@ -105,7 +105,7 @@
v-icon(left) mdi-code-tags v-icon(left) mdi-code-tags
span Source span Source
v-divider.mx-2(vertical) v-divider.mx-2(vertical)
v-btn(color='primary', text, :href='`/h/` + page.locale + `/` + page.path') v-btn(color='primary', text, :href='`/h/` + page.locale + `/` + page.path', disabled)
v-icon(left) mdi-history v-icon(left) mdi-history
span History span History
v-spacer v-spacer

View File

@ -3,6 +3,12 @@
v-toolbar(flat, color='primary', dark, dense) v-toolbar(flat, color='primary', dark, dense)
.subtitle-1 {{ $t('admin:utilities.contentTitle') }} .subtitle-1 {{ $t('admin:utilities.contentTitle') }}
v-card-text v-card-text
.subtitle-1.pb-3.primary--text Rebuild Page Tree
.body-2 The virtual structure of your wiki is automatically inferred from all page paths. You can trigger a full rebuild of the tree if some virtual folders are missing or not valid anymore.
v-btn(outlined, color='primary', @click='rebuildTree', :disabled='loading').ml-0.mt-3
v-icon(left) mdi-gesture-double-tap
span Proceed
v-divider.my-5
.subtitle-1.pb-3.pl-0.primary--text Migrate all pages to target locale .subtitle-1.pb-3.pl-0.primary--text Migrate all pages to target locale
.body-2 If you created content before selecting a different locale and activating the namespacing capabilities, you may want to transfer all content to the base locale. .body-2 If you created content before selecting a different locale and activating the namespacing capabilities, you may want to transfer all content to the base locale.
.body-2.red--text: strong This operation is destructive and cannot be reversed! Make sure you have proper backups! .body-2.red--text: strong This operation is destructive and cannot be reversed! Make sure you have proper backups!
@ -35,6 +41,7 @@
<script> <script>
import _ from 'lodash' import _ from 'lodash'
import utilityContentMigrateLocaleMutation from 'gql/admin/utilities/utilities-mutation-content-migratelocale.gql' import utilityContentMigrateLocaleMutation from 'gql/admin/utilities/utilities-mutation-content-migratelocale.gql'
import utilityContentRebuildTreeMutation from 'gql/admin/utilities/utilities-mutation-content-rebuildtree.gql'
/* global siteLangs, siteConfig */ /* global siteLangs, siteConfig */
@ -55,7 +62,32 @@ export default {
} }
}, },
methods: { methods: {
async migrateToLocale() { async rebuildTree () {
this.loading = true
this.$store.commit(`loadingStart`, 'admin-utilities-content-rebuildtree')
try {
const respRaw = await this.$apollo.mutate({
mutation: utilityContentRebuildTreeMutation
})
const resp = _.get(respRaw, 'data.pages.rebuildTree.responseResult', {})
if (resp.succeeded) {
this.$store.commit('showNotification', {
message: 'Page Tree rebuilt successfully.',
style: 'success',
icon: 'check'
})
} else {
throw new Error(resp.message)
}
} catch (err) {
this.$store.commit('pushGraphError', err)
}
this.$store.commit(`loadingStop`, 'admin-utilities-content-rebuildtree')
this.loading = false
},
async migrateToLocale () {
this.loading = true this.loading = true
this.$store.commit(`loadingStart`, 'admin-utilities-content-migratelocale') this.$store.commit(`loadingStart`, 'admin-utilities-content-migratelocale')

View File

@ -204,6 +204,8 @@ import { get, sync } from 'vuex-pathify'
import _ from 'lodash' import _ from 'lodash'
import Cookies from 'js-cookie' import Cookies from 'js-cookie'
import movePageMutation from 'gql/common/common-pages-mutation-move.gql'
/* global siteLangs */ /* global siteLangs */
export default { export default {
@ -342,8 +344,26 @@ export default {
pageMove () { pageMove () {
this.movePageModal = true this.movePageModal = true
}, },
pageMoveRename ({ path, locale }) { async pageMoveRename ({ path, locale }) {
this.$store.commit(`loadingStart`, 'page-move')
try {
const resp = await this.$apollo.mutate({
mutation: movePageMutation,
variables: {
id: this.$store.get('page/id'),
destinationLocale: locale,
destinationPath: path
}
})
if (_.get(resp, 'data.pages.move.responseResult.succeeded', false)) {
window.location.replace(`/${locale}/${path}`)
} else {
throw new Error(_.get(resp, 'data.pages.move.responseResult.message', this.$t('common:error.unexpected')))
}
} catch (err) {
this.$store.commit('pushGraphError', err)
this.$store.commit(`loadingStop`, 'page-move')
}
}, },
pageDelete () { pageDelete () {
this.deletePageModal = true this.deletePageModal = true

View File

@ -98,6 +98,8 @@ import _ from 'lodash'
import { get } from 'vuex-pathify' import { get } from 'vuex-pathify'
import pageTreeQuery from 'gql/common/common-pages-query-tree.gql' import pageTreeQuery from 'gql/common/common-pages-query-tree.gql'
const localeSegmentRegex = /^[A-Z]{2}(-[A-Z]{2})?$/i
/* global siteLangs, siteConfig */ /* global siteLangs, siteConfig */
export default { export default {
@ -152,7 +154,22 @@ export default {
return _.sortBy(_.filter(this.pages, ['parent', _.head(this.currentNode) || 0]), ['title', 'path']) return _.sortBy(_.filter(this.pages, ['parent', _.head(this.currentNode) || 0]), ['title', 'path'])
}, },
isValidPath () { isValidPath () {
return this.currentPath && this.currentPath.length > 2 if (!this.currentPath) {
return false
}
const firstSection = _.head(this.currentPath.split('/'))
if (firstSection.length <= 1) {
return false
} else if (localeSegmentRegex.test(firstSection)) {
return false
} else if (
_.some(['login', 'logout', 'register', 'verify', 'favicons', 'fonts', 'img', 'js', 'svg'], p => {
return p === firstSection
})) {
return false
} else {
return true
}
} }
}, },
watch: { watch: {

View File

@ -249,6 +249,11 @@ export default {
style: 'success', style: 'success',
icon: 'check' icon: 'check'
}) })
if (this.locale !== this.$store.get('page/locale') || this.path !== this.$store.get('page/path')) {
_.delay(() => {
window.location.replace(`/e/${this.$store.get('page/locale')}/${this.$store.get('page/path')}`)
}, 1000)
}
} else { } else {
throw new Error(_.get(resp, 'responseResult.message')) throw new Error(_.get(resp, 'responseResult.message'))
} }

View File

@ -52,7 +52,6 @@
:items='namespaces' :items='namespaces'
v-model='locale' v-model='locale'
hide-details hide-details
:disabled='mode !== "create"'
) )
v-flex(xs12, md10) v-flex(xs12, md10)
v-text-field( v-text-field(
@ -63,7 +62,6 @@
:hint='$t(`editor:props.pathHint`)' :hint='$t(`editor:props.pathHint`)'
persistent-hint persistent-hint
@click:append='showPathSelector' @click:append='showPathSelector'
:disabled='mode !== "create"'
) )
v-divider v-divider
v-card-text.grey.pt-5(:class='darkMode ? `darken-3-d5` : `lighten-4`') v-card-text.grey.pt-5(:class='darkMode ? `darken-3-d5` : `lighten-4`')
@ -219,7 +217,7 @@
inset inset
) )
page-selector(mode='create', v-model='pageSelectorShown', :path='path', :locale='locale', :open-handler='setPath') page-selector(:mode='pageSelectorMode', v-model='pageSelectorShown', :path='path', :locale='locale', :open-handler='setPath')
</template> </template>
<script> <script>
@ -257,7 +255,10 @@ export default {
path: sync('page/path'), path: sync('page/path'),
isPublished: sync('page/isPublished'), isPublished: sync('page/isPublished'),
publishStartDate: sync('page/publishStartDate'), publishStartDate: sync('page/publishStartDate'),
publishEndDate: sync('page/publishEndDate') publishEndDate: sync('page/publishEndDate'),
pageSelectorMode () {
return (this.mode === 'create') ? 'create' : 'move'
}
}, },
watch: { watch: {
value(newValue, oldValue) { value(newValue, oldValue) {

View File

@ -0,0 +1,12 @@
mutation {
pages {
rebuildTree {
responseResult {
succeeded
errorCode
slug
message
}
}
}
}

View File

@ -0,0 +1,12 @@
mutation($id: Int!, $destinationPath: String!, $destinationLocale: String!) {
pages {
move(id: $id, destinationPath: $destinationPath, destinationLocale: $destinationLocale) {
responseResult {
succeeded
errorCode
slug
message
}
}
}
}

View File

@ -226,7 +226,7 @@
} }
blockquote { blockquote {
padding: 0 0 1rem 0; padding: 0 0 1rem 1rem;
border-left: 5px solid mc('blue', '500'); border-left: 5px solid mc('blue', '500');
border-radius: .5rem; border-radius: .5rem;
margin: 1rem 0; margin: 1rem 0;
@ -242,9 +242,8 @@
&.is-info { &.is-info {
background-color: mc('blue', '50'); background-color: mc('blue', '50');
background-image: radial-gradient(ellipse at top, mc('blue', '50'), lighten(mc('blue', '50'), 5%)); border-color: mc('blue', '300');
border-color: mc('blue', '100'); color: mc('blue', '900');
box-shadow: 0 0 2px 0 mc('blue', '100');
code { code {
background-color: mc('blue', '50'); background-color: mc('blue', '50');
@ -252,17 +251,15 @@
} }
@at-root .theme--dark & { @at-root .theme--dark & {
background-color: mc('grey', '900'); background-color: mc('blue', '900');
background-image: radial-gradient(ellipse at top, rgba(mc('blue', '900'), .25), rgba(darken(mc('blue', '900'), 5%), .2)); color: mc('blue', '50');
border-color: mc('blue', '500'); border-color: mc('blue', '500');
box-shadow: 0 0 2px 0 mc('grey', '900');
} }
} }
&.is-warning { &.is-warning {
background-color: mc('orange', '50'); background-color: mc('orange', '50');
background-image: radial-gradient(ellipse at top, mc('orange', '50'), lighten(mc('orange', '50'), 5%)); border-color: mc('orange', '300');
border-color: mc('orange', '100'); color: darken(mc('orange', '900'), 10%);
box-shadow: 0 0 2px 0 mc('orange', '100');
code { code {
background-color: mc('orange', '50'); background-color: mc('orange', '50');
@ -270,17 +267,16 @@
} }
@at-root .theme--dark & { @at-root .theme--dark & {
background-color: mc('grey', '900'); background-color: darken(mc('orange', '900'), 5%);
background-image: radial-gradient(ellipse at top, rgba(mc('orange', '900'), .25), rgba(darken(mc('orange', '900'), 5%), .2)); color: mc('orange', '100');
border-color: mc('orange', '500'); border-color: mc('orange', '500');
box-shadow: 0 0 2px 0 mc('grey', '900'); box-shadow: 0 0 2px 0 mc('grey', '900');
} }
} }
&.is-danger { &.is-danger {
background-color: mc('red', '50'); background-color: mc('red', '50');
background-image: radial-gradient(ellipse at top, mc('red', '50'), lighten(mc('red', '50'), 5%)); border-color: mc('red', '300');
border-color: mc('red', '100'); color: mc('red', '900');
box-shadow: 0 0 2px 0 mc('red', '100');
code { code {
background-color: mc('red', '50'); background-color: mc('red', '50');
@ -288,17 +284,15 @@
} }
@at-root .theme--dark & { @at-root .theme--dark & {
background-color: mc('grey', '900'); background-color: mc('red', '900');
background-image: radial-gradient(ellipse at top, rgba(mc('red', '900'), .1), rgba(darken(mc('red', '900'), 5%), .2)); color: mc('red', '100');
border-color: mc('red', '500'); border-color: mc('red', '500');
box-shadow: 0 0 2px 0 mc('grey', '900');
} }
} }
&.is-success { &.is-success {
background-color: mc('green', '50'); background-color: mc('green', '50');
background-image: radial-gradient(ellipse at top, mc('green', '50'), lighten(mc('green', '50'), 5%)); border-color: mc('green', '300');
border-color: mc('green', '100'); color: mc('green', '900');
box-shadow: 0 0 2px 0 mc('green', '100');
code { code {
background-color: mc('green', '50'); background-color: mc('green', '50');
@ -306,10 +300,9 @@
} }
@at-root .theme--dark & { @at-root .theme--dark & {
background-color: mc('grey', '900'); background-color: mc('green', '900');
background-image: radial-gradient(ellipse at top, rgba(mc('green', '900'), .4), rgba(darken(mc('green', '900'), 5%), .2)); color: mc('green', '50');
border-color: mc('green', '500'); border-color: mc('green', '500');
box-shadow: 0 0 2px 0 mc('grey', '900');
} }
} }
} }

View File

@ -165,7 +165,7 @@ module.exports = {
} }
// Check Page Rules // Check Page Rules
if (path && user.groups) { if (page && user.groups) {
let checkState = { let checkState = {
deny: false, deny: false,
match: false, match: false,

View File

@ -11,6 +11,9 @@ module.exports = {
async pages() { return {} } async pages() { return {} }
}, },
PageQuery: { PageQuery: {
/**
* PAGE HISTORY
*/
async history(obj, args, context, info) { async history(obj, args, context, info) {
return WIKI.models.pageHistory.getHistory({ return WIKI.models.pageHistory.getHistory({
pageId: args.id, pageId: args.id,
@ -18,6 +21,9 @@ module.exports = {
offsetSize: args.offsetSize || 100 offsetSize: args.offsetSize || 100
}) })
}, },
/**
* SEARCH PAGES
*/
async search (obj, args, context) { async search (obj, args, context) {
if (WIKI.data.searchEngine) { if (WIKI.data.searchEngine) {
const resp = await WIKI.data.searchEngine.query(args.query, args) const resp = await WIKI.data.searchEngine.query(args.query, args)
@ -38,6 +44,9 @@ module.exports = {
} }
} }
}, },
/**
* LIST PAGES
*/
async list (obj, args, context, info) { async list (obj, args, context, info) {
let results = await WIKI.models.pages.query().column([ let results = await WIKI.models.pages.query().column([
'pages.id', 'pages.id',
@ -101,6 +110,9 @@ module.exports = {
} }
return results return results
}, },
/**
* FETCH SINGLE PAGE
*/
async single (obj, args, context, info) { async single (obj, args, context, info) {
let page = await WIKI.models.pages.getPageFromDb(args.id) let page = await WIKI.models.pages.getPageFromDb(args.id)
if (page) { if (page) {
@ -113,9 +125,15 @@ module.exports = {
throw new WIKI.Error.PageNotFound() throw new WIKI.Error.PageNotFound()
} }
}, },
/**
* FETCH TAGS
*/
async tags (obj, args, context, info) { async tags (obj, args, context, info) {
return WIKI.models.tags.query().orderBy('tag', 'asc') return WIKI.models.tags.query().orderBy('tag', 'asc')
}, },
/**
* FETCH PAGE TREE
*/
async tree (obj, args, context, info) { async tree (obj, args, context, info) {
let results = [] let results = []
let conds = { let conds = {
@ -147,35 +165,75 @@ module.exports = {
} }
}, },
PageMutation: { PageMutation: {
/**
* CREATE PAGE
*/
async create(obj, args, context) { async create(obj, args, context) {
try {
const page = await WIKI.models.pages.createPage({ const page = await WIKI.models.pages.createPage({
...args, ...args,
authorId: context.req.user.id user: context.req.user
}) })
return { return {
responseResult: graphHelper.generateSuccess('Page created successfully.'), responseResult: graphHelper.generateSuccess('Page created successfully.'),
page page
} }
}, } catch (err) {
async delete(obj, args, context) { return graphHelper.generateError(err)
await WIKI.models.pages.deletePage({
...args,
authorId: context.req.user.id
})
return {
responseResult: graphHelper.generateSuccess('Page has been deleted.')
} }
}, },
/**
* UPDATE PAGE
*/
async update(obj, args, context) { async update(obj, args, context) {
try {
const page = await WIKI.models.pages.updatePage({ const page = await WIKI.models.pages.updatePage({
...args, ...args,
authorId: context.req.user.id user: context.req.user
}) })
return { return {
responseResult: graphHelper.generateSuccess('Page has been updated.'), responseResult: graphHelper.generateSuccess('Page has been updated.'),
page page
} }
} catch (err) {
return graphHelper.generateError(err)
}
}, },
/**
* MOVE PAGE
*/
async move(obj, args, context) {
try {
await WIKI.models.pages.movePage({
...args,
user: context.req.user
})
return {
responseResult: graphHelper.generateSuccess('Page has been moved.')
}
} catch (err) {
return graphHelper.generateError(err)
}
},
/**
* DELETE PAGE
*/
async delete(obj, args, context) {
try {
await WIKI.models.pages.deletePage({
...args,
user: context.req.user
})
return {
responseResult: graphHelper.generateSuccess('Page has been deleted.')
}
} catch (err) {
return graphHelper.generateError(err)
}
},
/**
* FLUSH PAGE CACHE
*/
async flushCache(obj, args, context) { async flushCache(obj, args, context) {
try { try {
await WIKI.models.pages.flushCache() await WIKI.models.pages.flushCache()
@ -186,6 +244,9 @@ module.exports = {
return graphHelper.generateError(err) return graphHelper.generateError(err)
} }
}, },
/**
* MIGRATE ALL PAGES FROM SOURCE LOCALE TO TARGET LOCALE
*/
async migrateToLocale(obj, args, context) { async migrateToLocale(obj, args, context) {
try { try {
const count = await WIKI.models.pages.migrateToLocale(args) const count = await WIKI.models.pages.migrateToLocale(args)
@ -196,6 +257,24 @@ module.exports = {
} catch (err) { } catch (err) {
return graphHelper.generateError(err) return graphHelper.generateError(err)
} }
},
/**
* REBUILD TREE
*/
async rebuildTree(obj, args, context) {
try {
const rebuildJob = await WIKI.scheduler.registerJob({
name: 'rebuild-tree',
immediate: true,
worker: true
})
await rebuildJob.finished
return {
responseResult: graphHelper.generateSuccess('Page tree rebuilt successfully.')
}
} catch (err) {
return graphHelper.generateError(err)
}
} }
}, },
Page: { Page: {

View File

@ -80,7 +80,13 @@ type PageMutation {
publishStartDate: Date publishStartDate: Date
tags: [String] tags: [String]
title: String title: String
): PageResponse @auth(requires: ["manage:pages", "manage:system"]) ): PageResponse @auth(requires: ["write:pages", "manage:pages", "manage:system"])
move(
id: Int!
destinationPath: String!
destinationLocale: String!
): DefaultResponse @auth(requires: ["manage:pages", "manage:system"])
delete( delete(
id: Int! id: Int!
@ -92,6 +98,8 @@ type PageMutation {
sourceLocale: String! sourceLocale: String!
targetLocale: String! targetLocale: String!
): PageMigrationResponse @auth(requires: ["manage:system"]) ): PageMigrationResponse @auth(requires: ["manage:system"])
rebuildTree: DefaultResponse @auth(requires: ["manage:system"])
} }
# ----------------------------------------------- # -----------------------------------------------

View File

@ -117,6 +117,14 @@ module.exports = {
message: 'Mail template failed to load.', message: 'Mail template failed to load.',
code: 3003 code: 3003
}), }),
PageCreateForbidden: CustomError('PageCreateForbidden', {
message: 'You are not authorized to create this page.',
code: 6008
}),
PageDeleteForbidden: CustomError('PageDeleteForbidden', {
message: 'You are not authorized to delete this page.',
code: 6010
}),
PageGenericError: CustomError('PageGenericError', { PageGenericError: CustomError('PageGenericError', {
message: 'An unexpected error occured during a page operation.', message: 'An unexpected error occured during a page operation.',
code: 6001 code: 6001
@ -133,10 +141,22 @@ module.exports = {
message: 'Page path cannot contains illegal characters.', message: 'Page path cannot contains illegal characters.',
code: 6005 code: 6005
}), }),
PageMoveForbidden: CustomError('PageMoveForbidden', {
message: 'You are not authorized to move this page.',
code: 6007
}),
PageNotFound: CustomError('PageNotFound', { PageNotFound: CustomError('PageNotFound', {
message: 'This page does not exist.', message: 'This page does not exist.',
code: 6003 code: 6003
}), }),
PagePathCollision: CustomError('PagePathCollision', {
message: 'Destination page path already exists.',
code: 6006
}),
PageUpdateForbidden: CustomError('PageUpdateForbidden', {
message: 'You are not authorized to update this page.',
code: 6009
}),
SearchActivationFailed: CustomError('SearchActivationFailed', { SearchActivationFailed: CustomError('SearchActivationFailed', {
message: 'Search Engine activation failed.', message: 'Search Engine activation failed.',
code: 4002 code: 4002

View File

@ -213,23 +213,35 @@ module.exports = class Page extends Model {
* @returns {Promise} Promise of the Page Model Instance * @returns {Promise} Promise of the Page Model Instance
*/ */
static async createPage(opts) { static async createPage(opts) {
// -> Validate path
if (opts.path.indexOf('.') >= 0 || opts.path.indexOf(' ') >= 0) { if (opts.path.indexOf('.') >= 0 || opts.path.indexOf(' ') >= 0) {
throw new WIKI.Error.PageIllegalPath() throw new WIKI.Error.PageIllegalPath()
} }
// -> Check for page access
if (!WIKI.auth.checkAccess(opts.user, ['write:pages'], {
locale: opts.locale,
path: opts.path
})) {
throw new WIKI.Error.PageDeleteForbidden()
}
// -> Check for duplicate
const dupCheck = await WIKI.models.pages.query().select('id').where('localeCode', opts.locale).where('path', opts.path).first() const dupCheck = await WIKI.models.pages.query().select('id').where('localeCode', opts.locale).where('path', opts.path).first()
if (dupCheck) { if (dupCheck) {
throw new WIKI.Error.PageDuplicateCreate() throw new WIKI.Error.PageDuplicateCreate()
} }
// -> Check for empty content
if (!opts.content || _.trim(opts.content).length < 1) { if (!opts.content || _.trim(opts.content).length < 1) {
throw new WIKI.Error.PageEmptyContent() throw new WIKI.Error.PageEmptyContent()
} }
// -> Create page
await WIKI.models.pages.query().insert({ await WIKI.models.pages.query().insert({
authorId: opts.authorId, authorId: opts.user.id,
content: opts.content, content: opts.content,
creatorId: opts.authorId, creatorId: opts.user.id,
contentType: _.get(_.find(WIKI.data.editors, ['key', opts.editor]), `contentType`, 'text'), contentType: _.get(_.find(WIKI.data.editors, ['key', opts.editor]), `contentType`, 'text'),
description: opts.description, description: opts.description,
editorKey: opts.editor, editorKey: opts.editor,
@ -246,7 +258,7 @@ module.exports = class Page extends Model {
const page = await WIKI.models.pages.getPageFromDb({ const page = await WIKI.models.pages.getPageFromDb({
path: opts.path, path: opts.path,
locale: opts.locale, locale: opts.locale,
userId: opts.authorId, userId: opts.user.id,
isPrivate: opts.isPrivate isPrivate: opts.isPrivate
}) })
@ -288,22 +300,35 @@ module.exports = class Page extends Model {
* @returns {Promise} Promise of the Page Model Instance * @returns {Promise} Promise of the Page Model Instance
*/ */
static async updatePage(opts) { static async updatePage(opts) {
// -> Fetch original page
const ogPage = await WIKI.models.pages.query().findById(opts.id) const ogPage = await WIKI.models.pages.query().findById(opts.id)
if (!ogPage) { if (!ogPage) {
throw new Error('Invalid Page Id') throw new Error('Invalid Page Id')
} }
// -> Check for page access
if (!WIKI.auth.checkAccess(opts.user, ['write:pages'], {
locale: opts.locale,
path: opts.path
})) {
throw new WIKI.Error.PageUpdateForbidden()
}
// -> Check for empty content
if (!opts.content || _.trim(opts.content).length < 1) { if (!opts.content || _.trim(opts.content).length < 1) {
throw new WIKI.Error.PageEmptyContent() throw new WIKI.Error.PageEmptyContent()
} }
// -> Create version snapshot
await WIKI.models.pageHistory.addVersion({ await WIKI.models.pageHistory.addVersion({
...ogPage, ...ogPage,
isPublished: ogPage.isPublished === true || ogPage.isPublished === 1, isPublished: ogPage.isPublished === true || ogPage.isPublished === 1,
action: 'updated' action: 'updated'
}) })
// -> Update page
await WIKI.models.pages.query().patch({ await WIKI.models.pages.query().patch({
authorId: opts.authorId, authorId: opts.user.id,
content: opts.content, content: opts.content,
description: opts.description, description: opts.description,
isPublished: opts.isPublished === true || opts.isPublished === 1, isPublished: opts.isPublished === true || opts.isPublished === 1,
@ -311,7 +336,7 @@ module.exports = class Page extends Model {
publishStartDate: opts.publishStartDate || '', publishStartDate: opts.publishStartDate || '',
title: opts.title title: opts.title
}).where('id', ogPage.id) }).where('id', ogPage.id)
const page = await WIKI.models.pages.getPageFromDb({ let page = await WIKI.models.pages.getPageFromDb({
path: ogPage.path, path: ogPage.path,
locale: ogPage.localeCode, locale: ogPage.localeCode,
userId: ogPage.authorId, userId: ogPage.authorId,
@ -336,9 +361,106 @@ module.exports = class Page extends Model {
page page
}) })
} }
// -> Perform move?
if (opts.locale !== page.localeCode || opts.path !== page.path) {
await WIKI.models.pages.movePage({
id: page.id,
destinationLocale: opts.locale,
destinationPath: opts.path,
user: opts.user
})
}
return page return page
} }
/**
* Move a Page
*
* @param {Object} opts Page Properties
* @returns {Promise} Promise with no value
*/
static async movePage(opts) {
const page = await WIKI.models.pages.query().findById(opts.id)
if (!page) {
throw new WIKI.Error.PageNotFound()
}
// -> Check for source page access
if (!WIKI.auth.checkAccess(opts.user, ['manage:pages'], {
locale: page.sourceLocale,
path: page.sourcePath
})) {
throw new WIKI.Error.PageMoveForbidden()
}
// -> Check for destination page access
if (!WIKI.auth.checkAccess(opts.user, ['write:pages'], {
locale: opts.destinationLocale,
path: opts.destinationPath
})) {
throw new WIKI.Error.PageMoveForbidden()
}
// -> Check for existing page at destination path
const destPage = await await WIKI.models.pages.query().findOne({
path: opts.destinationPath,
localeCode: opts.destinationLocale
})
if (destPage) {
throw new WIKI.Error.PagePathCollision()
}
// -> Create version snapshot
await WIKI.models.pageHistory.addVersion({
...page,
action: 'moved'
})
const destinationHash = pageHelper.generateHash({ path: opts.destinationPath, locale: opts.destinationLocale, privateNS: opts.isPrivate ? 'TODO' : '' })
// -> Move page
await WIKI.models.pages.query().patch({
path: opts.destinationPath,
localeCode: opts.destinationLocale,
hash: destinationHash
}).findById(page.id)
await WIKI.models.pages.deletePageFromCache(page)
// -> Rename in Search Index
await WIKI.data.searchEngine.renamed({
...page,
destinationPath: opts.destinationPath,
destinationLocaleCode: opts.destinationLocale,
destinationHash
})
// -> Rename in Storage
if (!opts.skipStorage) {
await WIKI.models.storage.pageEvent({
event: 'renamed',
page: {
...page,
destinationPath: opts.destinationPath,
destinationLocaleCode: opts.destinationLocale,
destinationHash,
moveAuthorId: opts.user.id,
moveAuthorName: opts.user.name,
moveAuthorEmail: opts.user.email
}
})
}
// -> Reconnect Links
await WIKI.models.pages.reconnectLinks({
sourceLocale: page.localeCode,
sourcePath: page.path,
locale: opts.destinationLocale,
path: opts.destinationPath,
mode: 'move'
})
}
/** /**
* Delete an Existing Page * Delete an Existing Page
* *
@ -358,10 +480,22 @@ module.exports = class Page extends Model {
if (!page) { if (!page) {
throw new Error('Invalid Page Id') throw new Error('Invalid Page Id')
} }
// -> Check for page access
if (!WIKI.auth.checkAccess(opts.user, ['delete:pages'], {
locale: page.locale,
path: page.path
})) {
throw new WIKI.Error.PageDeleteForbidden()
}
// -> Create version snapshot
await WIKI.models.pageHistory.addVersion({ await WIKI.models.pageHistory.addVersion({
...page, ...page,
action: 'deleted' action: 'deleted'
}) })
// -> Delete page
await WIKI.models.pages.query().delete().where('id', page.id) await WIKI.models.pages.query().delete().where('id', page.id)
await WIKI.models.pages.deletePageFromCache(page) await WIKI.models.pages.deletePageFromCache(page)

View File

@ -109,10 +109,10 @@ module.exports = {
* @param {Object} page Page to rename * @param {Object} page Page to rename
*/ */
async renamed(page) { async renamed(page) {
await this.index.deleteObject(page.sourceHash) await this.index.deleteObject(page.hash)
await this.index.addObject({ await this.index.addObject({
objectID: page.destinationHash, objectID: page.destinationHash,
locale: page.localeCode, locale: page.destinationLocaleCode,
path: page.destinationPath, path: page.destinationPath,
title: page.title, title: page.title,
description: page.description, description: page.description,

View File

@ -255,7 +255,7 @@ module.exports = {
documents: JSON.stringify([ documents: JSON.stringify([
{ {
type: 'delete', type: 'delete',
id: page.sourceHash id: page.hash
} }
]) ])
}).promise() }).promise()
@ -266,7 +266,7 @@ module.exports = {
type: 'add', type: 'add',
id: page.destinationHash, id: page.destinationHash,
fields: { fields: {
locale: page.localeCode, locale: page.destinationLocaleCode,
path: page.destinationPath, path: page.destinationPath,
title: page.title, title: page.title,
description: page.description, description: page.description,

View File

@ -191,13 +191,13 @@ module.exports = {
await this.client.indexes.use(this.config.indexName).index([ await this.client.indexes.use(this.config.indexName).index([
{ {
'@search.action': 'delete', '@search.action': 'delete',
id: page.sourceHash id: page.hash
} }
]) ])
await this.client.indexes.use(this.config.indexName).index([ await this.client.indexes.use(this.config.indexName).index([
{ {
id: page.destinationHash, id: page.destinationHash,
locale: page.localeCode, locale: page.destinationLocaleCode,
path: page.destinationPath, path: page.destinationPath,
title: page.title, title: page.title,
description: page.description, description: page.description,

View File

@ -210,7 +210,7 @@ module.exports = {
await this.client.delete({ await this.client.delete({
index: this.config.indexName, index: this.config.indexName,
type: '_doc', type: '_doc',
id: page.sourceHash, id: page.hash,
refresh: true refresh: true
}) })
await this.client.index({ await this.client.index({
@ -219,7 +219,7 @@ module.exports = {
id: page.destinationHash, id: page.destinationHash,
body: { body: {
suggest: this.buildSuggest(page), suggest: this.buildSuggest(page),
locale: page.localeCode, locale: page.destinationLocaleCode,
path: page.destinationPath, path: page.destinationPath,
title: page.title, title: page.title,
description: page.description, description: page.description,

View File

@ -129,7 +129,7 @@ module.exports = {
locale: page.localeCode, locale: page.localeCode,
path: page.sourcePath path: page.sourcePath
}).update({ }).update({
locale: page.localeCode, locale: page.destinationLocaleCode,
path: page.destinationPath path: page.destinationPath
}) })
}, },

View File

@ -44,7 +44,7 @@ module.exports = {
}, },
async created(page) { async created(page) {
WIKI.logger.info(`(STORAGE/DISK) Creating file ${page.path}...`) WIKI.logger.info(`(STORAGE/DISK) Creating file ${page.path}...`)
let fileName = `${page.path}.${page.getFileExtension()}` let fileName = `${page.path}.${pageHelper.getFileExtension(page.contentType)}`
if (WIKI.config.lang.code !== page.localeCode) { if (WIKI.config.lang.code !== page.localeCode) {
fileName = `${page.localeCode}/${fileName}` fileName = `${page.localeCode}/${fileName}`
} }
@ -53,7 +53,7 @@ module.exports = {
}, },
async updated(page) { async updated(page) {
WIKI.logger.info(`(STORAGE/DISK) Updating file ${page.path}...`) WIKI.logger.info(`(STORAGE/DISK) Updating file ${page.path}...`)
let fileName = `${page.path}.${page.getFileExtension()}` let fileName = `${page.path}.${pageHelper.getFileExtension(page.contentType)}`
if (WIKI.config.lang.code !== page.localeCode) { if (WIKI.config.lang.code !== page.localeCode) {
fileName = `${page.localeCode}/${fileName}` fileName = `${page.localeCode}/${fileName}`
} }
@ -62,7 +62,7 @@ module.exports = {
}, },
async deleted(page) { async deleted(page) {
WIKI.logger.info(`(STORAGE/DISK) Deleting file ${page.path}...`) WIKI.logger.info(`(STORAGE/DISK) Deleting file ${page.path}...`)
let fileName = `${page.path}.${page.getFileExtension()}` let fileName = `${page.path}.${pageHelper.getFileExtension(page.contentType)}`
if (WIKI.config.lang.code !== page.localeCode) { if (WIKI.config.lang.code !== page.localeCode) {
fileName = `${page.localeCode}/${fileName}` fileName = `${page.localeCode}/${fileName}`
} }
@ -70,14 +70,19 @@ module.exports = {
await fs.unlink(filePath) await fs.unlink(filePath)
}, },
async renamed(page) { async renamed(page) {
WIKI.logger.info(`(STORAGE/DISK) Renaming file ${page.sourcePath} to ${page.destinationPath}...`) WIKI.logger.info(`(STORAGE/DISK) Renaming file ${page.path} to ${page.destinationPath}...`)
let sourceFilePath = `${page.sourcePath}.${page.getFileExtension()}` let sourceFilePath = `${page.path}.${pageHelper.getFileExtension(page.contentType)}`
let destinationFilePath = `${page.destinationPath}.${page.getFileExtension()}` let destinationFilePath = `${page.destinationPath}.${pageHelper.getFileExtension(page.contentType)}`
if (WIKI.config.lang.namespacing) {
if (WIKI.config.lang.code !== page.localeCode) { if (WIKI.config.lang.code !== page.localeCode) {
sourceFilePath = `${page.localeCode}/${sourceFilePath}` sourceFilePath = `${page.localeCode}/${sourceFilePath}`
destinationFilePath = `${page.localeCode}/${destinationFilePath}`
} }
if (WIKI.config.lang.code !== page.destinationLocaleCode) {
destinationFilePath = `${page.destinationLocaleCode}/${destinationFilePath}`
}
}
await fs.move(path.join(this.config.path, sourceFilePath), path.join(this.config.path, destinationFilePath), { overwrite: true }) await fs.move(path.join(this.config.path, sourceFilePath), path.join(this.config.path, destinationFilePath), { overwrite: true })
}, },
@ -93,7 +98,7 @@ module.exports = {
new stream.Transform({ new stream.Transform({
objectMode: true, objectMode: true,
transform: async (page, enc, cb) => { transform: async (page, enc, cb) => {
let fileName = `${page.path}.${page.getFileExtension()}` let fileName = `${page.path}.${pageHelper.getFileExtension(page.contentType)}`
if (WIKI.config.lang.code !== page.localeCode) { if (WIKI.config.lang.code !== page.localeCode) {
fileName = `${page.localeCode}/${fileName}` fileName = `${page.localeCode}/${fileName}`
} }

View File

@ -247,7 +247,7 @@ module.exports = {
*/ */
async created(page) { async created(page) {
WIKI.logger.info(`(STORAGE/GIT) Committing new file ${page.path}...`) WIKI.logger.info(`(STORAGE/GIT) Committing new file ${page.path}...`)
let fileName = `${page.path}.${page.getFileExtension()}` let fileName = `${page.path}.${pageHelper.getFileExtension(page.contentType)}`
if (WIKI.config.lang.namespacing && WIKI.config.lang.code !== page.localeCode) { if (WIKI.config.lang.namespacing && WIKI.config.lang.code !== page.localeCode) {
fileName = `${page.localeCode}/${fileName}` fileName = `${page.localeCode}/${fileName}`
} }
@ -266,7 +266,7 @@ module.exports = {
*/ */
async updated(page) { async updated(page) {
WIKI.logger.info(`(STORAGE/GIT) Committing updated file ${page.path}...`) WIKI.logger.info(`(STORAGE/GIT) Committing updated file ${page.path}...`)
let fileName = `${page.path}.${page.getFileExtension()}` let fileName = `${page.path}.${pageHelper.getFileExtension(page.contentType)}`
if (WIKI.config.lang.namespacing && WIKI.config.lang.code !== page.localeCode) { if (WIKI.config.lang.namespacing && WIKI.config.lang.code !== page.localeCode) {
fileName = `${page.localeCode}/${fileName}` fileName = `${page.localeCode}/${fileName}`
} }
@ -285,7 +285,7 @@ module.exports = {
*/ */
async deleted(page) { async deleted(page) {
WIKI.logger.info(`(STORAGE/GIT) Committing removed file ${page.path}...`) WIKI.logger.info(`(STORAGE/GIT) Committing removed file ${page.path}...`)
let fileName = `${page.path}.${page.getFileExtension()}` let fileName = `${page.path}.${pageHelper.getFileExtension(page.contentType)}`
if (WIKI.config.lang.namespacing && WIKI.config.lang.code !== page.localeCode) { if (WIKI.config.lang.namespacing && WIKI.config.lang.code !== page.localeCode) {
fileName = `${page.localeCode}/${fileName}` fileName = `${page.localeCode}/${fileName}`
} }
@ -301,18 +301,22 @@ module.exports = {
* @param {Object} page Page to rename * @param {Object} page Page to rename
*/ */
async renamed(page) { async renamed(page) {
WIKI.logger.info(`(STORAGE/GIT) Committing file move from ${page.sourcePath} to ${page.destinationPath}...`) WIKI.logger.info(`(STORAGE/GIT) Committing file move from ${page.path} to ${page.destinationPath}...`)
let sourceFilePath = `${page.sourcePath}.${page.getFileExtension()}` let sourceFilePath = `${page.path}.${pageHelper.getFileExtension(page.contentType)}`
let destinationFilePath = `${page.destinationPath}.${page.getFileExtension()}` let destinationFilePath = `${page.destinationPath}.${pageHelper.getFileExtension(page.contentType)}`
if (WIKI.config.lang.namespacing && WIKI.config.lang.code !== page.localeCode) { if (WIKI.config.lang.namespacing) {
if (WIKI.config.lang.code !== page.localeCode) {
sourceFilePath = `${page.localeCode}/${sourceFilePath}` sourceFilePath = `${page.localeCode}/${sourceFilePath}`
destinationFilePath = `${page.localeCode}/${destinationFilePath}` }
if (WIKI.config.lang.code !== page.destinationLocaleCode) {
destinationFilePath = `${page.destinationLocaleCode}/${destinationFilePath}`
}
} }
await this.git.mv(`./${sourceFilePath}`, `./${destinationFilePath}`) await this.git.mv(`./${sourceFilePath}`, `./${destinationFilePath}`)
await this.git.commit(`docs: rename ${page.sourcePath} to ${destinationFilePath}`, destinationFilePath, { await this.git.commit(`docs: rename ${page.path} to ${page.destinationPath}`, destinationFilePath, {
'--author': `"${page.authorName} <${page.authorEmail}>"` '--author': `"${page.moveAuthorName} <${page.moveAuthorEmail}>"`
}) })
}, },
/** /**

View File

@ -1,4 +1,5 @@
const S3 = require('aws-sdk/clients/s3') const S3 = require('aws-sdk/clients/s3')
const pageHelper = require('../../../helpers/page.js')
/* global WIKI */ /* global WIKI */
@ -6,7 +7,7 @@ const S3 = require('aws-sdk/clients/s3')
* Deduce the file path given the `page` object and the object's key to the page's path. * Deduce the file path given the `page` object and the object's key to the page's path.
*/ */
const getFilePath = (page, pathKey) => { const getFilePath = (page, pathKey) => {
const fileName = `${page[pathKey]}.${page.getFileExtension()}` const fileName = `${page[pathKey]}.${pageHelper.getFileExtension(page.contentType)}`
const withLocaleCode = WIKI.config.lang.namespacing && WIKI.config.lang.code !== page.localeCode const withLocaleCode = WIKI.config.lang.namespacing && WIKI.config.lang.code !== page.localeCode
return withLocaleCode ? `${page.localeCode}/${fileName}` : fileName return withLocaleCode ? `${page.localeCode}/${fileName}` : fileName
} }
@ -55,9 +56,17 @@ module.exports = class S3CompatibleStorage {
await this.s3.deleteObject({ Key: filePath }).promise() await this.s3.deleteObject({ Key: filePath }).promise()
} }
async renamed(page) { async renamed(page) {
WIKI.logger.info(`(STORAGE/${this.storageName}) Renaming file ${page.sourcePath} to ${page.destinationPath}...`) WIKI.logger.info(`(STORAGE/${this.storageName}) Renaming file ${page.path} to ${page.destinationPath}...`)
const sourceFilePath = getFilePath(page, 'sourcePath') let sourceFilePath = `${page.path}.${page.getFileExtension()}`
const destinationFilePath = getFilePath(page, 'destinationPath') let destinationFilePath = `${page.destinationPath}.${page.getFileExtension()}`
if (WIKI.config.lang.namespacing) {
if (WIKI.config.lang.code !== page.localeCode) {
sourceFilePath = `${page.localeCode}/${sourceFilePath}`
}
if (WIKI.config.lang.code !== page.destinationLocaleCode) {
destinationFilePath = `${page.destinationLocaleCode}/${destinationFilePath}`
}
}
await this.s3.copyObject({ CopySource: sourceFilePath, Key: destinationFilePath }).promise() await this.s3.copyObject({ CopySource: sourceFilePath, Key: destinationFilePath }).promise()
await this.s3.deleteObject({ Key: sourceFilePath }).promise() await this.s3.deleteObject({ Key: sourceFilePath }).promise()
} }