PromucFlow_constructor/app/client/src/entities/Action/index.ts
Rahul Barwal b4d5685d21
feat: integrates on page unload behavior with backend for deployed mode of the app (#41036)
## Description
**TLDR:** Adds support for executing page unload actions during
navigation in deployed mode, refactors related components, and improves
action handling.

<ins>Problem</ins>

Page unload actions were not triggered during navigation in deployed
mode, leading to incomplete workflows especially for cleanup.

<ins>Root cause</ins>

The application lacked integration for executing unload actions on page
transitions, and related components did not properly handle navigation
or action execution.

<ins>Solution</ins>

This PR handles the integration of page unload action execution during
navigation in deployed mode. It introduces selectors for unload actions,
refactors the MenuItem component for better navigation handling, and
improves the PluginActionSaga for executing plugin actions. Unused
parameters and functions are removed for clarity and maintainability.

Fixes #40997
_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.All"

### 🔍 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/16021075820>
> Commit: f09e3c44d379488e43aec6ab27228d7675f79415
> <a
href="https://internal.appsmith.com/app/cypress-dashboard/rundetails-65890b3c81d7400d08fa9ee5?branch=master&workflowId=16021075820&attempt=1"
target="_blank">Cypress dashboard</a>.
> Tags: `@tag.All`
> Spec:
> <hr>Wed, 02 Jul 2025 10:21:00 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**
* Added support for actions that execute automatically when navigating
away from a page.
* Introduced new navigation logic and hooks for consistent page
transitions within the app.
  * Added new menu and dropdown components for improved navigation UI.

* **Bug Fixes**
* Updated navigation item styling and active state detection for
improved accuracy.

* **Tests**
* Added comprehensive tests for navigation sagas and page unload
actions.
  * Added unit tests for navigation menu components.

* **Chores**
  * Refactored and centralized navigation logic for maintainability.
* Improved type safety and selector usage in navigation and action
execution.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-02 18:40:44 +05:30

320 lines
8.6 KiB
TypeScript

import type { EmbeddedRestDatasource } from "entities/Datasource";
import type { DynamicPath } from "utils/DynamicBindingUtils";
import _ from "lodash";
import type { LayoutOnLoadActionErrors } from "constants/AppsmithActionConstants/ActionConstants";
import type { EventLocation } from "ee/utils/analyticsUtilTypes";
import type { ActionParentEntityTypeInterface } from "ee/entities/Engine/actionHelpers";
import {
PluginPackageName,
PluginType,
type Plugin,
type PluginName,
} from "../Plugin";
import type { ActionRunBehaviourType } from "PluginActionEditor/types/PluginActionTypes";
export enum PaginationType {
NONE = "NONE",
PAGE_NO = "PAGE_NO",
URL = "URL",
CURSOR = "CURSOR",
}
// Used for analytic events
export enum ActionCreationSourceTypeEnum {
SELF = "SELF",
GENERATE_PAGE = "GENERATE_PAGE",
ONE_CLICK_BINDING = "ONE_CLICK_BINDING",
CLONE_PAGE = "CLONE_PAGE",
FORK_APPLICATION = "FORK_APPLICATION",
COPY_ACTION = "COPY_ACTION",
}
// Used for analytic events
export enum ActionExecutionContext {
SELF = "SELF",
ONE_CLICK_BINDING = "ONE_CLICK_BINDING",
GENERATE_CRUD_PAGE = "GENERATE_CRUD_PAGE",
CLONE_PAGE = "CLONE_PAGE",
FORK_TEMPLATE_PAGE = "FORK_TEMPLATE_PAGE",
PAGE_LOAD = "PAGE_LOAD",
EVALUATION_ACTION_TRIGGER = "EVALUATION_ACTION_TRIGGER",
REFRESH_ACTIONS_ON_ENV_CHANGE = "REFRESH_ACTIONS_ON_ENV_CHANGE",
PAGE_UNLOAD = "PAGE_UNLOAD",
}
export interface KeyValuePair {
key?: string;
value?: unknown;
}
export interface LimitOffset {
limit: Record<string, unknown>;
offset: Record<string, unknown>;
}
export interface SelfReferencingData {
limitBased?: LimitOffset;
curserBased?: {
previous?: LimitOffset;
next?: LimitOffset;
};
}
export interface ActionConfig {
timeoutInMillisecond?: number;
paginationType?: PaginationType;
formData?: Record<string, unknown>;
pluginSpecifiedTemplates?: KeyValuePair[];
path?: string;
queryParameters?: KeyValuePair[];
selfReferencingData?: SelfReferencingData;
}
export interface Property {
key: string;
value: string;
}
export interface BodyFormData {
editable: boolean;
mandatory: boolean;
description: string;
key: string;
value?: string;
type: string;
}
export interface AutoGeneratedHeader {
key: string;
value: string;
isInvalid: boolean;
}
export interface ApiActionConfig extends Omit<ActionConfig, "formData"> {
headers: Property[];
autoGeneratedHeaders?: AutoGeneratedHeader[];
httpMethod: string;
httpVersion: string;
path?: string;
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
body?: JSON | string | Record<string, any> | null;
encodeParamsToggle: boolean;
queryParameters?: Property[];
bodyFormData?: BodyFormData[];
formData: Record<string, unknown>;
query?: string | null;
variable?: string | null;
}
export interface QueryActionConfig extends ActionConfig {
body?: string;
}
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const isStoredDatasource = (val: any): val is StoredDatasource => {
if (!_.isObject(val)) return false;
if (!("id" in val)) return false;
return true;
};
export interface StoredDatasource {
name?: string;
id: string;
pluginId?: string;
datasourceConfiguration?: { url?: string };
}
export interface VisualizationElements {
css: string;
js: string;
html: string;
}
export interface BaseAction {
id: string;
baseId: string;
name: string;
workspaceId: string;
applicationId: string;
pageId: string;
collectionId?: string;
pluginId: string;
runBehaviour: ActionRunBehaviourType;
dynamicBindingPathList: DynamicPath[];
isValid: boolean;
invalids: string[];
jsonPathKeys: string[];
cacheResponse: string;
confirmBeforeExecute?: boolean;
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
eventData?: any;
messages: string[];
userPermissions?: string[];
errorReports?: Array<LayoutOnLoadActionErrors>;
isPublic?: boolean;
packageId?: string;
moduleId?: string;
moduleInstanceId?: string;
workflowId?: string;
contextType?: ActionParentEntityTypeInterface;
// This is used to identify the main js collection of a workflow
// added here to avoid ts error in entitiesSelector file, in practice
// will always be undefined for non js actions
isMainJSCollection?: boolean;
source?: ActionCreationSourceTypeEnum;
visualization?: {
result: VisualizationElements;
};
isDirtyMap?: {
SCHEMA_GENERATION: boolean;
};
}
interface BaseApiAction extends BaseAction {
pluginType: PluginType.API;
actionConfiguration: ApiActionConfig;
}
export interface SaaSAction extends BaseAction {
pluginType: PluginType.SAAS;
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
actionConfiguration: any;
datasource: StoredDatasource;
}
export interface RemoteAction extends BaseAction {
pluginType: PluginType.REMOTE;
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
actionConfiguration: any;
datasource: StoredDatasource;
}
export interface AIAction extends BaseAction {
pluginType: PluginType.AI;
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
actionConfiguration: any;
datasource: StoredDatasource;
}
export interface InternalAction extends BaseAction {
pluginType: PluginType.INTERNAL;
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
actionConfiguration: any;
datasource: StoredDatasource;
}
export interface EmbeddedApiAction extends BaseApiAction {
datasource: EmbeddedRestDatasource;
}
export interface StoredDatasourceApiAction extends BaseApiAction {
datasource: StoredDatasource;
}
export type ApiAction = EmbeddedApiAction | StoredDatasourceApiAction;
export interface QueryAction extends BaseAction {
pluginType: PluginType.DB;
pluginName?: PluginName;
actionConfiguration: QueryActionConfig;
datasource: StoredDatasource;
}
export interface ActionViewMode {
id: string;
baseId: string;
name: string;
pageId: string;
jsonPathKeys: string[];
confirmBeforeExecute?: boolean;
timeoutInMillisecond?: number;
}
export interface ExternalSaasAction extends BaseAction {
pluginType: PluginType.EXTERNAL_SAAS;
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
actionConfiguration: any;
datasource: StoredDatasource;
}
export type Action =
| ApiAction
| QueryAction
| SaaSAction
| RemoteAction
| AIAction
| InternalAction
| ExternalSaasAction;
export enum SlashCommand {
NEW_API,
NEW_QUERY,
NEW_INTEGRATION,
ASK_AI,
}
export interface SlashCommandPayload {
actionType: SlashCommand;
callback?: (binding: string) => void;
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
args: any;
}
export function isAPIAction(action: Action): action is ApiAction {
return action.pluginType === PluginType.API;
}
export function isQueryAction(action: Action): action is QueryAction {
return action.pluginType === PluginType.DB;
}
export function isSaaSAction(action: Action): action is SaaSAction {
return action.pluginType === PluginType.SAAS;
}
export function isAIAction(action: Action): action is AIAction {
return action.pluginType === PluginType.AI;
}
export function getGraphQLPlugin(plugins: Plugin[]): Plugin | undefined {
return plugins.find((p) => p.packageName === PluginPackageName.GRAPHQL);
}
export function getAppsmithAIPlugin(plugins: Plugin[]): Plugin | undefined {
return plugins.find((p) => p.packageName === PluginPackageName.APPSMITH_AI);
}
export function getAppsmithAgentPlugin(plugins: Plugin[]): Plugin | undefined {
return plugins.find(
(p) => p.packageName === PluginPackageName.APPSMITH_AGENT,
);
}
export function isGraphqlPlugin(plugin: Plugin | undefined) {
return plugin?.packageName === PluginPackageName.GRAPHQL;
}
export function isRESTAPIPlugin(plugin: Plugin | undefined) {
return plugin?.packageName === PluginPackageName.REST_API;
}
export const SCHEMA_SECTION_ID = "t--api-right-pane-schema";
export interface CreateApiActionDefaultsParams {
apiType: string;
from?: EventLocation;
newActionName?: string;
}
export interface CreateActionDefaultsParams {
datasourceId: string;
from?: EventLocation;
newActionName?: string;
queryDefaultTableName?: string;
}