Browse Source

feat: Make repair and refreshDimensions configurable in restoreElements (#6238)

* fix: don't repair during reconcilation

* Add opts to restoreElement and enable refreshDimensions and repair via config

* remove

* update changelog

* fix tests

* rename to repairBindings
Aakansha Doshi 2 years ago
parent
commit
0d7ee891e0

+ 1 - 1
src/components/App.tsx

@@ -836,7 +836,7 @@ class App extends React.Component<AppProps, AppState> {
         },
         },
       };
       };
     }
     }
-    const scene = restore(initialData, null, null);
+    const scene = restore(initialData, null, null, { repairBindings: true });
     scene.appState = {
     scene.appState = {
       ...scene.appState,
       ...scene.appState,
       theme: this.props.theme || scene.appState.theme,
       theme: this.props.theme || scene.appState.theme,

+ 1 - 0
src/data/blob.ts

@@ -156,6 +156,7 @@ export const loadSceneOrLibraryFromBlob = async (
           },
           },
           localAppState,
           localAppState,
           localElements,
           localElements,
+          { repairBindings: true },
         ),
         ),
       };
       };
     } else if (isValidLibrary(data)) {
     } else if (isValidLibrary(data)) {

+ 8 - 3
src/data/restore.ts

@@ -339,7 +339,7 @@ export const restoreElements = (
   elements: ImportedDataState["elements"],
   elements: ImportedDataState["elements"],
   /** NOTE doesn't serve for reconciliation */
   /** NOTE doesn't serve for reconciliation */
   localElements: readonly ExcalidrawElement[] | null | undefined,
   localElements: readonly ExcalidrawElement[] | null | undefined,
-  refreshDimensions = false,
+  opts?: { refreshDimensions?: boolean; repairBindings?: boolean } | undefined,
 ): ExcalidrawElement[] => {
 ): ExcalidrawElement[] => {
   const localElementsMap = localElements ? arrayToMap(localElements) : null;
   const localElementsMap = localElements ? arrayToMap(localElements) : null;
   const restoredElements = (elements || []).reduce((elements, element) => {
   const restoredElements = (elements || []).reduce((elements, element) => {
@@ -348,7 +348,7 @@ export const restoreElements = (
     if (element.type !== "selection" && !isInvisiblySmallElement(element)) {
     if (element.type !== "selection" && !isInvisiblySmallElement(element)) {
       let migratedElement: ExcalidrawElement | null = restoreElement(
       let migratedElement: ExcalidrawElement | null = restoreElement(
         element,
         element,
-        refreshDimensions,
+        opts?.refreshDimensions,
       );
       );
       if (migratedElement) {
       if (migratedElement) {
         const localElement = localElementsMap?.get(element.id);
         const localElement = localElementsMap?.get(element.id);
@@ -361,6 +361,10 @@ export const restoreElements = (
     return elements;
     return elements;
   }, [] as ExcalidrawElement[]);
   }, [] as ExcalidrawElement[]);
 
 
+  if (!opts?.repairBindings) {
+    return restoredElements;
+  }
+
   // repair binding. Mutates elements.
   // repair binding. Mutates elements.
   const restoredElementsMap = arrayToMap(restoredElements);
   const restoredElementsMap = arrayToMap(restoredElements);
   for (const element of restoredElements) {
   for (const element of restoredElements) {
@@ -497,9 +501,10 @@ export const restore = (
    */
    */
   localAppState: Partial<AppState> | null | undefined,
   localAppState: Partial<AppState> | null | undefined,
   localElements: readonly ExcalidrawElement[] | null | undefined,
   localElements: readonly ExcalidrawElement[] | null | undefined,
+  elementsConfig?: { refreshDimensions?: boolean; repairBindings?: boolean },
 ): RestoredDataState => {
 ): RestoredDataState => {
   return {
   return {
-    elements: restoreElements(data?.elements, localElements),
+    elements: restoreElements(data?.elements, localElements, elementsConfig),
     appState: restoreAppState(data?.appState, localAppState || null),
     appState: restoreAppState(data?.appState, localAppState || null),
     files: data?.files || {},
     files: data?.files || {},
   };
   };

+ 1 - 1
src/excalidraw-app/collab/Collab.tsx

@@ -610,7 +610,7 @@ class Collab extends PureComponent<Props, CollabState> {
     const localElements = this.getSceneElementsIncludingDeleted();
     const localElements = this.getSceneElementsIncludingDeleted();
     const appState = this.excalidrawAPI.getAppState();
     const appState = this.excalidrawAPI.getAppState();
 
 
-    remoteElements = restoreElements(remoteElements, null, false);
+    remoteElements = restoreElements(remoteElements, null);
 
 
     const reconciledElements = _reconcileElements(
     const reconciledElements = _reconcileElements(
       localElements,
       localElements,

+ 4 - 1
src/excalidraw-app/data/index.ts

@@ -263,9 +263,12 @@ export const loadScene = async (
       await importFromBackend(id, privateKey),
       await importFromBackend(id, privateKey),
       localDataState?.appState,
       localDataState?.appState,
       localDataState?.elements,
       localDataState?.elements,
+      { repairBindings: true },
     );
     );
   } else {
   } else {
-    data = restore(localDataState || null, null, null);
+    data = restore(localDataState || null, null, null, {
+      repairBindings: true,
+    });
   }
   }
 
 
   return {
   return {

+ 1 - 1
src/excalidraw-app/index.tsx

@@ -362,7 +362,7 @@ const ExcalidrawWrapper = () => {
           if (data.scene) {
           if (data.scene) {
             excalidrawAPI.updateScene({
             excalidrawAPI.updateScene({
               ...data.scene,
               ...data.scene,
-              ...restore(data.scene, null, null),
+              ...restore(data.scene, null, null, { repairBindings: true }),
               commitToHistory: true,
               commitToHistory: true,
             });
             });
           }
           }

+ 18 - 0
src/packages/excalidraw/CHANGELOG.md

@@ -11,6 +11,24 @@ The change should be grouped under one of the below section and must contain PR
 Please add the latest change on the top under the correct section.
 Please add the latest change on the top under the correct section.
 -->
 -->
 
 
+## Unreleased
+
+### Features
+
+- [`restoreElements`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/utils/restore#restoreelements) API now takes an optional parameter `opts` which currently supports the below attributes
+
+```js
+{ refreshDimensions?: boolean, repair?: boolean }
+```
+
+The same `opts` param has been added to [`restore`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/utils/restore#restore) API as well.
+
+For more details refer to the [docs](https://docs.excalidraw.com)
+
+#### BREAKING CHANGE
+
+- The optional parameter `refreshDimensions` in [`restoreElements`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/utils/restore#restoreelements) has been removed and can be enabled via `opts`
+
 ## 0.14.2 (2023-02-01)
 ## 0.14.2 (2023-02-01)
 
 
 ### Features
 ### Features

+ 76 - 12
src/tests/data/restore.test.ts

@@ -534,7 +534,7 @@ describe("restore", () => {
 });
 });
 
 
 describe("repairing bindings", () => {
 describe("repairing bindings", () => {
-  it("should repair container boundElements", () => {
+  it("should repair container boundElements when repair is true", () => {
     const container = API.createElement({
     const container = API.createElement({
       type: "rectangle",
       type: "rectangle",
       boundElements: [],
       boundElements: [],
@@ -546,9 +546,26 @@ describe("repairing bindings", () => {
 
 
     expect(container.boundElements).toEqual([]);
     expect(container.boundElements).toEqual([]);
 
 
-    const restoredElements = restore.restoreElements(
+    let restoredElements = restore.restoreElements(
+      [container, boundElement],
+      null,
+    );
+
+    expect(restoredElements).toEqual([
+      expect.objectContaining({
+        id: container.id,
+        boundElements: [],
+      }),
+      expect.objectContaining({
+        id: boundElement.id,
+        containerId: container.id,
+      }),
+    ]);
+
+    restoredElements = restore.restoreElements(
       [container, boundElement],
       [container, boundElement],
       null,
       null,
+      { repairBindings: true },
     );
     );
 
 
     expect(restoredElements).toEqual([
     expect(restoredElements).toEqual([
@@ -563,7 +580,7 @@ describe("repairing bindings", () => {
     ]);
     ]);
   });
   });
 
 
-  it("should repair containerId of boundElements", () => {
+  it("should repair containerId of boundElements when repair is true", () => {
     const boundElement = API.createElement({
     const boundElement = API.createElement({
       type: "text",
       type: "text",
       containerId: null,
       containerId: null,
@@ -573,9 +590,26 @@ describe("repairing bindings", () => {
       boundElements: [{ type: boundElement.type, id: boundElement.id }],
       boundElements: [{ type: boundElement.type, id: boundElement.id }],
     });
     });
 
 
-    const restoredElements = restore.restoreElements(
+    let restoredElements = restore.restoreElements(
+      [container, boundElement],
+      null,
+    );
+
+    expect(restoredElements).toEqual([
+      expect.objectContaining({
+        id: container.id,
+        boundElements: [{ type: boundElement.type, id: boundElement.id }],
+      }),
+      expect.objectContaining({
+        id: boundElement.id,
+        containerId: null,
+      }),
+    ]);
+
+    restoredElements = restore.restoreElements(
       [container, boundElement],
       [container, boundElement],
       null,
       null,
+      { repairBindings: true },
     );
     );
 
 
     expect(restoredElements).toEqual([
     expect(restoredElements).toEqual([
@@ -620,7 +654,7 @@ describe("repairing bindings", () => {
     ]);
     ]);
   });
   });
 
 
-  it("should remove bindings of deleted elements from boundElements", () => {
+  it("should remove bindings of deleted elements from boundElements when repair is true", () => {
     const container = API.createElement({
     const container = API.createElement({
       type: "rectangle",
       type: "rectangle",
       boundElements: [],
       boundElements: [],
@@ -642,6 +676,8 @@ describe("repairing bindings", () => {
       type: invisibleBoundElement.type,
       type: invisibleBoundElement.type,
       id: invisibleBoundElement.id,
       id: invisibleBoundElement.id,
     };
     };
+    expect(container.boundElements).toEqual([]);
+
     const nonExistentBinding = { type: "text", id: "non-existent" };
     const nonExistentBinding = { type: "text", id: "non-existent" };
     // @ts-ignore
     // @ts-ignore
     container.boundElements = [
     container.boundElements = [
@@ -650,15 +686,26 @@ describe("repairing bindings", () => {
       nonExistentBinding,
       nonExistentBinding,
     ];
     ];
 
 
-    expect(container.boundElements).toEqual([
-      obsoleteBinding,
-      invisibleBinding,
-      nonExistentBinding,
+    let restoredElements = restore.restoreElements(
+      [container, invisibleBoundElement, boundElement],
+      null,
+    );
+
+    expect(restoredElements).toEqual([
+      expect.objectContaining({
+        id: container.id,
+        boundElements: [obsoleteBinding, invisibleBinding, nonExistentBinding],
+      }),
+      expect.objectContaining({
+        id: boundElement.id,
+        containerId: container.id,
+      }),
     ]);
     ]);
 
 
-    const restoredElements = restore.restoreElements(
+    restoredElements = restore.restoreElements(
       [container, invisibleBoundElement, boundElement],
       [container, invisibleBoundElement, boundElement],
       null,
       null,
+      { repairBindings: true },
     );
     );
 
 
     expect(restoredElements).toEqual([
     expect(restoredElements).toEqual([
@@ -673,7 +720,7 @@ describe("repairing bindings", () => {
     ]);
     ]);
   });
   });
 
 
-  it("should remove containerId if container not exists", () => {
+  it("should remove containerId if container not exists when repair is true", () => {
     const boundElement = API.createElement({
     const boundElement = API.createElement({
       type: "text",
       type: "text",
       containerId: "non-existent",
       containerId: "non-existent",
@@ -684,9 +731,26 @@ describe("repairing bindings", () => {
       isDeleted: true,
       isDeleted: true,
     });
     });
 
 
-    const restoredElements = restore.restoreElements(
+    let restoredElements = restore.restoreElements(
+      [boundElement, boundElementDeleted],
+      null,
+    );
+
+    expect(restoredElements).toEqual([
+      expect.objectContaining({
+        id: boundElement.id,
+        containerId: "non-existent",
+      }),
+      expect.objectContaining({
+        id: boundElementDeleted.id,
+        containerId: "non-existent",
+      }),
+    ]);
+
+    restoredElements = restore.restoreElements(
       [boundElement, boundElementDeleted],
       [boundElement, boundElementDeleted],
       null,
       null,
+      { repairBindings: true },
     );
     );
 
 
     expect(restoredElements).toEqual([
     expect(restoredElements).toEqual([