feat: view version of page source

This commit is contained in:
NGPixel 2020-02-29 18:57:54 -05:00 committed by Nicolas Giard
parent 2ac9131244
commit e50dc89519
10 changed files with 188 additions and 30 deletions

View File

@ -23,30 +23,30 @@
dense dense
) )
v-timeline-item.pb-2( v-timeline-item.pb-2(
v-for='(ph, idx) in trail' v-for='(ph, idx) in fullTrail'
:key='ph.versionId' :key='ph.versionId'
:small='ph.actionType === `edit`' :small='ph.actionType === `edit`'
:color='trailColor(ph.actionType)' :color='trailColor(ph.actionType)'
:icon='trailIcon(ph.actionType)' :icon='trailIcon(ph.actionType)'
:class='idx >= trail.length - 1 ? `pb-4` : `pb-2`'
) )
v-card.radius-7(flat, :class='trailBgColor(ph.actionType)') v-card.radius-7(flat, :class='trailBgColor(ph.actionType)')
v-toolbar(flat, :color='trailBgColor(ph.actionType)', height='40') v-toolbar(flat, :color='trailBgColor(ph.actionType)', height='40')
.caption(:title='$options.filters.moment(ph.createdAt, `LLL`)') {{ ph.createdAt | moment('ll') }} .caption(:title='$options.filters.moment(ph.versionDate, `LLL`)') {{ ph.versionDate | moment('ll') }}
v-divider.mx-3(vertical) v-divider.mx-3(vertical)
.caption(v-if='ph.actionType === `edit`') Edited by #[strong {{ ph.authorName }}] .caption(v-if='ph.actionType === `edit`') Edited by #[strong {{ ph.authorName }}]
.caption(v-else-if='ph.actionType === `move`') Moved from #[strong {{ph.valueBefore}}] to #[strong {{ph.valueAfter}}] by #[strong {{ ph.authorName }}] .caption(v-else-if='ph.actionType === `move`') Moved from #[strong {{ph.valueBefore}}] to #[strong {{ph.valueAfter}}] by #[strong {{ ph.authorName }}]
.caption(v-else-if='ph.actionType === `initial`') Created by #[strong {{ ph.authorName }}] .caption(v-else-if='ph.actionType === `initial`') Created by #[strong {{ ph.authorName }}]
.caption(v-else-if='ph.actionType === `live`') Last Edited by #[strong {{ ph.authorName }}]
.caption(v-else) Unknown Action by #[strong {{ ph.authorName }}] .caption(v-else) Unknown Action by #[strong {{ ph.authorName }}]
v-spacer v-spacer
v-menu(offset-x, left) v-menu(offset-x, left)
template(v-slot:activator='{ on }') template(v-slot:activator='{ on }')
v-btn.mr-2.radius-4(icon, v-on='on', small, tile): v-icon mdi-dots-horizontal v-btn.mr-2.radius-4(icon, v-on='on', small, tile): v-icon mdi-dots-horizontal
v-list(dense, nav).history-promptmenu v-list(dense, nav).history-promptmenu
v-list-item(@click='setDiffSource(ph.versionId)', :disabled='ph.versionId >= diffTarget') v-list-item(@click='setDiffSource(ph.versionId)', :disabled='(ph.versionId >= diffTarget && diffTarget !== 0) || ph.versionId === 0')
v-list-item-avatar(size='24'): v-avatar A v-list-item-avatar(size='24'): v-avatar A
v-list-item-title Set as Differencing Source v-list-item-title Set as Differencing Source
v-list-item(@click='setDiffTarget(ph.versionId)', :disabled='ph.versionId <= diffSource') v-list-item(@click='setDiffTarget(ph.versionId)', :disabled='ph.versionId <= diffSource && ph.versionId !== 0')
v-list-item-avatar(size='24'): v-avatar B v-list-item-avatar(size='24'): v-avatar B
v-list-item-title Set as Differencing Target v-list-item-title Set as Differencing Target
v-list-item(@click='viewSource(ph.versionId)') v-list-item(@click='viewSource(ph.versionId)')
@ -55,8 +55,8 @@
v-list-item(@click='download(ph.versionId)') v-list-item(@click='download(ph.versionId)')
v-list-item-avatar(size='24'): v-icon mdi-cloud-download-outline v-list-item-avatar(size='24'): v-icon mdi-cloud-download-outline
v-list-item-title Download Version v-list-item-title Download Version
v-list-item(@click='restore(ph.versionId)') v-list-item(@click='restore(ph.versionId)', :disabled='ph.versionId === 0')
v-list-item-avatar(size='24'): v-icon mdi-history v-list-item-avatar(size='24'): v-icon(:disabled='ph.versionId === 0') mdi-history
v-list-item-title Restore v-list-item-title Restore
v-list-item(@click='branchOff(ph.versionId)') v-list-item(@click='branchOff(ph.versionId)')
v-list-item-avatar(size='24'): v-icon mdi-source-branch v-list-item-avatar(size='24'): v-icon mdi-source-branch
@ -68,7 +68,7 @@
depressed depressed
tile tile
:class='diffSource === ph.versionId ? `pink white--text` : ($vuetify.theme.dark ? `grey darken-2` : `grey lighten-2`)' :class='diffSource === ph.versionId ? `pink white--text` : ($vuetify.theme.dark ? `grey darken-2` : `grey lighten-2`)'
:disabled='ph.versionId >= diffTarget' :disabled='(ph.versionId >= diffTarget && diffTarget !== 0) || ph.versionId === 0'
): strong A ): strong A
v-btn.mr-0.radius-4( v-btn.mr-0.radius-4(
@click='setDiffTarget(ph.versionId)' @click='setDiffTarget(ph.versionId)'
@ -77,7 +77,7 @@
depressed depressed
tile tile
:class='diffTarget === ph.versionId ? `pink white--text` : ($vuetify.theme.dark ? `grey darken-2` : `grey lighten-2`)' :class='diffTarget === ph.versionId ? `pink white--text` : ($vuetify.theme.dark ? `grey darken-2` : `grey lighten-2`)'
:disabled='ph.versionId <= diffSource' :disabled='ph.versionId <= diffSource && ph.versionId !== 0'
): strong B ): strong B
v-btn.ma-0.radius-7( v-btn.ma-0.radius-7(
@ -137,6 +137,38 @@ export default {
type: String, type: String,
default: 'home' default: 'home'
}, },
title: {
type: String,
default: 'Untitled Page'
},
description: {
type: String,
default: ''
},
createdAt: {
type: String,
default: ''
},
updatedAt: {
type: String,
default: ''
},
tags: {
type: Array,
default: () => ([])
},
authorName: {
type: String,
default: 'Unknown'
},
authorId: {
type: Number,
default: 0
},
isPublished: {
type: Boolean,
default: false
},
liveContent: { liveContent: {
type: String, type: String,
default: '' default: ''
@ -167,6 +199,20 @@ export default {
}, },
computed: { computed: {
darkMode: get('site/dark'), darkMode: get('site/dark'),
fullTrail () {
return [
{
versionId: 0,
authorId: this.authorId,
authorName: this.authorName,
actionType: 'live',
valueBefore: null,
valueAfter: null,
versionDate: this.updatedAt
},
...this.trail
]
},
diffs () { diffs () {
return createPatch(`/${this.path}`, this.source.content, this.target.content) return createPatch(`/${this.path}`, this.source.content, this.target.content)
}, },
@ -182,8 +228,8 @@ export default {
watch: { watch: {
trail (newValue, oldValue) { trail (newValue, oldValue) {
if (newValue && newValue.length > 0) { if (newValue && newValue.length > 0) {
this.diffTarget = _.get(_.head(newValue), 'versionId', 0) this.diffTarget = 0
this.diffSource = _.get(_.nth(newValue, 1), 'versionId', 0) this.diffSource = _.get(_.head(newValue), 'versionId', 0)
} }
}, },
async diffSource (newValue, oldValue) { async diffSource (newValue, oldValue) {
@ -214,7 +260,29 @@ export default {
this.$store.commit('page/SET_MODE', 'history') this.$store.commit('page/SET_MODE', 'history')
this.target.content = this.liveContent this.cache.push({
action: 'live',
authorId: this.authorId,
authorName: this.authorName,
content: this.liveContent,
contentType: '',
createdAt: this.createdAt,
description: this.description,
editor: '',
isPrivate: false,
isPublished: this.isPublished,
locale: this.locale,
pageId: this.pageId,
path: this.path,
publishEndDate: '',
publishStartDate: '',
tags: this.tags,
title: this.title,
versionId: 0,
versionDate: this.updatedAt
})
this.target = this.cache[0]
}, },
methods: { methods: {
async loadVersion (versionId) { async loadVersion (versionId) {
@ -230,6 +298,7 @@ export default {
content content
contentType contentType
createdAt createdAt
versionDate
description description
editor editor
isPrivate isPrivate
@ -314,6 +383,8 @@ export default {
return 'purple' return 'purple'
case 'initial': case 'initial':
return 'teal' return 'teal'
case 'live':
return 'orange'
default: default:
return 'grey' return 'grey'
} }
@ -326,8 +397,10 @@ export default {
return 'forward' return 'forward'
case 'initial': case 'initial':
return 'mdi-plus' return 'mdi-plus'
case 'live':
return 'mdi-atom-variant'
default: default:
return 'warning' return 'mdi-alert'
} }
}, },
trailBgColor (actionType) { trailBgColor (actionType) {
@ -336,6 +409,8 @@ export default {
return this.darkMode ? 'purple' : 'purple lighten-5' return this.darkMode ? 'purple' : 'purple lighten-5'
case 'initial': case 'initial':
return this.darkMode ? 'teal darken-3' : 'teal lighten-5' return this.darkMode ? 'teal darken-3' : 'teal lighten-5'
case 'live':
return this.darkMode ? 'orange darken-3' : 'orange lighten-5'
default: default:
return this.darkMode ? 'grey darken-3' : 'grey lighten-4' return this.darkMode ? 'grey darken-3' : 'grey lighten-4'
} }
@ -354,7 +429,7 @@ export default {
actionType actionType
valueBefore valueBefore
valueAfter valueAfter
createdAt versionDate
} }
total total
} }

View File

@ -3,11 +3,17 @@
nav-header nav-header
v-content v-content
v-toolbar(color='primary', dark) v-toolbar(color='primary', dark)
i18next.subheading(path='common:page.viewingSource', tag='div') i18next.subheading(v-if='versionId > 0', path='common:page.viewingSourceVersion', tag='div')
strong(place='date', :title='$options.filters.moment(versionDate, `LLL`)') {{versionDate | moment('lll')}}
strong(place='path') /{{path}}
i18next.subheading(v-else, path='common:page.viewingSource', tag='div')
strong(place='path') /{{path}} strong(place='path') /{{path}}
template(v-if='$vuetify.breakpoint.mdAndUp') template(v-if='$vuetify.breakpoint.mdAndUp')
v-spacer v-spacer
.caption.blue--text.text--lighten-3 {{$t('common:page.id', { id: pageId })}} .caption.blue--text.text--lighten-3 {{$t('common:page.id', { id: pageId })}}
.caption.blue--text.text--lighten-3.ml-4(v-if='versionId > 0') {{$t('common:page.versionId', { id: versionId })}}
v-btn.ml-4(v-if='versionId > 0', depressed, color='blue darken-1', @click='goHistory')
v-icon mdi-history
v-btn.ml-4(depressed, color='blue darken-1', @click='goLive') {{$t('common:page.returnNormalView')}} v-btn.ml-4(depressed, color='blue darken-1', @click='goLive') {{$t('common:page.returnNormalView')}}
v-card(tile) v-card(tile)
v-card-text v-card-text
@ -38,6 +44,14 @@ export default {
path: { path: {
type: String, type: String,
default: 'home' default: 'home'
},
versionId: {
type: Number,
default: 0
},
versionDate: {
type: String,
default: ''
} }
}, },
data() { data() {
@ -55,7 +69,10 @@ export default {
}, },
methods: { methods: {
goLive() { goLive() {
window.location.assign(`/${this.path}`) window.location.assign(`/${this.locale}/${this.path}`)
},
goHistory () {
window.location.assign(`/h/${this.locale}/${this.path}`)
} }
} }
} }

View File

@ -1,7 +1,7 @@
# ==================== # ====================
# --- Build Assets --- # --- Build Assets ---
# ==================== # ====================
FROM node:12.14-alpine AS assets FROM node:12-alpine AS assets
RUN apk add yarn g++ make python --no-cache RUN apk add yarn g++ make python --no-cache
@ -23,7 +23,7 @@ RUN yarn --production --frozen-lockfile --non-interactive
# =============== # ===============
# --- Release --- # --- Release ---
# =============== # ===============
FROM node:12.14-alpine FROM node:12-alpine
LABEL maintainer="requarks.io" LABEL maintainer="requarks.io"
RUN apk add bash curl git openssh gnupg sqlite --no-cache && \ RUN apk add bash curl git openssh gnupg sqlite --no-cache && \

View File

@ -226,6 +226,8 @@ router.get(['/p', '/p/*'], (req, res, next) => {
*/ */
router.get(['/s', '/s/*'], async (req, res, next) => { router.get(['/s', '/s/*'], async (req, res, next) => {
const pageArgs = pageHelper.parsePath(req.path, { stripExt: true }) const pageArgs = pageHelper.parsePath(req.path, { stripExt: true })
const versionId = (req.query.v) ? _.toSafeInteger(req.query.v) : 0
const page = await WIKI.models.pages.getPageFromDb({ const page = await WIKI.models.pages.getPageFromDb({
path: pageArgs.path, path: pageArgs.path,
locale: pageArgs.locale, locale: pageArgs.locale,
@ -242,14 +244,34 @@ router.get(['/s', '/s/*'], async (req, res, next) => {
_.set(res, 'locals.siteConfig.lang', pageArgs.locale) _.set(res, 'locals.siteConfig.lang', pageArgs.locale)
_.set(res, 'locals.siteConfig.rtl', req.i18n.dir() === 'rtl') _.set(res, 'locals.siteConfig.rtl', req.i18n.dir() === 'rtl')
if (!WIKI.auth.checkAccess(req.user, ['read:source'], pageArgs)) { if (versionId > 0) {
return res.render('unauthorized', { action: 'source' }) if (!WIKI.auth.checkAccess(req.user, ['read:history'], pageArgs)) {
_.set(res.locals, 'pageMeta.title', 'Unauthorized')
return res.render('unauthorized', { action: 'sourceVersion' })
}
} else {
if (!WIKI.auth.checkAccess(req.user, ['read:source'], pageArgs)) {
_.set(res.locals, 'pageMeta.title', 'Unauthorized')
return res.render('unauthorized', { action: 'source' })
}
} }
if (page) { if (page) {
_.set(res.locals, 'pageMeta.title', page.title) if (versionId > 0) {
_.set(res.locals, 'pageMeta.description', page.description) const pageVersion = await WIKI.models.pageHistory.getVersion({ pageId: page.id, versionId })
res.render('source', { page }) _.set(res.locals, 'pageMeta.title', pageVersion.title)
_.set(res.locals, 'pageMeta.description', pageVersion.description)
res.render('source', {
page: {
...page,
...pageVersion
}
})
} else {
_.set(res.locals, 'pageMeta.title', page.title)
_.set(res.locals, 'pageMeta.description', page.description)
res.render('source', { page })
}
} else { } else {
res.redirect(`/${pageArgs.path}`) res.redirect(`/${pageArgs.path}`)
} }

View File

@ -0,0 +1,9 @@
exports.up = knex => {
return knex.schema
.alterTable('pageHistory', table => {
table.string('versionDate').notNullable().defaultTo('')
})
.raw(`UPDATE pageHistory AS h1 INNER JOIN pageHistory AS h2 ON h2.id = (SELECT prev.id FROM pageHistory AS prev WHERE prev.pageId = h1.pageId AND prev.id < h1.id ORDER BY prev.id DESC LIMIT 1) SET h1.versionDate = h2.createdAt`)
}
exports.down = knex => { }

View File

@ -0,0 +1,22 @@
/* global WIKI */
exports.up = knex => {
let sqlVersionDate = ''
switch (WIKI.config.db.type) {
case 'postgres':
case 'mssql':
sqlVersionDate = 'UPDATE "pageHistory" h1 SET "versionDate" = COALESCE((SELECT prev."createdAt" FROM "pageHistory" prev WHERE prev."pageId" = h1."pageId" AND prev.id < h1.id ORDER BY prev.id DESC LIMIT 1), h1.createdAt)'
break
case 'mysql':
case 'mariadb':
sqlVersionDate = `UPDATE pageHistory AS h1 INNER JOIN pageHistory AS h2 ON h2.id = (SELECT prev.id FROM pageHistory AS prev WHERE prev.pageId = h1.pageId AND prev.id < h1.id ORDER BY prev.id DESC LIMIT 1) SET h1.versionDate = h2.createdAt`
break
}
return knex.schema
.alterTable('pageHistory', table => {
table.string('versionDate').notNullable().defaultTo('')
})
.raw(sqlVersionDate)
}
exports.down = knex => { }

View File

@ -183,12 +183,12 @@ type PageTag {
type PageHistory { type PageHistory {
versionId: Int! versionId: Int!
versionDate: Date!
authorId: Int! authorId: Int!
authorName: String! authorName: String!
actionType: String! actionType: String!
valueBefore: String valueBefore: String
valueAfter: String valueAfter: String
createdAt: Date!
} }
type PageVersion { type PageVersion {
@ -198,6 +198,7 @@ type PageVersion {
content: String! content: String!
contentType: String! contentType: String!
createdAt: Date! createdAt: Date!
versionDate: Date!
description: String! description: String!
editor: String! editor: String!
isPrivate: Boolean! isPrivate: Boolean!

View File

@ -100,7 +100,8 @@ module.exports = class PageHistory extends Model {
publishEndDate: opts.publishEndDate || '', publishEndDate: opts.publishEndDate || '',
publishStartDate: opts.publishStartDate || '', publishStartDate: opts.publishStartDate || '',
title: opts.title, title: opts.title,
action: opts.action || 'updated' action: opts.action || 'updated',
versionDate: opts.versionDate
}) })
} }
@ -120,6 +121,7 @@ module.exports = class PageHistory extends Model {
'pageHistory.action', 'pageHistory.action',
'pageHistory.authorId', 'pageHistory.authorId',
'pageHistory.pageId', 'pageHistory.pageId',
'pageHistory.versionDate',
{ {
versionId: 'pageHistory.id', versionId: 'pageHistory.id',
editor: 'pageHistory.editorKey', editor: 'pageHistory.editorKey',
@ -146,7 +148,7 @@ module.exports = class PageHistory extends Model {
'pageHistory.path', 'pageHistory.path',
'pageHistory.authorId', 'pageHistory.authorId',
'pageHistory.action', 'pageHistory.action',
'pageHistory.createdAt', 'pageHistory.versionDate',
{ {
authorName: 'author.name' authorName: 'author.name'
} }
@ -155,7 +157,7 @@ module.exports = class PageHistory extends Model {
.where({ .where({
'pageHistory.pageId': pageId 'pageHistory.pageId': pageId
}) })
.orderBy('pageHistory.createdAt', 'desc') .orderBy('pageHistory.versionDate', 'desc')
.page(offsetPage, offsetSize) .page(offsetPage, offsetSize)
let prevPh = null let prevPh = null
@ -168,7 +170,7 @@ module.exports = class PageHistory extends Model {
'pageHistory.path', 'pageHistory.path',
'pageHistory.authorId', 'pageHistory.authorId',
'pageHistory.action', 'pageHistory.action',
'pageHistory.createdAt', 'pageHistory.versionDate',
{ {
authorName: 'author.name' authorName: 'author.name'
} }
@ -177,7 +179,7 @@ module.exports = class PageHistory extends Model {
.where({ .where({
'pageHistory.pageId': pageId 'pageHistory.pageId': pageId
}) })
.orderBy('pageHistory.createdAt', 'desc') .orderBy('pageHistory.versionDate', 'desc')
.offset((offsetPage + 1) * offsetSize) .offset((offsetPage + 1) * offsetSize)
.limit(1) .limit(1)
.first() .first()
@ -204,7 +206,7 @@ module.exports = class PageHistory extends Model {
actionType, actionType,
valueBefore, valueBefore,
valueAfter, valueAfter,
createdAt: ph.createdAt versionDate: ph.versionDate
}) })
prevPh = ph prevPh = ph

View File

@ -8,5 +8,13 @@ block body
:page-id=page.id :page-id=page.id
locale=page.localeCode locale=page.localeCode
path=page.path path=page.path
title=page.title
description=page.description
:tags=page.tags
created-at=page.createdAt
updated-at=page.updatedAt
author-name=page.authorName
:author-id=page.authorId
:is-published=page.isPublished.toString()
live-content=page.content live-content=page.content
) )

View File

@ -8,4 +8,6 @@ block body
:page-id=page.id :page-id=page.id
locale=page.localeCode locale=page.localeCode
path=page.path path=page.path
:version-id=page.versionId
version-date=page.versionDate
)= page.content )= page.content