From ae805567580d277cb5adcd18728e8cf6393be48d Mon Sep 17 00:00:00 2001 From: Pawan Kumar Date: Mon, 31 Mar 2025 15:43:18 +0530 Subject: [PATCH] chore: Update integration modal (#39976) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit /ok-to-test tags="@tag.Datasource" ## Summary by CodeRabbit ## Summary by CodeRabbit - **New Features** - Action creators now support optional callbacks and redirection controls to enhance query operations. - UI tabs can now be configured to remain persistently mounted, ensuring a smoother editing experience. - New functionality to add datasources to the existing list without complex tracking. - **Bug Fixes** - Simplified logic for determining the saving state of datasources, improving UI responsiveness. - **Refactor** - Datasource creation and saving state handling have been streamlined for improved reliability. - Enhanced management of action payloads and state updates enables more robust processing of user operations. - Improved type safety and clarity in action handling within sagas for better maintainability. > [!TIP] > 🟢 🟢 🟢 All cypress tests have passed! 🎉 🎉 🎉 > Workflow run: > Commit: be05d6bda56aff96421faf435804f56707a597ba > Cypress dashboard. > Tags: `@tag.Datasource` > Spec: >
Mon, 31 Mar 2025 09:14:22 UTC --- app/client/src/actions/datasourceActions.ts | 11 ++++ app/client/src/actions/pluginActionActions.ts | 16 ++++- app/client/src/ce/sagas/DatasourcesSagas.ts | 54 +++++----------- app/client/src/ce/sagas/helpers.ts | 44 +++++++++++++ .../editorComponents/EntityBottomTabs.tsx | 7 ++- .../pages/Editor/DataSourceEditor/index.tsx | 2 +- .../Editor/SaaSEditor/DatasourceForm.tsx | 2 +- .../entityReducers/datasourceReducer.ts | 19 +++--- app/client/src/sagas/ActionSagas.ts | 61 +++++++++++++------ app/client/src/sagas/QueryPaneSagas.ts | 31 ++++++---- app/client/src/sagas/selectors.tsx | 5 +- 11 files changed, 162 insertions(+), 90 deletions(-) diff --git a/app/client/src/actions/datasourceActions.ts b/app/client/src/actions/datasourceActions.ts index 876971a6fa..39e5e3e7ff 100644 --- a/app/client/src/actions/datasourceActions.ts +++ b/app/client/src/actions/datasourceActions.ts @@ -396,6 +396,17 @@ export const setUnconfiguredDatasourcesDuringImport = ( payload, }); +// this actions concats the newly created datasource to the datasource list +// it's not required if createDatasourceSuccess is used, that also concats the new datasource to the datasource list +// The main difference between this and createDatasourceSuccess is that this action only adds the datasource to the datasource list +// and is not tracked by any saga, whereas createDatasourceSuccess is tracked by many sagas with different logic. +export const updateDatasoruceRefs = (datasource: Datasource) => { + return { + type: ReduxActionTypes.UPDATE_DATASOURCE_REFS, + payload: datasource, + }; +}; + export const removeTempDatasource = () => { return { type: ReduxActionTypes.REMOVE_TEMP_DATASOURCE_SUCCESS, diff --git a/app/client/src/actions/pluginActionActions.ts b/app/client/src/actions/pluginActionActions.ts index f2afb2a8ff..ef0bcd204e 100644 --- a/app/client/src/actions/pluginActionActions.ts +++ b/app/client/src/actions/pluginActionActions.ts @@ -21,20 +21,30 @@ import type { GenerateDestinationIdInfoReturnType } from "ee/sagas/helpers"; import type { Span } from "instrumentation/types"; import type { EvaluationReduxAction } from "./EvaluationReduxActionTypes"; -export const createActionRequest = (payload: Partial) => { +export const createActionRequest = ( + payload: Partial, + onSuccess?: ReduxAction, +) => { return { type: ReduxActionTypes.CREATE_ACTION_REQUEST, payload, + onSuccess, }; }; -export const createActionInit = (payload: Partial) => { +export const createActionInit = ( + payload: Partial, + onSuccess?: ReduxAction, +) => { return { type: ReduxActionTypes.CREATE_ACTION_INIT, payload, + onSuccess, }; }; -export const createActionSuccess = (payload: Action) => { +export const createActionSuccess = ( + payload: Action & { shouldRedirectToQueryEditor?: boolean }, +) => { return { type: ReduxActionTypes.CREATE_ACTION_SUCCESS, payload, diff --git a/app/client/src/ce/sagas/DatasourcesSagas.ts b/app/client/src/ce/sagas/DatasourcesSagas.ts index 75d2bf7ee8..d3975bd164 100644 --- a/app/client/src/ce/sagas/DatasourcesSagas.ts +++ b/app/client/src/ce/sagas/DatasourcesSagas.ts @@ -52,6 +52,7 @@ import { changeDatasource, createDatasourceSuccess, createTempDatasourceFromForm, + deleteTempDSFromDraft, fetchDatasourceStructure, removeTempDatasource, resetDefaultKeyValPairFlag, @@ -184,6 +185,7 @@ import type { ActionParentEntityTypeInterface } from "ee/entities/Engine/actionH import { getCurrentModuleId } from "ee/selectors/modulesSelector"; import type { ApplicationPayload } from "entities/Application"; import { openGeneratePageModalWithSelectedDS } from "../../utils/GeneratePageUtils"; +import { createDatasourceAPIPayloadFromAction } from "ee/sagas/helpers"; export function* fetchDatasourcesSaga( action: ReduxAction< @@ -1189,9 +1191,8 @@ export function* createDatasourceFromFormSaga( checkAndGetPluginFormConfigsSaga, actionPayload.payload.pluginId, ); - // TODO: Fix this the next time the file is edited - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const formConfig: Record[] = yield select( + + const formConfig: ReturnType = yield select( getPluginForm, actionPayload.payload.pluginId, ); @@ -1199,35 +1200,16 @@ export function* createDatasourceFromFormSaga( getCurrentEditingEnvironmentId, ); - const initialValues: unknown = yield call( + const initialValues: ReturnType = yield call( getConfigInitialValues, formConfig, ); - let datasourceStoragePayload = - actionPayload.payload.datasourceStorages[currentEnvironment]; - datasourceStoragePayload = merge(initialValues, datasourceStoragePayload); - - // in the datasourcestorages, we only need one key, the currentEnvironment - // we need to remove any other keys present - const datasourceStorages = { - [currentEnvironment]: datasourceStoragePayload, - }; - - const payload = omit( - { - ...actionPayload.payload, - datasourceStorages, - }, - ["id", "new", "type", "datasourceConfiguration"], - ); - - if (payload.datasourceStorages) - datasourceStoragePayload.isConfigured = true; - - // remove datasourceId from payload if it is equal to TEMP_DATASOURCE_ID - if (datasourceStoragePayload.datasourceId === TEMP_DATASOURCE_ID) - datasourceStoragePayload.datasourceId = ""; + const payload = createDatasourceAPIPayloadFromAction({ + actionPayload: actionPayload.payload, + currentEnvId: currentEnvironment, + initialValues, + }); const response: ApiResponse = yield DatasourcesApi.createDatasource({ @@ -1260,14 +1242,11 @@ export function* createDatasourceFromFormSaga( isFormValid: isFormValid, editedFields: formDiffPaths, connectionMethod: getConnectionMethod( - datasourceStoragePayload, + payload.datasourceStorages?.[currentEnvironment] as DatasourceStorage, plugin?.packageName, ), }); - yield put({ - type: ReduxActionTypes.UPDATE_DATASOURCE_REFS, - payload: response.data, - }); + yield put( createDatasourceSuccess( response.data, @@ -1307,12 +1286,7 @@ export function* createDatasourceFromFormSaga( yield put(actionPayload.onSuccess); } - yield put({ - type: ReduxActionTypes.DELETE_DATASOURCE_DRAFT, - payload: { - id: TEMP_DATASOURCE_ID, - }, - }); + yield put(deleteTempDSFromDraft()); // for all datasources, except for REST and GraphQL, need to delete temp datasource data // as soon as possible, for REST and GraphQL it is getting deleted in APIPaneSagas.ts @@ -1323,6 +1297,8 @@ export function* createDatasourceFromFormSaga( // updating form initial values to latest data, so that next time when form is opened // isDirty will use updated initial values data to compare actual values with yield put(initialize(DATASOURCE_DB_FORM, response.data)); + + return response.data; } } catch (error) { yield put({ diff --git a/app/client/src/ce/sagas/helpers.ts b/app/client/src/ce/sagas/helpers.ts index 6a57f1cf32..ecc2cf2097 100644 --- a/app/client/src/ce/sagas/helpers.ts +++ b/app/client/src/ce/sagas/helpers.ts @@ -1,9 +1,15 @@ +import omit from "lodash/omit"; +import merge from "lodash/merge"; import type { CreateNewActionKeyInterface } from "ee/entities/Engine/actionHelpers"; import { CreateNewActionKey } from "ee/entities/Engine/actionHelpers"; import type { DeleteErrorLogPayload } from "actions/debuggerActions"; import type { Action } from "entities/Action"; import type { Log } from "entities/AppsmithConsole"; import type { EvaluationError } from "utils/DynamicBindingUtils"; +import { TEMP_DATASOURCE_ID } from "constants/Datasource"; +import type { getConfigInitialValues } from "components/formControls/utils"; +import type { CreateDatasourceConfig } from "ee/api/DatasourcesApi"; +import type { Datasource } from "entities/Datasource"; export interface ResolveParentEntityMetadataReturnType { parentEntityId?: string; @@ -52,3 +58,41 @@ export function* transformDeleteErrorLogsSaga(payload: DeleteErrorLogPayload) { export function* transformTriggerEvalErrors(errors: EvaluationError[]) { return errors; } + +interface CreateDatasourcePayloadFromActionParams { + currentEnvId: string; + actionPayload: Datasource | CreateDatasourceConfig; + initialValues: ReturnType; +} + +export const createDatasourceAPIPayloadFromAction = ( + props: CreateDatasourcePayloadFromActionParams, +) => { + const { actionPayload, currentEnvId, initialValues } = props; + + let datasourceStoragePayload = actionPayload.datasourceStorages[currentEnvId]; + + datasourceStoragePayload = merge(initialValues, datasourceStoragePayload); + + // in the datasourcestorages, we only need one key, the currentEnvironment + // we need to remove any other keys present + const datasourceStorages = { + [currentEnvId]: datasourceStoragePayload, + }; + + const payload = omit( + { + ...actionPayload, + datasourceStorages, + }, + ["id", "new", "type", "datasourceConfiguration"], + ); + + if (payload.datasourceStorages) datasourceStoragePayload.isConfigured = true; + + // remove datasourceId from payload if it is equal to TEMP_DATASOURCE_ID + if (datasourceStoragePayload.datasourceId === TEMP_DATASOURCE_ID) + datasourceStoragePayload.datasourceId = ""; + + return payload; +}; diff --git a/app/client/src/components/editorComponents/EntityBottomTabs.tsx b/app/client/src/components/editorComponents/EntityBottomTabs.tsx index 01f8eeb5da..c083faf6d7 100644 --- a/app/client/src/components/editorComponents/EntityBottomTabs.tsx +++ b/app/client/src/components/editorComponents/EntityBottomTabs.tsx @@ -35,6 +35,7 @@ export interface BottomTab { title: string; count?: number; panelComponent: React.ReactNode; + forceMount?: true; } interface EntityBottomTabsProps { @@ -93,7 +94,11 @@ function EntityBottomTabs( })} {props.tabs.map((tab) => ( - + {tab.panelComponent} ))} diff --git a/app/client/src/pages/Editor/DataSourceEditor/index.tsx b/app/client/src/pages/Editor/DataSourceEditor/index.tsx index 0e4634cbd9..d3c531ced9 100644 --- a/app/client/src/pages/Editor/DataSourceEditor/index.tsx +++ b/app/client/src/pages/Editor/DataSourceEditor/index.tsx @@ -1199,7 +1199,7 @@ const mapStateToProps = (state: AppState, props: any): ReduxStateProps => { formName, isInsideReconnectModal: props.isInsideReconnectModal ?? false, pluginId, - isSaving: datasources.loading && datasources.loadingPluginId === pluginId, + isSaving: datasources.loading, isDeleting: !!(datasource as Datasource)?.isDeleting, isPluginAuthorized: !!isPluginAuthorized, isTesting: datasources.isTesting, diff --git a/app/client/src/pages/Editor/SaaSEditor/DatasourceForm.tsx b/app/client/src/pages/Editor/SaaSEditor/DatasourceForm.tsx index 75e4dd88ec..575595a8fd 100644 --- a/app/client/src/pages/Editor/SaaSEditor/DatasourceForm.tsx +++ b/app/client/src/pages/Editor/SaaSEditor/DatasourceForm.tsx @@ -858,7 +858,7 @@ const mapStateToProps = (state: AppState, props: any) => { datasourceButtonConfiguration, datasourceId, documentationLink: documentationLinks[pluginId], - isSaving: datasources.loading && datasources.loadingPluginId === pluginId, + isSaving: datasources.loading, isDeleting: !!datasource?.isDeleting, isTesting: datasources.isTesting, formData: formData, diff --git a/app/client/src/reducers/entityReducers/datasourceReducer.ts b/app/client/src/reducers/entityReducers/datasourceReducer.ts index 7118b1d17f..70cbb19427 100644 --- a/app/client/src/reducers/entityReducers/datasourceReducer.ts +++ b/app/client/src/reducers/entityReducers/datasourceReducer.ts @@ -19,8 +19,6 @@ import { assign } from "lodash"; export interface DatasourceDataState { list: Datasource[]; loading: boolean; - // this prop tells which plugin is being loaded. Mainly used on the save button of datasource editor page. - loadingPluginId: string | null; loadingTokenForDatasourceId: string | null; isTesting: boolean; isListing: boolean; // fetching unconfigured datasource list @@ -51,7 +49,6 @@ export interface DatasourceDataState { const initialState: DatasourceDataState = { list: [], loading: false, - loadingPluginId: null, loadingTokenForDatasourceId: null, isTesting: false, isListing: false, @@ -121,14 +118,10 @@ const datasourceReducer = createReducer(initialState, { [ReduxActionTypes.FETCH_DATASOURCES_INIT]: (state: DatasourceDataState) => { return { ...state, loading: true }; }, - [ReduxActionTypes.CREATE_DATASOURCE_INIT]: ( - state: DatasourceDataState, - action: ReduxAction<{ pluginId: string }>, - ) => { + [ReduxActionTypes.CREATE_DATASOURCE_INIT]: (state: DatasourceDataState) => { return { ...state, loading: true, - loadingPluginId: action.payload.pluginId, }; }, [ReduxActionTypes.CREATE_DATASOURCE_FROM_FORM_INIT]: ( @@ -143,7 +136,6 @@ const datasourceReducer = createReducer(initialState, { return { ...state, loading: !!action.payload.loading, - loadingPluginId: null, }; }, [ReduxActionTypes.UPDATE_DATASOURCE_INIT]: (state: DatasourceDataState) => { @@ -342,6 +334,15 @@ const datasourceReducer = createReducer(initialState, { }), }; }, + [ReduxActionTypes.UPDATE_DATASOURCE_REFS]: ( + state: DatasourceDataState, + action: ReduxAction, + ) => { + return { + ...state, + list: state.list.concat(action.payload), + }; + }, [ReduxActionTypes.CREATE_DATASOURCE_SUCCESS]: ( state: DatasourceDataState, action: ReduxAction, diff --git a/app/client/src/sagas/ActionSagas.ts b/app/client/src/sagas/ActionSagas.ts index 42ab890dcf..71e7e1d40c 100644 --- a/app/client/src/sagas/ActionSagas.ts +++ b/app/client/src/sagas/ActionSagas.ts @@ -51,7 +51,10 @@ import { ERROR_ACTION_MOVE_FAIL, ERROR_ACTION_RENAME_FAIL, } from "ee/constants/messages"; -import type { ReduxAction } from "actions/ReduxActionTypes"; +import type { + ReduxAction, + ReduxActionWithCallbacks, +} from "actions/ReduxActionTypes"; import { ReduxActionErrorTypes, ReduxActionTypes, @@ -271,24 +274,31 @@ export function* getPluginActionDefaultValues(pluginId: string) { return initialValues; } +type CreateActionRequestSagaAction = Partial & { + eventData?: unknown; + pluginId: string; + shouldRedirectToQueryEditor?: boolean; +}; + /** * This saga prepares the action request i.e it helps generating a * new name of an action. This is to reduce any dependency on name generation * on the caller of this saga. */ export function* createActionRequestSaga( - actionPayload: ReduxAction< - Partial & { eventData?: unknown; pluginId: string } + action: ReduxActionWithCallbacks< + CreateActionRequestSagaAction, + unknown, + unknown >, ) { - const payload = { ...actionPayload.payload }; + const payload = { ...action.payload }; const pluginId = - actionPayload.payload.pluginId || - actionPayload.payload.datasource?.pluginId; + action.payload.pluginId || action.payload.datasource?.pluginId; - if (!actionPayload.payload.name) { + if (!action.payload.name) { const { parentEntityId, parentEntityKey } = resolveParentEntityMetadata( - actionPayload.payload, + action.payload, ); if (!parentEntityId || !parentEntityKey) return; @@ -314,20 +324,22 @@ export function* createActionRequestSaga( }); } - yield put(createActionInit(payload)); + yield put(createActionInit(payload, action.onSuccess)); } +type CreateActionSagaPayload = Partial & { + eventData: unknown; + pluginId: string; + shouldRedirectToQueryEditor?: boolean; +}; + export function* createActionSaga( - actionPayload: ReduxAction< - // TODO: Fix this the next time the file is edited - // eslint-disable-next-line @typescript-eslint/no-explicit-any - Partial & { eventData: any; pluginId: string } - >, + action: ReduxActionWithCallbacks, ) { try { // Indicates that source of action creation is self - actionPayload.payload.source = ActionCreationSourceTypeEnum.SELF; - const payload = actionPayload.payload; + action.payload.source = ActionCreationSourceTypeEnum.SELF; + const payload = action.payload; const response: ApiResponse = yield ActionAPI.createAction(payload); @@ -344,7 +356,7 @@ export function* createActionSaga( // @ts-expect-error: name does not exists on type ActionCreateUpdateResponse actionName: response.data.name, pageName: pageName, - ...actionPayload.payload.eventData, + ...action.payload.eventData, }); AppsmithConsole.info({ @@ -360,8 +372,17 @@ export function* createActionSaga( const newAction = response.data; - // @ts-expect-error: type mismatch ActionCreateUpdateResponse vs Action - yield put(createActionSuccess(newAction)); + yield put( + createActionSuccess({ + ...(newAction as unknown as Action), + shouldRedirectToQueryEditor: + action.payload.shouldRedirectToQueryEditor, + }), + ); + + if (action.onSuccess) { + yield put(action.onSuccess); + } // we fork to prevent the call from blocking yield fork(fetchActionDatasourceStructure, newAction); @@ -369,7 +390,7 @@ export function* createActionSaga( } catch (error) { yield put({ type: ReduxActionErrorTypes.CREATE_ACTION_ERROR, - payload: actionPayload.payload, + payload: action.payload, }); } } diff --git a/app/client/src/sagas/QueryPaneSagas.ts b/app/client/src/sagas/QueryPaneSagas.ts index 4ae9940a65..9fb8b91355 100644 --- a/app/client/src/sagas/QueryPaneSagas.ts +++ b/app/client/src/sagas/QueryPaneSagas.ts @@ -392,14 +392,17 @@ function* formValueChangeSaga( } } -function* handleQueryCreatedSaga(actionPayload: ReduxAction) { +function* handleQueryCreatedSaga( + action: ReduxAction, +) { const { actionConfiguration, baseId: baseActionId, pageId, pluginId, pluginType, - } = actionPayload.payload; + shouldRedirectToQueryEditor = true, + } = action.payload; if ( ![ @@ -424,17 +427,19 @@ function* handleQueryCreatedSaga(actionPayload: ReduxAction) { const basePageId: string = yield select(convertToBasePageIdSelector, pageId); - history.replace( - queryEditorIdURL({ - basePageId, - baseQueryId: baseActionId, - params: { - editName: "true", - showTemplate, - from: "datasources", - }, - }), - ); + if (shouldRedirectToQueryEditor) { + history.replace( + queryEditorIdURL({ + basePageId, + baseQueryId: baseActionId, + params: { + editName: "true", + showTemplate, + from: "datasources", + }, + }), + ); + } } function* handleDatasourceCreatedSaga( diff --git a/app/client/src/sagas/selectors.tsx b/app/client/src/sagas/selectors.tsx index a63601239d..0a4dd8469a 100644 --- a/app/client/src/sagas/selectors.tsx +++ b/app/client/src/sagas/selectors.tsx @@ -13,7 +13,6 @@ import type { ActionData } from "ee/reducers/entityReducers/actionsReducer"; import type { Page } from "entities/Page"; import { getActions, getPlugins } from "ee/selectors/entitiesSelector"; import type { Plugin } from "entities/Plugin"; -import type { DragDetails } from "reducers/uiReducers/dragResizeReducer"; import type { DataTreeForActionCreator } from "components/editorComponents/ActionCreator/types"; import type { MetaWidgetsReduxState } from "reducers/entityReducers/metaWidgetsReducer"; @@ -248,7 +247,7 @@ export const getIsNewWidgetBeingDragged = (state: AppState) => { if (!isDragging) return false; - const dragDetails: DragDetails = getDragDetails(state); + const dragDetails = getDragDetails(state); const { dragGroupActualParent: dragParent, newWidget } = dragDetails; return !!newWidget && !dragParent; @@ -258,7 +257,7 @@ export const isCurrentCanvasDragging = createSelector( (state: AppState) => state.ui.widgetDragResize.isDragging, getDragDetails, (state: AppState, canvasId: string) => canvasId, - (isDragging: boolean, dragDetails: DragDetails, canvasId: string) => { + (isDragging: boolean, dragDetails, canvasId: string) => { return dragDetails?.draggedOn === canvasId && isDragging; }, );