Browse Source

feat: source + history (wip)

Nicolas Giard 6 years ago
parent
commit
78ba895eee

+ 1 - 0
client/client-app.js

@@ -158,6 +158,7 @@ Vue.prototype.Velocity = Velocity
 Vue.component('admin', () => import(/* webpackChunkName: "admin" */ './components/admin.vue'))
 Vue.component('admin', () => import(/* webpackChunkName: "admin" */ './components/admin.vue'))
 Vue.component('editor', () => import(/* webpackPrefetch: -100, webpackChunkName: "editor" */ './components/editor.vue'))
 Vue.component('editor', () => import(/* webpackPrefetch: -100, webpackChunkName: "editor" */ './components/editor.vue'))
 Vue.component('history', () => import(/* webpackChunkName: "history" */ './components/history.vue'))
 Vue.component('history', () => import(/* webpackChunkName: "history" */ './components/history.vue'))
+Vue.component('page-source', () => import(/* webpackChunkName: "source" */ './components/source.vue'))
 Vue.component('login', () => import(/* webpackPrefetch: true, webpackChunkName: "login" */ './components/login.vue'))
 Vue.component('login', () => import(/* webpackPrefetch: true, webpackChunkName: "login" */ './components/login.vue'))
 Vue.component('nav-header', () => import(/* webpackMode: "eager" */ './components/common/nav-header.vue'))
 Vue.component('nav-header', () => import(/* webpackMode: "eager" */ './components/common/nav-header.vue'))
 Vue.component('page-selector', () => import(/* webpackPrefetch: true, webpackChunkName: "ui-extra" */ './components/common/page-selector.vue'))
 Vue.component('page-selector', () => import(/* webpackPrefetch: true, webpackChunkName: "ui-extra" */ './components/common/page-selector.vue'))

+ 1 - 11
client/components/common/page-selector.vue

@@ -31,17 +31,7 @@
         v-flex(xs8)
         v-flex(xs8)
           v-toolbar(color='grey darken-2', dark, dense, flat)
           v-toolbar(color='grey darken-2', dark, dense, flat)
             .body-2 Pages
             .body-2 Pages
-            v-divider.ml-4(vertical)
-            v-text-field(
-              prepend-inner-icon='search'
-              label='Search...'
-              hide-details
-              solo
-              background-color='grey darken-2'
-              flat
-              clearable
-              )
-            v-divider.mx-3(vertical)
+            v-spacer
             v-btn(icon): v-icon forward
             v-btn(icon): v-icon forward
             v-btn(icon): v-icon delete
             v-btn(icon): v-icon delete
           v-list(dense)
           v-list(dense)

+ 160 - 61
client/components/history.vue

@@ -5,11 +5,11 @@
       v-toolbar(color='primary', dark)
       v-toolbar(color='primary', dark)
         .subheading Viewing history of page #[strong /{{path}}]
         .subheading Viewing history of page #[strong /{{path}}]
         v-spacer
         v-spacer
-        .caption.blue--text.text--lighten-3 ID {{id}}
+        .caption.blue--text.text--lighten-3 ID {{pageId}}
         v-btn.ml-4(depressed, color='blue darken-1', @click='goLive') Return to Live Version
         v-btn.ml-4(depressed, color='blue darken-1', @click='goLive') Return to Live Version
       v-container(fluid, grid-list-xl)
       v-container(fluid, grid-list-xl)
         v-layout(row, wrap)
         v-layout(row, wrap)
-          v-flex(xs5)
+          v-flex(xs4)
             v-chip.ma-0.grey--text.text--darken-2(
             v-chip.ma-0.grey--text.text--darken-2(
               label
               label
               small
               small
@@ -20,86 +20,92 @@
               dense
               dense
               )
               )
               v-timeline-item(
               v-timeline-item(
+                v-for='ph in trail'
+                :key='ph.versionId'
+                :small='ph.actionType === `edit`'
                 fill-dot
                 fill-dot
-                color='primary'
-                icon='edit'
+                :color='trailColor(ph.actionType)'
+                :icon='trailIcon(ph.actionType)'
                 )
                 )
