Skip to content
Draft
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
11 changes: 11 additions & 0 deletions .changeset/fix-insert-patch-bug.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
'@portabletext/editor': patch
---

fix: use `set` instead of `setIfMissing` + `insert` when target value is empty

When a remote Portable Text field had a `null` value (as opposed to `undefined`
or `[]`), typing or pasting into the editor would produce patches that failed to
apply on the receiving end. This is because `setIfMissing` treats `null` as
"present", so the subsequent `insert` would attempt to insert into `null` rather
than an array.
22 changes: 4 additions & 18 deletions packages/editor/src/internal-utils/operation-to-patches.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,19 +126,13 @@ describe(insertNodePatch.name, () => {
).toEqual([
{
path: [],
type: 'setIfMissing',
value: [],
},
{
path: [0],
type: 'insert',
items: [
type: 'set',
value: [
{
_key: 'k2',
_type: 'image',
},
],
position: 'before',
},
])
})
Expand Down Expand Up @@ -277,21 +271,13 @@ describe('operationToPatches', () => {
[
{
"path": [],
"type": "setIfMissing",
"value": [],
},
{
"items": [
"type": "set",
"value": [
{
"_key": "c130395c640c",
"_type": "someObject",
},
],
"path": [
0,
],
"position": "before",
"type": "insert",
},
]
`)
Expand Down
13 changes: 9 additions & 4 deletions packages/editor/src/internal-utils/operation-to-patches.ts
Original file line number Diff line number Diff line change
Expand Up @@ -280,10 +280,12 @@ export function insertNodePatch(
beforeValue: Array<PortableTextBlock>,
): Array<Patch> {
const block = beforeValue[operation.path[0]!]

if (operation.path.length === 1) {
const position = operation.path[0] === 0 ? 'before' : 'after'
const beforeBlock = beforeValue[operation.path[0]! - 1]
const targetKey = operation.path[0] === 0 ? block?._key : beforeBlock?._key

if (targetKey) {
return [
insert(
Expand All @@ -293,12 +295,15 @@ export function insertNodePatch(
),
]
}

// When inserting into an empty array (no targetKey), use `set` instead of
// `setIfMissing` + `insert`. This handles the case where the field value is
// null (not just undefined or []), since `setIfMissing` treats null as
// "present".
return [
setIfMissing(beforeValue, []),
insert(
set(
[fromSlateBlock(operation.node as Descendant, schema.block.name)],
'before',
[operation.path[0]!],
[],
),
]
} else if (
Expand Down
9 changes: 6 additions & 3 deletions packages/editor/src/slate-plugins/slate-plugin.patches.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {insert, setIfMissing, unset, type Patch} from '@portabletext/patches'
import {set, setIfMissing, unset, type Patch} from '@portabletext/patches'
import type {PortableTextBlock} from '@portabletext/schema'
import {Editor, type Operation} from 'slate'
import type {EditorActor} from '../editor/editor-machine'
Expand Down Expand Up @@ -128,13 +128,16 @@ export function createPatchesPlugin({
return editor
}

// If the editor was empty and now isn't, insert the placeholder into it.
if (
editorWasEmpty &&
!editorIsEmpty &&
operation.type !== 'set_selection'
) {
patches.push(insert(previousValue, 'before', [0]))
// If the editor was empty and now isn't, set the value atomically.
// Using `set` instead of `insert` handles the case where the field is
// null (not just undefined or []), since `setIfMissing` treats null as
// "present".
patches.push(set(previousValue, []))
}

switch (operation.type) {
Expand Down
2 changes: 1 addition & 1 deletion packages/editor/tests/change-subject.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ describe('change$', () => {
}),
expect.objectContaining({
type: 'patch',
patch: expect.objectContaining({type: 'insert'}),
patch: expect.objectContaining({type: 'set'}),
}),
expect.objectContaining({
type: 'patch',
Expand Down
25 changes: 10 additions & 15 deletions packages/editor/tests/event.block.set.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ describe('event.block.set', () => {
])
expect(patches).toEqual([
setIfMissing([], []),
insert(
set(
[
{
_key: 'k0',
Expand All @@ -61,8 +61,7 @@ describe('event.block.set', () => {
style: 'normal',
},
],
'before',
[0],
[],
),
insert(
[
Expand Down Expand Up @@ -149,7 +148,7 @@ describe('event.block.set', () => {
])
expect(patches).toEqual([
setIfMissing([], []),
insert(
set(
[
{
_key: 'k0',
Expand All @@ -159,8 +158,7 @@ describe('event.block.set', () => {
style: 'normal',
},
],
'before',
[0],
[],
),
insert(
[
Expand Down Expand Up @@ -411,7 +409,7 @@ describe('event.block.set', () => {
])
expect(patches).toEqual([
setIfMissing([], []),
insert(
set(
[
{
_key: 'k0',
Expand All @@ -421,8 +419,7 @@ describe('event.block.set', () => {
style: 'normal',
},
],
'before',
[0],
[],
),
insert(
[
Expand Down Expand Up @@ -471,7 +468,7 @@ describe('event.block.set', () => {

expect(patches.slice(7)).toEqual([
setIfMissing([], []),
insert(
set(
[
{
_key: textBlockKey,
Expand All @@ -488,8 +485,7 @@ describe('event.block.set', () => {
style: 'normal',
},
],
'before',
[0],
[],
),
set('h1', [{_key: textBlockKey}, 'style']),
])
Expand Down Expand Up @@ -592,7 +588,7 @@ describe('event.block.set', () => {

expect(patches).toEqual([
setIfMissing([], []),
insert(
set(
[
{
_key: 'k0',
Expand All @@ -602,8 +598,7 @@ describe('event.block.set', () => {
style: 'normal',
},
],
'before',
[0],
[],
),
set('bar', [{_key: 'k0'}, 'foo']),
])
Expand Down
20 changes: 8 additions & 12 deletions packages/editor/tests/event.block.unset.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ describe('event.block.unset', () => {

expect(patches).toEqual([
setIfMissing([], []),
insert(
set(
[
{
_key: 'k0',
Expand All @@ -75,8 +75,7 @@ describe('event.block.unset', () => {
style: 'normal',
},
],
'before',
[0],
[],
),
insert(
[
Expand Down Expand Up @@ -137,7 +136,7 @@ describe('event.block.unset', () => {
])
expect(patches).toEqual([
setIfMissing([], []),
insert(
set(
[
{
_key: 'k0',
Expand All @@ -147,8 +146,7 @@ describe('event.block.unset', () => {
style: 'normal',
},
],
'before',
[0],
[],
),
insert(
[
Expand Down Expand Up @@ -243,7 +241,7 @@ describe('event.block.unset', () => {

expect(patches).toEqual([
setIfMissing([], []),
insert(
set(
[
{
_key: 'k0',
Expand All @@ -253,8 +251,7 @@ describe('event.block.unset', () => {
style: 'normal',
},
],
'before',
[0],
[],
),
insert(
[
Expand Down Expand Up @@ -390,7 +387,7 @@ describe('event.block.unset', () => {
])
expect(patches).toEqual([
setIfMissing([], []),
insert(
set(
[
{
_key: 'k0',
Expand All @@ -400,8 +397,7 @@ describe('event.block.unset', () => {
markDefs: [],
},
],
'before',
[0],
[],
),
insert(
[
Expand Down
7 changes: 3 additions & 4 deletions packages/editor/tests/event.child.unset.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -390,10 +390,9 @@ describe('event.child.unset', () => {
},
{
origin: 'local',
type: 'insert',
path: [0],
position: 'before',
items: [
type: 'set',
path: [],
value: [
{
_key: blockKey,
_type: 'block',
Expand Down
6 changes: 2 additions & 4 deletions packages/editor/tests/event.delete.backward.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import {
applyAll,
insert,
set,
setIfMissing,
unset,
Expand Down Expand Up @@ -95,7 +94,7 @@ describe('event.delete.backward', () => {
expect(foreignValue).toEqual(expectedValue)
expect(patches.slice(1)).toEqual([
setIfMissing([], []),
insert(
set(
[
{
_type: 'block',
Expand All @@ -105,8 +104,7 @@ describe('event.delete.backward', () => {
style: 'normal',
},
],
'before',
[0],
[],
),
set('bar', [{_key: 'k3'}, 'foo']),
])
Expand Down
6 changes: 2 additions & 4 deletions packages/editor/tests/event.delete.block.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import {
applyAll,
insert,
set,
setIfMissing,
unset,
Expand Down Expand Up @@ -87,7 +86,7 @@ describe('event.delete.block', () => {
expect(foreignValue).toEqual(expectedValue)
expect(patches.slice(1)).toEqual([
setIfMissing([], []),
insert(
set(
[
{
_type: 'block',
Expand All @@ -97,8 +96,7 @@ describe('event.delete.block', () => {
style: 'normal',
},
],
'before',
[0],
[],
),
set('bar', [{_key: 'k3'}, 'foo']),
])
Expand Down
6 changes: 2 additions & 4 deletions packages/editor/tests/event.delete.forward.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import {
applyAll,
insert,
set,
setIfMissing,
unset,
Expand Down Expand Up @@ -91,7 +90,7 @@ describe('event.delete.forward', () => {
expect(foreignValue).toEqual(expectedValue)
expect(patches.slice(1)).toEqual([
setIfMissing([], []),
insert(
set(
[
{
_type: 'block',
Expand All @@ -101,8 +100,7 @@ describe('event.delete.forward', () => {
style: 'normal',
},
],
'before',
[0],
[],
),
set('bar', [{_key: 'k3'}, 'foo']),
])
Expand Down
Loading