PromucFlow_constructor/app/client/src/sagas/ActionExecution/PluginActionSaga.ts

814 lines
23 KiB
TypeScript
Raw Normal View History

import { all, call, put, select, take, takeLatest } from "redux-saga/effects";
import {
executePluginActionError,
executePluginActionRequest,
executePluginActionSuccess,
runAction,
updateAction,
} from "actions/pluginActionActions";
import {
ApplicationPayload,
ReduxAction,
ReduxActionErrorTypes,
ReduxActionTypes,
} from "constants/ReduxActionConstants";
import ActionAPI, {
ActionExecutionResponse,
ActionResponse,
ExecuteActionRequest,
PaginationField,
} from "api/ActionAPI";
import {
getAction,
getCurrentPageNameByActionId,
isActionDirty,
isActionSaving,
} from "selectors/entitiesSelector";
import {
getAppMode,
getCurrentApplication,
} from "selectors/applicationSelectors";
import _, { get, isString, set } from "lodash";
import AppsmithConsole from "utils/AppsmithConsole";
import { ENTITY_TYPE, PLATFORM_ERROR } from "entities/AppsmithConsole";
import { validateResponse } from "sagas/ErrorSagas";
import AnalyticsUtil, { EventName } from "utils/AnalyticsUtil";
import { Action, PluginType } from "entities/Action";
import LOG_TYPE from "entities/AppsmithConsole/logtype";
import { Toaster } from "components/ads/Toast";
import {
createMessage,
ERROR_ACTION_EXECUTE_FAIL,
ERROR_FAIL_ON_PAGE_LOAD_ACTIONS,
ERROR_PLUGIN_ACTION_EXECUTE,
refactor: admin settings (#9906) * refactor admin settings feature * separated save-restart bar to separate component * created new CE dir to facilitate code split * created separate ee dir and exporting everything we have in ce file. * little mod * minor fix * splitting settings types config * using object literals for category types instead of enums * CE: support use of component for each category * minor style fix * authentication page UI changes implemented * github signup doc url added back * removed comments * routing updates * made subcategories listing in left pane optional * added muted saml to auth listing * added breadcrumbs and enabled button * created separate component for auth page and auth config * added callout and disconnect components * updated breadcrumbs component * minor updates to common components * updated warning callout and added icon * ce: test cases fixed * updated test file name * warning banner callout added on auth page * updated callout banner for form login * CE: Split config files * CE: moved the window declaration in EE file as its dependency will be updated in EE * CE: Splitting ApiConstants and SocialLogin constants * CE: split login page * CE: moved getSocialLoginButtonProps func to EE file as it's dependencies will be updated in EE * added key icon * CE: created a factory class to share social auths list * Minor style fix for social btns * Updated the third party auth styles * Small fixes to styling * ce: splitting forms constants * breadcrumbs implemented for all pages in admin settings * Settings breadcrumbs separated * splitted settings breadcrumbs between ce and ee * renamed default import * minor style fix * added login form config. * updated login/signup pages to use form login disabled config * removed common functionality outside * implemented breadcrumb component from scratch without using blueprint * removed unwanted code * Small style update * updated breadcrumb categories file name and breadcrumb icon * added cypress tests for admin settings auth page * added comments * update locator for upgrade button * added link for intercom on upgrade button * removed unnecessary file * minor style fix * style fix for auth option cards * split messages constant * fixed imports for message constants splitting. * added message constants * updated unit test cases * fixed messages import in cypress index * fixed messages import again, cypress fails to read re-exported objs. * added OIDC auth method on authentication page * updated import statements from ee to @appsmith * removed dead code * updated read more link UI * PR comments fixes * some UI fixes * used color and fonts from theme * fixed some imports * fixed some imports * removed warning imports * updated OIDC logo and auth method desc copies * css changes * css changes * css changes * updated cypress test for breadcrumb * moved callout component to ads as calloutv2 * UI changes for form fields * updated css for spacing between form fields * added sub-text on auth pages * added active class for breadcrumb item * added config for disable signup toggle and fixed UI issues of restart banner * fixed admin settings page bugs * assigned true as default state for signup * fixed messages import statements * updated code for PR comments related suggestions * reverted file path change in cypress support * updated cypress test * updated cypress test Co-authored-by: Ankita Kinger <ankita@appsmith.com>
2022-02-11 18:08:46 +00:00
} from "@appsmith/constants/messages";
import { Variant } from "components/ads/common";
import {
EventType,
PageAction,
RESP_HEADER_DATATYPE,
} from "constants/AppsmithActionConstants/ActionConstants";
import {
getCurrentPageId,
getIsSavingEntity,
getLayoutOnLoadActions,
} from "selectors/editorSelectors";
import PerformanceTracker, {
PerformanceTransactionName,
} from "utils/PerformanceTracker";
import * as log from "loglevel";
import { EMPTY_RESPONSE } from "components/editorComponents/ApiResponseView";
import { AppState } from "reducers";
import { DEFAULT_EXECUTE_ACTION_TIMEOUT_MS } from "@appsmith/constants/ApiConstants";
import { evaluateActionBindings } from "sagas/EvaluationsSaga";
import { isBlobUrl, parseBlobUrl } from "utils/AppsmithUtils";
import { getType, Types } from "utils/TypeHelpers";
import { matchPath } from "react-router";
import {
API_EDITOR_ID_URL,
API_EDITOR_URL,
API_EDITOR_URL_WITH_SELECTED_PAGE_ID,
INTEGRATION_EDITOR_URL,
QUERIES_EDITOR_ID_URL,
QUERIES_EDITOR_URL,
CURL_IMPORT_PAGE_PATH,
} from "constants/routes";
import { SAAS_EDITOR_API_ID_URL } from "pages/Editor/SaaSEditor/constants";
import {
ActionTriggerType,
RunPluginActionDescription,
} from "entities/DataTree/actionTriggers";
import { APP_MODE } from "entities/App";
import { FileDataTypes } from "widgets/constants";
import { hideDebuggerErrors } from "actions/debuggerActions";
import {
ActionValidationError,
getErrorAsString,
PluginActionExecutionError,
PluginTriggerFailureError,
UserCancelledActionExecutionError,
} from "sagas/ActionExecution/errorUtils";
import { trimQueryString } from "utils/helpers";
import {
executeAppAction,
TriggerMeta,
} from "sagas/ActionExecution/ActionExecutionSagas";
import { requestModalConfirmationSaga } from "sagas/UtilSagas";
import { ModalType } from "reducers/uiReducers/modalActionReducer";
import { getFormNames, getFormValues } from "redux-form";
import { CURL_IMPORT_FORM } from "constants/forms";
import { submitCurlImportForm } from "actions/importActions";
import { isTrueObject } from "workers/evaluationUtils";
enum ActionResponseDataTypes {
BINARY = "BINARY",
}
export const getActionTimeout = (
state: AppState,
actionId: string,
): number | undefined => {
const action = _.find(
state.entities.actions,
(a) => a.config.id === actionId,
);
if (action) {
const timeout = _.get(
action,
"config.actionConfiguration.timeoutInMillisecond",
DEFAULT_EXECUTE_ACTION_TIMEOUT_MS,
);
if (timeout) {
// Extra timeout padding to account for network calls
return timeout + 5000;
}
return undefined;
}
return undefined;
};
const createActionExecutionResponse = (
response: ActionExecutionResponse,
): ActionResponse => {
const payload = response.data;
if (payload.statusCode === "200 OK" && payload.hasOwnProperty("headers")) {
const respHeaders = payload.headers;
if (
respHeaders.hasOwnProperty(RESP_HEADER_DATATYPE) &&
respHeaders[RESP_HEADER_DATATYPE].length > 0 &&
respHeaders[RESP_HEADER_DATATYPE][0] === ActionResponseDataTypes.BINARY &&
getType(payload.body) === Types.STRING
) {
// Decoding from base64 to handle the binary files because direct
// conversion of binary files to string causes corruption in the final output
// this is to only handle the download of binary files
payload.body = atob(payload.body as string);
}
}
return {
...payload,
...response.clientMeta,
};
};
const isErrorResponse = (response: ActionExecutionResponse) => {
return !response.data.isExecutionSuccess;
};
/**
*
* @param blobUrl string A blob url with type added a query param
* @returns promise that resolves to file content
*/
function* readBlob(blobUrl: string): any {
const [url, fileType] = parseBlobUrl(blobUrl);
const file = yield fetch(url).then((r) => r.blob());
return yield new Promise((resolve) => {
const reader = new FileReader();
if (fileType === FileDataTypes.Base64) {
reader.readAsDataURL(file);
} else if (fileType === FileDataTypes.Binary) {
reader.readAsBinaryString(file);
} else {
reader.readAsText(file);
}
reader.onloadend = () => {
resolve(reader.result);
};
});
}
/**
* Api1
* URL: https://example.com/{{Text1.text}}
* Body: {
* "name": "{{this.params.name}}",
* "age": {{this.params.age}},
* "gender": {{Dropdown1.selectedOptionValue}}
* }
*
* If you call
* Api1.run(undefined, undefined, { name: "Hetu", age: Input1.text });
*
* executionParams is { name: "Hetu", age: Input1.text }
* bindings is [
* "Text1.text",
* "Dropdown1.selectedOptionValue",
* "this.params.name",
* "this.params.age",
* ]
*
* Return will be [
* { key: "Text1.text", value: "updateUser" },
* { key: "Dropdown1.selectedOptionValue", value: "M" },
* { key: "this.params.name", value: "Hetu" },
* { key: "this.params.age", value: 26 },
* ]
* @param bindings
* @param executionParams
*/
function* evaluateActionParams(
bindings: string[] | undefined,
formData: FormData,
executionParams?: Record<string, any> | string,
) {
if (_.isNil(bindings) || bindings.length === 0) return [];
// Evaluated all bindings of the actions. Pass executionParams if any
const values: any = yield call(
evaluateActionBindings,
bindings,
executionParams,
);
// Add keys values to formData for the multipart submission
for (let i = 0; i < bindings.length; i++) {
const key = bindings[i];
let value = values[i];
if (isTrueObject(value)) {
const blobUrlPaths: string[] = [];
Object.keys(value).forEach((propertyName) => {
if (isBlobUrl(value[propertyName])) {
blobUrlPaths.push(propertyName);
}
});
for (const blobUrlPath of blobUrlPaths) {
const blobUrl = value[blobUrlPath] as string;
const resolvedBlobValue = yield call(readBlob, blobUrl);
set(value, blobUrlPath, resolvedBlobValue);
}
}
if (typeof value === "object") value = JSON.stringify(value);
if (isBlobUrl(value)) {
value = yield call(readBlob, value);
}
formData.append(encodeURIComponent(key), value);
}
}
export default function* executePluginActionTriggerSaga(
pluginAction: RunPluginActionDescription["payload"],
eventType: EventType,
triggerMeta: TriggerMeta,
) {
const { actionId, onError, onSuccess, params } = pluginAction;
if (getType(params) !== Types.OBJECT) {
throw new ActionValidationError(
ActionTriggerType.RUN_PLUGIN_ACTION,
"params",
Types.OBJECT,
getType(params),
);
}
PerformanceTracker.startAsyncTracking(
PerformanceTransactionName.EXECUTE_ACTION,
{
actionId: actionId,
},
actionId,
);
const appMode = yield select(getAppMode);
const action: Action = yield select(getAction, actionId);
const currentApp: ApplicationPayload = yield select(getCurrentApplication);
AnalyticsUtil.logEvent("EXECUTE_ACTION", {
type: action.pluginType,
name: action.name,
pageId: action.pageId,
appId: currentApp.id,
appMode: appMode,
appName: currentApp.name,
isExampleApp: currentApp.appIsExample,
});
const pagination =
eventType === EventType.ON_NEXT_PAGE
? "NEXT"
: eventType === EventType.ON_PREV_PAGE
? "PREV"
: undefined;
AppsmithConsole.info({
text: "Execution started from widget request",
source: {
type: ENTITY_TYPE.ACTION,
name: action.name,
id: actionId,
},
state: action.actionConfiguration,
});
const executePluginActionResponse: ExecutePluginActionResponse = yield call(
executePluginActionSaga,
action.id,
pagination,
params,
);
const { isError, payload } = executePluginActionResponse;
if (isError) {
AppsmithConsole.addError({
id: actionId,
logType: LOG_TYPE.ACTION_EXECUTION_ERROR,
text: `Execution failed with status ${payload.statusCode}`,
source: {
type: ENTITY_TYPE.ACTION,
name: action.name,
id: actionId,
},
state: payload.request,
messages: [
{
// Need to stringify cause this gets rendered directly
// and rendering objects can crash the app
message: !isString(payload.body)
? JSON.stringify(payload.body)
: payload.body,
type: PLATFORM_ERROR.PLUGIN_EXECUTION,
subType: payload.errorType,
},
],
});
if (onError) {
yield call(executeAppAction, {
event: { type: eventType },
dynamicString: onError,
callbackData: [payload.body, params],
...triggerMeta,
});
} else {
throw new PluginTriggerFailureError(
createMessage(ERROR_PLUGIN_ACTION_EXECUTE, action.name),
[payload.body, params],
);
}
} else {
AppsmithConsole.info({
logType: LOG_TYPE.ACTION_EXECUTION_SUCCESS,
text: "Executed successfully from widget request",
timeTaken: payload.duration,
source: {
type: ENTITY_TYPE.ACTION,
name: action.name,
id: actionId,
},
state: {
response: payload.body,
request: payload.request,
},
});
if (onSuccess) {
yield call(executeAppAction, {
event: { type: eventType },
dynamicString: onSuccess,
callbackData: [payload.body, params],
...triggerMeta,
});
}
}
return [payload.body, params];
}
function* runActionShortcutSaga() {
const location = window.location.pathname;
const match: any = matchPath(location, {
path: [
trimQueryString(API_EDITOR_URL()),
trimQueryString(API_EDITOR_ID_URL()),
trimQueryString(QUERIES_EDITOR_URL()),
trimQueryString(QUERIES_EDITOR_ID_URL()),
trimQueryString(API_EDITOR_URL_WITH_SELECTED_PAGE_ID()),
trimQueryString(INTEGRATION_EDITOR_URL()),
trimQueryString(SAAS_EDITOR_API_ID_URL()),
CURL_IMPORT_PAGE_PATH, // check if the current location matches a curl editor page
],
exact: true,
strict: false,
});
// get the current form name
const currentFormNames = yield select(getFormNames());
if (!match || !match.params) return;
const { apiId, pageId, queryId } = match.params;
const actionId = apiId || queryId;
if (actionId) {
const trackerId = apiId
? PerformanceTransactionName.RUN_API_SHORTCUT
: PerformanceTransactionName.RUN_QUERY_SHORTCUT;
PerformanceTracker.startTracking(trackerId, {
actionId,
pageId,
});
AnalyticsUtil.logEvent(trackerId as EventName, {
actionId,
});
yield put(runAction(actionId));
} else if (
!!currentFormNames &&
currentFormNames.includes(CURL_IMPORT_FORM) &&
!actionId
) {
// if the current form names include the curl form and there are no actionIds i.e. its not an api or query
// get the form values and call the submit curl import form function with its data
const formValues = yield select(getFormValues(CURL_IMPORT_FORM));
// if the user has not edited the curl input field, assign an empty string to it, so it doesnt throw an error.
if (!formValues?.curl) formValues["curl"] = "";
yield put(submitCurlImportForm(formValues));
} else {
return;
}
}
function* runActionSaga(
reduxAction: ReduxAction<{
id: string;
paginationField: PaginationField;
}>,
) {
const actionId = reduxAction.payload.id;
const isSaving = yield select(isActionSaving(actionId));
const isDirty = yield select(isActionDirty(actionId));
const isSavingEntity = yield select(getIsSavingEntity);
if (isSaving || isDirty || isSavingEntity) {
if (isDirty && !isSaving) {
yield put(updateAction({ id: actionId }));
}
yield take(ReduxActionTypes.UPDATE_ACTION_SUCCESS);
}
const actionObject = yield select(getAction, actionId);
const datasourceUrl = get(
actionObject,
"datasource.datasourceConfiguration.url",
);
AppsmithConsole.info({
text: "Execution started from user request",
source: {
type: ENTITY_TYPE.ACTION,
name: actionObject.name,
id: actionId,
},
state: {
...actionObject.actionConfiguration,
...(datasourceUrl && {
url: datasourceUrl,
}),
},
});
const { id, paginationField } = reduxAction.payload;
let payload = EMPTY_RESPONSE;
let isError = true;
let error = "";
try {
const executePluginActionResponse: ExecutePluginActionResponse = yield call(
executePluginActionSaga,
id,
paginationField,
);
payload = executePluginActionResponse.payload;
isError = executePluginActionResponse.isError;
} catch (e) {
// When running from the pane, we just want to end the saga if the user has
// cancelled the call. No need to log any errors
if (e instanceof UserCancelledActionExecutionError) {
return;
}
log.error(e);
error = e.message;
}
// Error should be readable error if present.
// Otherwise, payload's body.
// Default to "An unexpected error occurred" if none is available
const readableError = payload.readableError
? getErrorAsString(payload.readableError)
: undefined;
const payloadBodyError = payload.body
? getErrorAsString(payload.body)
: undefined;
const defaultError = "An unexpected error occurred";
if (isError) {
error = readableError || payloadBodyError || defaultError;
// In case of debugger, both the current error message
// and the readableError needs to be present,
// since the readableError may be malformed for certain errors.
const appsmithConsoleErrorMessageList = [
{
message: error,
type: PLATFORM_ERROR.PLUGIN_EXECUTION,
subType: payload.errorType,
},
];
if (error === readableError && !!payloadBodyError) {
appsmithConsoleErrorMessageList.push({
message: payloadBodyError,
type: PLATFORM_ERROR.PLUGIN_EXECUTION,
subType: payload.errorType,
});
}
AppsmithConsole.addError({
id: actionId,
logType: LOG_TYPE.ACTION_EXECUTION_ERROR,
text: `Execution failed${
payload.statusCode ? ` with status ${payload.statusCode}` : ""
}`,
source: {
type: ENTITY_TYPE.ACTION,
name: actionObject.name,
id: actionId,
},
messages: appsmithConsoleErrorMessageList,
state: payload.request,
});
Toaster.show({
text: createMessage(ERROR_ACTION_EXECUTE_FAIL, actionObject.name),
variant: Variant.danger,
});
yield put({
type: ReduxActionErrorTypes.RUN_ACTION_ERROR,
payload: {
error: appsmithConsoleErrorMessageList[0],
id: reduxAction.payload.id,
},
});
return;
}
const pageName = yield select(getCurrentPageNameByActionId, actionId);
let eventName: EventName = "RUN_API";
if (actionObject.pluginType === PluginType.DB) {
eventName = "RUN_QUERY";
}
if (actionObject.pluginType === PluginType.SAAS) {
eventName = "RUN_SAAS_API";
}
AnalyticsUtil.logEvent(eventName, {
actionId,
actionName: actionObject.name,
pageName: pageName,
responseTime: payload.duration,
apiType: "INTERNAL",
});
yield put({
type: ReduxActionTypes.RUN_ACTION_SUCCESS,
payload: { [actionId]: payload },
});
if (payload.isExecutionSuccess) {
AppsmithConsole.info({
logType: LOG_TYPE.ACTION_EXECUTION_SUCCESS,
text: "Executed successfully from user request",
timeTaken: payload.duration,
source: {
type: ENTITY_TYPE.ACTION,
name: actionObject.name,
id: actionId,
},
state: {
response: payload.body,
request: payload.request,
},
});
}
}
function* executePageLoadAction(pageAction: PageAction) {
const pageId = yield select(getCurrentPageId);
let currentApp: ApplicationPayload = yield select(getCurrentApplication);
currentApp = currentApp || {};
const appMode = yield select(getAppMode);
AnalyticsUtil.logEvent("EXECUTE_ACTION", {
type: pageAction.pluginType,
name: pageAction.name,
pageId: pageId,
appMode: appMode,
appId: currentApp.id,
onPageLoad: true,
appName: currentApp.name,
isExampleApp: currentApp.appIsExample,
});
let payload = EMPTY_RESPONSE;
let isError = true;
const error = `The action "${pageAction.name}" has failed.`;
try {
const executePluginActionResponse: ExecutePluginActionResponse = yield call(
executePluginActionSaga,
pageAction,
);
payload = executePluginActionResponse.payload;
isError = executePluginActionResponse.isError;
} catch (e) {
log.error(e);
}
if (isError) {
AppsmithConsole.addError({
id: pageAction.id,
logType: LOG_TYPE.ACTION_EXECUTION_ERROR,
text: `Execution failed with status ${payload.statusCode}`,
source: {
type: ENTITY_TYPE.ACTION,
name: pageAction.name,
id: pageAction.id,
},
state: payload.request,
messages: [
{
message: error,
type: PLATFORM_ERROR.PLUGIN_EXECUTION,
subType: payload.errorType,
},
],
});
yield put(
executePluginActionError({
actionId: pageAction.id,
isPageLoad: true,
error: { message: error },
data: payload,
}),
);
PerformanceTracker.stopAsyncTracking(
PerformanceTransactionName.EXECUTE_ACTION,
{
failed: true,
},
pageAction.id,
);
} else {
PerformanceTracker.stopAsyncTracking(
PerformanceTransactionName.EXECUTE_ACTION,
undefined,
pageAction.id,
);
yield put(
executePluginActionSuccess({
id: pageAction.id,
response: payload,
isPageLoad: true,
}),
);
yield take(ReduxActionTypes.SET_EVALUATED_TREE);
}
}
function* executePageLoadActionsSaga() {
try {
const pageActions: PageAction[][] = yield select(getLayoutOnLoadActions);
const actionCount = _.flatten(pageActions).length;
PerformanceTracker.startAsyncTracking(
PerformanceTransactionName.EXECUTE_PAGE_LOAD_ACTIONS,
{ numActions: actionCount },
);
for (const actionSet of pageActions) {
// Load all sets in parallel
yield* yield all(
actionSet.map((apiAction) => call(executePageLoadAction, apiAction)),
);
}
PerformanceTracker.stopAsyncTracking(
PerformanceTransactionName.EXECUTE_PAGE_LOAD_ACTIONS,
);
// We show errors in the debugger once onPageLoad actions
// are executed
yield put(hideDebuggerErrors(false));
} catch (e) {
log.error(e);
Toaster.show({
text: createMessage(ERROR_FAIL_ON_PAGE_LOAD_ACTIONS),
variant: Variant.danger,
});
}
}
type ExecutePluginActionResponse = {
payload: ActionResponse;
isError: boolean;
};
/*
* This saga handles the complete plugin action execution flow. It will respond with a
* payload and isError property which indicates if the response is of an error type.
* In case of the execution was not completed, it will throw errors of type
* PluginActionExecutionError which needs to be handled by any saga that calls this.
* */
function* executePluginActionSaga(
actionOrActionId: PageAction | string,
paginationField?: PaginationField,
params?: Record<string, unknown>,
) {
let pluginAction;
let actionId;
if (isString(actionOrActionId)) {
pluginAction = yield select(getAction, actionOrActionId);
actionId = actionOrActionId;
} else {
pluginAction = actionOrActionId;
actionId = actionOrActionId.id;
}
if (pluginAction.confirmBeforeExecute) {
const modalPayload = {
name: pluginAction.name,
modalOpen: true,
modalType: ModalType.RUN_ACTION,
};
const confirmed = yield call(requestModalConfirmationSaga, modalPayload);
if (!confirmed) {
yield put({
type: ReduxActionTypes.RUN_ACTION_CANCELLED,
payload: { id: actionId },
});
throw new UserCancelledActionExecutionError();
}
}
PerformanceTracker.startAsyncTracking(
PerformanceTransactionName.EXECUTE_ACTION,
{
actionId: actionId,
},
actionId,
);
yield put(executePluginActionRequest({ id: actionId }));
const appMode = yield select(getAppMode);
const timeout = yield select(getActionTimeout, actionId);
const executeActionRequest: ExecuteActionRequest = {
actionId: actionId,
viewMode: appMode === APP_MODE.PUBLISHED,
};
if (paginationField) {
executeActionRequest.paginationField = paginationField;
}
const formData = new FormData();
formData.append("executeActionDTO", JSON.stringify(executeActionRequest));
yield call(evaluateActionParams, pluginAction.jsonPathKeys, formData, params);
const response: ActionExecutionResponse = yield ActionAPI.executeAction(
formData,
timeout,
);
PerformanceTracker.stopAsyncTracking(
PerformanceTransactionName.EXECUTE_ACTION,
);
try {
yield validateResponse(response);
const payload = createActionExecutionResponse(response);
yield put(
executePluginActionSuccess({
id: actionId,
response: payload,
}),
);
return {
payload,
isError: isErrorResponse(response),
};
} catch (e) {
yield put(
executePluginActionSuccess({
id: actionId,
response: EMPTY_RESPONSE,
}),
);
throw new PluginActionExecutionError("Response not valid", false, response);
}
}
export function* watchPluginActionExecutionSagas() {
yield all([
takeLatest(ReduxActionTypes.RUN_ACTION_REQUEST, runActionSaga),
takeLatest(
ReduxActionTypes.RUN_ACTION_SHORTCUT_REQUEST,
runActionShortcutSaga,
),
takeLatest(
ReduxActionTypes.EXECUTE_PAGE_LOAD_ACTIONS,
executePageLoadActionsSaga,
),
]);
}