-                v-card.grey.lighten-3.radius-7(flat)
-                  v-card-text
-                    v-layout(justify-space-between)
-                      v-flex(xs7)
-                        v-chip.ml-0.mr-3(
-                          label
-                          small
-                          color='primary'
-                          )
-                          span.white--text Viewing
-                        span Edited by John Doe
-                      v-flex(xs5, text-xs-right, align-center, d-flex)
-                        .caption Today at 12:34 PM
-
-              v-timeline-item(
-                fill-dot
-                small
-                color='primary'
-                icon='edit'
-                )
-                v-card.grey.lighten-3.radius-7(flat)
-                  v-card-text
-                    v-layout(justify-space-between)
-                      v-flex(xs7)
-                        span Edited by Jane Doe
-                      v-flex(xs5, text-xs-right, align-center, d-flex)
-                        .caption Today at 12:27 PM
-
-              v-timeline-item(
-                fill-dot
-                small
-                color='purple'
-                icon='forward'
-                )
-                v-card.purple.lighten-5.radius-7(flat)
-                  v-card-text
-                    v-layout(justify-space-between)
-                      v-flex(xs7)
-                        span Moved page from #[strong /test] to #[strong /home] by John Doe
-                      v-flex(xs5, text-xs-right, align-center, d-flex)
-                        .caption Yesterday at 10:45 AM
+                v-card.radius-7(flat, :class='trailBgColor(ph.actionType)')
+                  v-toolbar(flat, :color='trailBgColor(ph.actionType)')
+                    v-chip.ml-0.mr-3(
+                      v-if='diffSource === ph.versionId'
+                      label
+                      small
+                      color='pink'
+                      )
+                      .caption.white--text Source
+                    v-chip.ml-0.mr-3(
+                      v-if='diffTarget === ph.versionId'
+                      label
+                      small
+                      color='pink'
+                      )
+                      .caption.white--text Target
+                    .caption(v-if='ph.actionType === `edit`') Edited by {{ ph.authorName }}
+                    .caption(v-else-if='ph.actionType === `move`') Moved from #[strong {{ph.valueBefore}}] to #[strong {{ph.valueAfter}}] by {{ ph.authorName }}
+                    .caption(v-else-if='ph.actionType === `initial`') Created by {{ ph.authorName }}
+                    .caption(v-else) Unknown Action by {{ ph.authorName }}
+                    v-spacer
+                    .caption {{ ph.createdAt | moment('calendar') }}
+                    v-menu(offset-x, left)
+                      v-btn(icon, slot='activator'): v-icon more_horiz
+                      v-list(dense).history-promptmenu
+                        v-list-tile(@click='setDiffTarget(ph.versionId)')
+                          v-list-tile-avatar: v-icon call_made
+                          v-list-tile-title Set as Differencing Target
+                        v-divider
+                        v-list-tile(@click='setDiffSource(ph.versionId)')
+                          v-list-tile-avatar: v-icon call_received
+                          v-list-tile-title Set as Differencing Source
+                        v-divider
+                        v-list-tile
+                          v-list-tile-avatar: v-icon code
+                          v-list-tile-title View Source
+                        v-divider
+                        v-list-tile
+                          v-list-tile-avatar: v-icon cloud_download
+                          v-list-tile-title Download Version
+                        v-divider
+                        v-list-tile
+                          v-list-tile-avatar: v-icon restore
+                          v-list-tile-title Restore
+                        v-divider
+                        v-list-tile
+                          v-list-tile-avatar: v-icon call_split
+                          v-list-tile-title Branch off from here
 
 
-              v-timeline-item(
-                fill-dot
-                color='teal'
-                icon='add'
-                )
-                v-card.teal.lighten-5.radius-7(flat)
-                  v-card-text
-                    v-layout(justify-space-between)
-                      v-flex(xs7): span Initial page creation by John Doe
-                      v-flex(xs5, text-xs-right, align-center, d-flex)
-                        .caption Last Tuesday at 7:56 PM
             v-chip.ma-0.grey--text.text--darken-2(
             v-chip.ma-0.grey--text.text--darken-2(
               label
               label
               small
               small
               color='grey lighten-2'
               color='grey lighten-2'
-              ) End of history
+              ) End of history trail
 
 
-          v-flex(xs7)
+          v-flex(xs8)
             v-card.radius-7
             v-card.radius-7
               v-card-text
               v-card-text
                 v-card.grey.lighten-4.radius-7(flat)
                 v-card.grey.lighten-4.radius-7(flat)
                   v-card-text
                   v-card-text
                     .subheading Page Title
                     .subheading Page Title
                     .caption Some page description
                     .caption Some page description
