retain local appState props on restore (#2224)

Co-authored-by: Lipis <lipiridis@gmail.com>
This commit is contained in:
David Luzar 2020-10-13 13:46:52 +02:00 committed by GitHub
parent b91f929503
commit 7618ca48d7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 153 additions and 69 deletions

View file

@ -23,11 +23,11 @@ const loadFileContents = async (blob: any) => {
return contents;
};
/**
* @param blob
* @param appState if provided, used for centering scroll to restored scene
*/
export const loadFromBlob = async (blob: any, appState?: AppState) => {
export const loadFromBlob = async (
blob: any,
/** @see restore.localAppState */
localAppState: AppState | null,
) => {
if (blob.handle) {
// TODO: Make this part of `AppState`.
(window as any).handle = blob.handle;
@ -39,16 +39,19 @@ export const loadFromBlob = async (blob: any, appState?: AppState) => {
if (data.type !== "excalidraw") {
throw new Error(t("alerts.couldNotLoadInvalidFile"));
}
return restore({
elements: data.elements,
appState: {
appearance: appState?.appearance,
...cleanAppStateForExport(data.appState || {}),
...(appState
? calculateScrollCenter(data.elements || [], appState, null)
: {}),
return restore(
{
elements: data.elements,
appState: {
appearance: localAppState?.appearance,
...cleanAppStateForExport(data.appState || {}),
...(localAppState
? calculateScrollCenter(data.elements || [], localAppState, null)
: {}),
},
},
});
localAppState,
);
} catch {
throw new Error(t("alerts.couldNotLoadInvalidFile"));
}

View file

@ -233,18 +233,15 @@ const importFromBackend = async (
id: string | null,
privateKey?: string | null,
): Promise<ImportedDataState> => {
let elements: readonly ExcalidrawElement[] = [];
let appState = getDefaultAppState();
try {
const response = await fetch(
privateKey ? `${BACKEND_V2_GET}${id}` : `${BACKEND_GET}${id}.json`,
);
if (!response.ok) {
window.alert(t("alerts.importBackendFailed"));
return { elements, appState };
return {};
}
let data;
let data: ImportedDataState;
if (privateKey) {
const buffer = await response.arrayBuffer();
const key = await getImportedKey(privateKey, "decrypt");
@ -267,13 +264,14 @@ const importFromBackend = async (
data = await response.json();
}
elements = data.elements || elements;
appState = { ...appState, ...data.appState };
return {
elements: data.elements || null,
appState: data.appState || null,
};
} catch (error) {
window.alert(t("alerts.importBackendFailed"));
console.error(error);
} finally {
return { elements, appState };
return {};
}
};
@ -363,16 +361,22 @@ export const exportCanvas = async (
export const loadScene = async (
id: string | null,
privateKey?: string | null,
initialData?: ImportedDataState,
privateKey: string | null,
// Supply initialData even if importing from backend to ensure we restore
// localStorage user settings which we do not persist on server.
// Non-optional so we don't forget to pass it even if `undefined`.
initialData: ImportedDataState | undefined | null,
) => {
let data;
if (id != null) {
// the private key is used to decrypt the content from the server, take
// extra care not to leak it
data = restore(await importFromBackend(id, privateKey));
data = restore(
await importFromBackend(id, privateKey),
initialData?.appState,
);
} else {
data = restore(initialData || {});
data = restore(initialData || {}, null);
}
return {

View file

@ -45,13 +45,13 @@ export const saveAsJSON = async (
);
};
export const loadFromJSON = async (appState: AppState) => {
export const loadFromJSON = async (localAppState: AppState) => {
const blob = await fileOpen({
description: "Excalidraw files",
extensions: [".json", ".excalidraw"],
mimeTypes: ["application/json"],
});
return loadFromBlob(blob, appState);
return loadFromBlob(blob, localAppState);
};
export const isValidLibrary = (json: any) => {

View file

@ -1,7 +1,7 @@
import { ExcalidrawElement } from "../element/types";
import { AppState, LibraryItems } from "../types";
import { clearAppStateForLocalStorage, getDefaultAppState } from "../appState";
import { restore } from "./restore";
import { restoreElements } from "./restore";
const LOCAL_STORAGE_KEY = "excalidraw";
const LOCAL_STORAGE_KEY_STATE = "excalidraw-state";
@ -21,8 +21,8 @@ export const loadLibrary = (): Promise<LibraryItems> => {
return resolve([]);
}
const items = (JSON.parse(data) as LibraryItems).map(
(elements) => restore({ elements, appState: null }).elements,
const items = (JSON.parse(data) as LibraryItems).map((elements) =>
restoreElements(elements),
) as Mutable<LibraryItems>;
// clone to ensure we don't mutate the cached library elements in the app

View file

@ -118,7 +118,7 @@ const restoreElement = (
}
};
const restoreElements = (
export const restoreElements = (
elements: ImportedDataState["elements"],
): ExcalidrawElement[] => {
return (elements || []).reduce((elements, element) => {
@ -134,18 +134,27 @@ const restoreElements = (
}, [] as ExcalidrawElement[]);
};
const restoreAppState = (appState: ImportedDataState["appState"]): AppState => {
const restoreAppState = (
appState: ImportedDataState["appState"],
localAppState: Partial<AppState> | null,
): 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;
}
for (const [key, val] of Object.entries(defaultAppState) as [
keyof typeof defaultAppState,
any,
][]) {
const restoredValue = appState[key];
const localValue = localAppState ? localAppState[key] : undefined;
(nextAppState as any)[key] =
restoredValue !== undefined
? restoredValue
: localValue !== undefined
? localValue
: val;
}
return {
@ -155,9 +164,18 @@ const restoreAppState = (appState: ImportedDataState["appState"]): AppState => {
};
};
export const restore = (data: ImportedDataState): DataState => {
export const restore = (
data: ImportedDataState,
/**
* Local AppState (`this.state` or initial state from localStorage) so that we
* don't overwrite local state with default values (when values not
* explicitly specified).
* Supply `null` if you can't get access to it.
*/
localAppState: Partial<AppState> | null | undefined,
): DataState => {
return {
elements: restoreElements(data.elements),
appState: restoreAppState(data.appState),
appState: restoreAppState(data.appState, localAppState || null),
};
};