perf: Widget re-rendering refactor (#14485)

* initial commit

* props hoc

* changes

* removed ignores and withWidgetProps

* added extra props to canvasStructure

* widget props changes

* list widget changes

* reintroduced widget props hook and other refactors

* remove warnings

* added deepequal for childWidgets selector

* fix global hotkeys and tabs widget jest test

* fix main container test fix

* fixed view mode width

* fix form widget values

* minor fix

* fix skeleton

* form widget validity fix

* jest test fix

* fixed tests: GlobalHotkeys, Tabs, CanvasSelectectionArena and fixed main container rendering

* minor fix

* minor comments

* reverted commented code

* simplified structure, selective redux state updates and other inconsistencies

* fix junit test cases

* stop form widget from force rendering children

* fix test case

* random commit to re run tests

* update isFormValid prop only if it exists

* detangling circular dependency

* fixing cypress tests

* cleaned up code

* clean up man cnavas props and fix jest cases

* fix rendering order of child widgets for canvas

* fix dropdown reset spec

* adding comments

* cleaning up unwanted code

* fix multiselect widget on deploy

* adressing review comments

* addressing minor review comment changes

* destructuring modal widget child and fix test case

* fix communityIssues cypress spec

* rewrite isVisible logic to match previous behaviour

* merging widget props with component props before checking isVisible

* adressing review comments for modal widget's isVisible

Co-authored-by: rahulramesha <rahul@appsmith.com>
This commit is contained in:
ashit-rath 2022-08-19 15:40:36 +05:30 committed by GitHub
parent 9e3d95d216
commit 893fd34cdd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
63 changed files with 1351 additions and 337 deletions

View File

@ -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 @@
}
]
}
}
}

View File

@ -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();

View File

@ -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() {

View File

@ -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")

View File

@ -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 });

View File

@ -0,0 +1 @@
export const modifierKey = Cypress.platform === "darwin" ? "meta" : "ctrl";

View File

@ -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);
});

View File

@ -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 },
};
};

View File

@ -902,6 +902,7 @@ export interface UpdateCanvasPayload {
currentPageName: string;
currentApplicationId: string;
pageActions: PageAction[][];
updatedWidgetIds?: string[];
}
export interface ShowPropertyPanePayload {

View File

@ -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,
);

View File

@ -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 =

View File

@ -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;

View File

@ -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;

View File

@ -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 (
<PageView className="t--app-viewer-page" width={props.dsl.rightColumn}>
{props.dsl.widgetId &&
WidgetFactory.createWidget(props.dsl, RenderModes.PAGE)}
<PageView className="t--app-viewer-page" width={props.canvasWidth}>
{props.widgetsStructure.widgetId &&
WidgetFactory.createWidget(props.widgetsStructure, RenderModes.PAGE)}
</PageView>
);
}

View File

@ -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<AppViewerRouteParams>;
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 (
<Section>
<AppPage
appName={currentApplication?.name}
dsl={widgets}
canvasWidth={canvasWidth}
pageId={match.params.pageId}
pageName={currentPageName}
widgetsStructure={widgetsStructure}
/>
<RequestConfirmationModal />
</Section>

View File

@ -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 && (
<CanvasMultiPointerArena pageId={pageId} />
)}

View File

@ -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(
<MemoryRouter

View File

@ -6,13 +6,17 @@ import {
import { act, render, fireEvent } from "test/testUtils";
import GlobalHotKeys from "./GlobalHotKeys";
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 {
MockApplication,
mockCreateCanvasWidget,
mockGetCanvasWidgetDsl,
mockGetWidgetEvalValues,
syntheticTestMouseEvent,
} from "test/testCommon";
import lodash from "lodash";
@ -89,6 +93,16 @@ const renderNestedComponent = () => {
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);

View File

@ -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 = <Canvas dsl={widgets} pageId={params.pageId} />;
if (!isPageInitializing && widgetsStructure) {
node = (
<Canvas
canvasWidth={canvasWidth}
pageId={params.pageId}
widgetsStructure={widgetsStructure}
/>
);
}
// calculating exact height to not allow scroll at this component,
// calculating total height minus margin on top, top bar and bottom bar

View File

@ -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(
<MockPageDSL dsl={dsl}>
<Canvas dsl={dsl} pageId="" />
<Canvas
canvasWidth={dsl.rightColumn}
pageId="page_id"
widgetsStructure={dsl}
/>
</MockPageDSL>,
);
const selectionCanvas: any = component.queryByTestId(`canvas-${canvasId}`);

View File

@ -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]) || [];

