diff --git a/client/components/history.vue b/client/components/history.vue index b90fe13a..b22055f1 100644 --- a/client/components/history.vue +++ b/client/components/history.vue @@ -55,7 +55,7 @@ v-list-item(@click='download(ph.versionId)') v-list-item-avatar(size='24'): v-icon mdi-cloud-download-outline v-list-item-title Download Version - v-list-item(@click='restore(ph.versionId)', :disabled='ph.versionId === 0') + v-list-item(@click='restore(ph.versionId, ph.versionDate)', :disabled='ph.versionId === 0') v-list-item-avatar(size='24'): v-icon(:disabled='ph.versionId === 0') mdi-history v-list-item-title Restore v-list-item(@click='branchOff(ph.versionId)') @@ -111,6 +111,17 @@ .overline View Mode v-card.mt-3(light, v-html='diffHTML', flat) + v-dialog(v-model='isRestoreConfirmDialogShown', max-width='650', persistent) + v-card + .dialog-header.is-orange {{$t('history:restore.confirmTitle')}} + v-card-text.pa-4 + i18next(tag='span', path='history:restore.confirmText') + strong(place='date') {{ restoreTarget.versionDate | moment('LLL') }} + v-card-actions + v-spacer + v-btn(text, @click='isRestoreConfirmDialogShown = false', :disabled='restoreLoading') {{$t('common:actions.cancel')}} + v-btn(color='orange darken-2', dark, @click='restoreConfirm', :loading='restoreLoading') {{$t('history:restore.confirmButton')}} + nav-footer notify search-results @@ -124,6 +135,7 @@ import _ from 'lodash' import gql from 'graphql-tag' export default { + i18nOptions: { namespaces: 'history' }, props: { pageId: { type: Number, @@ -194,7 +206,13 @@ export default { offsetPage: 0, total: 0, viewMode: 'line-by-line', - cache: [] + cache: [], + restoreTarget: { + versionId: 0, + versionDate: '' + }, + isRestoreConfirmDialogShown: false, + restoreLoading: false } }, computed: { @@ -335,8 +353,59 @@ export default { download (versionId) { window.location.assign(`/d/${this.locale}/${this.path}?v=${versionId}`) }, - restore (versionId) { - + restore (versionId, versionDate) { + this.restoreTarget = { + versionId, + versionDate + } + this.isRestoreConfirmDialogShown = true + }, + async restoreConfirm () { + this.restoreLoading = true + this.$store.commit(`loadingStart`, 'history-restore') + try { + const resp = await this.$apollo.mutate({ + mutation: gql` + mutation ($pageId: Int!, $versionId: Int!) { + pages { + restore (pageId: $pageId, versionId: $versionId) { + responseResult { + succeeded + errorCode + slug + message + } + } + } + } + `, + variables: { + versionId: this.restoreTarget.versionId, + pageId: this.pageId + } + }) + if (_.get(resp, 'data.pages.restore.responseResult.succeeded', false) === true) { + this.$store.commit('showNotification', { + style: 'success', + message: this.$t('history:restore.success'), + icon: 'check' + }) + this.isRestoreConfirmDialogShown = false + setTimeout(() => { + window.location.assign(`/${this.locale}/${this.path}`) + }, 1000) + } else { + throw new Error(_.get(resp, 'data.pages.restore.responseResult.message', 'An unexpected error occured')) + } + } catch (err) { + this.$store.commit('showNotification', { + style: 'red', + message: err.message, + icon: 'alert' + }) + } + this.$store.commit(`loadingStop`, 'history-restore') + this.restoreLoading = false }, branchOff (versionId) { diff --git a/server/graph/resolvers/page.js b/server/graph/resolvers/page.js index 7039834d..7052d76d 100644 --- a/server/graph/resolvers/page.js +++ b/server/graph/resolvers/page.js @@ -385,7 +385,7 @@ module.exports = { try { const page = await WIKI.models.pages.query().findById(args.id) if (!page) { - throw new Error('Invalid Page Id') + throw new WIKI.Error.PageNotFound() } await WIKI.models.pages.renderPage(page) return { @@ -394,6 +394,42 @@ module.exports = { } catch (err) { return graphHelper.generateError(err) } + }, + /** + * RESTORE PAGE VERSION + */ + async restore (obj, args, context) { + try { + const page = await WIKI.models.pages.query().select('path', 'localeCode').findById(args.pageId) + if (!page) { + throw new WIKI.Error.PageNotFound() + } + + if (!WIKI.auth.checkAccess(context.req.user, ['write:pages'], { + path: page.path, + locale: page.localeCode + })) { + throw new WIKI.Error.PageRestoreForbidden() + } + + const targetVersion = await WIKI.models.pageHistory.getVersion({ pageId: args.pageId, versionId: args.versionId }) + if (!targetVersion) { + throw new WIKI.Error.PageNotFound() + } + + await WIKI.models.pages.updatePage({ + ...targetVersion, + id: targetVersion.pageId, + user: context.req.user, + action: 'restored' + }) + + return { + responseResult: graphHelper.generateSuccess('Page version restored successfully.') + } + } catch (err) { + return graphHelper.generateError(err) + } } }, Page: { diff --git a/server/graph/schemas/page.graphql b/server/graph/schemas/page.graphql index f17daa01..0eb9b319 100644 --- a/server/graph/schemas/page.graphql +++ b/server/graph/schemas/page.graphql @@ -129,6 +129,11 @@ type PageMutation { render( id: Int! ): DefaultResponse @auth(requires: ["manage:system"]) + + restore( + pageId: Int! + versionId: Int! + ): DefaultResponse @auth(requires: ["write:pages", "manage:pages", "manage:system"]) } # ----------------------------------------------- diff --git a/server/helpers/error.js b/server/helpers/error.js index 560162be..c7da98fa 100644 --- a/server/helpers/error.js +++ b/server/helpers/error.js @@ -153,6 +153,10 @@ module.exports = { message: 'Destination page path already exists.', code: 6006 }), + PageRestoreForbidden: CustomError('PageRestoreForbidden', { + message: 'You are not authorized to restore this page version.', + code: 6011 + }), PageUpdateForbidden: CustomError('PageUpdateForbidden', { message: 'You are not authorized to update this page.', code: 6009 diff --git a/server/models/pages.js b/server/models/pages.js index 21bce9f8..265cb767 100644 --- a/server/models/pages.js +++ b/server/models/pages.js @@ -326,7 +326,8 @@ module.exports = class Page extends Model { await WIKI.models.pageHistory.addVersion({ ...ogPage, isPublished: ogPage.isPublished === true || ogPage.isPublished === 1, - action: 'updated' + action: opts.action ? opts.action : 'updated', + versionDate: ogPage.updatedAt }) // -> Update page @@ -422,7 +423,8 @@ module.exports = class Page extends Model { // -> Create version snapshot await WIKI.models.pageHistory.addVersion({ ...page, - action: 'moved' + action: 'moved', + versionDate: page.updatedAt }) const destinationHash = pageHelper.generateHash({ path: opts.destinationPath, locale: opts.destinationLocale, privateNS: opts.isPrivate ? 'TODO' : '' }) @@ -503,7 +505,8 @@ module.exports = class Page extends Model { // -> Create version snapshot await WIKI.models.pageHistory.addVersion({ ...page, - action: 'deleted' + action: 'deleted', + versionDate: page.updatedAt }) // -> Delete page