123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577 |
- <template lang='pug'>
- v-app(:dark='$vuetify.theme.dark').history
- nav-header
- v-content
- v-toolbar(color='primary', dark)
- .subheading Viewing history of #[strong /{{path}}]
- 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
- v-container(fluid, grid-list-xl)
- v-layout(row, wrap)
- v-flex(xs12, md4)
- v-chip.my-0.ml-6(
- label
- small
- :color='$vuetify.theme.dark ? `grey darken-2` : `grey lighten-2`'
- :class='$vuetify.theme.dark ? `grey--text text--lighten-2` : `grey--text text--darken-2`'
- )
- span Live
- v-timeline(
- dense
- )
- v-timeline-item.pb-2(
- v-for='(ph, idx) in fullTrail'
- :key='ph.versionId'
- :small='ph.actionType === `edit`'
- :color='trailColor(ph.actionType)'
- :icon='trailIcon(ph.actionType)'
- )
- v-card.radius-7(flat, :class='trailBgColor(ph.actionType)')
- v-toolbar(flat, :color='trailBgColor(ph.actionType)', height='40')
- .caption(:title='$options.filters.moment(ph.versionDate, `LLL`)') {{ ph.versionDate | moment('ll') }}
- 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 }}]
- .caption(v-else-if='ph.actionType === `live`') Last Edited by #[strong {{ ph.authorName }}]
- .caption(v-else) Unknown Action by #[strong {{ ph.authorName }}]
- v-spacer
- v-menu(offset-x, left)
- template(v-slot:activator='{ on }')
- 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-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-title Set as Differencing Source
- 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-title Set as Differencing Target
- v-list-item(@click='viewSource(ph.versionId)')
- v-list-item-avatar(size='24'): v-icon mdi-code-tags
- v-list-item-title View Source
- v-list-item(@click='download(ph.versionId)')
- v-list-item-avatar(size='24'): v-icon mdi-cloud-download-outline
- v-list-item-title Download Version
- v-list-item(@click='restore(ph.versionId, ph.versionDate)', :disabled='ph.versionId === 0')
- v-list-item-avatar(size='24'): v-icon(:disabled='ph.versionId === 0') mdi-history
- v-list-item-title Restore
- v-list-item(@click='branchOff(ph.versionId)')
- v-list-item-avatar(size='24'): v-icon mdi-source-branch
- v-list-item-title Branch off from here
- v-btn.mr-2.radius-4(
- @click='setDiffSource(ph.versionId)'
- icon
- small
- depressed
- tile
- :class='diffSource === ph.versionId ? `pink white--text` : ($vuetify.theme.dark ? `grey darken-2` : `grey lighten-2`)'
- :disabled='(ph.versionId >= diffTarget && diffTarget !== 0) || ph.versionId === 0'
- ): strong A
- v-btn.mr-0.radius-4(
- @click='setDiffTarget(ph.versionId)'
- icon
- small
- depressed
- tile
- :class='diffTarget === ph.versionId ? `pink white--text` : ($vuetify.theme.dark ? `grey darken-2` : `grey lighten-2`)'
- :disabled='ph.versionId <= diffSource && ph.versionId !== 0'
- ): strong B
- v-btn.ma-0.radius-7(
- v-if='total > trail.length'
- block
- color='primary'
- @click='loadMore'
- )
- .caption.white--text Load More...
- v-chip.ma-0(
- v-else
- label
- small
- :color='$vuetify.theme.dark ? `grey darken-2` : `grey lighten-2`'
- :class='$vuetify.theme.dark ? `grey--text text--lighten-2` : `grey--text text--darken-2`'
- ) End of history trail
- v-flex(xs12, md8)
- v-card.radius-7(:class='$vuetify.breakpoint.mdAndUp ? `mt-8` : ``')
- v-card-text
- v-card.grey.radius-7(flat, :class='$vuetify.theme.dark ? `darken-2` : `lighten-4`')
- v-row(no-gutters, align='center')
- v-col
- v-card-text
- .subheading {{target.title}}
- .caption {{target.description}}
- 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')
- v-icon(left) mdi-eye
- .overline View Mode
- v-card.mt-3(light, v-html='diffHTML', flat)
- 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')
- nav-footer
- notify
- search-results
- </template>
- <script>
- import * as Diff2Html from 'diff2html'
- import { createPatch } from 'diff'
- import _ from 'lodash'
- import gql from 'graphql-tag'
- export default {
- i18nOptions: { namespaces: 'history' },
- props: {
- pageId: {
- type: Number,
- default: 0
- },
- locale: {
- type: String,
- default: 'en'
- },
- path: {
- type: String,
- 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: {
- type: String,
- default: ''
- },
- effectivePermissions: {
- type: String,
- default: ''
- }
- },
- data () {
- return {
- source: {
- versionId: 0,
- content: '',
- title: '',
- description: ''
- },
- target: {
- versionId: 0,
- content: '',
- title: '',
- description: ''
- },
- trail: [],
- diffSource: 0,
- diffTarget: 0,
- offsetPage: 0,
- total: 0,
- viewMode: 'line-by-line',
- cache: [],
- restoreTarget: {
- versionId: 0,
- versionDate: ''
- },
- branchOffOpts: {
- versionId: 0,
- locale: 'en',
- path: 'new-page',
- modal: false
- },
- isRestoreConfirmDialogShown: false,
- restoreLoading: false
- }
- },
- computed: {
- 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
- return [
- liveTrailItem,
- ...this.trail
- ]
- },
- diffs () {
- return createPatch(`/${this.path}`, this.source.content, this.target.content)
- },
- diffHTML () {
- return Diff2Html.html(this.diffs, {
- inputFormat: 'diff',
- drawFileList: false,
- matching: 'lines',
- outputFormat: this.viewMode
- })
- }
- },
- watch: {
- trail (newValue, oldValue) {
- if (newValue && newValue.length > 0) {
- this.diffTarget = 0
- this.diffSource = _.get(_.head(newValue), 'versionId', 0)
- }
- },
- 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)
- }
- }
- }
- },
- 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')
- 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) {
- this.$store.set('page/effectivePermissions', JSON.parse(Buffer.from(this.effectivePermissions, 'base64').toString()))
- }
- },
- methods: {
- 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
- versionDate
- 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: '' }
- }
- },
- viewSource (versionId) {
- window.location.assign(`/s/${this.locale}/${this.path}?v=${versionId}`)
- },
- download (versionId) {
- window.location.assign(`/d/${this.locale}/${this.path}?v=${versionId}`)
- },
- 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'))
- }
- } catch (err) {
- this.$store.commit('showNotification', {
- style: 'red',
- message: err.message,
- icon: 'alert'
- })
- }
- this.$store.commit(`loadingStop`, 'history-restore')
- this.restoreLoading = false
- },
- 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}`)
- },
- toggleViewMode () {
- this.viewMode = (this.viewMode === 'line-by-line') ? 'side-by-side' : 'line-by-line'
- },
- goLive () {
- window.location.assign(`/${this.path}`)
- },
- setDiffSource (versionId) {
- this.diffSource = versionId
- },
- setDiffTarget (versionId) {
- this.diffTarget = versionId
- },
- loadMore () {
- this.offsetPage++
- this.$apollo.queries.trail.fetchMore({
- variables: {
- id: this.pageId,
- offsetPage: this.offsetPage,
- 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
- }
- }
- }
- })
- },
- trailColor (actionType) {
- switch (actionType) {
- case 'edit':
- return 'primary'
- case 'move':
- return 'purple'
- case 'initial':
- return 'teal'
- case 'live':
- return 'orange'
- default:
- return 'grey'
- }
- },
- trailIcon (actionType) {
- switch (actionType) {
- case 'edit':
- return '' // 'mdi-pencil'
- case 'move':
- return 'mdi-forward'
- case 'initial':
- return 'mdi-plus'
- case 'live':
- return 'mdi-atom-variant'
- default:
- return 'mdi-alert'
- }
- },
- trailBgColor (actionType) {
- switch (actionType) {
- case 'move':
- return this.$vuetify.theme.dark ? 'purple' : 'purple lighten-5'
- case 'initial':
- return this.$vuetify.theme.dark ? 'teal darken-3' : 'teal lighten-5'
- case 'live':
- return this.$vuetify.theme.dark ? 'orange darken-3' : 'orange lighten-5'
- default:
- return this.$vuetify.theme.dark ? 'grey darken-3' : 'grey lighten-4'
- }
- }
- },
- apollo: {
- trail: {
- query: gql`
- query($id: Int!, $offsetPage: Int, $offsetSize: Int) {
- pages {
- history(id:$id, offsetPage:$offsetPage, offsetSize:$offsetSize) {
- trail {
- versionId
- authorId
- authorName
- actionType
- valueBefore
- valueAfter
- versionDate
- }
- total
- }
- }
- }
- `,
- variables () {
- return {
- id: this.pageId,
- offsetPage: 0,
- offsetSize: this.$vuetify.breakpoint.mdAndUp ? 25 : 5
- }
- },
- manual: true,
- result ({ data, loading, networkStatus }) {
- this.total = data.pages.history.total
- this.trail = data.pages.history.trail
- },
- watchLoading (isLoading) {
- this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'history-trail-refresh')
- }
- }
- }
- }
- </script>
- <style lang='scss'>
- .history {
- &-promptmenu {
- border-top: 5px solid mc('blue', '700');
- }
- .d2h-file-wrapper {
- border: 1px solid #EEE;
- border-left: none;
- }
- .d2h-file-header {
- display: none;
- }
- }
- </style>
|