소스 검색

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

David Luzar 4 년 전
부모
커밋
0ee2c15929
4개의 변경된 파일107개의 추가작업 그리고 36개의 파일을 삭제
  1. 89 24
      src/appState.ts
  2. 6 2
      src/data/blob.ts
  3. 1 1
      src/data/index.ts
  4. 11 9
      src/data/localStorage.ts

+ 89 - 24
src/appState.ts

@@ -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");
 };

+ 6 - 2
src/data/blob.ts

@@ -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"));
     }

+ 1 - 1
src/data/index.ts

@@ -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,
   };
 };

+ 11 - 9
src/data/localStorage.ts

@@ -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
     }
   }