## 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>
341 lines
8.9 KiB
TypeScript
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 ");
|
|
}
|
|
};
|