PromucFlow_constructor/app/client/src/sagas/WidgetOperationSagas.tsx
balajisoundar 5d44d4f2cf
chore: misc updates to custom widget (#30114)
#### PR fixes following issue(s)
Fixes https://github.com/appsmithorg/appsmith/issues/29991
Fixes https://github.com/appsmithorg/appsmith/issues/30154
Fixes https://github.com/appsmithorg/appsmith/issues/30020
Fixes https://github.com/appsmithorg/appsmith/issues/30019
Fixes https://github.com/appsmithorg/appsmith/issues/30130
Fixes https://github.com/appsmithorg/appsmith/issues/30159
Fixes https://github.com/appsmithorg/appsmith/issues/30223

#### Media
> A video or a GIF is preferred. when using Loom, don’t embed because it
looks like it’s a GIF. instead, just link to the video
>
>
#### Type of change
> Please delete options that are not relevant.
- Bug fix (non-breaking change which fixes an issue)
- New feature (non-breaking change which adds functionality)
- Breaking change (fix or feature that would cause existing
functionality to not work as expected)
- Chore (housekeeping or task changes that don't impact user perception)
- This change requires a documentation update
>
>
>
## Testing
>
#### How Has This Been Tested?
> Please describe the tests that you ran to verify your changes. Also
list any relevant details for your test configuration.
> Delete anything that is not relevant
- [ ] Manual
- [ ] JUnit
- [ ] Jest
- [ ] Cypress
>
>
#### 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
- [ ] 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
- [ ] 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:
- [ ] [Speedbreak
features](https://github.com/appsmithorg/TestSmith/wiki/Guidelines-for-test-plans#speedbreakers-)
have been covered
- [ ] Test plan covers all impacted features and [areas of
interest](https://github.com/appsmithorg/TestSmith/wiki/Guidelines-for-test-plans#areas-of-interest-)
- [ ] Test plan has been peer reviewed by project stakeholders and other
QA members
- [ ] Manually tested functionality on DP
- [ ] We had an implementation alignment call with stakeholders post QA
Round 2
- [ ] Cypress test cases have been added and approved by SDET/manual QA
- [ ] Added `Test Plan Approved` label after Cypress tests were reviewed
- [ ] Added `Test Plan Approved` label after JUnit tests were reviewed


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

## Summary by CodeRabbit

- **New Features**
- Custom widgets now support analytics events, enhancing visibility into
user interactions.
- Template selection, layout controls, and reference triggers in the
Custom Widget Builder are now integrated with analytics.
- Added new style options for custom widgets, including `primaryColor`,
`backgroundColor`, `borderRadius`, and `boxShadow`.

- **Bug Fixes**
  - Corrected a typo in the constant title for better clarity.
- Updated help text for the Container Widget to accurately describe the
widget's border edge.

- **Enhancements**
- Improved user interface with additional styling for reference names in
the Custom Widget Builder.
- Enhanced debugger functionality with `useCallback` optimization and
new analytics logging.

- **Refactor**
- Streamlined the property pane by introducing a new `LabelContainer`
styled component.
- Refined the handling of dynamic binding paths to ignore certain
properties efficiently.

- **Documentation**
- Added a new constant for the default model documentation URL in the
Custom Widget Builder.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2024-01-16 10:52:17 +05:30

2233 lines
71 KiB
TypeScript

import type {
ReduxAction,
ReduxActionType,
} from "@appsmith/constants/ReduxActionConstants";
import {
ReduxActionErrorTypes,
ReduxActionTypes,
WidgetReduxActionTypes,
} from "@appsmith/constants/ReduxActionConstants";
import { resetWidgetMetaProperty } from "actions/metaActions";
import { selectWidgetInitAction } from "actions/widgetSelectionActions";
import {
GridDefaults,
MAIN_CONTAINER_WIDGET_ID,
RenderModes,
WIDGET_ID_SHOW_WALKTHROUGH,
} from "constants/WidgetConstants";
import log from "loglevel";
import type { WidgetResize } from "actions/pageActions";
import { updateAndSaveLayout } from "actions/pageActions";
import type {
CanvasWidgetsReduxState,
FlattenedWidgetProps,
} from "reducers/entityReducers/canvasWidgetsReducer";
import {
actionChannel,
all,
call,
fork,
put,
select,
take,
takeEvery,
takeLatest,
takeLeading,
} from "redux-saga/effects";
import {
getCanvasWidth,
getContainerWidgetSpacesSelector,
getCurrentPageId,
getIsAutoLayout,
getIsAutoLayoutMobileBreakPoint,
} from "selectors/editorSelectors";
import AnalyticsUtil from "utils/AnalyticsUtil";
import { convertToString } from "utils/AppsmithUtils";
import type {
BatchUpdateDynamicPropertyUpdates,
BatchUpdateWidgetDynamicPropertyPayload,
DeleteWidgetPropertyPayload,
SetWidgetDynamicPropertyPayload,
UpdateWidgetPropertyPayload,
UpdateWidgetPropertyRequestPayload,
} from "actions/controlActions";
import {
batchUpdateWidgetProperty,
updateMultipleWidgetPropertiesAction,
} from "actions/controlActions";
import type { DynamicPath } from "utils/DynamicBindingUtils";
import {
getEntityDynamicBindingPathList,
getWidgetDynamicPropertyPathList,
getWidgetDynamicTriggerPathList,
isChildPropertyPath,
isDynamicValue,
isPathADynamicBinding,
isPathDynamicTrigger,
} from "utils/DynamicBindingUtils";
import type { WidgetProps } from "widgets/BaseWidget";
import _, { cloneDeep, get, isString, set, uniq } from "lodash";
import WidgetFactory from "WidgetProvider/factory";
import { generateReactKey } from "utils/generators";
import { getCopiedWidgets, saveCopiedWidgets } from "utils/storage";
import { getWidget, getWidgets, getWidgetsMeta } from "./selectors";
import {
ERROR_WIDGET_COPY_NOT_ALLOWED,
ERROR_WIDGET_COPY_NO_WIDGET_SELECTED,
ERROR_WIDGET_CUT_NOT_ALLOWED,
ERROR_WIDGET_CUT_NO_WIDGET_SELECTED,
WIDGET_COPY,
WIDGET_CUT,
createMessage,
} from "@appsmith/constants/messages";
import { getAllPaths } from "@appsmith/workers/Evaluation/evaluationUtils";
import { getDataTree, getConfigTree } from "selectors/dataTreeSelectors";
import { validateProperty } from "./EvaluationsSaga";
import type { ColumnProperties } from "widgets/TableWidget/component/Constants";
import {
getAllPathsFromPropertyConfig,
nextAvailableRowInContainer,
} from "entities/Widget/utils";
import { getSelectedWidgets } from "selectors/ui";
import { getReflow } from "selectors/widgetReflowSelectors";
import {
addChildToPastedFlexLayers,
getFlexLayersForSelectedWidgets,
getLayerIndexOfWidget,
getNewFlexLayers,
isStack,
pasteWidgetInFlexLayers,
} from "../layoutSystems/autolayout/utils/AutoLayoutUtils";
import type {
CopiedWidgetGroup,
NewPastePositionVariables,
} from "./WidgetOperationUtils";
import { WIDGET_PASTE_PADDING } from "./WidgetOperationUtils";
import {
changeIdsOfPastePositions,
createSelectedWidgetsAsCopiedWidgets,
createWidgetCopy,
doesTriggerPathsContainPropertyPath,
filterOutSelectedWidgets,
getBoundariesFromSelectedWidgets,
getBoundaryWidgetsFromCopiedGroups,
getCanvasIdForContainer,
getContainerIdForCanvas,
getDefaultCanvas,
getMousePositions,
getNewPositionsForCopiedWidgets,
getNextWidgetName,
getOccupiedSpacesFromProps,
getParentWidgetIdForGrouping,
getParentWidgetIdForPasting,
getPastePositionMapFromMousePointer,
getReflowedPositions,
getSelectedWidgetWhenPasting,
getSnappedGrid,
getValueFromTree,
getVerifiedSelectedWidgets,
getVerticallyAdjustedPositions,
getWidgetDescendantToReset,
groupWidgetsIntoContainer,
handleSpecificCasesWhilePasting,
isDropTarget,
isSelectedWidgetsColliding,
mergeDynamicPropertyPaths,
purgeOrphanedDynamicPaths,
} from "./WidgetOperationUtils";
import {
partialImportSaga,
partialExportSaga,
widgetSelectionSagas,
} from "./WidgetSelectionSagas";
import type { WidgetEntityConfig } from "@appsmith/entities/DataTree/types";
import type { DataTree, ConfigTree } from "entities/DataTree/dataTreeTypes";
import { getCanvasSizeAfterWidgetMove } from "./CanvasSagas/DraggingCanvasSagas";
import widgetAdditionSagas from "./WidgetAdditionSagas";
import widgetDeletionSagas from "./WidgetDeletionSagas";
import type { widgetReflow } from "reducers/uiReducers/reflowReducer";
import { stopReflowAction } from "actions/reflowActions";
import {
collisionCheckPostReflow,
getBottomRowAfterReflow,
} from "utils/reflowHookUtils";
import type { GridProps, PrevReflowState, SpaceMap } from "reflow/reflowTypes";
import { ReflowDirection } from "reflow/reflowTypes";
import type { WidgetSpace } from "constants/CanvasEditorConstants";
import { reflow } from "reflow";
import { getBottomMostRow } from "reflow/reflowUtils";
import { flashElementsById } from "utils/helpers";
import { getSlidingArenaName } from "constants/componentClassNameConstants";
import { builderURL } from "@appsmith/RouteBuilder";
import history from "utils/history";
import { generateAutoHeightLayoutTreeAction } from "actions/autoHeightActions";
import {
executeWidgetBlueprintBeforeOperations,
traverseTreeAndExecuteBlueprintChildOperations,
} from "./WidgetBlueprintSagas";
import type { MetaState } from "reducers/entityReducers/metaReducer";
import { SelectionRequestType } from "sagas/WidgetSelectUtils";
import { BlueprintOperationTypes } from "WidgetProvider/constants";
import { toast } from "design-system";
import { LayoutSystemTypes } from "layoutSystems/types";
import {
updatePositionsOfParentAndSiblings,
updateWidgetPositions,
} from "layoutSystems/autolayout/utils/positionUtils";
import { getWidgetWidth } from "layoutSystems/autolayout/utils/flexWidgetUtils";
import {
FlexLayerAlignment,
LayoutDirection,
} from "layoutSystems/common/utils/constants";
import localStorage from "utils/localStorage";
import type { FlexLayer } from "layoutSystems/autolayout/utils/types";
import { EMPTY_BINDING } from "components/editorComponents/ActionCreator/constants";
import { getLayoutSystemType } from "selectors/layoutSystemSelectors";
import { addSuggestedWidgetAnvilAction } from "layoutSystems/anvil/integrations/actions/draggingActions";
import { updateAndSaveAnvilLayout } from "layoutSystems/anvil/utils/anvilChecksUtils";
export function* resizeSaga(resizeAction: ReduxAction<WidgetResize>) {
try {
toast.dismiss();
const start = performance.now();
const stateWidgets: CanvasWidgetsReduxState = yield select(getWidgets);
const stateWidget: FlattenedWidgetProps = yield select(
getWidget,
resizeAction.payload.widgetId,
);
const isMobile: boolean = yield select(getIsAutoLayoutMobileBreakPoint);
const widgets = { ...stateWidgets };
let widget = { ...stateWidget };
const {
bottomRow = widget.bottomRow,
leftColumn = widget.leftColumn,
mobileBottomRow = widget.mobileBottomRow,
mobileLeftColumn = widget.mobileLeftColumn,
mobileRightColumn = widget.mobileRightColumn,
mobileTopRow = widget.mobileTopRow,
parentId,
rightColumn = widget.rightColumn,
snapColumnSpace,
snapRowSpace,
topRow = widget.topRow,
widgetId,
} = resizeAction.payload;
const layoutSystemType: LayoutSystemTypes =
yield select(getLayoutSystemType);
const mainCanvasWidth: number = yield select(getCanvasWidth);
widget = {
...widget,
leftColumn,
rightColumn,
topRow,
bottomRow,
mobileLeftColumn,
mobileRightColumn,
mobileTopRow,
mobileBottomRow,
};
if (layoutSystemType === LayoutSystemTypes.AUTO) {
// Keeps track of user defined widget width in terms of percentage
if (isMobile) {
widget.mobileWidthInPercentage =
(getWidgetWidth(widget, true) * snapColumnSpace) / mainCanvasWidth;
} else {
widget.widthInPercentage =
(getWidgetWidth(widget, false) * snapColumnSpace) / mainCanvasWidth;
}
}
const movedWidgets: {
[widgetId: string]: FlattenedWidgetProps;
} = yield call(
reflowWidgets,
widgets,
widget,
snapColumnSpace,
snapRowSpace,
);
const updatedCanvasBottomRow: number = yield call(
getCanvasSizeAfterWidgetMove,
parentId,
[widgetId],
bottomRow,
);
// If it is a fixed canvas, update bottomRow directly.
if (
updatedCanvasBottomRow &&
layoutSystemType === LayoutSystemTypes.FIXED
) {
const canvasWidget = movedWidgets[parentId];
movedWidgets[parentId] = {
...canvasWidget,
bottomRow: updatedCanvasBottomRow,
};
}
// If it is an auto-layout canvas, then use positionUtils to update canvas bottomRow.
let updatedWidgetsAfterResizing = movedWidgets;
if (layoutSystemType === LayoutSystemTypes.AUTO) {
const metaProps: Record<string, any> = yield select(getWidgetsMeta);
updatedWidgetsAfterResizing = updatePositionsOfParentAndSiblings(
movedWidgets,
parentId,
getLayerIndexOfWidget(widgets[parentId]?.flexLayers, widgetId),
isMobile,
mainCanvasWidth,
false,
metaProps,
);
}
log.debug("resize computations took", performance.now() - start, "ms");
yield put(stopReflowAction());
yield put(updateAndSaveLayout(updatedWidgetsAfterResizing));
// Widget resize based auto-height is only required for fixed-layout
// Auto-layout has UPDATE_WIDGET_DIMENSIONS to handle auto height
if (layoutSystemType === LayoutSystemTypes.FIXED) {
yield put(generateAutoHeightLayoutTreeAction(true, true));
}
} catch (error) {
yield put({
type: ReduxActionErrorTypes.WIDGET_OPERATION_ERROR,
payload: {
action: WidgetReduxActionTypes.WIDGET_RESIZE,
error,
},
});
}
}
export function* reflowWidgets(
widgets: {
[widgetId: string]: FlattenedWidgetProps;
},
widget: FlattenedWidgetProps,
snapColumnSpace: number,
snapRowSpace: number,
) {
const reflowState: widgetReflow = yield select(getReflow);
const currentWidgets: {
[widgetId: string]: FlattenedWidgetProps;
} = { ...widgets, [widget.widgetId]: { ...widget } };
if (!reflowState || !reflowState.isReflowing || !reflowState.reflowingWidgets)
return currentWidgets;
const reflowingWidgets = reflowState.reflowingWidgets;
const reflowWidgetKeys = Object.keys(reflowingWidgets || {});
if (reflowWidgetKeys.length <= 0) return widgets;
for (const reflowedWidgetId of reflowWidgetKeys) {
const reflowWidget = reflowingWidgets[reflowedWidgetId];
const canvasWidget = { ...currentWidgets[reflowedWidgetId] };
if (reflowWidget.X !== undefined && reflowWidget.width !== undefined) {
const leftColumn =
canvasWidget.leftColumn + reflowWidget.X / snapColumnSpace;
const rightColumn = leftColumn + reflowWidget.width / snapColumnSpace;
currentWidgets[reflowedWidgetId] = {
...canvasWidget,
leftColumn,
rightColumn,
};
} else if (
reflowWidget.Y !== undefined &&
reflowWidget.height !== undefined
) {
const topRow = canvasWidget.topRow + reflowWidget.Y / snapRowSpace;
const bottomRow = topRow + reflowWidget.height / snapRowSpace;
currentWidgets[reflowedWidgetId] = { ...canvasWidget, topRow, bottomRow };
}
}
if (
collisionCheckPostReflow(currentWidgets, reflowWidgetKeys, widget.parentId)
) {
return currentWidgets;
}
return widgets;
}
enum DynamicPathUpdateEffectEnum {
ADD = "ADD",
REMOVE = "REMOVE",
NOOP = "NOOP",
}
interface DynamicPathUpdate {
propertyPath: string;
effect: DynamicPathUpdateEffectEnum;
}
function getDynamicTriggerPathListUpdate(
widget: WidgetProps,
propertyPath: string,
propertyValue: string,
): DynamicPathUpdate {
if (propertyValue && !isPathDynamicTrigger(widget, propertyPath)) {
return {
propertyPath,
effect: DynamicPathUpdateEffectEnum.ADD,
};
} else if (!propertyValue && !isPathDynamicTrigger(widget, propertyPath)) {
return {
propertyPath,
effect: DynamicPathUpdateEffectEnum.REMOVE,
};
}
return {
propertyPath,
effect: DynamicPathUpdateEffectEnum.NOOP,
};
}
const DYNAMIC_BINDING_IGNORED_LIST = [
/* Table widget */
"primaryColumns",
"derivedColumns",
/* custom widget */
"srcDoc.html",
"srcDoc.css",
"srcDoc.js",
"uncompiledSrcDoc.html",
"uncompiledSrcDoc.css",
"uncompiledSrcDoc.js",
];
function getDynamicBindingPathListUpdate(
widget: WidgetProps,
propertyPath: string,
propertyValue: any,
): DynamicPathUpdate {
let stringProp = propertyValue;
if (_.isObject(propertyValue)) {
// Stringify this because composite controls may have bindings in the sub controls
stringProp = JSON.stringify(propertyValue);
}
/*
* TODO(Balaji Soundararajan): This is not appropriate from the platform's archtecture's point of view.
* This setting should come from widget configuration
*/
// Figure out a holistic solutions where we donot have to stringify above.
if (DYNAMIC_BINDING_IGNORED_LIST.includes(propertyPath)) {
return {
propertyPath,
effect: DynamicPathUpdateEffectEnum.NOOP,
};
}
const isDynamic = isDynamicValue(stringProp);
if (!isDynamic && isPathADynamicBinding(widget, propertyPath)) {
return {
propertyPath,
effect: DynamicPathUpdateEffectEnum.REMOVE,
};
} else if (isDynamic && !isPathADynamicBinding(widget, propertyPath)) {
return {
propertyPath,
effect: DynamicPathUpdateEffectEnum.ADD,
};
}
return {
propertyPath,
effect: DynamicPathUpdateEffectEnum.NOOP,
};
}
function applyDynamicPathUpdates(
currentList: DynamicPath[],
update: DynamicPathUpdate,
): DynamicPath[] {
if (update.effect === DynamicPathUpdateEffectEnum.ADD) {
currentList.push({
key: update.propertyPath,
});
} else if (update.effect === DynamicPathUpdateEffectEnum.REMOVE) {
currentList = _.reject(currentList, { key: update.propertyPath });
}
return currentList;
}
function* updateWidgetPropertySaga(
updateAction: ReduxAction<UpdateWidgetPropertyRequestPayload>,
) {
const {
payload: { propertyPath, propertyValue, widgetId },
} = updateAction;
// Holder object to collect all updates
const updates: Record<string, unknown> = {
[propertyPath]: propertyValue,
};
// Push these updates via the batch update
yield call(
batchUpdateWidgetPropertySaga,
batchUpdateWidgetProperty(widgetId, { modify: updates }),
);
}
export function removeDynamicBindingProperties(
propertyPath: string,
dynamicBindingPathList: DynamicPath[],
) {
/*
we are doing this because when you toggle js off we only
receive the `primaryColumns.` properties not the `derivedColumns.`
properties therefore we need just a hard-codded check.
(TODO) - Arsalan remove this primaryColumns check when the Table widget v2 is live.
*/
if (_.startsWith(propertyPath, "primaryColumns")) {
// primaryColumns.customColumn1.isVisible -> customColumn1.isVisible
const tableProperty = propertyPath.split(".").splice(1).join(".");
const tablePropertyPathsToRemove = [
propertyPath, // primaryColumns.customColumn1.isVisible
`derivedColumns.${tableProperty}`, // derivedColumns.customColumn1.isVisible
];
return _.reject(dynamicBindingPathList, ({ key }) =>
tablePropertyPathsToRemove.includes(key),
);
} else {
return _.reject(dynamicBindingPathList, {
key: propertyPath,
});
}
}
export function* handleUpdateWidgetDynamicProperty(
widget: WidgetProps,
update: BatchUpdateDynamicPropertyUpdates,
) {
const {
isDynamic,
propertyPath,
shouldRejectDynamicBindingPathList = true,
skipValidation = false,
} = update;
const propertyValue = _.get(widget, propertyPath);
let dynamicPropertyPathList = getWidgetDynamicPropertyPathList(widget);
let dynamicBindingPathList = getEntityDynamicBindingPathList(widget);
if (isDynamic) {
const keyExists =
dynamicPropertyPathList.findIndex((path) => path.key === propertyPath) >
-1;
if (!keyExists) {
dynamicPropertyPathList.push({
key: propertyPath,
});
}
widget = set(widget, propertyPath, convertToString(propertyValue));
} else {
dynamicPropertyPathList = _.reject(dynamicPropertyPathList, {
key: propertyPath,
});
if (shouldRejectDynamicBindingPathList) {
dynamicBindingPathList = removeDynamicBindingProperties(
propertyPath,
dynamicBindingPathList,
);
}
/*
* We need to run the validation function to parse the value present in the
* js mode to use that in the non js mode.
* - if the value is valid to be used on non js mode, we will use the same value
* - if the value is invalid to be used on non js mode, we will use the default value
* returned by the validation function.
*
* Sometimes (eg: in one click binding control) we don't want to do validation and retain the
* same value while switching from js to non js mode. use `skipValidation` flag to turn off validation.
*/
if (!skipValidation) {
const { parsed } = yield call(
validateProperty,
propertyPath,
propertyValue,
widget,
);
widget = set(widget, propertyPath, parsed);
}
}
widget.dynamicPropertyPathList = dynamicPropertyPathList;
widget.dynamicBindingPathList = dynamicBindingPathList;
return widget;
}
export function* batchUpdateWidgetDynamicPropertySaga(
action: ReduxAction<BatchUpdateWidgetDynamicPropertyPayload>,
) {
const { updates, widgetId } = action.payload;
const stateWidget: WidgetProps = yield select(getWidget, widgetId);
let widget = cloneDeep({ ...stateWidget });
for (const update of updates) {
widget = yield call(handleUpdateWidgetDynamicProperty, widget, update);
}
const stateWidgets: CanvasWidgetsReduxState = yield select(getWidgets);
const widgets = { ...stateWidgets, [widgetId]: widget };
// Save the layout
yield put(updateAndSaveLayout(widgets));
}
export function* setWidgetDynamicPropertySaga(
action: ReduxAction<SetWidgetDynamicPropertyPayload>,
) {
const {
isDynamic,
propertyPath,
shouldRejectDynamicBindingPathList = true,
skipValidation = false,
widgetId,
} = action.payload;
const stateWidget: WidgetProps = yield select(getWidget, widgetId);
let widget = cloneDeep({ ...stateWidget });
const update = {
isDynamic,
propertyPath,
shouldRejectDynamicBindingPathList,
skipValidation,
};
widget = yield call(handleUpdateWidgetDynamicProperty, widget, update);
const propertyValue = get(widget, propertyPath);
if (!propertyValue && isDynamic) {
set(widget, propertyPath, EMPTY_BINDING);
}
const stateWidgets: CanvasWidgetsReduxState = yield select(getWidgets);
const widgets = { ...stateWidgets, [widgetId]: widget };
// Save the layout
yield put(updateAndSaveLayout(widgets));
}
export function getPropertiesToUpdate(
widget: WidgetProps,
updates: Record<string, unknown>,
triggerPaths?: string[],
): {
propertyUpdates: Record<string, unknown>;
dynamicTriggerPathList: DynamicPath[];
dynamicBindingPathList: DynamicPath[];
} {
// Create a
const widgetWithUpdates = _.cloneDeep(widget);
Object.entries(updates).forEach(([propertyPath, propertyValue]) => {
set(widgetWithUpdates, propertyPath, propertyValue);
});
// get the flat list of all updates (in case values are objects)
const updatePaths = getAllPaths(updates);
const propertyUpdates: Record<string, unknown> = {
...updates,
};
const currentDynamicTriggerPathList: DynamicPath[] =
getWidgetDynamicTriggerPathList(widget);
const currentDynamicBindingPathList: DynamicPath[] =
getEntityDynamicBindingPathList(widget);
const dynamicTriggerPathListUpdates: DynamicPathUpdate[] = [];
const dynamicBindingPathListUpdates: DynamicPathUpdate[] = [];
const widgetConfig = WidgetFactory.getWidgetPropertyPaneConfig(
widget.type,
widget,
);
const { triggerPaths: triggerPathsFromPropertyConfig = {} } =
getAllPathsFromPropertyConfig(widgetWithUpdates, widgetConfig, {});
Object.keys(updatePaths).forEach((propertyPath) => {
const propertyValue = getValueFromTree(updates, propertyPath);
// only check if
if (!_.isString(propertyValue)) {
return;
}
let isTriggerProperty = propertyPath in triggerPathsFromPropertyConfig;
isTriggerProperty = doesTriggerPathsContainPropertyPath(
isTriggerProperty,
propertyPath,
triggerPaths,
);
// If it is a trigger property, it will go in a different list than the general
// dynamicBindingPathList.
if (isTriggerProperty) {
dynamicTriggerPathListUpdates.push(
getDynamicTriggerPathListUpdate(widget, propertyPath, propertyValue),
);
} else {
dynamicBindingPathListUpdates.push(
getDynamicBindingPathListUpdate(widget, propertyPath, propertyValue),
);
}
});
const dynamicTriggerPathList = dynamicTriggerPathListUpdates.reduce(
applyDynamicPathUpdates,
currentDynamicTriggerPathList,
);
const dynamicBindingPathList = dynamicBindingPathListUpdates.reduce(
applyDynamicPathUpdates,
currentDynamicBindingPathList,
);
return {
propertyUpdates,
dynamicTriggerPathList,
dynamicBindingPathList,
};
}
export function* getIsContainerLikeWidget(widget: FlattenedWidgetProps) {
const children = widget.children;
if (Array.isArray(children) && children.length > 0) {
const firstChild: FlattenedWidgetProps = yield select(
getWidget,
children[0],
);
if (firstChild.type === "CANVAS_WIDGET") return true;
}
return false;
}
export function* getPropertiesUpdatedWidget(
updatesObj: UpdateWidgetPropertyPayload,
) {
const { dynamicUpdates, updates, widgetId } = updatesObj;
const { modify = {}, postUpdateAction, remove = [], triggerPaths } = updates;
const stateWidget: WidgetProps = yield select(getWidget, widgetId);
// if there is no widget in the state, don't do anything
if (!stateWidget) return;
let widget = cloneDeep(stateWidget);
try {
if (Object.keys(modify).length > 0) {
const {
dynamicBindingPathList,
dynamicTriggerPathList,
propertyUpdates,
} = getPropertiesToUpdate(widget, modify, triggerPaths);
// We loop over all updates
Object.entries(propertyUpdates).forEach(
([propertyPath, propertyValue]) => {
// since property paths could be nested, we use lodash set method
widget = set(widget, propertyPath, propertyValue);
},
);
widget.dynamicBindingPathList = dynamicBindingPathList;
widget.dynamicTriggerPathList = dynamicTriggerPathList;
if (dynamicUpdates?.dynamicPropertyPathList?.length) {
widget.dynamicPropertyPathList = mergeDynamicPropertyPaths(
widget.dynamicPropertyPathList,
dynamicUpdates.dynamicPropertyPathList,
);
}
}
} catch (e) {
log.debug("Error updating property paths: ", { e });
}
if (Array.isArray(remove) && remove.length > 0) {
widget = yield removeWidgetProperties(widget, remove);
}
// Note: This may not be the best place to do this.
// If there exists another spot in this workflow, where we are iterating over the dynamicTriggerPathList and dynamicBindingPathList, after
// performing all updates to the widget, we can piggy back on that iteration to purge orphaned paths
// I couldn't find it, so here it is.
return {
updatedWidget: purgeOrphanedDynamicPaths(widget),
actionToDispatch: postUpdateAction,
};
}
function* batchUpdateWidgetPropertySaga(
action: ReduxAction<UpdateWidgetPropertyPayload>,
) {
const start = performance.now();
const { shouldReplay, widgetId } = action.payload;
if (!widgetId) {
// Handling the case where sometimes widget id is not passed through here
return;
}
const updatedWidgetAndActionsToDispatch: {
updatedWidget: WidgetProps;
actionToDispatch?: ReduxActionType;
} = yield call(getPropertiesUpdatedWidget, action.payload);
const stateWidgets: CanvasWidgetsReduxState = yield select(getWidgets);
const widgets = {
...stateWidgets,
[widgetId]: updatedWidgetAndActionsToDispatch.updatedWidget,
};
log.debug(
"Batch widget property update calculations took: ",
action,
performance.now() - start,
"ms",
);
// Save the layout
yield put(updateAndSaveLayout(widgets, { shouldReplay }));
if (updatedWidgetAndActionsToDispatch.actionToDispatch) {
yield put({
type: updatedWidgetAndActionsToDispatch.actionToDispatch,
payload: { widgetId },
});
}
}
function* batchUpdateMultipleWidgetsPropertiesSaga(
action: ReduxAction<{ updatesArray: UpdateWidgetPropertyPayload[] }>,
) {
const start = performance.now();
const { updatesArray } = action.payload;
const stateWidgets: CanvasWidgetsReduxState = yield select(getWidgets);
const updatedWidgetsAndActionsToDispatch: Array<{
updatedWidget: WidgetProps;
actionToDispatch?: ReduxActionType;
}> = yield all(
updatesArray.map((eachUpdate) => {
return call(getPropertiesUpdatedWidget, eachUpdate);
}),
);
const updatedStateWidgets = updatedWidgetsAndActionsToDispatch.reduce(
(allWidgets, eachUpdatedWidgetAndActionsToDispatch) => {
return {
...allWidgets,
[eachUpdatedWidgetAndActionsToDispatch.updatedWidget.widgetId]:
eachUpdatedWidgetAndActionsToDispatch.updatedWidget,
};
},
stateWidgets,
);
const updatedWidgetIds = uniq(
updatedWidgetsAndActionsToDispatch.map(
(each) => each.updatedWidget.widgetId,
),
);
log.debug(
"Batch multi-widget properties update calculations took: ",
performance.now() - start,
"ms",
);
// Save the layout
yield put(
updateAndSaveLayout(updatedStateWidgets, {
updatedWidgetIds,
}),
);
for (const updatedWidgetAndActions of updatedWidgetsAndActionsToDispatch) {
if (updatedWidgetAndActions.actionToDispatch) {
yield put({
type: updatedWidgetAndActions.actionToDispatch,
payload: { widgetId: updatedWidgetAndActions.updatedWidget.widgetId },
});
}
}
}
function* removeWidgetProperties(widget: WidgetProps, paths: string[]) {
try {
let dynamicTriggerPathList: DynamicPath[] =
getWidgetDynamicTriggerPathList(widget);
let dynamicBindingPathList: DynamicPath[] =
getEntityDynamicBindingPathList(widget);
let dynamicPropertyPathList: DynamicPath[] =
getWidgetDynamicPropertyPathList(widget);
paths.forEach((propertyPath) => {
dynamicTriggerPathList = dynamicTriggerPathList.filter((dynamicPath) => {
return !isChildPropertyPath(propertyPath, dynamicPath.key);
});
dynamicBindingPathList = dynamicBindingPathList.filter((dynamicPath) => {
return !isChildPropertyPath(propertyPath, dynamicPath.key);
});
dynamicPropertyPathList = dynamicPropertyPathList.filter(
(dynamicPath) => {
return !isChildPropertyPath(propertyPath, dynamicPath.key);
},
);
});
widget.dynamicBindingPathList = dynamicBindingPathList;
widget.dynamicTriggerPathList = dynamicTriggerPathList;
widget.dynamicPropertyPathList = dynamicPropertyPathList;
paths.forEach((propertyPath) => {
widget = unsetPropertyPath(widget, propertyPath) as WidgetProps;
});
} catch (e) {
log.debug("Error removing propertyPaths: ", { e });
}
return widget;
}
function* deleteWidgetPropertySaga(
action: ReduxAction<DeleteWidgetPropertyPayload>,
) {
const { propertyPaths, widgetId } = action.payload;
if (!widgetId) {
// Handling the case where sometimes widget id is not passed through here
return;
}
yield put(batchUpdateWidgetProperty(widgetId, { remove: propertyPaths }));
}
//TODO(abhinav): Move this to helpers and add tests
const unsetPropertyPath = (obj: Record<string, unknown>, path: string) => {
const regex = /(.*)\[\d+\]$/;
if (regex.test(path)) {
const matches = path.match(regex);
if (
matches &&
Array.isArray(matches) &&
matches[1] &&
matches[1].length > 0
) {
_.unset(obj, path);
const arr = _.get(obj, matches[1]);
if (arr && Array.isArray(arr)) {
_.set(obj, matches[1], arr.filter(Boolean));
}
}
} else {
_.unset(obj, path);
}
return obj;
};
function* resetChildrenMetaSaga(action: ReduxAction<{ widgetId: string }>) {
const { widgetId: parentWidgetId } = action.payload;
const canvasWidgets: CanvasWidgetsReduxState = yield select(getWidgets);
const evaluatedDataTree: DataTree = yield select(getDataTree);
const configTree: ConfigTree = yield select(getConfigTree);
const widgetsMeta: MetaState = yield select(getWidgetsMeta);
const childrenList = getWidgetDescendantToReset(
canvasWidgets,
parentWidgetId,
evaluatedDataTree,
widgetsMeta,
);
for (const childIndex in childrenList) {
const { evaluatedWidget: childWidget, id: childId } =
childrenList[childIndex];
const evaluatedWidgetConfig =
childWidget && configTree[childWidget?.widgetName];
yield put(
resetWidgetMetaProperty(
childId,
childWidget,
evaluatedWidgetConfig as WidgetEntityConfig,
),
);
}
}
function* updateCanvasSize(
action: ReduxAction<{ canvasWidgetId: string; snapRows: number }>,
) {
const { canvasWidgetId, snapRows } = action.payload;
const canvasWidget: FlattenedWidgetProps = yield select(
getWidget,
canvasWidgetId,
);
const originalSnapRows = canvasWidget.bottomRow - canvasWidget.topRow;
const newBottomRow = Math.round(
snapRows * GridDefaults.DEFAULT_GRID_ROW_HEIGHT,
);
/* Update the canvas's rows, ONLY if it has changed since the last render */
if (originalSnapRows !== newBottomRow) {
// TODO(abhinav): This considers that the topRow will always be zero
// Check this out when non canvas widgets are updating snapRows
// erstwhile: Math.round((rows * props.snapRowSpace) / props.parentRowSpace),
yield put(
updateMultipleWidgetPropertiesAction({
[canvasWidgetId]: [
{
propertyPath: "bottomRow",
propertyValue: newBottomRow,
},
],
}),
);
}
}
function* createSelectedWidgetsCopy(
selectedWidgets: FlattenedWidgetProps[],
flexLayers: FlexLayer[],
) {
if (!selectedWidgets || !selectedWidgets.length) return;
const widgetListsToStore: {
widgetId: string;
parentId: string;
list: FlattenedWidgetProps[];
}[] = yield all(selectedWidgets.map((each) => call(createWidgetCopy, each)));
const saveResult: boolean = yield saveCopiedWidgets(
JSON.stringify({
widgets: widgetListsToStore,
flexLayers,
}),
);
return saveResult;
}
/**
* copy here actually means saving a JSON in local storage
* so when a user hits copy on a selected widget, we save widget in localStorage
*
* @param action
* @returns
*/
function* copyWidgetSaga(action: ReduxAction<{ isShortcut: boolean }>) {
const allWidgets: { [widgetId: string]: FlattenedWidgetProps } =
yield select(getWidgets);
const selectedWidgets: string[] = yield select(getSelectedWidgets);
if (!selectedWidgets) {
toast.show(createMessage(ERROR_WIDGET_COPY_NO_WIDGET_SELECTED), {
kind: "info",
});
return;
}
const allAllowedToCopy = selectedWidgets.some((each) => {
//should not allow canvas widgets to be copied
return (
allWidgets[each] &&
!allWidgets[each].disallowCopy &&
allWidgets[each].type !== "CANVAS_WIDGET"
);
});
if (!allAllowedToCopy) {
toast.show(createMessage(ERROR_WIDGET_COPY_NOT_ALLOWED), {
kind: "info",
});
return;
}
const selectedWidgetProps = selectedWidgets.map((each) => allWidgets[each]);
const canvasId = selectedWidgetProps?.[0]?.parentId || "";
const flexLayers: FlexLayer[] = getFlexLayersForSelectedWidgets(
selectedWidgets,
canvasId ? allWidgets[canvasId] : undefined,
);
const saveResult: boolean = yield createSelectedWidgetsCopy(
selectedWidgetProps,
flexLayers,
);
selectedWidgetProps.forEach((each) => {
const eventName = action.payload.isShortcut
? "WIDGET_COPY_VIA_SHORTCUT"
: "WIDGET_COPY";
AnalyticsUtil.logEvent(eventName, {
widgetName: each.widgetName,
widgetType: each.type,
});
});
if (saveResult) {
toast.show(
createMessage(
WIDGET_COPY,
selectedWidgetProps.length > 1
? `${selectedWidgetProps.length} Widgets`
: selectedWidgetProps[0].widgetName,
),
{
kind: "success",
},
);
}
}
/**
* We take the bottom most widget in the canvas, then calculate the top,left,right,bottom
* co-ordinates for the new widget, such that it can be placed at the bottom of the canvas.
*
* @param widget
* @param parentId
* @param canvasWidgets
* @param parentBottomRow
* @param newPastingPositionMap
* @param shouldPersistColumnPosition
* @param isThereACollision
* @param shouldGroup
* @returns
*/
export function calculateNewWidgetPosition(
widget: WidgetProps,
parentId: string,
canvasWidgets: { [widgetId: string]: FlattenedWidgetProps },
parentBottomRow?: number,
newPastingPositionMap?: SpaceMap,
shouldPersistColumnPosition = false,
isThereACollision = false,
shouldGroup = false,
): {
topRow: number;
bottomRow: number;
leftColumn: number;
rightColumn: number;
} {
if (
!shouldGroup &&
newPastingPositionMap &&
newPastingPositionMap[widget.widgetId]
) {
const newPastingPosition = newPastingPositionMap[widget.widgetId];
return {
topRow: newPastingPosition.top,
bottomRow: newPastingPosition.bottom,
leftColumn: newPastingPosition.left,
rightColumn: newPastingPosition.right,
};
}
const nextAvailableRow = parentBottomRow
? parentBottomRow
: nextAvailableRowInContainer(parentId, canvasWidgets);
return {
leftColumn: shouldPersistColumnPosition ? widget.leftColumn : 0,
rightColumn: shouldPersistColumnPosition
? widget.rightColumn
: widget.rightColumn - widget.leftColumn,
topRow:
!isThereACollision && shouldGroup
? widget.topRow
: parentBottomRow
? nextAvailableRow + widget.topRow
: nextAvailableRow,
bottomRow:
!isThereACollision && shouldGroup
? widget.bottomRow
: parentBottomRow
? nextAvailableRow + widget.bottomRow
: nextAvailableRow + (widget.bottomRow - widget.topRow),
};
}
/**
* Method to provide the new positions where the widgets can be pasted.
* It will return an empty object if it doesn't have any selected widgets, or if the mouse is outside the canvas.
*
* @param copiedWidgetGroups Contains information on the copied widgets
* @param mouseLocation location of the mouse in absolute pixels
* @param copiedTotalWidth total width of the copied widgets
* @param copiedTopMostRow top row of the top most copied widget
* @param copiedLeftMostColumn left column of the left most copied widget
* @returns
*/
const getNewPositions = function* (
copiedWidgetGroups: CopiedWidgetGroup[],
mouseLocation: { x: number; y: number },
copiedTotalWidth: number,
copiedTopMostRow: number,
copiedLeftMostColumn: number,
) {
const selectedWidgetIDs: string[] = yield select(getSelectedWidgets);
const canvasWidgets: CanvasWidgetsReduxState = yield select(getWidgets);
const { isListWidgetPastingOnItself, selectedWidgets } =
getVerifiedSelectedWidgets(
selectedWidgetIDs,
copiedWidgetGroups,
canvasWidgets,
);
//if the copied widget is a modal widget, then it has to paste on the main container
if (
copiedWidgetGroups.length === 1 &&
copiedWidgetGroups[0].list[0] &&
copiedWidgetGroups[0].list[0].type === "MODAL_WIDGET"
)
return {};
//if multiple widgets are selected or if a single non-layout widget is selected,
// then call the method to calculate and return positions based on selected widgets.
if (
!(
selectedWidgets.length === 1 &&
isDropTarget(selectedWidgets[0].type, true) &&
!isListWidgetPastingOnItself
) &&
selectedWidgets.length > 0
) {
const newPastingPositionDetails: NewPastePositionVariables = yield call(
getNewPositionsBasedOnSelectedWidgets,
copiedWidgetGroups,
selectedWidgets,
canvasWidgets,
copiedTotalWidth,
copiedTopMostRow,
copiedLeftMostColumn,
);
return newPastingPositionDetails;
}
//if a layout widget is selected or mouse is on the main canvas
// then call the method to calculate and return positions mouse positions.
const newPastingPositionDetails: NewPastePositionVariables = yield call(
getNewPositionsBasedOnMousePositions,
copiedWidgetGroups,
mouseLocation,
selectedWidgets,
canvasWidgets,
copiedTotalWidth,
copiedTopMostRow,
copiedLeftMostColumn,
);
return newPastingPositionDetails;
};
/**
* Calculates the new positions of the pasting widgets, based on the selected widgets
* The new positions will be just below the selected widgets
*
* @param copiedWidgetGroups Contains information on the copied widgets
* @param selectedWidgets array of selected widgets
* @param canvasWidgets canvas widgets from the DSL
* @param copiedTotalWidth total width of the copied widgets
* @param copiedTopMostRow top row of the top most copied widget
* @param copiedLeftMostColumn left column of the left most copied widget
* @returns
*/
function* getNewPositionsBasedOnSelectedWidgets(
copiedWidgetGroups: CopiedWidgetGroup[],
selectedWidgets: WidgetProps[],
canvasWidgets: CanvasWidgetsReduxState,
copiedTotalWidth: number,
copiedTopMostRow: number,
copiedLeftMostColumn: number,
) {
//get Parent canvasId
const parentId: string | undefined = selectedWidgets[0].parentId;
// If we failed to get the parent canvas widget Id then return empty object
if (parentId === undefined) return {};
// get the Id of the container like widget based on the canvasId
const containerId = getContainerIdForCanvas(parentId);
// If we failed to get the containing container like widget Id then return empty object
if (containerId === undefined) return {};
const containerWidget = canvasWidgets[containerId];
const canvasDOM = document.querySelector(`#${getSlidingArenaName(parentId)}`);
if (!canvasDOM || !containerWidget) return {};
const rect = canvasDOM.getBoundingClientRect();
// get Grid values such as snapRowSpace and snapColumnSpace
const { snapGrid } = getSnappedGrid(containerWidget, rect.width);
const selectedWidgetsArray = selectedWidgets.length ? selectedWidgets : [];
//from selected widgets get some information required for position calculation
const {
leftMostColumn: selectedLeftMostColumn,
maxThickness,
topMostRow: selectedTopMostRow,
totalWidth,
} = getBoundariesFromSelectedWidgets(selectedWidgetsArray);
// calculation of left most column of where widgets are to be pasted
let pasteLeftMostColumn =
selectedLeftMostColumn - (copiedTotalWidth - totalWidth) / 2;
pasteLeftMostColumn = Math.round(pasteLeftMostColumn);
// conditions to adjust to the edges of the boundary, so that it doesn't go out of canvas
if (pasteLeftMostColumn < 0) pasteLeftMostColumn = 0;
if (
pasteLeftMostColumn + copiedTotalWidth >
GridDefaults.DEFAULT_GRID_COLUMNS
)
pasteLeftMostColumn = GridDefaults.DEFAULT_GRID_COLUMNS - copiedTotalWidth;
// based on the above calculation get the new Positions that are aligned to the top left of selected widgets
// i.e., the top of the selected widgets will be equal to the top of copied widgets and both are horizontally centered
const newPositionsForCopiedWidgets = getNewPositionsForCopiedWidgets(
copiedWidgetGroups,
copiedTopMostRow,
selectedTopMostRow,
copiedLeftMostColumn,
pasteLeftMostColumn,
);
// with the new positions, calculate the map of new position, which are moved down to the point where
// it doesn't overlap with any of the selected widgets.
const newPastingPositionMap = getVerticallyAdjustedPositions(
newPositionsForCopiedWidgets,
getOccupiedSpacesFromProps(selectedWidgetsArray),
maxThickness,
);
if (!newPastingPositionMap) return {};
const gridProps = {
parentColumnSpace: snapGrid.snapColumnSpace,
parentRowSpace: snapGrid.snapRowSpace,
maxGridColumns: GridDefaults.DEFAULT_GRID_COLUMNS,
};
const reflowSpacesSelector = getContainerWidgetSpacesSelector(parentId);
const widgetSpaces: WidgetSpace[] = yield select(reflowSpacesSelector) || [];
// Ids of each pasting are changed just for reflow
const newPastePositions = changeIdsOfPastePositions(newPastingPositionMap);
const { movementMap: reflowedMovementMap } = reflow(
newPastePositions,
newPastePositions,
widgetSpaces,
ReflowDirection.BOTTOM,
gridProps,
true,
false,
{ prevSpacesMap: {} } as PrevReflowState,
);
// calculate the new bottom most row of the canvas
const bottomMostRow = getBottomRowAfterReflow(
reflowedMovementMap,
getBottomMostRow(newPastePositions),
widgetSpaces,
gridProps,
);
return {
bottomMostRow:
(bottomMostRow + GridDefaults.CANVAS_EXTENSION_OFFSET) *
gridProps.parentRowSpace,
gridProps,
newPastingPositionMap,
reflowedMovementMap,
canvasId: parentId,
};
}
/**
* Calculates the new positions of the pasting widgets, based on the mouse position
* If the mouse position is on the canvas it the top left of the new positions aligns itself to the mouse position
* returns a empty object if the mouse is out of canvas
*
* @param copiedWidgetGroups Contains information on the copied widgets
* @param mouseLocation location of the mouse in absolute pixels
* @param selectedWidgets array of selected widgets
* @param canvasWidgets canvas widgets from the DSL
* @param copiedTotalWidth total width of the copied widgets
* @param copiedTopMostRow top row of the top most copied widget
* @param copiedLeftMostColumn left column of the left most copied widget
* @returns
*/
function* getNewPositionsBasedOnMousePositions(
copiedWidgetGroups: CopiedWidgetGroup[],
mouseLocation: { x: number; y: number },
selectedWidgets: WidgetProps[],
canvasWidgets: CanvasWidgetsReduxState,
copiedTotalWidth: number,
copiedTopMostRow: number,
copiedLeftMostColumn: number,
) {
let { canvasDOM, canvasId, containerWidget } =
getDefaultCanvas(canvasWidgets);
//if the selected widget is a layout widget then change the pasting canvas.
if (selectedWidgets.length === 1 && isDropTarget(selectedWidgets[0].type)) {
containerWidget = selectedWidgets[0];
({ canvasDOM, canvasId } = getCanvasIdForContainer(containerWidget));
}
if (!canvasDOM || !containerWidget || !canvasId) return {};
const canvasRect = canvasDOM.getBoundingClientRect();
// get Grid values such as snapRowSpace and snapColumnSpace
const { padding, snapGrid } = getSnappedGrid(
containerWidget,
canvasRect.width,
);
// get mouse positions in terms of grid rows and columns of the pasting canvas
const mousePositions = getMousePositions(
canvasRect,
canvasId,
snapGrid,
padding,
mouseLocation,
);
if (!snapGrid || !mousePositions) return {};
const reflowSpacesSelector = getContainerWidgetSpacesSelector(canvasId);
const widgetSpaces: WidgetSpace[] = yield select(reflowSpacesSelector) || [];
let mouseTopRow = mousePositions.top;
let mouseLeftColumn = mousePositions.left;
// if the mouse position is on another widget on the canvas, then new positions are below it.
for (const widgetSpace of widgetSpaces) {
if (
widgetSpace.top < mousePositions.top &&
widgetSpace.left < mousePositions.left &&
widgetSpace.bottom > mousePositions.top &&
widgetSpace.right > mousePositions.left
) {
mouseTopRow = widgetSpace.bottom + WIDGET_PASTE_PADDING;
mouseLeftColumn =
widgetSpace.left -
(copiedTotalWidth - (widgetSpace.right - widgetSpace.left)) / 2;
break;
}
}
mouseLeftColumn = Math.round(mouseLeftColumn);
// adjust the top left based on the edges of the canvas
if (mouseLeftColumn < 0) mouseLeftColumn = 0;
if (mouseLeftColumn + copiedTotalWidth > GridDefaults.DEFAULT_GRID_COLUMNS)
mouseLeftColumn = GridDefaults.DEFAULT_GRID_COLUMNS - copiedTotalWidth;
// get the new Pasting positions of the widgets based on the adjusted mouse top-left
const newPastingPositionMap = getPastePositionMapFromMousePointer(
copiedWidgetGroups,
copiedTopMostRow,
mouseTopRow,
copiedLeftMostColumn,
mouseLeftColumn,
);
const gridProps = {
parentColumnSpace: snapGrid.snapColumnSpace,
parentRowSpace: snapGrid.snapRowSpace,
maxGridColumns: GridDefaults.DEFAULT_GRID_COLUMNS,
};
// Ids of each pasting are changed just for reflow
const newPastePositions = changeIdsOfPastePositions(newPastingPositionMap);
const { movementMap: reflowedMovementMap } = reflow(
newPastePositions,
newPastePositions,
widgetSpaces,
ReflowDirection.BOTTOM,
gridProps,
true,
false,
{ prevSpacesMap: {} } as PrevReflowState,
);
// calculate the new bottom most row of the canvas.
const bottomMostRow = getBottomRowAfterReflow(
reflowedMovementMap,
getBottomMostRow(newPastePositions),
widgetSpaces,
gridProps,
);
return {
bottomMostRow:
(bottomMostRow + GridDefaults.CANVAS_EXTENSION_OFFSET) *
gridProps.parentRowSpace,
gridProps,
newPastingPositionMap,
reflowedMovementMap,
canvasId,
};
}
/**
* this saga create a new widget from the copied one to store
*/
function* pasteWidgetSaga(
action: ReduxAction<{
groupWidgets: boolean;
mouseLocation: { x: number; y: number };
}>,
) {
const {
flexLayers,
widgets: copiedWidgets,
}: {
widgets: CopiedWidgetGroup[];
flexLayers: FlexLayer[];
} = yield getCopiedWidgets();
let copiedWidgetGroups = copiedWidgets ? [...copiedWidgets] : [];
const shouldGroup: boolean = action.payload.groupWidgets;
const newlyCreatedWidgetIds: string[] = [];
const evalTree: DataTree = yield select(getDataTree);
const canvasWidgets: CanvasWidgetsReduxState = yield select(getWidgets);
let widgets: CanvasWidgetsReduxState = canvasWidgets;
const selectedWidget: FlattenedWidgetProps<undefined> =
yield getSelectedWidgetWhenPasting();
const isMobile: boolean = yield select(getIsAutoLayoutMobileBreakPoint);
const mainCanvasWidth: number = yield select(getCanvasWidth);
try {
let reflowedMovementMap,
gridProps: GridProps | undefined,
newPastingPositionMap: SpaceMap | undefined,
canvasId;
let pastingIntoWidgetId: string = yield getParentWidgetIdForPasting(
canvasWidgets,
selectedWidget,
);
let isThereACollision = false;
// if this is true, selected widgets will be grouped in container
if (shouldGroup) {
copiedWidgetGroups = yield createSelectedWidgetsAsCopiedWidgets();
pastingIntoWidgetId = yield getParentWidgetIdForGrouping(
widgets,
copiedWidgetGroups,
);
widgets = yield filterOutSelectedWidgets(
copiedWidgetGroups[0].parentId,
copiedWidgetGroups,
);
isThereACollision = yield isSelectedWidgetsColliding(
widgets,
copiedWidgetGroups,
pastingIntoWidgetId,
);
//while grouping, the container around the selected widgets will increase by 2 rows,
//hence if there are any widgets in that path then we reflow those widgets
// If there are already widgets inside the selection box even before grouping
//then we will have to move it down to the bottom most row
({ copiedWidgetGroups, gridProps, reflowedMovementMap } =
yield groupWidgetsIntoContainer(
copiedWidgetGroups,
pastingIntoWidgetId,
isThereACollision,
));
}
if (
// to avoid invoking old way of copied widgets implementaion
!Array.isArray(copiedWidgetGroups) ||
!copiedWidgetGroups.length
)
return;
const {
leftMostWidget,
topMostWidget,
totalWidth: copiedTotalWidth,
} = getBoundaryWidgetsFromCopiedGroups(copiedWidgetGroups);
const nextAvailableRow: number = nextAvailableRowInContainer(
pastingIntoWidgetId,
widgets,
);
// skip new position calculation if grouping
if (!shouldGroup) {
// new pasting positions, the variables are undefined if the positions cannot be calculated,
// then it pastes the regular way at the bottom of the canvas
({ canvasId, gridProps, newPastingPositionMap, reflowedMovementMap } =
yield call(
getNewPositions,
copiedWidgetGroups,
action.payload.mouseLocation,
copiedTotalWidth,
topMostWidget.topRow,
leftMostWidget.leftColumn,
));
if (canvasId) pastingIntoWidgetId = canvasId;
}
for (const widgetGroup of copiedWidgetGroups) {
//This is required when you cut the widget as CanvasWidgetState doesn't have the widget anymore
const widgetType = widgetGroup.list.find(
(widget) => widget.widgetId === widgetGroup.widgetId,
)?.type;
if (!widgetType) break;
yield call(
executeWidgetBlueprintBeforeOperations,
BlueprintOperationTypes.BEFORE_PASTE,
{
parentId: pastingIntoWidgetId,
widgetId: widgetGroup.widgetId,
widgets,
widgetType,
},
);
}
const widgetIdMap: Record<string, string> = {};
const reverseWidgetIdMap: Record<string, string> = {};
yield all(
copiedWidgetGroups.map((copiedWidgets) =>
call(function* () {
// Don't try to paste if there is no copied widget
if (!copiedWidgets) return;
const copiedWidgetId = copiedWidgets.widgetId;
const unUpdatedCopyOfWidget = copiedWidgets.list[0];
const newTopRow = shouldGroup
? isThereACollision
? topMostWidget.topRow
: 0
: topMostWidget.topRow;
const copiedWidget = {
...unUpdatedCopyOfWidget,
topRow: unUpdatedCopyOfWidget.topRow - newTopRow,
bottomRow: unUpdatedCopyOfWidget.bottomRow - newTopRow,
};
// Log the paste or group event.
if (shouldGroup) {
AnalyticsUtil.logEvent("WIDGET_GROUP", {
widgetName: copiedWidget.widgetName,
widgetType: copiedWidget.type,
});
} else {
AnalyticsUtil.logEvent("WIDGET_PASTE", {
widgetName: copiedWidget.widgetName,
widgetType: copiedWidget.type,
});
}
// Compute the new widget's positional properties
const newWidgetPosition = calculateNewWidgetPosition(
copiedWidget,
pastingIntoWidgetId,
widgets,
nextAvailableRow,
newPastingPositionMap,
true,
isThereACollision,
shouldGroup,
);
// Get a flat list of all the widgets to be updated
const widgetList = copiedWidgets.list;
const widgetNameMap: Record<string, string> = {};
const newWidgetList: FlattenedWidgetProps[] = [];
// Generate new widgetIds for the flat list of all the widgets to be updated
widgetList.forEach((widget) => {
// Create a copy of the widget properties
const newWidget = cloneDeep(widget);
newWidget.widgetId = generateReactKey();
// Add the new widget id so that it maps the previous widget id
widgetIdMap[widget.widgetId] = newWidget.widgetId;
reverseWidgetIdMap[newWidget.widgetId] = widget.widgetId;
// Add the new widget to the list
newWidgetList.push(newWidget);
});
// For each of the new widgets generated
for (let i = 0; i < newWidgetList.length; i++) {
const widget = newWidgetList[i];
const oldWidgetName = widget.widgetName;
let newWidgetName = oldWidgetName;
if (!shouldGroup) {
newWidgetName = getNextWidgetName(
widgets,
widget.type,
evalTree,
{
prefix: oldWidgetName,
startWithoutIndex: true,
},
);
}
// Update the children widgetIds if it has children
if (widget.children && widget.children.length > 0) {
widget.children.forEach(
(childWidgetId: string, index: number) => {
if (widget.children) {
widget.children[index] = widgetIdMap[childWidgetId];
}
},
);
}
// Update the tabs for the tabs widget.
if (widget.tabsObj && widget.type === "TABS_WIDGET") {
try {
const tabs = Object.values(widget.tabsObj);
if (Array.isArray(tabs)) {
widget.tabsObj = tabs.reduce((obj: any, tab) => {
tab.widgetId = widgetIdMap[tab.widgetId];
obj[tab.id] = tab;
return obj;
}, {});
}
} catch (error) {
log.debug("Error updating tabs", error);
}
}
// Update the table widget column properties
if (
widget.type === "TABLE_WIDGET_V2" ||
widget.type === "TABLE_WIDGET"
) {
try {
// If the primaryColumns of the table exist
if (widget.primaryColumns) {
// For each column
for (const [columnId, column] of Object.entries(
widget.primaryColumns,
)) {
// For each property in the column
for (const [key, value] of Object.entries(
column as ColumnProperties,
)) {
// Replace reference of previous widget with the new widgetName
// This handles binding scenarios like `{{Table2.tableData.map((currentRow) => (currentRow.id))}}`
widget.primaryColumns[columnId][key] = isString(value)
? value.replace(
`${oldWidgetName}.`,
`${newWidgetName}.`,
)
: value;
}
}
}
// Use the new widget name we used to replace the column properties above.
widget.widgetName = newWidgetName;
} catch (error) {
log.debug("Error updating table widget properties", error);
}
}
// TODO: here to move this to the widget definition
// Update the Select widget defaultValue properties
if (
widget.type === "MULTI_SELECT_WIDGET_V2" ||
widget.type === "SELECT_WIDGET"
) {
try {
// If the defaultOptionValue exist
if (widget.defaultOptionValue) {
const value = widget.defaultOptionValue;
// replace All occurrence of old widget name
widget.defaultOptionValue = isString(value)
? value.replaceAll(`${oldWidgetName}.`, `${newWidgetName}.`)
: value;
}
// Use the new widget name we used to replace the defaultValue properties above.
widget.widgetName = newWidgetName;
} catch (error) {
log.debug("Error updating widget properties", error);
}
}
// If it is the copied widget, update position properties
if (widget.widgetId === widgetIdMap[copiedWidget.widgetId]) {
//when the widget is a modal widget, it has to paste on the main container
const pastingParentId =
widget.type === "MODAL_WIDGET"
? MAIN_CONTAINER_WIDGET_ID
: pastingIntoWidgetId;
const { bottomRow, leftColumn, rightColumn, topRow } =
newWidgetPosition;
widget.leftColumn = leftColumn;
widget.topRow = topRow;
widget.bottomRow = bottomRow;
widget.rightColumn = rightColumn;
widget.parentId = pastingParentId;
// Also, update the parent widget in the canvas widgets
// to include this new copied widget's id in the parent's children
let parentChildren = [widget.widgetId];
const widgetChildren = widgets[pastingParentId].children;
if (widgetChildren && Array.isArray(widgetChildren)) {
// Add the new child to existing children after it's original siblings position.
const originalWidgetId: string = widgetList[i].widgetId;
const originalWidgetIndex: number =
widgetChildren.indexOf(originalWidgetId);
parentChildren = [
...widgetChildren.slice(0, originalWidgetIndex + 1),
...parentChildren,
...widgetChildren.slice(originalWidgetIndex + 1),
];
}
widgets = {
...widgets,
[pastingParentId]: {
...widgets[pastingParentId],
children: parentChildren,
},
};
// If the copied widget's boundaries exceed the parent's
// Make the parent scrollable
if (
widgets[pastingParentId].bottomRow *
widgets[widget.parentId].parentRowSpace <=
widget.bottomRow * widget.parentRowSpace &&
!widget.detachFromLayout
) {
const parentOfPastingWidget = widgets[pastingParentId].parentId;
if (
parentOfPastingWidget &&
widget.parentId !== MAIN_CONTAINER_WIDGET_ID
) {
const parent = widgets[parentOfPastingWidget];
widgets[parentOfPastingWidget] = {
...parent,
shouldScrollContents: true,
};
}
}
} else {
// For all other widgets in the list
// (These widgets will be descendants of the copied widget)
// This means, that their parents will also be newly copied widgets
// Update widget's parent widget ids with the new parent widget ids
const newParentId = newWidgetList.find((newWidget) =>
widget.parentId
? newWidget.widgetId === widgetIdMap[widget.parentId]
: false,
)?.widgetId;
if (newParentId) widget.parentId = newParentId;
}
// Generate a new unique widget name
if (!shouldGroup) {
widget.widgetName = newWidgetName;
}
widgetNameMap[oldWidgetName] = widget.widgetName;
// Add the new widget to the canvas widgets
widgets[widget.widgetId] = widget;
/**
* If new parent is a vertical stack, then update flex layers.
*/
if (widget.parentId) {
const pastingIntoWidget = widgets[widget.parentId];
if (
pastingIntoWidget &&
isStack(widgets, pastingIntoWidget) &&
(pastingIntoWidgetId !== pastingIntoWidget.widgetId ||
!flexLayers ||
flexLayers.length <= 0)
) {
const metaProps: Record<string, any> =
yield select(getWidgetsMeta);
if (widget.widgetId === widgetIdMap[copiedWidget.widgetId])
widgets = pasteWidgetInFlexLayers(
widgets,
widget.parentId,
widget,
reverseWidgetIdMap[widget.widgetId],
isMobile,
mainCanvasWidth,
metaProps,
);
else if (widget.type !== "CANVAS_WIDGET")
widgets = addChildToPastedFlexLayers(
widgets,
widget,
widgetIdMap,
isMobile,
mainCanvasWidth,
metaProps,
);
}
}
}
newlyCreatedWidgetIds.push(widgetIdMap[copiedWidgetId]);
// 1. updating template in the copied widget and deleting old template associations
// 2. updating dynamicBindingPathList in the copied grid widget
for (let i = 0; i < newWidgetList.length; i++) {
const widget = newWidgetList[i];
widgets = handleSpecificCasesWhilePasting(
widget,
widgets,
widgetNameMap,
newWidgetList,
);
}
}),
),
);
//calculate the new positions of the reflowed widgets
let reflowedWidgets = getReflowedPositions(
widgets,
gridProps,
reflowedMovementMap,
);
if (
pastingIntoWidgetId &&
reflowedWidgets[pastingIntoWidgetId] &&
flexLayers &&
flexLayers.length > 0
) {
const newFlexLayers = getNewFlexLayers(flexLayers, widgetIdMap);
reflowedWidgets[pastingIntoWidgetId] = {
...reflowedWidgets[pastingIntoWidgetId],
flexLayers: [
...(reflowedWidgets[pastingIntoWidgetId]?.flexLayers || []),
...newFlexLayers,
],
};
const metaProps: Record<string, any> = yield select(getWidgetsMeta);
reflowedWidgets = updateWidgetPositions(
reflowedWidgets,
pastingIntoWidgetId,
isMobile,
mainCanvasWidth,
false,
metaProps,
);
}
// some widgets need to update property of parent if the parent have CHILD_OPERATIONS
// so here we are traversing up the tree till we get to MAIN_CONTAINER_WIDGET_ID
// while traversing, if we find any widget which has CHILD_OPERATION, we will call the fn in it
const updatedWidgets: CanvasWidgetsReduxState = yield call(
traverseTreeAndExecuteBlueprintChildOperations,
reflowedWidgets[pastingIntoWidgetId],
newlyCreatedWidgetIds.filter(
(widgetId) => !reflowedWidgets[widgetId]?.detachFromLayout,
),
reflowedWidgets,
);
yield call(updateAndSaveAnvilLayout, updatedWidgets);
const pageId: string = yield select(getCurrentPageId);
if (copiedWidgetGroups && copiedWidgetGroups.length > 0) {
history.push(builderURL({ pageId }));
}
yield put({
type: ReduxActionTypes.RECORD_RECENTLY_ADDED_WIDGET,
payload: newlyCreatedWidgetIds,
});
yield put(generateAutoHeightLayoutTreeAction(true, true));
//if pasting at the bottom of the canvas, then flash it.
if (shouldGroup || !newPastingPositionMap) {
flashElementsById(newlyCreatedWidgetIds, 100);
}
yield put(
selectWidgetInitAction(
SelectionRequestType.Multiple,
newlyCreatedWidgetIds,
),
);
} catch (error) {
yield put({
type: ReduxActionErrorTypes.WIDGET_OPERATION_ERROR,
payload: {
action: ReduxActionTypes.PASTE_COPIED_WIDGET_INIT,
error,
},
});
}
}
function* cutWidgetSaga() {
const allWidgets: { [widgetId: string]: FlattenedWidgetProps } =
yield select(getWidgets);
const selectedWidgets: string[] = yield select(getSelectedWidgets);
if (!selectedWidgets) {
toast.show(createMessage(ERROR_WIDGET_CUT_NO_WIDGET_SELECTED), {
kind: "info",
});
return;
}
const allAllowedToCut = selectedWidgets.some((each) => {
//should not allow canvas widgets to be cut
return (
allWidgets[each] &&
!allWidgets[each].disallowCopy &&
allWidgets[each].type !== "CANVAS_WIDGET"
);
});
if (!allAllowedToCut) {
toast.show(createMessage(ERROR_WIDGET_CUT_NOT_ALLOWED), {
kind: "info",
});
return;
}
const selectedWidgetProps = selectedWidgets.map((each) => allWidgets[each]);
const canvasId = selectedWidgetProps?.[0]?.parentId || "";
const flexLayers: FlexLayer[] = getFlexLayersForSelectedWidgets(
selectedWidgets,
canvasId ? allWidgets[canvasId] : undefined,
);
const saveResult: boolean = yield createSelectedWidgetsCopy(
selectedWidgetProps,
flexLayers,
);
selectedWidgetProps.forEach((each) => {
const eventName = "WIDGET_CUT_VIA_SHORTCUT"; // cut only supported through a shortcut
AnalyticsUtil.logEvent(eventName, {
widgetName: each.widgetName,
widgetType: each.type,
});
});
if (saveResult) {
toast.show(
createMessage(
WIDGET_CUT,
selectedWidgetProps.length > 1
? `${selectedWidgetProps.length} Widgets`
: selectedWidgetProps[0].widgetName,
),
{
kind: "success",
},
);
}
yield put({
type: WidgetReduxActionTypes.WIDGET_DELETE,
payload: {
disallowUndo: true,
isShortcut: true,
},
});
}
function* addSuggestedWidget(action: ReduxAction<Partial<WidgetProps>>) {
const isSetWidgetIdForWalkthrough = !!(
action.payload.props.setWidgetIdForWalkthrough === "true"
);
const widgetConfig = action.payload;
delete widgetConfig.props?.setWidgetIdForWalkthrough;
if (!widgetConfig.type) return;
const defaultConfig = WidgetFactory.widgetConfigMap.get(widgetConfig.type);
const evalTree: DataTree = yield select(getDataTree);
const widgets: CanvasWidgetsReduxState = yield select(getWidgets);
const widgetName = getNextWidgetName(widgets, widgetConfig.type, evalTree);
const layoutSystemType: LayoutSystemTypes = yield select(getLayoutSystemType);
try {
let newWidget = {
newWidgetId: generateReactKey(),
widgetId: "0",
parentId: "0",
renderMode: RenderModes.CANVAS,
isLoading: false,
...defaultConfig,
widgetName,
...widgetConfig,
};
const { bottomRow, leftColumn, rightColumn, topRow } =
yield calculateNewWidgetPosition(
newWidget as WidgetProps,
MAIN_CONTAINER_WIDGET_ID,
widgets,
);
newWidget = {
...newWidget,
leftColumn,
topRow,
rightColumn,
bottomRow,
parentRowSpace: GridDefaults.DEFAULT_GRID_ROW_HEIGHT,
};
switch (layoutSystemType) {
case LayoutSystemTypes.AUTO:
yield put({
type: ReduxActionTypes.AUTOLAYOUT_ADD_NEW_WIDGETS,
payload: {
dropPayload: {
isNewLayer: true,
alignment: FlexLayerAlignment.Start,
},
newWidget,
parentId: MAIN_CONTAINER_WIDGET_ID,
direction: LayoutDirection.Vertical,
addToBottom: true,
},
});
break;
case LayoutSystemTypes.ANVIL:
yield put(
addSuggestedWidgetAnvilAction({
newWidgetId: newWidget.newWidgetId,
rows: newWidget.rows,
columns: newWidget.columns,
type: newWidget.type,
...widgetConfig,
}),
);
break;
default:
yield put({
type: WidgetReduxActionTypes.WIDGET_ADD_CHILD,
payload: newWidget,
});
break;
}
yield take(ReduxActionTypes.UPDATE_LAYOUT);
if (isSetWidgetIdForWalkthrough) {
localStorage.setItem(WIDGET_ID_SHOW_WALKTHROUGH, newWidget.newWidgetId);
}
yield put(
selectWidgetInitAction(SelectionRequestType.One, [newWidget.newWidgetId]),
);
} catch (error) {
log.error(error);
}
}
/**
* saga to group selected widgets into a new container
*
* @param action
*/
export function* groupWidgetsSaga() {
const selectedWidgetIDs: string[] = yield select(getSelectedWidgets);
const isMultipleWidgetsSelected = selectedWidgetIDs.length > 1;
// Grouping functionality has been temporarily disabled for auto-layout canvas.
const isAutoLayout: boolean = yield select(getIsAutoLayout);
if (isAutoLayout) return;
if (isMultipleWidgetsSelected) {
try {
yield put({
type: ReduxActionTypes.PASTE_COPIED_WIDGET_INIT,
payload: {
groupWidgets: true,
},
});
} catch (error) {
log.error(error);
}
}
}
function* widgetBatchUpdatePropertySaga() {
/*
* BATCH_UPDATE_WIDGET_PROPERTY should be processed serially as
* it updates the state. We want the state updates from previous
* batch update to be flushed out to the store before processing
* the another batch update.
*/
const batchUpdateWidgetPropertyChannel: unknown = yield actionChannel(
ReduxActionTypes.BATCH_UPDATE_WIDGET_PROPERTY,
);
while (true) {
// @ts-expect-error: Type mismatch
const action: unknown = yield take(batchUpdateWidgetPropertyChannel);
// @ts-expect-error: Type mismatch
yield call(batchUpdateWidgetPropertySaga, action);
}
}
export default function* widgetOperationSagas() {
yield fork(widgetAdditionSagas);
yield fork(widgetDeletionSagas);
yield fork(widgetSelectionSagas);
yield fork(widgetBatchUpdatePropertySaga);
yield all([
takeEvery(ReduxActionTypes.ADD_SUGGESTED_WIDGET, addSuggestedWidget),
takeLatest(WidgetReduxActionTypes.WIDGET_RESIZE, resizeSaga),
takeEvery(
ReduxActionTypes.UPDATE_WIDGET_PROPERTY_REQUEST,
updateWidgetPropertySaga,
),
takeEvery(
WidgetReduxActionTypes.WIDGET_UPDATE_PROPERTY,
updateWidgetPropertySaga,
),
takeEvery(
ReduxActionTypes.SET_WIDGET_DYNAMIC_PROPERTY,
setWidgetDynamicPropertySaga,
),
takeEvery(
ReduxActionTypes.BATCH_SET_WIDGET_DYNAMIC_PROPERTY,
batchUpdateWidgetDynamicPropertySaga,
),
takeEvery(
ReduxActionTypes.RESET_CHILDREN_WIDGET_META,
resetChildrenMetaSaga,
),
takeEvery(
ReduxActionTypes.BATCH_UPDATE_MULTIPLE_WIDGETS_PROPERTY,
batchUpdateMultipleWidgetsPropertiesSaga,
),
takeEvery(
ReduxActionTypes.DELETE_WIDGET_PROPERTY,
deleteWidgetPropertySaga,
),
takeLatest(ReduxActionTypes.UPDATE_CANVAS_SIZE, updateCanvasSize),
takeLatest(ReduxActionTypes.COPY_SELECTED_WIDGET_INIT, copyWidgetSaga),
takeLeading(ReduxActionTypes.PASTE_COPIED_WIDGET_INIT, pasteWidgetSaga),
takeEvery(ReduxActionTypes.CUT_SELECTED_WIDGET, cutWidgetSaga),
takeEvery(ReduxActionTypes.GROUP_WIDGETS_INIT, groupWidgetsSaga),
takeEvery(ReduxActionTypes.PARTIAL_IMPORT_INIT, partialImportSaga),
takeEvery(ReduxActionTypes.PARTIAL_EXPORT_INIT, partialExportSaga),
]);
}