PromucFlow_constructor/app/client/src/layoutSystems/autolayout/utils/highlightUtils.ts
Preet Sidhu 46dcf3a8f0
chore: Create layout system structure for Anvil and AnvilFlexComponent. (#27178)
## Description
1. Add new ```appPositioningType``` : ANVIL.
2. Create new code path and folder structure for Anvil layout system.
3. Move common pieces of functionalities between autoLayout and anvil to
anvil folder structure (e.g. ```CanvasResizer```).
4. Create ```AnvilFlexComponent```.
5. Use WDS Flex component in AnvilFlexComponent.
6. Pass min max size props in a data structure that is supported by
container queries in the Flex component.
    e.g. min-width: { base: "120px", "480px": "200px" }
7. Supply the following flex properties (flex-grow flex-shrink
flex-basis) to widgets depending on their ```responsiveBehvaiour```:
    a) Fill: ```flex: 1 1 0%;```
    b) Hug: ```flex: 0 0 auto;```

#### PR fixes following issue(s)
Fixes # (issue number)
1. [#26987](https://github.com/appsmithorg/appsmith/issues/26987)
2. [#26609](https://github.com/appsmithorg/appsmith/issues/26609)
3. [#26611](https://github.com/appsmithorg/appsmith/issues/26611)

#### Type of change
- New feature (non-breaking change which adds functionality)


## Testing
>
#### How Has This Been Tested?
> Please describe the tests that you ran to verify your changes. Also
list any relevant details for your test configuration.
> Delete anything that is not relevant
- [x] Manual
- [ ] JUnit
- [x] Jest
- [ ] Cypress


## Checklist:
#### Dev activity
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my own code
- [x] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [x] PR is being merged under a feature flag


#### QA activity:
- [ ] [Speedbreak
features](https://github.com/appsmithorg/TestSmith/wiki/Guidelines-for-test-plans#speedbreakers-)
have been covered
- [ ] Test plan covers all impacted features and [areas of
interest](https://github.com/appsmithorg/TestSmith/wiki/Guidelines-for-test-plans#areas-of-interest-)
- [ ] Test plan has been peer reviewed by project stakeholders and other
QA members
- [ ] Manually tested functionality on DP
- [ ] We had an implementation alignment call with stakeholders post QA
Round 2
- [ ] Cypress test cases have been added and approved by SDET/manual QA
- [ ] Added `Test Plan Approved` label after Cypress tests were reviewed
- [ ] Added `Test Plan Approved` label after JUnit tests were reviewed

---------

Co-authored-by: Ashok Kumar M <35134347+marks0351@users.noreply.github.com>
Co-authored-by: Aswath K <aswath.sana@gmail.com>
Co-authored-by: rahulramesha <rahul@appsmith.com>
Co-authored-by: rahulramesha <71900764+rahulramesha@users.noreply.github.com>
2023-10-02 15:41:05 -04:00

564 lines
17 KiB
TypeScript

import {
FlexLayerAlignment,
MOBILE_ROW_GAP,
ROW_GAP,
} from "layoutSystems/common/utils/constants";
import {
FLEXBOX_PADDING,
GridDefaults,
MAIN_CONTAINER_WIDGET_ID,
} from "constants/WidgetConstants";
import type {
CanvasWidgetsReduxState,
FlattenedWidgetProps,
} from "reducers/entityReducers/canvasWidgetsReducer";
import {
getLeftColumn,
getRightColumn,
getTopRow,
getWidgetHeight,
getWidgetWidth,
} from "./flexWidgetUtils";
import { getAlignmentSizeInfo, getWrappedAlignmentInfo } from "./positionUtils";
import type {
AlignmentChildren,
AlignmentInfo,
} from "../../autolayout/utils/types";
import { getTotalRowsOfAllChildren } from "./heightUpdateUtils";
import { DEFAULT_HIGHLIGHT_SIZE } from "../common/flexCanvas/FlexBoxComponent";
import type { FlexLayer, LayerChild } from "./types";
import type { DropZone, HighlightInfo } from "layoutSystems/common/utils/types";
/**
* @param allWidgets : CanvasWidgetsReduxState
* @param canvasId : string
* @param draggedWidgets : string[]
* @returns widgets: CanvasWidgetsReduxState
*
* This function is used to derive highlights from flex layers and stored in widget dsl.
*/
export function deriveHighlightsFromLayers(
allWidgets: CanvasWidgetsReduxState,
canvasId: string,
snapColumnSpace: number,
draggedWidgets: string[] = [],
hasFillWidget = false,
isMobile = false,
): HighlightInfo[] {
const widgets = { ...allWidgets };
const canvas = widgets[canvasId];
if (!canvas) return [];
const layers: FlexLayer[] = canvas.flexLayers || [];
let highlights: HighlightInfo[] = [];
let childCount = 0;
let layerIndex = 0;
const rowGap = isMobile ? MOBILE_ROW_GAP : ROW_GAP;
let offsetTop = FLEXBOX_PADDING; // used to calculate distance of a highlight from parents's top.
for (const layer of layers) {
/**
* Discard widgets that are detached from layout (Modals).
*/
const updatedLayer: FlexLayer = {
children: layer?.children?.filter(
(child: LayerChild) =>
widgets[child.id] && !widgets[child.id].detachFromLayout,
),
};
/**
* If the layer is empty, after discounting the dragged widgets,
* then don't process it for vertical highlights.
*/
const isEmpty: boolean =
updatedLayer?.children?.filter(
(child: LayerChild) => draggedWidgets.indexOf(child.id) === -1,
).length === 0;
const childrenRows = getTotalRowsOfAllChildren(
widgets,
updatedLayer.children?.map((child) => child.id) || [],
isMobile,
);
const payload: VerticalHighlightsPayload = generateVerticalHighlights({
widgets,
layer: updatedLayer,
childCount,
layerIndex,
offsetTop,
canvasId,
columnSpace: snapColumnSpace,
draggedWidgets,
isMobile,
});
if (!isEmpty) {
/**
* Add a layer of horizontal highlights before each flex layer
* to account for new vertical drop positions.
*/
highlights.push(
...generateHorizontalHighlights(
childCount,
layerIndex,
offsetTop,
snapColumnSpace * GridDefaults.DEFAULT_GRID_COLUMNS,
canvasId,
hasFillWidget,
childrenRows * GridDefaults.DEFAULT_GRID_ROW_HEIGHT,
getPreviousOffsetTop(highlights),
isMobile,
),
);
highlights.push(...payload.highlights);
layerIndex += 1;
} else highlights = updateHorizontalDropZone(highlights);
offsetTop +=
childrenRows * GridDefaults.DEFAULT_GRID_ROW_HEIGHT + rowGap || 0;
childCount += payload.childCount;
}
// Add a layer of horizontal highlights for the empty space at the bottom of a stack.
highlights.push(
...generateHorizontalHighlights(
childCount,
layerIndex,
offsetTop,
snapColumnSpace * GridDefaults.DEFAULT_GRID_COLUMNS,
canvasId,
hasFillWidget,
-1,
getPreviousOffsetTop(highlights),
isMobile,
),
);
return highlights;
}
export interface VerticalHighlightsPayload {
childCount: number;
highlights: HighlightInfo[];
}
/**
* Derive highlight information for all widgets within a layer.
* - Breakdown each layer into component alignments.
* - generate highlight information for each alignment.
*/
export function generateVerticalHighlights(data: {
widgets: CanvasWidgetsReduxState;
layer: FlexLayer;
childCount: number;
layerIndex: number;
offsetTop: number;
canvasId: string;
columnSpace: number;
draggedWidgets: string[];
isMobile: boolean;
}): VerticalHighlightsPayload {
const {
canvasId,
childCount,
columnSpace,
draggedWidgets,
isMobile,
layer,
layerIndex,
offsetTop,
widgets,
} = data;
const { children } = layer;
let count = 0;
const startChildren = [],
centerChildren = [],
endChildren = [];
let startColumns = 0,
centerColumns = 0,
endColumns = 0;
let maxHeight = 0;
const rowSpace = GridDefaults.DEFAULT_GRID_ROW_HEIGHT;
for (const child of children) {
const widget = widgets[child.id];
if (!widget) continue;
count += 1;
if (draggedWidgets.indexOf(child.id) > -1) continue;
const columns: number = getWidgetWidth(widget, isMobile);
const rows: number = getWidgetHeight(widget, isMobile);
maxHeight = Math.max(maxHeight, rows * rowSpace);
if (child.align === FlexLayerAlignment.End) {
endChildren.push({ widget, columns, rows });
endColumns += columns;
} else if (child.align === FlexLayerAlignment.Center) {
centerChildren.push({ widget, columns, rows });
centerColumns += columns;
} else {
startChildren.push({ widget, columns, rows });
startColumns += columns;
}
}
const alignmentInfo: AlignmentInfo[] = [
{
alignment: FlexLayerAlignment.Start,
children: startChildren,
columns: startColumns,
},
{
alignment: FlexLayerAlignment.Center,
children: centerChildren,
columns: centerColumns,
},
{
alignment: FlexLayerAlignment.End,
children: endChildren,
columns: endColumns,
},
];
const wrappingInfo: AlignmentInfo[][] = isMobile
? getWrappedAlignmentInfo(alignmentInfo)
: [alignmentInfo];
let highlights: HighlightInfo[] = [];
for (const each of wrappingInfo) {
if (!each.length) continue;
/**
* On mobile viewport,
* if the row is wrapped, i.e. it contains less than all three alignments
* and if total columns required by these alignments are zero;
* then don't add a highlight for them as they will be squashed.
*/
if (
isMobile &&
each.length < 3 &&
each.reduce((a, b) => a + b.columns, 0) === 0
)
continue;
let index = 0;
for (const item of each) {
let avoidInitialHighlight = false;
let startPosition: number | undefined;
if (item.alignment === FlexLayerAlignment.Center) {
const { centerSize } = getAlignmentSizeInfo(each);
avoidInitialHighlight =
((startColumns > 25 || endColumns > 25) && centerColumns === 0) ||
centerSize === 0;
if (each.length === 2)
startPosition =
index === 0
? centerSize / 2
: GridDefaults.DEFAULT_GRID_COLUMNS - centerSize / 2;
}
highlights.push(
...generateHighlightsForAlignment({
arr: item.children.map((each: AlignmentChildren) => each.widget),
childCount:
item.alignment === FlexLayerAlignment.Start
? childCount
: item.alignment === FlexLayerAlignment.Center
? childCount + startChildren.length
: childCount + startChildren.length + centerChildren.length,
layerIndex,
alignment: item.alignment,
maxHeight,
offsetTop,
canvasId,
parentColumnSpace: columnSpace,
isMobile,
avoidInitialHighlight,
startPosition,
}),
);
index += 1;
}
}
highlights = updateVerticalHighlightDropZone(highlights, columnSpace * 64);
return { highlights, childCount: count };
}
/**
* Generate highlight information for a single alignment within a layer.
* - For each widget in the alignment
* - generate a highlight to mark the the start position of the widget.
* - Add another highlight at the end position of the last widget in the alignment.
* - If the alignment has no children, then add an initial highlight to mark the start of the alignment.
*/
export function generateHighlightsForAlignment(data: {
arr: FlattenedWidgetProps[];
childCount: number;
layerIndex: number;
alignment: FlexLayerAlignment;
maxHeight: number;
offsetTop: number;
canvasId: string;
parentColumnSpace: number;
avoidInitialHighlight?: boolean;
isMobile: boolean;
startPosition: number | undefined;
}): HighlightInfo[] {
const {
alignment,
arr,
avoidInitialHighlight,
canvasId,
childCount,
isMobile,
layerIndex,
maxHeight,
offsetTop,
parentColumnSpace,
startPosition,
} = data;
const res: HighlightInfo[] = [];
let count = 0;
const rowSpace: number = GridDefaults.DEFAULT_GRID_ROW_HEIGHT;
for (const child of arr) {
const left = getLeftColumn(child, isMobile);
res.push({
isNewLayer: false,
index: count + childCount,
layerIndex,
rowIndex: count,
alignment,
posX: left * parentColumnSpace + FLEXBOX_PADDING / 2,
posY: getTopRow(child, isMobile) * rowSpace + FLEXBOX_PADDING,
width: DEFAULT_HIGHLIGHT_SIZE,
height: isMobile
? getWidgetHeight(child, isMobile) * rowSpace
: maxHeight,
isVertical: true,
canvasId,
dropZone: {},
});
count += 1;
}
if (!avoidInitialHighlight) {
const lastChild: FlattenedWidgetProps | null =
arr && arr.length ? arr[arr.length - 1] : null;
res.push({
isNewLayer: false,
index: count + childCount,
layerIndex,
rowIndex: count,
alignment,
posX: getPositionForInitialHighlight(
res,
alignment,
lastChild !== null
? getRightColumn(lastChild, isMobile) * parentColumnSpace +
FLEXBOX_PADDING / 2
: 0,
canvasId,
parentColumnSpace,
startPosition,
),
posY:
lastChild === null
? offsetTop
: getTopRow(lastChild, isMobile) * rowSpace + FLEXBOX_PADDING,
width: DEFAULT_HIGHLIGHT_SIZE,
height:
isMobile && lastChild !== null
? getWidgetHeight(lastChild, isMobile) * rowSpace
: maxHeight,
isVertical: true,
canvasId,
dropZone: {},
});
}
return res;
}
/**
* Get the position of the initial / final highlight for an alignment.
* @param highlights | HighlightInfo[] : highlights for the current alignment
* @param alignment | FlexLayerAlignment : alignment of the current highlights
* @param posX | number : end position of the last widget in the current alignment. (rightColumn * columnSpace)
* @param containerWidth | number : width of the container
* @param canvasId | string : id of the canvas
* @returns number
*/
function getPositionForInitialHighlight(
highlights: HighlightInfo[],
alignment: FlexLayerAlignment,
posX: number,
canvasId: string,
columnSpace: number,
startPosition: number | undefined,
): number {
const containerWidth = GridDefaults.DEFAULT_GRID_COLUMNS * columnSpace;
const endPosition = containerWidth + FLEXBOX_PADDING / 2;
if (alignment === FlexLayerAlignment.End) {
return endPosition;
} else if (alignment === FlexLayerAlignment.Center) {
if (!highlights.length)
return startPosition !== undefined ? startPosition : containerWidth / 2;
return Math.min(posX, endPosition);
} else {
if (!highlights.length) return 2;
return Math.min(posX, endPosition);
}
}
/**
* Create a layer of horizontal alignments to denote new vertical drop zones.
* - if the layer has a fill widget,
* - Start alignment spans the entire container width.
* - else each layer takes up a third of the container width and are placed side to side.
* @param childIndex | number : child count of children placed in preceding layers.
* @param layerIndex | number
* @param offsetTop | number
* @param containerWidth | number
* @param canvasId |
* @param hasFillWidget | boolean : whether the layer has a fill widget or not.
* @returns HighlightInfo[]
*/
function generateHorizontalHighlights(
childIndex: number,
layerIndex: number,
offsetTop: number,
containerWidth: number,
canvasId: string,
hasFillWidget: boolean,
rowHeight: number,
previousOffset: number,
isMobile: boolean,
): HighlightInfo[] {
const width = containerWidth / 3;
const arr: HighlightInfo[] = [];
const dropZone: DropZone = {
top: previousOffset === -1 ? offsetTop : (offsetTop - previousOffset) * 0.5,
bottom: rowHeight === -1 ? 10000 : rowHeight * 0.5,
};
const rowGap = isMobile ? MOBILE_ROW_GAP : ROW_GAP;
offsetTop = previousOffset === -1 ? offsetTop : offsetTop - rowGap;
[
FlexLayerAlignment.Start,
FlexLayerAlignment.Center,
FlexLayerAlignment.End,
].forEach((alignment, index) => {
arr.push({
isNewLayer: true,
index: childIndex,
layerIndex,
rowIndex: 0,
alignment,
posX: hasFillWidget
? alignment === FlexLayerAlignment.Start
? canvasId === MAIN_CONTAINER_WIDGET_ID
? FLEXBOX_PADDING
: FLEXBOX_PADDING * 1.5
: containerWidth
: width * index + FLEXBOX_PADDING,
posY: offsetTop,
width: hasFillWidget
? alignment === FlexLayerAlignment.Start
? containerWidth -
(canvasId === MAIN_CONTAINER_WIDGET_ID ? 0 : FLEXBOX_PADDING)
: 0
: width,
height: DEFAULT_HIGHLIGHT_SIZE,
isVertical: false,
canvasId,
dropZone,
});
});
return arr;
}
/**
* Calculate drop zones for vertical highlights.
* Drop zone of vertical highlights span 35% of the distance between two consecutive highlights.
* @param highlights | HighlightInfo[] : array of highlights to be updated.
* @param canvasWidth | number : width of the canvas.
* @returns HighlightInfo[] : updated highlights.
*/
function updateVerticalHighlightDropZone(
highlights: HighlightInfo[],
canvasWidth: number,
): HighlightInfo[] {
const zoneSize = 0.35;
for (const [index, highlight] of highlights.entries()) {
const nextHighlight: HighlightInfo | undefined = highlights[index + 1];
const previousHighlight: HighlightInfo | undefined = highlights[index - 1];
const leftZone = Math.max(
previousHighlight
? (highlight.posX -
(highlight.posY < previousHighlight.posY + previousHighlight.height
? previousHighlight.posX
: 0)) *
zoneSize
: highlight.posX + DEFAULT_HIGHLIGHT_SIZE,
DEFAULT_HIGHLIGHT_SIZE,
);
const rightZone = Math.max(
nextHighlight
? ((highlight.posY + highlight.height > nextHighlight.posY
? nextHighlight.posX
: canvasWidth) -
highlight.posX) *
zoneSize
: canvasWidth - highlight.posX,
DEFAULT_HIGHLIGHT_SIZE,
);
highlights[index] = {
...highlight,
dropZone: {
left: leftZone,
right: rightZone,
},
};
}
return highlights;
}
/**
* Update drop zones for horizontal highlights of the last row.
* Normally, bottom drop zone of a horizontal highlights spans 50% of the row height.
* However, if the next row of horizontal highlights is omitted on account of the dragged widgets,
* then update the previous row's bottom drop zone to span 100% of the row height.
* @param highlights | HighlightInfo[] : array of highlights to be updated.
* @returns HighlightInfo[] : updated highlights.
*/
function updateHorizontalDropZone(
highlights: HighlightInfo[],
): HighlightInfo[] {
let index = highlights.length - 1;
while (index >= 0 && highlights[index].isVertical) {
index -= 1;
}
if (index < 0) return highlights;
const dropZone = {
top: highlights[index].dropZone.top,
bottom: (highlights[index]?.dropZone?.bottom || 5) * 2,
};
const updatedHighlights: HighlightInfo[] = [
...highlights.slice(0, index - 2),
{
...highlights[index - 2],
dropZone,
},
{
...highlights[index - 1],
dropZone,
},
{
...highlights[index],
dropZone,
},
...highlights.slice(index + 1),
];
return updatedHighlights;
}
function getPreviousOffsetTop(highlights: HighlightInfo[]): number {
if (!highlights.length) return -1;
let index = highlights.length - 1;
while (highlights[index].isVertical) {
index--;
}
return highlights[index].posY + highlights[index].height;
}