[Feature] Widget Grouping Phase - 3 (Cut Copy Paste) (#5083)

* 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.
This commit is contained in:
Ashok Kumar M 2021-06-28 12:41:47 +05:30 committed by GitHub
parent d4dfa836b1
commit cf19b8e44d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 882 additions and 425 deletions

View File

@ -7,10 +7,17 @@ describe("Canvas Resize", function() {
});
it("Deleting bottom widget should resize canvas", function() {
const InitHeight = "2960px";
const FinalHeight = "1292px";
cy.get(commonlocators.dropTarget).should("have.css", "height", InitHeight);
cy.openPropertyPane("textwidget");
cy.intercept("PUT", "/api/v1/layouts/*/pages/*").as("deleteUpdate");
cy.get(commonlocators.deleteWidget).click();
cy.get(commonlocators.dropTarget).should("have.css", "height", FinalHeight);
cy.wait("@deleteUpdate").then((response) => {
const dsl = response.response.body.data.dsl;
cy.get(commonlocators.dropTarget).should(
"have.css",
"height",
`${dsl.bottomRow}px`,
);
});
});
});

View File

@ -1,4 +1,5 @@
import { ReduxActionTypes, ReduxAction } from "constants/ReduxActionConstants";
import { MAIN_CONTAINER_WIDGET_ID } from "constants/WidgetConstants";
export const selectWidgetAction = (
widgetId?: string,
@ -16,7 +17,7 @@ export const selectWidgetInitAction = (
payload: { widgetId, isMultiSelect },
});
export const selectAllWidgetsAction = (
export const selectMultipleWidgetsAction = (
widgetIds?: string[],
): ReduxAction<{ widgetIds?: string[] }> => {
return {
@ -25,7 +26,7 @@ export const selectAllWidgetsAction = (
};
};
export const selectMultipleWidgetsAction = (
export const silentAddSelectionsAction = (
widgetIds?: string[],
): ReduxAction<{ widgetIds?: string[] }> => {
return {
@ -43,9 +44,21 @@ export const deselectMultipleWidgetsAction = (
};
};
export const selectAllWidgetsInitAction = () => {
export const selectAllWidgetsInCanvasInitAction = (
canvasId = MAIN_CONTAINER_WIDGET_ID,
): ReduxAction<{ canvasId: string }> => {
return {
type: ReduxActionTypes.SELECT_ALL_WIDGETS_IN_CANVAS_INIT,
payload: {
canvasId,
},
};
};
export const selectMultipleWidgetsInitAction = (widgetIds: string[]) => {
return {
type: ReduxActionTypes.SELECT_MULTIPLE_WIDGETS_INIT,
payload: { widgetIds },
};
};

View File

@ -5,6 +5,7 @@ import { ComponentProps } from "./BaseComponent";
import { invisible } from "constants/DefaultTheme";
import { Color } from "constants/Colors";
import { generateClassName, getCanvasClassName } from "utils/generators";
import { useCanvasMinHeightUpdateHook } from "utils/hooks/useCanvasMinHeightUpdateHook";
const scrollContents = css`
overflow-y: auto;
@ -48,7 +49,7 @@ const StyledContainerComponent = styled.div<
function ContainerComponent(props: ContainerComponentProps) {
const containerStyle = props.containerStyle || "card";
const containerRef: RefObject<HTMLDivElement> = useRef<HTMLDivElement>(null);
useCanvasMinHeightUpdateHook(props.widgetId, props.minHeight);
useEffect(() => {
if (!props.shouldScrollContents) {
const supportsNativeSmoothScroll =
@ -89,6 +90,7 @@ export interface ContainerComponentProps extends ComponentProps {
resizeDisabled?: boolean;
selected?: boolean;
focused?: boolean;
minHeight?: number;
}
export default ContainerComponent;

View File

@ -306,6 +306,7 @@ export const ReduxActionTypes: { [key: string]: string } = {
SELECT_WIDGET: "SELECT_WIDGET",
SELECT_MULTIPLE_WIDGETS: "SELECT_MULTIPLE_WIDGETS",
SELECT_MULTIPLE_WIDGETS_INIT: "SELECT_MULTIPLE_WIDGETS_INIT",
SELECT_ALL_WIDGETS_IN_CANVAS_INIT: "SELECT_ALL_WIDGETS_IN_CANVAS_INIT",
DESELECT_WIDGETS: "DESELECT_WIDGETS",
SELECT_WIDGETS: "SELECT_WIDGETS",
FOCUS_WIDGET: "FOCUS_WIDGET",

View File

@ -103,8 +103,6 @@ export function PageContextMenu(props: {
});
}
console.log({ props });
if (!props.isDefaultPage) {
optionTree.push({
value: "delete",

View File

@ -1,29 +1,9 @@
// These need to be at the top to avoid imports not being mocked. ideally should be in setup.ts but will override for all other tests
const mockGenerator = function*() {
yield all([]);
};
// top avoid the first middleware run which wud initiate all sagas.
jest.mock("sagas", () => ({
rootSaga: mockGenerator,
}));
// only the deafault exports are mocked to avoid overriding utilities exported out of them. defaults are marked to avoid worker initiation and page api calls in tests.
jest.mock("sagas/EvaluationsSaga", () => ({
...jest.requireActual("sagas/EvaluationsSaga"),
default: mockGenerator,
}));
jest.mock("sagas/PageSagas", () => ({
...jest.requireActual("sagas/PageSagas"),
default: mockGenerator,
}));
import React from "react";
import {
buildChildren,
widgetCanvasFactory,
} from "test/factories/WidgetFactoryUtils";
import { render, fireEvent } from "test/testUtils";
import { act, render, fireEvent } from "test/testUtils";
import GlobalHotKeys from "./GlobalHotKeys";
import MainContainer from "./MainContainer";
import { MemoryRouter } from "react-router-dom";
@ -34,72 +14,316 @@ import { all } from "@redux-saga/core/effects";
import {
dispatchTestKeyboardEventWithCode,
MockApplication,
mockGetCanvasWidgetDsl,
MockPageDSL,
useMockDsl,
} from "test/testCommon";
const mockGetCanvasWidgetDsl = jest.spyOn(utilities, "getCanvasWidgetDsl");
const mockGetIsFetchingPage = jest.spyOn(utilities, "getIsFetchingPage");
function UpdatedMainContaner({ dsl }: any) {
useMockDsl(dsl);
return <MainContainer />;
}
it("Cmd + A - select all widgets on canvas", () => {
const children: any = buildChildren([
{ type: "TABS_WIDGET" },
{ type: "SWITCH_WIDGET" },
]);
const dsl: any = widgetCanvasFactory.build({
children,
});
mockGetCanvasWidgetDsl.mockImplementation(() => dsl);
mockGetIsFetchingPage.mockImplementation(() => false);
const component = render(
<MemoryRouter initialEntries={["/applications/app_id/pages/page_id/edit"]}>
<MockApplication>
<GlobalHotKeys>
<UpdatedMainContaner dsl={dsl} />
</GlobalHotKeys>
</MockApplication>
</MemoryRouter>,
{ initialState: store.getState(), sagasToRun: sagasToRunForTests },
);
let propPane = component.queryByTestId("t--propertypane");
expect(propPane).toBeNull();
const canvasWidgets = component.queryAllByTestId("test-widget");
expect(canvasWidgets.length).toBe(2);
if (canvasWidgets[1].firstChild) {
fireEvent.mouseOver(canvasWidgets[1].firstChild);
fireEvent.click(canvasWidgets[1].firstChild);
import { MockCanvas } from "test/testMockedWidgets";
import { MAIN_CONTAINER_WIDGET_ID } from "constants/WidgetConstants";
describe("Select all hotkey", () => {
const mockGetIsFetchingPage = jest.spyOn(utilities, "getIsFetchingPage");
const spyGetCanvasWidgetDsl = jest.spyOn(utilities, "getCanvasWidgetDsl");
function UpdatedMainContainer({ dsl }: any) {
useMockDsl(dsl);
return <MainContainer />;
}
propPane = component.queryByTestId("t--propertypane");
expect(propPane).not.toBeNull();
// These need to be at the top to avoid imports not being mocked. ideally should be in setup.ts but will override for all other tests
beforeAll(() => {
const mockGenerator = function*() {
yield all([]);
};
const artBoard: any = component.queryByTestId("t--canvas-artboard");
// deselect all other widgets
fireEvent.click(artBoard);
// top avoid the first middleware run which wud initiate all sagas.
jest.mock("sagas", () => ({
rootSaga: mockGenerator,
}));
dispatchTestKeyboardEventWithCode(
component.container,
"keydown",
"A",
65,
false,
true,
);
let selectedWidgets = component.queryAllByTestId(
"t--widget-propertypane-toggle",
);
expect(selectedWidgets.length).toBe(2);
dispatchTestKeyboardEventWithCode(
component.container,
"keydown",
"escape",
27,
false,
false,
);
selectedWidgets = component.queryAllByTestId("t--widget-propertypane-toggle");
expect(selectedWidgets.length).toBe(0);
// only the deafault exports are mocked to avoid overriding utilities exported out of them. defaults are marked to avoid worker initiation and page api calls in tests.
jest.mock("sagas/EvaluationsSaga", () => ({
...jest.requireActual("sagas/EvaluationsSaga"),
default: mockGenerator,
}));
jest.mock("sagas/PageSagas", () => ({
...jest.requireActual("sagas/PageSagas"),
default: mockGenerator,
}));
});
it("Cmd + A - select all widgets on canvas", () => {
const children: any = buildChildren([
{ type: "TABS_WIDGET" },
{ type: "SWITCH_WIDGET" },
]);
const dsl: any = widgetCanvasFactory.build({
children,
});
spyGetCanvasWidgetDsl.mockImplementation(mockGetCanvasWidgetDsl);
mockGetIsFetchingPage.mockImplementation(() => false);
const component = render(
<MemoryRouter
initialEntries={["/applications/app_id/pages/page_id/edit"]}
>
<MockApplication>
<GlobalHotKeys>
<UpdatedMainContainer dsl={dsl} />
</GlobalHotKeys>
</MockApplication>
</MemoryRouter>,
{ initialState: store.getState(), sagasToRun: sagasToRunForTests },
);
let propPane = component.queryByTestId("t--propertypane");
expect(propPane).toBeNull();
const canvasWidgets = component.queryAllByTestId("test-widget");
expect(canvasWidgets.length).toBe(2);
if (canvasWidgets[1].firstChild) {
fireEvent.mouseOver(canvasWidgets[1].firstChild);
fireEvent.click(canvasWidgets[1].firstChild);
}
propPane = component.queryByTestId("t--propertypane");
expect(propPane).not.toBeNull();
const artBoard: any = component.queryByTestId("t--canvas-artboard");
// deselect all other widgets
fireEvent.click(artBoard);
dispatchTestKeyboardEventWithCode(
component.container,
"keydown",
"A",
65,
false,
true,
);
let selectedWidgets = component.queryAllByTestId(
"t--widget-propertypane-toggle",
);
expect(selectedWidgets.length).toBe(2);
dispatchTestKeyboardEventWithCode(
component.container,
"keydown",
"escape",
27,
false,
false,
);
selectedWidgets = component.queryAllByTestId(
"t--widget-propertypane-toggle",
);
expect(selectedWidgets.length).toBe(0);
act(() => {
dispatchTestKeyboardEventWithCode(
component.container,
"keydown",
"A",
65,
false,
true,
);
});
selectedWidgets = component.queryAllByTestId(
"t--widget-propertypane-toggle",
);
expect(selectedWidgets.length).toBe(2);
act(() => {
dispatchTestKeyboardEventWithCode(
component.container,
"keydown",
"C",
67,
false,
true,
);
});
act(() => {
dispatchTestKeyboardEventWithCode(
component.container,
"keydown",
"V",
86,
false,
true,
);
});
selectedWidgets = component.queryAllByTestId(
"t--widget-propertypane-toggle",
);
expect(selectedWidgets.length).toBe(2);
});
afterAll(() => jest.resetModules());
});
describe("Cut/Copy/Paste hotkey", () => {
it("Should copy and paste all selected widgets with hotkey cmd + c and cmd + v ", async () => {
const children: any = buildChildren([
{
type: "TABS_WIDGET",
topRow: 5,
bottomRow: 30,
leftColumn: 5,
rightColumn: 30,
},
{
type: "SWITCH_WIDGET",
topRow: 5,
bottomRow: 10,
leftColumn: 40,
rightColumn: 48,
},
]);
const dsl: any = widgetCanvasFactory.build({
children,
});
const component = render(
<MockPageDSL dsl={dsl}>
<GlobalHotKeys>
<MockCanvas />
</GlobalHotKeys>
</MockPageDSL>,
);
const artBoard: any = await component.queryByTestId("t--canvas-artboard");
// deselect all other widgets
fireEvent.click(artBoard);
act(() => {
dispatchTestKeyboardEventWithCode(
component.container,
"keydown",
"A",
65,
false,
true,
);
});
let selectedWidgets = await component.queryAllByTestId(
"t--widget-propertypane-toggle",
);
expect(selectedWidgets.length).toBe(2);
act(() => {
dispatchTestKeyboardEventWithCode(
component.container,
"keydown",
"C",
67,
false,
true,
);
});
act(() => {
dispatchTestKeyboardEventWithCode(
component.container,
"keydown",
"V",
86,
false,
true,
);
});
await component.findByText(children[0].widgetName + "Copy");
act(() => {
dispatchTestKeyboardEventWithCode(
component.container,
"keydown",
"A",
65,
false,
true,
);
});
selectedWidgets = await component.queryAllByTestId(
"t--widget-propertypane-toggle",
);
expect(selectedWidgets.length).toBe(4);
});
it("Should cut and paste all selected widgets with hotkey cmd + x and cmd + v ", async () => {
const children: any = buildChildren([
{
type: "TABS_WIDGET",
topRow: 5,
bottomRow: 30,
leftColumn: 5,
rightColumn: 30,
parentId: MAIN_CONTAINER_WIDGET_ID,
},
{
type: "SWITCH_WIDGET",
topRow: 5,
bottomRow: 10,
leftColumn: 40,
rightColumn: 48,
parentId: MAIN_CONTAINER_WIDGET_ID,
},
]);
const dsl: any = widgetCanvasFactory.build({
children,
});
const component = render(
<MockPageDSL dsl={dsl}>
<GlobalHotKeys>
<MockCanvas />
</GlobalHotKeys>
</MockPageDSL>,
);
const artBoard: any = await component.queryByTestId("t--canvas-artboard");
// deselect all other widgets
fireEvent.click(artBoard);
act(() => {
dispatchTestKeyboardEventWithCode(
component.container,
"keydown",
"A",
65,
false,
true,
);
});
let selectedWidgets = await component.queryAllByTestId(
"t--widget-propertypane-toggle",
);
expect(selectedWidgets.length).toBe(2);
act(() => {
dispatchTestKeyboardEventWithCode(
component.container,
"keydown",
"X",
88,
false,
true,
);
});
await component.findByTestId("canvas-0");
selectedWidgets = await component.queryAllByTestId(
"t--widget-propertypane-toggle",
);
expect(selectedWidgets.length).toBe(0);
act(() => {
dispatchTestKeyboardEventWithCode(
component.container,
"keydown",
"V",
86,
false,
true,
);
});
await component.findByText(children[0].widgetName);
act(() => {
dispatchTestKeyboardEventWithCode(
component.container,
"keydown",
"A",
65,
false,
true,
);
});
selectedWidgets = await component.queryAllByTestId(
"t--widget-propertypane-toggle",
);
expect(selectedWidgets.length).toBe(2);
});
});
afterAll(() => jest.resetModules());

View File

@ -11,8 +11,8 @@ import {
pasteWidget,
} from "actions/widgetActions";
import {
selectAllWidgetsInitAction,
selectAllWidgetsAction,
selectAllWidgetsInCanvasInitAction,
selectMultipleWidgetsAction,
} from "actions/widgetSelectionActions";
import { toggleShowGlobalSearchModal } from "actions/globalSearchActions";
import { isMac } from "utils/helpers";
@ -64,12 +64,6 @@ class GlobalHotKeys extends React.Component<Props> {
return false;
}
public areMultipleWidgetsSelected() {
const multipleWidgetsSelected =
this.props.selectedWidgets && this.props.selectedWidgets.length >= 2;
return !!multipleWidgetsSelected;
}
public onOnmnibarHotKeyDown(e: KeyboardEvent) {
e.preventDefault();
this.props.toggleShowGlobalSearchModal();
@ -129,10 +123,7 @@ class GlobalHotKeys extends React.Component<Props> {
group="Canvas"
label="Copy Widget"
onKeyDown={(e: any) => {
if (
this.stopPropagationIfWidgetSelected(e) &&
!this.areMultipleWidgetsSelected()
) {
if (this.stopPropagationIfWidgetSelected(e)) {
this.props.copySelectedWidget();
}
}}
@ -174,10 +165,7 @@ class GlobalHotKeys extends React.Component<Props> {
group="Canvas"
label="Cut Widget"
onKeyDown={(e: any) => {
if (
this.stopPropagationIfWidgetSelected(e) &&
!this.areMultipleWidgetsSelected()
) {
if (this.stopPropagationIfWidgetSelected(e)) {
this.props.cutSelectedWidget();
}
}}
@ -250,8 +238,8 @@ const mapDispatchToProps = (dispatch: any) => {
resetCommentMode: () => dispatch(setCommentModeAction(false)),
openDebugger: () => dispatch(showDebugger()),
closeProppane: () => dispatch(closePropertyPane()),
selectAllWidgetsInit: () => dispatch(selectAllWidgetsInitAction()),
deselectAllWidgets: () => dispatch(selectAllWidgetsAction([])),
selectAllWidgetsInit: () => dispatch(selectAllWidgetsInCanvasInitAction()),
deselectAllWidgets: () => dispatch(selectMultipleWidgetsAction([])),
executeAction: () => dispatch(runActionViaShortcut()),
};
};

View File

@ -158,10 +158,12 @@ export const CanvasSelectionArena = memo(
const onMouseLeave = () => {
document.body.addEventListener("mouseup", onMouseUp, false);
document.body.addEventListener("click", onClick, false);
};
const onMouseEnter = () => {
document.body.removeEventListener("mouseup", onMouseUp);
document.body.removeEventListener("click", onClick);
};
const onClick = (e: any) => {
@ -236,6 +238,7 @@ export const CanvasSelectionArena = memo(
currentPageId,
mainContainer.rightColumn,
mainContainer.bottomRow,
mainContainer.minHeight,
]);
return appMode === APP_MODE.EDIT ? (

View File

@ -1,4 +1,4 @@
import { selectAllWidgetsAction } from "actions/widgetSelectionActions";
import { selectMultipleWidgetsAction } from "actions/widgetSelectionActions";
import { OccupiedSpace } from "constants/editorConstants";
import { ReduxAction, ReduxActionTypes } from "constants/ReduxActionConstants";
import {
@ -99,7 +99,7 @@ function* selectAllWidgetsInAreaSaga(
const currentSelectedWidgets: string[] = yield select(getSelectedWidgets);
if (!isEqual(filteredWidgetsToSelect, currentSelectedWidgets)) {
yield put(selectAllWidgetsAction(filteredWidgetsToSelect));
yield put(selectMultipleWidgetsAction(filteredWidgetsToSelect));
}
}
}

View File

@ -16,12 +16,7 @@ import {
CanvasWidgetsReduxState,
FlattenedWidgetProps,
} from "reducers/entityReducers/canvasWidgetsReducer";
import {
getSelectedWidget,
getWidget,
getWidgetMetaProps,
getWidgets,
} from "./selectors";
import { getSelectedWidget, getWidget, getWidgets } from "./selectors";
import {
generateWidgetProps,
updateWidgetPosition,
@ -93,7 +88,10 @@ import {
closePropertyPane,
forceOpenPropertyPane,
} from "actions/widgetActions";
import { selectWidgetInitAction } from "actions/widgetSelectionActions";
import {
selectMultipleWidgetsInitAction,
selectWidgetInitAction,
} from "actions/widgetSelectionActions";
import { getDataTree } from "selectors/dataTreeSelectors";
import {
@ -124,7 +122,9 @@ import AppsmithConsole from "utils/AppsmithConsole";
import { ENTITY_TYPE } from "entities/AppsmithConsole";
import LOG_TYPE from "entities/AppsmithConsole/logtype";
import {
checkIfPastingIntoListWidget,
doesTriggerPathsContainPropertyPath,
getParentWidgetIdForPasting,
getWidgetChildren,
handleSpecificCasesWhilePasting,
} from "./WidgetOperationUtils";
@ -443,7 +443,10 @@ const resizeCanvasToLowestWidget = (
return;
}
let lowestBottomRow = 0;
let lowestBottomRow = Math.ceil(
(finalWidgets[parentId].minHeight || 0) /
GridDefaults.DEFAULT_GRID_ROW_HEIGHT,
);
const childIds = finalWidgets[parentId].children || [];
// find lowest row
childIds.forEach((cId) => {
@ -492,6 +495,9 @@ export function* deleteAllSelectedWidgetsSaga(
parentUpdatedWidgets,
falttendedWidgets.map((widgets: any) => widgets.widgetId),
);
// assuming only widgets with same parent can be selected
const parentId = widgets[selectedWidgets[0]].parentId;
resizeCanvasToLowestWidget(finalWidgets, parentId);
yield put(updateAndSaveLayout(finalWidgets));
yield put(selectWidgetInitAction(""));
@ -815,8 +821,15 @@ export function* undoDeleteSaga(action: ReduxAction<{ widgetId: string }>) {
},
stateWidgets,
);
const parentId = deletedWidgets[0].parentId;
if (parentId) {
resizeCanvasToLowestWidget(finalWidgets, parentId);
}
yield put(updateAndSaveLayout(finalWidgets));
deletedWidgetIds.forEach((widgetId) => {
setTimeout(() => flashElementById(widgetId), 100);
});
yield put(selectMultipleWidgetsInitAction(deletedWidgetIds));
if (deletedWidgetIds.length === 1) {
yield put(forceOpenPropertyPane(action.payload.widgetId));
}
@ -1319,14 +1332,26 @@ function* updateCanvasSize(
}
}
function* createWidgetCopy() {
const selectedWidget = yield select(getSelectedWidget);
if (!selectedWidget) return;
const widgets = yield select(getWidgets);
const widgetsToStore = getAllWidgetsInTree(selectedWidget.widgetId, widgets);
return yield saveCopiedWidgets(
JSON.stringify({ widgetId: selectedWidget.widgetId, list: widgetsToStore }),
function* createWidgetCopy(widget: FlattenedWidgetProps) {
const allWidgets: { [widgetId: string]: FlattenedWidgetProps } = yield select(
getWidgets,
);
const widgetsToStore = getAllWidgetsInTree(widget.widgetId, allWidgets);
return {
widgetId: widget.widgetId,
list: widgetsToStore,
parentId: widget.parentId,
};
}
function* createSelectedWidgetsCopy(selectedWidgets: FlattenedWidgetProps[]) {
if (!selectedWidgets || !selectedWidgets.length) return;
const widgetListsToStore: {
widgetId: string;
parentId: string;
list: FlattenedWidgetProps[];
}[] = yield all(selectedWidgets.map((each) => call(createWidgetCopy, each)));
return yield saveCopiedWidgets(JSON.stringify(widgetListsToStore));
}
/**
@ -1337,8 +1362,11 @@ function* createWidgetCopy() {
* @returns
*/
function* copyWidgetSaga(action: ReduxAction<{ isShortcut: boolean }>) {
const selectedWidget = yield select(getSelectedWidget);
if (!selectedWidget) {
const allWidgets: { [widgetId: string]: FlattenedWidgetProps } = yield select(
getWidgets,
);
const selectedWidgets: string[] = yield select(getSelectedWidgets);
if (!selectedWidgets) {
Toaster.show({
text: createMessage(ERROR_WIDGET_COPY_NO_WIDGET_SELECTED),
variant: Variant.info,
@ -1346,7 +1374,11 @@ function* copyWidgetSaga(action: ReduxAction<{ isShortcut: boolean }>) {
return;
}
if (selectedWidget.disallowCopy === true) {
const allAllowedToCopy = selectedWidgets.some((each) => {
return !allWidgets[each].disallowCopy;
});
if (!allAllowedToCopy) {
Toaster.show({
text: createMessage(ERROR_WIDGET_COPY_NOT_ALLOWED),
variant: Variant.info,
@ -1354,20 +1386,28 @@ function* copyWidgetSaga(action: ReduxAction<{ isShortcut: boolean }>) {
return;
}
const selectedWidgetProps = selectedWidgets.map((each) => allWidgets[each]);
const saveResult = yield createWidgetCopy();
const saveResult = yield createSelectedWidgetsCopy(selectedWidgetProps);
const eventName = action.payload.isShortcut
? "WIDGET_COPY_VIA_SHORTCUT"
: "WIDGET_COPY";
AnalyticsUtil.logEvent(eventName, {
widgetName: selectedWidget.widgetName,
widgetType: selectedWidget.type,
selectedWidgetProps.forEach((each) => {
const eventName = action.payload.isShortcut
? "WIDGET_COPY_VIA_SHORTCUT"
: "WIDGET_COPY";
AnalyticsUtil.logEvent(eventName, {
widgetName: each.widgetName,
widgetType: each.type,
});
});
if (saveResult) {
Toaster.show({
text: createMessage(WIDGET_COPY, selectedWidget.widgetName),
text: createMessage(
WIDGET_COPY,
selectedWidgetProps.length > 1
? `${selectedWidgetProps.length} Widgets`
: selectedWidgetProps[0].widgetName,
),
variant: Variant.success,
});
}
@ -1377,16 +1417,26 @@ export function calculateNewWidgetPosition(
widget: WidgetProps,
parentId: string,
canvasWidgets: { [widgetId: string]: FlattenedWidgetProps },
parentBottomRow?: number,
persistColumnPosition = false,
) {
// Note: This is a very simple algorithm.
// We take the bottom most widget in the canvas, then calculate the top,left,right,bottom
// co-ordinates for the new widget, such that it can be placed at the bottom of the canvas.
const nextAvailableRow = nextAvailableRowInContainer(parentId, canvasWidgets);
const nextAvailableRow = parentBottomRow
? parentBottomRow
: nextAvailableRowInContainer(parentId, canvasWidgets);
return {
leftColumn: 0,
rightColumn: widget.rightColumn - widget.leftColumn,
topRow: nextAvailableRow,
bottomRow: nextAvailableRow + (widget.bottomRow - widget.topRow),
leftColumn: persistColumnPosition ? widget.leftColumn : 0,
rightColumn: persistColumnPosition
? widget.rightColumn
: widget.rightColumn - widget.leftColumn,
topRow: parentBottomRow
? nextAvailableRow + widget.topRow
: nextAvailableRow,
bottomRow: parentBottomRow
? nextAvailableRow + widget.bottomRow
: nextAvailableRow + (widget.bottomRow - widget.topRow),
};
}
@ -1398,7 +1448,12 @@ function* getEntityNames() {
function getNextWidgetName(
widgets: CanvasWidgetsReduxState,
type: WidgetType,
evalTree: Record<string, unknown>,
evalTree: {
bottomRow: any;
leftColumn: any;
rightColumn: any;
topRow: any;
},
options?: Record<string, unknown>,
) {
// Compute the new widget's name
@ -1423,286 +1478,274 @@ function getNextWidgetName(
* this saga create a new widget from the copied one to store
*/
function* pasteWidgetSaga() {
const copiedWidgets: {
const copiedWidgetGroups: {
widgetId: string;
parentId: string;
list: WidgetProps[];
} = yield getCopiedWidgets();
// Don't try to paste if there is no copied widget
if (!copiedWidgets) return;
const copiedWidgetId = copiedWidgets.widgetId;
const copiedWidget = copiedWidgets.list.find(
(widget) => widget.widgetId === copiedWidgetId,
}[] = yield getCopiedWidgets();
if (!Array.isArray(copiedWidgetGroups)) {
return;
// to avoid invoking old copied widgets
}
const stateWidgets: CanvasWidgetsReduxState = yield select(getWidgets);
let selectedWidget: FlattenedWidgetProps | undefined = yield select(
getSelectedWidget,
);
if (copiedWidget) {
// Log the paste event
AnalyticsUtil.logEvent("WIDGET_PASTE", {
widgetName: copiedWidget.widgetName,
widgetType: copiedWidget.type,
});
const stateWidgets = yield select(getWidgets);
let widgets = { ...stateWidgets };
const pastingIntoWidgetId: string = yield getParentWidgetIdForPasting(
{ ...stateWidgets },
selectedWidget,
);
let selectedWidget = yield select(getSelectedWidget);
// 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?.type === WidgetTypes.LIST_WIDGET) {
const childrenIds: string[] = yield call(
getWidgetChildren,
selectedWidget.children[0],
);
const firstChildId = childrenIds[0];
selectedWidget = yield select(getWidget, firstChildId);
}
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.parentId !== MAIN_CONTAINER_WIDGET_ID &&
widgets[selectedWidget.parentId] &&
widgets[selectedWidget.parentId].children &&
widgets[selectedWidget.parentId].children.length > 0
) {
parentWidget = widgets[selectedWidget.parentId];
newWidgetParentId = selectedWidget.parentId;
}
// Select the selected widget if the widget is container like
if (selectedWidget.children) {
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 = 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,
selectedWidget = yield checkIfPastingIntoListWidget(selectedWidget);
let widgets = { ...stateWidgets };
const newlyCreatedWidgetIds: string[] = [];
const sortedWidgetList = copiedWidgetGroups.sort(
(a, b) => a.list[0].topRow - b.list[0].topRow,
);
const copiedGroupTopRow = sortedWidgetList[0].list[0].topRow;
const nextAvailableRow: number = nextAvailableRowInContainer(
pastingIntoWidgetId,
widgets,
);
yield all(
copiedWidgetGroups.map((copiedWidgets) =>
call(function*() {
// Don't try to paste if there is no copied widget
if (!copiedWidgets) return;
const copiedWidgetId = copiedWidgets.widgetId;
const unUpdatedCopyOfWidget = copiedWidgets.list.find(
(widget) => widget.widgetId === copiedWidgetId,
);
if (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;
}
}
// Compute the new widget's positional properties
const {
bottomRow,
leftColumn,
rightColumn,
topRow,
} = yield calculateNewWidgetPosition(
copiedWidget,
newWidgetParentId,
widgets,
);
if (unUpdatedCopyOfWidget) {
const copiedWidget = {
...unUpdatedCopyOfWidget,
topRow: unUpdatedCopyOfWidget.topRow - copiedGroupTopRow,
bottomRow: unUpdatedCopyOfWidget.bottomRow - copiedGroupTopRow,
};
const evalTree = yield select(getDataTree);
// Log the paste event
AnalyticsUtil.logEvent("WIDGET_PASTE", {
widgetName: copiedWidget.widgetName,
widgetType: copiedWidget.type,
});
// Get a flat list of all the widgets to be updated
const widgetList = copiedWidgets.list;
const widgetIdMap: Record<string, string> = {};
const widgetNameMap: Record<string, string> = {};
const newWidgetList: FlattenedWidgetProps[] = [];
let newWidgetId: string = copiedWidget.widgetId;
// Generate new widgetIds for the flat list of all the widgets to be updated
widgetList.forEach((widget) => {
// Create a copy of the widget properties
const newWidget = cloneDeep(widget);
newWidget.widgetId = generateReactKey();
// Add the new widget id so that it maps the previous widget id
widgetIdMap[widget.widgetId] = newWidget.widgetId;
// Add the new widget to the list
newWidgetList.push(newWidget);
});
// For each of the new widgets generated
for (let i = 0; i < newWidgetList.length; i++) {
const widget = newWidgetList[i];
const oldWidgetName = widget.widgetName;
// Update the children widgetIds if it has children
if (widget.children && widget.children.length > 0) {
widget.children.forEach((childWidgetId: string, index: number) => {
if (widget.children) {
widget.children[index] = widgetIdMap[childWidgetId];
}
});
}
// Update the tabs for the tabs widget.
if (widget.tabsObj && widget.type === WidgetTypes.TABS_WIDGET) {
try {
const tabs = Object.values(widget.tabsObj);
if (Array.isArray(tabs)) {
widget.tabsObj = tabs.reduce((obj: any, tab) => {
tab.widgetId = widgetIdMap[tab.widgetId];
obj[tab.id] = tab;
return obj;
}, {});
}
} catch (error) {
log.debug("Error updating tabs", error);
}
}
// Update the table widget column properties
if (widget.type === WidgetTypes.TABLE_WIDGET) {
try {
const oldWidgetName = widget.widgetName;
const newWidgetName = getNextWidgetName(
// Compute the new widget's positional properties
const {
bottomRow,
leftColumn,
rightColumn,
topRow,
} = yield calculateNewWidgetPosition(
copiedWidget,
pastingIntoWidgetId,
widgets,
widget.type,
evalTree,
nextAvailableRow,
true,
);
// If the primaryColumns of the table exist
if (widget.primaryColumns) {
// For each column
for (const [columnId, column] of Object.entries(
widget.primaryColumns,
)) {
// For each property in the column
for (const [key, value] of Object.entries(
column as ColumnProperties,
)) {
// Replace reference of previous widget with the new widgetName
// This handles binding scenarios like `{{Table2.tableData.map((currentRow) => (currentRow.id))}}`
widget.primaryColumns[columnId][key] = isString(value)
? value.replace(`${oldWidgetName}.`, `${newWidgetName}.`)
: value;
// goToNextAvailableRow = true,
// persistColumnPosition = false,
const evalTree = yield select(getDataTree);
// Get a flat list of all the widgets to be updated
const widgetList = copiedWidgets.list;
const widgetIdMap: Record<string, string> = {};
const widgetNameMap: Record<string, string> = {};
const newWidgetList: FlattenedWidgetProps[] = [];
let newWidgetId: string = copiedWidget.widgetId;
// Generate new widgetIds for the flat list of all the widgets to be updated
widgetList.forEach((widget) => {
// Create a copy of the widget properties
const newWidget = cloneDeep(widget);
newWidget.widgetId = generateReactKey();
// Add the new widget id so that it maps the previous widget id
widgetIdMap[widget.widgetId] = newWidget.widgetId;
// Add the new widget to the list
newWidgetList.push(newWidget);
});
// For each of the new widgets generated
for (let i = 0; i < newWidgetList.length; i++) {
const widget = newWidgetList[i];
const oldWidgetName = widget.widgetName;
// Update the children widgetIds if it has children
if (widget.children && widget.children.length > 0) {
widget.children.forEach(
(childWidgetId: string, index: number) => {
if (widget.children) {
widget.children[index] = widgetIdMap[childWidgetId];
}
},
);
}
// Update the tabs for the tabs widget.
if (widget.tabsObj && widget.type === WidgetTypes.TABS_WIDGET) {
try {
const tabs = Object.values(widget.tabsObj);
if (Array.isArray(tabs)) {
widget.tabsObj = tabs.reduce((obj: any, tab) => {
tab.widgetId = widgetIdMap[tab.widgetId];
obj[tab.id] = tab;
return obj;
}, {});
}
} catch (error) {
log.debug("Error updating tabs", error);
}
}
}
// Use the new widget name we used to replace the column properties above.
widget.widgetName = newWidgetName;
} catch (error) {
log.debug("Error updating table widget properties", error);
}
}
// If it is the copied widget, update position properties
if (widget.widgetId === widgetIdMap[copiedWidget.widgetId]) {
newWidgetId = widget.widgetId;
widget.leftColumn = leftColumn;
widget.topRow = topRow;
widget.bottomRow = bottomRow;
widget.rightColumn = rightColumn;
widget.parentId = newWidgetParentId;
// Also, update the parent widget in the canvas widgets
// to include this new copied widget's id in the parent's children
let parentChildren = [widget.widgetId];
if (
widgets[newWidgetParentId].children &&
Array.isArray(widgets[newWidgetParentId].children)
) {
// Add the new child to existing children
parentChildren = parentChildren.concat(
widgets[newWidgetParentId].children,
);
}
widgets = {
...widgets,
[newWidgetParentId]: {
...widgets[newWidgetParentId],
children: parentChildren,
},
};
// If the copied widget's boundaries exceed the parent's
// Make the parent scrollable
if (
widgets[newWidgetParentId].bottomRow *
widgets[widget.parentId].parentRowSpace <=
widget.bottomRow * widget.parentRowSpace
) {
if (widget.parentId !== MAIN_CONTAINER_WIDGET_ID) {
const parent = widgets[widgets[newWidgetParentId].parentId];
widgets[widgets[newWidgetParentId].parentId] = {
...parent,
shouldScrollContents: true,
};
// Update the table widget column properties
if (widget.type === WidgetTypes.TABLE_WIDGET) {
try {
const oldWidgetName = widget.widgetName;
const newWidgetName = getNextWidgetName(
widgets,
widget.type,
evalTree,
);
// If the primaryColumns of the table exist
if (widget.primaryColumns) {
// For each column
for (const [columnId, column] of Object.entries(
widget.primaryColumns,
)) {
// For each property in the column
for (const [key, value] of Object.entries(
column as ColumnProperties,
)) {
// Replace reference of previous widget with the new widgetName
// This handles binding scenarios like `{{Table2.tableData.map((currentRow) => (currentRow.id))}}`
widget.primaryColumns[columnId][key] = isString(value)
? value.replace(
`${oldWidgetName}.`,
`${newWidgetName}.`,
)
: value;
}
}
}
// Use the new widget name we used to replace the column properties above.
widget.widgetName = newWidgetName;
} catch (error) {
log.debug("Error updating table widget properties", error);
}
}
// If it is the copied widget, update position properties
if (widget.widgetId === widgetIdMap[copiedWidget.widgetId]) {
newWidgetId = widget.widgetId;
widget.leftColumn = leftColumn;
widget.topRow = topRow;
widget.bottomRow = bottomRow;
widget.rightColumn = rightColumn;
widget.parentId = pastingIntoWidgetId;
// Also, update the parent widget in the canvas widgets
// to include this new copied widget's id in the parent's children
let parentChildren = [widget.widgetId];
const widgetChildren = widgets[pastingIntoWidgetId].children;
if (widgetChildren && Array.isArray(widgetChildren)) {
// Add the new child to existing children
parentChildren = parentChildren.concat(widgetChildren);
}
const updateBottomRow =
widget.bottomRow * widget.parentRowSpace >
widgets[pastingIntoWidgetId].bottomRow;
widgets = {
...widgets,
[pastingIntoWidgetId]: {
...widgets[pastingIntoWidgetId],
...(updateBottomRow
? {
bottomRow: widget.bottomRow * widget.parentRowSpace,
}
: {}),
children: parentChildren,
},
};
// If the copied widget's boundaries exceed the parent's
// Make the parent scrollable
if (
widgets[pastingIntoWidgetId].bottomRow *
widgets[widget.parentId].parentRowSpace <=
widget.bottomRow * widget.parentRowSpace
) {
const parentOfPastingWidget =
widgets[pastingIntoWidgetId].parentId;
if (
parentOfPastingWidget &&
widget.parentId !== MAIN_CONTAINER_WIDGET_ID
) {
const parent = widgets[parentOfPastingWidget];
widgets[parentOfPastingWidget] = {
...parent,
shouldScrollContents: true,
};
}
}
} else {
// For all other widgets in the list
// (These widgets will be descendants of the copied widget)
// This means, that their parents will also be newly copied widgets
// Update widget's parent widget ids with the new parent widget ids
const newParentId = newWidgetList.find((newWidget) =>
widget.parentId
? newWidget.widgetId === widgetIdMap[widget.parentId]
: false,
)?.widgetId;
if (newParentId) widget.parentId = newParentId;
}
// Generate a new unique widget name
widget.widgetName = getNextWidgetName(
widgets,
widget.type,
evalTree,
{
prefix: oldWidgetName,
startWithoutIndex: true,
},
);
widgetNameMap[oldWidgetName] = widget.widgetName;
// Add the new widget to the canvas widgets
widgets[widget.widgetId] = widget;
}
newlyCreatedWidgetIds.push(widgetIdMap[copiedWidgetId]);
// 1. updating template in the copied widget and deleting old template associations
// 2. updating dynamicBindingPathList in the copied grid widget
for (let i = 0; i < newWidgetList.length; i++) {
const widget = newWidgetList[i];
widgets = handleSpecificCasesWhilePasting(
widget,
widgets,
widgetNameMap,
newWidgetList,
);
}
}
} else {
// For all other widgets in the list
// (These widgets will be descendants of the copied widget)
// This means, that their parents will also be newly copied widgets
// Update widget's parent widget ids with the new parent widget ids
const newParentId = newWidgetList.find((newWidget) =>
widget.parentId
? newWidget.widgetId === widgetIdMap[widget.parentId]
: false,
)?.widgetId;
if (newParentId) widget.parentId = newParentId;
}
// Generate a new unique widget name
widget.widgetName = getNextWidgetName(widgets, widget.type, evalTree, {
prefix: oldWidgetName,
startWithoutIndex: true,
});
widgetNameMap[oldWidgetName] = widget.widgetName;
// Add the new widget to the canvas widgets
widgets[widget.widgetId] = widget;
}
// 1. updating template in the copied widget and deleting old template associations
// 2. updating dynamicBindingPathList in the copied grid widget
for (let i = 0; i < newWidgetList.length; i++) {
const widget = newWidgetList[i];
widgets = handleSpecificCasesWhilePasting(
widget,
widgets,
widgetNameMap,
newWidgetList,
);
}
// save the new DSL
yield put(updateAndSaveLayout(widgets));
// hydrating enhancements map after save layout so that enhancement map
// for newly copied widget is hydrated
// Flash the newly pasted widget once the DSL is re-rendered
}),
),
);
// save the new DSL
yield put(updateAndSaveLayout(widgets));
newlyCreatedWidgetIds.forEach((newWidgetId) => {
setTimeout(() => flashElementById(newWidgetId), 100);
yield put({
type: ReduxActionTypes.SELECT_WIDGET_INIT,
payload: { widgetId: newWidgetId },
});
}
});
// hydrating enhancements map after save layout so that enhancement map
// for newly copied widget is hydrated
yield put(selectMultipleWidgetsInitAction(newlyCreatedWidgetIds));
}
function* cutWidgetSaga() {
const selectedWidget = yield select(getSelectedWidget);
if (!selectedWidget) {
const allWidgets: { [widgetId: string]: FlattenedWidgetProps } = yield select(
getWidgets,
);
const selectedWidgets: string[] = yield select(getSelectedWidgets);
if (!selectedWidgets) {
Toaster.show({
text: createMessage(ERROR_WIDGET_CUT_NO_WIDGET_SELECTED),
variant: Variant.info,
@ -1710,17 +1753,26 @@ function* cutWidgetSaga() {
return;
}
const saveResult = yield createWidgetCopy();
const selectedWidgetProps = selectedWidgets.map((each) => allWidgets[each]);
const eventName = "WIDGET_CUT_VIA_SHORTCUT"; // cut only supported through a shortcut
AnalyticsUtil.logEvent(eventName, {
widgetName: selectedWidget.widgetName,
widgetType: selectedWidget.type,
const saveResult = yield createSelectedWidgetsCopy(selectedWidgetProps);
selectedWidgetProps.forEach((each) => {
const eventName = "WIDGET_CUT_VIA_SHORTCUT"; // cut only supported through a shortcut
AnalyticsUtil.logEvent(eventName, {
widgetName: each.widgetName,
widgetType: each.type,
});
});
if (saveResult) {
Toaster.show({
text: createMessage(WIDGET_CUT, selectedWidget.widgetName),
text: createMessage(
WIDGET_CUT,
selectedWidgetProps.length > 1
? `${selectedWidgetProps.length} Widgets`
: selectedWidgetProps[0].widgetName,
),
variant: Variant.success,
});
}

View File

@ -3,10 +3,13 @@ import {
WidgetTypes,
} from "constants/WidgetConstants";
import { cloneDeep, get, isString, filter, set } from "lodash";
import { FlattenedWidgetProps } from "reducers/entityReducers/canvasWidgetsReducer";
import {
CanvasWidgetsReduxState,
FlattenedWidgetProps,
} from "reducers/entityReducers/canvasWidgetsReducer";
import { call, select } from "redux-saga/effects";
import { getDynamicBindings } from "utils/DynamicBindingUtils";
import { getWidget } from "./selectors";
import { getWidget, getWidgetMetaProps } from "./selectors";
/**
* checks if triggerpaths contains property path passed
@ -224,3 +227,85 @@ export function* getWidgetChildren(widgetId: string): any {
}
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
if (selectedWidget.children) {
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*(
selectedWidget: FlattenedWidgetProps | undefined,
) {
// 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[] = yield call(
getWidgetChildren,
selectedWidget.children[0],
);
const firstChildId = childrenIds[0];
selectedWidget = yield select(getWidget, firstChildId);
}
return selectedWidget;
};

View File

@ -5,15 +5,16 @@ import { getWidgetImmediateChildren, getWidgets } from "./selectors";
import log from "loglevel";
import {
deselectMultipleWidgetsAction,
selectAllWidgetsAction,
selectMultipleWidgetsAction,
selectWidgetAction,
selectWidgetInitAction,
silentAddSelectionsAction,
} from "actions/widgetSelectionActions";
import { Toaster } from "components/ads/Toast";
import { createMessage, SELECT_ALL_WIDGETS_MSG } from "constants/messages";
import { Variant } from "components/ads/common";
import { getSelectedWidget, getSelectedWidgets } from "selectors/ui";
import { CanvasWidgetsReduxState } from "reducers/entityReducers/canvasWidgetsReducer";
// The following is computed to be used in the entity explorer
// Every time a widget is selected, we need to expand widget entities
@ -59,13 +60,16 @@ function* selectedWidgetAncestrySaga(
}
}
function* selectAllWidgetsSaga() {
const allWidgetsOnMainContainer: string[] = yield select(
function* selectAllWidgetsInCanvasSaga(
action: ReduxAction<{ canvasId: string }>,
) {
const { canvasId } = action.payload;
const allWidgetsOnCanvas: string[] = yield select(
getWidgetImmediateChildren,
MAIN_CONTAINER_WIDGET_ID,
canvasId,
);
if (allWidgetsOnMainContainer && allWidgetsOnMainContainer.length) {
yield put(selectAllWidgetsAction(allWidgetsOnMainContainer));
if (allWidgetsOnCanvas && allWidgetsOnCanvas.length) {
yield put(selectMultipleWidgetsAction(allWidgetsOnCanvas));
Toaster.show({
text: createMessage(SELECT_ALL_WIDGETS_MSG),
variant: Variant.info,
@ -79,8 +83,8 @@ function* deselectNonSiblingsOfWidgetSaga(
) {
const { isMultiSelect, widgetId } = action.payload;
if (isMultiSelect) {
const allWidgets = yield select(getWidgets);
const parentId = allWidgets[widgetId].parentId;
const allWidgets: CanvasWidgetsReduxState = yield select(getWidgets);
const parentId: any = allWidgets[widgetId].parentId;
const childWidgets: string[] = yield select(
getWidgetImmediateChildren,
parentId,
@ -127,12 +131,32 @@ function* shiftSelectWidgetsSaga(
: lastSelectedWidgetIndex;
const unSelectedSiblings = siblingWidgets.slice(start + 1, end);
if (unSelectedSiblings && unSelectedSiblings.length) {
yield put(selectMultipleWidgetsAction(unSelectedSiblings));
yield put(silentAddSelectionsAction(unSelectedSiblings));
}
}
yield put(selectWidgetInitAction(widgetId, true));
}
function* selectMultipleWidgetsSaga(
action: ReduxAction<{ widgetIds: string[] }>,
) {
const { widgetIds } = action.payload;
if (!widgetIds || !widgetIds.length) {
return;
}
const allWidgets: CanvasWidgetsReduxState = yield select(getWidgets);
const parentToMatch = allWidgets[widgetIds[0]].parentId;
const doesNotMatchParent = widgetIds.some((each) => {
return allWidgets[each].parentId !== parentToMatch;
});
if (doesNotMatchParent) {
return;
} else {
yield put(selectWidgetAction());
yield put(selectMultipleWidgetsAction(widgetIds));
}
}
export function* widgetSelectionSagas() {
yield all([
takeLatest(
@ -145,9 +169,13 @@ export function* widgetSelectionSagas() {
ReduxActionTypes.SELECT_WIDGET_INIT,
deselectNonSiblingsOfWidgetSaga,
),
takeLatest(
ReduxActionTypes.SELECT_ALL_WIDGETS_IN_CANVAS_INIT,
selectAllWidgetsInCanvasSaga,
),
takeLatest(
ReduxActionTypes.SELECT_MULTIPLE_WIDGETS_INIT,
selectAllWidgetsSaga,
selectMultipleWidgetsSaga,
),
]);
}

View File

@ -0,0 +1,30 @@
import { updateWidget } from "actions/pageActions";
import { WidgetTypes } from "constants/WidgetConstants";
import { useEffect } from "react";
import { useDispatch } from "react-redux";
import { AppState } from "reducers";
import { getWidget } from "sagas/selectors";
import { useSelector } from "store";
import { WidgetOperations } from "widgets/BaseWidget";
export const useCanvasMinHeightUpdateHook = (
widgetId: string,
minHeight = 0,
) => {
const widget = useSelector((state: AppState) => getWidget(state, widgetId));
const dispatch = useDispatch();
useEffect(() => {
if (
widget &&
widget.type === WidgetTypes.CANVAS_WIDGET &&
widget.minHeight !== minHeight
) {
dispatch(
updateWidget(WidgetOperations.UPDATE_PROPERTY, widgetId, {
propertyPath: "minHeight",
propertyValue: minHeight,
}),
);
}
}, [minHeight]);
};

View File

@ -1,7 +1,7 @@
import { useDispatch } from "react-redux";
import { focusWidget } from "actions/widgetActions";
import {
selectAllWidgetsAction,
selectMultipleWidgetsAction,
selectWidgetInitAction,
shiftSelectWidgetsEntityExplorerInitAction,
} from "actions/widgetSelectionActions";
@ -29,7 +29,7 @@ export const useWidgetSelection = () => {
(widgetId?: string) => dispatch(focusWidget(widgetId)),
[dispatch],
),
deselectAll: useCallback(() => dispatch(selectAllWidgetsAction([])), [
deselectAll: useCallback(() => dispatch(selectMultipleWidgetsAction([])), [
dispatch,
]),
};

View File

@ -10,6 +10,12 @@ import { useDispatch } from "react-redux";
import { extractCurrentDSL } from "utils/WidgetPropsUtils";
import { setAppMode } from "actions/pageActions";
import { APP_MODE } from "reducers/entityReducers/appReducer";
import { createSelector } from "reselect";
import { CanvasWidgetsReduxState } from "reducers/entityReducers/canvasWidgetsReducer";
import { getCanvasWidgets } from "selectors/entitiesSelector";
import { ContainerWidgetProps } from "widgets/ContainerWidget";
import { WidgetProps } from "widgets/BaseWidget";
import CanvasWidgetsNormalizer from "normalizers/CanvasWidgetsNormalizer";
export const useMockDsl = (dsl: any) => {
const dispatch = useDispatch();
@ -69,6 +75,17 @@ export function MockPageDSL({ dsl, children }: any) {
return children;
}
export const mockGetCanvasWidgetDsl = createSelector(
getCanvasWidgets,
(
canvasWidgets: CanvasWidgetsReduxState,
): ContainerWidgetProps<WidgetProps> => {
return CanvasWidgetsNormalizer.denormalize("0", {
canvasWidgets,
});
},
);
export const syntheticTestMouseEvent = (
event: MouseEvent,
optionsToAdd = {},

View File

@ -0,0 +1,9 @@
import Canvas from "pages/Editor/Canvas";
import React from "react";
import { useSelector } from "react-redux";
import { mockGetCanvasWidgetDsl } from "./testCommon";
export const MockCanvas = () => {
const dsl = useSelector(mockGetCanvasWidgetDsl);
return <Canvas dsl={dsl}></Canvas>;
};