## Description The aim of this PR is to make the editors reusable in the Module editor. Changes 1. A wrapper is introduced for API, Query and Curl editors which passes differentiating functions and make the form editors agnostic of pageId and applicationId 2. In order to pass down function, react contexts are added to avoid prop drilling #### PR fixes following issue(s) Fixes #26160 #### 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 - Chore (housekeeping or task changes that don't impact user perception) ## 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 - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my own code - [x] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] 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
525 lines
16 KiB
TypeScript
525 lines
16 KiB
TypeScript
import {
|
|
all,
|
|
call,
|
|
put,
|
|
select,
|
|
take,
|
|
takeEvery,
|
|
fork,
|
|
} from "redux-saga/effects";
|
|
import * as Sentry from "@sentry/react";
|
|
import type {
|
|
ReduxAction,
|
|
ReduxActionWithMeta,
|
|
} from "@appsmith/constants/ReduxActionConstants";
|
|
import {
|
|
ReduxActionErrorTypes,
|
|
ReduxActionTypes,
|
|
ReduxFormActionTypes,
|
|
} from "@appsmith/constants/ReduxActionConstants";
|
|
import { getDynamicTriggers, getFormData } from "selectors/formSelectors";
|
|
import {
|
|
DATASOURCE_DB_FORM,
|
|
QUERY_EDITOR_FORM_NAME,
|
|
} from "@appsmith/constants/forms";
|
|
import history from "utils/history";
|
|
import { APPLICATIONS_URL, INTEGRATION_TABS } from "constants/routes";
|
|
import { getCurrentPageId } from "selectors/editorSelectors";
|
|
import { autofill, change, initialize, reset } from "redux-form";
|
|
import {
|
|
getAction,
|
|
getDatasource,
|
|
getPluginTemplates,
|
|
getPlugin,
|
|
getEditorConfig,
|
|
getSettingConfig,
|
|
getPlugins,
|
|
getGenerateCRUDEnabledPluginMap,
|
|
} from "@appsmith/selectors/entitiesSelector";
|
|
import type { Action, QueryAction } from "entities/Action";
|
|
import { PluginType } from "entities/Action";
|
|
import {
|
|
createActionRequest,
|
|
setActionProperty,
|
|
} from "actions/pluginActionActions";
|
|
import { getQueryParams } from "utils/URLUtils";
|
|
import { isEmpty, merge } from "lodash";
|
|
import { getConfigInitialValues } from "components/formControls/utils";
|
|
import type { Datasource } from "entities/Datasource";
|
|
import omit from "lodash/omit";
|
|
import {
|
|
createMessage,
|
|
ERROR_ACTION_RENAME_FAIL,
|
|
} from "@appsmith/constants/messages";
|
|
import get from "lodash/get";
|
|
import {
|
|
initFormEvaluations,
|
|
startFormEvaluations,
|
|
} from "@appsmith/actions/evaluationActions";
|
|
import { updateReplayEntity } from "actions/pageActions";
|
|
import { ENTITY_TYPE } from "entities/AppsmithConsole";
|
|
import type { EventLocation } from "@appsmith/utils/analyticsUtilTypes";
|
|
import AnalyticsUtil from "utils/AnalyticsUtil";
|
|
import {
|
|
datasourcesEditorIdURL,
|
|
generateTemplateFormURL,
|
|
integrationEditorURL,
|
|
queryEditorIdURL,
|
|
} from "@appsmith/RouteBuilder";
|
|
import type { GenerateCRUDEnabledPluginMap, Plugin } from "api/PluginApi";
|
|
import { UIComponentTypes } from "api/PluginApi";
|
|
import { getUIComponent } from "pages/Editor/QueryEditor/helpers";
|
|
import { FormDataPaths } from "workers/Evaluation/formEval";
|
|
import { fetchDynamicValuesSaga } from "./FormEvaluationSaga";
|
|
import type { FormEvalOutput } from "reducers/evaluationReducers/formEvaluationReducer";
|
|
import { validateResponse } from "./ErrorSagas";
|
|
import { getIsGeneratePageInitiator } from "utils/GenerateCrudUtil";
|
|
import { toast } from "design-system";
|
|
import type { CreateDatasourceSuccessAction } from "actions/datasourceActions";
|
|
import { createDefaultActionPayload } from "./ActionSagas";
|
|
import { DB_NOT_SUPPORTED } from "@appsmith/utils/Environments";
|
|
import { getCurrentEnvironmentId } from "@appsmith/selectors/environmentSelectors";
|
|
import type { FeatureFlags } from "@appsmith/entities/FeatureFlag";
|
|
import { selectFeatureFlags } from "@appsmith/selectors/featureFlagsSelectors";
|
|
import { isGACEnabled } from "@appsmith/utils/planHelpers";
|
|
import { getHasManageActionPermission } from "@appsmith/utils/BusinessFeatures/permissionPageHelpers";
|
|
import type { ChangeQueryPayload } from "actions/queryPaneActions";
|
|
|
|
// Called whenever the query being edited is changed via the URL or query pane
|
|
function* changeQuerySaga(actionPayload: ReduxAction<ChangeQueryPayload>) {
|
|
const { applicationId, id, moduleId, packageId, pageId } =
|
|
actionPayload.payload;
|
|
let configInitialValues = {};
|
|
|
|
if (!(packageId && moduleId) && !(applicationId && pageId)) {
|
|
history.push(APPLICATIONS_URL);
|
|
return;
|
|
}
|
|
const action: Action | undefined = yield select(getAction, id);
|
|
if (!action) {
|
|
if (pageId) {
|
|
history.push(
|
|
integrationEditorURL({
|
|
pageId,
|
|
selectedTab: INTEGRATION_TABS.ACTIVE,
|
|
}),
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// fetching pluginId and the consequent configs from the action
|
|
const pluginId = action.pluginId;
|
|
const currentEditorConfig: any[] = yield select(getEditorConfig, pluginId);
|
|
const currentSettingConfig: any[] = yield select(getSettingConfig, pluginId);
|
|
|
|
// Update the evaluations when the queryID is changed by changing the
|
|
// URL or selecting new query from the query pane
|
|
yield put(initFormEvaluations(currentEditorConfig, currentSettingConfig, id));
|
|
|
|
const allPlugins: Plugin[] = yield select(getPlugins);
|
|
let uiComponent = UIComponentTypes.DbEditorForm;
|
|
if (!!pluginId) uiComponent = getUIComponent(pluginId, allPlugins);
|
|
|
|
// If config exists
|
|
if (currentEditorConfig) {
|
|
// Get initial values
|
|
configInitialValues = yield call(
|
|
getConfigInitialValues,
|
|
currentEditorConfig,
|
|
uiComponent === UIComponentTypes.UQIDbEditorForm,
|
|
);
|
|
}
|
|
|
|
if (currentSettingConfig) {
|
|
const settingInitialValues: Record<string, unknown> = yield call(
|
|
getConfigInitialValues,
|
|
currentSettingConfig,
|
|
uiComponent === UIComponentTypes.UQIDbEditorForm,
|
|
);
|
|
configInitialValues = merge(configInitialValues, settingInitialValues);
|
|
}
|
|
|
|
// Merge the initial values and action.
|
|
const formInitialValues = merge(configInitialValues, action);
|
|
|
|
// Set the initialValues in the state for redux-form lib
|
|
yield put(initialize(QUERY_EDITOR_FORM_NAME, formInitialValues));
|
|
|
|
if (uiComponent === UIComponentTypes.UQIDbEditorForm) {
|
|
// Once the initial values are set, we can run the evaluations based on them.
|
|
yield put(
|
|
startFormEvaluations(
|
|
id,
|
|
formInitialValues.actionConfiguration,
|
|
//@ts-expect-error: id does not exists
|
|
action.datasource.id,
|
|
pluginId,
|
|
),
|
|
);
|
|
}
|
|
|
|
yield put(
|
|
updateReplayEntity(
|
|
formInitialValues.id,
|
|
formInitialValues,
|
|
ENTITY_TYPE.ACTION,
|
|
),
|
|
);
|
|
}
|
|
|
|
function* formValueChangeSaga(
|
|
actionPayload: ReduxActionWithMeta<string, { field: string; form: string }>,
|
|
) {
|
|
try {
|
|
const { field, form } = actionPayload.meta;
|
|
if (field === "dynamicBindingPathList" || field === "name") return;
|
|
if (form !== QUERY_EDITOR_FORM_NAME) return;
|
|
const { values } = yield select(getFormData, QUERY_EDITOR_FORM_NAME);
|
|
const hasRouteChanged = field === "id";
|
|
|
|
const featureFlags: FeatureFlags = yield select(selectFeatureFlags);
|
|
const isFeatureEnabled = isGACEnabled(featureFlags);
|
|
|
|
if (
|
|
!getHasManageActionPermission(isFeatureEnabled, values.userPermissions)
|
|
) {
|
|
yield validateResponse({
|
|
status: 403,
|
|
resourceType: values?.pluginType,
|
|
resourceId: values.id,
|
|
});
|
|
}
|
|
|
|
// If there is a change in the command type of a form and the value is an empty string, we prevent the command action value from being updated and form evaluations from being performed on it.
|
|
// We do this because by default the command value of an action should always be set to a non empty string value (impossible case).
|
|
if (field === FormDataPaths.COMMAND && actionPayload.payload === "") {
|
|
return;
|
|
}
|
|
|
|
const plugins: Plugin[] = yield select(getPlugins);
|
|
const uiComponent = getUIComponent(values.pluginId, plugins);
|
|
const plugin = plugins.find((p) => p.id === values.pluginId);
|
|
|
|
if (field === "datasource.id") {
|
|
const datasource: Datasource | undefined = yield select(
|
|
getDatasource,
|
|
actionPayload.payload,
|
|
);
|
|
|
|
// Update the datasource not just the datasource id.
|
|
yield put(
|
|
setActionProperty({
|
|
actionId: values.id,
|
|
propertyName: "datasource",
|
|
value: datasource,
|
|
}),
|
|
);
|
|
|
|
// Update the datasource of the form as well
|
|
yield put(autofill(QUERY_EDITOR_FORM_NAME, "datasource", datasource));
|
|
|
|
AnalyticsUtil.logEvent("SWITCH_DATASOURCE");
|
|
|
|
if (
|
|
uiComponent === UIComponentTypes.UQIDbEditorForm &&
|
|
!!values?.id &&
|
|
!!datasource?.id &&
|
|
!!values?.pluginId
|
|
) {
|
|
// get dynamic triggers that need to be refetched. i.e. allowedToFetch is true.
|
|
const allTriggers: FormEvalOutput | undefined = yield select(
|
|
getDynamicTriggers,
|
|
values.id,
|
|
);
|
|
|
|
try {
|
|
// if all triggers exist then set their loading states to true and refetch them.
|
|
if (!!allTriggers) {
|
|
yield put({
|
|
type: ReduxActionTypes.SET_TRIGGER_VALUES_LOADING,
|
|
payload: {
|
|
formId: values.id,
|
|
keys: Object.keys(allTriggers),
|
|
value: true,
|
|
},
|
|
});
|
|
|
|
// refetch trigger values.
|
|
yield fork(
|
|
fetchDynamicValuesSaga,
|
|
allTriggers,
|
|
values.id,
|
|
datasource.id,
|
|
values.pluginId,
|
|
);
|
|
}
|
|
} catch (err) {}
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// get datasource configuration based on datasource id
|
|
// pass it to run form evaluations method
|
|
// This is required for google sheets, as we need to modify query
|
|
// state based on datasource config
|
|
const datasource: Datasource | undefined = yield select(
|
|
getDatasource,
|
|
values.datasource.id,
|
|
);
|
|
const datasourceStorages = datasource?.datasourceStorages || {};
|
|
|
|
// Editing form fields triggers evaluations.
|
|
// We pass the action to run form evaluations when the dataTree evaluation is complete
|
|
let currentEnvironment: string = yield select(getCurrentEnvironmentId);
|
|
const pluginType = plugin?.type;
|
|
if (
|
|
(!!pluginType && DB_NOT_SUPPORTED.includes(pluginType)) ||
|
|
!datasourceStorages.hasOwnProperty(currentEnvironment) ||
|
|
!datasourceStorages[currentEnvironment].hasOwnProperty(
|
|
"datasourceConfiguration",
|
|
)
|
|
) {
|
|
currentEnvironment = Object.keys(datasourceStorages)[0];
|
|
}
|
|
const postEvalActions =
|
|
uiComponent === UIComponentTypes.UQIDbEditorForm
|
|
? [
|
|
startFormEvaluations(
|
|
values.id,
|
|
values.actionConfiguration,
|
|
values.datasource.id,
|
|
values.pluginId,
|
|
field,
|
|
hasRouteChanged,
|
|
datasourceStorages[currentEnvironment].datasourceConfiguration,
|
|
),
|
|
]
|
|
: [];
|
|
|
|
if (
|
|
actionPayload.type === ReduxFormActionTypes.ARRAY_REMOVE ||
|
|
actionPayload.type === ReduxFormActionTypes.ARRAY_PUSH
|
|
) {
|
|
const value = get(values, field);
|
|
yield put(
|
|
setActionProperty(
|
|
{
|
|
actionId: values.id,
|
|
propertyName: field,
|
|
value,
|
|
},
|
|
postEvalActions,
|
|
),
|
|
);
|
|
} else {
|
|
yield put(
|
|
setActionProperty(
|
|
{
|
|
actionId: values.id,
|
|
propertyName: field,
|
|
value: actionPayload.payload,
|
|
},
|
|
postEvalActions,
|
|
),
|
|
);
|
|
}
|
|
yield put(updateReplayEntity(values.id, values, ENTITY_TYPE.ACTION));
|
|
} catch (error) {
|
|
yield put({
|
|
type: ReduxActionErrorTypes.SAVE_PAGE_ERROR,
|
|
payload: {
|
|
error,
|
|
},
|
|
});
|
|
yield put(reset(QUERY_EDITOR_FORM_NAME));
|
|
}
|
|
}
|
|
|
|
function* handleQueryCreatedSaga(actionPayload: ReduxAction<QueryAction>) {
|
|
const { actionConfiguration, id, pluginId, pluginType } =
|
|
actionPayload.payload;
|
|
const pageId: string = yield select(getCurrentPageId);
|
|
if (pluginType !== PluginType.DB && pluginType !== PluginType.REMOTE) return;
|
|
const pluginTemplates: Record<string, unknown> =
|
|
yield select(getPluginTemplates);
|
|
const queryTemplate = pluginTemplates[pluginId];
|
|
// Do not show template view if the query has body(code) or if there are no templates or if the plugin is MongoDB
|
|
const showTemplate = !(
|
|
!!actionConfiguration.body ||
|
|
!!actionConfiguration.formData?.body ||
|
|
isEmpty(queryTemplate)
|
|
);
|
|
history.replace(
|
|
queryEditorIdURL({
|
|
pageId,
|
|
queryId: id,
|
|
params: {
|
|
editName: "true",
|
|
showTemplate,
|
|
from: "datasources",
|
|
},
|
|
}),
|
|
);
|
|
}
|
|
|
|
function* handleDatasourceCreatedSaga(
|
|
actionPayload: CreateDatasourceSuccessAction,
|
|
) {
|
|
const pageId: string = yield select(getCurrentPageId);
|
|
const { isDBCreated, payload } = actionPayload;
|
|
const plugin: Plugin | undefined = yield select(getPlugin, payload.pluginId);
|
|
// Only look at db plugins
|
|
if (
|
|
plugin &&
|
|
plugin.type !== PluginType.DB &&
|
|
plugin.type !== PluginType.REMOTE
|
|
)
|
|
return;
|
|
|
|
yield put(initialize(DATASOURCE_DB_FORM, omit(payload, "name")));
|
|
|
|
const queryParams = getQueryParams();
|
|
const updatedDatasource = payload;
|
|
|
|
const isGeneratePageInitiator = getIsGeneratePageInitiator(
|
|
queryParams.isGeneratePageMode,
|
|
);
|
|
const generateCRUDSupportedPlugin: GenerateCRUDEnabledPluginMap =
|
|
yield select(getGenerateCRUDEnabledPluginMap);
|
|
|
|
// isGeneratePageInitiator ensures that datasource is being created from generate page with data
|
|
// then we check if the current plugin is supported for generate page with data functionality
|
|
// and finally isDBCreated ensures that datasource is not in temporary state and
|
|
// user has explicitly saved the datasource, before redirecting back to generate page
|
|
if (
|
|
isGeneratePageInitiator &&
|
|
updatedDatasource.pluginId &&
|
|
generateCRUDSupportedPlugin[updatedDatasource.pluginId] &&
|
|
isDBCreated
|
|
) {
|
|
history.push(
|
|
generateTemplateFormURL({
|
|
pageId,
|
|
params: {
|
|
datasourceId: updatedDatasource.id,
|
|
},
|
|
}),
|
|
);
|
|
} else {
|
|
history.push(
|
|
datasourcesEditorIdURL({
|
|
pageId,
|
|
datasourceId: payload.id,
|
|
params: {
|
|
from: "datasources",
|
|
...getQueryParams(),
|
|
pluginId: plugin?.id,
|
|
},
|
|
}),
|
|
);
|
|
}
|
|
}
|
|
|
|
function* handleNameChangeSaga(
|
|
action: ReduxAction<{ id: string; name: string }>,
|
|
) {
|
|
yield put(change(QUERY_EDITOR_FORM_NAME, "name", action.payload.name));
|
|
}
|
|
|
|
function* handleNameChangeSuccessSaga(
|
|
action: ReduxAction<{ actionId: string }>,
|
|
) {
|
|
const { actionId } = action.payload;
|
|
const actionObj: Action | undefined = yield select(getAction, actionId);
|
|
yield take(ReduxActionTypes.FETCH_ACTIONS_FOR_PAGE_SUCCESS);
|
|
if (!actionObj) {
|
|
// Error case, log to sentry
|
|
toast.show(createMessage(ERROR_ACTION_RENAME_FAIL, ""), {
|
|
kind: "error",
|
|
});
|
|
|
|
Sentry.captureException(
|
|
new Error(createMessage(ERROR_ACTION_RENAME_FAIL, "")),
|
|
{
|
|
extra: {
|
|
actionId: actionId,
|
|
},
|
|
},
|
|
);
|
|
return;
|
|
}
|
|
if (actionObj.pluginType === PluginType.DB) {
|
|
const params = getQueryParams();
|
|
if (params.editName) {
|
|
params.editName = "false";
|
|
}
|
|
history.replace(
|
|
queryEditorIdURL({
|
|
pageId: actionObj.pageId,
|
|
queryId: actionId,
|
|
params,
|
|
}),
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates an action with specific datasource created by a user
|
|
* @param action
|
|
*/
|
|
function* createNewQueryForDatasourceSaga(
|
|
action: ReduxAction<{
|
|
pageId: string;
|
|
datasourceId: string;
|
|
from: EventLocation;
|
|
}>,
|
|
) {
|
|
const { datasourceId } = action.payload;
|
|
if (!datasourceId) return;
|
|
|
|
const createActionPayload: Partial<Action> = yield call(
|
|
createDefaultActionPayload,
|
|
action.payload.pageId,
|
|
action.payload.datasourceId,
|
|
action.payload.from,
|
|
);
|
|
|
|
yield put(createActionRequest(createActionPayload));
|
|
}
|
|
|
|
function* handleNameChangeFailureSaga(
|
|
action: ReduxAction<{ oldName: string }>,
|
|
) {
|
|
yield put(change(QUERY_EDITOR_FORM_NAME, "name", action.payload.oldName));
|
|
}
|
|
|
|
export default function* root() {
|
|
yield all([
|
|
takeEvery(ReduxActionTypes.CREATE_ACTION_SUCCESS, handleQueryCreatedSaga),
|
|
takeEvery(
|
|
ReduxActionTypes.CREATE_DATASOURCE_SUCCESS,
|
|
handleDatasourceCreatedSaga,
|
|
),
|
|
takeEvery(ReduxActionTypes.QUERY_PANE_CHANGE, changeQuerySaga),
|
|
takeEvery(ReduxActionTypes.SAVE_ACTION_NAME_INIT, handleNameChangeSaga),
|
|
takeEvery(
|
|
ReduxActionTypes.SAVE_ACTION_NAME_SUCCESS,
|
|
handleNameChangeSuccessSaga,
|
|
),
|
|
takeEvery(
|
|
ReduxActionErrorTypes.SAVE_ACTION_NAME_ERROR,
|
|
handleNameChangeFailureSaga,
|
|
),
|
|
// Intercepting the redux-form change actionType
|
|
takeEvery(ReduxFormActionTypes.VALUE_CHANGE, formValueChangeSaga),
|
|
takeEvery(ReduxFormActionTypes.ARRAY_REMOVE, formValueChangeSaga),
|
|
takeEvery(ReduxFormActionTypes.ARRAY_PUSH, formValueChangeSaga),
|
|
takeEvery(
|
|
ReduxActionTypes.CREATE_NEW_QUERY_ACTION,
|
|
createNewQueryForDatasourceSaga,
|
|
),
|
|
]);
|
|
}
|