| 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 occured'))        }      } 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>
 |