+                .mt-3(v-html='diffHTML')
 
 
     nav-footer
     nav-footer
 </template>
 </template>
 
 
 <script>
 <script>
+import { Diff2Html } from 'diff2html'
+import { createPatch } from 'diff'
+import _ from 'lodash'
+
+import historyTrailQuery from 'gql/history/history-trail-query.gql'
+
 /* global siteConfig */
 /* global siteConfig */
 
 
 export default {
 export default {
   props: {
   props: {
-    id: {
+    pageId: {
       type: Number,
       type: Number,
       default: 0
       default: 0
     },
     },
@@ -110,13 +116,43 @@ export default {
     path: {
     path: {
       type: String,
       type: String,
       default: 'home'
       default: 'home'
+    },
+    liveContent: {
+      type: String,
+      default: ''
     }
     }
   },
   },
   data() {
   data() {
-    return {}
+    return {
+      sourceText: '',
+      targetText: '',
+      trail: [],
+      diffSource: 0,
+      diffTarget: 0,
+      offset: 0
+    }
   },
   },
   computed: {
   computed: {
-    darkMode() { return siteConfig.darkMode }
+    darkMode() { return siteConfig.darkMode },
+    diffs() {
+      return createPatch(`/${this.path}`, this.sourceText, this.targetText)
+    },
+    diffHTML() {
+      return Diff2Html.getPrettyHtml(this.diffs, {
+        inputFormat: 'diff',
+        showFiles: false,
+        matching: 'lines',
+        outputFormat: 'line-by-line'
+      })
+    }
+  },
+  watch: {
+    trail(newValue, oldValue) {
+      if (newValue && newValue.length > 0) {
+        this.diffTarget = _.get(_.head(newValue), 'versionId', 0)
+        this.diffSource = _.get(_.nth(newValue, 1), 'versionId', 0)
+      }
+    }
   },
   },
   created () {
   created () {
     this.$store.commit('page/SET_ID', this.id)
     this.$store.commit('page/SET_ID', this.id)
@@ -124,10 +160,67 @@ export default {
     this.$store.commit('page/SET_PATH', this.path)
     this.$store.commit('page/SET_PATH', this.path)
 
 
     this.$store.commit('page/SET_MODE', 'history')
     this.$store.commit('page/SET_MODE', 'history')
+
+    this.targetText = this.liveContent
   },
   },
   methods: {
   methods: {
     goLive() {
     goLive() {
       window.location.assign(`/${this.path}`)
       window.location.assign(`/${this.path}`)
+    },
+    setDiffSource(versionId) {
+      this.diffSource = versionId
+    },
+    setDiffTarget(versionId) {
+      this.diffTarget = versionId
+    },
+    trailColor(actionType) {
+      switch (actionType) {
+        case 'edit':
+          return 'primary'
+        case 'move':
+          return 'purple'
+        case 'initial':
+          return 'teal'
+        default:
+          return 'grey'
+      }
+    },
+    trailIcon(actionType) {
+      switch (actionType) {
+        case 'edit':
+          return 'edit'
+        case 'move':
+          return 'forward'
+        case 'initial':
+          return 'add'
+        default:
+          return 'warning'
+      }
+    },
+    trailBgColor(actionType) {
+      switch (actionType) {
+        case 'move':
+          return 'purple lighten-5'
+        case 'initial':
+          return 'teal lighten-5'
+        default:
+          return 'grey lighten-3'
+      }
+    }
+  },
+  apollo: {
+    trail: {
+      query: historyTrailQuery,
+      variables() {
+        return {
+          id: this.pageId,
+          offset: 0
+        }
+      },
+      update: (data) => data.pages.history,
+      watchLoading (isLoading) {
+        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'history-trail-refresh')
+      }
     }
     }
   }
   }
 }
 }
@@ -135,4 +228,10 @@ export default {
 
 
 <style lang='scss'>
 <style lang='scss'>
 
 
+.history {
+  &-promptmenu {
+    border-top: 5px solid mc('blue', '700');
+  }
+}
+
 </style>
 </style>

+ 76 - 0
client/components/source.vue

@@ -0,0 +1,76 @@
+<template lang='pug'>
+  v-app(:dark='darkMode').source
+    nav-header
+    v-content
+      v-toolbar(color='primary', dark)
+        .subheading Viewing source of page #[strong /{{path}}]
+        v-spacer
+        .caption.blue--text.text--lighten-3 ID {{pageId}}
+        v-btn.ml-4(depressed, color='blue darken-1', @click='goLive') Return to Normal View
+      v-card(tile)
+        v-card-text
+          v-card.grey.lighten-4.radius-7(flat)
+            v-card-text
+              pre
+                code
+                  slot
+
+    nav-footer
+</template>
+
+<script>
+/* global siteConfig */
+
+export default {
+  props: {
+    pageId: {
+      type: Number,
+      default: 0
+    },
+    locale: {
+      type: String,
+      default: 'en'
+    },
+    path: {
+      type: String,
+      default: 'home'
+    }
+  },
+  data() {
+    return {}
+  },
+  computed: {
+    darkMode() { return siteConfig.darkMode }
+  },
+  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')
+  },
+  methods: {
+    goLive() {
+      window.location.assign(`/${this.path}`)
+    }
+  }
+}
+</script>
+
+<style lang='scss'>
+
+.source {
+  pre > code {
+    box-shadow: none;
+    color: mc('grey', '800');
+    font-family: 'Source Code Pro', sans-serif;
+    font-weight: 400;
+    font-size: 1rem;
+
+    &::before {
+      display: none;
+    }
+  }
+}
+
+</style>

+ 13 - 0
client/graph/history/history-trail-query.gql

@@ -0,0 +1,13 @@
+query($id: Int!, $offset: Int) {
+  pages {
+    history(id:$id, offset:$offset) {
+      versionId
+      authorId
+      authorName
+      actionType
+      valueBefore
+      valueAfter
+      createdAt
+    }
+  }
+}

+ 1 - 0
client/scss/app.scss

@@ -5,6 +5,7 @@
 
 
 @import "../libs/animate/animate";
 @import "../libs/animate/animate";
 @import '~vue2-animate/src/sass/vue2-animate';
 @import '~vue2-animate/src/sass/vue2-animate';
+@import '~diff2html/dist/diff2html.min.css';
 
 
 @import 'components/v-btn';
 @import 'components/v-btn';
 @import 'components/v-data-table';
 @import 'components/v-data-table';

+ 1 - 1
client/scss/base/base.scss

@@ -20,7 +20,7 @@ html {
 }
 }
 
 
 
 
