PromucFlow_constructor/app/client/src/sagas/AutoLayoutUpdateSagas.tsx

418 lines
12 KiB
TypeScript
Raw Normal View History

import { updateAndSaveLayout } from "actions/pageActions";
import type { ReduxAction } from "@appsmith/constants/ReduxActionConstants";
import {
ReduxActionErrorTypes,
ReduxActionTypes,
} from "@appsmith/constants/ReduxActionConstants";
import log from "loglevel";
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 } from "reducers/entityReducers/canvasWidgetsReducer";
import {
all,
call,
debounce,
put,
select,
takeLatest,
} from "redux-saga/effects";
import {
alterLayoutForDesktop,
alterLayoutForMobile,
getCanvasDimensions,
} from "utils/autoLayout/AutoLayoutUtils";
import { getWidgets } from "./selectors";
import { AppPositioningTypes } from "reducers/entityReducers/pageListReducer";
import {
GridDefaults,
MAIN_CONTAINER_WIDGET_ID,
} from "constants/WidgetConstants";
import {
getCurrentApplicationId,
getCurrentAppPositioningType,
getIsAutoLayout,
getIsAutoLayoutMobileBreakPoint,
getMainCanvasProps,
} from "selectors/editorSelectors";
import type { MainCanvasReduxState } from "reducers/uiReducers/mainCanvasReducer";
import { updateLayoutForMobileBreakpointAction } from "actions/autoLayoutActions";
import CanvasWidgetsNormalizer from "normalizers/CanvasWidgetsNormalizer";
import convertDSLtoAuto from "utils/DSLConversions/fixedToAutoLayout";
import { convertNormalizedDSLToFixed } from "utils/DSLConversions/autoToFixedLayout";
import { updateWidgetPositions } from "utils/autoLayout/positionUtils";
import { getCanvasWidth as getMainCanvasWidth } from "selectors/editorSelectors";
import {
getLeftColumn,
getTopRow,
getWidgetMinMaxDimensionsInPixel,
setBottomRow,
setRightColumn,
} from "utils/autoLayout/flexWidgetUtils";
import { updateMultipleWidgetPropertiesAction } from "actions/controlActions";
import { isEmpty } from "lodash";
import { mutation_setPropertiesToUpdate } from "./autoHeightSagas/helpers";
import { updateApplication } from "@appsmith/actions/applicationActions";
import { getIsCurrentlyConvertingLayout } from "selectors/autoLayoutSelectors";
import { getIsResizing } from "selectors/widgetSelectors";
import { generateAutoHeightLayoutTreeAction } from "actions/autoHeightActions";
import type { AppState } from "@appsmith/reducers";
export function* updateLayoutForMobileCheckpoint(
actionPayload: ReduxAction<{
parentId: string;
isMobile: boolean;
canvasWidth: number;
widgets?: CanvasWidgetsReduxState;
}>,
) {
try {
const start = performance.now();
const isAutoLayout: boolean = yield select(getIsAutoLayout);
if (!isAutoLayout) return;
//Do not recalculate columns and update layout while converting layout
const isCurrentlyConvertingLayout: boolean = yield select(
getIsCurrentlyConvertingLayout,
);
if (isCurrentlyConvertingLayout) return;
const {
canvasWidth,
isMobile,
parentId,
widgets: payloadWidgets,
} = actionPayload.payload;
let allWidgets: CanvasWidgetsReduxState;
if (payloadWidgets) {
allWidgets = payloadWidgets;
} else {
allWidgets = yield select(getWidgets);
}
const mainCanvasWidth: number = yield select(getMainCanvasWidth);
const updatedWidgets: CanvasWidgetsReduxState = isMobile
? alterLayoutForMobile(allWidgets, parentId, canvasWidth, mainCanvasWidth)
: alterLayoutForDesktop(allWidgets, parentId, mainCanvasWidth);
yield put(updateAndSaveLayout(updatedWidgets));
yield put(generateAutoHeightLayoutTreeAction(true, true));
log.debug(
"Auto Layout : updating layout for mobile viewport took",
performance.now() - start,
"ms",
);
} catch (error) {
yield put({
type: ReduxActionErrorTypes.WIDGET_OPERATION_ERROR,
payload: {
action: ReduxActionTypes.RECALCULATE_COLUMNS,
error,
},
});
}
}
/**
* This Method is called when fixed and Auto are switched between each other using the Switch button on the right Pane
* @param actionPayload
* @returns
*/
export function* updateLayoutPositioningSaga(
actionPayload: ReduxAction<AppPositioningTypes>,
) {
try {
const currPositioningType: AppPositioningTypes = yield select(
getCurrentAppPositioningType,
);
const payloadPositioningType = actionPayload.payload;
if (currPositioningType === payloadPositioningType) return;
const allWidgets: CanvasWidgetsReduxState = yield select(getWidgets);
//Convert Fixed Layout to Auto
if (payloadPositioningType === AppPositioningTypes.AUTO) {
const denormalizedDSL = CanvasWidgetsNormalizer.denormalize(
MAIN_CONTAINER_WIDGET_ID,
{ canvasWidgets: allWidgets },
);
const autoDSL = convertDSLtoAuto(denormalizedDSL);
log.debug("autoDSL", autoDSL);
yield put(
updateAndSaveLayout(
CanvasWidgetsNormalizer.normalize(autoDSL).entities.canvasWidgets,
),
);
yield call(recalculateAutoLayoutColumnsAndSave);
}
// Convert Auto layout to fixed
else {
yield put(
updateAndSaveLayout(convertNormalizedDSLToFixed(allWidgets, "DESKTOP")),
);
}
yield call(updateApplicationLayoutType, payloadPositioningType);
} catch (error) {
yield put({
type: ReduxActionErrorTypes.WIDGET_OPERATION_ERROR,
payload: {
action: ReduxActionTypes.RECALCULATE_COLUMNS,
error,
},
});
}
}
//This Method is used to re calculate Positions based on canvas width
export function* recalculateAutoLayoutColumnsAndSave(
widgets?: CanvasWidgetsReduxState,
) {
const appPositioningType: AppPositioningTypes = yield select(
getCurrentAppPositioningType,
);
const mainCanvasProps: MainCanvasReduxState = yield select(
getMainCanvasProps,
);
yield put(
updateLayoutForMobileBreakpointAction(
MAIN_CONTAINER_WIDGET_ID,
appPositioningType === AppPositioningTypes.AUTO
? mainCanvasProps?.isMobile
: false,
mainCanvasProps.width,
widgets,
),
);
}
let autoLayoutWidgetDimensionUpdateBatch: Record<
string,
{ width: number; height: number }
> = {};
function batchWidgetDimensionsUpdateForAutoLayout(
widgetId: string,
width: number,
height: number,
) {
autoLayoutWidgetDimensionUpdateBatch[widgetId] = { width, height };
}
function* updateWidgetDimensionsSaga(
action: ReduxAction<{ widgetId: string; width: number; height: number }>,
) {
let { height, width } = action.payload;
const { widgetId } = action.payload;
const allWidgets: CanvasWidgetsReduxState = yield select(getWidgets);
const mainCanvasWidth: number = yield select(getMainCanvasWidth);
const isMobile: boolean = yield select(getIsAutoLayoutMobileBreakPoint);
const isWidgetResizing: boolean = yield select(getIsResizing);
const isCanvasResizing: boolean = yield select(
(state: AppState) => state.ui.widgetDragResize.isAutoCanvasResizing,
);
const widget = allWidgets[widgetId];
if (!widget) return;
const widgetMinMaxDimensions = getWidgetMinMaxDimensionsInPixel(
widget,
mainCanvasWidth,
);
if (!isMobile && widget.widthInPercentage) {
width = widget.widthInPercentage * mainCanvasWidth;
}
if (isMobile && widget.mobileWidthInPercentage) {
width = widget.mobileWidthInPercentage * mainCanvasWidth;
}
if (
widgetMinMaxDimensions.minHeight &&
height < widgetMinMaxDimensions.minHeight
) {
height = widgetMinMaxDimensions.minHeight;
}
if (
widgetMinMaxDimensions.maxHeight &&
height > widgetMinMaxDimensions.maxHeight
) {
height = widgetMinMaxDimensions.maxHeight;
}
if (
widgetMinMaxDimensions.minWidth &&
width < widgetMinMaxDimensions.minWidth
) {
width = widgetMinMaxDimensions.minWidth;
}
if (
widgetMinMaxDimensions.maxWidth &&
width > widgetMinMaxDimensions.maxWidth
) {
width = widgetMinMaxDimensions.maxWidth;
}
batchWidgetDimensionsUpdateForAutoLayout(widgetId, width, height);
if (!isWidgetResizing && !isCanvasResizing) {
yield put({
type: ReduxActionTypes.PROCESS_AUTO_LAYOUT_DIMENSION_UPDATES,
});
}
}
/**
* This saga is responsible for updating the bounding box of the widget
* when the widget component get resized internally.
* It also updates the position of other affected widgets as well.
*/
function* processAutoLayoutDimensionUpdatesSaga() {
if (Object.keys(autoLayoutWidgetDimensionUpdateBatch).length === 0) return;
const allWidgets: CanvasWidgetsReduxState = yield select(getWidgets);
const mainCanvasWidth: number = yield select(getMainCanvasWidth);
const isMobile: boolean = yield select(getIsAutoLayoutMobileBreakPoint);
let widgets = allWidgets;
const widgetsOld = { ...widgets };
const parentIds = new Set<string>();
// Iterate through the batch and update the new dimensions
for (const widgetId in autoLayoutWidgetDimensionUpdateBatch) {
const { height, width } = autoLayoutWidgetDimensionUpdateBatch[widgetId];
const widget = allWidgets[widgetId];
if (!widget) continue;
const parentId = widget.parentId;
if (parentId === undefined) continue;
if (parentId) parentIds.add(parentId);
const { columnSpace } = getCanvasDimensions(
widgets[parentId],
widgets,
mainCanvasWidth,
isMobile,
);
//get row space
const rowSpace = widget.detachFromLayout
? 1
: GridDefaults.DEFAULT_GRID_ROW_HEIGHT;
let widgetToBeUpdated = { ...widget };
widgetToBeUpdated = setBottomRow(
widgetToBeUpdated,
getTopRow(widget, isMobile) + height / rowSpace,
isMobile,
);
widgetToBeUpdated = setRightColumn(
widgetToBeUpdated,
getLeftColumn(widget, isMobile) + width / columnSpace,
isMobile,
);
widgets = {
...widgets,
[widgetId]: widgetToBeUpdated,
};
}
// Update the position of all the widgets
for (const parentId of parentIds) {
widgets = updateWidgetPositions(
widgets,
parentId,
isMobile,
mainCanvasWidth,
);
}
let widgetsToUpdate: any = {};
/**
* Iterate over all widgets and check if any of their dimensions have changed
* If they have, add them to the list of widgets to update
* Note: We need to iterate through all widgets since changing dimension of one widget might affect the dimensions of other widgets
*/
for (const widgetId of Object.keys(widgets)) {
const widget = widgets[widgetId];
const oldWidget = widgetsOld[widgetId];
const propertiesToUpdate: Record<string, any> = {};
const positionProperties = [
"topRow",
"bottomRow",
"leftColumn",
"rightColumn",
"mobileTopRow",
"mobileBottomRow",
"mobileLeftColumn",
"mobileRightColumn",
];
for (const prop of positionProperties) {
if (widget[prop] !== oldWidget[prop]) {
propertiesToUpdate[prop] = widget[prop];
}
}
if (isEmpty(propertiesToUpdate)) continue;
widgetsToUpdate = mutation_setPropertiesToUpdate(
widgetsToUpdate,
widgetId,
propertiesToUpdate,
);
}
// Push all updates to the CanvasWidgetsReducer.
// Note that we're not calling `UPDATE_LAYOUT`
// as we don't need to trigger an eval
if (!isEmpty(widgetsToUpdate)) {
yield put(updateMultipleWidgetPropertiesAction(widgetsToUpdate));
}
// clear the batch after processing
autoLayoutWidgetDimensionUpdateBatch = {};
}
export function* updateApplicationLayoutType(
positioningType: AppPositioningTypes,
) {
const applicationId: string = yield select(getCurrentApplicationId);
yield put(
updateApplication(applicationId || "", {
applicationDetail: {
appPositioning: {
type: positioningType,
},
},
}),
);
}
export default function* layoutUpdateSagas() {
yield all([
takeLatest(
ReduxActionTypes.RECALCULATE_COLUMNS,
updateLayoutForMobileCheckpoint,
),
takeLatest(
ReduxActionTypes.UPDATE_LAYOUT_POSITIONING,
updateLayoutPositioningSaga,
),
takeLatest(
ReduxActionTypes.UPDATE_WIDGET_DIMENSIONS,
updateWidgetDimensionsSaga,
),
debounce(
50,
ReduxActionTypes.PROCESS_AUTO_LAYOUT_DIMENSION_UPDATES,
processAutoLayoutDimensionUpdatesSaga,
),
]);
}