PromucFlow_constructor/app/client/src/sagas/WidgetOperationSagas.tsx
2021-07-28 12:59:51 +05:30

1944 lines
60 KiB
TypeScript

import {
ReduxAction,
ReduxActionErrorTypes,
ReduxActionTypes,
} from "constants/ReduxActionConstants";
import {
MultipleWidgetDeletePayload,
updateAndSaveLayout,
WidgetAddChild,
WidgetAddChildren,
WidgetDelete,
WidgetMove,
WidgetResize,
} from "actions/pageActions";
import {
CanvasWidgetsReduxState,
FlattenedWidgetProps,
} from "reducers/entityReducers/canvasWidgetsReducer";
import {
getFocusedWidget,
getSelectedWidget,
getWidget,
getWidgets,
} from "./selectors";
import {
generateWidgetProps,
updateWidgetPosition,
} from "utils/WidgetPropsUtils";
import {
all,
call,
fork,
put,
select,
takeEvery,
takeLatest,
} from "redux-saga/effects";
import { convertToString, getNextEntityName } from "utils/AppsmithUtils";
import {
batchUpdateWidgetProperty,
DeleteWidgetPropertyPayload,
SetWidgetDynamicPropertyPayload,
UpdateWidgetPropertyPayload,
UpdateWidgetPropertyRequestPayload,
} from "actions/controlActions";
import {
DynamicPath,
getEntityDynamicBindingPathList,
getWidgetDynamicPropertyPathList,
getWidgetDynamicTriggerPathList,
isChildPropertyPath,
isDynamicValue,
isPathADynamicBinding,
isPathADynamicTrigger,
} from "utils/DynamicBindingUtils";
import { WidgetProps } from "widgets/BaseWidget";
import _, { cloneDeep, flattenDeep, isString, set, remove } from "lodash";
import WidgetFactory from "utils/WidgetFactory";
import {
buildWidgetBlueprint,
executeWidgetBlueprintOperations,
traverseTreeAndExecuteBlueprintChildOperations,
} from "sagas/WidgetBlueprintSagas";
import { resetWidgetMetaProperty } from "actions/metaActions";
import {
GridDefaults,
MAIN_CONTAINER_WIDGET_ID,
RenderModes,
WIDGET_DELETE_UNDO_TIMEOUT,
WidgetType,
WidgetTypes,
} from "constants/WidgetConstants";
import WidgetConfigResponse, {
GRID_DENSITY_MIGRATION_V1,
} from "mockResponses/WidgetConfigResponse";
import {
flushDeletedWidgets,
getCopiedWidgets,
getDeletedWidgets,
saveCopiedWidgets,
saveDeletedWidgets,
} from "utils/storage";
import { generateReactKey } from "utils/generators";
import { flashElementById } from "utils/helpers";
import AnalyticsUtil from "utils/AnalyticsUtil";
import log from "loglevel";
import { navigateToCanvas } from "pages/Editor/Explorer/Widgets/utils";
import {
getCurrentApplicationId,
getCurrentPageId,
} from "selectors/editorSelectors";
import {
closePropertyPane,
closeTableFilterPane,
forceOpenPropertyPane,
} from "actions/widgetActions";
import {
selectMultipleWidgetsInitAction,
selectWidgetInitAction,
} from "actions/widgetSelectionActions";
import { getDataTree } from "selectors/dataTreeSelectors";
import {
clearEvalPropertyCacheOfWidget,
validateProperty,
} from "./EvaluationsSaga";
import { WidgetBlueprint } from "reducers/entityReducers/widgetConfigReducer";
import { Toaster } from "components/ads/Toast";
import { Variant } from "components/ads/common";
import { ColumnProperties } from "components/designSystems/appsmith/TableComponent/Constants";
import {
getAllPathsFromPropertyConfig,
nextAvailableRowInContainer,
} from "entities/Widget/utils";
import { getAllPaths } from "workers/evaluationUtils";
import {
createMessage,
ERROR_ADD_WIDGET_FROM_QUERY,
ERROR_WIDGET_COPY_NO_WIDGET_SELECTED,
ERROR_WIDGET_CUT_NO_WIDGET_SELECTED,
WIDGET_COPY,
WIDGET_CUT,
WIDGET_DELETE,
WIDGET_BULK_DELETE,
ERROR_WIDGET_COPY_NOT_ALLOWED,
} from "constants/messages";
import AppsmithConsole from "utils/AppsmithConsole";
import { ENTITY_TYPE } from "entities/AppsmithConsole";
import LOG_TYPE from "entities/AppsmithConsole/logtype";
import {
checkIfPastingIntoListWidget,
doesTriggerPathsContainPropertyPath,
getParentWidgetIdForPasting,
getWidgetChildren,
handleSpecificCasesWhilePasting,
} from "./WidgetOperationUtils";
import { getSelectedWidgets } from "selectors/ui";
import { getParentWithEnhancementFn } from "./WidgetEnhancementHelpers";
import { widgetSelectionSagas } from "./WidgetSelectionSagas";
function* getChildWidgetProps(
parent: FlattenedWidgetProps,
params: WidgetAddChild,
widgets: { [widgetId: string]: FlattenedWidgetProps },
) {
const { leftColumn, newWidgetId, props, topRow, type } = params;
let { columns, parentColumnSpace, parentRowSpace, rows, widgetName } = params;
let minHeight = undefined;
/* eslint-disable @typescript-eslint/no-unused-vars */
const { blueprint = undefined, ...restDefaultConfig } = {
...(WidgetConfigResponse as any).config[type],
};
if (!widgetName) {
const widgetNames = Object.keys(widgets).map((w) => widgets[w].widgetName);
const entityNames = yield call(getEntityNames);
widgetName = getNextEntityName(restDefaultConfig.widgetName, [
...widgetNames,
...entityNames,
]);
}
if (type === WidgetTypes.CANVAS_WIDGET) {
columns =
(parent.rightColumn - parent.leftColumn) * parent.parentColumnSpace;
parentColumnSpace = 1;
rows = (parent.bottomRow - parent.topRow) * parent.parentRowSpace;
parentRowSpace = 1;
minHeight = rows;
if (props) props.children = [];
}
const widgetProps = {
...restDefaultConfig,
...props,
columns,
rows,
minHeight,
};
const widget = generateWidgetProps(
parent,
type,
leftColumn,
topRow,
parentRowSpace,
parentColumnSpace,
widgetName,
widgetProps,
restDefaultConfig.version,
);
widget.widgetId = newWidgetId;
return widget;
}
type GeneratedWidgetPayload = {
widgetId: string;
widgets: { [widgetId: string]: FlattenedWidgetProps };
};
function* generateChildWidgets(
parent: FlattenedWidgetProps,
params: WidgetAddChild,
widgets: { [widgetId: string]: FlattenedWidgetProps },
propsBlueprint?: WidgetBlueprint,
): any {
// Get the props for the widget
const widget = yield getChildWidgetProps(parent, params, widgets);
// Add the widget to the canvasWidgets
// We need this in here as widgets will be used to get the current widget
widgets[widget.widgetId] = widget;
// Get the default config for the widget from WidgetConfigResponse
const defaultConfig = {
...(WidgetConfigResponse as any).config[widget.type],
};
// If blueprint is provided in the params, use that
// else use the blueprint available in WidgetConfigResponse
// else there is no blueprint for this widget
const blueprint =
propsBlueprint || { ...defaultConfig.blueprint } || undefined;
// If there is a blueprint.view
// We need to generate the children based on the view
if (blueprint && blueprint.view) {
// Get the list of children props in WidgetAddChild format
const childWidgetList: WidgetAddChild[] = yield call(
buildWidgetBlueprint,
blueprint,
widget.widgetId,
);
// For each child props
const childPropsList: GeneratedWidgetPayload[] = yield all(
childWidgetList.map((props: WidgetAddChild) => {
// Generate full widget props
// Notice that we're passing the blueprint if it exists.
return generateChildWidgets(
widget,
props,
widgets,
props.props?.blueprint,
);
}),
);
// Start children array from scratch
widget.children = [];
childPropsList.forEach((props: GeneratedWidgetPayload) => {
// Push the widgetIds of the children generated above into the widget.children array
widget.children.push(props.widgetId);
// Add the list of widgets generated into the canvasWidgets
widgets = props.widgets;
});
}
// Finally, add the widget to the canvasWidgets
// This is different from above, as this is the final widget props with
// a fully populated widget.children property
widgets[widget.widgetId] = widget;
// Some widgets need to run a few operations like modifying props or adding an action
// these operations can be performed on the parent of the widget we're adding
// therefore, we pass all widgets to executeWidgetBlueprintOperations
// blueprint.operations contain the set of operations to perform to update the canvasWidgets
if (blueprint && blueprint.operations && blueprint.operations.length > 0) {
// Finalize the canvasWidgets with everything that needs to be updated
widgets = yield call(
executeWidgetBlueprintOperations,
blueprint.operations,
widgets,
widget.widgetId,
);
}
// Add the parentId prop to this widget
widget.parentId = parent.widgetId;
// Remove the blueprint from the widget (if any)
// as blueprints are not useful beyond this point.
delete widget.blueprint;
// deleting propertyPaneEnchancements too as it shouldn't go in dsl because
// function can't be cloned into dsl
// instead of passing whole enhancments function in widget props, we are just setting
// enhancments as true so that we know this widget contains enhancments
if ("enhancements" in widget) {
widget.enhancements = true;
}
return { widgetId: widget.widgetId, widgets };
}
/**
* this saga is called when we drop a widget on the canvas.
*
* @param addChildAction
*/
export function* addChildSaga(addChildAction: ReduxAction<WidgetAddChild>) {
try {
const start = performance.now();
Toaster.clear();
// NOTE: widgetId here is the parentId of the dropped widget ( we should rename it to avoid confusion )
const { widgetId } = addChildAction.payload;
// Get the current parent widget whose child will be the new widget.
const stateParent: FlattenedWidgetProps = yield select(getWidget, widgetId);
// const parent = Object.assign({}, stateParent);
// Get all the widgets from the canvasWidgetsReducer
const stateWidgets = yield select(getWidgets);
let widgets = Object.assign({}, stateWidgets);
// Generate the full WidgetProps of the widget to be added.
const childWidgetPayload: GeneratedWidgetPayload = yield generateChildWidgets(
stateParent,
addChildAction.payload,
widgets,
);
// Update widgets to put back in the canvasWidgetsReducer
// TODO(abhinav): This won't work if dont already have an empty children: []
const parent = {
...stateParent,
children: [...(stateParent.children || []), childWidgetPayload.widgetId],
};
widgets[parent.widgetId] = parent;
AppsmithConsole.info({
text: "Widget was created",
source: {
type: ENTITY_TYPE.WIDGET,
id: childWidgetPayload.widgetId,
name:
childWidgetPayload.widgets[childWidgetPayload.widgetId].widgetName,
},
});
log.debug("add child computations took", performance.now() - start, "ms");
// 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 travesring, if we find any widget which has CHILD_OPERATION, we will call the fn in it
const updatedWidgets: {
[widgetId: string]: FlattenedWidgetProps;
} = yield call(
traverseTreeAndExecuteBlueprintChildOperations,
parent,
addChildAction.payload.newWidgetId,
widgets,
);
widgets = updatedWidgets;
yield put({
type: ReduxActionTypes.WIDGET_CHILD_ADDED,
payload: {
widgetId: childWidgetPayload.widgetId,
type: addChildAction.payload.type,
},
});
yield put(updateAndSaveLayout(widgets));
// go up till MAIN_CONTAINER, if there is a operation CHILD_OPERATIONS IN ANY PARENT,
// call execute
} catch (error) {
yield put({
type: ReduxActionErrorTypes.WIDGET_OPERATION_ERROR,
payload: {
action: ReduxActionTypes.WIDGET_ADD_CHILD,
error,
},
});
}
}
// This is different from addChildSaga
// It does not go through the blueprint based creation
// It simply uses the provided widget props to create widgets
// Use this only when we're 100% sure of all the props the children will need
export function* addChildrenSaga(
addChildrenAction: ReduxAction<WidgetAddChildren>,
) {
try {
const { children, widgetId } = addChildrenAction.payload;
const stateWidgets = yield select(getWidgets);
const widgets = { ...stateWidgets };
const widgetNames = Object.keys(widgets).map((w) => widgets[w].widgetName);
const entityNames = yield call(getEntityNames);
children.forEach((child) => {
// Create only if it doesn't already exist
if (!widgets[child.widgetId]) {
const defaultConfig: any = WidgetConfigResponse.config[child.type];
const newWidgetName = getNextEntityName(defaultConfig.widgetName, [
...widgetNames,
...entityNames,
]);
// update the list of widget names for the next iteration
widgetNames.push(newWidgetName);
widgets[child.widgetId] = {
...child,
widgetName: newWidgetName,
renderMode: RenderModes.CANVAS,
};
const existingChildren = widgets[widgetId].children || [];
widgets[widgetId] = {
...widgets[widgetId],
children: [...existingChildren, child.widgetId],
};
}
});
yield put(updateAndSaveLayout(widgets));
} catch (error) {
yield put({
type: ReduxActionErrorTypes.WIDGET_OPERATION_ERROR,
payload: {
action: ReduxActionTypes.WIDGET_ADD_CHILDREN,
error,
},
});
}
}
const getAllWidgetsInTree = (
widgetId: string,
canvasWidgets: CanvasWidgetsReduxState,
) => {
const widget = canvasWidgets[widgetId];
const widgetList = [widget];
if (widget && widget.children) {
widget.children
.filter(Boolean)
.forEach((childWidgetId: string) =>
widgetList.push(...getAllWidgetsInTree(childWidgetId, canvasWidgets)),
);
}
return widgetList;
};
/**
* Note: Mutates finalWidgets[parentId].bottomRow for CANVAS_WIDGET
* @param finalWidgets
* @param parentId
*/
const resizeCanvasToLowestWidget = (
finalWidgets: CanvasWidgetsReduxState,
parentId: string,
) => {
if (
!finalWidgets[parentId] ||
finalWidgets[parentId].type !== WidgetTypes.CANVAS_WIDGET
) {
return;
}
let lowestBottomRow = Math.ceil(
(finalWidgets[parentId].minHeight || 0) /
GridDefaults.DEFAULT_GRID_ROW_HEIGHT,
);
const childIds = finalWidgets[parentId].children || [];
// find lowest row
childIds.forEach((cId) => {
const child = finalWidgets[cId];
if (child.bottomRow > lowestBottomRow) {
lowestBottomRow = child.bottomRow;
}
});
finalWidgets[parentId].bottomRow =
(lowestBottomRow + GridDefaults.CANVAS_EXTENSION_OFFSET) *
GridDefaults.DEFAULT_GRID_ROW_HEIGHT;
};
export function* deleteAllSelectedWidgetsSaga(
deleteAction: ReduxAction<MultipleWidgetDeletePayload>,
) {
try {
const { disallowUndo = false, isShortcut } = deleteAction.payload;
const stateWidgets = yield select(getWidgets);
const widgets = { ...stateWidgets };
const selectedWidgets: string[] = yield select(getSelectedWidgets);
if (!(selectedWidgets && selectedWidgets.length !== 1)) return;
const widgetsToBeDeleted = yield all(
selectedWidgets.map((eachId) => {
return call(getAllWidgetsInTree, eachId, widgets);
}),
);
const falttendedWidgets: any = flattenDeep(widgetsToBeDeleted);
const parentUpdatedWidgets = falttendedWidgets.reduce(
(allWidgets: any, eachWidget: any) => {
const { parentId, widgetId } = eachWidget;
const stateParent: FlattenedWidgetProps = allWidgets[parentId];
let parent = { ...stateParent };
if (parent.children) {
parent = {
...parent,
children: parent.children.filter((c) => c !== widgetId),
};
allWidgets[parentId] = parent;
}
return allWidgets;
},
widgets,
);
const finalWidgets: CanvasWidgetsReduxState = _.omit(
parentUpdatedWidgets,
falttendedWidgets.map((widgets: any) => widgets.widgetId),
);
// assuming only widgets with same parent can be selected
const parentId = widgets[selectedWidgets[0]].parentId;
resizeCanvasToLowestWidget(finalWidgets, parentId);
yield put(updateAndSaveLayout(finalWidgets));
yield put(selectWidgetInitAction(""));
const bulkDeleteKey = selectedWidgets.join(",");
const saveStatus: boolean = yield saveDeletedWidgets(
falttendedWidgets,
bulkDeleteKey,
);
if (saveStatus && !disallowUndo) {
// close property pane after delete
yield put(closePropertyPane());
yield put(closeTableFilterPane());
Toaster.show({
text: createMessage(WIDGET_BULK_DELETE, `${selectedWidgets.length}`),
hideProgressBar: false,
variant: Variant.success,
dispatchableAction: {
type: ReduxActionTypes.UNDO_DELETE_WIDGET,
payload: {
widgetId: bulkDeleteKey,
},
},
});
setTimeout(() => {
if (bulkDeleteKey) {
flushDeletedWidgets(bulkDeleteKey);
falttendedWidgets.map((widget: any) => {
AppsmithConsole.info({
logType: LOG_TYPE.ENTITY_DELETED,
text: "Widget was deleted",
source: {
name: widget.widgetName,
type: ENTITY_TYPE.WIDGET,
id: widget.widgetId,
},
});
});
}
}, WIDGET_DELETE_UNDO_TIMEOUT);
}
} catch (error) {
yield put({
type: ReduxActionErrorTypes.WIDGET_OPERATION_ERROR,
payload: {
action: ReduxActionTypes.WIDGET_DELETE,
error,
},
});
}
}
export function* deleteSagaInit(deleteAction: ReduxAction<WidgetDelete>) {
const { widgetId } = deleteAction.payload;
const selectedWidget = yield select(getSelectedWidget);
const selectedWidgets: string[] = yield select(getSelectedWidgets);
if (selectedWidgets.length > 1) {
yield put({
type: ReduxActionTypes.WIDGET_BULK_DELETE,
payload: deleteAction.payload,
});
}
if (!!widgetId || !!selectedWidget) {
yield put({
type: ReduxActionTypes.WIDGET_SINGLE_DELETE,
payload: deleteAction.payload,
});
}
}
export function* deleteSaga(deleteAction: ReduxAction<WidgetDelete>) {
try {
let { parentId, widgetId } = deleteAction.payload;
const { disallowUndo, isShortcut } = deleteAction.payload;
if (!widgetId) {
const selectedWidget: FlattenedWidgetProps | undefined = yield select(
getSelectedWidget,
);
if (!selectedWidget) return;
// if widget is not deletable, don't don anything
if (selectedWidget.isDeletable === false) return false;
widgetId = selectedWidget.widgetId;
parentId = selectedWidget.parentId;
}
if (widgetId && parentId) {
const stateWidgets = yield select(getWidgets);
const widgets = { ...stateWidgets };
const stateWidget: WidgetProps = yield select(getWidget, widgetId);
const widget = { ...stateWidget };
const stateParent: FlattenedWidgetProps = yield select(
getWidget,
parentId,
);
let parent = { ...stateParent };
const analyticsEvent = isShortcut
? "WIDGET_DELETE_VIA_SHORTCUT"
: "WIDGET_DELETE";
AnalyticsUtil.logEvent(analyticsEvent, {
widgetName: widget.widgetName,
widgetType: widget.type,
});
// Remove entry from parent's children
if (parent.children) {
parent = {
...parent,
children: parent.children.filter((c) => c !== widgetId),
};
}
widgets[parentId] = parent;
const otherWidgetsToDelete = getAllWidgetsInTree(widgetId, widgets);
const saveStatus: boolean = yield saveDeletedWidgets(
otherWidgetsToDelete,
widgetId,
);
let widgetName = widget.widgetName;
// SPECIAL HANDLING FOR TABS IN A TABS WIDGET
if (parent.type === WidgetTypes.TABS_WIDGET && widget.tabName) {
widgetName = widget.tabName;
}
if (saveStatus && !disallowUndo) {
// close property pane after delete
yield put(closePropertyPane());
Toaster.show({
text: createMessage(WIDGET_DELETE, widgetName),
hideProgressBar: false,
variant: Variant.success,
dispatchableAction: {
type: ReduxActionTypes.UNDO_DELETE_WIDGET,
payload: {
widgetId,
},
},
});
setTimeout(() => {
if (widgetId) {
flushDeletedWidgets(widgetId);
otherWidgetsToDelete.map((widget) => {
AppsmithConsole.info({
logType: LOG_TYPE.ENTITY_DELETED,
text: "Widget was deleted",
source: {
name: widget.widgetName,
type: ENTITY_TYPE.WIDGET,
id: widget.widgetId,
},
});
});
}
}, WIDGET_DELETE_UNDO_TIMEOUT);
}
yield call(clearEvalPropertyCacheOfWidget, widgetName);
let finalWidgets: CanvasWidgetsReduxState = yield call(
updateListWidgetPropertiesOnChildDelete,
widgets,
widgetId,
widgetName,
);
finalWidgets = _.omit(
finalWidgets,
otherWidgetsToDelete.map((widgets) => widgets.widgetId),
);
// Note: mutates finalWidgets
resizeCanvasToLowestWidget(finalWidgets, parentId);
yield put(updateAndSaveLayout(finalWidgets));
}
} catch (error) {
yield put({
type: ReduxActionErrorTypes.WIDGET_OPERATION_ERROR,
payload: {
action: ReduxActionTypes.WIDGET_DELETE,
error,
},
});
}
}
/**
* this saga clears out the enhancementMap, template and dynamicBindingPathList when a child
* is deleted in list widget
*
* @param widgets
* @param widgetId
* @param widgetName
* @param parentId
*/
export function* updateListWidgetPropertiesOnChildDelete(
widgets: CanvasWidgetsReduxState,
widgetId: string,
widgetName: string,
) {
const clone = JSON.parse(JSON.stringify(widgets));
const parentWithEnhancementFn = getParentWithEnhancementFn(widgetId, clone);
if (parentWithEnhancementFn?.type === "LIST_WIDGET") {
const listWidget = parentWithEnhancementFn;
// delete widget in template of list
if (listWidget && widgetName in listWidget.template) {
listWidget.template[widgetName] = undefined;
}
// delete dynamic binding path if any
remove(listWidget?.dynamicBindingPathList || [], (path: any) =>
path.key.startsWith(`template.${widgetName}`),
);
return clone;
}
return clone;
}
export function* undoDeleteSaga(action: ReduxAction<{ widgetId: string }>) {
// Get the list of widget and its children which were deleted
const deletedWidgets: FlattenedWidgetProps[] = yield getDeletedWidgets(
action.payload.widgetId,
);
const deletedWidgetIds = action.payload.widgetId.split(",");
if (deletedWidgets && Array.isArray(deletedWidgets)) {
// Get the current list of widgets from reducer
const formTree = deletedWidgets.reduce((widgetTree, each) => {
widgetTree[each.widgetId] = each;
return widgetTree;
}, {} as CanvasWidgetsReduxState);
const stateWidgets = yield select(getWidgets);
const deletedWidgetGroups = deletedWidgetIds.map((each) => ({
widget: formTree[each],
widgetsToRestore: getAllWidgetsInTree(each, formTree),
}));
const finalWidgets = deletedWidgetGroups.reduce(
(reducedWidgets, deletedWidgetGroup) => {
const {
widget: deletedWidget,
widgetsToRestore: deletedWidgets,
} = deletedWidgetGroup;
let widgets = cloneDeep(reducedWidgets);
// If the deleted widget is in fact available.
if (deletedWidget) {
// Log an undo event
AnalyticsUtil.logEvent("WIDGET_DELETE_UNDO", {
widgetName: deletedWidget.widgetName,
widgetType: deletedWidget.type,
});
}
// For each deleted widget
deletedWidgets.forEach((widget: FlattenedWidgetProps) => {
// Add it to the widgets list we fetched from reducer
widgets[widget.widgetId] = widget;
// If the widget in question is the deleted widget
if (deletedWidgetIds.includes(widget.widgetId)) {
//SPECIAL HANDLING FOR TAB IN A TABS WIDGET
if (
widget.tabId &&
widget.type === WidgetTypes.CANVAS_WIDGET &&
widget.parentId
) {
const parent = cloneDeep(widgets[widget.parentId]);
if (parent.tabsObj) {
try {
const tabs = Object.values(parent.tabsObj);
parent.tabsObj[widget.tabId] = {
id: widget.tabId,
widgetId: widget.widgetId,
label: widget.tabName || widget.widgetName,
isVisible: true,
};
widgets = {
...widgets,
[widget.parentId]: {
...widgets[widget.parentId],
tabsObj: parent.tabsObj,
},
};
} catch (error) {
log.debug("Error deleting tabs widget: ", { error });
}
} else {
parent.tabs = JSON.stringify([
{
id: widget.tabId,
widgetId: widget.widgetId,
label: widget.tabName || widget.widgetName,
},
]);
widgets = {
...widgets,
[widget.parentId]: parent,
};
}
}
let newChildren = [widget.widgetId];
if (widget.parentId && widgets[widget.parentId].children) {
// Concatenate the list of parents children with the current widgetId
newChildren = newChildren.concat(
widgets[widget.parentId].children,
);
}
if (widget.parentId) {
widgets = {
...widgets,
[widget.parentId]: {
...widgets[widget.parentId],
children: newChildren,
},
};
}
}
});
return widgets;
},
stateWidgets,
);
const parentId = deletedWidgets[0].parentId;
if (parentId) {
resizeCanvasToLowestWidget(finalWidgets, parentId);
}
yield put(updateAndSaveLayout(finalWidgets));
deletedWidgetIds.forEach((widgetId) => {
setTimeout(() => flashElementById(widgetId), 100);
});
yield put(selectMultipleWidgetsInitAction(deletedWidgetIds));
if (deletedWidgetIds.length === 1) {
yield put(forceOpenPropertyPane(action.payload.widgetId));
}
yield flushDeletedWidgets(action.payload.widgetId);
}
}
export function* moveSaga(moveAction: ReduxAction<WidgetMove>) {
try {
Toaster.clear();
const start = performance.now();
const {
leftColumn,
newParentId,
parentId,
topRow,
widgetId,
} = moveAction.payload;
const stateWidget: FlattenedWidgetProps = yield select(getWidget, widgetId);
let widget = Object.assign({}, stateWidget);
// Get all widgets from DSL/Redux Store
const stateWidgets: CanvasWidgetsReduxState = yield select(getWidgets);
const widgets = Object.assign({}, stateWidgets);
// Get parent from DSL/Redux Store
const stateParent: FlattenedWidgetProps = yield select(getWidget, parentId);
const parent = {
...stateParent,
children: [...(stateParent.children || [])],
};
// Update position of widget
const updatedPosition = updateWidgetPosition(widget, leftColumn, topRow);
widget = { ...widget, ...updatedPosition };
// Replace widget with update widget props
widgets[widgetId] = widget;
// If the parent has changed i.e parentWidgetId is not parent.widgetId
if (parent.widgetId !== newParentId && widgetId !== newParentId) {
// Remove from the previous parent
if (parent.children && Array.isArray(parent.children)) {
const indexOfChild = parent.children.indexOf(widgetId);
if (indexOfChild > -1) delete parent.children[indexOfChild];
parent.children = parent.children.filter(Boolean);
}
// Add to new parent
widgets[parent.widgetId] = parent;
const newParent = {
...widgets[newParentId],
children: widgets[newParentId].children
? [...(widgets[newParentId].children || []), widgetId]
: [widgetId],
};
widgets[widgetId].parentId = newParentId;
widgets[newParentId] = newParent;
}
log.debug("move computations took", performance.now() - start, "ms");
yield put(updateAndSaveLayout(widgets));
} catch (error) {
yield put({
type: ReduxActionErrorTypes.WIDGET_OPERATION_ERROR,
payload: {
action: ReduxActionTypes.WIDGET_MOVE,
error,
},
});
}
}
export function* resizeSaga(resizeAction: ReduxAction<WidgetResize>) {
try {
Toaster.clear();
const start = performance.now();
const {
bottomRow,
leftColumn,
rightColumn,
topRow,
widgetId,
} = resizeAction.payload;
const stateWidget: FlattenedWidgetProps = yield select(getWidget, widgetId);
let widget = { ...stateWidget };
const stateWidgets = yield select(getWidgets);
const widgets = { ...stateWidgets };
widget = { ...widget, leftColumn, rightColumn, topRow, bottomRow };
widgets[widgetId] = widget;
log.debug("resize computations took", performance.now() - start, "ms");
yield put(updateAndSaveLayout(widgets));
} catch (error) {
yield put({
type: ReduxActionErrorTypes.WIDGET_OPERATION_ERROR,
payload: {
action: ReduxActionTypes.WIDGET_RESIZE,
error,
},
});
}
}
enum DynamicPathUpdateEffectEnum {
ADD = "ADD",
REMOVE = "REMOVE",
NOOP = "NOOP",
}
type DynamicPathUpdate = {
propertyPath: string;
effect: DynamicPathUpdateEffectEnum;
};
function getDynamicTriggerPathListUpdate(
widget: WidgetProps,
propertyPath: string,
propertyValue: string,
): DynamicPathUpdate {
if (propertyValue && !isPathADynamicTrigger(widget, propertyPath)) {
return {
propertyPath,
effect: DynamicPathUpdateEffectEnum.ADD,
};
} else if (!propertyValue && !isPathADynamicTrigger(widget, propertyPath)) {
return {
propertyPath,
effect: DynamicPathUpdateEffectEnum.REMOVE,
};
}
return {
propertyPath,
effect: DynamicPathUpdateEffectEnum.NOOP,
};
}
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(abhinav): This is not appropriate from the platform's archtecture's point of view.
// Figure out a holistic solutions where we donot have to stringify above.
if (propertyPath === "primaryColumns" || propertyPath === "derivedColumns") {
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;
}
const isPropertyATriggerPath = (
widget: WidgetProps,
propertyPath: string,
): boolean => {
const widgetConfig = WidgetFactory.getWidgetPropertyPaneConfig(widget.type);
const { triggerPaths } = getAllPathsFromPropertyConfig(
widget,
widgetConfig,
{},
);
return propertyPath in triggerPaths;
};
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 }),
);
}
function* setWidgetDynamicPropertySaga(
action: ReduxAction<SetWidgetDynamicPropertyPayload>,
) {
const { isDynamic, propertyPath, widgetId } = action.payload;
const stateWidget: WidgetProps = yield select(getWidget, widgetId);
let widget = cloneDeep({ ...stateWidget });
const propertyValue = _.get(widget, propertyPath);
let dynamicPropertyPathList = getWidgetDynamicPropertyPathList(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,
});
const { parsed } = yield call(
validateProperty,
propertyPath,
propertyValue,
widget,
);
widget = set(widget, propertyPath, parsed);
}
widget.dynamicPropertyPathList = dynamicPropertyPathList;
const stateWidgets = yield select(getWidgets);
const widgets = { ...stateWidgets, [widgetId]: widget };
// Save the layout
yield put(updateAndSaveLayout(widgets));
}
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[] = [];
Object.keys(updatePaths).forEach((propertyPath) => {
const propertyValue = _.get(updates, propertyPath);
// only check if
if (!_.isString(propertyValue)) {
return;
}
// Check if the path is a of a dynamic trigger property
let isTriggerProperty = isPropertyATriggerPath(
widgetWithUpdates,
propertyPath,
);
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,
};
}
function* batchUpdateWidgetPropertySaga(
action: ReduxAction<UpdateWidgetPropertyPayload>,
) {
const start = performance.now();
const { updates, widgetId } = action.payload;
if (!widgetId) {
// Handling the case where sometimes widget id is not passed through here
return;
}
const { modify = {}, 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;
}
} catch (e) {
log.debug("Error updating property paths: ", { e });
}
if (Array.isArray(remove) && remove.length > 0) {
widget = yield removeWidgetProperties(widget, remove);
}
const stateWidgets = yield select(getWidgets);
const widgets = { ...stateWidgets, [widgetId]: widget };
log.debug(
"Batch widget property update calculations took: ",
performance.now() - start,
"ms",
);
// Save the layout
yield put(updateAndSaveLayout(widgets));
}
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 parentWidgetId = action.payload.widgetId;
const canvasWidgets: CanvasWidgetsReduxState = yield select(getWidgets);
const childrenIds: string[] = getWidgetChildren(
canvasWidgets,
parentWidgetId,
);
for (const childIndex in childrenIds) {
const childId = childrenIds[childIndex];
yield put(resetWidgetMetaProperty(childId));
}
}
function* updateCanvasSize(
action: ReduxAction<{ canvasWidgetId: string; snapRows: number }>,
) {
const { canvasWidgetId, snapRows } = action.payload;
const canvasWidget = 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(
batchUpdateWidgetProperty(canvasWidgetId, {
modify: { bottomRow: newBottomRow },
}),
);
}
}
function* createWidgetCopy(widget: FlattenedWidgetProps) {
const allWidgets: { [widgetId: string]: FlattenedWidgetProps } = yield select(
getWidgets,
);
const widgetsToStore = getAllWidgetsInTree(widget.widgetId, allWidgets);
return {
widgetId: widget.widgetId,
list: widgetsToStore,
parentId: widget.parentId,
};
}
function* createSelectedWidgetsCopy(selectedWidgets: FlattenedWidgetProps[]) {
if (!selectedWidgets || !selectedWidgets.length) return;
const widgetListsToStore: {
widgetId: string;
parentId: string;
list: FlattenedWidgetProps[];
}[] = yield all(selectedWidgets.map((each) => call(createWidgetCopy, each)));
return yield saveCopiedWidgets(JSON.stringify(widgetListsToStore));
}
/**
* 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) {
Toaster.show({
text: createMessage(ERROR_WIDGET_COPY_NO_WIDGET_SELECTED),
variant: Variant.info,
});
return;
}
const allAllowedToCopy = selectedWidgets.some((each) => {
return !allWidgets[each].disallowCopy;
});
if (!allAllowedToCopy) {
Toaster.show({
text: createMessage(ERROR_WIDGET_COPY_NOT_ALLOWED),
variant: Variant.info,
});
return;
}
const selectedWidgetProps = selectedWidgets.map((each) => allWidgets[each]);
const saveResult = yield createSelectedWidgetsCopy(selectedWidgetProps);
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) {
Toaster.show({
text: createMessage(
WIDGET_COPY,
selectedWidgetProps.length > 1
? `${selectedWidgetProps.length} Widgets`
: selectedWidgetProps[0].widgetName,
),
variant: Variant.success,
});
}
}
export function calculateNewWidgetPosition(
widget: WidgetProps,
parentId: string,
canvasWidgets: { [widgetId: string]: FlattenedWidgetProps },
parentBottomRow?: number,
persistColumnPosition = false,
) {
// Note: This is a very simple algorithm.
// 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.
const nextAvailableRow = parentBottomRow
? parentBottomRow
: nextAvailableRowInContainer(parentId, canvasWidgets);
return {
leftColumn: persistColumnPosition ? widget.leftColumn : 0,
rightColumn: persistColumnPosition
? widget.rightColumn
: widget.rightColumn - widget.leftColumn,
topRow: parentBottomRow
? nextAvailableRow + widget.topRow
: nextAvailableRow,
bottomRow: parentBottomRow
? nextAvailableRow + widget.bottomRow
: nextAvailableRow + (widget.bottomRow - widget.topRow),
};
}
function* getEntityNames() {
const evalTree = yield select(getDataTree);
return Object.keys(evalTree);
}
function getNextWidgetName(
widgets: CanvasWidgetsReduxState,
type: WidgetType,
evalTree: {
bottomRow: any;
leftColumn: any;
rightColumn: any;
topRow: any;
},
options?: Record<string, unknown>,
) {
// Compute the new widget's name
const defaultConfig: any = WidgetConfigResponse.config[type];
const widgetNames = Object.keys(widgets).map((w) => widgets[w].widgetName);
const entityNames = Object.keys(evalTree);
let prefix = defaultConfig.widgetName;
if (options && options.prefix) {
prefix = `${options.prefix}${
widgetNames.indexOf(options.prefix as string) > -1 ? "Copy" : ""
}`;
}
return getNextEntityName(
prefix,
[...widgetNames, ...entityNames],
options?.startWithoutIndex as boolean,
);
}
/**
* this saga create a new widget from the copied one to store
*/
function* pasteWidgetSaga() {
const copiedWidgetGroups: {
widgetId: string;
parentId: string;
list: WidgetProps[];
}[] = yield getCopiedWidgets();
if (!Array.isArray(copiedWidgetGroups)) {
return;
// to avoid invoking old copied widgets
}
const stateWidgets: CanvasWidgetsReduxState = yield select(getWidgets);
let selectedWidget: FlattenedWidgetProps | undefined = yield select(
getSelectedWidget,
);
const focusedWidget: FlattenedWidgetProps | undefined = yield select(
getFocusedWidget,
);
selectedWidget = yield checkIfPastingIntoListWidget(
stateWidgets,
selectedWidget || focusedWidget,
copiedWidgetGroups,
);
selectedWidget = yield checkIfPastingIntoListWidget(
stateWidgets,
selectedWidget,
copiedWidgetGroups,
);
const pastingIntoWidgetId: string = yield getParentWidgetIdForPasting(
{ ...stateWidgets },
selectedWidget,
);
let widgets = { ...stateWidgets };
const newlyCreatedWidgetIds: string[] = [];
const sortedWidgetList = copiedWidgetGroups.sort(
(a, b) => a.list[0].topRow - b.list[0].topRow,
);
const copiedGroupTopRow = sortedWidgetList[0].list[0].topRow;
const nextAvailableRow: number = nextAvailableRowInContainer(
pastingIntoWidgetId,
widgets,
);
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.find(
(widget) => widget.widgetId === copiedWidgetId,
);
if (unUpdatedCopyOfWidget) {
const copiedWidget = {
...unUpdatedCopyOfWidget,
topRow: unUpdatedCopyOfWidget.topRow - copiedGroupTopRow,
bottomRow: unUpdatedCopyOfWidget.bottomRow - copiedGroupTopRow,
};
// Log the paste event
AnalyticsUtil.logEvent("WIDGET_PASTE", {
widgetName: copiedWidget.widgetName,
widgetType: copiedWidget.type,
});
// Compute the new widget's positional properties
const {
bottomRow,
leftColumn,
rightColumn,
topRow,
} = yield calculateNewWidgetPosition(
copiedWidget,
pastingIntoWidgetId,
widgets,
nextAvailableRow,
true,
);
// goToNextAvailableRow = true,
// persistColumnPosition = false,
const evalTree = yield select(getDataTree);
// Get a flat list of all the widgets to be updated
const widgetList = copiedWidgets.list;
const widgetIdMap: Record<string, string> = {};
const widgetNameMap: Record<string, string> = {};
const newWidgetList: FlattenedWidgetProps[] = [];
let newWidgetId: string = copiedWidget.widgetId;
// 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;
// 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;
// Generate a new unique widget name
const 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 === WidgetTypes.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 === WidgetTypes.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);
}
}
// If it is the copied widget, update position properties
if (widget.widgetId === widgetIdMap[copiedWidget.widgetId]) {
newWidgetId = widget.widgetId;
widget.leftColumn = leftColumn;
widget.topRow = topRow;
widget.bottomRow = bottomRow;
widget.rightColumn = rightColumn;
widget.parentId = pastingIntoWidgetId;
// 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[pastingIntoWidgetId].children;
if (widgetChildren && Array.isArray(widgetChildren)) {
// Add the new child to existing children
parentChildren = parentChildren.concat(widgetChildren);
}
const updateBottomRow =
widget.bottomRow * widget.parentRowSpace >
widgets[pastingIntoWidgetId].bottomRow;
widgets = {
...widgets,
[pastingIntoWidgetId]: {
...widgets[pastingIntoWidgetId],
...(updateBottomRow
? {
bottomRow: widget.bottomRow * widget.parentRowSpace,
}
: {}),
children: parentChildren,
},
};
// If the copied widget's boundaries exceed the parent's
// Make the parent scrollable
if (
widgets[pastingIntoWidgetId].bottomRow *
widgets[widget.parentId].parentRowSpace <=
widget.bottomRow * widget.parentRowSpace
) {
const parentOfPastingWidget =
widgets[pastingIntoWidgetId].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;
}
widget.widgetName = newWidgetName;
widgetNameMap[oldWidgetName] = widget.widgetName;
// Add the new widget to the canvas widgets
widgets[widget.widgetId] = widget;
}
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,
);
}
}
}),
),
);
// save the new DSL
yield put(updateAndSaveLayout(widgets));
newlyCreatedWidgetIds.forEach((newWidgetId) => {
setTimeout(() => flashElementById(newWidgetId), 100);
});
// hydrating enhancements map after save layout so that enhancement map
// for newly copied widget is hydrated
yield put(selectMultipleWidgetsInitAction(newlyCreatedWidgetIds));
}
function* cutWidgetSaga() {
const allWidgets: { [widgetId: string]: FlattenedWidgetProps } = yield select(
getWidgets,
);
const selectedWidgets: string[] = yield select(getSelectedWidgets);
if (!selectedWidgets) {
Toaster.show({
text: createMessage(ERROR_WIDGET_CUT_NO_WIDGET_SELECTED),
variant: Variant.info,
});
return;
}
const selectedWidgetProps = selectedWidgets.map((each) => allWidgets[each]);
const saveResult = yield createSelectedWidgetsCopy(selectedWidgetProps);
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) {
Toaster.show({
text: createMessage(
WIDGET_CUT,
selectedWidgetProps.length > 1
? `${selectedWidgetProps.length} Widgets`
: selectedWidgetProps[0].widgetName,
),
variant: Variant.success,
});
}
yield put({
type: ReduxActionTypes.WIDGET_DELETE,
payload: {
disallowUndo: true,
isShortcut: true,
},
});
}
function* addTableWidgetFromQuerySaga(action: ReduxAction<string>) {
try {
const columns = 8 * GRID_DENSITY_MIGRATION_V1;
const rows = 7 * GRID_DENSITY_MIGRATION_V1;
const queryName = action.payload;
const widgets = yield select(getWidgets);
const evalTree = yield select(getDataTree);
const widgetName = getNextWidgetName(widgets, "TABLE_WIDGET", evalTree);
let newWidget = {
type: WidgetTypes.TABLE_WIDGET,
newWidgetId: generateReactKey(),
widgetId: "0",
topRow: 0,
bottomRow: rows,
leftColumn: 0,
rightColumn: columns,
columns,
rows,
parentId: MAIN_CONTAINER_WIDGET_ID,
widgetName,
renderMode: RenderModes.CANVAS,
parentRowSpace: GridDefaults.DEFAULT_GRID_ROW_HEIGHT,
parentColumnSpace: 1,
isLoading: false,
version: 1,
props: {
tableData: `{{${queryName}.data}}`,
dynamicBindingPathList: [{ key: "tableData" }],
},
};
const {
bottomRow,
leftColumn,
rightColumn,
topRow,
} = yield calculateNewWidgetPosition(
newWidget,
MAIN_CONTAINER_WIDGET_ID,
widgets,
);
newWidget = {
...newWidget,
leftColumn,
topRow,
rightColumn,
bottomRow,
};
yield put({
type: ReduxActionTypes.WIDGET_ADD_CHILD,
payload: newWidget,
});
const applicationId = yield select(getCurrentApplicationId);
const pageId = yield select(getCurrentPageId);
navigateToCanvas(
{
applicationId,
pageId,
},
window.location.pathname,
pageId,
newWidget.newWidgetId,
);
yield put({
type: ReduxActionTypes.SELECT_WIDGET_INIT,
payload: { widgetId: newWidget.newWidgetId },
});
yield put(forceOpenPropertyPane(newWidget.newWidgetId));
} catch (error) {
Toaster.show({
text: createMessage(ERROR_ADD_WIDGET_FROM_QUERY),
variant: Variant.danger,
});
}
}
export default function* widgetOperationSagas() {
yield fork(widgetSelectionSagas);
yield all([
takeEvery(
ReduxActionTypes.ADD_TABLE_WIDGET_FROM_QUERY,
addTableWidgetFromQuerySaga,
),
takeEvery(ReduxActionTypes.WIDGET_ADD_CHILD, addChildSaga),
takeEvery(ReduxActionTypes.WIDGET_DELETE, deleteSagaInit),
takeEvery(ReduxActionTypes.WIDGET_SINGLE_DELETE, deleteSaga),
takeEvery(
ReduxActionTypes.WIDGET_BULK_DELETE,
deleteAllSelectedWidgetsSaga,
),
takeLatest(ReduxActionTypes.WIDGET_MOVE, moveSaga),
takeLatest(ReduxActionTypes.WIDGET_RESIZE, resizeSaga),
takeEvery(
ReduxActionTypes.UPDATE_WIDGET_PROPERTY_REQUEST,
updateWidgetPropertySaga,
),
takeEvery(
ReduxActionTypes.WIDGET_UPDATE_PROPERTY,
updateWidgetPropertySaga,
),
takeEvery(
ReduxActionTypes.SET_WIDGET_DYNAMIC_PROPERTY,
setWidgetDynamicPropertySaga,
),
takeEvery(
ReduxActionTypes.RESET_CHILDREN_WIDGET_META,
resetChildrenMetaSaga,
),
takeEvery(
ReduxActionTypes.BATCH_UPDATE_WIDGET_PROPERTY,
batchUpdateWidgetPropertySaga,
),
takeEvery(
ReduxActionTypes.DELETE_WIDGET_PROPERTY,
deleteWidgetPropertySaga,
),
takeLatest(ReduxActionTypes.UPDATE_CANVAS_SIZE, updateCanvasSize),
takeLatest(ReduxActionTypes.COPY_SELECTED_WIDGET_INIT, copyWidgetSaga),
takeEvery(ReduxActionTypes.PASTE_COPIED_WIDGET_INIT, pasteWidgetSaga),
takeEvery(ReduxActionTypes.UNDO_DELETE_WIDGET, undoDeleteSaga),
takeEvery(ReduxActionTypes.CUT_SELECTED_WIDGET, cutWidgetSaga),
takeEvery(ReduxActionTypes.WIDGET_ADD_CHILDREN, addChildrenSaga),
]);
}