feat: editor save conflict localization
This commit is contained in:
		| @@ -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
									
								
								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 | ||||
|         .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) { | ||||
|   | ||||
| @@ -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') | ||||
|   | ||||
| @@ -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,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')}} | ||||
|   | ||||
		Reference in New Issue
	
	Block a user