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:
parent
904fa89833
commit
67f7d217a1
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
162
app/client/src/utils/autoHeight/generateTree.test.ts
Normal file
162
app/client/src/utils/autoHeight/generateTree.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
83
app/client/src/utils/autoHeight/generateTree.ts
Normal file
83
app/client/src/utils/autoHeight/generateTree.ts
Normal 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;
|
||||
}
|
||||
221
app/client/src/utils/autoHeight/reflow.test.ts
Normal file
221
app/client/src/utils/autoHeight/reflow.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
204
app/client/src/utils/autoHeight/reflow.ts
Normal file
204
app/client/src/utils/autoHeight/reflow.ts
Normal 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;
|
||||
}
|
||||
45
app/client/src/utils/boxHelpers.test.ts
Normal file
45
app/client/src/utils/boxHelpers.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
15
app/client/src/utils/boxHelpers.ts
Normal file
15
app/client/src/utils/boxHelpers.ts
Normal 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
|
||||
);
|
||||
};
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user