View File

@ -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 = never> =
})
| orType;
/**
*
* @param updateLayoutDiff
* @returns list of widgets that were updated
*/
function getUpdatedWidgetLists(
updateLayoutDiff: Diff<
CanvasWidgetsReduxState,
{
[widgetId: string]: WidgetProps;
}
>[],
) {
return uniq(
updateLayoutDiff
.map((diff: Diff<CanvasWidgetsReduxState>) => 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<UpdateCanvasPayload>,
) => {
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;
}

View File

@ -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<orType = never> =
| (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<string, FlattenedWidgetProps>,
): 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<UpdateCanvasPayload>,
) => {
return denormalize("0", action.payload.widgets);
},
[ReduxActionTypes.UPDATE_LAYOUT]: (
state: CanvasWidgetsStructureReduxState,
action: ReduxAction<UpdateCanvasPayload>,
) => {
return denormalize("0", action.payload.widgets);
},
});
export default canvasWidgetsStructureReducer;

View File

@ -1,17 +1,19 @@
import { combineReducers } from "redux";
import canvasWidgetsReducer from "./canvasWidgetsReducer";
import widgetConfigReducer from "./widgetConfigReducer";
import actionsReducer from "./actionsReducer";
import datasourceReducer from "./datasourceReducer";
import pageListReducer from "./pageListReducer";
import jsExecutionsReducer from "./jsExecutionsReducer";
import pluginsReducer from "reducers/entityReducers/pluginsReducer";
import metaReducer from "./metaReducer";
import appReducer from "./appReducer";
import canvasWidgetsReducer from "./canvasWidgetsReducer";
import canvasWidgetsStructureReducer from "./canvasWidgetsStructureReducer";
import datasourceReducer from "./datasourceReducer";
import jsActionsReducer from "./jsActionsReducer";
import jsExecutionsReducer from "./jsExecutionsReducer";
import metaReducer from "./metaReducer";
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,

View File

@ -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;

View File

@ -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];
}
},

View File

