From 1e50debe2a5471cd258509df8cfd8daf37b04be6 Mon Sep 17 00:00:00 2001 From: Rahul Barwal Date: Wed, 17 Jan 2024 12:43:51 +0530 Subject: [PATCH] chore: Refactor widgetselectionSagas to extract PartialImportExportSagas (#30295) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description This pull request refactors `WidgetSelectionSagas` and extracts partial import export logic. Adds the PartialImportExportSagas file and updates the imports in multiple files to use the new file. #### PR fixes following issue(s) Fixes # (issue number) > if no issue exists, please create an issue and ask the maintainers about this first > > #### Media > A video or a GIF is preferred. when using Loom, don’t embed because it looks like it’s a GIF. instead, just link to the video > > #### Type of change > Please delete options that are not relevant. - Bug fix (non-breaking change which fixes an issue) - New feature (non-breaking change which adds functionality) - Breaking change (fix or feature that would cause existing functionality to not work as expected) - Chore (housekeeping or task changes that don't impact user perception) - This change requires a documentation update > > > ## Testing > #### How Has This Been Tested? > Please describe the tests that you ran to verify your changes. Also list any relevant details for your test configuration. > Delete anything that is not relevant - [ ] Manual - [ ] JUnit - [ ] Jest - [ ] Cypress > > #### Test Plan > Add Testsmith test cases links that relate to this PR > > #### Issues raised during DP testing > Link issues raised during DP testing for better visiblity and tracking (copy link from comments dropped on this PR) > > > ## Checklist: #### Dev activity - [ ] My code follows the style guidelines of this project - [ ] I have performed a self-review of my own code - [ ] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [ ] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] New and existing unit tests pass locally with my changes - [ ] PR is being merged under a feature flag #### QA activity: - [ ] [Speedbreak features](https://github.com/appsmithorg/TestSmith/wiki/Guidelines-for-test-plans#speedbreakers-) have been covered - [ ] Test plan covers all impacted features and [areas of interest](https://github.com/appsmithorg/TestSmith/wiki/Guidelines-for-test-plans#areas-of-interest-) - [ ] Test plan has been peer reviewed by project stakeholders and other QA members - [ ] Manually tested functionality on DP - [ ] We had an implementation alignment call with stakeholders post QA Round 2 - [ ] Cypress test cases have been added and approved by SDET/manual QA - [ ] Added `Test Plan Approved` label after Cypress tests were reviewed - [ ] Added `Test Plan Approved` label after JUnit tests were reviewed ## Summary by CodeRabbit - **New Features** - Introduced functionality for exporting and importing partial applications, including widgets, queries, datasources, and custom JavaScript libraries. - **Refactor** - Updated import statements to reflect the new structure for partial import and export functionality. - **Chores** - Performed code cleanup and reformatting in import and selection sagas. --- app/client/src/actions/widgetActions.tsx | 2 +- .../PartialExportModal/index.tsx | 2 +- .../PartialExportSagas.ts | 117 +++++++++ .../PartialImportSagas.ts | 111 +++++++++ .../sagas/PartialImportExportSagas/index.ts | 2 + app/client/src/sagas/WidgetOperationSagas.tsx | 6 +- app/client/src/sagas/WidgetSelectionSagas.ts | 226 +----------------- 7 files changed, 240 insertions(+), 226 deletions(-) create mode 100644 app/client/src/sagas/PartialImportExportSagas/PartialExportSagas.ts create mode 100644 app/client/src/sagas/PartialImportExportSagas/PartialImportSagas.ts create mode 100644 app/client/src/sagas/PartialImportExportSagas/index.ts diff --git a/app/client/src/actions/widgetActions.tsx b/app/client/src/actions/widgetActions.tsx index 8527bd716a..b67ca7ef21 100644 --- a/app/client/src/actions/widgetActions.tsx +++ b/app/client/src/actions/widgetActions.tsx @@ -7,7 +7,7 @@ import type { ExecuteTriggerPayload } from "constants/AppsmithActionConstants/Ac import type { BatchAction } from "actions/batchActions"; import { batchAction } from "actions/batchActions"; import type { WidgetProps } from "widgets/BaseWidget"; -import type { PartialExportParams } from "sagas/WidgetSelectionSagas"; +import type { PartialExportParams } from "sagas/PartialImportExportSagas"; export const widgetInitialisationSuccess = () => { return { diff --git a/app/client/src/components/editorComponents/PartialImportExport/PartialExportModal/index.tsx b/app/client/src/components/editorComponents/PartialImportExport/PartialExportModal/index.tsx index 525c2067fc..d4ed147236 100644 --- a/app/client/src/components/editorComponents/PartialImportExport/PartialExportModal/index.tsx +++ b/app/client/src/components/editorComponents/PartialImportExport/PartialExportModal/index.tsx @@ -28,7 +28,7 @@ import { useAppWideAndOtherDatasource } from "@appsmith/pages/Editor/Explorer/ho import React, { useEffect, useMemo, useState } from "react"; import { useDispatch, useSelector } from "react-redux"; import type { CanvasStructure } from "reducers/uiReducers/pageCanvasStructureReducer"; -import type { PartialExportParams } from "sagas/WidgetSelectionSagas"; +import type { PartialExportParams } from "sagas/PartialImportExportSagas"; import { getCurrentPageName } from "selectors/editorSelectors"; import type { JSLibrary } from "workers/common/JSLibrary"; import EntityCheckboxSelector from "./EntityCheckboxSelector"; diff --git a/app/client/src/sagas/PartialImportExportSagas/PartialExportSagas.ts b/app/client/src/sagas/PartialImportExportSagas/PartialExportSagas.ts new file mode 100644 index 0000000000..5291ab088e --- /dev/null +++ b/app/client/src/sagas/PartialImportExportSagas/PartialExportSagas.ts @@ -0,0 +1,117 @@ +import ApplicationApi, { + type exportApplicationRequest, +} from "@appsmith/api/ApplicationApi"; +import type { + ApplicationPayload, + ReduxAction, +} from "@appsmith/constants/ReduxActionConstants"; +import { + ReduxActionErrorTypes, + ReduxActionTypes, +} from "@appsmith/constants/ReduxActionConstants"; +import { getCurrentApplication } from "@appsmith/selectors/applicationSelectors"; +import { toast } from "design-system"; +import { getFlexLayersForSelectedWidgets } from "layoutSystems/autolayout/utils/AutoLayoutUtils"; +import type { FlexLayer } from "layoutSystems/autolayout/utils/types"; +import type { FlattenedWidgetProps } from "reducers/entityReducers/canvasWidgetsReducer"; +import { all, call, put, select } from "redux-saga/effects"; +import { + getCurrentApplicationId, + getCurrentPageId, +} from "selectors/editorSelectors"; +import { validateResponse } from "../ErrorSagas"; +import { createWidgetCopy } from "../WidgetOperationUtils"; +import { getWidgets } from "../selectors"; + +export interface PartialExportParams { + jsObjects: string[]; + datasources: string[]; + customJSLibs: string[]; + widgets: string[]; + queries: string[]; +} + +export function* partialExportSaga(action: ReduxAction) { + try { + const canvasWidgets: unknown = yield partialExportWidgetSaga( + action.payload.widgets, + ); + const applicationId: string = yield select(getCurrentApplicationId); + const currentPageId: string = yield select(getCurrentPageId); + + const body: exportApplicationRequest = { + actionList: action.payload.queries, + actionCollectionList: action.payload.jsObjects, + datasourceList: action.payload.datasources, + customJsLib: action.payload.customJSLibs, + widget: JSON.stringify(canvasWidgets), + }; + + const response: unknown = yield call( + ApplicationApi.exportPartialApplication, + applicationId, + currentPageId, + body, + ); + const isValid: boolean = yield validateResponse(response); + if (isValid) { + const application: ApplicationPayload = yield select( + getCurrentApplication, + ); + + (function downloadJSON(response: unknown) { + const dataStr = + "data:text/json;charset=utf-8," + + encodeURIComponent(JSON.stringify(response)); + const downloadAnchorNode = document.createElement("a"); + downloadAnchorNode.setAttribute("href", dataStr); + downloadAnchorNode.setAttribute("download", `${application.name}.json`); + document.body.appendChild(downloadAnchorNode); // required for firefox + downloadAnchorNode.click(); + downloadAnchorNode.remove(); + })((response as { data: unknown }).data); + yield put({ + type: ReduxActionTypes.PARTIAL_EXPORT_SUCCESS, + }); + } + } catch (e) { + toast.show(`Error exporting application. Please try again.`, { + kind: "error", + }); + yield put({ + type: ReduxActionErrorTypes.PARTIAL_EXPORT_ERROR, + payload: { + error: "Error exporting application", + }, + }); + } +} + +export function* partialExportWidgetSaga(widgetIds: string[]) { + const canvasWidgets: { + [widgetId: string]: FlattenedWidgetProps; + } = yield select(getWidgets); + const selectedWidgets = widgetIds.map((each) => canvasWidgets[each]); + + if (!selectedWidgets || !selectedWidgets.length) return; + + const widgetListsToStore: { + widgetId: string; + parentId: string; + list: FlattenedWidgetProps[]; + }[] = yield all( + selectedWidgets.map((widget) => call(createWidgetCopy, widget)), + ); + + const canvasId = selectedWidgets?.[0]?.parentId || ""; + + const flexLayers: FlexLayer[] = getFlexLayersForSelectedWidgets( + widgetIds, + canvasId ? canvasWidgets[canvasId] : undefined, + ); + const widgetsDSL = { + widgets: widgetListsToStore, + flexLayers, + }; + return widgetsDSL; +} diff --git a/app/client/src/sagas/PartialImportExportSagas/PartialImportSagas.ts b/app/client/src/sagas/PartialImportExportSagas/PartialImportSagas.ts new file mode 100644 index 0000000000..e908bc3b1f --- /dev/null +++ b/app/client/src/sagas/PartialImportExportSagas/PartialImportSagas.ts @@ -0,0 +1,111 @@ +import { + importPartialApplicationSuccess, + initDatasourceConnectionDuringImportRequest, +} from "@appsmith/actions/applicationActions"; +import ApplicationApi from "@appsmith/api/ApplicationApi"; +import type { ReduxAction } from "@appsmith/constants/ReduxActionConstants"; +import { ReduxActionErrorTypes } from "@appsmith/constants/ReduxActionConstants"; +import type { AppState } from "@appsmith/reducers"; +import { areEnvironmentsFetched } from "@appsmith/selectors/environmentSelectors"; +import { getCurrentWorkspaceId } from "@appsmith/selectors/workspaceSelectors"; +import { pasteWidget } from "actions/widgetActions"; +import { selectWidgetInitAction } from "actions/widgetSelectionActions"; +import type { ApiResponse } from "api/ApiResponses"; +import { toast } from "design-system"; +import { call, fork, put, select } from "redux-saga/effects"; +import { SelectionRequestType } from "sagas/WidgetSelectUtils"; +import { + getCurrentApplicationId, + getCurrentPageId, +} from "selectors/editorSelectors"; +import { getCopiedWidgets, saveCopiedWidgets } from "utils/storage"; +import { validateResponse } from "../ErrorSagas"; +import { postPageAdditionSaga } from "../TemplatesSagas"; + +async function readJSONFile(file: File) { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => { + try { + const json = JSON.parse(reader.result as string); + resolve(json); + } catch (e) { + reject(e); + } + }; + reader.readAsText(file); + }); +} + +function* partialImportWidgetsSaga(file: File) { + const existingCopiedWidgets: unknown = yield call(getCopiedWidgets); + try { + // assume that action.payload.applicationFile is a JSON file. Parse it and extract widgets property + const userUploadedJSON: { widgets: string } = yield call( + readJSONFile, + 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 })); + } + } finally { + if (existingCopiedWidgets) { + yield call(saveCopiedWidgets, JSON.stringify(existingCopiedWidgets)); + } + } +} + +export function* partialImportSaga( + action: ReduxAction<{ applicationFile: File }>, +) { + try { + // Step1: Import widgets from file, in parallel + yield fork(partialImportWidgetsSaga, action.payload.applicationFile); + // Step2: Send backend request to import pending items. + const workspaceId: string = yield select(getCurrentWorkspaceId); + const pageId: string = yield select(getCurrentPageId); + const applicationId: string = yield select(getCurrentApplicationId); + const response: ApiResponse = yield call( + ApplicationApi.importPartialApplication, + { + applicationFile: action.payload.applicationFile, + workspaceId, + pageId, + applicationId, + }, + ); + + const isValidResponse: boolean = yield validateResponse(response); + + if (isValidResponse) { + yield call(postPageAdditionSaga, applicationId); + toast.show("Partial Application imported successfully", { + kind: "success", + }); + + const environmentsFetched: boolean = yield select((state: AppState) => + areEnvironmentsFetched(state, workspaceId), + ); + + if (workspaceId && environmentsFetched) { + yield put( + initDatasourceConnectionDuringImportRequest({ + workspaceId: workspaceId as string, + isPartialImport: true, + }), + ); + } + + yield put(importPartialApplicationSuccess()); + } + } catch (error) { + yield put({ + type: ReduxActionErrorTypes.PARTIAL_IMPORT_ERROR, + payload: { + error, + }, + }); + } +} diff --git a/app/client/src/sagas/PartialImportExportSagas/index.ts b/app/client/src/sagas/PartialImportExportSagas/index.ts new file mode 100644 index 0000000000..55b89e7b47 --- /dev/null +++ b/app/client/src/sagas/PartialImportExportSagas/index.ts @@ -0,0 +1,2 @@ +export * from "./PartialExportSagas"; +export * from "./PartialImportSagas"; diff --git a/app/client/src/sagas/WidgetOperationSagas.tsx b/app/client/src/sagas/WidgetOperationSagas.tsx index 53869a27b2..42b68a12e6 100644 --- a/app/client/src/sagas/WidgetOperationSagas.tsx +++ b/app/client/src/sagas/WidgetOperationSagas.tsx @@ -136,11 +136,11 @@ import { mergeDynamicPropertyPaths, purgeOrphanedDynamicPaths, } from "./WidgetOperationUtils"; +import { widgetSelectionSagas } from "./WidgetSelectionSagas"; import { - partialImportSaga, partialExportSaga, - widgetSelectionSagas, -} from "./WidgetSelectionSagas"; + partialImportSaga, +} from "./PartialImportExportSagas"; import type { WidgetEntityConfig } from "@appsmith/entities/DataTree/types"; import type { DataTree, ConfigTree } from "entities/DataTree/dataTreeTypes"; import { getCanvasSizeAfterWidgetMove } from "./CanvasSagas/DraggingCanvasSagas"; diff --git a/app/client/src/sagas/WidgetSelectionSagas.ts b/app/client/src/sagas/WidgetSelectionSagas.ts index 483c78c2e6..715ee4a5f2 100644 --- a/app/client/src/sagas/WidgetSelectionSagas.ts +++ b/app/client/src/sagas/WidgetSelectionSagas.ts @@ -1,54 +1,27 @@ import { widgetURL } from "@appsmith/RouteBuilder"; -import { - importPartialApplicationSuccess, - initDatasourceConnectionDuringImportRequest, -} from "@appsmith/actions/applicationActions"; -import ApplicationApi, { - type exportApplicationRequest, -} from "@appsmith/api/ApplicationApi"; -import type { - ApplicationPayload, - ReduxAction, -} from "@appsmith/constants/ReduxActionConstants"; +import type { ReduxAction } from "@appsmith/constants/ReduxActionConstants"; import { ReduxActionErrorTypes, ReduxActionTypes, } from "@appsmith/constants/ReduxActionConstants"; -import { getCurrentApplication } from "@appsmith/selectors/applicationSelectors"; import { getAppMode, getCanvasWidgets, } from "@appsmith/selectors/entitiesSelector"; -import { pasteWidget, showModal } from "actions/widgetActions"; +import { showModal } from "actions/widgetActions"; import type { SetSelectedWidgetsPayload, WidgetSelectionRequestPayload, } from "actions/widgetSelectionActions"; import { - selectWidgetInitAction, setEntityExplorerAncestry, setSelectedWidgetAncestry, setSelectedWidgets, } from "actions/widgetSelectionActions"; -import type { ApiResponse } from "api/ApiResponses"; -import { getCurrentWorkspaceId } from "@appsmith/selectors/workspaceSelectors"; -import { toast } from "design-system"; +import { MAIN_CONTAINER_WIDGET_ID } from "constants/WidgetConstants"; import { APP_MODE } from "entities/App"; -import { getFlexLayersForSelectedWidgets } from "layoutSystems/autolayout/utils/AutoLayoutUtils"; -import type { FlexLayer } from "layoutSystems/autolayout/utils/types"; -import type { - CanvasWidgetsReduxState, - FlattenedWidgetProps, -} from "reducers/entityReducers/canvasWidgetsReducer"; -import { - all, - call, - fork, - put, - select, - take, - takeLatest, -} from "redux-saga/effects"; +import type { CanvasWidgetsReduxState } from "reducers/entityReducers/canvasWidgetsReducer"; +import { all, call, put, select, take, takeLatest } from "redux-saga/effects"; import type { SetSelectionResult } from "sagas/WidgetSelectUtils"; import { SelectionRequestType, @@ -63,7 +36,6 @@ import { unselectWidget, } from "sagas/WidgetSelectUtils"; import { - getCurrentApplicationId, getCurrentPageId, getIsEditorInitialized, getIsFetchingPage, @@ -73,18 +45,11 @@ import { getLastSelectedWidget, getSelectedWidgets } from "selectors/ui"; import { areArraysEqual } from "utils/AppsmithUtils"; import { quickScrollToWidget } from "utils/helpers"; import history, { NavigationMethod } from "utils/history"; -import { getCopiedWidgets, saveCopiedWidgets } from "utils/storage"; -import { validateResponse } from "./ErrorSagas"; -import { postPageAdditionSaga } from "./TemplatesSagas"; -import { createWidgetCopy } from "./WidgetOperationUtils"; import { getWidgetIdsByType, getWidgetImmediateChildren, getWidgets, } from "./selectors"; -import type { AppState } from "@appsmith/reducers"; -import { areEnvironmentsFetched } from "@appsmith/selectors/environmentSelectors"; -import { MAIN_CONTAINER_WIDGET_ID } from "constants/WidgetConstants"; // The following is computed to be used in the entity explorer // Every time a widget is selected, we need to expand widget entities @@ -365,184 +330,3 @@ export function* widgetSelectionSagas() { ), ]); } - -export interface PartialExportParams { - jsObjects: string[]; - datasources: string[]; - customJSLibs: string[]; - widgets: string[]; - queries: string[]; -} - -export function* partialExportSaga(action: ReduxAction) { - try { - const canvasWidgets: unknown = yield partialExportWidgetSaga( - action.payload.widgets, - ); - const applicationId: string = yield select(getCurrentApplicationId); - const currentPageId: string = yield select(getCurrentPageId); - - const body: exportApplicationRequest = { - actionList: action.payload.queries, - actionCollectionList: action.payload.jsObjects, - datasourceList: action.payload.datasources, - customJsLib: action.payload.customJSLibs, - widget: JSON.stringify(canvasWidgets), - }; - - const response: unknown = yield call( - ApplicationApi.exportPartialApplication, - applicationId, - currentPageId, - body, - ); - const isValid: boolean = yield validateResponse(response); - if (isValid) { - const application: ApplicationPayload = yield select( - getCurrentApplication, - ); - - (function downloadJSON(response: unknown) { - const dataStr = - "data:text/json;charset=utf-8," + - encodeURIComponent(JSON.stringify(response)); - const downloadAnchorNode = document.createElement("a"); - downloadAnchorNode.setAttribute("href", dataStr); - downloadAnchorNode.setAttribute("download", `${application.name}.json`); - document.body.appendChild(downloadAnchorNode); // required for firefox - downloadAnchorNode.click(); - downloadAnchorNode.remove(); - })((response as { data: unknown }).data); - yield put({ - type: ReduxActionTypes.PARTIAL_EXPORT_SUCCESS, - }); - } - } catch (e) { - toast.show(`Error exporting application. Please try again.`, { - kind: "error", - }); - yield put({ - type: ReduxActionErrorTypes.PARTIAL_EXPORT_ERROR, - payload: { - error: "Error exporting application", - }, - }); - } -} - -export function* partialExportWidgetSaga(widgetIds: string[]) { - const canvasWidgets: { - [widgetId: string]: FlattenedWidgetProps; - } = yield select(getWidgets); - const selectedWidgets = widgetIds.map((each) => canvasWidgets[each]); - - if (!selectedWidgets || !selectedWidgets.length) return; - - const widgetListsToStore: { - widgetId: string; - parentId: string; - list: FlattenedWidgetProps[]; - }[] = yield all( - selectedWidgets.map((widget) => call(createWidgetCopy, widget)), - ); - - const canvasId = selectedWidgets?.[0]?.parentId || ""; - - const flexLayers: FlexLayer[] = getFlexLayersForSelectedWidgets( - widgetIds, - canvasId ? canvasWidgets[canvasId] : undefined, - ); - const widgetsDSL = { - widgets: widgetListsToStore, - flexLayers, - }; - return widgetsDSL; -} - -async function readJSONFile(file: File) { - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onload = () => { - try { - const json = JSON.parse(reader.result as string); - resolve(json); - } catch (e) { - reject(e); - } - }; - reader.readAsText(file); - }); -} - -function* partialImportWidgetsSaga(file: File) { - const existingCopiedWidgets: unknown = yield call(getCopiedWidgets); - try { - // assume that action.payload.applicationFile is a JSON file. Parse it and extract widgets property - const userUploadedJSON: { widgets: string } = yield call( - readJSONFile, - 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 })); - } - } finally { - if (existingCopiedWidgets) { - yield call(saveCopiedWidgets, JSON.stringify(existingCopiedWidgets)); - } - } -} - -export function* partialImportSaga( - action: ReduxAction<{ applicationFile: File }>, -) { - try { - // Step1: Import widgets from file, in parallel - yield fork(partialImportWidgetsSaga, action.payload.applicationFile); - // Step2: Send backend request to import pending items. - const workspaceId: string = yield select(getCurrentWorkspaceId); - const pageId: string = yield select(getCurrentPageId); - const applicationId: string = yield select(getCurrentApplicationId); - const response: ApiResponse = yield call( - ApplicationApi.importPartialApplication, - { - applicationFile: action.payload.applicationFile, - workspaceId, - pageId, - applicationId, - }, - ); - - const isValidResponse: boolean = yield validateResponse(response); - - if (isValidResponse) { - yield call(postPageAdditionSaga, applicationId); - toast.show("Partial Application imported successfully", { - kind: "success", - }); - - const environmentsFetched: boolean = yield select((state: AppState) => - areEnvironmentsFetched(state, workspaceId), - ); - - if (workspaceId && environmentsFetched) { - yield put( - initDatasourceConnectionDuringImportRequest({ - workspaceId: workspaceId as string, - isPartialImport: true, - }), - ); - } - - yield put(importPartialApplicationSuccess()); - } - } catch (error) { - yield put({ - type: ReduxActionErrorTypes.PARTIAL_IMPORT_ERROR, - payload: { - error, - }, - }); - } -}