feat: editor save conflict localization

pull/1634/head
NGPixel 5 years ago
parent 4b0e3d1c43
commit 507928730a

@ -203,7 +203,7 @@ export default {
window.onbeforeunload = () => { window.onbeforeunload = () => {
if (!this.exitConfirmed && this.initContentParsed !== this.$store.get('editor/content')) { 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 { } else {
return undefined return undefined
} }
@ -291,7 +291,7 @@ export default {
}) })
if (_.get(conflictResp, 'data.pages.checkConflicts', false)) { if (_.get(conflictResp, 'data.pages.checkConflicts', false)) {
this.$root.$emit('saveConflict') 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({ let resp = await this.$apollo.mutate({

@ -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>

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

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

@ -3,66 +3,72 @@
.pa-4 .pa-4
v-toolbar.radius-7(flat, color='indigo', style='border-bottom-left-radius: 0; border-bottom-right-radius: 0;', dark) 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 v-icon.mr-3 mdi-merge
.subtitle-1 Resolve Save Conflict .subtitle-1 {{$t('editor:conflict.title')}}
v-spacer 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 v-icon(left) mdi-alpha-l-box
span Use Local span {{$t('editor:conflict.useLocal')}}
v-dialog( v-dialog(
v-model='isRemoteConfirmDiagShown' v-model='isRemoteConfirmDiagShown'
width='500' width='500'
) )
template(v-slot:activator='{ on }') 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 v-icon(left) mdi-alpha-r-box
span Use Remote span {{$t('editor:conflict.useRemote')}}
v-card v-card
.dialog-header.is-short.is-indigo .dialog-header.is-short.is-indigo
v-icon.mr-3(color='white') mdi-alpha-r-box 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 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-card-chin
v-spacer v-spacer
v-btn(outlined, color='indigo', @click='isRemoteConfirmDiagShown = false') v-btn(outlined, color='indigo', @click='isRemoteConfirmDiagShown = false')
v-icon(left) mdi-close v-icon(left) mdi-close
span Cancel span {{$t('common:actions.cancel')}}
v-btn(@click='useRemote', color='indigo', dark) v-btn(@click='useRemote', color='indigo', dark)
v-icon(left) mdi-check v-icon(left) mdi-check
span Confirm span {{$t('common:actions.confirm')}}
v-divider.mx-3(vertical) v-divider.mx-3(vertical)
v-btn(outlined, color='indigo lighten-4', @click='close') v-btn(outlined, color='indigo lighten-4', @click='close')
v-icon(left) mdi-close v-icon(left) mdi-close
span Cancel span {{$t('common:actions.cancel')}}
v-row.indigo.darken-1.body-2(no-gutters) v-row.indigo.darken-1.body-2(no-gutters)
v-col.pa-4 v-col.pa-4
v-icon.mr-3(color='white') mdi-alpha-l-box 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-divider(vertical)
v-col.pa-4 v-col.pa-4
v-icon.mr-3(color='white') mdi-alpha-r-box 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-row.grey.lighten-2.body-2(no-gutters)
v-col.px-4.py-2 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-divider(vertical)
v-col.px-4.py-2 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-row.grey.lighten-3.grey--text.text--darken-3(no-gutters)
v-col.pa-4 v-col.pa-4
.body-2 .body-2
strong.indigo--text Title: strong.indigo--text {{$t('editor:conflict.pageTitle')}}
strong.pl-2 {{title}} strong.pl-2 {{title}}
.caption .caption
strong.indigo--text Description: strong.indigo--text {{$t('editor:conflict.pageDescription')}}
span.pl-2 {{description}} span.pl-2 {{description}}
v-divider(vertical, light) v-divider(vertical, light)
v-col.pa-4 v-col.pa-4
.body-2 .body-2
strong.indigo--text Title: strong.indigo--text {{$t('editor:conflict.pageTitle')}}
strong.pl-2 {{latest.title}} strong.pl-2 {{latest.title}}
.caption .caption
strong.indigo--text Description: strong.indigo--text {{$t('editor:conflict.pageDescription')}}
span.pl-2 {{latest.description}} span.pl-2 {{latest.description}}
v-card.radius-7(:light='!$vuetify.theme.dark', :dark='$vuetify.theme.dark') v-card.radius-7(:light='!$vuetify.theme.dark', :dark='$vuetify.theme.dark')
div(ref='cm') div(ref='cm')

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

Loading…
Cancel
Save