fix: preserve order of widgets during multiple widget drag and drop (#31273)

## Description

Issues:
1. Order of widgets is not preserved during DnD.
2. DnD of small and large widgets together results in them being added
to the same row.

Fix:
1. Reuse zone utils to manage DnD of small and large widgets.
2. Account for the number of entities being added to preserve the order
of addition.

#### Media



https://github.com/appsmithorg/appsmith/assets/5424788/cc936242-4ea0-412e-b1bf-e62a196e228f



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

## Testing
>
#### How Has This Been Tested?
> Please describe the tests that you ran to verify your changes. Also
list any relevant details for your test configuration.
> Delete anything that is not relevant
- [x] Manual
- [ ] JUnit
- [ ] Jest
- [ ] Cypress

## 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
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] PR is being merged under a feature flag


#### QA activity:
- [ ] [Speedbreak
features](https://github.com/appsmithorg/TestSmith/wiki/Guidelines-for-test-plans#speedbreakers-)
have been covered
- [ ] Test plan covers all impacted features and [areas of
interest](https://github.com/appsmithorg/TestSmith/wiki/Guidelines-for-test-plans#areas-of-interest-)
- [ ] Test plan has been peer reviewed by project stakeholders and other
QA members
- [ ] Manually tested functionality on DP
- [ ] We had an implementation alignment call with stakeholders post QA
Round 2
- [ ] Cypress test cases have been added and approved by SDET/manual QA
- [ ] Added `Test Plan Approved` label after Cypress tests were reviewed
- [ ] Added `Test Plan Approved` label after JUnit tests were reviewed


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **New Features**
	- Enhanced widget movement logic for improved layout management.
- Introduced tracking for the number of items added to a section,
optimizing layout adjustments.
- **Refactor**
- Updated widget movement and relationship management functions for
better efficiency and clarity.
- **Documentation**
- Added explanatory comments in paste functionality for better
understanding of the logic involved.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Preet Sidhu 2024-02-27 11:55:35 +05:30 committed by GitHub
parent 88f8078500
commit c1207db0f5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 96 additions and 20 deletions

View File

@ -11,7 +11,6 @@ import type {
} from "../../utils/anvilTypes";
import { getWidget, getWidgets } from "sagas/selectors";
import { addWidgetsToPreset } from "../../utils/layouts/update/additionUtils";
import { moveWidgets } from "../../utils/layouts/update/moveUtils";
import type {
AnvilMoveWidgetsPayload,
AnvilNewWidgetsPayload,
@ -41,6 +40,7 @@ import {
getCreateWidgetPayload,
} from "layoutSystems/anvil/utils/widgetAdditionUtils";
import { updateAndSaveAnvilLayout } from "../../utils/anvilChecksUtils";
import { moveWidgetsToZone } from "layoutSystems/anvil/utils/layouts/update/zoneUtils";
// Function to retrieve highlighting information for the last row in the main canvas layout
export function* getMainCanvasLastRowHighlight() {
@ -363,8 +363,14 @@ export function* handleWidgetMovement(
highlight,
);
} else {
updatedWidgets = moveWidgets(allWidgets, movedWidgetIds, highlight);
updatedWidgets = yield call(
moveWidgetsToZone,
allWidgets,
movedWidgetIds,
highlight,
);
}
return updatedWidgets;
}

View File

@ -158,6 +158,7 @@ export function* addWidgetsToSection(
* Can this be prevent during DnD itself? i.e. Don't show highlights for sections that can't handle so many zones.
*/
const [zones, nonZones] = splitWidgets(draggedWidgets);
let itemsAdded = 0;
/**
* Step 2: Add zones to the section layout.
*/
@ -175,13 +176,14 @@ export function* addWidgetsToSection(
sectionProps,
sectionLayout,
sectionComp,
highlight,
{ ...highlight, rowIndex: highlight.rowIndex + itemsAdded },
zone,
);
sectionProps = res.canvasWidgets[sectionProps.widgetId];
sectionLayout = res.section;
canvasWidgets = res.canvasWidgets;
itemsAdded += 1;
}
/**
@ -198,7 +200,7 @@ export function* addWidgetsToSection(
createZoneAndAddWidgets,
canvasWidgets,
nonZones,
highlight,
{ ...highlight, rowIndex: highlight.rowIndex + itemsAdded },
sectionProps.widgetId,
);
sectionProps.children = [

View File

@ -16,6 +16,8 @@ import {
} from "../../widgetAdditionUtils";
import { isLargeWidget } from "../../widgetUtils";
import { anvilWidgets } from "widgets/anvil/constants";
import { severTiesFromParents, transformMovedWidgets } from "./moveUtils";
import type { WidgetProps } from "widgets/BaseWidget";
export function* createZoneAndAddWidgets(
allWidgets: CanvasWidgetsReduxState,
@ -27,7 +29,7 @@ export function* createZoneAndAddWidgets(
* Create Zone widget.
*/
const widgetId: string = generateReactKey();
let updatedWidgets: CanvasWidgetsReduxState = yield call(
const updatedWidgets: CanvasWidgetsReduxState = yield call(
addNewWidgetToDsl,
allWidgets,
getCreateWidgetPayload(widgetId, anvilWidgets.ZONE_WIDGET, parentId),
@ -37,9 +39,33 @@ export function* createZoneAndAddWidgets(
* Extract zone layout.
*/
const zoneProps: FlattenedWidgetProps = updatedWidgets[widgetId];
const { widgetId: zoneWidgetId } = zoneProps;
/**
* Add widgets to zone. and update relationships.
*/
const res: { canvasWidgets: CanvasWidgetsReduxState; zone: WidgetProps } =
yield call(
addWidgetsToZone,
updatedWidgets,
draggedWidgets,
highlight,
zoneProps,
);
return res;
}
export function* addWidgetsToZone(
allWidgets: CanvasWidgetsReduxState,
draggedWidgets: WidgetLayoutProps[],
highlight: AnvilHighlightInfo,
zone: WidgetProps,
) {
let updatedWidgets: CanvasWidgetsReduxState = { ...allWidgets };
const zoneProps = { ...zone };
const preset: LayoutProps[] = zoneProps.layout;
let zoneLayout: LayoutProps = preset[0];
const { widgetId: zoneWidgetId } = zoneProps;
/**
* If dragged widget is a new widget,
@ -67,6 +93,7 @@ export function* createZoneAndAddWidgets(
zoneLayout.layoutType,
);
let rowsAdded = 0;
if (smallWidgets.length) {
zoneLayout = addWidgetsToChildTemplate(
zoneLayout,
@ -74,34 +101,28 @@ export function* createZoneAndAddWidgets(
smallWidgets,
highlight,
);
rowsAdded += 1;
}
/**
* Add large widgets to the zone layout.
*/
largeWidgets.forEach((widget: WidgetLayoutProps) => {
zoneLayout = addWidgetsToChildTemplate(
zoneLayout,
zoneComp,
[widget],
highlight,
);
largeWidgets.forEach((widget: WidgetLayoutProps, index: number) => {
zoneLayout = addWidgetsToChildTemplate(zoneLayout, zoneComp, [widget], {
...highlight,
rowIndex: highlight.rowIndex + rowsAdded + index,
});
});
/**
* Update zone preset with the updated zone layout.
*/
preset[0] = zoneLayout;
/**
* Update zone widget with the updated preset.
*/
zoneProps.layout = preset;
zoneProps.layout = [zoneLayout];
return {
canvasWidgets: {
...updatedWidgets,
[zoneWidgetId]: zoneProps,
[zoneProps.widgetId]: zoneProps,
},
zone: zoneProps,
};
@ -154,3 +175,36 @@ function* updateDraggedWidgets(
}
return updatedWidgets;
}
export function* moveWidgetsToZone(
allWidgets: CanvasWidgetsReduxState,
movedWidgets: string[],
highlight: AnvilHighlightInfo,
) {
let widgets: CanvasWidgetsReduxState = { ...allWidgets };
/**
* Remove moved widgets from previous parents.
*/
widgets = severTiesFromParents(widgets, movedWidgets);
/**
* Get the new Zone parent and its Canvas.
*/
const { canvasId } = highlight;
const zone: FlattenedWidgetProps = widgets[canvasId];
/**
* Add moved widgets to the section.
*/
const { canvasWidgets } = yield call(
addWidgetsToZone,
widgets,
transformMovedWidgets(widgets, movedWidgets, highlight),
highlight,
zone,
);
return canvasWidgets;
}

View File

@ -70,6 +70,10 @@ function getPastingInfo(
};
}
const layout: LayoutProps = parent.layout[0];
/**
* If parentOrder.length === 1 => add the copied widgets at the end of the layout.
* Else find index of the child (usually selected widget) in the layout, and add the copied widgets after it.
*/
const info: Omit<PasteDestinationInfo, "parentOrder"> =
parentOrder.length === 1
? getLastIndexInLayout(parent)
@ -105,6 +109,10 @@ function getChildIndexInLayout(
const Comp: typeof BaseLayoutComponent = LayoutFactory.get(layout.layoutType);
if (Comp.rendersWidgets) {
/**
* if layout renders widgets, then find the index of the child in the layout.
* => If child is not found, then return -1.
*/
const index = (layout.layout as WidgetLayoutProps[]).findIndex(
(w: WidgetLayoutProps) => w.widgetId === childId,
);
@ -120,6 +128,9 @@ function getChildIndexInLayout(
rowIndex: [...rowIndex, index + 1],
};
} else {
/**
* If layout renders other layouts, then find the index of the child in the nested layouts.
*/
let res: Omit<PasteDestinationInfo, "parentOrder"> = {
alignment: FlexLayerAlignment.Start,
layoutOrder,
@ -134,6 +145,9 @@ function getChildIndexInLayout(
[...layoutOrder, each],
[...rowIndex, index + 1],
);
/**
* Acknowledge the result only if the child is found in the layout.
*/
if (temp.rowIndex[temp.rowIndex.length - 1] !== -1) res = temp;
},
);