chore: Refactor widgetselectionSagas to extract PartialImportExportSagas (#30295)

## 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


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## 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.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Rahul Barwal 2024-01-17 12:43:51 +05:30 committed by GitHub
parent 1949c79142
commit 1e50debe2a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 240 additions and 226 deletions

View File

@ -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 {

View File

@ -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";

View File

@ -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<PartialExportParams>) {
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;
}

View File

@ -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,
},
});
}
}

View File

@ -0,0 +1,2 @@
export * from "./PartialExportSagas";
export * from "./PartialImportSagas";

View File

@ -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";

View File

@ -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<PartialExportParams>) {
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,
},
});
}
}