make clearing state for storage more type-safe (#1884)

This commit is contained in:
David Luzar 2020-07-11 13:09:40 +02:00 committed by GitHub
parent 6428b59ccb
commit 0ee2c15929
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 107 additions and 36 deletions

View File

@ -62,30 +62,95 @@ export const getDefaultAppState = (): AppState => {
};
};
export const clearAppStateForLocalStorage = (appState: AppState) => {
const {
draggingElement,
resizingElement,
multiElement,
editingElement,
selectionElement,
isResizing,
isRotating,
collaborators,
isCollaborating,
isLoading,
errorMessage,
showShortcutsDialog,
editingLinearElement,
isLibraryOpen,
...exportedState
} = appState;
return exportedState;
/**
* Config containing all AppState keys. Used to determine whether given state
* prop should be stripped when exporting to given storage type.
*/
const APP_STATE_STORAGE_CONF = (<
Values extends {
/** whether to keep when storing to browser storage (localStorage/IDB) */
browser: boolean;
/** whether to keep when exporting to file/database */
export: boolean;
},
T extends Record<keyof AppState, Values>
>(
config: { [K in keyof T]: K extends keyof AppState ? T[K] : never },
) => config)({
collaborators: { browser: false, export: false },
currentItemBackgroundColor: { browser: true, export: false },
currentItemFillStyle: { browser: true, export: false },
currentItemFontFamily: { browser: true, export: false },
currentItemFontSize: { browser: true, export: false },
currentItemOpacity: { browser: true, export: false },
currentItemRoughness: { browser: true, export: false },
currentItemStrokeColor: { browser: true, export: false },
currentItemStrokeStyle: { browser: true, export: false },
currentItemStrokeWidth: { browser: true, export: false },
currentItemTextAlign: { browser: true, export: false },
cursorButton: { browser: true, export: false },
cursorX: { browser: true, export: false },
cursorY: { browser: true, export: false },
draggingElement: { browser: false, export: false },
editingElement: { browser: false, export: false },
editingGroupId: { browser: true, export: false },
editingLinearElement: { browser: false, export: false },
elementLocked: { browser: true, export: false },
elementType: { browser: true, export: false },
errorMessage: { browser: false, export: false },
exportBackground: { browser: true, export: false },
gridSize: { browser: true, export: true },
height: { browser: false, export: false },
isCollaborating: { browser: false, export: false },
isLibraryOpen: { browser: false, export: false },
isLoading: { browser: false, export: false },
isResizing: { browser: false, export: false },
isRotating: { browser: false, export: false },
lastPointerDownWith: { browser: true, export: false },
multiElement: { browser: false, export: false },
name: { browser: true, export: false },
openMenu: { browser: true, export: false },
previousSelectedElementIds: { browser: true, export: false },
resizingElement: { browser: false, export: false },
scrolledOutside: { browser: true, export: false },
scrollX: { browser: true, export: false },
scrollY: { browser: true, export: false },
selectedElementIds: { browser: true, export: false },
selectedGroupIds: { browser: true, export: false },
selectionElement: { browser: false, export: false },
shouldAddWatermark: { browser: true, export: false },
shouldCacheIgnoreZoom: { browser: true, export: false },
showShortcutsDialog: { browser: false, export: false },
username: { browser: true, export: false },
viewBackgroundColor: { browser: true, export: true },
width: { browser: false, export: false },
zenModeEnabled: { browser: true, export: false },
zoom: { browser: true, export: false },
});
const _clearAppStateForStorage = <ExportType extends "export" | "browser">(
appState: Partial<AppState>,
exportType: ExportType,
) => {
type ExportableKeys = {
[K in keyof typeof APP_STATE_STORAGE_CONF]: typeof APP_STATE_STORAGE_CONF[K][ExportType] extends true
? K
: never;
}[keyof typeof APP_STATE_STORAGE_CONF];
const stateForExport = {} as { [K in ExportableKeys]?: typeof appState[K] };
for (const key of Object.keys(appState) as (keyof typeof appState)[]) {
if (APP_STATE_STORAGE_CONF[key][exportType]) {
// @ts-ignore see https://github.com/microsoft/TypeScript/issues/31445
stateForExport[key] = appState[key];
}
}
return stateForExport;
};
export const cleanAppStateForExport = (appState: AppState) => {
return {
viewBackgroundColor: appState.viewBackgroundColor,
gridSize: appState.gridSize,
};
export const clearAppStateForLocalStorage = (appState: Partial<AppState>) => {
return _clearAppStateForStorage(appState, "browser");
};
export const cleanAppStateForExport = (appState: Partial<AppState>) => {
return _clearAppStateForStorage(appState, "export");
};

View File

@ -1,6 +1,7 @@
import { getDefaultAppState } from "../appState";
import { getDefaultAppState, cleanAppStateForExport } from "../appState";
import { restore } from "./restore";
import { t } from "../i18n";
import { AppState } from "../types";
export const loadFromBlob = async (blob: any) => {
const updateAppState = (contents: string) => {
@ -13,7 +14,10 @@ export const loadFromBlob = async (blob: any) => {
throw new Error(t("alerts.couldNotLoadInvalidFile"));
}
elements = data.elements || [];
appState = { ...defaultAppState, ...data.appState };
appState = {
...defaultAppState,
...cleanAppStateForExport(data.appState as Partial<AppState>),
};
} catch {
throw new Error(t("alerts.couldNotLoadInvalidFile"));
}

View File

@ -374,7 +374,7 @@ export const loadScene = async (id: string | null, privateKey?: string) => {
return {
elements: data.elements,
appState: data.appState && { ...data.appState },
appState: data.appState,
commitToHistory: false,
};
};

View File

@ -1,6 +1,6 @@
import { ExcalidrawElement } from "../element/types";
import { AppState, LibraryItems } from "../types";
import { clearAppStateForLocalStorage } from "../appState";
import { clearAppStateForLocalStorage, getDefaultAppState } from "../appState";
import { restore } from "./restore";
const LOCAL_STORAGE_KEY = "excalidraw";
@ -111,7 +111,8 @@ export const restoreFromLocalStorage = () => {
if (savedElements) {
try {
elements = JSON.parse(savedElements);
} catch {
} catch (error) {
console.error(error);
// Do nothing because elements array is already empty
}
}
@ -119,13 +120,14 @@ export const restoreFromLocalStorage = () => {
let appState = null;
if (savedState) {
try {
appState = JSON.parse(savedState) as AppState;
// If we're retrieving from local storage, we should not be collaborating
appState.isCollaborating = false;
appState.collaborators = new Map();
delete appState.width;
delete appState.height;
} catch {
appState = {
...getDefaultAppState(),
...clearAppStateForLocalStorage(
JSON.parse(savedState) as Partial<AppState>,
),
};
} catch (error) {
console.error(error);
// Do nothing because appState is already null
}
}