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