diff --git a/client/components/admin/admin-pages-edit.vue b/client/components/admin/admin-pages-edit.vue
index 1bd894a8..1a944a13 100644
--- a/client/components/admin/admin-pages-edit.vue
+++ b/client/components/admin/admin-pages-edit.vue
@@ -105,7 +105,7 @@
v-icon(left) mdi-code-tags
span Source
v-divider.mx-2(vertical)
- v-btn(color='primary', text, :href='`/h/` + page.locale + `/` + page.path', disabled)
+ v-btn(color='primary', text, :href='`/h/` + page.locale + `/` + page.path')
v-icon(left) mdi-history
span History
v-spacer
diff --git a/client/components/common/nav-header.vue b/client/components/common/nav-header.vue
index dac14075..f9d90510 100644
--- a/client/components/common/nav-header.vue
+++ b/client/components/common/nav-header.vue
@@ -147,7 +147,7 @@
v-tooltip(bottom)
template(v-slot:activator='{ on }')
v-btn(icon, tile, height='64', v-on='on', @click='pageNew')
- v-icon(color='grey') mdi-file-document-box-plus-outline
+ v-icon(color='grey') mdi-text-box-plus-outline
span {{$t('common:header.newPage')}}
v-divider(vertical)
@@ -172,18 +172,18 @@
v-list-item-content
v-list-item-title {{name}}
v-list-item-subtitle {{email}}
- v-list-item(href='/w', disabled)
- v-list-item-action: v-icon(color='blue') mdi-view-compact-outline
- v-list-item-content
- v-list-item-title {{$t('common:header.myWiki')}}
- v-list-item-subtitle.overline Coming soon
- v-list-item(href='/p', disabled)
- v-list-item-action: v-icon(color='blue') mdi-face-profile
- v-list-item-content
- v-list-item-title {{$t('common:header.profile')}}
- v-list-item-subtitle.overline Coming soon
+ //- v-list-item(href='/w', disabled)
+ //- v-list-item-action: v-icon(color='blue') mdi-view-compact-outline
+ //- v-list-item-content
+ //- v-list-item-title {{$t('common:header.myWiki')}}
+ //- v-list-item-subtitle.overline Coming soon
+ //- v-list-item(href='/p', disabled)
+ //- v-list-item-action: v-icon(color='blue') mdi-face-profile
+ //- v-list-item-content
+ //- v-list-item-title {{$t('common:header.profile')}}
+ //- v-list-item-subtitle.overline Coming soon
v-list-item(href='/a', v-if='isAuthenticated && isAdmin')
- v-list-item-action.btn-animate-rotate: v-icon(:color='$vuetify.theme.dark ? `blue-grey lighten-3` : `blue-grey`') mdi-settings
+ v-list-item-action.btn-animate-rotate: v-icon(:color='$vuetify.theme.dark ? `blue-grey lighten-3` : `blue-grey`') mdi-cog
v-list-item-title(:class='$vuetify.theme.dark ? `blue-grey--text text--lighten-3` : `blue-grey--text`') {{$t('common:header.admin')}}
v-list-item(@click='logout')
v-list-item-action: v-icon(color='red') mdi-logout
diff --git a/client/components/editor.vue b/client/components/editor.vue
index b233a6c9..bc72daa2 100644
--- a/client/components/editor.vue
+++ b/client/components/editor.vue
@@ -13,7 +13,7 @@
full-width
)
template(slot='actions')
- v-btn.mr-3.animated.fadeIn(color='amber', outlined, small, v-if='isConflict')
+ v-btn.mr-3.animated.fadeIn(color='amber', outlined, small, v-if='isConflict', @click='openConflict')
.overline.amber--text.mr-3 Conflict
status-indicator(intermediary, pulse)
v-btn.animated.fadeInDown(
@@ -22,10 +22,9 @@
@click='save'
@click.ctrl.exact='saveAndClose'
:class='{ "is-icon": $vuetify.breakpoint.mdAndDown }'
- :disabled='!isDirty'
)
v-icon(color='green', :left='$vuetify.breakpoint.lgAndUp') mdi-check
- span(v-if='$vuetify.breakpoint.lgAndUp && mode !== `create` && !isDirty') {{ $t('editor:save.saved') }}
+ span.grey--text(v-if='$vuetify.breakpoint.lgAndUp && mode !== `create` && !isDirty') {{ $t('editor:save.saved') }}
span.white--text(v-else-if='$vuetify.breakpoint.lgAndUp') {{ mode === 'create' ? $t('common:actions.create') : $t('common:actions.save') }}
v-btn.animated.fadeInDown.wait-p1s(
text
@@ -86,7 +85,8 @@ export default {
editorModalProperties: () => import(/* webpackChunkName: "editor", webpackMode: "eager" */ './editor/editor-modal-properties.vue'),
editorModalUnsaved: () => import(/* webpackChunkName: "editor", webpackMode: "eager" */ './editor/editor-modal-unsaved.vue'),
editorModalMedia: () => import(/* webpackChunkName: "editor", webpackMode: "eager" */ './editor/editor-modal-media.vue'),
- editorModalBlocks: () => import(/* webpackChunkName: "editor", webpackMode: "eager" */ './editor/editor-modal-blocks.vue')
+ editorModalBlocks: () => import(/* webpackChunkName: "editor", webpackMode: "eager" */ './editor/editor-modal-blocks.vue'),
+ editorModalConflict: () => import(/* webpackChunkName: "editor-conflict", webpackMode: "lazy" */ './editor/editor-modal-conflict.vue')
},
props: {
locale: {
@@ -136,6 +136,7 @@ export default {
},
data() {
return {
+ isSaving: false,
isConflict: false,
dialogProps: false,
dialogProgress: false,
@@ -152,6 +153,7 @@ export default {
mode: get('editor/mode'),
welcomeMode() { return this.mode === `create` && this.path === `home` },
currentPageTitle: sync('page/title'),
+ checkoutDateActive: sync('editor/checkoutDateActive'),
isDirty () {
return _.some([
this.initContentParsed !== this.$store.get('editor/content'),
@@ -183,6 +185,8 @@ export default {
this.$store.commit('page/SET_TITLE', this.title)
this.$store.commit('page/SET_MODE', 'edit')
+
+ this.checkoutDateActive = this.checkoutDate
},
mounted() {
this.$store.set('editor/mode', this.initMode || 'create')
@@ -205,6 +209,10 @@ export default {
}
}
+ this.$root.$on('resetEditorConflict', () => {
+ this.isConflict = false
+ })
+
// this.$store.set('editor/mode', 'edit')
// this.currentEditor = `editorApi`
},
@@ -218,8 +226,12 @@ export default {
hideProgressDialog() {
this.dialogProgress = false
},
- async save() {
+ openConflict() {
+ this.$root.$emit('saveConflict')
+ },
+ async save({ rethrow = false, overwrite = false } = {}) {
this.showProgressDialog('saving')
+ this.isSaving = true
try {
if (this.$store.get('editor/mode') === 'create') {
// --------------------------------------------
@@ -244,6 +256,8 @@ export default {
})
resp = _.get(resp, 'data.pages.create', {})
if (_.get(resp, 'responseResult.succeeded')) {
+ this.checkoutDateActive = _.get(resp, 'page.updatedAt', this.checkoutDateActive)
+ this.isConflict = false
this.$store.commit('showNotification', {
message: this.$t('editor:save.createSuccess'),
style: 'success',
@@ -261,6 +275,25 @@ export default {
// -> UPDATE EXISTING PAGE
// --------------------------------------------
+ const conflictResp = await this.$apollo.query({
+ query: gql`
+ query ($id: Int!, $checkoutDate: Date!) {
+ pages {
+ checkConflicts(id: $id, checkoutDate: $checkoutDate)
+ }
+ }
+ `,
+ fetchPolicy: 'network-only',
+ variables: {
+ id: this.pageId,
+ checkoutDate: this.checkoutDateActive
+ }
+ })
+ if (_.get(conflictResp, 'data.pages.checkConflicts', false)) {
+ this.$root.$emit('saveConflict')
+ throw new Error('Save conflict! Another user has already modified this page.')
+ }
+
let resp = await this.$apollo.mutate({
mutation: updatePageMutation,
variables: {
@@ -280,6 +313,8 @@ export default {
})
resp = _.get(resp, 'data.pages.update', {})
if (_.get(resp, 'responseResult.succeeded')) {
+ this.checkoutDateActive = _.get(resp, 'page.updatedAt', this.checkoutDateActive)
+ this.isConflict = false
this.$store.commit('showNotification', {
message: this.$t('editor:save.updateSuccess'),
style: 'success',
@@ -302,13 +337,16 @@ export default {
style: 'error',
icon: 'warning'
})
- throw err
+ if (rethrow === true) {
+ throw err
+ }
}
+ this.isSaving = false
this.hideProgressDialog()
},
async saveAndClose() {
try {
- await this.save()
+ await this.save({ rethrow: true })
await this.exit()
} catch (err) {
// Error is already handled
@@ -348,12 +386,12 @@ export default {
variables () {
return {
id: this.pageId,
- checkoutDate: this.checkoutDate
+ checkoutDate: this.checkoutDateActive
}
},
update: (data) => _.cloneDeep(data.pages.checkConflicts),
skip () {
- return this.mode === 'create' || !this.isDirty
+ return this.mode === 'create' || this.isSaving || !this.isDirty
}
}
}
diff --git a/client/components/editor/editor-markdown.vue b/client/components/editor/editor-markdown.vue
index 69b646cb..d03fbc99 100644
--- a/client/components/editor/editor-markdown.vue
+++ b/client/components/editor/editor-markdown.vue
@@ -633,6 +633,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')
diff --git a/client/components/editor/editor-modal-conflict.vue b/client/components/editor/editor-modal-conflict.vue
new file mode 100644
index 00000000..9377b55a
--- /dev/null
+++ b/client/components/editor/editor-modal-conflict.vue
@@ -0,0 +1,216 @@
+
+ v-card.editor-modal-conflict.animated.fadeIn(flat, tile)
+ .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
+ v-spacer
+ v-btn(outlined, color='white', @click='useLocal', title='Use content in the left panel')
+ v-icon(left) mdi-alpha-l-box
+ span Use Local
+ 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-icon(left) mdi-alpha-r-box
+ span Use Remote
+ v-card
+ .dialog-header.is-short.is-indigo
+ v-icon.mr-3(color='white') mdi-alpha-r-box
+ span Overwrite with Remote Version?
+ 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.]
+ v-card-chin
+ v-spacer
+ v-btn(outlined, color='indigo', @click='isRemoteConfirmDiagShown = false')
+ v-icon(left) mdi-close
+ span Cancel
+ v-btn(@click='useRemote', color='indigo', dark)
+ v-icon(left) mdi-check
+ span Confirm
+ v-divider.mx-3(vertical)
+ v-btn(outlined, color='indigo lighten-4', @click='close')
+ v-icon(left) mdi-close
+ span 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)]
+ 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)]
+ 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') }}]
+ 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') }}]
+ v-row.grey.lighten-3.grey--text.text--darken-3(no-gutters)
+ v-col.pa-4
+ .body-2
+ strong.indigo--text Title:
+ strong.pl-2 {{title}}
+ .caption
+ strong.indigo--text Description:
+ span.pl-2 {{description}}
+ v-divider(vertical, light)
+ v-col.pa-4
+ .body-2
+ strong.indigo--text Title:
+ strong.pl-2 {{latest.title}}
+ .caption
+ strong.indigo--text Description:
+ span.pl-2 {{latest.description}}
+ v-card.radius-7(:light='!$vuetify.theme.dark', :dark='$vuetify.theme.dark')
+ div(ref='cm')
+
+
+
+
+
diff --git a/client/graph/editor/create.gql b/client/graph/editor/create.gql
index 131082c9..90a5f5a2 100644
--- a/client/graph/editor/create.gql
+++ b/client/graph/editor/create.gql
@@ -9,6 +9,7 @@ mutation ($content: String!, $description: String!, $editor: String!, $isPrivate
}
page {
id
+ updatedAt
}
}
}
diff --git a/client/graph/editor/update.gql b/client/graph/editor/update.gql
index c6608e40..7d6f2c1c 100644
--- a/client/graph/editor/update.gql
+++ b/client/graph/editor/update.gql
@@ -7,6 +7,9 @@ mutation ($id: Int!, $content: String, $description: String, $editor: String, $i
slug
message
}
+ page {
+ updatedAt
+ }
}
}
}
diff --git a/client/libs/codemirror-merge/diff-match-patch.js b/client/libs/codemirror-merge/diff-match-patch.js
new file mode 100644
index 00000000..ab06ad91
--- /dev/null
+++ b/client/libs/codemirror-merge/diff-match-patch.js
@@ -0,0 +1,60 @@
+var diff_match_patch=function(){this.Diff_Timeout=1;this.Diff_EditCost=4;this.Match_Threshold=.5;this.Match_Distance=1E3;this.Patch_DeleteThreshold=.5;this.Patch_Margin=4;this.Match_MaxBits=32},DIFF_DELETE=-1,DIFF_INSERT=1,DIFF_EQUAL=0;diff_match_patch.Diff=function(a,b){this[0]=a;this[1]=b};diff_match_patch.Diff.prototype.length=2;diff_match_patch.Diff.prototype.toString=function(){return this[0]+","+this[1]};
+diff_match_patch.prototype.diff_main=function(a,b,c,d){"undefined"==typeof d&&(d=0>=this.Diff_Timeout?Number.MAX_VALUE:(new Date).getTime()+1E3*this.Diff_Timeout);if(null==a||null==b)throw Error("Null input. (diff_main)");if(a==b)return a?[new diff_match_patch.Diff(DIFF_EQUAL,a)]:[];"undefined"==typeof c&&(c=!0);var e=c,f=this.diff_commonPrefix(a,b);c=a.substring(0,f);a=a.substring(f);b=b.substring(f);f=this.diff_commonSuffix(a,b);var g=a.substring(a.length-f);a=a.substring(0,a.length-f);b=b.substring(0,
+b.length-f);a=this.diff_compute_(a,b,e,d);c&&a.unshift(new diff_match_patch.Diff(DIFF_EQUAL,c));g&&a.push(new diff_match_patch.Diff(DIFF_EQUAL,g));this.diff_cleanupMerge(a);return a};
+diff_match_patch.prototype.diff_compute_=function(a,b,c,d){if(!a)return[new diff_match_patch.Diff(DIFF_INSERT,b)];if(!b)return[new diff_match_patch.Diff(DIFF_DELETE,a)];var e=a.length>b.length?a:b,f=a.length>b.length?b:a,g=e.indexOf(f);return-1!=g?(c=[new diff_match_patch.Diff(DIFF_INSERT,e.substring(0,g)),new diff_match_patch.Diff(DIFF_EQUAL,f),new diff_match_patch.Diff(DIFF_INSERT,e.substring(g+f.length))],a.length>b.length&&(c[0][0]=c[2][0]=DIFF_DELETE),c):1==f.length?[new diff_match_patch.Diff(DIFF_DELETE,
+a),new diff_match_patch.Diff(DIFF_INSERT,b)]:(e=this.diff_halfMatch_(a,b))?(b=e[1],f=e[3],a=e[4],e=this.diff_main(e[0],e[2],c,d),c=this.diff_main(b,f,c,d),e.concat([new diff_match_patch.Diff(DIFF_EQUAL,a)],c)):c&&100c);t++){for(var v=-t+p;v<=t-x;v+=2){var n=f+v;var r=v==-t||v!=t&&h[n-1]d)x+=2;else if(y>e)p+=2;else if(m&&(n=f+k-v,0<=n&&n=
+u)return this.diff_bisectSplit_(a,b,r,y,c)}}for(v=-t+w;v<=t-q;v+=2){n=f+v;u=v==-t||v!=t&&l[n-1]d)q+=2;else if(r>e)w+=2;else if(!m&&(n=f+k-v,0<=n&&n=u)))return this.diff_bisectSplit_(a,b,r,y,c)}}return[new diff_match_patch.Diff(DIFF_DELETE,a),new diff_match_patch.Diff(DIFF_INSERT,b)]};
+diff_match_patch.prototype.diff_bisectSplit_=function(a,b,c,d,e){var f=a.substring(0,c),g=b.substring(0,d);a=a.substring(c);b=b.substring(d);f=this.diff_main(f,g,!1,e);e=this.diff_main(a,b,!1,e);return f.concat(e)};
+diff_match_patch.prototype.diff_linesToChars_=function(a,b){function c(a){for(var b="",c=0,g=-1,h=d.length;gd?a=a.substring(c-d):c=a.length?[h,k,l,m,g]:null}if(0>=this.Diff_Timeout)return null;
+var d=a.length>b.length?a:b,e=a.length>b.length?b:a;if(4>d.length||2*e.lengthd[4].length?g:d:d:g;else return null;if(a.length>b.length){d=g[0];e=g[1];var h=g[2];var l=g[3]}else h=g[0],l=g[1],d=g[2],e=g[3];return[d,e,h,l,g[4]]};
+diff_match_patch.prototype.diff_cleanupSemantic=function(a){for(var b=!1,c=[],d=0,e=null,f=0,g=0,h=0,l=0,k=0;f=e){if(d>=b.length/2||d>=c.length/2)a.splice(f,0,new diff_match_patch.Diff(DIFF_EQUAL,c.substring(0,d))),a[f-1][1]=b.substring(0,b.length-d),a[f+1][1]=c.substring(d),f++}else if(e>=b.length/2||e>=c.length/2)a.splice(f,0,new diff_match_patch.Diff(DIFF_EQUAL,b.substring(0,e))),a[f-1][0]=DIFF_INSERT,a[f-1][1]=c.substring(0,c.length-e),a[f+1][0]=DIFF_DELETE,
+a[f+1][1]=b.substring(e),f++;f++}f++}};
+diff_match_patch.prototype.diff_cleanupSemanticLossless=function(a){function b(a,b){if(!a||!b)return 6;var c=a.charAt(a.length-1),d=b.charAt(0),e=c.match(diff_match_patch.nonAlphaNumericRegex_),f=d.match(diff_match_patch.nonAlphaNumericRegex_),g=e&&c.match(diff_match_patch.whitespaceRegex_),h=f&&d.match(diff_match_patch.whitespaceRegex_);c=g&&c.match(diff_match_patch.linebreakRegex_);d=h&&d.match(diff_match_patch.linebreakRegex_);var k=c&&a.match(diff_match_patch.blanklineEndRegex_),l=d&&b.match(diff_match_patch.blanklineStartRegex_);
+return k||l?5:c||d?4:e&&!g&&h?3:g||h?2:e||f?1:0}for(var c=1;c=k&&(k=m,g=d,h=e,l=f)}a[c-1][1]!=g&&(g?a[c-1][1]=g:(a.splice(c-
+1,1),c--),a[c][1]=h,l?a[c+1][1]=l:(a.splice(c+1,1),c--))}c++}};diff_match_patch.nonAlphaNumericRegex_=/[^a-zA-Z0-9]/;diff_match_patch.whitespaceRegex_=/\s/;diff_match_patch.linebreakRegex_=/[\r\n]/;diff_match_patch.blanklineEndRegex_=/\n\r?\n$/;diff_match_patch.blanklineStartRegex_=/^\r?\n\r?\n/;
+diff_match_patch.prototype.diff_cleanupEfficiency=function(a){for(var b=!1,c=[],d=0,e=null,f=0,g=!1,h=!1,l=!1,k=!1;fb)break;e=c;f=d}return a.length!=g&&a[g][0]===DIFF_DELETE?f:f+(b-e)};
+diff_match_patch.prototype.diff_prettyHtml=function(a){for(var b=[],c=/&/g,d=//g,f=/\n/g,g=0;g");switch(h){case DIFF_INSERT:b[g]=''+l+"";break;case DIFF_DELETE:b[g]=''+l+"";break;case DIFF_EQUAL:b[g]=""+l+""}}return b.join("")};
+diff_match_patch.prototype.diff_text1=function(a){for(var b=[],c=0;cl)throw Error("Invalid number in diff_fromDelta: "+h);h=a.substring(e,e+=l);"="==f[g].charAt(0)?c[d++]=new diff_match_patch.Diff(DIFF_EQUAL,h):c[d++]=
+new diff_match_patch.Diff(DIFF_DELETE,h);break;default:if(f[g])throw Error("Invalid diff operation in diff_fromDelta: "+f[g]);}}if(e!=a.length)throw Error("Delta length ("+e+") does not equal source text length ("+a.length+").");return c};diff_match_patch.prototype.match_main=function(a,b,c){if(null==a||null==b||null==c)throw Error("Null input. (match_main)");c=Math.max(0,Math.min(c,a.length));return a==b?0:a.length?a.substring(c,c+b.length)==b?c:this.match_bitap_(a,b,c):-1};
+diff_match_patch.prototype.match_bitap_=function(a,b,c){function d(a,d){var e=a/b.length,g=Math.abs(c-d);return f.Match_Distance?e+g/f.Match_Distance:g?1:e}if(b.length>this.Match_MaxBits)throw Error("Pattern too long for this browser.");var e=this.match_alphabet_(b),f=this,g=this.Match_Threshold,h=a.indexOf(b,c);-1!=h&&(g=Math.min(d(0,h),g),h=a.lastIndexOf(b,c+b.length),-1!=h&&(g=Math.min(d(0,h),g)));var l=1<=k;q--){var t=e[a.charAt(q-1)];m[q]=0===w?(m[q+1]<<1|1)&t:(m[q+1]<<1|1)&t|(x[q+1]|x[q])<<1|1|x[q+1];if(m[q]&l&&(t=d(w,q-1),t<=g))if(g=t,h=q-1,h>c)k=Math.max(1,2*c-h);else break}if(d(w+1,c)>g)break;x=m}return h};
+diff_match_patch.prototype.match_alphabet_=function(a){for(var b={},c=0;c=2*this.Patch_Margin&&e&&(this.patch_addContext_(a,h),c.push(a),a=new diff_match_patch.patch_obj,e=0,h=d,f=g)}k!==DIFF_INSERT&&(f+=m.length);k!==DIFF_DELETE&&(g+=m.length)}e&&(this.patch_addContext_(a,h),c.push(a));return c};
+diff_match_patch.prototype.patch_deepCopy=function(a){for(var b=[],c=0;cthis.Match_MaxBits){var k=this.match_main(b,h.substring(0,this.Match_MaxBits),g);-1!=k&&(l=this.match_main(b,h.substring(h.length-this.Match_MaxBits),g+h.length-this.Match_MaxBits),-1==l||k>=l)&&(k=-1)}else k=this.match_main(b,h,
+g);if(-1==k)e[f]=!1,d-=a[f].length2-a[f].length1;else if(e[f]=!0,d=k-g,g=-1==l?b.substring(k,k+h.length):b.substring(k,l+this.Match_MaxBits),h==g)b=b.substring(0,k)+this.diff_text2(a[f].diffs)+b.substring(k+h.length);else if(g=this.diff_main(h,g,!1),h.length>this.Match_MaxBits&&this.diff_levenshtein(g)/h.length>this.Patch_DeleteThreshold)e[f]=!1;else{this.diff_cleanupSemanticLossless(g);h=0;var m;for(l=0;le[0][1].length){var f=b-e[0][1].length;e[0][1]=c.substring(e[0][1].length)+e[0][1];d.start1-=f;d.start2-=f;d.length1+=f;d.length2+=f}d=a[a.length-1];e=d.diffs;
+0==e.length||e[e.length-1][0]!=DIFF_EQUAL?(e.push(new diff_match_patch.Diff(DIFF_EQUAL,c)),d.length1+=b,d.length2+=b):b>e[e.length-1][1].length&&(f=b-e[e.length-1][1].length,e[e.length-1][1]+=c.substring(0,f),d.length1+=f,d.length2+=f);return c};
+diff_match_patch.prototype.patch_splitMax=function(a){for(var b=this.Match_MaxBits,c=0;c2*b?(h.length1+=k.length,e+=k.length,l=!1,h.diffs.push(new diff_match_patch.Diff(g,k)),d.diffs.shift()):(k=k.substring(0,b-h.length1-this.Patch_Margin),h.length1+=k.length,e+=k.length,g===DIFF_EQUAL?(h.length2+=k.length,f+=k.length):l=!1,h.diffs.push(new diff_match_patch.Diff(g,k)),k==d.diffs[0][1]?d.diffs.shift():d.diffs[0][1]=d.diffs[0][1].substring(k.length))}g=this.diff_text2(h.diffs);
+g=g.substring(g.length-this.Patch_Margin);k=this.diff_text1(d.diffs).substring(0,this.Patch_Margin);""!==k&&(h.length1+=k.length,h.length2+=k.length,0!==h.diffs.length&&h.diffs[h.diffs.length-1][0]===DIFF_EQUAL?h.diffs[h.diffs.length-1][1]+=k:h.diffs.push(new diff_match_patch.Diff(DIFF_EQUAL,k)));l||a.splice(++c,0,h)}}};diff_match_patch.prototype.patch_toText=function(a){for(var b=[],c=0;c args.checkoutDate
} else {
throw new WIKI.Error.PageUpdateForbidden()
}
} else {
throw new WIKI.Error.PageNotFound()
}
+ },
+ /**
+ * FETCH LATEST VERSION FOR CONFLICT COMPARISON
+ */
+ async conflictLatest (obj, args, context, info) {
+ let page = await WIKI.models.pages.getPageFromDb(args.id)
+ if (page) {
+ if (WIKI.auth.checkAccess(context.req.user, ['write:pages', 'manage:pages'], {
+ path: page.path,
+ locale: page.localeCode
+ })) {
+ return {
+ ...page,
+ tags: page.tags.map(t => t.tag),
+ locale: page.localeCode
+ }
+ } else {
+ throw new WIKI.Error.PageViewForbidden()
+ }
+ } else {
+ throw new WIKI.Error.PageNotFound()
+ }
}
},
PageMutation: {
diff --git a/server/graph/schemas/page.graphql b/server/graph/schemas/page.graphql
index 0ef5f0cb..a2875caf 100644
--- a/server/graph/schemas/page.graphql
+++ b/server/graph/schemas/page.graphql
@@ -66,6 +66,10 @@ type PageQuery {
id: Int!
checkoutDate: Date!
): Boolean! @auth(requires: ["write:pages", "manage:pages", "manage:system"])
+
+ conflictLatest(
+ id: Int!
+ ): PageConflictLatest! @auth(requires: ["write:pages", "manage:pages", "manage:system"])
}
# -----------------------------------------------
@@ -277,6 +281,21 @@ type PageLinkItem {
links: [String]!
}
+type PageConflictLatest {
+ id: Int!
+ authorId: String!
+ authorName: String!
+ content: String!
+ createdAt: Date!
+ description: String!
+ isPublished: Boolean!
+ locale: String!
+ path: String!
+ tags: [String]
+ title: String!
+ updatedAt: Date!
+}
+
enum PageOrderBy {
CREATED
ID
diff --git a/server/models/pages.js b/server/models/pages.js
index 265cb767..6ec39f81 100644
--- a/server/models/pages.js
+++ b/server/models/pages.js
@@ -293,6 +293,9 @@ module.exports = class Page extends Model {
mode: 'create'
})
+ // -> Get latest updatedAt
+ page.updatedAt = await WIKI.models.pages.query().findById(page.id).select('updatedAt').then(r => r.updatedAt)
+
return page
}
@@ -340,12 +343,7 @@ module.exports = class Page extends Model {
publishStartDate: opts.publishStartDate || '',
title: opts.title
}).where('id', ogPage.id)
- let page = await WIKI.models.pages.getPageFromDb({
- path: ogPage.path,
- locale: ogPage.localeCode,
- userId: ogPage.authorId,
- isPrivate: ogPage.isPrivate
- })
+ let page = await WIKI.models.pages.getPageFromDb(ogPage.id)
// -> Save Tags
await WIKI.models.tags.associateTags({ tags: opts.tags, page })
@@ -381,6 +379,9 @@ module.exports = class Page extends Model {
}).update('title', page.title)
}
+ // -> Get latest updatedAt
+ page.updatedAt = await WIKI.models.pages.query().findById(page.id).select('updatedAt').then(r => r.updatedAt)
+
return page
}