From 96bc72be1b798a28ffc43766b5f7092e998ecc8a Mon Sep 17 00:00:00 2001 From: Per-Kristian Nordnes Date: Thu, 14 Mar 2024 15:19:10 +0100 Subject: [PATCH] fix(portable-text-editor): fix and test issue with merge block operation (#5996) * test(portable-text-editor): fix wrong path used in package script (dev) * test(portable-text-editor): fix debug statement and remove redundant var * test(portable-text-editr): add collab test for merge empty block --- .../writingTogether.collaborative.test.ts | 61 +++++++++++++++++++ .../@sanity/portable-text-editor/package.json | 2 +- .../src/utils/applyPatch.ts | 12 ++-- 3 files changed, 70 insertions(+), 5 deletions(-) diff --git a/packages/@sanity/portable-text-editor/e2e-tests/__tests__/writingTogether.collaborative.test.ts b/packages/@sanity/portable-text-editor/e2e-tests/__tests__/writingTogether.collaborative.test.ts index 8072f3e1b0b..118c02352bb 100644 --- a/packages/@sanity/portable-text-editor/e2e-tests/__tests__/writingTogether.collaborative.test.ts +++ b/packages/@sanity/portable-text-editor/e2e-tests/__tests__/writingTogether.collaborative.test.ts @@ -65,6 +65,67 @@ describe('collaborate editing', () => { }) }) + it('should not remove content for both users if one user backspaces into a block that starts with a mark.', async () => { + const exampleValue = [ + { + _key: 'randomKey0', + _type: 'block', + children: [ + { + _key: 'randomKey1', + _type: 'span', + marks: ['strong'], + text: 'Example Text: ', + }, + { + _type: 'span', + marks: [], + text: "This is a very long example text that will completely disappear later on. It's kind of a bad magic trick, really. Just writing more text so the disappearance becomes more apparent. This is a very long example text that will completely disappear later on. It's kind of a bad magic trick, really. Just writing more text so the disappearance becomes more apparent. This is a very long example text that will completely disappear later on. It's kind of a bad magic trick, really. Just writing more text so the disappearance becomes more apparent.", + _key: 'randomKey2', + }, + ], + markDefs: [], + style: 'normal', + }, + ] + await setDocumentValue(exampleValue) + const [editorA, editorB] = await getEditors() + await editorA.setSelection({ + anchor: {path: [{_key: 'randomKey0'}, 'children', {_key: 'randomKey2'}], offset: 542}, + focus: {path: [{_key: 'randomKey0'}, 'children', {_key: 'randomKey2'}], offset: 542}, + }) + await editorA.pressKey('Enter') + await editorA.pressKey('Backspace') + + await new Promise((resolve) => setTimeout(resolve, 1000)) + + const valA = await editorA.getValue() + const valB = await editorB.getValue() + expect(valA).toEqual(valB) + expect(valB).toEqual([ + { + _key: 'randomKey0', + _type: 'block', + children: [ + { + _key: 'randomKey1', + _type: 'span', + marks: ['strong'], + text: 'Example Text: ', + }, + { + _type: 'span', + marks: [], + text: "This is a very long example text that will completely disappear later on. It's kind of a bad magic trick, really. Just writing more text so the disappearance becomes more apparent. This is a very long example text that will completely disappear later on. It's kind of a bad magic trick, really. Just writing more text so the disappearance becomes more apparent. This is a very long example text that will completely disappear later on. It's kind of a bad magic trick, really. Just writing more text so the disappearance becomes more apparent.", + _key: 'randomKey2', + }, + ], + markDefs: [], + style: 'normal', + }, + ]) + }) + it('will reset the value when someone deletes everything, and when they start to type again, they will produce their own respective blocks.', async () => { await setDocumentValue(initialValue) const [editorA, editorB] = await getEditors() diff --git a/packages/@sanity/portable-text-editor/package.json b/packages/@sanity/portable-text-editor/package.json index 088b77930ac..71788f43849 100644 --- a/packages/@sanity/portable-text-editor/package.json +++ b/packages/@sanity/portable-text-editor/package.json @@ -57,7 +57,7 @@ "clean": "rimraf lib", "lint": "eslint .", "prettier": "prettier --write './**/*.{ts,tsx,js,css,html}'", - "dev": "cd ./test/ && ts-node serve", + "dev": "cd ./e2e-tests/ && ts-node serve", "test": "jest", "test:e2e": "jest --config=e2e-tests/e2e.config.cjs", "test:watch": "jest --watch", diff --git a/packages/@sanity/portable-text-editor/src/utils/applyPatch.ts b/packages/@sanity/portable-text-editor/src/utils/applyPatch.ts index 17b7379ebd0..f3e0f824d0f 100644 --- a/packages/@sanity/portable-text-editor/src/utils/applyPatch.ts +++ b/packages/@sanity/portable-text-editor/src/utils/applyPatch.ts @@ -250,18 +250,22 @@ function setPatch(editor: PortableTextSlateEditor, patch: SetPatch) { } else if (Element.isElement(block) && patch.path.length === 1 && blockPath) { debug('Setting block property') const {children, ...nextRest} = value as unknown as PortableTextBlock - // eslint-disable-next-line @typescript-eslint/no-unused-vars + // eslint-disable-next-line @typescript-eslint/no-unused-vars, unused-imports/no-unused-vars const {children: prevChildren, ...prevRest} = block || {children: undefined} + // Set any block properties editor.apply({ type: 'set_node', path: blockPath, properties: {...prevRest}, newProperties: nextRest, }) + // Replace the children in the block + // Note that children must be explicitly inserted, and can't be set with set_node + debug('Setting children') block.children.forEach((c, cIndex) => { editor.apply({ type: 'remove_node', - path: blockPath.concat(cIndex), + path: blockPath.concat(block.children.length - 1 - cIndex), node: c, }) }) @@ -306,6 +310,7 @@ function unsetPatch(editor: PortableTextSlateEditor, patch: UnsetPatch, previous return true } const {block, blockPath, child, childPath} = findBlockAndChildFromPath(editor, patch.path) + // Single blocks if (patch.path.length === 1) { if (!block || !blockPath) { @@ -327,11 +332,10 @@ function unsetPatch(editor: PortableTextSlateEditor, patch: UnsetPatch, previous debug('Child not found') return false } - const childIndex = childPath[1] debug(`Unsetting child at path ${JSON.stringify(childPath)}`) debugState(editor, 'before') if (debugVerbose) { - debug(`Removing child at path ${JSON.stringify([childPath, childIndex])}`) + debug(`Removing child at path ${JSON.stringify(childPath)}`) } Transforms.removeNodes(editor, {at: childPath}) debugState(editor, 'after')