From 803d86ff6397a8838543465ef11d825e60cb3a60 Mon Sep 17 00:00:00 2001 From: NGPixel Date: Sun, 22 Jul 2018 21:13:01 -0400 Subject: [PATCH] feat: save page - updated + page history --- client/components/editor.vue | 99 ++++++++++++----- .../editor/editor-modal-properties.vue | 17 ++- client/components/setup.vue | 39 ++++--- client/graph/editor/update.gql | 12 +++ client/store/editor.js | 1 + server/db/migrations/2.0.0.js | 26 +++++ server/db/models/pageHistory.js | 100 ++++++++++++++++++ server/db/models/pages.js | 30 +++++- server/db/models/storage.js | 4 +- server/graph/resolvers/page.js | 11 +- server/graph/schemas/page.graphql | 3 +- 11 files changed, 289 insertions(+), 53 deletions(-) create mode 100644 client/graph/editor/update.gql create mode 100644 server/db/models/pageHistory.js diff --git a/client/components/editor.vue b/client/components/editor.vue index 28f802f0..043acf3e 100644 --- a/client/components/editor.vue +++ b/client/components/editor.vue @@ -6,7 +6,9 @@ v-icon(color='green', left) check span.white--text(v-if='mode === "create"') {{ $t('common:actions.create') }} span.white--text(v-else) {{ $t('common:actions.save') }} - v-btn.is-icon(outline, color='red').mx-0: v-icon(color='red') close + v-btn(outline, color='red').mx-0 + v-icon(color='red', left) close + span.white--text {{ $t('common:actions.discard') }} v-btn(outline, color='blue', @click.native.stop='openModal(`properties`)', dark) v-icon(left) sort_by_alpha span.white--text {{ $t('editor:page') }} @@ -42,6 +44,7 @@ import { get, sync } from 'vuex-pathify' import { AtomSpinner } from 'epic-spinners' import createPageMutation from 'gql/editor/create.gql' +import updatePageMutation from 'gql/editor/update.gql' import editorStore from '@/store/editor' @@ -90,35 +93,79 @@ export default { }, async save() { this.showProgressDialog('saving') - if (this.$store.get('editor/mode') === 'create') { - const resp = await this.$apollo.mutate({ - mutation: createPageMutation, - variables: { - content: this.$store.get('editor/content'), - description: this.$store.get('editor/description'), - editor: 'markdown', - locale: this.$store.get('editor/locale'), - isPrivate: false, - isPublished: this.$store.get('editor/isPublished'), - path: this.$store.get('editor/path'), - publishEndDate: this.$store.get('editor/publishEndDate'), - publishStartDate: this.$store.get('editor/publishStartDate'), - tags: this.$store.get('editor/tags'), - title: this.$store.get('editor/title') - } - }) - if (_.get(resp, 'data.pages.create.responseResult.succeeded')) { - this.$store.commit('showNotification', { - message: this.$t('editor:save.success'), - style: 'success', - icon: 'check' + try { + if (this.$store.get('editor/mode') === 'create') { + // -------------------------------------------- + // -> CREATE PAGE + // -------------------------------------------- + + let resp = await this.$apollo.mutate({ + mutation: createPageMutation, + variables: { + content: this.$store.get('editor/content'), + description: this.$store.get('editor/description'), + editor: 'markdown', + locale: this.$store.get('editor/locale'), + isPrivate: false, + isPublished: this.$store.get('editor/isPublished'), + path: this.$store.get('editor/path'), + publishEndDate: this.$store.get('editor/publishEndDate'), + publishStartDate: this.$store.get('editor/publishStartDate'), + tags: this.$store.get('editor/tags'), + title: this.$store.get('editor/title') + } }) - this.$store.set('editor/mode', 'update') + resp = _.get(resp, 'data.pages.create', {}) + if (_.get(resp, 'responseResult.succeeded')) { + this.$store.commit('showNotification', { + message: this.$t('editor:save.success'), + style: 'success', + icon: 'check' + }) + this.$store.set('editor/id', _.get(resp, 'page.id')) + this.$store.set('editor/mode', 'update') + } else { + throw new Error(_.get(resp, 'responseResult.message')) + } } else { + // -------------------------------------------- + // -> UPDATE EXISTING PAGE + // -------------------------------------------- + let resp = await this.$apollo.mutate({ + mutation: updatePageMutation, + variables: { + id: this.$store.get('editor/id'), + content: this.$store.get('editor/content'), + description: this.$store.get('editor/description'), + editor: 'markdown', + locale: this.$store.get('editor/locale'), + isPrivate: false, + isPublished: this.$store.get('editor/isPublished'), + path: this.$store.get('editor/path'), + publishEndDate: this.$store.get('editor/publishEndDate'), + publishStartDate: this.$store.get('editor/publishStartDate'), + tags: this.$store.get('editor/tags'), + title: this.$store.get('editor/title') + } + }) + resp = _.get(resp, 'data.pages.update', {}) + if (_.get(resp, 'responseResult.succeeded')) { + this.$store.commit('showNotification', { + message: this.$t('editor:save.success'), + style: 'success', + icon: 'check' + }) + } else { + throw new Error(_.get(resp, 'responseResult.message')) + } } - } else { - + } catch (err) { + this.$store.commit('showNotification', { + message: err.message, + style: 'error', + icon: 'warning' + }) } this.hideProgressDialog() } diff --git a/client/components/editor/editor-modal-properties.vue b/client/components/editor/editor-modal-properties.vue index 8cfb77ed..028c61ff 100644 --- a/client/components/editor/editor-modal-properties.vue +++ b/client/components/editor/editor-modal-properties.vue @@ -7,13 +7,24 @@ v-icon(color='white') sort_by_alpha .subheading.white--text.ml-2 Page Properties v-spacer - v-btn( + v-btn.mx-0( outline dark @click.native='close' ) - v-icon(left) close - span Close + v-icon(left) check + span {{ $t('common:actions.ok') }} + v-menu + v-btn.is-icon( + slot='activator' + outline + dark + ) + v-icon more_horiz + v-list + v-list-tile + v-list-tile-avatar: v-icon delete + v-list-tile-title Delete Page v-card(tile) v-card-text v-subheader.pl-0 Page Info diff --git a/client/components/setup.vue b/client/components/setup.vue index d85f1eb2..7f19b2f5 100644 --- a/client/components/setup.vue +++ b/client/components/setup.vue @@ -50,7 +50,7 @@ .body-1.pt-3 svg.icons.is-18.is-outlined.has-right-pad.is-text: use(xlink:href='#nc-cd-reader') span You are about to install Wiki.js #[strong {{wikiVersion}}]. - v-divider + v-divider.mt-3 v-form v-checkbox( color='primary', @@ -67,7 +67,7 @@ hint='Check this box if you are upgrading from Wiki.js 1.x and wish to migrate your existing data.' ) v-divider - .text-xs-center + .pt-3.text-xs-center v-btn(color='primary', @click='proceedToSyscheck', :disabled='loading') Start //- ============================================== @@ -94,7 +94,7 @@ v-list-tile-title {{rs.title}} v-list-tile-sub-title {{rs.subtitle}} v-divider - .text-xs-center + .pt-3.text-xs-center v-btn(@click='proceedToWelcome', :disabled='loading') Back v-btn(color='primary', @click='proceedToSyscheck', v-if='!loading && !syscheck.ok') Check Again v-btn(color='red', dark, @click='proceedToGeneral', v-if='!loading && !syscheck.ok') Continue Anyway @@ -113,6 +113,8 @@ v-layout(row, wrap) v-flex(xs12, sm6).pr-3 v-text-field( + outline + background-color='grey lighten-2' v-model='conf.title', label='Site Title', :counter='255', @@ -126,6 +128,8 @@ ) v-flex.pr-3(xs12, sm6) v-text-field( + outline + background-color='grey lighten-2' v-model='conf.port', label='Server Port', persistent-hint, @@ -139,6 +143,8 @@ v-layout(row, wrap).mt-3 v-flex(xs12, sm6).pr-3 v-text-field( + outline + background-color='grey lighten-2' v-model='conf.pathContent', label='Content Data Path', persistent-hint, @@ -151,6 +157,8 @@ ) v-flex(xs12, sm6) v-text-field( + outline + background-color='grey lighten-2' v-model='conf.pathData', label='Temporary Data Path', persistent-hint, @@ -170,15 +178,8 @@ persistent-hint, hint='Should the site be accessible (read only) without login.' ) - v-checkbox.mt-2( - color='primary', - v-model='conf.selfRegister', - label='Allow Self-Registration', - persistent-hint, - hint='Can users create their own account to gain access?' - ) v-divider - .text-xs-center + .pt-3.text-xs-center v-btn(@click='proceedToSyscheck', :disabled='loading') Back v-btn(color='primary', @click='proceedToAdmin', :disabled='loading') Continue @@ -196,7 +197,8 @@ v-layout(row, wrap) v-flex(xs12) v-text-field( - autofocus + outline + background-color='grey lighten-2' v-model='conf.adminEmail', label='Administrator Email', hint='The email address of the administrator account', @@ -208,6 +210,8 @@ ) v-flex.pr-3(xs6) v-text-field( + outline + background-color='grey lighten-2' ref='adminPassword', counter='255' v-model='conf.adminPassword', @@ -224,6 +228,8 @@ ) v-flex(xs6) v-text-field( + outline + background-color='grey lighten-2' ref='adminPasswordConfirm', counter='255' v-model='conf.adminPasswordConfirm', @@ -238,7 +244,7 @@ data-vv-scope='admin', :error-messages='errors.collect(`adminPasswordConfirm`)' ) - .text-xs-center + .pt-3.text-xs-center v-btn(@click='proceedToGeneral', :disabled='loading') Back v-btn(color='primary', @click='proceedToUpgrade', :disabled='loading') Continue @@ -256,6 +262,8 @@ v-layout(row) v-flex(xs12) v-text-field( + outline + background-color='grey lighten-2' v-model='conf.upgMongo', placeholder='mongodb://', label='Connection String to Wiki.js 1.x MongoDB database', @@ -267,7 +275,7 @@ data-vv-scope='upgrade', :error-messages='errors.collect(`upgMongo`)' ) - .text-xs-center + .pt-3.text-xs-center v-btn(@click='proceedToAdmin', :disabled='loading') Back v-btn(color='primary', @click='proceedToFinal', :disabled='loading') Continue @@ -290,7 +298,7 @@ v-alert(type='success', outline, :value='!loading && final.ok') Wiki.js was configured successfully and is now ready for use. v-alert(type='error', outline, :value='!loading && !final.ok') {{ final.error }} v-divider - .text-xs-center + .pt-3.text-xs-center v-btn(@click='!conf.upgrade ? proceedToAdmin() : proceedToUpgrade()', :disabled='loading') Back v-btn(color='primary', @click='proceedToFinal', v-if='!loading && !final.ok') Try Again v-btn(color='success', @click='finish', v-if='loading || final.ok', :disabled='loading') Continue @@ -342,7 +350,6 @@ export default { pathContent: './content', port: siteConfig.port || 80, public: (siteConfig.public === true), - selfRegister: (siteConfig.selfRegister === true), telemetry: true, title: siteConfig.title || 'Wiki', upgrade: false, diff --git a/client/graph/editor/update.gql b/client/graph/editor/update.gql new file mode 100644 index 00000000..c6608e40 --- /dev/null +++ b/client/graph/editor/update.gql @@ -0,0 +1,12 @@ +mutation ($id: Int!, $content: String, $description: String, $editor: String, $isPrivate: Boolean, $isPublished: Boolean, $locale: String, $path: String, $publishEndDate: Date, $publishStartDate: Date, $tags: [String], $title: String) { + pages { + update(id: $id, content: $content, description: $description, editor: $editor, isPrivate: $isPrivate, isPublished: $isPublished, locale: $locale, path: $path, publishEndDate: $publishEndDate, publishStartDate: $publishStartDate, tags: $tags, title: $title) { + responseResult { + succeeded + errorCode + slug + message + } + } + } +} diff --git a/client/store/editor.js b/client/store/editor.js index b66ee8f5..1aa8677f 100644 --- a/client/store/editor.js +++ b/client/store/editor.js @@ -1,6 +1,7 @@ import { make } from 'vuex-pathify' const state = { + id: 0, content: '', description: '', isPublished: true, diff --git a/server/db/migrations/2.0.0.js b/server/db/migrations/2.0.0.js index 5010a2a6..690df034 100644 --- a/server/db/migrations/2.0.0.js +++ b/server/db/migrations/2.0.0.js @@ -68,6 +68,19 @@ exports.up = knex => { table.string('createdAt').notNullable() table.string('updatedAt').notNullable() }) + // PAGE HISTORY ------------------------ + .createTable('pageHistory', table => { + table.increments('id').primary() + table.string('path').notNullable() + table.string('title').notNullable() + table.string('description') + table.boolean('isPrivate').notNullable().defaultTo(false) + table.boolean('isPublished').notNullable().defaultTo(false) + table.string('publishStartDate') + table.string('publishEndDate') + table.text('content') + table.string('createdAt').notNullable() + }) // PAGES ------------------------------- .createTable('pages', table => { table.increments('id').primary() @@ -126,6 +139,12 @@ exports.up = knex => { // ===================================== // RELATION TABLES // ===================================== + // PAGE HISTORY TAGS --------------------------- + .createTable('pageHistoryTags', table => { + table.increments('id').primary() + table.integer('pageId').unsigned().references('id').inTable('pageHistory').onDelete('CASCADE') + table.integer('tagId').unsigned().references('id').inTable('tags').onDelete('CASCADE') + }) // PAGE TAGS --------------------------- .createTable('pageTags', table => { table.increments('id').primary() @@ -149,10 +168,17 @@ exports.up = knex => { table.integer('pageId').unsigned().references('id').inTable('pages') table.integer('authorId').unsigned().references('id').inTable('users') }) + .table('pageHistory', table => { + table.integer('pageId').unsigned().references('id').inTable('pages') + table.string('editorKey').references('key').inTable('editors') + table.string('localeCode', 2).references('code').inTable('locales') + table.integer('authorId').unsigned().references('id').inTable('users') + }) .table('pages', table => { table.string('editorKey').references('key').inTable('editors') table.string('localeCode', 2).references('code').inTable('locales') table.integer('authorId').unsigned().references('id').inTable('users') + table.integer('creatorId').unsigned().references('id').inTable('users') }) .table('users', table => { table.string('providerKey').references('key').inTable('authentication').notNullable().defaultTo('local') diff --git a/server/db/models/pageHistory.js b/server/db/models/pageHistory.js new file mode 100644 index 00000000..adf782f0 --- /dev/null +++ b/server/db/models/pageHistory.js @@ -0,0 +1,100 @@ +const Model = require('objection').Model + +/* global WIKI */ + +/** + * Page History model + */ +module.exports = class PageHistory extends Model { + static get tableName() { return 'pageHistory' } + + static get jsonSchema () { + return { + type: 'object', + required: ['path', 'title'], + + properties: { + id: {type: 'integer'}, + path: {type: 'string'}, + title: {type: 'string'}, + description: {type: 'string'}, + isPublished: {type: 'boolean'}, + publishStartDate: {type: 'string'}, + publishEndDate: {type: 'string'}, + content: {type: 'string'}, + + createdAt: {type: 'string'} + } + } + } + + static get relationMappings() { + return { + tags: { + relation: Model.ManyToManyRelation, + modelClass: require('./tags'), + join: { + from: 'pageHistory.id', + through: { + from: 'pageHistoryTags.pageId', + to: 'pageHistoryTags.tagId' + }, + to: 'tags.id' + } + }, + page: { + relation: Model.BelongsToOneRelation, + modelClass: require('./pages'), + join: { + from: 'pageHistory.pageId', + to: 'pages.id' + } + }, + author: { + relation: Model.BelongsToOneRelation, + modelClass: require('./users'), + join: { + from: 'pageHistory.authorId', + to: 'users.id' + } + }, + editor: { + relation: Model.BelongsToOneRelation, + modelClass: require('./editors'), + join: { + from: 'pageHistory.editorKey', + to: 'editors.key' + } + }, + locale: { + relation: Model.BelongsToOneRelation, + modelClass: require('./locales'), + join: { + from: 'pageHistory.localeCode', + to: 'locales.code' + } + } + } + } + + $beforeInsert() { + this.createdAt = new Date().toISOString() + } + + static async addVersion(opts) { + await WIKI.db.pageHistory.query().insert({ + pageId: opts.id, + authorId: opts.authorId, + content: opts.content, + description: opts.description, + editorKey: opts.editorKey, + isPrivate: opts.isPrivate, + isPublished: opts.isPublished, + localeCode: opts.localeCode, + path: opts.path, + publishEndDate: opts.publishEndDate, + publishStartDate: opts.publishStartDate, + title: opts.title + }) + } +} diff --git a/server/db/models/pages.js b/server/db/models/pages.js index e8780934..f446b0d1 100644 --- a/server/db/models/pages.js +++ b/server/db/models/pages.js @@ -51,6 +51,14 @@ module.exports = class Page extends Model { to: 'users.id' } }, + creator: { + relation: Model.BelongsToOneRelation, + modelClass: require('./users'), + join: { + from: 'pages.creatorId', + to: 'users.id' + } + }, editor: { relation: Model.BelongsToOneRelation, modelClass: require('./editors'), @@ -82,6 +90,7 @@ module.exports = class Page extends Model { const page = await WIKI.db.pages.query().insertAndFetch({ authorId: opts.authorId, content: opts.content, + creatorId: opts.authorId, description: opts.description, editorKey: opts.editor, isPrivate: opts.isPrivate, @@ -92,7 +101,26 @@ module.exports = class Page extends Model { publishStartDate: opts.publishStartDate, title: opts.title }) - await WIKI.db.storage.createPage(page) + await WIKI.db.storage.pageEvent({ + event: 'created', + page + }) + return page + } + + static async updatePage(opts) { + const ogPage = await WIKI.db.pages.query().findById(opts.id) + if (!ogPage) { + throw new Error('Invalid Page Id') + } + await WIKI.db.pageHistory.addVersion(ogPage) + const page = await WIKI.db.pages.query().patchAndFetch({ + title: opts.title + }).where('id', opts.id) + await WIKI.db.storage.pageEvent({ + event: 'updated', + page + }) return page } } diff --git a/server/db/models/storage.js b/server/db/models/storage.js index b3eac75b..81fa8e27 100644 --- a/server/db/models/storage.js +++ b/server/db/models/storage.js @@ -87,12 +87,12 @@ module.exports = class Storage extends Model { } } - static async createPage(page) { + static async pageEvent(event, page) { const targets = await WIKI.db.storage.query().where('isEnabled', true) if (targets && targets.length > 0) { _.forEach(targets, target => { WIKI.queue.job.syncStorage.add({ - event: 'created', + event, target, page }, { diff --git a/server/graph/resolvers/page.js b/server/graph/resolvers/page.js index ca3dcb00..44874374 100644 --- a/server/graph/resolvers/page.js +++ b/server/graph/resolvers/page.js @@ -24,7 +24,6 @@ module.exports = { async create(obj, args, context) { const page = await WIKI.db.pages.createPage({ ...args, - isPrivate: false, authorId: context.req.user.id }) return { @@ -38,10 +37,14 @@ module.exports = { responseResult: graphHelper.generateSuccess('Page has been deleted.') } }, - async update(obj, args) { - await WIKI.db.groups.query().patch({ name: args.name }).where('id', args.id) + async update(obj, args, context) { + const page = await WIKI.db.pages.updatePage({ + ...args, + authorId: context.req.user.id + }) return { - responseResult: graphHelper.generateSuccess('Page has been updated.') + responseResult: graphHelper.generateSuccess('Page has been updated.'), + page } } }, diff --git a/server/graph/schemas/page.graphql b/server/graph/schemas/page.graphql index 7ec3fb0c..df3d6de3 100644 --- a/server/graph/schemas/page.graphql +++ b/server/graph/schemas/page.graphql @@ -52,6 +52,7 @@ type PageMutation { content: String description: String editor: String + isPrivate: Boolean isPublished: Boolean locale: String path: String @@ -59,7 +60,7 @@ type PageMutation { publishStartDate: Date tags: [String] title: String - ): DefaultResponse + ): PageResponse delete( id: Int!