PromucFlow_constructor/app/client/src/sagas/WidgetOperationUtils.ts
Preet Sidhu 196642b84b
fix: Remove excess padding on right side and fix widget drop in nested containers (#22533)
## Description

Issues:
1. Excess padding on right side on MainContainer and other
container-like widgets.
2. End highlight not visible in deeply nested containers.
3. Modal widget takes up space on MainContainer.

Causes:
1.a. Border around the MainContainer has been removed. However, the
border width was still being deducted from the total width.
1.b. For parentColumnSpace calculation, CONTAINER_GRID_PADDING (= 6px)
was used. However, on AutoLayout canvases, containers only account for
5px in padding, resulting in excess space of 2px on the right side.
2.a. End position highlight has negative drop zones causing it to be
excluded from selection calculations.
2.b. container scrollbars are causing the drag on the canvas to not get
triggered.
3.a. This happens when the modal widget is dropped in an existing flex
layer. Check for `detachFromLayout` prop and move the widget to the
bottom of flexLayers.

Fixes # (issue)

1. https://github.com/appsmithorg/appsmith/issues/20705
2. https://github.com/appsmithorg/appsmith/issues/21311
3. https://github.com/appsmithorg/appsmith/issues/22423
4. https://github.com/appsmithorg/appsmith/issues/20111
5. https://github.com/appsmithorg/appsmith/issues/22655


Media



https://user-images.githubusercontent.com/5424788/232890004-2f66b697-e84c-4625-966d-894cc63f70b7.mov




## Type of change

- Bug fix (non-breaking change which fixes an issue)


## How Has This Been Tested?
- Manual

## Checklist:
### Dev activity
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my own code
- [x] 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
2023-04-26 13:39:11 -04:00

1807 lines
53 KiB
TypeScript

import {
getFocusedWidget,
getSelectedWidget,
getWidgetMetaProps,
getWidgets,
} from "./selectors";
import _, { find, isString, reduce, remove } from "lodash";
import type { WidgetType } from "constants/WidgetConstants";
import { AUTO_LAYOUT_CONTAINER_PADDING } from "constants/WidgetConstants";
import {
CONTAINER_GRID_PADDING,
FLEXBOX_PADDING,
GridDefaults,
MAIN_CONTAINER_WIDGET_ID,
RenderModes,
WIDGET_PADDING,
} from "constants/WidgetConstants";
import { all, call } from "redux-saga/effects";
import type { DataTree } from "entities/DataTree/dataTreeFactory";
import { select } from "redux-saga/effects";
import { getCopiedWidgets } from "utils/storage";
import type { WidgetProps } from "widgets/BaseWidget";
import { getSelectedWidgets } from "selectors/ui";
import { generateReactKey } from "utils/generators";
import type {
CanvasWidgetsReduxState,
FlattenedWidgetProps,
} from "reducers/entityReducers/canvasWidgetsReducer";
import { getDataTree } from "selectors/dataTreeSelectors";
import type { DynamicPath } from "utils/DynamicBindingUtils";
import {
getDynamicBindings,
combineDynamicBindings,
} from "utils/DynamicBindingUtils";
import { getNextEntityName } from "utils/AppsmithUtils";
import WidgetFactory from "utils/WidgetFactory";
import { getParentWithEnhancementFn } from "./WidgetEnhancementHelpers";
import type {
OccupiedSpace,
WidgetSpace,
} from "constants/CanvasEditorConstants";
import { areIntersecting } from "utils/boxHelpers";
import type {
GridProps,
PrevReflowState,
ReflowedSpaceMap,
SpaceMap,
} from "reflow/reflowTypes";
import { ReflowDirection } from "reflow/reflowTypes";
import {
getBaseWidgetClassName,
getStickyCanvasName,
getSlidingArenaName,
POSITIONED_WIDGET,
} from "constants/componentClassNameConstants";
import { getContainerWidgetSpacesSelector } from "selectors/editorSelectors";
import { reflow } from "reflow";
import { getBottomRowAfterReflow } from "utils/reflowHookUtils";
import type { WidgetEntity } from "entities/DataTree/dataTreeFactory";
import { isWidget } from "@appsmith/workers/Evaluation/evaluationUtils";
import { CANVAS_DEFAULT_MIN_HEIGHT_PX } from "constants/AppConstants";
import type { MetaState } from "reducers/entityReducers/metaReducer";
import { Positioning } from "utils/autoLayout/constants";
import { AppPositioningTypes } from "reducers/entityReducers/pageListReducer";
export interface CopiedWidgetGroup {
widgetId: string;
parentId: string;
list: WidgetProps[];
}
export type NewPastePositionVariables = {
bottomMostRow?: number;
gridProps?: GridProps;
newPastingPositionMap?: SpaceMap;
reflowedMovementMap?: ReflowedSpaceMap;
canvasId?: string;
};
export const WIDGET_PASTE_PADDING = 1;
/**
* 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 === "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<string, string>,
newWidgetList: FlattenedWidgetProps[],
) => {
// this is the case when whole list widget is copied and pasted
if (widget?.type === "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 widget 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;
},
);
// updating dynamicTriggerPath in copied widget if the copied widget thas reference to oldWidgetNames
widget.dynamicTriggerPathList = (widget.dynamicTriggerPathList || []).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 === "MODAL_WIDGET") {
// if Modal is being copied 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" ||
copyWidget.type === "ICON_BUTTON_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 = handleListWidgetV2Pasting(widget, widgets, widgetNameMap);
widgets = handleIfParentIsListWidgetWhilePasting(widget, widgets);
return widgets;
};
export function getWidgetChildrenIds(
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 = getWidgetChildrenIds(canvasWidgets, child);
if (grandChildren.length) {
childrenIds.push(...grandChildren);
}
}
}
}
return childrenIds;
}
function sortWidgetsMetaByParent(widgetsMeta: MetaState, parentId: string) {
return reduce(
widgetsMeta,
function (
result: {
childrenWidgetsMeta: MetaState;
otherWidgetsMeta: MetaState;
},
currentWidgetMeta,
key,
) {
return key.startsWith(parentId + "_")
? {
...result,
childrenWidgetsMeta: {
...result.childrenWidgetsMeta,
[key]: currentWidgetMeta,
},
}
: {
...result,
otherWidgetsMeta: {
...result.otherWidgetsMeta,
[key]: currentWidgetMeta,
},
};
},
{
childrenWidgetsMeta: {},
otherWidgetsMeta: {},
},
);
}
export type DescendantWidgetMap = {
id: string;
// To accomodate metaWidgets which might not be present on the evalTree, evaluatedWidget might be undefined
evaluatedWidget: WidgetEntity | undefined;
};
/**
* As part of widget's descendant, we add both children and metaWidgets.
* children are assessed from "widget.children"
* metaWidgets are assessed from the metaState, since we care about only metawidgets whose values have been changed.
* NB: metaWidgets id start with parentId + "_"
*/
export function getWidgetDescendantToReset(
canvasWidgets: CanvasWidgetsReduxState,
widgetId: string,
evaluatedDataTree: DataTree,
widgetsMeta: MetaState,
): DescendantWidgetMap[] {
const descendantList: DescendantWidgetMap[] = [];
const widget = _.get(canvasWidgets, widgetId);
const sortedWidgetsMeta = sortWidgetsMetaByParent(widgetsMeta, widgetId);
for (const childMetaWidgetId of Object.keys(
sortedWidgetsMeta.childrenWidgetsMeta,
)) {
const evaluatedChildWidget = find(evaluatedDataTree, function (entity) {
return isWidget(entity) && entity.widgetId === childMetaWidgetId;
}) as WidgetEntity | undefined;
descendantList.push({
id: childMetaWidgetId,
evaluatedWidget: evaluatedChildWidget,
});
const grandChildren = getWidgetDescendantToReset(
canvasWidgets,
childMetaWidgetId,
evaluatedDataTree,
sortedWidgetsMeta.otherWidgetsMeta,
);
if (grandChildren.length) {
descendantList.push(...grandChildren);
}
}
if (widget) {
const { children = [] } = widget;
if (children && children.length) {
for (const childIndex in children) {
if (children.hasOwnProperty(childIndex)) {
const childWidgetId = children[childIndex];
const childCanvasWidget = _.get(canvasWidgets, childWidgetId);
const childWidgetName = childCanvasWidget.widgetName;
const childWidget = evaluatedDataTree[childWidgetName];
if (isWidget(childWidget)) {
descendantList.push({
id: childWidgetId,
evaluatedWidget: childWidget,
});
const grandChildren = getWidgetDescendantToReset(
canvasWidgets,
childWidgetId,
evaluatedDataTree,
sortedWidgetsMeta.otherWidgetsMeta,
);
if (grandChildren.length) {
descendantList.push(...grandChildren);
}
}
}
}
}
}
return descendantList;
}
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 !== "LIST_WIDGET" &&
selectedWidget.type !== "LIST_WIDGET_V2"
) {
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 !== "CANVAS_WIDGET"
) {
let childWidget;
// If the widget in which to paste the new widget is NOT
// a tabs widget
if (parentWidget.type !== "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,
);
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 === "CANVAS_WIDGET") {
newWidgetParentId = childWidget.widgetId;
}
} else if (selectedWidget && selectedWidget.type === "CANVAS_WIDGET") {
newWidgetParentId = selectedWidget.widgetId;
}
return newWidgetParentId;
};
export const getSelectedWidgetIfPastingIntoListWidget = function (
canvasWidgets: CanvasWidgetsReduxState,
selectedWidget: FlattenedWidgetProps | undefined,
copiedWidgets: CopiedWidgetGroup[],
) {
// 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 === "LIST_WIDGET"
) {
const childrenIds: string[] = getWidgetChildrenIds(
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 copiedWidget = copiedWidgets[i].list[0];
if (copiedWidget?.type === "LIST_WIDGET") {
return selectedWidget;
}
}
return _.get(canvasWidgets, firstChildId);
}
return selectedWidget;
};
/**
* get selected widgets that are verified to make sure that we are not pasting list widget onto another list widget
* also return a boolean to indicate if the list widget is pasting into a list widget
*
* @param selectedWidgetIDs
* @param copiedWidgetGroups
* @param canvasWidgets
* @returns
*/
export function getVerifiedSelectedWidgets(
selectedWidgetIDs: string[],
copiedWidgetGroups: CopiedWidgetGroup[],
canvasWidgets: CanvasWidgetsReduxState,
) {
const selectedWidgets = getWidgetsFromIds(selectedWidgetIDs, canvasWidgets);
//if there is no list widget in the copied widgets then return selected Widgets
if (
!checkForListWidgetInCopiedWidgets(copiedWidgetGroups) ||
selectedWidgets.length === 0
)
return { selectedWidgets };
//if the selected widget is a list widgets the return isListWidgetPastingOnItself as true
if (selectedWidgets.length === 1 && selectedWidgets[0].type === "LIST_WIDGET")
return { selectedWidgets, isListWidgetPastingOnItself: true };
//get list widget ancestor of selected widget if it has a list widget ancestor
const parentListWidgetId = document
.querySelector(
`.${POSITIONED_WIDGET}.${getBaseWidgetClassName(
selectedWidgets[0].widgetId,
)}`,
)
?.closest(".t--widget-listwidget")?.id;
//if the selected widgets do have a list widget ancestor then,
// return that list widget as selected widgets and isListWidgetPastingOnItself as true
if (parentListWidgetId && canvasWidgets[parentListWidgetId])
return {
selectedWidgets: [canvasWidgets[parentListWidgetId]],
isListWidgetPastingOnItself: true,
};
return { selectedWidgets };
}
/**
* returns true if list widget is among the copied widgets
*
* @param copiedWidgetGroups
* @returns boolean
*/
export function checkForListWidgetInCopiedWidgets(
copiedWidgetGroups: CopiedWidgetGroup[],
) {
for (let i = 0; i < copiedWidgetGroups.length; i++) {
const copiedWidget = copiedWidgetGroups[i].list[0];
if (copiedWidget?.type === "LIST_WIDGET") {
return true;
}
}
return false;
}
/**
* get top, left, right, bottom most widgets and totalWidth 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,
totalWidth: rightMostWidget.rightColumn - leftMostWidget.leftColumn,
};
};
/**
* get totalWidth, maxThickness, topMostRow and leftMostColumn from selected Widgets
*
* @param selectedWidgets
* @returns
*/
export function getBoundariesFromSelectedWidgets(
selectedWidgets: WidgetProps[],
) {
const topMostWidget = selectedWidgets.sort((a, b) => a.topRow - b.topRow)[0];
const leftMostWidget = selectedWidgets.sort(
(a, b) => a.leftColumn - b.leftColumn,
)[0];
const rightMostWidget = selectedWidgets.sort(
(a, b) => b.rightColumn - a.rightColumn,
)[0];
const bottomMostWidget = selectedWidgets.sort(
(a, b) => b.bottomRow - a.bottomRow,
)[0];
const thickestWidget = selectedWidgets.sort(
(a, b) => b.bottomRow - b.topRow - a.bottomRow + a.topRow,
)[0];
return {
totalWidth: rightMostWidget.rightColumn - leftMostWidget.leftColumn,
totalHeight: bottomMostWidget.bottomRow - topMostWidget.topRow,
maxThickness: thickestWidget.bottomRow - thickestWidget.topRow,
topMostRow: topMostWidget.topRow,
leftMostColumn: leftMostWidget.leftColumn,
};
}
/**
* -------------------------------------------------------------------------------
* 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 { widgets: copiedWidgetGroups }: { widgets: CopiedWidgetGroup[] } =
yield getCopiedWidgets();
let selectedWidget: FlattenedWidgetProps | undefined = yield select(
getSelectedWidget,
);
const focusedWidget: FlattenedWidgetProps | undefined = yield select(
getFocusedWidget,
);
selectedWidget = getSelectedWidgetIfPastingIntoListWidget(
canvasWidgets,
selectedWidget || focusedWidget,
copiedWidgetGroups,
);
return selectedWidget;
};
/**
* calculates mouse positions in terms of grid values
*
* @param canvasRect canvas DOM rect
* @param canvasId Id of the canvas widget
* @param snapGrid grid parameters
* @param padding padding inside of widget
* @param mouseLocation mouse Location in terms of absolute pixel values
* @returns
*/
export function getMousePositions(
canvasRect: DOMRect,
canvasId: string,
snapGrid: { snapRowSpace: number; snapColumnSpace: number },
padding: number,
mouseLocation?: { x: number; y: number },
) {
//check if the mouse location is inside of the container widget
if (
!mouseLocation ||
!(
canvasRect.top < mouseLocation.y &&
canvasRect.left < mouseLocation.x &&
canvasRect.bottom > mouseLocation.y &&
canvasRect.right > mouseLocation.x
)
)
return;
//get DOM of the overall canvas including it's total scroll height
const stickyCanvasDOM = document.querySelector(
`#${getSlidingArenaName(canvasId)}`,
);
if (!stickyCanvasDOM) return;
const rect = stickyCanvasDOM.getBoundingClientRect();
// get mouse position relative to the canvas.
const relativeMouseLocation = {
y: mouseLocation.y - rect.top - padding,
x: mouseLocation.x - rect.left - padding,
};
return {
top: Math.floor(relativeMouseLocation.y / snapGrid.snapRowSpace),
left: Math.floor(relativeMouseLocation.x / snapGrid.snapColumnSpace),
};
}
/**
* This method calculates the snap Grid dimensions.
*
* @param LayoutWidget
* @param canvasWidth
* @returns
*/
export function getSnappedGrid(LayoutWidget: WidgetProps, canvasWidth: number) {
// For all widgets inside a container, we remove both container padding as well as widget padding from component width
let padding =
((LayoutWidget?.appPositioningType === AppPositioningTypes.AUTO
? AUTO_LAYOUT_CONTAINER_PADDING
: CONTAINER_GRID_PADDING) +
WIDGET_PADDING) *
2;
if (
LayoutWidget.widgetId === MAIN_CONTAINER_WIDGET_ID ||
LayoutWidget.type === "CONTAINER_WIDGET"
) {
// For MainContainer and any Container Widget padding doesn't exist coz there is already container padding.
padding =
LayoutWidget.positioning === Positioning.Vertical
? FLEXBOX_PADDING * 2
: CONTAINER_GRID_PADDING * 2;
}
if (LayoutWidget.noPad) {
// Widgets like ListWidget choose to have no container padding so will only have widget padding
padding = WIDGET_PADDING * 2;
}
const width = canvasWidth - padding;
return {
snapGrid: {
snapRowSpace: GridDefaults.DEFAULT_GRID_ROW_HEIGHT,
snapColumnSpace: canvasWidth
? width / GridDefaults.DEFAULT_GRID_COLUMNS
: 0,
},
padding: padding / 2,
};
}
/**
* method to return default canvas,
* It is MAIN_CONTAINER_WIDGET_ID by default or
* if a modal is open, then default canvas is a Modal's canvas
*
* @param canvasWidgets
* @returns
*/
export function getDefaultCanvas(canvasWidgets: CanvasWidgetsReduxState) {
const containerDOM = document.querySelector(".t--modal-widget");
//if a modal is open, then get it's canvas Id
if (containerDOM && containerDOM.id && canvasWidgets[containerDOM.id]) {
const containerWidget = canvasWidgets[containerDOM.id];
const { canvasDOM, canvasId } = getCanvasIdForContainer(containerWidget);
return {
canvasId,
canvasDOM,
containerWidget,
};
} else {
//default canvas is set as MAIN_CONTAINER_WIDGET_ID
return {
canvasId: MAIN_CONTAINER_WIDGET_ID,
containerWidget: canvasWidgets[MAIN_CONTAINER_WIDGET_ID],
canvasDOM: document.querySelector(
`#${getSlidingArenaName(MAIN_CONTAINER_WIDGET_ID)}`,
),
};
}
}
/**
* This method returns the Id of the parent widget of the canvas widget
*
* @param canvasId
* @returns
*/
export function getContainerIdForCanvas(canvasId: string) {
if (canvasId === MAIN_CONTAINER_WIDGET_ID) return canvasId;
const selector = `#${getStickyCanvasName(canvasId)}`;
const canvasDOM = document.querySelector(selector);
if (!canvasDOM) return "";
//check for positionedWidget parent
let containerDOM = canvasDOM.closest(`.${POSITIONED_WIDGET}`);
//if positioned widget parent is not found, most likely is a modal widget
if (!containerDOM) containerDOM = canvasDOM.closest(".t--modal-widget");
return containerDOM ? containerDOM.id : "";
}
/**
* This method returns Id of the child canvas inside of the Layout Widget
*
* @param layoutWidget
* @returns
*/
export function getCanvasIdForContainer(layoutWidget: WidgetProps) {
const selector =
layoutWidget.type === "MODAL_WIDGET"
? `.${getBaseWidgetClassName(layoutWidget.widgetId)}`
: `.${POSITIONED_WIDGET}.${getBaseWidgetClassName(
layoutWidget.widgetId,
)}`;
const containerDOM = document.querySelector(selector);
if (!containerDOM) return {};
const dropTargetDOM = containerDOM.querySelector(".t--drop-target");
const canvasDOM = containerDOM.getElementsByTagName("canvas");
return {
canvasId: canvasDOM ? canvasDOM[0].id.split("-")[2] : undefined,
canvasDOM: dropTargetDOM,
};
}
/**
* This method returns array of occupiedSpaces with changes Ids
*
* @param newPastingPositionMap
* @returns
*/
export function changeIdsOfPastePositions(newPastingPositionMap: SpaceMap) {
const newPastePositions = [];
const newPastingPositionArray = Object.values(newPastingPositionMap);
let count = 1;
for (const position of newPastingPositionArray) {
newPastePositions.push({
...position,
id: count.toString(),
});
count++;
}
return newPastePositions;
}
/**
* Iterates over the selected widgets to find the next available space below the selected widgets
* where in the new pasting positions dont overlap with the selected widgets
*
* @param copiedSpaces
* @param selectedSpaces
* @param thickness
* @returns
*/
export function getVerticallyAdjustedPositions(
copiedSpaces: OccupiedSpace[],
selectedSpaces: OccupiedSpace[],
thickness: number,
) {
let verticalOffset = thickness;
const newPastingPositionMap: SpaceMap = {};
//iterate over the widgets to calculate verticalOffset
//TODO: find a better way to do this.
for (let i = 0; i < copiedSpaces.length; i++) {
const copiedSpace = {
...copiedSpaces[i],
top: copiedSpaces[i].top + verticalOffset,
bottom: copiedSpaces[i].bottom + verticalOffset,
};
for (let j = 0; j < selectedSpaces.length; j++) {
const selectedSpace = selectedSpaces[j];
if (areIntersecting(copiedSpace, selectedSpace)) {
const increment = selectedSpace.bottom - copiedSpace.top;
if (increment > 0) {
verticalOffset += increment;
i = 0;
j = 0;
break;
} else return;
}
}
}
verticalOffset += WIDGET_PASTE_PADDING;
// offset the pasting positions down
for (const copiedSpace of copiedSpaces) {
newPastingPositionMap[copiedSpace.id] = {
...copiedSpace,
top: copiedSpace.top + verticalOffset,
bottom: copiedSpace.bottom + verticalOffset,
};
}
return newPastingPositionMap;
}
/**
* Simple method to convert widget props to occupied spaces
*
* @param widgets
* @returns
*/
export function getOccupiedSpacesFromProps(
widgets: WidgetProps[],
): OccupiedSpace[] {
const occupiedSpaces = [];
for (const widget of widgets) {
const currentSpace = {
id: widget.widgetId,
top: widget.topRow,
left: widget.leftColumn,
bottom: widget.bottomRow,
right: widget.rightColumn,
} as OccupiedSpace;
occupiedSpaces.push(currentSpace);
}
return occupiedSpaces;
}
/**
* Method that adjusts the positions of copied spaces using,
* the top-left of copied widgets and top left of where it should be placed
*
* @param copiedWidgetGroups
* @param copiedTopMostRow
* @param selectedTopMostRow
* @param copiedLeftMostColumn
* @param pasteLeftMostColumn
* @returns
*/
export function getNewPositionsForCopiedWidgets(
copiedWidgetGroups: CopiedWidgetGroup[],
copiedTopMostRow: number,
selectedTopMostRow: number,
copiedLeftMostColumn: number,
pasteLeftMostColumn: number,
): OccupiedSpace[] {
const copiedSpacePositions = [];
// the logic is that, when subtracted by top-left of copied widget, the new position's top-left will be zero
// by adding the selectedTopMostRow or pasteLeftMostColumn, copied widget's top row is aligned there
const leftOffSet = copiedLeftMostColumn - pasteLeftMostColumn;
const topOffSet = copiedTopMostRow - selectedTopMostRow;
for (const copiedWidgetGroup of copiedWidgetGroups) {
const copiedWidget = copiedWidgetGroup.list[0];
const currentSpace = {
id: copiedWidgetGroup.widgetId,
top: copiedWidget.topRow - topOffSet,
left: copiedWidget.leftColumn - leftOffSet,
bottom: copiedWidget.bottomRow - topOffSet,
right: copiedWidget.rightColumn - leftOffSet,
} as OccupiedSpace;
copiedSpacePositions.push(currentSpace);
}
return copiedSpacePositions;
}
/**
* Method that adjusts the positions of copied spaces using,
* the top-left of copied widgets and top left of where it should be placed
*
* @param copiedWidgetGroups
* @param copiedTopMostRow
* @param mouseTopRow
* @param copiedLeftMostColumn
* @param mouseLeftColumn
* @returns
*/
export function getPastePositionMapFromMousePointer(
copiedWidgetGroups: CopiedWidgetGroup[],
copiedTopMostRow: number,
mouseTopRow: number,
copiedLeftMostColumn: number,
mouseLeftColumn: number,
): SpaceMap {
const newPastingPositionMap: SpaceMap = {};
// the logic is that, when subtracted by top-left of copied widget, the new position's top-left will be zero
// by adding the selectedTopMostRow or pasteLeftMostColumn, copied widget's top row is aligned there
const leftOffSet = copiedLeftMostColumn - mouseLeftColumn;
const topOffSet = copiedTopMostRow - mouseTopRow;
for (const copiedWidgetGroup of copiedWidgetGroups) {
const copiedWidget = copiedWidgetGroup.list[0];
newPastingPositionMap[copiedWidgetGroup.widgetId] = {
id: copiedWidgetGroup.widgetId,
top: copiedWidget.topRow - topOffSet,
left: copiedWidget.leftColumn - leftOffSet,
bottom: copiedWidget.bottomRow - topOffSet,
right: copiedWidget.rightColumn - leftOffSet,
type: copiedWidget.type,
} as OccupiedSpace;
}
return newPastingPositionMap;
}
/**
* Take the canvas widgets and move them with the reflowed values
*
*
* @param widgets
* @param gridProps
* @param reflowingWidgets
* @returns
*/
export function getReflowedPositions(
widgets: {
[widgetId: string]: FlattenedWidgetProps;
},
gridProps?: GridProps,
reflowingWidgets?: ReflowedSpaceMap,
) {
const currentWidgets: {
[widgetId: string]: FlattenedWidgetProps;
} = { ...widgets };
const reflowWidgetKeys = Object.keys(reflowingWidgets || {});
// if there are no reflowed widgets return the original widgets
if (!reflowingWidgets || !gridProps || reflowWidgetKeys.length <= 0)
return widgets;
for (const reflowedWidgetId of reflowWidgetKeys) {
const reflowWidget = reflowingWidgets[reflowedWidgetId];
const canvasWidget = { ...currentWidgets[reflowedWidgetId] };
let { bottomRow, leftColumn, rightColumn, topRow } = canvasWidget;
// adjust the positions with respect to the reflowed positions
if (reflowWidget.X !== undefined && reflowWidget.width !== undefined) {
leftColumn = Math.round(
canvasWidget.leftColumn + reflowWidget.X / gridProps.parentColumnSpace,
);
rightColumn = Math.round(
leftColumn + reflowWidget.width / gridProps.parentColumnSpace,
);
}
if (reflowWidget.Y !== undefined && reflowWidget.height !== undefined) {
topRow = Math.round(
canvasWidget.topRow + reflowWidget.Y / gridProps.parentRowSpace,
);
bottomRow = Math.round(
topRow + reflowWidget.height / gridProps.parentRowSpace,
);
}
currentWidgets[reflowedWidgetId] = {
...canvasWidget,
topRow,
leftColumn,
bottomRow,
rightColumn,
};
}
return currentWidgets;
}
/**
* method to return array of widget properties from widgetsIds, without any undefined values
*
* @param widgetsIds
* @param canvasWidgets
* @returns array of widgets properties
*/
export function getWidgetsFromIds(
widgetsIds: string[],
canvasWidgets: CanvasWidgetsReduxState,
) {
const widgets = [];
for (const currentId of widgetsIds) {
if (canvasWidgets[currentId]) widgets.push(canvasWidgets[currentId]);
}
return widgets;
}
/**
* Check if it is drop target Including the CANVAS_WIDGET
*
* @param type
* @returns
*/
export function isDropTarget(type: WidgetType, includeCanvasWidget = false) {
const isLayoutWidget = !!WidgetFactory.widgetConfigMap.get(type)?.isCanvas;
if (includeCanvasWidget) return isLayoutWidget || type === "CANVAS_WIDGET";
return isLayoutWidget;
}
/**
* group copied widgets into a container
*
* @param copiedWidgetGroups
* @param pastingIntoWidgetId
* @returns
*/
export const groupWidgetsIntoContainer = function* (
copiedWidgetGroups: CopiedWidgetGroup[],
pastingIntoWidgetId: string,
isThereACollision: boolean,
) {
const containerWidgetId = generateReactKey();
const evalTree: DataTree = yield select(getDataTree);
const canvasWidgets: CanvasWidgetsReduxState = yield select(getWidgets);
const newContainerName = getNextWidgetName(
canvasWidgets,
"CONTAINER_WIDGET",
evalTree,
);
const newCanvasName = getNextWidgetName(
canvasWidgets,
"CANVAS_WIDGET",
evalTree,
);
let reflowedMovementMap, bottomMostRow, gridProps;
const { bottomMostWidget, leftMostWidget, rightMostWidget, topMostWidget } =
getBoundaryWidgetsFromCopiedGroups(copiedWidgetGroups);
const copiedWidgets = copiedWidgetGroups.map((copiedWidgetGroup) =>
copiedWidgetGroup.list.find(
(w) => w.widgetId === copiedWidgetGroup.widgetId,
),
);
//calculating parentColumnSpace because the values stored inside widget DSL are not entirely reliable
const parentColumnSpace =
getParentColumnSpace(canvasWidgets, pastingIntoWidgetId) ||
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(
WidgetFactory.widgetConfigMap.get("CONTAINER_WIDGET"),
"blueprint.view[0]",
),
["position"],
),
..._.get(
WidgetFactory.widgetConfigMap.get("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(WidgetFactory.widgetConfigMap.get("CONTAINER_WIDGET"), [
"rows",
"columns",
"blueprint",
]),
parentId: pastingIntoWidgetId,
widgetName: newContainerName,
type: "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);
// if there are no collision already then reflow the below widgets by 2 rows.
if (!isThereACollision) {
const widgetSpacesSelector =
getContainerWidgetSpacesSelector(pastingIntoWidgetId);
const widgetSpaces: WidgetSpace[] = yield select(widgetSpacesSelector) ||
[];
const copiedWidgetIds = copiedWidgets
.map((widget) => widget?.widgetId)
.filter((id) => !!id);
// filter out copiedWidgets from occupied spaces
const widgetOccupiedSpaces = widgetSpaces.filter(
(widgetSpace) => copiedWidgetIds.indexOf(widgetSpace.id) === -1,
);
// create the object of the new container in the form of OccupiedSpace
const containerSpace = {
id: "1",
left: newContainerWidget.leftColumn,
top: newContainerWidget.topRow,
right: newContainerWidget.rightColumn,
bottom: newContainerWidget.bottomRow,
};
gridProps = {
parentColumnSpace,
parentRowSpace: GridDefaults.DEFAULT_GRID_ROW_HEIGHT,
maxGridColumns: GridDefaults.DEFAULT_GRID_COLUMNS,
};
//get movement map of reflowed widgets
const { movementMap } = reflow(
[containerSpace],
[containerSpace],
widgetOccupiedSpaces,
ReflowDirection.BOTTOM,
gridProps,
true,
false,
{ prevSpacesMap: {} } as PrevReflowState,
);
reflowedMovementMap = movementMap;
//get the new calculated bottom row
bottomMostRow = getBottomRowAfterReflow(
reflowedMovementMap,
containerSpace.bottom,
widgetOccupiedSpaces,
gridProps,
);
}
return {
reflowedMovementMap,
bottomMostRow,
gridProps,
copiedWidgetGroups: [
{
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 (!copiedWidgetGroups.length) 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 !== "MODAL_WIDGET",
);
for (let i = 0; i < widgetsArray.length; i++) {
const widget = widgetsArray[i];
if (
!(
widget.leftColumn >= rightMostWidget.rightColumn ||
widget.rightColumn <= leftMostWidget.leftColumn ||
widget.topRow >= bottomMostWidget.bottomRow ||
widget.bottomRow <= topMostWidget.topRow
)
)
return true;
}
return false;
};
/**
* 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<string, unknown>,
) {
// Compute the new widget's name
const defaultConfig: any = WidgetFactory.widgetConfigMap.get(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,
};
}
export type WidgetsInTree = (WidgetProps & {
children?: string[] | undefined;
})[];
/**
* get all widgets in tree
*
* @param widgetId
* @param canvasWidgets
* @returns
*/
export const getAllWidgetsInTree = (
widgetId: string,
canvasWidgets: CanvasWidgetsReduxState,
): WidgetsInTree => {
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;
};
/**
* sometimes, selected widgets contains the grouped widget,
* in those cases, we will just selected the main container as the
* pastingIntoWidget
*
* @param copiedWidgetGroups
* @param pastingIntoWidgetId
*/
export function* getParentWidgetIdForGrouping(
widgets: CanvasWidgetsReduxState,
copiedWidgetGroups: CopiedWidgetGroup[],
) {
const pastingIntoWidgetId = copiedWidgetGroups[0]?.parentId;
const widgetIds = copiedWidgetGroups.map(
(widgetGroup) => widgetGroup.widgetId,
);
// the pastingIntoWidgetId should parent of copiedWidgets
for (let i = 0; i < widgetIds.length; i++) {
const widgetId = widgetIds[i];
const widget = widgets[widgetId];
if (widget.parentId !== pastingIntoWidgetId) {
return MAIN_CONTAINER_WIDGET_ID;
}
}
return pastingIntoWidgetId;
}
/**
* this saga clears out the enhancementMap, template, dynamicBindingPathList and dynamicTriggerPathList 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}`),
);
// delete dynamic trigger path if any
remove(listWidget?.dynamicTriggerPathList || [], (path: any) =>
path.key.startsWith(`template.${widgetName}`),
);
return clone;
}
return clone;
}
/**
* Purge all paths in a provided widgets' dynamicTriggerPathList and dynamicBindingPathList, which no longer exist in the widget
* I call these paths orphaned dynamic paths
*
* @param widget : WidgetProps
*
* returns the updated widget
*/
// Purge all paths in a provided widgets' dynamicTriggerPathList, which don't exist in the widget
export function purgeOrphanedDynamicPaths(widget: WidgetProps) {
// Attempt to purge only if there are dynamicTriggerPaths in this widget
if (widget.dynamicTriggerPathList && widget.dynamicTriggerPathList.length) {
// Filter out all the paths from the dynamicTriggerPathList which don't exist in the widget
widget.dynamicTriggerPathList = widget.dynamicTriggerPathList.filter(
(path: DynamicPath) => {
// Use lodash _.has to check if the path exists in the widget
return _.has(widget, path.key);
},
);
}
if (widget.dynamicBindingPathList && widget.dynamicBindingPathList.length) {
// Filter out all the paths from the dynamicBindingPaths which don't exist in the widget
widget.dynamicBindingPathList = widget.dynamicBindingPathList.filter(
(path: DynamicPath) => {
// Use lodash _.has to check if the path exists in the widget
return _.has(widget, path.key);
},
);
}
return widget;
}
/**
*
* @param canvasWidgets
* @param pastingIntoWidgetId
* @returns
*/
export function getParentColumnSpace(
canvasWidgets: CanvasWidgetsReduxState,
pastingIntoWidgetId: string,
) {
const containerId = getContainerIdForCanvas(pastingIntoWidgetId);
const containerWidget = canvasWidgets[containerId];
const canvasDOM = document.querySelector(
`#${getStickyCanvasName(pastingIntoWidgetId)}`,
);
if (!canvasDOM || !containerWidget) return;
const rect = canvasDOM.getBoundingClientRect();
// get Grid values such as snapRowSpace and snapColumnSpace
const { snapGrid } = getSnappedGrid(containerWidget, rect.width);
return snapGrid?.snapColumnSpace;
}
/*
* Function to extend the lodash's get function to check
* paths which have dots in it's key
*
* Suppose, if the path is `path1.path2.path3.path4`, this function
* checks in following paths in the tree as well, if _.get doesn't return a value
* - path1.path2.path3 -> path4
* - path1.path2 -> path3.path4 (will recursively traverse with same logic)
* - path1 -> path2.path3.path4 (will recursively traverse with same logic)
*/
export function getValueFromTree(
obj: Record<string, unknown>,
path: string,
defaultValue?: unknown,
): unknown {
// Creating a symbol as we need a unique value that will not be present in the input obj
const defaultValueSymbol = Symbol("defaultValue");
//Call the original get function with defaultValueSymbol.
const value = _.get(obj, path, defaultValueSymbol);
/*
* if the value returned by get matches defaultValueSymbol,
* path is invalid.
*/
if (value === defaultValueSymbol && path.includes(".")) {
const pathArray = path.split(".");
const poppedPath: Array<string> = [];
while (pathArray.length) {
const currentPath = pathArray.join(".");
if (obj.hasOwnProperty(currentPath)) {
const currentValue = obj[currentPath];
if (!poppedPath.length) {
//Valid path
return currentValue;
} else if (typeof currentValue !== "object") {
//Invalid path
return defaultValue;
} else {
//Valid path, need to traverse recursively with same strategy
return getValueFromTree(
currentValue as Record<string, unknown>,
poppedPath.join("."),
defaultValue,
);
}
} else {
// We need the popped paths to traverse recursively
poppedPath.unshift(pathArray.pop() || "");
}
}
}
// Need to return the defaultValue, if there is no match for the path in the tree
return value !== defaultValueSymbol ? value : defaultValue;
}
/*
* Function to merge two dynamicpath arrays
*/
export function mergeDynamicPropertyPaths(
a?: DynamicPath[],
b?: DynamicPath[],
) {
return _.unionWith(a, b, (a, b) => a.key === b.key);
}
/**
* Note: Mutates widgets[0].bottomRow for CANVAS_WIDGET
* @param widgets
* @param parentId
*/
export function resizePublishedMainCanvasToLowestWidget(
widgets: CanvasWidgetsReduxState,
) {
if (!widgets[MAIN_CONTAINER_WIDGET_ID]) {
return;
}
const childIds = widgets[MAIN_CONTAINER_WIDGET_ID].children || [];
let lowestBottomRow = 0;
// find the lowest row
childIds.forEach((cId) => {
const child = widgets[cId];
if (!child.detachFromLayout && child.bottomRow > lowestBottomRow) {
lowestBottomRow = child.bottomRow;
}
});
widgets[MAIN_CONTAINER_WIDGET_ID].bottomRow = Math.max(
CANVAS_DEFAULT_MIN_HEIGHT_PX,
(lowestBottomRow + GridDefaults.VIEW_MODE_MAIN_CANVAS_EXTENSION_OFFSET) *
GridDefaults.DEFAULT_GRID_ROW_HEIGHT,
);
}
export const handleListWidgetV2Pasting = (
widget: FlattenedWidgetProps,
widgets: CanvasWidgetsReduxState,
widgetNameMap: Record<string, string>,
) => {
if (widget?.type !== "LIST_WIDGET_V2") return widgets;
widgets = updateListWidgetBindings(widgetNameMap, widgets, widget.widgetId);
return widgets;
};
// Updating PrimaryKeys, mainCanvasId and mainContainerId for ListWidgetV2
const updateListWidgetBindings = (
widgetNameMap: Record<string, string>,
widgets: CanvasWidgetsReduxState,
listWidgetId: string,
) => {
let mainCanvasId = "";
let mainContainerId = "";
const oldWidgetName =
Object.keys(widgetNameMap).find(
(widgetName) =>
widgetNameMap[widgetName] === widgets[listWidgetId].widgetName,
) ?? "";
Object.keys(widgets).forEach((widgetId) => {
if (widgets[widgetId].parentId === listWidgetId) {
mainCanvasId = widgetId;
mainContainerId = widgets[widgetId].children?.[0] ?? "";
}
});
widgets[listWidgetId].mainCanvasId = mainCanvasId;
widgets[listWidgetId].mainContainerId = mainContainerId;
const primaryKeys = widgets[listWidgetId].primaryKeys.replaceAll(
oldWidgetName,
widgets[listWidgetId].widgetName,
);
widgets[listWidgetId].primaryKeys = primaryKeys;
return widgets;
};