Document-level schema versioning.

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>
This commit is contained in:
Mark Tolmacs
2026-07-24 19:44:19 +02:00
parent 26f5a53eb3
commit 14b261f9df
37 changed files with 721 additions and 668 deletions
+7
View File
@@ -55,6 +55,7 @@ import {
} from "@excalidraw/excalidraw/data/restore";
import { newElementWith } from "@excalidraw/element";
import { isInitializedImageElement } from "@excalidraw/element";
import { CURRENT_SCHEMA_VERSION, getSchemaVersion } from "@excalidraw/element";
import clsx from "clsx";
import {
parseLibraryTokensFromUrl,
@@ -241,6 +242,7 @@ const initializeScene = async (opts: {
elements: restoreElements(localDataState?.elements, null, {
repairBindings: true,
deleteInvisibleElements: true,
schemaVersion: localDataState?.schemaVersion,
}),
appState: restoreAppState(localDataState?.appState, null),
};
@@ -267,6 +269,7 @@ const initializeScene = async (opts: {
restoreElements(imported.elements, null, {
repairBindings: true,
deleteInvisibleElements: true,
schemaVersion: getSchemaVersion(imported),
}),
localDataState?.elements,
),
@@ -545,8 +548,12 @@ const ExcalidrawWrapper = () => {
loadImages(data);
if (data.scene) {
excalidrawAPI.updateScene({
// NOTE: scene elements passed through restore during
// initialization, so they are already at the current
// schema version
elements: restoreElements(data.scene.elements, null, {
repairBindings: true,
schemaVersion: CURRENT_SCHEMA_VERSION,
}),
appState: restoreAppState(data.scene.appState, null),
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
+1
View File
@@ -38,6 +38,7 @@ export const ROOM_ID_BYTES = 10;
export const STORAGE_KEYS = {
LOCAL_STORAGE_ELEMENTS: "excalidraw",
LOCAL_STORAGE_SCHEMA_VERSION: "excalidraw-schema-version",
LOCAL_STORAGE_APP_STATE: "excalidraw-state",
LOCAL_STORAGE_COLLAB: "excalidraw-collab",
LOCAL_STORAGE_THEME: "excalidraw-theme",
+19 -3
View File
@@ -22,6 +22,7 @@ import { decryptData } from "@excalidraw/excalidraw/data/encryption";
import { getVisibleSceneBounds } from "@excalidraw/element";
import { newElementWith } from "@excalidraw/element";
import { isImageElement, isInitializedImageElement } from "@excalidraw/element";
import { CURRENT_SCHEMA_VERSION, getSchemaVersion } from "@excalidraw/element";
import { AbortError } from "@excalidraw/excalidraw/errors";
import { t } from "@excalidraw/excalidraw/i18n";
import { withBatchedUpdates } from "@excalidraw/excalidraw/reactUtils";
@@ -587,8 +588,10 @@ class Collab extends PureComponent<CollabProps, CollabState> {
const remoteElements = toBrandedType<
readonly RemoteExcalidrawElement[]
>(decryptedData.payload.elements);
const reconciledElements =
this._reconcileElements(remoteElements);
const reconciledElements = this._reconcileElements(
remoteElements,
getSchemaVersion(decryptedData.payload),
);
this.handleRemoteSceneUpdate(reconciledElements);
// noop if already resolved via init from firebase
scenePromise.resolve({
@@ -604,6 +607,7 @@ class Collab extends PureComponent<CollabProps, CollabState> {
toBrandedType<readonly RemoteExcalidrawElement[]>(
decryptedData.payload.elements,
),
getSchemaVersion(decryptedData.payload),
),
);
break;
@@ -753,15 +757,27 @@ class Collab extends PureComponent<CollabProps, CollabState> {
private _reconcileElements = (
remoteElements: readonly RemoteExcalidrawElement[],
remoteSchemaVersion: number = 0,
): ReconciledExcalidrawElement[] => {
const appState = this.excalidrawAPI.getAppState();
const existingElements = this.getSceneElementsIncludingDeleted();
if (remoteSchemaVersion > CURRENT_SCHEMA_VERSION) {
// NOTE: remote client is newer than us; we can't downgrade its elements,
// so restore as-is and rely on unknown properties being preserved
console.warn(
`Received collab update with schema version ${remoteSchemaVersion} ` +
`(local: ${CURRENT_SCHEMA_VERSION})`,
);
}
// NOTE ideally we restore _after_ reconciliation but we can't do that
// as we'd regenerate even elements such as appState.newElement which would
// break the state
remoteElements = restoreElements(remoteElements, existingElements);
remoteElements = restoreElements(remoteElements, existingElements, {
schemaVersion: remoteSchemaVersion,
});
let reconciledElements = reconcileElements(
existingElements,
+2
View File
@@ -2,6 +2,7 @@ import { CaptureUpdateAction } from "@excalidraw/excalidraw";
import { trackEvent } from "@excalidraw/excalidraw/analytics";
import { encryptData } from "@excalidraw/excalidraw/data/encryption";
import { newElementWith } from "@excalidraw/element";
import { CURRENT_SCHEMA_VERSION } from "@excalidraw/element";
import throttle from "lodash.throttle";
import type { UserIdleState } from "@excalidraw/common";
@@ -167,6 +168,7 @@ class Portal {
type: updateType,
payload: {
elements: syncableElements,
schemaVersion: CURRENT_SCHEMA_VERSION,
},
};
+5
View File
@@ -27,6 +27,7 @@ import {
} from "idb-keyval";
import { getNonDeletedElements } from "@excalidraw/element";
import { CURRENT_SCHEMA_VERSION } from "@excalidraw/element";
import type { LibraryPersistedData } from "@excalidraw/excalidraw/data/library";
import type { ImportedDataState } from "@excalidraw/excalidraw/data/types";
@@ -91,6 +92,10 @@ const saveDataStateToLocalStorage = (
STORAGE_KEYS.LOCAL_STORAGE_ELEMENTS,
JSON.stringify(getNonDeletedElements(elements)),
);
localStorage.setItem(
STORAGE_KEYS.LOCAL_STORAGE_SCHEMA_VERSION,
JSON.stringify(CURRENT_SCHEMA_VERSION),
);
localStorage.setItem(
STORAGE_KEYS.LOCAL_STORAGE_APP_STATE,
JSON.stringify(_appState),
+10 -2
View File
@@ -7,6 +7,7 @@ import {
} from "@excalidraw/excalidraw/data/encryption";
import { restoreElements } from "@excalidraw/excalidraw/data/restore";
import { getSceneVersion } from "@excalidraw/element";
import { CURRENT_SCHEMA_VERSION, getSchemaVersion } from "@excalidraw/element";
import { initializeApp } from "firebase/app";
import {
getFirestore,
@@ -86,6 +87,7 @@ export const loadFirebaseStorage = async () => {
type FirebaseStoredScene = {
sceneVersion: number;
schemaVersion?: number;
iv: Bytes;
ciphertext: Bytes;
};
@@ -179,6 +181,7 @@ const createFirebaseSceneDocument = async (
const { ciphertext, iv } = await encryptElements(roomKey, elements);
return {
sceneVersion,
schemaVersion: CURRENT_SCHEMA_VERSION,
ciphertext: Bytes.fromUint8Array(new Uint8Array(ciphertext)),
iv: Bytes.fromUint8Array(iv),
} as FirebaseStoredScene;
@@ -216,7 +219,9 @@ export const saveToFirebase = async (
const prevStoredScene = snapshot.data() as FirebaseStoredScene;
const prevStoredElements = getSyncableElements(
restoreElements(await decryptElements(prevStoredScene, roomKey), null),
restoreElements(await decryptElements(prevStoredScene, roomKey), null, {
schemaVersion: getSchemaVersion(prevStoredScene),
}),
);
const reconciledElements = getSyncableElements(
reconcileElements(
@@ -238,7 +243,9 @@ export const saveToFirebase = async (
});
const storedElements = getSyncableElements(
restoreElements(await decryptElements(storedScene, roomKey), null),
restoreElements(await decryptElements(storedScene, roomKey), null, {
schemaVersion: getSchemaVersion(storedScene),
}),
);
FirebaseSceneVersionCache.set(socket, storedElements);
@@ -261,6 +268,7 @@ export const loadFromFirebase = async (
const elements = getSyncableElements(
restoreElements(await decryptElements(storedScene, roomKey), null, {
deleteInvisibleElements: true,
schemaVersion: getSchemaVersion(storedScene),
}),
);
+5
View File
@@ -10,6 +10,7 @@ import {
import { serializeAsJSON } from "@excalidraw/excalidraw/data/json";
import { isInvisiblySmallElement } from "@excalidraw/element";
import { isInitializedImageElement } from "@excalidraw/element";
import { getSchemaVersion } from "@excalidraw/element";
import { t } from "@excalidraw/excalidraw/i18n";
import { bytesToHexString } from "@excalidraw/common";
@@ -84,12 +85,14 @@ export type SocketUpdateDataSource = {
type: WS_SUBTYPES.INIT;
payload: {
elements: readonly OrderedExcalidrawElement[];
schemaVersion?: number;
};
};
SCENE_UPDATE: {
type: WS_SUBTYPES.UPDATE;
payload: {
elements: readonly OrderedExcalidrawElement[];
schemaVersion?: number;
};
};
MOUSE_LOCATION: {
@@ -196,6 +199,7 @@ const legacy_decodeFromBackend = async ({
return {
elements: data.elements || null,
appState: data.appState || null,
schemaVersion: getSchemaVersion(data),
};
};
@@ -226,6 +230,7 @@ export const importFromBackend = async (
return {
elements: data.elements || null,
appState: data.appState || null,
schemaVersion: getSchemaVersion(data),
};
} catch (error: any) {
console.warn(
+18 -1
View File
@@ -3,6 +3,8 @@ import {
getDefaultAppState,
} from "@excalidraw/excalidraw/appState";
import { getSchemaVersion } from "@excalidraw/element";
import type { ExcalidrawElement } from "@excalidraw/element/types";
import type { AppState } from "@excalidraw/excalidraw/types";
@@ -37,10 +39,14 @@ export const importUsernameFromLocalStorage = (): string | null => {
export const importFromLocalStorage = () => {
let savedElements = null;
let savedState = null;
let savedSchemaVersion = null;
try {
savedElements = localStorage.getItem(STORAGE_KEYS.LOCAL_STORAGE_ELEMENTS);
savedState = localStorage.getItem(STORAGE_KEYS.LOCAL_STORAGE_APP_STATE);
savedSchemaVersion = localStorage.getItem(
STORAGE_KEYS.LOCAL_STORAGE_SCHEMA_VERSION,
);
} catch (error: any) {
// Unable to access localStorage
console.error(error);
@@ -56,6 +62,17 @@ export const importFromLocalStorage = () => {
}
}
let schemaVersion: number | undefined;
if (savedSchemaVersion) {
try {
schemaVersion = getSchemaVersion({
schemaVersion: JSON.parse(savedSchemaVersion),
});
} catch (error: any) {
console.error(error);
}
}
let appState = null;
if (savedState) {
try {
@@ -70,7 +87,7 @@ export const importFromLocalStorage = () => {
// Do nothing because appState is already null
}
}
return { elements, appState };
return { elements, appState, schemaVersion };
};
export const getElementsStorageSize = () => {
@@ -27,7 +27,6 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
"opacity": 100,
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": Any<Number>,
"strokeColor": "#66a80f",
"strokeStyle": "solid",
@@ -65,7 +64,6 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
"opacity": 100,
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": Any<Number>,
"strokeColor": "#9c36b5",
"strokeStyle": "solid",
@@ -118,7 +116,6 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
],
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": Any<Number>,
"startArrowhead": null,
"startBinding": {
@@ -180,7 +177,6 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
],
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": Any<Number>,
"startArrowhead": null,
"startBinding": {
@@ -227,7 +223,6 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
"opacity": 100,
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": Any<Number>,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
@@ -271,7 +266,6 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
"originalText": "HEYYYYY",
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": Any<Number>,
"strokeColor": "#c2255c",
"strokeStyle": "solid",
@@ -318,7 +312,6 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
"originalText": "Whats up ?",
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": Any<Number>,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
@@ -379,7 +372,6 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
],
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": Any<Number>,
"startArrowhead": null,
"startBinding": {
@@ -427,7 +419,6 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
"originalText": "HELLO WORLD!!",
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": Any<Number>,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
@@ -488,7 +479,6 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
],
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": Any<Number>,
"startArrowhead": null,
"startBinding": {
@@ -536,7 +526,6 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
"originalText": "HELLO WORLD!!",
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": Any<Number>,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
@@ -577,7 +566,6 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
"opacity": 100,
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": Any<Number>,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
@@ -615,7 +603,6 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
"opacity": 100,
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": Any<Number>,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
@@ -673,7 +660,6 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
],
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": Any<Number>,
"startArrowhead": null,
"startBinding": {
@@ -721,7 +707,6 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
"originalText": "HELLO WORLD!!",
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": Any<Number>,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
@@ -768,7 +753,6 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
"originalText": "HEYYYYY",
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": Any<Number>,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
@@ -815,7 +799,6 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
"originalText": "WHATS UP ?",
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": Any<Number>,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
@@ -851,7 +834,6 @@ exports[`Test Transform > should not allow duplicate ids 1`] = `
"opacity": 100,
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": Any<Number>,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
@@ -897,7 +879,6 @@ exports[`Test Transform > should transform linear elements 1`] = `
],
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": Any<Number>,
"startArrowhead": null,
"startBinding": null,
@@ -945,7 +926,6 @@ exports[`Test Transform > should transform linear elements 2`] = `
],
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": Any<Number>,
"startArrowhead": "dot",
"startBinding": null,
@@ -993,7 +973,6 @@ exports[`Test Transform > should transform linear elements 3`] = `
"polygon": false,
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": Any<Number>,
"startArrowhead": null,
"startBinding": null,
@@ -1041,7 +1020,6 @@ exports[`Test Transform > should transform linear elements 4`] = `
"polygon": false,
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": Any<Number>,
"startArrowhead": null,
"startBinding": null,
@@ -1076,7 +1054,6 @@ exports[`Test Transform > should transform regular shapes 1`] = `
"opacity": 100,
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": Any<Number>,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
@@ -1109,7 +1086,6 @@ exports[`Test Transform > should transform regular shapes 2`] = `
"opacity": 100,
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": Any<Number>,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
@@ -1142,7 +1118,6 @@ exports[`Test Transform > should transform regular shapes 3`] = `
"opacity": 100,
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": Any<Number>,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
@@ -1175,7 +1150,6 @@ exports[`Test Transform > should transform regular shapes 4`] = `
"opacity": 100,
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": Any<Number>,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
@@ -1208,7 +1182,6 @@ exports[`Test Transform > should transform regular shapes 5`] = `
"opacity": 100,
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": Any<Number>,
"strokeColor": "#1e1e1e",
"strokeStyle": "dotted",
@@ -1241,7 +1214,6 @@ exports[`Test Transform > should transform regular shapes 6`] = `
"opacity": 100,
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": Any<Number>,
"strokeColor": "#1971c2",
"strokeStyle": "dashed",
@@ -1280,7 +1252,6 @@ exports[`Test Transform > should transform text element 1`] = `
"originalText": "HELLO WORLD!",
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": Any<Number>,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
@@ -1322,7 +1293,6 @@ exports[`Test Transform > should transform text element 2`] = `
"originalText": "STYLED HELLO WORLD!",
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": Any<Number>,
"strokeColor": "#5f3dc4",
"strokeStyle": "solid",
@@ -1369,7 +1339,6 @@ exports[`Test Transform > should transform the elements correctly when linear el
"opacity": 100,
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": Any<Number>,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
@@ -1409,7 +1378,6 @@ exports[`Test Transform > should transform the elements correctly when linear el
"opacity": 100,
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": Any<Number>,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
@@ -1453,7 +1421,6 @@ exports[`Test Transform > should transform the elements correctly when linear el
"opacity": 100,
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": Any<Number>,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
@@ -1501,7 +1468,6 @@ exports[`Test Transform > should transform the elements correctly when linear el
"opacity": 100,
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": Any<Number>,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
@@ -1561,7 +1527,6 @@ exports[`Test Transform > should transform the elements correctly when linear el
"roundness": {
"type": 2,
},
"schemaVersion": 1,
"seed": Any<Number>,
"startArrowhead": null,
"startBinding": {
@@ -1626,7 +1591,6 @@ exports[`Test Transform > should transform the elements correctly when linear el
"roundness": {
"type": 2,
},
"schemaVersion": 1,
"seed": Any<Number>,
"startArrowhead": null,
"startBinding": {
@@ -1676,7 +1640,6 @@ exports[`Test Transform > should transform the elements correctly when linear el
"originalText": "B",
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": Any<Number>,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
@@ -1720,7 +1683,6 @@ exports[`Test Transform > should transform the elements correctly when linear el
"originalText": "A",
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": Any<Number>,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
@@ -1764,7 +1726,6 @@ exports[`Test Transform > should transform the elements correctly when linear el
"originalText": "Alice",
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": Any<Number>,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
@@ -1808,7 +1769,6 @@ exports[`Test Transform > should transform the elements correctly when linear el
"originalText": "Bob",
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": Any<Number>,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
@@ -1850,7 +1810,6 @@ exports[`Test Transform > should transform the elements correctly when linear el
"originalText": "How are you?",
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": Any<Number>,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
@@ -1892,7 +1851,6 @@ exports[`Test Transform > should transform the elements correctly when linear el
"originalText": "Friendship",
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": Any<Number>,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
@@ -1946,7 +1904,6 @@ exports[`Test Transform > should transform to labelled arrows when label provide
],
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": Any<Number>,
"startArrowhead": null,
"startBinding": null,
@@ -1999,7 +1956,6 @@ exports[`Test Transform > should transform to labelled arrows when label provide
],
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": Any<Number>,
"startArrowhead": null,
"startBinding": null,
@@ -2052,7 +2008,6 @@ exports[`Test Transform > should transform to labelled arrows when label provide
],
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": Any<Number>,
"startArrowhead": null,
"startBinding": null,
@@ -2105,7 +2060,6 @@ exports[`Test Transform > should transform to labelled arrows when label provide
],
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": Any<Number>,
"startArrowhead": null,
"startBinding": null,
@@ -2146,7 +2100,6 @@ exports[`Test Transform > should transform to labelled arrows when label provide
"originalText": "LABELED ARROW",
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": Any<Number>,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
@@ -2188,7 +2141,6 @@ exports[`Test Transform > should transform to labelled arrows when label provide
"originalText": "STYLED LABELED ARROW",
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": Any<Number>,
"strokeColor": "#099268",
"strokeStyle": "solid",
@@ -2230,7 +2182,6 @@ exports[`Test Transform > should transform to labelled arrows when label provide
"originalText": "ANOTHER STYLED LABELLED ARROW",
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": Any<Number>,
"strokeColor": "#1098ad",
"strokeStyle": "solid",
@@ -2273,7 +2224,6 @@ exports[`Test Transform > should transform to labelled arrows when label provide
"originalText": "ANOTHER STYLED LABELLED ARROW",
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": Any<Number>,
"strokeColor": "#099268",
"strokeStyle": "solid",
@@ -2315,7 +2265,6 @@ exports[`Test Transform > should transform to text containers when label provide
"opacity": 100,
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": Any<Number>,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
@@ -2353,7 +2302,6 @@ exports[`Test Transform > should transform to text containers when label provide
"opacity": 100,
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": Any<Number>,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
@@ -2391,7 +2339,6 @@ exports[`Test Transform > should transform to text containers when label provide
"opacity": 100,
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": Any<Number>,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
@@ -2429,7 +2376,6 @@ exports[`Test Transform > should transform to text containers when label provide
"opacity": 100,
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": Any<Number>,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
@@ -2467,7 +2413,6 @@ exports[`Test Transform > should transform to text containers when label provide
"opacity": 100,
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": Any<Number>,
"strokeColor": "#c2255c",
"strokeStyle": "solid",
@@ -2505,7 +2450,6 @@ exports[`Test Transform > should transform to text containers when label provide
"opacity": 100,
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": Any<Number>,
"strokeColor": "#f08c00",
"strokeStyle": "solid",
@@ -2544,7 +2488,6 @@ exports[`Test Transform > should transform to text containers when label provide
"originalText": "RECTANGLE TEXT CONTAINER",
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": Any<Number>,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
@@ -2586,7 +2529,6 @@ exports[`Test Transform > should transform to text containers when label provide
"originalText": "ELLIPSE TEXT CONTAINER",
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": Any<Number>,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
@@ -2630,7 +2572,6 @@ exports[`Test Transform > should transform to text containers when label provide
TEXT CONTAINER",
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": Any<Number>,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
@@ -2674,7 +2615,6 @@ exports[`Test Transform > should transform to text containers when label provide
"originalText": "STYLED DIAMOND TEXT CONTAINER",
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": Any<Number>,
"strokeColor": "#099268",
"strokeStyle": "solid",
@@ -2717,7 +2657,6 @@ exports[`Test Transform > should transform to text containers when label provide
"originalText": "TOP LEFT ALIGNED RECTANGLE TEXT CONTAINER",
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": Any<Number>,
"strokeColor": "#c2255c",
"strokeStyle": "solid",
@@ -2761,7 +2700,6 @@ exports[`Test Transform > should transform to text containers when label provide
"originalText": "STYLED ELLIPSE TEXT CONTAINER",
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": Any<Number>,
"strokeColor": "#c2255c",
"strokeStyle": "solid",
@@ -1,72 +1,88 @@
import {
CURRENT_ELEMENT_SCHEMA_VERSION,
getElementSchemaVersion,
upgradeElementSchema,
CURRENT_SCHEMA_VERSION,
getSchemaVersion,
migrateElement,
migrateElementPartial,
migrateElements,
} from "../versioning";
describe("element schema versioning", () => {
describe("getElementSchemaVersion", () => {
it("treats an element without schemaVersion as legacy (version 0)", () => {
expect(getElementSchemaVersion({})).toBe(0);
expect(getElementSchemaVersion({ schemaVersion: undefined })).toBe(0);
describe("scene schema versioning", () => {
describe("getSchemaVersion", () => {
it("treats a container without schemaVersion as legacy (version 0)", () => {
expect(getSchemaVersion({})).toBe(0);
expect(getSchemaVersion({ schemaVersion: undefined })).toBe(0);
expect(getSchemaVersion(null)).toBe(0);
expect(getSchemaVersion(undefined)).toBe(0);
});
it("returns the element's schemaVersion when present", () => {
expect(getElementSchemaVersion({ schemaVersion: 1 })).toBe(1);
expect(getElementSchemaVersion({ schemaVersion: 3 })).toBe(3);
it("treats an invalid schemaVersion as legacy (version 0)", () => {
expect(getSchemaVersion({ schemaVersion: "2" })).toBe(0);
expect(getSchemaVersion({ schemaVersion: NaN })).toBe(0);
expect(getSchemaVersion({ schemaVersion: Infinity })).toBe(0);
});
it("returns the container's schemaVersion when present", () => {
expect(getSchemaVersion({ schemaVersion: 1 })).toBe(1);
expect(getSchemaVersion({ schemaVersion: 3 })).toBe(3);
});
});
describe("upgradeElementSchema", () => {
it("lifts a legacy element to the current schema version", () => {
const legacy = { id: "a", type: "rectangle", backgroundColor: "#fff" };
describe("migrateElements", () => {
it("lifts legacy elements through the whole migration chain", () => {
const legacy = [{ id: "a", type: "rectangle", backgroundColor: "#fff" }];
const upgraded = upgradeElementSchema(legacy);
const migrated = migrateElements(legacy, 0);
expect(upgraded.schemaVersion).toBe(CURRENT_ELEMENT_SCHEMA_VERSION);
// v0 -> v1 migration keeps the element shape intact
expect(migrated).toEqual(legacy);
});
it("preserves all other properties when upgrading", () => {
const legacy = { id: "a", type: "rectangle", backgroundColor: "#fff" };
it("returns the input array unchanged when already current", () => {
const elements = [{ id: "a", type: "rectangle" }];
const upgraded = upgradeElementSchema(legacy);
expect(upgraded).toEqual({
...legacy,
schemaVersion: CURRENT_ELEMENT_SCHEMA_VERSION,
});
expect(migrateElements(elements, CURRENT_SCHEMA_VERSION)).toBe(elements);
});
it("is idempotent on an already-current element", () => {
const current = {
id: "a",
type: "rectangle",
schemaVersion: CURRENT_ELEMENT_SCHEMA_VERSION,
};
it("returns the input array unchanged when coming from a newer client", () => {
const elements = [{ id: "a", type: "rectangle" }];
const upgraded = upgradeElementSchema(current);
expect(upgraded).toEqual(current);
});
it("does not mutate the input element", () => {
const legacy = { id: "a", type: "rectangle" };
upgradeElementSchema(legacy);
expect(legacy).not.toHaveProperty("schemaVersion");
});
// Template for future N -> N+1 migrations. When a real migration is added
// (e.g. repurposing an attribute at schema version 2), assert here that a
// legacy element is transformed as expected end-to-end. Kept minimal on
// purpose so it documents the pattern without asserting fake behavior.
it("runs migrations in sequence up to the current version", () => {
const upgraded = upgradeElementSchema({ id: "a", schemaVersion: 0 });
expect(getElementSchemaVersion(upgraded)).toBe(
CURRENT_ELEMENT_SCHEMA_VERSION,
expect(migrateElements(elements, CURRENT_SCHEMA_VERSION + 1)).toBe(
elements,
);
});
});
describe("migrateElement", () => {
it("does not mutate the input element", () => {
const legacy = { id: "a", type: "rectangle" };
const migrated = migrateElement(legacy, 0);
expect(migrated).toEqual({ id: "a", type: "rectangle" });
expect(legacy).toEqual({ id: "a", type: "rectangle" });
});
});
describe("migrateElementPartial", () => {
it("lifts a legacy delta partial to the current schema version", () => {
const partial = { backgroundColor: "#fff", version: 2 };
expect(migrateElementPartial(partial, 0)).toEqual(partial);
});
it("returns the partial unchanged when already current", () => {
const partial = { backgroundColor: "#fff" };
expect(migrateElementPartial(partial, CURRENT_SCHEMA_VERSION)).toBe(
partial,
);
});
});
// Template for future migrations
it("runs migrations in sequence up to the current version", () => {
const migrated = migrateElement({ id: "a" }, 0);
expect(migrated).toEqual({ id: "a" });
});
});
+28 -2
View File
@@ -36,6 +36,8 @@ import type {
import { getObservedAppState } from "./store";
import { CURRENT_SCHEMA_VERSION, migrateElementPartial } from "./versioning";
import {
BoundElement,
BindableElement,
@@ -1084,9 +1086,33 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
return delta;
}
public static restore(elementsDeltaDTO: DTO<ElementsDelta>): ElementsDelta {
public static restore(
elementsDeltaDTO: DTO<ElementsDelta>,
schemaVersion: number = CURRENT_SCHEMA_VERSION,
): ElementsDelta {
const { added, removed, updated } = elementsDeltaDTO;
return ElementsDelta.create(added, removed, updated);
return ElementsDelta.create(
ElementsDelta.migratePartialDeltas(added, schemaVersion),
ElementsDelta.migratePartialDeltas(removed, schemaVersion),
ElementsDelta.migratePartialDeltas(updated, schemaVersion),
);
}
private static migratePartialDeltas(
deltas: Record<string, Delta<ElementPartial>>,
fromVersion: number,
): Record<string, Delta<ElementPartial>> {
if (fromVersion >= CURRENT_SCHEMA_VERSION) {
return deltas;
}
return Object.entries(deltas).reduce((acc, [id, delta]) => {
acc[id] = Delta.create(
migrateElementPartial(delta.deleted, fromVersion),
migrateElementPartial(delta.inserted, fromVersion),
);
return acc;
}, {} as Record<string, Delta<ElementPartial>>);
}
private static satisfiesAddition = ({
-3
View File
@@ -27,7 +27,6 @@ import { normalizeText, measureText } from "./textMeasurements";
import { wrapText } from "./textWrapping";
import { isLineElement } from "./typeChecks";
import { CURRENT_ELEMENT_SCHEMA_VERSION } from "./versioning";
import type {
ExcalidrawElement,
@@ -64,7 +63,6 @@ export type ElementConstructorOpts = MarkOptional<
| "seed"
| "version"
| "versionNonce"
| "schemaVersion"
| "link"
| "strokeStyle"
| "fillStyle"
@@ -151,7 +149,6 @@ const _newElementBase = <T extends ExcalidrawElement>(
seed: rest.seed ?? randomInteger(),
version: rest.version || 1,
versionNonce: rest.versionNonce ?? 0,
schemaVersion: rest.schemaVersion ?? CURRENT_ELEMENT_SCHEMA_VERSION,
isDeleted: false as false,
boundElements,
updated: getUpdatedTimestamp(),
+18 -10
View File
@@ -19,6 +19,8 @@ import { newElementWith } from "./mutateElement";
import { ElementsDelta, AppStateDelta, Delta } from "./delta";
import { CURRENT_SCHEMA_VERSION, getSchemaVersion } from "./versioning";
import {
syncInvalidIndicesImmutable,
hashElementsVersion,
@@ -499,6 +501,7 @@ export class StoreDelta {
public readonly id: string,
public readonly elements: ElementsDelta,
public readonly appState: AppStateDelta,
public readonly schemaVersion: number = CURRENT_SCHEMA_VERSION,
) {}
/**
@@ -513,7 +516,7 @@ export class StoreDelta {
id: randomId(),
},
) {
return new this(opts.id, elements, appState);
return new this(opts.id, elements, appState, CURRENT_SCHEMA_VERSION);
}
/**
@@ -535,26 +538,31 @@ export class StoreDelta {
}
/**
* Restore a store delta instance from a DTO.
* Restore a store delta instance from a DTO, lifting the contained element
* partials to the current schema version.
*/
public static restore(storeDeltaDTO: DTO<StoreDelta>) {
const { id, elements, appState } = storeDeltaDTO;
return new this(
id,
ElementsDelta.restore(elements),
ElementsDelta.restore(elements, getSchemaVersion(storeDeltaDTO)),
AppStateDelta.restore(appState),
);
}
/**
* Parse and load the delta from the remote payload.
* Parse and load the delta from the remote payload, lifting the contained
* element partials to the current schema version.
*/
public static load({
id,
elements: { added, removed, updated },
appState: { delta: appStateDelta },
}: DTO<StoreDelta>) {
const elements = ElementsDelta.create(added, removed, updated);
public static load(storeDeltaDTO: DTO<StoreDelta>) {
const {
id,
appState: { delta: appStateDelta },
} = storeDeltaDTO;
const elements = ElementsDelta.restore(
storeDeltaDTO.elements,
getSchemaVersion(storeDeltaDTO),
);
const appState = AppStateDelta.create(appStateDelta);
return new this(id, elements, appState);
-5
View File
@@ -62,11 +62,6 @@ type _ExcalidrawElementBase = Readonly<{
Used for deterministic reconciliation of updates during collaboration,
in case the versions (see above) are identical. */
versionNonce: number;
/** Schema version of the element's shape/semantics. Unrelated to `version`
(the collab/reconciliation counter). Legacy elements persisted before
schema versioning lack it and are lifted to the current version on
restore. See `CURRENT_ELEMENT_SCHEMA_VERSION`. */
schemaVersion: number;
/** String in a fractional form defined by https://github.com/rocicorp/fractional-indexing.
Used for ordering in multiplayer scenarios, such as during reconciliation or undo / redo.
Always kept in sync with the array order by `syncMovedIndices` and `syncInvalidIndices`.
+69 -39
View File
@@ -1,57 +1,87 @@
/**
* The schema version that the current in-memory element shape corresponds to.
* The schema version the current in-memory element shape corresponds to.
*/
export const CURRENT_ELEMENT_SCHEMA_VERSION = 1;
export const CURRENT_SCHEMA_VERSION = 1;
type SchemaMigration = {
// Upgrades a full element from schema version n to n + 1.
element: (element: any) => any;
// Upgrades a partial element (durable undo/redo).
elementPartial: (partial: any) => any;
};
const identity = <T>(value: T): T => value;
/**
* A migration that upgrades an element from one schema version to the next.
*/
type ElementSchemaMigration = (element: any) => any;
/**
* `migrations[n]` upgrades an element from schema version `n` to `n + 1`.
* migrations[n] upgrades data from schema version to n + 1.
*
* Index `0` migrates legacy (unversioned) elements to version 1.
* NOTE: Index 0 migrates legacy (unversioned) data to version 1.
*
* IMPORTANT: data of unknown provenance (API input, clipboard from older
* builds etc.) is considered 'legacy' v0!
*/
const migrations: Record<number, ElementSchemaMigration> = {
0: (element) => element,
const migrations: Record<number, SchemaMigration> = {
0: { element: identity, elementPartial: identity },
};
/**
* Returns an element's schema version, treating a missing `schemaVersion` (i.e.
* a legacy element persisted before schema versioning) as version 0.
* Returns the schema version indicated by a container.
*/
export const getElementSchemaVersion = (element: {
schemaVersion?: number;
}): number =>
typeof element.schemaVersion === "number" ? element.schemaVersion : 0;
export const getSchemaVersion = (source: unknown): number => {
const schemaVersion = (source as { schemaVersion?: unknown } | null)
?.schemaVersion;
/**
* Lifts an element up to {@link CURRENT_ELEMENT_SCHEMA_VERSION} by running each
* migration in sequence, then stamps the current schema version onto it.
*
* Safe to call on an already-current element (the migration chain is empty and
* it is simply re-stamped). If a migration step is missing (which shouldn't
* happen), it stops early rather than throwing, so a partially-known element is
* still returned rather than lost.
*/
export const upgradeElementSchema = <T>(
element: T,
): T & { schemaVersion: number } => {
let version = getElementSchemaVersion(element as { schemaVersion?: number });
let upgraded: any = element;
return typeof schemaVersion === "number" && Number.isFinite(schemaVersion)
? schemaVersion
: 0;
};
while (version < CURRENT_ELEMENT_SCHEMA_VERSION) {
const migrate = migrations[version];
if (!migrate) {
const runMigrations = <T>(
kind: keyof SchemaMigration,
value: T,
fromVersion: number,
): T => {
let version = fromVersion;
let migrated: any = value;
// NOTE: data from a newer client (fromVersion > current) is returned as-is;
// unknown properties are preserved down the line for forward-compatibility
while (version < CURRENT_SCHEMA_VERSION) {
const migration = migrations[version];
if (!migration) {
break;
}
upgraded = migrate(upgraded);
migrated = migration[kind](migrated);
version += 1;
}
return {
...upgraded,
schemaVersion: CURRENT_ELEMENT_SCHEMA_VERSION,
};
return migrated;
};
/**
* Lifts a full element from `fromVersion` up to
* {@link CURRENT_SCHEMA_VERSION} by running each migration in sequence.
*/
export const migrateElement = <T>(element: T, fromVersion: number): T =>
runMigrations("element", element, fromVersion);
/**
* Lifts all elements of a container from `fromVersion` up to
* {@link CURRENT_SCHEMA_VERSION}.
*/
export const migrateElements = <T>(
elements: readonly T[],
fromVersion: number,
): readonly T[] =>
fromVersion >= CURRENT_SCHEMA_VERSION
? elements
: elements.map((element) => migrateElement(element, fromVersion));
/**
* Lifts a partial element shape (as contained in deltas) from `fromVersion`
* up to {@link CURRENT_SCHEMA_VERSION}.
*/
export const migrateElementPartial = <T>(partial: T, fromVersion: number): T =>
fromVersion >= CURRENT_SCHEMA_VERSION
? partial
: runMigrations("elementPartial", partial, fromVersion);
+5
View File
@@ -10,6 +10,7 @@ import {
import { mutateElement } from "@excalidraw/element";
import { deepCopyElement } from "@excalidraw/element";
import { CURRENT_SCHEMA_VERSION, getSchemaVersion } from "@excalidraw/element";
import {
isFrameLikeElement,
isInitializedImageElement,
@@ -38,6 +39,7 @@ import type { BinaryFiles } from "./types";
type ElementsClipboard = {
type: typeof EXPORT_DATA_TYPES.excalidrawClipboard;
schemaVersion: number;
elements: readonly NonDeletedExcalidrawElement[];
files: BinaryFiles | undefined;
};
@@ -46,6 +48,7 @@ export type PastedMixedContent = { type: "text" | "imageUrl"; value: string }[];
export interface ClipboardData {
elements?: readonly ExcalidrawElement[];
schemaVersion?: number;
files?: BinaryFiles;
text?: string;
mixedContent?: PastedMixedContent;
@@ -172,6 +175,7 @@ export const serializeAsClipboardJSON = ({
// select bound text elements when copying
const contents: ElementsClipboard = {
type: EXPORT_DATA_TYPES.excalidrawClipboard,
schemaVersion: CURRENT_SCHEMA_VERSION,
elements: elements.map((element) => {
if (
getContainingFrame(element, elementsMap) &&
@@ -542,6 +546,7 @@ export const parseClipboard = async (
if (clipboardContainsElements(systemClipboardData)) {
return {
elements: systemClipboardData.elements,
schemaVersion: getSchemaVersion(systemClipboardData),
files: systemClipboardData.files,
text: isPlainPaste
? JSON.stringify(systemClipboardData.elements, null, 2)
+6
View File
@@ -258,6 +258,8 @@ import {
isEligibleFrameChildType,
getBindingStrategyForDraggingBindingElementEndpoints,
isNonDeletedElement,
CURRENT_SCHEMA_VERSION,
getSchemaVersion,
} from "@excalidraw/element";
import type { GlobalPoint, LocalPoint, Radians } from "@excalidraw/math";
@@ -3482,6 +3484,7 @@ class App extends React.Component<AppProps, AppState> {
const restoredElements = restoreElements(initialData?.elements, null, {
repairBindings: true,
deleteInvisibleElements: true,
schemaVersion: getSchemaVersion(initialData),
});
let restoredAppState = restoreAppState(initialData?.appState, null);
const activeTool = restoredAppState.activeTool;
@@ -4496,6 +4499,7 @@ class App extends React.Component<AppProps, AppState> {
this.editorInterface.formFactor === "desktop" ? "cursor" : "center",
retainSeed: isPlainPaste,
preserveFrameChildrenOrder: true,
schemaVersion: data.programmaticAPI ? undefined : data.schemaVersion,
});
return;
}
@@ -4644,9 +4648,11 @@ class App extends React.Component<AppProps, AppState> {
retainSeed?: boolean;
fit?: SetViewportOptions["fit"];
preserveFrameChildrenOrder?: boolean;
schemaVersion?: number;
}) => {
const elements = restoreElements(opts.elements, null, {
deleteInvisibleElements: true,
schemaVersion: opts.schemaVersion ?? CURRENT_SCHEMA_VERSION,
});
const [minX, minY, maxX, maxY] = getCommonBounds(elements);
@@ -10,6 +10,8 @@ import {
getExportSource,
} from "@excalidraw/common";
import { CURRENT_SCHEMA_VERSION } from "@excalidraw/element";
import { EditorLocalStorage } from "../data/EditorLocalStorage";
import { canvasToBlob, resizeImageFile } from "../data/blob";
import { t } from "../i18n";
@@ -280,6 +282,7 @@ const PublishLibrary = ({
const libContent: ExportedLibraryData = {
type: EXPORT_DATA_TYPES.excalidrawLibrary,
version: VERSIONS.excalidrawLibrary,
schemaVersion: CURRENT_SCHEMA_VERSION,
source: getExportSource(),
libraryItems: clonedLibItems,
};
+6 -1
View File
@@ -7,6 +7,8 @@ import {
isPromiseLike,
} from "@excalidraw/common";
import { getSchemaVersion } from "@excalidraw/element";
import type { ValueOf } from "@excalidraw/common/utility-types";
import type { ExcalidrawElement, FileId } from "@excalidraw/element/types";
@@ -164,6 +166,7 @@ export const loadSceneOrLibraryFromBlob = async (
elements: restoreElements(data.elements, localElements, {
repairBindings: true,
deleteInvisibleElements: true,
schemaVersion: getSchemaVersion(data),
}),
appState: restoreAppState(
{
@@ -223,7 +226,9 @@ export const parseLibraryJSON = (
throw new Error("Invalid library");
}
const libraryItems = data.libraryItems || data.library;
return restoreLibraryItems(libraryItems, defaultStatus);
return restoreLibraryItems(libraryItems, defaultStatus, {
schemaVersion: getSchemaVersion(data),
});
};
export const loadLibraryFromBlob = async (
+4
View File
@@ -5,6 +5,8 @@ import {
VERSIONS,
} from "@excalidraw/common";
import { CURRENT_SCHEMA_VERSION } from "@excalidraw/element";
import type { ExcalidrawElement } from "@excalidraw/element/types";
import type { MaybePromise } from "@excalidraw/common/utility-types";
@@ -58,6 +60,7 @@ export const serializeAsJSON = (
const data: ExportedDataState = {
type: EXPORT_DATA_TYPES.excalidraw,
version: VERSIONS.excalidraw,
schemaVersion: CURRENT_SCHEMA_VERSION,
source: getExportSource(),
elements,
appState:
@@ -138,6 +141,7 @@ export const serializeLibraryAsJSON = (libraryItems: LibraryItems) => {
const data: ExportedLibraryData = {
type: EXPORT_DATA_TYPES.excalidrawLibrary,
version: VERSIONS.excalidrawLibrary,
schemaVersion: CURRENT_SCHEMA_VERSION,
source: getExportSource(),
libraryItems,
};
+16 -5
View File
@@ -18,6 +18,7 @@ import {
} from "@excalidraw/common";
import { hashElementsVersion, hashString } from "@excalidraw/element";
import { CURRENT_SCHEMA_VERSION, getSchemaVersion } from "@excalidraw/element";
import { getCommonBoundingBox } from "@excalidraw/element";
@@ -65,9 +66,11 @@ type LibraryUpdate = {
updatedItems: Map<LibraryItem["id"], LibraryItem>;
};
// an object so that we can later add more properties to it without breaking,
// such as schema version
export type LibraryPersistedData = { libraryItems: LibraryItems };
// an object so that we can later add more properties to it without breaking
export type LibraryPersistedData = {
libraryItems: LibraryItems;
schemaVersion?: number;
};
const onLibraryUpdateEmitter = new Emitter<
[update: LibraryUpdate, libraryItems: LibraryItems]
@@ -554,7 +557,11 @@ class AdapterTransaction {
new Promise<LibraryItems>(async (resolve, reject) => {
try {
const data = await adapter.load({ source });
resolve(restoreLibraryItems(data?.libraryItems || [], "published"));
resolve(
restoreLibraryItems(data?.libraryItems || [], "published", {
schemaVersion: getSchemaVersion(data),
}),
);
} catch (error: any) {
reject(error);
}
@@ -662,7 +669,10 @@ const persistLibraryUpdate = async (
const version = getLibraryItemsHash(nextLibraryItems);
if (version !== lastSavedLibraryItemsHash) {
await adapter.save({ libraryItems: nextLibraryItems });
await adapter.save({
libraryItems: nextLibraryItems,
schemaVersion: CURRENT_SCHEMA_VERSION,
});
}
lastSavedLibraryItemsHash = version;
@@ -862,6 +872,7 @@ export const useHandleLibrary = (
restoredData = restoreLibraryItems(
libraryData.libraryItems || [],
"published",
{ schemaVersion: getSchemaVersion(libraryData) },
);
// we don't queue this operation because it's running inside
+38 -22
View File
@@ -39,10 +39,7 @@ import {
} from "@excalidraw/element";
import { LinearElementEditor } from "@excalidraw/element";
import { bumpVersion } from "@excalidraw/element";
import {
CURRENT_ELEMENT_SCHEMA_VERSION,
upgradeElementSchema,
} from "@excalidraw/element";
import { migrateElements } from "@excalidraw/element";
import { getContainerElement } from "@excalidraw/element";
import { detectLineHeight } from "@excalidraw/element";
import {
@@ -434,7 +431,6 @@ const restoreElementWithProperties = <
// newly added elements
version: element.version || 1,
versionNonce: element.versionNonce ?? 0,
schemaVersion: element.schemaVersion || CURRENT_ELEMENT_SCHEMA_VERSION,
index: element.index ?? null,
isDeleted: element.isDeleted ?? false,
id: element.id || randomId(),
@@ -507,7 +503,7 @@ export const restoreElement = (
deleteInvisibleElements?: boolean;
},
): typeof element | null => {
element = upgradeElementSchema({ ...element });
element = { ...element };
switch (element.type) {
case "text":
@@ -829,18 +825,23 @@ export const restoreElements = <T extends ExcalidrawElement>(
refreshDimensions?: boolean;
repairBindings?: boolean;
deleteInvisibleElements?: boolean;
schemaVersion?: number;
}
| undefined,
): CombineBrandsIfNeeded<T, OrderedExcalidrawElement> => {
// used to detect duplicate top-level element ids
const existingIds = new Set<string>();
const targetElementsMap = arrayToMap(targetElements || []);
const migratedTargetElements = migrateElements(
targetElements || [],
opts?.schemaVersion ?? 0,
);
const targetElementsMap = arrayToMap(migratedTargetElements);
const existingElementsMap = existingElements
? arrayToMap(existingElements)
: null;
const restoredElements = syncInvalidIndices(
(targetElements || []).reduce((elements, element) => {
migratedTargetElements.reduce((elements, element) => {
// filtering out selection, which is legacy, no longer kept in elements,
// and causing issues if retained
if (element.type === "selection") {
@@ -1163,9 +1164,14 @@ export const restoreAppState = (
};
};
const restoreLibraryItem = (libraryItem: LibraryItem): LibraryItem | null => {
const restoreLibraryItem = (
libraryItem: LibraryItem,
schemaVersion: number | undefined,
) => {
const elements = getNonDeletedElements(
restoreElements(libraryItem.elements, null),
restoreElements(getNonDeletedElements(libraryItem.elements), null, {
schemaVersion,
}),
);
return elements.length ? { ...libraryItem, elements } : null;
};
@@ -1173,17 +1179,24 @@ const restoreLibraryItem = (libraryItem: LibraryItem): LibraryItem | null => {
export const restoreLibraryItems = (
libraryItems: ImportedDataState["libraryItems"] = [],
defaultStatus: LibraryItem["status"],
opts?: {
/** @see restoreElements */
schemaVersion?: number;
},
) => {
const restoredItems: LibraryItem[] = [];
for (const item of libraryItems) {
// migrate older libraries
if (Array.isArray(item)) {
const restoredItem = restoreLibraryItem({
status: defaultStatus,
elements: item,
id: randomId(),
created: Date.now(),
});
const restoredItem = restoreLibraryItem(
{
status: defaultStatus,
elements: item,
id: randomId(),
created: Date.now(),
},
opts?.schemaVersion,
);
if (restoredItem) {
restoredItems.push(restoredItem);
}
@@ -1192,12 +1205,15 @@ export const restoreLibraryItems = (
LibraryItem,
"id" | "status" | "created"
>;
const restoredItem = restoreLibraryItem({
..._item,
id: _item.id || randomId(),
status: _item.status || defaultStatus,
created: _item.created || Date.now(),
});
const restoredItem = restoreLibraryItem(
{
..._item,
id: _item.id || randomId(),
status: _item.status || defaultStatus,
created: _item.created || Date.now(),
},
opts?.schemaVersion,
);
if (restoredItem) {
restoredItems.push(restoredItem);
}
+3
View File
@@ -14,6 +14,7 @@ import type {
export interface ExportedDataState {
type: string;
version: number;
schemaVersion: number;
source: string;
elements: readonly ExcalidrawElement[];
appState: ReturnType<typeof cleanAppStateForExport>;
@@ -35,6 +36,7 @@ export type LegacyAppState = {
export interface ImportedDataState {
type?: string;
version?: number;
schemaVersion?: number;
source?: string;
elements?: readonly ExcalidrawElement[] | null;
appState?: Readonly<
@@ -52,6 +54,7 @@ export interface ImportedDataState {
export interface ExportedLibraryData {
type: string;
version: typeof VERSIONS.excalidrawLibrary;
schemaVersion: number;
source: string;
libraryItems: LibraryItems;
}
@@ -1019,7 +1019,6 @@ exports[`contextMenu element > right-clicking on a group should select whole gro
"opacity": 100,
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": 1,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
@@ -1054,7 +1053,6 @@ exports[`contextMenu element > right-clicking on a group should select whole gro
"opacity": 100,
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": 1,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
@@ -1216,7 +1214,6 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e
"opacity": 100,
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": 1278240551,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
@@ -1272,7 +1269,6 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e
"opacity": 100,
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 2,
@@ -1291,6 +1287,7 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e
"updated": {},
},
"id": "id2",
"schemaVersion": 1,
},
]
`;
@@ -1432,7 +1429,6 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings
"opacity": 100,
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": 1014066025,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
@@ -1465,7 +1461,6 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings
"opacity": 100,
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": 1278240551,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
@@ -1521,7 +1516,6 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings
"opacity": 100,
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 2,
@@ -1540,6 +1534,7 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings
"updated": {},
},
"id": "id2",
"schemaVersion": 1,
},
{
"appState": AppStateDelta {
@@ -1576,7 +1571,6 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings
"opacity": 100,
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 2,
@@ -1595,6 +1589,7 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings
"updated": {},
},
"id": "id5",
"schemaVersion": 1,
},
{
"appState": AppStateDelta {
@@ -1628,6 +1623,7 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings
},
},
"id": "id7",
"schemaVersion": 1,
},
]
`;
@@ -1769,7 +1765,6 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings
"opacity": 100,
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": 1014066025,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
@@ -1802,7 +1797,6 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings
"opacity": 100,
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": 1278240551,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
@@ -1858,7 +1852,6 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings
"opacity": 100,
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 2,
@@ -1877,6 +1870,7 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings
"updated": {},
},
"id": "id2",
"schemaVersion": 1,
},
{
"appState": AppStateDelta {
@@ -1913,7 +1907,6 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings
"opacity": 100,
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 2,
@@ -1932,6 +1925,7 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings
"updated": {},
},
"id": "id5",
"schemaVersion": 1,
},
{
"appState": AppStateDelta {
@@ -1965,6 +1959,7 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings
},
},
"id": "id7",
"schemaVersion": 1,
},
]
`;
@@ -2108,7 +2103,6 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st
"opacity": 100,
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": 1278240551,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
@@ -2164,7 +2158,6 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st
"opacity": 100,
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 2,
@@ -2183,6 +2176,7 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st
"updated": {},
},
"id": "id2",
"schemaVersion": 1,
},
]
`;
@@ -2322,7 +2316,6 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen
"opacity": 100,
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": 1278240551,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
@@ -2378,7 +2371,6 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen
"opacity": 100,
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 2,
@@ -2397,6 +2389,7 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen
"updated": {},
},
"id": "id2",
"schemaVersion": 1,
},
{
"appState": AppStateDelta {
@@ -2428,6 +2421,7 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen
"updated": {},
},
"id": "id4",
"schemaVersion": 1,
},
]
`;
@@ -2569,7 +2563,6 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
"opacity": 100,
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": 1278240551,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
@@ -2602,7 +2595,6 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
"opacity": 100,
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": 1014066025,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
@@ -2658,7 +2650,6 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
"opacity": 100,
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 2,
@@ -2677,6 +2668,7 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
"updated": {},
},
"id": "id2",
"schemaVersion": 1,
},
{
"appState": AppStateDelta {
@@ -2713,7 +2705,6 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
"opacity": 100,
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 2,
@@ -2732,6 +2723,7 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
"updated": {},
},
"id": "id5",
"schemaVersion": 1,
},
]
`;
@@ -2880,7 +2872,6 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
"opacity": 100,
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": 1278240551,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
@@ -2915,7 +2906,6 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
"opacity": 100,
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": 1014066025,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
@@ -2971,7 +2961,6 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
"opacity": 100,
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 2,
@@ -2990,6 +2979,7 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
"updated": {},
},
"id": "id2",
"schemaVersion": 1,
},
{
"appState": AppStateDelta {
@@ -3026,7 +3016,6 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
"opacity": 100,
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 2,
@@ -3045,6 +3034,7 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
"updated": {},
},
"id": "id5",
"schemaVersion": 1,
},
{
"appState": AppStateDelta {
@@ -3065,6 +3055,7 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
"updated": {},
},
"id": "id8",
"schemaVersion": 1,
},
{
"appState": AppStateDelta {
@@ -3110,6 +3101,7 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
},
},
"id": "id11",
"schemaVersion": 1,
},
]
`;
@@ -3253,7 +3245,6 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
"opacity": 60,
"roughness": 2,
"roundness": null,
"schemaVersion": 1,
"seed": 1278240551,
"strokeColor": "#e03131",
"strokeStyle": "dotted",
@@ -3286,7 +3277,6 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
"opacity": 60,
"roughness": 2,
"roundness": null,
"schemaVersion": 1,
"seed": 406373543,
"strokeColor": "#e03131",
"strokeStyle": "dotted",
@@ -3342,7 +3332,6 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
"opacity": 100,
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 2,
@@ -3361,6 +3350,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
"updated": {},
},
"id": "id2",
"schemaVersion": 1,
},
{
"appState": AppStateDelta {
@@ -3397,7 +3387,6 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
"opacity": 100,
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 2,
@@ -3416,6 +3405,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
"updated": {},
},
"id": "id5",
"schemaVersion": 1,
},
{
"appState": AppStateDelta {
@@ -3441,6 +3431,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
},
},
"id": "id7",
"schemaVersion": 1,
},
{
"appState": AppStateDelta {
@@ -3466,6 +3457,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
},
},
"id": "id9",
"schemaVersion": 1,
},
{
"appState": AppStateDelta {
@@ -3491,6 +3483,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
},
},
"id": "id11",
"schemaVersion": 1,
},
{
"appState": AppStateDelta {
@@ -3516,6 +3509,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
},
},
"id": "id13",
"schemaVersion": 1,
},
{
"appState": AppStateDelta {
@@ -3541,6 +3535,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
},
},
"id": "id15",
"schemaVersion": 1,
},
{
"appState": AppStateDelta {
@@ -3566,6 +3561,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
},
},
"id": "id17",
"schemaVersion": 1,
},
{
"appState": AppStateDelta {
@@ -3591,6 +3587,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
},
},
"id": "id19",
"schemaVersion": 1,
},
{
"appState": AppStateDelta {
@@ -3636,6 +3633,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
},
},
"id": "id21",
"schemaVersion": 1,
},
]
`;
@@ -3777,7 +3775,6 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e
"opacity": 100,
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": 238820263,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
@@ -3810,7 +3807,6 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e
"opacity": 100,
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": 1278240551,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
@@ -3866,7 +3862,6 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e
"opacity": 100,
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 2,
@@ -3885,6 +3880,7 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e
"updated": {},
},
"id": "id2",
"schemaVersion": 1,
},
{
"appState": AppStateDelta {
@@ -3921,7 +3917,6 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e
"opacity": 100,
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 2,
@@ -3940,6 +3935,7 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e
"updated": {},
},
"id": "id5",
"schemaVersion": 1,
},
{
"appState": AppStateDelta {
@@ -3965,6 +3961,7 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e
},
},
"id": "id7",
"schemaVersion": 1,
},
]
`;
@@ -4106,7 +4103,6 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
"opacity": 100,
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": 1014066025,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
@@ -4139,7 +4135,6 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
"opacity": 100,
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": 1278240551,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
@@ -4195,7 +4190,6 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
"opacity": 100,
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 2,
@@ -4214,6 +4208,7 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
"updated": {},
},
"id": "id2",
"schemaVersion": 1,
},
{
"appState": AppStateDelta {
@@ -4250,7 +4245,6 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
"opacity": 100,
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 2,
@@ -4269,6 +4263,7 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
"updated": {},
},
"id": "id5",
"schemaVersion": 1,
},
{
"appState": AppStateDelta {
@@ -4294,6 +4289,7 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
},
},
"id": "id7",
"schemaVersion": 1,
},
]
`;
@@ -4438,7 +4434,6 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
"opacity": 100,
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": 1278240551,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
@@ -4471,7 +4466,6 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
"opacity": 100,
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": 400692809,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
@@ -4527,7 +4521,6 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
"opacity": 100,
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 2,
@@ -4546,6 +4539,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
"updated": {},
},
"id": "id2",
"schemaVersion": 1,
},
{
"appState": AppStateDelta {
@@ -4582,7 +4576,6 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
"opacity": 100,
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 2,
@@ -4601,6 +4594,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
"updated": {},
},
"id": "id5",
"schemaVersion": 1,
},
{
"appState": AppStateDelta {
@@ -4621,6 +4615,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
"updated": {},
},
"id": "id8",
"schemaVersion": 1,
},
{
"appState": AppStateDelta {
@@ -4666,6 +4661,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
},
},
"id": "id11",
"schemaVersion": 1,
},
{
"appState": AppStateDelta {
@@ -4711,6 +4707,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
},
},
"id": "id13",
"schemaVersion": 1,
},
]
`;
@@ -5729,7 +5726,6 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
"opacity": 100,
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": 1278240551,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
@@ -5762,7 +5758,6 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
"opacity": 100,
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": 400692809,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
@@ -5818,7 +5813,6 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
"opacity": 100,
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 2,
@@ -5837,6 +5831,7 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
"updated": {},
},
"id": "id2",
"schemaVersion": 1,
},
{
"appState": AppStateDelta {
@@ -5873,7 +5868,6 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
"opacity": 100,
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 2,
@@ -5892,6 +5886,7 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
"updated": {},
},
"id": "id5",
"schemaVersion": 1,
},
{
"appState": AppStateDelta {
@@ -5914,6 +5909,7 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
"updated": {},
},
"id": "id8",
"schemaVersion": 1,
},
{
"appState": AppStateDelta {
@@ -5934,6 +5930,7 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
"updated": {},
},
"id": "id11",
"schemaVersion": 1,
},
]
`;
@@ -6956,7 +6953,6 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
"opacity": 100,
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": 1278240551,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
@@ -6991,7 +6987,6 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
"opacity": 100,
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": 400692809,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
@@ -7047,7 +7042,6 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
"opacity": 100,
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 2,
@@ -7066,6 +7060,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
"updated": {},
},
"id": "id2",
"schemaVersion": 1,
},
{
"appState": AppStateDelta {
@@ -7102,7 +7097,6 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
"opacity": 100,
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 2,
@@ -7121,6 +7115,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
"updated": {},
},
"id": "id5",
"schemaVersion": 1,
},
{
"appState": AppStateDelta {
@@ -7143,6 +7138,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
"updated": {},
},
"id": "id8",
"schemaVersion": 1,
},
{
"appState": AppStateDelta {
@@ -7163,6 +7159,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
"updated": {},
},
"id": "id11",
"schemaVersion": 1,
},
{
"appState": AppStateDelta {
@@ -7208,6 +7205,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
},
},
"id": "id14",
"schemaVersion": 1,
},
]
`;
@@ -9910,7 +9908,6 @@ exports[`contextMenu element > shows context menu for element > [end of test] el
"opacity": 100,
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": 1278240551,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
@@ -9943,7 +9940,6 @@ exports[`contextMenu element > shows context menu for element > [end of test] el
"opacity": 100,
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": 1,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
@@ -9976,7 +9972,6 @@ exports[`contextMenu element > shows context menu for element > [end of test] el
"opacity": 100,
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": 1,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
@@ -10038,7 +10033,6 @@ exports[`contextMenu element > shows context menu for element > [end of test] un
"opacity": 100,
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 2,
@@ -10057,6 +10051,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] un
"updated": {},
},
"id": "id2",
"schemaVersion": 1,
},
]
`;
@@ -35,7 +35,6 @@ exports[`Test dragCreate > add element to the scene when pointer dragging long e
"roundness": {
"type": 2,
},
"schemaVersion": 1,
"seed": 1278240551,
"startArrowhead": null,
"startBinding": null,
@@ -72,7 +71,6 @@ exports[`Test dragCreate > add element to the scene when pointer dragging long e
"opacity": 100,
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": 1278240551,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
@@ -107,7 +105,6 @@ exports[`Test dragCreate > add element to the scene when pointer dragging long e
"opacity": 100,
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": 1278240551,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
@@ -153,7 +150,6 @@ exports[`Test dragCreate > add element to the scene when pointer dragging long e
"polygon": false,
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": 1278240551,
"startArrowhead": null,
"startBinding": null,
@@ -190,7 +186,6 @@ exports[`Test dragCreate > add element to the scene when pointer dragging long e
"opacity": 100,
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": 1278240551,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
@@ -1,7 +1,7 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`export > export svg-embedded scene > svg-embdedded scene export output 1`] = `
"<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36" width="36" height="36"><!-- svg-source:excalidraw --><metadata><!-- payload-type:application/vnd.excalidraw+json --><!-- payload-version:2 --><!-- payload-start -->eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nHVTwW7bMFxmve8rXGb3WqxOiu6QW7u2aFx1MDAwZu2hXHUwMDE5tkOxg2oxNlx1MDAxMUVcdTAwMTIkuklcdTAwMTZcdTAwMDTYZ+zWX+wnlFI8K3bSXHUwMDA0MKBHUnx8fNp8ybKc1lx1MDAxNvJJlsOqXHUwMDE0XG6lXHUwMDEzy/w04K/gPFx1MDAxYc2hcTx707gyZtZEdnJ2plxmXHUwMDE31MbT5Lwoil1cdTAwMTEoWIAmz2nPfM6yTfxyXHUwMDA0ZSi9jGlcdTAwMTH435dgRVx0XTFUdKd177RESTUjo29cdTAwMWRUXHUwMDAzVjX1MaErXHUwMDA1vUJPzszhu1HGhY4nI1xi/9T0RZTzyplGyy6HnNDeXG7Hw6S8XHUwMDE5KjWldbyd9WC18kGPXy3F8Vx1MDAwMP+siptWtVx1MDAwNlx1MDAxZlx1MDAwNFx1MDAxYnWosaJEXG7Dj4o0RWBo72XU9nfi5MRcdTAwMDLug7i6UaqDUUtYXHLBOGLbrVx1MDAxN/BcdTAwMDCyxyAt/1x1MDAwMHs0ulx1MDAxY8hb1rBcdTAwMTA/j5Sgv2ZHULx8JpSHJHmgcpPc0qPTWClowEihnlx1MDAwZvPYgfMjd0dHsdbvb//+7i3PaJrin0B9XFz00FuxQFx1MDAxNdS+6F1xqbBcbuPkXG5meyZgXHUwMDE5XGLZ+V2YjE3Rku9cdTAwMTOowVx1MDAxZK7EOKxQXHUwMDBi9eMoPdGQeVx1MDAwMr8jSK6B/cnhrvP51/FFXGZs+Vx1MDAxYl2QXHUwMDBia6fEenF099rYKShcdTAwMDej7jBcdTAwMDKbxozQg5Fwo8WLXHUwMDFh6pi/XCIsr1x1MDAwZVx1MDAxZsbJLP5a4u1cblx1MDAxZVx1MDAxYUU45VWXxFx1MDAxNlxi+9xcdTAwMDR+28iPX1xytND2XHUwMDAzo9gvdiJ9<!-- payload-end --></metadata><defs><style class="style-fonts">
"<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36" width="36" height="36"><!-- svg-source:excalidraw --><metadata><!-- payload-type:application/vnd.excalidraw+json --><!-- payload-version:2 --><!-- payload-start -->eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nHVTTW/UMFx1MDAxML3zK6L0imh2UTnsrUCr9lBcdTAwMGUsglx1MDAwM+LgxrPJaL22ZU/2g9VK/FxmbvxFflx1MDAwMmMnjddpSaRIfjOeee/N5PiqKEo6WChcdTAwMTdFXHT7WiiUTuzK11x1MDAwMd+C82g0h+bx7OtcdTAwMTY24uuIznrUdK6O91tcIru4vFSGy7TG0+JtVVV9KVCwXHUwMDAxTZ7TvvO5KI7xy1x1MDAxMZTh6nVMi8BcdTAwMTNcdTAwMWKCPSV0z1A1nlx1MDAwZdlph5LawOfdXGK1gE1LOSZ0oyC76MmZNXwwyrjQ8WJcdTAwMDbhTU1cdTAwMWZFvW6c6bRcdTAwMWNzyFx07a1wLCblrVCpJVx1MDAxZGJ19oM9LCc9vlxyXHUwMDE051x1MDAxM/x/t7hp02rw/snmiFx1MDAxYStqpCB+ViVcdTAwMTWBob2X0dtcdTAwMWaJk1x1MDAxM1x1MDAxYrhcdTAwMGbm6k6pXHUwMDExRi1hP1x1MDAwNaPEoVtcdTAwMTbwXHUwMDAwMmOwzYd/jn0yus7tRf+R506xxEooXHUwMDBmydjQ8CbtRNa0s1LQpK9CvZ7m8Z6tX6hcdTAwMWT3hlx1MDAxZP375/evs1x1MDAxMVx1MDAxOU1L/Fx1MDAxOVxizqtcZr1cdTAwMTVcdTAwMWJUwdOrrMS1wiboLFx1MDAxNazORs1iXHR5v8cwXHUwMDE5m6I111x1MDAxM6jBPTfeOGxQXHUwMDBi9eVFeqIj81x1MDAxOXxPkFxcXHUwMDA358rhbtzmN/OrXHUwMDE4OPE3zrpcdTAwMTTWLon94mj/T/E+oJxI7TFcdTAwMDKbZEbowUi40eJRTX0st1xiu/fP1/9iXHUwMDE1n4H4MIKHTlx1MDAxMS551DXxXHUwMDFlhHlcdTAwMWVcdTAwMDO/U+TH/1x1MDAwNlxm0OlcdTAwMWZm+S72In0=<!-- payload-end --></metadata><defs><style class="style-fonts">
</style></defs><rect x="0" y="0" width="36" height="36" fill="#ffffff"></rect><g transform="translate(10 10) rotate(0 8 8)" data-id="A"><text x="0" y="17.619999999999997" font-family="Excalifont, Xiaolai, sans-serif, Segoe UI Emoji" font-size="20px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">😀</text></g></svg>"
`;
File diff suppressed because it is too large Load Diff
@@ -18,7 +18,6 @@ exports[`duplicate element on move when ALT is clicked > rectangle 5`] = `
"opacity": 100,
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": 1278240551,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
@@ -51,7 +50,6 @@ exports[`duplicate element on move when ALT is clicked > rectangle 6`] = `
"opacity": 100,
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": 1505387817,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
@@ -84,7 +82,6 @@ exports[`move element > rectangle 5`] = `
"opacity": 100,
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": 1278240551,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
@@ -122,7 +119,6 @@ exports[`move element > rectangles with binding arrow 5`] = `
"opacity": 100,
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": 1278240551,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
@@ -160,7 +156,6 @@ exports[`move element > rectangles with binding arrow 6`] = `
"opacity": 100,
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": 1116226695,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
@@ -216,7 +211,6 @@ exports[`move element > rectangles with binding arrow 7`] = `
"roundness": {
"type": 2,
},
"schemaVersion": 1,
"seed": 23633383,
"startArrowhead": null,
"startBinding": {
@@ -37,7 +37,6 @@ exports[`multi point mode in linear elements > arrow 3`] = `
"roundness": {
"type": 2,
},
"schemaVersion": 1,
"seed": 1278240551,
"startArrowhead": null,
"startBinding": null,
@@ -89,7 +88,6 @@ exports[`multi point mode in linear elements > line 3`] = `
"polygon": false,
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": 1278240551,
"startArrowhead": null,
"startBinding": null,
File diff suppressed because it is too large Load Diff
@@ -33,7 +33,6 @@ exports[`select single element on the scene > arrow 1`] = `
"roundness": {
"type": 2,
},
"schemaVersion": 1,
"seed": 1278240551,
"startArrowhead": null,
"startBinding": null,
@@ -81,7 +80,6 @@ exports[`select single element on the scene > arrow escape 1`] = `
"polygon": false,
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": 1278240551,
"startArrowhead": null,
"startBinding": null,
@@ -116,7 +114,6 @@ exports[`select single element on the scene > diamond 1`] = `
"opacity": 100,
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": 1278240551,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
@@ -149,7 +146,6 @@ exports[`select single element on the scene > ellipse 1`] = `
"opacity": 100,
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": 1278240551,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
@@ -182,7 +178,6 @@ exports[`select single element on the scene > rectangle 1`] = `
"opacity": 100,
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": 1278240551,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
@@ -31,7 +31,6 @@ exports[`repairing bindings > should strip arrow binding if repair throws 1`] =
],
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": Any<Number>,
"startArrowhead": null,
"startBinding": null,
@@ -79,7 +78,6 @@ exports[`restoreElements > should restore arrow element correctly 1`] = `
],
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": Any<Number>,
"startArrowhead": null,
"startBinding": null,
@@ -120,7 +118,6 @@ exports[`restoreElements > should restore correctly with rectangle, ellipse and
"roundness": {
"type": 3,
},
"schemaVersion": 1,
"seed": Any<Number>,
"strokeColor": "red",
"strokeStyle": "dashed",
@@ -159,7 +156,6 @@ exports[`restoreElements > should restore correctly with rectangle, ellipse and
"roundness": {
"type": 2,
},
"schemaVersion": 1,
"seed": Any<Number>,
"strokeColor": "red",
"strokeStyle": "dashed",
@@ -198,7 +194,6 @@ exports[`restoreElements > should restore correctly with rectangle, ellipse and
"roundness": {
"type": 2,
},
"schemaVersion": 1,
"seed": Any<Number>,
"strokeColor": "red",
"strokeStyle": "dashed",
@@ -242,7 +237,6 @@ exports[`restoreElements > should restore freedraw element correctly 1`] = `
"pressures": [],
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": Any<Number>,
"simulatePressure": true,
"strokeColor": "#1e1e1e",
@@ -293,7 +287,6 @@ exports[`restoreElements > should restore line and draw elements correctly 1`] =
"polygon": false,
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": Any<Number>,
"startArrowhead": null,
"startBinding": null,
@@ -341,7 +334,6 @@ exports[`restoreElements > should restore line and draw elements correctly 2`] =
"polygon": false,
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": Any<Number>,
"startArrowhead": null,
"startBinding": null,
@@ -382,7 +374,6 @@ exports[`restoreElements > should restore text element correctly passing value f
"originalText": "text",
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": Any<Number>,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
@@ -425,7 +416,6 @@ exports[`restoreElements > should restore text element correctly with unknown fo
"originalText": "",
"roughness": 1,
"roundness": null,
"schemaVersion": 1,
"seed": Any<Number>,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
+10 -22
View File
@@ -4,7 +4,7 @@ import { vi } from "vitest";
import { DEFAULT_SIDEBAR, FONT_FAMILY, ROUNDNESS } from "@excalidraw/common";
import { newElementWith } from "@excalidraw/element";
import { CURRENT_ELEMENT_SCHEMA_VERSION } from "@excalidraw/element";
import { CURRENT_SCHEMA_VERSION } from "@excalidraw/element";
import * as sizeHelpers from "@excalidraw/element";
import type { LocalPoint } from "@excalidraw/math";
@@ -44,7 +44,7 @@ describe("restoreElements", () => {
expect(restoredElements.length).toBe(elements.length);
});
it("lifts a legacy (unversioned) element to the current schema version", () => {
it("restores legacy (unversioned) elements through the migration pipeline", () => {
// element persisted before schema versioning existed
const legacyElement = {
type: "rectangle",
@@ -53,30 +53,18 @@ describe("restoreElements", () => {
y: 0,
} as any as ExcalidrawElement;
expect(legacyElement).not.toHaveProperty("schemaVersion");
const [restored] = restore.restoreElements([legacyElement], null);
expect(restored.schemaVersion).toBe(CURRENT_ELEMENT_SCHEMA_VERSION);
expect(restored.id).toBe("legacy");
expect(restored.type).toBe("rectangle");
});
it("keeps the schema version of an already-versioned element", () => {
const versionedElement = API.createElement({ type: "rectangle" });
expect(versionedElement.schemaVersion).toBe(CURRENT_ELEMENT_SCHEMA_VERSION);
it("restores elements coming from a container at the current schema version", () => {
const element = API.createElement({ type: "rectangle" });
const [restored] = restore.restoreElements([versionedElement], null);
expect(restored.schemaVersion).toBe(CURRENT_ELEMENT_SCHEMA_VERSION);
});
it("stamps newly created elements with the current schema version", () => {
expect(API.createElement({ type: "rectangle" }).schemaVersion).toBe(
CURRENT_ELEMENT_SCHEMA_VERSION,
);
expect(API.createElement({ type: "text" }).schemaVersion).toBe(
CURRENT_ELEMENT_SCHEMA_VERSION,
);
expect(API.createElement({ type: "arrow" }).schemaVersion).toBe(
CURRENT_ELEMENT_SCHEMA_VERSION,
);
const [restored] = restore.restoreElements([element], null, {
schemaVersion: CURRENT_SCHEMA_VERSION,
});
expect(restored.id).toBe(element.id);
});
it("when imported data state is null it should return an empty array of elements", () => {
-1
View File
@@ -25,7 +25,6 @@ const elementBase: Omit<ExcalidrawElement, "type"> = {
seed: 1041657908,
version: 120,
versionNonce: 1188004276,
schemaVersion: 1,
isDeleted: false,
boundElements: null,
updated: 1,
-1
View File
@@ -253,7 +253,6 @@ export class API {
| "type"
| "version"
| "versionNonce"
| "schemaVersion"
| "isDeleted"
| "groupIds"
| "link"
-3
View File
@@ -359,7 +359,6 @@ describe("Basic lasso selection tests", () => {
...e,
angle: e.angle as Radians,
index: null,
schemaVersion: 1,
} as ExcalidrawElement),
);
@@ -1045,7 +1044,6 @@ describe("Special cases", () => {
...e,
index: null,
angle: e.angle as Radians,
schemaVersion: 1,
})) as ExcalidrawElement[];
h.elements = elements;
@@ -1765,7 +1763,6 @@ describe("Special cases", () => {
...e,
index: null,
angle: e.angle as Radians,
schemaVersion: 1,
})) as ExcalidrawElement[];
h.elements = elements;
@@ -240,7 +240,7 @@ exports[`exportToSvg > with elements that have a link 1`] = `
`;
exports[`exportToSvg > with exportEmbedScene 1`] = `
"<!-- svg-source:excalidraw --><metadata><!-- payload-type:application/vnd.excalidraw+json --><!-- payload-version:2 --><!-- payload-start -->eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1WS4/TMFx1MDAxML7vr4jMdcU6abstvVx1MDAxNZbHSlxiJIpYXHTEwVx1MDAxYk9cdTAwMTIrjp21nT6o+t+xXHUwMDFkXHUwMDFhp2GFOLK0qVx1MDAxNGneM998o3R3XHUwMDExRchsa0DzXGLBJiWcUUXW6NLpV6A0k8KaXHUwMDEyL2vZqNR7XHUwMDE2xtTzqysubUAhtZmPMMZtXHUwMDEwcKhAXHUwMDE4bd2+WTmKdv5tLYy60NWdevi4yEomaPKQVtP3N4u3X32od9pYn3E87uStqz6advKaUVNYXYxxpyuA5YVcdTAwMTkoici56zVotFGyhFeSS+VcdTAwMWF5hv1cdTAwMTNK35O0zJVsXHUwMDA0XHI+8YSQ+yz4ZIzzpdnyXHUwMDE2XHUwMDA1klx1MDAxNo1cdTAwMDI0qHB3aHGg7+K0tCiHKFsyL1x1MDAwNGh9XHUwMDE0I2uSMrNcdTAwMWRM5fqrb6lH93voSpFcbm5cdTAwMWS8ouG8n1jQX4mPXGZcdTAwMTZ7cEAj0lx1MDAxYl9cdTAwMDNQX21cdTAwMWNfT6Yv8KyzXHUwMDA0XHUwMDFlxFx0XHUwMDFlaj9I4TlcdTAwMTHHs1x1MDAxOcbjZHpcdTAwMWQyplx1MDAwNVTkS4hcdTAwMGVccuhcdTAwMWLLXHUwMDEz41x1MDAwYmaEa1xiS3A9v1x1MDAwZVx1MDAxYzrqu6kpaYNCKs5EOfSzvCxcdTAwMWbJfeA5ZaSSglwir99fnln6r7M0PkWWXHUwMDAy56zW8DRYXHUwMDFhg/udNkuTU2SpgY3prVRcbrNkP5ylN53TviFcdTAwMTXjblx1MDAwM5OQwoXaXHUwMDE0UrGcXHTCo+NcXFx1MDAwN/XnP7s5acFZ7kBBXHUwMDFjsp7JomqY/YfSmY2sgzW1XVx1MDAxMSZA/b5si1x1MDAwM7zrXHUwMDBl4XlcdTAwMTJ6Jo2Rn0C3I3pEzuf5RM5zdD7PvzjPMNL/dJ727SmESF0vjVx1MDAwNdaa22NFK1x1MDAwNuuXj5xD5lx1MDAxZvf19cftzlx1MDAwMdyWdvuL/U+/32JDIn0=<!-- payload-end --></metadata><defs><style class="style-fonts">
"<!-- svg-source:excalidraw --><metadata><!-- payload-type:application/vnd.excalidraw+json --><!-- payload-version:2 --><!-- payload-start -->eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1WW2/TMFx1MDAxOH3fr4jM68SctF1L31xu4zJcdIFEXHUwMDExk0A8ePGXxIpjZ7bTXHUwMDBiVf/7bIfGaZh4ZqWpXHUwMDE0yee7+Pj4fEp3XHUwMDE3UYTMtlx1MDAwNjSPXHUwMDEwbFLCXHUwMDE5VWSNLlx1MDAxZL5cdTAwMDKlmVx1MDAxNDaU+LVOXHUwMDBiqMi3XHUwMDBljVtUNir19YUx9fzqikvbppDazEdcdTAwMTjjtlx1MDAxNXCoQFx1MDAxOG3Tfth1XHUwMDE07fzbRlx1MDAxOHWlqzv18HmRlUzQ5CGtplx1MDAxZm9cdTAwMTbvv/tSn7SxOeN43K23jtNo2q3XjJrCMcK4w1xuYHlhXHUwMDA2IFx1MDAxMTl3XFxcdTAwMDOijZIlvJFcXCpH5Fx1MDAwNfZP2PqepGWuZCNoyIknhNxnISdjnC/NlrcqkLRoXHUwMDE0oMFcdTAwMGV3XHUwMDA3ilx1MDAwM7yr09JqXHUwMDFmquyWeSFA66NcdTAwMWFZk5SZ7eBUjl99S726P1x1MDAwMytFKrh18oqG835jQX83PlxuWO3BXHSNSO/4XHUwMDFhgPrdxvH1ZPpcbs+6SHBHnOAh+klcbu+JOJ7NMFx1MDAxZSfT67CNvrFuML5tRriGILVj9jY45YhdU1PSXHUwMDE2XHUwMDA1NThcdTAwMTPlMM+6r3yi98HjlJFKXG6KPL6/PHvxX/difLpeXHUwMDA0zlmt4Xl4MVx1MDAwNvf7v72YnK5cdTAwMTdccmxM7+KkMEv2y0V6Z3DoO1Ix7nSehFx1MDAxNq7UtpCK5UxcdTAwMTBcdTAwMWVcdTAwMWT3OsBf/57mVlx1MDAwYs5yJ1x1MDAxY+KQ9UJWO8Psf4oubGRcdTAwMWSiqWVFmFx1MDAwMPXnlVpcdTAwMWTgQ2f3l0ngTFx1MDAxYSO/gG6P6Fx1MDAxNTlcdTAwMGbhM1x1MDAxOcLReVxiuyFcZsRPaVxi7dtcdTAwMWJcdTAwMDWRul5cdTAwMWErrFxytyOJVlxm1q+fMH3mXHUwMDFm9yX1I+xMXHUwMDBm7pZ2+4v9Iz9vTG8ifQ==<!-- payload-end --></metadata><defs><style class="style-fonts">
@font-face { font-family: Excalifont; src: url(data:font/woff2;base64,d09GMgABAAAAAAf0AA4AAAAADbQAAAegAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGhYbgjgcNAZgAHwRCAqPYItcCxoAATYCJAMwBCAFgxgHIBujClGUblKX7Edh3Pg8OJbt5MlDFPwWk6FmJ08terdayeDx9O8Fa5eunTJ1ykAUGuAvgOF5bPfuH0nXrvQrNkFoNlERBLIQDd/mj2kP7fdg8avL4M/sm4cbu4L2i1VQVjShz/+90/YE3IJFmOC+f0Z1Xhs3bVxiJZZGa1IgY5sHCJ2EM2iBndkLWsZvFxCAhRBGCknKEOQenHCpmE2fVQTl1atzWyjvzs3aQPnWd20PJQZeG3mQp1lnLimFBAhOKFIQSrGa+vOB69gLCRkjLr4NIjd5rXiz8hhu+LIe9DZCgkJjmZEy34MkhXNBJ8GhNMMnJUgmSYA0AowAwT2zRor05iZAeN/80c4UkYlgSTOaKgOQCUOSE5F+z6+DXqndDTk4CEHP2v3/aKw7lXQXgAUAQP5+kgIpTYMHT9MEjIyWxJ5V5jAzgnrNtNBKWx101tXee86rA5O1f1fiIzEtJsS4GBMjYlikIhEvwwXDjYgiApUFdjgCFAA1m2GorwgdA5a6fzxxX0ei+Eqw6MspHfbmte7eLSh2FSEREgnySedVN+VfsbKAVQygCrGALXgDQoSA0kZqfB2OiyyaZolc/i3tnXswySlmifL9eo1ReyWQcYadAK/DD9c80uaI4pamykJM2hp/MF2uj+teZm9LiRlrltgBlcVYlWVWf+WPTpVJj40Kz9Gvi5p21Zcov8bKEL+5Uirz+j2Hjzr+99zosad2XtbpfsbKgyIQ3RJHxrTH1ysQQAjTUaii13B+OTtxrFoLubjpMYtit2uul6DdSPvKaQl/9FkppthYCXEvjoEdStVVjti2fUQf34q9YjychmIsxCAjKkj0GW4mIhlX1snPOEVRVq4UMRQwkzJNhK143IsBOlghz9meJAGf4apbAmYRMj2hsj5X1OGDPuIKXoOyN8yIMYbeusXTa9f4a+1UvYG9FgQxzoQBUoSPD5/ezAuQIP8s8IyVrUGrwSxC95WPAdlx3EkkQq5yNjcaKAAZ4LEHheIMvgHV3fe6X41/6rzCiMBwSxrnvl9RcR0gK5fy4jDVigBySy99iwlppT9aTlWIMSMKBx9WkbupSbZXNC/CKUC6z53jA0mS7340zruutV7xYguW6p/01tYHOQZQ7ptAagFiLAzFWqyWUwN8/eNGSyn2elPguvF9At0XUNlNv4t202nq+EnkKvdW6aLVlJUxhpXYUqMsn5svp9eoTpvKKf6BY8QwAkaJWYSggEuximEYiCVE0TjN5ojkVoznAP0GytsVT1SesrOn/s8LsMpVpXtSc6To4sT6XIl1q0jH96rd5/g5vmi35+y6eeYPNqjBGHtX0nKFGywfPr19aO9Gh/Q1sWZGuqckOoOJmlK2pSPTSgm/46h389ap+VmOnxsPdspP82Xyy2l7u6i2908pVjRkDbEddOKOprOPJ/94eJ59fi6jDtB6VbzuwXb5puJcqcKnUeM72fFnIlssAv/Z6uMq2s1Jd8qpd7DZo538w5PG7cHm2TU2TD41PjYiaUSikYspa/kjJ84AdgqXsT226/L+fW6j99QmJZlWN99tfcdbUPO17tLu8+gVw73h3OfmurdS9BCa5twNfokJhx7/agK3fHC9rXVewbT0kZG9vXqRBOtS6xGOZ5dC3pwleZIyidL1le+xC3btOlwJkXqGESosoDUG2TqcuE/6OcolnOykkpKdD+5l7+JODWBpAycPke+STDMRWidsWCLvfQJbKM7JpnkLG1WV73qjg6eZ1EMVF1NgG5rTaZF2r7tJNs2VzabNrTQfhSOtgxRxRLP5mldJB6/b6R5LWk9ss9nNUGyTTx8iytdCS9xlAlvtN/1hTrYObLVe5T8zLWNk2oko/bq0dv1cvjXrGC5k+pfYm+L1MtXCFsSzKDTY/dhu66JJWqlfkIxw7T5ufjDBa4pCy7ObPC0cojzlZMlCh2VjKwfX8qy5cpyRt1hp6DeKs/UZjc6zfGxC0rJHl1IUtIm2p7Tg+6VKn8vbfWR9fHljiebwTXPXTQ13HmoSbMssDsa32NnSfmmoH92xuU/E7U2qS8vWYFD9D73VJodqvWU8l7tB+b9ZdPiXvC2qoboerfoamG78+oOX7uWn1/WJy87rJPSHcVmUO7sN0ypzcG5zdfzU8k/NSh5OCbHvUSJlOk2xGrkmubA+nV7YtUCdoJk2vZ/dNSvrAkFnE0toKlC7rHllSrrOzt4Yb91LaLga9TtY+etg/uCpE8rVr604yv7YaE2x450wAACIfUvVNvFrrWXyNylLvwSAR70CtQDwePGbPuLQ/32Y10wCAClKi8AXKzralCj+/e0Dwri9RW2ArkwEqBW+cYJzghDkKyuPEB0FElKG8DIXEQkASS4s/2PSN2DGs1Bi6eh9EhVoZDAH0NblyEbw9tsoChtttEDDbQxvDTZWKm+YcYcDOj01qtdWK8110F5XIQq+nMJuwnpSopmb7KJVWbUgQqhwkyQxlOd76aglFYEKoxAJ8OeFkYPjBPTuQejsbXStQgY5klp1cr1LoQHSUa8StKqVbKnrXAqNAgg8wm1EB6RBL7ejLWnSmSI9hGJQZdAW2trTXRJoBsJm6M5VNQlFM3yJ/7EAAAA=); }
@font-face { font-family: Excalifont; src: url(data:font/woff2;base64,d09GMgABAAAAAAHcAA0AAAAAA9gAAAGMAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGx4cNAZgAAQRCAoAKgsEAAE2AiQDBAQgBYMYByAbHQPIrgp4MjSeIQLqpl4jnPFQwveqm205gmrZevZ2/8kKGaJ6ictBuCwMMnsKo8hC4RwaLBrJ7dXaFrFk0Re1e/Dk3t9wCUVrYAiZkgiZ5rGDVFdY04wBF4APOCzXHr/ljrNIDqLAY/slkQf0SyjxLNBaAq8Z1AJqYW5j0GjbckRLQhoJCgwYoyfY3q33QS5DrSAwYB4hFruNve4WoN2On4P29GEFWsC+nVM/UvSlVwIcsWycI/5gbqfUhE/9Q2P7UppTOAVeIR4TYIocELVKIEACZJgXCJ5GXl0o+esbFP3g+763DcDPx6/60MrnBaBPAsGPUDRC/8+FGIqGUSBA+NIJOwTWe13HRCIrgEvfRhj1RjLiE42eG7I5DIpVNuqkNNhxwaItTI2srRz4dfHGjhZoO0O8nb2pilFYQKhenFaycLUxsYcoAQRyoRBEnPvcgfysPq+npClt8UxLyvHWjaudqbGJA+TCckNECBGBGFforuTs0M4CUMbCAvp+P4in4o864XECREBtFQAAAAA=); }
@font-face { font-family: Nunito; src: url(data:font/woff2;base64,d09GMgABAAAAAAIsAA4AAAAABLQAAAHYAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGx4cLgZgP1NUQVREAAQRCAoAKgsEAAE2AiQDBAQgBYQkByAb5wPIrgp4Mt6IIcJZFNCfWmZY6KzqazTt6AiVHCFawzJ7V0CQBKoSQQEpFKrCVxiSFbqqqsaTevKs/q7s6uBYmujMyA9wxb6a7XnEOcNnDLgG4nW8PoPzHzYpS2uqp51pL3eB/xZoxIllCQc80B9o4j/4xMbxQB+j+SC3hsm6JmI8RMaHj+aJApW6ZbkXlg4vXSE5FECg0og6LzxP9pOarug4tF1RLpbHeZqLX0pIt2mfy3pNG6eyGaRIjrnrr/gv2c//yGdjpJ/7DuJLin5eIZRLaObBMM/NpYpuXJ8z3SE088mEFANcCARESfwChCioAwESsVxBgeyxp6+vZ3Xzv8uz7Ae8tRk+p6RUTPOR7BmlEgh+STsAimNuKibilyluhBNe5/wCACSBKrcykVfgyb+RYcK/TGq9yMyCt3bOskSnR1FqTLMSVBsMGLQVltDKKw2IYA+wcGxHJCINY9mDOCaN4IZEo1ChY4xNg8DaB0RxDqnbMNjXJJRzF9iInqahtm67M8dOHFvXaYbn+wrGxKG3YZI+2V4CW58ovdfV1tFHXFJJiPH7T1FAJxEgYo5BKkA5iDIVQluOqZYWhQapGF6TAFhaFAAoTBIZsCFHi/0oV3gpVKwbAA==); }