PromucFlow_constructor/app/client/src/sagas/WidgetOperationUtils.ts
Pawan Kumar 960159eef3
Grouping widgets into container (#5704)
* Cut copy paste first cut

* removed different parent groups logic

* mouseup on the outer canvas removes selections.

* bug fix

* remove unwanted dead code.

* Adding tests

* build fix

* min height fixes

* fixing specs.

* fixing specs.

* fix merge conflcits

* fix border positioning

* fix canvas widgets incorrect bouding box

* fix bounding box position issue

* fix bounding box position issue

* fix

* border issue fix

* update test case

* add colors in theme

* use layers + use click capture on actions

* add icon for grouping

* fix overflow issue in contextmenu in containers

* fix context menu display issue

* update position of context menu

* fix container box-shadow issue

* fix container box-shadow issue

* revert container box shadow

* stop opening of property pane on shift clicking a widget

* remove console.log

* fix multiselect box issue

* add container on copy

* add analytics middleware

* refactor paste widget saga

* change flash element to accept array + revert refactor

* add logic to create containers from selected widgets

* update positions of grouped widgets

* fix comments + remove console

* update flashElementbyId to flashElementsById

* remove analytics middleware + remove unecessary imports

* add shorcut for grouping

* fix position issue when pasting

* allow grouping only when multi widgets are selected

* fix ux issues with widget grouping

* fix help text for grouping actions

* filter out the modal widget when calculting next row

* fix delete issue when grouping

* persist positin when grouping if there is no collision

* fix typo for new position

* changes for review comments

* changes for review comments

* fix position issue when pasting

* fix new container position issue

* move utils function to utils

* fix import issue

* fix the composite widget grouping issue

* fix table name bug

* remove repeated code

* move copied groups existence check;

* fix copied group check

Co-authored-by: Ashok Kumar M <35134347+marks0351@users.noreply.github.com>
Co-authored-by: root <root@DESKTOP-9GENCK0.localdomain>
Co-authored-by: Pawan Kumar <pawankumar@Pawans-MacBook-Pro.local>
2021-08-25 10:30:31 +05:30

782 lines
22 KiB
TypeScript

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<string, string>,
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<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,
);
}
/**
* 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;
};