@ -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<HTMLDivElement>(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;

View File

@ -264,6 +264,7 @@ export function* resizeModalSaga(resizeAction: ReduxAction<ModalWidgetResize>) {
}
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({

View File

@ -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());

View File

@ -214,7 +214,13 @@ export function* undoRedoSaga(action: ReduxAction<UndoRedoPayload>) {
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;
}

View File

@ -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;

View File

@ -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) ||

View File

@ -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(

View File

@ -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,
);
// 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,
): OccupiedSpace[] | undefined => {
if (containerId === null || containerId === undefined || !fetchNow)
return undefined;
const containerWidget: FlattenedWidgetProps = widgets[containerId];
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,
);
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):
| OccupiedSpace[]
| undefined => {
if (containerId === null || containerId === undefined) return undefined;
return createSelector(getWidgets, (widgets: CanvasWidgetsReduxState) => {
return generateOccupiedSpacesForContainer(widgets, true, containerId);
});
}
const containerWidget: FlattenedWidgetProps = widgets[containerId];
/**
*
* @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,
): WidgetSpace[] | undefined => {
if (containerId === null || containerId === undefined || !fetchNow)
return undefined;
if (!containerWidget || !containerWidget.children) return undefined;
const containerWidget: FlattenedWidgetProps = widgets[containerId];
// Get child widgets for the container
const childWidgets = Object.keys(widgets).filter(
(widgetId) =>
containerWidget.children &&
containerWidget.children.indexOf(widgetId) > -1 &&
!widgets[widgetId].detachFromLayout,
);
if (!containerWidget || !containerWidget.children) return undefined;
const occupiedSpaces = getOccupiedSpacesForContainer(
containerId,
childWidgets.map((widgetId) => widgets[widgetId]),
);
return occupiedSpaces;
// 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;
};
// 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 getWidgetSpacesSelectorForContainer(
export function getContainerWidgetSpacesSelectorWhileMoving(
containerId: string | undefined,
) {
return createSelector(getWidgets, (widgets: CanvasWidgetsReduxState):
| WidgetSpace[]
| undefined => {
if (containerId === null || containerId === undefined) return undefined;
const containerWidget: FlattenedWidgetProps = widgets[containerId];
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,
);
const occupiedSpaces = getWidgetSpacesForContainer(
containerId,
childWidgets.map((widgetId) => widgets[widgetId]),
);
return occupiedSpaces;
});
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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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);
});
};

View File

@ -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);
});
});

View File

@ -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;
}

View File

@ -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<string, string>;
@ -25,7 +21,7 @@ class WidgetFactory {
static widgetTypes: Record<string, string> = {};
static widgetMap: Map<
WidgetType,
WidgetBuilder<WidgetProps, WidgetState>
WidgetBuilder<CanvasWidgetStructure, WidgetState>
> = 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 {

View File

@ -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,

View File

@ -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

View File

@ -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;

View File

@ -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<boolean>(false);
const reflowSpacesSelector = getWidgetSpacesSelectorForContainer(parentId);
const reflowSpacesSelector = getContainerWidgetSpacesSelectorWhileMoving(
parentId,
);
const widgetSpaces: WidgetSpace[] = useSelector(reflowSpacesSelector) || [];
const prevPositions = useRef<OccupiedSpace[] | undefined>(OGPositions);

View File

@ -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<string>("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<string>("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<string>("one"), "1"),
).toEqual(childWidgetTree);
});
});
});

View File

@ -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"];
}
}

View File

@ -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 <Skeleton />;
}
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<string, unknown>;
export interface WidgetBuilder<T extends WidgetProps, S extends WidgetState> {
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 = {

View File

@ -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() {

View File

@ -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 = () => {

View File

@ -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 {

View File

@ -88,7 +88,7 @@ class ListWidget extends BaseWidget<ListWidgetProps<WidgetProps>, WidgetState> {
}
this.props.updateWidgetMetaProperty(
"templateBottomRow",
get(this.props.children, "0.children.0.bottomRow"),
get(this.props.childWidgets, "0.children.0.bottomRow"),
);
// generate childMetaPropertyMap
@ -268,12 +268,12 @@ class ListWidget extends BaseWidget<ListWidgetProps<WidgetProps>, 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,
@ -663,11 +663,26 @@ class ListWidget extends BaseWidget<ListWidgetProps<WidgetProps>, 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;
};
@ -707,12 +722,12 @@ class ListWidget extends BaseWidget<ListWidgetProps<WidgetProps>, 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 { perPage } = this.shouldPaginate();
@ -814,7 +829,7 @@ class ListWidget extends BaseWidget<ListWidgetProps<WidgetProps>, 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 =

View File

@ -200,18 +200,19 @@ export class ModalWidget extends BaseWidget<ModalWidgetProps, WidgetState> {
}
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 = () => {

View File

@ -964,6 +964,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),
)

View File

@ -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 <SkeletonWrapper className="bp3-skeleton" />;
}
export default Skeleton;

View File

@ -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";
}

View File

@ -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(
<MockPageDSL dsl={dsl}>
<Canvas dsl={dsl} pageId="" />
<Canvas
canvasWidth={dsl.rightColumn}
pageId="page_id"
widgetsStructure={dsl}
/>
</MockPageDSL>,
);
const tab1 = component.queryByText("Tab 1");
@ -41,7 +62,11 @@ describe("Tabs widget functional cases", () => {
});
const component = render(
<MockPageDSL dsl={dsl}>
<Canvas dsl={dsl} pageId="" />
<Canvas
canvasWidth={dsl.rightColumn}
pageId="page_id"
widgetsStructure={dsl}
/>
</MockPageDSL>,
);
const tab1 = component.queryByText("Tab 1");

View File

@ -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;
}

View File

@ -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",

View File

@ -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 <WrappedWidget {...widgetProps} />;
}
return WrappedPropsComponent;
}
export default withWidgetProps;

View File

@ -141,5 +141,5 @@ export const TabsFactory = Factory.Sync.makeFactory<WidgetProps>({
widgetName: Factory.each((i) => `Tabs${i + 1}`),
widgetId: generateReactKey(),
renderMode: "CANVAS",
version: 1,
version: 3,
});

View File

@ -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 = {},

View File

@ -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 <Canvas dsl={dsl} pageId="" />;
const canvasWidgetsStructure = useSelector(getCanvasWidgetsStructure);
return <Canvas widgetsStructure={canvasWidgetsStructure} />;
}
export function UpdatedMainContainer({ dsl }: any) {
useMockDsl(dsl);