feat: Container Jump Improvements and Code for tracking Container Jumps (#12686)

* conatiner Jump Optimization and tracking

* Round of mouse positions ans add negative jest test cases

* addressing review comments
This commit is contained in:
rahulramesha 2022-04-13 20:27:44 +05:30 committed by GitHub
parent c7a4ac7353
commit faaf977728
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 228 additions and 18 deletions

View File

@ -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) ||

View File

@ -0,0 +1,20 @@
//class to maintain containerJump metrics across containers.
export default class ContainerJumpMetrics<T> {
private containerJumpValues: T = {} as T;
public setMetrics(args: T) {
this.containerJumpValues = {
...args,
};
}
public getMetrics() {
return {
...this.containerJumpValues,
};
}
public clearMetrics() {
this.containerJumpValues = {} as T;
}
}

View File

@ -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,

View File

@ -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<HTMLDivElement>,
stickyCanvasRef: React.RefObject<HTMLCanvasElement>,
@ -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 },

View File

@ -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"

View File

@ -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",

View File

@ -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 ||

View File

@ -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,
};
};
};