feat: (Internal Change) Add auto height computation functions (#17962)

* Add auto height reflow functions libary

* Add comments in hard to understand parts

* feat: auto height reflow lib (#17978)

added 2 tests for boxHelper and 1 simple test for reflow computeChangeInPositionBasedOnDelta

Co-authored-by: Ankur Singhal <ankurrsinghal@gmail.com>

* Reduce one loop. Fix typo

* Add helper functions and use them in lib

* Use helper function

* Add types

* Fix issue where widgets don't get close to the bottom most above widget if that widget hasn't changed

* fix: auto height reflow lib merge release (#18193)

* feat: show number of tabs on the header (#18071)

* number of tabs displayed alongside label

* styling for span removed

* feature added and cypress test cases written

* code refactoring after review

* Update top contributors

* feat: Auto-height add reducers and actions (#17953)

* Add reducers for auto height feature (Internal Change, No changes reflected to users)
Co-authored-by: ankurrsinghal <ankur@appsmith.com>

* [Bug] Incorrect count of users in workspace when adding multiple users (#17728)

fix: filtering unique users by userId

Co-authored-by: Anubhav <anubhav@appsmith.com>

* fix: Instrumentation for execution errors (#18093)

* fix: Instrumentation for execution errors

* added widget editor error event

* fix:  Sidebar heading fontSize & checkbox alignment  (#18104)

sidebar heading & checkbox alignment to heading

* chore: added type for feature flag (#18152)

add: new type for env variable

* Update top contributors

* feat: [Context Switching]: Change focus target and fix cursor position (#17794)

Co-authored-by: 
rahulramesha <rahul@appsmith.com>

* fix: JS Objects save failures due to AST changes (#18018)

* fix: update local_testing.sh to build image for external contributor PRs (#18024)

* chore: use `typography` and `getTypographyFromKey` from the design-system (#18050)

Change typography imports, change function call

* dummy

Co-authored-by: Rishabh Kashyap <rishabh.kashyap@appsmith.com>
Co-authored-by: Appsmith Bot <74705725+appsmith-bot@users.noreply.github.com>
Co-authored-by: Abhinav Jha <abhinav@appsmith.com>
Co-authored-by: Ankit Srivastava <67647761+ankitsrivas14@users.noreply.github.com>
Co-authored-by: Anubhav <anubhav@appsmith.com>
Co-authored-by: ChandanBalajiBP <104058110+ChandanBalajiBP@users.noreply.github.com>
Co-authored-by: Rohit Agarwal <rohit_agarwal@live.in>
Co-authored-by: Ayush Pahwa <ayush@appsmith.com>
Co-authored-by: Hetu Nandu <hetu@appsmith.com>
Co-authored-by: subratadeypappu <subrata@appsmith.com>
Co-authored-by: Sumit Kumar <sumit@appsmith.com>
Co-authored-by: Tanvi Bhakta <tanvibhakta@gmail.com>
Co-authored-by: Ankur Singhal <ankurrsinghal@gmail.com>

Co-authored-by: ankurrsinghal <ankur@appsmith.com>
Co-authored-by: Ankur Singhal <ankurrsinghal@gmail.com>
Co-authored-by: Rishabh Kashyap <rishabh.kashyap@appsmith.com>
Co-authored-by: Appsmith Bot <74705725+appsmith-bot@users.noreply.github.com>
Co-authored-by: Ankit Srivastava <67647761+ankitsrivas14@users.noreply.github.com>
Co-authored-by: Anubhav <anubhav@appsmith.com>
Co-authored-by: ChandanBalajiBP <104058110+ChandanBalajiBP@users.noreply.github.com>
Co-authored-by: Rohit Agarwal <rohit_agarwal@live.in>
Co-authored-by: Ayush Pahwa <ayush@appsmith.com>
Co-authored-by: Hetu Nandu <hetu@appsmith.com>
Co-authored-by: subratadeypappu <subrata@appsmith.com>
Co-authored-by: Sumit Kumar <sumit@appsmith.com>
Co-authored-by: Tanvi Bhakta <tanvibhakta@gmail.com>
This commit is contained in:
Abhinav Jha 2022-11-20 11:42:32 +05:30 committed by GitHub
parent 904fa89833
commit 67f7d217a1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 835 additions and 21 deletions

View File

@ -1,6 +1,6 @@
import { OccupiedSpace } from "constants/CanvasEditorConstants";
import { cloneDeep, isUndefined } from "lodash";
import { Rect } from "utils/WidgetPropsUtils";
import { Rect } from "utils/boxHelpers";
import {
CollidingSpace,
CollidingSpaceMap,

View File

@ -12,7 +12,7 @@ import { all, cancel, put, select, take, takeLatest } from "redux-saga/effects";
import { getOccupiedSpaces } from "selectors/editorSelectors";
import { getSelectedWidgets } from "selectors/ui";
import { snapToGrid } from "utils/helpers";
import { areIntersecting } from "utils/WidgetPropsUtils";
import { areIntersecting } from "utils/boxHelpers";
import { WidgetProps } from "widgets/BaseWidget";
import { getWidgets } from "sagas/selectors";
import { CanvasWidgetsReduxState } from "reducers/entityReducers/canvasWidgetsReducer";

View File

@ -34,7 +34,7 @@ import { getNextEntityName } from "utils/AppsmithUtils";
import WidgetFactory from "utils/WidgetFactory";
import { getParentWithEnhancementFn } from "./WidgetEnhancementHelpers";
import { OccupiedSpace, WidgetSpace } from "constants/CanvasEditorConstants";
import { areIntersecting } from "utils/WidgetPropsUtils";
import { areIntersecting } from "utils/boxHelpers";
import {
GridProps,
PrevReflowState,

View File

@ -22,6 +22,7 @@ import { WidgetDraggingBlock } from "pages/common/CanvasArenas/hooks/useBlocksTo
import { XYCord } from "pages/common/CanvasArenas/hooks/useCanvasDragging";
import { ContainerWidgetProps } from "widgets/ContainerWidget/widget";
import { GridProps } from "reflow/reflowTypes";
import { areIntersecting, Rect } from "./boxHelpers";
export type WidgetOperationParams = {
operation: WidgetOperation;
@ -29,13 +30,6 @@ export type WidgetOperationParams = {
payload: any;
};
export type Rect = {
top: number;
left: number;
right: number;
bottom: number;
};
const defaultDSL = defaultTemplate;
export const extractCurrentDSL = (
@ -124,15 +118,6 @@ export const getMousePositionsOnCanvas = (
};
};
export const areIntersecting = (r1: Rect, r2: Rect) => {
return !(
r2.left >= r1.right ||
r2.right <= r1.left ||
r2.top >= r1.bottom ||
r2.bottom <= r1.top
);
};
export const isDropZoneOccupied = (
offset: Rect,
widgetId: string,

View File

@ -6,3 +6,13 @@ export type TreeNode = {
originalTopRow: number;
originalBottomRow: number;
};
export type NodeSpace = {
left: number;
right: number;
top: number;
bottom: number;
id: string;
};
export const MAX_BOX_SIZE = 20000;

View File

@ -0,0 +1,162 @@
import { NodeSpace, TreeNode } from "./constants";
import { generateTree } from "./generateTree";
describe("Generate Auto Height Layout tree", () => {
it("Does not conflict when only one horizontal edge is the same", () => {
const input: NodeSpace[] = [
{ left: 0, right: 100, top: 0, bottom: 30, id: "1" },
{ left: 100, top: 0, bottom: 30, right: 120, id: "2" },
];
const previousTree: Record<string, TreeNode> = {};
const layoutUpdated = false;
const expected = {
"1": {
aboves: [],
belows: [],
topRow: 0,
bottomRow: 30,
originalBottomRow: 30,
originalTopRow: 0,
},
"2": {
aboves: [],
belows: [],
topRow: 0,
bottomRow: 30,
originalBottomRow: 30,
originalTopRow: 0,
},
};
const result = generateTree(input, layoutUpdated, previousTree);
expect(result).toStrictEqual(expected);
});
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" },
];
const previousTree: Record<string, TreeNode> = {};
const layoutUpdated = false;
const expected = {
"1": {
aboves: [],
belows: ["2"],
topRow: 0,
bottomRow: 30,
originalBottomRow: 30,
originalTopRow: 0,
},
"2": {
aboves: ["1"],
belows: [],
topRow: 30,
bottomRow: 40,
originalBottomRow: 40,
originalTopRow: 30,
},
};
const result = generateTree(input, layoutUpdated, previousTree);
expect(result).toStrictEqual(expected);
});
it("Uses existing originals if available in prevTree when layout hasn't updated", () => {
const input: NodeSpace[] = [
{ left: 0, right: 100, top: 0, bottom: 30, id: "1" },
{ left: 80, top: 30, bottom: 40, right: 120, id: "2" },
];
const previousTree: Record<string, TreeNode> = {
"1": {
aboves: [],
belows: ["2"],
topRow: 0,
bottomRow: 30,
originalBottomRow: 20,
originalTopRow: 0,
},
"2": {
aboves: ["1"],
belows: [],
topRow: 30,
bottomRow: 40,
originalBottomRow: 30,
originalTopRow: 20,
},
};
const layoutUpdated = false;
const expected = {
"1": {
aboves: [],
belows: ["2"],
topRow: 0,
bottomRow: 30,
originalBottomRow: 20,
originalTopRow: 0,
},
"2": {
aboves: ["1"],
belows: [],
topRow: 30,
bottomRow: 40,
originalBottomRow: 30,
originalTopRow: 20,
},
};
const result = generateTree(input, layoutUpdated, previousTree);
expect(result).toStrictEqual(expected);
});
it("Ignores existing originals if available in prevTree when layout has updated", () => {
const input: NodeSpace[] = [
{ left: 0, right: 100, top: 0, bottom: 30, id: "1" },
{ left: 80, top: 30, bottom: 40, right: 120, id: "2" },
];
const previousTree: Record<string, TreeNode> = {
"1": {
aboves: [],
belows: ["2"],
topRow: 0,
bottomRow: 30,
originalBottomRow: 20,
originalTopRow: 0,
},
"2": {
aboves: ["1"],
belows: [],
topRow: 30,
bottomRow: 40,
originalBottomRow: 30,
originalTopRow: 20,
},
};
const layoutUpdated = true;
const expected = {
"1": {
aboves: [],
belows: ["2"],
topRow: 0,
bottomRow: 30,
originalBottomRow: 30,
originalTopRow: 0,
},
"2": {
aboves: ["1"],
belows: [],
topRow: 30,
bottomRow: 40,
originalBottomRow: 40,
originalTopRow: 30,
},
};
const result = generateTree(input, layoutUpdated, previousTree);
expect(result).toStrictEqual(expected);
});
});

View File

@ -0,0 +1,83 @@
import { areIntersecting } from "utils/boxHelpers";
import { pushToArray } from "utils/helpers";
import { MAX_BOX_SIZE, NodeSpace, TreeNode } from "./constants";
// 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
// in the form of "aboves" and "belows" for each box, which are array of box ids
export function generateTree(
spaces: NodeSpace[],
layoutUpdated: boolean,
previousTree: Record<string, TreeNode>,
): Record<string, TreeNode> {
// If widget doesn't exist in this DS, this means that its height changes does not effect any other sibling
spaces.sort((a, b) => a.top - b.top); // Sort based on position, top to bottom, so that we know which is above the other
const _spaces = [...spaces];
const aboveMap: Record<string, string[]> = {};
const belowMap: Record<string, string[]> = {};
const tree: Record<string, TreeNode> = {};
// For each of the sibling boxes
for (let i = 0; i < spaces.length; i++) {
// Get the left most box in the array (Remember: we sorted from top to bottom, so the leftmost will be the top most)
const _curr = _spaces.shift();
if (_curr) {
// Create a reference copy as we need to override the bottom value
const currentSpace = { ..._curr };
// Add a randomly large value to the bottom; this will help us know if any box is below this box
currentSpace.bottom += MAX_BOX_SIZE;
// For each of the remaining sibling widgets
for (let j = 0; j < _spaces.length; j++) {
// Create a reference copy as we need to override the bottom value
const comparisionSpace = { ..._spaces[j] };
// Add a randomly large value to the bottom; this will help us know if any box is below this box
// TODO(abhinav): This addition may not be necessary, as we're only looking to see if these boxes
// are below the currentSpace
comparisionSpace.bottom += MAX_BOX_SIZE;
// Check if comparison space has an overlap with current space
if (areIntersecting(currentSpace, comparisionSpace)) {
// If there is an overlap, comparisonSpace is below the current space
// so, we update the aboveMap and belowMap accordingly
aboveMap[comparisionSpace.id] = pushToArray(
currentSpace.id,
aboveMap[comparisionSpace.id],
) as string[];
belowMap[currentSpace.id] = pushToArray(
comparisionSpace.id,
belowMap[currentSpace.id],
) as string[];
}
}
// Get the originalTop and originalBottom from the previous tree.
// This is so that we can get close to the original (user defined) positions of the boxes
// For example, if box1 increases in size and pushes box2 by 100 rows, while box3 is also above box2
// When the box1 subsequently decrease by 50 rows, we need to maintain spacing between box3 and box2
// Otherwise, if box1 happens to go below the bottomRow of box3, box2 will tend to overlap with box3.
let originalTopRow = previousTree[currentSpace.id]?.originalTopRow;
let originalBottomRow = previousTree[currentSpace.id]?.originalBottomRow;
// We also udpate the original if the layout is being updated
// This happens when the user repositions/resizes boxes
// If the previousTree doesn't have any originals, we can assume that this is the
// first time we're generating the tree, hence we need to keep the current top and bottom
// for subsequent tree generation
if (originalTopRow === undefined || layoutUpdated) {
originalTopRow = currentSpace.top;
}
if (originalBottomRow === undefined || layoutUpdated) {
originalBottomRow = currentSpace.bottom - MAX_BOX_SIZE;
}
tree[currentSpace.id] = {
aboves: aboveMap[currentSpace.id] || [],
belows: belowMap[currentSpace.id] || [],
topRow: currentSpace.top,
bottomRow: currentSpace.bottom - MAX_BOX_SIZE,
originalTopRow,
originalBottomRow,
};
}
}
return tree;
}

View File

@ -0,0 +1,221 @@
import { TreeNode } from "./constants";
import { computeChangeInPositionBasedOnDelta } from "./reflow";
describe("reflow", () => {
describe("computeChangeInPositionBasedOnDelta (should compute new positions for boxes based on boxes which has changed heights)", () => {
it("simple 2 boxes test where the top grows by 5 rows and should shifts the bottom one by 5 rows", () => {
const box1TopRow = 10;
const box1BottomRow = 20;
const box2TopRow = 30;
const box2BottomRow = 40;
const tree: Record<string, TreeNode> = {
"1": {
aboves: [],
belows: ["2"],
topRow: box1TopRow,
bottomRow: box1BottomRow,
originalTopRow: box1TopRow,
originalBottomRow: box1BottomRow,
},
"2": {
aboves: ["1"],
belows: [],
topRow: box2TopRow,
bottomRow: box2BottomRow,
originalTopRow: box2TopRow,
originalBottomRow: box2BottomRow,
},
};
const box1DeltaHeightIncrease = 5;
const delta: Record<string, number> = {
"1": box1DeltaHeightIncrease,
};
const expectedChanges = {
"1": {
topRow: box1TopRow,
bottomRow: box1BottomRow + box1DeltaHeightIncrease,
},
"2": {
topRow: box2TopRow + box1DeltaHeightIncrease,
bottomRow: box2BottomRow + box1DeltaHeightIncrease,
},
};
const changes = computeChangeInPositionBasedOnDelta(tree, delta);
expect(expectedChanges).toMatchObject(changes);
});
it("When delta is negative, the original spacing is maintained", () => {
const box1TopRow = 10;
const box1BottomRow = 100;
const box1OriginalTopRow = 10;
const box1OriginalBottomRow = 20;
const box2TopRow = 110;
const box2BottomRow = 200;
const box2OriginalTopRow = 30;
const box2OriginalBottomRow = 40;
const tree: Record<string, TreeNode> = {
"1": {
aboves: [],
belows: ["2"],
topRow: box1TopRow,
bottomRow: box1BottomRow,
originalTopRow: box1OriginalTopRow,
originalBottomRow: box1OriginalBottomRow,
},
"2": {
aboves: ["1"],
belows: [],
topRow: box2TopRow,
bottomRow: box2BottomRow,
originalTopRow: box2OriginalTopRow,
originalBottomRow: box2OriginalBottomRow,
},
};
const box1DeltaHeight = -50;
const delta: Record<string, number> = {
"1": box1DeltaHeight,
"2": box1DeltaHeight,
};
const expectedChanges = {
"1": {
topRow: box1TopRow,
bottomRow: box1BottomRow + box1DeltaHeight,
},
"2": {
topRow: 60,
bottomRow: 100,
},
};
const changes = computeChangeInPositionBasedOnDelta(tree, delta);
expect(expectedChanges).toMatchObject(changes);
});
it("When delta is negative, bottom box moves up", () => {
const box1TopRow = 10;
const box1BottomRow = 100;
const box1OriginalTopRow = 10;
const box1OriginalBottomRow = 15;
const box2TopRow = 140;
const box2BottomRow = 230;
const box2OriginalTopRow = 30;
const box2OriginalBottomRow = 40;
const tree: Record<string, TreeNode> = {
"1": {
aboves: [],
belows: ["2"],
topRow: box1TopRow,
bottomRow: box1BottomRow,
originalTopRow: box1OriginalTopRow,
originalBottomRow: box1OriginalBottomRow,
},
"2": {
aboves: ["1"],
belows: [],
topRow: box2TopRow,
bottomRow: box2BottomRow,
originalTopRow: box2OriginalTopRow,
originalBottomRow: box2OriginalBottomRow,
},
};
const box1DeltaHeight = -50;
const delta: Record<string, number> = {
"1": box1DeltaHeight,
"2": box1DeltaHeight,
};
const expectedChanges = {
"1": {
topRow: box1TopRow,
bottomRow: box1BottomRow + box1DeltaHeight,
},
"2": {
topRow: 90,
bottomRow: 130,
},
};
const changes = computeChangeInPositionBasedOnDelta(tree, delta);
expect(expectedChanges).toMatchObject(changes);
});
it("When a widget is blocking and delta is negative, bottom box moves up to the original spacing between the blocking box and the bottom box", () => {
const box1TopRow = 10;
const box1BottomRow = 100;
const box1OriginalTopRow = 10;
const box1OriginalBottomRow = 15;
const box2TopRow = 140;
const box2BottomRow = 230;
const box2OriginalTopRow = 30;
const box2OriginalBottomRow = 40;
const box3 = {
aboves: [],
belows: ["2"],
topRow: 50,
bottomRow: 120,
originalBottomRow: 20,
originalTopRow: 10,
};
const tree: Record<string, TreeNode> = {
"1": {
aboves: [],
belows: ["2"],
topRow: box1TopRow,
bottomRow: box1BottomRow,
originalTopRow: box1OriginalTopRow,
originalBottomRow: box1OriginalBottomRow,
},
"2": {
aboves: ["1", "3"],
belows: [],
topRow: box2TopRow,
bottomRow: box2BottomRow,
originalTopRow: box2OriginalTopRow,
originalBottomRow: box2OriginalBottomRow,
},
"3": box3,
};
const box1DeltaHeight = -50;
const delta: Record<string, number> = {
"1": box1DeltaHeight,
"2": box1DeltaHeight,
};
const expectedChanges = {
"1": {
topRow: box1TopRow,
bottomRow: box1BottomRow + box1DeltaHeight,
},
"2": {
topRow: 130,
bottomRow: 170,
},
};
const changes = computeChangeInPositionBasedOnDelta(tree, delta);
expect(expectedChanges).toMatchObject(changes);
});
});
});

View File

@ -0,0 +1,204 @@
import { pushToArray } from "utils/helpers";
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<string, TreeNode>,
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<string, TreeNode>,
effectedBoxId: string,
repositionedBoxes: Record<string, { topRow: number; bottomRow: number }>,
) {
// 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;
}, []);
}
// This function computes the new positions for boxes based on the boxes which have changed height
// delta: a map of boxes with change in heights
// tree: a layout tree which contains the current state of the boxes.
export function computeChangeInPositionBasedOnDelta(
tree: Record<string, TreeNode>,
delta: Record<string, number>,
): Record<string, { topRow: number; bottomRow: number }> {
const repositionedBoxes: Record<
string,
{ topRow: number; bottomRow: number }
> = {};
const effectedBoxMap: Record<string, number[]> = {};
// For each box which has changed height (box delta)
for (const boxId in delta) {
// Create an effectedBoxMap, which contains the changes for each of the boxes effected by the delta of this box
// This is a map, because multiple box deltas can effect one box
// We simply take all the boxes which are below this box from the tree
// and add the delta to the effectedBoxMap where the key is the below boxId from the tree
tree[boxId].belows.forEach((effectedId) => {
effectedBoxMap[effectedId] = pushToArray(
delta[boxId],
effectedBoxMap[effectedId],
) as number[];
});
// Add this box's delta to the repositioning, as this won't show up in the effectedBoxMap
repositionedBoxes[boxId] = {
topRow: tree[boxId].topRow,
bottomRow: tree[boxId].bottomRow + delta[boxId],
};
}
// Sort the effected box ids, this is to make sure we compute from top to bottom.
const sortedEffectedBoxIds = Object.keys(effectedBoxMap).sort(
(a, b) => tree[a].topRow - tree[b].topRow,
);
// For each of the boxes which have been effected
for (const effectedBoxId of sortedEffectedBoxIds) {
let _offset;
const bottomMostAboves = getNearestAbove(
tree,
effectedBoxId,
repositionedBoxes,
);
// for each of the bottom most above boxes.
// Note: There can be more than one if two above widgets have the same bottomrow
for (const aboveId of bottomMostAboves) {
// If the above box has been effected by another box change height
// Or, if this above box itself has changed height
if (Array.isArray(effectedBoxMap[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);
}
} 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;
}
}
// If _offset is not defined, this means that this box is the topmost box
if (_offset === undefined) {
// The effectedBoxId is the topmost box, so the _offset will most likely always be 0
_offset = effectedBoxMap[effectedBoxId].reduce(
(prev, next) => prev + next,
0,
);
}
// Finally update the repositioned box with the _offset.
if (repositionedBoxes[effectedBoxId]) {
repositionedBoxes[effectedBoxId].bottomRow += _offset;
repositionedBoxes[effectedBoxId].topRow += _offset;
} else {
repositionedBoxes[effectedBoxId] = {
topRow: tree[effectedBoxId].topRow + _offset,
bottomRow: tree[effectedBoxId].bottomRow + _offset,
};
}
}
return repositionedBoxes;
}

View File

@ -0,0 +1,45 @@
import { areIntersecting, Rect } from "./boxHelpers";
describe("boxHelpers", () => {
describe("areIntersecting", () => {
it("should return true when rect 1 and rect 2 are intersecting", () => {
const rect1: Rect = {
top: 0,
bottom: 2,
left: 0,
right: 2,
};
const rect2: Rect = {
top: 0,
bottom: 3,
left: 1,
right: 3,
};
const isIntersecting = areIntersecting(rect1, rect2);
expect(isIntersecting).toBe(true);
});
it("should return false when rect 1 and rect 2 are not intersecting", () => {
const rect1: Rect = {
top: 0,
bottom: 2,
left: 0,
right: 2,
};
const rect2: Rect = {
top: 3,
bottom: 6,
left: 3,
right: 6,
};
const isIntersecting = areIntersecting(rect1, rect2);
expect(isIntersecting).toBe(false);
});
});
});

View File

@ -0,0 +1,15 @@
export type Rect = {
top: number;
left: number;
right: number;
bottom: number;
};
export const areIntersecting = (r1: Rect, r2: Rect) => {
return !(
r2.left >= r1.right ||
r2.right <= r1.left ||
r2.top >= r1.bottom ||
r2.bottom <= r1.top
);
};

View File

@ -10,6 +10,8 @@ import {
mergeWidgetConfig,
extractColorsFromString,
isNameValid,
pushToArray,
concatWithArray,
} from "./helpers";
import WidgetFactory from "./WidgetFactory";
import * as Sentry from "@sentry/react";
@ -597,3 +599,49 @@ describe("isNameValid()", () => {
}
});
});
describe("pushToArray", () => {
it("adds to an undefined array", () => {
const item = "something";
const expected = ["something"];
const result = pushToArray(item);
expect(result).toStrictEqual(expected);
});
it("adds to an existing array", () => {
const item = "something";
const arr1 = ["another"];
const expected = ["another", "something"];
const result = pushToArray(item, arr1);
expect(result).toStrictEqual(expected);
});
it("adds to an existing array and make unique", () => {
const item = "something";
const arr1 = ["another", "another"];
const expected = ["another", "something"];
const result = pushToArray(item, arr1, true);
expect(result).toStrictEqual(expected);
});
});
describe("concatWithArray", () => {
it("adds to an undefined array", () => {
const items = ["something"];
const expected = ["something"];
const result = concatWithArray(items);
expect(result).toStrictEqual(expected);
});
it("adds to an existing array", () => {
const items = ["something"];
const arr1 = ["another"];
const expected = ["another", "something"];
const result = concatWithArray(items, arr1);
expect(result).toStrictEqual(expected);
});
it("adds to an existing array and make unique", () => {
const items = ["something"];
const arr1 = ["another", "another"];
const expected = ["another", "something"];
const result = concatWithArray(items, arr1, true);
expect(result).toStrictEqual(expected);
});
});

View File

@ -9,7 +9,7 @@ import {
DEDICATED_WORKER_GLOBAL_SCOPE_IDENTIFIERS,
JAVASCRIPT_KEYWORDS,
} from "constants/WidgetValidation";
import { get, set, isNil, has } from "lodash";
import { get, set, isNil, has, uniq } from "lodash";
import { Workspace } from "@appsmith/constants/workspaceConstants";
import {
isPermitted,
@ -936,3 +936,42 @@ export function AutoBind(target: any, _: string, descriptor: any) {
descriptor.value = descriptor.value.bind(target);
return descriptor;
}
/**
* Add item to an array which could be undefined
* @param arr1 Base Array (could be undefined)
* @param item Item to add to array
* @param makeUnique Should make sure array has unique entries
* @returns array which includes items from arr1 and item
*/
export function pushToArray(
item: unknown,
arr1?: unknown[],
makeUnique = false,
) {
if (Array.isArray(arr1)) arr1.push(item);
else return [item];
if (makeUnique) return uniq(arr1);
return arr1;
}
/**
* Add items to array which could be undefined
* @param arr1 Base Array (could be undefined)
* @param items Items to add to arr1
* @param makeUnique Should make sure array has unique entries
* @returns array which contains items from arr1 and items
*/
export function concatWithArray(
items: unknown[],
arr1?: unknown[],
makeUnique = false,
) {
let finalArr: unknown[] = [];
if (Array.isArray(arr1)) finalArr = arr1.concat(items);
else finalArr = finalArr.concat(items);
if (makeUnique) return uniq(finalArr);
return finalArr;
}

View File

@ -24,7 +24,7 @@ import { getBottomRowAfterReflow } from "utils/reflowHookUtils";
import { checkIsDropTarget } from "components/designSystems/appsmith/PositionedContainer";
import { getIsReflowing } from "selectors/widgetReflowSelectors";
import { AppState } from "@appsmith/reducers";
import { areIntersecting } from "utils/WidgetPropsUtils";
import { areIntersecting } from "utils/boxHelpers";
type WidgetCollidingSpace = CollidingSpace & {
type: string;

View File

@ -39,6 +39,8 @@ export function collisionCheckPostReflow(
return true;
}
// TODO(ashok): There is a name clash here. Fine for now, but might get confusing in the future.
// maybe we should create a task for this.
function areIntersecting(r1: FlattenedWidgetProps, r2: FlattenedWidgetProps) {
if (r1.widgetId === r2.widgetId) return false;