chore: harden local schema hints and tests
This commit is contained in:
+25
-8
@@ -54,6 +54,7 @@ import {
|
||||
restoreElements,
|
||||
} from "@excalidraw/excalidraw/data/restore";
|
||||
import {
|
||||
hasElementSchemaVersion,
|
||||
migrateSceneElements,
|
||||
SCHEMA_VERSIONS,
|
||||
} from "@excalidraw/excalidraw/data/schema";
|
||||
@@ -231,6 +232,17 @@ const initializeScene = async (opts: {
|
||||
const externalUrlMatch = window.location.hash.match(/^#url=(.*)$/);
|
||||
|
||||
const localDataState = importFromLocalStorage();
|
||||
const localStorageSchemaVersionSource = hasElementSchemaVersion(
|
||||
localDataState?.elements,
|
||||
)
|
||||
? {
|
||||
payloadSchemaVersion: undefined,
|
||||
fallbackVersion: SCHEMA_VERSIONS.initial,
|
||||
}
|
||||
: {
|
||||
payloadSchemaVersion: localDataState?.schemaVersion,
|
||||
fallbackVersion: SCHEMA_VERSIONS.initial,
|
||||
};
|
||||
|
||||
let scene: Omit<
|
||||
RestoredDataState,
|
||||
@@ -243,10 +255,7 @@ const initializeScene = async (opts: {
|
||||
elements: restoreElements(
|
||||
migrateSceneElements(
|
||||
localDataState?.elements,
|
||||
{
|
||||
payloadSchemaVersion: localDataState?.schemaVersion,
|
||||
fallbackVersion: SCHEMA_VERSIONS.initial,
|
||||
},
|
||||
localStorageSchemaVersionSource,
|
||||
),
|
||||
null,
|
||||
{
|
||||
@@ -573,12 +582,20 @@ const ExcalidrawWrapper = () => {
|
||||
if (isBrowserStorageStateNewer(STORAGE_KEYS.VERSION_DATA_STATE)) {
|
||||
const localDataState = importFromLocalStorage();
|
||||
const username = importUsernameFromLocalStorage();
|
||||
const localStorageSchemaVersionSource = hasElementSchemaVersion(
|
||||
localDataState?.elements,
|
||||
)
|
||||
? {
|
||||
payloadSchemaVersion: undefined,
|
||||
fallbackVersion: SCHEMA_VERSIONS.initial,
|
||||
}
|
||||
: {
|
||||
payloadSchemaVersion: localDataState?.schemaVersion,
|
||||
fallbackVersion: SCHEMA_VERSIONS.initial,
|
||||
};
|
||||
const migratedElements = migrateSceneElements(
|
||||
localDataState?.elements,
|
||||
{
|
||||
payloadSchemaVersion: localDataState?.schemaVersion,
|
||||
fallbackVersion: SCHEMA_VERSIONS.initial,
|
||||
},
|
||||
localStorageSchemaVersionSource,
|
||||
);
|
||||
setLangCode(getPreferredLanguage());
|
||||
excalidrawAPI.updateScene({
|
||||
|
||||
@@ -87,9 +87,14 @@ const saveDataStateToLocalStorage = (
|
||||
_appState.openSidebar = null;
|
||||
}
|
||||
|
||||
const persistedElements = getNonDeletedElements(elements).map((element) => ({
|
||||
...element,
|
||||
schemaVersion: SCHEMA_VERSIONS.latest,
|
||||
}));
|
||||
|
||||
localStorage.setItem(
|
||||
STORAGE_KEYS.LOCAL_STORAGE_ELEMENTS,
|
||||
JSON.stringify(getNonDeletedElements(elements)),
|
||||
JSON.stringify(persistedElements),
|
||||
);
|
||||
localStorage.setItem(
|
||||
STORAGE_KEYS.LOCAL_STORAGE_APP_STATE,
|
||||
|
||||
@@ -3,6 +3,7 @@ import { DEFAULT_ELEMENT_PROPS } from "@excalidraw/common";
|
||||
import { API } from "../tests/helpers/api";
|
||||
import {
|
||||
ALL_SCOPES,
|
||||
hasElementSchemaVersion,
|
||||
type SchemaMigration,
|
||||
migrateAPIElements,
|
||||
migrateClipboardElements,
|
||||
@@ -179,6 +180,34 @@ describe("schema migration", () => {
|
||||
expect(migrated[0].backgroundColor).toBe("#ff0000");
|
||||
});
|
||||
|
||||
it("should migrate mixed-hint elements individually when payload schema is missing", () => {
|
||||
const legacyFrame = API.createElement({
|
||||
type: "frame",
|
||||
backgroundColor: "#ff0000",
|
||||
});
|
||||
const modernFrame = API.createElement({
|
||||
type: "frame",
|
||||
backgroundColor: "#00ff00",
|
||||
});
|
||||
const modernFrameWithHint = {
|
||||
...modernFrame,
|
||||
schemaVersion: SCHEMA_VERSIONS.latest,
|
||||
} as typeof modernFrame & { schemaVersion: number };
|
||||
|
||||
const migrated = migrateSceneElements(
|
||||
[legacyFrame, modernFrameWithHint],
|
||||
{
|
||||
payloadSchemaVersion: undefined,
|
||||
fallbackVersion: SCHEMA_VERSIONS.initial,
|
||||
},
|
||||
)!;
|
||||
|
||||
expect(migrated[0].backgroundColor).toBe(
|
||||
DEFAULT_ELEMENT_PROPS.backgroundColor,
|
||||
);
|
||||
expect(migrated[1].backgroundColor).toBe("#00ff00");
|
||||
});
|
||||
|
||||
it("should prefer payload schema over per-element schema", () => {
|
||||
const frame = API.createElement({
|
||||
type: "frame",
|
||||
@@ -198,4 +227,15 @@ describe("schema migration", () => {
|
||||
DEFAULT_ELEMENT_PROPS.backgroundColor,
|
||||
);
|
||||
});
|
||||
|
||||
it("should detect schema hints on elements", () => {
|
||||
const frame = API.createElement({ type: "frame" });
|
||||
const withHint = {
|
||||
...frame,
|
||||
schemaVersion: SCHEMA_VERSIONS.latest,
|
||||
} as typeof frame & { schemaVersion: number };
|
||||
|
||||
expect(hasElementSchemaVersion([frame])).toBe(false);
|
||||
expect(hasElementSchemaVersion([withHint])).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -85,6 +85,15 @@ const isValidSchemaVersion = (
|
||||
);
|
||||
};
|
||||
|
||||
export const hasElementSchemaVersion = (
|
||||
elements: readonly ExcalidrawElement[] | null | undefined,
|
||||
) =>
|
||||
!!elements?.some((element) =>
|
||||
isValidSchemaVersion(
|
||||
(element as ExcalidrawElement & { schemaVersion?: number }).schemaVersion,
|
||||
),
|
||||
);
|
||||
|
||||
export const validateSchemaMigrations = (
|
||||
migrations: readonly SchemaMigration[],
|
||||
) => {
|
||||
|
||||
Reference in New Issue
Block a user