diff --git a/app/client/package.json b/app/client/package.json index eec0818de8..49f2533503 100644 --- a/app/client/package.json +++ b/app/client/package.json @@ -46,6 +46,7 @@ "react-dom": "^16.7.0", "react-netlify-identity": "^0.1.9", "react-redux": "^6.0.0", + "react-rnd": "^10.1.1", "react-router": "^5.0.1", "react-router-dom": "^5.0.1", "react-scripts": "^3.1.1", diff --git a/app/client/src/actions/pageActions.tsx b/app/client/src/actions/pageActions.tsx index ae0bfbf849..29d9137f59 100644 --- a/app/client/src/actions/pageActions.tsx +++ b/app/client/src/actions/pageActions.tsx @@ -111,8 +111,10 @@ export type WidgetDelete = { export type WidgetResize = { widgetId: string; - width: number; // delta/diff - height: number; // delta/diff + leftColumn: number; + rightColumn: number; + topRow: number; + bottomRow: number; }; export const updateWidget = ( diff --git a/app/client/src/constants/DefaultTheme.tsx b/app/client/src/constants/DefaultTheme.tsx index 62e02cfdb7..34b6872f51 100644 --- a/app/client/src/constants/DefaultTheme.tsx +++ b/app/client/src/constants/DefaultTheme.tsx @@ -24,9 +24,9 @@ export type Theme = { }; export const theme: Theme = { - radii: [0, 4, 8, 10, 20], + radii: [0, 4, 8, 10, 20, 50], fontSizes: [0, 10, 12, 14, 16, 18, 24, 28, 32, 48, 64], - spaces: [0, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24], + spaces: [0, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24], fontWeights: [0, 400, 500, 700], colors: { primary: Colors.GREEN, diff --git a/app/client/src/constants/ReduxActionConstants.tsx b/app/client/src/constants/ReduxActionConstants.tsx index 6cecce4af4..a40adcf4e7 100644 --- a/app/client/src/constants/ReduxActionConstants.tsx +++ b/app/client/src/constants/ReduxActionConstants.tsx @@ -48,6 +48,7 @@ export const ReduxActionErrorTypes: { [key: string]: string } = { FETCH_PAGE_ERROR: "FETCH_PAGE_ERROR", SAVE_PAGE_ERROR: "SAVE_PAGE_ERROR", FETCH_WIDGET_CARDS_ERROR: "FETCH_WIDGET_CARDS_ERROR", + WIDGET_OPERATION_ERROR: "WIDGET_OPERATION_ERROR", }; export type ReduxActionErrorType = (typeof ReduxActionErrorTypes)[keyof typeof ReduxActionErrorTypes]; diff --git a/app/client/src/editorComponents/BaseComponent.tsx b/app/client/src/editorComponents/BaseComponent.tsx index 3389208508..886a8be1fe 100644 --- a/app/client/src/editorComponents/BaseComponent.tsx +++ b/app/client/src/editorComponents/BaseComponent.tsx @@ -22,6 +22,7 @@ export interface BaseStyle { export interface ComponentProps { widgetId: string; + widgetName?: string; style: BaseStyle; } diff --git a/app/client/src/editorComponents/ContainerComponent.tsx b/app/client/src/editorComponents/ContainerComponent.tsx index eb6a53b842..8e67cf1e73 100644 --- a/app/client/src/editorComponents/ContainerComponent.tsx +++ b/app/client/src/editorComponents/ContainerComponent.tsx @@ -24,6 +24,18 @@ export const Container = styled("div")` left: 0; top: 0; width: 100%; + padding: ${props => props.theme.spaces[8]}px ${props => + props.theme.spaces[1]}px ${props => props.theme.spaces[1]}px; + &:after { + content: "${props => props.widgetName}"; + position: absolute; + left: ${props => props.theme.spaces[1]}px; + top: ${props => props.theme.spaces[1]}px; + font-size: ${props => props.theme.fontSizes[2]}px; + color: ${props => props.theme.colors.containerBorder}; + text-align: left; + width: 100%; + } `; export const FocusContext: Context<{ diff --git a/app/client/src/editorComponents/DraggableComponent.tsx b/app/client/src/editorComponents/DraggableComponent.tsx index bb4ada102e..fa8347ec2c 100644 --- a/app/client/src/editorComponents/DraggableComponent.tsx +++ b/app/client/src/editorComponents/DraggableComponent.tsx @@ -18,6 +18,7 @@ const DraggableWrapper = styled.div<{ show: boolean }>` &:hover > div { display: block; } + display: block; `; const DragHandle = styled.div` @@ -86,6 +87,10 @@ const DraggableComponent = (props: DraggableComponentProps) => { top: props.style ? props.style.yPosition + props.style.yPositionUnit : 0, + minWidth: + props.style.componentWidth + (props.style.widthUnit || "px"), + minHeight: + props.style.componentHeight + (props.style.heightUnit || "px"), }} > diff --git a/app/client/src/editorComponents/DropTargetComponent.tsx b/app/client/src/editorComponents/DropTargetComponent.tsx index b6285d1714..d5697a7157 100644 --- a/app/client/src/editorComponents/DropTargetComponent.tsx +++ b/app/client/src/editorComponents/DropTargetComponent.tsx @@ -35,7 +35,7 @@ export const DropTargetComponent = (props: DropTargetComponentProps) => { accept: Object.values(WidgetFactory.getWidgetTypes()), drop(widget: WidgetProps & Partial, monitor) { // Make sure we're dropping in this container. - if (isOver && monitor.canDrop()) { + if (isOver) { props.updateWidget && props.updateWidget( ...widgetOperationParams( @@ -52,13 +52,17 @@ export const DropTargetComponent = (props: DropTargetComponentProps) => { }, // Collect isOver for ui transforms when hovering over this component collect: monitor => ({ - isOver: !!monitor.isOver({ shallow: true }), + isOver: + (monitor.isOver({ shallow: true }) && + props.widgetId !== monitor.getItem().widgetId) || + (monitor.isOver() && props.widgetId !== monitor.getItem().widgetId), }), // 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 // Also only if the dropzone does not overlap any existing children canDrop: (widget, monitor) => { - if (monitor.isOver({ shallow: true })) { + // Check if the draggable is the same as the dropTarget + if (isOver) { return noCollision( monitor.getClientOffset() as XYCoord, props.snapColumnSpace, @@ -85,7 +89,7 @@ export const DropTargetComponent = (props: DropTargetComponentProps) => {
{ width: wrapperProps.width + "px", top: wrapperProps.top + "px", height: wrapperProps.height + "px", - background: props.canDrop ? "blue" : "red", + background: props.canDrop ? theme.colors.hover : theme.colors.error, }} /> ); diff --git a/app/client/src/editorComponents/ResizableComponent.tsx b/app/client/src/editorComponents/ResizableComponent.tsx index d24eea983f..81f9e8d4f4 100644 --- a/app/client/src/editorComponents/ResizableComponent.tsx +++ b/app/client/src/editorComponents/ResizableComponent.tsx @@ -1,12 +1,13 @@ import React, { useContext } from "react"; import styled from "styled-components"; -import { Resizable, ResizeDirection } from "re-resizable"; +import { Rnd } from "react-rnd"; +import { XYCoord } from "react-dnd"; import { WidgetProps, WidgetOperations } from "../widgets/BaseWidget"; import { ContainerProps, ParentBoundsContext } from "./ContainerComponent"; export type ResizableComponentProps = WidgetProps & ContainerProps; -const ResizableContainer = styled(Resizable)` +const ResizableContainer = styled(Rnd)` position: relative; z-index: 10; border: ${props => { @@ -16,52 +17,77 @@ const ResizableContainer = styled(Resizable)` &:before { content: ""; position: absolute; - width: 8px; - height: 8px; - border-radius: 50%; + width: ${props => props.theme.spaces[2]}px; + height: ${props => props.theme.spaces[2]}px; + border-radius: ${props => props.theme.radii[5]}%; z-index: 9; background: ${props => props.theme.colors.containerBorder}; } &:after { - right: -4px; - top: 50%; + right: -${props => props.theme.spaces[1]}px; + top: calc(50% - ${props => props.theme.spaces[1]}px); } &:before { - left: calc(50%); - top: calc(100% - 4px); + left: calc(50% - ${props => props.theme.spaces[1]}px); + bottom: -${props => props.theme.spaces[1]}px; } `; export const ResizableComponent = (props: ResizableComponentProps) => { const { boundingParent } = useContext(ParentBoundsContext); + let bounds = "body"; + if (boundingParent && boundingParent.current) { + bounds = "." + boundingParent.current.className.split(" ")[1]; + } const updateSize = ( e: Event, - dir: ResizeDirection, + dir: any, ref: any, delta: { width: number; height: number }, + position: XYCoord, ) => { + const leftColumn = props.leftColumn + position.x / props.parentColumnSpace; + const topRow = props.topRow + position.y / props.parentRowSpace; + + const rightColumn = + props.rightColumn + (delta.width + position.x) / props.parentColumnSpace; + const bottomRow = + props.bottomRow + (delta.height + position.y) / props.parentRowSpace; + props.updateWidget && - props.updateWidget(WidgetOperations.RESIZE, props.widgetId, delta); + props.updateWidget(WidgetOperations.RESIZE, props.widgetId, { + leftColumn, + rightColumn, + topRow, + bottomRow, + }); }; return ( diff --git a/app/client/src/pages/Editor/WidgetCardsPane.tsx b/app/client/src/pages/Editor/WidgetCardsPane.tsx index 6e7652af77..5c4a048979 100644 --- a/app/client/src/pages/Editor/WidgetCardsPane.tsx +++ b/app/client/src/pages/Editor/WidgetCardsPane.tsx @@ -19,7 +19,7 @@ const CardsPaneWrapper = styled.div` const CardsWrapper = styled.div` display: grid; grid-template-columns: 1fr 1fr 1fr; - grid-gap: ${props => props.theme.spaces[2]}px; + grid-gap: ${props => props.theme.spaces[1]}px; justify-items: stretch; align-items: stretch; `; diff --git a/app/client/src/pages/Editor/index.tsx b/app/client/src/pages/Editor/index.tsx index 9c0a334015..a4c781d9ee 100644 --- a/app/client/src/pages/Editor/index.tsx +++ b/app/client/src/pages/Editor/index.tsx @@ -105,15 +105,14 @@ const mapStateToProps = (state: AppState): EditorReduxState => { state.ui.editor.pageWidgetId, state.entities, ); - const configs = state.entities.widgetConfig.config; const cards = state.ui.editor.cards; const groups: string[] = Object.keys(cards); groups.forEach((group: string) => { - cards[group] = cards[group].map((widget: WidgetCardProps) => ({ - ...widget, - ...configs[widget.type], - })); + cards[group] = cards[group].map((widget: WidgetCardProps) => { + const { rows, columns } = state.entities.widgetConfig.config[widget.type]; + return { ...widget, rows, columns }; + }); }); return { diff --git a/app/client/src/sagas/ErrorSagas.tsx b/app/client/src/sagas/ErrorSagas.tsx index 6e3ba93c51..754999101f 100644 --- a/app/client/src/sagas/ErrorSagas.tsx +++ b/app/client/src/sagas/ErrorSagas.tsx @@ -24,6 +24,7 @@ export function* validateResponse(response: ApiResponse) { export function* errorSaga(errorAction: ReduxAction<{ error: any }>) { // Just a pass through for now. // Add procedures to customize errors here + console.log(errorAction.payload.error); yield put({ type: ReduxActionTypes.REPORT_ERROR, payload: { diff --git a/app/client/src/sagas/WidgetOperationSagas.tsx b/app/client/src/sagas/WidgetOperationSagas.tsx index 88504f0705..c5e856738c 100644 --- a/app/client/src/sagas/WidgetOperationSagas.tsx +++ b/app/client/src/sagas/WidgetOperationSagas.tsx @@ -1,5 +1,6 @@ import { ReduxActionTypes, + ReduxActionErrorTypes, ReduxAction, } from "../constants/ReduxActionConstants"; import { @@ -9,13 +10,18 @@ import { WidgetDelete, } from "../actions/pageActions"; import { FlattenedWidgetProps } from "../reducers/entityReducers/canvasWidgetsReducer"; -import { getWidgets, getWidget, getWidgetParent } from "./selectors"; +import { + getWidgets, + getWidget, + getWidgetParent, + getDefaultWidgetConfig, +} from "./selectors"; import { generateWidgetProps, - updateWidgetSize, updateWidgetPosition, } from "../utils/WidgetPropsUtils"; import { put, select, takeEvery, takeLatest, all } from "redux-saga/effects"; +import { getNextWidgetName } from "../utils/AppsmithUtils"; export function* addChildSaga(addChildAction: ReduxAction) { try { @@ -31,7 +37,7 @@ export function* addChildSaga(addChildAction: ReduxAction) { } = addChildAction.payload; const widget: FlattenedWidgetProps = yield select(getWidget, widgetId); const widgets = yield select(getWidgets); - + const defaultWidgetConfig = yield select(getDefaultWidgetConfig, type); const childWidget = generateWidgetProps( widget, type, @@ -41,6 +47,8 @@ export function* addChildSaga(addChildAction: ReduxAction) { rows, parentRowSpace, parentColumnSpace, + getNextWidgetName(type, widgets), + defaultWidgetConfig, ); widgets[childWidget.widgetId] = childWidget; if (widget && widget.children) { @@ -51,11 +59,13 @@ export function* addChildSaga(addChildAction: ReduxAction) { type: ReduxActionTypes.UPDATE_LAYOUT, payload: { widgets }, }); - } catch (err) { + } catch (error) { yield put({ - type: ReduxActionTypes.WIDGET_OPERATION_ERROR, - action: ReduxActionTypes.WIDGET_ADD_CHILD, - ...err, + type: ReduxActionErrorTypes.WIDGET_OPERATION_ERROR, + payload: { + action: ReduxActionTypes.WIDGET_ADD_CHILD, + error, + }, }); } } @@ -74,12 +84,13 @@ export function* deleteSaga(deleteAction: ReduxAction) { type: ReduxActionTypes.UPDATE_LAYOUT, payload: { widgets }, }); - } catch (err) { - console.log(err); + } catch (error) { yield put({ - type: ReduxActionTypes.WIDGET_OPERATION_ERROR, - action: ReduxActionTypes.WIDGET_DELETE, - ...err, + type: ReduxActionErrorTypes.WIDGET_OPERATION_ERROR, + payload: { + action: ReduxActionTypes.WIDGET_DELETE, + error, + }, }); } } @@ -97,7 +108,7 @@ export function* moveSaga(moveAction: ReduxAction) { // Replace widget with update widget props widgets[widgetId] = widget; // If the parent has changed i.e parentWidgetId is not parent.widgetId - if (parent.widgetId !== parentWidgetId) { + if (parent.widgetId !== parentWidgetId && widgetId !== parentWidgetId) { // Remove from the previous parent parent.children = parent.children.filter( (child: string) => child !== widgetId, @@ -110,34 +121,44 @@ export function* moveSaga(moveAction: ReduxAction) { type: ReduxActionTypes.UPDATE_LAYOUT, payload: { widgets }, }); - } catch (err) { + } catch (error) { yield put({ - type: ReduxActionTypes.WIDGET_OPERATION_ERROR, - action: ReduxActionTypes.WIDGET_MOVE, - ...err, + type: ReduxActionErrorTypes.WIDGET_OPERATION_ERROR, + payload: { + action: ReduxActionTypes.WIDGET_MOVE, + error, + }, }); } } export function* resizeSaga(resizeAction: ReduxAction) { try { - const { widgetId, height, width } = resizeAction.payload; + const { + widgetId, + leftColumn, + rightColumn, + topRow, + bottomRow, + } = resizeAction.payload; let widget: FlattenedWidgetProps = yield select(getWidget, widgetId); const widgets = yield select(getWidgets); - widget = updateWidgetSize(widget, height, width); + widget = { ...widget, leftColumn, rightColumn, topRow, bottomRow }; widgets[widgetId] = widget; yield put({ type: ReduxActionTypes.UPDATE_LAYOUT, payload: { widgets }, }); - } catch (err) { + } catch (error) { yield put({ - type: ReduxActionTypes.WIDGET_OPERATION_ERROR, - action: ReduxActionTypes.WIDGET_RESIZE, - ...err, + type: ReduxActionErrorTypes.WIDGET_OPERATION_ERROR, + payload: { + action: ReduxActionTypes.WIDGET_RESIZE, + error, + }, }); } } diff --git a/app/client/src/sagas/selectors.tsx b/app/client/src/sagas/selectors.tsx index a32b31a914..fc1c5eacf8 100644 --- a/app/client/src/sagas/selectors.tsx +++ b/app/client/src/sagas/selectors.tsx @@ -1,7 +1,7 @@ import { AppState } from "../reducers"; import { FlattenedWidgetProps } from "../reducers/entityReducers/canvasWidgetsReducer"; import { WidgetProps } from "../widgets/BaseWidget"; - +import { WidgetType } from "../constants/WidgetConstants"; export const getWidgets = ( state: AppState, ): { [widgetId: string]: FlattenedWidgetProps } => { @@ -35,3 +35,14 @@ export const getWidgetParent = ( widget.children.indexOf(widgetId) > -1, ); }; + +export const getDefaultWidgetConfig = ( + state: AppState, + type: WidgetType, +): Partial => { + const configs = state.entities.widgetConfig.config; + const widgetConfig = { ...configs[type] }; + delete widgetConfig.rows; + delete widgetConfig.columns; + return widgetConfig; +}; diff --git a/app/client/src/utils/AppsmithUtils.tsx b/app/client/src/utils/AppsmithUtils.tsx index af48741e0f..ba3636bba0 100644 --- a/app/client/src/utils/AppsmithUtils.tsx +++ b/app/client/src/utils/AppsmithUtils.tsx @@ -12,6 +12,8 @@ import FontFaceObserver from "fontfaceobserver"; import PropertyControlRegistry from "./PropertyControlRegistry"; import WidgetBuilderRegistry from "./WidgetRegistry"; import { Property } from "../api/ActionAPI"; +import { WidgetType } from "../constants/WidgetConstants"; +import { FlattenedWidgetProps } from "../reducers/entityReducers/canvasWidgetsReducer"; import _ from "lodash"; export const createReducer = ( @@ -60,3 +62,29 @@ export const mapToPropList = (map: Record): Property[] => { return { key: key, value: value }; }); }; + +export const getNextWidgetName = ( + type: WidgetType, + widgets: { + [id: string]: FlattenedWidgetProps; + }, +) => { + const prefix = type + .split("_") + .filter(token => token !== "WIDGET") + .join("") + .toLowerCase(); + const usedIndices: number[] = Object.values(widgets).map(widget => { + if (widget.type === type) { + const ind = widget.widgetName + ? parseInt(widget.widgetName.split(prefix)[1], 10) + : 0; + return Number.isNaN(ind) ? 0 : ind; + } + return 0; + }) as number[]; + + const lastIndex = Math.max(...usedIndices); + + return prefix + (lastIndex + 1); +}; diff --git a/app/client/src/utils/WidgetPropsUtils.tsx b/app/client/src/utils/WidgetPropsUtils.tsx index 13966d9f3f..4b4b1a7160 100644 --- a/app/client/src/utils/WidgetPropsUtils.tsx +++ b/app/client/src/utils/WidgetPropsUtils.tsx @@ -71,7 +71,10 @@ export const isDropZoneOccupied = ( ) => { if (occupied) { occupied = occupied.filter(widgetDetails => { - return widgetDetails.id !== widget.widgetId; + return ( + widgetDetails.id !== widget.widgetId && + widgetDetails.parentId !== widget.widgetId + ); }); for (let i = 0; i < occupied.length; i++) { if (areIntersecting(occupied[i], offset)) { @@ -211,6 +214,8 @@ export const generateWidgetProps = ( rows: number, parentRowSpace: number, parentColumnSpace: number, + widgetName: string, + widgetConfig: Partial, ): ContainerWidgetProps => { if (parent && parent.snapColumns && parent.snapRows) { const sizes = { @@ -230,10 +235,11 @@ export const generateWidgetProps = ( }; } return { + ...widgetConfig, type, executeAction: () => {}, widgetId: generateReactKey(), - widgetName: generateReactKey(), //TODO: figure out what this is to populate appropriately + widgetName: widgetName || generateReactKey(), //TODO: figure out what this is to populate appropriately isVisible: true, parentColumnSpace, parentRowSpace, diff --git a/app/client/src/widgets/ButtonWidget.tsx b/app/client/src/widgets/ButtonWidget.tsx index 63e4a04c92..75f56b8b39 100644 --- a/app/client/src/widgets/ButtonWidget.tsx +++ b/app/client/src/widgets/ButtonWidget.tsx @@ -14,6 +14,7 @@ class ButtonWidget extends BaseWidget { { diff --git a/app/client/src/widgets/ContainerWidget.tsx b/app/client/src/widgets/ContainerWidget.tsx index 768e95d5b8..49f283bfee 100644 --- a/app/client/src/widgets/ContainerWidget.tsx +++ b/app/client/src/widgets/ContainerWidget.tsx @@ -35,9 +35,10 @@ class ContainerWidget extends BaseWidget< super.componentDidUpdate(previousProps); let snapColumnSpace = this.state.snapColumnSpace; if (this.state.componentWidth) - snapColumnSpace = + snapColumnSpace = Math.floor( this.state.componentWidth / - (this.props.snapColumns || DEFAULT_GRID_COLUMNS); + (this.props.snapColumns || DEFAULT_GRID_COLUMNS), + ); if (this.state.snapColumnSpace !== snapColumnSpace) { this.setState({ snapColumnSpace, @@ -67,6 +68,7 @@ class ContainerWidget extends BaseWidget< }} isRoot={!this.props.parentId} orientation={this.props.orientation || "VERTICAL"} + widgetName={this.props.widgetName} > {_.map(this.props.children, this.renderChildWidget)} @@ -77,6 +79,7 @@ class ContainerWidget extends BaseWidget< return this.props.children ? this.props.children.map(child => ({ id: child.widgetId, + parentId: this.props.widgetId, left: child.leftColumn, top: child.topRow, bottom: child.bottomRow, @@ -87,7 +90,6 @@ class ContainerWidget extends BaseWidget< getCanvasView() { const style = this.getPositionStyle(); const occupiedSpaces = this.getOccupiedSpaces(); - const renderDraggableComponent = ( {