Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/nextcloud/text.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJulius Härtl <jus@bitgrid.net>2019-05-08 18:46:33 +0300
committerJulius Härtl <jus@bitgrid.net>2019-05-08 18:46:33 +0300
commit6b5cb314c748d5725f32fbca5e5b15be67749569 (patch)
treee1206fcf5136e1e5815471619f2bd90ff2b0e4e4
parentca5c912de08221c1e1087190a24f4d630f0ccb98 (diff)
Add collision handling frontend and rework fetching
Signed-off-by: Julius Härtl <jus@bitgrid.net>
-rw-r--r--css/style.scss15
-rw-r--r--src/collab.js46
-rw-r--r--src/components/Editor.vue74
3 files changed, 85 insertions, 50 deletions
diff --git a/css/style.scss b/css/style.scss
index c27227535..ca43c5ded 100644
--- a/css/style.scss
+++ b/css/style.scss
@@ -75,7 +75,7 @@ li.ProseMirror-selectednode:after {
}
.ProseMirror-menuseparator {
- border-right: 1px solid var(--color-text-maxcontrast);
+ border: 0;
margin-right: 3px;
}
@@ -176,14 +176,22 @@ li.ProseMirror-selectednode:after {
color: var(--color-text-light);
padding: 1px 6px;
top: 0; left: 0; right: 0;
- background: transparent;
+ background: var(--color-main-background-translucent);
z-index: 10;
-moz-box-sizing: border-box;
box-sizing: border-box;
overflow: visible;
}
+.has-conflicts,
+#editor-wrapper.icon-loading {
+ .ProseMirror-menubar {
+ display: none;
+ }
+}
+
.ProseMirror {
+ margin-top: 44px;
overflow: scroll;
height: 100%;
position: relative;
@@ -282,6 +290,7 @@ li.ProseMirror-selectednode:after {
.ProseMirror-example-setup-style img {
cursor: default;
max-height: 50vh;
+ max-width: 100%;
}
.ProseMirror-prompt {
@@ -345,6 +354,8 @@ div[contenteditable=false] {
border: none !important;
width: 100%;
background-color: transparent;
+ color: var(--color-main-text);
+ opacity: 1;
}
.ProseMirror p:first-child,
diff --git a/src/collab.js b/src/collab.js
index 4135516e2..8a0dec498 100644
--- a/src/collab.js
+++ b/src/collab.js
@@ -21,7 +21,7 @@
*/
import axios from 'nextcloud-axios'
-import {schema, defaultMarkdownParser, defaultMarkdownSerializer} from "prosemirror-markdown"
+import {schema, defaultMarkdownSerializer} from "prosemirror-markdown"
import {receiveTransaction, sendableSteps, getVersion} from 'prosemirror-collab';
import {Step} from 'prosemirror-transform';
@@ -65,9 +65,8 @@ const ERROR_TYPE = {
PUSH_FAILURE: 1,
}
-// TODO to fetch changes more frequently while typing
-// we either need to have a state machine similar to the prosemirror example to fetch
-// changes inbetween push tries or return updates with the push error
+const URL_SYNC = OC.generateUrl('/apps/text/session/sync')
+const URL_PUSH = OC.generateUrl('/apps/text/session/push');
class EditorSync {
constructor(doc, data) {
@@ -91,6 +90,10 @@ class EditorSync {
this.fetcher = setInterval(() => this.fetchSteps(), this.fetchInverval)
}
+ destroy() {
+ clearInterval(this.fetcher)
+ }
+
onSync(handler) {
this.onSyncHandlers.push(handler)
}
@@ -127,23 +130,20 @@ class EditorSync {
this.triggerStateChange()
const authority = this;
let autosaveContent = undefined
- // TODO only send if not saved already
if (
- this._forcedSave ||
+ this._forcedSave || this._manualSave ||
(!sendableSteps(this.view.state) && (authority.steps.length > this.document.lastSavedVersion))
) {
autosaveContent = this.content()
}
- axios.get(OC.generateUrl('/apps/text/session/sync'), {
- params: {
- documentId: this.document.id,
- sessionId: this.session.id,
- token: this.session.token,
- version: authority.steps.length,
- autosaveContent,
- force: !!this._forcedSave,
- manualSave: !!this._manualSave
- }
+ axios.post(URL_SYNC, {
+ documentId: this.document.id,
+ sessionId: this.session.id,
+ token: this.session.token,
+ version: authority.steps.length,
+ autosaveContent,
+ force: !!this._forcedSave,
+ manualSave: !!this._manualSave
}).then((response) => {
if (this.document.lastSavedVersion < response.data.document.lastSavedVersion) {
console.debug('Saved document', response.data.document)
@@ -151,8 +151,6 @@ class EditorSync {
}
this.view.setProps({editable: () => true})
-
-
this.onSyncHandlers.forEach((handler) => handler(response.data))
if (response.data.steps.length === 0) {
@@ -178,11 +176,13 @@ class EditorSync {
)
console.log(getVersion(authority.view.state))
this.lock = false;
- this.sendSteps()
+ this._forcedSave = false;
+ //this.sendSteps()
this.resetRefetchTimer();
}).catch((e) => {
this.lock = false;
- this.sendSteps()
+ console.log('fetch error sendSteps')
+ //this.sendSteps()
if (e.response.status === 409) {
console.log('Conflict during file save, please resolve')
this.view.setProps({editable: () => false})
@@ -192,6 +192,8 @@ class EditorSync {
}))
}
})
+ this._manualSave = false;
+ this._forcedSave = false;
}
resetRefetchTimer() {
@@ -253,7 +255,7 @@ class EditorSync {
this.lock = true
const authority = this
let steps = sendable.steps
- axios.post(OC.generateUrl('/apps/text/session/push'), {
+ axios.post(URL_PUSH, {
documentId: this.document.id,
sessionId: this.session.id,
token: this.session.token,
@@ -271,6 +273,7 @@ class EditorSync {
)
this.carefulRetryReset()
this.lock = false
+ this.fetchSteps()
}).catch((e) =>
@@ -282,6 +285,7 @@ class EditorSync {
this.fetchSteps()
this.carefulRetry(() => {
+ console.log('carefulRetry sendSteps')
this.sendSteps()
})
})
diff --git a/src/components/Editor.vue b/src/components/Editor.vue
index 5d7725bb9..0e6c3c289 100644
--- a/src/components/Editor.vue
+++ b/src/components/Editor.vue
@@ -26,9 +26,12 @@
<div class="save-status" :class="lastSavedStatusClass" v-tooltip="lastSavedStatusTooltip">{{ lastSavedStatus }}</div>
<avatar v-for="session in activeSessions" :key="session.id" :user="session.userId" :displayName="session.displayName" :style="sessionStyle(session)"></avatar>
</div>
+ <div><!-- needs a div wrapping to not force rerendering of the editor -->
+ <p class="msg error" v-if="lastSavedStatusTooltip.content">{{ lastSavedStatusTooltip.content }}</p>
+ </div>
<div id="editor-wrapper" :class="{'has-conflicts': syncError && syncError.type === ERROR_TYPE.SAVE_COLLISSION, 'icon-loading': !initialLoading}">
- <div id="editor"></div>
- <div id="remote" v-if="syncError && syncError.type === ERROR_TYPE.SAVE_COLLISSION"></div>
+ <div id="editor" ref="editor" v-once></div>
+ <div id="remote" ref="remote" v-show="syncError && syncError.type === ERROR_TYPE.SAVE_COLLISSION"></div>
</div>
<div v-if="syncError && syncError.type === ERROR_TYPE.SAVE_COLLISSION" id="resolve-conflicts">
<button @click="resolveUseThisVersion">Use your version</button>
@@ -140,9 +143,7 @@
// TODO: implement conflict resolving
return {
content: 'The document has been changed outside of the editor. The changes cannot be applied.',
- show: true,
- trigger: 'manual',
- placement: 'bottom'
+ placement: 'left'
}
}
},
@@ -178,7 +179,7 @@
}
}
).then((fileContent) => {
- const {editor, authority} = this.initEditor(response.data, fileContent.data);
+ const {editor, authority} = this.initEditor(this.$refs.editor, response.data, fileContent.data);
this.authority = authority
this.authority.onSync((data) => {
this.syncError = null
@@ -188,7 +189,7 @@
this.sessions = data.sessions
})
this.authority.onError((error, data) => {
- if (error === ERROR_TYPE.SAVE_COLLISSION) {
+ if (error === ERROR_TYPE.SAVE_COLLISSION && (!this.syncError || this.syncError.type !== ERROR_TYPE.SAVE_COLLISSION)) {
this.syncError = {
type: ERROR_TYPE.SAVE_COLLISSION,
data: data
@@ -201,6 +202,7 @@
this.authority.onStateChange(() => {
this.dirty = this.authority.dirty
if (!this.initialLoading) {
+ // TODO: this doesn't work with a limit on the steps fetched
this.initialLoading = !this.authority.dirty
}
})
@@ -219,48 +221,44 @@
if (this.remoteView) {
return;
}
- this.remoteView = new EditorView(document.querySelector("#remote"), {
+ this.remoteView = new EditorView(this.$refs.remote, {
state: EditorState.create({
doc: defaultMarkdownParser.parse(this.syncError.data.outsideChange),
plugins: [
...exampleSetup({schema})
]
}),
- focus() { this.view.focus() },
- destroy() { this.view.destroy() }
})
- view.setProps({editable: () => false})
+ this.remoteView.setProps({editable: () => false})
},
resolveUseThisVersion() {
this.authority.forceSave()
- this.removeRemoteView()
+ this.remoteView.destroy()
+ this.remoteView = null;
+ this.authority.view.setProps({editable: () => true})
},
resolveUseServerVersion() {
- this.removeRemoteView()
- },
-
- removeRemoteView() {
this.remoteView.destroy()
+ this.remoteView = null;
+ this.authority.view.destroy()
+ this.initSession()
},
- initEditor: (data, fileContent) => {
+ initEditor: (ref, data, fileContent) => {
const authority = new EditorSync(defaultMarkdownParser.parse(fileContent), data)
- const sendStepsDebounce = () => {
- console.log('debounced SENDSTEPS')
- authority.sendSteps()
- }
+ const sendStepsDebounce = () => authority.sendSteps()
const sendStepsDebounced = debounce(sendStepsDebounce, EDITOR_PUSH_DEBOUNCE, { maxWait: 500 })
- const view = new EditorView(document.querySelector("#editor"), {
+ const view = new EditorView(ref, {
state: EditorState.create({
doc: authority.doc,
plugins: [
keymap({
"Ctrl-s": (state, dispatch, event) => {
- authority.forceSave()
+ authority.manualSave()
authority.fetchSteps()
return true;
}
@@ -273,7 +271,11 @@
]
}),
focus() { this.view.focus() },
- destroy() { this.view.destroy() },
+ destroy() {
+ console.log('destroy view')
+ this.view.destroy()
+ authority.destroy()
+ },
dispatchTransaction: (transaction) => {
const state = view.state.apply(transaction);
view.updateState(state);
@@ -312,7 +314,10 @@
#editor-wrapper {
display: flex;
+ width: 100%;
height: 100%;
+ overflow: hidden;
+ position: absolute;
&.icon-loading {
#editor {
opacity: 0.3;
@@ -322,6 +327,15 @@
#resolve-conflicts {
display: flex;
+ position: fixed;
+ z-index: 10000;
+ bottom: 0;
+ max-width: 900px;
+ width: 100vw;
+ margin: auto;
+ padding: 10px;
+ filter: drop-shadow(0px 0px 5px #000);
+
button {
margin: auto;
}
@@ -332,12 +346,18 @@
overflow-y: scroll;
}
- #editor-container.has-conflicts {
- #remove, #editor {
+ .msg.error {
+ padding: 11px;
+ }
+
+ #editor-container #editor-wrapper.has-conflicts {
+ height: calc(100% - 50px);
+
+ #remote, #editor {
width: 50%;
}
- #remote .ProseMirror-menubar {
+ .ProseMirror-menubar {
visibility: hidden;
}
}