From 55cf16ae9d6f39f5853f16185a167f84664eb072 Mon Sep 17 00:00:00 2001 From: Abhinav Jha Date: Tue, 14 Feb 2023 19:06:19 +0530 Subject: [PATCH] feat: Non auto height invisible widgets (#20118) ## Description This PR adds another feature update we had planned for Auto Height - [ ] For new applications, in View and Preview mode, any widget which is invisible will let go of its space and collapse if it's either on the main Canvas or a container-like widget which has Auto-height enabled. - [ ] Widgets within a container-like Widget, say Tabs, that doesn't have Auto-height enabled, will now let go of their space if they're invisible. - [ ] The experience in Edit mode has not changed. TL;DR: In new applications, in the Preview and Published _AKA_ View modes, if a widget is invisible and within an Auto-height-enabled container like a Tab, a Modal, a Form, or the main Canvas, it will fully collapse, allowing widgets below it to move up and take its space. This changes the behavior today prior to the release of this PR for Auto-height-enabled widgets. Fixes #19983 Fixes #18681 --- .../autoHeightInvisibleWidgetsDSL.json | 282 ++++++++++++++++ .../fixtures/autoHeightOverlapDSL.json | 189 +++++++++++ .../DynamicHeight_Invisible_Widgets_spec.ts | 73 +++++ .../DynamicHeight_Overlap_Test_spec.ts | 32 ++ app/client/src/actions/controlActions.tsx | 3 +- .../src/ce/constants/ReduxActionConstants.tsx | 1 + .../uiReducers/applicationsReducer.tsx | 4 +- app/client/src/constants/WidgetConstants.tsx | 4 + .../autoHeightLayoutTreeReducer.ts | 4 +- .../canvasWidgetsReducer.test.ts | 60 ++-- .../entityReducers/canvasWidgetsReducer.ts | 12 +- app/client/src/sagas/PageSagas.tsx | 4 +- app/client/src/sagas/WidgetOperationSagas.tsx | 4 +- .../src/sagas/autoHeightSagas/batcher.ts | 20 ++ .../src/sagas/autoHeightSagas/containers.ts | 22 +- .../src/sagas/autoHeightSagas/helpers.test.ts | 78 +++++ .../src/sagas/autoHeightSagas/helpers.ts | 120 +++++++ app/client/src/sagas/autoHeightSagas/index.ts | 9 +- .../src/sagas/autoHeightSagas/layoutTree.ts | 4 +- .../src/sagas/autoHeightSagas/widgets.ts | 304 +++++++++++------- app/client/src/selectors/editorSelectors.tsx | 12 +- .../src/utils/WidgetRegisterHelpers.tsx | 1 + app/client/src/utils/WidgetSizeUtils.ts | 7 +- .../src/utils/autoHeight/generateTree.test.ts | 32 +- .../src/utils/autoHeight/generateTree.ts | 27 +- app/client/src/utils/autoHeight/reflow.ts | 11 +- .../ContainerWidget/component/index.tsx | 7 + .../src/widgets/ContainerWidget/index.ts | 14 +- .../widgets/ContainerWidget/widget/index.tsx | 2 + app/client/src/widgets/FormWidget/index.ts | 13 + app/client/src/widgets/ListWidget/index.ts | 1 + .../src/widgets/ModalWidget/widget/index.tsx | 2 - app/client/src/widgets/StatboxWidget/index.ts | 13 + .../widgets/StatboxWidget/widget/index.tsx | 2 + app/client/src/widgets/TableWidget/index.ts | 1 + app/client/src/widgets/TableWidgetV2/index.ts | 1 + app/client/src/widgets/TabsWidget/index.ts | 18 +- .../src/widgets/TabsWidget/widget/index.tsx | 1 + app/client/src/widgets/constants.ts | 8 +- app/client/src/widgets/withWidgetProps.tsx | 28 +- 40 files changed, 1220 insertions(+), 210 deletions(-) create mode 100644 app/client/cypress/fixtures/autoHeightInvisibleWidgetsDSL.json create mode 100644 app/client/cypress/fixtures/autoHeightOverlapDSL.json create mode 100644 app/client/cypress/integration/Regression_TestSuite/ClientSideTests/DynamicHeight/DynamicHeight_Invisible_Widgets_spec.ts create mode 100644 app/client/cypress/integration/Regression_TestSuite/ClientSideTests/DynamicHeight/DynamicHeight_Overlap_Test_spec.ts create mode 100644 app/client/src/sagas/autoHeightSagas/helpers.test.ts 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 ;