import { getFocusedWidget, getSelectedWidget, getWidgetMetaProps, getWidgets, } from "./selectors"; import _, { isString } from "lodash"; import { GridDefaults, MAIN_CONTAINER_WIDGET_ID, RenderModes, WidgetType, WidgetTypes, } from "constants/WidgetConstants"; import { all, call } from "redux-saga/effects"; import { DataTree } from "entities/DataTree/dataTreeFactory"; import { select } from "redux-saga/effects"; import { getCopiedWidgets } from "utils/storage"; import { WidgetProps } from "widgets/BaseWidget"; import { getSelectedWidgets } from "selectors/ui"; import { generateReactKey } from "utils/generators"; import { CanvasWidgetsReduxState, FlattenedWidgetProps, } from "reducers/entityReducers/canvasWidgetsReducer"; import { getDataTree } from "selectors/dataTreeSelectors"; import { getDynamicBindings, combineDynamicBindings, } from "utils/DynamicBindingUtils"; import WidgetConfigResponse from "mockResponses/WidgetConfigResponse"; import { getNextEntityName } from "utils/AppsmithUtils"; export interface CopiedWidgetGroup { widgetId: string; parentId: string; list: WidgetProps[]; } /** * checks if triggerpaths contains property path passed * * @param isTriggerProperty * @param propertyPath * @param triggerPaths * @returns */ export const doesTriggerPathsContainPropertyPath = ( isTriggerProperty: boolean, propertyPath: string, triggerPaths?: string[], ) => { if (!isTriggerProperty) { if ( triggerPaths && triggerPaths.length && triggerPaths.includes(propertyPath) ) { return true; } } return isTriggerProperty; }; /** * * check if copied widget is being pasted in list widget, * if yes, change all keys in template of list widget and * update dynamicBindingPathList of ListWidget * * updates in list widget : * 1. `dynamicBindingPathList` * 2. `template` * * @param widget * @param widgets */ export const handleIfParentIsListWidgetWhilePasting = ( widget: FlattenedWidgetProps, widgets: { [widgetId: string]: FlattenedWidgetProps }, ): { [widgetId: string]: FlattenedWidgetProps } => { let root = _.get(widgets, `${widget.parentId}`); while (root && root.parentId && root.widgetId !== MAIN_CONTAINER_WIDGET_ID) { if (root.type === WidgetTypes.LIST_WIDGET) { const listWidget = root; const currentWidget = _.cloneDeep(widget); let template = _.get(listWidget, "template", {}); const dynamicBindingPathList: any[] = _.get( listWidget, "dynamicBindingPathList", [], ).slice(); // iterating over each keys of the new createdWidget checking if value contains currentItem const keys = Object.keys(currentWidget); for (let i = 0; i < keys.length; i++) { const key = keys[i]; let value = currentWidget[key]; if (isString(value) && value.indexOf("currentItem") > -1) { const { jsSnippets, stringSegments } = getDynamicBindings(value); const js = combineDynamicBindings(jsSnippets, stringSegments); value = `{{${listWidget.widgetName}.listData.map((currentItem) => ${js})}}`; currentWidget[key] = value; dynamicBindingPathList.push({ key: `template.${currentWidget.widgetName}.${key}`, }); } } template = { ...template, [currentWidget.widgetName]: currentWidget, }; // now we have updated `dynamicBindingPathList` and updatedTemplate // we need to update it the list widget widgets[listWidget.widgetId] = { ...listWidget, template, dynamicBindingPathList, }; } root = widgets[root.parentId]; } return widgets; }; /** * this saga handles special cases when pasting the widget * * for e.g - when the list widget is being copied, we want to update template of list widget * with new widgets name * * @param widget * @param widgets * @param widgetNameMap * @param newWidgetList * @returns */ export const handleSpecificCasesWhilePasting = ( widget: FlattenedWidgetProps, widgets: { [widgetId: string]: FlattenedWidgetProps }, widgetNameMap: Record, newWidgetList: FlattenedWidgetProps[], ) => { // this is the case when whole list widget is copied and pasted if (widget.type === WidgetTypes.LIST_WIDGET) { Object.keys(widget.template).map((widgetName) => { const oldWidgetName = widgetName; const newWidgetName = widgetNameMap[oldWidgetName]; const newWidget = newWidgetList.find( (w: any) => w.widgetName === newWidgetName, ); if (newWidget) { newWidget.widgetName = newWidgetName; if (widgetName === oldWidgetName) { widget.template[newWidgetName] = { ...widget.template[oldWidgetName], widgetId: newWidget.widgetId, widgetName: newWidget.widgetName, }; delete widget.template[oldWidgetName]; } } // updating dynamicBindingPath in copied widget if the copied widge thas reference to oldWidgetNames widget.dynamicBindingPathList = (widget.dynamicBindingPathList || []).map( (path: any) => { if (path.key.startsWith(`template.${oldWidgetName}`)) { return { key: path.key.replace( `template.${oldWidgetName}`, `template.${newWidgetName}`, ), }; } return path; }, ); }); widgets[widget.widgetId] = widget; } else if (widget.type === WidgetTypes.MODAL_WIDGET) { // if Modal is being coppied handle all onClose action rename const oldWidgetName = Object.keys(widgetNameMap).find( (key) => widgetNameMap[key] === widget.widgetName, ); // get all the button, icon widgets const copiedBtnIcnWidgets = _.filter( newWidgetList, (copyWidget) => copyWidget.type === "BUTTON_WIDGET" || copyWidget.type === "ICON_WIDGET", ); // replace oldName with new one if any of this widget have onClick action for old modal copiedBtnIcnWidgets.map((copyWidget) => { if (copyWidget.onClick) { const newOnClick = widgets[copyWidget.widgetId].onClick.replace( oldWidgetName, widget.widgetName, ); _.set(widgets[copyWidget.widgetId], "onClick", newOnClick); } }); } widgets = handleIfParentIsListWidgetWhilePasting(widget, widgets); return widgets; }; export function getWidgetChildren( canvasWidgets: CanvasWidgetsReduxState, widgetId: string, ): any { const childrenIds: string[] = []; const widget = _.get(canvasWidgets, widgetId); // When a form widget tries to resetChildrenMetaProperties // But one or more of its container like children // have just been deleted, widget can be undefined if (widget === undefined) { return []; } const { children = [] } = widget; if (children && children.length) { for (const childIndex in children) { if (children.hasOwnProperty(childIndex)) { const child = children[childIndex]; childrenIds.push(child); const grandChildren = getWidgetChildren(canvasWidgets, child); if (grandChildren.length) { childrenIds.push(...grandChildren); } } } } return childrenIds; } export const getParentWidgetIdForPasting = function*( widgets: CanvasWidgetsReduxState, selectedWidget: FlattenedWidgetProps | undefined, ) { let newWidgetParentId = MAIN_CONTAINER_WIDGET_ID; let parentWidget = widgets[MAIN_CONTAINER_WIDGET_ID]; // If the selected widget is not the main container if (selectedWidget && selectedWidget.widgetId !== MAIN_CONTAINER_WIDGET_ID) { // Select the parent of the selected widget if parent is not // the main container if ( selectedWidget && selectedWidget.parentId && selectedWidget.parentId !== MAIN_CONTAINER_WIDGET_ID && widgets[selectedWidget.parentId] ) { const children = widgets[selectedWidget.parentId].children || []; if (children.length > 0) { parentWidget = widgets[selectedWidget.parentId]; newWidgetParentId = selectedWidget.parentId; } } // Select the selected widget if the widget is container like ( excluding list widget ) if ( selectedWidget.children && selectedWidget.type !== WidgetTypes.LIST_WIDGET ) { parentWidget = widgets[selectedWidget.widgetId]; } } // If the parent widget in which to paste the copied widget // is not the main container and is not a canvas widget if ( parentWidget.widgetId !== MAIN_CONTAINER_WIDGET_ID && parentWidget.type !== WidgetTypes.CANVAS_WIDGET ) { let childWidget; // If the widget in which to paste the new widget is NOT // a tabs widget if (parentWidget.type !== WidgetTypes.TABS_WIDGET) { // The child will be a CANVAS_WIDGET, as we've established // this parent widget to be a container like widget // Which always has its first child as a canvas widget childWidget = parentWidget.children && widgets[parentWidget.children[0]]; } else { // If the widget in which to paste the new widget is a tabs widget // Find the currently selected tab canvas widget const { selectedTabWidgetId } = yield select( getWidgetMetaProps, parentWidget.widgetId, ); if (selectedTabWidgetId) childWidget = widgets[selectedTabWidgetId]; } // If the finally selected parent in which to paste the widget // is a CANVAS_WIDGET, use its widgetId as the new widget's parent Id if (childWidget && childWidget.type === WidgetTypes.CANVAS_WIDGET) { newWidgetParentId = childWidget.widgetId; } } return newWidgetParentId; }; export const checkIfPastingIntoListWidget = function( canvasWidgets: CanvasWidgetsReduxState, selectedWidget: FlattenedWidgetProps | undefined, copiedWidgets: { widgetId: string; parentId: string; list: WidgetProps[]; }[], ) { // when list widget is selected, if the user is pasting, we want it to be pasted in the template // which is first children of list widget if ( selectedWidget && selectedWidget.children && selectedWidget?.type === WidgetTypes.LIST_WIDGET ) { const childrenIds: string[] = getWidgetChildren( canvasWidgets, selectedWidget.children[0], ); const firstChildId = childrenIds[0]; // if any copiedWidget is a list widget, we will paste into the parent of list widget for (let i = 0; i < copiedWidgets.length; i++) { const copiedWidgetId = copiedWidgets[i].widgetId; const copiedWidget = canvasWidgets[copiedWidgetId]; if (copiedWidget?.type === WidgetTypes.LIST_WIDGET) { return selectedWidget; } } return _.get(canvasWidgets, firstChildId); } return selectedWidget; }; /** * get top, left, right, bottom most widgets from copied groups when pasting * * @param copiedWidgetGroups * @returns */ export const getBoundaryWidgetsFromCopiedGroups = function( copiedWidgetGroups: CopiedWidgetGroup[], ) { const topMostWidget = copiedWidgetGroups.sort( (a, b) => a.list[0].topRow - b.list[0].topRow, )[0].list[0]; const leftMostWidget = copiedWidgetGroups.sort( (a, b) => a.list[0].leftColumn - b.list[0].leftColumn, )[0].list[0]; const rightMostWidget = copiedWidgetGroups.sort( (a, b) => b.list[0].rightColumn - a.list[0].rightColumn, )[0].list[0]; const bottomMostWidget = copiedWidgetGroups.sort( (a, b) => b.list[0].bottomRow - a.list[0].bottomRow, )[0].list[0]; return { topMostWidget, leftMostWidget, rightMostWidget, bottomMostWidget, }; }; /** * ------------------------------------------------------------------------------- * OPERATION = PASTING * ------------------------------------------------------------------------------- * * following are the functions are that used in pasting operation */ /** * selects the selectedWidget. * In case of LIST_WIDGET, it selects the list widget instead of selecting the * container inside the list widget * * @param canvasWidgets * @param copiedWidgetGroups * @returns */ export const getSelectedWidgetWhenPasting = function*() { const canvasWidgets: CanvasWidgetsReduxState = yield select(getWidgets); const copiedWidgetGroups: CopiedWidgetGroup[] = yield getCopiedWidgets(); let selectedWidget: FlattenedWidgetProps | undefined = yield select( getSelectedWidget, ); const focusedWidget: FlattenedWidgetProps | undefined = yield select( getFocusedWidget, ); selectedWidget = checkIfPastingIntoListWidget( canvasWidgets, selectedWidget || focusedWidget, copiedWidgetGroups, ); return selectedWidget; }; /** * group copied widgets into a container * * @param copiedWidgetGroups * @param pastingIntoWidgetId * @returns */ export const groupWidgetsIntoContainer = function*( copiedWidgetGroups: CopiedWidgetGroup[], pastingIntoWidgetId: string, ) { const containerWidgetId = generateReactKey(); const evalTree: DataTree = yield select(getDataTree); const canvasWidgets: CanvasWidgetsReduxState = yield select(getWidgets); const newContainerName = getNextWidgetName( canvasWidgets, WidgetTypes.CONTAINER_WIDGET, evalTree, ); const newCanvasName = getNextWidgetName( canvasWidgets, WidgetTypes.CANVAS_WIDGET, evalTree, ); const { bottomMostWidget, leftMostWidget, rightMostWidget, topMostWidget, } = getBoundaryWidgetsFromCopiedGroups(copiedWidgetGroups); const copiedWidgets = copiedWidgetGroups.map((copiedWidgetGroup) => copiedWidgetGroup.list.find( (w) => w.widgetId === copiedWidgetGroup.widgetId, ), ); const parentColumnSpace = copiedWidgetGroups[0].list[0].parentColumnSpace || 1; const boundary = { top: _.minBy(copiedWidgets, (copiedWidget) => copiedWidget?.topRow), left: _.minBy(copiedWidgets, (copiedWidget) => copiedWidget?.leftColumn), bottom: _.maxBy(copiedWidgets, (copiedWidget) => copiedWidget?.bottomRow), right: _.maxBy(copiedWidgets, (copiedWidget) => copiedWidget?.rightColumn), }; const widthPerColumn = ((rightMostWidget.rightColumn - leftMostWidget.leftColumn) * parentColumnSpace) / GridDefaults.DEFAULT_GRID_COLUMNS; const heightOfCanvas = (bottomMostWidget.bottomRow - topMostWidget.topRow) * parentColumnSpace; const widthOfCanvas = (rightMostWidget.rightColumn - leftMostWidget.leftColumn) * parentColumnSpace; const newCanvasWidget: FlattenedWidgetProps = { ..._.omit( _.get( WidgetConfigResponse.config[WidgetTypes.CONTAINER_WIDGET], "blueprint.view[0]", ), ["position"], ), ..._.get( WidgetConfigResponse.config[WidgetTypes.CONTAINER_WIDGET], "blueprint.view[0].props", ), bottomRow: heightOfCanvas, isLoading: false, isVisible: true, leftColumn: 0, minHeight: heightOfCanvas, parentColumnSpace: 1, parentId: pastingIntoWidgetId, parentRowSpace: 1, rightColumn: widthOfCanvas, topRow: 0, renderMode: RenderModes.CANVAS, version: 1, widgetId: generateReactKey(), widgetName: newCanvasName, }; const newContainerWidget: FlattenedWidgetProps = { ..._.omit(WidgetConfigResponse.config[WidgetTypes.CONTAINER_WIDGET], [ "rows", "columns", "blueprint", ]), parentId: pastingIntoWidgetId, widgetName: newContainerName, type: WidgetTypes.CONTAINER_WIDGET, widgetId: containerWidgetId, leftColumn: boundary.left?.leftColumn || 0, topRow: boundary.top?.topRow || 0, bottomRow: (boundary.bottom?.bottomRow || 0) + 2, rightColumn: boundary.right?.rightColumn || 0, tabId: "", children: [newCanvasWidget.widgetId], renderMode: RenderModes.CANVAS, version: 1, isLoading: false, isVisible: true, parentRowSpace: GridDefaults.DEFAULT_GRID_ROW_HEIGHT, parentColumnSpace: widthPerColumn, }; newCanvasWidget.parentId = newContainerWidget.widgetId; const percentageIncrease = parentColumnSpace / widthPerColumn; const list = copiedWidgetGroups.map((copiedWidgetGroup) => { return [ ...copiedWidgetGroup.list.map((listItem) => { if (listItem.widgetId === copiedWidgetGroup.widgetId) { newCanvasWidget.children = _.get(newCanvasWidget, "children", []); newCanvasWidget.children = [ ...newCanvasWidget.children, listItem.widgetId, ]; return { ...listItem, leftColumn: (listItem.leftColumn - leftMostWidget.leftColumn) * percentageIncrease, rightColumn: (listItem.rightColumn - leftMostWidget.leftColumn) * percentageIncrease, topRow: listItem.topRow - topMostWidget.topRow, bottomRow: listItem.bottomRow - topMostWidget.topRow, parentId: newCanvasWidget.widgetId, parentRowSpace: GridDefaults.DEFAULT_GRID_ROW_HEIGHT, parentColumnSpace: widthPerColumn, }; } return listItem; }), ]; }); const flatList = _.flattenDeep(list); return [ { list: [newContainerWidget, newCanvasWidget, ...flatList], widgetId: newContainerWidget.widgetId, parentId: pastingIntoWidgetId, }, ]; }; /** * create copiedWidgets objects from selected widgets * * @returns */ export const createSelectedWidgetsAsCopiedWidgets = function*() { const canvasWidgets: { [widgetId: string]: FlattenedWidgetProps; } = yield select(getWidgets); const selectedWidgetIDs: string[] = yield select(getSelectedWidgets); const selectedWidgets = selectedWidgetIDs.map((each) => canvasWidgets[each]); if (!selectedWidgets || !selectedWidgets.length) return; const widgetListsToStore: { widgetId: string; parentId: string; list: FlattenedWidgetProps[]; }[] = yield all(selectedWidgets.map((each) => call(createWidgetCopy, each))); return widgetListsToStore; }; /** * return canvasWidgets without selectedWidgets and remove the selected widgets * ids in the children of parent widget * * @return */ export const filterOutSelectedWidgets = function*( parentId: string, copiedWidgetGroups: CopiedWidgetGroup[], ) { const canvasWidgets: CanvasWidgetsReduxState = yield _.cloneDeep( select(getWidgets), ); const selectedWidgetIDs: string[] = _.flattenDeep( copiedWidgetGroups.map((copiedWidgetGroup) => { return copiedWidgetGroup.list.map((widget) => widget.widgetId); }), ); const filteredWidgets: CanvasWidgetsReduxState = _.omit( canvasWidgets, selectedWidgetIDs, ); return { ...filteredWidgets, [parentId]: { ...filteredWidgets[parentId], // removing the selected widgets ids in the children of parent widget children: _.get(filteredWidgets[parentId], "children", []).filter( (widgetId) => { return !selectedWidgetIDs.includes(widgetId); }, ), }, }; }; /** * checks if selected widgets are colliding with other widgets or not * * @param widgets * @param copiedWidgetGroups * @returns */ export const isSelectedWidgetsColliding = function*( widgets: CanvasWidgetsReduxState, copiedWidgetGroups: CopiedWidgetGroup[], pastingIntoWidgetId: string, ) { if (!Array.isArray(copiedWidgetGroups)) return false; const { bottomMostWidget, leftMostWidget, rightMostWidget, topMostWidget, } = getBoundaryWidgetsFromCopiedGroups(copiedWidgetGroups); const widgetsWithSameParent = _.omitBy(widgets, (widget) => { return widget.parentId !== pastingIntoWidgetId; }); const widgetsArray = Object.values(widgetsWithSameParent).filter( (widget) => widget.parentId === pastingIntoWidgetId && widget.type !== WidgetTypes.MODAL_WIDGET, ); let isColliding = false; for (let i = 0; i < widgetsArray.length; i++) { const widget = widgetsArray[i]; if ( widget.bottomRow + 2 < topMostWidget.topRow || widget.topRow > bottomMostWidget.bottomRow ) { isColliding = false; } else if ( widget.rightColumn < leftMostWidget.leftColumn || widget.leftColumn > rightMostWidget.rightColumn ) { isColliding = false; } else { return true; } } return isColliding; }; /** * get next widget name to be used * * @param widgets * @param type * @param evalTree * @param options * @returns */ export function getNextWidgetName( widgets: CanvasWidgetsReduxState, type: WidgetType, evalTree: DataTree, options?: Record, ) { // 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, ); } /** * creates widget copied groups * * @param widget * @returns */ export 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, }; } /** * get all widgets in tree * * @param widgetId * @param canvasWidgets * @returns */ export 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; }; export const getParentBottomRowAfterAddingWidget = ( stateParent: FlattenedWidgetProps, newWidget: FlattenedWidgetProps, ) => { const parentRowSpace = newWidget.parentRowSpace || GridDefaults.DEFAULT_GRID_ROW_HEIGHT; const updateBottomRow = stateParent.type === WidgetTypes.CANVAS_WIDGET && newWidget.bottomRow * parentRowSpace > stateParent.bottomRow; return updateBottomRow ? Math.max( (newWidget.bottomRow + GridDefaults.CANVAS_EXTENSION_OFFSET) * parentRowSpace, stateParent.bottomRow, ) : stateParent.bottomRow; };