wikijs-fork/client/components/history.vue

578 lines
18 KiB
Vue
Raw Permalink Normal View History

2018-10-29 02:09:58 +00:00
<template lang='pug'>
2020-05-08 22:48:07 +00:00
v-app(:dark='$vuetify.theme.dark').history
2018-10-29 02:09:58 +00:00
nav-header
v-content
v-toolbar(color='primary', dark)
2020-02-24 04:53:27 +00:00
.subheading Viewing history of #[strong /{{path}}]
2020-02-25 02:10:43 +00:00
template(v-if='$vuetify.breakpoint.mdAndUp')
v-spacer
.caption.blue--text.text--lighten-3.mr-4 Trail Length: {{total}}
.caption.blue--text.text--lighten-3 ID: {{pageId}}
v-btn.ml-4(depressed, color='blue darken-1', @click='goLive') Return to Live Version
2018-10-29 02:09:58 +00:00
v-container(fluid, grid-list-xl)
v-layout(row, wrap)
2020-02-24 02:22:45 +00:00
v-flex(xs12, md4)
v-chip.my-0.ml-6(
2018-10-29 02:09:58 +00:00
label
small
2020-05-08 22:48:07 +00:00
:color='$vuetify.theme.dark ? `grey darken-2` : `grey lighten-2`'
:class='$vuetify.theme.dark ? `grey--text text--lighten-2` : `grey--text text--darken-2`'
2018-10-29 02:09:58 +00:00
)
span Live
v-timeline(
dense
)
v-timeline-item.pb-2(
2020-02-29 23:57:54 +00:00
v-for='(ph, idx) in fullTrail'
2018-11-25 06:28:20 +00:00
:key='ph.versionId'
:small='ph.actionType === `edit`'
:color='trailColor(ph.actionType)'
:icon='trailIcon(ph.actionType)'
2018-10-29 02:09:58 +00:00
)
2018-11-25 06:28:20 +00:00
v-card.radius-7(flat, :class='trailBgColor(ph.actionType)')
v-toolbar(flat, :color='trailBgColor(ph.actionType)', height='40')
2020-02-29 23:57:54 +00:00
.caption(:title='$options.filters.moment(ph.versionDate, `LLL`)') {{ ph.versionDate | moment('ll') }}
2020-02-24 02:22:45 +00:00
v-divider.mx-3(vertical)
.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 === `initial`') Created by #[strong {{ ph.authorName }}]
2020-02-29 23:57:54 +00:00
.caption(v-else-if='ph.actionType === `live`') Last Edited by #[strong {{ ph.authorName }}]
.caption(v-else) Unknown Action by #[strong {{ ph.authorName }}]
2018-11-25 06:28:20 +00:00
v-spacer
v-menu(offset-x, left)
2019-08-25 18:23:56 +00:00
template(v-slot:activator='{ on }')
2020-02-24 02:22:45 +00:00
v-btn.mr-2.radius-4(icon, v-on='on', small, tile): v-icon mdi-dots-horizontal
2019-08-25 18:23:56 +00:00
v-list(dense, nav).history-promptmenu
2020-02-29 23:57:54 +00:00
v-list-item(@click='setDiffSource(ph.versionId)', :disabled='(ph.versionId >= diffTarget && diffTarget !== 0) || ph.versionId === 0')
2020-02-25 02:10:43 +00:00
v-list-item-avatar(size='24'): v-avatar A
v-list-item-title Set as Differencing Source
2020-02-29 23:57:54 +00:00
v-list-item(@click='setDiffTarget(ph.versionId)', :disabled='ph.versionId <= diffSource && ph.versionId !== 0')
2020-02-25 02:10:43 +00:00
v-list-item-avatar(size='24'): v-avatar B
v-list-item-title Set as Differencing Target
v-list-item(@click='viewSource(ph.versionId)')
2019-08-25 18:23:56 +00:00
v-list-item-avatar(size='24'): v-icon mdi-code-tags
2019-07-29 04:50:03 +00:00
v-list-item-title View Source
2020-02-25 02:10:43 +00:00
v-list-item(@click='download(ph.versionId)')
2019-08-25 18:23:56 +00:00
v-list-item-avatar(size='24'): v-icon mdi-cloud-download-outline
2019-07-29 04:50:03 +00:00
v-list-item-title Download Version
2020-03-01 04:40:07 +00:00
v-list-item(@click='restore(ph.versionId, ph.versionDate)', :disabled='ph.versionId === 0')
2020-02-29 23:57:54 +00:00
v-list-item-avatar(size='24'): v-icon(:disabled='ph.versionId === 0') mdi-history
2019-07-29 04:50:03 +00:00
v-list-item-title Restore
2020-02-25 02:10:43 +00:00
v-list-item(@click='branchOff(ph.versionId)')
2019-08-25 18:23:56 +00:00
v-list-item-avatar(size='24'): v-icon mdi-source-branch
2019-07-29 04:50:03 +00:00
v-list-item-title Branch off from here
2020-02-24 02:22:45 +00:00
v-btn.mr-2.radius-4(
@click='setDiffSource(ph.versionId)'
icon
small
depressed
tile
2020-02-25 02:10:43 +00:00
:class='diffSource === ph.versionId ? `pink white--text` : ($vuetify.theme.dark ? `grey darken-2` : `grey lighten-2`)'
2020-02-29 23:57:54 +00:00
:disabled='(ph.versionId >= diffTarget && diffTarget !== 0) || ph.versionId === 0'
2020-02-24 02:22:45 +00:00
): strong A
v-btn.mr-0.radius-4(
@click='setDiffTarget(ph.versionId)'
icon
small
depressed
tile
2020-02-25 02:10:43 +00:00
:class='diffTarget === ph.versionId ? `pink white--text` : ($vuetify.theme.dark ? `grey darken-2` : `grey lighten-2`)'
2020-02-29 23:57:54 +00:00
:disabled='ph.versionId <= diffSource && ph.versionId !== 0'
2020-02-24 02:22:45 +00:00
): strong B
2018-10-29 02:09:58 +00:00
v-btn.ma-0.radius-7(
v-if='total > trail.length'
block
2020-02-25 02:10:43 +00:00
color='primary'
@click='loadMore'
)
.caption.white--text Load More...
v-chip.ma-0(
v-else
2018-10-29 02:09:58 +00:00
label
small
2020-05-08 22:48:07 +00:00
:color='$vuetify.theme.dark ? `grey darken-2` : `grey lighten-2`'
:class='$vuetify.theme.dark ? `grey--text text--lighten-2` : `grey--text text--darken-2`'
2018-11-25 06:28:20 +00:00
) End of history trail
2018-10-29 02:09:58 +00:00
2020-02-24 02:22:45 +00:00
v-flex(xs12, md8)
2020-02-25 02:10:43 +00:00
v-card.radius-7(:class='$vuetify.breakpoint.mdAndUp ? `mt-8` : ``')
2018-10-29 02:09:58 +00:00
v-card-text
2020-05-08 22:48:07 +00:00
v-card.grey.radius-7(flat, :class='$vuetify.theme.dark ? `darken-2` : `lighten-4`')
2020-02-24 02:22:45 +00:00
v-row(no-gutters, align='center')
2020-02-25 02:10:43 +00:00
v-col
2020-02-24 02:22:45 +00:00
v-card-text
.subheading {{target.title}}
.caption {{target.description}}
2020-02-25 02:10:43 +00:00
v-col.text-right.py-3(cols='2', v-if='$vuetify.breakpoint.mdAndUp')
v-btn.mr-3(:color='$vuetify.theme.dark ? `white` : `grey darken-3`', small, dark, outlined, @click='toggleViewMode')
2020-02-24 02:22:45 +00:00
v-icon(left) mdi-eye
.overline View Mode
v-card.mt-3(light, v-html='diffHTML', flat)
2018-10-29 02:09:58 +00:00
2020-03-01 04:40:07 +00:00
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')}}
page-selector(mode='create', v-model='branchOffOpts.modal', :open-handler='branchOffHandle', :path='branchOffOpts.path', :locale='branchOffOpts.locale')
2018-10-29 02:09:58 +00:00
nav-footer
notify
search-results
2018-10-29 02:09:58 +00:00
</template>
<script>
2020-02-24 02:22:45 +00:00
import * as Diff2Html from 'diff2html'
2018-11-25 06:28:20 +00:00
import { createPatch } from 'diff'
import _ from 'lodash'
2020-02-24 04:53:27 +00:00
import gql from 'graphql-tag'
2018-11-25 06:28:20 +00:00
2018-10-29 02:09:58 +00:00
export default {
2020-03-01 04:40:07 +00:00
i18nOptions: { namespaces: 'history' },
2018-10-29 02:09:58 +00:00
props: {
2018-11-25 06:28:20 +00:00
pageId: {
2018-10-29 02:09:58 +00:00
type: Number,
default: 0
},
locale: {
type: String,
default: 'en'
},
path: {
type: String,
default: 'home'
2018-11-25 06:28:20 +00:00
},
2020-02-29 23:57:54 +00:00
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
},
2018-11-25 06:28:20 +00:00
liveContent: {
type: String,
default: ''
},
effectivePermissions: {
type: String,
default: ''
2018-10-29 02:09:58 +00:00
}
},
2020-02-24 04:53:27 +00:00
data () {
2018-11-25 06:28:20 +00:00
return {
2020-02-24 02:22:45 +00:00
source: {
2020-02-24 04:53:27 +00:00
versionId: 0,
2020-02-24 02:22:45 +00:00
content: '',
title: '',
description: ''
},
target: {
2020-02-24 04:53:27 +00:00
versionId: 0,
2020-02-24 02:22:45 +00:00
content: '',
title: '',
description: ''
},
2018-11-25 06:28:20 +00:00
trail: [],
diffSource: 0,
diffTarget: 0,
offsetPage: 0,
2020-02-24 02:22:45 +00:00
total: 0,
2020-02-24 04:53:27 +00:00
viewMode: 'line-by-line',
2020-03-01 04:40:07 +00:00
cache: [],
restoreTarget: {
versionId: 0,
versionDate: ''
},
branchOffOpts: {
versionId: 0,
locale: 'en',
path: 'new-page',
modal: false
},
2020-03-01 04:40:07 +00:00
isRestoreConfirmDialogShown: false,
restoreLoading: false
2018-11-25 06:28:20 +00:00
}
2018-10-29 02:09:58 +00:00
},
computed: {
2020-02-29 23:57:54 +00:00
fullTrail () {
const liveTrailItem = {
versionId: 0,
authorId: this.authorId,
authorName: this.authorName,
actionType: 'live',
valueBefore: null,
valueAfter: null,
versionDate: this.updatedAt
}
// -> Check for move between latest and live
const prevPage = _.find(this.cache, ['versionId', _.get(this.trail, '[0].versionId', -1)])
if (prevPage && this.path !== prevPage.path) {
liveTrailItem.actionType = 'move'
liveTrailItem.valueBefore = prevPage.path
liveTrailItem.valueAfter = this.path
}
// -> Combine trail with live
2020-02-29 23:57:54 +00:00
return [
liveTrailItem,
2020-02-29 23:57:54 +00:00
...this.trail
]
},
2020-02-24 04:53:27 +00:00
diffs () {
2020-02-24 02:22:45 +00:00
return createPatch(`/${this.path}`, this.source.content, this.target.content)
2018-11-25 06:28:20 +00:00
},
2020-02-24 04:53:27 +00:00
diffHTML () {
2020-02-24 02:22:45 +00:00
return Diff2Html.html(this.diffs, {
2018-11-25 06:28:20 +00:00
inputFormat: 'diff',
2020-02-24 02:22:45 +00:00
drawFileList: false,
2018-11-25 06:28:20 +00:00
matching: 'lines',
2020-02-24 02:22:45 +00:00
outputFormat: this.viewMode
2018-11-25 06:28:20 +00:00
})
}
},
watch: {
2020-02-24 04:53:27 +00:00
trail (newValue, oldValue) {
2018-11-25 06:28:20 +00:00
if (newValue && newValue.length > 0) {
2020-02-29 23:57:54 +00:00
this.diffTarget = 0
this.diffSource = _.get(_.head(newValue), 'versionId', 0)
2018-11-25 06:28:20 +00:00
}
2020-02-24 04:53:27 +00:00
},
async diffSource (newValue, oldValue) {
if (this.diffSource !== this.source.versionId) {
const page = _.find(this.cache, { versionId: newValue })
if (page) {
this.source = page
} else {
this.source = await this.loadVersion(newValue)
}
}
},
async diffTarget (newValue, oldValue) {
if (this.diffTarget !== this.target.versionId) {
const page = _.find(this.cache, { versionId: newValue })
if (page) {
this.target = page
} else {
this.target = await this.loadVersion(newValue)
}
}
2018-11-25 06:28:20 +00:00
}
2018-10-29 02:09:58 +00:00
},
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')
2018-11-25 06:28:20 +00:00
2020-02-29 23:57:54 +00:00
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]
if (this.effectivePermissions) {
2020-07-05 19:59:02 +00:00
this.$store.set('page/effectivePermissions', JSON.parse(Buffer.from(this.effectivePermissions, 'base64').toString()))
}
2018-10-29 02:09:58 +00:00
},
methods: {
2020-02-24 04:53:27 +00:00
async loadVersion (versionId) {
this.$store.commit(`loadingStart`, 'history-version-' + versionId)
const resp = await this.$apollo.query({
query: gql`
query ($pageId: Int!, $versionId: Int!) {
pages {
version (pageId: $pageId, versionId: $versionId) {
action
authorId
authorName
content
contentType
createdAt
2020-02-29 23:57:54 +00:00
versionDate
2020-02-24 04:53:27 +00:00
description
editor
isPrivate
isPublished
locale
pageId
path
publishEndDate
publishStartDate
tags
title
versionId
}
}
}
`,
variables: {
versionId,
pageId: this.pageId
}
})
this.$store.commit(`loadingStop`, 'history-version-' + versionId)
const page = _.get(resp, 'data.pages.version', null)
if (page) {
this.cache.push(page)
return page
} else {
return { content: '' }
}
2020-02-25 02:10:43 +00:00
},
viewSource (versionId) {
window.location.assign(`/s/${this.locale}/${this.path}?v=${versionId}`)
},
download (versionId) {
window.location.assign(`/d/${this.locale}/${this.path}?v=${versionId}`)
},
2020-03-01 04:40:07 +00:00
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 occurred'))
2020-03-01 04:40:07 +00:00
}
} catch (err) {
this.$store.commit('showNotification', {
style: 'red',
message: err.message,
icon: 'alert'
})
}
this.$store.commit(`loadingStop`, 'history-restore')
this.restoreLoading = false
2020-02-25 02:10:43 +00:00
},
branchOff (versionId) {
const pathParts = this.path.split('/')
this.branchOffOpts = {
versionId: versionId,
locale: this.locale,
path: (pathParts.length > 1) ? _.initial(pathParts).join('/') + `/new-page` : `new-page`,
modal: true
}
},
branchOffHandle ({ locale, path }) {
window.location.assign(`/e/${locale}/${path}?from=${this.pageId},${this.branchOffOpts.versionId}`)
2020-02-24 04:53:27 +00:00
},
2020-02-24 02:22:45 +00:00
toggleViewMode () {
this.viewMode = (this.viewMode === 'line-by-line') ? 'side-by-side' : 'line-by-line'
},
2020-02-24 04:53:27 +00:00
goLive () {
2018-10-29 02:09:58 +00:00
window.location.assign(`/${this.path}`)
2018-11-25 06:28:20 +00:00
},
2020-02-24 04:53:27 +00:00
setDiffSource (versionId) {
2018-11-25 06:28:20 +00:00
this.diffSource = versionId
},
2020-02-24 04:53:27 +00:00
setDiffTarget (versionId) {
2018-11-25 06:28:20 +00:00
this.diffTarget = versionId
},
2020-02-24 04:53:27 +00:00
loadMore () {
this.offsetPage++
this.$apollo.queries.trail.fetchMore({
variables: {
id: this.pageId,
offsetPage: this.offsetPage,
2020-02-25 02:10:43 +00:00
offsetSize: this.$vuetify.breakpoint.mdAndUp ? 25 : 5
},
updateQuery: (previousResult, { fetchMoreResult }) => {
return {
pages: {
history: {
total: previousResult.pages.history.total,
trail: [...previousResult.pages.history.trail, ...fetchMoreResult.pages.history.trail],
__typename: previousResult.pages.history.__typename
},
__typename: previousResult.pages.__typename
}
}
}
})
},
2020-02-24 04:53:27 +00:00
trailColor (actionType) {
2018-11-25 06:28:20 +00:00
switch (actionType) {
case 'edit':
return 'primary'
case 'move':
return 'purple'
case 'initial':
return 'teal'
2020-02-29 23:57:54 +00:00
case 'live':
return 'orange'
2018-11-25 06:28:20 +00:00
default:
return 'grey'
}
},
2020-02-24 04:53:27 +00:00
trailIcon (actionType) {
2018-11-25 06:28:20 +00:00
switch (actionType) {
case 'edit':
2020-02-24 02:22:45 +00:00
return '' // 'mdi-pencil'
2018-11-25 06:28:20 +00:00
case 'move':
return 'mdi-forward'
2018-11-25 06:28:20 +00:00
case 'initial':
2019-08-25 18:23:56 +00:00
return 'mdi-plus'
2020-02-29 23:57:54 +00:00
case 'live':
return 'mdi-atom-variant'
2018-11-25 06:28:20 +00:00
default:
2020-02-29 23:57:54 +00:00
return 'mdi-alert'
2018-11-25 06:28:20 +00:00
}
},
2020-02-24 04:53:27 +00:00
trailBgColor (actionType) {
2018-11-25 06:28:20 +00:00
switch (actionType) {
case 'move':
2020-05-08 22:48:07 +00:00
return this.$vuetify.theme.dark ? 'purple' : 'purple lighten-5'
2018-11-25 06:28:20 +00:00
case 'initial':
2020-05-08 22:48:07 +00:00
return this.$vuetify.theme.dark ? 'teal darken-3' : 'teal lighten-5'
2020-02-29 23:57:54 +00:00
case 'live':
2020-05-08 22:48:07 +00:00
return this.$vuetify.theme.dark ? 'orange darken-3' : 'orange lighten-5'
2018-11-25 06:28:20 +00:00
default:
2020-05-08 22:48:07 +00:00
return this.$vuetify.theme.dark ? 'grey darken-3' : 'grey lighten-4'
2018-11-25 06:28:20 +00:00
}
}
},
apollo: {
trail: {
2020-02-24 04:53:27 +00:00
query: gql`
query($id: Int!, $offsetPage: Int, $offsetSize: Int) {
pages {
history(id:$id, offsetPage:$offsetPage, offsetSize:$offsetSize) {
trail {
versionId
authorId
authorName
actionType
valueBefore
valueAfter
2020-02-29 23:57:54 +00:00
versionDate
2020-02-24 04:53:27 +00:00
}
total
}
}
}
`,
variables () {
2018-11-25 06:28:20 +00:00
return {
id: this.pageId,
offsetPage: 0,
2020-02-25 02:10:43 +00:00
offsetSize: this.$vuetify.breakpoint.mdAndUp ? 25 : 5
2018-11-25 06:28:20 +00:00
}
},
manual: true,
2020-02-24 04:53:27 +00:00
result ({ data, loading, networkStatus }) {
this.total = data.pages.history.total
this.trail = data.pages.history.trail
},
2018-11-25 06:28:20 +00:00
watchLoading (isLoading) {
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'history-trail-refresh')
}
2018-10-29 02:09:58 +00:00
}
}
}
</script>
<style lang='scss'>
2018-11-25 06:28:20 +00:00
.history {
&-promptmenu {
border-top: 5px solid mc('blue', '700');
}
2020-02-24 02:22:45 +00:00
.d2h-file-wrapper {
border: 1px solid #EEE;
border-left: none;
}
.d2h-file-header {
display: none;
}
2018-11-25 06:28:20 +00:00
}
2018-10-29 02:09:58 +00:00
</style>