PromucFlow_constructor/app/client/src/utils/WidgetPropsUtils.tsx
Ivan Akulov 424d2f6965
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
7cbb12af88,
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 17:11:47 +05:30

341 lines
8.9 KiB
TypeScript

import type { FetchPageResponse } from "api/PageApi";
import type { WidgetConfigProps } from "reducers/entityReducers/widgetConfigReducer";
import type { WidgetOperation, WidgetProps } from "widgets/BaseWidget";
import { WidgetOperations } from "widgets/BaseWidget";
import type { RenderMode } from "constants/WidgetConstants";
import {
CONTAINER_GRID_PADDING,
GridDefaults,
WIDGET_PADDING,
} from "constants/WidgetConstants";
import { snapToGrid } from "./helpers";
import type { OccupiedSpace } from "constants/CanvasEditorConstants";
import defaultTemplate from "templates/default";
import type { FlattenedWidgetProps } from "reducers/entityReducers/canvasWidgetsReducer";
import { transformDSL } from "./DSLMigrations";
import type { WidgetType } from "./WidgetFactory";
import type { DSLWidget } from "widgets/constants";
import type { WidgetDraggingBlock } from "pages/common/CanvasArenas/hooks/useBlocksToBeDraggedOnCanvas";
import type { XYCord } from "pages/common/CanvasArenas/hooks/useRenderBlocksOnCanvas";
import type { ContainerWidgetProps } from "widgets/ContainerWidget/widget";
import type { BlockSpace, GridProps } from "reflow/reflowTypes";
import type { Rect } from "./boxHelpers";
import { areIntersecting } from "./boxHelpers";
export type WidgetOperationParams = {
operation: WidgetOperation;
widgetId: string;
payload: any;
};
const defaultDSL = defaultTemplate;
export const extractCurrentDSL = (
fetchPageResponse?: FetchPageResponse,
): DSLWidget => {
const newPage = !fetchPageResponse;
const currentDSL = fetchPageResponse?.data.layouts[0].dsl || {
...defaultDSL,
};
return transformDSL(currentDSL as ContainerWidgetProps<WidgetProps>, newPage);
};
/**
* To get updated positions of the dragging blocks
*
* @param draggingBlocks
* @param snapColumnSpace
* @param snapRowSpace
* @returns An array of updated positions of the dragging blocks
*/
export function getDraggingSpacesFromBlocks(
draggingBlocks: WidgetDraggingBlock[],
snapColumnSpace: number,
snapRowSpace: number,
): BlockSpace[] {
const draggingSpaces = [];
for (const draggingBlock of draggingBlocks) {
//gets top and left position of the block
const [leftColumn, topRow] = getDropZoneOffsets(
snapColumnSpace,
snapRowSpace,
{
x: draggingBlock.left,
y: draggingBlock.top,
},
{
x: 0,
y: 0,
},
);
draggingSpaces.push({
left: leftColumn,
top: topRow,
right: leftColumn + draggingBlock.width / snapColumnSpace,
bottom: topRow + draggingBlock.height / snapRowSpace,
id: draggingBlock.widgetId,
fixedHeight:
draggingBlock.fixedHeight !== undefined
? draggingBlock.rowHeight
: undefined,
});
}
return draggingSpaces;
}
export const getDropZoneOffsets = (
colWidth: number,
rowHeight: number,
dragOffset: XYCord,
parentOffset: XYCord,
) => {
// Calculate actual drop position by snapping based on x, y and grid cell size
return snapToGrid(
colWidth,
rowHeight,
dragOffset.x - parentOffset.x,
dragOffset.y - parentOffset.y,
);
};
export const getMousePositionsOnCanvas = (
e: MouseEvent,
gridProps: GridProps,
) => {
const mouseTop = Math.floor(
(e.offsetY - CONTAINER_GRID_PADDING - WIDGET_PADDING) /
gridProps.parentRowSpace,
);
const mouseLeft = Math.floor(
(e.offsetX - CONTAINER_GRID_PADDING - WIDGET_PADDING) /
gridProps.parentColumnSpace,
);
return {
id: "mouse",
top: mouseTop,
left: mouseLeft,
bottom: mouseTop + 1,
right: mouseLeft + 1,
};
};
export const isDropZoneOccupied = (
offset: Rect,
widgetId: string,
occupied?: OccupiedSpace[],
) => {
if (occupied) {
occupied = occupied.filter((widgetDetails) => {
return (
widgetDetails.id !== widgetId && widgetDetails.parentId !== widgetId
);
});
for (let i = 0; i < occupied.length; i++) {
if (areIntersecting(occupied[i], offset)) {
return true;
}
}
return false;
}
return false;
};
export const isWidgetOverflowingParentBounds = (
parentRowCols: { rows?: number; cols?: number },
offset: Rect,
): boolean => {
return (
offset.right < 0 ||
offset.top < 0 ||
(parentRowCols.cols || GridDefaults.DEFAULT_GRID_COLUMNS) < offset.right ||
(parentRowCols.rows || 0) < offset.bottom
);
};
export const noCollision = (
clientOffset: XYCord,
colWidth: number,
rowHeight: number,
dropTargetOffset: XYCord,
widgetWidth: number,
widgetHeight: number,
widgetId: string,
occupiedSpaces?: OccupiedSpace[],
rows?: number,
cols?: number,
detachFromLayout = false,
): boolean => {
if (detachFromLayout) {
return true;
}
if (clientOffset && dropTargetOffset) {
const [left, top] = getDropZoneOffsets(
colWidth,
rowHeight,
clientOffset as XYCord,
dropTargetOffset,
);
if (left < 0 || top < 0) {
return false;
}
const currentOffset = {
left,
right: left + widgetWidth,
top,
bottom: top + widgetHeight,
};
return (
!isDropZoneOccupied(currentOffset, widgetId, occupiedSpaces) &&
!isWidgetOverflowingParentBounds({ rows, cols }, currentOffset)
);
}
return false;
};
export const currentDropRow = (
dropTargetRowSpace: number,
dropTargetVerticalOffset: number,
draggableItemVerticalOffset: number,
widget: WidgetProps & Partial<WidgetConfigProps>,
) => {
const widgetHeight = widget.rows
? widget.rows
: widget.bottomRow - widget.topRow;
const top = Math.round(
(draggableItemVerticalOffset - dropTargetVerticalOffset) /
dropTargetRowSpace,
);
const currentBottomOffset = top + widgetHeight;
return currentBottomOffset;
};
export const widgetOperationParams = (
widget: WidgetProps & Partial<WidgetConfigProps>,
widgetOffset: XYCord,
parentOffset: XYCord,
parentColumnSpace: number,
parentRowSpace: number,
parentWidgetId: string, // parentWidget
widgetSizeUpdates: {
width: number;
height: number;
},
fullWidth = false,
): WidgetOperationParams => {
const [leftColumn, topRow] = getDropZoneOffsets(
parentColumnSpace,
parentRowSpace,
widgetOffset,
parentOffset,
);
// If this is an existing widget, we'll have the widgetId
// Therefore, this is a move operation on drop of the widget
if (widget.widgetName) {
return {
operation: WidgetOperations.MOVE,
widgetId: widget.widgetId,
payload: {
leftColumn,
topRow,
bottomRow: Math.round(
topRow + widgetSizeUpdates.height / parentRowSpace,
),
rightColumn: fullWidth
? 64
: Math.round(
leftColumn + widgetSizeUpdates.width / parentColumnSpace,
),
parentId: widget.parentId,
newParentId: parentWidgetId,
},
};
// If this is not an existing widget, we'll not have the widgetId
// Therefore, this is an operation to add child to this container
}
const widgetDimensions = {
columns: fullWidth ? 64 : widget.columns,
rows: widget.rows,
};
return {
operation: WidgetOperations.ADD_CHILD,
widgetId: parentWidgetId,
payload: {
type: widget.type,
leftColumn,
topRow,
...widgetDimensions,
parentRowSpace,
parentColumnSpace,
newWidgetId: widget.widgetId,
},
};
};
export const getCanvasSnapRows = (
bottomRow: number,
mobileBottomRow?: number,
isMobile?: boolean,
isAutoLayoutActive?: boolean,
): number => {
const bottom =
isMobile && mobileBottomRow !== undefined && isAutoLayoutActive
? mobileBottomRow
: bottomRow;
const totalRows = Math.floor(bottom / GridDefaults.DEFAULT_GRID_ROW_HEIGHT);
return totalRows - 1;
};
export const getSnapColumns = (): number => {
return GridDefaults.DEFAULT_GRID_COLUMNS;
};
export const generateWidgetProps = (
parent: FlattenedWidgetProps,
type: WidgetType,
leftColumn: number,
topRow: number,
parentRowSpace: number,
parentColumnSpace: number,
widgetName: string,
widgetConfig: {
widgetId: string;
renderMode: RenderMode;
} & Partial<WidgetProps>,
version: number,
): DSLWidget => {
if (parent) {
const sizes = {
leftColumn,
rightColumn: leftColumn + widgetConfig.columns,
topRow,
bottomRow: topRow + widgetConfig.rows,
};
const others = {};
const props: DSLWidget = {
// Todo(abhinav): abstraction leak
isVisible: "MODAL_WIDGET" === type ? undefined : true,
...widgetConfig,
type,
widgetName,
isLoading: false,
parentColumnSpace,
parentRowSpace,
...sizes,
...others,
parentId: parent.widgetId,
version,
};
delete props.rows;
delete props.columns;
return props;
} else {
if (parent) {
throw Error("Failed to create widget: Parent's size cannot be calculate");
} else throw Error("Failed to create widget: Parent was not provided ");
}
};