PromucFlow_constructor/app/client/src/sagas/ActionExecution/PluginActionSaga.ts
Rahul Barwal 3421f8bfd1
feat: implements onPageUnload functionality for the edit app mode page selector (#41074)
## Description
<ins>Problem</ins>

onPageUnload functionality was not consistently triggered during all
types of page navigation in edit mode, leading to potential missed
cleanup or actions when navigating between pages via different UI
elements or programmatic flows.

<ins>Root cause</ins>

Navigation logic was fragmented across multiple components and methods
(button clicks, navigation tabs, page navigator), and direct history
manipulation bypassed centralized handling, preventing reliable
invocation of onPageUnload actions.

<ins>Solution</ins>

This PR handles the integration of onPageUnload functionality with all
page navigation flows in edit mode by centralizing navigation logic
through the navigateToAnotherPage action, enhancing type safety, and
ensuring onPageUnload actions are filtered and executed based on the
current page context.

Fixes #40998
_or_  
Fixes `Issue URL`
> [!WARNING]  
> _If no issue exists, please create an issue first, and check with the
maintainers if the issue is valid._

## Automation

/ok-to-test tags="@tag.JS, @tag.Sanity"

### 🔍 Cypress test results
<!-- This is an auto-generated comment: Cypress test results  -->
> [!TIP]
> 🟢 🟢 🟢 All cypress tests have passed! 🎉 🎉 🎉
> Workflow run:
<https://github.com/appsmithorg/appsmith/actions/runs/16042398132>
> Commit: 8ea04e6bb1312d9f468ed3d74ccc080ed6e9bac9
> <a
href="https://internal.appsmith.com/app/cypress-dashboard/rundetails-65890b3c81d7400d08fa9ee5?branch=master&workflowId=16042398132&attempt=1"
target="_blank">Cypress dashboard</a>.
> Tags: `@tag.JS, @tag.Sanity`
> Spec:
> <hr>Thu, 03 Jul 2025 06:44:33 UTC
<!-- end of auto-generated comment: Cypress test results  -->


## Communication
Should the DevRel and Marketing teams inform users about this change?
- [ ] Yes
- [ ] No


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Enhanced page unload actions to only trigger for the current page,
improving accuracy and reliability.

* **Bug Fixes**
* Improved navigation consistency by updating the page switching
mechanism to use a unified action.

* **Tests**
* Added tests to ensure correct filtering of JavaScript actions executed
on page unload.

* **Refactor**
* Streamlined selector logic for better maintainability and performance.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-03 12:24:53 +05:30

1755 lines
50 KiB
TypeScript

import {
all,
call,
delay,
put,
select,
take,
takeLatest,
} from "redux-saga/effects";
import {
clearActionResponse,
executePageLoadActions,
executePluginActionError,
executePluginActionRequest,
executePluginActionSuccess,
runAction,
updateAction,
updateActionData,
} from "actions/pluginActionActions";
import { handleExecuteJSFunctionSaga } from "sagas/JSPaneSagas";
import type { ApplicationPayload } from "entities/Application";
import type { ReduxAction } from "actions/ReduxActionTypes";
import {
ReduxActionErrorTypes,
ReduxActionTypes,
} from "ee/constants/ReduxActionConstants";
import type {
ActionExecutionResponse,
ActionResponse,
ExecuteActionRequest,
PaginationField,
} from "api/ActionAPI";
import ActionAPI from "api/ActionAPI";
import {
getAction,
getCurrentActions,
getCurrentPageNameByActionId,
getDatasource,
getJSCollectionFromAllEntities,
getPlugin,
} from "ee/selectors/entitiesSelector";
import {
getAppMode,
getCurrentApplication,
} from "ee/selectors/applicationSelectors";
import {
find,
flatten,
get,
isArray,
isArrayBuffer,
isEmpty,
isNil,
isString,
set,
unset,
zipObject,
} from "lodash";
import AppsmithConsole from "utils/AppsmithConsole";
import { ENTITY_TYPE, PLATFORM_ERROR } from "ee/entities/AppsmithConsole/utils";
import {
extractClientDefinedErrorMetadata,
validateResponse,
} from "sagas/ErrorSagas";
import AnalyticsUtil from "ee/utils/AnalyticsUtil";
import type { Action } from "entities/Action";
import { ActionExecutionContext } from "entities/Action";
import LOG_TYPE from "entities/AppsmithConsole/logtype";
import {
ACTION_EXECUTION_CANCELLED,
ACTION_EXECUTION_FAILED,
createMessage,
ERROR_ACTION_EXECUTE_FAIL,
ERROR_FAIL_ON_PAGE_LOAD_ACTIONS,
ERROR_PLUGIN_ACTION_EXECUTE,
SWITCH_ENVIRONMENT_SUCCESS,
} from "ee/constants/messages";
import type {
LayoutOnLoadActionErrors,
PageAction,
} from "constants/AppsmithActionConstants/ActionConstants";
import { EventType } from "constants/AppsmithActionConstants/ActionConstants";
import {
getCurrentApplicationId,
getCurrentBasePageId,
getCurrentPageId,
getIsSavingEntity,
getLayoutOnLoadActions,
getLayoutOnLoadIssues,
getLayoutOnUnloadActions,
} from "selectors/editorSelectors";
import log from "loglevel";
import { EMPTY_RESPONSE } from "components/editorComponents/emptyResponse";
import type { DefaultRootState } from "react-redux";
import { DEFAULT_EXECUTE_ACTION_TIMEOUT_MS } from "ee/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_BASE_PATH,
API_EDITOR_ID_PATH,
API_EDITOR_PATH_WITH_SELECTED_PAGE_ID,
INTEGRATION_EDITOR_PATH,
matchQueryBuilderPath,
QUERIES_EDITOR_BASE_PATH,
QUERIES_EDITOR_ID_PATH,
} from "constants/routes";
import { SAAS_EDITOR_API_ID_PATH } from "pages/Editor/SaaSEditor/constants";
import { APP_MODE } from "entities/App";
import { FileDataTypes } from "WidgetProvider/types";
import { hideDebuggerErrors } from "actions/debuggerActions";
import {
ActionValidationError,
getErrorAsString,
PluginActionExecutionError,
PluginTriggerFailureError,
UserCancelledActionExecutionError,
} from "sagas/ActionExecution/errorUtils";
import { shouldBeDefined, trimQueryString } from "utils/helpers";
import { requestModalConfirmationSaga } from "sagas/UtilSagas";
import { ModalType } from "reducers/uiReducers/modalActionReducer";
import { matchBasePath } from "ee/pages/Editor/Explorer/helpers";
import {
findDatatype,
isTrueObject,
} from "ee/workers/Evaluation/evaluationUtils";
import { type Plugin, PluginType } from "entities/Plugin";
import { getIsAnvilEnabledInCurrentApplication } from "../../layoutSystems/anvil/integrations/selectors";
import { setDefaultActionDisplayFormat } from "./PluginActionSagaUtils";
import { checkAndLogErrorsIfCyclicDependency } from "sagas/helper";
import { toast } from "@appsmith/ads";
import type { TRunDescription } from "workers/Evaluation/fns/actionFns";
import { DEBUGGER_TAB_KEYS } from "components/editorComponents/Debugger/constants";
import { FILE_SIZE_LIMIT_FOR_BLOBS } from "constants/WidgetConstants";
import type { ActionData } from "ee/reducers/entityReducers/actionsReducer";
import { handleStoreOperations } from "./StoreActionSaga";
import { fetchPageAction } from "actions/pageActions";
import type { Datasource } from "entities/Datasource";
import { softRefreshDatasourceStructure } from "actions/datasourceActions";
import {
getCurrentEnvironmentDetails,
getCurrentEnvironmentName,
} from "ee/selectors/environmentSelectors";
import { getIsActionCreatedInApp } from "ee/utils/getIsActionCreatedInApp";
import {
endSpan,
setAttributesToSpan,
startRootSpan,
} from "instrumentation/generateTraces";
import {
getActionExecutionAnalytics,
getActionProperties,
getJSActionPathNameToDisplay,
getPluginActionNameToDisplay,
} from "ee/utils/actionExecutionUtils";
import type { JSAction, JSCollection } from "entities/JSCollection";
import { getAllowedActionAnalyticsKeys } from "constants/AppsmithActionConstants/formConfig/ActionAnalyticsConfig";
import {
changeQuery,
isActionDirty,
isActionSaving,
setPluginActionEditorDebuggerState,
} from "PluginActionEditor/store";
import { objectKeys } from "@appsmith/utils";
import type { Span } from "instrumentation/types";
import {
selectGitConnectModalOpen,
selectGitOpsModalOpen,
} from "selectors/gitModSelectors";
import { createActionExecutionResponse } from "./PluginActionSagaUtils";
import { ActionRunBehaviour } from "PluginActionEditor/types/PluginActionTypes";
import { appsmithTelemetry } from "instrumentation";
interface FilePickerInstumentationObject {
numberOfFiles: number;
totalSize: number;
fileTypes: Array<string>;
fileSizes: Array<number>;
}
export const getActionTimeout = (
state: DefaultRootState,
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 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
*/
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function* readBlob(blobUrl: string): any {
const [url, fileType] = parseBlobUrl(blobUrl);
const file = yield fetch(url).then(async (r) => r.blob());
return yield new Promise((resolve) => {
const reader = new FileReader();
if (fileType === FileDataTypes.Base64) {
reader.readAsDataURL(file);
} else if (fileType === FileDataTypes.Binary) {
if (file.size < FILE_SIZE_LIMIT_FOR_BLOBS) {
//check size of the file, if less than 5mb, go with binary string method
// TODO: this method is deprecated, use readAsText instead
reader.readAsBinaryString(file);
} else {
// For files greater than 5 mb, use array buffer method
// This is to remove the bloat from the file which is added
// when using read as binary string method
reader.readAsArrayBuffer(file);
}
} else {
reader.readAsText(file);
}
reader.onloadend = () => {
resolve(reader.result);
};
});
}
/**
* This function resolves :
* - individual objects containing blob urls
* - blob urls directly
* - else returns the value unchanged
* - finds datatype of evaluated value
* - binds dataype to payload
*
* @param value
* @param executeActionRequest
* @param index
* @param isArray
* @param arrDatatype
*/
function* resolvingBlobUrls(
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
value: any,
executeActionRequest: ExecuteActionRequest,
index: number,
isArray?: boolean,
arrDatatype?: string[],
) {
//Get datatypes of evaluated value.
const dataType: string = findDatatype(value);
//If array elements then dont push datatypes to payload.
isArray
? arrDatatype?.push(dataType)
: (executeActionRequest.paramProperties[`k${index}`] = {
datatype: dataType,
});
if (isTrueObject(value)) {
const blobUrlPaths: string[] = [];
objectKeys(value).forEach((propertyName) => {
if (isBlobUrl(value[propertyName])) {
blobUrlPaths.push(propertyName);
}
});
for (const blobUrlPath of blobUrlPaths) {
const blobUrl = value[blobUrlPath] as string;
const resolvedBlobValue: unknown = yield call(readBlob, blobUrl);
set(value, blobUrlPath, resolvedBlobValue);
// We need to store the url path map to be able to update the blob data
// and send the info to server
// Here we fetch the blobUrlPathMap from the action payload and update it
const blobUrlPathMap = get(value, "blobUrlPaths", {}) as Record<
string,
string
>;
set(blobUrlPathMap, blobUrlPath, blobUrl);
set(value, "blobUrlPaths", blobUrlPathMap);
}
} else if (isBlobUrl(value)) {
// @ts-expect-error: Values can take many types
value = yield call(readBlob, value);
}
return value;
}
// Function that updates the blob data in the action payload for large file
// uploads
function updateBlobDataFromUrls(
blobUrlPaths: Record<string, string>,
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
newVal: any,
blobMap: string[],
blobDataMap: Record<string, Blob>,
) {
Object.entries(blobUrlPaths as Record<string, string>).forEach(
// blobUrl: string eg: blob:1234-1234-1234?type=binary
([path, blobUrl]) => {
if (isArrayBuffer(newVal[path])) {
// remove the ?type=binary from the blob url if present
const sanitisedBlobURL = blobUrl.split("?")[0];
blobMap.push(sanitisedBlobURL);
set(blobDataMap, sanitisedBlobURL, new Blob([newVal[path]]));
set(newVal, path, sanitisedBlobURL);
}
},
);
}
/**
* 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 formData
* @param executeActionRequest
* @param filePickerInstrumentation
* @param executionParams
*/
function* evaluateActionParams(
bindings: string[] | undefined,
formData: FormData,
executeActionRequest: ExecuteActionRequest,
filePickerInstrumentation: FilePickerInstumentationObject,
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
executionParams?: Record<string, any> | string,
) {
if (isNil(bindings) || bindings.length === 0) {
formData.append("executeActionDTO", JSON.stringify(executeActionRequest));
return [];
}
// Evaluated all bindings of the actions. Pass executionParams if any
// @ts-expect-error: Values can take many types
const values = yield call(evaluateActionBindings, bindings, executionParams);
const bindingsMap: Record<string, string> = {};
const bindingBlob = [];
const evaluatedParams = zipObject(bindings, values);
// Maintain a blob data map to resolve blob urls of large files as array buffer
const blobDataMap: Record<string, Blob> = {};
// if json bindings have filepicker reference, we need to init the instrumentation object
// which we will send post execution
const recordFilePickerInstrumentation = bindings.some((binding) =>
binding.includes(".files"),
);
// Add keys values to formData for the multipart submission
for (let i = 0; i < bindings.length; i++) {
const key = bindings[i];
let value = isArray(values) && values[i];
let useBlobMaps = false;
// Maintain a blob map to resolve blob urls of large files
const blobMap: Array<string> = [];
if (isArray(value)) {
const tempArr = [];
const arrDatatype: Array<string> = [];
// array of objects containing blob urls that is loops and individual object is checked for resolution of blob urls.
const BATCH_CHUNK_SIZE = 100;
for (let j = 0; j < value.length; j++) {
const val = value[j];
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const newVal: Record<string, any> = yield call(
resolvingBlobUrls,
val,
executeActionRequest,
i,
true,
arrDatatype,
);
if (newVal.hasOwnProperty("blobUrlPaths")) {
updateBlobDataFromUrls(
newVal.blobUrlPaths,
newVal,
blobMap,
blobDataMap,
);
useBlobMaps = true;
unset(newVal, "blobUrlPaths");
evaluatedParams[key] = "blob";
}
tempArr.push(newVal);
if (key.includes(".files") && recordFilePickerInstrumentation) {
filePickerInstrumentation["numberOfFiles"] += 1;
const { size, type } = newVal;
filePickerInstrumentation["totalSize"] += size;
filePickerInstrumentation["fileSizes"].push(size);
filePickerInstrumentation["fileTypes"].push(type);
evaluatedParams[key] = "file";
}
if ((j + 1) % BATCH_CHUNK_SIZE === 0) {
// Yield control back to the event loop and empty the stack trace
yield delay(0);
}
}
//Adding array datatype along with the datatype of first element of the array
executeActionRequest.paramProperties[`k${i}`] = {
datatype: { array: [arrDatatype[0]] },
};
value = tempArr;
} else {
// @ts-expect-error: Values can take many types
value = yield call(resolvingBlobUrls, value, executeActionRequest, i);
if (key.includes(".files") && recordFilePickerInstrumentation) {
filePickerInstrumentation["numberOfFiles"] += 1;
filePickerInstrumentation["totalSize"] += value.size;
filePickerInstrumentation["fileSizes"].push(value.size);
filePickerInstrumentation["fileTypes"].push(value.type);
evaluatedParams[key] = "file";
}
}
if (typeof value === "object") {
// This is used in cases of large files, we store the bloburls with the path they were set in
// This helps in creating a unique map of blob urls to blob data when passing to the server
if (!!value && value.hasOwnProperty("blobUrlPaths")) {
updateBlobDataFromUrls(value.blobUrlPaths, value, blobMap, blobDataMap);
unset(value, "blobUrlPaths");
evaluatedParams[key] = "blob";
}
// Handle null values separately to avoid stringifying them
if (value === null) {
value = null;
evaluatedParams[key] = null;
} else {
value = JSON.stringify(value);
evaluatedParams[key] = value;
}
}
// If there are no blob urls in the value, we can directly add it to the formData
// If there are blob urls, we need to add them to the blobDataMap
if (!useBlobMaps) {
// Handle null values separately to avoid creating a Blob with "null" string
if (value === null) {
value = null;
} else {
value = new Blob([value], { type: "text/plain" });
}
}
bindingsMap[key] = `k${i}`;
bindingBlob.push({ name: `k${i}`, value: value });
// We need to add the blob map to the param properties
// This will allow the server to handle the scenaio of large files upload using blob data
const paramProperties = executeActionRequest.paramProperties[`k${i}`];
if (!!paramProperties && typeof paramProperties === "object") {
paramProperties["blobIdentifiers"] = blobMap;
}
}
formData.append("executeActionDTO", JSON.stringify(executeActionRequest));
formData.append("parameterMap", JSON.stringify(bindingsMap));
bindingBlob?.forEach((item) => formData.append(item.name, item.value));
// Append blob data map to formData if not empty
if (!isEmpty(blobDataMap)) {
// blobDataMap is used to resolve blob urls of large files as array buffer
// we need to add each blob data to formData as a separate entry
Object.entries(blobDataMap).forEach(([path, blobData]) =>
formData.append(path, blobData),
);
}
return evaluatedParams;
}
export default function* executePluginActionTriggerSaga(
pluginAction: TRunDescription,
eventType: EventType,
) {
const span = startRootSpan("executePluginActionTriggerSaga");
const { payload: pluginPayload } = pluginAction;
const { actionId, onError, params } = pluginPayload;
if (getType(params) !== Types.OBJECT) {
throw new ActionValidationError(
"RUN_PLUGIN_ACTION",
"params",
Types.OBJECT,
getType(params),
);
}
setAttributesToSpan(span, {
actionId: actionId,
});
const action = shouldBeDefined<Action>(
yield select(getAction, actionId),
`Action not found for id - ${actionId}`,
);
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const datasourceId: string = (action?.datasource as any)?.id;
const plugin: Plugin = yield select(getPlugin, action?.pluginId);
const currentApp: ApplicationPayload = yield select(getCurrentApplication);
const currentEnvDetails: { id: string; name: string } = yield select(
getCurrentEnvironmentDetails,
);
const pluginActionNameToDisplay = getPluginActionNameToDisplay(action);
const actionExecutionAnalytics = getActionExecutionAnalytics(
action,
plugin,
params,
currentApp,
datasourceId,
);
AnalyticsUtil.logEvent("EXECUTE_ACTION", actionExecutionAnalytics);
const pagination =
eventType === EventType.ON_NEXT_PAGE
? "NEXT"
: eventType === EventType.ON_PREV_PAGE
? "PREV"
: undefined;
const executePluginActionResponse: ExecutePluginActionResponse = yield call(
executePluginActionSaga,
action,
pagination,
params,
undefined,
span,
);
const { isError, payload } = executePluginActionResponse;
if (isError) {
AppsmithConsole.addErrors([
{
payload: {
id: actionId,
iconId: action.pluginId,
logType: LOG_TYPE.ACTION_EXECUTION_ERROR,
text: `Failed execution in ${payload.duration}(ms)`,
environmentName: currentEnvDetails.name,
source: {
type: ENTITY_TYPE.ACTION,
name: pluginActionNameToDisplay,
id: actionId,
httpMethod: action?.actionConfiguration?.httpMethod,
pluginType: action.pluginType,
},
state: {
error: !isString(payload.body)
? JSON.stringify(payload.body)
: payload.body,
request: payload.request,
},
pluginErrorDetails: payload.pluginErrorDetails,
},
},
]);
AnalyticsUtil.logEvent("EXECUTE_ACTION_FAILURE", {
...actionExecutionAnalytics,
...payload.pluginErrorDetails,
});
if (onError) {
throw new PluginTriggerFailureError(
createMessage(ERROR_ACTION_EXECUTE_FAIL, pluginActionNameToDisplay),
[payload.body, params],
);
} else {
throw new PluginTriggerFailureError(
createMessage(ERROR_PLUGIN_ACTION_EXECUTE, pluginActionNameToDisplay),
[],
);
}
} else {
AnalyticsUtil.logEvent("EXECUTE_ACTION_SUCCESS", actionExecutionAnalytics);
AppsmithConsole.info({
logType: LOG_TYPE.ACTION_EXECUTION_SUCCESS,
text: `Successfully executed in ${payload.duration}(ms)`,
source: {
type: ENTITY_TYPE.ACTION,
name: pluginActionNameToDisplay,
id: actionId,
},
state: {
response: payload.body,
request: payload.request,
},
});
}
return [
payload.body,
params,
{
isExecutionSuccess: payload.isExecutionSuccess,
statusCode: payload.statusCode,
headers: payload.headers,
},
];
}
function* runActionShortcutSaga() {
const pathname = window.location.pathname;
const baseMatch = matchBasePath(pathname);
if (!baseMatch) return;
// get gitSyncModal status
const isGitOpsModalOpen: boolean = yield select(selectGitOpsModalOpen);
const isGitConnectModalOpen: boolean = yield select(
selectGitConnectModalOpen,
);
// if git sync modal is open, prevent action from being executed via shortcut keys.
if (isGitOpsModalOpen || isGitConnectModalOpen) return;
const { path } = baseMatch;
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const match: any = matchPath(pathname, {
path: [
trimQueryString(`${path}${API_EDITOR_BASE_PATH}`),
trimQueryString(`${path}${API_EDITOR_ID_PATH}`),
trimQueryString(`${path}${QUERIES_EDITOR_BASE_PATH}`),
trimQueryString(`${path}${QUERIES_EDITOR_ID_PATH}`),
trimQueryString(`${path}${API_EDITOR_PATH_WITH_SELECTED_PAGE_ID}`),
trimQueryString(`${path}${INTEGRATION_EDITOR_PATH}`),
trimQueryString(`${path}${SAAS_EDITOR_API_ID_PATH}`),
],
exact: true,
strict: false,
});
if (!match || !match.params) return;
const { baseApiId, baseQueryId } = match.params;
const actionId = baseApiId || baseQueryId;
if (actionId) {
yield put(runAction(actionId));
} else {
return;
}
}
interface RunActionError {
name: string;
message: string;
clientDefinedError?: boolean;
}
export function* runActionSaga(
reduxAction: ReduxAction<{
id: string;
paginationField?: PaginationField;
skipOpeningDebugger: boolean;
action?: Action;
actionExecutionContext?: ActionExecutionContext;
}>,
) {
const span = startRootSpan("runActionSaga");
const actionId = reduxAction.payload.id;
const isSaving: boolean = yield select(isActionSaving(actionId));
const isDirty: boolean = yield select(isActionDirty(actionId));
const isSavingEntity: boolean = yield select(getIsSavingEntity);
if (isSaving || isDirty || isSavingEntity) {
if (isDirty && !isSaving) {
yield put(updateAction({ id: actionId }));
yield take(ReduxActionTypes.UPDATE_ACTION_SUCCESS);
}
}
const currentEnvDetails: { id: string; name: string } = yield select(
getCurrentEnvironmentDetails,
);
const actionObject =
reduxAction.payload.action ||
shouldBeDefined<Action>(
yield select(getAction, actionId),
`action not found for id - ${actionId}`,
);
const plugin: Plugin = yield select(getPlugin, actionObject?.pluginId);
const datasource: Datasource = yield select(
getDatasource,
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(actionObject?.datasource as any)?.id,
);
const pageName: string = yield select(getCurrentPageNameByActionId, actionId);
const { paginationField } = reduxAction.payload;
// open response tab in debugger on exection of action.
if (!reduxAction.payload.skipOpeningDebugger) {
yield put(
setPluginActionEditorDebuggerState({
open: true,
selectedTab: DEBUGGER_TAB_KEYS.RESPONSE_TAB,
}),
);
}
let payload = EMPTY_RESPONSE;
let isError = true;
let error: RunActionError = {
name: "",
message: "",
};
const pluginActionNameToDisplay = getPluginActionNameToDisplay(actionObject);
try {
const executePluginActionResponse: ExecutePluginActionResponse = yield call(
executePluginActionSaga,
actionObject,
paginationField,
{},
true,
span,
);
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) {
// cancel action but do not throw any error.
yield put({
type: ReduxActionErrorTypes.RUN_ACTION_ERROR,
payload: {
error: e.name,
id: reduxAction.payload.id,
show: false,
},
});
toast.show(
createMessage(ACTION_EXECUTION_CANCELLED, pluginActionNameToDisplay),
{
kind: "error",
},
);
return;
}
log.error(e);
error = { name: (e as Error).name, message: (e as Error).message };
const clientDefinedErrorMetadata = extractClientDefinedErrorMetadata(e);
if (clientDefinedErrorMetadata) {
set(
payload,
"statusCode",
`${clientDefinedErrorMetadata?.statusCode || ""}`,
);
set(payload, "request", {});
set(
payload,
"pluginErrorDetails",
clientDefinedErrorMetadata?.pluginErrorDetails,
);
set(error, "clientDefinedError", true);
}
}
// 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
? {
name: "PluginExecutionError",
message: getErrorAsString(payload.readableError),
}
: undefined;
const payloadBodyError = payload.body
? {
name: "PluginExecutionError",
message: getErrorAsString(payload.body),
}
: undefined;
const clientDefinedError = error.clientDefinedError
? {
name: "PluginExecutionError",
message: error?.message,
clientDefinedError: true,
}
: undefined;
const defaultError = {
name: "PluginExecutionError",
message: "An unexpected error occurred",
};
const allowedActionAnalyticsKeys = getAllowedActionAnalyticsKeys(
plugin?.packageName,
);
const actionAnalyticsPayload = getActionProperties(
actionObject,
allowedActionAnalyticsKeys,
);
if (isError) {
error =
readableError || payloadBodyError || clientDefinedError || 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.addErrors([
{
payload: {
id: actionId,
iconId: actionObject.pluginId,
logType: LOG_TYPE.ACTION_EXECUTION_ERROR,
environmentName: currentEnvDetails.name,
text: `Failed execution in ${payload.duration}(ms)`,
source: {
type: ENTITY_TYPE.ACTION,
name: pluginActionNameToDisplay,
id: actionId,
httpMethod: actionObject?.actionConfiguration?.httpMethod,
pluginType: actionObject.pluginType,
},
state: {
error: error.message,
request: payload.request,
},
pluginErrorDetails: payload?.pluginErrorDetails,
},
},
]);
yield put({
type: ReduxActionErrorTypes.RUN_ACTION_ERROR,
payload: {
error: appsmithConsoleErrorMessageList[0].message,
id: reduxAction.payload.id,
show: false,
},
});
AnalyticsUtil.logEvent("EXECUTE_ACTION_FAILURE", {
actionId,
actionName: pluginActionNameToDisplay,
environmentId: currentEnvDetails.id,
environmentName: currentEnvDetails.name,
pageName: pageName,
apiType: "INTERNAL",
datasourceId: datasource?.id,
pluginName: plugin?.name,
isMock: !!datasource?.isMock,
actionConfig: actionAnalyticsPayload,
...payload?.pluginErrorDetails,
source: reduxAction.payload.actionExecutionContext,
});
return;
}
AnalyticsUtil.logEvent("EXECUTE_ACTION", {
actionId,
actionName: pluginActionNameToDisplay,
environmentId: currentEnvDetails.id,
environmentName: currentEnvDetails.name,
pageName: pageName,
responseTime: payload.duration,
apiType: "INTERNAL",
datasourceId: datasource?.id,
pluginName: plugin?.name,
isMock: !!datasource?.isMock,
actionConfig: actionAnalyticsPayload,
source: reduxAction.payload.actionExecutionContext,
runBehaviour: actionObject?.runBehaviour,
});
yield put({
type: ReduxActionTypes.RUN_ACTION_SUCCESS,
payload: { [actionId]: payload },
});
if (payload.isExecutionSuccess) {
AppsmithConsole.info({
logType: LOG_TYPE.ACTION_EXECUTION_SUCCESS,
text: `Successfully executed in ${payload.duration}(ms)`,
source: {
type: ENTITY_TYPE.ACTION,
name: pluginActionNameToDisplay,
id: actionId,
},
state: {
response: payload.body,
request: payload.request,
},
});
}
}
// This gets called for "onPageLoad" JS actions
function* executeOnPageLoadJSAction(pageAction: PageAction) {
const collectionId: string = pageAction.collectionId || "";
const pageId: string | undefined = yield select(getCurrentPageId);
if (!collectionId) return;
const collection: JSCollection = yield select(
getJSCollectionFromAllEntities,
collectionId,
);
if (!collection) {
appsmithTelemetry.captureException(
new Error(
"Collection present in layoutOnLoadActions but no collection exists ",
),
{
errorName: "MissingJSCollection",
extra: {
collectionId,
actionId: pageAction.id,
pageId,
},
},
);
return;
}
const jsAction = collection.actions.find(
(action: JSAction) => action.id === pageAction.id,
);
if (!!jsAction) {
if (jsAction.confirmBeforeExecute) {
const jsActionPathNameToDisplay = getJSActionPathNameToDisplay(
jsAction,
collection,
);
const modalPayload = {
name: jsActionPathNameToDisplay,
modalOpen: true,
modalType: ModalType.RUN_ACTION,
};
const confirmed: boolean = yield call(
requestModalConfirmationSaga,
modalPayload,
);
if (!confirmed) {
yield put({
type: ReduxActionTypes.RUN_ACTION_CANCELLED,
payload: { id: pageAction.id },
});
const jsActionPathNameToDisplay = getJSActionPathNameToDisplay(
jsAction,
collection,
);
toast.show(
createMessage(ACTION_EXECUTION_CANCELLED, jsActionPathNameToDisplay),
{
kind: "error",
},
);
// Don't proceed to executing the js function
return;
}
}
const data = {
action: jsAction,
collection,
isExecuteJSFunc: true,
onPageLoad: true,
};
yield call(handleExecuteJSFunctionSaga, data);
}
}
function* executePageLoadAction(
pageAction: PageAction,
span?: Span,
actionExecutionContext?: ActionExecutionContext,
) {
const currentEnvDetails: { id: string; name: string } = yield select(
getCurrentEnvironmentDetails,
);
if (pageAction.hasOwnProperty("collectionId")) {
yield call(executeOnPageLoadJSAction, pageAction);
} else {
const pageId: string | undefined = yield select(getCurrentPageId);
let currentApp: ApplicationPayload = yield select(getCurrentApplication);
currentApp = currentApp || {};
const appMode: APP_MODE | undefined = yield select(getAppMode);
// action is required to fetch the pluginId and pluginType.
const action = shouldBeDefined<Action>(
yield select(getAction, pageAction.id),
`action not found for id - ${pageAction.id}`,
);
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const datasourceId: string = (action?.datasource as any)?.id;
const datasource: Datasource = yield select(getDatasource, datasourceId);
const plugin: Plugin = yield select(getPlugin, action?.pluginId);
const isAnvilEnabled: boolean = yield select(
getIsAnvilEnabledInCurrentApplication,
);
AnalyticsUtil.logEvent("EXECUTE_ACTION", {
type: pageAction.pluginType,
name: pageAction.name,
pageId: pageId,
appMode: appMode,
appId: currentApp.id,
onPageLoad: true,
appName: currentApp.name,
environmentId: currentEnvDetails.id,
environmentName: currentEnvDetails.name,
isExampleApp: currentApp.appIsExample,
pluginName: plugin?.name,
datasourceId: datasourceId,
isMock: !!datasource?.isMock,
actionId: pageAction?.id,
inputParams: 0,
source: !!actionExecutionContext
? actionExecutionContext
: ActionExecutionContext.PAGE_LOAD,
runBehaviour: action?.runBehaviour,
});
const actionName = getPluginActionNameToDisplay(
pageAction as unknown as Action,
);
let payload = EMPTY_RESPONSE;
let isError = true;
let error = {
name: "PluginExecutionError",
message: createMessage(ACTION_EXECUTION_FAILED, actionName),
};
try {
const executePluginActionResponse: ExecutePluginActionResponse =
yield call(
executePluginActionSaga,
action,
undefined,
undefined,
undefined,
span,
);
payload = executePluginActionResponse.payload;
isError = executePluginActionResponse.isError;
} catch (e) {
log.error(e);
if (e instanceof UserCancelledActionExecutionError) {
error = {
name: "PluginExecutionError",
message: createMessage(ACTION_EXECUTION_CANCELLED, actionName),
};
}
}
// open response tab in debugger on exection of action on page load.
// Only if current page is the page on which the action is executed.
if (
window.location.pathname.includes(pageAction.id) &&
!(isAnvilEnabled && pageAction.pluginType === PluginType.AI)
)
yield put(
setPluginActionEditorDebuggerState({
open: true,
selectedTab: DEBUGGER_TAB_KEYS.RESPONSE_TAB,
}),
);
if (isError) {
AppsmithConsole.addErrors([
{
payload: {
id: pageAction.id,
iconId: action.pluginId,
logType: LOG_TYPE.ACTION_EXECUTION_ERROR,
environmentName: currentEnvDetails.name,
text: `Failed execution in ${payload.duration}(ms)`,
source: {
type: ENTITY_TYPE.ACTION,
name: actionName,
id: pageAction.id,
httpMethod: action?.actionConfiguration?.httpMethod,
pluginType: action.pluginType,
},
state: {
error:
payload.pluginErrorDetails?.downstreamErrorMessage ||
error.message,
request: payload.request,
},
pluginErrorDetails: payload.pluginErrorDetails,
},
},
]);
yield put(
executePluginActionError({
actionId: pageAction.id,
isPageLoad: true,
error: { message: error.message },
data: payload,
}),
);
AnalyticsUtil.logEvent("EXECUTE_ACTION_FAILURE", {
type: pageAction.pluginType,
name: actionName,
pageId: pageId,
appMode: appMode,
appId: currentApp.id,
onPageLoad: true,
appName: currentApp.name,
environmentId: currentEnvDetails.id,
environmentName: currentEnvDetails.name,
isExampleApp: currentApp.appIsExample,
pluginName: plugin?.name,
datasourceId: datasourceId,
isMock: !!datasource?.isMock,
actionId: pageAction?.id,
inputParams: 0,
...payload.pluginErrorDetails,
source: !!actionExecutionContext
? actionExecutionContext
: ActionExecutionContext.PAGE_LOAD,
});
} else {
AnalyticsUtil.logEvent("EXECUTE_ACTION_SUCCESS", {
type: pageAction.pluginType,
name: actionName,
pageId: pageId,
appMode: appMode,
appId: currentApp.id,
onPageLoad: true,
appName: currentApp.name,
environmentId: currentEnvDetails.id,
environmentName: currentEnvDetails.name,
isExampleApp: currentApp.appIsExample,
pluginName: plugin?.name,
datasourceId: datasourceId,
isMock: !!datasource?.isMock,
actionId: pageAction?.id,
inputParams: 0,
source: !!actionExecutionContext
? actionExecutionContext
: ActionExecutionContext.PAGE_LOAD,
});
yield take(ReduxActionTypes.SET_EVALUATED_TREE);
}
}
}
function* executePageLoadActionsSaga(
actionPayload: ReduxAction<{
actionExecutionContext?: ActionExecutionContext;
}>,
) {
const span = startRootSpan("executePageLoadActionsSaga");
try {
const pageActions: PageAction[][] = yield select(getLayoutOnLoadActions);
const layoutOnLoadActionErrors: LayoutOnLoadActionErrors[] = yield select(
getLayoutOnLoadIssues,
);
const actionCount = flatten(pageActions).length;
setAttributesToSpan(span, { numActions: actionCount });
// when cyclical depedency issue is there,
// none of the page load actions would be executed
for (const actionSet of pageActions) {
// Load all sets in parallel
// @ts-expect-error: no idea how to type this
yield* yield all(
actionSet.map((apiAction) =>
call(
executePageLoadAction,
apiAction,
span,
actionPayload.payload.actionExecutionContext,
),
),
);
}
yield put({
type: ReduxActionTypes.SET_ONLOAD_ACTION_EXECUTED,
payload: true,
});
// We show errors in the debugger once onPageLoad actions
// are executed
yield put(hideDebuggerErrors(false));
checkAndLogErrorsIfCyclicDependency(layoutOnLoadActionErrors);
} catch (e) {
log.error(e);
AppsmithConsole.error({
text: createMessage(ERROR_FAIL_ON_PAGE_LOAD_ACTIONS),
});
}
endSpan(span);
}
interface 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(
pluginAction: Action,
paginationField?: PaginationField,
params?: Record<string, unknown>,
isUserInitiated?: boolean,
parentSpan?: Span,
) {
const actionId = pluginAction.id;
const baseActionId = pluginAction.baseId;
const pluginActionNameToDisplay = getPluginActionNameToDisplay(pluginAction);
setAttributesToSpan(parentSpan, {
actionId,
pluginName: pluginActionNameToDisplay,
});
if (pluginAction.confirmBeforeExecute) {
const modalPayload = {
name: pluginActionNameToDisplay,
modalOpen: true,
modalType: ModalType.RUN_ACTION,
};
const confirmed: unknown = yield call(
requestModalConfirmationSaga,
modalPayload,
);
if (!confirmed) {
yield put({
type: ReduxActionTypes.RUN_ACTION_CANCELLED,
payload: { id: actionId },
});
throw new UserCancelledActionExecutionError();
}
}
yield put(executePluginActionRequest({ id: actionId }));
const appMode: APP_MODE | undefined = yield select(getAppMode);
const timeout: number | undefined = yield select(getActionTimeout, actionId);
const executeActionRequest: ExecuteActionRequest = {
actionId: actionId,
viewMode: appMode === APP_MODE.PUBLISHED,
paramProperties: {},
analyticsProperties: {
isUserInitiated: !!isUserInitiated,
},
};
if (paginationField) {
executeActionRequest.paginationField = paginationField;
}
const formData = new FormData();
// Initialising instrumentation object, will only be populated in case
// files are being uplaoded
const filePickerInstrumentation: FilePickerInstumentationObject = {
numberOfFiles: 0,
totalSize: 0,
fileTypes: [],
fileSizes: [],
};
const evaluatedBindings: Record<string, unknown> = yield call(
evaluateActionParams,
pluginAction.jsonPathKeys,
formData,
executeActionRequest,
filePickerInstrumentation,
params,
);
AppsmithConsole.info({
text: "Began execution",
source: {
type: ENTITY_TYPE.ACTION,
name: pluginAction.name,
id: actionId,
},
state: { requestParams: { ...params, ...evaluatedBindings } },
});
let payload = EMPTY_RESPONSE;
let response: ActionExecutionResponse;
try {
response = yield ActionAPI.executeAction(formData, timeout);
const isError = isErrorResponse(response);
yield validateResponse(response);
payload = createActionExecutionResponse(response);
yield put(
executePluginActionSuccess({
id: actionId,
baseId: baseActionId,
response: payload,
isActionCreatedInApp: getIsActionCreatedInApp(pluginAction),
}),
);
yield put(
updateActionData(
[
{
entityName: pluginAction.name,
dataPath: "data",
data: payload.body,
},
],
parentSpan,
),
);
// TODO: Plugins are not always fetched before on page load actions are executed.
try {
let plugin: Plugin | undefined;
if (!!pluginAction.pluginId) {
plugin = shouldBeDefined<Plugin>(
yield select(getPlugin, pluginAction.pluginId),
`Plugin not found for id - ${pluginAction.pluginId}`,
);
}
// sets the default display format for action response e.g Raw, Json or Table
yield setDefaultActionDisplayFormat(actionId, plugin, payload);
} catch (e) {
log.error("plugin no found", e);
}
if (filePickerInstrumentation.numberOfFiles > 0) {
triggerFileUploadInstrumentation(
filePickerInstrumentation,
isError ? "ERROR" : "SUCCESS",
response.data.statusCode,
pluginAction.name,
pluginAction.pluginType,
response.clientMeta.duration,
);
}
return {
payload,
isError,
};
} catch (e) {
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if ("clientDefinedError" in (e as any)) {
// Case: error from client side validation
if (filePickerInstrumentation.numberOfFiles > 0) {
triggerFileUploadInstrumentation(
filePickerInstrumentation,
"ERROR",
"400",
pluginAction.name,
pluginAction.pluginType,
"NA",
);
}
throw e;
}
yield put(
executePluginActionSuccess({
id: actionId,
baseId: baseActionId,
response: EMPTY_RESPONSE,
isActionCreatedInApp: getIsActionCreatedInApp(pluginAction),
}),
);
yield put(
updateActionData(
[
{
entityName: pluginAction.name,
dataPath: "data",
data: EMPTY_RESPONSE.body,
},
],
parentSpan,
),
);
if (e instanceof UserCancelledActionExecutionError) {
// Case: user cancelled the request of file upload
if (filePickerInstrumentation.numberOfFiles > 0) {
triggerFileUploadInstrumentation(
filePickerInstrumentation,
"CANCELLED",
"499",
pluginAction.name,
pluginAction.pluginType,
"NA",
);
}
throw new UserCancelledActionExecutionError();
}
// In case there is no response from server and files are being uploaded
// we report it as INVALID_RESPONSE. The server didn't send any code or the
// request was cancelled due to timeout
if (filePickerInstrumentation.numberOfFiles > 0) {
triggerFileUploadInstrumentation(
filePickerInstrumentation,
"INVALID_RESPONSE",
"444",
pluginAction.name,
pluginAction.pluginType,
"NA",
);
}
throw new PluginActionExecutionError("Response not valid", false);
}
}
// Function to send the file upload event to segment
function triggerFileUploadInstrumentation(
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
filePickerInfo: Record<string, any>,
status: string,
statusCode: string,
pluginName: string,
pluginType: string,
timeTaken: string,
) {
const { fileSizes, fileTypes, numberOfFiles, totalSize } = filePickerInfo;
AnalyticsUtil.logEvent("FILE_UPLOAD_COMPLETE", {
totalSize,
fileSizes,
numberOfFiles,
fileTypes,
status,
statusCode,
pluginName,
pluginType,
timeTaken,
});
}
// Function to clear the action responses for the actions which are not runBehaviour: ON_PAGE_LOAD.
function* clearTriggerActionResponse() {
const currentPageActions: ActionData[] = yield select(getCurrentActions);
for (const action of currentPageActions) {
// Clear the action response if the action has data and is not runBehaviour: ON_PAGE_LOAD.
if (
action.data &&
action.config.runBehaviour !== ActionRunBehaviour.ON_PAGE_LOAD
) {
yield put(clearActionResponse(action.config.id));
yield put(
updateActionData([
{
entityName: action.config.name,
dataPath: "data",
data: undefined,
},
]),
);
}
}
}
// Function to soft refresh the all the actions on the page.
function* softRefreshActionsSaga() {
//get current pageId
const pageId: string = yield select(getCurrentPageId);
const applicationId: string = yield select(getCurrentApplicationId);
// Fetch the page data before refreshing the actions.
yield put(fetchPageAction(pageId));
//wait for the page to be fetched.
yield take([
ReduxActionErrorTypes.FETCH_PAGE_ERROR,
ReduxActionTypes.FETCH_PAGE_SUCCESS,
]);
// Clear appsmith store
yield call(handleStoreOperations, [
{
payload: null,
type: "CLEAR_STORE",
},
]);
// Clear all the action responses on the page
yield call(clearTriggerActionResponse);
//Rerun all the page load actions on the page
yield put(
executePageLoadActions(
ActionExecutionContext.REFRESH_ACTIONS_ON_ENV_CHANGE,
),
);
try {
// we fork to prevent the call from blocking
yield put(softRefreshDatasourceStructure());
} catch (error) {}
//This will refresh the query editor with the latest datasource structure.
// TODO: fix typing of matchQueryBuilderPath, it always returns "any" which can lead to bugs
const isQueryPane = matchQueryBuilderPath(window.location.pathname);
//This is reuired only when the query editor is open.
if (isQueryPane) {
const basePageId: string = yield select(getCurrentBasePageId);
yield put(
changeQuery({
baseQueryId: isQueryPane.params.baseQueryId,
basePageId,
applicationId,
}),
);
}
const currentEnvName: string = yield select(getCurrentEnvironmentName);
toast.show(createMessage(SWITCH_ENVIRONMENT_SUCCESS, currentEnvName), {
kind: "success",
});
yield put({ type: ReduxActionTypes.SWITCH_ENVIRONMENT_SUCCESS });
}
// This gets called for "onPageUnload" JS actions
function* executeOnPageUnloadJSAction(pageAction: Action) {
const collectionId: string = pageAction.collectionId || "";
const pageId: string | undefined = yield select(getCurrentPageId);
if (!collectionId) return;
const collection: JSCollection = yield select(
getJSCollectionFromAllEntities,
collectionId,
);
if (!collection) {
appsmithTelemetry.captureException(
new Error(
"Collection present in layoutOnUnloadActions but no collection exists ",
),
{
errorName: "MissingJSCollection",
extra: {
collectionId,
actionId: pageAction.id,
pageId,
},
},
);
return;
}
const jsAction = collection.actions.find(
(action: JSAction) => action.id === pageAction.id,
);
if (!!jsAction) {
yield call(handleExecuteJSFunctionSaga, {
action: jsAction,
collection,
isExecuteJSFunc: true,
onPageLoad: false,
});
}
}
export function* executePageUnloadActionsSaga() {
const span = startRootSpan("executePageUnloadActionsSaga");
try {
const pageOnUnloadActions: Action[] = yield select(
getLayoutOnUnloadActions,
);
const actionCount = pageOnUnloadActions.length;
setAttributesToSpan(span, { numActions: actionCount });
// Execute unload actions in parallel batches
yield all(
pageOnUnloadActions.map((action) =>
call(executeOnPageUnloadJSAction, action),
),
);
// Publish success event after all actions are executed
yield put({
type: ReduxActionTypes.EXECUTE_PAGE_UNLOAD_ACTIONS_SUCCESS,
});
} catch (e) {
log.error(e);
AppsmithConsole.error({
text: "Failed to execute actions during page unload",
});
// Publish error event if something goes wrong
yield put({
type: ReduxActionTypes.EXECUTE_PAGE_UNLOAD_ACTIONS_ERROR,
});
}
endSpan(span);
}
// End of Selection
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,
),
takeLatest(ReduxActionTypes.PLUGIN_SOFT_REFRESH, softRefreshActionsSaga),
takeLatest(
ReduxActionTypes.EXECUTE_PAGE_UNLOAD_ACTIONS,
executePageUnloadActionsSaga,
),
]);
}