diff --git a/excalidraw-app/App.tsx b/excalidraw-app/App.tsx index 9ba64e1df9..d87c345256 100644 --- a/excalidraw-app/App.tsx +++ b/excalidraw-app/App.tsx @@ -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, diff --git a/excalidraw-app/app_constants.ts b/excalidraw-app/app_constants.ts index e4370e7986..a856eb74c1 100644 --- a/excalidraw-app/app_constants.ts +++ b/excalidraw-app/app_constants.ts @@ -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", diff --git a/excalidraw-app/collab/Collab.tsx b/excalidraw-app/collab/Collab.tsx index 87bf7fb1ad..2e2309c328 100644 --- a/excalidraw-app/collab/Collab.tsx +++ b/excalidraw-app/collab/Collab.tsx @@ -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 { 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 { toBrandedType( decryptedData.payload.elements, ), + getSchemaVersion(decryptedData.payload), ), ); break; @@ -753,15 +757,27 @@ class Collab extends PureComponent { 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, diff --git a/excalidraw-app/collab/Portal.tsx b/excalidraw-app/collab/Portal.tsx index 2e076a661e..11b4d32062 100644 --- a/excalidraw-app/collab/Portal.tsx +++ b/excalidraw-app/collab/Portal.tsx @@ -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, }, }; diff --git a/excalidraw-app/data/LocalData.ts b/excalidraw-app/data/LocalData.ts index d09d8aa88e..dc58167217 100644 --- a/excalidraw-app/data/LocalData.ts +++ b/excalidraw-app/data/LocalData.ts @@ -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), diff --git a/excalidraw-app/data/firebase.ts b/excalidraw-app/data/firebase.ts index 11177d90aa..722e60c6ec 100644 --- a/excalidraw-app/data/firebase.ts +++ b/excalidraw-app/data/firebase.ts @@ -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), }), ); diff --git a/excalidraw-app/data/index.ts b/excalidraw-app/data/index.ts index bf765941a2..2946196743 100644 --- a/excalidraw-app/data/index.ts +++ b/excalidraw-app/data/index.ts @@ -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( diff --git a/excalidraw-app/data/localStorage.ts b/excalidraw-app/data/localStorage.ts index 28c166cd74..c5294d7223 100644 --- a/excalidraw-app/data/localStorage.ts +++ b/excalidraw-app/data/localStorage.ts @@ -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 = () => { diff --git a/packages/element/src/__tests__/__snapshots__/transform.test.ts.snap b/packages/element/src/__tests__/__snapshots__/transform.test.ts.snap index 5a59847cc1..afef25eeff 100644 --- a/packages/element/src/__tests__/__snapshots__/transform.test.ts.snap +++ b/packages/element/src/__tests__/__snapshots__/transform.test.ts.snap @@ -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, "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, "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, "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, "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, "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, "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, "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, "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, "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, "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, "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, "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, "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, "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, "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, "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, "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, "strokeColor": "#1e1e1e", "strokeStyle": "solid", @@ -897,7 +879,6 @@ exports[`Test Transform > should transform linear elements 1`] = ` ], "roughness": 1, "roundness": null, - "schemaVersion": 1, "seed": Any, "startArrowhead": null, "startBinding": null, @@ -945,7 +926,6 @@ exports[`Test Transform > should transform linear elements 2`] = ` ], "roughness": 1, "roundness": null, - "schemaVersion": 1, "seed": Any, "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, "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, "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, "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, "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, "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, "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, "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, "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, "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, "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, "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, "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, "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, "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, "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, "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, "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, "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, "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, "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, "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, "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, "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, "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, "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, "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, "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, "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, "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, "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, "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, "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, "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, "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, "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, "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, "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, "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, "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, "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, "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, "strokeColor": "#c2255c", "strokeStyle": "solid", diff --git a/packages/element/src/__tests__/versioning.test.ts b/packages/element/src/__tests__/versioning.test.ts index b10f9e4b7e..f310179d4d 100644 --- a/packages/element/src/__tests__/versioning.test.ts +++ b/packages/element/src/__tests__/versioning.test.ts @@ -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" }); + }); }); diff --git a/packages/element/src/delta.ts b/packages/element/src/delta.ts index 1f26da07c8..6da3cf5062 100644 --- a/packages/element/src/delta.ts +++ b/packages/element/src/delta.ts @@ -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 { return delta; } - public static restore(elementsDeltaDTO: DTO): ElementsDelta { + public static restore( + elementsDeltaDTO: DTO, + 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>, + fromVersion: number, + ): Record> { + 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>); } private static satisfiesAddition = ({ diff --git a/packages/element/src/newElement.ts b/packages/element/src/newElement.ts index fe1a982360..db509ba774 100644 --- a/packages/element/src/newElement.ts +++ b/packages/element/src/newElement.ts @@ -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 = ( 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(), diff --git a/packages/element/src/store.ts b/packages/element/src/store.ts index 38235e752c..b4ac545822 100644 --- a/packages/element/src/store.ts +++ b/packages/element/src/store.ts @@ -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) { 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) { - const elements = ElementsDelta.create(added, removed, updated); + public static load(storeDeltaDTO: DTO) { + 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); diff --git a/packages/element/src/types.ts b/packages/element/src/types.ts index b875ba649f..100b13db5b 100644 --- a/packages/element/src/types.ts +++ b/packages/element/src/types.ts @@ -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`. diff --git a/packages/element/src/versioning.ts b/packages/element/src/versioning.ts index 4f49a5a1c7..5502cd5990 100644 --- a/packages/element/src/versioning.ts +++ b/packages/element/src/versioning.ts @@ -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 = (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 = { - 0: (element) => element, +const migrations: Record = { + 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 = ( - 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 = ( + 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 = (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 = ( + 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 = (partial: T, fromVersion: number): T => + fromVersion >= CURRENT_SCHEMA_VERSION + ? partial + : runMigrations("elementPartial", partial, fromVersion); diff --git a/packages/excalidraw/clipboard.ts b/packages/excalidraw/clipboard.ts index 5075343f65..914a905568 100644 --- a/packages/excalidraw/clipboard.ts +++ b/packages/excalidraw/clipboard.ts @@ -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) diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx index 0e573219f1..52a70f5160 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -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 { 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 { 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 { 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); diff --git a/packages/excalidraw/components/PublishLibrary.tsx b/packages/excalidraw/components/PublishLibrary.tsx index c9376895da..e7a4dd0ee5 100644 --- a/packages/excalidraw/components/PublishLibrary.tsx +++ b/packages/excalidraw/components/PublishLibrary.tsx @@ -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, }; diff --git a/packages/excalidraw/data/blob.ts b/packages/excalidraw/data/blob.ts index 05fb47b7bb..09a488edfd 100644 --- a/packages/excalidraw/data/blob.ts +++ b/packages/excalidraw/data/blob.ts @@ -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 ( diff --git a/packages/excalidraw/data/json.ts b/packages/excalidraw/data/json.ts index a49d58cd02..feee628498 100644 --- a/packages/excalidraw/data/json.ts +++ b/packages/excalidraw/data/json.ts @@ -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, }; diff --git a/packages/excalidraw/data/library.ts b/packages/excalidraw/data/library.ts index abe2fec853..1bed9ea02c 100644 --- a/packages/excalidraw/data/library.ts +++ b/packages/excalidraw/data/library.ts @@ -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; }; -// 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(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 diff --git a/packages/excalidraw/data/restore.ts b/packages/excalidraw/data/restore.ts index c650556bce..8c33c4d63a 100644 --- a/packages/excalidraw/data/restore.ts +++ b/packages/excalidraw/data/restore.ts @@ -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 = ( refreshDimensions?: boolean; repairBindings?: boolean; deleteInvisibleElements?: boolean; + schemaVersion?: number; } | undefined, ): CombineBrandsIfNeeded => { // used to detect duplicate top-level element ids const existingIds = new Set(); - 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); } diff --git a/packages/excalidraw/data/types.ts b/packages/excalidraw/data/types.ts index 94947c2b98..615a59b29d 100644 --- a/packages/excalidraw/data/types.ts +++ b/packages/excalidraw/data/types.ts @@ -14,6 +14,7 @@ import type { export interface ExportedDataState { type: string; version: number; + schemaVersion: number; source: string; elements: readonly ExcalidrawElement[]; appState: ReturnType; @@ -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; } diff --git a/packages/excalidraw/tests/__snapshots__/contextmenu.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/contextmenu.test.tsx.snap index 992bddb450..db15bfd23d 100644 --- a/packages/excalidraw/tests/__snapshots__/contextmenu.test.tsx.snap +++ b/packages/excalidraw/tests/__snapshots__/contextmenu.test.tsx.snap @@ -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, }, ] `; diff --git a/packages/excalidraw/tests/__snapshots__/dragCreate.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/dragCreate.test.tsx.snap index 4cef993184..a538500c25 100644 --- a/packages/excalidraw/tests/__snapshots__/dragCreate.test.tsx.snap +++ b/packages/excalidraw/tests/__snapshots__/dragCreate.test.tsx.snap @@ -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", diff --git a/packages/excalidraw/tests/__snapshots__/export.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/export.test.tsx.snap index 566e1cf97c..a8c8bcc461 100644 --- a/packages/excalidraw/tests/__snapshots__/export.test.tsx.snap +++ b/packages/excalidraw/tests/__snapshots__/export.test.tsx.snap @@ -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`] = ` -"eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nHVTwW7bMFxmve8rXGb3WqxOiu6QW7u2aFx1MDAwZu2hXHUwMDE5tkOxg2oxNlx1MDAxMUVcdTAwMTIkuklcdTAwMTZcdTAwMDTYZ+zWX+wnlFI8K3bSXHUwMDA0MKBHUnx8fNp8ybKc1lx1MDAxNvJJlsOqXHUwMDE0XG6lXHUwMDEzy/w04K/gPFx1MDAxYc2hcTx707gyZtZEdnJ2plxmXHUwMDE31MbT5Lwoil1cdTAwMTEoWIAmz2nPfM6yTfxyXHUwMDA0ZSi9jGlcdTAwMTH435dgRVx0XTFUdKd177RESTUjo29cdTAwMWRUXHUwMDAzVjX1MaErXHUwMDA1vUJPzszhu1HGhY4nI1xi/9T0RZTzyplGyy6HnNDeXG7Hw6S8XHUwMDE5KjWldbyd9WC18kGPXy3F8Vx1MDAwMP+siptWtVx1MDAwNlx1MDAxZlx1MDAwNFx1MDAxYnWosaJEXG7Dj4o0RWBo72XU9nfi5MRcdTAwMDLug7i6UaqDUUtYXHLBOGLbrVx1MDAxN/BcdTAwMDCyxyAt/1x1MDAwMHs0ulx1MDAxY8hb1rBcdTAwMTA/j5Sgv2ZHULx8JpSHJHmgcpPc0qPTWClowEihnlx1MDAwZvPYgfMjd0dHsdbvb//+7i3PaJrin0B9XFz00FuxQFx1MDAxNdS+6F1xqbBcbuPkXG5meyZgXHUwMDE5XGLZ+V2YjE3Rku9cdTAwMTOowVx1MDAxZK7EOKxQXHUwMDBi9eMoPdGQeVx1MDAwMr8jSK6B/cnhrvP51/FFXGZs+Vx1MDAxYl2QXHUwMDBia6fEenF099rYKShcdTAwMDej7jBcdTAwMDKbxozQg5Fwo8WLXHUwMDFh6pi/XCIsr1x1MDAwZVx1MDAxZsbJLP5a4u1cblx1MDAxZVx1MDAxYUU45VWXxFx1MDAxNlxi+9xcdTAwMDR+28iPX1xytND2XHUwMDAzo9gvdiJ9