Skip to content

Commit 5eca8a8

Browse files
authored
fix(schema-migration): more robust migration of background-color & text-color attributes (#2154)
1 parent d2a600c commit 5eca8a8

File tree

4 files changed

+186
-36
lines changed

4 files changed

+186
-36
lines changed

packages/core/src/extensions/Collaboration/schemaMigration/SchemaMigrationPlugin.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { Plugin, PluginKey } from "@tiptap/pm/state";
2-
import { ySyncPluginKey } from "y-prosemirror";
32
import * as Y from "yjs";
43

54
import { BlockNoteExtension } from "../../../editor/BlockNoteExtension.js";
@@ -31,8 +30,12 @@ export class SchemaMigrationPlugin extends BlockNoteExtension {
3130
}
3231

3332
if (
34-
transactions.length !== 1 ||
35-
!transactions[0].getMeta(ySyncPluginKey)
33+
// If any of the transactions are not due to a yjs sync, we don't need to run the migration
34+
!transactions.some((tr) => tr.getMeta("y-sync$")) ||
35+
// If none of the transactions result in a document change, we don't need to run the migration
36+
transactions.every((tr) => !tr.docChanged) ||
37+
// If the fragment is still empty, we can't run the migration (since it has not yet been applied to the Y.Doc)
38+
!fragment.firstChild
3639
) {
3740
return undefined;
3841
}
@@ -44,6 +47,10 @@ export class SchemaMigrationPlugin extends BlockNoteExtension {
4447

4548
this.migrationDone = true;
4649

50+
if (!tr.docChanged) {
51+
return undefined;
52+
}
53+
4754
return tr;
4855
},
4956
}),
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import { expect, it } from "vitest";
2+
import * as Y from "yjs";
3+
import { BlockNoteEditor } from "../../../../editor/BlockNoteEditor.js";
4+
import { moveColorAttributes } from "./moveColorAttributes.js";
5+
import { prosemirrorJSONToYXmlFragment } from "y-prosemirror";
6+
7+
it("can move color attributes on older documents", async () => {
8+
const doc = new Y.Doc();
9+
const fragment = doc.getXmlFragment("doc");
10+
const editor = BlockNoteEditor.create({
11+
initialContent: [
12+
{
13+
type: "paragraph",
14+
content: "Welcome to this demo!",
15+
},
16+
],
17+
});
18+
19+
// Because this was a previous schema, we are creating the YFragment manually
20+
const blockGroup = new Y.XmlElement("blockGroup");
21+
const el = new Y.XmlElement("blockContainer");
22+
el.setAttribute("id", "0");
23+
el.setAttribute("backgroundColor", "red");
24+
el.setAttribute("textColor", "blue");
25+
const para = new Y.XmlElement("paragraph");
26+
para.setAttribute("textAlignment", "left");
27+
para.insert(0, [new Y.XmlText("Welcome to this demo!")]);
28+
el.insert(0, [para]);
29+
blockGroup.insert(0, [el]);
30+
fragment.insert(0, [blockGroup]);
31+
32+
// Note that the blockContainer has the color attributes, but the paragraph does not.
33+
expect(fragment.toJSON()).toMatchInlineSnapshot(
34+
`"<blockgroup><blockcontainer backgroundColor="red" id="0" textColor="blue"><paragraph textAlignment="left">Welcome to this demo!</paragraph></blockcontainer></blockgroup>"`,
35+
);
36+
37+
const tr = editor.prosemirrorState.tr;
38+
moveColorAttributes(fragment, tr);
39+
// Note that the color attributes have been moved to the paragraph.
40+
expect(JSON.stringify(tr.doc.toJSON())).toMatchInlineSnapshot(
41+
`"{"type":"doc","content":[{"type":"blockGroup","content":[{"type":"blockContainer","attrs":{"id":"0"},"content":[{"type":"paragraph","attrs":{"backgroundColor":"red","textColor":"blue","textAlignment":"left"},"content":[{"type":"text","text":"Welcome to this demo!"}]}]}]}]}"`,
42+
);
43+
});
44+
45+
it("does not move color attributes on newer documents", async () => {
46+
const doc = new Y.Doc();
47+
const fragment = doc.getXmlFragment("doc");
48+
const editor = BlockNoteEditor.create({
49+
initialContent: [
50+
{
51+
type: "paragraph",
52+
content: "Welcome to this demo!",
53+
props: {
54+
backgroundColor: "red",
55+
textColor: "blue",
56+
// Set to non-default value to ensure it is not overridden by the migration rule.
57+
textAlignment: "right",
58+
},
59+
},
60+
],
61+
});
62+
63+
prosemirrorJSONToYXmlFragment(
64+
editor.pmSchema,
65+
JSON.parse(JSON.stringify(editor.prosemirrorState.doc.toJSON())),
66+
fragment,
67+
);
68+
69+
expect(fragment.toJSON()).toMatchInlineSnapshot(
70+
// The color attributes are on the paragraph, not the blockContainer.
71+
`"<blockgroup><blockcontainer id="0"><paragraph backgroundColor="red" textAlignment="right" textColor="blue">Welcome to this demo!</paragraph></blockcontainer></blockgroup>"`,
72+
);
73+
74+
const tr = editor.prosemirrorState.tr;
75+
moveColorAttributes(fragment, tr);
76+
// The document will be unchanged because the color attributes are already on the paragraph.
77+
expect(tr.docChanged).toBe(false);
78+
});
79+
80+
it("can move color attributes on older documents multiple times", async () => {
81+
const doc = new Y.Doc();
82+
const fragment = doc.getXmlFragment("doc");
83+
const editor = BlockNoteEditor.create({
84+
initialContent: [
85+
{
86+
type: "paragraph",
87+
content: "Welcome to this demo!",
88+
},
89+
],
90+
});
91+
92+
// Because this was a previous schema, we are creating the YFragment manually
93+
const blockGroup = new Y.XmlElement("blockGroup");
94+
const el = new Y.XmlElement("blockContainer");
95+
el.setAttribute("id", "0");
96+
el.setAttribute("backgroundColor", "red");
97+
el.setAttribute("textColor", "blue");
98+
const para = new Y.XmlElement("paragraph");
99+
para.setAttribute("textAlignment", "left");
100+
para.insert(0, [new Y.XmlText("Welcome to this demo!")]);
101+
el.insert(0, [para]);
102+
blockGroup.insert(0, [el]);
103+
fragment.insert(0, [blockGroup]);
104+
105+
// Note that the blockContainer has the color attributes, but the paragraph does not.
106+
expect(fragment.toJSON()).toMatchInlineSnapshot(
107+
`"<blockgroup><blockcontainer backgroundColor="red" id="0" textColor="blue"><paragraph textAlignment="left">Welcome to this demo!</paragraph></blockcontainer></blockgroup>"`,
108+
);
109+
110+
const tr = editor.prosemirrorState.tr;
111+
moveColorAttributes(fragment, tr);
112+
// Note that the color attributes have been moved to the paragraph.
113+
expect(JSON.stringify(tr.doc.toJSON())).toMatchInlineSnapshot(
114+
`"{"type":"doc","content":[{"type":"blockGroup","content":[{"type":"blockContainer","attrs":{"id":"0"},"content":[{"type":"paragraph","attrs":{"backgroundColor":"red","textColor":"blue","textAlignment":"left"},"content":[{"type":"text","text":"Welcome to this demo!"}]}]}]}]}"`,
115+
);
116+
117+
el.setAttribute("backgroundColor", "green");
118+
el.setAttribute("textColor", "yellow");
119+
120+
expect(fragment.toJSON()).toMatchInlineSnapshot(
121+
`"<blockgroup><blockcontainer backgroundColor="green" id="0" textColor="yellow"><paragraph textAlignment="left">Welcome to this demo!</paragraph></blockcontainer></blockgroup>"`,
122+
);
123+
124+
const nextTr = editor.prosemirrorState.tr;
125+
moveColorAttributes(fragment, nextTr);
126+
// Note that the color attributes have been moved to the paragraph.
127+
expect(JSON.stringify(nextTr.doc.toJSON())).toMatchInlineSnapshot(
128+
`"{"type":"doc","content":[{"type":"blockGroup","content":[{"type":"blockContainer","attrs":{"id":"0"},"content":[{"type":"paragraph","attrs":{"backgroundColor":"green","textColor":"yellow","textAlignment":"left"},"content":[{"type":"text","text":"Welcome to this demo!"}]}]}]}]}"`,
129+
);
130+
});