-@for $i from 1 through 25 {
+@for $i from 0 through 25 {
   .radius-#{$i} {
   .radius-#{$i} {
     border-radius: #{$i}px;
     border-radius: #{$i}px;
   }
   }

+ 14 - 2
client/themes/default/scss/app.scss

@@ -95,6 +95,13 @@
     text-align: justify;
     text-align: justify;
   }
   }
 
 
+  hr {
+    margin: 1rem;
+    height: 1px;
+    border: none;
+    background-color: mc('grey', '400');
+  }
+
   blockquote {
   blockquote {
     padding: 0 0 1rem 0;
     padding: 0 0 1rem 0;
     border: 1px solid mc('blue', '500');
     border: 1px solid mc('blue', '500');
@@ -204,17 +211,18 @@
 
 
   .task-list-item {
   .task-list-item {
     position: relative;
     position: relative;
+    list-style-type: none;
 
 
     &-checkbox[disabled] {
     &-checkbox[disabled] {
       display: none;
       display: none;
 
 
       & + label {
       & + label {
-        padding-left: 1.4rem;
+        padding-left: 1.5rem;
       }
       }
 
 
       & + label::before {
       & + label::before {
         position: absolute;
         position: absolute;
-        left: 1rem;
+        left: 0;
         top: 2px;
         top: 2px;
         content: ' ';
         content: ' ';
         display: block;
         display: block;
@@ -233,6 +241,10 @@
         content: '✓';
         content: '✓';
       }
       }
     }
     }
+
+    .contains-task-list {
+      padding: .5rem 0 0 1.5rem;
+    }
   }
   }
 
 
 }
 }

