diff --git a/app/client/src/api/Api.tsx b/app/client/src/api/Api.tsx index ace2e70327..806b0c0389 100644 --- a/app/client/src/api/Api.tsx +++ b/app/client/src/api/Api.tsx @@ -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); diff --git a/app/client/src/components/designSystems/appsmith/ContainerComponent.tsx b/app/client/src/components/designSystems/appsmith/ContainerComponent.tsx index b9c2ad6a6f..41c941f769 100644 --- a/app/client/src/components/designSystems/appsmith/ContainerComponent.tsx +++ b/app/client/src/components/designSystems/appsmith/ContainerComponent.tsx @@ -9,7 +9,6 @@ const StyledContainerComponent = styled.div` 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` height: 100%; width: 100%; background: ${props => props.backgroundColor}; - padding: ${props => props.theme.spaces[1]}px; + position: relative; }`; /* eslint-disable react/display-name */ diff --git a/app/client/src/components/designSystems/appsmith/PositionedContainer.tsx b/app/client/src/components/designSystems/appsmith/PositionedContainer.tsx index 1b6f4e0372..b53f25304d 100644 --- a/app/client/src/components/designSystems/appsmith/PositionedContainer.tsx +++ b/app/client/src/components/designSystems/appsmith/PositionedContainer.tsx @@ -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; diff --git a/app/client/src/components/editorComponents/DragLayerComponent.tsx b/app/client/src/components/editorComponents/DragLayerComponent.tsx index 2eec2c267a..6153f15e98 100644 --- a/app/client/src/components/editorComponents/DragLayerComponent.tsx +++ b/app/client/src/components/editorComponents/DragLayerComponent.tsx @@ -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 ( + ` 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, }} > - {props.children} + {moveControlIcon} @@ -229,6 +234,9 @@ const DraggableComponent = (props: DraggableComponentProps) => { {editControlIcon} + ); diff --git a/app/client/src/components/editorComponents/DropTargetComponent.tsx b/app/client/src/components/editorComponents/DropTargetComponent.tsx index 2743e1f56c..f98f95904b 100644 --- a/app/client/src/components/editorComponents/DropTargetComponent.tsx +++ b/app/client/src/components/editorComponents/DropTargetComponent.tsx @@ -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, 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 ( -
- {props.children} - -
+ + {props.children} + + + ); }; diff --git a/app/client/src/components/editorComponents/DropTargetMask.tsx b/app/client/src/components/editorComponents/DropTargetMask.tsx index b1f51263eb..80f57cad97 100644 --- a/app/client/src/components/editorComponents/DropTargetMask.tsx +++ b/app/client/src/components/editorComponents/DropTargetMask.tsx @@ -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` 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 = React.useRef( null, ); @@ -44,7 +36,8 @@ export const DropTargetMask = (props: DropTargetMaskProps) => { props.setBounds && props.setBounds(rect); } }); + return ; -}; +}); export default DropTargetMask; diff --git a/app/client/src/components/editorComponents/DropTargetUtils.ts b/app/client/src/components/editorComponents/DropTargetUtils.ts new file mode 100644 index 0000000000..5884bc1121 --- /dev/null +++ b/app/client/src/components/editorComponents/DropTargetUtils.ts @@ -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; +}; diff --git a/app/client/src/components/editorComponents/Dropzone.tsx b/app/client/src/components/editorComponents/Dropzone.tsx index b32db32943..34555290a1 100644 --- a/app/client/src/components/editorComponents/Dropzone.tsx +++ b/app/client/src/components/editorComponents/Dropzone.tsx @@ -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 ( - + + + + ); }; diff --git a/app/client/src/components/editorComponents/ResizableComponent.tsx b/app/client/src/components/editorComponents/ResizableComponent.tsx index fbb28ff8fd..389aa45b6e 100644 --- a/app/client/src/components/editorComponents/ResizableComponent.tsx +++ b/app/client/src/components/editorComponents/ResizableComponent.tsx @@ -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 ( { 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 ( diff --git a/app/client/src/constants/AppConstants.ts b/app/client/src/constants/AppConstants.ts new file mode 100644 index 0000000000..769f76d7f3 --- /dev/null +++ b/app/client/src/constants/AppConstants.ts @@ -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"; diff --git a/app/client/src/constants/DefaultTheme.tsx b/app/client/src/constants/DefaultTheme.tsx index 48b3b7a631..a152fdbbfc 100644 --- a/app/client/src/constants/DefaultTheme.tsx +++ b/app/client/src/constants/DefaultTheme.tsx @@ -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, diff --git a/app/client/src/constants/WidgetConstants.tsx b/app/client/src/constants/WidgetConstants.tsx index e32cfb3446..40208da2f4 100644 --- a/app/client/src/constants/WidgetConstants.tsx +++ b/app/client/src/constants/WidgetConstants.tsx @@ -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"; diff --git a/app/client/src/pages/AppViewer/AppPage.tsx b/app/client/src/pages/AppViewer/AppPage.tsx index 3e7ab4dd8c..eb342c34a7 100644 --- a/app/client/src/pages/AppViewer/AppPage.tsx +++ b/app/client/src/pages/AppViewer/AppPage.tsx @@ -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 ( - + {props.dsl.widgetId && WidgetFactory.createWidget(props.dsl, RenderModes.PAGE)} diff --git a/app/client/src/pages/AppViewer/AppViewerPageContainer.tsx b/app/client/src/pages/AppViewer/AppViewerPageContainer.tsx index d1184e3bb0..a539a20f54 100644 --- a/app/client/src/pages/AppViewer/AppViewerPageContainer.tsx +++ b/app/client/src/pages/AppViewer/AppViewerPageContainer.tsx @@ -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; @@ -69,7 +78,11 @@ class AppViewerPageContainer extends Component { } else if (!this.props.isFetchingPage && !this.props.dsl) { return pageNotFound; } else if (!this.props.isFetchingPage && this.props.dsl) { - return ; + return ( +
+ +
+ ); } } } diff --git a/app/client/src/pages/Editor/Canvas.tsx b/app/client/src/pages/Editor/Canvas.tsx index e78d64133a..3a76f5d799 100644 --- a/app/client/src/pages/Editor/Canvas.tsx +++ b/app/client/src/pages/Editor/Canvas.tsx @@ -36,7 +36,7 @@ const Canvas = (props: CanvasProps) => { }} > - + {props.dsl.widgetId && WidgetFactory.createWidget(props.dsl, RenderModes.CANVAS)} diff --git a/app/client/src/pages/common/ArtBoard.tsx b/app/client/src/pages/common/ArtBoard.tsx index abbb18a422..2dcad182e5 100644 --- a/app/client/src/pages/common/ArtBoard.tsx +++ b/app/client/src/pages/common/ArtBoard.tsx @@ -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; `; diff --git a/app/client/src/reducers/uiReducers/editorReducer.tsx b/app/client/src/reducers/uiReducers/editorReducer.tsx index 4480778346..6abe83a129 100644 --- a/app/client/src/reducers/uiReducers/editorReducer.tsx +++ b/app/client/src/reducers/uiReducers/editorReducer.tsx @@ -116,6 +116,8 @@ export interface EditorReduxState { currentPageId?: string; currentLayoutId?: string; currentPageName?: string; + selectedWidget?: string; + focusedWidget?: string; loadingStates: { saving: boolean; savingError: boolean; diff --git a/app/client/src/sagas/WidgetOperationSagas.tsx b/app/client/src/sagas/WidgetOperationSagas.tsx index b559de6a1b..6c23d0fc6b 100644 --- a/app/client/src/sagas/WidgetOperationSagas.tsx +++ b/app/client/src/sagas/WidgetOperationSagas.tsx @@ -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) { try { @@ -111,7 +112,7 @@ export function* moveSaga(moveAction: ReduxAction) { // 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) { 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; diff --git a/app/client/src/utils/WidgetPropsUtils.tsx b/app/client/src/utils/WidgetPropsUtils.tsx index c914900cb7..5a19a2cbd2 100644 --- a/app/client/src/utils/WidgetPropsUtils.tsx +++ b/app/client/src/utils/WidgetPropsUtils.tsx @@ -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 => { 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, +) => { + 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, 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, 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 "); } }; diff --git a/app/client/src/utils/helpers.tsx b/app/client/src/utils/helpers.tsx index cb2a6909cd..f64fbc8cf5 100644 --- a/app/client/src/utils/helpers.tsx +++ b/app/client/src/utils/helpers.tsx @@ -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]; }; diff --git a/app/client/src/utils/storage.ts b/app/client/src/utils/storage.ts index 7c7dc39ba4..4ee68f01fd 100644 --- a/app/client/src/utils/storage.ts +++ b/app/client/src/utils/storage.ts @@ -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; +}; diff --git a/app/client/src/widgets/BaseWidget.tsx b/app/client/src/widgets/BaseWidget.tsx index 51ab157b98..de59ee14e4 100644 --- a/app/client/src/widgets/BaseWidget.tsx +++ b/app/client/src/widgets/BaseWidget.tsx @@ -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< ); } - return ( - - {this.getCanvasView()} - - ); + return this.getCanvasView(); case RenderModes.PAGE: if (this.props.isVisible) { return ( - + {this.getPageView()} ); @@ -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, }; diff --git a/app/client/src/widgets/ContainerWidget.tsx b/app/client/src/widgets/ContainerWidget.tsx index 1322a52f7c..f47b902a21 100644 --- a/app/client/src/widgets/ContainerWidget.tsx +++ b/app/client/src/widgets/ContainerWidget.tsx @@ -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, 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 = { ...this.props }; containerProps.backgroundColor = this.props.backgroundColor || "white"; - if (!this.props.parentId) { - containerProps.containerStyle = "none"; - } + return containerProps; };