From 4005f23baa2c69b3e760a02e94f76198970774ea Mon Sep 17 00:00:00 2001 From: Pawan Kumar Date: Mon, 8 Nov 2021 17:52:41 +0530 Subject: [PATCH] fix: List Widget shows cyclic dependency error when children widgets with bindings in action are deleted (#8942) * fix list widget dynamic trigger path issue * Update app/client/src/sagas/WidgetOperationUtils.ts Co-authored-by: Hetu Nandu Co-authored-by: root Co-authored-by: Hetu Nandu --- app/client/src/sagas/WidgetDeletionSagas.ts | 47 +------ .../src/sagas/WidgetOperationUtils.test.ts | 125 ++++++++++++++++++ app/client/src/sagas/WidgetOperationUtils.ts | 45 ++++++- 3 files changed, 175 insertions(+), 42 deletions(-) diff --git a/app/client/src/sagas/WidgetDeletionSagas.ts b/app/client/src/sagas/WidgetDeletionSagas.ts index 8ecf211ff1..b3a60a2811 100644 --- a/app/client/src/sagas/WidgetDeletionSagas.ts +++ b/app/client/src/sagas/WidgetDeletionSagas.ts @@ -14,7 +14,7 @@ import { import { GridDefaults } from "constants/WidgetConstants"; import { ENTITY_TYPE } from "entities/AppsmithConsole"; import LOG_TYPE from "entities/AppsmithConsole/logtype"; -import { flattenDeep, omit, remove } from "lodash"; +import { flattenDeep, omit } from "lodash"; import { CanvasWidgetsReduxState, FlattenedWidgetProps, @@ -26,8 +26,10 @@ import AppsmithConsole from "utils/AppsmithConsole"; import { WidgetProps } from "widgets/BaseWidget"; import { clearEvalPropertyCacheOfWidget } from "./EvaluationsSaga"; import { getSelectedWidget, getWidget, getWidgets } from "./selectors"; -import { getParentWithEnhancementFn } from "./WidgetEnhancementHelpers"; -import { getAllWidgetsInTree } from "./WidgetOperationUtils"; +import { + getAllWidgetsInTree, + updateListWidgetPropertiesOnChildDelete, +} from "./WidgetOperationUtils"; import { showUndoRedoToast } from "utils/replayHelpers"; import WidgetFactory from "utils/WidgetFactory"; const WidgetTypes = WidgetFactory.widgetTypes; @@ -40,42 +42,6 @@ type WidgetDeleteTabChild = { widgetId: string; }; -/** - * this saga clears out the enhancementMap, template and dynamicBindingPathList when a child - * is deleted in list widget - * - * @param widgets - * @param widgetId - * @param widgetName - * @param parentId - */ -function* updateListWidgetPropertiesOnChildDelete( - widgets: CanvasWidgetsReduxState, - widgetId: string, - widgetName: string, -) { - const clone = JSON.parse(JSON.stringify(widgets)); - - const parentWithEnhancementFn = getParentWithEnhancementFn(widgetId, clone); - - if (parentWithEnhancementFn?.type === "LIST_WIDGET") { - const listWidget = parentWithEnhancementFn; - - // delete widget in template of list - if (listWidget && widgetName in listWidget.template) { - listWidget.template[widgetName] = undefined; - } - - // delete dynamic binding path if any - remove(listWidget?.dynamicBindingPathList || [], (path: any) => - path.key.startsWith(`template.${widgetName}`), - ); - - return clone; - } - - return clone; -} function* deleteTabChildSaga( deleteChildTabAction: ReduxAction, ) { @@ -177,8 +143,7 @@ function* getUpdatedDslAfterDeletingWidget(widgetId: string, parentId: string) { yield call(clearEvalPropertyCacheOfWidget, widgetName); - let finalWidgets: CanvasWidgetsReduxState = yield call( - updateListWidgetPropertiesOnChildDelete, + let finalWidgets: CanvasWidgetsReduxState = updateListWidgetPropertiesOnChildDelete( widgets, widgetId, widgetName, diff --git a/app/client/src/sagas/WidgetOperationUtils.test.ts b/app/client/src/sagas/WidgetOperationUtils.test.ts index 492cd5f15b..06fe9f94c6 100644 --- a/app/client/src/sagas/WidgetOperationUtils.test.ts +++ b/app/client/src/sagas/WidgetOperationUtils.test.ts @@ -4,6 +4,7 @@ import { handleSpecificCasesWhilePasting, doesTriggerPathsContainPropertyPath, checkIfPastingIntoListWidget, + updateListWidgetPropertiesOnChildDelete, } from "./WidgetOperationUtils"; describe("WidgetOperationSaga", () => { @@ -456,4 +457,128 @@ describe("WidgetOperationSaga", () => { expect(result?.type).toStrictEqual("LIST_WIDGET"); }); + + it("should return widgets after executing updateListWidgetPropertiesOnChildDelete", () => { + const result = updateListWidgetPropertiesOnChildDelete( + { + list1: { + widgetId: "list1", + type: "LIST_WIDGET", + widgetName: "List1", + parentId: "0", + renderMode: "CANVAS", + parentColumnSpace: 2, + parentRowSpace: 3, + leftColumn: 2, + rightColumn: 3, + topRow: 1, + bottomRow: 3, + isLoading: false, + listData: [], + version: 16, + disablePropertyPane: false, + template: {}, + enhancements: {}, + dynamicBindingPathList: [{ key: "template.ButtonWidget1.text" }], + dynamicTriggerPathList: [ + { + key: "template.ButtonWidget1.onClick", + }, + ], + }, + buttonWidget1: { + type: "BUTTON_WIDGET", + widgetId: "buttonWidget1", + widgetName: "buttonWidget1", + version: 16, + parentColumnSpace: 2, + parentRowSpace: 3, + leftColumn: 2, + rightColumn: 3, + topRow: 1, + bottomRow: 3, + renderMode: "CANVAS", + isLoading: false, + parentId: "list1", + }, + 0: { + type: "CANVAS_WIDGET", + widgetId: "0", + widgetName: "MainContainer", + version: 16, + parentColumnSpace: 2, + parentRowSpace: 3, + leftColumn: 2, + rightColumn: 3, + topRow: 1, + bottomRow: 3, + renderMode: "CANVAS", + isLoading: false, + parentId: "list1", + }, + }, + "buttonWidget1", + "ButtonWidget1", + ); + + const expected = updateListWidgetPropertiesOnChildDelete( + { + list1: { + widgetId: "list1", + type: "LIST_WIDGET", + widgetName: "List1", + parentId: "0", + renderMode: "CANVAS", + parentColumnSpace: 2, + parentRowSpace: 3, + leftColumn: 2, + rightColumn: 3, + topRow: 1, + bottomRow: 3, + isLoading: false, + listData: [], + version: 16, + disablePropertyPane: false, + template: {}, + enhancements: {}, + dynamicBindingPathList: [], + dynamicTriggerPathList: [], + }, + buttonWidget1: { + type: "BUTTON_WIDGET", + widgetId: "buttonWidget1", + widgetName: "buttonWidget1", + version: 16, + parentColumnSpace: 2, + parentRowSpace: 3, + leftColumn: 2, + rightColumn: 3, + topRow: 1, + bottomRow: 3, + renderMode: "CANVAS", + isLoading: false, + parentId: "list1", + }, + 0: { + type: "CANVAS_WIDGET", + widgetId: "0", + widgetName: "MainContainer", + version: 16, + parentColumnSpace: 2, + parentRowSpace: 3, + leftColumn: 2, + rightColumn: 3, + topRow: 1, + bottomRow: 3, + renderMode: "CANVAS", + isLoading: false, + parentId: "list1", + }, + }, + "buttonWidget1", + "ButtonWidget1", + ); + + expect(result).toStrictEqual(expected); + }); }); diff --git a/app/client/src/sagas/WidgetOperationUtils.ts b/app/client/src/sagas/WidgetOperationUtils.ts index 3dca05a28d..e6c15a553c 100644 --- a/app/client/src/sagas/WidgetOperationUtils.ts +++ b/app/client/src/sagas/WidgetOperationUtils.ts @@ -4,7 +4,7 @@ import { getWidgetMetaProps, getWidgets, } from "./selectors"; -import _, { isString } from "lodash"; +import _, { isString, remove } from "lodash"; import { GridDefaults, MAIN_CONTAINER_WIDGET_ID, @@ -29,6 +29,7 @@ import { } from "utils/DynamicBindingUtils"; import { getNextEntityName } from "utils/AppsmithUtils"; import WidgetFactory from "utils/WidgetFactory"; +import { getParentWithEnhancementFn } from "./WidgetEnhancementHelpers"; export interface CopiedWidgetGroup { widgetId: string; @@ -817,3 +818,45 @@ export function* getParentWidgetIdForGrouping( return pastingIntoWidgetId; } + +/** + * this saga clears out the enhancementMap, template, dynamicBindingPathList and dynamicTriggerPathList when a child + * is deleted in list widget + * + * @param widgets + * @param widgetId + * @param widgetName + * @param parentId + */ +export function updateListWidgetPropertiesOnChildDelete( + widgets: CanvasWidgetsReduxState, + widgetId: string, + widgetName: string, +) { + const clone = JSON.parse(JSON.stringify(widgets)); + + const parentWithEnhancementFn = getParentWithEnhancementFn(widgetId, clone); + + if (parentWithEnhancementFn?.type === "LIST_WIDGET") { + const listWidget = parentWithEnhancementFn; + + // delete widget in template of list + if (listWidget && widgetName in listWidget.template) { + listWidget.template[widgetName] = undefined; + } + + // delete dynamic binding path if any + remove(listWidget?.dynamicBindingPathList || [], (path: any) => + path.key.startsWith(`template.${widgetName}`), + ); + + // delete dynamic trigger path if any + remove(listWidget?.dynamicTriggerPathList || [], (path: any) => + path.key.startsWith(`template.${widgetName}`), + ); + + return clone; + } + + return clone; +}