From adb1ac57889c41d2bfaa692b08cf694def1534b2 Mon Sep 17 00:00:00 2001 From: David Luzar Date: Tue, 22 Sep 2020 21:51:49 +0200 Subject: [PATCH] fix restoring appState (#2182) --- src/components/App.tsx | 2 +- src/data/blob.ts | 28 +++++++++---------- src/data/index.ts | 14 ++++------ src/data/localStorage.ts | 2 +- src/data/restore.ts | 59 ++++++++++++++++++++++++++++------------ src/data/types.ts | 10 ++++++- src/index.tsx | 4 +-- src/types.ts | 4 +-- 8 files changed, 74 insertions(+), 49 deletions(-) diff --git a/src/components/App.tsx b/src/components/App.tsx index 82023c35..29fb7baf 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -3927,7 +3927,7 @@ class App extends React.Component { this.setState({ shouldCacheIgnoreZoom: false }); }, 300); - private getCanvasOffsets() { + private getCanvasOffsets(): Pick { if (this.excalidrawRef?.current) { const parentElement = this.excalidrawRef.current.parentElement; const { left, top } = parentElement.getBoundingClientRect(); diff --git a/src/data/blob.ts b/src/data/blob.ts index 87a14b78..d83eb1ad 100644 --- a/src/data/blob.ts +++ b/src/data/blob.ts @@ -1,8 +1,8 @@ -import { getDefaultAppState, cleanAppStateForExport } from "../appState"; +import { cleanAppStateForExport } from "../appState"; import { restore } from "./restore"; import { t } from "../i18n"; import { AppState } from "../types"; -import { LibraryData } from "./types"; +import { LibraryData, ImportedDataState } from "./types"; import { calculateScrollCenter } from "../scene"; const loadFileContents = async (blob: any) => { @@ -34,26 +34,24 @@ export const loadFromBlob = async (blob: any, appState?: AppState) => { } const contents = await loadFileContents(blob); - const defaultAppState = getDefaultAppState(); - let elements = []; - let _appState = appState || defaultAppState; try { - const data = JSON.parse(contents); + const data: ImportedDataState = JSON.parse(contents); if (data.type !== "excalidraw") { throw new Error(t("alerts.couldNotLoadInvalidFile")); } - elements = data.elements || []; - _appState = { - ...defaultAppState, - appearance: _appState.appearance, - ...cleanAppStateForExport(data.appState as Partial), - ...(appState ? calculateScrollCenter(elements, appState, null) : {}), - }; + return restore({ + elements: data.elements, + appState: { + appearance: appState?.appearance, + ...cleanAppStateForExport(data.appState || {}), + ...(appState + ? calculateScrollCenter(data.elements || [], appState, null) + : {}), + }, + }); } catch { throw new Error(t("alerts.couldNotLoadInvalidFile")); } - - return restore(elements, _appState); }; export const loadLibraryFromBlob = async (blob: any) => { diff --git a/src/data/index.ts b/src/data/index.ts index 37c31e7f..fdad554c 100644 --- a/src/data/index.ts +++ b/src/data/index.ts @@ -18,7 +18,7 @@ import { serializeAsJSON } from "./json"; import { ExportType } from "../scene/types"; import { restore } from "./restore"; -import { DataState } from "./types"; +import { ImportedDataState } from "./types"; export { loadFromBlob } from "./blob"; export { saveAsJSON, loadFromJSON } from "./json"; @@ -232,7 +232,7 @@ export const exportToBackend = async ( const importFromBackend = async ( id: string | null, privateKey?: string | null, -) => { +): Promise => { let elements: readonly ExcalidrawElement[] = []; let appState = getDefaultAppState(); @@ -364,19 +364,15 @@ export const exportCanvas = async ( export const loadScene = async ( id: string | null, privateKey?: string | null, - initialData?: DataState, + initialData?: ImportedDataState, ) => { let data; if (id != null) { // the private key is used to decrypt the content from the server, take // extra care not to leak it - const { elements, appState } = await importFromBackend(id, privateKey); - data = restore(elements, appState); + data = restore(await importFromBackend(id, privateKey)); } else { - data = restore( - initialData?.elements ?? [], - initialData?.appState ?? getDefaultAppState(), - ); + data = restore(initialData || {}); } return { diff --git a/src/data/localStorage.ts b/src/data/localStorage.ts index c38db71e..ec5f60ed 100644 --- a/src/data/localStorage.ts +++ b/src/data/localStorage.ts @@ -22,7 +22,7 @@ export const loadLibrary = (): Promise => { } const items = (JSON.parse(data) as LibraryItems).map( - (elements) => restore(elements, null).elements, + (elements) => restore({ elements, appState: null }).elements, ) as Mutable; // clone to ensure we don't mutate the cached library elements in the app diff --git a/src/data/restore.ts b/src/data/restore.ts index cb0df162..f641525b 100644 --- a/src/data/restore.ts +++ b/src/data/restore.ts @@ -4,7 +4,7 @@ import { ExcalidrawSelectionElement, } from "../element/types"; import { AppState } from "../types"; -import { DataState } from "./types"; +import { DataState, ImportedDataState } from "./types"; import { isInvisiblySmallElement, getNormalizedDimensions } from "../element"; import { isLinearElementType } from "../element/typeChecks"; import { randomId } from "../random"; @@ -14,6 +14,7 @@ import { DEFAULT_TEXT_ALIGN, DEFAULT_VERTICAL_ALIGN, } from "../constants"; +import { getDefaultAppState } from "../appState"; const getFontFamilyByName = (fontFamilyName: string): FontFamily => { for (const [id, fontFamilyString] of Object.entries(FONT_FAMILY)) { @@ -24,10 +25,10 @@ const getFontFamilyByName = (fontFamilyName: string): FontFamily => { return DEFAULT_FONT_FAMILY; }; -function migrateElementWithProperties( +const restoreElementWithProperties = ( element: Required, extra: Omit, keyof ExcalidrawElement>, -): T { +): T => { const base: Pick = { type: element.type, // all elements must have version > 0 so getDrawingVersion() will pick up @@ -61,9 +62,9 @@ function migrateElementWithProperties( ...getNormalizedDimensions(base), ...extra, } as T; -} +}; -const migrateElement = ( +const restoreElement = ( element: Exclude, ): typeof element => { switch (element.type) { @@ -78,7 +79,7 @@ const migrateElement = ( fontSize = parseInt(fontPx, 10); fontFamily = getFontFamilyByName(_fontFamily); } - return migrateElementWithProperties(element, { + return restoreElementWithProperties(element, { fontSize, fontFamily, text: element.text ?? "", @@ -89,7 +90,7 @@ const migrateElement = ( case "draw": case "line": case "arrow": { - return migrateElementWithProperties(element, { + return restoreElementWithProperties(element, { startBinding: element.startBinding, endBinding: element.endBinding, points: @@ -105,11 +106,11 @@ const migrateElement = ( } // generic elements case "ellipse": - return migrateElementWithProperties(element, {}); + return restoreElementWithProperties(element, {}); case "rectangle": - return migrateElementWithProperties(element, {}); + return restoreElementWithProperties(element, {}); case "diamond": - return migrateElementWithProperties(element, {}); + return restoreElementWithProperties(element, {}); // don't use default case so as to catch a missing an element type case // (we also don't want to throw, but instead return void so we @@ -117,24 +118,46 @@ const migrateElement = ( } }; -export const restore = ( - savedElements: readonly ExcalidrawElement[], - savedState: MarkOptional | null, -): DataState => { - const elements = savedElements.reduce((elements, element) => { +const restoreElements = ( + elements: ImportedDataState["elements"], +): ExcalidrawElement[] => { + return (elements || []).reduce((elements, element) => { // filtering out selection, which is legacy, no longer kept in elements, // and causing issues if retained if (element.type !== "selection" && !isInvisiblySmallElement(element)) { - const migratedElement = migrateElement(element); + const migratedElement = restoreElement(element); if (migratedElement) { elements.push(migratedElement); } } return elements; }, [] as ExcalidrawElement[]); +}; + +const restoreAppState = (appState: ImportedDataState["appState"]): AppState => { + appState = appState || {}; + + const defaultAppState = getDefaultAppState(); + const nextAppState = {} as typeof defaultAppState; + + for (const [key, val] of Object.entries(defaultAppState)) { + if ((appState as any)[key] !== undefined) { + (nextAppState as any)[key] = (appState as any)[key]; + } else { + (nextAppState as any)[key] = val; + } + } return { - elements: elements, - appState: savedState, + ...nextAppState, + offsetLeft: appState.offsetLeft || 0, + offsetTop: appState.offsetTop || 0, + }; +}; + +export const restore = (data: ImportedDataState): DataState => { + return { + elements: restoreElements(data.elements), + appState: restoreAppState(data.appState), }; }; diff --git a/src/data/types.ts b/src/data/types.ts index 49526b4b..34973f9d 100644 --- a/src/data/types.ts +++ b/src/data/types.ts @@ -6,7 +6,15 @@ export interface DataState { version?: string; source?: string; elements: readonly ExcalidrawElement[]; - appState: MarkOptional | null; + appState: MarkOptional; +} + +export interface ImportedDataState { + type?: string; + version?: string; + source?: string; + elements?: DataState["elements"] | null; + appState?: Partial | null; } export interface LibraryData { diff --git a/src/index.tsx b/src/index.tsx index 09bdac2c..5ef8a51e 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -17,7 +17,7 @@ import { } from "./data/localStorage"; import { SAVE_TO_LOCAL_STORAGE_TIMEOUT } from "./time_constants"; -import { DataState } from "./data/types"; +import { ImportedDataState } from "./data/types"; import { LoadingMessage } from "./components/LoadingMessage"; import { ExcalidrawElement } from "./element/types"; import { AppState } from "./types"; @@ -112,7 +112,7 @@ function ExcalidrawApp() { // --------------------------------------------------------------------------- const [initialState, setInitialState] = useState<{ - data: DataState; + data: ImportedDataState; user: { name: string | null; }; diff --git a/src/types.ts b/src/types.ts index d51476fd..0450e33e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -14,7 +14,7 @@ import { Point as RoughPoint } from "roughjs/bin/geometry"; import { SocketUpdateDataSource } from "./data"; import { LinearElementEditor } from "./element/linearElementEditor"; import { SuggestedBinding } from "./element/binding"; -import { DataState } from "./data/types"; +import { ImportedDataState } from "./data/types"; export type FlooredNumber = number & { _brand: "FlooredNumber" }; export type Point = Readonly; @@ -127,7 +127,7 @@ export interface ExcalidrawProps { elements: readonly ExcalidrawElement[], appState: AppState, ) => void; - initialData?: DataState; + initialData?: ImportedDataState; user?: { name?: string | null; };