packages/core/src/extensions/Collaboration/schemaMigration/migrationRules/moveColorAttributes.ts

Lines changed: 34 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,13 @@ const traverseElement = (
2323
export const moveColorAttributes: MigrationRule = (fragment, tr) => {
2424
// Stores necessary info for all `blockContainer` nodes which still have
2525
// `textColor` or `backgroundColor` attributes that need to be moved.
26-
const targetBlockContainers: Record<
26+
const targetBlockContainers: Map<
2727
string,
2828
{
29-
textColor?: string;
30-
backgroundColor?: string;
29+
textColor: string | undefined;
30+
backgroundColor: string | undefined;
3131
}
32-
> = {};
33-
32+
> = new Map();
3433
// Finds all elements which still have `textColor` or `backgroundColor`
3534
// attributes in the current Yjs fragment.
3635
fragment.forEach((element) => {
@@ -40,39 +39,53 @@ export const moveColorAttributes: MigrationRule = (fragment, tr) => {
4039
element.nodeName === "blockContainer" &&
4140
element.hasAttribute("id")
4241
) {
42+
const textColor = element.getAttribute("textColor");
43+
const backgroundColor = element.getAttribute("backgroundColor");
44+
4345
const colors = {
44-
textColor: element.getAttribute("textColor"),
45-
backgroundColor: element.getAttribute("backgroundColor"),
46+
textColor:
47+
textColor === defaultProps.textColor.default
48+
? undefined
49+
: textColor,
50+
backgroundColor:
51+
backgroundColor === defaultProps.backgroundColor.default
52+
? undefined
53+
: backgroundColor,
4654
};
4755

48-
if (colors.textColor === defaultProps.textColor.default) {
49-
colors.textColor = undefined;
50-
}
51-
if (colors.backgroundColor === defaultProps.backgroundColor.default) {
52-
colors.backgroundColor = undefined;
53-
}
54-
5556
if (colors.textColor || colors.backgroundColor) {
56-
targetBlockContainers[element.getAttribute("id")!] = colors;
57+
targetBlockContainers.set(element.getAttribute("id")!, colors);
5758
}
5859
}
5960
});
6061
}
6162
});
6263

64+
if (targetBlockContainers.size === 0) {
65+
return false;
66+
}
67+
6368
// Appends transactions to add the `textColor` and `backgroundColor`
6469
// attributes found on each `blockContainer` node to move them to the child
6570
// `blockContent` node.
6671
tr.doc.descendants((node, pos) => {
6772
if (
6873
node.type.name === "blockContainer" &&
69-
targetBlockContainers[node.attrs.id]
74+
targetBlockContainers.has(node.attrs.id)
7075
) {
71-
tr = tr.setNodeMarkup(
72-
pos + 1,
73-
undefined,
74-
targetBlockContainers[node.attrs.id],
75-
);
76+
const el = tr.doc.nodeAt(pos + 1);
77+
if (!el) {
78+
throw new Error("No element found");
79+
}
80+
81+
tr.setNodeMarkup(pos + 1, undefined, {
82+
// preserve existing attributes
83+
...el.attrs,
84+
// add the textColor and backgroundColor attributes
85+
...targetBlockContainers.get(node.attrs.id),
86+
});
7687
}
7788
});
89+
90+
return true;
7891
};

pnpm-lock.yaml

Lines changed: 12 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)