feat: source + history (wip)

This commit is contained in:
Nicolas Giard 2018-11-25 01:28:20 -05:00
parent 076aeaf749
commit 78ba895eee
17 changed files with 374 additions and 81 deletions

View File

@ -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'))

View File

@ -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)

View File

@ -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
</template>
<script>
import { Diff2Html } from 'diff2html'
import { createPatch } from 'diff'
import _ from 'lodash'
import historyTrailQuery from 'gql/history/history-trail-query.gql'
/* global siteConfig */
export default {
props: {
id: {
pageId: {
type: Number,
default: 0
},
@ -110,13 +116,43 @@ export default {
path: {
type: String,
default: 'home'
},
liveContent: {
type: String,
default: ''
}
},
data() {
return {}
return {
sourceText: '',
targetText: '',
trail: [],
diffSource: 0,
diffTarget: 0,
offset: 0
}
},
computed: {
darkMode() { return siteConfig.darkMode }
darkMode() { return siteConfig.darkMode },
diffs() {
return createPatch(`/${this.path}`, this.sourceText, this.targetText)
},
diffHTML() {
return Diff2Html.getPrettyHtml(this.diffs, {
inputFormat: 'diff',
showFiles: false,
matching: 'lines',
outputFormat: 'line-by-line'
})
}
},
watch: {
trail(newValue, oldValue) {
if (newValue && newValue.length > 0) {
this.diffTarget = _.get(_.head(newValue), 'versionId', 0)
this.diffSource = _.get(_.nth(newValue, 1), 'versionId', 0)
}
}
},
created () {
this.$store.commit('page/SET_ID', this.id)
@ -124,10 +160,67 @@ export default {
this.$store.commit('page/SET_PATH', this.path)
this.$store.commit('page/SET_MODE', 'history')
this.targetText = this.liveContent
},
methods: {
goLive() {
window.location.assign(`/${this.path}`)
},
setDiffSource(versionId) {
this.diffSource = versionId
},
setDiffTarget(versionId) {
this.diffTarget = versionId
},
trailColor(actionType) {
switch (actionType) {
case 'edit':
return 'primary'
case 'move':
return 'purple'
case 'initial':
return 'teal'
default:
return 'grey'
}
},
trailIcon(actionType) {
switch (actionType) {
case 'edit':
return 'edit'
case 'move':
return 'forward'
case 'initial':
return 'add'
default:
return 'warning'
}
},
trailBgColor(actionType) {
switch (actionType) {
case 'move':
return 'purple lighten-5'
case 'initial':
return 'teal lighten-5'
default:
return 'grey lighten-3'
}
}
},
apollo: {
trail: {
query: historyTrailQuery,
variables() {
return {
id: this.pageId,
offset: 0
}
},
update: (data) => data.pages.history,
watchLoading (isLoading) {
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'history-trail-refresh')
}
}
}
}
@ -135,4 +228,10 @@ export default {
<style lang='scss'>
.history {
&-promptmenu {
border-top: 5px solid mc('blue', '700');
}
}
</style>

View File

@ -0,0 +1,76 @@
<template lang='pug'>
v-app(:dark='darkMode').source
nav-header
v-content
v-toolbar(color='primary', dark)
.subheading Viewing source of page #[strong /{{path}}]
v-spacer
.caption.blue--text.text--lighten-3 ID {{pageId}}
v-btn.ml-4(depressed, color='blue darken-1', @click='goLive') Return to Normal View
v-card(tile)
v-card-text
v-card.grey.lighten-4.radius-7(flat)
v-card-text
pre
code
slot
nav-footer
</template>
<script>
/* global siteConfig */
export default {
props: {
pageId: {
type: Number,
default: 0
},
locale: {
type: String,
default: 'en'
},
path: {
type: String,
default: 'home'
}
},
data() {
return {}
},
computed: {
darkMode() { return siteConfig.darkMode }
},
created () {
this.$store.commit('page/SET_ID', this.id)
this.$store.commit('page/SET_LOCALE', this.locale)
this.$store.commit('page/SET_PATH', this.path)
this.$store.commit('page/SET_MODE', 'history')
},
methods: {
goLive() {
window.location.assign(`/${this.path}`)
}
}
}
</script>
<style lang='scss'>
.source {
pre > code {
box-shadow: none;
color: mc('grey', '800');
font-family: 'Source Code Pro', sans-serif;
font-weight: 400;
font-size: 1rem;
&::before {
display: none;
}
}
}
</style>

View File

@ -0,0 +1,13 @@
query($id: Int!, $offset: Int) {
pages {
history(id:$id, offset:$offset) {
versionId
authorId
authorName
actionType
valueBefore
valueAfter
createdAt
}
}
}

View File

@ -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';

View File

@ -20,7 +20,7 @@ html {
}
@for $i from 1 through 25 {
@for $i from 0 through 25 {
.radius-#{$i} {
border-radius: #{$i}px;
}

View File

@ -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;
}
}
}

View File

@ -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",

View File

@ -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
*/

View File

@ -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.*',

View File

@ -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!
}

View File

@ -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
}, [])
}
}

View File

@ -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) {

View File

@ -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
)

11
server/views/source.pug Normal file
View File

@ -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

View File

@ -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==