Merge branch 'feature/property-pane-position' into 'release'

Property pane positioning

See merge request theappsmith/internal-tools-client!60
This commit is contained in:
Abhinav Jha 2019-10-21 11:40:24 +00:00
commit 618ede4ffb
32 changed files with 662 additions and 137 deletions

View File

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

View File

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

View File

@ -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<PropertyPaneConfigsResponse> {
return Api.get(
PropertyPaneConfigsApi.url + "/" + request.propertyPaneConfigsId,
);
}
}
export default PropertyPaneConfigsApi;

View File

@ -0,0 +1,4 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="10" cy="10" r="10" fill="#21282C"/>
<path d="M5.5 12.625V14.5H7.375L12.905 8.97L11.03 7.095L5.5 12.625ZM14.355 7.52C14.4014 7.47374 14.4381 7.4188 14.4632 7.35831C14.4883 7.29783 14.5012 7.23299 14.5012 7.1675C14.5012 7.10202 14.4883 7.03718 14.4632 6.97669C14.4381 6.9162 14.4014 6.86126 14.355 6.815L13.185 5.645C13.1387 5.59865 13.0838 5.56188 13.0233 5.53679C12.9628 5.51169 12.898 5.49878 12.8325 5.49878C12.767 5.49878 12.7022 5.51169 12.6417 5.53679C12.5812 5.56188 12.5263 5.59865 12.48 5.645L11.565 6.56L13.44 8.435L14.355 7.52Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 669 B

View File

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

View File

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

View File

@ -18,6 +18,11 @@ export type ThemeBorder = {
color: Color;
};
type PropertyPaneTheme = {
width: number;
height: number;
};
export type Theme = {
radii: Array<number>;
fontSizes: Array<number>;
@ -27,6 +32,7 @@ export type Theme = {
lineHeights: Array<number>;
fonts: Array<FontFamily>;
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,

View File

@ -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<HTMLDivElement>;
toggle: boolean;
}
// export interface LoadAPIResponsePayload extends ExecuteActionResponse {}

View File

@ -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<HTMLDivElement>();
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 (
<DraggingContext.Provider value={{ isDragging }}>
<DraggableComponentContext.Provider
value={{ isDragging, widgetNode: currentNode }}
>
<DragPreviewImage src={blankImage} connect={preview} />
<DraggableWrapper
ref={drag}
onClick={(e: any) => {
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"),
}}
>
<DraggableMask ref={referenceRef} />
{props.children}
<DragHandle className="control" ref={drag}>
{moveControlIcon}
@ -113,8 +180,11 @@ const DraggableComponent = (props: DraggableComponentProps) => {
<DeleteControl className="control" onClick={deleteWidget}>
{deleteControlIcon}
</DeleteControl>
<EditControl className="control" onClick={togglePropertyEditor}>
{editControlIcon}
</EditControl>
</DraggableWrapper>
</DraggingContext.Provider>
</DraggableComponentContext.Provider>
);
};

View File

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

View File

@ -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: {
<MoveIcon />
</IconWrapper>
),
EDIT_CONTROL: (props: IconProps) => (
<IconWrapper {...props}>
<EditIcon />
</IconWrapper>
),
};

View File

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

View File

@ -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<WidgetProps>;
showPropertyPane: (
widgetId?: string,
node?: HTMLDivElement,
toggle?: boolean,
) => void;
}
export const FocusContext: Context<{
isFocused?: string;
setFocus?: Dispatch<SetStateAction<string>>;
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 (
<FocusContext.Provider value={{ isFocused, setFocus }}>
<FocusContext.Provider
value={{ isFocused, setFocus, showPropertyPane: props.showPropertyPane }}
>
<PropertyPane />
<ArtBoard>
{props.dsl.widgetId &&
WidgetFactory.createWidget(props.dsl, RenderModes.CANVAS)}

View File

@ -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(
<PopperWrapper ref={contentRef}>{props.children}</PopperWrapper>,
document.body,
);
};

View File

@ -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 (
<div>
{!_.isNil(this.props.propertySections)
? _.map(
this.props.propertySections,
(propertySection: PropertySection) => {
return this.renderPropertySection(
propertySection,
propertySection.id,
);
},
)
: undefined}
</div>
<Popper isOpen={true} targetRefNode={this.props.targetNode}>
{content}
</Popper>
);
} else {
return null;
}
}
renderPropertyPane(propertySections?: PropertySection[]) {
return (
<div>
{!_.isNil(propertySections)
? _.map(propertySections, (propertySection: PropertySection) => {
return this.renderPropertySection(
propertySection,
propertySection.id,
);
})
: undefined}
</div>
);
}
renderPropertySection(propertySection: PropertySection, key: string) {
return (
<div key={key}>
@ -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 {

View File

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

View File

@ -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 (
<PaneWrapper>
{groups.map((group: string) => (
<React.Fragment key={group}>
<h5>{group}</h5>
<CardsWrapper>
{props.cards &&
props.cards[group].map((card: WidgetCardProps) => (
<WidgetCard details={card} key={card.key} />
))}
</CardsWrapper>
</React.Fragment>
))}
</PaneWrapper>
);
};
export default WidgetCardsPane;

View File

@ -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<WidgetSidebarProps> = (
props: WidgetSidebarProps,
) => {
const groups = Object.keys(props.cards);
return (
<MainWrapper>
{groups.map((group: string) => (
<React.Fragment key={group}>
<h5>{group}</h5>
<CardsWrapper>
{props.cards[group].map((card: WidgetCardProps) => (
<WidgetCard details={card} key={card.key} />
))}
</CardsWrapper>
</React.Fragment>
))}
</MainWrapper>
);
class WidgetSidebar extends React.Component<WidgetSidebarProps> {
render(): React.ReactNode {
const groups = Object.keys(this.props.cards);
return (
<MainWrapper>
{groups.map((group: string) => (
<React.Fragment key={group}>
<h5>{group}</h5>
<CardsWrapper>
{this.props.cards[group].map((card: WidgetCardProps) => (
<WidgetCard details={card} key={card.key} />
))}
</CardsWrapper>
</React.Fragment>
))}
</MainWrapper>
);
}
}
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);

View File

@ -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<WidgetFunctions> = createContext(
@ -61,6 +78,11 @@ export const WidgetFunctionsContext: Context<WidgetFunctions> = createContext(
class WidgetsEditor extends React.Component<EditorProps> {
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<EditorProps> {
>
<EditorWrapper>
<CanvasContainer>
{this.props.dsl && <Canvas dsl={this.props.dsl} />}
{this.props.dsl && (
<Canvas
dsl={this.props.dsl}
showPropertyPane={this.props.showPropertyPane}
/>
)}
</CanvasContainer>
<PropertyPane />
</EditorWrapper>
@ -83,26 +110,14 @@ class WidgetsEditor extends React.Component<EditorProps> {
}
}
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<WidgetProps>,
) => 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 },
});
},
};
};

View File

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

View File

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

View File

@ -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<PropertyPaneConfigState>,
) => {
@ -54,4 +54,4 @@ const widgetConfigReducer = createReducer(initialState, {
},
});
export default widgetConfigReducer;
export default propertyPaneConfigReducer;

View File

@ -26,6 +26,7 @@ export interface EditorReduxState {
currentPageId: string;
currentLayoutId: string;
currentPageName: string;
propertyPaneConfigsId: string;
isSaving: boolean;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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