diff --git a/app/client/package.json b/app/client/package.json index 4b2a4c80d3..b6ebc6207b 100644 --- a/app/client/package.json +++ b/app/client/package.json @@ -38,7 +38,9 @@ "netlify-identity-widget": "^1.5.5", "node-sass": "^4.11.0", "normalizr": "^3.3.0", + "popper.js": "^1.15.0", "prettier": "^1.18.2", + "re-reselect": "^3.4.0", "react": "^16.7.0", "react-dnd": "^9.3.4", "react-dnd-html5-backend": "^9.3.4", @@ -52,6 +54,7 @@ "react-scripts": "^3.1.1", "redux": "^4.0.1", "redux-saga": "^1.0.0", + "reselect": "^4.0.0", "styled-components": "^4.1.3", "ts-loader": "^6.0.4", "typescript": "^3.6.3" diff --git a/app/client/src/actions/configsActions.tsx b/app/client/src/actions/configsActions.tsx new file mode 100644 index 0000000000..8033b8a5b6 --- /dev/null +++ b/app/client/src/actions/configsActions.tsx @@ -0,0 +1,18 @@ +import { ReduxActionTypes } from "../constants/ReduxActionConstants"; + +export type EditorConfigIdsType = { + propertyPaneConfigsId?: string; + widgetCardsPaneId?: string; + widgetConfigsId?: string; +}; + +export const fetchEditorConfigs = (configsIds: EditorConfigIdsType) => { + return { + type: ReduxActionTypes.FETCH_CONFIGS_INIT, + payload: { + propertyPaneConfigsId: configsIds.propertyPaneConfigsId, + widgetCardsPaneId: configsIds.widgetCardsPaneId, + widgetConfigsId: configsIds.widgetConfigsId, + }, + }; +}; diff --git a/app/client/src/api/PropertPaneConfigsApi.tsx b/app/client/src/api/PropertPaneConfigsApi.tsx new file mode 100644 index 0000000000..850095c082 --- /dev/null +++ b/app/client/src/api/PropertPaneConfigsApi.tsx @@ -0,0 +1,26 @@ +import Api from "./Api"; +import { ApiResponse } from "./ApiResponses"; +import { PropertyConfig } from "../reducers/entityReducers/propertyPaneConfigReducer"; + +export interface PropertyPaneConfigsResponse extends ApiResponse { + data: { + config: PropertyConfig; + }; +} + +export interface PropertyPaneConfigsRequest { + propertyPaneConfigsId: string; +} + +class PropertyPaneConfigsApi extends Api { + static url = "v1/properties"; + static fetch( + request: PropertyPaneConfigsRequest, + ): Promise { + return Api.get( + PropertyPaneConfigsApi.url + "/" + request.propertyPaneConfigsId, + ); + } +} + +export default PropertyPaneConfigsApi; diff --git a/app/client/src/assets/icons/control/edit.svg b/app/client/src/assets/icons/control/edit.svg new file mode 100644 index 0000000000..f2ec080903 --- /dev/null +++ b/app/client/src/assets/icons/control/edit.svg @@ -0,0 +1,4 @@ + + + + diff --git a/app/client/src/common/PaneWrapper.tsx b/app/client/src/common/PaneWrapper.tsx new file mode 100644 index 0000000000..fd49685d31 --- /dev/null +++ b/app/client/src/common/PaneWrapper.tsx @@ -0,0 +1,10 @@ +import styled from "styled-components"; + +export default styled.div` + background-color: ${props => props.theme.colors.paneBG}; + border-radius: ${props => props.theme.radii[1]}px; + box-shadow: 0px 0px 3px ${props => props.theme.colors.paneBG}; + padding: 24px 16px; + color: ${props => props.theme.colors.textOnDarkBG}; + text-transform: capitalize; +`; diff --git a/app/client/src/constants/ApiConstants.tsx b/app/client/src/constants/ApiConstants.tsx index 594ffea494..408a280c9c 100644 --- a/app/client/src/constants/ApiConstants.tsx +++ b/app/client/src/constants/ApiConstants.tsx @@ -36,12 +36,14 @@ export const getEditorConfigs = () => { currentPageId: "5d807e7f795dc6000482bc78", currentLayoutId: "5d807e7f795dc6000482bc77", currentPageName: "page1", + propertyPaneConfigsId: "5d8a04195cf8050004db6e30", }; } else { return { currentPageId: "5d807e76795dc6000482bc76", currentLayoutId: "5d807e76795dc6000482bc75", currentPageName: "page1", + propertyPaneConfigsId: "5d8a04195cf8050004db6e30", }; } }; diff --git a/app/client/src/constants/DefaultTheme.tsx b/app/client/src/constants/DefaultTheme.tsx index 235f802393..35fe44eae4 100644 --- a/app/client/src/constants/DefaultTheme.tsx +++ b/app/client/src/constants/DefaultTheme.tsx @@ -18,6 +18,11 @@ export type ThemeBorder = { color: Color; }; +type PropertyPaneTheme = { + width: number; + height: number; +}; + export type Theme = { radii: Array; fontSizes: Array; @@ -27,6 +32,7 @@ export type Theme = { lineHeights: Array; fonts: Array; borders: ThemeBorder[]; + propertyPane: PropertyPaneTheme; headerHeight: string; sidebarWidth: string; }; @@ -52,6 +58,10 @@ export const theme: Theme = { fontSizes: [0, 10, 12, 14, 16, 18, 24, 28, 32, 48, 64], spaces: [0, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 30], fontWeights: [0, 400, 500, 700], + propertyPane: { + width: 250, + height: 600, + }, colors: { primary: Colors.GREEN, error: Colors.RED, diff --git a/app/client/src/constants/ReduxActionConstants.tsx b/app/client/src/constants/ReduxActionConstants.tsx index 85a7c744b2..997b210412 100644 --- a/app/client/src/constants/ReduxActionConstants.tsx +++ b/app/client/src/constants/ReduxActionConstants.tsx @@ -1,4 +1,5 @@ import { WidgetProps, WidgetCardProps } from "../widgets/BaseWidget"; +import { RefObject } from "react"; export const ReduxActionTypes: { [key: string]: string } = { REPORT_ERROR: "REPORT_ERROR", @@ -35,6 +36,10 @@ export const ReduxActionTypes: { [key: string]: string } = { WIDGET_DELETE: "WIDGET_DELETE", SHOW_PROPERTY_PANE: "SHOW_PROPERTY_PANE", UPDATE_WIDGET_PROPERTY: "UPDATE_WIDGET_PROPERTY", + FETCH_PROPERTY_PANE_CONFIGS_INIT: "FETCH_PROPERTY_PANE_CONFIGS_INIT", + FETCH_PROPERTY_PANE_CONFIGS_SUCCESS: "FETCH_PROPERTY_PANE_CONFIGS_SUCCESS", + FETCH_CONFIGS_INIT: "FETCH_CONFIGS_INIT", + ADD_WIDGET_REF: "ADD_WIDGET_REF", }; export type ReduxActionType = (typeof ReduxActionTypes)[keyof typeof ReduxActionTypes]; @@ -49,6 +54,9 @@ export const ReduxActionErrorTypes: { [key: string]: string } = { SAVE_PAGE_ERROR: "SAVE_PAGE_ERROR", FETCH_WIDGET_CARDS_ERROR: "FETCH_WIDGET_CARDS_ERROR", WIDGET_OPERATION_ERROR: "WIDGET_OPERATION_ERROR", + FETCH_PROPERTY_PANE_CONFIGS_ERROR: "FETCH_PROPERTY_PANE_CONFIGS_ERROR", + FETCH_CONFIGS_ERROR: "FETCH_CONFIGS_ERROR", + PROPERTY_PANE_ERROR: "PROPERTY_PANE_ERROR", }; export type ReduxActionErrorType = (typeof ReduxActionErrorTypes)[keyof typeof ReduxActionErrorTypes]; @@ -72,6 +80,8 @@ export interface UpdateCanvasPayload { export interface ShowPropertyPanePayload { widgetId: string; + node: RefObject; + toggle: boolean; } // export interface LoadAPIResponsePayload extends ExecuteActionResponse {} diff --git a/app/client/src/editorComponents/DraggableComponent.tsx b/app/client/src/editorComponents/DraggableComponent.tsx index 444fcb985d..bc52c84ec7 100644 --- a/app/client/src/editorComponents/DraggableComponent.tsx +++ b/app/client/src/editorComponents/DraggableComponent.tsx @@ -1,4 +1,10 @@ -import React, { useContext, createContext, Context } from "react"; +import React, { + useContext, + createContext, + useState, + Context, + useCallback, +} from "react"; import styled from "styled-components"; import { WidgetProps, WidgetOperations } from "../widgets/BaseWidget"; import { useDrag, DragPreviewImage, DragSourceMonitor } from "react-dnd"; @@ -34,6 +40,14 @@ const DragHandle = styled.div` `; const DeleteControl = styled.div` + position: absolute; + right: ${props => props.theme.fontSizes[CONTROL_THEME_FONTSIZE_INDEX]}px; + top: -${props => props.theme.fontSizes[CONTROL_THEME_FONTSIZE_INDEX] / 2}px; + display: none; + cursor: pointer; +`; + +const EditControl = styled.div` position: absolute; right: -${props => props.theme.fontSizes[CONTROL_THEME_FONTSIZE_INDEX] / 2}px; top: -${props => props.theme.fontSizes[CONTROL_THEME_FONTSIZE_INDEX] / 2}px; @@ -41,6 +55,15 @@ const DeleteControl = styled.div` cursor: pointer; `; +const DraggableMask = styled.div` + width: 100%; + height: 100%; + position: absolute; + left: 0; + top: 0; + z-index: -1; +`; + const moveControlIcon = ControlIcons.MOVE_CONTROL({ width: theme.fontSizes[CONTROL_THEME_FONTSIZE_INDEX], height: theme.fontSizes[CONTROL_THEME_FONTSIZE_INDEX], @@ -51,41 +74,84 @@ const deleteControlIcon = ControlIcons.DELETE_CONTROL({ height: theme.fontSizes[CONTROL_THEME_FONTSIZE_INDEX], }); +const editControlIcon = ControlIcons.EDIT_CONTROL({ + width: theme.fontSizes[CONTROL_THEME_FONTSIZE_INDEX], + height: theme.fontSizes[CONTROL_THEME_FONTSIZE_INDEX], +}); + type DraggableComponentProps = WidgetProps & ContainerProps; -export const DraggingContext: Context<{ +export const DraggableComponentContext: Context<{ isDragging?: boolean; + widgetNode?: HTMLDivElement; }> = createContext({}); +/* eslint-disable react/display-name */ + +//TODO(abhinav): the contexts and states are getting out of hand. +// Refactor here and in ResizableComponent const DraggableComponent = (props: DraggableComponentProps) => { - const { isFocused, setFocus } = useContext(FocusContext); + const { isFocused, setFocus, showPropertyPane } = useContext(FocusContext); const { updateWidget } = useContext(WidgetFunctionsContext); + const [currentNode, setCurrentNode] = useState(); + const referenceRef = useCallback( + node => { + if (node !== null && node !== currentNode) { + setCurrentNode(node); + } + }, + [setCurrentNode, currentNode], + ); const { isResizing } = useContext(ResizingContext); const deleteWidget = () => { + showPropertyPane && showPropertyPane(); updateWidget && updateWidget(WidgetOperations.DELETE, props.widgetId, { parentId: props.parentId, }); }; + + const togglePropertyEditor = (e: any) => { + if (showPropertyPane && props.widgetId && currentNode) { + showPropertyPane(props.widgetId, currentNode, true); + } + e.stopPropagation(); + }; + const [{ isDragging }, drag, preview] = useDrag({ item: props, collect: (monitor: DragSourceMonitor) => ({ isDragging: monitor.isDragging(), }), + end: (widget, monitor) => { + if (monitor.didDrop()) { + if (isFocused === props.widgetId && showPropertyPane && currentNode) { + showPropertyPane(props.widgetId, currentNode, true); + } + } + }, + begin: () => { + if (isFocused === props.widgetId && showPropertyPane && currentNode) { + showPropertyPane(props.widgetId, undefined); + } + }, canDrag: () => { return !isResizing && !!isFocused && isFocused === props.widgetId; }, }); return ( - + { - if (setFocus) { + if (setFocus && showPropertyPane) { setFocus(props.widgetId); + showPropertyPane(props.widgetId, currentNode); e.stopPropagation(); } }} @@ -106,6 +172,7 @@ const DraggableComponent = (props: DraggableComponentProps) => { props.style.componentHeight + (props.style.heightUnit || "px"), }} > + {props.children} {moveControlIcon} @@ -113,8 +180,11 @@ const DraggableComponent = (props: DraggableComponentProps) => { {deleteControlIcon} + + {editControlIcon} + - + ); }; diff --git a/app/client/src/editorComponents/ResizableComponent.tsx b/app/client/src/editorComponents/ResizableComponent.tsx index e6a206be20..c167a83bd6 100644 --- a/app/client/src/editorComponents/ResizableComponent.tsx +++ b/app/client/src/editorComponents/ResizableComponent.tsx @@ -7,7 +7,7 @@ import { OccupiedSpaceContext } from "../widgets/ContainerWidget"; import { ContainerProps, ParentBoundsContext } from "./ContainerComponent"; import { isDropZoneOccupied } from "../utils/WidgetPropsUtils"; import { FocusContext } from "../pages/Editor/Canvas"; -import { DraggingContext } from "./DraggableComponent"; +import { DraggableComponentContext } from "./DraggableComponent"; import { WidgetFunctionsContext } from "../pages/Editor/WidgetsEditor"; import { ResizingContext } from "./DropTargetComponent"; import { @@ -88,11 +88,11 @@ const ResizableContainer = styled(Rnd)` `; export const ResizableComponent = (props: ResizableComponentProps) => { - const { isDragging } = useContext(DraggingContext); + const { isDragging } = useContext(DraggableComponentContext); const { setIsResizing } = useContext(ResizingContext); const { boundingParent } = useContext(ParentBoundsContext); const { updateWidget } = useContext(WidgetFunctionsContext); - const { isFocused, setFocus } = useContext(FocusContext); + const { isFocused, setFocus, showPropertyPane } = useContext(FocusContext); const occupiedSpaces = useContext(OccupiedSpaceContext); const [isColliding, setIsColliding] = useState(false); @@ -199,6 +199,7 @@ export const ResizableComponent = (props: ResizableComponentProps) => { onResize={checkForCollision} onResizeStart={() => { setIsResizing && setIsResizing(true); + showPropertyPane && showPropertyPane(props.widgetId, undefined); }} resizeGrid={[props.parentColumnSpace, props.parentRowSpace]} bounds={bounds} diff --git a/app/client/src/icons/ControlIcons.tsx b/app/client/src/icons/ControlIcons.tsx index b69acedcd5..5e47a71724 100644 --- a/app/client/src/icons/ControlIcons.tsx +++ b/app/client/src/icons/ControlIcons.tsx @@ -2,6 +2,7 @@ import React from "react"; import { IconProps, IconWrapper } from "../constants/IconConstants"; import { ReactComponent as DeleteIcon } from "../assets/icons/control/delete.svg"; import { ReactComponent as MoveIcon } from "../assets/icons/control/move.svg"; +import { ReactComponent as EditIcon } from "../assets/icons/control/edit.svg"; /* eslint-disable react/display-name */ @@ -18,4 +19,9 @@ export const ControlIcons: { ), + EDIT_CONTROL: (props: IconProps) => ( + + + + ), }; diff --git a/app/client/src/mockResponses/WidgetSidebarResponse.tsx b/app/client/src/mockResponses/WidgetSidebarResponse.tsx index 7eefbbfb0c..9e2aa457bc 100644 --- a/app/client/src/mockResponses/WidgetSidebarResponse.tsx +++ b/app/client/src/mockResponses/WidgetSidebarResponse.tsx @@ -17,12 +17,6 @@ const WidgetSidebarResponse: { widgetCardName: "Button", key: generateReactKey(), }, - { - type: "SPINNER_WIDGET", - icon: "icon-switch", - widgetCardName: "Spinner", - key: generateReactKey(), - }, { type: "CONTAINER_WIDGET", icon: "icon-container", @@ -31,12 +25,6 @@ const WidgetSidebarResponse: { }, ], form: [ - { - type: "BUTTON_WIDGET", - icon: "icon-button", - widgetCardName: "Button", - key: generateReactKey(), - }, { type: "BUTTON_WIDGET", icon: "icon-button", @@ -64,7 +52,7 @@ const WidgetSidebarResponse: { { type: "SWITCH_WIDGET", icon: "icon-switch", - widgetCardName: "Toggle", + widgetCardName: "Switch", key: generateReactKey(), }, ], @@ -81,12 +69,6 @@ const WidgetSidebarResponse: { widgetCardName: "Container", key: generateReactKey(), }, - { - type: "SPINNER_WIDGET", - icon: "icon-spinner", - widgetCardName: "Spinner", - key: generateReactKey(), - }, { type: "TABLE_WIDGET", icon: "icon-table", diff --git a/app/client/src/pages/Editor/Canvas.tsx b/app/client/src/pages/Editor/Canvas.tsx index a5e9350d9a..3eab9a956b 100644 --- a/app/client/src/pages/Editor/Canvas.tsx +++ b/app/client/src/pages/Editor/Canvas.tsx @@ -1,15 +1,10 @@ -import React, { - createContext, - useState, - Context, - Dispatch, - SetStateAction, -} from "react"; +import React, { createContext, useState, Context } from "react"; import styled from "styled-components"; import WidgetFactory from "../../utils/WidgetFactory"; import { RenderModes } from "../../constants/WidgetConstants"; import { ContainerWidgetProps } from "../../widgets/ContainerWidget"; import { WidgetProps } from "../../widgets/BaseWidget"; +import PropertyPane from "./PropertyPane"; const ArtBoard = styled.div` width: 100%; @@ -20,17 +15,33 @@ const ArtBoard = styled.div` interface CanvasProps { dsl: ContainerWidgetProps; + showPropertyPane: ( + widgetId?: string, + node?: HTMLDivElement, + toggle?: boolean, + ) => void; } export const FocusContext: Context<{ isFocused?: string; - setFocus?: Dispatch>; + setFocus?: Function; + showPropertyPane?: ( + widgetId?: string, + node?: HTMLDivElement, + toggle?: boolean, + ) => void; }> = createContext({}); +/* eslint-disable react/display-name */ +/* eslint-disable react/prop-types */ + const Canvas = (props: CanvasProps) => { const [isFocused, setFocus] = useState(""); return ( - + + {props.dsl.widgetId && WidgetFactory.createWidget(props.dsl, RenderModes.CANVAS)} diff --git a/app/client/src/pages/Editor/Popper.tsx b/app/client/src/pages/Editor/Popper.tsx new file mode 100644 index 0000000000..442d50c6ba --- /dev/null +++ b/app/client/src/pages/Editor/Popper.tsx @@ -0,0 +1,52 @@ +import React, { useRef, useEffect } from "react"; +import styled from "styled-components"; +import { createPortal } from "react-dom"; +import PopperJS from "popper.js"; +import PaneWrapper from "../common/PaneWrapper"; + +type PopperProps = { + isOpen: boolean; + targetRefNode: HTMLDivElement; + children: JSX.Element; +}; + +const PopperWrapper = styled(PaneWrapper)` + position: absolute; + z-index: 1; + height: ${props => props.theme.propertyPane.height}px; + width: ${props => props.theme.propertyPane.width}px; + margin: ${props => props.theme.spaces[6]}px; +`; + +/* eslint-disable react/display-name */ +export default (props: PopperProps) => { + const contentRef = useRef(null); + useEffect(() => { + //TODO(abhinav): optimize this, remove previous Popper instance. + new PopperJS( + props.targetRefNode, + (contentRef.current as unknown) as Element, + { + placement: "right", + modifiers: { + flip: { + behavior: ["right", "left", "bottom", "top"], + }, + keepTogether: { + enabled: false, + }, + arrow: { + enabled: false, + }, + preventOverflow: { + boundariesElement: "viewport", + }, + }, + }, + ); + }, [props.targetRefNode]); + return createPortal( + {props.children}, + document.body, + ); +}; diff --git a/app/client/src/pages/Editor/PropertyPane.tsx b/app/client/src/pages/Editor/PropertyPane.tsx index 47120dd499..64212aa9cd 100644 --- a/app/client/src/pages/Editor/PropertyPane.tsx +++ b/app/client/src/pages/Editor/PropertyPane.tsx @@ -6,6 +6,14 @@ import _ from "lodash"; import { ControlProps } from "../../propertyControls/BaseControl"; import { PropertySection } from "../../reducers/entityReducers/propertyPaneConfigReducer"; import { updateWidgetProperty } from "../../actions/controlActions"; +import { + getCurrentWidgetId, + getCurrentReferenceNode, + getPropertyConfig, + getIsPropertyPaneVisible, +} from "../../selectors/propertyPaneSelectors"; + +import Popper from "./Popper"; class PropertyPane extends Component< PropertyPaneProps & PropertyPaneFunctions @@ -16,27 +24,38 @@ class PropertyPane extends Component< } render() { - if (this.props.isVisible) { + if ( + this.props.isVisible && + this.props.widgetId && + this.props.targetNode && + this.props.propertySections + ) { + const content = this.renderPropertyPane(this.props.propertySections); return ( -
- {!_.isNil(this.props.propertySections) - ? _.map( - this.props.propertySections, - (propertySection: PropertySection) => { - return this.renderPropertySection( - propertySection, - propertySection.id, - ); - }, - ) - : undefined} -
+ + {content} + ); } else { return null; } } + renderPropertyPane(propertySections?: PropertySection[]) { + return ( +
+ {!_.isNil(propertySections) + ? _.map(propertySections, (propertySection: PropertySection) => { + return this.renderPropertySection( + propertySection, + propertySection.id, + ); + }) + : undefined} +
+ ); + } + renderPropertySection(propertySection: PropertySection, key: string) { return (
@@ -61,10 +80,14 @@ class PropertyPane extends Component< propertyControlOrSection.id, ); } else { - return PropertyControlFactory.createControl( - propertyControlOrSection, - { onPropertyChange: this.onPropertyChange }, - ); + try { + return PropertyControlFactory.createControl( + propertyControlOrSection, + { onPropertyChange: this.onPropertyChange }, + ); + } catch (e) { + console.log(e); + } } }, )} @@ -74,24 +97,20 @@ class PropertyPane extends Component< } onPropertyChange(propertyName: string, propertyValue: any) { - this.props.updateWidgetProperty( - this.props.widgetId, - propertyName, - propertyValue, - ); + // this.props.updateWidgetProperty( + // this.props.widgetId, + // propertyName, + // propertyValue, + // ); } } const mapStateToProps = (state: AppState): PropertyPaneProps => { - let propertyConfig = undefined; - if (!_.isNil(state.ui.propertyPane.widgetId)) { - const widget = state.entities.canvasWidgets[state.ui.propertyPane.widgetId]; - propertyConfig = state.entities.propertyConfig.config[widget.type]; - } return { - propertySections: propertyConfig, - widgetId: state.ui.propertyPane.widgetId, - isVisible: state.ui.propertyPane.isVisible, + propertySections: getPropertyConfig(state), + widgetId: getCurrentWidgetId(state), + isVisible: getIsPropertyPaneVisible(state), + targetNode: getCurrentReferenceNode(state), }; }; @@ -109,6 +128,7 @@ export interface PropertyPaneProps { propertySections?: PropertySection[]; widgetId?: string; isVisible: boolean; + targetNode?: HTMLDivElement; } export interface PropertyPaneFunctions { diff --git a/app/client/src/pages/Editor/WidgetCard.tsx b/app/client/src/pages/Editor/WidgetCard.tsx index ee1b8a6dec..8900bdb952 100644 --- a/app/client/src/pages/Editor/WidgetCard.tsx +++ b/app/client/src/pages/Editor/WidgetCard.tsx @@ -15,6 +15,7 @@ export const Wrapper = styled.div` background: ${props => props.theme.colors.paneCard}; border: 1px solid ${props => props.theme.colors.paneCard}; color: ${props => props.theme.colors.textOnDarkBG}; + height: 80px; & > div { display: flex; flex-direction: column; diff --git a/app/client/src/pages/Editor/WidgetCardsPane.tsx b/app/client/src/pages/Editor/WidgetCardsPane.tsx new file mode 100644 index 0000000000..9dd346cbdf --- /dev/null +++ b/app/client/src/pages/Editor/WidgetCardsPane.tsx @@ -0,0 +1,41 @@ +import React from "react"; +import WidgetCard from "./WidgetCard"; +import styled from "styled-components"; +import { WidgetCardProps } from "../../widgets/BaseWidget"; +import PaneWrapper from "../common/PaneWrapper"; + +type WidgetCardPaneProps = { + cards?: { [id: string]: WidgetCardProps[] }; +}; + +const CardsWrapper = styled.div` + display: grid; + grid-template-columns: 1fr 1fr 1fr; + grid-gap: ${props => props.theme.spaces[1]}px; + justify-items: stretch; + align-items: stretch; +`; + +const WidgetCardsPane = (props: WidgetCardPaneProps) => { + if (!props.cards) { + return null; + } + const groups = Object.keys(props.cards); + return ( + + {groups.map((group: string) => ( + +
{group}
+ + {props.cards && + props.cards[group].map((card: WidgetCardProps) => ( + + ))} + +
+ ))} +
+ ); +}; + +export default WidgetCardsPane; diff --git a/app/client/src/pages/Editor/WidgetSidebar.tsx b/app/client/src/pages/Editor/WidgetSidebar.tsx index edc4693399..dc8f1b4e0a 100644 --- a/app/client/src/pages/Editor/WidgetSidebar.tsx +++ b/app/client/src/pages/Editor/WidgetSidebar.tsx @@ -4,7 +4,7 @@ import WidgetCard from "./WidgetCard"; import styled from "styled-components"; import { WidgetCardProps } from "../../widgets/BaseWidget"; import { AppState } from "../../reducers"; -import { WidgetSidebarReduxState } from "../../reducers/uiReducers/widgetSidebarReducer"; +import { getWidgetCards } from "../../selectors/editorSelectors"; type WidgetSidebarProps = { cards: { [id: string]: WidgetCardProps[] }; @@ -22,39 +22,33 @@ const CardsWrapper = styled.div` align-items: stretch; `; -const WidgetSidebar: React.FC = ( - props: WidgetSidebarProps, -) => { - const groups = Object.keys(props.cards); - return ( - - {groups.map((group: string) => ( - -
{group}
- - {props.cards[group].map((card: WidgetCardProps) => ( - - ))} - -
- ))} -
- ); +class WidgetSidebar extends React.Component { + render(): React.ReactNode { + const groups = Object.keys(this.props.cards); + return ( + + {groups.map((group: string) => ( + +
{group}
+ + {this.props.cards[group].map((card: WidgetCardProps) => ( + + ))} + +
+ ))} +
+ ); + } +} + +const mapStateToProps = (state: AppState) => { + return { + cards: getWidgetCards(state), + }; }; export default connect( - (state: AppState): WidgetSidebarReduxState => { - // TODO(hetu) Should utilise reselect instead - const cards = state.ui.widgetSidebar.cards; - const groups: string[] = Object.keys(cards); - groups.forEach((group: string) => { - cards[group] = cards[group].map((widget: WidgetCardProps) => { - const { rows, columns } = state.entities.widgetConfig.config[ - widget.type - ]; - return { ...widget, rows, columns }; - }); - }); - return { cards }; - }, + mapStateToProps, + null, )(WidgetSidebar); diff --git a/app/client/src/pages/Editor/WidgetsEditor.tsx b/app/client/src/pages/Editor/WidgetsEditor.tsx index b51864794b..c9cc3b8a1d 100644 --- a/app/client/src/pages/Editor/WidgetsEditor.tsx +++ b/app/client/src/pages/Editor/WidgetsEditor.tsx @@ -4,8 +4,6 @@ import styled from "styled-components"; import Canvas from "./Canvas"; import PropertyPane from "./PropertyPane"; import { AppState } from "../../reducers"; -import { EditorReduxState } from "../../reducers/uiReducers/editorReducer"; -import CanvasWidgetsNormalizer from "../../normalizers/CanvasWidgetsNormalizer"; import { WidgetFunctions, WidgetOperation, @@ -14,8 +12,21 @@ import { import { ActionPayload } from "../../constants/ActionConstants"; import { executeAction } from "../../actions/widgetActions"; import { fetchPage, savePage, updateWidget } from "../../actions/pageActions"; +import { + getPropertyPaneConfigsId, + getCurrentLayoutId, + getCurrentPageId, + getDenormalizedDSL, + getCurrentPageName, + getPageWidgetId, +} from "../../selectors/editorSelectors"; import { RenderModes } from "../../constants/WidgetConstants"; import { ContainerWidgetProps } from "../../widgets/ContainerWidget"; +import { + EditorConfigIdsType, + fetchEditorConfigs, +} from "../../actions/configsActions"; +import { ReduxActionTypes } from "../../constants/ReduxActionConstants"; const CanvasContainer = styled.section` height: 100%; @@ -52,7 +63,13 @@ type EditorProps = { currentPageName: string; currentPageId: string; currentLayoutId: string; - isSaving: boolean; + showPropertyPane: ( + widgetId?: string, + node?: HTMLDivElement, + toggle?: boolean, + ) => void; + fetchConfigs: Function; + propertyPaneConfigsId: string; }; export const WidgetFunctionsContext: Context = createContext( @@ -61,6 +78,11 @@ export const WidgetFunctionsContext: Context = createContext( class WidgetsEditor extends React.Component { componentDidMount() { + this.props.fetchConfigs({ + propertyPaneConfigsId: this.props.propertyPaneConfigsId, + // widgetCardsPaneId: this.props.widgetCardsPaneId, + // widgetConfigsId: this.props.widgetConfigsId, + }); this.props.fetchCanvasWidgets(this.props.currentPageId); } @@ -74,7 +96,12 @@ class WidgetsEditor extends React.Component { > - {this.props.dsl && } + {this.props.dsl && ( + + )} @@ -83,26 +110,14 @@ class WidgetsEditor extends React.Component { } } -const mapStateToProps = (state: AppState): EditorReduxState => { - // TODO(abhinav) : Benchmark this, see how many times this is called in the application - // lifecycle. Move to using flattend redux state for widgets if necessary. - - // Also, try to merge the widgetCards and widgetConfigs in the fetch Saga. - // No point in storing widgetCards, without widgetConfig - // Alternatively, try to see if we can continue to use only WidgetConfig and eliminate WidgetCards - - const dsl = CanvasWidgetsNormalizer.denormalize( - state.ui.editor.pageWidgetId, - state.entities, - ); - +const mapStateToProps = (state: AppState) => { return { - dsl, - pageWidgetId: state.ui.editor.pageWidgetId, - currentPageId: state.ui.editor.currentPageId, - currentLayoutId: state.ui.editor.currentLayoutId, - currentPageName: state.ui.editor.currentPageName, - isSaving: state.ui.editor.isSaving, + dsl: getDenormalizedDSL(state), + pageWidgetId: getPageWidgetId(state), + currentPageId: getCurrentPageId(state), + currentLayoutId: getCurrentLayoutId(state), + currentPageName: getCurrentPageName(state), + propertyPaneConfigsId: getPropertyPaneConfigsId(state), }; }; @@ -122,6 +137,18 @@ const mapDispatchToProps = (dispatch: any) => { layoutId: string, dsl: ContainerWidgetProps, ) => dispatch(savePage(pageId, layoutId, dsl)), + fetchConfigs: (configsIds: EditorConfigIdsType) => + dispatch(fetchEditorConfigs(configsIds)), + showPropertyPane: ( + widgetId?: string, + node?: HTMLDivElement, + toggle = false, + ) => { + dispatch({ + type: ReduxActionTypes.SHOW_PROPERTY_PANE, + payload: { widgetId, node, toggle }, + }); + }, }; }; diff --git a/app/client/src/pages/common/PaneWrapper.tsx b/app/client/src/pages/common/PaneWrapper.tsx new file mode 100644 index 0000000000..419be2697c --- /dev/null +++ b/app/client/src/pages/common/PaneWrapper.tsx @@ -0,0 +1,10 @@ +import styled from "styled-components"; + +export default styled.div` + background-color: ${props => props.theme.colors.paneBG}; + border-radius: ${props => props.theme.radii[2]}px; + box-shadow: 0px 0px 3px ${props => props.theme.colors.paneBG}; + padding: 5px 10px; + color: ${props => props.theme.colors.textOnDarkBG}; + text-transform: capitalize; +`; diff --git a/app/client/src/reducers/entityReducers/index.tsx b/app/client/src/reducers/entityReducers/index.tsx index 6656e0badf..f83444dc09 100644 --- a/app/client/src/reducers/entityReducers/index.tsx +++ b/app/client/src/reducers/entityReducers/index.tsx @@ -4,6 +4,7 @@ import apiDataReducer from "./apiDataReducer"; import queryDataReducer from "./queryDataReducer"; import widgetConfigReducer from "./widgetConfigReducer"; import actionsReducer from "./actionsReducer"; +import propertyPaneConfigReducer from "./propertyPaneConfigReducer"; const entityReducer = combineReducers({ canvasWidgets: canvasWidgetsReducer, @@ -11,5 +12,6 @@ const entityReducer = combineReducers({ queryData: queryDataReducer, widgetConfig: widgetConfigReducer, actions: actionsReducer, + propertyConfig: propertyPaneConfigReducer, }); export default entityReducer; diff --git a/app/client/src/reducers/entityReducers/propertyPaneConfigReducer.tsx b/app/client/src/reducers/entityReducers/propertyPaneConfigReducer.tsx index da8d6fdfc8..0134e71861 100644 --- a/app/client/src/reducers/entityReducers/propertyPaneConfigReducer.tsx +++ b/app/client/src/reducers/entityReducers/propertyPaneConfigReducer.tsx @@ -45,8 +45,8 @@ export interface PropertyPaneConfigState { configVersion: number; } -const widgetConfigReducer = createReducer(initialState, { - [ReduxActionTypes.LOAD_PROPERTY_CONFIG]: ( +const propertyPaneConfigReducer = createReducer(initialState, { + [ReduxActionTypes.FETCH_PROPERTY_PANE_CONFIGS_SUCCESS]: ( state: PropertyPaneConfigState, action: ReduxAction, ) => { @@ -54,4 +54,4 @@ const widgetConfigReducer = createReducer(initialState, { }, }); -export default widgetConfigReducer; +export default propertyPaneConfigReducer; diff --git a/app/client/src/reducers/uiReducers/editorReducer.tsx b/app/client/src/reducers/uiReducers/editorReducer.tsx index 07dc533d1a..2db0844974 100644 --- a/app/client/src/reducers/uiReducers/editorReducer.tsx +++ b/app/client/src/reducers/uiReducers/editorReducer.tsx @@ -26,6 +26,7 @@ export interface EditorReduxState { currentPageId: string; currentLayoutId: string; currentPageName: string; + propertyPaneConfigsId: string; isSaving: boolean; } diff --git a/app/client/src/reducers/uiReducers/propertyPaneReducer.tsx b/app/client/src/reducers/uiReducers/propertyPaneReducer.tsx index 41bf3e1406..c73ac52e8a 100644 --- a/app/client/src/reducers/uiReducers/propertyPaneReducer.tsx +++ b/app/client/src/reducers/uiReducers/propertyPaneReducer.tsx @@ -7,6 +7,8 @@ import { const initialState: PropertyPaneReduxState = { isVisible: false, + widgetId: undefined, + node: undefined, }; const propertyPaneReducer = createReducer(initialState, { @@ -14,13 +16,19 @@ const propertyPaneReducer = createReducer(initialState, { state: PropertyPaneReduxState, action: ReduxAction, ) => { - return { widgetId: action.payload }; + let isVisible = true; + const { widgetId, node, toggle } = action.payload; + if (toggle) { + isVisible = !state.isVisible; + } + return { widgetId, node, isVisible }; }, }); export interface PropertyPaneReduxState { widgetId?: string; isVisible: boolean; + node?: HTMLDivElement; } export default propertyPaneReducer; diff --git a/app/client/src/sagas/ConfigsSagas.tsx b/app/client/src/sagas/ConfigsSagas.tsx new file mode 100644 index 0000000000..a3f791b594 --- /dev/null +++ b/app/client/src/sagas/ConfigsSagas.tsx @@ -0,0 +1,75 @@ +import { all, call, put, takeLatest } from "redux-saga/effects"; +import { + ReduxAction, + ReduxActionTypes, + ReduxActionErrorTypes, +} from "../constants/ReduxActionConstants"; + +import PropertyPaneConfigsApi, { + PropertyPaneConfigsResponse, + PropertyPaneConfigsRequest, +} from "../api/PropertPaneConfigsApi"; + +import { EditorConfigIdsType } from "../actions/configsActions"; + +import { validateResponse } from "./ErrorSagas"; + +export function* fetchPropertyPaneConfigsSaga(propertyPaneConfigsId: string) { + const request: PropertyPaneConfigsRequest = { propertyPaneConfigsId }; + try { + const response: PropertyPaneConfigsResponse = yield call( + PropertyPaneConfigsApi.fetch, + request, + ); + const isValidResponse = yield validateResponse(response); + if (isValidResponse) { + yield put({ + type: ReduxActionTypes.FETCH_PROPERTY_PANE_CONFIGS_SUCCESS, + payload: { + config: response.data.config, + }, + }); + } + } catch (error) { + yield put({ + type: ReduxActionErrorTypes.FETCH_PROPERTY_PANE_CONFIGS_ERROR, + payload: { + error, + }, + }); + } +} + +export function* configsSaga(configsIds: ReduxAction) { + const { + propertyPaneConfigsId, + widgetCardsPaneId, + widgetConfigsId, + } = configsIds.payload; + try { + const sagasToCall = []; + if (propertyPaneConfigsId) { + sagasToCall.push( + call(fetchPropertyPaneConfigsSaga, propertyPaneConfigsId), + ); + } + if (widgetCardsPaneId) { + // sagasToCall.push(call(fetchWidgetCardsConfigsSaga, widgetCardsPaneId)); + } + if (widgetConfigsId) { + // sagasToCall.push(call(fetchWidgetConfigsSaga, widgetConfigsId)); + } + yield all(sagasToCall); + } catch (error) { + yield put({ + type: ReduxActionErrorTypes.FETCH_CONFIGS_ERROR, + payload: { + error, + }, + }); + } +} + +export default function* configsSagas() { + yield takeLatest(ReduxActionTypes.FETCH_CONFIGS_INIT, configsSaga); +} diff --git a/app/client/src/sagas/ErrorSagas.tsx b/app/client/src/sagas/ErrorSagas.tsx index 4f66de37a6..6741b80f6e 100644 --- a/app/client/src/sagas/ErrorSagas.tsx +++ b/app/client/src/sagas/ErrorSagas.tsx @@ -28,18 +28,23 @@ export function* validateResponse(response: ApiResponse) { } type ErrorPayloadType = object | { message: string }; - -const ActionErrorDisplayMap: { +let ActionErrorDisplayMap: { [key: string]: (error: ErrorPayloadType) => string; -} = { +} = {}; + +Object.keys(ReduxActionErrorTypes).forEach((type: string) => { + ActionErrorDisplayMap[type] = () => + DEFAULT_ERROR_MESSAGE + " action: " + type; +}); + +ActionErrorDisplayMap = { + ...ActionErrorDisplayMap, [ReduxActionErrorTypes.API_ERROR]: error => _.get(error, "message", DEFAULT_ERROR_MESSAGE), [ReduxActionErrorTypes.FETCH_PAGE_ERROR]: () => DEFAULT_ACTION_ERROR("fetching the page"), [ReduxActionErrorTypes.SAVE_PAGE_ERROR]: () => DEFAULT_ACTION_ERROR("saving the page"), - [ReduxActionErrorTypes.FETCH_WIDGET_CARDS_ERROR]: () => DEFAULT_ERROR_MESSAGE, - [ReduxActionErrorTypes.WIDGET_OPERATION_ERROR]: () => DEFAULT_ERROR_MESSAGE, }; export function* errorSaga( diff --git a/app/client/src/sagas/index.tsx b/app/client/src/sagas/index.tsx index e20cb128ae..2ca8d32cc6 100644 --- a/app/client/src/sagas/index.tsx +++ b/app/client/src/sagas/index.tsx @@ -4,7 +4,7 @@ import { fetchWidgetCardsSaga } from "./WidgetSidebarSagas"; import { watchExecuteActionSaga } from "./ActionSagas"; import widgetOperationSagas from "./WidgetOperationSagas"; import errorSagas from "./ErrorSagas"; - +import configsSagas from "./ConfigsSagas"; export function* rootSaga() { yield all([ spawn(pageSagas), @@ -12,5 +12,6 @@ export function* rootSaga() { spawn(watchExecuteActionSaga), spawn(widgetOperationSagas), spawn(errorSagas), + spawn(configsSagas), ]); } diff --git a/app/client/src/selectors/editorSelectors.tsx b/app/client/src/selectors/editorSelectors.tsx new file mode 100644 index 0000000000..7ff90a64b7 --- /dev/null +++ b/app/client/src/selectors/editorSelectors.tsx @@ -0,0 +1,78 @@ +import { createSelector } from "reselect"; +import createCachedSelector from "re-reselect"; + +import { AppState } from "../reducers"; +import { EditorReduxState } from "../reducers/uiReducers/editorReducer"; +import { WidgetConfigReducerState } from "../reducers/entityReducers/widgetConfigReducer"; +import { WidgetCardProps } from "../widgets/BaseWidget"; +import { WidgetSidebarReduxState } from "../reducers/uiReducers/widgetSidebarReducer"; +import CanvasWidgetsNormalizer from "../normalizers/CanvasWidgetsNormalizer"; + +const getEditorState = (state: AppState) => state.ui.editor; +const getWidgetConfigs = (state: AppState) => state.entities.widgetConfig; +const getEntities = (state: AppState) => state.entities; +const getWidgetSideBar = (state: AppState) => state.ui.widgetSidebar; + +export const getPropertyPaneConfigsId = createSelector( + getEditorState, + (editor: EditorReduxState) => editor.propertyPaneConfigsId, +); + +export const getCurrentPageId = createSelector( + getEditorState, + (editor: EditorReduxState) => editor.currentPageId, +); + +export const getCurrentLayoutId = createSelector( + getEditorState, + (editor: EditorReduxState) => editor.currentLayoutId, +); + +export const getPageWidgetId = createSelector( + getEditorState, + (editor: EditorReduxState) => editor.pageWidgetId, +); + +export const getCurrentPageName = createSelector( + getEditorState, + (editor: EditorReduxState) => editor.currentPageName, +); + +export const getIsPageSaving = createSelector( + getEditorState, + (editor: EditorReduxState) => editor.isSaving, +); + +export const getWidgetCards = createSelector( + getWidgetSideBar, + getWidgetConfigs, + ( + widgetCards: WidgetSidebarReduxState, + widgetConfigs: WidgetConfigReducerState, + ) => { + const cards = widgetCards.cards; + const groups: string[] = Object.keys(cards); + groups.forEach((group: string) => { + cards[group] = cards[group].map((widget: WidgetCardProps) => { + const { rows, columns } = widgetConfigs.config[widget.type]; + return { ...widget, rows, columns }; + }); + }); + return cards; + }, +); + +// TODO(abhinav) : Benchmark this, see how many times this is called in the application +// lifecycle. Move to using flattend redux state for widgets if necessary. + +// Also, try to merge the widgetCards and widgetConfigs in the fetch Saga. +// No point in storing widgetCards, without widgetConfig +// Alternatively, try to see if we can continue to use only WidgetConfig and eliminate WidgetCards + +export const getDenormalizedDSL = createCachedSelector( + getPageWidgetId, + getEntities, + (pageWidgetId: string, entities: any) => { + return CanvasWidgetsNormalizer.denormalize(pageWidgetId, entities); + }, +)((pageWidgetId, entities) => entities || 0); diff --git a/app/client/src/selectors/propertyPaneSelectors.tsx b/app/client/src/selectors/propertyPaneSelectors.tsx new file mode 100644 index 0000000000..a20bcdd867 --- /dev/null +++ b/app/client/src/selectors/propertyPaneSelectors.tsx @@ -0,0 +1,47 @@ +import { createSelector } from "reselect"; +import { AppState } from "../reducers"; +import { PropertyPaneReduxState } from "../reducers/uiReducers/propertyPaneReducer"; +import { PropertyPaneConfigState } from "../reducers/entityReducers/propertyPaneConfigReducer"; +import { CanvasWidgetsReduxState } from "../reducers/entityReducers/canvasWidgetsReducer"; + +const getPropertyPaneState = (state: AppState): PropertyPaneReduxState => + state.ui.propertyPane; + +const getPropertyPaneConfig = (state: AppState): PropertyPaneConfigState => + state.entities.propertyConfig; + +const getCanvasWidgets = (state: AppState): CanvasWidgetsReduxState => + state.entities.canvasWidgets; + +export const getCurrentWidgetId = createSelector( + getPropertyPaneState, + (propertyPane: PropertyPaneReduxState) => propertyPane.widgetId, +); + +export const getCurrentReferenceNode = createSelector( + getPropertyPaneState, + (pane: PropertyPaneReduxState) => { + return pane.widgetId && pane.node ? pane.node : undefined; + }, +); + +export const getPropertyConfig = createSelector( + getPropertyPaneConfig, + getPropertyPaneState, + getCanvasWidgets, + ( + configs: PropertyPaneConfigState, + pane: PropertyPaneReduxState, + widgets: CanvasWidgetsReduxState, + ) => { + if (pane.widgetId && configs && widgets[pane.widgetId]) { + return configs.config[widgets[pane.widgetId].type]; + } + return undefined; + }, +); + +export const getIsPropertyPaneVisible = createSelector( + getPropertyPaneState, + (pane: PropertyPaneReduxState) => pane.isVisible, +); diff --git a/app/client/src/utils/PropertyControlFactory.tsx b/app/client/src/utils/PropertyControlFactory.tsx index 0e764a4145..6c0ae4c075 100644 --- a/app/client/src/utils/PropertyControlFactory.tsx +++ b/app/client/src/utils/PropertyControlFactory.tsx @@ -32,7 +32,7 @@ class PropertyControlFactory { } else { const ex: ControlCreationException = { message: - "Control Builder not registered for control type" + + "Control Builder not registered for control type " + controlData.controlType, }; throw ex; diff --git a/app/client/src/utils/WidgetPropsUtils.tsx b/app/client/src/utils/WidgetPropsUtils.tsx index 7ed2423934..c1574f2407 100644 --- a/app/client/src/utils/WidgetPropsUtils.tsx +++ b/app/client/src/utils/WidgetPropsUtils.tsx @@ -252,11 +252,11 @@ export const generateWidgetProps = ( ...widgetConfig, type, widgetId: generateReactKey(), - widgetName: widgetName || generateReactKey(), //TODO: figure out what this is to populate appropriately + widgetName: widgetName, isVisible: true, parentColumnSpace, parentRowSpace, - renderMode: RenderModes.CANVAS, //Is this required? + renderMode: RenderModes.CANVAS, ...sizes, ...others, backgroundColor: Colors.WHITE, diff --git a/app/client/yarn.lock b/app/client/yarn.lock index 197cfdc4a4..1f8104a633 100644 --- a/app/client/yarn.lock +++ b/app/client/yarn.lock @@ -9531,6 +9531,11 @@ rc@^1.2.7: minimist "^1.2.0" strip-json-comments "~2.0.1" +re-reselect@^3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/re-reselect/-/re-reselect-3.4.0.tgz#0f2303f3c84394f57f0cd31fea08a1ca4840a7cd" + integrity sha512-JsecfN+JlckncVXTWFWjn0Vk6uInl8GSf4eEd9tTk5qXHlgqkPdILpnYpgZcISXNYAzvfvsCZviaDk8AxyS5sg== + re-resizable@6.1.0: version "6.1.0" resolved "https://registry.yarnpkg.com/re-resizable/-/re-resizable-6.1.0.tgz#ba4ece505b48f05691446d57837151349d7575e8" @@ -10132,6 +10137,11 @@ requires-port@^1.0.0: resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8= +reselect@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.0.0.tgz#f2529830e5d3d0e021408b246a206ef4ea4437f7" + integrity sha512-qUgANli03jjAyGlnbYVAV5vvnOmJnODyABz51RdBN7M4WaVu8mecZWgyQNkG8Yqe3KRGRt0l4K4B3XVEULC4CA== + resize-observer-polyfill@^1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464"