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 { * Config containing all AppState keys. Used to determine whether given state
draggingElement, * prop should be stripped when exporting to given storage type.
resizingElement, */
multiElement, const APP_STATE_STORAGE_CONF = (<
editingElement, Values extends {
selectionElement, /** whether to keep when storing to browser storage (localStorage/IDB) */
isResizing, browser: boolean;
isRotating, /** whether to keep when exporting to file/database */
collaborators, export: boolean;
isCollaborating, },
isLoading, T extends Record<keyof AppState, Values>
errorMessage, >(
showShortcutsDialog, config: { [K in keyof T]: K extends keyof AppState ? T[K] : never },
editingLinearElement, ) => config)({
isLibraryOpen, collaborators: { browser: false, export: false },
...exportedState currentItemBackgroundColor: { browser: true, export: false },
} = appState; currentItemFillStyle: { browser: true, export: false },
return exportedState; 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) => { export const clearAppStateForLocalStorage = (appState: Partial<AppState>) => {
return { return _clearAppStateForStorage(appState, "browser");
viewBackgroundColor: appState.viewBackgroundColor,
gridSize: appState.gridSize,
}; };
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 { restore } from "./restore";
import { t } from "../i18n"; import { t } from "../i18n";
import { AppState } from "../types";
export const loadFromBlob = async (blob: any) => { export const loadFromBlob = async (blob: any) => {
const updateAppState = (contents: string) => { const updateAppState = (contents: string) => {
@ -13,7 +14,10 @@ export const loadFromBlob = async (blob: any) => {
throw new Error(t("alerts.couldNotLoadInvalidFile")); throw new Error(t("alerts.couldNotLoadInvalidFile"));
} }
elements = data.elements || []; elements = data.elements || [];
appState = { ...defaultAppState, ...data.appState }; appState = {
...defaultAppState,
...cleanAppStateForExport(data.appState as Partial<AppState>),
};
} catch { } catch {
throw new Error(t("alerts.couldNotLoadInvalidFile")); throw new Error(t("alerts.couldNotLoadInvalidFile"));
} }

View File

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

View File

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