diff --git a/app/client/src/PluginActionEditor/components/PluginActionResponse/components/Response/Response.test.tsx b/app/client/src/PluginActionEditor/components/PluginActionResponse/components/Response/Response.test.tsx
index 1c9ed20e28..2181237e22 100644
--- a/app/client/src/PluginActionEditor/components/PluginActionResponse/components/Response/Response.test.tsx
+++ b/app/client/src/PluginActionEditor/components/PluginActionResponse/components/Response/Response.test.tsx
@@ -28,6 +28,13 @@ const defaultProps = {
responseTabHeight: 200,
};
+// mock the postrunactionmap
+jest.mock("ee/components/PostActionRunComponents", () => ({
+ PostRunActionComponentMap: {
+ test_modal: () =>
,
+ },
+}));
+
const storeData = getIDETestState({});
describe("Response", () => {
@@ -36,6 +43,7 @@ describe("Response", () => {
beforeEach(() => {
store = mockStore(storeData);
+ jest.clearAllMocks();
});
/** Test use prepared statement warning **/
@@ -208,4 +216,152 @@ describe("Response", () => {
container.querySelector("[data-testid='t--prepared-statement-warning']"),
).toBeNull();
});
+
+ it("6. Should show post run action container when post run action exists", () => {
+ const postRunAction = {
+ type: "FORM",
+ name: "test_modal",
+ };
+ const actionResponse = {
+ isExecutionSuccess: true,
+ body: [{ key: "value" }],
+ postRunAction,
+ dataTypes: [{ dataType: "JSON" }],
+ responseDisplayFormat: "JSON",
+ } as unknown as ActionResponse;
+
+ store = mockStore({
+ ...storeData,
+ entities: {
+ ...storeData.entities,
+ actions: [
+ {
+ config: {
+ id: "test-action-id",
+ name: "Test Action",
+ },
+ isLoading: false,
+ data: actionResponse,
+ },
+ ],
+ },
+ });
+
+ const props = {
+ ...defaultProps,
+ actionResponse,
+ currentContentType: "JSON",
+ };
+
+ const { getByTestId } = render(
+
+
+
+
+
+
+ ,
+ );
+
+ // Check if post run action container is showing
+ expect(getByTestId("t--post-run-action-container")).not.toBeNull();
+ expect(getByTestId("t--post-run-action-test-modal-form")).not.toBeNull();
+ });
+
+ it("7. Should not show post run action container when post run action doesn't exist", () => {
+ const actionResponse = {
+ isExecutionSuccess: true,
+ body: [{ key: "value" }],
+ dataTypes: [{ dataType: "JSON" }],
+ responseDisplayFormat: "JSON",
+ } as unknown as ActionResponse;
+
+ store = mockStore({
+ ...storeData,
+ entities: {
+ ...storeData.entities,
+ actions: [
+ {
+ config: {
+ id: "test-action-id",
+ name: "Test Action",
+ },
+ isLoading: false,
+ data: actionResponse,
+ },
+ ],
+ },
+ });
+
+ const props = {
+ ...defaultProps,
+ actionResponse,
+ };
+
+ const { container } = render(
+
+
+
+
+
+
+ ,
+ );
+
+ // Check if post run action container is not showing
+ expect(
+ container.querySelector("[data-testid='t--post-run-action-container']"),
+ ).toBeNull();
+ });
+
+ it("8. Should not show post run action container when correct mapping is not found", () => {
+ const postRunAction = {
+ type: "FORM",
+ name: "invalid_modal",
+ };
+ const actionResponse = {
+ isExecutionSuccess: true,
+ body: [{ key: "value" }],
+ postRunAction,
+ dataTypes: [{ dataType: "JSON" }],
+ responseDisplayFormat: "JSON",
+ } as unknown as ActionResponse;
+
+ store = mockStore({
+ ...storeData,
+ entities: {
+ ...storeData.entities,
+ actions: [
+ {
+ config: {
+ id: "test-action-id",
+ name: "Test Action",
+ },
+ isLoading: false,
+ data: actionResponse,
+ },
+ ],
+ },
+ });
+
+ const props = {
+ ...defaultProps,
+ actionResponse,
+ };
+
+ const { container } = render(
+
+
+
+
+
+
+ ,
+ );
+
+ // Check if post run action container is not showing
+ expect(
+ container.querySelector("[data-testid='t--post-run-action-container']"),
+ ).toBeNull();
+ });
});
diff --git a/app/client/src/PluginActionEditor/components/PluginActionResponse/components/Response/Response.tsx b/app/client/src/PluginActionEditor/components/PluginActionResponse/components/Response/Response.tsx
index c14056cf2b..0ff01ee580 100644
--- a/app/client/src/PluginActionEditor/components/PluginActionResponse/components/Response/Response.tsx
+++ b/app/client/src/PluginActionEditor/components/PluginActionResponse/components/Response/Response.tsx
@@ -32,6 +32,8 @@ import { RESPONSE_TABLE_HEIGHT_OFFSET } from "./constants";
import * as Styled from "./styles";
import { checkForPreparedStatement, parseActionResponse } from "./utils";
import ActionExecutionInProgressView from "./components/ActionExecutionInProgressView";
+import { checkForPostRunAction } from "./utils/postRunActionsUtil";
+import PostActionRunContainer from "./components/PostActionRunContainer";
interface ResponseProps {
action: Action;
@@ -126,6 +128,9 @@ export function Response(props: ResponseProps) {
checkForPreparedStatement(action) && errorMessage,
);
+ const showPostRunAction =
+ actionResponse && checkForPostRunAction(actionResponse?.postRunAction);
+
const actionSource: SourceEntity = useMemo(
() => ({
type: ENTITY_TYPE.ACTION,
@@ -267,6 +272,11 @@ export function Response(props: ResponseProps) {
}
/>
+ {showPostRunAction && (
+
+ )}
+
+
+ );
+}
diff --git a/app/client/src/PluginActionEditor/components/PluginActionResponse/components/Response/utils/postRunActionsUtil.test.ts b/app/client/src/PluginActionEditor/components/PluginActionResponse/components/Response/utils/postRunActionsUtil.test.ts
new file mode 100644
index 0000000000..92c51c1f98
--- /dev/null
+++ b/app/client/src/PluginActionEditor/components/PluginActionResponse/components/Response/utils/postRunActionsUtil.test.ts
@@ -0,0 +1,54 @@
+import {
+ checkForPostRunAction,
+ getPostRunActionName,
+} from "./postRunActionsUtil";
+import type { PostActionRunConfig } from "api/types";
+
+describe("checkForPostRunAction", () => {
+ it("should return true for valid post run action", () => {
+ const validAction: PostActionRunConfig = {
+ type: "FORM",
+ name: "some_name",
+ };
+
+ expect(checkForPostRunAction(validAction)).toBe(true);
+ });
+
+ it("should return false for undefined input", () => {
+ expect(checkForPostRunAction(undefined)).toBe(false);
+ });
+
+ it("should return false for input without type property", () => {
+ const invalidAction = {
+ name: "some_name",
+ };
+
+ expect(checkForPostRunAction(invalidAction as PostActionRunConfig)).toBe(
+ false,
+ );
+ });
+});
+
+describe("getPostRunActionName", () => {
+ it("should return name for valid post run action", () => {
+ const validAction: PostActionRunConfig = {
+ type: "FORM",
+ name: "test_action",
+ };
+
+ expect(getPostRunActionName(validAction)).toBe("test_action");
+ });
+
+ it("should return empty string for undefined input", () => {
+ expect(getPostRunActionName(undefined)).toBe("");
+ });
+
+ it("should return empty string for action without name", () => {
+ const actionWithoutName: PostActionRunConfig = {
+ type: "FORM",
+ name: "",
+ };
+
+ expect(getPostRunActionName(actionWithoutName)).toBe("");
+ });
+});
diff --git a/app/client/src/PluginActionEditor/components/PluginActionResponse/components/Response/utils/postRunActionsUtil.ts b/app/client/src/PluginActionEditor/components/PluginActionResponse/components/Response/utils/postRunActionsUtil.ts
new file mode 100644
index 0000000000..e815a1c405
--- /dev/null
+++ b/app/client/src/PluginActionEditor/components/PluginActionResponse/components/Response/utils/postRunActionsUtil.ts
@@ -0,0 +1,27 @@
+import type { PostActionRunConfig } from "api/types";
+
+export function checkForPostRunAction(postRunAction?: PostActionRunConfig) {
+ if (
+ postRunAction &&
+ typeof postRunAction === "object" &&
+ "type" in postRunAction
+ ) {
+ return true;
+ }
+
+ return false;
+}
+
+export function getPostRunActionName(postRunAction?: PostActionRunConfig) {
+ if (!postRunAction) {
+ return "";
+ }
+
+ const { name } = postRunAction;
+
+ if (!name) {
+ return "";
+ }
+
+ return name;
+}
diff --git a/app/client/src/api/ActionAPI.tsx b/app/client/src/api/ActionAPI.tsx
index d0957fb733..1cd3c0f14d 100644
--- a/app/client/src/api/ActionAPI.tsx
+++ b/app/client/src/api/ActionAPI.tsx
@@ -9,6 +9,7 @@ import type { Action, ActionViewMode } from "entities/Action";
import type { APIRequest } from "constants/AppsmithActionConstants/ActionConstants";
import type { WidgetType } from "constants/WidgetConstants";
import type { ActionParentEntityTypeInterface } from "ee/entities/Engine/actionHelpers";
+import type { PostActionRunConfig } from "./types";
export interface Property {
key: string;
@@ -86,6 +87,7 @@ export interface ActionResponse {
readableError?: string;
responseDisplayFormat?: string;
pluginErrorDetails?: PluginErrorDetails;
+ postRunAction?: PostActionRunConfig;
}
//This contains the error details from the plugin that is sent to the client in the response
diff --git a/app/client/src/api/types.ts b/app/client/src/api/types.ts
index 4ce60997ef..88fdf76c06 100644
--- a/app/client/src/api/types.ts
+++ b/app/client/src/api/types.ts
@@ -22,3 +22,9 @@ export type AxiosResponseData = AxiosResponse>["data"];
export type ErrorHandler = (
error: AxiosError,
) => Promise;
+
+export interface PostActionRunConfig {
+ type: "FORM";
+ name: string;
+ config?: Record;
+}
diff --git a/app/client/src/ce/components/PostActionRunComponents/index.ts b/app/client/src/ce/components/PostActionRunComponents/index.ts
new file mode 100644
index 0000000000..a4776490e1
--- /dev/null
+++ b/app/client/src/ce/components/PostActionRunComponents/index.ts
@@ -0,0 +1,6 @@
+import type { PostRunActionNamesInterface } from "./types";
+
+export const PostRunActionComponentMap: Record<
+ PostRunActionNamesInterface,
+ React.ElementType
+> = {};
diff --git a/app/client/src/ce/components/PostActionRunComponents/types.ts b/app/client/src/ce/components/PostActionRunComponents/types.ts
new file mode 100644
index 0000000000..1ccc539068
--- /dev/null
+++ b/app/client/src/ce/components/PostActionRunComponents/types.ts
@@ -0,0 +1,4 @@
+export const PostRunActionNames = {} as const;
+
+export type PostRunActionNamesInterface =
+ (typeof PostRunActionNames)[keyof typeof PostRunActionNames];
diff --git a/app/client/src/ee/components/PostActionRunComponents/index.ts b/app/client/src/ee/components/PostActionRunComponents/index.ts
new file mode 100644
index 0000000000..d9b40cf421
--- /dev/null
+++ b/app/client/src/ee/components/PostActionRunComponents/index.ts
@@ -0,0 +1 @@
+export * from "ce/components/PostActionRunComponents";
diff --git a/app/client/src/ee/components/PostActionRunComponents/types.ts b/app/client/src/ee/components/PostActionRunComponents/types.ts
new file mode 100644
index 0000000000..73587152cc
--- /dev/null
+++ b/app/client/src/ee/components/PostActionRunComponents/types.ts
@@ -0,0 +1 @@
+export * from "ce/components/PostActionRunComponents/types";
diff --git a/app/client/src/sagas/ActionExecution/PluginActionSaga.ts b/app/client/src/sagas/ActionExecution/PluginActionSaga.ts
index 3096c7b1fe..c21366784f 100644
--- a/app/client/src/sagas/ActionExecution/PluginActionSaga.ts
+++ b/app/client/src/sagas/ActionExecution/PluginActionSaga.ts
@@ -81,10 +81,7 @@ import type {
LayoutOnLoadActionErrors,
PageAction,
} from "constants/AppsmithActionConstants/ActionConstants";
-import {
- EventType,
- RESP_HEADER_DATATYPE,
-} from "constants/AppsmithActionConstants/ActionConstants";
+import { EventType } from "constants/AppsmithActionConstants/ActionConstants";
import {
getCurrentApplicationId,
getCurrentBasePageId,
@@ -172,10 +169,7 @@ import {
selectGitConnectModalOpen,
selectGitOpsModalOpen,
} from "selectors/gitModSelectors";
-
-enum ActionResponseDataTypes {
- BINARY = "BINARY",
-}
+import { createActionExecutionResponse } from "./PluginActionSagaUtils";
interface FilePickerInstumentationObject {
numberOfFiles: number;
@@ -208,32 +202,6 @@ export const getActionTimeout = (
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;
};
diff --git a/app/client/src/sagas/ActionExecution/PluginActionSagaUtils.test.ts b/app/client/src/sagas/ActionExecution/PluginActionSagaUtils.test.ts
index 876bb97ef9..a86363f5a6 100644
--- a/app/client/src/sagas/ActionExecution/PluginActionSagaUtils.test.ts
+++ b/app/client/src/sagas/ActionExecution/PluginActionSagaUtils.test.ts
@@ -1,5 +1,11 @@
import { put } from "redux-saga/effects";
-import { setDefaultActionDisplayFormat } from "./PluginActionSagaUtils";
+import {
+ RESP_HEADER_DATATYPE,
+ setDefaultActionDisplayFormat,
+} from "./PluginActionSagaUtils";
+import { createActionExecutionResponse } from "./PluginActionSagaUtils";
+import { ActionResponseDataTypes } from "./PluginActionSagaUtils";
+import { HTTP_METHOD } from "PluginActionEditor/constants/CommonApiConstants";
const actionid = "test-id";
@@ -99,3 +105,138 @@ describe("PluginActionSagasUtils", () => {
expect(generator.next().value).toBeUndefined();
});
});
+
+describe("createActionExecutionResponse", () => {
+ it("should handle regular response without binary data", () => {
+ const response = {
+ data: {
+ body: { key: "value" },
+ statusCode: "200 OK",
+ headers: {},
+ isExecutionSuccess: true,
+ request: {
+ url: "https://example.com",
+ headers: {},
+ body: {},
+ httpMethod: HTTP_METHOD.GET,
+ },
+ dataTypes: [{ dataType: "JSON" }],
+ },
+ clientMeta: {
+ duration: "100",
+ size: "50",
+ },
+ responseMeta: {
+ status: 200,
+ success: true,
+ },
+ };
+
+ const result = createActionExecutionResponse(response);
+
+ expect(result).toEqual({
+ ...response.data,
+ ...response.clientMeta,
+ });
+ });
+
+ it("should decode base64 binary response", () => {
+ const rawData = "test binary data";
+ const base64String = btoa(rawData);
+ const response = {
+ data: {
+ body: base64String,
+ statusCode: "200 OK",
+ headers: {
+ [RESP_HEADER_DATATYPE]: [ActionResponseDataTypes.BINARY],
+ },
+ isExecutionSuccess: true,
+ request: {
+ url: "https://example.com",
+ headers: {},
+ body: {},
+ httpMethod: HTTP_METHOD.GET,
+ },
+ dataTypes: [{ dataType: "JSON" }],
+ },
+ clientMeta: {
+ duration: "100",
+ size: "50",
+ },
+ responseMeta: {
+ status: 200,
+ success: true,
+ },
+ };
+
+ const result = createActionExecutionResponse(response);
+
+ expect(result.body).toBe(rawData);
+ });
+
+ it("should not decode response if status code is not 200 OK", () => {
+ const base64String = btoa("test binary data");
+ const response = {
+ data: {
+ body: base64String,
+ statusCode: "404 Not Found",
+ headers: {
+ [RESP_HEADER_DATATYPE]: [ActionResponseDataTypes.BINARY],
+ },
+ isExecutionSuccess: true,
+ request: {
+ url: "https://example.com",
+ headers: {},
+ body: {},
+ httpMethod: HTTP_METHOD.GET,
+ },
+ dataTypes: [{ dataType: "JSON" }],
+ },
+ clientMeta: {
+ duration: "100",
+ size: "50",
+ },
+ responseMeta: {
+ status: 200,
+ success: true,
+ },
+ };
+
+ const result = createActionExecutionResponse(response);
+
+ expect(result.body).toBe(base64String);
+ });
+
+ it("should not decode response if header type is not BINARY", () => {
+ const base64String = btoa("test binary data");
+ const response = {
+ data: {
+ body: base64String,
+ statusCode: "200 OK",
+ headers: {
+ [RESP_HEADER_DATATYPE]: ["JSON"],
+ },
+ isExecutionSuccess: true,
+ request: {
+ url: "https://example.com",
+ headers: {},
+ body: {},
+ httpMethod: HTTP_METHOD.GET,
+ },
+ dataTypes: [{ dataType: "JSON" }],
+ },
+ clientMeta: {
+ duration: "100",
+ size: "50",
+ },
+ responseMeta: {
+ status: 200,
+ success: true,
+ },
+ };
+
+ const result = createActionExecutionResponse(response);
+
+ expect(result.body).toBe(base64String);
+ });
+});
diff --git a/app/client/src/sagas/ActionExecution/PluginActionSagaUtils.ts b/app/client/src/sagas/ActionExecution/PluginActionSagaUtils.ts
index f353104e5b..bdb5466dcb 100644
--- a/app/client/src/sagas/ActionExecution/PluginActionSagaUtils.ts
+++ b/app/client/src/sagas/ActionExecution/PluginActionSagaUtils.ts
@@ -1,8 +1,14 @@
import { put } from "redux-saga/effects";
import { setActionResponseDisplayFormat } from "actions/pluginActionActions";
-import type { ActionResponse } from "api/ActionAPI";
+import type { ActionExecutionResponse, ActionResponse } from "api/ActionAPI";
import type { Plugin } from "entities/Plugin";
+import { getType, Types } from "utils/TypeHelpers";
+export enum ActionResponseDataTypes {
+ BINARY = "BINARY",
+}
+
+export const RESP_HEADER_DATATYPE = "X-APPSMITH-DATATYPE";
export function* setDefaultActionDisplayFormat(
actionId: string,
plugin: Plugin | undefined,
@@ -24,3 +30,30 @@ export function* setDefaultActionDisplayFormat(
);
}
}
+
+export 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,
+ };
+};