diff --git a/app/client/src/actions/widgetActions.tsx b/app/client/src/actions/widgetActions.tsx index 5effb4cbf98c..19ecc0b85955 100644 --- a/app/client/src/actions/widgetActions.tsx +++ b/app/client/src/actions/widgetActions.tsx @@ -8,6 +8,7 @@ import type { BatchAction } from "actions/batchActions"; import { batchAction } from "actions/batchActions"; import type { WidgetProps } from "widgets/BaseWidget"; import type { PartialExportParams } from "sagas/PartialImportExportSagas"; +import type { PasteWidgetReduxAction } from "constants/WidgetConstants"; export const widgetInitialisationSuccess = () => { return { @@ -95,15 +96,17 @@ export const copyWidget = (isShortcut: boolean) => { }; }; -export const pasteWidget = ( +export const pasteWidget = ({ + gridPosition, groupWidgets = false, - mouseLocation: { x: number; y: number }, -) => { + mouseLocation, +}: PasteWidgetReduxAction) => { return { type: ReduxActionTypes.PASTE_COPIED_WIDGET_INIT, payload: { - groupWidgets: groupWidgets, + groupWidgets, mouseLocation, + gridPosition, }, }; }; diff --git a/app/client/src/ce/sagas/index.tsx b/app/client/src/ce/sagas/index.tsx index 1928ed26f9ad..d6dc60910abf 100644 --- a/app/client/src/ce/sagas/index.tsx +++ b/app/client/src/ce/sagas/index.tsx @@ -41,7 +41,7 @@ import saaSPaneSagas from "sagas/SaaSPaneSagas"; import snapshotSagas from "sagas/SnapshotSagas"; import snipingModeSagas from "sagas/SnipingModeSagas"; import templateSagas from "sagas/TemplatesSagas"; -import buildingBlocksSagas from "sagas/BuildingBlocksSagas"; +import buildingBlockSagas from "sagas/BuildingBlockSagas"; import themeSagas from "sagas/ThemeSaga"; import utilSagas from "sagas/UtilSagas"; import websocketSagas from "sagas/WebsocketSagas/WebsocketSagas"; @@ -111,5 +111,5 @@ export const sagas = [ anvilSagas, ternSagas, ideSagas, - buildingBlocksSagas, + buildingBlockSagas, ]; diff --git a/app/client/src/constants/WidgetConstants.tsx b/app/client/src/constants/WidgetConstants.tsx index 89ee7deda6ea..9c7525118107 100644 --- a/app/client/src/constants/WidgetConstants.tsx +++ b/app/client/src/constants/WidgetConstants.tsx @@ -270,3 +270,11 @@ export const DEFAULT_COLUMNS_FOR_EXPLORER_BUILDING_BLOCKS = 62; export const BUILDING_BLOCK_MIN_HORIZONTAL_LIMIT = 2000; export const BUILDING_BLOCK_MIN_VERTICAL_LIMIT = 800; export const BUILDING_BLOCK_EXPLORER_TYPE = "BUILDING_BLOCK"; + +export type EitherMouseLocationORGridPosition = + | { mouseLocation: { x: number; y: number }; gridPosition?: never } + | { mouseLocation?: never; gridPosition: { top: number; left: number } }; + +export type PasteWidgetReduxAction = { + groupWidgets: boolean; +} & EitherMouseLocationORGridPosition; diff --git a/app/client/src/pages/Editor/GlobalHotKeys/GlobalHotKeys.tsx b/app/client/src/pages/Editor/GlobalHotKeys/GlobalHotKeys.tsx index ce9557b9c937..5f1c5b65dce2 100644 --- a/app/client/src/pages/Editor/GlobalHotKeys/GlobalHotKeys.tsx +++ b/app/client/src/pages/Editor/GlobalHotKeys/GlobalHotKeys.tsx @@ -360,7 +360,12 @@ const mapDispatchToProps = (dispatch: any) => { return { copySelectedWidget: () => dispatch(copyWidget(true)), pasteCopiedWidget: (mouseLocation: { x: number; y: number }) => - dispatch(pasteWidget(false, mouseLocation)), + dispatch( + pasteWidget({ + groupWidgets: false, + mouseLocation, + }), + ), deleteSelectedWidget: () => dispatch(deleteSelectedWidget(true)), cutSelectedWidget: () => dispatch(cutWidget()), groupSelectedWidget: () => dispatch(groupWidgets()), diff --git a/app/client/src/sagas/BuildingBlockAdditionSagas.ts b/app/client/src/sagas/BuildingBlockAdditionSagas.ts new file mode 100644 index 000000000000..bb0b50e2fe24 --- /dev/null +++ b/app/client/src/sagas/BuildingBlockAdditionSagas.ts @@ -0,0 +1,296 @@ +import type { FlattenedWidgetProps } from "WidgetProvider/constants"; +import type { WidgetAddChild } from "actions/pageActions"; +import { runAction } from "actions/pluginActionActions"; +import type { ApiResponse } from "api/ApiResponses"; +import type { Template } from "api/TemplatesApi"; +import ApplicationApi, { + type ImportBuildingBlockToApplicationRequest, + type ImportBuildingBlockToApplicationResponse, +} from "@appsmith/api/ApplicationApi"; +import { + ReduxActionErrorTypes, + ReduxActionTypes, + WidgetReduxActionTypes, + type ReduxAction, +} from "@appsmith/constants/ReduxActionConstants"; +import type { ActionDataState } from "@appsmith/reducers/entityReducers/actionsReducer"; +import { getActions } from "@appsmith/selectors/entitiesSelector"; +import { getCurrentWorkspaceId } from "@appsmith/selectors/selectedWorkspaceSelectors"; +import AnalyticsUtil from "@appsmith/utils/AnalyticsUtil"; +import { MAIN_CONTAINER_WIDGET_ID } from "constants/WidgetConstants"; +import type { DragDetails } from "reducers/uiReducers/dragResizeReducer"; +import { put, race, select, take, call } from "redux-saga/effects"; +import { getBuildingBlockDragStartTimestamp } from "selectors/buildingBlocksSelectors"; +import { + getCurrentApplicationId, + getCurrentPageId, +} from "selectors/editorSelectors"; +import { getTemplatesSelector } from "selectors/templatesSelectors"; +import { initiateBuildingBlockDropEvent } from "utils/buildingBlockUtils"; +import { getCopiedWidgets, saveCopiedWidgets } from "utils/storage"; +import { saveBuildingBlockWidgetsToStore } from "./BuildingBlockSagas"; +import { validateResponse } from "./ErrorSagas"; +import { postPageAdditionSaga } from "./TemplatesSagas"; +import { addChildSaga } from "./WidgetAdditionSagas"; +import { getDragDetails, getWidgetByName } from "./selectors"; +import type { WidgetDraggingUpdateParams } from "layoutSystems/common/canvasArenas/ArenaTypes"; +import { addWidgetAndMoveWidgetsSaga } from "./CanvasSagas/DraggingCanvasSagas"; +import { pasteWidget } from "actions/widgetActions"; + +function* addBuildingBlockActionsToApplication(dragDetails: DragDetails) { + const applicationId: string = yield select(getCurrentApplicationId); + const buildingblockName = dragDetails.newWidget.displayName; + const buildingBlocks: Template[] = yield select(getTemplatesSelector); + const currentPageId: string = yield select(getCurrentPageId); + const workspaceId: string = yield select(getCurrentWorkspaceId); + const selectedBuildingBlock = buildingBlocks.find( + (buildingBlock) => buildingBlock.title === buildingblockName, + ) as Template; + + const body: ImportBuildingBlockToApplicationRequest = { + pageId: currentPageId, + applicationId, + workspaceId, + templateId: selectedBuildingBlock.id, + }; + + // api call adds DS, queries and JS to page and returns new page dsl with building block + const response: ApiResponse = + yield call(ApplicationApi.importBuildingBlockToApplication, body); + + return response; +} + +function* runSingleAction(actionId: string) { + yield put(runAction(actionId)); + yield take(ReduxActionTypes.RUN_ACTION_SUCCESS); +} + +function* runNewlyCreatedActions( + actionsBeforeAddingBuildingBlock: ActionDataState, + actionsAfterAddingBuildingBlocks: ActionDataState, +) { + const actionIdsBeforeAddingBB = actionsBeforeAddingBuildingBlock.map( + (obj) => obj.config.id, + ); + + const newlyAddedActions = actionsAfterAddingBuildingBlocks.filter( + (obj) => !actionIdsBeforeAddingBB.includes(obj.config.id), + ); + + // Run each action sequentially. We have a max of 2-3 actions per building block. + // If we run this in parallel, we will have a racing condition when multiple building blocks are drag and dropped quickly. + for (const action of newlyAddedActions) { + if (action.config.executeOnLoad) { + yield runSingleAction(action.config.id); + } + } +} + +export function* loadBuildingBlocksIntoApplication( + buildingBlockWidget: WidgetAddChild, + skeletonLoaderId: string, +) { + const { leftColumn, topRow } = buildingBlockWidget; + try { + const dragDetails: DragDetails = yield select(getDragDetails); + const applicationId: string = yield select(getCurrentApplicationId); + const workspaceId: string = yield select(getCurrentWorkspaceId); + const actionsBeforeAddingBuildingBlock: ActionDataState = + yield select(getActions); + const existingCopiedWidgets: unknown = yield call(getCopiedWidgets); + const buildingBlockDragStartTimestamp: number = yield select( + getBuildingBlockDragStartTimestamp, + ); + + // start loading for dropping building blocks + yield put({ + type: ReduxActionTypes.DRAGGING_BUILDING_BLOCK_TO_CANVAS_INIT, + }); + + // makes sure updateAndSaveLayout completes first for initial skeletonWidget addition + const saveResult: unknown = yield race({ + success: take(ReduxActionTypes.SAVE_PAGE_SUCCESS), + failure: take(ReduxActionErrorTypes.SAVE_PAGE_ERROR), + }); + + if (typeof saveResult === "object" && "failure" in saveResult!) { + throw new Error("Save page failed"); + } + + const response: ApiResponse = + yield call(addBuildingBlockActionsToApplication, dragDetails); + const isValid: boolean = yield validateResponse(response); + + if (isValid) { + yield saveBuildingBlockWidgetsToStore(response); + + // remove skeleton loader just before pasting the building block + yield put({ + type: WidgetReduxActionTypes.WIDGET_SINGLE_DELETE, + payload: { + widgetId: skeletonLoaderId, + parentId: MAIN_CONTAINER_WIDGET_ID, + disallowUndo: true, + isShortcut: false, + }, + }); + + yield put( + pasteWidget({ + groupWidgets: false, + gridPosition: { + top: topRow, + left: leftColumn, + }, + }), + ); + + const timeTakenToDropWidgetsInSeconds = + (Date.now() - buildingBlockDragStartTimestamp) / 1000; + yield call(postPageAdditionSaga, applicationId); + + // stop loading after pasting process is complete + yield put({ + type: ReduxActionTypes.DRAGGING_BUILDING_BLOCK_TO_CANVAS_SUCCESS, + }); + + const actionsAfterAddingBuildingBlocks: ActionDataState = + yield select(getActions); + + if ( + response.data.onPageLoadActions && + response.data.onPageLoadActions.length > 0 + ) { + yield runNewlyCreatedActions( + actionsBeforeAddingBuildingBlock, + actionsAfterAddingBuildingBlocks, + ); + } + + const timeTakenToCompleteInMs = buildingBlockDragStartTimestamp + ? Date.now() - buildingBlockDragStartTimestamp + : 0; + const timeTakenToCompleteInSeconds = timeTakenToCompleteInMs / 1000; + + AnalyticsUtil.logEvent("DROP_BUILDING_BLOCK_COMPLETED", { + applicationId, + workspaceId, + source: "explorer", + eventData: { + buildingBlockName: dragDetails.newWidget.displayName, + timeTakenToCompletion: timeTakenToCompleteInSeconds, + timeTakenToDropWidgets: timeTakenToDropWidgetsInSeconds, + }, + }); + yield put({ + type: ReduxActionTypes.RESET_BUILDING_BLOCK_DRAG_START_TIME, + }); + + if (existingCopiedWidgets) { + yield call(saveCopiedWidgets, JSON.stringify(existingCopiedWidgets)); + } + } + } catch (error) { + yield put({ + type: WidgetReduxActionTypes.WIDGET_SINGLE_DELETE, + payload: { + widgetId: skeletonLoaderId, + parentId: MAIN_CONTAINER_WIDGET_ID, + disallowUndo: true, + isShortcut: false, + }, + }); + yield put({ + type: ReduxActionErrorTypes.DRAGGING_BUILDING_BLOCK_TO_CANVAS_ERROR, + }); + } +} + +export function* addBuildingBlockToCanvasSaga( + addEntityAction: ReduxAction, +) { + const applicationId: string = yield select(getCurrentApplicationId); + const workspaceId: string = yield select(getCurrentWorkspaceId); + const dragDetails: DragDetails = yield select(getDragDetails); + const buildingblockName = dragDetails.newWidget.displayName; + const skeletonWidgetName = `loading_${buildingblockName + .toLowerCase() + .replace(/ /g, "_")}`; + const addSkeletonWidgetAction: ReduxAction< + WidgetAddChild & { shouldReplay: boolean } + > = { + ...addEntityAction, + payload: { + ...addEntityAction.payload, + type: "SKELETON_WIDGET", + widgetName: skeletonWidgetName, + widgetId: MAIN_CONTAINER_WIDGET_ID, + // so that the skeleton loader does not get included when the users uses the undo/redo + shouldReplay: false, + }, + }; + + yield call(initiateBuildingBlockDropEvent, { + applicationId, + workspaceId, + buildingblockName, + }); + + yield call(addChildSaga, addSkeletonWidgetAction); + const skeletonWidget: FlattenedWidgetProps = yield select( + getWidgetByName, + skeletonWidgetName, + ); + yield call( + loadBuildingBlocksIntoApplication, + addEntityAction.payload, + skeletonWidget.widgetId, + ); +} + +export function* addAndMoveBuildingBlockToCanvasSaga( + actionPayload: ReduxAction<{ + newWidget: WidgetAddChild; + draggedBlocksToUpdate: WidgetDraggingUpdateParams[]; + canvasId: string; + }>, +) { + const applicationId: string = yield select(getCurrentApplicationId); + const workspaceId: string = yield select(getCurrentApplicationId); + const dragDetails: DragDetails = yield select(getDragDetails); + const buildingblockName = dragDetails.newWidget.displayName; + const skeletonWidgetName = `loading_${buildingblockName + .toLowerCase() + .replace(/ /g, "_")}`; + + yield call(addWidgetAndMoveWidgetsSaga, { + ...actionPayload, + payload: { + ...actionPayload.payload, + // so that the skeleton loader does not get included when the users uses the undo/redo + shouldReplay: false, + newWidget: { + ...actionPayload.payload.newWidget, + type: "SKELETON_WIDGET", + widgetName: skeletonWidgetName, + widgetId: MAIN_CONTAINER_WIDGET_ID, + }, + }, + }); + yield call(initiateBuildingBlockDropEvent, { + applicationId, + workspaceId, + buildingblockName, + }); + + const skeletonWidget: FlattenedWidgetProps = yield select( + getWidgetByName, + skeletonWidgetName, + ); + yield call( + loadBuildingBlocksIntoApplication, + actionPayload.payload.newWidget, + skeletonWidget.widgetId, + ); +} diff --git a/app/client/src/sagas/BuildingBlocksSagas.ts b/app/client/src/sagas/BuildingBlockSagas.ts similarity index 97% rename from app/client/src/sagas/BuildingBlocksSagas.ts rename to app/client/src/sagas/BuildingBlockSagas.ts index f81fb0a72419..812e95c7b5d1 100644 --- a/app/client/src/sagas/BuildingBlocksSagas.ts +++ b/app/client/src/sagas/BuildingBlockSagas.ts @@ -93,7 +93,12 @@ function* apiCallForForkBuildingBlockToApplication(request: { if (isValid) { yield saveBuildingBlockWidgetsToStore(response); - yield put(pasteWidget(false, { x: 0, y: 0 })); + yield put( + pasteWidget({ + groupWidgets: false, + mouseLocation: { x: 0, y: 0 }, + }), + ); yield call(postPageAdditionSaga, request.applicationId); // remove selecting of recently imported widgets yield put(selectWidgetInitAction(SelectionRequestType.Empty)); @@ -153,6 +158,7 @@ function* forkStarterBuildingBlockToApplicationSaga( yield call(saveCopiedWidgets, JSON.stringify(existingCopiedWidgets)); } } + export default function* watchActionSagas() { if (!isAirgappedInstance) yield all([ diff --git a/app/client/src/sagas/CanvasSagas/DraggingCanvasSagas.ts b/app/client/src/sagas/CanvasSagas/DraggingCanvasSagas.ts index 9e74ee7c72ab..55c6bc191630 100644 --- a/app/client/src/sagas/CanvasSagas/DraggingCanvasSagas.ts +++ b/app/client/src/sagas/CanvasSagas/DraggingCanvasSagas.ts @@ -26,33 +26,22 @@ import type { CanvasWidgetsReduxState, FlattenedWidgetProps, } from "reducers/entityReducers/canvasWidgetsReducer"; -import type { DragDetails } from "reducers/uiReducers/dragResizeReducer"; import type { MainCanvasReduxState } from "reducers/uiReducers/mainCanvasReducer"; import { all, call, put, select, takeLatest } from "redux-saga/effects"; -import { - addBuildingBlockToApplication, - getUpdateDslAfterCreatingChild, -} from "sagas/WidgetAdditionSagas"; +import { addAndMoveBuildingBlockToCanvasSaga } from "sagas/BuildingBlockAdditionSagas"; +import { getUpdateDslAfterCreatingChild } from "sagas/WidgetAdditionSagas"; import { executeWidgetBlueprintBeforeOperations, traverseTreeAndExecuteBlueprintChildOperations, } from "sagas/WidgetBlueprintSagas"; -import { - getDragDetails, - getWidget, - getWidgetByName, - getWidgets, - getWidgetsMeta, -} from "sagas/selectors"; +import { getWidget, getWidgets, getWidgetsMeta } from "sagas/selectors"; import { getCanvasWidth, - getCurrentApplicationId, getIsAutoLayoutMobileBreakPoint, getMainCanvasProps, getOccupiedSpacesSelectorForContainer, } from "selectors/editorSelectors"; import { getLayoutSystemType } from "selectors/layoutSystemSelectors"; -import { initiateBuildingBlockDropEvent } from "utils/buildingBlockUtils"; import { collisionCheckPostReflow } from "utils/reflowHookUtils"; import type { WidgetProps } from "widgets/BaseWidget"; @@ -152,52 +141,6 @@ const getBottomMostRowAfterMove = ( return widgetBottomRow; }; -function* addBuildingBlockAndMoveWidgetsSaga( - actionPayload: ReduxAction<{ - newWidget: WidgetAddChild; - draggedBlocksToUpdate: WidgetDraggingUpdateParams[]; - canvasId: string; - }>, -) { - const applicationId: string = yield select(getCurrentApplicationId); - const workspaceId: string = yield select(getCurrentApplicationId); - const dragDetails: DragDetails = yield select(getDragDetails); - const buildingblockName = dragDetails.newWidget.displayName; - const skeletonWidgetName = `loading_${buildingblockName - .toLowerCase() - .replace(/ /g, "_")}`; - - yield call(addWidgetAndMoveWidgetsSaga, { - ...actionPayload, - payload: { - ...actionPayload.payload, - // so that the skeleton loader does not get included when the users uses the undo/redo - shouldReplay: false, - newWidget: { - ...actionPayload.payload.newWidget, - type: "SKELETON_WIDGET", - widgetName: skeletonWidgetName, - widgetId: MAIN_CONTAINER_WIDGET_ID, - }, - }, - }); - yield call(initiateBuildingBlockDropEvent, { - applicationId, - workspaceId, - buildingblockName, - }); - - const skeletonWidget: FlattenedWidgetProps = yield select( - getWidgetByName, - skeletonWidgetName, - ); - yield call( - addBuildingBlockToApplication, - actionPayload.payload.newWidget, - skeletonWidget.widgetId, - ); -} - function* addAndMoveUIEntitySaga( actionPayload: ReduxAction<{ newWidget: WidgetAddChild; @@ -206,7 +149,7 @@ function* addAndMoveUIEntitySaga( }>, ) { if (actionPayload.payload.newWidget.type === BUILDING_BLOCK_EXPLORER_TYPE) { - yield call(addBuildingBlockAndMoveWidgetsSaga, actionPayload); + yield call(addAndMoveBuildingBlockToCanvasSaga, actionPayload); } else { yield call(addWidgetAndMoveWidgetsSaga, actionPayload); } diff --git a/app/client/src/sagas/PartialImportExportSagas/PartialImportSagas.ts b/app/client/src/sagas/PartialImportExportSagas/PartialImportSagas.ts index 8ec0096841f0..078fc6553a09 100644 --- a/app/client/src/sagas/PartialImportExportSagas/PartialImportSagas.ts +++ b/app/client/src/sagas/PartialImportExportSagas/PartialImportSagas.ts @@ -48,7 +48,12 @@ function* partialImportWidgetsSaga(file: File) { if ("widgets" in userUploadedJSON && userUploadedJSON.widgets.length > 0) { yield saveCopiedWidgets(userUploadedJSON.widgets); yield put(selectWidgetInitAction(SelectionRequestType.Empty)); - yield put(pasteWidget(false, { x: 0, y: 0 })); + yield put( + pasteWidget({ + groupWidgets: false, + mouseLocation: { x: 0, y: 0 }, + }), + ); } } finally { if (existingCopiedWidgets) { diff --git a/app/client/src/sagas/WidgetAdditionSagas.ts b/app/client/src/sagas/WidgetAdditionSagas.ts index 8f58936cddd8..ae51297db2ff 100644 --- a/app/client/src/sagas/WidgetAdditionSagas.ts +++ b/app/client/src/sagas/WidgetAdditionSagas.ts @@ -1,8 +1,3 @@ -import type { - ImportBuildingBlockToApplicationRequest, - ImportBuildingBlockToApplicationResponse, -} from "@appsmith/api/ApplicationApi"; -import ApplicationApi from "@appsmith/api/ApplicationApi"; import type { ReduxAction } from "@appsmith/constants/ReduxActionConstants"; import { ReduxActionErrorTypes, @@ -10,12 +5,6 @@ import { WidgetReduxActionTypes, } from "@appsmith/constants/ReduxActionConstants"; import { ENTITY_TYPE } from "@appsmith/entities/AppsmithConsole/utils"; -import type { ActionDataState } from "@appsmith/reducers/entityReducers/actionsReducer"; -import { - getActions, - getCanvasWidgets, -} from "@appsmith/selectors/entitiesSelector"; -import { getCurrentWorkspaceId } from "@appsmith/selectors/selectedWorkspaceSelectors"; import type { WidgetBlueprint } from "WidgetProvider/constants"; import { BlueprintOperationTypes, @@ -25,14 +14,8 @@ import WidgetFactory from "WidgetProvider/factory"; import { generateAutoHeightLayoutTreeAction } from "actions/autoHeightActions"; import type { WidgetAddChild } from "actions/pageActions"; import { updateAndSaveLayout } from "actions/pageActions"; -import { runAction } from "actions/pluginActionActions"; -import { pasteWidget } from "actions/widgetActions"; -import { selectWidgetInitAction } from "actions/widgetSelectionActions"; -import type { ApiResponse } from "api/ApiResponses"; -import type { Template } from "api/TemplatesApi"; import { BUILDING_BLOCK_EXPLORER_TYPE, - MAIN_CONTAINER_WIDGET_ID, RenderModes, } from "constants/WidgetConstants"; import { toast } from "design-system"; @@ -48,27 +31,19 @@ import type { CanvasWidgetsReduxState, FlattenedWidgetProps, } from "reducers/entityReducers/canvasWidgetsReducer"; -import type { DragDetails } from "reducers/uiReducers/dragResizeReducer"; -import { all, call, put, select, take, takeEvery } from "redux-saga/effects"; +import { all, call, put, select, takeEvery } from "redux-saga/effects"; import { getDataTree } from "selectors/dataTreeSelectors"; import { getCanvasWidth, - getCurrentApplicationId, - getCurrentPageId, getIsAutoLayout, getIsAutoLayoutMobileBreakPoint, } from "selectors/editorSelectors"; -import { getTemplatesSelector } from "selectors/templatesSelectors"; import AppsmithConsole from "utils/AppsmithConsole"; import { getNextEntityName } from "utils/AppsmithUtils"; import { generateWidgetProps } from "utils/WidgetPropsUtils"; import { generateReactKey } from "utils/generators"; -import { getCopiedWidgets, saveCopiedWidgets } from "utils/storage"; import type { WidgetProps } from "widgets/BaseWidget"; import { isStack } from "../layoutSystems/autolayout/utils/AutoLayoutUtils"; -import { saveBuildingBlockWidgetsToStore } from "./BuildingBlocksSagas"; -import { validateResponse } from "./ErrorSagas"; -import { postPageAdditionSaga } from "./TemplatesSagas"; import { buildWidgetBlueprint, executeWidgetBlueprintBeforeOperations, @@ -76,22 +51,8 @@ import { traverseTreeAndExecuteBlueprintChildOperations, } from "./WidgetBlueprintSagas"; import { getPropertiesToUpdate } from "./WidgetOperationSagas"; -import { - getDefaultCanvas, - getMousePositionFromCanvasGridPosition, - getSnappedGrid, -} from "./WidgetOperationUtils"; -import { SelectionRequestType } from "./WidgetSelectUtils"; -import { - getDragDetails, - getWidget, - getWidgetByName, - getWidgets, -} from "./selectors"; - -import { getBuildingBlockDragStartTimestamp } from "selectors/buildingBlocksSelectors"; -import { initiateBuildingBlockDropEvent } from "utils/buildingBlockUtils"; -import AnalyticsUtil from "@appsmith/utils/AnalyticsUtil"; +import { getWidget, getWidgets } from "./selectors"; +import { addBuildingBlockToCanvasSaga } from "./BuildingBlockAdditionSagas"; const WidgetTypes = WidgetFactory.widgetTypes; @@ -528,245 +489,13 @@ function* addNewTabChildSaga( yield put(updateAndSaveLayout(updatedWidgets)); } -function* addBuildingBlockActionsToApp(dragDetails: DragDetails) { - const applicationId: string = yield select(getCurrentApplicationId); - const buildingblockName = dragDetails.newWidget.displayName; - const buildingBlocks: Template[] = yield select(getTemplatesSelector); - const currentPageId: string = yield select(getCurrentPageId); - const workspaceId: string = yield select(getCurrentWorkspaceId); - const selectedBuildingBlock = buildingBlocks.find( - (buildingBlock) => buildingBlock.title === buildingblockName, - ) as Template; - - const body: ImportBuildingBlockToApplicationRequest = { - pageId: currentPageId, - applicationId, - workspaceId, - templateId: selectedBuildingBlock.id, - }; - - // api call adds DS, queries and JS to page and returns new page dsl with building block - const response: ApiResponse = - yield call(ApplicationApi.importBuildingBlockToApplication, body); - - return response; -} - -function* getBuildingBlocksDropMousePosition( - topRow: number, - leftColumn: number, -) { - const canvasWidgets: CanvasWidgetsReduxState = yield select(getCanvasWidgets); - let mousePosition = { x: 0, y: 0 }; - - // convert grid position to mouse position for paste functionality - const { canvasDOM, canvasId, containerWidget } = - getDefaultCanvas(canvasWidgets); - if (!canvasDOM || !containerWidget || !canvasId) { - mousePosition = { x: 0, y: 0 }; - } else { - const canvasRect = canvasDOM.getBoundingClientRect(); - const { padding, snapGrid } = getSnappedGrid( - containerWidget, - canvasRect.width, - ); - mousePosition = getMousePositionFromCanvasGridPosition( - topRow, - leftColumn, - snapGrid, - padding, - canvasId as string, - ); - } - - return mousePosition; -} - -function* runSingleAction(actionId: string) { - yield put(runAction(actionId)); - yield take(ReduxActionTypes.RUN_ACTION_SUCCESS); -} - -function* runNewlyCreatedActions( - actionsBeforeAddingBuildingBlock: ActionDataState, - actionsAfterAddingBuildingBlocks: ActionDataState, -) { - const actionIdsBeforeAddingBB = actionsBeforeAddingBuildingBlock.map( - (obj) => obj.config.id, - ); - - const newlyAddedActions = actionsAfterAddingBuildingBlocks.filter( - (obj) => !actionIdsBeforeAddingBB.includes(obj.config.id), - ); - - // Run each action sequentially. We have a max of 2-3 actions per building block. - // If we run this in parallel, we will have a racing condition when multiple building blocks are drag and dropped quickly. - for (const action of newlyAddedActions) { - if (action.config.executeOnLoad) { - yield runSingleAction(action.config.id); - } - } -} - -export function* addBuildingBlockToApplication( - buildingBlockWidget: WidgetAddChild, - skeletonLoaderId: string, -) { - const { leftColumn, topRow } = buildingBlockWidget; - try { - const dragDetails: DragDetails = yield select(getDragDetails); - const applicationId: string = yield select(getCurrentApplicationId); - const workspaceId: string = yield select(getCurrentWorkspaceId); - const actionsBeforeAddingBuildingBlock: ActionDataState = - yield select(getActions); - const existingCopiedWidgets: unknown = yield call(getCopiedWidgets); - const buildingBlockDragStartTimestamp: number = yield select( - getBuildingBlockDragStartTimestamp, - ); - - // start loading for dragging building blocks - yield put({ - type: ReduxActionTypes.DRAGGING_BUILDING_BLOCK_TO_CANVAS_INIT, - }); - - // makes sure updateAndSaveLayout completes first for skeletonWidget addition - yield take(ReduxActionTypes.SAVE_PAGE_SUCCESS); - - const response: ApiResponse = - yield call(addBuildingBlockActionsToApp, dragDetails); - const isValid: boolean = yield validateResponse(response); - - if (isValid) { - yield saveBuildingBlockWidgetsToStore(response); - - const mousePosition: { x: number; y: number } = yield call( - getBuildingBlocksDropMousePosition, - topRow, - leftColumn, - ); - - // remove skeleton loader just before pasting the building block - yield put({ - type: WidgetReduxActionTypes.WIDGET_SINGLE_DELETE, - payload: { - widgetId: skeletonLoaderId, - parentId: MAIN_CONTAINER_WIDGET_ID, - disallowUndo: true, - isShortcut: false, - }, - }); - - yield put(pasteWidget(false, mousePosition)); - const timeTakenToDropWidgetsInSeconds = - (Date.now() - buildingBlockDragStartTimestamp) / 1000; - yield call(postPageAdditionSaga, applicationId); - // remove selecting of recently pasted widgets caused by pasteWidget - yield put(selectWidgetInitAction(SelectionRequestType.Empty)); - - // stop loading after pasting process is complete - yield put({ - type: ReduxActionTypes.DRAGGING_BUILDING_BLOCK_TO_CANVAS_SUCCESS, - }); - - const actionsAfterAddingBuildingBlocks: ActionDataState = - yield select(getActions); - - if ( - response.data.onPageLoadActions && - response.data.onPageLoadActions.length > 0 - ) { - yield runNewlyCreatedActions( - actionsBeforeAddingBuildingBlock, - actionsAfterAddingBuildingBlocks, - ); - } - - const timeTakenToCompleteInMs = buildingBlockDragStartTimestamp - ? Date.now() - buildingBlockDragStartTimestamp - : 0; - const timeTakenToCompleteInSeconds = timeTakenToCompleteInMs / 1000; - - AnalyticsUtil.logEvent("DROP_BUILDING_BLOCK_COMPLETED", { - applicationId, - workspaceId, - source: "explorer", - eventData: { - buildingBlockName: dragDetails.newWidget.displayName, - timeTakenToCompletion: timeTakenToCompleteInSeconds, - timeTakenToDropWidgets: timeTakenToDropWidgetsInSeconds, - }, - }); - yield put({ - type: ReduxActionTypes.RESET_BUILDING_BLOCK_DRAG_START_TIME, - }); - - if (existingCopiedWidgets) { - yield call(saveCopiedWidgets, JSON.stringify(existingCopiedWidgets)); - } - } - } catch (error) { - yield put({ - type: WidgetReduxActionTypes.WIDGET_SINGLE_DELETE, - payload: { - widgetId: skeletonLoaderId, - parentId: MAIN_CONTAINER_WIDGET_ID, - disallowUndo: true, - isShortcut: false, - }, - }); - yield put({ - type: ReduxActionErrorTypes.DRAGGING_BUILDING_BLOCK_TO_CANVAS_ERROR, - }); - } -} - -function* addBuildingBlockSaga(addEntityAction: ReduxAction) { - const applicationId: string = yield select(getCurrentApplicationId); - const workspaceId: string = yield select(getCurrentWorkspaceId); - const dragDetails: DragDetails = yield select(getDragDetails); - const buildingblockName = dragDetails.newWidget.displayName; - const skeletonWidgetName = `loading_${buildingblockName - .toLowerCase() - .replace(/ /g, "_")}`; - const addSkeletonWidgetAction: ReduxAction< - WidgetAddChild & { shouldReplay: boolean } - > = { - ...addEntityAction, - payload: { - ...addEntityAction.payload, - type: "SKELETON_WIDGET", - widgetName: skeletonWidgetName, - widgetId: MAIN_CONTAINER_WIDGET_ID, - // so that the skeleton loader does not get included when the users uses the undo/redo - shouldReplay: false, - }, - }; - - yield call(initiateBuildingBlockDropEvent, { - applicationId, - workspaceId, - buildingblockName, - }); - - yield call(addChildSaga, addSkeletonWidgetAction); - const skeletonWidget: FlattenedWidgetProps = yield select( - getWidgetByName, - skeletonWidgetName, - ); - yield call( - addBuildingBlockToApplication, - addEntityAction.payload, - skeletonWidget.widgetId, - ); -} - function* addUIEntitySaga(addEntityAction: ReduxAction) { try { const { payload } = addEntityAction; const { type } = payload; if (type === BUILDING_BLOCK_EXPLORER_TYPE) { - yield call(addBuildingBlockSaga, addEntityAction); + yield call(addBuildingBlockToCanvasSaga, addEntityAction); } else { yield call(addChildSaga, addEntityAction); } diff --git a/app/client/src/sagas/WidgetOperationSagas.tsx b/app/client/src/sagas/WidgetOperationSagas.tsx index ce2b4fd697ff..9b19df2531eb 100644 --- a/app/client/src/sagas/WidgetOperationSagas.tsx +++ b/app/client/src/sagas/WidgetOperationSagas.tsx @@ -9,6 +9,10 @@ import { } from "@appsmith/constants/ReduxActionConstants"; import { resetWidgetMetaProperty } from "actions/metaActions"; import { selectWidgetInitAction } from "actions/widgetSelectionActions"; +import type { + EitherMouseLocationORGridPosition, + PasteWidgetReduxAction, +} from "constants/WidgetConstants"; import { GridDefaults, MAIN_CONTAINER_WIDGET_ID, @@ -1162,14 +1166,15 @@ export function calculateNewWidgetPosition( * @param copiedTotalWidth total width of the copied widgets * @param copiedTopMostRow top row of the top most copied widget * @param copiedLeftMostColumn left column of the left most copied widget + * @param gridPosition left and top canvas grid position values * @returns */ const getNewPositions = function* ( copiedWidgetGroups: CopiedWidgetGroup[], - mouseLocation: { x: number; y: number }, copiedTotalWidth: number, copiedTopMostRow: number, copiedLeftMostColumn: number, + whereToPasteWidget: EitherMouseLocationORGridPosition, ) { const selectedWidgetIDs: string[] = yield select(getSelectedWidgets); const canvasWidgets: CanvasWidgetsReduxState = yield select(getWidgets); @@ -1215,12 +1220,12 @@ const getNewPositions = function* ( const newPastingPositionDetails: NewPastePositionVariables = yield call( getNewPositionsBasedOnMousePositions, copiedWidgetGroups, - mouseLocation, selectedWidgets, canvasWidgets, copiedTotalWidth, copiedTopMostRow, copiedLeftMostColumn, + whereToPasteWidget, ); return newPastingPositionDetails; }; @@ -1364,16 +1369,17 @@ function* getNewPositionsBasedOnSelectedWidgets( * @param copiedTotalWidth total width of the copied widgets * @param copiedTopMostRow top row of the top most copied widget * @param copiedLeftMostColumn left column of the left most copied widget + * @param gridPosition left and top canvas grid position values * @returns */ function* getNewPositionsBasedOnMousePositions( copiedWidgetGroups: CopiedWidgetGroup[], - mouseLocation: { x: number; y: number }, selectedWidgets: WidgetProps[], canvasWidgets: CanvasWidgetsReduxState, copiedTotalWidth: number, copiedTopMostRow: number, copiedLeftMostColumn: number, + whereToPasteWidget: EitherMouseLocationORGridPosition, ) { let { canvasDOM, canvasId, containerWidget } = getDefaultCanvas(canvasWidgets); @@ -1395,13 +1401,15 @@ function* getNewPositionsBasedOnMousePositions( ); // get mouse positions in terms of grid rows and columns of the pasting canvas - const mousePositions = getMousePositions( - canvasRect, - canvasId, - snapGrid, - padding, - mouseLocation, - ); + const mousePositions = whereToPasteWidget.gridPosition + ? whereToPasteWidget.gridPosition + : getMousePositions( + canvasRect, + canvasId, + snapGrid, + padding, + whereToPasteWidget.mouseLocation, + ); if (!snapGrid || !mousePositions) return {}; @@ -1483,14 +1491,11 @@ function* getNewPositionsBasedOnMousePositions( } /** - * this saga create a new widget from the copied one to store + * This saga create a new widget from the copied one to store. + * It allows using both mouseLocation or gridPosition to locate where the copied widgets should be dropped. + * If gridPosition is available, use it, else, calculate gridPosition from mousePosition */ -function* pasteWidgetSaga( - action: ReduxAction<{ - groupWidgets: boolean; - mouseLocation: { x: number; y: number }; - }>, -) { +function* pasteWidgetSaga(action: ReduxAction) { const { flexLayers, widgets: copiedWidgets, @@ -1580,10 +1585,10 @@ function* pasteWidgetSaga( yield call( getNewPositions, copiedWidgetGroups, - action.payload.mouseLocation, copiedTotalWidth, topMostWidget.topRow, leftMostWidget.leftColumn, + action.payload, )); if (canvasId) pastingIntoWidgetId = canvasId; diff --git a/app/client/src/sagas/WidgetOperationUtils.ts b/app/client/src/sagas/WidgetOperationUtils.ts index 79691a4e8e39..00447a476004 100644 --- a/app/client/src/sagas/WidgetOperationUtils.ts +++ b/app/client/src/sagas/WidgetOperationUtils.ts @@ -700,41 +700,6 @@ function getStickyCanvasDOM(canvasId: string) { return stickyCanvasDOM; } -/** - * calculates mouse positions given canvas grid positions - * - * @param canvasRect canvas DOM rect - * @param canvasId Id of the canvas widget - * @param snapGrid grid parameters - * @param padding padding inside of widget - * @param canvasPosition position in canvas rows and columns - * @returns - */ -export function getMousePositionFromCanvasGridPosition( - top: number, - left: number, - snapGrid: { snapRowSpace: number; snapColumnSpace: number }, - padding: number, - canvasId: string, -) { - // Get the canvas element - const stickyCanvasDOM = getStickyCanvasDOM(canvasId); - - if (!stickyCanvasDOM) return { x: 0, y: 0 }; - - const canvasRect = stickyCanvasDOM.getBoundingClientRect(); - - // Calculate actual mouse positions - const x = left * snapGrid.snapColumnSpace + padding; - const y = top * snapGrid.snapRowSpace + padding; - - // Calculate actual mouse positions relative to the window - const actualX = x + canvasRect.left; - const actualY = y + canvasRect.top; - - return { x: actualX, y: actualY }; -} - /** * calculates mouse positions in terms of grid values *