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
This commit is contained in:
Aakansha Doshi 2023-02-15 10:41:11 +05:30 committed by GitHub
parent 71fb60394a
commit 0d7ee891e0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 112 additions and 21 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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,11 +546,28 @@ describe("repairing bindings", () => {
expect(container.boundElements).toEqual([]); expect(container.boundElements).toEqual([]);
const restoredElements = restore.restoreElements( let restoredElements = restore.restoreElements(
[container, boundElement], [container, boundElement],
null, null,
); );
expect(restoredElements).toEqual([
expect.objectContaining({
id: container.id,
boundElements: [],
}),
expect.objectContaining({
id: boundElement.id,
containerId: container.id,
}),
]);
restoredElements = restore.restoreElements(
[container, boundElement],
null,
{ repairBindings: true },
);
expect(restoredElements).toEqual([ expect(restoredElements).toEqual([
expect.objectContaining({ expect.objectContaining({
id: container.id, id: container.id,
@ -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,11 +590,28 @@ 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], [container, boundElement],
null, 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],
null,
{ repairBindings: true },
);
expect(restoredElements).toEqual([ expect(restoredElements).toEqual([
expect.objectContaining({ expect.objectContaining({
id: container.id, id: container.id,
@ -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,17 +686,28 @@ describe("repairing bindings", () => {
nonExistentBinding, nonExistentBinding,
]; ];
expect(container.boundElements).toEqual([ let restoredElements = restore.restoreElements(
obsoleteBinding,
invisibleBinding,
nonExistentBinding,
]);
const restoredElements = restore.restoreElements(
[container, invisibleBoundElement, boundElement], [container, invisibleBoundElement, boundElement],
null, null,
); );
expect(restoredElements).toEqual([
expect.objectContaining({
id: container.id,
boundElements: [obsoleteBinding, invisibleBinding, nonExistentBinding],
}),
expect.objectContaining({
id: boundElement.id,
containerId: container.id,
}),
]);
restoredElements = restore.restoreElements(
[container, invisibleBoundElement, boundElement],
null,
{ repairBindings: true },
);
expect(restoredElements).toEqual([ expect(restoredElements).toEqual([
expect.objectContaining({ expect.objectContaining({
id: container.id, id: container.id,
@ -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,11 +731,28 @@ describe("repairing bindings", () => {
isDeleted: true, isDeleted: true,
}); });
const restoredElements = restore.restoreElements( let restoredElements = restore.restoreElements(
[boundElement, boundElementDeleted], [boundElement, boundElementDeleted],
null, 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],
null,
{ repairBindings: true },
);
expect(restoredElements).toEqual([ expect(restoredElements).toEqual([
expect.objectContaining({ expect.objectContaining({
id: boundElement.id, id: boundElement.id,