diff --git a/app/client/cypress/fixtures/autoHeightInvisibleWidgetsDSL.json b/app/client/cypress/fixtures/autoHeightInvisibleWidgetsDSL.json new file mode 100644 index 0000000000..2f313a36dd --- /dev/null +++ b/app/client/cypress/fixtures/autoHeightInvisibleWidgetsDSL.json @@ -0,0 +1,282 @@ +{"dsl": { + "widgetName": "MainContainer", + "backgroundColor": "none", + "rightColumn": 4896, + "snapColumns": 64, + "detachFromLayout": true, + "widgetId": "0", + "topRow": 0, + "bottomRow": 590, + "containerStyle": "none", + "snapRows": 125, + "parentRowSpace": 1, + "type": "CANVAS_WIDGET", + "canExtend": true, + "version": 76, + "minHeight": 1292, + "dynamicTriggerPathList": [], + "parentColumnSpace": 1, + "dynamicBindingPathList": [], + "leftColumn": 0, + "children": [ + { + "resetFormOnClick": false, + "boxShadow": "none", + "widgetName": "Button1", + "buttonColor": "{{appsmith.theme.colors.primaryColor}}", + "displayName": "Button", + "iconSVG": "/static/media/icon.cca026338f1c8eb6df8ba03d084c2fca.svg", + "searchTags": [ + "click", + "submit" + ], + "topRow": 1, + "bottomRow": 24, + "parentRowSpace": 10, + "type": "BUTTON_WIDGET", + "hideCard": false, + "topRowBeforeCollapse": 1, + "animateLoading": true, + "parentColumnSpace": 16.234375, + "dynamicTriggerPathList": [], + "leftColumn": 18, + "dynamicBindingPathList": [ + { + "key": "buttonColor" + }, + { + "key": "borderRadius" + } + ], + "text": "Submit", + "isDisabled": false, + "key": "wgi0jkm894", + "isDeprecated": false, + "rightColumn": 34, + "isDefaultClickDisabled": true, + "widgetId": "h2kgzv66ca", + "bottomRowBeforeCollapse": 24, + "isVisible": false, + "recaptchaType": "V3", + "version": 1, + "parentId": "0", + "renderMode": "CANVAS", + "isLoading": false, + "originalTopRow": 1, + "disabledWhenInvalid": false, + "borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}", + "originalBottomRow": 24, + "buttonVariant": "PRIMARY", + "placement": "CENTER" + }, + { + "widgetName": "Divider1", + "thickness": 2, + "displayName": "Divider", + "iconSVG": "/static/media/icon.cbe8f608ca868e1eb44607e5fbd4a9e5.svg", + "searchTags": [ + "line" + ], + "topRow": 24, + "bottomRow": 28, + "parentRowSpace": 10, + "type": "DIVIDER_WIDGET", + "capType": "nc", + "hideCard": false, + "animateLoading": true, + "parentColumnSpace": 16.234375, + "leftColumn": 16, + "dynamicBindingPathList": [], + "key": "tzfnvdc0dc", + "dividerColor": "#858282", + "orientation": "horizontal", + "strokeStyle": "solid", + "isDeprecated": false, + "rightColumn": 36, + "widgetId": "cq1cekp1z2", + "capSide": 0, + "isVisible": true, + "version": 1, + "parentId": "0", + "renderMode": "CANVAS", + "isLoading": false, + "originalTopRow": 24, + "originalBottomRow": 28 + }, + { + "boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}", + "widgetName": "Container1", + "borderColor": "#E0DEDE", + "isCanvas": true, + "displayName": "Container", + "iconSVG": "/static/media/icon.1977dca3370505e2db3a8e44cfd54907.svg", + "searchTags": [ + "div", + "parent", + "group" + ], + "topRow": 36, + "bottomRow": 51, + "parentRowSpace": 10, + "type": "CONTAINER_WIDGET", + "hideCard": false, + "shouldScrollContents": true, + "animateLoading": true, + "parentColumnSpace": 16.234375, + "leftColumn": 15, + "dynamicBindingPathList": [ + { + "key": "borderRadius" + }, + { + "key": "boxShadow" + } + ], + "children": [ + { + "widgetName": "Canvas1", + "displayName": "Canvas", + "topRow": 0, + "bottomRow": 150, + "parentRowSpace": 1, + "type": "CANVAS_WIDGET", + "canExtend": false, + "hideCard": true, + "minHeight": 150, + "parentColumnSpace": 1, + "leftColumn": 0, + "dynamicBindingPathList": [], + "children": [ + { + "boxShadow": "none", + "widgetName": "FilePicker1", + "buttonColor": "{{appsmith.theme.colors.primaryColor}}", + "displayName": "FilePicker", + "iconSVG": "/static/media/icon.7c5ad9c357928c6ff5701bf51a56c2e5.svg", + "searchTags": [ + "upload" + ], + "topRow": 0, + "bottomRow": 9, + "parentRowSpace": 10, + "allowedFileTypes": [], + "type": "FILE_PICKER_WIDGET_V2", + "hideCard": false, + "topRowBeforeCollapse": 0, + "animateLoading": true, + "parentColumnSpace": 5.775390625, + "dynamicTriggerPathList": [], + "leftColumn": 12, + "dynamicBindingPathList": [ + { + "key": "buttonColor" + }, + { + "key": "borderRadius" + } + ], + "isDisabled": false, + "key": "5w3ckl5gj6", + "isRequired": false, + "isDeprecated": false, + "rightColumn": 47, + "isDefaultClickDisabled": true, + "widgetId": "dsp6wm4sk3", + "bottomRowBeforeCollapse": 9, + "isVisible": false, + "label": "Select Files", + "maxFileSize": 5, + "dynamicTyping": true, + "version": 1, + "fileDataType": "Base64", + "parentId": "ay573tnz48", + "selectedFiles": [], + "renderMode": "CANVAS", + "isLoading": false, + "originalTopRow": 0, + "borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}", + "originalBottomRow": 0, + "files": [], + "maxNumFiles": 1 + }, + { + "widgetName": "Checkbox1", + "displayName": "Checkbox", + "iconSVG": "/static/media/icon.aaab032b43383e4fa53ffc0ef40c90ef.svg", + "searchTags": [ + "boolean" + ], + "topRow": 9, + "bottomRow": 13, + "parentRowSpace": 10, + "type": "CHECKBOX_WIDGET", + "alignWidget": "LEFT", + "hideCard": false, + "animateLoading": true, + "parentColumnSpace": 5.775390625, + "leftColumn": 12, + "dynamicBindingPathList": [ + { + "key": "accentColor" + }, + { + "key": "borderRadius" + } + ], + "labelPosition": "Left", + "isDisabled": false, + "key": "5jfy6xde7m", + "isRequired": false, + "isDeprecated": false, + "rightColumn": 47, + "dynamicHeight": "AUTO_HEIGHT", + "widgetId": "3271vh5f32", + "accentColor": "{{appsmith.theme.colors.primaryColor}}", + "isVisible": true, + "label": "Label", + "version": 1, + "parentId": "ay573tnz48", + "renderMode": "CANVAS", + "isLoading": false, + "originalTopRow": 4, + "borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}", + "defaultCheckedState": true, + "maxDynamicHeight": 9000, + "originalBottomRow": 8, + "minDynamicHeight": 4 + } + ], + "key": "dkvxavs6qg", + "isDeprecated": false, + "rightColumn": 389.625, + "detachFromLayout": true, + "widgetId": "ay573tnz48", + "containerStyle": "none", + "isVisible": true, + "version": 1, + "parentId": "ps28fk6mg0", + "renderMode": "CANVAS", + "isLoading": false + } + ], + "borderWidth": "1", + "key": "d72uz4r1l6", + "backgroundColor": "#FFFFFF", + "isDeprecated": false, + "rightColumn": 39, + "dynamicHeight": "AUTO_HEIGHT", + "widgetId": "ps28fk6mg0", + "containerStyle": "card", + "isVisible": true, + "version": 1, + "parentId": "0", + "renderMode": "CANVAS", + "isLoading": false, + "originalTopRow": 36, + "borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}", + "maxDynamicHeight": 9000, + "originalBottomRow": 55, + "minDynamicHeight": 10 + } + ] +}} \ No newline at end of file diff --git a/app/client/cypress/fixtures/autoHeightOverlapDSL.json b/app/client/cypress/fixtures/autoHeightOverlapDSL.json new file mode 100644 index 0000000000..6080aa4bd3 --- /dev/null +++ b/app/client/cypress/fixtures/autoHeightOverlapDSL.json @@ -0,0 +1,189 @@ +{ + "dsl": { + "widgetName": "MainContainer", + "backgroundColor": "none", + "rightColumn": 4896, + "snapColumns": 64, + "detachFromLayout": true, + "widgetId": "0", + "topRow": 0, + "bottomRow": 380, + "containerStyle": "none", + "snapRows": 125, + "parentRowSpace": 1, + "propertyValue": 1292, + "type": "CANVAS_WIDGET", + "canExtend": true, + "propertyPath": "bottomRow", + "version": 76, + "minHeight": 1292, + "dynamicTriggerPathList": [], + "parentColumnSpace": 1, + "dynamicBindingPathList": [], + "leftColumn": 0, + "children": [ + { + "widgetName": "Text1", + "displayName": "Text", + "iconSVG": "/static/media/icon.97c59b523e6f70ba6f40a10fc2c7c5b5.svg", + "searchTags": [ + "typography", + "paragraph", + "label" + ], + "topRow": 5, + "bottomRow": 9, + "parentRowSpace": 10, + "type": "TEXT_WIDGET", + "hideCard": false, + "animateLoading": true, + "overflow": "NONE", + "fontFamily": "{{appsmith.theme.fontFamily.appFont}}", + "parentColumnSpace": 27.5625, + "dynamicTriggerPathList": [], + "leftColumn": 11, + "dynamicBindingPathList": [ + { + "key": "truncateButtonColor" + }, + { + "key": "fontFamily" + }, + { + "key": "borderRadius" + } + ], + "shouldTruncate": false, + "truncateButtonColor": "{{appsmith.theme.colors.primaryColor}}", + "text": "something", + "key": "ryg1uo07f7", + "isDeprecated": false, + "rightColumn": 27, + "textAlign": "LEFT", + "dynamicHeight": "AUTO_HEIGHT", + "widgetId": "m4doxmviiu", + "isVisible": false, + "fontStyle": "BOLD", + "textColor": "#231F20", + "version": 1, + "parentId": "0", + "renderMode": "CANVAS", + "isLoading": false, + "originalTopRow": 5, + "borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}", + "maxDynamicHeight": 9000, + "originalBottomRow": 9, + "fontSize": "1rem", + "minDynamicHeight": 4 + }, + { + "widgetName": "Text2", + "displayName": "Text", + "iconSVG": "/static/media/icon.97c59b523e6f70ba6f40a10fc2c7c5b5.svg", + "searchTags": [ + "typography", + "paragraph", + "label" + ], + "topRow": 13, + "bottomRow": 17, + "parentRowSpace": 10, + "type": "TEXT_WIDGET", + "hideCard": false, + "animateLoading": true, + "overflow": "NONE", + "fontFamily": "{{appsmith.theme.fontFamily.appFont}}", + "parentColumnSpace": 27.5625, + "dynamicTriggerPathList": [], + "leftColumn": 11, + "dynamicBindingPathList": [ + { + "key": "truncateButtonColor" + }, + { + "key": "fontFamily" + }, + { + "key": "borderRadius" + } + ], + "shouldTruncate": false, + "truncateButtonColor": "{{appsmith.theme.colors.primaryColor}}", + "text": "anything", + "key": "ryg1uo07f7", + "isDeprecated": false, + "rightColumn": 27, + "textAlign": "LEFT", + "dynamicHeight": "AUTO_HEIGHT", + "widgetId": "ryq5qy60cg", + "isVisible": false, + "fontStyle": "BOLD", + "textColor": "#231F20", + "version": 1, + "parentId": "0", + "renderMode": "CANVAS", + "isLoading": false, + "originalTopRow": 9, + "borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}", + "maxDynamicHeight": 9000, + "originalBottomRow": 13, + "fontSize": "1rem", + "minDynamicHeight": 4 + }, + { + "widgetName": "Text3", + "displayName": "Text", + "iconSVG": "/static/media/icon.97c59b523e6f70ba6f40a10fc2c7c5b5.svg", + "searchTags": [ + "typography", + "paragraph", + "label" + ], + "topRow": 9, + "bottomRow": 13, + "parentRowSpace": 10, + "type": "TEXT_WIDGET", + "hideCard": false, + "animateLoading": true, + "overflow": "NONE", + "fontFamily": "{{appsmith.theme.fontFamily.appFont}}", + "parentColumnSpace": 27.5625, + "dynamicTriggerPathList": [], + "leftColumn": 11, + "dynamicBindingPathList": [ + { + "key": "truncateButtonColor" + }, + { + "key": "fontFamily" + }, + { + "key": "borderRadius" + } + ], + "shouldTruncate": false, + "truncateButtonColor": "{{appsmith.theme.colors.primaryColor}}", + "text": "another", + "key": "ryg1uo07f7", + "isDeprecated": false, + "rightColumn": 27, + "textAlign": "LEFT", + "dynamicHeight": "AUTO_HEIGHT", + "widgetId": "kx7mvoopqu", + "isVisible": false, + "fontStyle": "BOLD", + "textColor": "#231F20", + "version": 1, + "parentId": "0", + "renderMode": "CANVAS", + "isLoading": false, + "originalTopRow": 13, + "borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}", + "maxDynamicHeight": 9000, + "originalBottomRow": 17, + "fontSize": "1rem", + "minDynamicHeight": 4 + } + ] + } +} \ No newline at end of file diff --git a/app/client/cypress/integration/Regression_TestSuite/ClientSideTests/DynamicHeight/DynamicHeight_Invisible_Widgets_spec.ts b/app/client/cypress/integration/Regression_TestSuite/ClientSideTests/DynamicHeight/DynamicHeight_Invisible_Widgets_spec.ts new file mode 100644 index 0000000000..dbec054deb --- /dev/null +++ b/app/client/cypress/integration/Regression_TestSuite/ClientSideTests/DynamicHeight/DynamicHeight_Invisible_Widgets_spec.ts @@ -0,0 +1,73 @@ +import { ObjectsRegistry } from "../../../../support/Objects/Registry"; + +const { AggregateHelper, CommonLocators, DeployMode } = ObjectsRegistry; + +describe("Fixed Invisible widgets and auto height containers", () => { + before(() => { + // Create a page with a divider below a button widget and a checkbox widget below a filepicker widget + // Button widget and filepicker widgets are fixed height widgets + cy.fixture("autoHeightInvisibleWidgetsDSL").then((val: any) => { + AggregateHelper.AddDsl(val); + }); + }); + + it("1. Divider should be below Button Widget in edit mode", () => { + // This test checks for the height of the button widget and the filepicker widget + // As well as the top value for the widgets below button and filepicker (divider and checkbox respectively) + cy.get(CommonLocators._widgetInDeployed("buttonwidget")).should( + "have.css", + "height", + "230px", + ); + cy.get(CommonLocators._widgetInDeployed("filepickerwidgetv2")).should( + "have.css", + "height", + "90px", + ); + + cy.get(CommonLocators._widgetInDeployed("dividerwidget")).should( + "have.css", + "top", + "246px", + ); + cy.get(CommonLocators._widgetInDeployed("checkboxwidget")).should( + "have.css", + "top", + "96px", + ); + }); + + it("2. Divider should move up by the height of the button widget in preview mode", () => { + // This tests if the divider and checkbox widget move up by an appropriate amount in preview mode. + AggregateHelper.AssertElementVisible( + CommonLocators._previewModeToggle("edit"), + ); + AggregateHelper.GetNClick(CommonLocators._previewModeToggle("edit")); + + cy.get(CommonLocators._widgetInDeployed("dividerwidget")).should( + "have.css", + "top", + "16px", + ); + cy.get(CommonLocators._widgetInDeployed("checkboxwidget")).should( + "have.css", + "top", + "6px", + ); + }); + + it("3. Divider should move up by the height of the button widget in view mode", () => { + // This tests if the divider and checkbox widget move up by an appropriate amount in view mode. + DeployMode.DeployApp(); + cy.get(CommonLocators._widgetInDeployed("dividerwidget")).should( + "have.css", + "top", + "16px", + ); + cy.get(CommonLocators._widgetInDeployed("checkboxwidget")).should( + "have.css", + "top", + "6px", + ); + }); +}); diff --git a/app/client/cypress/integration/Regression_TestSuite/ClientSideTests/DynamicHeight/DynamicHeight_Overlap_Test_spec.ts b/app/client/cypress/integration/Regression_TestSuite/ClientSideTests/DynamicHeight/DynamicHeight_Overlap_Test_spec.ts new file mode 100644 index 0000000000..0bb46fb914 --- /dev/null +++ b/app/client/cypress/integration/Regression_TestSuite/ClientSideTests/DynamicHeight/DynamicHeight_Overlap_Test_spec.ts @@ -0,0 +1,32 @@ +import { ObjectsRegistry } from "../../../../support/Objects/Registry"; + +const { AggregateHelper, CommonLocators, DeployMode } = ObjectsRegistry; + +describe("Fixed Invisible widgets and auto height containers", () => { + before(() => { + // Create a page with a divider below a button widget and a checkbox widget below a filepicker widget + // Button widget and filepicker widgets are fixed height widgets + cy.fixture("autoHeightOverlapDSL").then((val: any) => { + AggregateHelper.AddDsl(val); + }); + }); + + it("1. Invisible widgets should not overlap when returning from preview mode to edit mode", () => { + cy.get(CommonLocators._widgetInDeployed("textwidget")); + AggregateHelper.AssertContains("anything", "exist", "#ryq5qy60cg"); + + AggregateHelper.AssertElementVisible( + CommonLocators._previewModeToggle("edit"), + ); + AggregateHelper.GetNClick(CommonLocators._previewModeToggle("edit")); + + AggregateHelper.AssertElementVisible( + CommonLocators._previewModeToggle("preview"), + ); + AggregateHelper.GetNClick(CommonLocators._previewModeToggle("preview")); + + cy.get("#ryq5qy60cg").should("have.css", "top", "136px"); + cy.get("#kx7mvoopqu").should("have.css", "top", "96px"); + cy.get("#m4doxmviiu").should("have.css", "top", "56px"); + }); +}); diff --git a/app/client/src/actions/controlActions.tsx b/app/client/src/actions/controlActions.tsx index 12eab893cb..d34f0e32c5 100644 --- a/app/client/src/actions/controlActions.tsx +++ b/app/client/src/actions/controlActions.tsx @@ -79,10 +79,11 @@ export const setWidgetDynamicProperty = ( export const updateMultipleWidgetPropertiesAction = ( widgetsToUpdate: UpdateWidgetsPayload, + shouldEval = false, ) => { return { type: ReduxActionTypes.UPDATE_MULTIPLE_WIDGET_PROPERTIES, - payload: widgetsToUpdate, + payload: { widgetsToUpdate, shouldEval }, }; }; diff --git a/app/client/src/ce/constants/ReduxActionConstants.tsx b/app/client/src/ce/constants/ReduxActionConstants.tsx index 03f447a6de..28e5a7007f 100644 --- a/app/client/src/ce/constants/ReduxActionConstants.tsx +++ b/app/client/src/ce/constants/ReduxActionConstants.tsx @@ -1062,6 +1062,7 @@ export interface ApplicationPayload { isAutoUpdate?: boolean; isManualUpdate?: boolean; embedSetting?: AppEmbedSetting; + collapseInvisibleWidgets?: boolean; } export type WorkspaceDetails = { diff --git a/app/client/src/ce/reducers/uiReducers/applicationsReducer.tsx b/app/client/src/ce/reducers/uiReducers/applicationsReducer.tsx index 63daebb1c0..a94d6aa7b3 100644 --- a/app/client/src/ce/reducers/uiReducers/applicationsReducer.tsx +++ b/app/client/src/ce/reducers/uiReducers/applicationsReducer.tsx @@ -132,7 +132,9 @@ export const handlers = { action: ReduxAction<{ applicationList: ApplicationPayload[] }>, ) => ({ ...state, - currentApplication: action.payload, + currentApplication: { + ...action.payload, + }, isFetchingApplication: false, }), [ReduxActionTypes.CURRENT_APPLICATION_NAME_UPDATE]: ( diff --git a/app/client/src/constants/WidgetConstants.tsx b/app/client/src/constants/WidgetConstants.tsx index 10a23cc032..9698c9ab3f 100644 --- a/app/client/src/constants/WidgetConstants.tsx +++ b/app/client/src/constants/WidgetConstants.tsx @@ -142,6 +142,8 @@ export const WIDGET_STATIC_PROPS = { detachFromLayout: true, noContainerOffset: false, height: false, + topRowBeforeCollapse: false, + bottomRowBeforeCollapse: false, }; export const WIDGET_DSL_STRUCTURE_PROPS = { @@ -181,4 +183,6 @@ export const WIDGET_PROPS_TO_SKIP_FROM_EVAL = { iconSVG: true, version: true, displayName: true, + topRowBeforeCollapse: false, + bottomRowBeforeCollapse: false, }; diff --git a/app/client/src/reducers/entityReducers/autoHeightReducers/autoHeightLayoutTreeReducer.ts b/app/client/src/reducers/entityReducers/autoHeightReducers/autoHeightLayoutTreeReducer.ts index 5fe94c8af7..24c2d3b066 100644 --- a/app/client/src/reducers/entityReducers/autoHeightReducers/autoHeightLayoutTreeReducer.ts +++ b/app/client/src/reducers/entityReducers/autoHeightReducers/autoHeightLayoutTreeReducer.ts @@ -22,8 +22,8 @@ const autoHeightLayoutTreeReducer = createImmerReducer(initialState, { action: ReduxAction, ) => { const { tree } = action.payload; - const diff = xor(Object.keys(state), [...Object.keys(tree)]); - for (const widgetId in diff) { + const diff: string[] = xor(Object.keys(state), [...Object.keys(tree)]); + for (const widgetId of diff) { delete state[widgetId]; } for (const widgetId in tree) { diff --git a/app/client/src/reducers/entityReducers/canvasWidgetsReducer.test.ts b/app/client/src/reducers/entityReducers/canvasWidgetsReducer.test.ts index 9c755840f9..b657bf923d 100644 --- a/app/client/src/reducers/entityReducers/canvasWidgetsReducer.test.ts +++ b/app/client/src/reducers/entityReducers/canvasWidgetsReducer.test.ts @@ -19,12 +19,15 @@ describe("Canvas Widgets Reducer", () => { }; const type = ReduxActionTypes.UPDATE_MULTIPLE_WIDGET_PROPERTIES; const payload = { - xyz123: [ - { - propertyPath: "someValue.apple", - propertyValue: "apple", - }, - ], + widgetsToUpdate: { + xyz123: [ + { + propertyPath: "someValue.apple", + propertyValue: "apple", + }, + ], + }, + shouldEval: false, }; const expected = { "0": { children: ["xyz123"] }, @@ -53,12 +56,15 @@ describe("Canvas Widgets Reducer", () => { }; const type = ReduxActionTypes.UPDATE_MULTIPLE_WIDGET_PROPERTIES; const payload = { - xyz123: [ - { - propertyPath: "someValue.games.ball", - propertyValue: ["football"], - }, - ], + widgetsToUpdate: { + xyz123: [ + { + propertyPath: "someValue.games.ball", + propertyValue: ["football"], + }, + ], + }, + shouldEval: false, }; const expected = { "0": { children: ["xyz123"] }, @@ -90,12 +96,15 @@ describe("Canvas Widgets Reducer", () => { }; const type = ReduxActionTypes.UPDATE_MULTIPLE_WIDGET_PROPERTIES; const payload = { - xyz123: [ - { - propertyPath: "someValue.apple", - propertyValue: "orange", - }, - ], + widgetsToUpdate: { + xyz123: [ + { + propertyPath: "someValue.apple", + propertyValue: "orange", + }, + ], + }, + shouldEval: false, }; // Reference equality check using toBe @@ -118,12 +127,15 @@ describe("Canvas Widgets Reducer", () => { }; const type = ReduxActionTypes.UPDATE_MULTIPLE_WIDGET_PROPERTIES; const payload = { - xyz123: [ - { - propertyPath: "someValue.apple", - propertyValue: "orange", - }, - ], + widgetsToUpdate: { + xyz123: [ + { + propertyPath: "someValue.apple", + propertyValue: "orange", + }, + ], + }, + shouldEval: true, }; const result = reducer(initialState, { type, payload }).xyz123.someValue diff --git a/app/client/src/reducers/entityReducers/canvasWidgetsReducer.ts b/app/client/src/reducers/entityReducers/canvasWidgetsReducer.ts index 4612d9a8de..cc5eab5e34 100644 --- a/app/client/src/reducers/entityReducers/canvasWidgetsReducer.ts +++ b/app/client/src/reducers/entityReducers/canvasWidgetsReducer.ts @@ -104,11 +104,14 @@ const canvasWidgetsReducer = createImmerReducer(initialState, { }, [ReduxActionTypes.UPDATE_MULTIPLE_WIDGET_PROPERTIES]: ( state: CanvasWidgetsReduxState, - action: ReduxAction, + action: ReduxAction<{ + widgetsToUpdate: UpdateWidgetsPayload; + shouldEval: boolean; + }>, ) => { // For each widget whose properties we would like to update for (const [widgetId, propertyPathsToUpdate] of Object.entries( - action.payload, + action.payload.widgetsToUpdate, )) { // Iterate through each property to update in `widgetId` propertyPathsToUpdate.forEach(({ propertyPath, propertyValue }) => { @@ -125,7 +128,10 @@ const canvasWidgetsReducer = createImmerReducer(initialState, { const canvasWidgetHeightsToUpdate: Record< string, number - > = getCanvasWidgetHeightsToUpdate(Object.keys(action.payload), state); + > = getCanvasWidgetHeightsToUpdate( + Object.keys(action.payload.widgetsToUpdate), + state, + ); for (const widgetId in canvasWidgetHeightsToUpdate) { state[widgetId].bottomRow = canvasWidgetHeightsToUpdate[widgetId]; } diff --git a/app/client/src/sagas/PageSagas.tsx b/app/client/src/sagas/PageSagas.tsx index e025541fb0..9a0f327cb4 100644 --- a/app/client/src/sagas/PageSagas.tsx +++ b/app/client/src/sagas/PageSagas.tsx @@ -77,6 +77,7 @@ import { getCurrentPageId, getCurrentPageName, getPageById, + previewModeSelector, } from "selectors/editorSelectors"; import { executePageLoadActions, @@ -571,6 +572,7 @@ export function* saveLayoutSaga(action: ReduxAction<{ isRetry?: boolean }>) { try { const currentPageId: string = yield select(getCurrentPageId); const currentPage: Page = yield select(getPageById(currentPageId)); + const isPreviewMode: boolean = yield select(previewModeSelector); const appMode: APP_MODE | undefined = yield select(getAppMode); @@ -585,7 +587,7 @@ export function* saveLayoutSaga(action: ReduxAction<{ isRetry?: boolean }>) { }); } - if (appMode === APP_MODE.EDIT) { + if (appMode === APP_MODE.EDIT && !isPreviewMode) { yield put(saveLayout(action.payload.isRetry)); } } catch (error) { diff --git a/app/client/src/sagas/WidgetOperationSagas.tsx b/app/client/src/sagas/WidgetOperationSagas.tsx index 1947ebaee0..144febaeed 100644 --- a/app/client/src/sagas/WidgetOperationSagas.tsx +++ b/app/client/src/sagas/WidgetOperationSagas.tsx @@ -28,6 +28,7 @@ import { batchUpdateWidgetProperty, DeleteWidgetPropertyPayload, SetWidgetDynamicPropertyPayload, + updateMultipleWidgetPropertiesAction, UpdateWidgetPropertyPayload, UpdateWidgetPropertyRequestPayload, } from "actions/controlActions"; @@ -140,7 +141,6 @@ import { flashElementsById } from "utils/helpers"; import { getSlidingArenaName } from "constants/componentClassNameConstants"; import { builderURL } from "RouteBuilder"; import history from "utils/history"; -import { updateMultipleWidgetProperties } from "actions/widgetActions"; import { generateAutoHeightLayoutTreeAction } from "actions/autoHeightActions"; import { traverseTreeAndExecuteBlueprintChildOperations } from "./WidgetBlueprintSagas"; import { MetaState } from "reducers/entityReducers/metaReducer"; @@ -798,7 +798,7 @@ function* updateCanvasSize( // Check this out when non canvas widgets are updating snapRows // erstwhile: Math.round((rows * props.snapRowSpace) / props.parentRowSpace), yield put( - updateMultipleWidgetProperties({ + updateMultipleWidgetPropertiesAction({ [canvasWidgetId]: [ { propertyPath: "bottomRow", diff --git a/app/client/src/sagas/autoHeightSagas/batcher.ts b/app/client/src/sagas/autoHeightSagas/batcher.ts index bb1c518d84..5bcc5b7039 100644 --- a/app/client/src/sagas/autoHeightSagas/batcher.ts +++ b/app/client/src/sagas/autoHeightSagas/batcher.ts @@ -3,8 +3,11 @@ import { ReduxActionTypes, } from "@appsmith/constants/ReduxActionConstants"; import { UpdateWidgetAutoHeightPayload } from "actions/autoHeightActions"; +import { updateAndSaveLayout } from "actions/pageActions"; import log from "loglevel"; +import { CanvasWidgetsReduxState } from "reducers/entityReducers/canvasWidgetsReducer"; import { put, select } from "redux-saga/effects"; +import { getWidgets } from "sagas/selectors"; import { getIsDraggingOrResizing } from "selectors/widgetSelectors"; // eslint-disable-next-line no-var @@ -39,3 +42,20 @@ export function* batchCallsToUpdateWidgetAutoHeightSaga( type: ReduxActionTypes.PROCESS_AUTO_HEIGHT_UPDATES, }); } + +// In this saga, we simply call the UPDATE_LAYOUT, with shouldReplay: false +// This makes sure that we call eval, but we don't add the updates to the replay stack +export function* callEvalWithoutReplay( + action: ReduxAction<{ widgetsToUpdate: any; shouldEval: boolean }>, +) { + if (action.payload.shouldEval) { + const widgets: CanvasWidgetsReduxState = yield select(getWidgets); + yield put( + updateAndSaveLayout(widgets, { + shouldReplay: false, + isRetry: false, + updatedWidgetIds: [], + }), + ); + } +} diff --git a/app/client/src/sagas/autoHeightSagas/containers.ts b/app/client/src/sagas/autoHeightSagas/containers.ts index 9c261b89d4..e027dede55 100644 --- a/app/client/src/sagas/autoHeightSagas/containers.ts +++ b/app/client/src/sagas/autoHeightSagas/containers.ts @@ -4,13 +4,11 @@ import { } from "@appsmith/constants/ReduxActionConstants"; import { GridDefaults } from "constants/WidgetConstants"; import log from "loglevel"; -import { AutoHeightLayoutTreeReduxState } from "reducers/entityReducers/autoHeightReducers/autoHeightLayoutTreeReducer"; import { CanvasWidgetsReduxState } from "reducers/entityReducers/canvasWidgetsReducer"; import { call, put, select } from "redux-saga/effects"; import { getMinHeightBasedOnChildren, shouldWidgetsCollapse } from "./helpers"; import { getWidgets } from "sagas/selectors"; import { getCanvasHeightOffset } from "utils/WidgetSizeUtils"; -import { getAutoHeightLayoutTree } from "selectors/autoHeightSelectors"; import { FlattenedWidgetProps } from "widgets/constants"; import { getWidgetMaxAutoHeight, @@ -20,6 +18,7 @@ import { import { getChildOfContainerLikeWidget } from "./helpers"; import { getDataTree } from "selectors/dataTreeSelectors"; import { DataTree, DataTreeWidget } from "entities/DataTree/dataTreeFactory"; +import { getLayoutTree } from "./layoutTree"; export function* dynamicallyUpdateContainersSaga( action?: ReduxAction<{ resettingTabs: boolean }>, @@ -37,9 +36,7 @@ export function* dynamicallyUpdateContainersSaga( return isCanvasWidget; }); - const dynamicHeightLayoutTree: AutoHeightLayoutTreeReduxState = yield select( - getAutoHeightLayoutTree, - ); + const { tree: dynamicHeightLayoutTree } = yield getLayoutTree(false); const updates: Record = {}; const shouldCollapse: boolean = yield call(shouldWidgetsCollapse); @@ -120,6 +117,17 @@ export function* dynamicallyUpdateContainersSaga( // Get the larger value between the minDynamicHeightInRows and bottomMostRowForChild maxBottomRow = Math.max(maxBottomRowBasedOnChildren, maxBottomRow); + } else { + // If the parent is not supposed to be collapsed + // Use the canvasHeight offset, as that would be the + // minimum + if ( + parentContainerWidget.bottomRow - parentContainerWidget.topRow > + 0 || + !shouldCollapse + ) { + maxBottomRow += canvasHeightOffset; + } } // The following makes sure we stay within bounds @@ -156,7 +164,7 @@ export function* dynamicallyUpdateContainersSaga( } } - log.debug("Dynamic Height: Container Updates", { updates }); + log.debug("Auto Height: Container Updates", { updates }); if (Object.keys(updates).length > 0) { // TODO(abhinav): Make sure there are no race conditions or scenarios where these updates are not considered. @@ -171,7 +179,7 @@ export function* dynamicallyUpdateContainersSaga( } } log.debug( - "Dynamic height: Container computations time taken:", + "Auto height: Container computations time taken:", performance.now() - start, "ms", ); diff --git a/app/client/src/sagas/autoHeightSagas/helpers.test.ts b/app/client/src/sagas/autoHeightSagas/helpers.test.ts new file mode 100644 index 0000000000..b561b77904 --- /dev/null +++ b/app/client/src/sagas/autoHeightSagas/helpers.test.ts @@ -0,0 +1,78 @@ +import { mutation_setPropertiesToUpdate } from "./helpers"; + +describe("auto height saga helpers", () => { + it("When property exists, it should update correctly", () => { + const propertiesToUpdate = { + x: 50, + y: "newValue", + }; + const originalObject = { + key1: [ + { + propertyPath: "z", + propertyValue: 20, + }, + ], + }; + const expectedResult = { + key1: [ + { + propertyPath: "z", + propertyValue: 20, + }, + { + propertyPath: "x", + propertyValue: 50, + }, + { + propertyPath: "y", + propertyValue: "newValue", + }, + ], + }; + const result = mutation_setPropertiesToUpdate( + originalObject, + "key1", + propertiesToUpdate, + ); + expect(result).toStrictEqual(expectedResult); + }); + it("When property does not exist, it should update correctly", () => { + const propertiesToUpdate = { + x: 50, + y: "newValue", + }; + const originalObject = { + key1: [ + { + propertyPath: "z", + propertyValue: 20, + }, + ], + }; + const expectedResult = { + key1: [ + { + propertyPath: "z", + propertyValue: 20, + }, + ], + key2: [ + { + propertyPath: "x", + propertyValue: 50, + }, + { + propertyPath: "y", + propertyValue: "newValue", + }, + ], + }; + const result = mutation_setPropertiesToUpdate( + originalObject, + "key2", + propertiesToUpdate, + ); + expect(result).toStrictEqual(expectedResult); + }); +}); diff --git a/app/client/src/sagas/autoHeightSagas/helpers.ts b/app/client/src/sagas/autoHeightSagas/helpers.ts index c933438032..456cfa8f4f 100644 --- a/app/client/src/sagas/autoHeightSagas/helpers.ts +++ b/app/client/src/sagas/autoHeightSagas/helpers.ts @@ -1,3 +1,4 @@ +import { AppState } from "@appsmith/reducers"; import { GridDefaults, MAIN_CONTAINER_WIDGET_ID, @@ -12,7 +13,10 @@ import { select } from "redux-saga/effects"; import { getWidgetMetaProps, getWidgets } from "sagas/selectors"; import { previewModeSelector } from "selectors/editorSelectors"; import { getAppMode } from "selectors/entitiesSelector"; +import { isAutoHeightEnabledForWidget } from "widgets/WidgetUtils"; import { getCanvasHeightOffset } from "utils/WidgetSizeUtils"; +import { DataTree, DataTreeWidget } from "entities/DataTree/dataTreeFactory"; +import { getDataTree } from "selectors/dataTreeSelectors"; export function* shouldWidgetsCollapse() { const isPreviewMode: boolean = yield select(previewModeSelector); @@ -21,6 +25,14 @@ export function* shouldWidgetsCollapse() { return isPreviewMode || appMode === APP_MODE.PUBLISHED; } +export function* shouldAllInvisibleWidgetsInAutoHeightContainersCollapse() { + const flag: boolean = yield select((state: AppState) => { + return !!state.ui.applications.currentApplication?.collapseInvisibleWidgets; + }); + + return flag; +} + export function* getChildOfContainerLikeWidget( containerLikeWidget: FlattenedWidgetProps, ) { @@ -84,6 +96,9 @@ export function* getMinHeightBasedOnChildren( const shouldCollapse: boolean = yield shouldWidgetsCollapse(); // Get all widgets in the DSL const stateWidgets: CanvasWidgetsReduxState = yield select(getWidgets); + // Skip this whole process if the parent is collapsed: Process: + // Get the DataTree + const dataTree: DataTree = yield select(getDataTree); const { children = [], parentId } = stateWidgets[widgetId]; // If we need to consider the parent height @@ -140,6 +155,23 @@ export function* getMinHeightBasedOnChildren( // detachFromLayout helps us identify such widgets if (detachFromLayout) continue; + // Seems like sometimes, the children comes in as a string instead of string array. + // I'm not completely sure why that is, or which widgets use "children" properties as strings + // So, we're skipping computations for the children if such a thing happens. + if (tree[childWidgetId] === undefined) continue; + + // Get this parentContainerWidget from the DataTree + const dataTreeWidget = dataTree[stateWidgets[childWidgetId].widgetName]; + // If the widget exists, is not visible and we can collapse widgets + + if ( + dataTreeWidget && + (dataTreeWidget as DataTreeWidget).isVisible !== true && + shouldCollapse + ) { + continue; + } + // Get the child widget's dimenstions from the tree const { bottomRow, topRow } = tree[childWidgetId]; @@ -169,3 +201,91 @@ export function* getMinHeightBasedOnChildren( return minHeightInRows; } +/** + * This function takes a widgetId and computes whether it can have zero height + * Widget can have zero height if it has auto height enabled + * + * + * Or if it is a child of a widget which has auto height enabled + * (This is verified using shouldAllInvisibleWidgetsInAutoHeightContainersCollapse) + * + * @param stateWidgets The canvas widgets redux state needed for computations + * @param widgetId The widget which is trying to collapse + * @returns true if this widget can be collapsed to zero height + */ +export function* shouldCollapseThisWidget( + stateWidgets: CanvasWidgetsReduxState, + widgetId: string, +) { + const shouldCollapse: boolean = yield shouldWidgetsCollapse(); + const canCollapseAllWidgets: boolean = yield shouldAllInvisibleWidgetsInAutoHeightContainersCollapse(); + const widget = stateWidgets[widgetId]; + + // If we're in preview or view mode + if (shouldCollapse) { + // If this widget has auto height enabled + if (isAutoHeightEnabledForWidget(widget)) { + return true; + } + + // Get the parent Canvas widgetId + const parentId = widget.parentId; + + if (parentId === MAIN_CONTAINER_WIDGET_ID && canCollapseAllWidgets) { + return true; + } + + // Get the grandparent or the parent container like widget + const parentContainerLikeWidgetId = parentId + ? stateWidgets[parentId].parentId + : false; + + // If the parent container like widget exists + if (parentContainerLikeWidgetId) { + const parentContainerLikeWidget = + stateWidgets[parentContainerLikeWidgetId]; + // If we can collapse widgets within all auto height container like widgets + // and if the parent container like widget exists + // and if auto height is enabled for the parent container + // or if the parent is the main container + if ( + parentContainerLikeWidget && + canCollapseAllWidgets && + isAutoHeightEnabledForWidget(parentContainerLikeWidget) + ) { + return true; + } + } + } + return false; +} + +/** + * This function converts a standard object containing the properties to update + * into the expected structure of { propertyPath: string, propertyValue: unknown } + * @param originalObject The original object to mutate + * @param widgetId The widgetId which will be the key in the object to mutate + * @param propertiesToUpdate The properties which need to be added in the original object's widgetId key + * @returns mutated object + */ +export function mutation_setPropertiesToUpdate( + originalObject: Record< + string, + Array<{ propertyPath: string; propertyValue: unknown }> + >, + widgetId: string, + propertiesToUpdate: Record, +) { + if (!originalObject.hasOwnProperty(widgetId)) { + originalObject[widgetId] = []; + } + + for (const [key, value] of Object.entries(propertiesToUpdate)) { + originalObject[widgetId].push({ + propertyPath: key, + propertyValue: value, + }); + } + + return originalObject; +} diff --git a/app/client/src/sagas/autoHeightSagas/index.ts b/app/client/src/sagas/autoHeightSagas/index.ts index d5a02f7751..1a2364c6aa 100644 --- a/app/client/src/sagas/autoHeightSagas/index.ts +++ b/app/client/src/sagas/autoHeightSagas/index.ts @@ -1,6 +1,9 @@ import { ReduxActionTypes } from "@appsmith/constants/ReduxActionConstants"; import { all, debounce, takeEvery, takeLatest } from "redux-saga/effects"; -import { batchCallsToUpdateWidgetAutoHeightSaga } from "./batcher"; +import { + batchCallsToUpdateWidgetAutoHeightSaga, + callEvalWithoutReplay, +} from "./batcher"; import { dynamicallyUpdateContainersSaga } from "./containers"; import { generateTreeForAutoHeightComputations } from "./layoutTree"; import { updateWidgetAutoHeightSaga } from "./widgets"; @@ -31,5 +34,9 @@ export default function* autoHeightSagas() { ReduxActionTypes.GENERATE_AUTO_HEIGHT_LAYOUT_TREE, // add, move, paste, cut, delete, undo/redo generateTreeForAutoHeightComputations, ), + takeLatest( + ReduxActionTypes.UPDATE_MULTIPLE_WIDGET_PROPERTIES, + callEvalWithoutReplay, + ), ]); } diff --git a/app/client/src/sagas/autoHeightSagas/layoutTree.ts b/app/client/src/sagas/autoHeightSagas/layoutTree.ts index dee6d89538..30c1e65069 100644 --- a/app/client/src/sagas/autoHeightSagas/layoutTree.ts +++ b/app/client/src/sagas/autoHeightSagas/layoutTree.ts @@ -29,7 +29,7 @@ export function* getLayoutTree(layoutUpdated: boolean) { getAutoHeightLayoutTree, ); for (const canvasWidgetId in occupiedSpaces) { - if (occupiedSpaces[canvasWidgetId].length > 0) { + if (Object.keys(occupiedSpaces[canvasWidgetId]).length > 0) { const treeForThisCanvas = generateTree( occupiedSpaces[canvasWidgetId], !shouldCollapse && layoutUpdated, @@ -39,7 +39,7 @@ export function* getLayoutTree(layoutUpdated: boolean) { } } log.debug( - "Dynamic Height: Tree generation time taken:", + "Auto Height: Tree generation time taken:", performance.now() - start, "ms", ); diff --git a/app/client/src/sagas/autoHeightSagas/widgets.ts b/app/client/src/sagas/autoHeightSagas/widgets.ts index ba869dd41f..2aefc1ec9c 100644 --- a/app/client/src/sagas/autoHeightSagas/widgets.ts +++ b/app/client/src/sagas/autoHeightSagas/widgets.ts @@ -22,7 +22,12 @@ import { getAutoHeightUpdateQueue, resetAutoHeightUpdateQueue, } from "./batcher"; -import { getMinHeightBasedOnChildren, shouldWidgetsCollapse } from "./helpers"; +import { + getChildOfContainerLikeWidget, + getMinHeightBasedOnChildren, + mutation_setPropertiesToUpdate, + shouldCollapseThisWidget, +} from "./helpers"; import { updateMultipleWidgetPropertiesAction } from "actions/controlActions"; import { generateAutoHeightLayoutTreeAction, @@ -35,6 +40,7 @@ import { getCanvasLevelMap, } from "selectors/autoHeightSelectors"; import { getLayoutTree } from "./layoutTree"; +import WidgetFactory from "utils/WidgetFactory"; import { ReduxAction } from "@appsmith/constants/ReduxActionConstants"; import { TreeNode } from "utils/autoHeight/constants"; import { directlyMutateDOMNodes } from "utils/autoHeight/mutateDOM"; @@ -71,8 +77,8 @@ export function* updateWidgetAutoHeightSaga( ) { const start = performance.now(); let shouldRecomputeContainers = false; + let shouldEval = false; - const shouldCollapse: boolean = yield shouldWidgetsCollapse(); const appMode: APP_MODE = yield select(getAppMode); let updates = getAutoHeightUpdateQueue(); @@ -108,7 +114,7 @@ export function* updateWidgetAutoHeightSaga( log.debug("Auto Height: updates to process", { updates }); // Initialise all the widgets we will be updating - const widgetsToUpdate: UpdateWidgetsPayload = {}; + let widgetsToUpdate: UpdateWidgetsPayload = {}; // Initialise all expected updates const expectedUpdates: Array<{ @@ -133,12 +139,44 @@ export function* updateWidgetAutoHeightSaga( getWidgetMinAutoHeight(widget) * GridDefaults.DEFAULT_GRID_ROW_HEIGHT; if (widget.type === "TABS_WIDGET") shouldRecomputeContainers = true; + const config = WidgetFactory.widgetConfigMap.get(widget.type); + if (config && config.needsHeightForContent) { + shouldEval = true; + } // In case of a widget going invisible in view mode if (updates[widgetId] === 0) { - if (shouldCollapse && isAutoHeightEnabledForWidget(widget)) { + // Should we allow zero height for this widget? + const shouldCollapse: boolean = yield shouldCollapseThisWidget( + stateWidgets, + widgetId, + ); + + // If zero height is allowed + if (shouldCollapse) { + // setting the min to be 0, will take care of things with the same algorithm minDynamicHeightInPixels = 0; - } else continue; + // We also need a way to reset this widget if it is fixed, this is because, + // for fixed widgets, auto height doesn't trigger, and there is a chance + // that the widget will remain the same zero height even after they become + // visible. + // To do this, we're going to add some extra properties which we can later use to reset + if ( + !isAutoHeightEnabledForWidget(widget) && + widget.topRow !== widget.bottomRow + ) { + widgetsToUpdate = mutation_setPropertiesToUpdate( + widgetsToUpdate, + widgetId, + { + topRowBeforeCollapse: widget.topRow + 0, + bottomRowBeforeCollapse: widget.bottomRow + 0, + }, + ); + } + } else { + continue; + } } const maxDynamicHeightInPixels = @@ -184,20 +222,15 @@ export function* updateWidgetAutoHeightSaga( widgetsMeasuredInPixels.push(widgetId); // Setting the height and dimensions of the Modal Widget - widgetsToUpdate[widgetId] = [ + widgetsToUpdate = mutation_setPropertiesToUpdate( + widgetsToUpdate, + widgetId, { - propertyPath: "height", - propertyValue: newHeight, + height: newHeight, + bottomRow: widget.topRow + newHeight, + topRow: widget.topRow, }, - { - propertyPath: "bottomRow", - propertyValue: widget.topRow + newHeight, - }, - { - propertyPath: "topRow", - propertyValue: widget.topRow, - }, - ]; + ); } } @@ -272,17 +305,14 @@ export function* updateWidgetAutoHeightSaga( // For each widget to update, add to the delta, the expected change. expectedUpdatesGroupedByParentCanvasWidget[ parentCanvasWidgetId - ].forEach((update) => { - delta[ - (update as { - widgetId: string; - expectedChangeInHeightInRows: number; - }).widgetId - ] = (update as { + ].forEach( + (update: { widgetId: string; expectedChangeInHeightInRows: number; - }).expectedChangeInHeightInRows; - }); + }) => { + delta[update.widgetId] = update.expectedChangeInHeightInRows; + }, + ); } }); } @@ -310,6 +340,18 @@ export function* updateWidgetAutoHeightSaga( const parentContainerLikeWidget: FlattenedWidgetProps = stateWidgets[parentCanvasWidget.parentId]; + // Get the child we need to consider + // For a container widget, it will be the child canvas + // For a tabs widget, it will be the currently open tab's canvas + const childWidgetId: + | string + | undefined = yield getChildOfContainerLikeWidget( + parentContainerLikeWidget, + ); + // Skip computations for the parent container like widget + // if this child canvas is not the one currently visible + if (childWidgetId !== parentCanvasWidget.widgetId) continue; + let minCanvasHeightInRows: number = yield getMinHeightBasedOnChildren( parentCanvasWidget.widgetId, changesSoFar, @@ -364,19 +406,6 @@ export function* updateWidgetAutoHeightSaga( }; } - // Convert this change into the standard expected update format. - const expectedUpdate = { - widgetId: parentContainerLikeWidget.widgetId, - expectedHeightinPx: - minHeightInRows * GridDefaults.DEFAULT_GRID_ROW_HEIGHT, - expectedChangeInHeightInRows: - minHeightInRows - (layoutData.bottomRow - layoutData.topRow), - currentTopRow: layoutData.topRow, - currentBottomRow: layoutData.bottomRow, - expectedBottomRow: layoutData.topRow + minHeightInRows, - parentId: parentContainerLikeWidget.parentId, - }; - // If this widget is actually removed from the layout // For example, if this is a ModalWidget // We need to make sure that we change properties other than bottomRow and topRow @@ -387,66 +416,105 @@ export function* updateWidgetAutoHeightSaga( ); // DRY this - widgetsToUpdate[parentContainerLikeWidget.widgetId] = [ + widgetsToUpdate = mutation_setPropertiesToUpdate( + widgetsToUpdate, + parentContainerLikeWidget.widgetId, { - propertyPath: "bottomRow", - propertyValue: minHeightInRows, + bottomRow: minHeightInRows, + height: + minHeightInRows * GridDefaults.DEFAULT_GRID_ROW_HEIGHT, + minHeight: + minHeightInRows * GridDefaults.DEFAULT_GRID_ROW_HEIGHT, }, - { - propertyPath: "height", - propertyValue: - (minHeightInRows + GridDefaults.CANVAS_EXTENSION_OFFSET) * - GridDefaults.DEFAULT_GRID_ROW_HEIGHT, - }, - { - propertyPath: "minHeight", - propertyValue: - (minHeightInRows + GridDefaults.CANVAS_EXTENSION_OFFSET) * - GridDefaults.DEFAULT_GRID_ROW_HEIGHT, - }, - ]; + ); } - // If this is not a widget which is outside of the layout, - // We must check if it has a parent - // It most likely will, as this widget cannot be the MainContainer - // The maincontainer is a Canvas Widget, not a container like widget. - if ( - !parentContainerLikeWidget.detachFromLayout && - parentContainerLikeWidget.parentId - ) { - // If this widget's parent canvas already has some updates - // We push this update to the existing array. - // DRY THIS - if ( - expectedUpdatesGroupedByParentCanvasWidget.hasOwnProperty( - parentContainerLikeWidget.parentId, - ) - ) { - expectedUpdatesGroupedByParentCanvasWidget[ - parentContainerLikeWidget.parentId - ].push(expectedUpdate); - } else { - // Otherwise, we add a new entry. - expectedUpdatesGroupedByParentCanvasWidget[ - parentContainerLikeWidget.parentId - ] = [expectedUpdate]; - } + // If the parent container is trying to collapse already + // Then the changes in the child should not effect the parent + // For this we need to check for two different scenarios + // 1. The parent is collapsing in this computation cycle + // 2. The parent is already collapsed and should stay collapsed - // The parent might not have been added to the previously created group - // parentCanvasWidgetGroupedByLevel - const _level = - canvasLevelMap[parentContainerLikeWidget.parentId]; - // So, we add it, if it is not the MainContainer. - // This way it will be used in parentCanvasWidgetsToConsider - // MainContainer was added when we initialised this variable, - // so we're skipping it. level === 0 is true only for the MainContainer. - if (_level !== 0) { + // Get the parent from existing updates in this computation + // cycle. + const existingUpdate = expectedUpdates.find( + (update) => + update.widgetId === parentContainerLikeWidget.widgetId, + ); + + // Check if the parent has collapsed previously + // And it needs to stay collapsed + const shouldCollapseParent = + shouldCollapseThisWidget( + stateWidgets, + parentContainerLikeWidget.widgetId, + ) && + parentContainerLikeWidget.topRow === + parentContainerLikeWidget.bottomRow && + !existingUpdate; + + // If both the above conditions are false + // Then update the expected updates for further + // computations + if ( + (existingUpdate === undefined || + existingUpdate.expectedHeightinPx !== 0) && + !shouldCollapseParent + ) { + // Convert this change into the standard expected update format. + const expectedUpdate = { + widgetId: parentContainerLikeWidget.widgetId, + expectedHeightinPx: + minHeightInRows * GridDefaults.DEFAULT_GRID_ROW_HEIGHT, + expectedChangeInHeightInRows: + minHeightInRows - + (layoutData.bottomRow - layoutData.topRow), + currentTopRow: layoutData.topRow, + currentBottomRow: layoutData.bottomRow, + expectedBottomRow: layoutData.topRow + minHeightInRows, + parentId: parentContainerLikeWidget.parentId, + }; + // If this is not a widget which is outside of the layout, + // We must check if it has a parent + // It most likely will, as this widget cannot be the MainContainer + // The maincontainer is a Canvas Widget, not a container like widget. + if ( + !parentContainerLikeWidget.detachFromLayout && + parentContainerLikeWidget.parentId + ) { + // If this widget's parent canvas already has some updates + // We push this update to the existing array. // DRY THIS - parentCanvasWidgetsGroupedByLevel[_level] = uniq([ - ...(parentCanvasWidgetsGroupedByLevel[_level] || []), - parentContainerLikeWidget.parentId, - ]); + if ( + expectedUpdatesGroupedByParentCanvasWidget.hasOwnProperty( + parentContainerLikeWidget.parentId, + ) + ) { + expectedUpdatesGroupedByParentCanvasWidget[ + parentContainerLikeWidget.parentId + ].push(expectedUpdate); + } else { + // Otherwise, we add a new entry. + expectedUpdatesGroupedByParentCanvasWidget[ + parentContainerLikeWidget.parentId + ] = [expectedUpdate]; + } + + // The parent might not have been added to the previously created group + // parentCanvasWidgetGroupedByLevel + const _level = + canvasLevelMap[parentContainerLikeWidget.parentId]; + // So, we add it, if it is not the MainContainer. + // This way it will be used in parentCanvasWidgetsToConsider + // MainContainer was added when we initialised this variable, + // so we're skipping it. level === 0 is true only for the MainContainer. + if (_level !== 0) { + // DRY THIS + parentCanvasWidgetsGroupedByLevel[_level] = uniq([ + ...(parentCanvasWidgetsGroupedByLevel[_level] || []), + parentContainerLikeWidget.parentId, + ]); + } } } } @@ -482,13 +550,13 @@ export function* updateWidgetAutoHeightSaga( widgetsMeasuredInPixels.push(MAIN_CONTAINER_WIDGET_ID); // Add the MainContainer's update. - widgetsToUpdate[MAIN_CONTAINER_WIDGET_ID] = [ + widgetsToUpdate = mutation_setPropertiesToUpdate( + widgetsToUpdate, + MAIN_CONTAINER_WIDGET_ID, { - propertyPath: "bottomRow", - propertyValue: - maxCanvasHeightInRows * GridDefaults.DEFAULT_GRID_ROW_HEIGHT, + bottomRow: maxCanvasHeightInRows * GridDefaults.DEFAULT_GRID_ROW_HEIGHT, }, - ]; + ); // Convert the changesSoFar (this are the computed changes) // To the widgetsToUpdate data structure for final reducer update. @@ -498,33 +566,23 @@ export function* updateWidgetAutoHeightSaga( changedWidgetId ]; - if (!action?.payload) { - const canvasOffset = getCanvasHeightOffset( - stateWidgets[changedWidgetId].type, - stateWidgets[changedWidgetId], - ); + const canvasOffset = getCanvasHeightOffset( + stateWidgets[changedWidgetId].type, + stateWidgets[changedWidgetId], + ); - widgetCanvasOffsets[changedWidgetId] = canvasOffset; - } + widgetCanvasOffsets[changedWidgetId] = canvasOffset; - widgetsToUpdate[changedWidgetId] = [ + widgetsToUpdate = mutation_setPropertiesToUpdate( + widgetsToUpdate, + changedWidgetId, { - propertyPath: "bottomRow", - propertyValue: changesSoFar[changedWidgetId].bottomRow, + bottomRow: changesSoFar[changedWidgetId].bottomRow, + topRow: changesSoFar[changedWidgetId].topRow, + originalTopRow: originalTopRow, + originalBottomRow: originalBottomRow, }, - { - propertyPath: "topRow", - propertyValue: changesSoFar[changedWidgetId].topRow, - }, - { - propertyPath: "originalTopRow", - propertyValue: originalTopRow, - }, - { - propertyPath: "originalBottomRow", - propertyValue: originalBottomRow, - }, - ]; + ); } } @@ -535,7 +593,9 @@ export function* updateWidgetAutoHeightSaga( // Push all updates to the CanvasWidgetsReducer. // Note that we're not calling `UPDATE_LAYOUT` // as we don't need to trigger an eval - yield put(updateMultipleWidgetPropertiesAction(widgetsToUpdate)); + yield put( + updateMultipleWidgetPropertiesAction(widgetsToUpdate, shouldEval), + ); resetAutoHeightUpdateQueue(); yield put( generateAutoHeightLayoutTreeAction( diff --git a/app/client/src/selectors/editorSelectors.tsx b/app/client/src/selectors/editorSelectors.tsx index 5e4d268cd4..27fcc7cb1e 100644 --- a/app/client/src/selectors/editorSelectors.tsx +++ b/app/client/src/selectors/editorSelectors.tsx @@ -482,14 +482,16 @@ export const getOccupiedSpacesGroupedByParentCanvas = createSelector( widgets: CanvasWidgetsReduxState, ): { occupiedSpaces: { - [parentCanvasWidgetId: string]: Array< + [parentCanvasWidgetId: string]: Record< + string, OccupiedSpace & { originalTop: number; originalBottom: number } >; }; canvasLevelMap: Record; } => { const occupiedSpaces: { - [parentCanvasWidgetId: string]: Array< + [parentCanvasWidgetId: string]: Record< + string, OccupiedSpace & { originalTop: number; originalBottom: number } >; } = {}; @@ -521,7 +523,7 @@ export const getOccupiedSpacesGroupedByParentCanvas = createSelector( } canvasLevelMap[canvasWidget.widgetId] = level; // Initialise the occupied spaces with an empty array - occupiedSpaces[canvasWidgetId] = []; + occupiedSpaces[canvasWidgetId] = {}; // If this canvas widget has children if (canvasWidget.children && canvasWidget.children.length > 0) { // Iterate through all children @@ -533,7 +535,7 @@ export const getOccupiedSpacesGroupedByParentCanvas = createSelector( // (unlike a modal widget or another canvas widget) if (!widget.detachFromLayout) { // Add the occupied space co-ordinates to the initialised array - occupiedSpaces[canvasWidgetId].push({ + occupiedSpaces[canvasWidgetId][widget.widgetId] = { id: widget.widgetId, parentId: canvasWidgetId, left: widget.leftColumn, @@ -542,7 +544,7 @@ export const getOccupiedSpacesGroupedByParentCanvas = createSelector( right: widget.rightColumn, originalTop: widget.originalTopRow, originalBottom: widget.originalBottomRow, - }); + }; } }); } diff --git a/app/client/src/utils/WidgetRegisterHelpers.tsx b/app/client/src/utils/WidgetRegisterHelpers.tsx index 884e972504..306a934d53 100644 --- a/app/client/src/utils/WidgetRegisterHelpers.tsx +++ b/app/client/src/utils/WidgetRegisterHelpers.tsx @@ -92,6 +92,7 @@ export const configureWidget = (config: WidgetConfiguration) => { iconSVG: config.iconSVG, isCanvas: config.isCanvas, canvasHeightOffset: config.canvasHeightOffset, + needsHeightForContent: config.needsHeightForContent, }; const nonSerialisableWidgetConfigs: Record = {}; diff --git a/app/client/src/utils/WidgetSizeUtils.ts b/app/client/src/utils/WidgetSizeUtils.ts index a164c582ee..45b76b50e5 100644 --- a/app/client/src/utils/WidgetSizeUtils.ts +++ b/app/client/src/utils/WidgetSizeUtils.ts @@ -163,7 +163,12 @@ export function getCanvasBottomRow( if (Array.isArray(children) && children.length > 0) { const bottomRow = children.reduce((prev, next) => { - if (canvasWidgets[next].detachFromLayout) return prev; + if (canvasWidgets[next].detachFromLayout) { + return prev; + } + if (canvasWidgets[next].bottomRow === canvasWidgets[next].topRow) { + return prev; + } return canvasWidgets[next].bottomRow > prev ? canvasWidgets[next].bottomRow : prev; diff --git a/app/client/src/utils/autoHeight/generateTree.test.ts b/app/client/src/utils/autoHeight/generateTree.test.ts index f723195aef..5a6ec8211b 100644 --- a/app/client/src/utils/autoHeight/generateTree.test.ts +++ b/app/client/src/utils/autoHeight/generateTree.test.ts @@ -3,10 +3,10 @@ import { generateTree } from "./generateTree"; describe("Generate Auto Height Layout tree", () => { it("Does not conflict when only one horizontal edge is the same", () => { - const input: NodeSpace[] = [ - { left: 0, right: 100, top: 0, bottom: 30, id: "1" }, - { left: 100, top: 0, bottom: 30, right: 120, id: "2" }, - ]; + const input: Record = { + "1": { left: 0, right: 100, top: 0, bottom: 30, id: "1" }, + "2": { left: 100, top: 0, bottom: 30, right: 120, id: "2" }, + }; const previousTree: Record = {}; const layoutUpdated = false; const expected = { @@ -36,10 +36,10 @@ describe("Generate Auto Height Layout tree", () => { }); it("Does conflict when part of the boxes overlap horizontally", () => { - const input: NodeSpace[] = [ - { left: 0, right: 100, top: 0, bottom: 30, id: "1" }, - { left: 80, top: 40, bottom: 80, right: 120, id: "2" }, - ]; + const input: Record = { + "1": { left: 0, right: 100, top: 0, bottom: 30, id: "1" }, + "2": { left: 80, top: 40, bottom: 80, right: 120, id: "2" }, + }; const previousTree: Record = {}; const layoutUpdated = false; const expected = { @@ -69,10 +69,10 @@ describe("Generate Auto Height Layout tree", () => { }); it("Uses existing originals if available in prevTree when layout hasn't updated", () => { - const input: NodeSpace[] = [ - { left: 0, right: 100, top: 0, bottom: 30, id: "1" }, - { left: 80, top: 30, bottom: 40, right: 120, id: "2" }, - ]; + const input: Record = { + "1": { left: 0, right: 100, top: 0, bottom: 30, id: "1" }, + "2": { left: 80, top: 30, bottom: 40, right: 120, id: "2" }, + }; const previousTree: Record = { "1": { aboves: [], @@ -121,10 +121,10 @@ describe("Generate Auto Height Layout tree", () => { }); it("Ignores existing originals if available in prevTree when layout has updated", () => { - const input: NodeSpace[] = [ - { left: 0, right: 100, top: 0, bottom: 30, id: "1" }, - { left: 80, top: 30, bottom: 40, right: 120, id: "2" }, - ]; + const input: Record = { + "1": { left: 0, right: 100, top: 0, bottom: 30, id: "1" }, + "2": { left: 80, top: 30, bottom: 40, right: 120, id: "2" }, + }; const previousTree: Record = { "1": { aboves: [], diff --git a/app/client/src/utils/autoHeight/generateTree.ts b/app/client/src/utils/autoHeight/generateTree.ts index dbfb13ee8d..c7bd0bad44 100644 --- a/app/client/src/utils/autoHeight/generateTree.ts +++ b/app/client/src/utils/autoHeight/generateTree.ts @@ -2,17 +2,21 @@ import { areIntersecting } from "utils/boxHelpers"; import { pushToArray } from "utils/helpers"; import { MAX_BOX_SIZE, NodeSpace, TreeNode } from "./constants"; import { getNearestAbove } from "./helpers"; - // This function uses the spaces occupied by sibling boxes and provides us with // a data structure which defines the relative vertical positioning of the boxes // in the form of "aboves" and "belows" for each box, which are array of box ids export function generateTree( - spaces: NodeSpace[], + spaces: Record, layoutUpdated: boolean, previousTree: Record, ): Record { + const spaceMap: Record = spaces; + + const _spaces: string[] = Object.keys(spaceMap); // If widget doesn't exist in this DS, this means that its height changes does not effect any other sibling - spaces.sort((a, b) => { + _spaces.sort((A, B) => { + const a: NodeSpace = spaceMap[A]; + const b: NodeSpace = spaceMap[B]; //if both are of the same level and previous tree exists, check originalTops if (a.top === b.top && previousTree[a.id] && previousTree[b.id]) { return ( @@ -21,7 +25,6 @@ export function generateTree( } return a.top - b.top; }); // Sort based on position, top to bottom, so that we know which is above the other - const _spaces = [...spaces]; const aboveMap: Record = {}; const belowMap: Record = {}; @@ -29,18 +32,18 @@ export function generateTree( const tree: Record = {}; // For each of the sibling boxes - for (let i = 0; i < spaces.length; i++) { + for (let i = 0; i < Object.keys(spaces).length; i++) { // Get the left most box in the array (Remember: we sorted from top to bottom, so the leftmost will be the top most) - const _curr = _spaces.shift(); - if (_curr) { + const _curr: string | undefined = _spaces.shift(); + if (_curr !== undefined) { // Create a reference copy as we need to override the bottom value - const currentSpace = { ..._curr }; + const currentSpace = { ...spaceMap[_curr] }; // Add a randomly large value to the bottom; this will help us know if any box is below this box currentSpace.bottom += MAX_BOX_SIZE; // For each of the remaining sibling widgets for (let j = 0; j < _spaces.length; j++) { // Create a reference copy as we need to override the bottom value - const comparisionSpace = { ..._spaces[j] }; + const comparisionSpace = { ...spaceMap[_spaces[j]] }; // Add a randomly large value to the bottom; this will help us know if any box is below this box // TODO(abhinav): This addition may not be necessary, as we're only looking to see if these boxes // are below the currentSpace @@ -94,10 +97,12 @@ export function generateTree( // For each box, get the nearest above node // Then get the distance between this node and the nearest above // We'll try to maintain this distance when reflowing due to auto height + // We also need to make sure that the nearest above doesn't go below 0, otherwise, + // they can overlap. const nearestAbove = getNearestAbove(tree, boxId, {}); if (nearestAbove.length > 0) { - tree[boxId].distanceToNearestAbove = - tree[boxId].topRow - tree[nearestAbove[0]].bottomRow; + const distance = tree[boxId].topRow - tree[nearestAbove[0]].bottomRow; + tree[boxId].distanceToNearestAbove = Math.max(distance, 0); } } diff --git a/app/client/src/utils/autoHeight/reflow.ts b/app/client/src/utils/autoHeight/reflow.ts index 9118b50af4..1a34ff8b40 100644 --- a/app/client/src/utils/autoHeight/reflow.ts +++ b/app/client/src/utils/autoHeight/reflow.ts @@ -53,9 +53,14 @@ export function computeChangeInPositionBasedOnDelta( } // Sort the effected box ids, this is to make sure we compute from top to bottom. - const sortedEffectedBoxIds = effectedBoxes.sort( - (a, b) => tree[a].topRow - tree[b].topRow, - ); + const sortedEffectedBoxIds = effectedBoxes.sort((a, b) => { + const A = tree[a].topRow; + const B = tree[b].topRow; + if (A === B) { + return tree[a].originalTopRow - tree[b].originalTopRow; + } + return tree[a].topRow - tree[b].topRow; + }); // For each of the boxes which have been effected for (const effectedBoxId of sortedEffectedBoxIds) { diff --git a/app/client/src/widgets/ContainerWidget/component/index.tsx b/app/client/src/widgets/ContainerWidget/component/index.tsx index 333dde6e3d..578e427817 100644 --- a/app/client/src/widgets/ContainerWidget/component/index.tsx +++ b/app/client/src/widgets/ContainerWidget/component/index.tsx @@ -21,6 +21,8 @@ const StyledContainerComponent = styled.div< height: 100%; width: 100%; overflow: hidden; + ${(props) => (!!props.dropDisabled ? `position: relative;` : ``)} + ${(props) => (props.shouldScrollContents ? scrollCSS : ``)} opacity: ${(props) => (props.resizeDisabled ? "0.8" : "1")}; @@ -45,6 +47,7 @@ interface ContainerWrapperProps { backgroundColor?: string; widgetId: string; type: WidgetType; + dropDisabled?: boolean; } function ContainerComponentWrapper( props: PropsWithChildren, @@ -71,6 +74,7 @@ function ContainerComponentWrapper( className={`${ props.shouldScrollContents ? getCanvasClassName() : "" } ${generateClassName(props.widgetId)} container-with-scrollbar`} + dropDisabled={props.dropDisabled} onClickCapture={props.onClickCapture} ref={containerRef} resizeDisabled={props.resizeDisabled} @@ -87,6 +91,7 @@ function ContainerComponent(props: ContainerComponentProps) { if (props.detachFromLayout) { return ( { + const offset = + props.borderWidth && props.borderWidth > 1 + ? Math.ceil( + (2 * parseInt(props.borderWidth, 10) || 0) / + GridDefaults.DEFAULT_GRID_ROW_HEIGHT, + ) + : 0; + + return offset; + }, searchTags: ["div", "parent", "group"], defaults: { backgroundColor: "#FFFFFF", diff --git a/app/client/src/widgets/ContainerWidget/widget/index.tsx b/app/client/src/widgets/ContainerWidget/widget/index.tsx index 754eb58b05..813ce0790f 100644 --- a/app/client/src/widgets/ContainerWidget/widget/index.tsx +++ b/app/client/src/widgets/ContainerWidget/widget/index.tsx @@ -21,6 +21,7 @@ import WidgetsMultiSelectBox from "pages/Editor/WidgetsMultiSelectBox"; import { CanvasDraggingArena } from "pages/common/CanvasArenas/CanvasDraggingArena"; import { getCanvasSnapRows } from "utils/WidgetPropsUtils"; import { Stylesheet } from "entities/AppTheming"; +import { ReduxActionTypes } from "@appsmith/constants/ReduxActionConstants"; class ContainerWidget extends BaseWidget< ContainerWidgetProps, @@ -110,6 +111,7 @@ class ContainerWidget extends BaseWidget< isBindProperty: true, isTriggerProperty: false, validation: { type: ValidationTypes.NUMBER }, + postUpdateAction: ReduxActionTypes.CHECK_CONTAINERS_FOR_AUTO_HEIGHT, }, { propertyName: "borderRadius", diff --git a/app/client/src/widgets/FormWidget/index.ts b/app/client/src/widgets/FormWidget/index.ts index 25912f432e..b3aa648b68 100644 --- a/app/client/src/widgets/FormWidget/index.ts +++ b/app/client/src/widgets/FormWidget/index.ts @@ -1,5 +1,7 @@ import { ButtonVariantTypes, RecaptchaTypes } from "components/constants"; import { Colors } from "constants/Colors"; +import { GridDefaults } from "constants/WidgetConstants"; +import { WidgetProps } from "widgets/BaseWidget"; import IconSVG from "./icon.svg"; import Widget from "./widget"; @@ -15,6 +17,17 @@ export const CONFIG = { active: true, }, }, + canvasHeightOffset: (props: WidgetProps): number => { + const offset = + props.borderWidth && props.borderWidth > 1 + ? Math.round( + (2 * parseInt(props.borderWidth, 10) || 0) / + GridDefaults.DEFAULT_GRID_ROW_HEIGHT, + ) + : 0; + + return offset; + }, searchTags: ["group"], defaults: { rows: 40, diff --git a/app/client/src/widgets/ListWidget/index.ts b/app/client/src/widgets/ListWidget/index.ts index 2713440782..a3da914e05 100644 --- a/app/client/src/widgets/ListWidget/index.ts +++ b/app/client/src/widgets/ListWidget/index.ts @@ -17,6 +17,7 @@ export const CONFIG = { iconSVG: IconSVG, needsMeta: true, isCanvas: true, + needsHeightForContent: true, defaults: { backgroundColor: "transparent", itemBackgroundColor: "#FFFFFF", diff --git a/app/client/src/widgets/ModalWidget/widget/index.tsx b/app/client/src/widgets/ModalWidget/widget/index.tsx index 3987fe2a81..c6332146b3 100644 --- a/app/client/src/widgets/ModalWidget/widget/index.tsx +++ b/app/client/src/widgets/ModalWidget/widget/index.tsx @@ -274,8 +274,6 @@ export class ModalWidget extends BaseWidget { getCanvasView() { let children = this.getChildren(); children = this.makeModalSelectable(children); - // children = this.showWidgetName(children, true); - return this.makeModalComponent(children, true); } diff --git a/app/client/src/widgets/StatboxWidget/index.ts b/app/client/src/widgets/StatboxWidget/index.ts index cbc48fe0e4..4598f2a088 100644 --- a/app/client/src/widgets/StatboxWidget/index.ts +++ b/app/client/src/widgets/StatboxWidget/index.ts @@ -1,5 +1,7 @@ import { ButtonVariantTypes } from "components/constants"; import { Colors } from "constants/Colors"; +import { GridDefaults } from "constants/WidgetConstants"; +import { WidgetProps } from "widgets/BaseWidget"; import IconSVG from "./icon.svg"; import Widget from "./widget"; @@ -16,6 +18,17 @@ export const CONFIG = { iconSVG: IconSVG, needsMeta: true, isCanvas: true, + canvasHeightOffset: (props: WidgetProps): number => { + const offset = + props.borderWidth && props.borderWidth > 1 + ? Math.ceil( + (2 * parseInt(props.borderWidth, 10) || 0) / + GridDefaults.DEFAULT_GRID_ROW_HEIGHT, + ) + : 0; + + return offset; + }, defaults: { rows: 14, columns: 22, diff --git a/app/client/src/widgets/StatboxWidget/widget/index.tsx b/app/client/src/widgets/StatboxWidget/widget/index.tsx index 7f50804a71..d320a4e653 100644 --- a/app/client/src/widgets/StatboxWidget/widget/index.tsx +++ b/app/client/src/widgets/StatboxWidget/widget/index.tsx @@ -3,6 +3,7 @@ import ContainerWidget from "widgets/ContainerWidget"; import { ValidationTypes } from "constants/WidgetValidation"; import { Stylesheet } from "entities/AppTheming"; +import { ReduxActionTypes } from "@appsmith/constants/ReduxActionConstants"; class StatboxWidget extends ContainerWidget { static getPropertyPaneContentConfig() { @@ -85,6 +86,7 @@ class StatboxWidget extends ContainerWidget { isBindProperty: true, isTriggerProperty: false, validation: { type: ValidationTypes.NUMBER }, + postUpdateAction: ReduxActionTypes.CHECK_CONTAINERS_FOR_AUTO_HEIGHT, }, { propertyName: "borderRadius", diff --git a/app/client/src/widgets/TableWidget/index.ts b/app/client/src/widgets/TableWidget/index.ts index c656ca46ce..0c0c9881df 100644 --- a/app/client/src/widgets/TableWidget/index.ts +++ b/app/client/src/widgets/TableWidget/index.ts @@ -16,6 +16,7 @@ export const CONFIG = { needsMeta: true, searchTags: ["datagrid"], hideCard: true, + needsHeightForContent: true, defaults: { rows: 28, columns: 34, diff --git a/app/client/src/widgets/TableWidgetV2/index.ts b/app/client/src/widgets/TableWidgetV2/index.ts index c8830a73cd..b4724b21b1 100644 --- a/app/client/src/widgets/TableWidgetV2/index.ts +++ b/app/client/src/widgets/TableWidgetV2/index.ts @@ -16,6 +16,7 @@ export const CONFIG = { name: "Table", iconSVG: IconSVG, needsMeta: true, + needsHeightForContent: true, defaults: { rows: 28, columns: 34, diff --git a/app/client/src/widgets/TabsWidget/index.ts b/app/client/src/widgets/TabsWidget/index.ts index 4927827156..57153b7135 100644 --- a/app/client/src/widgets/TabsWidget/index.ts +++ b/app/client/src/widgets/TabsWidget/index.ts @@ -1,5 +1,5 @@ import { Colors } from "constants/Colors"; -import { WidgetHeightLimits } from "constants/WidgetConstants"; +import { GridDefaults, WidgetHeightLimits } from "constants/WidgetConstants"; import { WidgetProps } from "widgets/BaseWidget"; import { BlueprintOperationTypes } from "widgets/constants"; import IconSVG from "./icon.svg"; @@ -16,8 +16,20 @@ export const CONFIG = { // evaluations. One way to handle these types of properties is to // define them in a Map which the platform understands to have // them stored only in the WidgetFactory. - canvasHeightOffset: (props: WidgetProps): number => - props.shouldShowTabs === true ? 4 : 0, + canvasHeightOffset: (props: WidgetProps): number => { + let offset = + props.borderWidth && props.borderWidth > 1 + ? Math.ceil( + (2 * parseInt(props.borderWidth, 10) || 0) / + GridDefaults.DEFAULT_GRID_ROW_HEIGHT, + ) + : 0; + + if (props.shouldShowTabs === true) { + offset += 4; + } + return offset; + }, features: { dynamicHeight: { sectionIndex: 1, diff --git a/app/client/src/widgets/TabsWidget/widget/index.tsx b/app/client/src/widgets/TabsWidget/widget/index.tsx index adaaec3b86..7376ce94e1 100644 --- a/app/client/src/widgets/TabsWidget/widget/index.tsx +++ b/app/client/src/widgets/TabsWidget/widget/index.tsx @@ -239,6 +239,7 @@ class TabsWidget extends BaseWidget< isBindProperty: true, isTriggerProperty: false, validation: { type: ValidationTypes.NUMBER }, + postUpdateAction: ReduxActionTypes.CHECK_CONTAINERS_FOR_AUTO_HEIGHT, }, { propertyName: "borderRadius", diff --git a/app/client/src/widgets/constants.ts b/app/client/src/widgets/constants.ts index 5511d571f0..da569f697c 100644 --- a/app/client/src/widgets/constants.ts +++ b/app/client/src/widgets/constants.ts @@ -24,6 +24,7 @@ export interface WidgetConfiguration { features?: WidgetFeatures; canvasHeightOffset?: (props: WidgetProps) => number; searchTags?: string[]; + needsHeightForContent?: boolean; properties: { config?: PropertyPaneConfig[]; contentConfig?: PropertyPaneConfig[]; @@ -52,7 +53,12 @@ export interface DSLWidget extends WidgetProps { children?: DSLWidget[]; } -const staticProps = omit(WIDGET_STATIC_PROPS, "children"); +const staticProps = omit( + WIDGET_STATIC_PROPS, + "children", + "topRowBeforeCollapse", + "bottomRowBeforeCollapse", +); export type CanvasWidgetStructure = Pick< WidgetProps, keyof typeof staticProps diff --git a/app/client/src/widgets/withWidgetProps.tsx b/app/client/src/widgets/withWidgetProps.tsx index 4bdc3ce98a..681be2178c 100644 --- a/app/client/src/widgets/withWidgetProps.tsx +++ b/app/client/src/widgets/withWidgetProps.tsx @@ -27,6 +27,7 @@ import { } from "utils/widgetRenderUtils"; import { ReduxActionTypes } from "@appsmith/constants/ReduxActionConstants"; import { checkContainersForAutoHeightAction } from "actions/autoHeightActions"; +import { isAutoHeightEnabledForWidget } from "./WidgetUtils"; import { CANVAS_DEFAULT_MIN_HEIGHT_PX } from "constants/AppConstants"; import { getGoogleMapsApiKey } from "ce/selectors/tenantSelectors"; @@ -180,7 +181,32 @@ function withWidgetProps(WrappedWidget: typeof BaseWidget) { shouldResetCollapsedContainerHeightInViewOrPreviewMode || shouldResetCollapsedContainerHeightInCanvasMode ) { - dispatch(checkContainersForAutoHeightAction()); + // We also need to check if a non-auto height widget has collapsed earlier + // We can figure this out if the widget height is zero and the beforeCollapse + // topRow and bottomRow are available. + + // If the above is true, we call an auto height update call + // so that the widget can be reset correctly. + if ( + widgetProps.topRow === widgetProps.bottomRow && + widgetProps.topRowBeforeCollapse !== undefined && + widgetProps.bottomRowBeforeCollapse !== undefined && + !isAutoHeightEnabledForWidget(widgetProps) + ) { + const heightBeforeCollapse = + (widgetProps.bottomRowBeforeCollapse - + widgetProps.topRowBeforeCollapse) * + GridDefaults.DEFAULT_GRID_ROW_HEIGHT; + dispatch({ + type: ReduxActionTypes.UPDATE_WIDGET_AUTO_HEIGHT, + payload: { + widgetId: props.widgetId, + height: heightBeforeCollapse, + }, + }); + } else { + dispatch(checkContainersForAutoHeightAction()); + } } return ;