fix: keep schema hints across clipboard roundtrips
This commit is contained in:
+12
-13
@@ -55,7 +55,6 @@ import {
|
|||||||
} from "@excalidraw/excalidraw/data/restore";
|
} from "@excalidraw/excalidraw/data/restore";
|
||||||
import {
|
import {
|
||||||
migrateSceneElements,
|
migrateSceneElements,
|
||||||
resolveSchemaVersion,
|
|
||||||
SCHEMA_VERSIONS,
|
SCHEMA_VERSIONS,
|
||||||
} from "@excalidraw/excalidraw/data/schema";
|
} from "@excalidraw/excalidraw/data/schema";
|
||||||
import { newElementWith } from "@excalidraw/element";
|
import { newElementWith } from "@excalidraw/element";
|
||||||
@@ -244,10 +243,10 @@ const initializeScene = async (opts: {
|
|||||||
elements: restoreElements(
|
elements: restoreElements(
|
||||||
migrateSceneElements(
|
migrateSceneElements(
|
||||||
localDataState?.elements,
|
localDataState?.elements,
|
||||||
resolveSchemaVersion(
|
{
|
||||||
localDataState?.schemaVersion,
|
payloadSchemaVersion: localDataState?.schemaVersion,
|
||||||
SCHEMA_VERSIONS.initial,
|
fallbackVersion: SCHEMA_VERSIONS.initial,
|
||||||
),
|
},
|
||||||
),
|
),
|
||||||
null,
|
null,
|
||||||
{
|
{
|
||||||
@@ -280,10 +279,10 @@ const initializeScene = async (opts: {
|
|||||||
restoreElements(
|
restoreElements(
|
||||||
migrateSceneElements(
|
migrateSceneElements(
|
||||||
imported.elements,
|
imported.elements,
|
||||||
resolveSchemaVersion(
|
{
|
||||||
imported.schemaVersion,
|
payloadSchemaVersion: imported.schemaVersion,
|
||||||
SCHEMA_VERSIONS.initial,
|
fallbackVersion: SCHEMA_VERSIONS.initial,
|
||||||
),
|
},
|
||||||
),
|
),
|
||||||
null,
|
null,
|
||||||
{
|
{
|
||||||
@@ -576,10 +575,10 @@ const ExcalidrawWrapper = () => {
|
|||||||
const username = importUsernameFromLocalStorage();
|
const username = importUsernameFromLocalStorage();
|
||||||
const migratedElements = migrateSceneElements(
|
const migratedElements = migrateSceneElements(
|
||||||
localDataState?.elements,
|
localDataState?.elements,
|
||||||
resolveSchemaVersion(
|
{
|
||||||
localDataState?.schemaVersion,
|
payloadSchemaVersion: localDataState?.schemaVersion,
|
||||||
SCHEMA_VERSIONS.initial,
|
fallbackVersion: SCHEMA_VERSIONS.initial,
|
||||||
),
|
},
|
||||||
);
|
);
|
||||||
setLangCode(getPreferredLanguage());
|
setLangCode(getPreferredLanguage());
|
||||||
excalidrawAPI.updateScene({
|
excalidrawAPI.updateScene({
|
||||||
|
|||||||
@@ -55,7 +55,13 @@ describe("parseClipboard()", () => {
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
expect(clipboardData.elements).toEqual([rect]);
|
expect(clipboardData.elements).toEqual([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: rect.id,
|
||||||
|
type: rect.type,
|
||||||
|
schemaVersion: SCHEMA_VERSIONS.latest,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
expect(clipboardData.schemaVersion).toBe(SCHEMA_VERSIONS.latest);
|
expect(clipboardData.schemaVersion).toBe(SCHEMA_VERSIONS.latest);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -75,7 +81,13 @@ describe("parseClipboard()", () => {
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
expect(clipboardData.elements).toEqual([rect]);
|
expect(clipboardData.elements).toEqual([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: rect.id,
|
||||||
|
type: rect.type,
|
||||||
|
schemaVersion: SCHEMA_VERSIONS.latest,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
json = serializeAsClipboardJSON({ elements: [rect], files: null });
|
json = serializeAsClipboardJSON({ elements: [rect], files: null });
|
||||||
clipboardData = await parseClipboard(
|
clipboardData = await parseClipboard(
|
||||||
@@ -87,10 +99,42 @@ describe("parseClipboard()", () => {
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
expect(clipboardData.elements).toEqual([rect]);
|
expect(clipboardData.elements).toEqual([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: rect.id,
|
||||||
|
type: rect.type,
|
||||||
|
schemaVersion: SCHEMA_VERSIONS.latest,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should preserve per-element schema when payload schema is missing", async () => {
|
||||||
|
const rect = API.createElement({ type: "rectangle" });
|
||||||
|
const clipboardPayload = JSON.parse(
|
||||||
|
serializeAsClipboardJSON({ elements: [rect], files: null }),
|
||||||
|
);
|
||||||
|
delete clipboardPayload.schemaVersion;
|
||||||
|
|
||||||
|
const clipboardData = await parseClipboard(
|
||||||
|
await parseDataTransferEvent(
|
||||||
|
createPasteEvent({
|
||||||
|
types: {
|
||||||
|
"text/plain": JSON.stringify(clipboardPayload),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(clipboardData.schemaVersion).toBeUndefined();
|
||||||
|
expect(clipboardData.elements?.[0]).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
id: rect.id,
|
||||||
|
schemaVersion: SCHEMA_VERSIONS.latest,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it("should parse <image> `src` urls out of text/html", async () => {
|
it("should parse <image> `src` urls out of text/html", async () => {
|
||||||
let clipboardData;
|
let clipboardData;
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -45,7 +45,9 @@ import { SCHEMA_VERSIONS } from "./data/schema";
|
|||||||
type ElementsClipboard = {
|
type ElementsClipboard = {
|
||||||
type: typeof EXPORT_DATA_TYPES.excalidrawClipboard;
|
type: typeof EXPORT_DATA_TYPES.excalidrawClipboard;
|
||||||
schemaVersion: number;
|
schemaVersion: number;
|
||||||
elements: readonly NonDeletedExcalidrawElement[];
|
elements: readonly (NonDeletedExcalidrawElement & {
|
||||||
|
schemaVersion?: number;
|
||||||
|
})[];
|
||||||
files: BinaryFiles | undefined;
|
files: BinaryFiles | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -187,18 +189,22 @@ export const serializeAsClipboardJSON = ({
|
|||||||
type: EXPORT_DATA_TYPES.excalidrawClipboard,
|
type: EXPORT_DATA_TYPES.excalidrawClipboard,
|
||||||
schemaVersion: SCHEMA_VERSIONS.latest,
|
schemaVersion: SCHEMA_VERSIONS.latest,
|
||||||
elements: elements.map((element) => {
|
elements: elements.map((element) => {
|
||||||
|
const elementWithSchema = {
|
||||||
|
...element,
|
||||||
|
schemaVersion: SCHEMA_VERSIONS.latest,
|
||||||
|
};
|
||||||
if (
|
if (
|
||||||
getContainingFrame(element, elementsMap) &&
|
getContainingFrame(element, elementsMap) &&
|
||||||
!framesToCopy.has(getContainingFrame(element, elementsMap)!)
|
!framesToCopy.has(getContainingFrame(element, elementsMap)!)
|
||||||
) {
|
) {
|
||||||
const copiedElement = deepCopyElement(element);
|
const copiedElement = deepCopyElement(elementWithSchema);
|
||||||
mutateElement(copiedElement, elementsMap, {
|
mutateElement(copiedElement, elementsMap, {
|
||||||
frameId: null,
|
frameId: null,
|
||||||
});
|
});
|
||||||
return copiedElement;
|
return copiedElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
return element;
|
return elementWithSchema;
|
||||||
}),
|
}),
|
||||||
files: files ? _files : undefined,
|
files: files ? _files : undefined,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -360,8 +360,8 @@ import {
|
|||||||
migrateClipboardElements,
|
migrateClipboardElements,
|
||||||
migrateLibraryElements,
|
migrateLibraryElements,
|
||||||
migrateSceneElements,
|
migrateSceneElements,
|
||||||
resolveSchemaVersion,
|
|
||||||
SCHEMA_VERSIONS,
|
SCHEMA_VERSIONS,
|
||||||
|
type SchemaVersionSource,
|
||||||
type SchemaMigrationScope,
|
type SchemaMigrationScope,
|
||||||
} from "../data/schema";
|
} from "../data/schema";
|
||||||
import { getCenter, getDistance } from "../gesture";
|
import { getCenter, getDistance } from "../gesture";
|
||||||
@@ -2335,7 +2335,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
elements,
|
elements,
|
||||||
position: "center",
|
position: "center",
|
||||||
files: null,
|
files: null,
|
||||||
schemaVersion: SCHEMA_VERSIONS.latest,
|
schemaVersionSource: SCHEMA_VERSIONS.latest,
|
||||||
migrationScope: "api",
|
migrationScope: "api",
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -2824,10 +2824,10 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
const restoredElements = restoreElements(
|
const restoredElements = restoreElements(
|
||||||
migrateSceneElements(
|
migrateSceneElements(
|
||||||
initialData?.elements,
|
initialData?.elements,
|
||||||
resolveSchemaVersion(
|
{
|
||||||
initialData?.schemaVersion,
|
payloadSchemaVersion: initialData?.schemaVersion,
|
||||||
initialDataSchemaFallback,
|
fallbackVersion: initialDataSchemaFallback,
|
||||||
),
|
},
|
||||||
),
|
),
|
||||||
null,
|
null,
|
||||||
{
|
{
|
||||||
@@ -3599,9 +3599,12 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.addElementsFromPasteOrLibrary({
|
this.addElementsFromPasteOrLibrary({
|
||||||
elements,
|
elements,
|
||||||
files: data.files || null,
|
files: data.files || null,
|
||||||
schemaVersion: data.programmaticAPI
|
schemaVersionSource: data.programmaticAPI
|
||||||
? SCHEMA_VERSIONS.latest
|
? SCHEMA_VERSIONS.latest
|
||||||
: resolveSchemaVersion(data.schemaVersion, SCHEMA_VERSIONS.initial),
|
: {
|
||||||
|
payloadSchemaVersion: data.schemaVersion,
|
||||||
|
fallbackVersion: SCHEMA_VERSIONS.initial,
|
||||||
|
},
|
||||||
migrationScope: data.programmaticAPI ? "api" : "clipboard",
|
migrationScope: data.programmaticAPI ? "api" : "clipboard",
|
||||||
position:
|
position:
|
||||||
this.editorInterface.formFactor === "desktop" ? "cursor" : "center",
|
this.editorInterface.formFactor === "desktop" ? "cursor" : "center",
|
||||||
@@ -3629,7 +3632,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.addElementsFromPasteOrLibrary({
|
this.addElementsFromPasteOrLibrary({
|
||||||
elements,
|
elements,
|
||||||
files,
|
files,
|
||||||
schemaVersion: SCHEMA_VERSIONS.latest,
|
schemaVersionSource: SCHEMA_VERSIONS.latest,
|
||||||
migrationScope: "api",
|
migrationScope: "api",
|
||||||
position:
|
position:
|
||||||
this.editorInterface.formFactor === "desktop" ? "cursor" : "center",
|
this.editorInterface.formFactor === "desktop" ? "cursor" : "center",
|
||||||
@@ -3748,22 +3751,18 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
addElementsFromPasteOrLibrary = (opts: {
|
addElementsFromPasteOrLibrary = (opts: {
|
||||||
elements: readonly ExcalidrawElement[];
|
elements: readonly ExcalidrawElement[];
|
||||||
files: BinaryFiles | null;
|
files: BinaryFiles | null;
|
||||||
schemaVersion?: number;
|
schemaVersionSource: SchemaVersionSource;
|
||||||
migrationScope: SchemaMigrationScope;
|
migrationScope: SchemaMigrationScope;
|
||||||
position: { clientX: number; clientY: number } | "cursor" | "center";
|
position: { clientX: number; clientY: number } | "cursor" | "center";
|
||||||
retainSeed?: boolean;
|
retainSeed?: boolean;
|
||||||
fitToContent?: boolean;
|
fitToContent?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const schemaVersion = resolveSchemaVersion(
|
|
||||||
opts.schemaVersion,
|
|
||||||
SCHEMA_VERSIONS.latest,
|
|
||||||
);
|
|
||||||
const migratedElements =
|
const migratedElements =
|
||||||
opts.migrationScope === "clipboard"
|
opts.migrationScope === "clipboard"
|
||||||
? migrateClipboardElements(opts.elements, schemaVersion)
|
? migrateClipboardElements(opts.elements, opts.schemaVersionSource)
|
||||||
: opts.migrationScope === "library"
|
: opts.migrationScope === "library"
|
||||||
? migrateLibraryElements(opts.elements, schemaVersion)
|
? migrateLibraryElements(opts.elements, opts.schemaVersionSource)
|
||||||
: migrateAPIElements(opts.elements, schemaVersion);
|
: migrateAPIElements(opts.elements, opts.schemaVersionSource);
|
||||||
|
|
||||||
const elements = restoreElements(
|
const elements = restoreElements(
|
||||||
migratedElements,
|
migratedElements,
|
||||||
@@ -11540,7 +11539,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
elements: distributeLibraryItemsOnSquareGrid(libraryItems),
|
elements: distributeLibraryItemsOnSquareGrid(libraryItems),
|
||||||
position: event,
|
position: event,
|
||||||
files: null,
|
files: null,
|
||||||
schemaVersion: SCHEMA_VERSIONS.latest,
|
schemaVersionSource: SCHEMA_VERSIONS.latest,
|
||||||
migrationScope: "library",
|
migrationScope: "library",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -149,7 +149,7 @@ export const insertToEditor = ({
|
|||||||
app.addElementsFromPasteOrLibrary({
|
app.addElementsFromPasteOrLibrary({
|
||||||
elements: newElements,
|
elements: newElements,
|
||||||
files,
|
files,
|
||||||
schemaVersion: SCHEMA_VERSIONS.latest,
|
schemaVersionSource: SCHEMA_VERSIONS.latest,
|
||||||
migrationScope: "api",
|
migrationScope: "api",
|
||||||
position: "center",
|
position: "center",
|
||||||
fitToContent: true,
|
fitToContent: true,
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ import {
|
|||||||
} from "./restore";
|
} from "./restore";
|
||||||
import {
|
import {
|
||||||
migrateSceneElements,
|
migrateSceneElements,
|
||||||
resolveSchemaVersion,
|
|
||||||
SCHEMA_VERSIONS,
|
SCHEMA_VERSIONS,
|
||||||
} from "./schema";
|
} from "./schema";
|
||||||
|
|
||||||
@@ -162,13 +161,12 @@ export const loadSceneOrLibraryFromBlob = async (
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
if (isValidExcalidrawData(data)) {
|
if (isValidExcalidrawData(data)) {
|
||||||
const schemaVersion = resolveSchemaVersion(
|
|
||||||
data.schemaVersion,
|
|
||||||
SCHEMA_VERSIONS.initial,
|
|
||||||
);
|
|
||||||
const migratedElements = migrateSceneElements(
|
const migratedElements = migrateSceneElements(
|
||||||
data.elements,
|
data.elements,
|
||||||
schemaVersion,
|
{
|
||||||
|
payloadSchemaVersion: data.schemaVersion,
|
||||||
|
fallbackVersion: SCHEMA_VERSIONS.initial,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
type: MIME_TYPES.excalidraw,
|
type: MIME_TYPES.excalidraw,
|
||||||
@@ -238,7 +236,10 @@ export const parseLibraryJSON = (
|
|||||||
return restoreLibraryItems(
|
return restoreLibraryItems(
|
||||||
libraryItems,
|
libraryItems,
|
||||||
defaultStatus,
|
defaultStatus,
|
||||||
resolveSchemaVersion(data.schemaVersion, SCHEMA_VERSIONS.initial),
|
{
|
||||||
|
payloadSchemaVersion: data.schemaVersion,
|
||||||
|
fallbackVersion: SCHEMA_VERSIONS.initial,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ import { t } from "../i18n";
|
|||||||
|
|
||||||
import { loadLibraryFromBlob } from "./blob";
|
import { loadLibraryFromBlob } from "./blob";
|
||||||
import { restoreLibraryItems } from "./restore";
|
import { restoreLibraryItems } from "./restore";
|
||||||
import { resolveSchemaVersion, SCHEMA_VERSIONS } from "./schema";
|
import { SCHEMA_VERSIONS } from "./schema";
|
||||||
|
|
||||||
import type App from "../components/App";
|
import type App from "../components/App";
|
||||||
|
|
||||||
@@ -564,7 +564,10 @@ class AdapterTransaction {
|
|||||||
restoreLibraryItems(
|
restoreLibraryItems(
|
||||||
data?.libraryItems || [],
|
data?.libraryItems || [],
|
||||||
"published",
|
"published",
|
||||||
resolveSchemaVersion(data?.schemaVersion, SCHEMA_VERSIONS.initial),
|
{
|
||||||
|
payloadSchemaVersion: data?.schemaVersion,
|
||||||
|
fallbackVersion: SCHEMA_VERSIONS.initial,
|
||||||
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
@@ -877,10 +880,10 @@ export const useHandleLibrary = (
|
|||||||
restoredData = restoreLibraryItems(
|
restoredData = restoreLibraryItems(
|
||||||
libraryData.libraryItems || [],
|
libraryData.libraryItems || [],
|
||||||
"published",
|
"published",
|
||||||
resolveSchemaVersion(
|
{
|
||||||
libraryData.schemaVersion,
|
payloadSchemaVersion: libraryData.schemaVersion,
|
||||||
SCHEMA_VERSIONS.initial,
|
fallbackVersion: SCHEMA_VERSIONS.initial,
|
||||||
),
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// we don't queue this operation because it's running inside
|
// we don't queue this operation because it's running inside
|
||||||
|
|||||||
@@ -81,7 +81,11 @@ import {
|
|||||||
getNormalizedGridStep,
|
getNormalizedGridStep,
|
||||||
getNormalizedZoom,
|
getNormalizedZoom,
|
||||||
} from "../scene";
|
} from "../scene";
|
||||||
import { migrateLibraryElements, SCHEMA_VERSIONS } from "./schema";
|
import {
|
||||||
|
migrateLibraryElements,
|
||||||
|
type SchemaVersionSource,
|
||||||
|
SCHEMA_VERSIONS,
|
||||||
|
} from "./schema";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
AppState,
|
AppState,
|
||||||
@@ -959,12 +963,12 @@ export const restoreAppState = (
|
|||||||
|
|
||||||
const restoreLibraryItem = (
|
const restoreLibraryItem = (
|
||||||
libraryItem: LibraryItem,
|
libraryItem: LibraryItem,
|
||||||
schemaVersion: number,
|
schemaVersionSource: SchemaVersionSource,
|
||||||
) => {
|
) => {
|
||||||
const elements = restoreElements(
|
const elements = restoreElements(
|
||||||
migrateLibraryElements(
|
migrateLibraryElements(
|
||||||
getNonDeletedElements(libraryItem.elements),
|
getNonDeletedElements(libraryItem.elements),
|
||||||
schemaVersion,
|
schemaVersionSource,
|
||||||
),
|
),
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
@@ -974,7 +978,7 @@ const restoreLibraryItem = (
|
|||||||
export const restoreLibraryItems = (
|
export const restoreLibraryItems = (
|
||||||
libraryItems: ImportedDataState["libraryItems"] = [],
|
libraryItems: ImportedDataState["libraryItems"] = [],
|
||||||
defaultStatus: LibraryItem["status"],
|
defaultStatus: LibraryItem["status"],
|
||||||
schemaVersion: number = SCHEMA_VERSIONS.latest,
|
schemaVersionSource: SchemaVersionSource = SCHEMA_VERSIONS.latest,
|
||||||
) => {
|
) => {
|
||||||
const restoredItems: LibraryItem[] = [];
|
const restoredItems: LibraryItem[] = [];
|
||||||
for (const item of libraryItems) {
|
for (const item of libraryItems) {
|
||||||
@@ -985,7 +989,7 @@ export const restoreLibraryItems = (
|
|||||||
elements: item,
|
elements: item,
|
||||||
id: randomId(),
|
id: randomId(),
|
||||||
created: Date.now(),
|
created: Date.now(),
|
||||||
}, schemaVersion);
|
}, schemaVersionSource);
|
||||||
if (restoredItem) {
|
if (restoredItem) {
|
||||||
restoredItems.push(restoredItem);
|
restoredItems.push(restoredItem);
|
||||||
}
|
}
|
||||||
@@ -999,7 +1003,7 @@ export const restoreLibraryItems = (
|
|||||||
id: _item.id || randomId(),
|
id: _item.id || randomId(),
|
||||||
status: _item.status || defaultStatus,
|
status: _item.status || defaultStatus,
|
||||||
created: _item.created || Date.now(),
|
created: _item.created || Date.now(),
|
||||||
}, schemaVersion);
|
}, schemaVersionSource);
|
||||||
if (restoredItem) {
|
if (restoredItem) {
|
||||||
restoredItems.push(restoredItem);
|
restoredItems.push(restoredItem);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -160,4 +160,42 @@ describe("schema migration", () => {
|
|||||||
DEFAULT_ELEMENT_PROPS.backgroundColor,
|
DEFAULT_ELEMENT_PROPS.backgroundColor,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should use per-element schema when payload schema is missing", () => {
|
||||||
|
const frame = API.createElement({
|
||||||
|
type: "frame",
|
||||||
|
backgroundColor: "#ff0000",
|
||||||
|
});
|
||||||
|
const frameFromModernSource = {
|
||||||
|
...frame,
|
||||||
|
schemaVersion: SCHEMA_VERSIONS.latest,
|
||||||
|
} as typeof frame & { schemaVersion: number };
|
||||||
|
|
||||||
|
const migrated = migrateClipboardElements([frameFromModernSource], {
|
||||||
|
payloadSchemaVersion: undefined,
|
||||||
|
fallbackVersion: SCHEMA_VERSIONS.initial,
|
||||||
|
})!;
|
||||||
|
|
||||||
|
expect(migrated[0].backgroundColor).toBe("#ff0000");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should prefer payload schema over per-element schema", () => {
|
||||||
|
const frame = API.createElement({
|
||||||
|
type: "frame",
|
||||||
|
backgroundColor: "#ff0000",
|
||||||
|
});
|
||||||
|
const frameFromModernSource = {
|
||||||
|
...frame,
|
||||||
|
schemaVersion: SCHEMA_VERSIONS.latest,
|
||||||
|
} as typeof frame & { schemaVersion: number };
|
||||||
|
|
||||||
|
const migrated = migrateClipboardElements([frameFromModernSource], {
|
||||||
|
payloadSchemaVersion: SCHEMA_VERSIONS.initial,
|
||||||
|
fallbackVersion: SCHEMA_VERSIONS.latest,
|
||||||
|
})!;
|
||||||
|
|
||||||
|
expect(migrated[0].backgroundColor).toBe(
|
||||||
|
DEFAULT_ELEMENT_PROPS.backgroundColor,
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -76,6 +76,15 @@ export const resolveSchemaVersion = (
|
|||||||
return fallbackVersion;
|
return fallbackVersion;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isValidSchemaVersion = (
|
||||||
|
schemaVersion: number | undefined,
|
||||||
|
): schemaVersion is number => {
|
||||||
|
return (
|
||||||
|
Number.isInteger(schemaVersion) &&
|
||||||
|
(schemaVersion as number) >= SCHEMA_VERSIONS.initial
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const validateSchemaMigrations = (
|
export const validateSchemaMigrations = (
|
||||||
migrations: readonly SchemaMigration[],
|
migrations: readonly SchemaMigration[],
|
||||||
) => {
|
) => {
|
||||||
@@ -171,38 +180,69 @@ export const migrateElementsBySchema = (
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type SchemaVersionSource =
|
||||||
|
| number
|
||||||
|
| {
|
||||||
|
payloadSchemaVersion?: number;
|
||||||
|
fallbackVersion: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const migrateElementsByScope = (
|
||||||
|
elements: readonly ExcalidrawElement[] | null | undefined,
|
||||||
|
scope: SchemaMigrationScope,
|
||||||
|
source: SchemaVersionSource,
|
||||||
|
) => {
|
||||||
|
if (!elements) {
|
||||||
|
return elements;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof source === "number") {
|
||||||
|
return migrateElementsBySchema(elements, {
|
||||||
|
schemaVersion: source,
|
||||||
|
scope,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isValidSchemaVersion(source.payloadSchemaVersion)) {
|
||||||
|
return migrateElementsBySchema(elements, {
|
||||||
|
schemaVersion: source.payloadSchemaVersion,
|
||||||
|
scope,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return elements.map((element) => {
|
||||||
|
const schemaVersion = resolveSchemaVersion(
|
||||||
|
(element as ExcalidrawElement & { schemaVersion?: number }).schemaVersion,
|
||||||
|
source.fallbackVersion,
|
||||||
|
);
|
||||||
|
const migratedElement = migrateElementsBySchema([element], {
|
||||||
|
schemaVersion,
|
||||||
|
scope,
|
||||||
|
});
|
||||||
|
return migratedElement?.[0] || element;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const migrateSceneElements = (
|
export const migrateSceneElements = (
|
||||||
elements: readonly ExcalidrawElement[] | null | undefined,
|
elements: readonly ExcalidrawElement[] | null | undefined,
|
||||||
schemaVersion: number,
|
source: SchemaVersionSource,
|
||||||
) =>
|
) =>
|
||||||
migrateElementsBySchema(elements, {
|
migrateElementsByScope(elements, "scene", source);
|
||||||
schemaVersion,
|
|
||||||
scope: "scene",
|
|
||||||
});
|
|
||||||
|
|
||||||
export const migrateLibraryElements = (
|
export const migrateLibraryElements = (
|
||||||
elements: readonly ExcalidrawElement[] | null | undefined,
|
elements: readonly ExcalidrawElement[] | null | undefined,
|
||||||
schemaVersion: number,
|
source: SchemaVersionSource,
|
||||||
) =>
|
) =>
|
||||||
migrateElementsBySchema(elements, {
|
migrateElementsByScope(elements, "library", source);
|
||||||
schemaVersion,
|
|
||||||
scope: "library",
|
|
||||||
});
|
|
||||||
|
|
||||||
export const migrateClipboardElements = (
|
export const migrateClipboardElements = (
|
||||||
elements: readonly ExcalidrawElement[] | null | undefined,
|
elements: readonly ExcalidrawElement[] | null | undefined,
|
||||||
schemaVersion: number,
|
source: SchemaVersionSource,
|
||||||
) =>
|
) =>
|
||||||
migrateElementsBySchema(elements, {
|
migrateElementsByScope(elements, "clipboard", source);
|
||||||
schemaVersion,
|
|
||||||
scope: "clipboard",
|
|
||||||
});
|
|
||||||
|
|
||||||
export const migrateAPIElements = (
|
export const migrateAPIElements = (
|
||||||
elements: readonly ExcalidrawElement[] | null | undefined,
|
elements: readonly ExcalidrawElement[] | null | undefined,
|
||||||
schemaVersion: number,
|
source: SchemaVersionSource,
|
||||||
) =>
|
) =>
|
||||||
migrateElementsBySchema(elements, {
|
migrateElementsByScope(elements, "api", source);
|
||||||
schemaVersion,
|
|
||||||
scope: "api",
|
|
||||||
});
|
|
||||||
|
|||||||
Reference in New Issue
Block a user