diff --git a/app/client/cypress/fixtures/newFormDsl.json b/app/client/cypress/fixtures/newFormDsl.json index 7af0b0cf4997..f84440bfc517 100644 --- a/app/client/cypress/fixtures/newFormDsl.json +++ b/app/client/cypress/fixtures/newFormDsl.json @@ -278,8 +278,8 @@ "parentRowSpace": 38, "leftColumn": 10, "rightColumn": 14, - "topRow": 11, - "bottomRow": 12, + "topRow": 17, + "bottomRow": 18, "parentId": "e3tq9qwta6", "widgetId": "ca22py6vlv" }, @@ -346,18 +346,18 @@ "parentColumnSpace": 6.6856445312499995, "leftColumn": 2, "options": [ - { - "label": "Blue", - "value": "BLUE" - }, - { - "label": "Green", - "value": "GREEN" - }, - { - "label": "Red", - "value": "RED" - } + { + "label": "Blue", + "value": "BLUE" + }, + { + "label": "Green", + "value": "GREEN" + }, + { + "label": "Red", + "value": "RED" + } ], "isDisabled": false, "key": "vrhzyvir7s", @@ -382,4 +382,4 @@ } ] } -} +} \ No newline at end of file diff --git a/app/client/cypress/integration/Smoke_TestSuite/Application/CommunityIssues_Spec.ts b/app/client/cypress/integration/Smoke_TestSuite/Application/CommunityIssues_Spec.ts index 999b5f32b0a6..ebb115c2c882 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/Application/CommunityIssues_Spec.ts +++ b/app/client/cypress/integration/Smoke_TestSuite/Application/CommunityIssues_Spec.ts @@ -311,10 +311,10 @@ describe("AForce - Community Issues page validations", function() { table.SelectTableRow(0); agHelper.AssertElementVisible(locator._widgetInDeployed("tabswidget")); agHelper - .GetNClick(locator._inputWidgetv1InDeployed) + .GetNClick(locator._inputWidgetv1InDeployed, 0, true, 0) .type("-updating title"); agHelper - .GetNClick(locator._textAreainputWidgetv1InDeployed) + .GetNClick(locator._textAreainputWidgetv1InDeployed, 0, true, 0) .type("-updating desc"); agHelper .GetNClick(locator._inputWidgetv1InDeployed, 1) @@ -377,7 +377,7 @@ describe("AForce - Community Issues page validations", function() { agHelper.Sleep(); cy.get(table._trashIcon) .closest("div") - .click(); + .click({ force: true }); agHelper.Sleep(3000); //allowing time to delete! agHelper.AssertElementAbsence(locator._widgetInDeployed("tabswidget")); table.WaitForTableEmpty(); diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/OtherUIFeatures/Replay_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/OtherUIFeatures/Replay_spec.js index 70ecc766ceb5..af6b9c7dc154 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/OtherUIFeatures/Replay_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/OtherUIFeatures/Replay_spec.js @@ -166,6 +166,7 @@ describe("Undo/Redo functionality", function() { cy.get(commonlocators.toastmsg) .eq(1) .contains("UNDO"); + cy.deleteWidget(widgetsPage.textWidget); }); it("checks undo/redo for color picker", function() { diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Widgets/Button/ButtonGroup_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Widgets/Button/ButtonGroup_spec.js index 99a02cf2f3f5..2927158401a6 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Widgets/Button/ButtonGroup_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Widgets/Button/ButtonGroup_spec.js @@ -1,4 +1,5 @@ const explorer = require("../../../../../locators/explorerlocators.json"); +const { modifierKey } = require("../../../../../support/Constants"); const firstButton = ".t--buttongroup-widget > div > button > div"; const menuButton = @@ -36,7 +37,7 @@ describe("Button Group Widget Functionality", function() { cy.get(firstButton).contains("Add"); // Undo - cy.get("body").type("{ctrl+z}"); + cy.get("body").type(`{${modifierKey}+z}`); // Check if the button is back cy.get(".t--buttongroup-widget") diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Widgets/List/List1_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Widgets/List/List1_spec.js index 03d7a4e62366..6282c89b1bd7 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Widgets/List/List1_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Widgets/List/List1_spec.js @@ -108,6 +108,7 @@ describe("Binding the list widget with text widget", function() { .click({ force: true }) .type("#$%1234", { delay: 300 }) .type("{enter}"); + cy.wait(500); cy.get(".t--widget-name").contains("___1234"); cy.verifyUpdatedWidgetName("12345"); cy.get(".t--delete-widget").click({ force: true }); diff --git a/app/client/cypress/support/Constants.js b/app/client/cypress/support/Constants.js new file mode 100644 index 000000000000..dbbaa80b9e28 --- /dev/null +++ b/app/client/cypress/support/Constants.js @@ -0,0 +1 @@ +export const modifierKey = Cypress.platform === "darwin" ? "meta" : "ctrl"; diff --git a/app/client/cypress/support/widgetCommands.js b/app/client/cypress/support/widgetCommands.js index ecea7e3c6d27..8456cb31f225 100644 --- a/app/client/cypress/support/widgetCommands.js +++ b/app/client/cypress/support/widgetCommands.js @@ -245,6 +245,7 @@ Cypress.Commands.add("verifyUpdatedWidgetName", (text) => { .click({ force: true }) .type(text, { delay: 300 }) .type("{enter}"); + cy.wait(500); cy.get(".t--widget-name").contains(text); }); diff --git a/app/client/src/actions/pageActions.tsx b/app/client/src/actions/pageActions.tsx index cb35c924916e..624f56afef1c 100644 --- a/app/client/src/actions/pageActions.tsx +++ b/app/client/src/actions/pageActions.tsx @@ -37,6 +37,12 @@ export interface CreatePageActionPayload { blockNavigation?: boolean; } +export type updateLayoutOptions = { + isRetry?: boolean; + shouldReplay?: boolean; + updatedWidgetIds?: string[]; +}; + export const fetchPage = ( pageId: string, isFirstLoad = false, @@ -130,12 +136,12 @@ export const deletePageSuccess = () => { export const updateAndSaveLayout = ( widgets: CanvasWidgetsReduxState, - isRetry?: boolean, - shouldReplay?: boolean, + options: updateLayoutOptions = {}, ) => { + const { isRetry, shouldReplay, updatedWidgetIds } = options; return { type: ReduxActionTypes.UPDATE_LAYOUT, - payload: { widgets, isRetry, shouldReplay }, + payload: { widgets, isRetry, shouldReplay, updatedWidgetIds }, }; }; diff --git a/app/client/src/ce/constants/ReduxActionConstants.tsx b/app/client/src/ce/constants/ReduxActionConstants.tsx index afbcb82c0417..3a239fe4dbcc 100644 --- a/app/client/src/ce/constants/ReduxActionConstants.tsx +++ b/app/client/src/ce/constants/ReduxActionConstants.tsx @@ -902,6 +902,7 @@ export interface UpdateCanvasPayload { currentPageName: string; currentApplicationId: string; pageActions: PageAction[][]; + updatedWidgetIds?: string[]; } export interface ShowPropertyPanePayload { diff --git a/app/client/src/components/designSystems/appsmith/PositionedContainer.tsx b/app/client/src/components/designSystems/appsmith/PositionedContainer.tsx index fd67df21aa51..673d932cade2 100644 --- a/app/client/src/components/designSystems/appsmith/PositionedContainer.tsx +++ b/app/client/src/components/designSystems/appsmith/PositionedContainer.tsx @@ -8,7 +8,7 @@ import { usePositionedContainerZIndex } from "utils/hooks/usePositionedContainer import { useSelector } from "react-redux"; import { snipingModeSelector } from "selectors/editorSelectors"; import WidgetFactory from "utils/WidgetFactory"; -import { isEqual, memoize } from "lodash"; +import { memoize } from "lodash"; import { getReflowSelector } from "selectors/widgetReflowSelectors"; import { AppState } from "reducers"; import { POSITIONED_WIDGET } from "constants/componentClassNameConstants"; @@ -59,7 +59,7 @@ export function PositionedContainer(props: PositionedContainerProps) { const reflowSelector = getReflowSelector(props.widgetId); - const reflowedPosition = useSelector(reflowSelector, isEqual); + const reflowedPosition = useSelector(reflowSelector); const dragDetails = useSelector( (state: AppState) => state.ui.widgetDragResize.dragDetails, ); diff --git a/app/client/src/components/editorComponents/ResizableComponent.tsx b/app/client/src/components/editorComponents/ResizableComponent.tsx index bf655c910e57..945592515363 100644 --- a/app/client/src/components/editorComponents/ResizableComponent.tsx +++ b/app/client/src/components/editorComponents/ResizableComponent.tsx @@ -37,12 +37,11 @@ import { snipingModeSelector, } from "selectors/editorSelectors"; import { useWidgetSelection } from "utils/hooks/useWidgetSelection"; -import { getCanvasWidgets } from "selectors/entitiesSelector"; import { focusWidget } from "actions/widgetActions"; -import { getParentToOpenIfAny } from "utils/hooks/useClickToSelectWidget"; import { GridDefaults } from "constants/WidgetConstants"; import { DropTargetContext } from "./DropTargetComponent"; import { XYCord } from "pages/common/CanvasArenas/hooks/useCanvasDragging"; +import { getParentToOpenSelector } from "selectors/widgetSelectors"; export type ResizableComponentProps = WidgetProps & { paddingOffset: number; @@ -53,7 +52,6 @@ export const ResizableComponent = memo(function ResizableComponent( ) { // Fetch information from the context const { updateWidget } = useContext(EditorContext); - const canvasWidgets = useSelector(getCanvasWidgets); const isSnipingMode = useSelector(snipingModeSelector); const isPreviewMode = useSelector(previewModeSelector); @@ -78,9 +76,8 @@ export const ResizableComponent = memo(function ResizableComponent( const isResizing = useSelector( (state: AppState) => state.ui.widgetDragResize.isResizing, ); - const parentWidgetToSelect = getParentToOpenIfAny( - props.widgetId, - canvasWidgets, + const parentWidgetToSelect = useSelector( + getParentToOpenSelector(props.widgetId), ); const isWidgetFocused = diff --git a/app/client/src/constants/AppConstants.ts b/app/client/src/constants/AppConstants.ts index 1dd4c05d6fd1..43532ad02a86 100644 --- a/app/client/src/constants/AppConstants.ts +++ b/app/client/src/constants/AppConstants.ts @@ -1,9 +1,13 @@ import localStorage from "utils/localStorage"; +import { GridDefaults } from "./WidgetConstants"; export const CANVAS_DEFAULT_HEIGHT_PX = 1292; export const CANVAS_DEFAULT_MIN_HEIGHT_PX = 380; export const CANVAS_DEFAULT_GRID_HEIGHT_PX = 1; export const CANVAS_DEFAULT_GRID_WIDTH_PX = 1; +export const CANVAS_DEFAULT_MIN_ROWS = Math.ceil( + CANVAS_DEFAULT_MIN_HEIGHT_PX / GridDefaults.DEFAULT_GRID_ROW_HEIGHT, +); export const CANVAS_BACKGROUND_COLOR = "#FFFFFF"; export const DEFAULT_ENTITY_EXPLORER_WIDTH = 256; export const DEFAULT_PROPERTY_PANE_WIDTH = 256; diff --git a/app/client/src/constants/WidgetConstants.tsx b/app/client/src/constants/WidgetConstants.tsx index b541a8aaf973..6adfca906cd7 100644 --- a/app/client/src/constants/WidgetConstants.tsx +++ b/app/client/src/constants/WidgetConstants.tsx @@ -138,6 +138,15 @@ export const WIDGET_STATIC_PROPS = { noContainerOffset: false, }; +export const WIDGET_DSL_STRUCTURE_PROPS = { + children: true, + type: true, + widgetId: true, + parentId: true, + topRow: true, + bottomRow: true, +}; + export type TextSize = keyof typeof TextSizes; export const DEFAULT_FONT_SIZE = THEMEING_TEXT_SIZES.base; diff --git a/app/client/src/pages/AppViewer/AppPage.tsx b/app/client/src/pages/AppViewer/AppPage.tsx index 4b3edf827dab..9a49c476e17b 100644 --- a/app/client/src/pages/AppViewer/AppPage.tsx +++ b/app/client/src/pages/AppViewer/AppPage.tsx @@ -3,7 +3,7 @@ import styled from "styled-components"; import WidgetFactory from "utils/WidgetFactory"; import AnalyticsUtil from "utils/AnalyticsUtil"; import { useDynamicAppLayout } from "utils/hooks/useDynamicAppLayout"; -import { DSLWidget } from "widgets/constants"; +import { CanvasWidgetStructure } from "widgets/constants"; import { RenderModes } from "constants/WidgetConstants"; const PageView = styled.div<{ width: number }>` @@ -14,10 +14,11 @@ const PageView = styled.div<{ width: number }>` `; type AppPageProps = { - dsl: DSLWidget; - pageName?: string; - pageId?: string; appName?: string; + canvasWidth: number; + pageId?: string; + pageName?: string; + widgetsStructure: CanvasWidgetStructure; }; export function AppPage(props: AppPageProps) { @@ -33,9 +34,9 @@ export function AppPage(props: AppPageProps) { }, [props.pageId, props.pageName]); return ( - - {props.dsl.widgetId && - WidgetFactory.createWidget(props.dsl, RenderModes.PAGE)} + + {props.widgetsStructure.widgetId && + WidgetFactory.createWidget(props.widgetsStructure, RenderModes.PAGE)} ); } diff --git a/app/client/src/pages/AppViewer/AppViewerPageContainer.tsx b/app/client/src/pages/AppViewer/AppViewerPageContainer.tsx index 8c7618e0ca15..5c3e7bd3fc0a 100644 --- a/app/client/src/pages/AppViewer/AppViewerPageContainer.tsx +++ b/app/client/src/pages/AppViewer/AppViewerPageContainer.tsx @@ -8,10 +8,7 @@ import { theme } from "constants/DefaultTheme"; import { Icon, NonIdealState, Spinner } from "@blueprintjs/core"; import Centered from "components/designSystems/appsmith/CenteredWrapper"; import AppPage from "./AppPage"; -import { - getCanvasWidgetDsl, - getCurrentPageName, -} from "selectors/editorSelectors"; +import { getCanvasWidth, getCurrentPageName } from "selectors/editorSelectors"; import RequestConfirmationModal from "pages/Editor/RequestConfirmationModal"; import { getCurrentApplication } from "selectors/applicationSelectors"; import { @@ -19,6 +16,8 @@ import { PERMISSION_TYPE, } from "../Applications/permissionHelpers"; import { builderURL } from "RouteBuilder"; +import { getCanvasWidgetsStructure } from "selectors/entitiesSelector"; +import { isEqual } from "lodash"; const Section = styled.section` height: 100%; @@ -33,7 +32,8 @@ type AppViewerPageContainerProps = RouteComponentProps; function AppViewerPageContainer(props: AppViewerPageContainerProps) { const currentPageName = useSelector(getCurrentPageName); - const widgets = useSelector(getCanvasWidgetDsl); + const widgetsStructure = useSelector(getCanvasWidgetsStructure, isEqual); + const canvasWidth = useSelector(getCanvasWidth); const isFetchingPage = useSelector(getIsFetchingPage); const currentApplication = useSelector(getCurrentApplication); const { match } = props; @@ -86,15 +86,17 @@ function AppViewerPageContainer(props: AppViewerPageContainerProps) { if (isFetchingPage) return pageLoading; - if (!(widgets.children && widgets.children.length > 0)) return pageNotFound; + if (!(widgetsStructure.children && widgetsStructure.children.length > 0)) + return pageNotFound; return (
diff --git a/app/client/src/pages/Editor/Canvas.tsx b/app/client/src/pages/Editor/Canvas.tsx index 16f1926cfca2..25c6c874076c 100644 --- a/app/client/src/pages/Editor/Canvas.tsx +++ b/app/client/src/pages/Editor/Canvas.tsx @@ -2,7 +2,7 @@ import log from "loglevel"; import * as Sentry from "@sentry/react"; import styled from "styled-components"; import store, { useSelector } from "store"; -import { DSLWidget } from "widgets/constants"; +import { CanvasWidgetStructure } from "widgets/constants"; import WidgetFactory from "utils/WidgetFactory"; import React, { memo, useCallback, useEffect } from "react"; @@ -22,8 +22,9 @@ import { getPageLevelSocketRoomId } from "sagas/WebsocketSagas/utils"; import { previewModeSelector } from "selectors/editorSelectors"; interface CanvasProps { - dsl: DSLWidget; + widgetsStructure: CanvasWidgetStructure; pageId: string; + canvasWidth: number; } type PointerEventDataType = { @@ -72,7 +73,7 @@ const useShareMousePointerEvent = () => { // TODO(abhinav): get the render mode from context const Canvas = memo((props: CanvasProps) => { - const { pageId } = props; + const { canvasWidth, pageId } = props; const isPreviewMode = useSelector(previewModeSelector); const selectedTheme = useSelector(getSelectedAppTheme); @@ -118,11 +119,14 @@ const Canvas = memo((props: CanvasProps) => { !!data && delayedShareMousePointer(data); }} style={{ - width: props.dsl.rightColumn, + width: canvasWidth, }} > - {props.dsl.widgetId && - WidgetFactory.createWidget(props.dsl, RenderModes.CANVAS)} + {props.widgetsStructure.widgetId && + WidgetFactory.createWidget( + props.widgetsStructure, + RenderModes.CANVAS, + )} {isMultiplayerEnabledForUser && ( )} diff --git a/app/client/src/pages/Editor/GlobalHotKeys/GlobalHotKeys.test.tsx b/app/client/src/pages/Editor/GlobalHotKeys/GlobalHotKeys.test.tsx index 3c399a951f5f..3ff0ee54dd06 100644 --- a/app/client/src/pages/Editor/GlobalHotKeys/GlobalHotKeys.test.tsx +++ b/app/client/src/pages/Editor/GlobalHotKeys/GlobalHotKeys.test.tsx @@ -9,14 +9,19 @@ import { act, render, fireEvent, waitFor } from "test/testUtils"; import GlobalHotKeys from "./GlobalHotKeys"; import MainContainer from "../MainContainer"; import { MemoryRouter } from "react-router-dom"; +import * as widgetRenderUtils from "utils/widgetRenderUtils"; import * as utilities from "selectors/editorSelectors"; +import * as dataTreeSelectors from "selectors/dataTreeSelectors"; import store from "store"; import { sagasToRunForTests } from "test/sagas"; import { all } from "@redux-saga/core/effects"; import { dispatchTestKeyboardEventWithCode, MockApplication, + mockCreateCanvasWidget, mockGetCanvasWidgetDsl, + mockGetChildWidgets, + mockGetWidgetEvalValues, MockPageDSL, useMockDsl, } from "test/testCommon"; @@ -40,6 +45,11 @@ jest.mock("constants/routes", () => { describe("Canvas Hot Keys", () => { const mockGetIsFetchingPage = jest.spyOn(utilities, "getIsFetchingPage"); const spyGetCanvasWidgetDsl = jest.spyOn(utilities, "getCanvasWidgetDsl"); + const spyGetChildWidgets = jest.spyOn(utilities, "getChildWidgets"); + const spyCreateCanvasWidget = jest.spyOn( + widgetRenderUtils, + "createCanvasWidget", + ); function UpdatedMainContainer({ dsl }: any) { useMockDsl(dsl); @@ -68,6 +78,16 @@ describe("Canvas Hot Keys", () => { }); describe("Select all hotkey", () => { + jest + .spyOn(widgetRenderUtils, "createCanvasWidget") + .mockImplementation(mockCreateCanvasWidget); + jest + .spyOn(dataTreeSelectors, "getWidgetEvalValues") + .mockImplementation(mockGetWidgetEvalValues); + jest + .spyOn(utilities, "computeMainContainerWidget") + .mockImplementation((widget) => widget as any); + it("Cmd + A - select all widgets on canvas", async () => { const children: any = buildChildren([ { type: "TABS_WIDGET", parentId: MAIN_CONTAINER_WIDGET_ID }, @@ -240,12 +260,14 @@ describe("Canvas Hot Keys", () => { expect(selectedWidgets.length).toBe(children.length); }); it("Cmd + A - select all widgets inside a form", async () => { + spyGetChildWidgets.mockImplementation(mockGetChildWidgets); const children: any = buildChildren([ { type: "FORM_WIDGET", parentId: MAIN_CONTAINER_WIDGET_ID }, ]); const dsl: any = widgetCanvasFactory.build({ children, }); + spyGetCanvasWidgetDsl.mockImplementation(mockGetCanvasWidgetDsl); mockGetIsFetchingPage.mockImplementation(() => false); @@ -337,6 +359,8 @@ describe("Canvas Hot Keys", () => { }); spyGetCanvasWidgetDsl.mockImplementation(mockGetCanvasWidgetDsl); mockGetIsFetchingPage.mockImplementation(() => false); + spyGetChildWidgets.mockImplementation(mockGetChildWidgets); + spyCreateCanvasWidget.mockImplementation(mockCreateCanvasWidget); const component = render( { describe("Drag and Drop widgets into Main container", () => { const mockGetIsFetchingPage = jest.spyOn(utilities, "getIsFetchingPage"); const spyGetCanvasWidgetDsl = jest.spyOn(utilities, "getCanvasWidgetDsl"); + + jest + .spyOn(widgetRenderUtils, "createCanvasWidget") + .mockImplementation(mockCreateCanvasWidget); + jest + .spyOn(dataTreeSelectors, "getWidgetEvalValues") + .mockImplementation(mockGetWidgetEvalValues); + jest + .spyOn(utilities, "computeMainContainerWidget") + .mockImplementation((widget) => widget as any); jest .spyOn(useDynamicAppLayoutHook, "useDynamicAppLayout") .mockImplementation(() => true); @@ -451,6 +465,7 @@ describe("Drag and Drop widgets into Main container", () => { children, }); dsl.bottomRow = 250; + spyGetCanvasWidgetDsl.mockImplementation(mockGetCanvasWidgetDsl); mockGetIsFetchingPage.mockImplementation(() => false); diff --git a/app/client/src/pages/Editor/WidgetsEditor/CanvasContainer.tsx b/app/client/src/pages/Editor/WidgetsEditor/CanvasContainer.tsx index 5edd05622c34..aaeec2bf88a6 100644 --- a/app/client/src/pages/Editor/WidgetsEditor/CanvasContainer.tsx +++ b/app/client/src/pages/Editor/WidgetsEditor/CanvasContainer.tsx @@ -3,9 +3,9 @@ import { useSelector } from "react-redux"; import { getCurrentPageId, getIsFetchingPage, - getCanvasWidgetDsl, getViewModePageList, previewModeSelector, + getCanvasWidth, } from "selectors/editorSelectors"; import styled from "styled-components"; import { getCanvasClassName } from "utils/generators"; @@ -25,6 +25,8 @@ import useGoogleFont from "utils/hooks/useGoogleFont"; import { IconSize } from "components/ads/Icon"; import { useDynamicAppLayout } from "utils/hooks/useDynamicAppLayout"; import { getCurrentThemeDetails } from "selectors/themeSelectors"; +import { getCanvasWidgetsStructure } from "selectors/entitiesSelector"; +import { isEqual } from "lodash"; import { WidgetGlobaStyles } from "globalStyles/WidgetGlobalStyles"; const Container = styled.section<{ @@ -49,7 +51,8 @@ function CanvasContainer() { const dispatch = useDispatch(); const currentPageId = useSelector(getCurrentPageId); const isFetchingPage = useSelector(getIsFetchingPage); - const widgets = useSelector(getCanvasWidgetDsl); + const canvasWidth = useSelector(getCanvasWidth); + const widgetsStructure = useSelector(getCanvasWidgetsStructure, isEqual); const pages = useSelector(getViewModePageList); const theme = useSelector(getCurrentThemeDetails); const isPreviewMode = useSelector(previewModeSelector); @@ -80,8 +83,14 @@ function CanvasContainer() { node = pageLoading; } - if (!isPageInitializing && widgets) { - node = ; + if (!isPageInitializing && widgetsStructure) { + node = ( + + ); } // calculating exact height to not allow scroll at this component, // calculating total height minus margin on top, top bar and bottom bar diff --git a/app/client/src/pages/common/CanvasArenas/CanvasSelectionArena.test.tsx b/app/client/src/pages/common/CanvasArenas/CanvasSelectionArena.test.tsx index 74477bd9b05d..e69b63701807 100644 --- a/app/client/src/pages/common/CanvasArenas/CanvasSelectionArena.test.tsx +++ b/app/client/src/pages/common/CanvasArenas/CanvasSelectionArena.test.tsx @@ -6,7 +6,9 @@ import { } from "test/factories/WidgetFactoryUtils"; import { MockApplication, + mockCreateCanvasWidget, mockGetCanvasWidgetDsl, + mockGetWidgetEvalValues, MockPageDSL, syntheticTestMouseEvent, } from "test/testCommon"; @@ -17,10 +19,22 @@ import { sagasToRunForTests } from "test/sagas"; import GlobalHotKeys from "pages/Editor/GlobalHotKeys"; import { UpdatedMainContainer } from "test/testMockedWidgets"; import { MemoryRouter } from "react-router-dom"; +import * as widgetRenderUtils from "utils/widgetRenderUtils"; import * as utilities from "selectors/editorSelectors"; import Canvas from "pages/Editor/Canvas"; +import * as dataTreeSelectors from "selectors/dataTreeSelectors"; describe("Canvas selection test cases", () => { + jest + .spyOn(dataTreeSelectors, "getWidgetEvalValues") + .mockImplementation(mockGetWidgetEvalValues); + jest + .spyOn(utilities, "computeMainContainerWidget") + .mockImplementation((widget) => widget as any); + jest + .spyOn(widgetRenderUtils, "createCanvasWidget") + .mockImplementation(mockCreateCanvasWidget); + it("Should select using canvas draw", () => { const children: any = buildChildren([ { @@ -263,7 +277,11 @@ describe("Canvas selection test cases", () => { const component = render( - + , ); const selectionCanvas: any = component.queryByTestId(`canvas-${canvasId}`); diff --git a/app/client/src/pages/common/CanvasArenas/hooks/useBlocksToBeDraggedOnCanvas.ts b/app/client/src/pages/common/CanvasArenas/hooks/useBlocksToBeDraggedOnCanvas.ts index d0ab292d73bc..50e8de8b4837 100644 --- a/app/client/src/pages/common/CanvasArenas/hooks/useBlocksToBeDraggedOnCanvas.ts +++ b/app/client/src/pages/common/CanvasArenas/hooks/useBlocksToBeDraggedOnCanvas.ts @@ -7,7 +7,7 @@ import { import { useSelector } from "store"; import { AppState } from "reducers"; import { getSelectedWidgets } from "selectors/ui"; -import { getOccupiedSpaces } from "selectors/editorSelectors"; +import { getOccupiedSpacesWhileMoving } from "selectors/editorSelectors"; import { getTableFilterState } from "selectors/tableFilterSelectors"; import { OccupiedSpace } from "constants/CanvasEditorConstants"; import { getDragDetails, getWidgetByID, getWidgets } from "sagas/selectors"; @@ -117,7 +117,8 @@ export const useBlocksToBeDraggedOnCanvas = ({ (state: AppState) => state.ui.widgetDragResize.isResizing, ); const selectedWidgets = useSelector(getSelectedWidgets); - const occupiedSpaces = useSelector(getOccupiedSpaces, isEqual) || {}; + const occupiedSpaces = + useSelector(getOccupiedSpacesWhileMoving, isEqual) || {}; const isNewWidget = !!newWidget && !dragParent; const childrenOccupiedSpaces: OccupiedSpace[] = (dragParent && occupiedSpaces[dragParent]) || []; diff --git a/app/client/src/reducers/entityReducers/canvasWidgetsReducer.tsx b/app/client/src/reducers/entityReducers/canvasWidgetsReducer.tsx index 27759f5e9f0f..c079063970dc 100644 --- a/app/client/src/reducers/entityReducers/canvasWidgetsReducer.tsx +++ b/app/client/src/reducers/entityReducers/canvasWidgetsReducer.tsx @@ -5,6 +5,8 @@ import { ReduxAction, } from "@appsmith/constants/ReduxActionConstants"; import { WidgetProps } from "widgets/BaseWidget"; +import { Diff, diff } from "deep-diff"; +import { uniq } from "lodash"; const initialState: CanvasWidgetsReduxState = {}; @@ -14,6 +16,26 @@ export type FlattenedWidgetProps = }) | orType; +/** + * + * @param updateLayoutDiff + * @returns list of widgets that were updated + */ +function getUpdatedWidgetLists( + updateLayoutDiff: Diff< + CanvasWidgetsReduxState, + { + [widgetId: string]: WidgetProps; + } + >[], +) { + return uniq( + updateLayoutDiff + .map((diff: Diff) => diff.path?.[0]) + .filter((widgetId) => !!widgetId), + ); +} + const canvasWidgetsReducer = createImmerReducer(initialState, { [ReduxActionTypes.INIT_CANVAS_LAYOUT]: ( state: CanvasWidgetsReduxState, @@ -25,10 +47,29 @@ const canvasWidgetsReducer = createImmerReducer(initialState, { state: CanvasWidgetsReduxState, action: ReduxAction, ) => { - return action.payload.widgets; + let listOfUpdatedWidgets; + // if payload has knowledge of which widgets were changed, use that + if (action.payload.updatedWidgetIds) { + listOfUpdatedWidgets = action.payload.updatedWidgetIds; + } // else diff out the widgets that need to be updated + else { + const updatedLayoutDiffs = diff(state, action.payload.widgets); + if (!updatedLayoutDiffs) return state; + + listOfUpdatedWidgets = getUpdatedWidgetLists(updatedLayoutDiffs); + } + + //update only the widgets that need to be updated. + for (const widgetId of listOfUpdatedWidgets) { + const updatedWidget = action.payload.widgets[widgetId]; + if (updatedWidget) { + state[widgetId] = updatedWidget; + } else { + delete state[widgetId]; + } + } }, }); - export interface CanvasWidgetsReduxState { [widgetId: string]: FlattenedWidgetProps; } diff --git a/app/client/src/reducers/entityReducers/canvasWidgetsStructureReducer.ts b/app/client/src/reducers/entityReducers/canvasWidgetsStructureReducer.ts new file mode 100644 index 000000000000..0a62832e0615 --- /dev/null +++ b/app/client/src/reducers/entityReducers/canvasWidgetsStructureReducer.ts @@ -0,0 +1,79 @@ +import { createImmerReducer } from "utils/ReducerUtils"; +import { + ReduxActionTypes, + UpdateCanvasPayload, + ReduxAction, +} from "@appsmith/constants/ReduxActionConstants"; +import { WidgetProps } from "widgets/BaseWidget"; +import { CanvasWidgetStructure } from "widgets/constants"; +import { pick } from "lodash"; +import { + MAIN_CONTAINER_WIDGET_ID, + WidgetType, + WIDGET_DSL_STRUCTURE_PROPS, +} from "constants/WidgetConstants"; +import { CANVAS_DEFAULT_MIN_ROWS } from "constants/AppConstants"; + +export type FlattenedWidgetProps = + | (WidgetProps & { + children?: string[]; + }) + | orType; + +export type CanvasWidgetsStructureReduxState = { + children?: CanvasWidgetsStructureReduxState[]; + type: WidgetType; + widgetId: string; + parentId?: string; + bottomRow: number; + topRow: number; +}; + +const initialState: CanvasWidgetsStructureReduxState = { + type: "CANVAS_WIDGET", + widgetId: MAIN_CONTAINER_WIDGET_ID, + topRow: 0, + bottomRow: CANVAS_DEFAULT_MIN_ROWS, +}; + +/** + * Generate dsl type skeletal structure from canvas widgets + * @param rootWidgetId + * @param widgets + * @returns + */ +function denormalize( + rootWidgetId: string, + widgets: Record, +): CanvasWidgetStructure { + const rootWidget = widgets[rootWidgetId]; + + const children = (rootWidget.children || []).map((childId) => + denormalize(childId, widgets), + ); + + const staticProps = Object.keys(WIDGET_DSL_STRUCTURE_PROPS); + + const structure = pick(rootWidget, staticProps) as CanvasWidgetStructure; + + structure.children = children; + + return structure; +} + +const canvasWidgetsStructureReducer = createImmerReducer(initialState, { + [ReduxActionTypes.INIT_CANVAS_LAYOUT]: ( + state: CanvasWidgetsStructureReduxState, + action: ReduxAction, + ) => { + return denormalize("0", action.payload.widgets); + }, + [ReduxActionTypes.UPDATE_LAYOUT]: ( + state: CanvasWidgetsStructureReduxState, + action: ReduxAction, + ) => { + return denormalize("0", action.payload.widgets); + }, +}); + +export default canvasWidgetsStructureReducer; diff --git a/app/client/src/reducers/entityReducers/index.ts b/app/client/src/reducers/entityReducers/index.ts index ddbc2ca56ca0..86d7de4f1bb7 100644 --- a/app/client/src/reducers/entityReducers/index.ts +++ b/app/client/src/reducers/entityReducers/index.ts @@ -1,17 +1,19 @@ import { combineReducers } from "redux"; -import canvasWidgetsReducer from "./canvasWidgetsReducer"; -import widgetConfigReducer from "./widgetConfigReducer"; import actionsReducer from "./actionsReducer"; +import appReducer from "./appReducer"; +import canvasWidgetsReducer from "./canvasWidgetsReducer"; +import canvasWidgetsStructureReducer from "./canvasWidgetsStructureReducer"; import datasourceReducer from "./datasourceReducer"; -import pageListReducer from "./pageListReducer"; +import jsActionsReducer from "./jsActionsReducer"; import jsExecutionsReducer from "./jsExecutionsReducer"; -import pluginsReducer from "reducers/entityReducers/pluginsReducer"; import metaReducer from "./metaReducer"; -import appReducer from "./appReducer"; -import jsActionsReducer from "./jsActionsReducer"; +import pageListReducer from "./pageListReducer"; +import pluginsReducer from "reducers/entityReducers/pluginsReducer"; +import widgetConfigReducer from "./widgetConfigReducer"; const entityReducer = combineReducers({ canvasWidgets: canvasWidgetsReducer, + canvasWidgetsStructure: canvasWidgetsStructureReducer, widgetConfig: widgetConfigReducer, actions: actionsReducer, datasources: datasourceReducer, diff --git a/app/client/src/reducers/index.tsx b/app/client/src/reducers/index.tsx index b397493d9ad4..e649cab1b58b 100644 --- a/app/client/src/reducers/index.tsx +++ b/app/client/src/reducers/index.tsx @@ -60,6 +60,7 @@ import SettingsReducer, { SettingsReduxState, } from "@appsmith/reducers/settingsReducer"; import { TriggerValuesEvaluationState } from "./evaluationReducers/triggerReducer"; +import { CanvasWidgetStructure } from "widgets/constants"; const appReducer = combineReducers({ entities: entityReducer, @@ -115,6 +116,7 @@ export interface AppState { mainCanvas: MainCanvasReduxState; }; entities: { + canvasWidgetsStructure: CanvasWidgetStructure; canvasWidgets: CanvasWidgetsReduxState; actions: ActionDataState; widgetConfig: WidgetConfigReducerState; diff --git a/app/client/src/reducers/uiReducers/dragResizeReducer.ts b/app/client/src/reducers/uiReducers/dragResizeReducer.ts index 0f8701a9ceb9..e93b31c29150 100644 --- a/app/client/src/reducers/uiReducers/dragResizeReducer.ts +++ b/app/client/src/reducers/uiReducers/dragResizeReducer.ts @@ -1,3 +1,4 @@ +import { areArraysEqual } from "utils/AppsmithUtils"; import { createImmerReducer } from "utils/ReducerUtils"; import { ReduxAction, @@ -86,10 +87,12 @@ export const widgetDraggingReducer = createImmerReducer(initialState, { } } else { state.lastSelectedWidget = action.payload.widgetId; - if (action.payload.widgetId) { - state.selectedWidgets = [action.payload.widgetId]; - } else { + if (!action.payload.widgetId) { state.selectedWidgets = []; + } else if ( + !areArraysEqual(state.selectedWidgets, [action.payload.widgetId]) + ) { + state.selectedWidgets = [action.payload.widgetId]; } } }, @@ -109,7 +112,7 @@ export const widgetDraggingReducer = createImmerReducer(initialState, { action: ReduxAction<{ widgetIds?: string[] }>, ) => { const { widgetIds } = action.payload; - if (widgetIds) { + if (widgetIds && !areArraysEqual(widgetIds, state.selectedWidgets)) { state.selectedWidgets = widgetIds || []; if (widgetIds.length > 1) { state.lastSelectedWidget = ""; @@ -123,7 +126,7 @@ export const widgetDraggingReducer = createImmerReducer(initialState, { action: ReduxAction<{ widgetIds?: string[] }>, ) => { const { widgetIds } = action.payload; - if (widgetIds) { + if (widgetIds && !areArraysEqual(widgetIds, state.selectedWidgets)) { state.selectedWidgets = [...state.selectedWidgets, ...widgetIds]; } }, diff --git a/app/client/src/resizable/resizenreflow/index.tsx b/app/client/src/resizable/resizenreflow/index.tsx index 90147f84cc28..d93ee2116873 100644 --- a/app/client/src/resizable/resizenreflow/index.tsx +++ b/app/client/src/resizable/resizenreflow/index.tsx @@ -1,4 +1,4 @@ -import React, { ReactNode, useState, useEffect, useRef, useMemo } from "react"; +import React, { ReactNode, useState, useEffect, useRef } from "react"; import styled, { StyledComponent } from "styled-components"; import { WIDGET_PADDING } from "constants/WidgetConstants"; import { useDrag } from "react-use-gesture"; @@ -17,7 +17,7 @@ import { ReflowedSpace, } from "reflow/reflowTypes"; import { getNearestParentCanvas } from "utils/generators"; -import { getOccupiedSpaces } from "selectors/editorSelectors"; +import { getContainerOccupiedSpacesSelectorWhileResizing } from "selectors/editorSelectors"; import { isDropZoneOccupied } from "utils/WidgetPropsUtils"; const ResizeWrapper = styled(animated.div)<{ prevents: boolean }>` @@ -161,12 +161,9 @@ export function ReflowResizable(props: ResizableProps) { const resizableRef = useRef(null); const [isResizing, setResizing] = useState(false); - const occupiedSpaces = useSelector(getOccupiedSpaces); - const occupiedSpacesBySiblingWidgets = useMemo(() => { - return occupiedSpaces && props.parentId && occupiedSpaces[props.parentId] - ? occupiedSpaces[props.parentId] - : undefined; - }, [occupiedSpaces, props.parentId]); + const occupiedSpacesBySiblingWidgets = useSelector( + getContainerOccupiedSpacesSelectorWhileResizing(props.parentId), + ); const checkForCollision = (widgetNewSize: { left: number; top: number; diff --git a/app/client/src/sagas/ModalSagas.ts b/app/client/src/sagas/ModalSagas.ts index 422a4beb2915..2f32acc132c9 100644 --- a/app/client/src/sagas/ModalSagas.ts +++ b/app/client/src/sagas/ModalSagas.ts @@ -264,6 +264,7 @@ export function* resizeModalSaga(resizeAction: ReduxAction) { } log.debug("resize computations took", performance.now() - start, "ms"); + //TODO Identify the updated widgets and pass the values yield put(updateAndSaveLayout(widgets)); } catch (error) { yield put({ diff --git a/app/client/src/sagas/PageSagas.tsx b/app/client/src/sagas/PageSagas.tsx index f4e21f06f11e..8e1b76ece9d2 100644 --- a/app/client/src/sagas/PageSagas.tsx +++ b/app/client/src/sagas/PageSagas.tsx @@ -500,7 +500,9 @@ function* savePageSaga(action: ReduxAction<{ isRetry?: boolean }>) { correctWidget: JSON.stringify(normalizedWidgets), }); yield put( - updateAndSaveLayout(normalizedWidgets.entities.canvasWidgets, true), + updateAndSaveLayout(normalizedWidgets.entities.canvasWidgets, { + isRetry: true, + }), ); } } @@ -810,6 +812,7 @@ export function* updateWidgetNameSaga( // @ts-expect-error parentId can be undefined widgets[parentId] = parent; // Update and save the new widgets + //TODO Identify the updated widgets and pass the values yield put(updateAndSaveLayout(widgets)); // Send a update saying that we've successfully updated the name yield put(updateWidgetNameSuccess()); diff --git a/app/client/src/sagas/ReplaySaga.ts b/app/client/src/sagas/ReplaySaga.ts index f3a70f695a4b..47a11a23af0d 100644 --- a/app/client/src/sagas/ReplaySaga.ts +++ b/app/client/src/sagas/ReplaySaga.ts @@ -214,7 +214,13 @@ export function* undoRedoSaga(action: ReduxAction) { const isPropertyUpdate = replay.widgets && replay.propertyUpdates; AnalyticsUtil.logEvent(event, { paths, timeTaken }); if (isPropertyUpdate) yield call(openPropertyPaneSaga, replay); - yield put(updateAndSaveLayout(replayEntity.widgets, false, false)); + //TODO Identify the updated widgets and pass the values + yield put( + updateAndSaveLayout(replayEntity.widgets, { + isRetry: false, + shouldReplay: false, + }), + ); if (!isPropertyUpdate) yield call(postUndoRedoSaga, replay); break; } diff --git a/app/client/src/sagas/WidgetOperationSagas.tsx b/app/client/src/sagas/WidgetOperationSagas.tsx index 3a4808e658c4..a59603cb36eb 100644 --- a/app/client/src/sagas/WidgetOperationSagas.tsx +++ b/app/client/src/sagas/WidgetOperationSagas.tsx @@ -41,7 +41,7 @@ import { isPathADynamicTrigger, } from "utils/DynamicBindingUtils"; import { WidgetProps } from "widgets/BaseWidget"; -import _, { cloneDeep, isString, set } from "lodash"; +import _, { cloneDeep, isString, set, uniq } from "lodash"; import WidgetFactory from "utils/WidgetFactory"; import { resetWidgetMetaProperty } from "actions/metaActions"; import { @@ -56,7 +56,7 @@ import log from "loglevel"; import { navigateToCanvas } from "pages/Editor/Explorer/Widgets/utils"; import { getCurrentPageId, - getWidgetSpacesSelectorForContainer, + getContainerWidgetSpacesSelector, } from "selectors/editorSelectors"; import { selectMultipleWidgetsInitAction } from "actions/widgetSelectionActions"; @@ -596,7 +596,7 @@ function* batchUpdateWidgetPropertySaga( "ms", ); // Save the layout - yield put(updateAndSaveLayout(widgets, undefined, shouldReplay)); + yield put(updateAndSaveLayout(widgets, { shouldReplay })); } function* batchUpdateMultipleWidgetsPropertiesSaga( @@ -620,6 +620,8 @@ function* batchUpdateMultipleWidgetsPropertiesSaga( stateWidgets, ); + const updatedWidgetIds = uniq(updatedWidgets.map((each) => each.widgetId)); + log.debug( "Batch multi-widget properties update calculations took: ", performance.now() - start, @@ -627,7 +629,11 @@ function* batchUpdateMultipleWidgetsPropertiesSaga( ); // Save the layout - yield put(updateAndSaveLayout(updatedStateWidgets)); + yield put( + updateAndSaveLayout(updatedStateWidgets, { + updatedWidgetIds, + }), + ); } function* removeWidgetProperties(widget: WidgetProps, paths: string[]) { @@ -1055,7 +1061,7 @@ function* getNewPositionsBasedOnSelectedWidgets( maxGridColumns: GridDefaults.DEFAULT_GRID_COLUMNS, }; - const reflowSpacesSelector = getWidgetSpacesSelectorForContainer(parentId); + const reflowSpacesSelector = getContainerWidgetSpacesSelector(parentId); const widgetSpaces: WidgetSpace[] = yield select(reflowSpacesSelector) || []; // Ids of each pasting are changed just for reflow @@ -1145,7 +1151,7 @@ function* getNewPositionsBasedOnMousePositions( if (!snapGrid || !mousePositions) return {}; - const reflowSpacesSelector = getWidgetSpacesSelectorForContainer(canvasId); + const reflowSpacesSelector = getContainerWidgetSpacesSelector(canvasId); const widgetSpaces: WidgetSpace[] = yield select(reflowSpacesSelector) || []; let mouseTopRow = mousePositions.top; diff --git a/app/client/src/sagas/WidgetOperationUtils.ts b/app/client/src/sagas/WidgetOperationUtils.ts index b7cd2658187b..819d9e7ae1a1 100644 --- a/app/client/src/sagas/WidgetOperationUtils.ts +++ b/app/client/src/sagas/WidgetOperationUtils.ts @@ -48,7 +48,7 @@ import { getStickyCanvasName, POSITIONED_WIDGET, } from "constants/componentClassNameConstants"; -import { getWidgetSpacesSelectorForContainer } from "selectors/editorSelectors"; +import { getContainerWidgetSpacesSelector } from "selectors/editorSelectors"; import { reflow } from "reflow"; import { getBottomRowAfterReflow } from "utils/reflowHookUtils"; import { DataTreeWidget } from "entities/DataTree/dataTreeFactory"; @@ -1197,7 +1197,7 @@ export const groupWidgetsIntoContainer = function*( // if there are no collision already then reflow the below widgets by 2 rows. if (!isThereACollision) { - const widgetSpacesSelector = getWidgetSpacesSelectorForContainer( + const widgetSpacesSelector = getContainerWidgetSpacesSelector( pastingIntoWidgetId, ); const widgetSpaces: WidgetSpace[] = yield select(widgetSpacesSelector) || diff --git a/app/client/src/selectors/dataTreeSelectors.ts b/app/client/src/selectors/dataTreeSelectors.ts index 0f7ee986f9ef..37d1b265a235 100644 --- a/app/client/src/selectors/dataTreeSelectors.ts +++ b/app/client/src/selectors/dataTreeSelectors.ts @@ -7,12 +7,17 @@ import { getJSCollectionsForCurrentPage, } from "./entitiesSelector"; import { ActionDataState } from "reducers/entityReducers/actionsReducer"; -import { DataTree, DataTreeFactory } from "entities/DataTree/dataTreeFactory"; +import { + DataTree, + DataTreeFactory, + DataTreeWidget, +} from "entities/DataTree/dataTreeFactory"; import { getWidgets, getWidgetsMeta } from "sagas/selectors"; import "url-search-params-polyfill"; import { getPageList } from "./appViewSelectors"; import { AppState } from "reducers"; import { getSelectedAppThemeProperties } from "./appThemingSelectors"; +import { LoadingEntitiesState } from "reducers/evaluationReducers/loadingEntitiesReducer"; export const getUnevaluatedDataTree = createSelector( getActionsForCurrentPage, @@ -56,6 +61,12 @@ export const getEvaluationInverseDependencyMap = (state: AppState) => export const getLoadingEntities = (state: AppState) => state.evaluations.loadingEntities; +export const getIsWidgetLoading = createSelector( + [getLoadingEntities, (_state: AppState, widgetName: string) => widgetName], + (loadingEntities: LoadingEntitiesState, widgetName: string) => + loadingEntities.has(widgetName), +); + /** * returns evaluation tree object * @@ -64,6 +75,11 @@ export const getLoadingEntities = (state: AppState) => export const getDataTree = (state: AppState): DataTree => state.evaluations.tree; +export const getWidgetEvalValues = createSelector( + [getDataTree, (_state: AppState, widgetName: string) => widgetName], + (tree: DataTree, widgetName: string) => tree[widgetName] as DataTreeWidget, +); + // For autocomplete. Use actions cached responses if // there isn't a response already export const getDataTreeForAutocomplete = createSelector( diff --git a/app/client/src/selectors/editorSelectors.tsx b/app/client/src/selectors/editorSelectors.tsx index 94480758bd3c..6b34398529b6 100644 --- a/app/client/src/selectors/editorSelectors.tsx +++ b/app/client/src/selectors/editorSelectors.tsx @@ -18,23 +18,27 @@ import { import { MAIN_CONTAINER_WIDGET_ID, RenderModes, - WIDGET_STATIC_PROPS, } from "constants/WidgetConstants"; import CanvasWidgetsNormalizer from "normalizers/CanvasWidgetsNormalizer"; -import { - DataTree, - DataTreeWidget, - ENTITY_TYPE, -} from "entities/DataTree/dataTreeFactory"; +import { DataTree, DataTreeWidget } from "entities/DataTree/dataTreeFactory"; import { ContainerWidgetProps } from "widgets/ContainerWidget/widget"; -import { find, pick, sortBy } from "lodash"; -import WidgetFactory from "utils/WidgetFactory"; +import { find, sortBy } from "lodash"; import { APP_MODE } from "entities/App"; import { getDataTree, getLoadingEntities } from "selectors/dataTreeSelectors"; import { Page } from "@appsmith/constants/ReduxActionConstants"; import { PLACEHOLDER_APP_SLUG, PLACEHOLDER_PAGE_SLUG } from "constants/routes"; import { ApplicationVersion } from "actions/applicationActions"; import { MainCanvasReduxState } from "reducers/uiReducers/mainCanvasReducer"; +import { + buildChildWidgetTree, + createCanvasWidget, + createLoadingWidget, +} from "utils/widgetRenderUtils"; + +const getIsDraggingOrResizing = (state: AppState) => + state.ui.widgetDragResize.isResizing || state.ui.widgetDragResize.isDragging; + +const getIsResizing = (state: AppState) => state.ui.widgetDragResize.isResizing; export const getWidgetConfigs = (state: AppState) => state.entities.widgetConfig; @@ -233,16 +237,25 @@ export const getWidgetCards = createSelector( }, ); -const getMainContainer = ( +export const computeMainContainerWidget = ( + widget: FlattenedWidgetProps, + mainCanvasProps: MainCanvasReduxState, +) => ({ + ...widget, + rightColumn: mainCanvasProps.width, + minHeight: mainCanvasProps.height, +}); + +export const getMainContainer = ( canvasWidgets: CanvasWidgetsReduxState, evaluatedDataTree: DataTree, mainCanvasProps: MainCanvasReduxState, ) => { - const canvasWidget = { - ...canvasWidgets[MAIN_CONTAINER_WIDGET_ID], - rightColumn: mainCanvasProps.width, - minHeight: mainCanvasProps.height, - }; + const canvasWidget = computeMainContainerWidget( + canvasWidgets[MAIN_CONTAINER_WIDGET_ID], + mainCanvasProps, + ); + //TODO: Need to verify why `evaluatedDataTree` is required here. const evaluatedWidget = find(evaluatedDataTree, { widgetId: MAIN_CONTAINER_WIDGET_ID, @@ -294,6 +307,16 @@ export const getCanvasWidgetDsl = createSelector( }, ); +export const getChildWidgets = createSelector( + [ + getCanvasWidgets, + getDataTree, + getLoadingEntities, + (_state: AppState, widgetId: string) => widgetId, + ], + buildChildWidgetTree, +); + const getOccupiedSpacesForContainer = ( containerWidgetId: string, widgets: FlattenedWidgetProps[], @@ -329,101 +352,181 @@ const getWidgetSpacesForContainer = ( }); }; +/** + * Method to build occupied spaces + * + * @param widgets canvas Widgets + * @param fetchNow would return undefined if false + * @returns An array of occupied spaces + */ +const generateOccupiedSpacesMap = ( + widgets: CanvasWidgetsReduxState, + fetchNow = true, +): { [containerWidgetId: string]: OccupiedSpace[] } | undefined => { + const occupiedSpaces: { + [containerWidgetId: string]: OccupiedSpace[]; + } = {}; + if (!fetchNow) return; + // Get all widgets with type "CONTAINER_WIDGET" and has children + const containerWidgets: FlattenedWidgetProps[] = Object.values( + widgets, + ).filter((widget) => widget.children && widget.children.length > 0); + + // If we have any container widgets + if (containerWidgets) { + containerWidgets.forEach((containerWidget: FlattenedWidgetProps) => { + const containerWidgetId = containerWidget.widgetId; + // Get child widgets for the container + const childWidgets = Object.keys(widgets).filter( + (widgetId) => + containerWidget.children && + containerWidget.children.indexOf(widgetId) > -1 && + !widgets[widgetId].detachFromLayout, + ); + // Get the occupied spaces in this container + // Assign it to the containerWidgetId key in occupiedSpaces + occupiedSpaces[containerWidgetId] = getOccupiedSpacesForContainer( + containerWidgetId, + childWidgets.map((widgetId) => widgets[widgetId]), + ); + }); + } + // Return undefined if there are no occupiedSpaces. + return Object.keys(occupiedSpaces).length > 0 ? occupiedSpaces : undefined; +}; + +// returns occupied spaces export const getOccupiedSpaces = createSelector( getWidgets, - ( - widgets: CanvasWidgetsReduxState, - ): { [containerWidgetId: string]: OccupiedSpace[] } | undefined => { - const occupiedSpaces: { - [containerWidgetId: string]: OccupiedSpace[]; - } = {}; - // Get all widgets with type "CONTAINER_WIDGET" and has children - const containerWidgets: FlattenedWidgetProps[] = Object.values( - widgets, - ).filter((widget) => widget.children && widget.children.length > 0); - - // If we have any container widgets - if (containerWidgets) { - containerWidgets.forEach((containerWidget: FlattenedWidgetProps) => { - const containerWidgetId = containerWidget.widgetId; - // Get child widgets for the container - const childWidgets = Object.keys(widgets).filter( - (widgetId) => - containerWidget.children && - containerWidget.children.indexOf(widgetId) > -1 && - !widgets[widgetId].detachFromLayout, - ); - // Get the occupied spaces in this container - // Assign it to the containerWidgetId key in occupiedSpaces - occupiedSpaces[containerWidgetId] = getOccupiedSpacesForContainer( - containerWidgetId, - childWidgets.map((widgetId) => widgets[widgetId]), - ); - }); - } - // Return undefined if there are no occupiedSpaces. - return Object.keys(occupiedSpaces).length > 0 ? occupiedSpaces : undefined; - }, + generateOccupiedSpacesMap, ); -// same as getOccupiedSpaces but gets only the container specific ocupied Spaces -export function getOccupiedSpacesSelectorForContainer( +// returns occupied spaces only while dragging or moving +export const getOccupiedSpacesWhileMoving = createSelector( + getWidgets, + getIsDraggingOrResizing, + generateOccupiedSpacesMap, +); + +/** + * + * @param widgets + * @param fetchNow returns undined if false + * @param containerId id of container whose occupied spaces we are fetching + * @returns + */ +const generateOccupiedSpacesForContainer = ( + widgets: CanvasWidgetsReduxState, + fetchNow: boolean, containerId: string | undefined, -) { - return createSelector(getWidgets, (widgets: CanvasWidgetsReduxState): - | OccupiedSpace[] - | undefined => { - if (containerId === null || containerId === undefined) return undefined; +): OccupiedSpace[] | undefined => { + if (containerId === null || containerId === undefined || !fetchNow) + return undefined; - const containerWidget: FlattenedWidgetProps = widgets[containerId]; + const containerWidget: FlattenedWidgetProps = widgets[containerId]; - if (!containerWidget || !containerWidget.children) return undefined; + if (!containerWidget || !containerWidget.children) return undefined; - // Get child widgets for the container - const childWidgets = Object.keys(widgets).filter( - (widgetId) => - containerWidget.children && - containerWidget.children.indexOf(widgetId) > -1 && - !widgets[widgetId].detachFromLayout, - ); + // Get child widgets for the container + const childWidgets = Object.keys(widgets).filter( + (widgetId) => + containerWidget.children && + containerWidget.children.indexOf(widgetId) > -1 && + !widgets[widgetId].detachFromLayout, + ); - const occupiedSpaces = getOccupiedSpacesForContainer( - containerId, - childWidgets.map((widgetId) => widgets[widgetId]), - ); - return occupiedSpaces; + const occupiedSpaces = getOccupiedSpacesForContainer( + containerId, + childWidgets.map((widgetId) => widgets[widgetId]), + ); + return occupiedSpaces; +}; + +// same as getOccupiedSpaces but gets only the container specific ocupied Spaces +export function getOccupiedSpacesSelectorForContainer( + containerId: string | undefined, +) { + return createSelector(getWidgets, (widgets: CanvasWidgetsReduxState) => { + return generateOccupiedSpacesForContainer(widgets, true, containerId); }); } -// same as getOccupiedSpaces but gets only the container specific occupied Spaces -export function getWidgetSpacesSelectorForContainer( +/** + * + * @param widgets + * @param fetchNow returns undined if false + * @param containerId id of container whose occupied spaces we are fetching + * @returns + */ +const generateWidgetSpacesForContainer = ( + widgets: CanvasWidgetsReduxState, + fetchNow: boolean, containerId: string | undefined, -) { - return createSelector(getWidgets, (widgets: CanvasWidgetsReduxState): - | WidgetSpace[] - | undefined => { - if (containerId === null || containerId === undefined) return undefined; +): WidgetSpace[] | undefined => { + if (containerId === null || containerId === undefined || !fetchNow) + return undefined; - const containerWidget: FlattenedWidgetProps = widgets[containerId]; + const containerWidget: FlattenedWidgetProps = widgets[containerId]; - if (!containerWidget || !containerWidget.children) return undefined; + if (!containerWidget || !containerWidget.children) return undefined; - // Get child widgets for the container - const childWidgets = Object.keys(widgets).filter( - (widgetId) => - containerWidget.children && - containerWidget.children.indexOf(widgetId) > -1 && - !widgets[widgetId].detachFromLayout, - ); + // Get child widgets for the container + const childWidgets = Object.keys(widgets).filter( + (widgetId) => + containerWidget.children && + containerWidget.children.indexOf(widgetId) > -1 && + !widgets[widgetId].detachFromLayout, + ); - const occupiedSpaces = getWidgetSpacesForContainer( - containerId, - childWidgets.map((widgetId) => widgets[widgetId]), - ); - return occupiedSpaces; + const occupiedSpaces = getWidgetSpacesForContainer( + containerId, + childWidgets.map((widgetId) => widgets[widgetId]), + ); + return occupiedSpaces; +}; + +// same as getOccupiedSpaces but gets only the container specific ocupied Spaces only while resizing +export function getContainerOccupiedSpacesSelectorWhileResizing( + containerId: string | undefined, +) { + return createSelector( + getWidgets, + getIsResizing, + (widgets: CanvasWidgetsReduxState, isResizing: boolean) => { + return generateOccupiedSpacesForContainer( + widgets, + isResizing, + containerId, + ); + }, + ); +} + +// same as getOccupiedSpaces but gets only the container specific occupied Spaces +export function getContainerWidgetSpacesSelector( + containerId: string | undefined, +) { + return createSelector(getWidgets, (widgets: CanvasWidgetsReduxState) => { + return generateWidgetSpacesForContainer(widgets, true, containerId); }); } +// same as getOccupiedSpaces but gets only the container specific occupied Spaces +export function getContainerWidgetSpacesSelectorWhileMoving( + containerId: string | undefined, +) { + return createSelector( + getWidgets, + getIsDraggingOrResizing, + (widgets: CanvasWidgetsReduxState, isDraggingOrResizing: boolean) => { + return generateWidgetSpacesForContainer( + widgets, + isDraggingOrResizing, + containerId, + ); + }, + ); +} export const getActionById = createSelector( [getActions, (state: any, props: any) => props.match.params.apiId], (actions, id) => { @@ -436,45 +539,6 @@ export const getActionById = createSelector( }, ); -const createCanvasWidget = ( - canvasWidget: FlattenedWidgetProps, - evaluatedWidget: DataTreeWidget, -) => { - const widgetStaticProps = pick( - canvasWidget, - Object.keys(WIDGET_STATIC_PROPS), - ); - return { - ...evaluatedWidget, - ...widgetStaticProps, - }; -}; - -const WidgetTypes = WidgetFactory.widgetTypes; -const createLoadingWidget = ( - canvasWidget: FlattenedWidgetProps, -): DataTreeWidget => { - const widgetStaticProps = pick( - canvasWidget, - Object.keys(WIDGET_STATIC_PROPS), - ) as WidgetProps; - return { - ...widgetStaticProps, - type: WidgetTypes.SKELETON_WIDGET, - ENTITY_TYPE: ENTITY_TYPE.WIDGET, - bindingPaths: {}, - reactivePaths: {}, - triggerPaths: {}, - validationPaths: {}, - logBlackList: {}, - isLoading: true, - propertyOverrideDependency: {}, - overridingPropertyPaths: {}, - privateWidgets: {}, - meta: {}, - }; -}; - export const getJSCollectionById = createSelector( [ getJSCollections, diff --git a/app/client/src/selectors/entitiesSelector.ts b/app/client/src/selectors/entitiesSelector.ts index b64f11588b3e..2813500a2fe6 100644 --- a/app/client/src/selectors/entitiesSelector.ts +++ b/app/client/src/selectors/entitiesSelector.ts @@ -466,6 +466,9 @@ export const getAppStoreData = (state: AppState): AppStoreState => export const getCanvasWidgets = (state: AppState): CanvasWidgetsReduxState => state.entities.canvasWidgets; +export const getCanvasWidgetsStructure = (state: AppState) => + state.entities.canvasWidgetsStructure; + const getPageWidgets = (state: AppState) => state.ui.pageWidgets; export const getCurrentPageWidgets = createSelector( getPageWidgets, diff --git a/app/client/src/selectors/onboardingSelectors.tsx b/app/client/src/selectors/onboardingSelectors.tsx index cefbacaa1025..d60d7007f76b 100644 --- a/app/client/src/selectors/onboardingSelectors.tsx +++ b/app/client/src/selectors/onboardingSelectors.tsx @@ -13,7 +13,6 @@ import { } from "./entitiesSelector"; import { getSelectedWidget } from "./ui"; import { GuidedTourEntityNames } from "pages/Editor/GuidedTour/constants"; -import { previewModeSelector } from "./editorSelectors"; // Signposting selectors export const getEnableFirstTimeUserOnboarding = (state: AppState) => { @@ -46,6 +45,10 @@ export const getInOnboardingWidgetSelection = (state: AppState) => export const getIsOnboardingWidgetSelection = (state: AppState) => state.ui.onBoarding.inOnboardingWidgetSelection; +const previewModeSelector = (state: AppState) => { + return state.ui.editor.isPreviewMode; +}; + export const getIsOnboardingTasksView = createSelector( getCanvasWidgets, getIsFirstTimeUserOnboardingEnabled, diff --git a/app/client/src/selectors/propertyPaneSelectors.tsx b/app/client/src/selectors/propertyPaneSelectors.tsx index 50e412e60dba..fa46f89b30ca 100644 --- a/app/client/src/selectors/propertyPaneSelectors.tsx +++ b/app/client/src/selectors/propertyPaneSelectors.tsx @@ -153,6 +153,19 @@ const populateEvaluatedWidgetProperties = ( return evaluatedProperties; }; +const getCurrentEvaluatedWidget = createSelector( + getCurrentWidgetProperties, + getDataTree, + ( + widget: WidgetProps | undefined, + evaluatedTree: DataTree, + ): DataTreeWidget => { + return (widget?.widgetName + ? evaluatedTree[widget.widgetName] + : {}) as DataTreeWidget; + }, +); + export const getWidgetPropsForPropertyName = ( propertyName: string, dependencies: string[] = [], @@ -160,15 +173,11 @@ export const getWidgetPropsForPropertyName = ( ) => { return createSelector( getCurrentWidgetProperties, - getDataTree, + getCurrentEvaluatedWidget, ( widget: WidgetProps | undefined, - evaluatedTree: DataTree, + evaluatedWidget: DataTreeWidget, ): WidgetProperties => { - const evaluatedWidget = find(evaluatedTree, { - widgetId: widget?.widgetId, - }) as DataTreeWidget; - const widgetProperties = populateWidgetProperties( widget, propertyName, diff --git a/app/client/src/selectors/widgetSelectors.ts b/app/client/src/selectors/widgetSelectors.ts index dfe8f9a30627..16b1f12f7085 100644 --- a/app/client/src/selectors/widgetSelectors.ts +++ b/app/client/src/selectors/widgetSelectors.ts @@ -5,10 +5,14 @@ import { getExistingWidgetNames } from "sagas/selectors"; import { getNextEntityName } from "utils/AppsmithUtils"; import WidgetFactory from "utils/WidgetFactory"; +import { getParentToOpenIfAny } from "utils/hooks/useClickToSelectWidget"; export const getIsDraggingOrResizing = (state: AppState) => state.ui.widgetDragResize.isResizing || state.ui.widgetDragResize.isDragging; +export const getIsResizing = (state: AppState) => + state.ui.widgetDragResize.isResizing; + const getCanvasWidgets = (state: AppState) => state.entities.canvasWidgets; export const getModalDropdownList = createSelector( getCanvasWidgets, @@ -52,3 +56,17 @@ export const getParentWidget = createSelector( return; }, ); + +export const getFocusedParentToOpen = createSelector( + getCanvasWidgets, + (state: AppState) => state.ui.widgetDragResize.focusedWidget, + (canvasWidgets, focusedWidgetId) => { + return getParentToOpenIfAny(focusedWidgetId, canvasWidgets); + }, +); + +export const getParentToOpenSelector = (widgetId: string) => { + return createSelector(getCanvasWidgets, (canvasWidgets) => { + return getParentToOpenIfAny(widgetId, canvasWidgets); + }); +}; diff --git a/app/client/src/utils/AppsmithUtils.test.ts b/app/client/src/utils/AppsmithUtils.test.ts index 57fe7efb7b55..865aa3eb39e3 100644 --- a/app/client/src/utils/AppsmithUtils.test.ts +++ b/app/client/src/utils/AppsmithUtils.test.ts @@ -1,4 +1,4 @@ -import { getCamelCaseString } from "utils/AppsmithUtils"; +import { areArraysEqual, getCamelCaseString } from "utils/AppsmithUtils"; describe("getCamelCaseString", () => { it("Should return a string in camelCase", () => { @@ -11,3 +11,21 @@ describe("getCamelCaseString", () => { }); }); }); + +describe("test areArraysEqual", () => { + it("test areArraysEqual method", () => { + const OGArray = ["test1", "test2", "test3"]; + + let testArray: string[] = []; + expect(areArraysEqual(OGArray, testArray)).toBe(false); + + testArray = ["test1", "test3"]; + expect(areArraysEqual(OGArray, testArray)).toBe(false); + + testArray = ["test1", "test2", "test3"]; + expect(areArraysEqual(OGArray, testArray)).toBe(true); + + testArray = ["test1", "test3", "test2"]; + expect(areArraysEqual(OGArray, testArray)).toBe(true); + }); +}); diff --git a/app/client/src/utils/AppsmithUtils.tsx b/app/client/src/utils/AppsmithUtils.tsx index f2a87024dac6..3aebbf55b7b3 100644 --- a/app/client/src/utils/AppsmithUtils.tsx +++ b/app/client/src/utils/AppsmithUtils.tsx @@ -397,3 +397,17 @@ export const base64ToBlob = ( export const isMacOs = () => { return osName === "Mac OS"; }; + +/** + * checks if array of strings are equal regardless of order + * @param arr1 + * @param arr2 + * @returns + */ +export function areArraysEqual(arr1: string[], arr2: string[]) { + if (arr1.length !== arr2.length) return false; + + if (arr1.sort().join(",") === arr2.sort().join(",")) return true; + + return false; +} diff --git a/app/client/src/utils/WidgetFactory.tsx b/app/client/src/utils/WidgetFactory.tsx index 234dae70a1e1..f2fbbc7ecd75 100644 --- a/app/client/src/utils/WidgetFactory.tsx +++ b/app/client/src/utils/WidgetFactory.tsx @@ -1,9 +1,4 @@ -import { - WidgetBuilder, - WidgetDataProps, - WidgetProps, - WidgetState, -} from "widgets/BaseWidget"; +import { WidgetBuilder, WidgetProps, WidgetState } from "widgets/BaseWidget"; import React from "react"; import { PropertyPaneConfig } from "constants/PropertyControlConstants"; @@ -16,6 +11,7 @@ import { convertFunctionsToString, enhancePropertyPaneConfig, } from "./WidgetFactoryHelpers"; +import { CanvasWidgetStructure } from "widgets/constants"; type WidgetDerivedPropertyType = any; export type DerivedPropertiesMap = Record; @@ -25,7 +21,7 @@ class WidgetFactory { static widgetTypes: Record = {}; static widgetMap: Map< WidgetType, - WidgetBuilder + WidgetBuilder > = new Map(); static widgetDerivedPropertiesGetterMap: Map< WidgetType, @@ -146,10 +142,10 @@ class WidgetFactory { } static createWidget( - widgetData: WidgetDataProps, + widgetData: CanvasWidgetStructure, renderMode: RenderMode, ): React.ReactNode { - const widgetProps: WidgetProps = { + const widgetProps = { key: widgetData.widgetId, isVisible: true, ...widgetData, @@ -157,7 +153,6 @@ class WidgetFactory { }; const widgetBuilder = this.widgetMap.get(widgetData.type); if (widgetBuilder) { - // TODO validate props here const widget = widgetBuilder.buildWidget(widgetProps); return widget; } else { diff --git a/app/client/src/utils/WidgetRegisterHelpers.tsx b/app/client/src/utils/WidgetRegisterHelpers.tsx index 23bc1b075406..71e3f754651b 100644 --- a/app/client/src/utils/WidgetRegisterHelpers.tsx +++ b/app/client/src/utils/WidgetRegisterHelpers.tsx @@ -12,12 +12,15 @@ import { generateReactKey } from "./generators"; import { memoize } from "lodash"; import { WidgetFeatureProps } from "./WidgetFeatures"; import { WidgetConfiguration } from "widgets/constants"; +import withWidgetProps from "widgets/withWidgetProps"; const generateWidget = memoize(function getWidgetComponent( Widget: typeof BaseWidget, needsMeta: boolean, ) { - const widget = needsMeta ? withMeta(Widget) : Widget; + let widget = needsMeta ? withMeta(Widget) : Widget; + //@ts-expect-error: type mismatch + widget = withWidgetProps(widget); return Sentry.withProfiler( // @ts-expect-error: Types are not available widget, diff --git a/app/client/src/utils/hooks/useClickToSelectWidget.tsx b/app/client/src/utils/hooks/useClickToSelectWidget.tsx index 74b19bb41d60..6978f590c1c6 100644 --- a/app/client/src/utils/hooks/useClickToSelectWidget.tsx +++ b/app/client/src/utils/hooks/useClickToSelectWidget.tsx @@ -10,10 +10,10 @@ import { AppState } from "reducers"; import { CanvasWidgetsReduxState } from "reducers/entityReducers/canvasWidgetsReducer"; import { APP_MODE } from "entities/App"; import { getAppMode } from "selectors/applicationSelectors"; -import { getWidgets } from "sagas/selectors"; import { useWidgetSelection } from "./useWidgetSelection"; import React, { ReactNode, useCallback } from "react"; import { stopEventPropagation } from "utils/AppsmithUtils"; +import { getFocusedParentToOpen } from "selectors/widgetSelectors"; /** * @@ -103,7 +103,6 @@ export const useClickToSelectWidget = () => { const { focusWidget, selectWidget } = useWidgetSelection(); const isPropPaneVisible = useSelector(getIsPropertyPaneVisible); const isTableFilterPaneVisible = useSelector(getIsTableFilterPaneVisible); - const widgets: CanvasWidgetsReduxState = useSelector(getWidgets); const selectedWidgetId = useSelector(getCurrentWidgetId); const focusedWidgetId = useSelector( (state: AppState) => state.ui.widgetDragResize.focusedWidget, @@ -119,7 +118,7 @@ export const useClickToSelectWidget = () => { (state: AppState) => state.ui.widgetDragResize.isDragging, ); - const parentWidgetToOpen = getParentToOpenIfAny(focusedWidgetId, widgets); + const parentWidgetToOpen = useSelector(getFocusedParentToOpen); const clickToSelectWidget = (e: any, targetWidgetId: string) => { // ignore click captures // 1. if the component was resizing or dragging coz it is handled internally in draggable component diff --git a/app/client/src/utils/hooks/useDynamicAppLayout.tsx b/app/client/src/utils/hooks/useDynamicAppLayout.tsx index 4621edaa4f8c..aa1504c4e4c0 100644 --- a/app/client/src/utils/hooks/useDynamicAppLayout.tsx +++ b/app/client/src/utils/hooks/useDynamicAppLayout.tsx @@ -21,8 +21,8 @@ import { scrollbarWidth } from "utils/helpers"; import { useWindowSizeHooks } from "./dragResizeHooks"; import { getAppMode } from "selectors/entitiesSelector"; import { updateCanvasLayoutAction } from "actions/editorActions"; -import { calculateDynamicHeight } from "utils/DSLMigrations"; import { getIsCanvasInitialized } from "selectors/mainCanvasSelectors"; +import { calculateDynamicHeight } from "utils/DSLMigrations"; const BORDERS_WIDTH = 2; const GUTTER_WIDTH = 72; diff --git a/app/client/src/utils/hooks/useReflow.ts b/app/client/src/utils/hooks/useReflow.ts index 6effa1c16be3..abba0af0fd55 100644 --- a/app/client/src/utils/hooks/useReflow.ts +++ b/app/client/src/utils/hooks/useReflow.ts @@ -3,7 +3,7 @@ import { OccupiedSpace, WidgetSpace } from "constants/CanvasEditorConstants"; import { isEmpty, throttle } from "lodash"; import { useEffect, useRef } from "react"; import { useDispatch, useSelector } from "react-redux"; -import { getWidgetSpacesSelectorForContainer } from "selectors/editorSelectors"; +import { getContainerWidgetSpacesSelectorWhileMoving } from "selectors/editorSelectors"; import { reflow } from "reflow"; import { CollidingSpace, @@ -70,7 +70,9 @@ export const useReflow = ( const isReflowing = useRef(false); - const reflowSpacesSelector = getWidgetSpacesSelectorForContainer(parentId); + const reflowSpacesSelector = getContainerWidgetSpacesSelectorWhileMoving( + parentId, + ); const widgetSpaces: WidgetSpace[] = useSelector(reflowSpacesSelector) || []; const prevPositions = useRef(OGPositions); diff --git a/app/client/src/utils/widgetRenderUtils.test.ts b/app/client/src/utils/widgetRenderUtils.test.ts new file mode 100644 index 000000000000..d7b9f5a0d3ee --- /dev/null +++ b/app/client/src/utils/widgetRenderUtils.test.ts @@ -0,0 +1,266 @@ +import { DataTree } from "entities/DataTree/dataTreeFactory"; +import { CanvasWidgetsReduxState } from "reducers/entityReducers/canvasWidgetsReducer"; +import { buildChildWidgetTree } from "./widgetRenderUtils"; + +describe("test EditorUtils methods", () => { + describe("should test buildChildWidgetTree method", () => { + const canvasWidgets = ({ + "1": { + children: ["2"], + type: "FORM_WIDGET", + widgetId: "1", + parentId: "0", + topRow: 0, + bottomRow: 10, + widgetName: "one", + }, + "2": { + children: ["3", "4"], + type: "CANVAS", + widgetId: "2", + parentId: "1", + topRow: 0, + bottomRow: 100, + widgetName: "two", + }, + "3": { + children: [], + type: "TEXT", + widgetId: "3", + parentId: "2", + topRow: 4, + bottomRow: 5, + widgetName: "three", + }, + "4": { + children: [], + type: "BUTTON", + widgetId: "4", + parentId: "2", + topRow: 6, + bottomRow: 18, + widgetName: "four", + }, + } as unknown) as CanvasWidgetsReduxState; + + const dataTree = ({ + one: { + children: ["2"], + type: "FORM_WIDGET", + widgetId: "1", + parentId: "0", + topRow: 0, + bottomRow: 10, + widgetName: "one", + skipForFormWidget: "test", + value: "test", + isDirty: true, + isValid: true, + }, + two: { + children: ["3", "4"], + type: "CANVAS", + widgetId: "2", + parentId: "1", + topRow: 0, + bottomRow: 100, + widgetName: "two", + skipForFormWidget: "test", + value: "test", + isDirty: true, + isValid: true, + }, + three: { + children: [], + type: "TEXT", + widgetId: "3", + parentId: "2", + topRow: 4, + bottomRow: 5, + widgetName: "three", + skipForFormWidget: "test", + value: "test", + isDirty: true, + isValid: true, + }, + four: { + children: [], + type: "BUTTON", + widgetId: "4", + parentId: "2", + topRow: 6, + bottomRow: 18, + widgetName: "four", + skipForFormWidget: "test", + value: "test", + isDirty: true, + isValid: true, + }, + } as unknown) as DataTree; + + it("should return a complete childwidgets Tree", () => { + const childWidgetTree = [ + { + bottomRow: 5, + children: [], + skipForFormWidget: "test", + isDirty: true, + isLoading: false, + isValid: true, + parentId: "2", + topRow: 4, + type: "TEXT", + value: "test", + widgetId: "3", + widgetName: "three", + }, + { + bottomRow: 18, + children: [], + skipForFormWidget: "test", + isDirty: true, + isLoading: false, + isValid: true, + parentId: "2", + topRow: 6, + type: "BUTTON", + value: "test", + widgetId: "4", + widgetName: "four", + }, + ]; + + expect( + buildChildWidgetTree( + canvasWidgets, + dataTree, + new Set("one"), + "2", + ), + ).toEqual(childWidgetTree); + }); + + it("should return a partial childwidgets Tree with properties specified", () => { + const childWidgetTree = [ + { + bottomRow: 100, + children: [ + { + bottomRow: 5, + children: [], + isDirty: true, + isLoading: false, + isValid: true, + parentId: "2", + topRow: 4, + type: "TEXT", + value: "test", + widgetId: "3", + widgetName: "three", + }, + { + bottomRow: 18, + children: [], + isDirty: true, + isLoading: false, + isValid: true, + parentId: "2", + topRow: 6, + type: "BUTTON", + value: "test", + widgetId: "4", + widgetName: "four", + }, + ], + isDirty: true, + isLoading: false, + isValid: true, + parentId: "1", + topRow: 0, + type: "CANVAS", + value: "test", + widgetId: "2", + widgetName: "two", + }, + ]; + + expect( + buildChildWidgetTree( + canvasWidgets, + dataTree, + new Set("two"), + "1", + ), + ).toEqual(childWidgetTree); + }); + + it("should return a partial childwidgets Tree with just loading widgets", () => { + const childWidgetTree = [ + { + ENTITY_TYPE: "WIDGET", + bindingPaths: {}, + bottomRow: 100, + children: [ + { + ENTITY_TYPE: "WIDGET", + bindingPaths: {}, + bottomRow: 5, + children: [], + isLoading: false, + logBlackList: {}, + meta: {}, + overridingPropertyPaths: {}, + parentId: "2", + privateWidgets: {}, + propertyOverrideDependency: {}, + reactivePaths: {}, + topRow: 4, + triggerPaths: {}, + type: undefined, + validationPaths: {}, + widgetId: "3", + widgetName: "three", + }, + { + ENTITY_TYPE: "WIDGET", + bindingPaths: {}, + bottomRow: 18, + children: [], + isLoading: false, + logBlackList: {}, + meta: {}, + overridingPropertyPaths: {}, + parentId: "2", + privateWidgets: {}, + propertyOverrideDependency: {}, + reactivePaths: {}, + topRow: 6, + triggerPaths: {}, + type: undefined, + validationPaths: {}, + widgetId: "4", + widgetName: "four", + }, + ], + isLoading: false, + logBlackList: {}, + meta: {}, + overridingPropertyPaths: {}, + parentId: "1", + privateWidgets: {}, + propertyOverrideDependency: {}, + reactivePaths: {}, + topRow: 0, + triggerPaths: {}, + type: undefined, + validationPaths: {}, + widgetId: "2", + widgetName: "two", + }, + ]; + expect( + buildChildWidgetTree(canvasWidgets, {}, new Set("one"), "1"), + ).toEqual(childWidgetTree); + }); + }); +}); diff --git a/app/client/src/utils/widgetRenderUtils.tsx b/app/client/src/utils/widgetRenderUtils.tsx new file mode 100644 index 000000000000..284c1abfe21b --- /dev/null +++ b/app/client/src/utils/widgetRenderUtils.tsx @@ -0,0 +1,122 @@ +import { + CanvasWidgetsReduxState, + FlattenedWidgetProps, +} from "reducers/entityReducers/canvasWidgetsReducer"; +import { + DataTree, + DataTreeWidget, + ENTITY_TYPE, +} from "entities/DataTree/dataTreeFactory"; +import { pick } from "lodash"; +import { WIDGET_STATIC_PROPS } from "constants/WidgetConstants"; +import WidgetFactory from "./WidgetFactory"; +import { WidgetProps } from "widgets/BaseWidget"; +import { LoadingEntitiesState } from "reducers/evaluationReducers/loadingEntitiesReducer"; + +export const createCanvasWidget = ( + canvasWidget: FlattenedWidgetProps, + evaluatedWidget: DataTreeWidget, + specificChildProps?: string[], +) => { + const widgetStaticProps = pick( + canvasWidget, + Object.keys(WIDGET_STATIC_PROPS), + ); + + //Pick required only contents for specific widgets + const evaluatedStaticProps = specificChildProps + ? pick(evaluatedWidget, specificChildProps) + : evaluatedWidget; + + return { + ...evaluatedStaticProps, + ...widgetStaticProps, + } as DataTreeWidget; +}; + +const WidgetTypes = WidgetFactory.widgetTypes; +export const createLoadingWidget = ( + canvasWidget: FlattenedWidgetProps, +): DataTreeWidget => { + const widgetStaticProps = pick( + canvasWidget, + Object.keys(WIDGET_STATIC_PROPS), + ) as WidgetProps; + return { + ...widgetStaticProps, + type: WidgetTypes.SKELETON_WIDGET, + ENTITY_TYPE: ENTITY_TYPE.WIDGET, + bindingPaths: {}, + reactivePaths: {}, + triggerPaths: {}, + validationPaths: {}, + logBlackList: {}, + isLoading: true, + propertyOverrideDependency: {}, + overridingPropertyPaths: {}, + privateWidgets: {}, + meta: {}, + }; +}; + +/** + * Method to build a child widget tree + * This method is used to build the child widgets array for widgets like Form, or List widget, + * That need to know the state of its child or grandChild to derive properties + * This can be replaced with deived properties of the individual widgets + * + * @param canvasWidgets + * @param evaluatedDataTree + * @param loadingEntities + * @param widgetId + * @param requiredWidgetProps + * @returns + */ +export function buildChildWidgetTree( + canvasWidgets: CanvasWidgetsReduxState, + evaluatedDataTree: DataTree, + loadingEntities: LoadingEntitiesState, + widgetId: string, + requiredWidgetProps?: string[], +) { + const parentWidget = canvasWidgets[widgetId]; + + // specificChildProps are the only properties required by the parent to derive it's properties + const specificChildProps = + requiredWidgetProps || + getWidgetSpecificChildProps(canvasWidgets[widgetId].type); + + if (parentWidget.children) { + return parentWidget.children.map((childWidgetId) => { + const childWidget = canvasWidgets[childWidgetId]; + const evaluatedWidget = evaluatedDataTree[ + childWidget.widgetName + ] as DataTreeWidget; + const widget = evaluatedWidget + ? createCanvasWidget(childWidget, evaluatedWidget, specificChildProps) + : createLoadingWidget(childWidget); + + widget.isLoading = loadingEntities.has(childWidget.widgetName); + + if (widget?.children?.length > 0) { + widget.children = buildChildWidgetTree( + canvasWidgets, + evaluatedDataTree, + loadingEntities, + childWidgetId, + specificChildProps, + ); + } + + return widget; + }); + } + + return []; +} + +function getWidgetSpecificChildProps(type: string) { + if (type === "FORM_WIDGET") { + return ["value", "isDirty", "isValid", "isLoading", "children"]; + } +} diff --git a/app/client/src/widgets/BaseWidget.tsx b/app/client/src/widgets/BaseWidget.tsx index e25a4cfc7514..be02526017d3 100644 --- a/app/client/src/widgets/BaseWidget.tsx +++ b/app/client/src/widgets/BaseWidget.tsx @@ -37,6 +37,9 @@ import { BatchPropertyUpdatePayload } from "actions/controlActions"; import AppsmithConsole from "utils/AppsmithConsole"; import { ENTITY_TYPE } from "entities/AppsmithConsole"; import PreviewModeComponent from "components/editorComponents/PreviewModeComponent"; +import { CanvasWidgetStructure } from "./constants"; +import { DataTreeWidget } from "entities/DataTree/dataTreeFactory"; +import Skeleton from "./Skeleton"; /*** * BaseWidget @@ -299,11 +302,32 @@ abstract class BaseWidget< ); } + getWidgetComponent = () => { + const { renderMode, type } = this.props; + + /** + * The widget mount calls the withWidgetProps with the widgetId and type to fetch the + * widget props. During the computation of the props (in withWidgetProps) if the evaluated + * values are not present (which will not be during mount), the widget type is changed to + * SKELETON_WIDGET. + * + * Note: This is done to retain the old rendering flow without any breaking changes. + * This could be refactored into not changing the widget type but to have a boolean flag. + */ + if (type === "SKELETON_WIDGET") { + return ; + } + + return renderMode === RenderModes.CANVAS + ? this.getCanvasView() + : this.getPageView(); + }; + private getWidgetView(): ReactNode { let content: ReactNode; switch (this.props.renderMode) { case RenderModes.CANVAS: - content = this.getCanvasView(); + content = this.getWidgetComponent(); content = this.addPreviewModeWidget(content); if (!this.props.detachFromLayout) { if (!this.props.resizeDisabled) content = this.makeResizable(content); @@ -317,7 +341,7 @@ abstract class BaseWidget< // return this.getCanvasView(); case RenderModes.PAGE: - content = this.getPageView(); + content = this.getWidgetComponent(); if (this.props.isVisible) { content = this.addErrorBoundary(content); if (!this.props.detachFromLayout) { @@ -399,7 +423,10 @@ export interface BaseStyle { export type WidgetState = Record; -export interface WidgetBuilder { +export interface WidgetBuilder< + T extends CanvasWidgetStructure, + S extends WidgetState +> { buildWidget(widgetProps: T): JSX.Element; } @@ -410,6 +437,7 @@ export interface WidgetBaseProps { parentId?: string; renderMode: RenderMode; version: number; + childWidgets?: DataTreeWidget[]; } export type WidgetRowCols = { diff --git a/app/client/src/widgets/CanvasWidget.tsx b/app/client/src/widgets/CanvasWidget.tsx index ea5060e10635..5eed86d8ecf6 100644 --- a/app/client/src/widgets/CanvasWidget.tsx +++ b/app/client/src/widgets/CanvasWidget.tsx @@ -3,11 +3,12 @@ import { WidgetProps } from "widgets/BaseWidget"; import ContainerWidget, { ContainerWidgetProps, } from "widgets/ContainerWidget/widget"; -import { GridDefaults, RenderModes } from "constants/WidgetConstants"; +import { GridDefaults } from "constants/WidgetConstants"; import DropTargetComponent from "components/editorComponents/DropTargetComponent"; import { getCanvasSnapRows } from "utils/WidgetPropsUtils"; import { getCanvasClassName } from "utils/generators"; import WidgetFactory, { DerivedPropertiesMap } from "utils/WidgetFactory"; +import { CanvasWidgetStructure } from "./constants"; import { CANVAS_DEFAULT_MIN_HEIGHT_PX } from "constants/AppConstants"; class CanvasWidget extends ContainerWidget { @@ -43,29 +44,19 @@ class CanvasWidget extends ContainerWidget { ); } - renderChildWidget(childWidgetData: WidgetProps): React.ReactNode { + renderChildWidget(childWidgetData: CanvasWidgetStructure): React.ReactNode { if (!childWidgetData) return null; - // For now, isVisible prop defines whether to render a detached widget - if (childWidgetData.detachFromLayout && !childWidgetData.isVisible) { - return null; - } - // We don't render invisible widgets in view mode - if ( - this.props.renderMode === RenderModes.PAGE && - !childWidgetData.isVisible - ) { - return null; - } + const childWidget = { ...childWidgetData }; const snapSpaces = this.getSnapSpaces(); - childWidgetData.parentColumnSpace = snapSpaces.snapColumnSpace; - childWidgetData.parentRowSpace = snapSpaces.snapRowSpace; - if (this.props.noPad) childWidgetData.noContainerOffset = true; - childWidgetData.parentId = this.props.widgetId; + childWidget.parentColumnSpace = snapSpaces.snapColumnSpace; + childWidget.parentRowSpace = snapSpaces.snapRowSpace; + if (this.props.noPad) childWidget.noContainerOffset = true; + childWidget.parentId = this.props.widgetId; - return WidgetFactory.createWidget(childWidgetData, this.props.renderMode); + return WidgetFactory.createWidget(childWidget, this.props.renderMode); } getPageView() { diff --git a/app/client/src/widgets/ContainerWidget/widget/index.tsx b/app/client/src/widgets/ContainerWidget/widget/index.tsx index b580af1e105d..d025f6c9fdd8 100644 --- a/app/client/src/widgets/ContainerWidget/widget/index.tsx +++ b/app/client/src/widgets/ContainerWidget/widget/index.tsx @@ -269,25 +269,21 @@ class ContainerWidget extends BaseWidget< }; renderChildWidget(childWidgetData: WidgetProps): React.ReactNode { - // For now, isVisible prop defines whether to render a detached widget - if (childWidgetData.detachFromLayout && !childWidgetData.isVisible) { - return null; - } + const childWidget = { ...childWidgetData }; const { componentHeight, componentWidth } = this.getComponentDimensions(); - childWidgetData.rightColumn = componentWidth; - childWidgetData.bottomRow = this.props.shouldScrollContents - ? childWidgetData.bottomRow + childWidget.rightColumn = componentWidth; + childWidget.bottomRow = this.props.shouldScrollContents + ? childWidget.bottomRow : componentHeight; - childWidgetData.minHeight = componentHeight; - childWidgetData.isVisible = this.props.isVisible; - childWidgetData.shouldScrollContents = false; - childWidgetData.canExtend = this.props.shouldScrollContents; + childWidget.minHeight = componentHeight; + childWidget.shouldScrollContents = false; + childWidget.canExtend = this.props.shouldScrollContents; - childWidgetData.parentId = this.props.widgetId; + childWidget.parentId = this.props.widgetId; - return WidgetFactory.createWidget(childWidgetData, this.props.renderMode); + return WidgetFactory.createWidget(childWidget, this.props.renderMode); } renderChildren = () => { diff --git a/app/client/src/widgets/FormWidget/widget/index.tsx b/app/client/src/widgets/FormWidget/widget/index.tsx index 9f8c508f84f0..5f731c59c61f 100644 --- a/app/client/src/widgets/FormWidget/widget/index.tsx +++ b/app/client/src/widgets/FormWidget/widget/index.tsx @@ -11,7 +11,7 @@ class FormWidget extends ContainerWidget { checkInvalidChildren = (children: WidgetProps[]): boolean => { return some(children, (child) => { if ("children" in child) { - return this.checkInvalidChildren(child.children); + return this.checkInvalidChildren(child.children || []); } if ("isValid" in child) { return !child.isValid; @@ -29,9 +29,7 @@ class FormWidget extends ContainerWidget { this.updateFormData(); // Check if the form is dirty - const hasChanges = this.checkFormValueChanges( - get(this.props, "children[0]"), - ); + const hasChanges = this.checkFormValueChanges(this.getChildContainer()); if (hasChanges !== this.props.hasChanges) { this.props.updateWidgetMetaProperty("hasChanges", hasChanges); @@ -42,9 +40,7 @@ class FormWidget extends ContainerWidget { super.componentDidUpdate(prevProps); this.updateFormData(); // Check if the form is dirty - const hasChanges = this.checkFormValueChanges( - get(this.props, "children[0]"), - ); + const hasChanges = this.checkFormValueChanges(this.getChildContainer()); if (hasChanges !== this.props.hasChanges) { this.props.updateWidgetMetaProperty("hasChanges", hasChanges); @@ -60,7 +56,7 @@ class FormWidget extends ContainerWidget { if (!hasChanges) { return childWidgets.some( (child) => - child.children && + child.children?.length && this.checkFormValueChanges(get(child, "children[0]")), ); } @@ -68,8 +64,13 @@ class FormWidget extends ContainerWidget { return hasChanges; } + getChildContainer = () => { + const { childWidgets = [] } = this.props; + return { ...childWidgets[0] }; + }; + updateFormData() { - const firstChild = get(this.props, "children[0]"); + const firstChild = this.getChildContainer(); if (firstChild) { const formData = this.getFormData(firstChild); if (!isEqual(formData, this.props.data)) { @@ -89,16 +90,23 @@ class FormWidget extends ContainerWidget { return formData; } - renderChildWidget(childWidgetData: WidgetProps): React.ReactNode { - if (childWidgetData.children) { - const isInvalid = this.checkInvalidChildren(childWidgetData.children); - childWidgetData.children.forEach((grandChild: WidgetProps) => { - if (isInvalid) grandChild.isFormValid = false; - // Add submit and reset handlers - grandChild.onReset = this.handleResetInputs; - }); + renderChildWidget(): React.ReactNode { + const childContainer = this.getChildContainer(); + + if (childContainer.children) { + const isInvalid = this.checkInvalidChildren(childContainer.children); + childContainer.children = childContainer.children.map( + (child: WidgetProps) => { + const grandChild = { ...child }; + if (isInvalid) grandChild.isFormValid = false; + // Add submit and reset handlers + grandChild.onReset = this.handleResetInputs; + return grandChild; + }, + ); } - return super.renderChildWidget(childWidgetData); + + return super.renderChildWidget(childContainer); } static getWidgetType(): WidgetType { diff --git a/app/client/src/widgets/ListWidget/widget/index.tsx b/app/client/src/widgets/ListWidget/widget/index.tsx index da6440b51c6a..7dc8b97b2361 100644 --- a/app/client/src/widgets/ListWidget/widget/index.tsx +++ b/app/client/src/widgets/ListWidget/widget/index.tsx @@ -86,7 +86,7 @@ class ListWidget extends BaseWidget, WidgetState> { } this.props.updateWidgetMetaProperty( "templateBottomRow", - get(this.props.children, "0.children.0.bottomRow"), + get(this.props.childWidgets, "0.children.0.bottomRow"), ); // generate childMetaPropertyMap @@ -266,12 +266,12 @@ class ListWidget extends BaseWidget, WidgetState> { } if ( - get(this.props.children, "0.children.0.bottomRow") !== - get(prevProps.children, "0.children.0.bottomRow") + get(this.props.childWidgets, "0.children.0.bottomRow") !== + get(prevProps.childWidgets, "0.children.0.bottomRow") ) { this.props.updateWidgetMetaProperty( "templateBottomRow", - get(this.props.children, "0.children.0.bottomRow"), + get(this.props.childWidgets, "0.children.0.bottomRow"), { triggerPropertyName: "onPageSizeChange", dynamicString: this.props.onPageSizeChange, @@ -661,11 +661,26 @@ class ListWidget extends BaseWidget, WidgetState> { return updatedChildren; }; + /** + * We add a flag here to not fetch the widgets from the canvasWidgets + * in the metaHOC base on the widget id. Rather use the props as is. + */ + addFlags = (children: DSLWidget[]) => { + return (children || []).map((childWidget) => { + childWidget.skipWidgetPropsHydration = true; + + childWidget.children = this.addFlags(childWidget?.children || []); + + return childWidget; + }); + }; + updateGridChildrenProps = (children: DSLWidget[]) => { let updatedChildren = this.useNewValues(children); updatedChildren = this.updateActions(updatedChildren); updatedChildren = this.paginateItems(updatedChildren); updatedChildren = this.updatePosition(updatedChildren); + updatedChildren = this.addFlags(updatedChildren); return updatedChildren; }; @@ -705,12 +720,12 @@ class ListWidget extends BaseWidget, WidgetState> { */ renderChildren = () => { if ( - this.props.children && - this.props.children.length > 0 && + this.props.childWidgets && + this.props.childWidgets.length > 0 && this.props.listData ) { const { page } = this.state; - const children = removeFalsyEntries(klona(this.props.children)); + const children = removeFalsyEntries(klona(this.props.childWidgets)); const childCanvas = children[0]; const canvasChildren = childCanvas.children; @@ -807,7 +822,7 @@ class ListWidget extends BaseWidget, WidgetState> { const { pageNo, serverSidePaginationEnabled } = this.props; const { perPage, shouldPaginate } = this.shouldPaginate(); const templateBottomRow = get( - this.props.children, + this.props.childWidgets, "0.children.0.bottomRow", ); const templateHeight = diff --git a/app/client/src/widgets/ModalWidget/widget/index.tsx b/app/client/src/widgets/ModalWidget/widget/index.tsx index 7a15e80d0a6c..7b422e82a0d4 100644 --- a/app/client/src/widgets/ModalWidget/widget/index.tsx +++ b/app/client/src/widgets/ModalWidget/widget/index.tsx @@ -200,18 +200,19 @@ export class ModalWidget extends BaseWidget { } renderChildWidget = (childWidgetData: WidgetProps): ReactNode => { - childWidgetData.parentId = this.props.widgetId; - childWidgetData.shouldScrollContents = false; - childWidgetData.canExtend = this.props.shouldScrollContents; - childWidgetData.bottomRow = this.props.shouldScrollContents - ? Math.max(childWidgetData.bottomRow, this.props.height) + const childData = { ...childWidgetData }; + childData.parentId = this.props.widgetId; + childData.shouldScrollContents = false; + childData.canExtend = this.props.shouldScrollContents; + childData.bottomRow = this.props.shouldScrollContents + ? Math.max(childData.bottomRow, this.props.height) : this.props.height; - childWidgetData.isVisible = this.props.isVisible; - childWidgetData.containerStyle = "none"; - childWidgetData.minHeight = this.props.height; - childWidgetData.rightColumn = + childData.containerStyle = "none"; + childData.minHeight = this.props.height; + childData.rightColumn = this.getModalWidth(this.props.width) + WIDGET_PADDING * 2; - return WidgetFactory.createWidget(childWidgetData, this.props.renderMode); + + return WidgetFactory.createWidget(childData, this.props.renderMode); }; onModalClose = () => { diff --git a/app/client/src/widgets/MultiSelectWidgetV2/widget/index.tsx b/app/client/src/widgets/MultiSelectWidgetV2/widget/index.tsx index d9744ce7e00d..9ef428883395 100644 --- a/app/client/src/widgets/MultiSelectWidgetV2/widget/index.tsx +++ b/app/client/src/widgets/MultiSelectWidgetV2/widget/index.tsx @@ -968,6 +968,7 @@ class MultiSelectWidget extends BaseWidget< // Check if defaultOptionValue is string let isStringArray = false; if ( + this.props.defaultOptionValue && this.props.defaultOptionValue.some( (value: any) => isString(value) || isFinite(value), ) diff --git a/app/client/src/widgets/Skeleton.tsx b/app/client/src/widgets/Skeleton.tsx new file mode 100644 index 000000000000..26c641470899 --- /dev/null +++ b/app/client/src/widgets/Skeleton.tsx @@ -0,0 +1,13 @@ +import React from "react"; +import styled from "styled-components"; + +export const SkeletonWrapper = styled.div` + height: 100%; + width: 100%; +`; + +function Skeleton() { + return ; +} + +export default Skeleton; diff --git a/app/client/src/widgets/StatboxWidget/widget/index.tsx b/app/client/src/widgets/StatboxWidget/widget/index.tsx index ab80bc6a8d6d..ff69bcf0d4e8 100644 --- a/app/client/src/widgets/StatboxWidget/widget/index.tsx +++ b/app/client/src/widgets/StatboxWidget/widget/index.tsx @@ -1,5 +1,4 @@ import { WidgetType } from "constants/WidgetConstants"; -import { WidgetProps } from "widgets/BaseWidget"; import ContainerWidget from "widgets/ContainerWidget"; import { ValidationTypes } from "constants/WidgetValidation"; @@ -211,17 +210,6 @@ class StatboxWidget extends ContainerWidget { ]; } - renderChildWidget(childWidgetData: WidgetProps): React.ReactNode { - if (childWidgetData.children) { - childWidgetData.children.forEach((grandChild: WidgetProps) => { - if (grandChild.type === "ICON_BUTTON_WIDGET" && !!grandChild.onClick) { - grandChild.boxShadow = "VARIANT1"; - } - }); - } - return super.renderChildWidget(childWidgetData); - } - static getWidgetType(): WidgetType { return "STATBOX_WIDGET"; } diff --git a/app/client/src/widgets/TabsWidget/widget/index.test.tsx b/app/client/src/widgets/TabsWidget/widget/index.test.tsx index 1ebdb3e2fed4..f0488cf6489a 100644 --- a/app/client/src/widgets/TabsWidget/widget/index.test.tsx +++ b/app/client/src/widgets/TabsWidget/widget/index.test.tsx @@ -3,11 +3,28 @@ import { widgetCanvasFactory, } from "test/factories/WidgetFactoryUtils"; import { render, fireEvent } from "test/testUtils"; +import * as widgetRenderUtils from "utils/widgetRenderUtils"; +import * as dataTreeSelectors from "selectors/dataTreeSelectors"; +import * as editorSelectors from "selectors/editorSelectors"; import Canvas from "pages/Editor/Canvas"; import React from "react"; -import { MockPageDSL } from "test/testCommon"; +import { + mockCreateCanvasWidget, + mockGetWidgetEvalValues, + MockPageDSL, +} from "test/testCommon"; describe("Tabs widget functional cases", () => { + jest + .spyOn(dataTreeSelectors, "getWidgetEvalValues") + .mockImplementation(mockGetWidgetEvalValues); + jest + .spyOn(editorSelectors, "computeMainContainerWidget") + .mockImplementation((widget) => widget as any); + jest + .spyOn(widgetRenderUtils, "createCanvasWidget") + .mockImplementation(mockCreateCanvasWidget); + it("Should render 2 tabs by default", () => { const children: any = buildChildren([{ type: "TABS_WIDGET" }]); const dsl: any = widgetCanvasFactory.build({ @@ -15,7 +32,11 @@ describe("Tabs widget functional cases", () => { }); const component = render( - + , ); const tab1 = component.queryByText("Tab 1"); @@ -41,7 +62,11 @@ describe("Tabs widget functional cases", () => { }); const component = render( - + , ); const tab1 = component.queryByText("Tab 1"); diff --git a/app/client/src/widgets/TabsWidget/widget/index.tsx b/app/client/src/widgets/TabsWidget/widget/index.tsx index fc8df5cc7b63..a7fdad741d36 100644 --- a/app/client/src/widgets/TabsWidget/widget/index.tsx +++ b/app/client/src/widgets/TabsWidget/widget/index.tsx @@ -452,11 +452,11 @@ class TabsWidget extends BaseWidget< renderComponent = () => { const selectedTabWidgetId = this.props.selectedTabWidgetId; - const childWidgetData: TabContainerWidgetProps = this.props.children - ?.filter(Boolean) - .filter((item) => { + const childWidgetData = { + ...this.props.children?.filter(Boolean).filter((item) => { return selectedTabWidgetId === item.widgetId; - })[0]; + })[0], + }; if (!childWidgetData) { return null; } diff --git a/app/client/src/widgets/constants.ts b/app/client/src/widgets/constants.ts index 12a774750c82..ad657dc0855d 100644 --- a/app/client/src/widgets/constants.ts +++ b/app/client/src/widgets/constants.ts @@ -1,5 +1,7 @@ import { IconNames } from "@blueprintjs/icons"; import { PropertyPaneConfig } from "constants/PropertyControlConstants"; +import { WIDGET_STATIC_PROPS } from "constants/WidgetConstants"; +import { omit } from "lodash"; import { WidgetConfigProps } from "reducers/entityReducers/widgetConfigReducer"; import { DerivedPropertiesMap } from "utils/WidgetFactory"; import { WidgetFeatures } from "utils/WidgetFeatures"; @@ -43,6 +45,14 @@ export interface DSLWidget extends WidgetProps { children?: DSLWidget[]; } +const staticProps = omit(WIDGET_STATIC_PROPS, "children"); +export type CanvasWidgetStructure = Pick< + WidgetProps, + keyof typeof staticProps +> & { + children?: CanvasWidgetStructure[]; +}; + export enum FileDataTypes { Base64 = "Base64", Text = "Text", diff --git a/app/client/src/widgets/withWidgetProps.tsx b/app/client/src/widgets/withWidgetProps.tsx new file mode 100644 index 000000000000..02b527251302 --- /dev/null +++ b/app/client/src/widgets/withWidgetProps.tsx @@ -0,0 +1,124 @@ +import equal from "fast-deep-equal/es6"; +import React from "react"; + +import BaseWidget, { WidgetProps } from "./BaseWidget"; +import { + MAIN_CONTAINER_WIDGET_ID, + RenderModes, +} from "constants/WidgetConstants"; +import { + getWidgetEvalValues, + getIsWidgetLoading, +} from "selectors/dataTreeSelectors"; +import { + getMainCanvasProps, + computeMainContainerWidget, + getChildWidgets, + getRenderMode, +} from "selectors/editorSelectors"; +import { AppState } from "reducers"; +import { useSelector } from "react-redux"; +import { getWidget } from "sagas/selectors"; +import { + createCanvasWidget, + createLoadingWidget, +} from "utils/widgetRenderUtils"; + +const WIDGETS_WITH_CHILD_WIDGETS = ["LIST_WIDGET", "FORM_WIDGET"]; + +function withWidgetProps(WrappedWidget: typeof BaseWidget) { + function WrappedPropsComponent( + props: WidgetProps & { skipWidgetPropsHydration?: boolean }, + ) { + const { children, skipWidgetPropsHydration, type, widgetId } = props; + + const canvasWidget = useSelector((state: AppState) => + getWidget(state, widgetId), + ); + const mainCanvasProps = useSelector((state: AppState) => + getMainCanvasProps(state), + ); + const renderMode = useSelector(getRenderMode); + const evaluatedWidget = useSelector((state: AppState) => + getWidgetEvalValues(state, canvasWidget?.widgetName), + ); + const isLoading = useSelector((state: AppState) => + getIsWidgetLoading(state, canvasWidget?.widgetName), + ); + + const childWidgets = useSelector((state: AppState) => { + if (!WIDGETS_WITH_CHILD_WIDGETS.includes(type)) return undefined; + return getChildWidgets(state, widgetId); + }, equal); + + let widgetProps: WidgetProps = {} as WidgetProps; + + if (!skipWidgetPropsHydration) { + const canvasWidgetProps = (() => { + if (widgetId === MAIN_CONTAINER_WIDGET_ID) { + return computeMainContainerWidget(canvasWidget, mainCanvasProps); + } + + return evaluatedWidget + ? createCanvasWidget(canvasWidget, evaluatedWidget) + : createLoadingWidget(canvasWidget); + })(); + + widgetProps = { ...canvasWidgetProps }; + /** + * MODAL_WIDGET by default is to be hidden unless the isVisible property is found. + * If the isVisible property is undefined and the widget is MODAL_WIDGET then isVisible + * is set to false + * If the isVisible property is undefined and the widget is not MODAL_WIDGET then isVisible + * is set to true + */ + widgetProps.isVisible = + canvasWidgetProps.isVisible ?? + canvasWidgetProps.type !== "MODAL_WIDGET"; + + if ( + props.type === "CANVAS_WIDGET" && + widgetId !== MAIN_CONTAINER_WIDGET_ID + ) { + widgetProps.rightColumn = props.rightColumn; + widgetProps.bottomRow = props.bottomRow; + widgetProps.minHeight = props.minHeight; + widgetProps.shouldScrollContents = props.shouldScrollContents; + widgetProps.canExtend = props.canExtend; + widgetProps.parentId = props.parentId; + } else if (widgetId !== MAIN_CONTAINER_WIDGET_ID) { + widgetProps.parentColumnSpace = props.parentColumnSpace; + widgetProps.parentRowSpace = props.parentRowSpace; + widgetProps.parentId = props.parentId; + + // Form Widget Props + widgetProps.onReset = props.onReset; + if ("isFormValid" in props) widgetProps.isFormValid = props.isFormValid; + } + + widgetProps.children = children; + + widgetProps.isLoading = isLoading; + widgetProps.childWidgets = childWidgets; + } + + //merging with original props + widgetProps = { ...props, ...widgetProps, renderMode }; + + // isVisible prop defines whether to render a detached widget + if (widgetProps.detachFromLayout && !widgetProps.isVisible) { + return null; + } + + // We don't render invisible widgets in view mode + if (renderMode === RenderModes.PAGE && !widgetProps.isVisible) { + return null; + } + + return ; + } + + return WrappedPropsComponent; +} + +export default withWidgetProps; diff --git a/app/client/test/factories/Widgets/TabsFactory.ts b/app/client/test/factories/Widgets/TabsFactory.ts index d948f39c493a..5c73ad4a8fb7 100644 --- a/app/client/test/factories/Widgets/TabsFactory.ts +++ b/app/client/test/factories/Widgets/TabsFactory.ts @@ -141,5 +141,5 @@ export const TabsFactory = Factory.Sync.makeFactory({ widgetName: Factory.each((i) => `Tabs${i + 1}`), widgetId: generateReactKey(), renderMode: "CANVAS", - version: 1, + version: 3, }); diff --git a/app/client/test/testCommon.ts b/app/client/test/testCommon.ts index 410f9e447a1b..b7c9471c3b6a 100644 --- a/app/client/test/testCommon.ts +++ b/app/client/test/testCommon.ts @@ -16,6 +16,9 @@ import { getCanvasWidgets } from "selectors/entitiesSelector"; import CanvasWidgetsNormalizer from "normalizers/CanvasWidgetsNormalizer"; import { DSLWidget } from "widgets/constants"; +import { DataTreeWidget } from "entities/DataTree/dataTreeFactory"; +import { AppState } from "reducers"; +import { FlattenedWidgetProps } from "reducers/entityReducers/canvasWidgetsStructureReducer"; import urlBuilder from "entities/URLRedirect/URLAssembly"; export const useMockDsl = (dsl: any) => { @@ -87,6 +90,48 @@ export const mockGetCanvasWidgetDsl = createSelector( }, ); +const getChildWidgets = ( + canvasWidgets: CanvasWidgetsReduxState, + widgetId: string, +) => { + const parentWidget = canvasWidgets[widgetId]; + + if (parentWidget.children) { + return parentWidget.children.map((childWidgetId) => { + const childWidget = { ...canvasWidgets[childWidgetId] } as DataTreeWidget; + + if (childWidget?.children?.length > 0) { + childWidget.children = getChildWidgets(canvasWidgets, childWidgetId); + } + + return childWidget; + }); + } + + return []; +}; + +export const mockGetChildWidgets = (state: AppState, widgetId: string) => { + return getChildWidgets(state.entities.canvasWidgets, widgetId); +}; + +export const mockCreateCanvasWidget = ( + canvasWidget: FlattenedWidgetProps, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + evaluatedWidget: DataTreeWidget, +): any => { + return { ...canvasWidget }; +}; + +export const mockGetWidgetEvalValues = ( + state: AppState, + widgetName: string, +) => { + return Object.values(state.entities.canvasWidgets).find( + (widget) => widget.widgetName === widgetName, + ) as DataTreeWidget; +}; + export const syntheticTestMouseEvent = ( event: MouseEvent, optionsToAdd = {}, diff --git a/app/client/test/testMockedWidgets.tsx b/app/client/test/testMockedWidgets.tsx index 5580a540bebf..7ba4305ead77 100644 --- a/app/client/test/testMockedWidgets.tsx +++ b/app/client/test/testMockedWidgets.tsx @@ -2,11 +2,12 @@ import Canvas from "pages/Editor/Canvas"; import MainContainer from "pages/Editor/MainContainer"; import React from "react"; import { useSelector } from "react-redux"; -import { mockGetCanvasWidgetDsl, useMockDsl } from "./testCommon"; +import { getCanvasWidgetsStructure } from "selectors/entitiesSelector"; +import { useMockDsl } from "./testCommon"; export function MockCanvas() { - const dsl = useSelector(mockGetCanvasWidgetDsl); - return ; + const canvasWidgetsStructure = useSelector(getCanvasWidgetsStructure); + return ; } export function UpdatedMainContainer({ dsl }: any) { useMockDsl(dsl);