PromucFlow_constructor/app/client/src/sagas/WidgetSelectUtils.ts

333 lines
10 KiB
TypeScript

import {
createMessage,
SELECT_ALL_WIDGETS_MSG,
} from "@appsmith/constants/messages";
import {
ReduxActionErrorTypes,
ReduxActionTypes,
} from "@appsmith/constants/ReduxActionConstants";
import { MAIN_CONTAINER_WIDGET_ID } from "constants/WidgetConstants";
import { uniq } from "lodash";
import type {
CanvasWidgetsReduxState,
FlattenedWidgetProps,
} from "reducers/entityReducers/canvasWidgetsReducer";
import { call, put, select } from "redux-saga/effects";
import {
getWidgetImmediateChildren,
getWidgetMetaProps,
getWidgets,
} from "sagas/selectors";
import { getWidgetChildrenIds } from "sagas/WidgetOperationUtils";
import { getLastSelectedWidget, getSelectedWidgets } from "selectors/ui";
import WidgetFactory from "WidgetProvider/factory";
import { toast } from "design-system";
import { checkIsDropTarget } from "WidgetProvider/factory/helpers";
/**
* Selection types that are possible for widget select
*
* It is currently used for widget selection,
* but can be used for other types of selections like tabs
*/
export enum SelectionRequestType {
/** Remove all selections, reset last selected widget to the main container */
Empty = "Empty",
/** Replace the existing selection with a new single selection.
* The new selection will be the last selected widget */
One = "One",
/** Replace the existing selection with a new selection of multiple widgets.
* The new selection's first widget becomes the last selected widget
* */
Multiple = "Multiple",
/** Adds or removes a widget selection. Similar to CMD/Ctrl selections,
* if the payload exits in the selection, it will be removed.
* If the payload is new, it will be added.*/
PushPop = "PushPop",
/** Selects all widgets in the last selected canvas */
All = "All",
/** Add selection like shift select where the widgets between two selections
* are also selected. Widget order is taken from children order of the canvas */
ShiftSelect = "ShiftSelect",
/**
* Unselect specific widgets */
Unselect = "Unselect",
/** Skip checks and just try to select. Page ID can be supplied to select a
* widget on another page */
UnsafeSelect = "UnsafeSelect",
}
export type SelectionPayload = string[];
export type SetSelectionResult = string[] | undefined;
// Main container cannot be a selection, dont honour this request
export const isInvalidSelectionRequest = (id: unknown) =>
typeof id !== "string";
export class WidgetSelectionError extends Error {
request?: SelectionPayload;
type?: SelectionRequestType;
constructor(
message: string,
request?: SelectionPayload,
type?: SelectionRequestType,
) {
super(message);
this.request = request;
this.type = type;
this.name = "WidgetSelectionError";
}
}
export const deselectAll = (request: SelectionPayload): SetSelectionResult => {
if (request.length > 0) {
throw new WidgetSelectionError(
"Wrong payload supplied",
request,
SelectionRequestType.Empty,
);
}
return [];
};
export const selectOneWidget = (
request: SelectionPayload,
): SetSelectionResult => {
if (request.length !== 1) {
throw new WidgetSelectionError(
"Wrong payload supplied",
request,
SelectionRequestType.One,
);
}
return request;
};
export const selectMultipleWidgets = (
request: SelectionPayload,
allWidgets: CanvasWidgetsReduxState,
): SetSelectionResult => {
const parentToMatch = allWidgets[request[0]]?.parentId;
const areSiblings = request.every((each) => {
return allWidgets[each]?.parentId === parentToMatch;
});
if (!areSiblings) return;
return request;
};
export const shiftSelectWidgets = (
request: SelectionPayload,
siblingWidgets: string[],
currentlySelectedWidgets: string[],
lastSelectedWidget: string,
): SetSelectionResult => {
const selectedWidgetIndex = siblingWidgets.indexOf(request[0]);
const siblingIndexOfLastSelectedWidget =
siblingWidgets.indexOf(lastSelectedWidget);
if (siblingIndexOfLastSelectedWidget === -1) {
return request;
}
if (currentlySelectedWidgets.includes(request[0])) {
return currentlySelectedWidgets.filter((w) => request[0] !== w);
}
let widgets: string[] = [...currentlySelectedWidgets, ...request];
const start =
siblingIndexOfLastSelectedWidget < selectedWidgetIndex
? siblingIndexOfLastSelectedWidget
: selectedWidgetIndex;
const end =
siblingIndexOfLastSelectedWidget < selectedWidgetIndex
? selectedWidgetIndex
: siblingIndexOfLastSelectedWidget;
const unSelectedSiblings = siblingWidgets.slice(start, end + 1);
if (unSelectedSiblings && unSelectedSiblings.length) {
widgets = widgets.concat(...unSelectedSiblings);
}
return uniq(widgets);
};
export const pushPopWidgetSelection = (
request: SelectionPayload,
currentlySelectedWidgets: string[],
siblingWidgets: string[],
): SetSelectionResult => {
const widgetId = request[0];
const alreadySelected = currentlySelectedWidgets.includes(widgetId);
if (alreadySelected) {
return currentlySelectedWidgets.filter((each) => each !== widgetId);
} else if (!!widgetId) {
return [...currentlySelectedWidgets, widgetId].filter((w) =>
siblingWidgets.includes(w),
);
}
};
export const unselectWidget = (
request: SelectionPayload,
currentlySelectedWidgets: string[],
): SetSelectionResult => {
return currentlySelectedWidgets.filter((w) => !request.includes(w));
};
const WidgetTypes = WidgetFactory.widgetTypes;
function* getDroppingCanvasOfWidget(widgetLastSelected: FlattenedWidgetProps) {
if (checkIsDropTarget(widgetLastSelected.type)) {
const canvasWidgets: CanvasWidgetsReduxState = yield select(getWidgets);
const childWidgets: string[] = yield select(
getWidgetImmediateChildren,
widgetLastSelected.widgetId,
);
const firstCanvas = childWidgets.find((each) => {
const widget = canvasWidgets[each];
return widget.type === WidgetTypes.CANVAS_WIDGET;
});
if (widgetLastSelected.type === WidgetTypes.TABS_WIDGET) {
const tabMetaProps: Record<string, unknown> = yield select(
getWidgetMetaProps,
widgetLastSelected,
);
return tabMetaProps.selectedTabWidgetId;
}
if (firstCanvas) {
return firstCanvas;
}
}
return widgetLastSelected.parentId;
}
function* getLastSelectedCanvas() {
const lastSelectedWidget: string = yield select(getLastSelectedWidget);
const selectedWidgets: string[] = yield select(getSelectedWidgets);
const areMultipleWidgetsSelected: boolean = selectedWidgets.length > 1;
const canvasWidgets: CanvasWidgetsReduxState = yield select(getWidgets);
const widgetLastSelected =
lastSelectedWidget && canvasWidgets[lastSelectedWidget];
if (widgetLastSelected) {
if (areMultipleWidgetsSelected) {
return widgetLastSelected.parentId || MAIN_CONTAINER_WIDGET_ID;
}
if (!areMultipleWidgetsSelected) {
const canvasToSelect: string = yield call(
getDroppingCanvasOfWidget,
widgetLastSelected,
);
return canvasToSelect ? canvasToSelect : MAIN_CONTAINER_WIDGET_ID;
}
}
return MAIN_CONTAINER_WIDGET_ID;
}
// used for List widget cases
const isChildOfDropDisabledCanvas = (
canvasWidgets: CanvasWidgetsReduxState,
widgetId: string,
) => {
const widget = canvasWidgets[widgetId];
const parentId = widget.parentId || MAIN_CONTAINER_WIDGET_ID;
const parent = canvasWidgets[parentId];
return !!parent?.dropDisabled;
};
export function* getAllSelectableChildren() {
const lastSelectedWidget: string = yield select(getLastSelectedWidget);
const canvasWidgets: CanvasWidgetsReduxState = yield select(getWidgets);
const widgetLastSelected = canvasWidgets[lastSelectedWidget];
const canvasId: string = yield call(getLastSelectedCanvas);
let allChildren: string[];
const selectGrandChildren: boolean = lastSelectedWidget
? widgetLastSelected && widgetLastSelected.type === WidgetTypes.LIST_WIDGET
: false;
if (selectGrandChildren) {
allChildren = yield call(
getWidgetChildrenIds,
canvasWidgets,
lastSelectedWidget,
);
} else {
allChildren = yield select(getWidgetImmediateChildren, canvasId);
}
if (allChildren && allChildren.length) {
return allChildren.filter((each) => {
const isCanvasWidget =
each &&
canvasWidgets[each] &&
canvasWidgets[each].type === WidgetTypes.CANVAS_WIDGET;
const isImmovableWidget = isChildOfDropDisabledCanvas(
canvasWidgets,
each,
);
return !(isCanvasWidget || isImmovableWidget);
});
}
return [];
}
export function assertParentId(parentId: unknown): asserts parentId is string {
if (!parentId || typeof parentId !== "string") {
throw new WidgetSelectionError("Could not find a parent for the widget");
}
}
export function getWidgetAncestry(
widgetId: string | undefined,
allWidgets: CanvasWidgetsReduxState,
) {
// Fill up the ancestry of widget
// The following is computed to be used in the entity explorer
// Every time a widget is selected, we need to expand widget entities
// in the entity explorer so that the selected widget is visible
// It is also used for finding the selected widget ancestry so that we can
// show widgets that could be invisible in the current state like widgets inside
// hidden tabs
const widgetAncestry: string[] = [];
let ancestorWidgetId = widgetId;
while (ancestorWidgetId) {
widgetAncestry.push(ancestorWidgetId);
if (allWidgets[ancestorWidgetId] && allWidgets[ancestorWidgetId].parentId) {
const parentId = allWidgets[ancestorWidgetId].parentId;
assertParentId(parentId);
ancestorWidgetId = parentId;
} else {
break;
}
}
return widgetAncestry;
}
export function* selectAllWidgetsInCanvasSaga() {
try {
const canvasWidgets: CanvasWidgetsReduxState = yield select(getWidgets);
const allSelectableChildren: string[] = yield call(
getAllSelectableChildren,
);
if (allSelectableChildren && allSelectableChildren.length) {
const isAnyModalSelected = allSelectableChildren.some((each) => {
return (
each &&
canvasWidgets[each] &&
canvasWidgets[each].type === WidgetFactory.widgetTypes.MODAL_WIDGET
);
});
if (isAnyModalSelected) {
toast.show(createMessage(SELECT_ALL_WIDGETS_MSG), {
kind: "info",
});
}
return allSelectableChildren;
}
} catch (error) {
yield put({
type: ReduxActionErrorTypes.WIDGET_SELECTION_ERROR,
payload: {
action: ReduxActionTypes.SELECT_WIDGET_INIT,
error,
},
});
}
}