Property Pane Controls

- Fixes #121, #122, #123, #124, #90, #46, #65, #100, #101, #68, #102
This commit is contained in:
Abhinav Jha 2019-10-24 05:24:45 +00:00
parent b27dbaa470
commit 99ce65c756
28 changed files with 1297 additions and 1068 deletions

View File

@ -13,9 +13,7 @@
"@blueprintjs/select": "^3.10.0",
"@blueprintjs/table": "^3.7.1",
"@sentry/browser": "^5.6.3",
"@types/axios": "^0.14.0",
"@types/fontfaceobserver": "^0.0.6",
"@types/jest": "^24.0.18",
"@types/lodash": "^4.14.120",
"@types/moment-timezone": "^0.5.10",
"@types/nanoid": "^2.0.0",
@ -71,7 +69,7 @@
"eject": "react-scripts eject",
"flow": "flow"
},
"resolutions": {
"resolution": {
"jest": "24.8.0"
},
"eslintConfig": {

View File

@ -1,5 +1,5 @@
import React from "react";
import { Button, IButtonProps, MaybeElement } from "@blueprintjs/core";
import { AnchorButton, IButtonProps, MaybeElement } from "@blueprintjs/core";
import styled, { css } from "styled-components";
import { Container } from "../../editorComponents/ContainerComponent";
import { TextComponentProps } from "./TextViewComponent";
@ -15,7 +15,7 @@ const ButtonColorStyles = css<ButtonStyleProps>`
}};
`;
const ButtonWrapper = styled(Button)<ButtonStyleProps>`
const ButtonWrapper = styled(AnchorButton)<ButtonStyleProps>`
&& {
${ButtonColorStyles};
width: 100%;
@ -64,9 +64,10 @@ const ButtonWrapper = styled(Button)<ButtonStyleProps>`
}
}
`;
export type ButtonStyleName = "primary" | "secondary" | "error";
type ButtonStyleProps = {
styleName?: "primary" | "secondary" | "error";
styleName?: ButtonStyleName;
filled?: boolean;
};
@ -77,6 +78,7 @@ export const BaseButton = (props: IButtonProps & ButtonStyleProps) => {
BaseButton.defaultProps = {
styleName: "secondary",
disabled: false,
text: "Button Text",
minimal: true,
};
@ -84,13 +86,20 @@ BaseButton.defaultProps = {
interface ButtonContainerProps extends TextComponentProps {
icon?: MaybeElement;
onClick?: (event: React.MouseEvent<HTMLElement>) => void;
disabled?: boolean;
}
// To be used with the canvas
const ButtonContainer = (props: ButtonContainerProps) => {
const ButtonContainer = (props: ButtonContainerProps & ButtonStyleProps) => {
return (
<Container {...props}>
<BaseButton icon={props.icon} text={props.text} onClick={props.onClick} />
<BaseButton
styleName={props.styleName}
icon={props.icon}
text={props.text}
onClick={props.onClick}
disabled={props.disabled}
/>
</Container>
);
};

View File

@ -8,6 +8,7 @@ export const Colors: Record<string, string> = {
CONCRETE: "#F3F3F3",
MYSTIC: "#E1E8ED",
AQUA_HAZE: "#EEF2F5",
GRAY_CHATEAU: "#A2A6A8",
BLACK: "#000000",
BLACK_PEARL: "#040627",
@ -27,6 +28,7 @@ export const Colors: Record<string, string> = {
PURPLE: "#6871EF",
OXFORD_BLUE: "#2E3D49",
FRENCH_PASS: "#BBE8FE",
CADET_BLUE: "#A3B3BF",
};
export type Color = (typeof Colors)[keyof typeof Colors];

View File

@ -81,7 +81,10 @@ export const theme: Theme = {
textAnchor: Colors.PURPLE,
border: Colors.GEYSER,
paneCard: Colors.SHARK,
paneInputBG: Colors.SHARK,
paneBG: Colors.OUTER_SPACE,
paneText: Colors.GRAY_CHATEAU,
paneSectionLabel: Colors.CADET_BLUE,
navBG: Colors.DEEP_SPACE,
grid: Colors.GEYSER,
containerBorder: Colors.FRENCH_PASS,
@ -101,6 +104,11 @@ export const theme: Theme = {
style: "solid",
color: Colors.FRENCH_PASS,
},
{
thickness: "1px",
style: "solid",
color: Colors.GEYSER_LIGHT,
},
],
sidebarWidth: "300px",
headerHeight: "50px",

View File

@ -70,6 +70,7 @@ export const ReduxActionErrorTypes: { [key: string]: string } = {
FETCH_CONFIGS_ERROR: "FETCH_CONFIGS_ERROR",
PROPERTY_PANE_ERROR: "PROPERTY_PANE_ERROR",
FETCH_ACTIONS_ERROR: "FETCH_ACTIONS_ERROR",
UPDATE_WIDGET_PROPERTY_ERROR: "UPDATE_WIDGET_PROPERTY_ERROR",
FETCH_RESOURCES_ERROR: "FETCH_RESOURCES_ERROR",
CREATE_RESOURCE_ERROR: "CREATE_RESOURCE_ERROR",
};

View File

@ -18,6 +18,7 @@ export interface BaseStyle {
heightUnit?: CSSUnit;
widthUnit?: CSSUnit;
backgroundColor?: Color;
border?: string;
}
export interface ComponentProps {

View File

@ -1,9 +1,21 @@
import { ComponentProps } from "./BaseComponent";
import { ContainerOrientation } from "../constants/WidgetConstants";
import styled from "../constants/DefaultTheme";
import React, { createContext, Context, useRef } from "react";
import React, {
createContext,
Context,
useRef,
useContext,
forwardRef,
} from "react";
import { FocusContext } from "../pages/Editor/Canvas";
import { getBorderCSSShorthand } from "../constants/DefaultTheme";
export const Container = styled("div")<ContainerProps>`
type StyledContainerProps = ContainerProps & {
focus?: boolean;
};
export const StyledContainer = styled("div")<StyledContainerProps>`
display: flex;
flex-direction: ${props => {
return props.orientation === "HORIZONTAL" ? "row" : "column";
@ -19,26 +31,49 @@ export const Container = styled("div")<ContainerProps>`
width: 100%;
padding: ${props => props.theme.spaces[1]}px;
&:after {
content: "${props => props.widgetName}";
content: "${props => (props.focus ? props.widgetName : "")}";
position: absolute;
top: -${props => props.theme.spaces[8]}px;
font-size: ${props => props.theme.fontSizes[2]}px;
color: ${props => props.theme.colors.containerBorder};
text-align: left;
width: 100%;
}
`;
}`;
/* eslint-disable react/display-name */
/* eslint-disable react/prop-types */
type Ref = HTMLDivElement;
export const Container = forwardRef<Ref, ContainerProps>((props, ref) => {
const { isFocused } = useContext(FocusContext);
const focus = isFocused === props.widgetId;
return <StyledContainer ref={ref} {...props} focus={focus} />;
});
export const ParentBoundsContext: Context<{
boundingParent?: React.RefObject<HTMLDivElement>;
}> = createContext({});
type ContainerComponentWrapperProps = ContainerStyleProps & {
isRoot?: boolean;
};
const ContainerComponentWrapper = styled.div<ContainerComponentWrapperProps>`
/* TODO(abhinav)(Issue: #107): this will changed based on the ContainerStyleProps */
border: ${props =>
!props.isRoot && getBorderCSSShorthand(props.theme.borders[2])};
box-shadow: ${props =>
!props.isRoot ? "0px 2px 4px rgba(67, 70, 74, 0.14)" : "none"};
height: 100%;
width: 100%;
`;
const ContainerComponent = (props: ContainerProps) => {
const container = useRef(null);
return (
<ParentBoundsContext.Provider value={{ boundingParent: container }}>
<Container ref={container} {...props}>
{props.children}
<ContainerComponentWrapper isRoot={props.isRoot}>
{props.children}
</ContainerComponentWrapper>
</Container>
</ParentBoundsContext.Provider>
);
@ -50,4 +85,8 @@ export interface ContainerProps extends ComponentProps {
isRoot?: boolean;
}
type ContainerStyleProps = {
styleName?: "border" | "card" | "rounded-border";
};
export default ContainerComponent;

View File

@ -15,6 +15,7 @@ import { WidgetFunctionsContext } from "../pages/Editor/WidgetsEditor";
import { ControlIcons } from "../icons/ControlIcons";
import { theme } from "../constants/DefaultTheme";
import { ResizingContext } from "./DropTargetComponent";
import { Tooltip } from "@blueprintjs/core";
// FontSizes array in DefaultTheme.tsx
// Change this to toggle the size of delete and move handles.
@ -124,18 +125,18 @@ const DraggableComponent = (props: DraggableComponentProps) => {
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);
}
},
end: (widget, monitor) => {
if (monitor.didDrop()) {
if (isFocused === props.widgetId && showPropertyPane && currentNode) {
showPropertyPane(props.widgetId, currentNode);
}
}
},
canDrag: () => {
return !isResizing && !!isFocused && isFocused === props.widgetId;
},
@ -175,13 +176,19 @@ const DraggableComponent = (props: DraggableComponentProps) => {
<DraggableMask ref={referenceRef} />
{props.children}
<DragHandle className="control" ref={drag}>
{moveControlIcon}
<Tooltip content="Move" hoverOpenDelay={500}>
{moveControlIcon}
</Tooltip>
</DragHandle>
<DeleteControl className="control" onClick={deleteWidget}>
{deleteControlIcon}
<Tooltip content="Delete" hoverOpenDelay={500}>
{deleteControlIcon}
</Tooltip>
</DeleteControl>
<EditControl className="control" onClick={togglePropertyEditor}>
{editControlIcon}
<Tooltip content="Toggle properties pane" hoverOpenDelay={500}>
{editControlIcon}
</Tooltip>
</EditControl>
</DraggableWrapper>
</DraggableComponentContext.Provider>

View File

@ -26,7 +26,7 @@ type DropTargetBounds = {
};
export const ResizingContext: Context<{
isResizing?: boolean;
isResizing?: boolean | string;
setIsResizing?: Function;
}> = createContext({});
@ -36,8 +36,7 @@ export const DropTargetComponent = (props: DropTargetComponentProps) => {
const [isResizing, setIsResizing] = useState(false);
const { updateWidget } = useContext(WidgetFunctionsContext);
const occupiedSpaces = useContext(OccupiedSpaceContext);
const { setFocus } = useContext(FocusContext);
const { setFocus, showPropertyPane } = useContext(FocusContext);
// Make this component a drop target
const [{ isOver, isExactlyOver }, drop] = useDrop({
accept: Object.values(WidgetFactory.getWidgetTypes()),
@ -99,6 +98,7 @@ export const DropTargetComponent = (props: DropTargetComponentProps) => {
const handleFocus = () => {
if (props.isRoot) {
setFocus && setFocus(props.widgetId);
showPropertyPane && showPropertyPane();
}
};

View File

@ -72,7 +72,8 @@ const ResizableContainer = styled(Rnd)`
width: ${props => props.theme.spaces[2]}px;
height: ${props => props.theme.spaces[2]}px;
border-radius: ${props => props.theme.radii[5]}%;
background: ${props => props.theme.colors.containerBorder};
background: ${props =>
props.isfocused && props.theme.colors.containerBorder};
}
&:after {
right: -${props => props.theme.spaces[1]}px;
@ -88,11 +89,11 @@ const ResizableContainer = styled(Rnd)`
`;
export const ResizableComponent = (props: ResizableComponentProps) => {
const { isDragging } = useContext(DraggableComponentContext);
const { isDragging, widgetNode } = useContext(DraggableComponentContext);
const { setIsResizing } = useContext(ResizingContext);
const { boundingParent } = useContext(ParentBoundsContext);
const { updateWidget } = useContext(WidgetFunctionsContext);
const { isFocused, setFocus, showPropertyPane } = useContext(FocusContext);
const { showPropertyPane, isFocused, setFocus } = useContext(FocusContext);
const occupiedSpaces = useContext(OccupiedSpaceContext);
const [isColliding, setIsColliding] = useState(false);
@ -145,6 +146,7 @@ export const ResizableComponent = (props: ResizableComponentProps) => {
) => {
setIsResizing && setIsResizing(false);
setFocus && setFocus(props.widgetId);
showPropertyPane && showPropertyPane(props.widgetId, widgetNode);
const leftColumn = props.leftColumn + position.x / props.parentColumnSpace;
const topRow = props.topRow + position.y / props.parentRowSpace;
@ -174,6 +176,7 @@ export const ResizableComponent = (props: ResizableComponentProps) => {
const canResize = !isDragging && isFocused === props.widgetId;
return (
<ResizableContainer
isfocused={isFocused === props.widgetId ? "true" : undefined}
position={{
x: 0,
y: 0,
@ -193,13 +196,14 @@ export const ResizableComponent = (props: ResizableComponentProps) => {
border:
isFocused === props.widgetId
? getBorderCSSShorthand(theme.borders[1])
: getBorderCSSShorthand(theme.borders[0]),
: "none",
boxSizing: "content-box",
}}
onResizeStop={updateSize}
onResize={checkForCollision}
onResizeStart={() => {
setIsResizing && setIsResizing(true);
showPropertyPane && showPropertyPane(props.widgetId, undefined);
showPropertyPane && showPropertyPane(props.widgetId);
}}
resizeGrid={[props.parentColumnSpace, props.parentRowSpace]}
bounds={bounds}

View File

@ -8,6 +8,8 @@ const WidgetConfigResponse: WidgetConfigReducerState = {
rows: 1,
columns: 2,
widgetName: "Button",
isDisabled: false,
isVisible: true,
},
TEXT_WIDGET: {
text: "Not all labels are bad!",

View File

@ -13,7 +13,7 @@ type PopperProps = {
const PopperWrapper = styled(PaneWrapper)`
position: absolute;
z-index: 1;
height: ${props => props.theme.propertyPane.height}px;
max-height: ${props => props.theme.propertyPane.height}px;
width: ${props => props.theme.propertyPane.width}px;
margin: ${props => props.theme.spaces[6]}px;
`;
@ -23,27 +23,27 @@ 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",
const parentElement = props.targetRefNode.parentElement;
if (parentElement && parentElement.parentElement) {
new PopperJS(
props.targetRefNode,
(contentRef.current as unknown) as Element,
{
placement: "right-start",
modifiers: {
flip: {
behavior: ["right", "left", "bottom", "top"],
},
keepTogether: {
enabled: false,
},
arrow: {
enabled: false,
},
},
},
},
);
);
}
}, [props.targetRefNode]);
return createPortal(
<PopperWrapper ref={contentRef}>{props.children}</PopperWrapper>,

View File

@ -1,4 +1,5 @@
import React, { Component } from "react";
import styled from "styled-components";
import { connect } from "react-redux";
import { AppState } from "../../reducers";
import PropertyControlFactory from "../../utils/PropertyControlFactory";
@ -11,10 +12,21 @@ import {
getCurrentReferenceNode,
getPropertyConfig,
getIsPropertyPaneVisible,
getCurrentWidgetProperties,
} from "../../selectors/propertyPaneSelectors";
import Popper from "./Popper";
const PropertySectionLabel = styled.div`
text-transform: uppercase;
color: ${props => props.theme.colors.paneSectionLabel};
padding: ${props => props.theme.spaces[5]}px 0;
font-size: ${props => props.theme.fontSizes[2]}px;
display: flex;
justify-content: flex-start;
align-items: center;
`;
class PropertyPane extends Component<
PropertyPaneProps & PropertyPaneFunctions
> {
@ -24,12 +36,7 @@ class PropertyPane extends Component<
}
render() {
if (
this.props.isVisible &&
this.props.widgetId &&
this.props.targetNode &&
this.props.propertySections
) {
if (this.props.isVisible && this.props.widgetId && this.props.targetNode) {
const content = this.renderPropertyPane(this.props.propertySections);
return (
<Popper isOpen={true} targetRefNode={this.props.targetNode}>
@ -48,7 +55,7 @@ class PropertyPane extends Component<
? _.map(propertySections, (propertySection: PropertySection) => {
return this.renderPropertySection(
propertySection,
propertySection.id,
this.props.widgetId + propertySection.id,
);
})
: undefined}
@ -60,7 +67,9 @@ class PropertyPane extends Component<
return (
<div key={key}>
{!_.isNil(propertySection) ? (
<div>{propertySection.sectionName}</div>
<PropertySectionLabel>
{propertySection.sectionName}
</PropertySectionLabel>
) : (
undefined
)}
@ -81,6 +90,9 @@ class PropertyPane extends Component<
);
} else {
try {
propertyControlOrSection.propertyValue = this.props.widgetProperties[
propertyControlOrSection.propertyName
];
return PropertyControlFactory.createControl(
propertyControlOrSection,
{ onPropertyChange: this.onPropertyChange },
@ -97,11 +109,11 @@ 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,
);
}
}
@ -109,6 +121,7 @@ const mapStateToProps = (state: AppState): PropertyPaneProps => {
return {
propertySections: getPropertyConfig(state),
widgetId: getCurrentWidgetId(state),
widgetProperties: getCurrentWidgetProperties(state),
isVisible: getIsPropertyPaneVisible(state),
targetNode: getCurrentReferenceNode(state),
};
@ -127,6 +140,7 @@ const mapDispatchToProps = (dispatch: any): PropertyPaneFunctions => {
export interface PropertyPaneProps {
propertySections?: PropertySection[];
widgetId?: string;
widgetProperties?: any; //TODO(abhinav): Secure type definition
isVisible: boolean;
targetNode?: HTMLDivElement;
}

View File

@ -4,7 +4,8 @@ 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;
padding: ${props => props.theme.spaces[5]}px
${props => props.theme.spaces[7]}px;
color: ${props => props.theme.colors.textOnDarkBG};
text-transform: capitalize;
`;

View File

@ -28,6 +28,7 @@ export interface ControlData {
label: string;
propertyName: string;
controlType: ControlType;
propertyValue?: any;
}
export interface ControlFunctions {

View File

@ -2,7 +2,8 @@ import React, { SyntheticEvent } from "react";
import BaseControl, { ControlProps } from "./BaseControl";
import { ControlType } from "../constants/PropertyControlConstants";
import { Button, MenuItem } from "@blueprintjs/core";
import { Select, IItemRendererProps } from "@blueprintjs/select";
import { IItemRendererProps } from "@blueprintjs/select";
import { ControlWrapper, StyledDropDown } from "./StyledControls";
class DropDownControl extends BaseControl<DropDownControlProps> {
constructor(props: DropDownControlProps) {
@ -11,20 +12,25 @@ class DropDownControl extends BaseControl<DropDownControlProps> {
}
render() {
const DropDown = Select.ofType<DropdownOption>();
const selected: DropdownOption | undefined = this.props.options.find(
option => option.value === this.props.propertyValue,
);
return (
<DropDown
items={this.props.options}
itemPredicate={this.filterOption}
itemRenderer={this.renderItem}
onItemSelect={this.onItemSelect}
noResults={<MenuItem disabled={true} text="No results." />}
>
<Button
text={this.props.options[0].label}
rightIcon="double-caret-vertical"
/>
</DropDown>
<ControlWrapper>
<label>{this.props.label}</label>
<StyledDropDown
items={this.props.options}
itemPredicate={this.filterOption}
itemRenderer={this.renderItem}
onItemSelect={this.onItemSelect}
noResults={<MenuItem disabled={true} text="No results." />}
>
<Button
text={selected ? selected.label : ""}
rightIcon="chevron-down"
/>
</StyledDropDown>
</ControlWrapper>
);
}

View File

@ -1,17 +1,25 @@
import React from "react";
import BaseControl, { ControlProps } from "./BaseControl";
import { ControlType } from "../constants/PropertyControlConstants";
import { InputGroup } from "@blueprintjs/core";
import { InputType } from "../widgets/InputWidget";
import { ControlWrapper, StyledInputGroup } from "./StyledControls";
class InputTextControl extends BaseControl<InputControlProps> {
render() {
return <InputGroup onChange={this.onTextChange} />;
return (
<ControlWrapper>
<label>{this.props.label}</label>
<StyledInputGroup
onChange={this.onTextChange}
defaultValue={this.props.propertyValue}
/>
</ControlWrapper>
);
}
onTextChange(event: React.ChangeEvent<HTMLInputElement>) {
onTextChange = (event: React.ChangeEvent<HTMLInputElement>) => {
this.updateProperty(this.props.propertyName, event.target.value);
}
};
getControlType(): ControlType {
return "INPUT_TEXT";

View File

@ -0,0 +1,35 @@
import styled from "styled-components";
import { Select } from "@blueprintjs/select";
import { Switch, InputGroup } from "@blueprintjs/core";
export const ControlWrapper = styled.div`
margin: ${props => props.theme.spaces[3]}px 0;
& > label {
display: block;
color: ${props => props.theme.colors.paneText};
margin-bottom: ${props => props.theme.spaces[1]}px;
font-size: ${props => props.theme.fontSizes[3]}px;
}
`;
const DropDown = Select.ofType<{ label: string; value: string }>();
export const StyledDropDown = styled(DropDown)`
&&& button {
background: ${props => props.theme.colors.paneInputBG};
color: ${props => props.theme.colors.textOnDarkBG};
box-shadow: none;
}
`;
export const StyledSwitch = styled(Switch)`
&&&&& input:checked ~ span {
background: ${props => props.theme.colors.primary};
}
`;
export const StyledInputGroup = styled(InputGroup)`
& > input {
color: ${props => props.theme.colors.textOnDarkBG};
background: ${props => props.theme.colors.paneInputBG};
}
`;

View File

@ -0,0 +1,31 @@
import React from "react";
import BaseControl, { ControlProps } from "./BaseControl";
import { ControlType } from "../constants/PropertyControlConstants";
import { ControlWrapper, StyledSwitch } from "./StyledControls";
class SwitchControl extends BaseControl<ControlProps> {
render() {
return (
<ControlWrapper>
<label>{this.props.label}</label>
<StyledSwitch
onChange={this.onToggle}
defaultChecked={this.props.propertyValue}
large
/>
</ControlWrapper>
);
}
onToggle = () => {
this.updateProperty(this.props.propertyName, !this.props.propertyValue);
};
getControlType(): ControlType {
return "SWITCH";
}
}
export type SwitchControlProps = ControlProps;
export default SwitchControl;

View File

@ -33,7 +33,7 @@ const canvasWidgetsReducer = createReducer(initialState, {
) => {
const widget = state[action.payload.widgetId];
return {
state,
...state,
[action.payload.widgetId]: {
...widget,
[action.payload.propertyName]: action.payload.propertyValue,

View File

@ -13,7 +13,6 @@ const initialState: PropertyPaneConfigState = PropertyPaneConfigResponse;
export type ControlConfig =
| InputControlProps
| DropDownControlProps
| InputControlProps
| ControlProps;
export type SectionOrientation = "HORIZONTAL" | "VERTICAL";

View File

@ -18,6 +18,9 @@ const propertyPaneReducer = createReducer(initialState, {
) => {
let isVisible = true;
const { widgetId, node, toggle } = action.payload;
if (state.widgetId === action.payload.widgetId) {
isVisible = state.isVisible;
}
if (toggle) {
isVisible = !state.isVisible;
}

View File

@ -23,7 +23,7 @@ import {
} from "redux-saga/effects";
import { extractCurrentDSL } from "../utils/WidgetPropsUtils";
import { getEditorConfigs } from "./selectors";
import { getEditorConfigs, getWidgets } from "./selectors";
import { validateResponse } from "./ErrorSagas";
export function* fetchPageSaga(
@ -86,6 +86,22 @@ export function* savePageSaga(savePageAction: ReduxAction<SavePageRequest>) {
}
}
function getLayoutSavePayload(
widgets: {
[widgetId: string]: FlattenedWidgetProps;
},
editorConfigs: any,
) {
const denormalizedDSL = CanvasWidgetsNormalizer.denormalize(
Object.keys(widgets)[0],
{ canvasWidgets: widgets },
);
return {
...editorConfigs,
dsl: denormalizedDSL,
};
}
export function* saveLayoutSaga(
updateLayoutAction: ReduxAction<{
widgets: { [widgetId: string]: FlattenedWidgetProps };
@ -93,27 +109,54 @@ export function* saveLayoutSaga(
) {
try {
const { widgets } = updateLayoutAction.payload;
const denormalizedDSL = CanvasWidgetsNormalizer.denormalize(
Object.keys(widgets)[0],
{ canvasWidgets: widgets },
);
const editorConfigs = yield select(getEditorConfigs) as any;
yield put({
type: ReduxActionTypes.SAVE_PAGE_INIT,
payload: {
...editorConfigs,
dsl: denormalizedDSL,
},
payload: getLayoutSavePayload(widgets, editorConfigs),
});
} catch (err) {
console.log(err);
}
}
// TODO(abhinav): This has redundant code. The only thing different here is the lack of state update.
// For now this is fire and forget.
export function* asyncSaveLayout() {
try {
const widgets = yield select(getWidgets);
const editorConfigs = yield select(getEditorConfigs) as any;
const request: SavePageRequest = getLayoutSavePayload(
widgets,
editorConfigs,
);
const savePageResponse: SavePageResponse = yield call(
PageApi.savePage,
request,
);
if (!validateResponse(savePageResponse)) {
throw Error("Error when saving layout");
}
} catch (error) {
console.log(error);
yield put({
type: ReduxActionErrorTypes.UPDATE_WIDGET_PROPERTY_ERROR,
payload: {
error,
},
});
}
}
export default function* pageSagas() {
yield all([
takeLatest(ReduxActionTypes.FETCH_PAGE, fetchPageSaga),
takeLatest(ReduxActionTypes.SAVE_PAGE_INIT, savePageSaga),
takeEvery(ReduxActionTypes.UPDATE_LAYOUT, saveLayoutSaga),
// No need to save layout everytime a property is updated.
// We save the latest request to update layout.
takeLatest(ReduxActionTypes.UPDATE_WIDGET_PROPERTY, asyncSaveLayout),
]);
}

View File

@ -18,6 +18,14 @@ export const getCurrentWidgetId = createSelector(
(propertyPane: PropertyPaneReduxState) => propertyPane.widgetId,
);
export const getCurrentWidgetProperties = createSelector(
getCanvasWidgets,
getPropertyPaneState,
(widgets: CanvasWidgetsReduxState, pane: PropertyPaneReduxState) => {
return pane.widgetId && widgets ? widgets[pane.widgetId] : undefined;
},
);
export const getCurrentReferenceNode = createSelector(
getPropertyPaneState,
(pane: PropertyPaneReduxState) => {

View File

@ -6,6 +6,9 @@ import InputTextControl, {
import DropDownControl, {
DropDownControlProps,
} from "../propertyControls/DropDownControl";
import SwitchControl, {
SwitchControlProps,
} from "../propertyControls/SwitchControl";
class PropertyControlRegistry {
static registerPropertyControlBuilders() {
@ -19,6 +22,11 @@ class PropertyControlRegistry {
return <DropDownControl {...controlProps} />;
},
});
PropertyControlFactory.registerControlBuilder("SWITCH", {
buildPropertyControl(controlProps: SwitchControlProps): JSX.Element {
return <SwitchControl {...controlProps} />;
},
});
}
}

View File

@ -5,7 +5,6 @@ import { WidgetConfigProps } from "../reducers/entityReducers/widgetConfigReduce
import { WidgetProps, WidgetOperations } from "../widgets/BaseWidget";
import { WidgetType, RenderModes } from "../constants/WidgetConstants";
import { generateReactKey } from "../utils/generators";
import { Colors } from "../constants/Colors";
import { GridDefaults, WidgetTypes } from "../constants/WidgetConstants";
import { snapToGrid } from "./helpers";
import { OccupiedSpace } from "../widgets/ContainerWidget";
@ -26,7 +25,7 @@ const defaultDSL = {
parentColumnSpace: 1,
parentRowSpace: 1,
renderMode: "CANVAS",
rightColumn: 1024,
rightColumn: 1300,
snapColumns: 16,
snapRows: 32,
topRow: 0,
@ -37,7 +36,10 @@ const defaultDSL = {
export const extractCurrentDSL = (
fetchPageResponse: FetchPageResponse,
): ContainerWidgetProps<WidgetProps> => {
return fetchPageResponse.data.layouts[0].dsl || defaultDSL;
const currentDSL = fetchPageResponse.data.layouts[0].dsl;
currentDSL.rightColumn = 1200;
currentDSL.snapColumns = 24;
return currentDSL || defaultDSL;
};
export const getDropZoneOffsets = (
@ -259,7 +261,6 @@ export const generateWidgetProps = (
renderMode: RenderModes.CANVAS,
...sizes,
...others,
backgroundColor: Colors.WHITE,
};
} else {
if (parent)

View File

@ -1,7 +1,7 @@
import * as React from "react";
import React from "react";
import BaseWidget, { WidgetProps, WidgetState } from "./BaseWidget";
import { WidgetType } from "../constants/WidgetConstants";
import ButtonComponent from "../components/canvas/Button";
import ButtonComponent, { ButtonStyleName } from "../components/canvas/Button";
import { ActionPayload } from "../constants/ActionConstants";
class ButtonWidget extends BaseWidget<ButtonWidgetProps, WidgetState> {
@ -10,13 +10,19 @@ class ButtonWidget extends BaseWidget<ButtonWidgetProps, WidgetState> {
}
getPageView() {
// TODO(abhinav): This is a hack. Need to standardize the style names
const translatedButtonStyleName: ButtonStyleName | undefined =
this.props.buttonStyle &&
(this.props.buttonStyle.split("_")[0].toLowerCase() as ButtonStyleName);
return (
<ButtonComponent
style={this.getPositionStyle()}
styleName={translatedButtonStyleName}
widgetId={this.props.widgetId}
widgetName={this.props.widgetName}
key={this.props.widgetId}
text={this.props.text}
disabled={this.props.isDisabled}
onClick={() => {
this.onButtonClick();
}}
@ -40,6 +46,7 @@ export interface ButtonWidgetProps extends WidgetProps {
buttonStyle?: ButtonStyle;
onClick?: ActionPayload[];
isDisabled?: boolean;
isVisible?: boolean;
}
export default ButtonWidget;

File diff suppressed because it is too large Load Diff