PromucFlow_constructor/app/client/src/widgets/withWidgetProps.tsx
balajisoundar 837d0cc76a
chore: custom widget (#28926)
## Description
This PR's adds the custom widget.

#### PR fixes following issue(s)
Fixes #28607
Fixes #28610
Fixes #28615
Fixes #28608
Fixes #28612
> if no issue exists, please create an issue and ask the maintainers
about this first
>
>
#### Media
> A video or a GIF is preferred. when using Loom, don’t embed because it
looks like it’s a GIF. instead, just link to the video
>
>
#### Type of change
- New feature (non-breaking change which adds functionality)
>
>
>
## Testing
>
#### How Has This Been Tested?
> Please describe the tests that you ran to verify your changes. Also
list any relevant details for your test configuration.
> Delete anything that is not relevant
- [ ] Manual
- [ ] JUnit
- [x] Jest
- [x] Cypress
>
>
#### Test Plan
> Add Testsmith test cases links that relate to this PR
>
>
#### Issues raised during DP testing
> Link issues raised during DP testing for better visiblity and tracking
(copy link from comments dropped on this PR)
>
>
>
## Checklist:
#### Dev activity
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my own code
- [x] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] PR is being merged under a feature flag


#### QA activity:
- [ ] [Speedbreak
features](https://github.com/appsmithorg/TestSmith/wiki/Guidelines-for-test-plans#speedbreakers-)
have been covered
- [ ] Test plan covers all impacted features and [areas of
interest](https://github.com/appsmithorg/TestSmith/wiki/Guidelines-for-test-plans#areas-of-interest-)
- [ ] Test plan has been peer reviewed by project stakeholders and other
QA members
- [ ] Manually tested functionality on DP
- [ ] We had an implementation alignment call with stakeholders post QA
Round 2
- [ ] Cypress test cases have been added and approved by SDET/manual QA
- [ ] Added `Test Plan Approved` label after Cypress tests were reviewed
- [ ] Added `Test Plan Approved` label after JUnit tests were reviewed


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Introduced search functionality with a wildcard option for unmatched
widget searches.
  - Added new routes for custom widget editing.
  - Implemented additional editor modes for HTML and CSS.
- Created new code templates and help components for custom widget
builders.
- Enhanced property controls with new button controls for custom widget
editing.
  - Updated theming interfaces and constants for better theming support.
- Added the CustomWidget component for embedding custom widgets in
iframes.
  - Expanded widgets library to include the CustomWidget.

- **Enhancements**
- Improved `CodeEditor` with additional modes, props, and resize
behavior.
- Enhanced `PropertyPaneControlConfig` interface with dynamic
dependencies and additional properties.
- Refined custom widget scripts with communication channel and event
handling.

- **Bug Fixes**
  - Fixed visibility logic for the `ExternalWidget` card.
  
- **Documentation**
- Added new messages and documentation links for custom widget features.

- **Tests**
- Implemented new Cypress tests for custom widget default components and
property pane interactions.
  - Updated workspace commands in Cypress tests.

- **Refactor**
- Streamlined `PropertyControl` state management and editing functions.
  - Refactored code editor hint helper logic.
  
- **Style**
  - Added `borderLess` prop to style components without borders.

- **Chores**
  - Updated constants and messages related to custom widget features.
  - Adjusted webpack configuration to ignore specific module warnings.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2023-12-28 12:16:28 +05:30

338 lines
12 KiB
TypeScript

import equal from "fast-deep-equal/es6";
import React from "react";
import { ReduxActionTypes } from "@appsmith/constants/ReduxActionConstants";
import type { AppState } from "@appsmith/reducers";
import { checkContainersForAutoHeightAction } from "actions/autoHeightActions";
import {
GridDefaults,
MAIN_CONTAINER_WIDGET_ID,
RenderModes,
WIDGET_PADDING,
} from "constants/WidgetConstants";
import { useDispatch, useSelector } from "react-redux";
import { getWidget } from "sagas/selectors";
import {
getIsWidgetLoading,
getWidgetEvalValues,
} from "selectors/dataTreeSelectors";
import {
computeMainContainerWidget,
getChildWidgets,
getMainCanvasProps,
getRenderMode,
getMetaWidgetChildrenStructure,
getMetaWidget,
getIsAutoLayoutMobileBreakPoint,
getCanvasWidth,
combinedPreviewModeSelector,
} from "selectors/editorSelectors";
import {
createCanvasWidget,
createLoadingWidget,
} from "utils/widgetRenderUtils";
import type { WidgetProps } from "./BaseWidget";
import type BaseWidget from "./BaseWidget";
import type { WidgetEntityConfig } from "@appsmith/entities/DataTree/types";
import { Positioning } from "layoutSystems/common/utils/constants";
import { isAutoHeightEnabledForWidget } from "./WidgetUtils";
import { CANVAS_DEFAULT_MIN_HEIGHT_PX } from "constants/AppConstants";
import { getGoogleMapsApiKey } from "@appsmith/selectors/tenantSelectors";
import ConfigTreeActions from "utils/configTree";
import { getSelectedWidgetAncestry } from "../selectors/widgetSelectors";
import { getWidgetMinMaxDimensionsInPixel } from "layoutSystems/autolayout/utils/flexWidgetUtils";
import { defaultAutoLayoutWidgets } from "layoutSystems/autolayout/utils/constants";
import { getFlattenedChildCanvasWidgets } from "selectors/flattenedChildCanvasSelector";
import { LayoutSystemTypes } from "layoutSystems/types";
import { getLayoutSystemType } from "selectors/layoutSystemSelectors";
import { isWidgetSelectedForPropertyPane } from "selectors/propertyPaneSelectors";
const WIDGETS_WITH_CHILD_WIDGETS = ["LIST_WIDGET", "FORM_WIDGET"];
const WIDGETS_REQUIRING_SELECTED_ANCESTRY = ["MODAL_WIDGET", "TABS_WIDGET"];
function withWidgetProps(WrappedWidget: typeof BaseWidget) {
function WrappedPropsComponent(
props: WidgetProps & { skipWidgetPropsHydration?: boolean },
) {
const {
children,
hasMetaWidgets,
referencedWidgetId,
requiresFlatWidgetChildren,
skipWidgetPropsHydration,
type,
widgetId,
} = props;
const isPreviewMode = useSelector(combinedPreviewModeSelector);
const canvasWidget = useSelector((state: AppState) =>
getWidget(state, widgetId),
);
const mainCanvasWidth = useSelector(getCanvasWidth);
const metaWidget = useSelector(getMetaWidget(widgetId));
const mainCanvasProps = useSelector((state: AppState) =>
getMainCanvasProps(state),
);
const googleMapsApiKey = useSelector(getGoogleMapsApiKey);
const renderMode = useSelector(getRenderMode);
const widgetName = canvasWidget?.widgetName || metaWidget?.widgetName;
const evaluatedWidget = useSelector((state: AppState) =>
getWidgetEvalValues(state, widgetName),
);
const isLoading = useSelector((state: AppState) =>
getIsWidgetLoading(state, widgetName),
);
const metaWidgetChildrenStructure = useSelector(
getMetaWidgetChildrenStructure(widgetId, type, hasMetaWidgets),
equal,
);
const isWidgetSelected = useSelector((state: AppState) =>
isWidgetSelectedForPropertyPane(state, widgetId),
);
const isMobile = useSelector(getIsAutoLayoutMobileBreakPoint);
const layoutSystemType = useSelector(getLayoutSystemType);
const isAutoLayout = layoutSystemType === LayoutSystemTypes.AUTO;
const configTree = ConfigTreeActions.getConfigTree();
const evaluatedWidgetConfig = configTree[
canvasWidget?.widgetName
] as WidgetEntityConfig;
const dispatch = useDispatch();
const childWidgets = useSelector((state: AppState) => {
if (!WIDGETS_WITH_CHILD_WIDGETS.includes(type)) return undefined;
return getChildWidgets(state, widgetId);
}, equal);
const flattenedChildCanvasWidgets = useSelector((state: AppState) => {
if (requiresFlatWidgetChildren) {
return getFlattenedChildCanvasWidgets(
state,
referencedWidgetId || widgetId,
);
}
}, equal);
const selectedWidgetAncestry: string[] = useSelector((state: AppState) => {
if (!WIDGETS_REQUIRING_SELECTED_ANCESTRY.includes(type)) {
return [];
}
return getSelectedWidgetAncestry(state);
}, equal);
let widgetProps: WidgetProps = {} as WidgetProps;
const widget = metaWidget || canvasWidget;
if (!skipWidgetPropsHydration) {
const canvasWidgetProps = (() => {
if (widgetId === MAIN_CONTAINER_WIDGET_ID) {
const computed = computeMainContainerWidget(
canvasWidget,
mainCanvasProps,
);
if (renderMode === RenderModes.CANVAS) {
return {
...computed,
bottomRow: Math.max(
computed.minHeight,
computed.bottomRow +
GridDefaults.MAIN_CANVAS_EXTENSION_OFFSET *
GridDefaults.DEFAULT_GRID_ROW_HEIGHT,
),
};
} else {
return {
...computed,
bottomRow: Math.max(
CANVAS_DEFAULT_MIN_HEIGHT_PX,
computed.bottomRow +
GridDefaults.VIEW_MODE_MAIN_CANVAS_EXTENSION_OFFSET *
GridDefaults.DEFAULT_GRID_ROW_HEIGHT,
),
};
}
}
return evaluatedWidget
? createCanvasWidget(widget, evaluatedWidget, evaluatedWidgetConfig)
: createLoadingWidget(widget);
})();
widgetProps = { ...canvasWidgetProps };
widgetProps.isMobile = !!isMobile;
widgetProps.selectedWidgetAncestry = selectedWidgetAncestry || [];
widgetProps.isWidgetSelected = isWidgetSelected;
/**
* 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
) {
const isListWidgetCanvas =
props.noPad && props.dropDisabled && props.openParentPropertyPane;
widgetProps.rightColumn = props.rightColumn;
if (isListWidgetCanvas) {
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;
}
if (defaultAutoLayoutWidgets.includes(props.type)) {
widgetProps.positioning = isAutoLayout
? Positioning.Vertical
: Positioning.Fixed;
}
widgetProps.children = children;
widgetProps.metaWidgetChildrenStructure = metaWidgetChildrenStructure;
widgetProps.isLoading = isLoading;
widgetProps.childWidgets = childWidgets;
widgetProps.flattenedChildCanvasWidgets = flattenedChildCanvasWidgets;
}
//merging with original props
widgetProps = { ...props, ...widgetProps, layoutSystemType, renderMode };
// adding google maps api key to widget props (although meant for map widget only)
widgetProps.googleMapsApiKey = googleMapsApiKey;
// isVisible prop defines whether to render a detached widget
if (
widgetProps.detachFromLayout &&
!widgetProps.isVisible &&
!selectedWidgetAncestry.includes(widgetProps.widgetId)
) {
return null;
}
const shouldCollapseWidgetInViewOrPreviewMode =
!widgetProps.isVisible &&
!selectedWidgetAncestry.includes(widgetProps.widgetId) &&
(renderMode === RenderModes.PAGE || isPreviewMode);
const shouldResetCollapsedContainerHeightInViewOrPreviewMode =
widgetProps.isVisible && widgetProps.topRow === widgetProps.bottomRow;
const shouldResetCollapsedContainerHeightInCanvasMode =
widgetProps.topRow === widgetProps.bottomRow &&
renderMode === RenderModes.CANVAS &&
!isPreviewMode;
widgetProps.mainCanvasWidth = mainCanvasWidth;
if (layoutSystemType === LayoutSystemTypes.ANVIL) {
if (shouldCollapseWidgetInViewOrPreviewMode) {
return null;
}
} else {
// We don't render invisible widgets in view mode
if (shouldCollapseWidgetInViewOrPreviewMode) {
// This flag (isMetaWidget) is used to prevent the Auto height saga from updating
// the List widget Child Widgets. Auto height is disabled in the List widget and
// this flag serves as a way to avoid any unintended changes to the child widget's height.
if (
widgetProps.bottomRow !== widgetProps.topRow &&
!widgetProps.isMetaWidget
) {
dispatch({
type: ReduxActionTypes.UPDATE_WIDGET_AUTO_HEIGHT,
payload: {
widgetId: props.widgetId,
height: 0,
},
});
}
return null;
} else if (
shouldResetCollapsedContainerHeightInViewOrPreviewMode ||
shouldResetCollapsedContainerHeightInCanvasMode
) {
// We also need to check if a non-auto height widget has collapsed earlier
// We can figure this out if the widget height is zero and the beforeCollapse
// topRow and bottomRow are available.
// If the above is true, we call an auto height update call
// so that the widget can be reset correctly.
if (
widgetProps.topRow === widgetProps.bottomRow &&
widgetProps.topRowBeforeCollapse !== undefined &&
widgetProps.bottomRowBeforeCollapse !== undefined &&
!isAutoHeightEnabledForWidget(widgetProps)
) {
const heightBeforeCollapse =
(widgetProps.bottomRowBeforeCollapse -
widgetProps.topRowBeforeCollapse) *
GridDefaults.DEFAULT_GRID_ROW_HEIGHT;
dispatch({
type: ReduxActionTypes.UPDATE_WIDGET_AUTO_HEIGHT,
payload: {
widgetId: props.widgetId,
height: heightBeforeCollapse,
},
});
} else {
dispatch(checkContainersForAutoHeightAction());
}
}
// Sets the min/max height/width of the widget
if (isAutoLayout) {
const minMaxDimensions = getWidgetMinMaxDimensionsInPixel(
widgetProps,
mainCanvasWidth,
);
widgetProps = {
...widgetProps,
minWidth: minMaxDimensions.minWidth
? minMaxDimensions.minWidth - 2 * WIDGET_PADDING
: undefined,
minHeight: minMaxDimensions.minHeight
? minMaxDimensions.minHeight - 2 * WIDGET_PADDING
: undefined,
maxWidth: minMaxDimensions.maxWidth
? minMaxDimensions.maxWidth - 2 * WIDGET_PADDING
: undefined,
maxHeight: minMaxDimensions.maxHeight
? minMaxDimensions.maxHeight - 2 * WIDGET_PADDING
: undefined,
};
}
}
return <WrappedWidget {...widgetProps} />;
}
return WrappedPropsComponent;
}
export default withWidgetProps;