diff --git a/cypress/e2e/api/SessionApi.spec.js b/cypress/e2e/api/SessionApi.spec.js index 13361cf8617..c84834f97d9 100644 --- a/cypress/e2e/api/SessionApi.spec.js +++ b/cypress/e2e/api/SessionApi.spec.js @@ -73,23 +73,19 @@ describe('The session Api', function () { cy.closeConnection(connection) }) - // Echoes all message types but queries - Object.entries(messages) - .filter(([key, _value]) => key !== 'query') - .forEach(([type, sample]) => { - it(`echos ${type} messages`, function () { - const steps = [sample] - const version = 0 - cy.pushSteps({ connection, steps, version }) - .its('version') - .should('eql', 0) - cy.syncSteps(connection) - .its('steps[0].data') - .should('eql', steps) - }) + // Echoes updates and responses + ;['update', 'response'].forEach((type) => { + it(`echos ${type} messages`, function () { + const steps = [messages[type]] + const version = 0 + cy.pushSteps({ connection, steps, version }) + .its('version') + .should('eql', 0) + cy.syncSteps(connection).its('steps[0].data').should('eql', steps) }) + }) - it('responds to queries', function () { + it('responds to queries with updates and responses', function () { const version = 0 Object.entries(messages).forEach(([type, sample]) => { cy.pushSteps({ connection, steps: [sample], version }) @@ -97,10 +93,13 @@ describe('The session Api', function () { cy.pushSteps({ connection, steps: [messages.query], version }).then( (response) => { cy.wrap(response).its('version').should('eql', 0) - cy.wrap(response).its('steps.length').should('eql', 1) + cy.wrap(response).its('steps.length').should('eql', 2) cy.wrap(response) .its('steps[0].data') .should('eql', [messages.update]) + cy.wrap(response) + .its('steps[1].data') + .should('eql', [messages.response]) }, ) }) @@ -111,7 +110,6 @@ describe('The session Api', function () { let connection let fileId let filePath - let joining beforeEach(function () { cy.testName().then((name) => { @@ -156,13 +154,10 @@ describe('The session Api', function () { manualSave: true, }) cy.openConnection({ fileId, filePath }) - .then(({ connection: con, data }) => { - joining = con - return data - }) - .its('documentState') + .as('joining') + .its('data.documentState') .should('eql', documentState) - cy.closeConnection(joining) + cy.get('@joining').its('connection').then(cy.closeConnection) }) afterEach(function () { @@ -175,7 +170,6 @@ describe('The session Api', function () { let connection let filePath let shareToken - let joining beforeEach(function () { cy.testName().then((name) => { @@ -232,13 +226,10 @@ describe('The session Api', function () { manualSave: true, }) cy.openConnection({ filePath: '', token: shareToken }) - .then(({ connection: con, data }) => { - joining = con - return data - }) - .its('documentState') + .as('joining') + .its('data.documentState') .should('eql', documentState) - cy.closeConnection(joining) + cy.get('@joining').its('connection').then(cy.closeConnection) }) }) diff --git a/cypress/e2e/api/SyncServiceProvider.spec.js b/cypress/e2e/api/SyncServiceProvider.spec.js index 8844754aab0..257315af0e4 100644 --- a/cypress/e2e/api/SyncServiceProvider.spec.js +++ b/cypress/e2e/api/SyncServiceProvider.spec.js @@ -43,15 +43,20 @@ describe('Sync service provider', function () { */ function createProvider(ydoc) { const relativePath = '.' - const { connection, openConnection, baseVersionEtag } = provideConnection({ - fileId, - relativePath, - }) - const { syncService } = provideSyncService( - connection, - openConnection, - baseVersionEtag, + let baseVersionEtag + const setBaseVersionEtag = (val) => { + baseVersionEtag = val + } + const getBaseVersionEtag = () => baseVersionEtag + const { connection, openConnection } = provideConnection( + { + fileId, + relativePath, + }, + getBaseVersionEtag, + setBaseVersionEtag, ) + const { syncService } = provideSyncService(connection, openConnection) const queue = [] syncService.bus.on('opened', () => syncService.startSync()) return createSyncServiceProvider({ diff --git a/cypress/e2e/conflict.spec.js b/cypress/e2e/conflict.spec.js index da4b2199d6c..f1edac7fac3 100644 --- a/cypress/e2e/conflict.spec.js +++ b/cypress/e2e/conflict.spec.js @@ -36,14 +36,9 @@ variants.forEach(function ({ fixture, mime }) { cy.getContent().should('contain', 'Heading') cy.uploadFile(fileName, mime, testName + '/' + fileName) - cy.get('#editor-container .document-status', { - timeout: 40000, - }).should('contain', 'session has expired') - - // Reload button works - cy.get('#editor-container .document-status a.button') - .contains('Reload') - .click() + cy.intercept({ method: 'POST', url: '**/session/*/push' }).as('push') + cy.wait('@push', { timeout: 20_000 }) + // Autoreload works getWrapper().should('not.exist') cy.getContent().should('contain', 'Hello world') cy.getContent().should('not.contain', 'Heading') diff --git a/cypress/e2e/sync.spec.js b/cypress/e2e/sync.spec.js index 5f35ee65085..c76d4a13eab 100644 --- a/cypress/e2e/sync.spec.js +++ b/cypress/e2e/sync.spec.js @@ -108,7 +108,10 @@ describe('Sync', () => { 'contain', 'The document could not be loaded.', ) + cy.intercept('**/apps/text/session/*/create').as('create') cy.get('#editor-container .document-status').find('.button.primary').click() + // let first attempt fail + cy.wait('@create', { timeout: 10000 }) cy.get('#editor-container .document-status', { timeout: 30000 }).should( 'contain', 'The document could not be loaded.', @@ -117,13 +120,13 @@ describe('Sync', () => { cy.intercept('**/apps/text/session/*/*', (req) => { req.continue() }).as('alive') - cy.intercept('**/apps/text/session/*/create').as('create') cy.get('#editor-container .document-status').find('.button.primary').click() + // this is the create request... - now with the alive alias cy.wait('@alive', { timeout: 30000 }) - cy.wait('@create', { timeout: 10000 }) .its('request.body') .should('have.property', 'baseVersionEtag') .should('not.be.empty') + cy.getContent().should('contain', 'Hello world') }) it('recovers from a lost and closed connection', () => { @@ -176,18 +179,9 @@ describe('Sync', () => { cy.wait('@save') cy.uploadTestFile('test.md') - cy.get('#editor-container .document-status', { timeout: 30000 }).should( - 'contain', - 'Editing session has expired.', - ) - - // Reload button works - cy.get('#editor-container .document-status a.button') - .contains('Reload') - .click() - - cy.getContent() - cy.get('#editor-container .document-status .notecard').should('not.exist') + cy.getContent().should('not.exist') + cy.getContent().find('h2').should('contain', 'Hello world') + cy.getContent().find('li').should('not.exist') // was overwritten after the save }) it('passes the doc content from one session to the next', () => { diff --git a/lib/Cron/Cleanup.php b/lib/Cron/Cleanup.php index eb749c6da30..839a20ff11c 100644 --- a/lib/Cron/Cleanup.php +++ b/lib/Cron/Cleanup.php @@ -11,7 +11,6 @@ namespace OCA\Text\Cron; -use OCA\Text\Exception\DocumentHasUnsavedChangesException; use OCA\Text\Service\AttachmentService; use OCA\Text\Service\DocumentService; use OCA\Text\Service\SessionService; @@ -42,11 +41,6 @@ protected function run($argument): void { // Inactive sessions will get removed further down and will trigger a reset next time continue; } - - try { - $this->documentService->resetDocument($document->getId()); - } catch (DocumentHasUnsavedChangesException) { - } $this->attachmentService->cleanupAttachments($document->getId()); } diff --git a/lib/Listeners/BeforeNodeWrittenListener.php b/lib/Listeners/BeforeNodeWrittenListener.php index b634a9f396c..9a145639ed9 100644 --- a/lib/Listeners/BeforeNodeWrittenListener.php +++ b/lib/Listeners/BeforeNodeWrittenListener.php @@ -40,12 +40,12 @@ public function handle(Event $event): void { } // Reset document session to avoid manual conflict resolution if there's no unsaved steps try { - $this->documentService->resetDocument($node->getId()); + $this->documentService->resetDocument($node->getId(), true); } catch (DocumentHasUnsavedChangesException|NotFoundException $e) { // Do not throw during event handling in this is expected to happen // DocumentHasUnsavedChangesException: A document editing session is likely ongoing, someone can resolve the conflict // NotFoundException: The event was called oin a file that was just created so a NonExistingFile object is used that has no id yet - $this->logger->debug('Reset document skipped in BeforeNodeWrittenEvent', ['exception' => $e]); + $this->logger->warning('Reset document skipped in BeforeNodeWrittenEvent', ['exception' => $e]); } } } diff --git a/lib/Service/DocumentService.php b/lib/Service/DocumentService.php index 7fb51e67333..756cd769ffb 100644 --- a/lib/Service/DocumentService.php +++ b/lib/Service/DocumentService.php @@ -208,8 +208,12 @@ public function addStep(Document $document, Session $session, array $steps, int if ($readOnly && $message->isUpdate()) { continue; } + // Only accept sync protocol + if ($message->getYjsMessageType() !== YjsMessage::YJS_MESSAGE_SYNC) { + continue; + } // Filter out query steps as they would just trigger clients to send their steps again - if ($message->getYjsMessageType() === YjsMessage::YJS_MESSAGE_SYNC && $message->getYjsSyncType() === YjsMessage::YJS_MESSAGE_SYNC_STEP1) { + if ($message->getYjsSyncType() === YjsMessage::YJS_MESSAGE_SYNC_STEP1) { $stepsIncludeQuery = true; } else { $stepsToInsert[] = $step; @@ -249,7 +253,7 @@ public function addStep(Document $document, Session $session, array $steps, int $stepsToReturn = []; foreach ($allSteps as $step) { $message = YjsMessage::fromBase64($step->getData()); - if ($message->getYjsMessageType() === YjsMessage::YJS_MESSAGE_SYNC && $message->getYjsSyncType() === YjsMessage::YJS_MESSAGE_SYNC_UPDATE) { + if ($message->isUpdate()) { $stepsToReturn[] = $step; } } diff --git a/package-lock.json b/package-lock.json index 4e79d49f6e0..d038b5da03c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -80,6 +80,7 @@ "vue-click-outside": "^1.1.0", "vue-material-design-icons": "^5.3.1", "webdav": "^5.8.0", + "y-indexeddb": "^9.0.12", "y-prosemirror": "^1.3.7", "y-protocols": "^1.0.7", "yjs": "^13.6.28" @@ -21802,6 +21803,26 @@ "node": ">=0.4" } }, + "node_modules/y-indexeddb": { + "version": "9.0.12", + "resolved": "https://registry.npmjs.org/y-indexeddb/-/y-indexeddb-9.0.12.tgz", + "integrity": "sha512-9oCFRSPPzBK7/w5vOkJBaVCQZKHXB/v6SIT+WYhnJxlEC61juqG0hBrAf+y3gmSMLFLwICNH9nQ53uscuse6Hg==", + "license": "MIT", + "dependencies": { + "lib0": "^0.2.74" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=8.0.0" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + }, + "peerDependencies": { + "yjs": "^13.0.0" + } + }, "node_modules/y-prosemirror": { "version": "1.3.7", "resolved": "https://registry.npmjs.org/y-prosemirror/-/y-prosemirror-1.3.7.tgz", diff --git a/package.json b/package.json index ad5d674f867..3d36e5a183b 100644 --- a/package.json +++ b/package.json @@ -100,6 +100,7 @@ "vue-click-outside": "^1.1.0", "vue-material-design-icons": "^5.3.1", "webdav": "^5.8.0", + "y-indexeddb": "^9.0.12", "y-prosemirror": "^1.3.7", "y-protocols": "^1.0.7", "yjs": "^13.6.28" diff --git a/playwright/e2e/indexed-db.spec.ts b/playwright/e2e/indexed-db.spec.ts new file mode 100644 index 00000000000..dba6964ce94 --- /dev/null +++ b/playwright/e2e/indexed-db.spec.ts @@ -0,0 +1,58 @@ +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { expect, mergeTests } from '@playwright/test' +import { test as editorTest } from '../support/fixtures/editor' +import { test as offlineTest } from '../support/fixtures/offline' +import { test as randomUserTest } from '../support/fixtures/random-user' +import { test as uploadFileTest } from '../support/fixtures/upload-file' + +const test = mergeTests(editorTest, offlineTest, randomUserTest, uploadFileTest) + +// As we switch on and off the network +// we cannot run tests in parallel. +test.describe.configure({ mode: 'serial' }) + +test.beforeEach(async ({ open }) => { + await open() +}) + +test('recovering from indexed db', async ({ + close, + editor, + file, + setOffline, + setOnline, +}) => { + await expect(editor.el).toBeVisible() + await setOffline() + await editor.typeHeading('Hello world') + await close() + await setOnline() + await file.open() + await expect(editor.getHeading({ name: 'Hello world' })).toBeVisible() + await expect(editor.offlineState).not.toBeVisible() + await expect(editor.saveIndicator).toHaveAttribute('title', /Unsaved changes/) +}) + +test('conflict when recovering from indexed db', async ({ + close, + editor, + file, + setOffline, + setOnline, + user, +}) => { + await expect(editor.el).toBeVisible() + await setOffline() + await editor.typeHeading('Hello world') + await close() + await setOnline() + await user.uploadFile({ name: file.name, content: 'Good bye' }) + await file.open() + await expect(editor.getHeading({name: 'Hello world'})).toBeVisible() + await expect(editor.offlineState).not.toBeVisible() + await expect(editor.saveIndicator).toHaveAttribute('title', /Unsaved changes/) +}) diff --git a/playwright/support/fixtures/editor-api.ts b/playwright/support/fixtures/editor-api.ts index 8663d178cc5..d188fdd31ea 100644 --- a/playwright/support/fixtures/editor-api.ts +++ b/playwright/support/fixtures/editor-api.ts @@ -49,7 +49,6 @@ export const test = base.extend({ document.body.appendChild(container) const method = type === 'editor' ? 'createEditor' : 'createTable' - // @ts-expect-error - OCA.Text is a global await window.OCA.Text[method]({ el: container, ...(fileId != null ? { fileId } : { content }), diff --git a/playwright/support/fixtures/upload-file.ts b/playwright/support/fixtures/upload-file.ts index 578b5283e79..287c3934abd 100644 --- a/playwright/support/fixtures/upload-file.ts +++ b/playwright/support/fixtures/upload-file.ts @@ -39,5 +39,6 @@ export const test = base.extend({ open: ({ file }, use) => use(() => file.open()), viewer: ({ fileName, page }, use) => use(new ViewerSection(fileName, page)), + close: ({ viewer }, use) => use(() => viewer.close()), }) diff --git a/src/EditorFactory.js b/src/EditorFactory.ts similarity index 71% rename from src/EditorFactory.js rename to src/EditorFactory.ts index 74235a30862..b220c5fb4ed 100644 --- a/src/EditorFactory.js +++ b/src/EditorFactory.ts @@ -5,21 +5,23 @@ import 'proxy-polyfill' -import { Editor } from '@tiptap/core' +import { Editor, Extension } from '@tiptap/core' import hljs from 'highlight.js/lib/core' import { createLowlight } from 'lowlight' +import type { Node } from '@tiptap/pm/model' +import type { Connection } from './composables/useConnection' import { FocusTrap, PlainText, RichText } from './extensions/index.js' -import { logger } from './helpers/logger.js' +import { logger } from './helpers/logger' const lowlight = createLowlight() -const loadSyntaxHighlight = async (language) => { +const loadSyntaxHighlight = async (language: string) => { const list = hljs.listLanguages() logger.debug('Supported languages', { list }) if (!lowlight.listLanguages().includes(language)) { try { - logger.debug('Loading language', language) + logger.debug('Loading language ' + language) // eslint-disable-next-line n/no-missing-import const syntax = await import( `../node_modules/highlight.js/lib/languages/${language}.js` @@ -42,6 +44,11 @@ const createRichEditor = ({ connection, relativePath, isEmbedded = false, +}: { + extensions?: Extension[] + connection?: Connection + relativePath?: string + isEmbedded?: boolean } = {}) => { return new Editor({ editorProps, @@ -53,7 +60,10 @@ const createRichEditor = ({ }) } -const createPlainEditor = ({ language = 'plaintext', extensions = [] } = {}) => { +const createPlainEditor = ({ + language = 'plaintext', + extensions = [], +}: { language?: string; extensions?: Extension[] } = {}) => { return new Editor({ editorProps, extensions: [ @@ -68,7 +78,7 @@ const createPlainEditor = ({ language = 'plaintext', extensions = [] } = {}) => }) } -const serializePlainText = (doc) => { +const serializePlainText = (doc: Node) => { return doc.textContent } diff --git a/src/components/Editor.vue b/src/components/Editor.vue index dd720865b40..89e84b02fc2 100644 --- a/src/components/Editor.vue +++ b/src/components/Editor.vue @@ -86,7 +86,7 @@ import { File } from '@nextcloud/files' import { Collaboration } from '@tiptap/extension-collaboration' import { useElementSize } from '@vueuse/core' import { defineComponent, ref, shallowRef, watch } from 'vue' -import { Doc } from 'yjs' +import { Doc, logUpdate } from 'yjs' import Autofocus from '../extensions/Autofocus.js' import { provideEditor } from '../composables/useEditor.ts' @@ -101,20 +101,21 @@ import { useDelayedFlag } from '../composables/useDelayedFlag.ts' import { provideEditorHeadings } from '../composables/useEditorHeadings.ts' import { useEditorMethods } from '../composables/useEditorMethods.ts' import { provideEditorWidth } from '../composables/useEditorWidth.ts' +import { useIndexedDbProvider } from '../composables/useIndexedDbProvider.ts' import { provideSaveService } from '../composables/useSaveService.ts' import { provideSyncService } from '../composables/useSyncService.ts' import { useSyntaxHighlighting } from '../composables/useSyntaxHighlighting.ts' import { CollaborationCaret } from '../extensions/index.js' -import { exposeForDebugging, removeFromDebugging } from '../helpers/debug.js' -import { logger } from '../helpers/logger.js' -import { setInitialYjsState } from '../helpers/setInitialYjsState.js' +import { exposeForDebugging, removeFromDebugging } from '../helpers/debug.ts' +import { logger } from '../helpers/logger.ts' +import { setInitialYjsState } from '../helpers/setInitialYjsState.ts' import { ERROR_TYPE, IDLE_TIMEOUT } from '../services/SyncService.ts' import { fetchNode } from '../services/WebdavClient.ts' import { createPlainEditor, createRichEditor, serializePlainText, -} from './../EditorFactory.js' +} from './../EditorFactory.ts' import { createMarkdownSerializer } from './../extensions/Markdown.js' import markdownit from './../markdownit/index.js' import isMobile from './../mixins/isMobile.js' @@ -224,6 +225,14 @@ export default defineComponent({ }) const ydoc = new Doc() const awareness = new Awareness(ydoc) + const { + clearIndexedDb, + dirty, + getBaseVersionEtag, + setBaseVersionEtag, + setDirty, + } = useIndexedDbProvider(props, ydoc) + const hasConnectionIssue = ref(false) const { delayed: requireReconnect } = useDelayedFlag(hasConnectionIssue) const { isPublic, isRichEditor, isRichWorkspace, useTableOfContents } = @@ -232,7 +241,11 @@ export default defineComponent({ isRichEditor, props, ) - const { connection, openConnection } = provideConnection(props) + const { connection, openConnection } = provideConnection( + props, + getBaseVersionEtag, + setBaseVersionEtag, + ) const { syncService } = provideSyncService(connection, openConnection) const extensions = [ Autofocus.configure({ fileId: props.fileId }), @@ -274,7 +287,9 @@ export default defineComponent({ return { awareness, + clearIndexedDb, connection, + dirty, editor, el, hasConnectionIssue, @@ -286,6 +301,7 @@ export default defineComponent({ requireReconnect, saveService, serialize, + setDirty, setEditable, syncProvider, syncService, @@ -303,7 +319,6 @@ export default defineComponent({ fileNode: null, idle: false, - dirty: false, contentLoaded: false, syncError: null, readOnly: true, @@ -329,6 +344,13 @@ export default defineComponent({ hasDocumentParameters() { return this.fileId || this.shareToken || this.initialSession }, + hasOutdatedDocument() { + return ( + this.syncError + && this.syncError.type === ERROR_TYPE.LOAD_ERROR + && this.syncError.data.status === 412 + ) + }, currentDirectory() { return this.relativePath ? this.relativePath.split('/').slice(0, -1).join('/') @@ -383,6 +405,24 @@ export default defineComponent({ } this.setEditable(!val) }, + hasOutdatedDocument(val) { + if (!val) { + return + } + logger.debug('Document is outdated') + if (this.dirty) { + logger.debug('There are local edits, need to resolve conflict') + // handle conflict between active editing session and offline content + } else { + // clear the outdated cached content and reload without it. + logger.debug( + 'No local edits... clearing storage and reloading the editor', + ) + this.clearIndexedDb().then(() => { + this.$emit('reload') + }) + } + }, }, mounted() { if (!this.richWorkspace) { @@ -400,10 +440,18 @@ export default defineComponent({ }, created() { // The following can be useful for debugging ydoc updates - // this.ydoc.on('update', function(update, origin, doc, tr) { - // console.debug('ydoc update', update, origin, doc, tr) - // Y.logUpdate(update) - // }); + this.ydoc.on('update', function (update, origin, doc, tr) { + if (window.OCA.Text.logYjsUpdates) { + logger.debug('ydoc update', { + update, + origin, + doc, + tr, + content: doc.getXmlFragment('default').toJSON(), + }) + logUpdate(update) + } + }) this.$attachmentResolver = null if (this.active && this.hasDocumentParameters) { this.initSession() @@ -411,6 +459,7 @@ export default defineComponent({ } }, async beforeDestroy() { + logger.debug('beforeDestroy') if (!this.richWorkspace) { window.removeEventListener('beforeprint', this.preparePrinting) window.removeEventListener('afterprint', this.preparePrinting) @@ -434,7 +483,7 @@ export default defineComponent({ syncService: this.syncService, fileId: this.fileId, initialSession: this.initialSession, - disableBC: true, + disableBc: true, awareness: this.awareness, }) }, @@ -515,6 +564,10 @@ export default defineComponent({ this.lowlightLoaded.then(() => { this.syncService.startSync() if (!documentState) { + logger.debug('loading initial content', { + content, + isRichEditor: this.isRichEditor, + }) setInitialYjsState(this.ydoc, content, { isRichEditor: this.isRichEditor, }) @@ -527,7 +580,7 @@ export default defineComponent({ this.document = document this.syncError = null - this.setEditable(this.editMode && !this.requireReconnect) + this.setEditable(this.editMode) // && !this.requireReconnect) }, onCreate({ editor }) { @@ -538,7 +591,9 @@ export default defineComponent({ }, onUpdate({ editor }) { - // this.debugContent(editor) + if (window.OCA.Text.logEditorUpdates) { + this.debugContent(editor) + } const proseMirrorMarkdown = this.serialize() this.emit('update:content', { markdown: proseMirrorMarkdown, @@ -610,7 +665,7 @@ export default defineComponent({ if (Object.prototype.hasOwnProperty.call(state, 'dirty')) { // ignore initial loading and other automated changes before first user change if (this.editor.can().undo() || this.editor.can().redo()) { - this.dirty = state.dirty + this.setDirty(state.dirty) if (this.dirty) { this.saveService.autosave() } @@ -665,6 +720,7 @@ export default defineComponent({ }, async disconnect() { + logger.debug('disconnecting') await this.syncService.close() this.unlistenSyncServiceEvents() this.syncProvider?.destroy() @@ -673,11 +729,13 @@ export default defineComponent({ }, async close() { + logger.debug('closing') await this.syncService .sendRemainingSteps() .catch((err) => logger.warn('Failed to send remaining steps', { err }), ) + logger.debug('sent remaining steps') await this.disconnect().catch((err) => logger.warn('Failed to disconnect', { err }), ) @@ -725,17 +783,18 @@ export default defineComponent({ * @param {object} editor The Tiptap editor */ debugContent(editor) { + // markdown, serialized from editor state by prosemirror-markdown const proseMirrorMarkdown = this.serialize() + // HTML, serialized from markdown by markdown-it const markdownItHtml = markdownit.render(proseMirrorMarkdown) + // HTML, as rendered in the browser by Tiptap + const tiptapHtml = editor.getHTML() - logger.debug( - 'markdown, serialized from editor state by prosemirror-markdown', - ) - console.debug(proseMirrorMarkdown) - logger.debug('HTML, serialized from markdown by markdown-it') - console.debug(markdownItHtml) - logger.debug('HTML, as rendered in the browser by Tiptap') - console.debug(editor.getHTML()) + logger.debug('editor update', { + proseMirrorMarkdown, + markdownItHtml, + tiptapHtml, + }) }, /** diff --git a/src/components/Editor/MediaHandler.vue b/src/components/Editor/MediaHandler.vue index 910d9d2a402..9fcd9f12d5b 100644 --- a/src/components/Editor/MediaHandler.vue +++ b/src/components/Editor/MediaHandler.vue @@ -36,7 +36,7 @@ import { insertAttachmentFile, uploadAttachment, } from '../../apis/attach.ts' -import { logger } from '../../helpers/logger.js' +import { logger } from '../../helpers/logger.ts' import { useEditor } from '../../composables/useEditor.ts' import { useFileMixin } from '../Editor.provider.ts' diff --git a/src/components/ViewerComponent.vue b/src/components/ViewerComponent.vue index 57925e1be80..14481f42297 100644 --- a/src/components/ViewerComponent.vue +++ b/src/components/ViewerComponent.vue @@ -5,16 +5,17 @@