+ 1 - 0
package.json

@@ -60,6 +60,7 @@
     "cookie-parser": "1.4.3",
     "cookie-parser": "1.4.3",
     "cors": "2.8.5",
     "cors": "2.8.5",
     "dependency-graph": "0.7.2",
     "dependency-graph": "0.7.2",
+    "diff": "3.5.0",
     "diff2html": "2.5.0",
     "diff2html": "2.5.0",
     "dotize": "^0.2.0",
     "dotize": "^0.2.0",
     "execa": "1.0.0",
     "execa": "1.0.0",

+ 20 - 2
server/controllers/common.js

@@ -46,11 +46,11 @@ router.get(['/p', '/p/*'], (req, res, next) => {
 })
 })
 
 
 /**
 /**
- * View document
+ * History
  */
  */
 router.get(['/h', '/h/*'], async (req, res, next) => {
 router.get(['/h', '/h/*'], async (req, res, next) => {
   const pageArgs = pageHelper.parsePath(req.path)
   const pageArgs = pageHelper.parsePath(req.path)
-  const page = await WIKI.models.pages.getPage({
+  const page = await WIKI.models.pages.getPageFromDb({
     path: pageArgs.path,
     path: pageArgs.path,
     locale: pageArgs.locale,
     locale: pageArgs.locale,
     userId: req.user.id,
     userId: req.user.id,
@@ -63,6 +63,24 @@ router.get(['/h', '/h/*'], async (req, res, next) => {
   }
   }
 })
 })
 
 
+/**
+ * Source
+ */
+router.get(['/s', '/s/*'], async (req, res, next) => {
+  const pageArgs = pageHelper.parsePath(req.path)
+  const page = await WIKI.models.pages.getPageFromDb({
+    path: pageArgs.path,
+    locale: pageArgs.locale,
+    userId: req.user.id,
+    isPrivate: false
+  })
+  if (page) {
+    res.render('source', { page })
+  } else {
+    res.redirect(`/${pageArgs.path}`)
+  }
+})
+
 /**
 /**
  * View document
  * View document
  */
  */

+ 6 - 0
server/graph/resolvers/page.js

@@ -10,6 +10,12 @@ module.exports = {
     async pages() { return {} }
     async pages() { return {} }
   },
   },
   PageQuery: {
   PageQuery: {
+    async history(obj, args, context, info) {
+      return WIKI.models.pageHistory.getHistory({
+        pageId: args.id,
+        offset: args.offset || 0
+      })
+    },
     async list(obj, args, context, info) {
     async list(obj, args, context, info) {
       return WIKI.models.pages.query().select(
       return WIKI.models.pages.query().select(
         'pages.*',
         'pages.*',

+ 15 - 0
server/graph/schemas/page.graphql

@@ -15,6 +15,11 @@ extend type Mutation {
 # -----------------------------------------------
 # -----------------------------------------------
 
 
 type PageQuery {
 type PageQuery {
+  history(
+    id: Int!
+    offset: Int
+  ): [PageHistory]
+
   list(
   list(
     filter: String
     filter: String
     orderBy: String
     orderBy: String
@@ -92,3 +97,13 @@ type Page {
   createdAt: Date!
   createdAt: Date!
   updatedAt: Date!
   updatedAt: Date!
 }
 }
+
+type PageHistory {
+  versionId: Int!
+  authorId: Int!
+  authorName: String!
+  actionType: String!
+  valueBefore: String
+  valueAfter: String
+  createdAt: Date!
+}

+ 50 - 0
server/models/pageHistory.js

@@ -1,4 +1,5 @@
 const Model = require('objection').Model
 const Model = require('objection').Model
+const _ = require('lodash')
 
 
 /* global WIKI */
 /* global WIKI */
 
 
@@ -101,4 +102,53 @@ module.exports = class PageHistory extends Model {
       title: opts.title
       title: opts.title
     })
     })
   }
   }
+
+  static async getHistory({ pageId, offset = 0 }) {
+    const history = await WIKI.models.pageHistory.query()
+      .column([
+        'pageHistory.id',
+        'pageHistory.path',
+        'pageHistory.authorId',
+        'pageHistory.createdAt',
+        {
+          authorName: 'author.name'
+        }
+      ])
+      .joinRelation('author')
+      .where({
+        'pageHistory.pageId': pageId
+      })
+      .orderBy('pageHistory.createdAt', 'asc')
+      .offset(offset)
+      .limit(20)
+
+    let prevPh = null
+
+    return _.reduce(history, (res, ph) => {
+      let actionType = 'edit'
+      let valueBefore = null
+      let valueAfter = null
+
+      if (!prevPh && offset === 0) {
+        actionType = 'initial'
+      } else if (_.get(prevPh, 'path', '') !== ph.path) {
+        actionType = 'move'
+        valueBefore = _.get(prevPh, 'path', '')
+        valueAfter = ph.path
+      }
+
+      res.unshift({
+        versionId: ph.id,
+        authorId: ph.authorId,
+        authorName: ph.authorName,
+        actionType,
+        valueBefore,
+        valueAfter,
+        createdAt: ph.createdAt
+      })
+
+      prevPh = ph
+      return res
+    }, [])
+  }
 }
 }

+ 1 - 2
server/models/pages.js

@@ -197,7 +197,7 @@ module.exports = class Page extends Model {
   }
   }
 
 
   static async getPageFromDb(opts) {
   static async getPageFromDb(opts) {
-    const page = await WIKI.models.pages.query()
+    return WIKI.models.pages.query()
       .column([
       .column([
         'pages.*',
         'pages.*',
         {
         {
@@ -227,7 +227,6 @@ module.exports = class Page extends Model {
         }
         }
       })
       })
       .first()
       .first()
-    return page
   }
   }
 
 
   static async savePageToCache(page) {
   static async savePageToCache(page) {

+ 2 - 1
server/views/history.pug

@@ -5,7 +5,8 @@ block head
 block body
 block body
   #root
   #root
     history(
     history(
-      id=page.id
+      :page-id=page.id
       locale=page.localeCode
       locale=page.localeCode
       path=page.path
       path=page.path
+      live-content=page.content
       )
       )

+ 11 - 0
server/views/source.pug

@@ -0,0 +1,11 @@
+extends master.pug
+
+block head
+
+block body
+  #root
+    page-source(
+      page-id=page.id
+      locale=page.localeCode
+      path=page.path
+      )= page.content

+ 1 - 1
yarn.lock

@@ -4934,7 +4934,7 @@ diff2html@2.5.0:
     lodash "^4.17.11"
     lodash "^4.17.11"
     whatwg-fetch "^3.0.0"
     whatwg-fetch "^3.0.0"
 
 
-diff@^3.1.0, diff@^3.5.0:
+diff@3.5.0, diff@^3.1.0, diff@^3.5.0:
   version "3.5.0"
   version "3.5.0"
   resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12"
   resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12"
   integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==
   integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==