diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/OtherUIFeatures/PreviewMode_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/OtherUIFeatures/PreviewMode_spec.js index 1de9c30b50..ff4ee09730 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/OtherUIFeatures/PreviewMode_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/OtherUIFeatures/PreviewMode_spec.js @@ -18,7 +18,7 @@ describe("Preview mode functionality", function() { it("checks if widgets can be selected or not", function() { // in preview mode, entity explorer and property pane are not visible // Also, draggable and resizable components are not available. - const selector = `.t--widget-buttonwidget`; + const selector = `.t--draggable-buttonwidget`; cy.wait(500); cy.get(selector) .first() diff --git a/app/client/src/components/autoHeight/AutoHeightContainerWrapper.tsx b/app/client/src/components/autoHeight/AutoHeightContainerWrapper.tsx index 7e50512211..b0a228e88d 100644 --- a/app/client/src/components/autoHeight/AutoHeightContainerWrapper.tsx +++ b/app/client/src/components/autoHeight/AutoHeightContainerWrapper.tsx @@ -22,9 +22,7 @@ function AutoHeightContainerWrapper(props: AutoHeightWrapperProps) { if (isCanvas) return <>{children}; const onHeightUpdate = (height: number) => { - requestAnimationFrame(() => { - props.onUpdateDynamicHeight(height); - }); + props.onUpdateDynamicHeight(height); }; const maxDynamicHeight = getWidgetMaxAutoHeight(widgetProps); diff --git a/app/client/src/components/autoHeightOverlay/AutoHeightLimitHandleGroup.tsx b/app/client/src/components/autoHeightOverlay/AutoHeightLimitHandleGroup.tsx index 841a527b4c..91c5104329 100644 --- a/app/client/src/components/autoHeightOverlay/AutoHeightLimitHandleGroup.tsx +++ b/app/client/src/components/autoHeightOverlay/AutoHeightLimitHandleGroup.tsx @@ -151,9 +151,9 @@ const AutoHeightLimitHandleGroup: React.FC = ({ cypressDataID="t--auto-height-overlay-handles-max" height={maxY} isActive={isMaxDotActive} - isColliding={isColliding} + isColliding={false} isDragging={isMaxDotDragging} - label="Max-Height" + label={isColliding ? "Height" : "Max-Height"} onDragCallbacks={onMaxLimitDragCallbacks} onMouseHoverFunctions={onMaxLimitMouseHoverCallbacks} /> diff --git a/app/client/src/components/autoHeightOverlay/index.tsx b/app/client/src/components/autoHeightOverlay/index.tsx index ce1fac83ad..f5519b1384 100644 --- a/app/client/src/components/autoHeightOverlay/index.tsx +++ b/app/client/src/components/autoHeightOverlay/index.tsx @@ -1,6 +1,11 @@ import { focusWidget } from "actions/widgetActions"; -import React, { CSSProperties, memo, useEffect, useMemo } from "react"; -import { useState } from "react"; +import React, { + CSSProperties, + memo, + useEffect, + useMemo, + useReducer, +} from "react"; import { useSelector } from "react-redux"; import { AppState } from "@appsmith/reducers"; import styled from "styled-components"; @@ -18,6 +23,11 @@ import { useHoverState, usePositionedStyles } from "./hooks"; import { getSnappedValues } from "./utils"; import { useAutoHeightUIState } from "utils/hooks/autoHeightUIHooks"; import { LayersContext } from "constants/Layers"; +import { + AutoHeightOverlayUIStateReducer, + createInitialAutoHeightUIState, +} from "./store"; +import { previewModeSelector } from "selectors/editorSelectors"; interface StyledAutoHeightOverlayProps { layerIndex: number; @@ -72,41 +82,93 @@ const AutoHeightOverlay: React.FC = memo( getParentToOpenSelector(props.widgetId), ); const showTableFilterPane = useShowTableFilterPane(); - const { setIsAutoHeightWithLimitsChanging } = useAutoHeightUIState(); - const isAutoHeightWithLimitsChanging = useSelector( - (state: AppState) => state.ui.autoHeightUI.isAutoHeightWithLimitsChanging, + const { + isAutoHeightWithLimitsChanging, + setIsAutoHeightWithLimitsChanging, + } = useAutoHeightUIState(); + + const [autoHeightUIState, autoHeightUIStateDispatch] = useReducer( + AutoHeightOverlayUIStateReducer, + createInitialAutoHeightUIState({ maxDynamicHeight, minDynamicHeight }), ); - const [isMinDotDragging, setIsMinDotDragging] = useState(false); - const [isMaxDotDragging, setIsMaxDotDragging] = useState(false); + const { + isMaxDotDragging, + isMinDotDragging, + maxdY, + maxY, + mindY, + minY, + } = autoHeightUIState; - const [maxY, setMaxY] = useState( - maxDynamicHeight * GridDefaults.DEFAULT_GRID_ROW_HEIGHT, - ); - const [maxdY, setMaxdY] = useState(0); + function setIsMaxDotDragging(isMaxDotDragging: boolean) { + autoHeightUIStateDispatch({ + type: "SET_IS_MAX_DOT_DRAGGING", + payload: { + isMaxDotDragging, + }, + }); + } - const [minY, setMinY] = useState( - minDynamicHeight * GridDefaults.DEFAULT_GRID_ROW_HEIGHT, - ); - const [mindY, setMindY] = useState(0); + function setIsMinDotDragging(isMinDotDragging: boolean) { + autoHeightUIStateDispatch({ + type: "SET_IS_MIN_DOT_DRAGGING", + payload: { + isMinDotDragging, + }, + }); + } + + function setMaxY(maxY: number) { + autoHeightUIStateDispatch({ + type: "SET_MAX_Y", + payload: { + maxY, + }, + }); + } + + function setMinY(minY: number) { + autoHeightUIStateDispatch({ + type: "SET_MIN_Y", + payload: { + minY, + }, + }); + } + + function setMaxdY(maxdY: number) { + autoHeightUIStateDispatch({ + type: "SET_MAX_D_Y", + payload: { + maxdY, + }, + }); + } + + function setMindY(mindY: number) { + autoHeightUIStateDispatch({ + type: "SET_MIN_D_Y", + payload: { + mindY, + }, + }); + } const finalMaxY = maxY + maxdY; const finalMinY = minY + mindY; - // to be included when min and max fields are - // added back to the property pane - // const { - // isPropertyPaneMaxFieldFocused, - // isPropertyPaneMinFieldFocused, - // } = useMaxMinPropertyPaneFieldsFocused(); - useEffect(() => { setMaxY(maxDynamicHeight * GridDefaults.DEFAULT_GRID_ROW_HEIGHT); }, [maxDynamicHeight]); function onAnyDotStop() { - setIsAutoHeightWithLimitsChanging && - setIsAutoHeightWithLimitsChanging(false); + // Tell the Canvas that we've stopped resizing + // Put it later in the stack so that other updates like click, are not propagated to the parent container + setTimeout(() => { + setIsAutoHeightWithLimitsChanging && + setIsAutoHeightWithLimitsChanging(false); + }, 0); selectWidget && selectWidget(props.widgetId); @@ -317,11 +379,13 @@ const AutoHeightOverlayContainer: React.FC = me selectedWidgets, } = useSelector((state: AppState) => state.ui.widgetDragResize); + const isPreviewMode = useSelector(previewModeSelector); + const isWidgetSelected = selectedWidget === widgetId; const multipleWidgetsSelected = selectedWidgets.length > 1; const isHidden = multipleWidgetsSelected || isDragging || isResizing; - if (isWidgetSelected) { + if (isWidgetSelected && !isPreviewMode) { return ; } diff --git a/app/client/src/components/autoHeightOverlay/store.ts b/app/client/src/components/autoHeightOverlay/store.ts new file mode 100644 index 0000000000..d1174da885 --- /dev/null +++ b/app/client/src/components/autoHeightOverlay/store.ts @@ -0,0 +1,100 @@ +import { GridDefaults } from "constants/WidgetConstants"; + +interface AutoHeightLimitsUIState { + isMaxDotDragging: boolean; + isMinDotDragging: boolean; + maxY: number; // the actual value + maxdY: number; // the difference during dragging + minY: number; // the actual value + mindY: number; // the difference during dragging +} + +type SET_MAX_Y = { type: "SET_MAX_Y"; payload: { maxY: number } }; +type SET_MIN_Y = { type: "SET_MIN_Y"; payload: { minY: number } }; +type SET_MAX_D_Y = { type: "SET_MAX_D_Y"; payload: { maxdY: number } }; +type SET_MIN_D_Y = { type: "SET_MIN_D_Y"; payload: { mindY: number } }; +type SET_IS_MIN_DOT_DRAGGING = { + type: "SET_IS_MIN_DOT_DRAGGING"; + payload: { isMinDotDragging: boolean }; +}; + +type SET_IS_MAX_DOT_DRAGGING = { + type: "SET_IS_MAX_DOT_DRAGGING"; + payload: { isMaxDotDragging: boolean }; +}; + +type AutoHeightLimitsUIAction = + | SET_MAX_Y + | SET_MIN_Y + | SET_MAX_D_Y + | SET_MIN_D_Y + | SET_IS_MIN_DOT_DRAGGING + | SET_IS_MAX_DOT_DRAGGING; + +export function AutoHeightOverlayUIStateReducer( + state: AutoHeightLimitsUIState, + action: AutoHeightLimitsUIAction, +) { + if (action.type === "SET_IS_MAX_DOT_DRAGGING") { + return { + ...state, + isMaxDotDragging: action.payload.isMaxDotDragging, + }; + } + + if (action.type === "SET_IS_MIN_DOT_DRAGGING") { + return { + ...state, + isMinDotDragging: action.payload.isMinDotDragging, + }; + } + + if (action.type === "SET_MAX_Y") { + return { + ...state, + maxY: action.payload.maxY, + }; + } + + if (action.type === "SET_MIN_Y") { + return { + ...state, + minY: action.payload.minY, + }; + } + + if (action.type === "SET_MAX_D_Y") { + return { + ...state, + maxdY: action.payload.maxdY, + }; + } + + if (action.type === "SET_MIN_D_Y") { + return { + ...state, + mindY: action.payload.mindY, + }; + } + + return state; +} + +interface CreateInitialAutoHeightUIStateProps { + maxDynamicHeight: number; + minDynamicHeight: number; +} + +export function createInitialAutoHeightUIState({ + maxDynamicHeight, + minDynamicHeight, +}: CreateInitialAutoHeightUIStateProps) { + return { + isMinDotDragging: false, + isMaxDotDragging: false, + maxY: maxDynamicHeight * GridDefaults.DEFAULT_GRID_ROW_HEIGHT, // the actual value + maxdY: 0, // the difference during dragging + minY: minDynamicHeight * GridDefaults.DEFAULT_GRID_ROW_HEIGHT, // the actual value + mindY: 0, // the difference during dragging + }; +} diff --git a/app/client/src/components/editorComponents/DraggableComponent.test.tsx b/app/client/src/components/editorComponents/DraggableComponent.test.tsx index 02298d16d8..809af31b51 100644 --- a/app/client/src/components/editorComponents/DraggableComponent.test.tsx +++ b/app/client/src/components/editorComponents/DraggableComponent.test.tsx @@ -2,9 +2,17 @@ import { canDrag } from "./DraggableComponent"; describe("DraggableComponent", () => { it("it tests draggable canDrag helper function", () => { - expect(canDrag(false, false, { dragDisabled: false }, false)).toBe(true); - expect(canDrag(true, false, { dragDisabled: false }, false)).toBe(false); - expect(canDrag(false, true, { dragDisabled: false }, false)).toBe(false); - expect(canDrag(false, false, { dragDisabled: true }, false)).toBe(false); + expect(canDrag(false, false, { dragDisabled: false }, false, false)).toBe( + true, + ); + expect(canDrag(true, false, { dragDisabled: false }, false, false)).toBe( + false, + ); + expect(canDrag(false, true, { dragDisabled: false }, false, false)).toBe( + false, + ); + expect(canDrag(false, false, { dragDisabled: true }, false, false)).toBe( + false, + ); }); }); diff --git a/app/client/src/components/editorComponents/DraggableComponent.tsx b/app/client/src/components/editorComponents/DraggableComponent.tsx index 190717d816..c712a52f97 100644 --- a/app/client/src/components/editorComponents/DraggableComponent.tsx +++ b/app/client/src/components/editorComponents/DraggableComponent.tsx @@ -9,7 +9,10 @@ import { useShowTableFilterPane, useWidgetDragResize, } from "utils/hooks/dragResizeHooks"; -import { snipingModeSelector } from "selectors/editorSelectors"; +import { + previewModeSelector, + snipingModeSelector, +} from "selectors/editorSelectors"; import { useWidgetSelection } from "utils/hooks/useWidgetSelection"; import { isCurrentWidgetFocused, @@ -56,12 +59,14 @@ export const canDrag = ( isDraggingDisabled: boolean, props: any, isSnipingMode: boolean, + isPreviewMode: boolean, ) => { return ( !isResizingOrDragging && !isDraggingDisabled && !props?.dragDisabled && - !isSnipingMode + !isSnipingMode && + !isPreviewMode ); }; @@ -69,6 +74,7 @@ function DraggableComponent(props: DraggableComponentProps) { // Dispatch hook handy to set a widget as focused/selected const { focusWidget, selectWidget } = useWidgetSelection(); const isSnipingMode = useSelector(snipingModeSelector); + const isPreviewMode = useSelector(previewModeSelector); // Dispatch hook handy to set any `DraggableComponent` as dragging/ not dragging // The value is boolean const { setDraggingCanvas, setDraggingState } = useWidgetDragResize(); @@ -136,6 +142,7 @@ function DraggableComponent(props: DraggableComponentProps) { isDraggingDisabled, props, isSnipingMode, + isPreviewMode, ); const className = `${classNameForTesting}`; const draggableRef = useRef(null); diff --git a/app/client/src/components/editorComponents/DropTargetComponent.tsx b/app/client/src/components/editorComponents/DropTargetComponent.tsx index fff6f987c8..857cec1ba5 100644 --- a/app/client/src/components/editorComponents/DropTargetComponent.tsx +++ b/app/client/src/components/editorComponents/DropTargetComponent.tsx @@ -24,9 +24,13 @@ import { useShowPropertyPane, useCanvasSnapRowsUpdateHook, } from "utils/hooks/dragResizeHooks"; -import { getOccupiedSpacesSelectorForContainer } from "selectors/editorSelectors"; +import { + getOccupiedSpacesSelectorForContainer, + previewModeSelector, +} from "selectors/editorSelectors"; import { useWidgetSelection } from "utils/hooks/useWidgetSelection"; import { getDragDetails } from "sagas/selectors"; +import { useAutoHeightUIState } from "utils/hooks/autoHeightUIHooks"; type DropTargetComponentProps = WidgetProps & { children?: ReactNode; @@ -64,19 +68,52 @@ export const DropTargetContext: Context<{ ) => number | false; }> = createContext({}); -export function DropTargetComponent(props: DropTargetComponentProps) { - const canDropTargetExtend = props.canExtend; - const snapRows = getCanvasSnapRows(props.bottomRow, props.canExtend); +/** + * Gets the dropTarget height + * @param canDropTargetExtend boolean: Can we put widgets below the scrollview in this canvas? + * @param isPreviewMode boolean: Are we in the preview mode + * @param currentHeight number: Current height in the ref and what we have set in the dropTarget + * @param snapRowSpace number: This is a static value actually, GridDefaults.DEFAULT_GRID_ROW_HEIGHT + * @param minHeight number: The minHeight we've set to the widget in the reducer + * @returns number: A new height style to set in the dropTarget. + */ +function getDropTargetHeight( + canDropTargetExtend: boolean, + isPreviewMode: boolean, + currentHeight: number, + snapRowSpace: number, + minHeight: number, +) { + let height = canDropTargetExtend + ? `${Math.max(currentHeight * snapRowSpace, minHeight)}px` + : "100%"; + if (isPreviewMode && canDropTargetExtend) + height = `${currentHeight * snapRowSpace}px`; + return height; +} +export function DropTargetComponent(props: DropTargetComponentProps) { + // Get if this is in preview mode. + const isPreviewMode = useSelector(previewModeSelector); + // Pretty much the shouldScrollContents from the parent container like widget + const canDropTargetExtend = props.canExtend; + // If in preview mode, we don't need that extra row + // This gives us the number of rows + const snapRows = getCanvasSnapRows( + props.bottomRow, + props.canExtend && !isPreviewMode, + ); + + // Are we currently resizing? const isResizing = useSelector( (state: AppState) => state.ui.widgetDragResize.isResizing, ); + // Are we currently dragging? const isDragging = useSelector( (state: AppState) => state.ui.widgetDragResize.isDragging, ); - const isAutoHeightWithLimitsChanging = useSelector( - (state: AppState) => state.ui.autoHeightUI.isAutoHeightWithLimitsChanging, - ); + // Are we changing the auto height limits by dragging the signifiers? + const { isAutoHeightWithLimitsChanging } = useAutoHeightUIState(); // dragDetails contains of info needed for a container jump: // which parent the dragging widget belongs, @@ -87,37 +124,57 @@ export function DropTargetComponent(props: DropTargetComponentProps) { const { draggedOn } = dragDetails; + // All the widgets in this canvas const childWidgets: string[] | undefined = useSelector( (state: AppState) => state.entities.canvasWidgets[props.widgetId]?.children, ); + // The occupied spaces in this canvas. It is a data structure which has the rect values of each child. const selectOccupiedSpaces = useCallback( getOccupiedSpacesSelectorForContainer(props.widgetId), [props.widgetId], ); + // Call the selector above. const occupiedSpacesByChildren = useSelector(selectOccupiedSpaces, equal); + // Put the existing snap rows in a ref. const rowRef = useRef(snapRows); + // This shows the property pane const showPropertyPane = useShowPropertyPane(); - const { deselectAll, focusWidget } = useWidgetSelection(); - const updateCanvasSnapRows = useCanvasSnapRowsUpdateHook(); - const showDragLayer = - (isDragging && draggedOn === props.widgetId) || - isResizing || - isAutoHeightWithLimitsChanging; + const { deselectAll, focusWidget } = useWidgetSelection(); + + // This updates the bottomRow of this canvas, as simple as that + // This also doesn't cause an eval as it uses the action which is + // not registered to cause an eval + const updateCanvasSnapRows = useCanvasSnapRowsUpdateHook(); + + // Everytime we get a new bottomRow, or we toggle shouldScrollContents + // we call this effect useEffect(() => { - const snapRows = getCanvasSnapRows(props.bottomRow, props.canExtend); + const snapRows = getCanvasSnapRows( + props.bottomRow, + props.canExtend && !isPreviewMode, + ); + // If the current ref is not set to the new snaprows we've received (based on bottomRow) if (rowRef.current !== snapRows) { rowRef.current = snapRows; + // This sets the "height" property of the dropTarget div + // This makes the div change heights if new heights are different updateHeight(); - if (canDropTargetExtend) { + // This sets the new rows in the reducer + // Not sure why, as we've just received the values from the props. + // seems like a potential way to cause recursive renders + // See this: https://github.com/appsmithorg/appsmith/pull/18457#issuecomment-1327615572 + if (canDropTargetExtend && !isPreviewMode) { updateCanvasSnapRows(props.widgetId, snapRows); } } - }, [props.bottomRow, props.canExtend]); + }, [props.bottomRow, props.canExtend, isPreviewMode]); + + // If we've stopped dragging, resizing or changing auto height limits useEffect(() => { if (!isDragging || !isResizing || !isAutoHeightWithLimitsChanging) { // bottom row of canvas can increase by any number as user moves/resizes any widget towards the bottom of the canvas @@ -129,14 +186,38 @@ export function DropTargetComponent(props: DropTargetComponentProps) { } }, [isDragging, isResizing, isAutoHeightWithLimitsChanging]); + // Update the drop target height style directly. const updateHeight = () => { if (dropTargetRef.current) { - const height = canDropTargetExtend - ? `${Math.max(rowRef.current * props.snapRowSpace, props.minHeight)}px` - : "100%"; + const height = getDropTargetHeight( + canDropTargetExtend, + isPreviewMode, + rowRef.current, + props.snapRowSpace, + props.minHeight, + ); + dropTargetRef.current.style.height = height; } }; + + const handleFocus = (e: any) => { + // Making sure that we don't deselect the widget + // after we are done dragging the limits in auto height with limits + if (!isResizing && !isDragging && !isAutoHeightWithLimitsChanging) { + if (!props.parentId) { + deselectAll(); + focusWidget && focusWidget(props.widgetId); + showPropertyPane && showPropertyPane(); + } + } + e.preventDefault(); + }; + + /** PREPARE CONTEXT */ + + // Function which computes and updates the height of the dropTarget + // This is used in a context and hence in one of the children of this dropTarget const updateDropTargetRows = ( widgetIdsToExclude: string[], widgetBottomRow: number, @@ -158,23 +239,23 @@ export function DropTargetComponent(props: DropTargetComponentProps) { } return false; }; + // memoizing context values + const contextValue = useMemo(() => { + return { + updateDropTargetRows, + }; + }, [updateDropTargetRows, occupiedSpacesByChildren]); - const handleFocus = (e: any) => { - if (!isResizing && !isDragging && !isAutoHeightWithLimitsChanging) { - if (!props.parentId) { - deselectAll(); - focusWidget && focusWidget(props.widgetId); - showPropertyPane && showPropertyPane(); - } - } - // commenting this out to allow propagation of click events - // e.stopPropagation(); - e.preventDefault(); - }; + /** EO PREPARE CONTEXT */ + + const height = getDropTargetHeight( + canDropTargetExtend, + isPreviewMode, + rowRef.current, + props.snapRowSpace, + props.minHeight, + ); - const height = canDropTargetExtend - ? `${Math.max(rowRef.current * props.snapRowSpace, props.minHeight)}px` - : "100%"; const boxShadow = (isResizing || isDragging || isAutoHeightWithLimitsChanging) && props.widgetId === MAIN_CONTAINER_WIDGET_ID @@ -185,25 +266,19 @@ export function DropTargetComponent(props: DropTargetComponentProps) { height, boxShadow, }; - const dropTargetRef = useRef(null); - - // memoizing context values - const contextValue = useMemo(() => { - return { - updateDropTargetRows, - }; - }, [updateDropTargetRows, occupiedSpacesByChildren]); const shouldOnboard = !(childWidgets && childWidgets.length) && !isDragging && !props.parentId; - if (props.widgetId !== MAIN_CONTAINER_WIDGET_ID) { - // console.log( - // "Dynamic height: Drop Target Height:", - // { height }, - // { snapRows }, - // ); - } + // The drag layer is the one with the grid dots. + // They need to show in certain scenarios + const showDragLayer = + ((isDragging && draggedOn === props.widgetId) || + isResizing || + isAutoHeightWithLimitsChanging) && + !isPreviewMode; + + const dropTargetRef = useRef(null); return ( diff --git a/app/client/src/components/editorComponents/ResizableComponent.tsx b/app/client/src/components/editorComponents/ResizableComponent.tsx index a5a1d7ed51..fad2e5f6ab 100644 --- a/app/client/src/components/editorComponents/ResizableComponent.tsx +++ b/app/client/src/components/editorComponents/ResizableComponent.tsx @@ -32,7 +32,10 @@ import { BottomRightHandleStyles, } from "./ResizeStyledComponents"; import AnalyticsUtil from "utils/AnalyticsUtil"; -import { snipingModeSelector } from "selectors/editorSelectors"; +import { + snipingModeSelector, + previewModeSelector, +} from "selectors/editorSelectors"; import { useWidgetSelection } from "utils/hooks/useWidgetSelection"; import { focusWidget } from "actions/widgetActions"; import { GridDefaults } from "constants/WidgetConstants"; @@ -58,6 +61,7 @@ export const ResizableComponent = memo(function ResizableComponent( const { updateWidget } = useContext(EditorContext); const isSnipingMode = useSelector(snipingModeSelector); + const isPreviewMode = useSelector(previewModeSelector); const showPropertyPane = useShowPropertyPane(); const showTableFilterPane = useShowTableFilterPane(); @@ -243,7 +247,11 @@ export const ResizableComponent = memo(function ResizableComponent( }, [props]); const isEnabled = - !isDragging && isWidgetFocused && !props.resizeDisabled && !isSnipingMode; + !isDragging && + isWidgetFocused && + !props.resizeDisabled && + !isSnipingMode && + !isPreviewMode; const { updateDropTargetRows } = useContext(DropTargetContext); const gridProps = { diff --git a/app/client/src/components/editorComponents/WidgetNameComponent/index.tsx b/app/client/src/components/editorComponents/WidgetNameComponent/index.tsx index 694ce50c81..eca357f556 100644 --- a/app/client/src/components/editorComponents/WidgetNameComponent/index.tsx +++ b/app/client/src/components/editorComponents/WidgetNameComponent/index.tsx @@ -14,7 +14,10 @@ import { useWidgetSelection } from "utils/hooks/useWidgetSelection"; import WidgetFactory from "utils/WidgetFactory"; const WidgetTypes = WidgetFactory.widgetTypes; -import { snipingModeSelector } from "selectors/editorSelectors"; +import { + previewModeSelector, + snipingModeSelector, +} from "selectors/editorSelectors"; import { bindDataToWidget } from "actions/propertyPaneActions"; import { hideErrors } from "selectors/debuggerSelectors"; import { getIsPropertyPaneVisible } from "selectors/propertyPaneSelectors"; @@ -54,6 +57,7 @@ type WidgetNameComponentProps = { export function WidgetNameComponent(props: WidgetNameComponentProps) { const dispatch = useDispatch(); const isSnipingMode = useSelector(snipingModeSelector); + const isPreviewMode = useSelector(previewModeSelector); const showTableFilterPane = useShowTableFilterPane(); // Dispatch hook handy to set a widget as focused/selected const { selectWidget } = useWidgetSelection(); @@ -126,6 +130,7 @@ export function WidgetNameComponent(props: WidgetNameComponentProps) { selectedWidgets.includes(props.widgetId); const shouldShowWidgetName = () => { return ( + !isPreviewMode && !isMultiSelectedWidget && (isSnipingMode ? focusedWidget === props.widgetId diff --git a/app/client/src/constants/WidgetConstants.tsx b/app/client/src/constants/WidgetConstants.tsx index 8e767691e0..2a6e15ca35 100644 --- a/app/client/src/constants/WidgetConstants.tsx +++ b/app/client/src/constants/WidgetConstants.tsx @@ -34,15 +34,13 @@ export type RenderMode = | "COMPONENT_PANE" | "CANVAS" | "PAGE" - | "CANVAS_SELECTED" - | "PREVIEW"; + | "CANVAS_SELECTED"; export const RenderModes: { [id: string]: RenderMode } = { COMPONENT_PANE: "COMPONENT_PANE", CANVAS: "CANVAS", PAGE: "PAGE", CANVAS_SELECTED: "CANVAS_SELECTED", - PREVIEW: "PREVIEW", }; export const CSSUnits: { [id: string]: CSSUnit } = { diff --git a/app/client/src/pages/Editor/Canvas.tsx b/app/client/src/pages/Editor/Canvas.tsx index e0d2fee529..25c6c87407 100644 --- a/app/client/src/pages/Editor/Canvas.tsx +++ b/app/client/src/pages/Editor/Canvas.tsx @@ -94,11 +94,9 @@ const Canvas = memo((props: CanvasProps) => { * background for canvas */ let backgroundForCanvas; - let renderMode = RenderModes.CANVAS; if (isPreviewMode) { backgroundForCanvas = "initial"; - renderMode = RenderModes.PREVIEW; } else { backgroundForCanvas = selectedTheme.properties.colors.backgroundColor; } @@ -125,7 +123,10 @@ const Canvas = memo((props: CanvasProps) => { }} > {props.widgetsStructure.widgetId && - WidgetFactory.createWidget(props.widgetsStructure, renderMode)} + WidgetFactory.createWidget( + props.widgetsStructure, + RenderModes.CANVAS, + )} {isMultiplayerEnabledForUser && ( )} diff --git a/app/client/src/pages/Editor/WidgetsEditor/index.tsx b/app/client/src/pages/Editor/WidgetsEditor/index.tsx index ed6308bcd8..fb91b8c1d6 100644 --- a/app/client/src/pages/Editor/WidgetsEditor/index.tsx +++ b/app/client/src/pages/Editor/WidgetsEditor/index.tsx @@ -29,6 +29,7 @@ import EditorContextProvider from "components/editorComponents/EditorContextProv import Guide from "../GuidedTour/Guide"; import PropertyPaneContainer from "./PropertyPaneContainer"; import CanvasTopSection from "./EmptyCanvasSection"; +import { useAutoHeightUIState } from "utils/hooks/autoHeightUIHooks"; /* eslint-disable react/display-name */ function WidgetsEditor() { @@ -71,16 +72,24 @@ function WidgetsEditor() { }, [isFetchingPage, selectWidget, guidedTourEnabled]); const allowDragToSelect = useAllowEditorDragToSelect(); + const { isAutoHeightWithLimitsChanging } = useAutoHeightUIState(); const handleWrapperClick = useCallback(() => { - if (allowDragToSelect) { + // Making sure that we don't deselect the widget + // after we are done dragging the limits in auto height with limits + if (allowDragToSelect && !isAutoHeightWithLimitsChanging) { focusWidget && focusWidget(); deselectAll && deselectAll(); dispatch(closePropertyPane()); dispatch(closeTableFilterPane()); dispatch(setCanvasSelectionFromEditor(false)); } - }, [allowDragToSelect, focusWidget, deselectAll]); + }, [ + allowDragToSelect, + focusWidget, + deselectAll, + isAutoHeightWithLimitsChanging, + ]); /** * drag event handler for selection drawing diff --git a/app/client/src/reflow/reflowHelpers.ts b/app/client/src/reflow/reflowHelpers.ts index 25b0492ca6..07a27d2987 100644 --- a/app/client/src/reflow/reflowHelpers.ts +++ b/app/client/src/reflow/reflowHelpers.ts @@ -736,7 +736,8 @@ function getMovementMapHelper( collisionTree[accessors.parallelMax] - collisionTree[accessors.parallelMin], occupiedLength: - (movementMap[collisionTree.id].horizontalOccupiedLength || 0) + 1, + (movementMap[collisionTree.id].horizontalOccupiedLength || 0) + + HORIZONTAL_RESIZE_LIMIT, currentEmptySpaces: (movementMap[collisionTree.id].horizontalEmptySpaces as number) || 0, @@ -747,7 +748,10 @@ function getMovementMapHelper( collisionTree[accessors.parallelMax] - collisionTree[accessors.parallelMin], occupiedLength: - (movementMap[collisionTree.id].verticalOccupiedLength || 0) + 1, + (movementMap[collisionTree.id].verticalOccupiedLength || 0) + + (collisionTree.fixedHeight && accessors.directionIndicator < 0 + ? collisionTree.fixedHeight + : VERTICAL_RESIZE_LIMIT), currentEmptySpaces: (movementMap[collisionTree.id].verticalEmptySpaces as number) || 0, }; diff --git a/app/client/src/sagas/autoHeightSagas/batcher.ts b/app/client/src/sagas/autoHeightSagas/batcher.ts index d7bf961c42..bb1c518d84 100644 --- a/app/client/src/sagas/autoHeightSagas/batcher.ts +++ b/app/client/src/sagas/autoHeightSagas/batcher.ts @@ -33,7 +33,6 @@ export function* batchCallsToUpdateWidgetAutoHeightSaga( const isLayoutUpdating: boolean = yield select(getIsDraggingOrResizing); const { height, widgetId } = action.payload; log.debug("Dynamic height: batching update:", { widgetId, height }); - addWidgetToAutoHeightUpdateQueue(widgetId, height); if (isLayoutUpdating) return; yield put({ diff --git a/app/client/src/sagas/autoHeightSagas/containers.ts b/app/client/src/sagas/autoHeightSagas/containers.ts index 59c0bf2cce..f919cc631b 100644 --- a/app/client/src/sagas/autoHeightSagas/containers.ts +++ b/app/client/src/sagas/autoHeightSagas/containers.ts @@ -28,7 +28,7 @@ export function* dynamicallyUpdateContainersSaga() { const isCanvasWidget = widget.type === "CANVAS_WIDGET"; const parent = widget.parentId ? stateWidgets[widget.parentId] : undefined; if (parent?.type === "LIST_WIDGET") return false; - if (!parent) return false; + if (parent === undefined) return false; return isCanvasWidget; }); @@ -112,7 +112,15 @@ export function* dynamicallyUpdateContainersSaga() { let maxBottomRow = minDynamicHeightInRows; // For the child Canvas, use the value in pixels. - let canvasBottomRow = maxBottomRow; + let canvasBottomRow = maxBottomRow + 0; + + // For widgets like Tabs Widget, some of the height is occupied by the + // tabs themselves, the child canvas as a result has less number of rows available + // To accommodate for this, we need to increase the new height by the offset amount. + const canvasHeightOffset: number = getCanvasHeightOffset( + parentContainerWidget.type, + parentContainerWidget, + ); // If this canvas has children // we need to consider the bottom most child for the height @@ -130,19 +138,14 @@ export function* dynamicallyUpdateContainersSaga() { maxBottomRowBasedOnChildren += GridDefaults.CANVAS_EXTENSION_OFFSET; // Set the canvas bottom row as a new variable with a new reference canvasBottomRow = maxBottomRowBasedOnChildren + 0; - // For widgets like Tabs Widget, some of the height is occupied by the - // tabs themselves, the child canvas as a result has less number of rows available - // To accommodate for this, we need to increase the new height by the offset amount. - const canvasHeightOffset: number = getCanvasHeightOffset( - parentContainerWidget.type, - parentContainerWidget, - ); // Add the offset to the total height of the parent widget maxBottomRowBasedOnChildren += canvasHeightOffset; // Get the larger value between the minDynamicHeightInRows and bottomMostRowForChild maxBottomRow = Math.max(maxBottomRowBasedOnChildren, maxBottomRow); + } else { + canvasBottomRow = maxBottomRow - canvasHeightOffset; } // The following makes sure we stay within bounds @@ -156,7 +159,7 @@ export function* dynamicallyUpdateContainersSaga() { } canvasBottomRow = - Math.max(maxBottomRow, canvasBottomRow) * + Math.max(maxBottomRow - canvasHeightOffset, canvasBottomRow) * GridDefaults.DEFAULT_GRID_ROW_HEIGHT; // If we have a new height to set and @@ -189,7 +192,7 @@ export function* dynamicallyUpdateContainersSaga() { } } log.debug( - "Dynamic height: Container computations took:", + "Dynamic height: Container computations time taken:", performance.now() - start, "ms", ); diff --git a/app/client/src/sagas/autoHeightSagas/helpers.ts b/app/client/src/sagas/autoHeightSagas/helpers.ts index b0d4003b9a..4ba1473217 100644 --- a/app/client/src/sagas/autoHeightSagas/helpers.ts +++ b/app/client/src/sagas/autoHeightSagas/helpers.ts @@ -7,8 +7,12 @@ import { } from "reducers/entityReducers/canvasWidgetsReducer"; import { select } from "redux-saga/effects"; import { getWidgetMetaProps, getWidgets } from "sagas/selectors"; -import { previewModeSelector } from "selectors/editorSelectors"; +import { + getCanvasHeightOffset, + previewModeSelector, +} from "selectors/editorSelectors"; import { getAppMode } from "selectors/entitiesSelector"; +import { TreeNode } from "utils/autoHeight/constants"; export function* shouldWidgetsCollapse() { const isPreviewMode: boolean = yield select(previewModeSelector); @@ -50,6 +54,22 @@ export function* getChildOfContainerLikeWidget( } } +export function getParentCurrentHeightInRows( + tree: Record, + parentId: string, + changesSoFar: Record, +) { + // Get the parentHeight in rows + let parentHeightInRows = tree[parentId].bottomRow - tree[parentId].topRow; + + // If the parent has changed so far. + if (changesSoFar.hasOwnProperty(parentId)) { + parentHeightInRows = + changesSoFar[parentId].bottomRow - changesSoFar[parentId].topRow; + } + return parentHeightInRows; +} + export function* getMinHeightBasedOnChildren( widgetId: string, changesSoFar: Record, @@ -67,17 +87,22 @@ export function* getMinHeightBasedOnChildren( const { children = [], parentId } = stateWidgets[widgetId]; // If we need to consider the parent height if (parentId && !ignoreParent) { - // Get the parentHeight in rows - let parentHeightInRows = tree[parentId].bottomRow - tree[parentId].topRow; - - // If the parent has changed so far. - if (changesSoFar.hasOwnProperty(parentId)) { - parentHeightInRows = - changesSoFar[parentId].bottomRow - changesSoFar[parentId].topRow; - } - + const parent = stateWidgets[parentId]; + const parentHeightInRows = getParentCurrentHeightInRows( + tree, + parentId, + changesSoFar, + ); // The canvas will be an extension smaller than the parent? minHeightInRows = parentHeightInRows - GridDefaults.CANVAS_EXTENSION_OFFSET; + + // We will also remove any extra offsets the parent has + // As we're dealing with the child canvas widget here. + const canvasHeightOffset: number = getCanvasHeightOffset( + parent.type, + parent, + ); + minHeightInRows = minHeightInRows - canvasHeightOffset; // If the canvas is empty return the parent's height in rows, without // the canvas extension offset if (!children.length) { diff --git a/app/client/src/sagas/autoHeightSagas/index.ts b/app/client/src/sagas/autoHeightSagas/index.ts index 6f2d799c38..003817cff0 100644 --- a/app/client/src/sagas/autoHeightSagas/index.ts +++ b/app/client/src/sagas/autoHeightSagas/index.ts @@ -8,7 +8,10 @@ import { updateWidgetAutoHeightSaga } from "./widgets"; export default function* autoHeightSagas() { yield all([ takeLatest( - ReduxActionTypes.CHECK_CONTAINERS_FOR_AUTO_HEIGHT, + [ + ReduxActionTypes.CHECK_CONTAINERS_FOR_AUTO_HEIGHT, + ReduxActionTypes.SET_PREVIEW_MODE, + ], dynamicallyUpdateContainersSaga, ), takeEvery( @@ -16,7 +19,7 @@ export default function* autoHeightSagas() { batchCallsToUpdateWidgetAutoHeightSaga, ), debounce( - 100, + 50, ReduxActionTypes.PROCESS_AUTO_HEIGHT_UPDATES, updateWidgetAutoHeightSaga, ), diff --git a/app/client/src/sagas/autoHeightSagas/layoutTree.ts b/app/client/src/sagas/autoHeightSagas/layoutTree.ts index 69ba7ea8eb..9bfc651668 100644 --- a/app/client/src/sagas/autoHeightSagas/layoutTree.ts +++ b/app/client/src/sagas/autoHeightSagas/layoutTree.ts @@ -39,7 +39,7 @@ export function* getLayoutTree(layoutUpdated: boolean) { } } log.debug( - "Dynamic Height: Tree generation took:", + "Dynamic Height: Tree generation time taken:", performance.now() - start, "ms", ); diff --git a/app/client/src/sagas/autoHeightSagas/widgets.ts b/app/client/src/sagas/autoHeightSagas/widgets.ts index 63c935ff5c..abaab1f7ca 100644 --- a/app/client/src/sagas/autoHeightSagas/widgets.ts +++ b/app/client/src/sagas/autoHeightSagas/widgets.ts @@ -25,6 +25,7 @@ import { import { getChildOfContainerLikeWidget, getMinHeightBasedOnChildren, + getParentCurrentHeightInRows, shouldWidgetsCollapse, } from "./helpers"; import { updateMultipleWidgetPropertiesAction } from "actions/controlActions"; @@ -63,6 +64,7 @@ export function* updateWidgetAutoHeightSaga() { const updates = getAutoHeightUpdateQueue(); log.debug("Dynamic Height: updates to process", { updates }); const start = performance.now(); + let shouldRecomputeContainers = false; const shouldCollapse: boolean = yield shouldWidgetsCollapse(); @@ -96,6 +98,8 @@ export function* updateWidgetAutoHeightSaga() { let minDynamicHeightInPixels = getWidgetMinAutoHeight(widget) * GridDefaults.DEFAULT_GRID_ROW_HEIGHT; + if (widget.type === "TABS_WIDGET") shouldRecomputeContainers = true; + // In case of a widget going invisible in view mode if (updates[widgetId] === 0) { if (shouldCollapse && isAutoHeightEnabledForWidget(widget)) { @@ -138,7 +142,6 @@ export function* updateWidgetAutoHeightSaga() { newHeightInPixels / GridDefaults.DEFAULT_GRID_ROW_HEIGHT, ), parentId: widget.parentId, - hasScroll: widget.isCanvas ? true : false, }); } else if (widget) { // For widgets like Modal Widget. (Rather this assumes that it is only the modal widget which needs a change) @@ -297,26 +300,14 @@ export function* updateWidgetAutoHeightSaga() { // Add extra rows, this is to accommodate for padding and margins in the parent minCanvasHeightInRows += GridDefaults.CANVAS_EXTENSION_OFFSET; - // Setting this in a variable, as this will be the total scroll height in the canvas. - const minCanvasHeightInPixels = - minCanvasHeightInRows * GridDefaults.DEFAULT_GRID_ROW_HEIGHT; - // We need to make sure that the canvas widget doesn't have - // any extra scroll, to this end, we need to add the `minHeight` update - // for the canvas widgets. Canvas Widgets are never updated in other flows - // As they simply take up whatever space the parent has, but this doesn't effect - // the `minHeight`, which leads to scroll if the `minHeight` is a larger value. - // Also, for canvas widgets, the values are in pure pixels instead of rows. - widgetsToUpdate[parentCanvasWidgetId] = [ - { - propertyPath: "bottomRow", - propertyValue: minCanvasHeightInPixels, - }, - { - propertyPath: "minHeight", - propertyValue: minCanvasHeightInPixels, - }, - ]; + // For widgets like Tabs Widget, some of the height is occupied by the + // tabs themselves, the child canvas as a result has less number of rows available + // To accommodate for this, we need to increase the new height by the offset amount. + const canvasHeightOffset: number = getCanvasHeightOffset( + parentContainerLikeWidget.type, + parentContainerLikeWidget, + ); // Widgets need to consider changing heights, only if they have dynamic height // enabled. @@ -329,17 +320,30 @@ export function* updateWidgetAutoHeightSaga() { minHeightInRows = Math.max( minHeightInRows, - minCanvasHeightInRows, + minCanvasHeightInRows + canvasHeightOffset, ); - // For widgets like Tabs Widget, some of the height is occupied by the - // tabs themselves, the child canvas as a result has less number of rows available - // To accommodate for this, we need to increase the new height by the offset amount. - const canvasHeightOffset: number = getCanvasHeightOffset( - parentContainerLikeWidget.type, - parentContainerLikeWidget, - ); - minHeightInRows += canvasHeightOffset; + // Setting this in a variable, as this will be the total scroll height in the canvas. + const minCanvasHeightInPixels = + (minHeightInRows - canvasHeightOffset) * + GridDefaults.DEFAULT_GRID_ROW_HEIGHT; + + // We need to make sure that the canvas widget doesn't have + // any extra scroll, to this end, we need to add the `minHeight` update + // for the canvas widgets. Canvas Widgets are never updated in other flows + // As they simply take up whatever space the parent has, but this doesn't effect + // the `minHeight`, which leads to scroll if the `minHeight` is a larger value. + // Also, for canvas widgets, the values are in pure pixels instead of rows. + widgetsToUpdate[parentCanvasWidgetId] = [ + { + propertyPath: "bottomRow", + propertyValue: minCanvasHeightInPixels, + }, + { + propertyPath: "minHeight", + propertyValue: minCanvasHeightInPixels, + }, + ]; // Make sure we're not overflowing the max height bounds const maxDynamicHeight = getWidgetMaxAutoHeight( @@ -436,6 +440,36 @@ export function* updateWidgetAutoHeightSaga() { ]); } } + } else { + let parentContainerHeightInRows = getParentCurrentHeightInRows( + dynamicHeightLayoutTree, + parentContainerLikeWidget.widgetId, + changesSoFar, + ); + + parentContainerHeightInRows -= canvasHeightOffset; + + // Setting this in a variable, as this will be the total scroll height in the canvas. + const minCanvasHeightInPixels = + Math.max(minCanvasHeightInRows, parentContainerHeightInRows) * + GridDefaults.DEFAULT_GRID_ROW_HEIGHT; + + // We need to make sure that the canvas widget doesn't have + // any extra scroll, to this end, we need to add the `minHeight` update + // for the canvas widgets. Canvas Widgets are never updated in other flows + // As they simply take up whatever space the parent has, but this doesn't effect + // the `minHeight`, which leads to scroll if the `minHeight` is a larger value. + // Also, for canvas widgets, the values are in pure pixels instead of rows. + widgetsToUpdate[parentCanvasWidgetId] = [ + { + propertyPath: "bottomRow", + propertyValue: minCanvasHeightInPixels, + }, + { + propertyPath: "minHeight", + propertyValue: minCanvasHeightInPixels, + }, + ]; } } } @@ -471,10 +505,8 @@ export function* updateWidgetAutoHeightSaga() { // Convert the changesSoFar (this are the computed changes) // To the widgetsToUpdate data structure for final reducer update. + for (const changedWidgetId in changesSoFar) { - const hasScroll = Object.values(expectedUpdates).find( - (entry) => entry.widgetId === changedWidgetId, - )?.hasScroll; const { originalBottomRow, originalTopRow } = dynamicHeightLayoutTree[ changedWidgetId ]; @@ -497,19 +529,22 @@ export function* updateWidgetAutoHeightSaga() { propertyValue: originalBottomRow, }, ]; - if (hasScroll) { - const containerLikeWidget = stateWidgets[changedWidgetId]; + const containerLikeWidget = stateWidgets[changedWidgetId]; - if ( - Array.isArray(containerLikeWidget.children) && - containerLikeWidget.children.length > 0 - ) { - const childWidgetId: - | string - | undefined = yield getChildOfContainerLikeWidget( - containerLikeWidget, - ); - if (childWidgetId) { + if ( + Array.isArray(containerLikeWidget.children) && + containerLikeWidget.children.length > 0 + ) { + const childWidgetId: + | string + | undefined = yield getChildOfContainerLikeWidget( + containerLikeWidget, + ); + + if (childWidgetId) { + const childCanvasWidget = stateWidgets[childWidgetId]; + const isCanvasWidget = childCanvasWidget?.type === "CANVAS_WIDGET"; + if (isCanvasWidget) { let canvasHeight: number = yield getMinHeightBasedOnChildren( childWidgetId, changesSoFar, @@ -517,11 +552,7 @@ export function* updateWidgetAutoHeightSaga() { dynamicHeightLayoutTree, ); canvasHeight += GridDefaults.CANVAS_EXTENSION_OFFSET; - const canvasHeightOffset: number = getCanvasHeightOffset( - containerLikeWidget.type, - containerLikeWidget, - ); - canvasHeight -= canvasHeightOffset; + const propertyUpdates = [ { propertyPath: "minHeight", @@ -547,13 +578,16 @@ export function* updateWidgetAutoHeightSaga() { } log.debug("Dynamic height: Widgets to update:", { widgetsToUpdate }); + if (Object.keys(widgetsToUpdate).length > 0) { // Push all updates to the CanvasWidgetsReducer. // Note that we're not calling `UPDATE_LAYOUT` // as we don't need to trigger an eval yield put(updateMultipleWidgetPropertiesAction(widgetsToUpdate)); resetAutoHeightUpdateQueue(); - yield put(generateAutoHeightLayoutTreeAction(false, false)); + yield put( + generateAutoHeightLayoutTreeAction(shouldRecomputeContainers, false), + ); } log.debug( diff --git a/app/client/src/selectors/editorSelectors.tsx b/app/client/src/selectors/editorSelectors.tsx index 1728ebdc2e..a897f285a4 100644 --- a/app/client/src/selectors/editorSelectors.tsx +++ b/app/client/src/selectors/editorSelectors.tsx @@ -178,9 +178,9 @@ export const selectURLSlugs = createSelector( ); export const getRenderMode = (state: AppState) => { - if (state.ui.editor.isPreviewMode) return RenderModes.PREVIEW; - else if (state.entities.app.mode === APP_MODE.EDIT) return RenderModes.CANVAS; - else return RenderModes.PAGE; + return state.entities.app.mode === APP_MODE.EDIT + ? RenderModes.CANVAS + : RenderModes.PAGE; }; export const getViewModePageList = createSelector( diff --git a/app/client/src/utils/autoHeight/constants.ts b/app/client/src/utils/autoHeight/constants.ts index 4c4f254a7a..3b8272fed0 100644 --- a/app/client/src/utils/autoHeight/constants.ts +++ b/app/client/src/utils/autoHeight/constants.ts @@ -5,6 +5,7 @@ export type TreeNode = { bottomRow: number; originalTopRow: number; originalBottomRow: number; + distanceToNearestAbove: number; }; export type NodeSpace = { diff --git a/app/client/src/utils/autoHeight/generateTree.test.ts b/app/client/src/utils/autoHeight/generateTree.test.ts index 1a15087973..f723195aef 100644 --- a/app/client/src/utils/autoHeight/generateTree.test.ts +++ b/app/client/src/utils/autoHeight/generateTree.test.ts @@ -17,6 +17,7 @@ describe("Generate Auto Height Layout tree", () => { bottomRow: 30, originalBottomRow: 30, originalTopRow: 0, + distanceToNearestAbove: 0, }, "2": { aboves: [], @@ -25,6 +26,7 @@ describe("Generate Auto Height Layout tree", () => { bottomRow: 30, originalBottomRow: 30, originalTopRow: 0, + distanceToNearestAbove: 0, }, }; @@ -36,7 +38,7 @@ describe("Generate Auto Height Layout tree", () => { it("Does conflict when part of the boxes overlap horizontally", () => { const input: NodeSpace[] = [ { left: 0, right: 100, top: 0, bottom: 30, id: "1" }, - { left: 80, top: 30, bottom: 40, right: 120, id: "2" }, + { left: 80, top: 40, bottom: 80, right: 120, id: "2" }, ]; const previousTree: Record = {}; const layoutUpdated = false; @@ -48,14 +50,16 @@ describe("Generate Auto Height Layout tree", () => { bottomRow: 30, originalBottomRow: 30, originalTopRow: 0, + distanceToNearestAbove: 0, }, "2": { aboves: ["1"], belows: [], - topRow: 30, - bottomRow: 40, - originalBottomRow: 40, - originalTopRow: 30, + topRow: 40, + bottomRow: 80, + originalBottomRow: 80, + originalTopRow: 40, + distanceToNearestAbove: 10, }, }; @@ -77,6 +81,7 @@ describe("Generate Auto Height Layout tree", () => { bottomRow: 30, originalBottomRow: 20, originalTopRow: 0, + distanceToNearestAbove: 0, }, "2": { aboves: ["1"], @@ -85,6 +90,7 @@ describe("Generate Auto Height Layout tree", () => { bottomRow: 40, originalBottomRow: 30, originalTopRow: 20, + distanceToNearestAbove: 0, }, }; const layoutUpdated = false; @@ -96,6 +102,7 @@ describe("Generate Auto Height Layout tree", () => { bottomRow: 30, originalBottomRow: 20, originalTopRow: 0, + distanceToNearestAbove: 0, }, "2": { aboves: ["1"], @@ -104,6 +111,7 @@ describe("Generate Auto Height Layout tree", () => { bottomRow: 40, originalBottomRow: 30, originalTopRow: 20, + distanceToNearestAbove: 0, }, }; @@ -125,6 +133,7 @@ describe("Generate Auto Height Layout tree", () => { bottomRow: 30, originalBottomRow: 20, originalTopRow: 0, + distanceToNearestAbove: 0, }, "2": { aboves: ["1"], @@ -133,6 +142,7 @@ describe("Generate Auto Height Layout tree", () => { bottomRow: 40, originalBottomRow: 30, originalTopRow: 20, + distanceToNearestAbove: 0, }, }; const layoutUpdated = true; @@ -144,6 +154,7 @@ describe("Generate Auto Height Layout tree", () => { bottomRow: 30, originalBottomRow: 30, originalTopRow: 0, + distanceToNearestAbove: 0, }, "2": { aboves: ["1"], @@ -152,6 +163,7 @@ describe("Generate Auto Height Layout tree", () => { bottomRow: 40, originalBottomRow: 40, originalTopRow: 30, + distanceToNearestAbove: 0, }, }; diff --git a/app/client/src/utils/autoHeight/generateTree.ts b/app/client/src/utils/autoHeight/generateTree.ts index 9c03a91de6..dbfb13ee8d 100644 --- a/app/client/src/utils/autoHeight/generateTree.ts +++ b/app/client/src/utils/autoHeight/generateTree.ts @@ -1,6 +1,7 @@ import { areIntersecting } from "utils/boxHelpers"; import { pushToArray } from "utils/helpers"; import { MAX_BOX_SIZE, NodeSpace, TreeNode } from "./constants"; +import { getNearestAbove } from "./helpers"; // This function uses the spaces occupied by sibling boxes and provides us with // a data structure which defines the relative vertical positioning of the boxes @@ -76,6 +77,7 @@ export function generateTree( if (originalBottomRow === undefined || layoutUpdated) { originalBottomRow = currentSpace.bottom - MAX_BOX_SIZE; } + tree[currentSpace.id] = { aboves: aboveMap[currentSpace.id] || [], belows: belowMap[currentSpace.id] || [], @@ -83,9 +85,21 @@ export function generateTree( bottomRow: currentSpace.bottom - MAX_BOX_SIZE, originalTopRow, originalBottomRow, + distanceToNearestAbove: 0, }; } } + for (const boxId in tree) { + // For each box, get the nearest above node + // Then get the distance between this node and the nearest above + // We'll try to maintain this distance when reflowing due to auto height + const nearestAbove = getNearestAbove(tree, boxId, {}); + if (nearestAbove.length > 0) { + tree[boxId].distanceToNearestAbove = + tree[boxId].topRow - tree[nearestAbove[0]].bottomRow; + } + } + return tree; } diff --git a/app/client/src/utils/autoHeight/helpers.ts b/app/client/src/utils/autoHeight/helpers.ts new file mode 100644 index 0000000000..de6b058595 --- /dev/null +++ b/app/client/src/utils/autoHeight/helpers.ts @@ -0,0 +1,58 @@ +import { TreeNode } from "./constants"; + +/** + * Gets the nearest above box for the current box. Including the aboves which have changes so far. + * + * @param tree: Auto Height Layout Tree + * @param effectedBoxId: Current box in consideration + * @param repositionedBoxes: Boxes repositioned so far + * @returns An array of boxIds which are above and nearest the effectedBoxId + */ +export function getNearestAbove( + tree: Record, + effectedBoxId: string, + repositionedBoxes: Record, +) { + // Get all the above boxes + const aboves = tree[effectedBoxId].aboves; + // We're trying to find the nearest boxes above this box + + return aboves.reduce((prev: string[], next: string) => { + if (!prev[0]) return [next]; + // Get the bottomRow of the above box + let nextBottomRow = tree[next].bottomRow; + let prevBottomRow = tree[prev[0]].bottomRow; + // If we've already repositioned this, use the new bottomRow of the box + if (repositionedBoxes[next]) { + nextBottomRow = repositionedBoxes[next].bottomRow; + } + if (repositionedBoxes[prev[0]]) { + prevBottomRow = repositionedBoxes[prev[0]].bottomRow; + } + + // If the current box's (next) bottomRow is larger than the previous + // This (next) box is the bottom most above so far + if (nextBottomRow > prevBottomRow) return [next]; + // If this (next) box's bottom row is the same as the previous + // We have two bottom most boxes + else if (nextBottomRow === prevBottomRow) { + if ( + repositionedBoxes[prev[0]] && + repositionedBoxes[prev[0]].bottomRow === + repositionedBoxes[prev[0]].topRow + ) { + return prev; + } + if ( + repositionedBoxes[next] && + repositionedBoxes[next].bottomRow === repositionedBoxes[next].topRow + ) { + return [next]; + } + return [...prev, next]; + } + // This (next) box's bottom row is lower than the boxes selected so far + // so, we ignore it. + else return prev; + }, []); +} diff --git a/app/client/src/utils/autoHeight/reflow.test.ts b/app/client/src/utils/autoHeight/reflow.test.ts index 21190ae337..22a3960fbf 100644 --- a/app/client/src/utils/autoHeight/reflow.test.ts +++ b/app/client/src/utils/autoHeight/reflow.test.ts @@ -18,6 +18,7 @@ describe("reflow", () => { bottomRow: box1BottomRow, originalTopRow: box1TopRow, originalBottomRow: box1BottomRow, + distanceToNearestAbove: 0, }, "2": { aboves: ["1"], @@ -26,6 +27,7 @@ describe("reflow", () => { bottomRow: box2BottomRow, originalTopRow: box2TopRow, originalBottomRow: box2BottomRow, + distanceToNearestAbove: 10, }, }; @@ -70,6 +72,7 @@ describe("reflow", () => { bottomRow: box1BottomRow, originalTopRow: box1OriginalTopRow, originalBottomRow: box1OriginalBottomRow, + distanceToNearestAbove: 0, }, "2": { aboves: ["1"], @@ -78,6 +81,7 @@ describe("reflow", () => { bottomRow: box2BottomRow, originalTopRow: box2OriginalTopRow, originalBottomRow: box2OriginalBottomRow, + distanceToNearestAbove: 10, }, }; @@ -122,6 +126,7 @@ describe("reflow", () => { bottomRow: box1BottomRow, originalTopRow: box1OriginalTopRow, originalBottomRow: box1OriginalBottomRow, + distanceToNearestAbove: 0, }, "2": { aboves: ["1"], @@ -130,6 +135,7 @@ describe("reflow", () => { bottomRow: box2BottomRow, originalTopRow: box2OriginalTopRow, originalBottomRow: box2OriginalBottomRow, + distanceToNearestAbove: 40, }, }; @@ -173,6 +179,7 @@ describe("reflow", () => { bottomRow: 120, originalBottomRow: 20, originalTopRow: 10, + distanceToNearestAbove: 0, }; const tree: Record = { @@ -183,6 +190,7 @@ describe("reflow", () => { bottomRow: box1BottomRow, originalTopRow: box1OriginalTopRow, originalBottomRow: box1OriginalBottomRow, + distanceToNearestAbove: 0, }, "2": { aboves: ["1", "3"], @@ -191,6 +199,7 @@ describe("reflow", () => { bottomRow: box2BottomRow, originalTopRow: box2OriginalTopRow, originalBottomRow: box2OriginalBottomRow, + distanceToNearestAbove: 20, }, "3": box3, }; @@ -208,8 +217,8 @@ describe("reflow", () => { bottomRow: box1BottomRow + box1DeltaHeight, }, "2": { - topRow: 130, - bottomRow: 170, + topRow: 140, + bottomRow: 180, }, }; diff --git a/app/client/src/utils/autoHeight/reflow.ts b/app/client/src/utils/autoHeight/reflow.ts index 5fdb41e72a..9118b50af4 100644 --- a/app/client/src/utils/autoHeight/reflow.ts +++ b/app/client/src/utils/autoHeight/reflow.ts @@ -1,92 +1,5 @@ import { TreeNode } from "./constants"; - -/** - * - * @param tree : Auto Height Layout Tree - * @param effectedBoxId : Current box in consideration - * @param aboveId : Above box which may or maynot have changed - * @param offsetSoFar : Offset of the above box, or changes to be applied so far - * @returns : The offset expected to be applied to the effectedBoxId. This is how much this box should move - */ -export function getNegativeOffset( - tree: Record, - effectedBoxId: string, - aboveId: string, - offsetSoFar = 0, -): number { - if (offsetSoFar <= 0) { - // Let's take in to account the old spacing between the effected box and bottom most above box - // when the layout was last updated. - const oldSpacing = - tree[effectedBoxId].originalTopRow - tree[aboveId].originalBottomRow; - // Let's compute the spacing between the effected box and bottom most above box - const currentSpacing = tree[effectedBoxId].topRow - tree[aboveId].bottomRow; - // If the old spacing is less than current spacing and the offset of the bottom most above, - // we need to make sure that we're sticking to the original spacing between the bottom most above - // and the current effected box. - // Note: This applies only if the offset is negative, which is to say that the box is to move up - if (oldSpacing < currentSpacing + offsetSoFar) { - return oldSpacing + offsetSoFar - currentSpacing; - } - } - return offsetSoFar; -} -/** - * Gets the nearest above box for the current box. Including the aboves which have changes so far. - * - * @param tree: Auto Height Layout Tree - * @param effectedBoxId: Current box in consideration - * @param repositionedBoxes: Boxes repositioned so far - * @returns An array of boxIds which are above and nearest the effectedBoxId - */ -export function getNearestAbove( - tree: Record, - effectedBoxId: string, - repositionedBoxes: Record, -) { - // Get all the above boxes - const aboves = tree[effectedBoxId].aboves; - // We're trying to find the nearest boxes above this box - - return aboves.reduce((prev: string[], next: string) => { - if (!prev[0]) return [next]; - // Get the bottomRow of the above box - let nextBottomRow = tree[next].bottomRow; - let prevBottomRow = tree[prev[0]].bottomRow; - // If we've already repositioned this, use the new bottomRow of the box - if (repositionedBoxes[next]) { - nextBottomRow = repositionedBoxes[next].bottomRow; - } - if (repositionedBoxes[prev[0]]) { - prevBottomRow = repositionedBoxes[prev[0]].bottomRow; - } - - // If the current box's (next) bottomRow is larger than the previous - // This (next) box is the bottom most above so far - if (nextBottomRow > prevBottomRow) return [next]; - // If this (next) box's bottom row is the same as the previous - // We have two bottom most boxes - else if (nextBottomRow === prevBottomRow) { - if ( - repositionedBoxes[prev[0]] && - repositionedBoxes[prev[0]].bottomRow === - repositionedBoxes[prev[0]].topRow - ) { - return prev; - } - if ( - repositionedBoxes[next] && - repositionedBoxes[next].bottomRow === repositionedBoxes[next].topRow - ) { - return [next]; - } - return [...prev, next]; - } - // This (next) box's bottom row is lower than the boxes selected so far - // so, we ignore it. - else return prev; - }, []); -} +import { getNearestAbove } from "./helpers"; function getAllEffectedBoxes( effectorBoxId: string, @@ -159,39 +72,23 @@ export function computeChangeInPositionBasedOnDelta( // If the above box has been effected by another box change height // Or, if this above box itself has changed height if (effectedBoxes.includes(aboveId) || delta[aboveId]) { - // In case the above box has changed heights - const _aboveOffset = repositionedBoxes[aboveId] - ? repositionedBoxes[aboveId].bottomRow - tree[aboveId].bottomRow - : 0; - - // If so far, we haven't got any _offset updates - // This can happen if this is the first aboveId we're checking - if (_offset === undefined) _offset = _aboveOffset; - - const negativeOffset = getNegativeOffset( - tree, - effectedBoxId, - aboveId, - _aboveOffset, - ); - - // If the bottom most above (_aboveOffset), has moved down (either by increasing height and/or due to its above) - // Let's take the effected boxs' change to be the max of _offset and _aboveOffset - // The _offset so far will be due to other bottomMostAbove effecting this effected box. - if (_aboveOffset > 0) _offset = Math.max(_aboveOffset, _offset); - // If the bottom most above (_aboveOffset) has moved up (either by decreasing height and/or due to its above) - // Let's take the Min (negative values, so max offset in the upward direction) of the _aboveOffset, _offset, negativeOffset. - else if (_aboveOffset < 0) { - _offset = Math.min(_aboveOffset, _offset, negativeOffset); + // If we have the above repositioned + if (repositionedBoxes[aboveId]) { + // Get the new expected top row of this effectedBox + const newTopRow = + repositionedBoxes[aboveId].bottomRow + + tree[effectedBoxId].distanceToNearestAbove; + // Get the offset this effectedBox needs to consider moving + _offset = newTopRow - tree[effectedBoxId].topRow; + } else { + // Since the above hasn't changed, don't change this. + _offset = 0; } } else { - // Stick to the widget above if the bottomMost above box hasn't changed - // TODO(abhinav): Here we may want to use the same logic as negativeOffset using originals as done previously. - // Test this. - // Let's take in to account the old spacing between the effected box and bottom most above box - // when the layout was last updated. - const negativeOffset = getNegativeOffset(tree, effectedBoxId, aboveId); - _offset = negativeOffset; + // Maintain distance from the bottom most above. + const newTopRow = + tree[aboveId].bottomRow + tree[effectedBoxId].distanceToNearestAbove; + _offset = newTopRow - tree[effectedBoxId].topRow; } } diff --git a/app/client/src/utils/hooks/autoHeightUIHooks.ts b/app/client/src/utils/hooks/autoHeightUIHooks.ts index ead5599967..59e6aa14b6 100644 --- a/app/client/src/utils/hooks/autoHeightUIHooks.ts +++ b/app/client/src/utils/hooks/autoHeightUIHooks.ts @@ -1,10 +1,14 @@ import { ReduxActionTypes } from "ce/constants/ReduxActionConstants"; +import { AppState } from "ce/reducers"; import { useCallback } from "react"; -import { useDispatch } from "react-redux"; +import { useDispatch, useSelector } from "react-redux"; export const useAutoHeightUIState = () => { const dispatch = useDispatch(); return { + isAutoHeightWithLimitsChanging: useSelector( + (state: AppState) => state.ui.autoHeightUI.isAutoHeightWithLimitsChanging, + ), setIsAutoHeightWithLimitsChanging: useCallback( (isAutoHeightWithLimitsChanging: boolean) => { dispatch({ diff --git a/app/client/src/utils/hooks/useAllowEditorDragToSelect.ts b/app/client/src/utils/hooks/useAllowEditorDragToSelect.ts index 594d4036c9..a1399115b6 100644 --- a/app/client/src/utils/hooks/useAllowEditorDragToSelect.ts +++ b/app/client/src/utils/hooks/useAllowEditorDragToSelect.ts @@ -1,5 +1,8 @@ import { AppState } from "@appsmith/reducers"; -import { snipingModeSelector } from "selectors/editorSelectors"; +import { + previewModeSelector, + snipingModeSelector, +} from "selectors/editorSelectors"; import { useSelector } from "store"; export const useAllowEditorDragToSelect = () => { @@ -28,6 +31,12 @@ export const useAllowEditorDragToSelect = () => { // True when any widget is dragging or resizing, including this one const isResizingOrDragging = !!isResizing || !!isDragging || !!isSelecting; const isSnipingMode = useSelector(snipingModeSelector); + const isPreviewMode = useSelector(previewModeSelector); - return !isResizingOrDragging && !isDraggingDisabled && !isSnipingMode; + return ( + !isResizingOrDragging && + !isDraggingDisabled && + !isSnipingMode && + !isPreviewMode + ); }; diff --git a/app/client/src/widgets/BaseWidget.tsx b/app/client/src/widgets/BaseWidget.tsx index b1a9bb0154..e518c4ad02 100644 --- a/app/client/src/widgets/BaseWidget.tsx +++ b/app/client/src/widgets/BaseWidget.tsx @@ -420,7 +420,7 @@ abstract class BaseWidget< if ( isAutoHeightEnabledForWidget(this.props) && - !this.props.isAutoGeneratedWidget //To skip list widget's auto generated widgets + !this.props.isAutoGeneratedWidget // To skip list widget's auto generated widgets ) { return ( `1px solid ${props.theme.colors.bodyBG}`}; -// } -// `; - const scrollContents = css` overflow-y: auto; position: absolute; @@ -111,39 +96,6 @@ export interface TabsContainerProps { isScrollable: boolean; } -// const TabsContainer = styled.div` -// position: absolute; -// top: 0; -// overflow-x: auto; -// overflow-y: hidden; -// display: flex; -// height: ${TAB_CONTAINER_HEIGHT}; -// background: ${(props) => props.theme.colors.builderBodyBG}; -// overflow: hidden; -// border-bottom: ${(props) => `1px solid ${props.theme.colors.bodyBG}`}; - -// overflow-x: scroll; -// &::-webkit-scrollbar { -// display: none; -// } -// /* Hide scrollbar for IE, Edge and Firefox */ -// -ms-overflow-style: none; /* IE and Edge */ -// scrollbar-width: none; /* Firefox */ - -// && { -// width: 100%; -// display: flex; -// justify-content: flex-start; -// align-items: flex-end; -// } -// `; - -// type TabProps = { -// selected?: boolean; -// onClick: (e: React.MouseEvent) => void; -// primaryColor: string; -// }; - const Container = styled.div` width: 100%; align-items: flex-end; @@ -194,19 +146,6 @@ export interface ScrollNavControlProps { className?: string; } -// function ScrollNavControl(props: ScrollNavControlProps) { -// const { className, disabled, icon, onClick } = props; -// return ( -//