PromucFlow_constructor/app/client/src/sagas/FormEvaluationSaga.ts
Ayangade Adeoluwa cb82b299b1
fix: datasource switching issue for Google sheets (#18397)
* Fix datasource switching issue for Goole sheets

* Fix failing vercel deployments
2022-12-03 15:25:42 +05:30

297 lines
9.7 KiB
TypeScript

import {
call,
take,
select,
put,
actionChannel,
ActionPattern,
} from "redux-saga/effects";
import {
ReduxAction,
ReduxActionTypes,
} from "@appsmith/constants/ReduxActionConstants";
import log from "loglevel";
import * as Sentry from "@sentry/react";
import { getFormEvaluationState } from "selectors/formSelectors";
import { evalFormConfig } from "./EvaluationsSaga";
import {
ConditionalOutput,
DynamicValues,
FormEvaluationState,
} from "reducers/evaluationReducers/formEvaluationReducer";
import { FORM_EVALUATION_REDUX_ACTIONS } from "actions/evaluationActions";
import { Action, ActionConfig } from "entities/Action";
import { FormConfigType } from "components/formControls/BaseControl";
import PluginsApi from "api/PluginApi";
import { ApiResponse } from "api/ApiResponses";
import { getAction } from "selectors/entitiesSelector";
import { getDataTreeActionConfigPath } from "entities/Action/actionProperties";
import { getDataTree } from "selectors/dataTreeSelectors";
import { getDynamicBindings, isDynamicValue } from "utils/DynamicBindingUtils";
import get from "lodash/get";
import { klona } from "klona/lite";
import { DataTree } from "entities/DataTree/dataTreeFactory";
import {
extractFetchDynamicValueFormConfigs,
extractQueueOfValuesToBeFetched,
} from "./helper";
import { Action as ReduxActionType } from "redux";
export type FormEvalActionPayload = {
formId: string;
datasourceId?: string;
pluginId?: string;
actionConfiguration?: ActionConfig;
editorConfig?: FormConfigType[];
settingConfig?: FormConfigType[];
actionDiffPath?: string;
hasRouteChanged?: boolean;
};
// This value holds an array of values that needs to be dynamically fetched
// when we run form evaluations we store dynamic values to be fetched in this array
// and when evaluations are finally done, we pick the last dynamic values and call it.
function* setFormEvaluationSagaAsync(
action: ReduxAction<FormEvalActionPayload>,
): any {
try {
// Get current state from redux
const currentEvalState: FormEvaluationState = yield select(
getFormEvaluationState,
);
// Trigger the worker to compute the new eval state
const workerResponse = yield call(evalFormConfig, {
...action,
currentEvalState,
});
if (action?.type === ReduxActionTypes.INIT_FORM_EVALUATION) {
const fetchDynamicValueFormConfigs = extractFetchDynamicValueFormConfigs(
workerResponse[action?.payload?.formId],
);
yield put({
type: ReduxActionTypes.INIT_TRIGGER_VALUES,
payload: {
[action?.payload?.formId]: klona(fetchDynamicValueFormConfigs),
},
});
}
// RUN_FORM_EVALUATION shouldn't be called before INIT_FORM_EVALUATION has been called with
// the same `formId` else `extractQueueOfValuesToBeFetched` will be sent an undefined value.
let queueOfValuesToBeFetched;
if (
action?.type === ReduxActionTypes.RUN_FORM_EVALUATION &&
workerResponse[action?.payload?.formId]
) {
queueOfValuesToBeFetched = extractQueueOfValuesToBeFetched(
workerResponse[action?.payload?.formId],
);
}
// Update the eval state in redux only if it is not empty
if (workerResponse) {
yield put({
type: ReduxActionTypes.SET_FORM_EVALUATION,
payload: workerResponse,
});
}
// If there are any actions in the queue, run them
// Once all the actions are done, extract the actions that need to be fetched dynamically
const formId = action.payload.formId;
const evalOutput = workerResponse[formId];
if (evalOutput && typeof evalOutput === "object") {
if (queueOfValuesToBeFetched) {
yield put({
type: ReduxActionTypes.FETCH_TRIGGER_VALUES_INIT,
payload: {
formId,
values: queueOfValuesToBeFetched,
},
});
// Pass the queue to the saga to fetch the dynamic values
yield call(
fetchDynamicValuesSaga,
queueOfValuesToBeFetched,
formId,
action.payload.datasourceId ? action.payload.datasourceId : "",
action.payload.pluginId ? action.payload.pluginId : "",
);
}
}
} catch (e) {
log.error(e);
}
}
// Function to fetch the dynamic values one by one from the queue
export function* fetchDynamicValuesSaga(
queueOfValuesToBeFetched: Record<string, ConditionalOutput>,
formId: string,
datasourceId: string,
pluginId: string,
) {
for (const key of Object.keys(queueOfValuesToBeFetched)) {
queueOfValuesToBeFetched[key].fetchDynamicValues = yield call(
fetchDynamicValueSaga,
queueOfValuesToBeFetched[key],
Object.assign(
{},
queueOfValuesToBeFetched[key].fetchDynamicValues as DynamicValues,
),
formId,
datasourceId,
pluginId,
key,
);
}
// Set the values to the state once all values are fetched
yield put({
type: ReduxActionTypes.FETCH_TRIGGER_VALUES_SUCCESS,
payload: {
formId,
values: queueOfValuesToBeFetched,
},
});
}
function* fetchDynamicValueSaga(
value: ConditionalOutput,
dynamicFetchedValues: DynamicValues,
actionId: string,
datasourceId: string,
pluginId: string,
configProperty: string,
) {
try {
const {
config,
evaluatedConfig,
} = value.fetchDynamicValues as DynamicValues;
const { params } = evaluatedConfig;
dynamicFetchedValues.hasStarted = true;
let url = PluginsApi.defaultDynamicTriggerURL(datasourceId);
if (
"url" in evaluatedConfig &&
!!evaluatedConfig.url &&
evaluatedConfig.url.length > 0
)
url = evaluatedConfig.url;
// Eval Action is the current action as it is stored in the dataTree
let evalAction: any;
// Evaluated params is the object that will hold the evaluated values of the parameters as computed in the dataTree
let evaluatedParams;
// this is a temporary variable, used to derive the evaluated value of the current parameters before being stored in the evaluated params
let substitutedParameters = {};
const action: Action = yield select(getAction, actionId);
const dataTree: DataTree = yield select(getDataTree);
if (!!action) {
evalAction = dataTree[action.name];
}
// we use the config parameters to get the action diff path value of the parameters i.e. {{actionConfiguration.formData.sheetUrl.data}. Note that it is enclosed within dynamic bindings
if ("parameters" in config?.params && !!evalAction) {
Object.entries(config?.params.parameters).forEach(([key, value]) => {
// we extract the action diff path of the param value from the dynamic binding i.e. actionConfiguration.formData.sheetUrl.data
const dynamicBindingValue = getDynamicBindings(value as string)
?.jsSnippets[0];
// we convert this action Diff path into the same format as it is stored in the dataTree i.e. config.formData.sheetUrl.data
const dataTreeActionConfigPath = getDataTreeActionConfigPath(
dynamicBindingValue,
);
// then we get the value of the current parameter from the evaluatedValues in the action object stored in the dataTree.
const evaluatedValue = get(
evalAction?.__evaluation__?.evaluatedValues,
dataTreeActionConfigPath,
);
// if it exists, we store it in the substituted params object.
// we check if that value is enclosed in dynamic bindings i.e the value has not been evaluated or somehow still contains a js expression
// if it is, we return an empty string since we don't want to send dynamic bindings to the server.
// if it contains a value, we send the value to the server
if (!!evaluatedValue) {
substitutedParameters = {
...substitutedParameters,
[key]: isDynamicValue(evaluatedValue) ? "" : evaluatedValue,
};
}
});
}
// we destructure the values back to the appropriate places.
if ("parameters" in params) {
evaluatedParams = {
...params,
parameters: {
...params.parameters,
...substitutedParameters,
},
};
} else {
evaluatedParams = {
...params,
};
}
// Call the API to fetch the dynamic values
const response: ApiResponse = yield call(
PluginsApi.fetchDynamicFormValues,
url,
{
actionId,
configProperty,
datasourceId,
pluginId,
...evaluatedParams,
},
);
dynamicFetchedValues.isLoading = false;
// @ts-expect-error: we don't know what the response will be
if (response.responseMeta.status === 200 && "trigger" in response.data) {
// @ts-expect-error: we don't know what the response will be
dynamicFetchedValues.data = response.data.trigger;
dynamicFetchedValues.hasFetchFailed = false;
} else {
dynamicFetchedValues.hasFetchFailed = true;
dynamicFetchedValues.data = [];
}
} catch (e) {
log.error(e);
dynamicFetchedValues.hasFetchFailed = true;
dynamicFetchedValues.isLoading = false;
dynamicFetchedValues.data = [];
}
return dynamicFetchedValues;
}
function* formEvaluationChangeListenerSaga() {
const formEvalChannel: ActionPattern<ReduxActionType<
FormEvalActionPayload
>> = yield actionChannel(FORM_EVALUATION_REDUX_ACTIONS);
while (true) {
const action: ReduxAction<FormEvalActionPayload> = yield take(
formEvalChannel,
);
yield call(setFormEvaluationSagaAsync, action);
}
}
export default function* formEvaluationChangeListener() {
yield take(ReduxActionTypes.START_EVALUATION);
while (true) {
try {
yield call(formEvaluationChangeListenerSaga);
} catch (e) {
log.error(e);
Sentry.captureException(e);
}
}
}