diff --git a/app/client/src/components/designSystems/appsmith/PositionedContainer.tsx b/app/client/src/components/designSystems/appsmith/PositionedContainer.tsx index c17bfecd8d..acffc2b0af 100644 --- a/app/client/src/components/designSystems/appsmith/PositionedContainer.tsx +++ b/app/client/src/components/designSystems/appsmith/PositionedContainer.tsx @@ -74,7 +74,8 @@ export function PositionedContainer(props: PositionedContainerProps) { const reflowWidth = reflowedPosition?.width; const reflowHeight = reflowedPosition?.height; const reflowEffected = isCurrentCanvasReflowing && reflowedPosition; - const hasReflowedPosition = reflowEffected && reflowX + reflowY !== 0; + const hasReflowedPosition = + reflowEffected && (reflowX !== 0 || reflowY !== 0); const hasReflowedDimensions = reflowEffected && ((reflowHeight && reflowHeight !== props.style.componentHeight) || diff --git a/app/client/src/pages/common/CanvasArenas/hooks/ContainerJumpMetric.ts b/app/client/src/pages/common/CanvasArenas/hooks/ContainerJumpMetric.ts new file mode 100644 index 0000000000..8c39cbcfdc --- /dev/null +++ b/app/client/src/pages/common/CanvasArenas/hooks/ContainerJumpMetric.ts @@ -0,0 +1,20 @@ +//class to maintain containerJump metrics across containers. +export default class ContainerJumpMetrics { + private containerJumpValues: T = {} as T; + + public setMetrics(args: T) { + this.containerJumpValues = { + ...args, + }; + } + + public getMetrics() { + return { + ...this.containerJumpValues, + }; + } + + public clearMetrics() { + this.containerJumpValues = {} as T; + } +} diff --git a/app/client/src/pages/common/CanvasArenas/hooks/useBlocksToBeDraggedOnCanvas.ts b/app/client/src/pages/common/CanvasArenas/hooks/useBlocksToBeDraggedOnCanvas.ts index 85fc679859..d0ab292d73 100644 --- a/app/client/src/pages/common/CanvasArenas/hooks/useBlocksToBeDraggedOnCanvas.ts +++ b/app/client/src/pages/common/CanvasArenas/hooks/useBlocksToBeDraggedOnCanvas.ts @@ -29,6 +29,7 @@ import { stopReflowAction } from "actions/reflowActions"; import { DragDetails } from "reducers/uiReducers/dragResizeReducer"; import { getIsReflowing } from "selectors/widgetReflowSelectors"; import { XYCord } from "./useCanvasDragging"; +import ContainerJumpMetrics from "./ContainerJumpMetric"; export interface WidgetDraggingUpdateParams extends WidgetDraggingBlock { updateWidgetParams: WidgetOperationParams; @@ -46,6 +47,28 @@ export type WidgetDraggingBlock = { detachFromLayout?: boolean; }; +const containerJumpMetrics = new ContainerJumpMetrics<{ + speed?: number; + acceleration?: number; + movingInto?: string; +}>(); + +// This method is called on drop, +// This method logs the metrics container jump and marks it as successful container jump, +// If widget has moves into a container and drops there. +const logContainerJumpOnDrop = () => { + const { acceleration, movingInto, speed } = containerJumpMetrics.getMetrics(); + // If it is dropped into a container after jumping, then + if (movingInto) { + AnalyticsUtil.logEvent("CONTAINER_JUMP", { + speed: speed, + acceleration: acceleration, + isAccidental: false, + }); + } + containerJumpMetrics.clearMetrics(); +}; + export const useBlocksToBeDraggedOnCanvas = ({ noPad, snapColumnSpace, @@ -104,6 +127,52 @@ export const useBlocksToBeDraggedOnCanvas = ({ const { updateWidget } = useContext(EditorContext); const allWidgets = useSelector(getWidgets); + + //This method is called whenever a there is a canvas change. + //canvas is the Layer inside the widgets or on main container where widgets are positioned or dragged. + //This method records the container jump metrics when a widget moves into a container from main Canvas, + // if the widget moves back to the main Canvas then, it is marked as accidental container jump. + const logContainerJump = ( + dropTargetWidgetId: string, + dragSpeed?: number, + dragAcceleration?: number, + ) => { + //If triggered on the same canvas that it started dragging on return + if (!dragDetails.draggedOn || dropTargetWidgetId === dragDetails.draggedOn) + return; + + const { + acceleration, + movingInto, + speed, + } = containerJumpMetrics.getMetrics(); + + // record Only + // if it was not previously recorded + // if not moving into mainContainer + // dragSpeed and dragAcceleration is not undefined + if ( + !movingInto && + dropTargetWidgetId !== MAIN_CONTAINER_WIDGET_ID && + dragSpeed && + dragAcceleration + ) { + containerJumpMetrics.setMetrics({ + speed: dragSpeed, + acceleration: dragAcceleration, + movingInto: dropTargetWidgetId, + }); + } // record only for mainContainer jumps, + //If it is coming back to main canvas after moving into a container then it is a accidental container jump + else if (movingInto && dropTargetWidgetId === MAIN_CONTAINER_WIDGET_ID) { + AnalyticsUtil.logEvent("CONTAINER_JUMP", { + speed: speed, + acceleration: acceleration, + isAccidental: true, + }); + containerJumpMetrics.clearMetrics(); + } + }; const getDragCenterSpace = () => { if (dragCenter && dragCenter.widgetId) { // Dragging by widget @@ -203,6 +272,7 @@ export const useBlocksToBeDraggedOnCanvas = ({ drawingBlocks: WidgetDraggingBlock[], reflowedPositionsUpdatesWidgets: OccupiedSpace[], ) => { + logContainerJumpOnDrop(); const reflowedBlocks: WidgetDraggingBlock[] = reflowedPositionsUpdatesWidgets.map( (each) => { const widget = allWidgets[each.id]; @@ -412,6 +482,7 @@ export const useBlocksToBeDraggedOnCanvas = ({ isNewWidgetInitialTargetCanvas, isResizing, lastDraggedCanvas, + logContainerJump, occSpaces, draggingSpaces, onDrop, diff --git a/app/client/src/pages/common/CanvasArenas/hooks/useCanvasDragging.ts b/app/client/src/pages/common/CanvasArenas/hooks/useCanvasDragging.ts index 536c3ae9f2..2edad5aabb 100644 --- a/app/client/src/pages/common/CanvasArenas/hooks/useCanvasDragging.ts +++ b/app/client/src/pages/common/CanvasArenas/hooks/useCanvasDragging.ts @@ -21,6 +21,7 @@ import { ReflowInterface, useReflow } from "utils/hooks/useReflow"; import { getDraggingSpacesFromBlocks, getDropZoneOffsets, + getMousePositionsOnCanvas, noCollision, } from "utils/WidgetPropsUtils"; import { @@ -28,11 +29,22 @@ import { WidgetDraggingBlock, } from "./useBlocksToBeDraggedOnCanvas"; import { useCanvasDragToScroll } from "./useCanvasDragToScroll"; +import ContainerJumpMetrics from "./ContainerJumpMetric"; export interface XYCord { x: number; y: number; } + +const CONTAINER_JUMP_ACC_THRESHOLD = 8000; +const CONTAINER_JUMP_SPEED_THRESHOLD = 800; + +//Since useCanvasDragging's Instance changes during container jump, metrics is stored outside +const containerJumpThresholdMetrics = new ContainerJumpMetrics<{ + speed?: number; + acceleration?: number; +}>(); + export const useCanvasDragging = ( slidingArenaRef: React.RefObject, stickyCanvasRef: React.RefObject, @@ -62,6 +74,7 @@ export const useCanvasDragging = ( isNewWidgetInitialTargetCanvas, isResizing, lastDraggedCanvas, + logContainerJump, occSpaces, onDrop, parentDiff, @@ -199,7 +212,13 @@ export const useCanvasDragging = ( movementLimitMap?: MovementLimitMap; bottomMostRow: number; movementMap: ReflowedSpaceMap; - } = { movementLimitMap: {}, bottomMostRow: 0, movementMap: {} }; + isIdealToJumpContainer: boolean; + } = { + movementLimitMap: {}, + bottomMostRow: 0, + movementMap: {}, + isIdealToJumpContainer: false, + }; let lastMousePosition = { x: 0, y: 0, @@ -295,6 +314,13 @@ export const useCanvasDragging = ( relativeStartPoints.top || defaultHandlePositions.top; } if (!isCurrentDraggedCanvas) { + //Called when canvas Changes + const { + acceleration, + speed, + } = containerJumpThresholdMetrics.getMetrics(); + logContainerJump(widgetId, speed, acceleration); + containerJumpThresholdMetrics.clearMetrics(); // we can just use canvasIsDragging but this is needed to render the relative DragLayerComponent setDraggingCanvas(widgetId); } @@ -316,18 +342,12 @@ export const useCanvasDragging = ( }; const canReflowForCurrentMouseMove = () => { - const { - maxNegativeAcc, - maxPositiveAcc, - maxSpeed, - prevAcceleration, - prevSpeed, - } = mouseAttributesRef.current; - const limit = Math.abs( - prevAcceleration < 0 ? maxNegativeAcc : maxPositiveAcc, - ); + const { prevAcceleration, prevSpeed } = mouseAttributesRef.current; const acceleration = Math.abs(prevAcceleration); - return acceleration < limit / 10 || prevSpeed < maxSpeed / 10; + return ( + acceleration < CONTAINER_JUMP_ACC_THRESHOLD || + prevSpeed < CONTAINER_JUMP_SPEED_THRESHOLD + ); }; const getMouseMoveDirection = (event: any) => { if (lastMousePosition) { @@ -366,7 +386,9 @@ export const useCanvasDragging = ( const canReflowBasedOnMouseSpeed = canReflowForCurrentMouseMove(); const isReflowing = !isEmpty(currentReflowParams.movementMap); const canReflow = - reflowEnabled && !currentRectanglesToDraw[0].detachFromLayout; + reflowEnabled && + !currentRectanglesToDraw[0].detachFromLayout && + !dropDisabled; const currentBlock = currentRectanglesToDraw[0]; const [leftColumn, topRow] = getDropZoneOffsets( snapColumnSpace, @@ -380,6 +402,7 @@ export const useCanvasDragging = ( y: 0, }, ); + const mousePosition = getMousePositionsOnCanvas(e, gridProps); const needsReflow = !( lastSnappedPosition.leftColumn === leftColumn && lastSnappedPosition.topRow === topRow @@ -408,11 +431,28 @@ export const useCanvasDragging = ( !canReflowBasedOnMouseSpeed, firstMove, immediateExitContainer, + mousePosition, ); } if (isReflowing) { - const { movementLimitMap } = currentReflowParams; + const { + isIdealToJumpContainer, + movementLimitMap, + } = currentReflowParams; + + if (isIdealToJumpContainer) { + const { + prevAcceleration, + prevSpeed: speed, + } = mouseAttributesRef.current; + const acceleration = Math.abs(prevAcceleration); + containerJumpThresholdMetrics.setMetrics({ + speed, + acceleration, + }); + } + for (const block of currentRectanglesToDraw) { const isWithinParentBoundaries = noCollision( { x: block.left, y: block.top }, diff --git a/app/client/src/utils/AnalyticsUtil.tsx b/app/client/src/utils/AnalyticsUtil.tsx index 2f4201c62a..e35206329a 100644 --- a/app/client/src/utils/AnalyticsUtil.tsx +++ b/app/client/src/utils/AnalyticsUtil.tsx @@ -200,6 +200,7 @@ export type EventName = | "GS_REGENERATE_SSH_KEY_MORE_CLICK" | "GS_SWITCH_BRANCH" | "REFLOW_BETA_FLAG" + | "CONTAINER_JUMP" | "CONNECT_GIT_CLICK" | "REPO_URL_EDIT" | "GENERATE_KEY_BUTTON_CLICK" diff --git a/app/client/src/utils/WidgetPropsUtils.test.tsx b/app/client/src/utils/WidgetPropsUtils.test.tsx index deddf142c7..3d6f706fce 100644 --- a/app/client/src/utils/WidgetPropsUtils.test.tsx +++ b/app/client/src/utils/WidgetPropsUtils.test.tsx @@ -16,6 +16,7 @@ import { GRID_DENSITY_MIGRATION_V1 } from "widgets/constants"; import { extractCurrentDSL, getDraggingSpacesFromBlocks, + getMousePositionsOnCanvas, } from "./WidgetPropsUtils"; import { WidgetDraggingBlock } from "pages/common/CanvasArenas/hooks/useBlocksToBeDraggedOnCanvas"; @@ -70,7 +71,42 @@ describe("WidgetProps tests", () => { ), ).toEqual(draggingSpaces); }); - + it("getMousePositionsOnCanvas should Return Mouse Position relative to Canvas", () => { + const gridProps = { + parentColumnSpace: 10, + parentRowSpace: 10, + maxGridColumns: 64, + }; + const mouseEvent = ({ + offsetX: 500, + offsetY: 600, + } as unknown) as MouseEvent; + expect(getMousePositionsOnCanvas(mouseEvent, gridProps)).toEqual({ + id: "mouse", + top: 59, + left: 49, + bottom: 60, + right: 50, + }); + }); + it("getMousePositionsOnCanvas should even return negative Mouse Position relative to Canvas", () => { + const gridProps = { + parentColumnSpace: 10, + parentRowSpace: 10, + maxGridColumns: 64, + }; + const mouseEvent = ({ + offsetX: 2, + offsetY: 5, + } as unknown) as MouseEvent; + expect(getMousePositionsOnCanvas(mouseEvent, gridProps)).toEqual({ + id: "mouse", + top: -1, + left: -1, + bottom: 0, + right: 0, + }); + }); it("it checks if array to object migration functions for chart widget ", () => { const input = { type: "CANVAS_WIDGET", diff --git a/app/client/src/utils/WidgetPropsUtils.tsx b/app/client/src/utils/WidgetPropsUtils.tsx index 18696ffcd8..ab7b2226ea 100644 --- a/app/client/src/utils/WidgetPropsUtils.tsx +++ b/app/client/src/utils/WidgetPropsUtils.tsx @@ -5,7 +5,12 @@ import { WidgetOperations, WidgetProps, } from "widgets/BaseWidget"; -import { GridDefaults, RenderMode } from "constants/WidgetConstants"; +import { + CONTAINER_GRID_PADDING, + GridDefaults, + RenderMode, + WIDGET_PADDING, +} from "constants/WidgetConstants"; import { snapToGrid } from "./helpers"; import { OccupiedSpace } from "constants/CanvasEditorConstants"; import defaultTemplate from "templates/default"; @@ -16,6 +21,7 @@ import { DSLWidget } from "widgets/constants"; import { WidgetDraggingBlock } from "pages/common/CanvasArenas/hooks/useBlocksToBeDraggedOnCanvas"; import { XYCord } from "pages/common/CanvasArenas/hooks/useCanvasDragging"; import { ContainerWidgetProps } from "widgets/ContainerWidget/widget"; +import { GridProps } from "reflow/reflowTypes"; export type WidgetOperationParams = { operation: WidgetOperation; @@ -96,6 +102,28 @@ export const getDropZoneOffsets = ( ); }; +export const getMousePositionsOnCanvas = ( + e: MouseEvent, + gridProps: GridProps, +) => { + const mouseTop = Math.floor( + (e.offsetY - CONTAINER_GRID_PADDING - WIDGET_PADDING) / + gridProps.parentRowSpace, + ); + const mouseLeft = Math.floor( + (e.offsetX - CONTAINER_GRID_PADDING - WIDGET_PADDING) / + gridProps.parentColumnSpace, + ); + + return { + id: "mouse", + top: mouseTop, + left: mouseLeft, + bottom: mouseTop + 1, + right: mouseLeft + 1, + }; +}; + export const areIntersecting = (r1: Rect, r2: Rect) => { return !( r2.left >= r1.right || diff --git a/app/client/src/utils/hooks/useReflow.ts b/app/client/src/utils/hooks/useReflow.ts index 9393824139..6effa1c16b 100644 --- a/app/client/src/utils/hooks/useReflow.ts +++ b/app/client/src/utils/hooks/useReflow.ts @@ -24,6 +24,7 @@ import { getBottomRowAfterReflow } from "utils/reflowHookUtils"; import { checkIsDropTarget } from "components/designSystems/appsmith/PositionedContainer"; import { getIsReflowing } from "selectors/widgetReflowSelectors"; import { AppState } from "reducers"; +import { areIntersecting } from "utils/WidgetPropsUtils"; type WidgetCollidingSpace = CollidingSpace & { type: string; @@ -45,10 +46,12 @@ export interface ReflowInterface { shouldSkipContainerReflow?: boolean, forceDirection?: boolean, immediateExitContainer?: string, + mousePosition?: OccupiedSpace, ): { movementLimitMap?: MovementLimitMap; movementMap: ReflowedSpaceMap; bottomMostRow: number; + isIdealToJumpContainer: boolean; }; } @@ -95,6 +98,7 @@ export const useReflow = ( shouldSkipContainerReflow = false, forceDirection = false, immediateExitContainer?: string, + mousePosition?: OccupiedSpace, ) { const prevReflowState: PrevReflowState = { prevSpacesMap: getSpacesMapFromArray(prevPositions.current), @@ -103,6 +107,9 @@ export const useReflow = ( prevSecondOrderCollisionMap: prevSecondOrderCollisionMap.current, }; + // To track container jumps + let isIdealToJumpContainer = false; + const { collidingSpaceMap, movementLimitMap, @@ -140,7 +147,12 @@ export const useReflow = ( ] as WidgetCollidingSpace[]; for (const collidingSpace of collidingSpaces) { - if (checkIsDropTarget(collidingSpace.type)) { + if ( + checkIsDropTarget(collidingSpace.type) && + mousePosition && + areIntersecting(mousePosition, collidingSpace) + ) { + isIdealToJumpContainer = true; correctedMovementMap = {}; } } @@ -169,6 +181,7 @@ export const useReflow = ( movementLimitMap, movementMap: correctedMovementMap, bottomMostRow, + isIdealToJumpContainer, }; }; };