feat: editor save conflict localization
This commit is contained in:
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({
|
||||||
|
129
client/components/editor/ckeditor/conflict.vue
Normal file
129
client/components/editor/ckeditor/conflict.vue
Normal file
@ -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…
Reference in New Issue
Block a user