#### 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 -->
2233 lines
71 KiB
TypeScript
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),
|
|
]);
|
|
}
|