PromucFlow_constructor/app/client/src/utils/autoLayout/highlightUtils.ts

547 lines
16 KiB
TypeScript
Raw Normal View History

import { FlexLayerAlignment } from "utils/autoLayout/constants";
import { DEFAULT_HIGHLIGHT_SIZE } from "components/designSystems/appsmith/autoLayout/FlexBoxComponent";
import {
FLEXBOX_PADDING,
GridDefaults,
MAIN_CONTAINER_WIDGET_ID,
} from "constants/WidgetConstants";
chore: upgrade to prettier v2 + enforce import types (#21013)Co-authored-by: Satish Gandham <hello@satishgandham.com> Co-authored-by: Satish Gandham <satish.iitg@gmail.com> ## Description This PR upgrades Prettier to v2 + enforces TypeScript’s [`import type`](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-8.html#type-only-imports-and-export) syntax where applicable. It’s submitted as a separate PR so we can merge it easily. As a part of this PR, we reformat the codebase heavily: - add `import type` everywhere where it’s required, and - re-format the code to account for Prettier 2’s breaking changes: https://prettier.io/blog/2020/03/21/2.0.0.html#breaking-changes This PR is submitted against `release` to make sure all new code by team members will adhere to new formatting standards, and we’ll have fewer conflicts when merging `bundle-optimizations` into `release`. (I’ll merge `release` back into `bundle-optimizations` once this PR is merged.) ### Why is this needed? This PR is needed because, for the Lodash optimization from https://github.com/appsmithorg/appsmith/commit/7cbb12af886621256224be0c93e6a465dd710ad3, we need to use `import type`. Otherwise, `babel-plugin-lodash` complains that `LoDashStatic` is not a lodash function. However, just using `import type` in the current codebase will give you this: <img width="962" alt="Screenshot 2023-03-08 at 17 45 59" src="https://user-images.githubusercontent.com/2953267/223775744-407afa0c-e8b9-44a1-90f9-b879348da57f.png"> That’s because Prettier 1 can’t parse `import type` at all. To parse it, we need to upgrade to Prettier 2. ### Why enforce `import type`? Apart from just enabling `import type` support, this PR enforces specifying `import type` everywhere it’s needed. (Developers will get immediate TypeScript and ESLint errors when they forget to do so.) I’m doing this because I believe `import type` improves DX and makes refactorings easier. Let’s say you had a few imports like below. Can you tell which of these imports will increase the bundle size? (Tip: it’s not all of them!) ```ts // app/client/src/workers/Linting/utils.ts import { Position } from "codemirror"; import { LintError as JSHintError, LintOptions } from "jshint"; import { get, isEmpty, isNumber, keys, last, set } from "lodash"; ``` It’s pretty hard, right? What about now? ```ts // app/client/src/workers/Linting/utils.ts import type { Position } from "codemirror"; import type { LintError as JSHintError, LintOptions } from "jshint"; import { get, isEmpty, isNumber, keys, last, set } from "lodash"; ``` Now, it’s clear that only `lodash` will be bundled. This helps developers to see which imports are problematic, but it _also_ helps with refactorings. Now, if you want to see where `codemirror` is bundled, you can just grep for `import \{.*\} from "codemirror"` – and you won’t get any type-only imports. This also helps (some) bundlers. Upon transpiling, TypeScript erases type-only imports completely. In some environment (not ours), this makes the bundle smaller, as the bundler doesn’t need to bundle type-only imports anymore. ## Type of change - Chore (housekeeping or task changes that don't impact user perception) ## How Has This Been Tested? This was tested to not break the build. ### Test Plan > Add Testsmith test cases links that relate to this PR ### Issues raised during DP testing > Link issues raised during DP testing for better visiblity and tracking (copy link from comments dropped on this PR) ## Checklist: ### Dev activity - [x] My code follows the style guidelines of this project - [ ] I have performed a self-review of my own code - [ ] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [ ] 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 - [ ] PR is being merged under a feature flag ### QA activity: - [ ] Test plan has been approved by relevant developers - [ ] Test plan has been peer reviewed by QA - [ ] Cypress test cases have been added and approved by either SDET or manual QA - [ ] Organized project review call with relevant stakeholders after Round 1/2 of QA - [ ] Added Test Plan Approved label after reveiwing all Cypress test --------- Co-authored-by: Satish Gandham <hello@satishgandham.com> Co-authored-by: Satish Gandham <satish.iitg@gmail.com>
2023-03-16 11:41:47 +00:00
import type {
CanvasWidgetsReduxState,
FlattenedWidgetProps,
} from "reducers/entityReducers/canvasWidgetsReducer";
import {
getLeftColumn,
getRightColumn,
getTopRow,
getWidgetHeight,
getWidgetWidth,
} from "./flexWidgetUtils";
import {
getAlignmentSizeInfo,
getTotalRowsOfAllChildren,
getWrappedAlignmentInfo,
} from "./positionUtils";
chore: upgrade to prettier v2 + enforce import types (#21013)Co-authored-by: Satish Gandham <hello@satishgandham.com> Co-authored-by: Satish Gandham <satish.iitg@gmail.com> ## Description This PR upgrades Prettier to v2 + enforces TypeScript’s [`import type`](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-8.html#type-only-imports-and-export) syntax where applicable. It’s submitted as a separate PR so we can merge it easily. As a part of this PR, we reformat the codebase heavily: - add `import type` everywhere where it’s required, and - re-format the code to account for Prettier 2’s breaking changes: https://prettier.io/blog/2020/03/21/2.0.0.html#breaking-changes This PR is submitted against `release` to make sure all new code by team members will adhere to new formatting standards, and we’ll have fewer conflicts when merging `bundle-optimizations` into `release`. (I’ll merge `release` back into `bundle-optimizations` once this PR is merged.) ### Why is this needed? This PR is needed because, for the Lodash optimization from https://github.com/appsmithorg/appsmith/commit/7cbb12af886621256224be0c93e6a465dd710ad3, we need to use `import type`. Otherwise, `babel-plugin-lodash` complains that `LoDashStatic` is not a lodash function. However, just using `import type` in the current codebase will give you this: <img width="962" alt="Screenshot 2023-03-08 at 17 45 59" src="https://user-images.githubusercontent.com/2953267/223775744-407afa0c-e8b9-44a1-90f9-b879348da57f.png"> That’s because Prettier 1 can’t parse `import type` at all. To parse it, we need to upgrade to Prettier 2. ### Why enforce `import type`? Apart from just enabling `import type` support, this PR enforces specifying `import type` everywhere it’s needed. (Developers will get immediate TypeScript and ESLint errors when they forget to do so.) I’m doing this because I believe `import type` improves DX and makes refactorings easier. Let’s say you had a few imports like below. Can you tell which of these imports will increase the bundle size? (Tip: it’s not all of them!) ```ts // app/client/src/workers/Linting/utils.ts import { Position } from "codemirror"; import { LintError as JSHintError, LintOptions } from "jshint"; import { get, isEmpty, isNumber, keys, last, set } from "lodash"; ``` It’s pretty hard, right? What about now? ```ts // app/client/src/workers/Linting/utils.ts import type { Position } from "codemirror"; import type { LintError as JSHintError, LintOptions } from "jshint"; import { get, isEmpty, isNumber, keys, last, set } from "lodash"; ``` Now, it’s clear that only `lodash` will be bundled. This helps developers to see which imports are problematic, but it _also_ helps with refactorings. Now, if you want to see where `codemirror` is bundled, you can just grep for `import \{.*\} from "codemirror"` – and you won’t get any type-only imports. This also helps (some) bundlers. Upon transpiling, TypeScript erases type-only imports completely. In some environment (not ours), this makes the bundle smaller, as the bundler doesn’t need to bundle type-only imports anymore. ## Type of change - Chore (housekeeping or task changes that don't impact user perception) ## How Has This Been Tested? This was tested to not break the build. ### Test Plan > Add Testsmith test cases links that relate to this PR ### Issues raised during DP testing > Link issues raised during DP testing for better visiblity and tracking (copy link from comments dropped on this PR) ## Checklist: ### Dev activity - [x] My code follows the style guidelines of this project - [ ] I have performed a self-review of my own code - [ ] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [ ] 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 - [ ] PR is being merged under a feature flag ### QA activity: - [ ] Test plan has been approved by relevant developers - [ ] Test plan has been peer reviewed by QA - [ ] Cypress test cases have been added and approved by either SDET or manual QA - [ ] Organized project review call with relevant stakeholders after Round 1/2 of QA - [ ] Added Test Plan Approved label after reveiwing all Cypress test --------- Co-authored-by: Satish Gandham <hello@satishgandham.com> Co-authored-by: Satish Gandham <satish.iitg@gmail.com>
2023-03-16 11:41:47 +00:00
import type {
AlignmentChildren,
AlignmentInfo,
DropZone,
FlexLayer,
HighlightInfo,
LayerChild,
} from "./autoLayoutTypes";
/**
* @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;
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),
),
);
highlights.push(...payload.highlights);
layerIndex += 1;
} else highlights = updateHorizontalDropZone(highlights);
offsetTop += childrenRows * GridDefaults.DEFAULT_GRID_ROW_HEIGHT || 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),
),
);
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 = 64 * columnSpace;
const endPosition =
containerWidth +
(canvasId !== MAIN_CONTAINER_WIDGET_ID
? FLEXBOX_PADDING
: 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,
): 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,
};
[
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 = previousHighlight
? (highlight.posX - previousHighlight.posX) * zoneSize
: highlight.posX + DEFAULT_HIGHLIGHT_SIZE;
const rightZone = nextHighlight
? (nextHighlight.posX - highlight.posX) * zoneSize
: canvasWidth - highlight.posX;
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;
}