From 59f2688575b9848e5dbdc61629667c68a06f4e4a Mon Sep 17 00:00:00 2001 From: Aman Agarwal Date: Mon, 24 Mar 2025 18:38:50 +0530 Subject: [PATCH] feat: added custom action config control type for paragon integrations (#39764) --- app/client/src/ce/constants/messages.ts | 9 ++ .../NoSearchCommandFound.tsx | 69 +++++++++ .../CustomActionsConfigControl/index.tsx | 143 ++++++++++++++++++ .../formControls/DropDownControl.test.tsx | 73 +++++++++ .../formControls/DropDownControl.tsx | 9 ++ .../utils/formControl/FormControlRegistry.tsx | 10 ++ .../src/utils/formControl/formControlTypes.ts | 1 + 7 files changed, 314 insertions(+) create mode 100644 app/client/src/components/formControls/CustomActionsConfigControl/NoSearchCommandFound.tsx create mode 100644 app/client/src/components/formControls/CustomActionsConfigControl/index.tsx 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", };