ソースを参照

feat: view version of page source

NGPixel 5 年 前
コミット
e50dc89519

+ 89 - 14
client/components/history.vue

@@ -23,30 +23,30 @@
               dense
               )
               v-timeline-item.pb-2(
-                v-for='(ph, idx) in trail'
+                v-for='(ph, idx) in fullTrail'
                 :key='ph.versionId'
                 :small='ph.actionType === `edit`'
                 :color='trailColor(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-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)
                     .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')
+                        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')
+                        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)')
@@ -55,8 +55,8 @@
                         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)')
-                          v-list-item-avatar(size='24'): v-icon mdi-history
+                        v-list-item(@click='restore(ph.versionId)', :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
@@ -68,7 +68,7 @@
                       depressed
                       tile
                       :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
                     v-btn.mr-0.radius-4(
                       @click='setDiffTarget(ph.versionId)'
@@ -77,7 +77,7 @@
                       depressed
                       tile
                       :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
 
             v-btn.ma-0.radius-7(
@@ -137,6 +137,38 @@ export default {
       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: ''
@@ -167,6 +199,20 @@ export default {
   },
   computed: {
     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 () {
       return createPatch(`/${this.path}`, this.source.content, this.target.content)
     },
@@ -182,8 +228,8 @@ export default {
   watch: {
     trail (newValue, oldValue) {
       if (newValue && newValue.length > 0) {
-        this.diffTarget = _.get(_.head(newValue), 'versionId', 0)
-        this.diffSource = _.get(_.nth(newValue, 1), 'versionId', 0)
+        this.diffTarget = 0
+        this.diffSource = _.get(_.head(newValue), 'versionId', 0)
       }
     },
     async diffSource (newValue, oldValue) {
@@ -214,7 +260,29 @@ export default {
 
     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: {
     async loadVersion (versionId) {
@@ -230,6 +298,7 @@ export default {
                 content
                 contentType
                 createdAt
+                versionDate
                 description
                 editor
                 isPrivate
@@ -314,6 +383,8 @@ export default {
           return 'purple'
         case 'initial':
           return 'teal'
+        case 'live':
+          return 'orange'
         default:
           return 'grey'
       }
@@ -326,8 +397,10 @@ export default {
           return 'forward'
         case 'initial':
           return 'mdi-plus'
+        case 'live':
+          return 'mdi-atom-variant'
         default:
-          return 'warning'
+          return 'mdi-alert'
       }
     },
     trailBgColor (actionType) {
@@ -336,6 +409,8 @@ export default {
           return this.darkMode ? 'purple' : 'purple lighten-5'
         case 'initial':
           return this.darkMode ? 'teal darken-3' : 'teal lighten-5'
+        case 'live':
+          return this.darkMode ? 'orange darken-3' : 'orange lighten-5'
         default:
           return this.darkMode ? 'grey darken-3' : 'grey lighten-4'
       }
@@ -354,7 +429,7 @@ export default {
                 actionType
                 valueBefore
                 valueAfter
-                createdAt
+                versionDate
               }
               total
             }

+ 19 - 2
client/components/source.vue

@@ -3,11 +3,17 @@
     nav-header
     v-content
       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}}
         template(v-if='$vuetify.breakpoint.mdAndUp')
           v-spacer
           .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-card(tile)
         v-card-text
@@ -38,6 +44,14 @@ export default {
     path: {
       type: String,
       default: 'home'
+    },
+    versionId: {
+      type: Number,
+      default: 0
+    },
+    versionDate: {
+      type: String,
+      default: ''
     }
   },
   data() {
@@ -55,7 +69,10 @@ export default {
   },
   methods: {
     goLive() {
-      window.location.assign(`/${this.path}`)
+      window.location.assign(`/${this.locale}/${this.path}`)
+    },
+    goHistory () {
+      window.location.assign(`/h/${this.locale}/${this.path}`)
     }
   }
 }

+ 2 - 2
dev/build/Dockerfile

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

+ 27 - 5
server/controllers/common.js

@@ -226,6 +226,8 @@ router.get(['/p', '/p/*'], (req, res, next) => {
  */
 router.get(['/s', '/s/*'], async (req, res, next) => {
   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({
     path: pageArgs.path,
     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.rtl', req.i18n.dir() === 'rtl')
 
-  if (!WIKI.auth.checkAccess(req.user, ['read:source'], pageArgs)) {
-    return res.render('unauthorized', { action: 'source' })
+  if (versionId > 0) {
+    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) {
-    _.set(res.locals, 'pageMeta.title', page.title)
-    _.set(res.locals, 'pageMeta.description', page.description)
-    res.render('source', { page })
+    if (versionId > 0) {
+      const pageVersion = await WIKI.models.pageHistory.getVersion({ pageId: page.id, versionId })
+      _.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 {
     res.redirect(`/${pageArgs.path}`)
   }

+ 9 - 0
server/db/migrations-sqlite/2.2.17.js

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

+ 22 - 0
server/db/migrations/2.2.17.js

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

+ 2 - 1
server/graph/schemas/page.graphql

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

+ 8 - 6
server/models/pageHistory.js

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

+ 8 - 0
server/views/history.pug

@@ -8,5 +8,13 @@ block body
       :page-id=page.id
       locale=page.localeCode
       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
       )

+ 2 - 0
server/views/source.pug

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