Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions cypress/e2e/sync.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,15 +69,23 @@ describe('Sync', () => {
.should('include', 'after the lost connection')
})

it('handles brief network outages', () => {
cy.intercept('**/apps/text/session/*/*', req => req.destroy()).as('dead')
cy.wait('@dead', { timeout: 30000 })
// bring back the network connection
cy.intercept('**/apps/text/session/*/*', req => { req.continue() }).as('alive')
cy.wait('@alive', { timeout: 30000 })
cy.getContent().type('staying alive')
cy.getContent().should('contain', 'staying alive')
})

it('reconnects via button after a short lost connection', () => {
cy.intercept('**/apps/text/session/*/*', req => req.destroy()).as('dead')
cy.wait('@dead', { timeout: 30000 })
cy.get('#editor-container .document-status', { timeout: 30000 })
.should('contain', 'Document could not be loaded.')
cy.get('#editor-container .document-status')
.find('.button.primary').click()
cy.get('.toastify').should('contain', 'Connection failed.')
cy.get('.toastify', { timeout: 30000 }).should('not.exist')
cy.get('#editor-container .document-status', { timeout: 30000 })
.should('contain', 'Document could not be loaded.')
// bring back the network connection
Expand Down
44 changes: 26 additions & 18 deletions src/components/Editor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@
:lock="lock"
:is-resolving-conflict="isResolvingConflict"
:sync-error="syncError"
:has-connection-issue="hasConnectionIssue"
:has-connection-issue="requireReconnect"
@reconnect="reconnect" />

<SkeletonLoading v-if="showLoadingSkeleton" />
<Wrapper v-if="displayed"
:is-resolving-conflict="isResolvingConflict"
:has-connection-issue="hasConnectionIssue"
:has-connection-issue="requireReconnect"
:content-loaded="contentLoaded"
:show-outline-outside="showOutlineOutside"
@outline-toggled="outlineToggled">
Expand All @@ -34,7 +34,7 @@
:dirty="dirty"
:sessions="filteredSessions"
:sync-error="syncError"
:has-connection-issue="hasConnectionIssue" />
:has-connection-issue="requireReconnect" />
</ReadonlyBar>
</slot>
</div>
Expand All @@ -48,7 +48,7 @@
:dirty="dirty"
:sessions="filteredSessions"
:sync-error="syncError"
:has-connection-issue="hasConnectionIssue" />
:has-connection-issue="requireReconnect" />
<slot name="header" />
</MenuBar>
<div v-else class="menubar-placeholder" />
Expand Down Expand Up @@ -117,6 +117,7 @@ import Assistant from './Assistant.vue'
import Translate from './Modal/Translate.vue'
import { generateRemoteUrl } from '@nextcloud/router'
import { fetchNode } from '../services/WebdavClient.ts'
import { useDelayedFlag } from './Editor/useDelayedFlag.ts'

export default {
name: 'Editor',
Expand Down Expand Up @@ -232,7 +233,9 @@ export default {
const maxWidth = Math.floor(value) - 36
el.value.style.setProperty('--widget-full-width', `${maxWidth}px`)
})
return { el, width }
const hasConnectionIssue = ref(false)
const { delayed: requireReconnect } = useDelayedFlag(hasConnectionIssue)
return { el, width, hasConnectionIssue, requireReconnect }
},

data() {
Expand All @@ -250,7 +253,6 @@ export default {
dirty: false,
contentLoaded: false,
syncError: null,
hasConnectionIssue: false,
hasEditor: false,
readOnly: true,
forceRecreate: false,
Expand Down Expand Up @@ -334,6 +336,14 @@ export default {
window.removeEventListener('beforeunload', this.saveBeforeUnload)
}
},
requireReconnect(val) {
if (val) {
this.emit('sync-service:error')
}
if (this.$editor?.isEditable === val) {
this.$editor.setEditable(!val)
}
},
},
mounted() {
if (this.active && (this.hasDocumentParameters)) {
Expand Down Expand Up @@ -585,14 +595,20 @@ export default {
this.document = document

this.syncError = null
const editable = !this.readOnly && !this.hasConnectionIssue
const editable = !this.readOnly && !this.requireReconnect
if (this.$editor.isEditable !== editable) {
this.$editor.setEditable(editable)
}
},

onSync({ steps, document }) {
this.hasConnectionIssue = this.$syncService.backend.fetcher === 0 || !this.$providers[0].wsconnected || this.$syncService.pushError > 0
this.hasConnectionIssue = this.$syncService.backend.fetcher === 0
|| !this.$providers[0].wsconnected
|| this.$syncService.pushError > 0
if (this.$syncService.pushError > 0) {
// successfully received steps - so let's try and also push
this.$syncService.sendStepsNow()
}
this.$nextTick(() => {
this.emit('sync-service:sync')
})
Expand All @@ -602,11 +618,6 @@ export default {
},

onError({ type, data }) {
this.$nextTick(() => {
this.$editor?.setEditable(false)
this.emit('sync-service:error')
})

if (type === ERROR_TYPE.LOAD_ERROR) {
this.syncError = {
type,
Expand All @@ -621,11 +632,8 @@ export default {
data,
}
}
if (type === ERROR_TYPE.CONNECTION_FAILED && !this.hasConnectionIssue) {
this.hasConnectionIssue = true
OC.Notification.showTemporary(t('text', 'Connection failed.'))
}
if (type === ERROR_TYPE.SOURCE_NOT_FOUND) {
if (type === ERROR_TYPE.CONNECTION_FAILED
|| type === ERROR_TYPE.SOURCE_NOT_FOUND) {
this.hasConnectionIssue = true
}

Expand Down
29 changes: 29 additions & 0 deletions src/components/Editor/useDelayedFlag.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import { ref, watch, type Ref } from 'vue'

/**
* Delay the changing of the boolean
* @param input - ref to react to
*/
export function useDelayedFlag(input: Ref<boolean>): { delayed: Ref<boolean> } {

let timeout: ReturnType<typeof setTimeout> | undefined
const delayed = ref(input.value)

watch(input, (val) => {
if (timeout) {
clearTimeout(timeout)
}
const delay = val ? 5000 : 200
timeout = setTimeout(() => {
delayed.value = val
}, delay)

})

return { delayed }
}
Loading