DragLayerComponent
This commit is contained in:
Abhinav Jha 2020-01-16 11:46:21 +00:00
parent af8b400c05
commit 99d660370d
25 changed files with 546 additions and 193 deletions

View File

@ -7,6 +7,7 @@ import {
} from "constants/ApiConstants";
import { ActionApiResponse } from "./ActionAPI";
import { AUTH_LOGIN_URL } from "constants/routes";
import { setRouteBeforeLogin } from "utils/storage";
const { apiUrl, baseUrl } = getAppsmithConfigs();
//TODO(abhinav): Refactor this to make more composable.
@ -41,6 +42,9 @@ axiosInstance.interceptors.response.use(
return response.data;
},
function(error: any) {
if (error.code === "ECONNABORTED") {
console.log("CONNECTION TIMEOUT");
}
if (error.config.url.match(executeActionRegex)) {
return makeExecuteActionResponse(error.response);
}
@ -51,6 +55,7 @@ axiosInstance.interceptors.response.use(
// console.log(error.response.status);
// console.log(error.response.headers);
if (error.response.status === 401) {
setRouteBeforeLogin(window.location.pathname);
window.location.href = AUTH_LOGIN_URL;
}
return Promise.reject(error.response.data);

View File

@ -9,7 +9,6 @@ const StyledContainerComponent = styled.div<ContainerComponentProps>`
props.containerStyle !== "none"
? `
border: ${getBorderCSSShorthand(props.theme.borders[2])};
box-shadow: ${props.theme.shadows[0]};
border-radius: ${
props.containerStyle === "card" || props.containerStyle === "rounded-border"
? props.theme.radii[1]
@ -19,7 +18,7 @@ const StyledContainerComponent = styled.div<ContainerComponentProps>`
height: 100%;
width: 100%;
background: ${props => props.backgroundColor};
padding: ${props => props.theme.spaces[1]}px;
position: relative;
}`;
/* eslint-disable react/display-name */

View File

@ -1,11 +1,11 @@
import React from "react";
import { BaseStyle } from "widgets/BaseWidget";
import { PositionTypes } from "constants/WidgetConstants";
import { PositionTypes, WIDGET_PADDING } from "constants/WidgetConstants";
import { theme } from "constants/DefaultTheme";
type PositionedContainerProps = {
style: BaseStyle;
children: JSX.Element | JSX.Element[];
isMainContainer?: boolean;
};
export const PositionedContainer = (props: PositionedContainerProps) => {
@ -19,8 +19,10 @@ export const PositionedContainer = (props: PositionedContainerProps) => {
height: props.style.componentHeight + (props.style.heightUnit || "px"),
width: props.style.componentWidth + (props.style.widthUnit || "px"),
left: props.style.xPosition + (props.style.xPositionUnit || "px"),
top: props.style.yPosition + (props.style.yPositionUnit || "px"),
padding: PositionedContainer.padding + "px",
top: props.isMainContainer
? theme.spaces[9]
: props.style.yPosition + (props.style.yPositionUnit || "px"),
padding: props.isMainContainer ? 0 : WIDGET_PADDING + "px",
}}
>
{props.children}
@ -28,6 +30,6 @@ export const PositionedContainer = (props: PositionedContainerProps) => {
);
};
PositionedContainer.padding = theme.spaces[2];
PositionedContainer.padding = WIDGET_PADDING;
export default PositionedContainer;

View File

@ -1,18 +1,19 @@
import React from "react";
import React, { useContext } from "react";
import styled from "styled-components";
import { useDragLayer, XYCoord } from "react-dnd";
import DropZone from "./Dropzone";
import { noCollision } from "utils/WidgetPropsUtils";
import { noCollision, currentDropRow } from "utils/WidgetPropsUtils";
import { OccupiedSpace } from "constants/editorConstants";
import DropTargetMask from "./DropTargetMask";
import { MAIN_CONTAINER_WIDGET_ID } from "constants/WidgetConstants";
import { DropTargetContext } from "./DropTargetComponent";
const WrappedDragLayer = styled.div`
position: absolute;
pointer-events: none;
left: 0;
right: 0;
bottom: 0;
top: 0;
width: 100%;
height: 100%;
`;
type DragLayerProps = {
@ -27,9 +28,11 @@ type DragLayerProps = {
parentRows?: number;
parentCols?: number;
isResizing?: boolean;
parentWidgetId: string;
};
const DragLayerComponent = (props: DragLayerProps) => {
const { updateDropTargetRows } = useContext(DropTargetContext);
const { isDragging, currentOffset, widget, canDrop } = useDragLayer(
monitor => ({
isDragging: monitor.isDragging(),
@ -48,6 +51,22 @@ const DragLayerComponent = (props: DragLayerProps) => {
}),
);
if (
props.visible &&
props.parentWidgetId === MAIN_CONTAINER_WIDGET_ID &&
currentOffset &&
props.parentRows
) {
const row = currentDropRow(
props.parentRowHeight,
props.parentOffset.y,
currentOffset.y,
widget,
);
updateDropTargetRows && updateDropTargetRows(row);
}
let widgetWidth = 0;
let widgetHeight = 0;
if (widget) {
@ -59,14 +78,15 @@ const DragLayerComponent = (props: DragLayerProps) => {
if ((!isDragging || !props.visible) && !props.isResizing) {
return null;
}
return (
<WrappedDragLayer>
<DropTargetMask
rowHeight={props.parentRowHeight}
columnWidth={props.parentColumnWidth}
setBounds={props.onBoundsUpdate}
showGrid={(isDragging && props.isOver) || props.isResizing}
/>
<DropZone
{...props}
visible={props.visible && props.isOver}

View File

@ -33,7 +33,7 @@ const DraggableWrapper = styled.div<{ show: boolean }>`
const WidgetBoundaries = styled.div`
left: 0;
right: 0;
z-index: 1;
z-index: 0;
width: 100%;
height: 100%;
border: 1px dashed
@ -160,6 +160,13 @@ const DraggableComponent = (props: DraggableComponentProps) => {
},
});
let stackingContext = 0;
if (props.widgetId === selectedWidget) {
stackingContext = 1;
}
if (props.widgetId === focusedWidget) {
stackingContext = 2;
}
const isResizingOrDragging =
selectedWidget !== props.widgetId && (!!isResizing || !!isDragging);
@ -207,13 +214,11 @@ const DraggableComponent = (props: DraggableComponentProps) => {
height: "100%",
userSelect: "none",
cursor: "drag",
zIndex: props.widgetId === selectedWidget ? 3 : 1,
zIndex: stackingContext,
}}
>
<WidgetBoundaries
style={{ display: isResizingOrDragging ? "block" : "none" }}
/>
{props.children}
<DragHandle className="control" ref={drag}>
<Tooltip content="Move" hoverOpenDelay={500}>
{moveControlIcon}
@ -229,6 +234,9 @@ const DraggableComponent = (props: DraggableComponentProps) => {
{editControlIcon}
</Tooltip>
</EditControl>
<WidgetBoundaries
style={{ display: isResizingOrDragging ? "block" : "none" }}
/>
</DraggableWrapper>
</React.Fragment>
);

View File

@ -1,4 +1,12 @@
import React, { useState, useContext, ReactNode } from "react";
import React, {
useState,
useContext,
ReactNode,
Context,
createContext,
useEffect,
} from "react";
import styled from "styled-components";
import { useDrop, XYCoord, DropTargetMonitor } from "react-dnd";
import { WidgetProps } from "widgets/BaseWidget";
import { WidgetConfigProps } from "reducers/entityReducers/widgetConfigReducer";
@ -6,8 +14,15 @@ import WidgetFactory from "utils/WidgetFactory";
import { widgetOperationParams, noCollision } from "utils/WidgetPropsUtils";
import { EditorContext } from "components/editorComponents/EditorContextProvider";
import { FocusContext, DragResizeContext } from "pages/Editor/CanvasContexts";
import {
MAIN_CONTAINER_WIDGET_ID,
WIDGET_PADDING,
} from "constants/WidgetConstants";
import { calculateDropTargetRows } from "./DropTargetUtils";
import DragLayerComponent from "./DragLayerComponent";
import { AppState } from "reducers";
import { useSelector } from "react-redux";
import { theme } from "constants/DefaultTheme";
type DropTargetComponentProps = WidgetProps & {
children?: ReactNode;
@ -22,18 +37,99 @@ type DropTargetBounds = {
height: number;
};
const StyledDropTarget = styled.div`
transition: height 100ms ease-in;
`;
/*
This context will provide the function which will help the draglayer and resizablecomponents trigger
an update of the main container's rows
*/
export const DropTargetContext: Context<{
updateDropTargetRows?: (row: number) => boolean;
persistDropTargetRows?: (widgetId: string, rows: number) => void;
}> = createContext({});
export const DropTargetComponent = (props: DropTargetComponentProps) => {
// Hook to keep the offset of the drop target container in state
const [dropTargetOffset, setDropTargetOffset] = useState({ x: 0, y: 0 });
const { updateWidget, occupiedSpaces } = useContext(EditorContext);
const { selectWidget, showPropertyPane } = useContext(FocusContext);
const [rows, setRows] = useState(props.snapRows);
useEffect(() => {
setRows(props.snapRows);
}, [props.snapRows]);
const { updateWidget, occupiedSpaces, updateWidgetProperty } = useContext(
EditorContext,
);
const { selectWidget, showPropertyPane, selectedWidget } = useContext(
FocusContext,
);
const { isResizing } = useContext(DragResizeContext);
const spacesOccupiedBySiblingWidgets =
occupiedSpaces && occupiedSpaces[props.widgetId]
? occupiedSpaces[props.widgetId]
: undefined;
const childWidgets = useSelector(
(state: AppState) => state.entities.canvasWidgets[props.widgetId].children,
);
const persistDropTargetRows = (widgetId: string, widgetBottomRow: number) => {
if (props.widgetId === MAIN_CONTAINER_WIDGET_ID) {
const occupiedSpacesByChildren =
occupiedSpaces && occupiedSpaces[MAIN_CONTAINER_WIDGET_ID];
const rowsToPersist = calculateDropTargetRows(
widgetId,
widgetBottomRow,
rows,
occupiedSpacesByChildren,
);
setRows(rowsToPersist);
/* Update the main container's rows, ONLY if it has changed since the last render */
if (props.snapRows !== rowsToPersist) {
updateWidgetProperty &&
updateWidgetProperty(props.widgetId, "snapRows", rowsToPersist);
updateWidgetProperty &&
updateWidgetProperty(
props.widgetId,
"bottomRow",
Math.round(
(rowsToPersist * props.snapRowSpace) / props.parentRowSpace,
),
);
}
}
};
/* Update the rows of the main container based on the current widget's (dragging/resizing) bottom row */
const updateDropTargetRows = (widgetBottomRow: number) => {
if (props.widgetId === MAIN_CONTAINER_WIDGET_ID) {
/* If the widget has reached the penultimate row of the main container */
if (widgetBottomRow > rows - 1) {
setRows(rows + 2);
return true;
// If the current widget's (dragging/resizing) bottom row has moved back up
} else if (widgetBottomRow < rows - 2 && rows - props.snapRows >= 2) {
setRows(rows - 2);
return true;
}
return false;
}
return false;
};
const isChildFocused =
!!childWidgets &&
!!selectedWidget &&
childWidgets.length > 0 &&
childWidgets.indexOf(selectedWidget) > -1;
const isChildResizing = !!isResizing && isChildFocused;
// Make this component a drop target
const [{ isOver, isExactlyOver }, drop] = useDrop({
const [{ isExactlyOver }, drop] = useDrop({
accept: Object.values(WidgetFactory.getWidgetTypes()),
drop(widget: WidgetProps & Partial<WidgetConfigProps>, monitor) {
// Make sure we're dropping in this container.
@ -46,16 +142,30 @@ export const DropTargetComponent = (props: DropTargetComponentProps) => {
props.snapRowSpace,
props.widgetId,
);
// Only show propertypane if this is a new widget.
// If it is not a new widget, then let the DraggableComponent handle it.
showPropertyPane &&
updateWidgetParams.payload.newWidgetId &&
showPropertyPane(updateWidgetParams.payload.newWidgetId);
// Select the widget if it is a new widget
selectWidget &&
updateWidgetParams.payload.newWidgetId &&
selectWidget(updateWidgetParams.payload.newWidgetId);
/* currently dropped widget's bottom row */
const droppedWidgetBottomRow = updateWidgetParams.payload.rows
? updateWidgetParams.payload.topRow + updateWidgetParams.payload.rows
: updateWidgetParams.payload.topRow +
(widget.bottomRow - widget.topRow);
persistDropTargetRows(
widget.widgetId || updateWidgetParams.payload.newWidgetId,
droppedWidgetBottomRow,
);
/* Finally update the widget */
updateWidget &&
updateWidget(
updateWidgetParams.operation,
@ -72,7 +182,6 @@ export const DropTargetComponent = (props: DropTargetComponentProps) => {
props.widgetId !== monitor.getItem().widgetId) ||
(monitor.isOver() && props.widgetId !== monitor.getItem().widgetId),
isExactlyOver: monitor.isOver({ shallow: true }),
draggingItem: monitor.getItem() as WidgetProps,
}),
// Only allow drop if the drag object is directly over this component
// As opposed to the drag object being over a child component, or outside the component bounds
@ -87,7 +196,7 @@ export const DropTargetComponent = (props: DropTargetComponentProps) => {
widget,
dropTargetOffset,
spacesOccupiedBySiblingWidgets,
props.snapRows,
rows,
props.snapColumns,
);
return !hasCollision;
@ -112,35 +221,55 @@ export const DropTargetComponent = (props: DropTargetComponentProps) => {
}
};
const width =
props.widgetId === MAIN_CONTAINER_WIDGET_ID
? `calc(100% - ${WIDGET_PADDING * 2}px)`
: "100%";
const height =
props.widgetId === MAIN_CONTAINER_WIDGET_ID
? `${rows * props.snapRowSpace}px`
: "100%";
const marginTop =
props.widgetId === MAIN_CONTAINER_WIDGET_ID ? `${theme.spaces[9]}px` : 0;
const marginBottom =
props.widgetId === MAIN_CONTAINER_WIDGET_ID ? "500px" : 0;
return (
<div
onClick={handleFocus}
ref={drop}
style={{
position: "relative",
left: 0,
height: "100%",
width: "100%",
top: 0,
userSelect: "none",
opacity: 0.99,
}}
<DropTargetContext.Provider
value={{ updateDropTargetRows, persistDropTargetRows }}
>
{props.children}
<DragLayerComponent
parentOffset={dropTargetOffset}
parentRowHeight={props.snapRowSpace}
parentColumnWidth={props.snapColumnSpace}
visible={isOver || !!isResizing}
isOver={isExactlyOver}
dropTargetOffset={dropTargetOffset}
occupiedSpaces={spacesOccupiedBySiblingWidgets}
onBoundsUpdate={handleBoundsUpdate}
parentRows={props.snapRows}
parentCols={props.snapColumns}
isResizing={isResizing}
/>
</div>
<StyledDropTarget
onClick={handleFocus}
ref={drop}
style={{
position: "relative",
width,
height,
marginTop,
marginBottom,
userSelect: "none",
opacity: 0.99,
}}
>
{props.children}
<DragLayerComponent
parentOffset={dropTargetOffset}
parentWidgetId={props.widgetId}
parentRowHeight={props.snapRowSpace}
parentColumnWidth={props.snapColumnSpace}
visible={isExactlyOver || isChildResizing}
isOver={isExactlyOver}
dropTargetOffset={dropTargetOffset}
occupiedSpaces={spacesOccupiedBySiblingWidgets}
onBoundsUpdate={handleBoundsUpdate}
parentRows={rows}
parentCols={props.snapColumns}
isResizing={isChildResizing}
/>
</StyledDropTarget>
</DropTargetContext.Provider>
);
};

View File

@ -1,38 +1,30 @@
import React, { useLayoutEffect, MutableRefObject } from "react";
import styled, { css } from "styled-components";
import React, { useLayoutEffect, MutableRefObject, memo } from "react";
import styled from "styled-components";
import { CONTAINER_GRID_PADDING } from "constants/WidgetConstants";
type DropTargetMaskProps = {
rowHeight: number;
columnWidth: number;
setBounds?: Function;
showGrid?: boolean;
};
export const DropTargetMaskWrapper = styled.div<DropTargetMaskProps>`
position: absolute;
left: 0;
top: 0;
bottom: 0;
right: 0;
width: 100%;
height: 100%;
${props =>
props.showGrid &&
css`
background-image: radial-gradient(
circle,
${props => props.theme.colors.grid} 2px,
transparent 0
);
background-size: ${props => props.columnWidth}px
${props => props.rowHeight}px;
background-position: -${props => props.columnWidth / 2}px -${props =>
props.rowHeight / 2}px;
`}
left: ${CONTAINER_GRID_PADDING}px;
top: ${CONTAINER_GRID_PADDING}px;
bottom: ${CONTAINER_GRID_PADDING}px;
right: ${CONTAINER_GRID_PADDING}px;
background-image: radial-gradient(
circle,
${props => props.theme.colors.grid} 2px,
transparent 0
);
background-size: ${props => props.columnWidth}px ${props => props.rowHeight}px;
background-position: -${props => props.columnWidth / 2}px -${props =>
props.rowHeight / 2}px;
`;
/* eslint-disable react/display-name */
export const DropTargetMask = (props: DropTargetMaskProps) => {
// An underlay div for Grid markers and calculating the width, height, x and y positions
export const DropTargetMask = memo((props: DropTargetMaskProps) => {
const dropTargetMask: MutableRefObject<HTMLDivElement | null> = React.useRef(
null,
);
@ -44,7 +36,8 @@ export const DropTargetMask = (props: DropTargetMaskProps) => {
props.setBounds && props.setBounds(rect);
}
});
return <DropTargetMaskWrapper {...props} ref={dropTargetMask} />;
};
});
export default DropTargetMask;

View File

@ -0,0 +1,33 @@
import { GridDefaults } from "constants/WidgetConstants";
import { CANVAS_DEFAULT_HEIGHT_PX } from "constants/AppConstants";
import { OccupiedSpace } from "constants/editorConstants";
export const calculateDropTargetRows = (
widgetId: string,
widgetBottomRow: number,
currentDropTargetRows: number,
occupiedSpacesByChildren?: OccupiedSpace[],
) => {
/* Max bottom row including the existing widgets as well as the widget we just dropped */
const maxBottomRow =
occupiedSpacesByChildren &&
Math.max(
...occupiedSpacesByChildren
.filter(child => child.id !== widgetId)
.map(child => child.bottom),
widgetBottomRow,
);
let _rows = currentDropTargetRows;
/*
If the main container's rows are greater than the max bottom row of children widgets (by 4)
Update the rows of the container. Making sure that it does not go below the default canvas rows
*/
if (maxBottomRow && _rows - maxBottomRow > 2) {
_rows = Math.max(
maxBottomRow + 2,
CANVAS_DEFAULT_HEIGHT_PX / GridDefaults.DEFAULT_GRID_ROW_HEIGHT - 1,
);
}
return _rows + 1;
};

View File

@ -2,7 +2,8 @@ import React from "react";
import { XYCoord } from "react-dnd";
import styled from "styled-components";
import { snapToGrid } from "utils/helpers";
import { theme } from "constants/DefaultTheme";
import { theme, IntentColors } from "constants/DefaultTheme";
import { CONTAINER_GRID_PADDING } from "constants/WidgetConstants";
const DropZoneWrapper = styled.div`
position: absolute;
@ -22,6 +23,33 @@ type DropZoneProps = {
canDrop: boolean;
};
const generateDropZoneStyles = (
props: {
visible: boolean;
left: number;
top: number;
height: number;
width: number;
},
canDrop: boolean,
isSnapping: boolean,
) => {
let background = theme.colors.hover;
if (!isSnapping) {
background = IntentColors.success;
} else if (!canDrop) {
background = theme.colors.error;
}
return {
display: props.visible ? "block" : "none",
left: props.left + "px",
width: props.width + "px",
top: props.top + "px",
height: props.height + "px",
background,
transition: isSnapping ? "all 0.1s linear" : "none",
};
};
/* eslint-disable react/display-name */
export const DropZone = (props: DropZoneProps) => {
let wrapperProps = {
@ -31,35 +59,53 @@ export const DropZone = (props: DropZoneProps) => {
height: 0,
width: 0,
};
if (props.visible) {
if (props.currentOffset && props.currentOffset.x >= props.parentOffset.x) {
const [leftColumn, topRow] = snapToGrid(
props.parentColumnWidth,
props.parentRowHeight,
props.currentOffset.x - props.parentOffset.x,
props.currentOffset.y - props.parentOffset.y,
);
wrapperProps = {
visible: true,
left: leftColumn * props.parentColumnWidth,
top: topRow * props.parentRowHeight,
height: props.height * props.parentRowHeight,
width: props.width * props.parentColumnWidth,
};
}
let wrapperPropsWithSnap = {
visible: false,
left: 0,
top: 0,
height: 0,
width: 0,
};
if (
props.visible &&
props.currentOffset &&
props.currentOffset.x >= props.parentOffset.x
) {
wrapperProps = {
visible: true,
left: props.currentOffset.x - props.parentOffset.x,
top: props.currentOffset.y - props.parentOffset.y,
height: props.height * props.parentRowHeight,
width: props.width * props.parentColumnWidth,
};
const [leftColumn, topRow] = snapToGrid(
props.parentColumnWidth,
props.parentRowHeight,
props.currentOffset.x - props.parentOffset.x,
props.currentOffset.y - props.parentOffset.y,
);
wrapperPropsWithSnap = {
visible: true,
left: leftColumn * props.parentColumnWidth + CONTAINER_GRID_PADDING,
top: topRow * props.parentRowHeight + CONTAINER_GRID_PADDING,
height: props.height * props.parentRowHeight,
width: props.width * props.parentColumnWidth,
};
}
return (
<DropZoneWrapper
style={{
display: wrapperProps.visible ? "block" : "none",
left: wrapperProps.left + "px",
width: wrapperProps.width + "px",
top: wrapperProps.top + "px",
height: wrapperProps.height + "px",
background: props.canDrop ? theme.colors.hover : theme.colors.error,
}}
/>
<React.Fragment>
<DropZoneWrapper
style={generateDropZoneStyles(
wrapperPropsWithSnap,
props.canDrop,
true,
)}
/>
<DropZoneWrapper
style={generateDropZoneStyles(wrapperProps, props.canDrop, false)}
/>
</React.Fragment>
);
};

View File

@ -1,12 +1,13 @@
import React, { useContext, useState, memo } from "react";
import { ResizeDirection } from "re-resizable";
import { XYCoord } from "react-dnd";
import { MAIN_CONTAINER_WIDGET_ID } from "constants/WidgetConstants";
import { getAbsolutePixels } from "utils/helpers";
import { WidgetOperations, WidgetRowCols } from "widgets/BaseWidget";
import { EditorContext } from "components/editorComponents/EditorContextProvider";
import { FocusContext, DragResizeContext } from "pages/Editor/CanvasContexts";
import { generateClassName } from "utils/generators";
import { DropTargetContext } from "./DropTargetComponent";
import ResizableContainer, {
ResizeBorderDotDiv,
ResizableComponentProps,
@ -24,6 +25,9 @@ export const ResizableComponent = memo((props: ResizableComponentProps) => {
// Fetch information from the context
const { isDragging, setIsResizing } = useContext(DragResizeContext);
const { updateWidget, occupiedSpaces } = useContext(EditorContext);
const { updateDropTargetRows, persistDropTargetRows } = useContext(
DropTargetContext,
);
const {
showPropertyPane,
selectedWidget,
@ -72,14 +76,24 @@ export const ResizableComponent = memo((props: ResizableComponentProps) => {
delta: UIElementSize,
position: XYCoord,
) => {
const isResizePossible = !hasCollision(
delta,
position,
props,
occupiedSpacesBySiblingWidgets,
);
if (isResizePossible === isColliding) {
setIsColliding(!isColliding);
const bottom =
props.bottomRow + (delta.height + position.y) / props.parentRowSpace;
// Make sure to calculate collision IF we don't update the main container's rows
let updated = false;
if (updateDropTargetRows && props.parentId === MAIN_CONTAINER_WIDGET_ID)
updated = updateDropTargetRows(bottom);
if (!updated) {
const isResizePossible = !hasCollision(
delta,
position,
props,
occupiedSpacesBySiblingWidgets,
);
if (isResizePossible === isColliding) {
setIsColliding(!isColliding);
}
}
};
@ -112,6 +126,9 @@ export const ResizableComponent = memo((props: ResizableComponentProps) => {
);
if (newRowCols) {
persistDropTargetRows &&
props.parentId === MAIN_CONTAINER_WIDGET_ID &&
persistDropTargetRows(props.widgetId, newRowCols.bottomRow);
updateWidget &&
updateWidget(WidgetOperations.RESIZE, props.widgetId, newRowCols);
}
@ -132,7 +149,7 @@ export const ResizableComponent = memo((props: ResizableComponentProps) => {
const style = getBorderStyles(
isWidgetFocused,
isColliding,
props.paddingOffset,
props.paddingOffset - 2,
);
return (
<ResizableContainer

View File

@ -5,6 +5,7 @@ import { theme } from "constants/DefaultTheme";
import { WidgetProps, WidgetRowCols } from "widgets/BaseWidget";
import { isDropZoneOccupied } from "utils/WidgetPropsUtils";
import { OccupiedSpace } from "constants/editorConstants";
import { GridDefaults } from "constants/WidgetConstants";
export type UIElementSize = { height: number; width: number };
@ -75,13 +76,22 @@ export const computeUpdatedRowCols = (
): WidgetRowCols | false => {
if (isColliding) return false;
const newRowCols: WidgetRowCols = {
leftColumn: props.leftColumn + position.x / props.parentColumnSpace,
topRow: props.topRow + position.y / props.parentRowSpace,
leftColumn: Math.max(
Math.round(props.leftColumn + position.x / props.parentColumnSpace),
0,
),
topRow: Math.round(props.topRow + position.y / props.parentRowSpace),
rightColumn:
props.rightColumn + (delta.width + position.x) / props.parentColumnSpace,
bottomRow:
rightColumn: Math.min(
Math.round(
props.rightColumn +
(delta.width + position.x) / props.parentColumnSpace,
),
GridDefaults.DEFAULT_GRID_COLUMNS,
),
bottomRow: Math.round(
props.bottomRow + (delta.height + position.y) / props.parentRowSpace,
),
};
if (

View File

@ -0,0 +1,5 @@
export const CANVAS_DEFAULT_WIDTH_PX = 1024;
export const CANVAS_DEFAULT_HEIGHT_PX = 1280;
export const CANVAS_DEFAULT_GRID_HEIGHT_PX = 1;
export const CANVAS_DEFAULT_GRID_WIDTH_PX = 1;
export const CANVAS_BACKGROUND_COLOR = "#FFFFFF";

View File

@ -229,7 +229,7 @@ export const theme: Theme = {
paneText: Colors.GRAY_CHATEAU,
paneSectionLabel: Colors.CADET_BLUE,
navBG: Colors.SHARK,
grid: Colors.GEYSER,
grid: Colors.GEYSER_LIGHT,
containerBorder: Colors.FRENCH_PASS,
menuButtonBGInactive: Colors.JUNGLE_MIST,
menuIconColorInactive: Colors.OXFORD_BLUE,
@ -269,7 +269,7 @@ export const theme: Theme = {
sidebarWidth: "300px",
headerHeight: "50px",
sideNav: {
maxWidth: 250,
maxWidth: 300,
minWidth: 50,
bgColor: Colors.OXFORD_BLUE,
fontColor: Colors.WHITE,

View File

@ -77,8 +77,14 @@ export const GridDefaults = {
DEFAULT_WIDGET_WIDTH: 200,
DEFAULT_WIDGET_HEIGHT: 100,
DEFAULT_GRID_COLUMNS: 16,
DEFAULT_GRID_ROWS: 32,
DEFAULT_GRID_ROW_HEIGHT: 40,
};
export const CONTAINER_GRID_PADDING =
(GridDefaults.DEFAULT_GRID_ROW_HEIGHT / 2) * 0.6;
export const WIDGET_PADDING = (GridDefaults.DEFAULT_GRID_ROW_HEIGHT / 2) * 0.4;
export const WIDGET_CLASSNAME_PREFIX = "WIDGET_";
export const MAIN_CONTAINER_WIDGET_ID = "0";
export const MAIN_CONTAINER_WIDGET_NAME = "MainContainer";

View File

@ -5,11 +5,11 @@ import { RenderModes } from "constants/WidgetConstants";
import WidgetFactory from "utils/WidgetFactory";
import { ContainerWidgetProps } from "widgets/ContainerWidget";
const PageView = styled.div`
flex-grow: 1;
const PageView = styled.div<{ width: number }>`
height: 100%;
margin-top: ${props => props.theme.spaces[1]}px;
position: relative;
width: ${props => props.width}px;
margin: 0 auto;
`;
type AppPageProps = {
@ -18,7 +18,7 @@ type AppPageProps = {
export const AppPage = (props: AppPageProps) => {
return (
<PageView>
<PageView width={props.dsl.rightColumn}>
{props.dsl.widgetId &&
WidgetFactory.createWidget(props.dsl, RenderModes.PAGE)}
</PageView>

View File

@ -6,6 +6,7 @@ import {
getIsFetchingPage,
getCurrentPageLayoutDSL,
} from "selectors/appViewSelectors";
import styled from "styled-components";
import { ContainerWidgetProps } from "widgets/ContainerWidget";
import { WidgetProps } from "widgets/BaseWidget";
import { AppViewerRouteParams } from "constants/routes";
@ -15,6 +16,14 @@ import { NonIdealState, Icon, Spinner } from "@blueprintjs/core";
import Centered from "components/designSystems/appsmith/CenteredWrapper";
import AppPage from "./AppPage";
const Section = styled.section`
background: ${props => props.theme.colors.bodyBG};
height: 100%;
width: 100%;
position: relative;
overflow-x: auto;
overflow-y: auto;
`;
type AppViewerPageContainerProps = {
isFetchingPage: boolean;
dsl?: ContainerWidgetProps<WidgetProps>;
@ -69,7 +78,11 @@ class AppViewerPageContainer extends Component<AppViewerPageContainerProps> {
} else if (!this.props.isFetchingPage && !this.props.dsl) {
return pageNotFound;
} else if (!this.props.isFetchingPage && this.props.dsl) {
return <AppPage dsl={this.props.dsl} />;
return (
<Section>
<AppPage dsl={this.props.dsl} />
</Section>
);
}
}
}

View File

@ -36,7 +36,7 @@ const Canvas = (props: CanvasProps) => {
}}
>
<PropertyPane />
<ArtBoard>
<ArtBoard width={props.dsl.rightColumn}>
{props.dsl.widgetId &&
WidgetFactory.createWidget(props.dsl, RenderModes.CANVAS)}
</ArtBoard>

View File

@ -1,8 +1,7 @@
import styled from "styled-components";
export default styled.div`
width: 100%;
export default styled.div<{ width: number }>`
width: ${props => props.width}px;
height: 100%;
margin: 0 auto;
position: relative;
overflow: auto;
`;

View File

@ -116,6 +116,8 @@ export interface EditorReduxState {
currentPageId?: string;
currentLayoutId?: string;
currentPageName?: string;
selectedWidget?: string;
focusedWidget?: string;
loadingStates: {
saving: boolean;
savingError: boolean;

View File

@ -21,6 +21,7 @@ import { UpdateWidgetPropertyPayload } from "actions/controlActions";
import { isDynamicValue } from "utils/DynamicBindingUtils";
import { WidgetProps } from "widgets/BaseWidget";
import _ from "lodash";
import { WidgetTypes } from "constants/WidgetConstants";
export function* addChildSaga(addChildAction: ReduxAction<WidgetAddChild>) {
try {
@ -111,7 +112,7 @@ export function* moveSaga(moveAction: ReduxAction<WidgetMove>) {
// Get parent from DSL/Redux Store
const parent = yield select(getWidget, parentId);
// Update position of widget
widget = updateWidgetPosition(widget, leftColumn, topRow, parent);
widget = updateWidgetPosition(widget, leftColumn, topRow);
// Replace widget with update widget props
widgets[widgetId] = widget;
// If the parent has changed i.e parentWidgetId is not parent.widgetId
@ -152,6 +153,10 @@ export function* resizeSaga(resizeAction: ReduxAction<WidgetResize>) {
let widget: FlattenedWidgetProps = yield select(getWidget, widgetId);
const widgets = yield select(getWidgets);
if (widget.type === WidgetTypes.CONTAINER_WIDGET) {
console.log(resizeAction.payload);
widget.snapRows = bottomRow - topRow - 1;
}
widget = { ...widget, leftColumn, rightColumn, topRow, bottomRow };
widgets[widgetId] = widget;

View File

@ -1,4 +1,11 @@
import { FetchPageResponse } from "api/PageApi";
import {
CANVAS_DEFAULT_WIDTH_PX,
CANVAS_DEFAULT_HEIGHT_PX,
CANVAS_BACKGROUND_COLOR,
CANVAS_DEFAULT_GRID_HEIGHT_PX,
CANVAS_DEFAULT_GRID_WIDTH_PX,
} from "constants/AppConstants";
import { XYCoord } from "react-dnd";
import { ContainerWidgetProps } from "widgets/ContainerWidget";
import { WidgetConfigProps } from "reducers/entityReducers/widgetConfigReducer";
@ -9,7 +16,12 @@ import {
} from "widgets/BaseWidget";
import { WidgetType, RenderModes } from "constants/WidgetConstants";
import { generateReactKey } from "utils/generators";
import { GridDefaults, WidgetTypes } from "constants/WidgetConstants";
import {
GridDefaults,
WidgetTypes,
MAIN_CONTAINER_WIDGET_ID,
MAIN_CONTAINER_WIDGET_NAME,
} from "constants/WidgetConstants";
import { snapToGrid } from "./helpers";
import { OccupiedSpace } from "constants/editorConstants";
@ -19,7 +31,7 @@ export type WidgetOperationParams = {
payload: any;
};
const { DEFAULT_GRID_COLUMNS, DEFAULT_GRID_ROWS } = GridDefaults;
const { DEFAULT_GRID_COLUMNS, DEFAULT_GRID_ROW_HEIGHT } = GridDefaults;
type Rect = {
top: number;
left: number;
@ -28,26 +40,38 @@ type Rect = {
};
const defaultDSL = {
backgroundColor: "#ffffff",
bottomRow: 1024,
type: WidgetTypes.CONTAINER_WIDGET,
widgetId: MAIN_CONTAINER_WIDGET_ID,
widgetName: MAIN_CONTAINER_WIDGET_NAME,
backgroundColor: CANVAS_BACKGROUND_COLOR,
children: [],
leftColumn: 0,
parentColumnSpace: 1,
parentRowSpace: 1,
renderMode: "CANVAS",
rightColumn: 1200,
snapColumns: 24,
snapRows: 32,
rightColumn: CANVAS_DEFAULT_WIDTH_PX,
parentColumnSpace: CANVAS_DEFAULT_GRID_WIDTH_PX,
snapColumns: GridDefaults.DEFAULT_GRID_COLUMNS,
topRow: 0,
type: "CONTAINER_WIDGET",
widgetId: "0",
widgetName: "MainContainer",
bottomRow: CANVAS_DEFAULT_HEIGHT_PX,
parentRowSpace: CANVAS_DEFAULT_GRID_HEIGHT_PX,
// 1 row needs to be removed, as padding top and bottom takes up some 1 row worth of space.
// Widget padding: 8px
// Container padding: 12px;
// Total = (8 + 12) * 2 = GridDefaults.DEFAULT_GRID_ROW_HEIGHT = 40
snapRows: CANVAS_DEFAULT_HEIGHT_PX / GridDefaults.DEFAULT_GRID_ROW_HEIGHT - 1,
};
export const extractCurrentDSL = (
fetchPageResponse: FetchPageResponse,
): ContainerWidgetProps<WidgetProps> => {
const currentDSL = fetchPageResponse.data.layouts[0].dsl || defaultDSL;
// 1 row needs to be removed, as padding top and bottom takes up some 1 row worth of space.
// Widget padding: 8px
// Container padding: 12px;
// Total = (8 + 12) * 2 = GridDefaults.DEFAULT_GRID_ROW_HEIGHT = 40
currentDSL.snapRows =
Math.floor(currentDSL.bottomRow / DEFAULT_GRID_ROW_HEIGHT) - 1;
return currentDSL;
};
@ -99,11 +123,14 @@ export const isDropZoneOccupied = (
export const isWidgetOverflowingParentBounds = (
parentRowCols: { rows?: number; cols?: number },
offset: Rect,
) => {
return (
): boolean => {
const result =
offset.right < 0 ||
offset.top < 0 ||
(parentRowCols.cols || GridDefaults.DEFAULT_GRID_COLUMNS) < offset.right ||
(parentRowCols.rows || GridDefaults.DEFAULT_GRID_ROWS) < offset.bottom
);
(parentRowCols.rows || 0) < offset.bottom;
return result;
};
export const noCollision = (
@ -123,6 +150,9 @@ export const noCollision = (
clientOffset as XYCoord,
dropTargetOffset,
);
if (left < 0 || top < 0) {
return false;
}
const widgetWidth = widget.columns
? widget.columns
: widget.rightColumn - widget.leftColumn;
@ -143,6 +173,23 @@ export const noCollision = (
return false;
};
export const currentDropRow = (
dropTargetRowSpace: number,
dropTargetVerticalOffset: number,
draggableItemVerticalOffset: number,
widget: WidgetProps & Partial<WidgetConfigProps>,
) => {
const widgetHeight = widget.rows
? widget.rows
: widget.bottomRow - widget.topRow;
const top = Math.round(
(draggableItemVerticalOffset - dropTargetVerticalOffset) /
dropTargetRowSpace,
);
const currentBottomOffset = top + widgetHeight;
return currentBottomOffset;
};
export const widgetOperationParams = (
widget: WidgetProps & Partial<WidgetConfigProps>,
widgetOffset: XYCoord,
@ -196,7 +243,6 @@ export const updateWidgetPosition = (
widget: WidgetProps,
leftColumn: number,
topRow: number,
parent?: WidgetProps,
) => {
const newPositions = {
leftColumn,
@ -204,31 +250,16 @@ export const updateWidgetPosition = (
rightColumn: leftColumn + (widget.rightColumn - widget.leftColumn),
bottomRow: topRow + (widget.bottomRow - widget.topRow),
};
if (parent) {
if (widget.type === WidgetTypes.CONTAINER_WIDGET) {
widget.snapRows = newPositions.bottomRow - newPositions.topRow - 1;
}
return {
...widget,
...newPositions,
};
};
export const updateWidgetSize = (
widget: WidgetProps,
deltaHeight: number,
deltaWidth: number,
): WidgetProps => {
const origHeight = (widget.bottomRow - widget.topRow) * widget.parentRowSpace;
const origWidth =
(widget.rightColumn - widget.leftColumn) * widget.parentColumnSpace;
return {
...widget,
rightColumn:
widget.leftColumn + (origWidth + deltaWidth) / widget.parentColumnSpace,
bottomRow:
widget.topRow + (origHeight + deltaHeight) / widget.parentRowSpace,
};
};
export const generateWidgetProps = (
parent: ContainerWidgetProps<WidgetProps>,
type: WidgetType,
@ -252,7 +283,7 @@ export const generateWidgetProps = (
if (type === WidgetTypes.CONTAINER_WIDGET) {
others = {
snapColumns: DEFAULT_GRID_COLUMNS,
snapRows: DEFAULT_GRID_ROWS,
snapRows: rows - 1,
orientation: "VERTICAL",
children: [],
};
@ -270,9 +301,9 @@ export const generateWidgetProps = (
...others,
};
} else {
if (parent)
if (parent) {
throw Error("Failed to create widget: Parent's size cannot be calculate");
else throw Error("Failed to create widget: Parent was not provided ");
} else throw Error("Failed to create widget: Parent was not provided ");
}
};

View File

@ -4,8 +4,8 @@ export const snapToGrid = (
x: number,
y: number,
) => {
const snappedX = Math.floor(x / columnWidth);
const snappedY = Math.floor(y / rowHeight);
const snappedX = Math.round(x / columnWidth);
const snappedY = Math.round(y / rowHeight);
return [snappedX, snappedY];
};

View File

@ -3,6 +3,7 @@ import moment from "moment";
const STORAGE_KEYS: { [id: string]: string } = {
AUTH_EXPIRATION: "Auth.expiration",
ROUTE_BEFORE_LOGIN: "RedirectPath",
};
const store = localforage.createInstance({
@ -25,3 +26,20 @@ export const hasAuthExpired = async () => {
}
return false;
};
export const setRouteBeforeLogin = (path: string | null) => {
store.setItem(STORAGE_KEYS.ROUTE_BEFORE_LOGIN, path).catch(error => {
console.log("Unable to set last path");
});
};
export const getRouteBeforeLogin = async () => {
const routeBeforeLogin: string = await store.getItem(
STORAGE_KEYS.ROUTE_BEFORE_LOGIN,
);
if (routeBeforeLogin && routeBeforeLogin.length > 0) {
setRouteBeforeLogin(null);
return routeBeforeLogin;
}
return;
};

View File

@ -10,7 +10,12 @@ import {
CSSUnits,
} from "constants/WidgetConstants";
import React, { Component } from "react";
import { PositionType, CSSUnit } from "constants/WidgetConstants";
import {
PositionType,
CSSUnit,
CONTAINER_GRID_PADDING,
WidgetTypes,
} from "constants/WidgetConstants";
import _ from "lodash";
import DraggableComponent from "components/editorComponents/DraggableComponent";
import ResizableComponent from "components/editorComponents/ResizableComponent";
@ -126,6 +131,7 @@ abstract class BaseWidget<
componentWidth: (rightColumn - leftColumn) * parentColumnSpace,
componentHeight: (bottomRow - topRow) * parentRowSpace,
};
if (
_.isNil(this.state) ||
widgetState.componentHeight !== this.state.componentHeight ||
@ -161,15 +167,17 @@ abstract class BaseWidget<
</PositionedContainer>
);
}
return (
<PositionedContainer style={style}>
{this.getCanvasView()}
</PositionedContainer>
);
return this.getCanvasView();
case RenderModes.PAGE:
if (this.props.isVisible) {
return (
<PositionedContainer style={this.getPositionStyle()}>
<PositionedContainer
style={this.getPositionStyle()}
isMainContainer={
this.props.type === WidgetTypes.CONTAINER_WIDGET &&
this.props.widgetId === "0"
}
>
{this.getPageView()}
</PositionedContainer>
);
@ -200,8 +208,11 @@ abstract class BaseWidget<
positionType: PositionTypes.ABSOLUTE,
componentHeight: this.state.componentHeight,
componentWidth: this.state.componentWidth,
yPosition: this.props.topRow * this.props.parentRowSpace,
xPosition: this.props.leftColumn * this.props.parentColumnSpace,
yPosition:
this.props.topRow * this.props.parentRowSpace + CONTAINER_GRID_PADDING,
xPosition:
this.props.leftColumn * this.props.parentColumnSpace +
CONTAINER_GRID_PADDING,
xPositionUnit: CSSUnits.PIXEL,
yPositionUnit: CSSUnits.PIXEL,
};

View File

@ -8,13 +8,16 @@ import { ContainerOrientation, WidgetType } from "constants/WidgetConstants";
import WidgetFactory from "utils/WidgetFactory";
import { Color } from "constants/Colors";
import DropTargetComponent from "components/editorComponents/DropTargetComponent";
import { GridDefaults } from "constants/WidgetConstants";
import {
GridDefaults,
CONTAINER_GRID_PADDING,
WIDGET_PADDING,
} from "constants/WidgetConstants";
import ResizeBoundsContainerComponent from "components/editorComponents/ResizeBoundsContainerComponent";
import BaseWidget, { WidgetProps, WidgetState } from "./BaseWidget";
const { DEFAULT_GRID_COLUMNS, DEFAULT_GRID_ROW_HEIGHT } = GridDefaults;
class ContainerWidget extends BaseWidget<
ContainerWidgetProps<WidgetProps>,
ContainerWidgetState
@ -34,10 +37,10 @@ class ContainerWidget extends BaseWidget<
super.componentDidUpdate(previousProps);
let snapColumnSpace = this.state.snapColumnSpace;
if (this.state.componentWidth)
snapColumnSpace = Math.floor(
this.state.componentWidth /
(this.props.snapColumns || DEFAULT_GRID_COLUMNS),
);
snapColumnSpace =
(this.state.componentWidth -
(CONTAINER_GRID_PADDING + WIDGET_PADDING) * 2) /
(this.props.snapColumns || DEFAULT_GRID_COLUMNS);
if (this.state.snapColumnSpace !== snapColumnSpace) {
this.setState({
snapColumnSpace,
@ -70,9 +73,7 @@ class ContainerWidget extends BaseWidget<
getContainerComponentProps = () => {
const containerProps: ContainerWidgetProps<WidgetProps> = { ...this.props };
containerProps.backgroundColor = this.props.backgroundColor || "white";
if (!this.props.parentId) {
containerProps.containerStyle = "none";
}
return containerProps;
};