diff --git a/client/client-app.js b/client/client-app.js index 6c943378..9b08f91a 100644 --- a/client/client-app.js +++ b/client/client-app.js @@ -158,6 +158,7 @@ Vue.prototype.Velocity = Velocity Vue.component('admin', () => import(/* webpackChunkName: "admin" */ './components/admin.vue')) Vue.component('editor', () => import(/* webpackPrefetch: -100, webpackChunkName: "editor" */ './components/editor.vue')) Vue.component('history', () => import(/* webpackChunkName: "history" */ './components/history.vue')) +Vue.component('page-source', () => import(/* webpackChunkName: "source" */ './components/source.vue')) Vue.component('login', () => import(/* webpackPrefetch: true, webpackChunkName: "login" */ './components/login.vue')) Vue.component('nav-header', () => import(/* webpackMode: "eager" */ './components/common/nav-header.vue')) Vue.component('page-selector', () => import(/* webpackPrefetch: true, webpackChunkName: "ui-extra" */ './components/common/page-selector.vue')) diff --git a/client/components/common/page-selector.vue b/client/components/common/page-selector.vue index 42853b9c..6242e35f 100644 --- a/client/components/common/page-selector.vue +++ b/client/components/common/page-selector.vue @@ -31,17 +31,7 @@ v-flex(xs8) v-toolbar(color='grey darken-2', dark, dense, flat) .body-2 Pages - v-divider.ml-4(vertical) - v-text-field( - prepend-inner-icon='search' - label='Search...' - hide-details - solo - background-color='grey darken-2' - flat - clearable - ) - v-divider.mx-3(vertical) + v-spacer v-btn(icon): v-icon forward v-btn(icon): v-icon delete v-list(dense) diff --git a/client/components/history.vue b/client/components/history.vue index aef4d001..950ab3bd 100644 --- a/client/components/history.vue +++ b/client/components/history.vue @@ -5,11 +5,11 @@ v-toolbar(color='primary', dark) .subheading Viewing history of page #[strong /{{path}}] v-spacer - .caption.blue--text.text--lighten-3 ID {{id}} + .caption.blue--text.text--lighten-3 ID {{pageId}} v-btn.ml-4(depressed, color='blue darken-1', @click='goLive') Return to Live Version v-container(fluid, grid-list-xl) v-layout(row, wrap) - v-flex(xs5) + v-flex(xs4) v-chip.ma-0.grey--text.text--darken-2( label small @@ -20,86 +20,92 @@ dense ) v-timeline-item( + v-for='ph in trail' + :key='ph.versionId' + :small='ph.actionType === `edit`' fill-dot - color='primary' - icon='edit' + :color='trailColor(ph.actionType)' + :icon='trailIcon(ph.actionType)' ) - v-card.grey.lighten-3.radius-7(flat) - v-card-text - v-layout(justify-space-between) - v-flex(xs7) - v-chip.ml-0.mr-3( - label - small - color='primary' - ) - span.white--text Viewing - span Edited by John Doe - v-flex(xs5, text-xs-right, align-center, d-flex) - .caption Today at 12:34 PM + v-card.radius-7(flat, :class='trailBgColor(ph.actionType)') + v-toolbar(flat, :color='trailBgColor(ph.actionType)') + v-chip.ml-0.mr-3( + v-if='diffSource === ph.versionId' + label + small + color='pink' + ) + .caption.white--text Source + v-chip.ml-0.mr-3( + v-if='diffTarget === ph.versionId' + label + small + color='pink' + ) + .caption.white--text Target + .caption(v-if='ph.actionType === `edit`') Edited by {{ ph.authorName }} + .caption(v-else-if='ph.actionType === `move`') Moved from #[strong {{ph.valueBefore}}] to #[strong {{ph.valueAfter}}] by {{ ph.authorName }} + .caption(v-else-if='ph.actionType === `initial`') Created by {{ ph.authorName }} + .caption(v-else) Unknown Action by {{ ph.authorName }} + v-spacer + .caption {{ ph.createdAt | moment('calendar') }} + v-menu(offset-x, left) + v-btn(icon, slot='activator'): v-icon more_horiz + v-list(dense).history-promptmenu + v-list-tile(@click='setDiffTarget(ph.versionId)') + v-list-tile-avatar: v-icon call_made + v-list-tile-title Set as Differencing Target + v-divider + v-list-tile(@click='setDiffSource(ph.versionId)') + v-list-tile-avatar: v-icon call_received + v-list-tile-title Set as Differencing Source + v-divider + v-list-tile + v-list-tile-avatar: v-icon code + v-list-tile-title View Source + v-divider + v-list-tile + v-list-tile-avatar: v-icon cloud_download + v-list-tile-title Download Version + v-divider + v-list-tile + v-list-tile-avatar: v-icon restore + v-list-tile-title Restore + v-divider + v-list-tile + v-list-tile-avatar: v-icon call_split + v-list-tile-title Branch off from here - v-timeline-item( - fill-dot - small - color='primary' - icon='edit' - ) - v-card.grey.lighten-3.radius-7(flat) - v-card-text - v-layout(justify-space-between) - v-flex(xs7) - span Edited by Jane Doe - v-flex(xs5, text-xs-right, align-center, d-flex) - .caption Today at 12:27 PM - - v-timeline-item( - fill-dot - small - color='purple' - icon='forward' - ) - v-card.purple.lighten-5.radius-7(flat) - v-card-text - v-layout(justify-space-between) - v-flex(xs7) - span Moved page from #[strong /test] to #[strong /home] by John Doe - v-flex(xs5, text-xs-right, align-center, d-flex) - .caption Yesterday at 10:45 AM - - v-timeline-item( - fill-dot - color='teal' - icon='add' - ) - v-card.teal.lighten-5.radius-7(flat) - v-card-text - v-layout(justify-space-between) - v-flex(xs7): span Initial page creation by John Doe - v-flex(xs5, text-xs-right, align-center, d-flex) - .caption Last Tuesday at 7:56 PM v-chip.ma-0.grey--text.text--darken-2( label small color='grey lighten-2' - ) End of history + ) End of history trail - v-flex(xs7) + v-flex(xs8) v-card.radius-7 v-card-text v-card.grey.lighten-4.radius-7(flat) v-card-text .subheading Page Title .caption Some page description + .mt-3(v-html='diffHTML') nav-footer + + diff --git a/client/graph/history/history-trail-query.gql b/client/graph/history/history-trail-query.gql new file mode 100644 index 00000000..d7ff2d25 --- /dev/null +++ b/client/graph/history/history-trail-query.gql @@ -0,0 +1,13 @@ +query($id: Int!, $offset: Int) { + pages { + history(id:$id, offset:$offset) { + versionId + authorId + authorName + actionType + valueBefore + valueAfter + createdAt + } + } +} diff --git a/client/scss/app.scss b/client/scss/app.scss index 5df054c6..faa049ad 100644 --- a/client/scss/app.scss +++ b/client/scss/app.scss @@ -5,6 +5,7 @@ @import "../libs/animate/animate"; @import '~vue2-animate/src/sass/vue2-animate'; +@import '~diff2html/dist/diff2html.min.css'; @import 'components/v-btn'; @import 'components/v-data-table'; diff --git a/client/scss/base/base.scss b/client/scss/base/base.scss index bad02832..ba364a78 100644 --- a/client/scss/base/base.scss +++ b/client/scss/base/base.scss @@ -20,7 +20,7 @@ html { } -@for $i from 1 through 25 { +@for $i from 0 through 25 { .radius-#{$i} { border-radius: #{$i}px; } diff --git a/client/themes/default/scss/app.scss b/client/themes/default/scss/app.scss index 77ee021e..b77bf11e 100644 --- a/client/themes/default/scss/app.scss +++ b/client/themes/default/scss/app.scss @@ -95,6 +95,13 @@ text-align: justify; } + hr { + margin: 1rem; + height: 1px; + border: none; + background-color: mc('grey', '400'); + } + blockquote { padding: 0 0 1rem 0; border: 1px solid mc('blue', '500'); @@ -204,17 +211,18 @@ .task-list-item { position: relative; + list-style-type: none; &-checkbox[disabled] { display: none; & + label { - padding-left: 1.4rem; + padding-left: 1.5rem; } & + label::before { position: absolute; - left: 1rem; + left: 0; top: 2px; content: ' '; display: block; @@ -233,6 +241,10 @@ content: '✓'; } } + + .contains-task-list { + padding: .5rem 0 0 1.5rem; + } } } diff --git a/package.json b/package.json index d7ac22cd..5d9ba49c 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,7 @@ "cookie-parser": "1.4.3", "cors": "2.8.5", "dependency-graph": "0.7.2", + "diff": "3.5.0", "diff2html": "2.5.0", "dotize": "^0.2.0", "execa": "1.0.0", diff --git a/server/controllers/common.js b/server/controllers/common.js index 835b81d1..bbde3529 100644 --- a/server/controllers/common.js +++ b/server/controllers/common.js @@ -46,11 +46,11 @@ router.get(['/p', '/p/*'], (req, res, next) => { }) /** - * View document + * History */ router.get(['/h', '/h/*'], async (req, res, next) => { const pageArgs = pageHelper.parsePath(req.path) - const page = await WIKI.models.pages.getPage({ + const page = await WIKI.models.pages.getPageFromDb({ path: pageArgs.path, locale: pageArgs.locale, userId: req.user.id, @@ -63,6 +63,24 @@ router.get(['/h', '/h/*'], async (req, res, next) => { } }) +/** + * Source + */ +router.get(['/s', '/s/*'], async (req, res, next) => { + const pageArgs = pageHelper.parsePath(req.path) + const page = await WIKI.models.pages.getPageFromDb({ + path: pageArgs.path, + locale: pageArgs.locale, + userId: req.user.id, + isPrivate: false + }) + if (page) { + res.render('source', { page }) + } else { + res.redirect(`/${pageArgs.path}`) + } +}) + /** * View document */ diff --git a/server/graph/resolvers/page.js b/server/graph/resolvers/page.js index e621bf04..3e560826 100644 --- a/server/graph/resolvers/page.js +++ b/server/graph/resolvers/page.js @@ -10,6 +10,12 @@ module.exports = { async pages() { return {} } }, PageQuery: { + async history(obj, args, context, info) { + return WIKI.models.pageHistory.getHistory({ + pageId: args.id, + offset: args.offset || 0 + }) + }, async list(obj, args, context, info) { return WIKI.models.pages.query().select( 'pages.*', diff --git a/server/graph/schemas/page.graphql b/server/graph/schemas/page.graphql index 9816209a..808a0286 100644 --- a/server/graph/schemas/page.graphql +++ b/server/graph/schemas/page.graphql @@ -15,6 +15,11 @@ extend type Mutation { # ----------------------------------------------- type PageQuery { + history( + id: Int! + offset: Int + ): [PageHistory] + list( filter: String orderBy: String @@ -92,3 +97,13 @@ type Page { createdAt: Date! updatedAt: Date! } + +type PageHistory { + versionId: Int! + authorId: Int! + authorName: String! + actionType: String! + valueBefore: String + valueAfter: String + createdAt: Date! +} diff --git a/server/models/pageHistory.js b/server/models/pageHistory.js index d9862fcf..aae8d379 100644 --- a/server/models/pageHistory.js +++ b/server/models/pageHistory.js @@ -1,4 +1,5 @@ const Model = require('objection').Model +const _ = require('lodash') /* global WIKI */ @@ -101,4 +102,53 @@ module.exports = class PageHistory extends Model { title: opts.title }) } + + static async getHistory({ pageId, offset = 0 }) { + const history = await WIKI.models.pageHistory.query() + .column([ + 'pageHistory.id', + 'pageHistory.path', + 'pageHistory.authorId', + 'pageHistory.createdAt', + { + authorName: 'author.name' + } + ]) + .joinRelation('author') + .where({ + 'pageHistory.pageId': pageId + }) + .orderBy('pageHistory.createdAt', 'asc') + .offset(offset) + .limit(20) + + let prevPh = null + + return _.reduce(history, (res, ph) => { + let actionType = 'edit' + let valueBefore = null + let valueAfter = null + + if (!prevPh && offset === 0) { + actionType = 'initial' + } else if (_.get(prevPh, 'path', '') !== ph.path) { + actionType = 'move' + valueBefore = _.get(prevPh, 'path', '') + valueAfter = ph.path + } + + res.unshift({ + versionId: ph.id, + authorId: ph.authorId, + authorName: ph.authorName, + actionType, + valueBefore, + valueAfter, + createdAt: ph.createdAt + }) + + prevPh = ph + return res + }, []) + } } diff --git a/server/models/pages.js b/server/models/pages.js index 187bde44..9eb26774 100644 --- a/server/models/pages.js +++ b/server/models/pages.js @@ -197,7 +197,7 @@ module.exports = class Page extends Model { } static async getPageFromDb(opts) { - const page = await WIKI.models.pages.query() + return WIKI.models.pages.query() .column([ 'pages.*', { @@ -227,7 +227,6 @@ module.exports = class Page extends Model { } }) .first() - return page } static async savePageToCache(page) { diff --git a/server/views/history.pug b/server/views/history.pug index 80643849..2380f9f5 100644 --- a/server/views/history.pug +++ b/server/views/history.pug @@ -5,7 +5,8 @@ block head block body #root history( - id=page.id + :page-id=page.id locale=page.localeCode path=page.path + live-content=page.content ) diff --git a/server/views/source.pug b/server/views/source.pug new file mode 100644 index 00000000..55d56ffc --- /dev/null +++ b/server/views/source.pug @@ -0,0 +1,11 @@ +extends master.pug + +block head + +block body + #root + page-source( + page-id=page.id + locale=page.localeCode + path=page.path + )= page.content diff --git a/yarn.lock b/yarn.lock index 621e5d46..38a85ed3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4934,7 +4934,7 @@ diff2html@2.5.0: lodash "^4.17.11" whatwg-fetch "^3.0.0" -diff@^3.1.0, diff@^3.5.0: +diff@3.5.0, diff@^3.1.0, diff@^3.5.0: version "3.5.0" resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==