diff --git a/app/client/src/actions/pageActions.tsx b/app/client/src/actions/pageActions.tsx index acecde88ec..691e45ed45 100644 --- a/app/client/src/actions/pageActions.tsx +++ b/app/client/src/actions/pageActions.tsx @@ -8,7 +8,7 @@ import { SavePageSuccessPayload, FetchPageListPayload, } from "constants/ReduxActionConstants"; -import { FlattenedWidgetProps } from "reducers/entityReducers/canvasWidgetsReducer"; +import { CanvasWidgetsReduxState } from "reducers/entityReducers/canvasWidgetsReducer"; import { ContainerWidgetProps } from "widgets/ContainerWidget"; import AnalyticsUtil from "utils/AnalyticsUtil"; import { APP_MODE, UrlDataState } from "reducers/entityReducers/appReducer"; @@ -85,7 +85,7 @@ export const deletePageSuccess = () => { }; }; -export const updateAndSaveLayout = (widgets: FlattenedWidgetProps) => { +export const updateAndSaveLayout = (widgets: CanvasWidgetsReduxState) => { return { type: ReduxActionTypes.UPDATE_LAYOUT, payload: { widgets }, diff --git a/app/client/src/entities/DataTree/dataTreeFactory.ts b/app/client/src/entities/DataTree/dataTreeFactory.ts index f629b8cfb9..a32f8a6b5b 100644 --- a/app/client/src/entities/DataTree/dataTreeFactory.ts +++ b/app/client/src/entities/DataTree/dataTreeFactory.ts @@ -139,7 +139,7 @@ export class DataTreeFactory { widget.type, ); const derivedProps: any = {}; - const dynamicBindings = widget.dynamicBindings || {}; + const dynamicBindings = { ...widget.dynamicBindings } || {}; Object.keys(dynamicBindings).forEach(propertyName => { if (_.isObject(widget[propertyName])) { // Stringify this because composite controls may have bindings in the sub controls diff --git a/app/client/src/reducers/entityReducers/canvasWidgetsReducer.tsx b/app/client/src/reducers/entityReducers/canvasWidgetsReducer.tsx index 0066393c28..2588ca7dcc 100644 --- a/app/client/src/reducers/entityReducers/canvasWidgetsReducer.tsx +++ b/app/client/src/reducers/entityReducers/canvasWidgetsReducer.tsx @@ -1,4 +1,4 @@ -import { createReducer } from "utils/AppsmithUtils"; +import { createImmerReducer } from "utils/AppsmithUtils"; import { ReduxActionTypes, UpdateCanvasPayload, @@ -13,31 +13,25 @@ export type FlattenedWidgetProps = WidgetProps & { children?: string[]; }; -const canvasWidgetsReducer = createReducer(initialState, { +const canvasWidgetsReducer = createImmerReducer(initialState, { [ReduxActionTypes.UPDATE_CANVAS]: ( state: CanvasWidgetsReduxState, action: ReduxAction, ) => { - return { ...action.payload.widgets }; + return action.payload.widgets; }, [ReduxActionTypes.UPDATE_LAYOUT]: ( state: CanvasWidgetsReduxState, action: ReduxAction, ) => { - return { ...action.payload.widgets }; + return action.payload.widgets; }, [ReduxActionTypes.UPDATE_WIDGET_PROPERTY]: ( state: CanvasWidgetsReduxState, action: ReduxAction, ) => { - const widget = state[action.payload.widgetId]; - return { - ...state, - [action.payload.widgetId]: { - ...widget, - [action.payload.propertyName]: action.payload.propertyValue, - }, - }; + state[action.payload.widgetId][action.payload.propertyName] = + action.payload.propertyValue; }, }); diff --git a/app/client/src/sagas/ErrorSagas.tsx b/app/client/src/sagas/ErrorSagas.tsx index b823abee67..c268d8f5bc 100644 --- a/app/client/src/sagas/ErrorSagas.tsx +++ b/app/client/src/sagas/ErrorSagas.tsx @@ -10,6 +10,7 @@ import { ApiResponse } from "api/ApiResponses"; import { put, takeLatest, call } from "redux-saga/effects"; import { ERROR_401, ERROR_500, ERROR_0 } from "constants/messages"; import { ToastType } from "react-toastify"; +import log from "loglevel"; export function* callAPI(apiCall: any, requestPayload: any) { try { @@ -84,7 +85,8 @@ export function* errorSaga( ) { // Just a pass through for now. // Add procedures to customize errors here - console.log({ error: errorAction }); + log.debug(`Error in action ${errorAction.type}`); + log.error(errorAction.payload.error); // Show a toast when the error occurs const { type, diff --git a/app/client/src/sagas/WidgetOperationSagas.tsx b/app/client/src/sagas/WidgetOperationSagas.tsx index d7e8650d4f..c31fd35d58 100644 --- a/app/client/src/sagas/WidgetOperationSagas.tsx +++ b/app/client/src/sagas/WidgetOperationSagas.tsx @@ -158,26 +158,32 @@ function* generateChildWidgets( export function* addChildSaga(addChildAction: ReduxAction) { try { + const start = performance.now(); AppToaster.clear(); const { widgetId } = addChildAction.payload; // Get the current parent widget whose child will be the new widget. - const parent: FlattenedWidgetProps = yield select(getWidget, widgetId); + const stateParent: FlattenedWidgetProps = yield select(getWidget, widgetId); + // const parent = Object.assign({}, stateParent); // Get all the widgets from the canvasWidgetsReducer - const widgets = yield select(getWidgets); + const stateWidgets = yield select(getWidgets); + const widgets = Object.assign({}, stateWidgets); // Generate the full WidgetProps of the widget to be added. const childWidgetPayload: GeneratedWidgetPayload = yield generateChildWidgets( - parent, + stateParent, addChildAction.payload, widgets, ); // Update widgets to put back in the canvasWidgetsReducer // TODO(abhinav): This won't work if dont already have an empty children: [] - - if (parent.children) parent.children.push(childWidgetPayload.widgetId); + const parent = { + ...stateParent, + children: [...stateParent.children, childWidgetPayload.widgetId], + }; widgets[parent.widgetId] = parent; + log.debug("add child computations took", performance.now() - start, "ms"); yield put(updateAndSaveLayout(widgets)); } catch (error) { yield put({ @@ -199,7 +205,8 @@ export function* addChildrenSaga( ) { try { const { widgetId, children } = addChildrenAction.payload; - const widgets = yield select(getWidgets); + const stateWidgets = yield select(getWidgets); + const widgets = { ...stateWidgets }; const widgetNames = Object.keys(widgets).map(w => widgets[w].widgetName); children.forEach(child => { @@ -215,12 +222,13 @@ export function* addChildrenSaga( widgetName: newWidgetName, renderMode: RenderModes.CANVAS, }; - if ( - widgets[widgetId].children && - Array.isArray(widgets[widgetId].children) - ) { - widgets[widgetId].children?.push(child.widgetId); - } else widgets[widgetId].children = [child.widgetId]; + + const existingChildren = widgets[widgetId].children || []; + + widgets[widgetId] = { + ...widgets[widgetId], + children: [...existingChildren, child.widgetId], + }; } }); @@ -265,9 +273,15 @@ export function* deleteSaga(deleteAction: ReduxAction) { } if (widgetId && parentId) { - const widgets = yield select(getWidgets); - const widget = yield select(getWidget, widgetId); - const parent: FlattenedWidgetProps = yield select(getWidget, parentId); + const stateWidgets = yield select(getWidgets); + const widgets = { ...stateWidgets }; + const stateWidget = yield select(getWidget, widgetId); + const widget = { ...stateWidget }; + const stateParent: FlattenedWidgetProps = yield select( + getWidget, + parentId, + ); + let parent = { ...stateParent }; const analyticsEvent = isShortcut ? "WIDGET_DELETE_VIA_SHORTCUT" @@ -281,9 +295,10 @@ export function* deleteSaga(deleteAction: ReduxAction) { // Remove entry from parent's children if (parent.children) { - const indexOfChild = parent.children.indexOf(widgetId); - if (indexOfChild > -1) delete parent.children[indexOfChild]; - parent.children = parent.children.filter(Boolean); + parent = { + ...parent, + children: parent.children.filter(c => c !== widgetId), + }; } widgets[parentId] = parent; @@ -319,11 +334,12 @@ export function* deleteSaga(deleteAction: ReduxAction) { }, WIDGET_DELETE_UNDO_TIMEOUT); } - otherWidgetsToDelete.forEach(widget => { - delete widgets[widget.widgetId]; - }); + const finalWidgets = _.omit( + widgets, + otherWidgetsToDelete.map(widgets => widgets.widgetId), + ); - yield put(updateAndSaveLayout(widgets)); + yield put(updateAndSaveLayout(finalWidgets)); } } catch (error) { yield put({ @@ -337,13 +353,18 @@ export function* deleteSaga(deleteAction: ReduxAction) { } export function* undoDeleteSaga(action: ReduxAction<{ widgetId: string }>) { + // Get the list of widget and its children which were deleted const deletedWidgets: FlattenedWidgetProps[] = yield getDeletedWidgets( action.payload.widgetId, ); + // Find the parent in the list of deleted widgets const deletedWidget = deletedWidgets.find( widget => widget.widgetId === action.payload.widgetId, ); + + // If the deleted widget is infact available. if (deletedWidget) { + // Log an undo event AnalyticsUtil.logEvent("WIDGET_DELETE_UNDO", { widgetName: deletedWidget.widgetName, widgetType: deletedWidget.type, @@ -351,13 +372,18 @@ export function* undoDeleteSaga(action: ReduxAction<{ widgetId: string }>) { } if (deletedWidgets) { - const widgets = yield select(getWidgets); + // Get the current list of widgets from reducer + const stateWidgets = yield select(getWidgets); + let widgets = { ...stateWidgets }; + // For each deleted widget deletedWidgets.forEach(widget => { + // Add it to the widgets list we fetched from reducer widgets[widget.widgetId] = widget; + // If the widget in question is the deleted widget if (widget.widgetId === action.payload.widgetId) { //SPECIAL HANDLING FOR TAB IN A TABS WIDGET if (widget.tabId && widget.type === WidgetTypes.CANVAS_WIDGET) { - const parent = widgets[widget.parentId]; + const parent = { ...widgets[widget.parentId] }; if (parent.tabs) { try { const tabs = _.isString(parent.tabs) @@ -368,7 +394,13 @@ export function* undoDeleteSaga(action: ReduxAction<{ widgetId: string }>) { widgetId: widget.widgetId, label: widget.tabName || widget.widgetName, }); - widgets[widget.parentId].tabs = JSON.stringify(tabs); + widgets = { + ...widgets, + [widget.parentId]: { + ...widgets[widget.parentId], + tabs: JSON.stringify(tabs), + }, + }; } catch (error) { log.debug("Error deleting tabs widget: ", { error }); } @@ -380,11 +412,24 @@ export function* undoDeleteSaga(action: ReduxAction<{ widgetId: string }>) { label: widget.tabName || widget.widgetName, }, ]); + widgets = { + ...widgets, + [widget.parentId]: parent, + }; } } - if (widgets[widget.parentId].children) - widgets[widget.parentId].children?.push(widget.widgetId); - else widgets[widget.parentId].children = [widget.widgetId]; + let newChildren = [widget.widgetId]; + if (widgets[widget.parentId].children) { + // Concatenate the list of paren't children with the current widgetId + newChildren = newChildren.concat(widgets[widget.parentId].children); + } + widgets = { + ...widgets, + [widget.parentId]: { + ...widgets[widget.parentId], + children: newChildren, + }, + }; } }); @@ -396,7 +441,7 @@ export function* undoDeleteSaga(action: ReduxAction<{ widgetId: string }>) { export function* moveSaga(moveAction: ReduxAction) { try { AppToaster.clear(); - + const start = performance.now(); const { widgetId, leftColumn, @@ -404,20 +449,25 @@ export function* moveSaga(moveAction: ReduxAction) { parentId, newParentId, } = moveAction.payload; - let widget: FlattenedWidgetProps = yield select(getWidget, widgetId); + const stateWidget: FlattenedWidgetProps = yield select(getWidget, widgetId); + let widget = Object.assign({}, stateWidget); // Get all widgets from DSL/Redux Store - const widgets = yield select(getWidgets) as any; + const stateWidgets: CanvasWidgetsReduxState = yield select(getWidgets); + const widgets = Object.assign({}, stateWidgets); // Get parent from DSL/Redux Store - const parent: FlattenedWidgetProps = yield select(getWidget, parentId); + const stateParent: FlattenedWidgetProps = yield select(getWidget, parentId); + const parent = { ...stateParent, children: [...stateParent.children] }; // Update position of widget - widget = updateWidgetPosition(widget, leftColumn, topRow); + const updatedPosition = updateWidgetPosition(widget, leftColumn, topRow); + widget = { ...widget, ...updatedPosition }; + // Replace widget with update widget props widgets[widgetId] = widget; // If the parent has changed i.e parentWidgetId is not parent.widgetId if (parent.widgetId !== newParentId && widgetId !== newParentId) { // Remove from the previous parent - if (parent.children) { + if (parent.children && Array.isArray(parent.children)) { const indexOfChild = parent.children.indexOf(widgetId); if (indexOfChild > -1) delete parent.children[indexOfChild]; parent.children = parent.children.filter(Boolean); @@ -426,16 +476,17 @@ export function* moveSaga(moveAction: ReduxAction) { // Add to new parent widgets[parent.widgetId] = parent; - if ( - widgets[newParentId].children && - Array.isArray(widgets[newParentId].children) - ) { - widgets[newParentId].children?.push(widgetId); - } else { - widgets[newParentId].children = [widgetId]; - } + const newParent = { + ...widgets[newParentId], + children: widgets[newParentId].children + ? [...widgets[newParentId].children, widgetId] + : [widgetId], + }; widgets[widgetId].parentId = newParentId; + widgets[newParentId] = newParent; } + log.debug("move computations took", performance.now() - start, "ms"); + yield put(updateAndSaveLayout(widgets)); } catch (error) { yield put({ @@ -451,7 +502,7 @@ export function* moveSaga(moveAction: ReduxAction) { export function* resizeSaga(resizeAction: ReduxAction) { try { AppToaster.clear(); - + const start = performance.now(); const { widgetId, leftColumn, @@ -460,11 +511,14 @@ export function* resizeSaga(resizeAction: ReduxAction) { bottomRow, } = resizeAction.payload; - let widget: FlattenedWidgetProps = yield select(getWidget, widgetId); - const widgets = yield select(getWidgets); + const stateWidget: FlattenedWidgetProps = yield select(getWidget, widgetId); + let widget = { ...stateWidget }; + const stateWidgets = yield select(getWidgets); + const widgets = { ...stateWidgets }; widget = { ...widget, leftColumn, rightColumn, topRow, bottomRow }; widgets[widgetId] = widget; + log.debug("resize computations took", performance.now() - start, "ms"); yield put(updateAndSaveLayout(widgets)); } catch (error) { yield put({ @@ -513,7 +567,8 @@ function* updateDynamicBindings( stringProp = JSON.stringify(propertyValue); } const isDynamic = isDynamicValue(stringProp); - let dynamicBindings: Record = widget.dynamicBindings || {}; + let dynamicBindings: Record = + { ...widget.dynamicBindings } || {}; if (!isDynamic && propertyName in dynamicBindings) { dynamicBindings = _.omit(dynamicBindings, propertyName); } @@ -531,7 +586,8 @@ function* updateWidgetPropertySaga( const { payload: { propertyValue, propertyName, widgetId }, } = updateAction; - const widget: WidgetProps = yield select(getWidget, widgetId); + const stateWidget: WidgetProps = yield select(getWidget, widgetId); + const widget = { ...stateWidget }; const dynamicTriggersUpdated = yield updateDynamicTriggers( widget, @@ -542,7 +598,8 @@ function* updateWidgetPropertySaga( yield updateDynamicBindings(widget, propertyName, propertyValue); yield put(updateWidgetProperty(widgetId, propertyName, propertyValue)); - const widgets = yield select(getWidgets); + const stateWidgets = yield select(getWidgets); + const widgets = { ...stateWidgets, [widgetId]: widget }; yield put(updateAndSaveLayout(widgets)); } @@ -623,7 +680,6 @@ function* updateCanvasSize( function* copyWidgetSaga(action: ReduxAction<{ isShortcut: boolean }>) { const selectedWidget = yield select(getSelectedWidget); - console.log({ selectedWidget }); if (!selectedWidget) return; const widgets = yield select(getWidgets); const widgetsToStore = getAllWidgetsInTree(selectedWidget.widgetId, widgets); @@ -696,7 +752,8 @@ function* pasteWidgetSaga() { widgetType: copiedWidget.type, }); - const widgets = yield select(getWidgets); + const stateWidgets = yield select(getWidgets); + let widgets = { ...stateWidgets }; const selectedWidget = yield select(getSelectedWidget); let newWidgetParentId = MAIN_CONTAINER_WIDGET_ID; @@ -808,14 +865,23 @@ function* pasteWidgetSaga() { widget.parentId = newWidgetParentId; // Also, update the parent widget in the canvas widgets // to include this new copied widget's id in the parent's children + let parentChildren = [widget.widgetId]; if ( widgets[newWidgetParentId].children && Array.isArray(widgets[newWidgetParentId].children) ) { - widgets[newWidgetParentId].children.push(widget.widgetId); - } else { - widgets[newWidgetParentId].children = [widget.widgetId]; + // Add the new child to existing children + parentChildren = parentChildren.concat( + widgets[newWidgetParentId].children, + ); } + widgets = { + ...widgets, + [newWidgetParentId]: { + ...widgets[newWidgetParentId], + children: parentChildren, + }, + }; // If the copied widget's boundaries exceed the parent's // Make the parent scrollable if ( @@ -824,9 +890,11 @@ function* pasteWidgetSaga() { widget.bottomRow * widget.parentRowSpace ) { if (widget.parentId !== MAIN_CONTAINER_WIDGET_ID) { - widgets[ - widgets[newWidgetParentId].parentId - ].shouldScrollContents = true; + const parent = widgets[widgets[newWidgetParentId].parentId]; + widgets[widgets[newWidgetParentId].parentId] = { + ...parent, + shouldScrollContents: true, + }; } } } else { diff --git a/app/client/src/utils/WidgetPropsUtils.tsx b/app/client/src/utils/WidgetPropsUtils.tsx index afaa13f133..792c5e3083 100644 --- a/app/client/src/utils/WidgetPropsUtils.tsx +++ b/app/client/src/utils/WidgetPropsUtils.tsx @@ -461,7 +461,6 @@ export const updateWidgetPosition = ( }; return { - ...widget, ...newPositions, }; };