Property Pane Controls
- Fixes #121, #122, #123, #124, #90, #46, #65, #100, #101, #68, #102
This commit is contained in:
parent
b27dbaa470
commit
99ce65c756
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
};
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ export interface BaseStyle {
|
|||
heightUnit?: CSSUnit;
|
||||
widthUnit?: CSSUnit;
|
||||
backgroundColor?: Color;
|
||||
border?: string;
|
||||
}
|
||||
|
||||
export interface ComponentProps {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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!",
|
||||
|
|
|
|||
|
|
@ -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>,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ export interface ControlData {
|
|||
label: string;
|
||||
propertyName: string;
|
||||
controlType: ControlType;
|
||||
propertyValue?: any;
|
||||
}
|
||||
|
||||
export interface ControlFunctions {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
35
app/client/src/propertyControls/StyledControls.tsx
Normal file
35
app/client/src/propertyControls/StyledControls.tsx
Normal 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};
|
||||
}
|
||||
`;
|
||||
31
app/client/src/propertyControls/SwitchControl.tsx
Normal file
31
app/client/src/propertyControls/SwitchControl.tsx
Normal 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;
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ const initialState: PropertyPaneConfigState = PropertyPaneConfigResponse;
|
|||
export type ControlConfig =
|
||||
| InputControlProps
|
||||
| DropDownControlProps
|
||||
| InputControlProps
|
||||
| ControlProps;
|
||||
|
||||
export type SectionOrientation = "HORIZONTAL" | "VERTICAL";
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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} />;
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
1927
app/client/yarn.lock
1927
app/client/yarn.lock
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user