PromucFlow_constructor/app/client/src/sagas/AutoLayoutUpdateSagas.tsx
Ashok Kumar M 7b527c9024
chore: Merge wds and anvil feature flags (#32609)
[![workerB](https://img.shields.io/endpoint?url=https%3A%2F%2Fworkerb.linearb.io%2Fv2%2Fbadge%2Fprivate%2FU2FsdGVkX1LNwrMHgs05enX0VDk8QxZH7uP7Ii4HE%2Fcollaboration.svg%3FcacheSeconds%3D60)](https://workerb.linearb.io/v2/badge/collaboration-page?magicLinkId=M7zehz4)
## Description

Cleaning up three patterns of checks to enable wds and anvil into two.
wds and anvil had to have different flags coz anvil had to play catch up
with wds, now that's not the case so it does not make sense to have two
flags.

Old patterns
- checking if the wds feature flag is enabled
- checking if the anvil feature flag is enabled
- checking if the layout system of the app is anvil

New Pattern
- checking if anvil feature flag is enabled (used only for creating an
anvil app)
- checking if layout system of the app is anvil

Fixes #32590
_or_  
Fixes `Issue URL`
> [!WARNING]  
> _If no issue exists, please create an issue first, and check with the
maintainers if the issue is valid._

## Automation

/ok-to-test tags="@tag.All"

### 🔍 Cypress test results
<!-- This is an auto-generated comment: Cypress test results  -->
> [!TIP]
> 🟢 🟢 🟢 All cypress tests have passed! 🎉 🎉 🎉
> Workflow run:
<https://github.com/appsmithorg/appsmith/actions/runs/8663918496>
> Commit: e10cc2a84ed680b29c49c5b2e8175df4c18da2f8
> Cypress dashboard url: <a
href="https://internal.appsmith.com/app/cypress-dashboard/rundetails-65890b3c81d7400d08fa9ee5?branch=master&workflowId=8663918496&attempt=1"
target="_blank">Click here!</a>

<!-- end of auto-generated comment: Cypress test results  -->

















<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **Refactor**
- Consolidated the usage of layout system checks across the application
to use a unified Anvil layout selector, enhancing consistency in
layout-related conditional logic.
- **Bug Fixes**
- Removed outdated feature flags related to the Anvil + WDS integration,
ensuring the application's feature toggling aligns with the current
development strategy.
- **Tests**
- Updated unit tests to align with the new method of layout system
determination, ensuring test environments accurately reflect production
behavior.
- **Chores**
- Cleaned up redundant code and feature flags that are no longer in use,
simplifying the codebase and reducing potential for errors.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2024-04-12 22:54:04 +05:30

538 lines
16 KiB
TypeScript

import { updateAndSaveLayout } from "actions/pageActions";
import type { ReduxAction } from "@appsmith/constants/ReduxActionConstants";
import {
ReduxActionErrorTypes,
ReduxActionTypes,
} from "@appsmith/constants/ReduxActionConstants";
import log from "loglevel";
import type {
CanvasWidgetsReduxState,
UpdateWidgetsPayload,
} from "reducers/entityReducers/canvasWidgetsReducer";
import {
all,
call,
debounce,
put,
select,
takeLatest,
} from "redux-saga/effects";
import {
alterLayoutForDesktop,
alterLayoutForMobile,
getCanvasDimensions,
} from "layoutSystems/autolayout/utils/AutoLayoutUtils";
import {
getCanvasAndMetaWidgets,
getWidgets,
getWidgetsMeta,
} from "./selectors";
import { LayoutSystemTypes } from "layoutSystems/types";
import {
GridDefaults,
MAIN_CONTAINER_WIDGET_ID,
} from "constants/WidgetConstants";
import {
getCurrentApplicationId,
getIsAutoLayout,
getIsAutoLayoutMobileBreakPoint,
getMainCanvasProps,
} from "selectors/editorSelectors";
import type { MainCanvasReduxState } from "reducers/uiReducers/mainCanvasReducer";
import { updateLayoutForMobileBreakpointAction } from "actions/autoLayoutActions";
import convertDSLtoAuto from "layoutSystems/common/DSLConversions/fixedToAutoLayout";
import { convertNormalizedDSLToFixed } from "layoutSystems/common/DSLConversions/autoToFixedLayout";
import { updateWidgetPositions } from "layoutSystems/autolayout/utils/positionUtils";
import { getCanvasWidth as getMainCanvasWidth } from "selectors/editorSelectors";
import {
getLeftColumn,
getTopRow,
getWidgetMinMaxDimensionsInPixel,
setBottomRow,
setRightColumn,
} from "layoutSystems/autolayout/utils/flexWidgetUtils";
import {
updateMultipleMetaWidgetPropertiesAction,
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";
import { nestDSL, flattenDSL } from "@shared/dsl";
import { getLayoutSystemType } from "selectors/layoutSystemSelectors";
import { getIsAnvilLayout } from "layoutSystems/anvil/integrations/selectors";
// Saga check : if layout system is not anvil, then run the saga
// An alternative implementation would be to re-use shouldRunSaga,
// however we will still have to check for individula action types.
// This seems cleaner
function* preventForAnvil(saga: any, action: ReduxAction<unknown>) {
const isAnvilLayout: boolean = yield select(getIsAnvilLayout);
if (!isAnvilLayout) {
yield call(shouldRunSaga, saga, action);
}
}
function* shouldRunSaga(saga: any, action: ReduxAction<unknown>) {
const isAutoLayout: boolean = yield select(getIsAutoLayout);
if (isAutoLayout) {
yield call(saga, action);
}
}
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 metaProps: Record<string, any> = yield select(getWidgetsMeta);
const updatedWidgets: CanvasWidgetsReduxState = isMobile
? alterLayoutForMobile(
allWidgets,
parentId,
canvasWidth,
canvasWidth,
false,
metaProps,
)
: alterLayoutForDesktop(
allWidgets,
parentId,
canvasWidth,
false,
metaProps,
);
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* updateLayoutSystemTypeSaga(
actionPayload: ReduxAction<LayoutSystemTypes>,
) {
try {
const currLayoutSystemType: LayoutSystemTypes =
yield select(getLayoutSystemType);
const payloadLayoutSystemType = actionPayload.payload;
if (currLayoutSystemType === payloadLayoutSystemType) return;
const allWidgets: CanvasWidgetsReduxState = yield select(getWidgets);
//Convert fixed layout to auto-layout
if (payloadLayoutSystemType === LayoutSystemTypes.AUTO) {
const nestedDSL = nestDSL(allWidgets);
const autoDSL = convertDSLtoAuto(nestedDSL);
log.debug("autoDSL", autoDSL);
const flattenedDSL = flattenDSL(autoDSL);
yield put(updateAndSaveLayout(flattenedDSL));
yield call(recalculateAutoLayoutColumnsAndSave);
}
// Convert auto-layout to fixed
else {
yield put(
updateAndSaveLayout(convertNormalizedDSLToFixed(allWidgets, "DESKTOP")),
);
}
yield call(updateApplicationLayoutType, payloadLayoutSystemType);
} 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 layoutSystemType: LayoutSystemTypes = yield select(getLayoutSystemType);
const mainCanvasProps: MainCanvasReduxState =
yield select(getMainCanvasProps);
yield put(
updateLayoutForMobileBreakpointAction(
MAIN_CONTAINER_WIDGET_ID,
layoutSystemType === LayoutSystemTypes.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(
getCanvasAndMetaWidgets,
);
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(
getCanvasAndMetaWidgets,
);
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,
};
}
const metaProps: Record<string, any> = yield select(getWidgetsMeta);
// Update the position of all the widgets
for (const parentId of parentIds) {
widgets = updateWidgetPositions(
widgets,
parentId,
isMobile,
mainCanvasWidth,
false,
metaProps,
);
}
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",
"height",
];
for (const prop of positionProperties) {
if (widget[prop] !== oldWidget[prop]) {
propertiesToUpdate[prop] = widget[prop];
}
}
if (isEmpty(propertiesToUpdate)) continue;
widgetsToUpdate = mutation_setPropertiesToUpdate(
widgetsToUpdate,
widgetId,
propertiesToUpdate,
);
}
const canvasWidgetsToUpdate: UpdateWidgetsPayload = {};
const metaWidgetsToUpdate: UpdateWidgetsPayload = {};
for (const widgetId in widgetsToUpdate) {
const widget = widgets[widgetId];
if (widget.isMetaWidget) {
metaWidgetsToUpdate[widgetId] = widgetsToUpdate[widgetId];
} else {
canvasWidgetsToUpdate[widgetId] = widgetsToUpdate[widgetId];
}
}
// 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(canvasWidgetsToUpdate)) {
yield put(updateMultipleWidgetPropertiesAction(canvasWidgetsToUpdate));
}
if (!isEmpty(metaWidgetsToUpdate)) {
yield put(updateMultipleMetaWidgetPropertiesAction(metaWidgetsToUpdate));
}
// clear the batch after processing
autoLayoutWidgetDimensionUpdateBatch = {};
}
export function* updateApplicationLayoutType(
layoutSystemType: LayoutSystemTypes,
) {
const applicationId: string = yield select(getCurrentApplicationId);
yield put(
updateApplication(applicationId || "", {
applicationDetail: {
appPositioning: {
type: layoutSystemType,
},
},
}),
);
}
function* updatePositionsOnTabChangeSaga(
action: ReduxAction<{ selectedTabWidgetId: string; widgetId: string }>,
) {
const { selectedTabWidgetId, widgetId } = action.payload;
const allWidgets: CanvasWidgetsReduxState = yield select(getWidgets);
if (!selectedTabWidgetId || !allWidgets[selectedTabWidgetId]) return;
const isMobile: boolean = yield select(getIsAutoLayoutMobileBreakPoint);
const mainCanvasWidth: number = yield select(getMainCanvasWidth);
const metaProps: Record<string, any> = yield select(getWidgetsMeta);
const updatedWidgets: CanvasWidgetsReduxState = updateWidgetPositions(
allWidgets,
selectedTabWidgetId,
isMobile,
mainCanvasWidth,
false,
{
...metaProps,
[widgetId]: { ...metaProps[widgetId], selectedTabWidgetId },
},
);
yield put(updateAndSaveLayout(updatedWidgets));
}
// TODO: BATCH_UPDATE_MULTIPLE_WIDGETS_PROPERTY is already updating the height of tabs widget and the canvas. Why?
function* updatePositionsSaga(action: ReduxAction<{ widgetId: string }>) {
const { widgetId } = action.payload;
const allWidgets: CanvasWidgetsReduxState = yield select(getWidgets);
if (!widgetId || !allWidgets[widgetId]) return;
const isMobile: boolean = yield select(getIsAutoLayoutMobileBreakPoint);
const mainCanvasWidth: number = yield select(getMainCanvasWidth);
const metaProps: Record<string, any> = yield select(getWidgetsMeta);
let canvasId: string = widgetId;
if (allWidgets[canvasId].type === "TABS_WIDGET") {
// For tabs widget, recalculate the height of child canvas.
if (
metaProps &&
metaProps[canvasId] &&
metaProps[canvasId]?.selectedTabWidgetId
)
canvasId = metaProps[canvasId]?.selectedTabWidgetId;
}
const updatedWidgets: CanvasWidgetsReduxState = updateWidgetPositions(
allWidgets,
canvasId,
isMobile,
mainCanvasWidth,
false,
metaProps,
);
yield put(updateAndSaveLayout(updatedWidgets));
}
export default function* layoutUpdateSagas() {
yield all([
takeLatest(
ReduxActionTypes.RECALCULATE_COLUMNS,
preventForAnvil,
updateLayoutForMobileCheckpoint,
),
takeLatest(
ReduxActionTypes.UPDATE_LAYOUT_SYSTEM_TYPE,
preventForAnvil,
updateLayoutSystemTypeSaga,
),
takeLatest(
ReduxActionTypes.UPDATE_WIDGET_DIMENSIONS,
preventForAnvil,
updateWidgetDimensionsSaga,
),
debounce(
50,
ReduxActionTypes.PROCESS_AUTO_LAYOUT_DIMENSION_UPDATES,
preventForAnvil,
processAutoLayoutDimensionUpdatesSaga,
),
takeLatest(
ReduxActionTypes.UPDATE_POSITIONS_ON_TAB_CHANGE,
shouldRunSaga,
updatePositionsOnTabChangeSaga,
),
takeLatest(
ReduxActionTypes.CHECK_CONTAINERS_FOR_AUTO_HEIGHT,
shouldRunSaga,
updatePositionsSaga,
),
]);
}