Browse Source

feat: editor save conflict localization

NGPixel 5 years ago
parent
commit
507928730a

+ 2 - 2
client/components/editor.vue

@@ -203,7 +203,7 @@ export default {
 
     window.onbeforeunload = () => {
       if (!this.exitConfirmed && this.initContentParsed !== this.$store.get('editor/content')) {
-        return 'You have unsaved edits. Are you sure you want to leave the editor?'
+        return this.$t('editor:unsavedWarning')
       } else {
         return undefined
       }
@@ -291,7 +291,7 @@ export default {
           })
           if (_.get(conflictResp, 'data.pages.checkConflicts', false)) {
             this.$root.$emit('saveConflict')
-            throw new Error('Save conflict! Another user has already modified this page.')
+            throw new Error(this.$t('editor:conflict.warning'))
           }
 
           let resp = await this.$apollo.mutate({

+ 129 - 0
client/components/editor/ckeditor/conflict.vue

@@ -0,0 +1,129 @@
+<template lang="pug">
+  v-dialog(
+    v-model='isShown'
+    max-width='700'
+    )
+    v-card
+      .dialog-header.is-short.is-indigo
+        v-icon.mr-2(color='white') mdi-alert
+        span {{$t('editor:conflict.title')}}
+      v-card-text.pt-4
+        i18next.body-2(tag='div', path='editor:conflict.infoGeneric')
+          strong(place='authorName') {{latest.authorName}}
+          span(place='date', :title='$options.filters.moment(latest.updatedAt, `LLL`)') {{ latest.updatedAt | moment('from') }}.
+        v-btn.mt-2(outlined, color='indigo', small, :href='`/` + latest.locale + `/` + latest.path', target='_blank')
+          v-icon(left) mdi-open-in-new
+          span {{$t('editor:conflict.viewLatestVersion')}}
+        .body-2.mt-5: strong {{$t('editor:conflict.whatToDo')}}
+        .body-2.mt-1 #[v-icon(color='indigo') mdi-alpha-l-box] {{$t('editor:conflict.whatToDoLocal')}}
+        .body-2.mt-1 #[v-icon(color='indigo') mdi-alpha-r-box] {{$t('editor:conflict.whatToDoRemote')}}
+      v-card-chin
+        v-spacer
+        v-btn(text, @click='close') {{$t('common:actions.cancel')}}
+        v-btn.px-4(color='indigo', @click='useLocal', dark, :title='$t(`editor:conflict.useLocalHint`)')
+          v-icon(left) mdi-alpha-l-box
+          span {{$t('editor:conflict.useLocal')}}
+        v-dialog(
+          v-model='isRemoteConfirmDiagShown'
+          width='500'
+          )
+          template(v-slot:activator='{ on }')
+            v-btn.ml-3(color='indigo', dark, v-on='on', :title='$t(`editor:conflict.useRemoteHint`)')
+              v-icon(left) mdi-alpha-r-box
+              span {{$t('editor:conflict.useRemote')}}
+          v-card
+            .dialog-header.is-short.is-indigo
+              v-icon.mr-3(color='white') mdi-alpha-r-box
+              span {{$t('editor:conflict.overwrite.title')}}
+            v-card-text.pa-4
+              i18next.body-2(tag='div', path='editor:conflict.overwrite.description')
+                strong(place='refEditsLost') {{$t('editor:conflict.overwrite.editsLost')}}
+            v-card-chin
+              v-spacer
+              v-btn(outlined, color='indigo', @click='isRemoteConfirmDiagShown = false')
+                v-icon(left) mdi-close
+                span {{$t('common:actions.cancel')}}
+              v-btn(@click='useRemote', color='indigo', dark)
+                v-icon(left) mdi-check
+                span {{$t('common:actions.confirm')}}
+</template>
+
+<script>
+import _ from 'lodash'
+import gql from 'graphql-tag'
+
+export default {
+  props: {
+    value: {
+      type: Boolean,
+      default: false
+    }
+  },
+  data() {
+    return {
+      latest: {
+        updatedAt: '',
+        authorName: '',
+        content: '',
+        locale: '',
+        path: ''
+      },
+      isRemoteConfirmDiagShown: false
+    }
+  },
+  computed: {
+    isShown: {
+      get() { return this.value },
+      set(val) { this.$emit('input', val) }
+    }
+  },
+  methods: {
+    close () {
+      this.isShown = false
+    },
+    useLocal () {
+      this.$store.set('editor/checkoutDateActive', this.latest.updatedAt)
+      this.$root.$emit('resetEditorConflict')
+      this.close()
+    },
+    useRemote () {
+      this.$store.set('editor/checkoutDateActive', this.latest.updatedAt)
+      this.$store.set('editor/content', this.latest.content)
+      this.$root.$emit('overwriteEditorContent')
+      this.$root.$emit('resetEditorConflict')
+      this.close()
+    }
+  },
+  async mounted () {
+    let resp = await this.$apollo.query({
+      query: gql`
+        query ($id: Int!) {
+          pages {
+            conflictLatest(id: $id) {
+              authorName
+              locale
+              path
+              content
+              updatedAt
+            }
+          }
+        }
+      `,
+      fetchPolicy: 'network-only',
+      variables: {
+        id: this.$store.get('page/id')
+      }
+    })
+    resp = _.get(resp, 'data.pages.conflictLatest', false)
+
+    if (!resp) {
+      return this.$store.commit('showNotification', {
+        message: 'Failed to fetch latest version.',
+        style: 'warning',
+        icon: 'warning'
+      })
+    }
+    this.latest = resp
+  }
+}
+</script>

+ 16 - 2
client/components/editor/editor-ckeditor.vue

@@ -9,15 +9,20 @@
         v-spacer
         .caption Visual Editor
         v-spacer
-        .caption {{stats.characters}} Chars, {{stats.words}} Words
+        .caption {{$t('editor:ckeditor.stats', { chars: stats.characters, words: stats.words })}}
+    editor-conflict(v-model='isConflict', v-if='isConflict')
 </template>
 
 <script>
 import _ from 'lodash'
 import { get, sync } from 'vuex-pathify'
 import DecoupledEditor from '@requarks/ckeditor5'
+import EditorConflict from './ckeditor/conflict.vue'
 
 export default {
+  components: {
+    EditorConflict
+  },
   props: {
     save: {
       type: Function,
@@ -31,7 +36,8 @@ export default {
         characters: 0,
         words: 0
       },
-      content: ''
+      content: '',
+      isConflict: false
     }
   },
   computed: {
@@ -82,6 +88,14 @@ export default {
           break
       }
     })
+
+    // Handle save conflict
+    this.$root.$on('saveConflict', () => {
+      this.isConflict = true
+    })
+    this.$root.$on('overwriteEditorContent', () => {
+      this.editor.setData(this.$store.get('editor/content'))
+    })
   },
   beforeDestroy () {
     if (this.editor) {

+ 8 - 0
client/components/editor/editor-code.vue

@@ -245,6 +245,14 @@ export default {
           break
       }
     })
+
+    // Handle save conflict
+    this.$root.$on('saveConflict', () => {
+      this.toggleModal(`editorModalConflict`)
+    })
+    this.$root.$on('overwriteEditorContent', () => {
+      this.cm.setValue(this.$store.get('editor/content'))
+    })
   },
   beforeDestroy() {
     this.$root.$off('editorInsert')

+ 24 - 18
client/components/editor/editor-modal-conflict.vue

@@ -3,66 +3,72 @@
     .pa-4
       v-toolbar.radius-7(flat, color='indigo', style='border-bottom-left-radius: 0; border-bottom-right-radius: 0;', dark)
         v-icon.mr-3 mdi-merge
-        .subtitle-1 Resolve Save Conflict
+        .subtitle-1 {{$t('editor:conflict.title')}}
         v-spacer
-        v-btn(outlined, color='white', @click='useLocal', title='Use content in the left panel')
+        v-btn(outlined, color='white', @click='useLocal', :title='$t(`editor:conflict.useLocalHint`)')
           v-icon(left) mdi-alpha-l-box
-          span Use Local
+          span {{$t('editor:conflict.useLocal')}}
         v-dialog(
           v-model='isRemoteConfirmDiagShown'
           width='500'
           )
           template(v-slot:activator='{ on }')
-            v-btn.ml-3(outlined, color='white', v-on='on', title='Discard local changes and use latest version')
+            v-btn.ml-3(outlined, color='white', v-on='on', :title='$t(`editor:conflict.useRemoteHint`)')
               v-icon(left) mdi-alpha-r-box
-              span Use Remote
+              span {{$t('editor:conflict.useRemote')}}
           v-card
             .dialog-header.is-short.is-indigo
               v-icon.mr-3(color='white') mdi-alpha-r-box
-              span Overwrite with Remote Version?
+              span {{$t('editor:conflict.overwrite.title')}}
             v-card-text.pa-4
-              .body-2 Are you sure you want to replace your current version with the latest remote content? #[strong Your current edits will be lost.]
+              i18next.body-2(tag='div', path='editor:conflict.overwrite.description')
+                strong(place='refEditsLost') {{$t('editor:conflict.overwrite.editsLost')}}
             v-card-chin
               v-spacer
               v-btn(outlined, color='indigo', @click='isRemoteConfirmDiagShown = false')
                 v-icon(left) mdi-close
-                span Cancel
+                span {{$t('common:actions.cancel')}}
               v-btn(@click='useRemote', color='indigo', dark)
                 v-icon(left) mdi-check
-                span Confirm
+                span {{$t('common:actions.confirm')}}
         v-divider.mx-3(vertical)
         v-btn(outlined, color='indigo lighten-4', @click='close')
           v-icon(left) mdi-close
-          span Cancel
+          span {{$t('common:actions.cancel')}}
       v-row.indigo.darken-1.body-2(no-gutters)
         v-col.pa-4
           v-icon.mr-3(color='white') mdi-alpha-l-box
-          span.white--text Local Version #[em.indigo--text.text--lighten-4 (editable)]
+          i18next.white--text(tag='span', path='editor:conflict.localVersion')
+            em.indigo--text.text--lighten-4(place='refEditable') {{$t('editor:conflict.editable')}}
         v-divider(vertical)
         v-col.pa-4
           v-icon.mr-3(color='white') mdi-alpha-r-box
-          span.white--text Remote Version #[em.indigo--text.text--lighten-4 (read-only)]
+          i18next.white--text(tag='span', path='editor:conflict.remoteVersion')
+            em.indigo--text.text--lighten-4(place='refReadOnly') {{$t('editor:conflict.readonly')}}
       v-row.grey.lighten-2.body-2(no-gutters)
         v-col.px-4.py-2
-          em.grey--text.text--darken-2 Your current edit, based on page version from #[span(:title='$options.filters.moment(checkoutDateActive, `LLL`)') {{ checkoutDateActive | moment('from') }}]
+          i18next.grey--text.text--darken-2(tag='em', path='editor:conflict.leftPanelInfo')
+            span(place='date', :title='$options.filters.moment(checkoutDateActive, `LLL`)') {{ checkoutDateActive | moment('from') }}
         v-divider(vertical)
         v-col.px-4.py-2
-          em.grey--text.text--darken-2 Last edited by #[strong {{latest.authorName}}], #[span(:title='$options.filters.moment(latest.updatedAt, `LLL`)') {{ latest.updatedAt | moment('from') }}]
+          i18next.grey--text.text--darken-2(tag='em', path='editor:conflict.rightPanelInfo')
+            strong(place='authorName') {{latest.authorName}}
+            span(place='date', :title='$options.filters.moment(latest.updatedAt, `LLL`)') {{ latest.updatedAt | moment('from') }}
       v-row.grey.lighten-3.grey--text.text--darken-3(no-gutters)
         v-col.pa-4
           .body-2
-            strong.indigo--text Title:
+            strong.indigo--text {{$t('editor:conflict.pageTitle')}}
             strong.pl-2 {{title}}
           .caption
-            strong.indigo--text Description:
+            strong.indigo--text {{$t('editor:conflict.pageDescription')}}
             span.pl-2 {{description}}
         v-divider(vertical, light)
         v-col.pa-4
           .body-2
-            strong.indigo--text Title:
+            strong.indigo--text {{$t('editor:conflict.pageTitle')}}
             strong.pl-2 {{latest.title}}
           .caption
-            strong.indigo--text Description:
+            strong.indigo--text {{$t('editor:conflict.pageDescription')}}
             span.pl-2 {{latest.description}}
       v-card.radius-7(:light='!$vuetify.theme.dark', :dark='$vuetify.theme.dark')
         div(ref='cm')

+ 1 - 1
client/components/editor/editor-modal-unsaved.vue

@@ -1,6 +1,6 @@
 <template lang="pug">
   v-dialog(v-model='isShown', max-width='550')
-    v-card.wiki-form
+    v-card
       .dialog-header.is-short.is-red
         v-icon.mr-2(color='white') mdi-alert
         span {{$t('editor:unsaved.title')}}