diff --git a/app/client/src/ce/constants/messages.ts b/app/client/src/ce/constants/messages.ts
index a0100624b7..221b122e0a 100644
--- a/app/client/src/ce/constants/messages.ts
+++ b/app/client/src/ce/constants/messages.ts
@@ -2634,3 +2634,12 @@ export const TABLE_LOADING_RECORDS = () => "loading records";
export const TABLE_LOAD_MORE = () => "Load More";
export const UPCOMING_SAAS_INTEGRATIONS = () => "Upcoming SaaS Integrations";
+
+export const NO_SEARCH_COMMAND_FOUND_EXTERNAL_SAAS = () =>
+ "No actions match your search";
+
+export const ADD_CUSTOM_ACTION = () => "Add custom action";
+
+export const CONFIG_PROPERTY_COMMAND = () => "command";
+
+export const CUSTOM_ACTION_LABEL = () => "Custom Action";
diff --git a/app/client/src/components/formControls/CustomActionsConfigControl/NoSearchCommandFound.tsx b/app/client/src/components/formControls/CustomActionsConfigControl/NoSearchCommandFound.tsx
new file mode 100644
index 0000000000..f99f3c9821
--- /dev/null
+++ b/app/client/src/components/formControls/CustomActionsConfigControl/NoSearchCommandFound.tsx
@@ -0,0 +1,69 @@
+import React from "react";
+import {
+ ADD_CUSTOM_ACTION,
+ CONFIG_PROPERTY_COMMAND,
+ createMessage,
+ CUSTOM_ACTION_LABEL,
+ NO_SEARCH_COMMAND_FOUND_EXTERNAL_SAAS,
+ NOT_FOUND,
+} from "ee/constants/messages";
+import { Button, Flex, Text, type SelectOptionProps } from "@appsmith/ads";
+import { useSelector } from "react-redux";
+import { getPlugin } from "ee/selectors/entitiesSelector";
+import { PluginType, type Plugin } from "entities/Plugin";
+export default function NoSearchCommandFound({
+ configProperty,
+ onSelectOptions,
+ options,
+ pluginId,
+}: {
+ configProperty: string;
+ onSelectOptions: (optionValueToSelect: string) => void;
+ options: SelectOptionProps[];
+ pluginId?: string;
+}) {
+ const plugin: Plugin | undefined = useSelector((state) =>
+ getPlugin(state, pluginId || ""),
+ );
+
+ const isExternalSaasPluginCommandDropdown =
+ plugin?.type === PluginType.EXTERNAL_SAAS &&
+ configProperty.includes(createMessage(CONFIG_PROPERTY_COMMAND));
+
+ const customActionOption = options.find((option) =>
+ option.label
+ .toLowerCase()
+ .includes(createMessage(CUSTOM_ACTION_LABEL).toLowerCase()),
+ );
+
+ const onClick = () => {
+ onSelectOptions(customActionOption!.value);
+ document.dispatchEvent(new MouseEvent("mousedown", { bubbles: true }));
+ };
+
+ if (isExternalSaasPluginCommandDropdown && customActionOption) {
+ return (
+
+
+ {createMessage(NO_SEARCH_COMMAND_FOUND_EXTERNAL_SAAS)}
+
+
+
+ );
+ }
+
+ return <>{createMessage(NOT_FOUND)}>;
+}
diff --git a/app/client/src/components/formControls/CustomActionsConfigControl/index.tsx b/app/client/src/components/formControls/CustomActionsConfigControl/index.tsx
new file mode 100644
index 0000000000..09a408ed74
--- /dev/null
+++ b/app/client/src/components/formControls/CustomActionsConfigControl/index.tsx
@@ -0,0 +1,143 @@
+import React from "react";
+import type { ControlType } from "constants/PropertyControlConstants";
+import FormControl from "pages/Editor/FormControl";
+import { Grid, Tabs, TabPanel, TabsList, Tab, Flex } from "@appsmith/ads";
+import BaseControl, { type ControlProps } from "../BaseControl";
+import { HTTP_METHOD } from "PluginActionEditor/constants/CommonApiConstants";
+import { API_EDITOR_TAB_TITLES } from "ee/constants/messages";
+import { createMessage } from "ee/constants/messages";
+import styled from "styled-components";
+
+enum CUSTOM_ACTION_TABS {
+ HEADERS = "HEADERS",
+ PARAMS = "PARAMS",
+ BODY = "BODY",
+}
+
+const TabbedWrapper = styled(Tabs)`
+ .t--form-control-KEYVALUE_ARRAY {
+ & > div {
+ margin-bottom: var(--ads-v2-spaces-3);
+ & > * {
+ flex-grow: 1;
+ }
+ & > *:first-child {
+ max-width: 184px;
+ }
+ & > *:nth-child(2) {
+ margin-left: var(--ads-v2-spaces-3);
+ }
+ & > .t--delete-field {
+ max-width: 34px;
+ }
+ }
+ & .t--add-field {
+ height: 24px;
+ }
+ }
+`;
+
+const TabbedControls = (props: ControlProps) => {
+ return (
+
+
+ {Object.values(CUSTOM_ACTION_TABS).map((tab) => (
+
+ {createMessage(API_EDITOR_TAB_TITLES[tab])}
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+/**
+ * This component is used to configure the custom actions for the external integration.
+ * It allows the user to add or update details for the custom action like method type, path, headers, params, body.
+ */
+export class CustomActionsControl extends BaseControl {
+ getControlType(): ControlType {
+ return "CUSTOM_ACTIONS_CONFIG_FORM";
+ }
+ render() {
+ const { props } = this;
+
+ return (
+
+
+ ({
+ label: method,
+ value: method,
+ })),
+ }}
+ formName={props.formName}
+ />
+
+
+
+
+ );
+ }
+}
diff --git a/app/client/src/components/formControls/DropDownControl.test.tsx b/app/client/src/components/formControls/DropDownControl.test.tsx
index c523719cea..fce08eefa8 100644
--- a/app/client/src/components/formControls/DropDownControl.test.tsx
+++ b/app/client/src/components/formControls/DropDownControl.test.tsx
@@ -7,6 +7,7 @@ import { Provider } from "react-redux";
import configureStore from "redux-mock-store";
import type { SelectOptionProps } from "@appsmith/ads";
import type { ReduxAction } from "actions/ReduxActionTypes";
+import { PluginType } from "entities/Plugin";
const mockStore = configureStore([]);
@@ -57,6 +58,8 @@ const dropDownProps = {
maxTagCount: 3,
};
+const EXTERNAL_SAAS_PLUGIN_ID = "external-saas-pluginid";
+
describe("DropDownControl", () => {
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -524,6 +527,7 @@ describe("DropdownControl Single select tests", () => {
actionConfiguration: {
testPath: "option1",
},
+ pluginId: EXTERNAL_SAAS_PLUGIN_ID,
};
const mockActionSingleSelect = {
@@ -564,6 +568,16 @@ describe("DropdownControl Single select tests", () => {
values: initialValuesSingleSelect,
},
},
+ entities: {
+ plugins: {
+ list: [
+ {
+ id: EXTERNAL_SAAS_PLUGIN_ID,
+ type: PluginType.EXTERNAL_SAAS,
+ },
+ ],
+ },
+ },
appState: {},
});
});
@@ -618,4 +632,63 @@ describe("DropdownControl Single select tests", () => {
// The visible option should be "Option 1"
expect(visibleOptions[0]).toHaveTextContent("Option 2");
});
+ it("should have no command found ui for custom action for external saas plugin", async () => {
+ const externalSaasDropdownprops = {
+ ...dropDownPropsSingleSelect,
+ configProperty: "actionConfiguration.command",
+ options: [
+ ...mockOptions,
+ {
+ label: "Custom Action",
+ value: "custom action",
+ children: "Custom Action",
+ },
+ ],
+ formName: "TestForm",
+ };
+
+ render(
+
+
+
+
+ ,
+ );
+
+ // Find and click the dropdown to open it
+ const dropdownSelect = await screen.findByTestId(
+ "t--dropdown-actionConfiguration.command",
+ );
+
+ fireEvent.mouseDown(dropdownSelect.querySelector(".rc-select-selector")!);
+
+ // Find the search input and type "Option 2"
+ const searchInput = screen.getByPlaceholderText("Type to search...");
+
+ fireEvent.change(searchInput, { target: { value: "Test" } });
+
+ expect(screen.getByTestId("t--select-custom--action")).toBeInTheDocument();
+ });
+ it("should have not found ui dropdown with no command property and no custom action present", async () => {
+ render(
+
+
+
+
+ ,
+ );
+
+ // Find and click the dropdown to open it
+ const dropdownSelect = await screen.findByTestId(
+ "t--dropdown-actionConfiguration.testPath",
+ );
+
+ fireEvent.mouseDown(dropdownSelect.querySelector(".rc-select-selector")!);
+
+ // Find the search input and type "Option 2"
+ const searchInput = screen.getByPlaceholderText("Type to search...");
+
+ fireEvent.change(searchInput, { target: { value: "Test" } });
+ expect(screen.getByText("Not found")).toBeInTheDocument();
+ });
});
diff --git a/app/client/src/components/formControls/DropDownControl.tsx b/app/client/src/components/formControls/DropDownControl.tsx
index 5aa7fa4492..2296a19db7 100644
--- a/app/client/src/components/formControls/DropDownControl.tsx
+++ b/app/client/src/components/formControls/DropDownControl.tsx
@@ -28,6 +28,7 @@ import type {
ConditionalOutput,
DynamicValues,
} from "reducers/evaluationReducers/formEvaluationReducer";
+import NoSearchCommandFound from "./CustomActionsConfigControl/NoSearchCommandFound";
export interface DropDownGroupedOptions {
label: string;
@@ -462,6 +463,14 @@ function renderDropdown(
isLoading={props.isLoading}
isMultiSelect={isMultiSelect}
maxTagCount={props.maxTagCount}
+ notFoundContent={
+
+ }
onClear={clearAllOptions}
onDeselect={onRemoveOptions}
onPopupScroll={handlePopupScroll}
diff --git a/app/client/src/utils/formControl/FormControlRegistry.tsx b/app/client/src/utils/formControl/FormControlRegistry.tsx
index 51307c97b8..ed1f892f41 100644
--- a/app/client/src/utils/formControl/FormControlRegistry.tsx
+++ b/app/client/src/utils/formControl/FormControlRegistry.tsx
@@ -51,6 +51,7 @@ import {
DatasourceLinkControl,
type DatasourceLinkControlProps,
} from "components/formControls/DatasourceLinkControl";
+import { CustomActionsControl } from "components/formControls/CustomActionsConfigControl";
/**
* NOTE: If you are adding a component that uses FormControl
@@ -254,6 +255,15 @@ class FormControlRegistry {
},
},
);
+
+ FormControlFactory.registerControlBuilder(
+ formControlTypes.CUSTOM_ACTIONS_CONFIG_FORM,
+ {
+ buildPropertyControl(controlProps): JSX.Element {
+ return ;
+ },
+ },
+ );
}
}
diff --git a/app/client/src/utils/formControl/formControlTypes.ts b/app/client/src/utils/formControl/formControlTypes.ts
index ba8143bc89..777bfacd78 100644
--- a/app/client/src/utils/formControl/formControlTypes.ts
+++ b/app/client/src/utils/formControl/formControlTypes.ts
@@ -25,4 +25,5 @@ export default {
HYBRID_SEARCH: "HYBRID_SEARCH",
FUNCTION_CALLING_CONFIG_FORM: "FUNCTION_CALLING_CONFIG_FORM",
DATASOURCE_LINK: "DATASOURCE_LINK",
+ CUSTOM_ACTIONS_CONFIG_FORM: "CUSTOM_ACTIONS_CONFIG_FORM",
};