PromucFlow_constructor/app/client/src/widgets/BaseWidget.tsx

381 lines
11 KiB
TypeScript
Raw Normal View History

/**
* Widget are responsible for accepting the abstraction layer inputs, interpretting them into rederable props and
* spawing components based on those props
* Widgets are also responsible for dispatching actions and updating the state tree
*/
import {
WidgetType,
RenderMode,
2019-03-19 14:05:48 +00:00
RenderModes,
2019-09-09 09:08:54 +00:00
CSSUnits,
2019-11-25 05:07:27 +00:00
} from "constants/WidgetConstants";
import React, { Component, ReactNode } from "react";
2020-01-16 11:46:21 +00:00
import {
PositionType,
CSSUnit,
CONTAINER_GRID_PADDING,
} from "constants/WidgetConstants";
2019-09-09 09:08:54 +00:00
import _ from "lodash";
import DraggableComponent from "components/editorComponents/DraggableComponent";
import ResizableComponent from "components/editorComponents/ResizableComponent";
2020-02-18 10:41:52 +00:00
import { ExecuteActionPayload } from "constants/ActionConstants";
import PositionedContainer from "components/designSystems/appsmith/PositionedContainer";
import WidgetNameComponent from "components/editorComponents/WidgetNameComponent";
2019-11-04 14:22:50 +00:00
import shallowequal from "shallowequal";
import { EditorContext } from "components/editorComponents/EditorContextProvider";
import { PositionTypes } from "constants/WidgetConstants";
2019-11-20 08:10:01 +00:00
2019-11-25 05:07:27 +00:00
import ErrorBoundary from "components/editorComponents/ErrorBoundry";
2020-03-16 07:59:07 +00:00
import {
BASE_WIDGET_VALIDATION,
WidgetPropertyValidationType,
} from "utils/ValidationFactory";
2020-02-18 10:41:52 +00:00
import {
DerivedPropertiesMap,
TriggerPropertiesMap,
} from "utils/WidgetFactory";
import { clearPropertyCache } from "utils/DynamicBindingUtils";
2020-03-06 09:45:21 +00:00
/***
* BaseWidget
*
* The abstract class which is extended/implemented by all widgets.
* Widgets must adhere to the abstractions provided by BaseWidget.
*
* Do not:
* 1) Use the context directly in the widgets
* 2) Update or access the dsl in the widgets
* 3) Call actions in widgets or connect the widgets to the entity reducers
*
*/
abstract class BaseWidget<
T extends WidgetProps,
2019-09-09 09:08:54 +00:00
K extends WidgetState
> extends Component<T, K> {
static contextType = EditorContext;
2019-11-19 12:44:58 +00:00
// Needed to send a default no validation option. In case a widget needs
// validation implement this in the widget class again
static getPropertyValidationMap(): WidgetPropertyValidationType {
2020-03-16 07:59:07 +00:00
return BASE_WIDGET_VALIDATION;
2019-11-19 12:44:58 +00:00
}
2020-01-17 09:28:26 +00:00
static getDerivedPropertiesMap(): DerivedPropertiesMap {
return {};
}
2020-02-18 10:41:52 +00:00
static getTriggerPropertyMap(): TriggerPropertiesMap {
return {};
2020-04-17 16:15:09 +00:00
}
static getDefaultPropertiesMap(): Record<string, string> {
return {};
}
static getMetaPropertiesMap(): Record<string, any> {
2020-04-17 16:15:09 +00:00
return {};
2020-02-18 10:41:52 +00:00
}
/**
* Widget abstraction to register the widget type
* ```javascript
* getWidgetType() {
* return "MY_AWESOME_WIDGET",
* }
* ```
*/
abstract getWidgetType(): WidgetType;
/**
* Widgets can execute actions using this `executeAction` method.
* Triggers may be specific to the widget
*/
2020-02-18 10:41:52 +00:00
executeAction(actionPayload: ExecuteActionPayload): void {
const { executeAction } = this.context;
2020-02-18 10:41:52 +00:00
executeAction && executeAction(actionPayload);
}
disableDrag(disable: boolean) {
const { disableDrag } = this.context;
disableDrag && disable !== undefined && disableDrag(disable);
}
2020-04-15 11:42:11 +00:00
updateWidget(
operationName: string,
widgetId: string,
widgetProperties: any,
): void {
const { updateWidget } = this.context;
updateWidget && updateWidget(operationName, widgetId, widgetProperties);
}
updateWidgetProperty(propertyName: string, propertyValue: any): void {
2019-11-07 11:17:53 +00:00
const { updateWidgetProperty } = this.context;
const { widgetId } = this.props;
2019-11-07 11:17:53 +00:00
updateWidgetProperty &&
updateWidgetProperty(widgetId, propertyName, propertyValue);
}
updateWidgetMetaProperty(propertyName: string, propertyValue: any): void {
2020-02-07 02:32:52 +00:00
const { updateWidgetMetaProperty } = this.context;
const { widgetId } = this.props;
// Whenever this value updates, we need to clear cache to handle correct evaluation
clearPropertyCache(`${this.props.widgetName}.${propertyName}`);
2020-02-07 02:32:52 +00:00
updateWidgetMetaProperty &&
updateWidgetMetaProperty(widgetId, propertyName, propertyValue);
}
2020-03-06 09:45:21 +00:00
resetChildrenMetaProperty(widgetId: string) {
const { resetChildrenMetaProperty } = this.context;
resetChildrenMetaProperty(widgetId);
}
/* eslint-disable @typescript-eslint/no-empty-function */
componentDidUpdate(prevProps: T) {}
componentDidMount(): void {}
/* eslint-enable @typescript-eslint/no-empty-function */
getComponentDimensions = () => {
return this.calculateWidgetBounds(
this.props.rightColumn,
this.props.leftColumn,
this.props.topRow,
this.props.bottomRow,
this.props.parentColumnSpace,
2019-09-09 09:08:54 +00:00
this.props.parentRowSpace,
);
};
calculateWidgetBounds(
rightColumn: number,
leftColumn: number,
topRow: number,
bottomRow: number,
parentColumnSpace: number,
2019-09-09 09:08:54 +00:00
parentRowSpace: number,
): {
componentWidth: number;
componentHeight: number;
} {
return {
componentWidth: (rightColumn - leftColumn) * parentColumnSpace,
componentHeight: (bottomRow - topRow) * parentRowSpace,
2019-09-09 09:08:54 +00:00
};
}
render() {
return this.getWidgetView();
}
makeResizable(content: ReactNode) {
return (
<ResizableComponent
{...this.props}
paddingOffset={PositionedContainer.padding}
>
{content}
</ResizableComponent>
);
}
showWidgetName(content: ReactNode, showControls = false) {
return (
<React.Fragment>
<WidgetNameComponent
widgetName={this.props.widgetName}
widgetId={this.props.widgetId}
parentId={this.props.parentId}
type={this.props.type}
showControls={showControls}
/>
{content}
</React.Fragment>
);
}
makeDraggable(content: ReactNode) {
return <DraggableComponent {...this.props}>{content}</DraggableComponent>;
}
makePositioned(content: ReactNode) {
const style = this.getPositionStyle();
return (
<PositionedContainer
widgetId={this.props.widgetId}
widgetType={this.props.type}
style={style}
>
{content}
</PositionedContainer>
);
}
addErrorBoundary(content: ReactNode, isValid: boolean) {
return <ErrorBoundary isValid={isValid}>{content}</ErrorBoundary>;
}
private getWidgetView(): ReactNode {
let content: ReactNode;
switch (this.props.renderMode) {
case RenderModes.CANVAS:
content = this.getCanvasView();
if (!this.props.detachFromLayout) {
content = this.makeResizable(content);
content = this.showWidgetName(content);
content = this.makeDraggable(content);
content = this.makePositioned(content);
}
return content;
// return this.getCanvasView();
case RenderModes.PAGE:
content = this.getPageView();
if (this.props.isVisible) {
content = this.addErrorBoundary(content, true);
if (!this.props.detachFromLayout) {
content = this.makePositioned(content);
}
return content;
}
return <React.Fragment />;
default:
throw Error("RenderMode not defined");
}
}
abstract getPageView(): ReactNode;
getCanvasView(): ReactNode {
let isValid = true;
if (this.props.invalidProps) {
isValid = _.keys(this.props.invalidProps).length === 0;
}
if (this.props.isLoading) isValid = true;
const content = this.getPageView();
return this.addErrorBoundary(content, isValid);
}
// TODO(abhinav): Maybe make this a pure component to bailout from updating altogether.
// This would involve making all widgets which have "states" to not have states,
// as they're extending this one.
2020-04-13 08:24:13 +00:00
shouldComponentUpdate(nextProps: WidgetProps, nextState: WidgetState) {
return (
!shallowequal(nextProps, this.props) ||
!shallowequal(nextState, this.state)
);
2019-11-04 14:22:50 +00:00
}
private getPositionStyle(): BaseStyle {
const { componentHeight, componentWidth } = this.getComponentDimensions();
2019-03-19 14:05:48 +00:00
return {
positionType: PositionTypes.ABSOLUTE,
componentHeight,
componentWidth,
2020-01-16 11:46:21 +00:00
yPosition:
this.props.topRow * this.props.parentRowSpace + CONTAINER_GRID_PADDING,
xPosition:
this.props.leftColumn * this.props.parentColumnSpace +
CONTAINER_GRID_PADDING,
2019-03-19 14:05:48 +00:00
xPositionUnit: CSSUnits.PIXEL,
2019-09-09 09:08:54 +00:00
yPositionUnit: CSSUnits.PIXEL,
};
2019-03-19 14:05:48 +00:00
}
// TODO(abhinav): These defaultProps seem unneccessary. Check it out.
static defaultProps: Partial<WidgetProps> | undefined = {
parentRowSpace: 1,
parentColumnSpace: 1,
topRow: 0,
2019-09-09 09:08:54 +00:00
leftColumn: 0,
};
}
export interface BaseStyle {
componentHeight: number;
componentWidth: number;
positionType: PositionType;
xPosition: number;
yPosition: number;
xPositionUnit: CSSUnit;
yPositionUnit: CSSUnit;
heightUnit?: CSSUnit;
widthUnit?: CSSUnit;
}
export type WidgetState = {};
2020-04-13 08:24:13 +00:00
export interface WidgetBuilder<T extends WidgetProps, S extends WidgetState> {
buildWidget(widgetProps: T): JSX.Element;
}
export interface WidgetBaseProps {
2019-09-12 08:11:25 +00:00
widgetId: string;
type: WidgetType;
2019-09-12 08:11:25 +00:00
widgetName: string;
parentId: string;
renderMode: RenderMode;
}
export type WidgetRowCols = {
2019-08-29 11:22:09 +00:00
leftColumn: number;
rightColumn: number;
topRow: number;
bottomRow: number;
minHeight?: number; // Required to reduce the size of CanvasWidgets.
};
export interface WidgetPositionProps extends WidgetRowCols {
2019-08-29 11:22:09 +00:00
parentColumnSpace: number;
parentRowSpace: number;
// The detachFromLayout flag tells use about the following properties when enabled
// 1) Widget does not drag/resize
// 2) Widget CAN (but not neccessarily) be a dropTarget
// Examples: MainContainer is detached from layout,
// MODAL_WIDGET is also detached from layout.
detachFromLayout?: boolean;
}
export interface WidgetDisplayProps {
//TODO(abhinav): Some of these props are mandatory
isVisible?: boolean;
2019-11-20 08:10:01 +00:00
isLoading: boolean;
isDisabled?: boolean;
backgroundColor?: string;
}
export interface WidgetDataProps
extends WidgetBaseProps,
WidgetPositionProps,
WidgetDisplayProps {}
export interface WidgetProps extends WidgetDataProps {
key?: string;
dynamicBindings?: Record<string, true>;
dynamicTriggers?: Record<string, true>;
dynamicProperties?: Record<string, true>;
invalidProps?: Record<string, boolean>;
validationMessages?: Record<string, string>;
2020-06-04 13:49:22 +00:00
evaluatedValues?: Record<string, any>;
isDefaultClickDisabled?: boolean;
[key: string]: any;
}
export interface WidgetCardProps {
type: WidgetType;
2019-08-29 11:22:09 +00:00
key?: string;
2019-09-26 11:37:09 +00:00
widgetCardName: string;
2019-08-21 12:49:16 +00:00
}
export const WidgetOperations = {
MOVE: "MOVE",
RESIZE: "RESIZE",
ADD_CHILD: "ADD_CHILD",
REMOVE_CHILD: "REMOVE_CHILD",
UPDATE_PROPERTY: "UPDATE_PROPERTY",
DELETE: "DELETE",
};
export type WidgetOperation = typeof WidgetOperations[keyof typeof WidgetOperations];
2019-09-09 09:08:54 +00:00
export default BaseWidget;