feat: added custom action config control type for paragon integrations (#39764)

This commit is contained in:
Aman Agarwal 2025-03-24 18:38:50 +05:30 committed by GitHub
parent f27afff281
commit 59f2688575
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 314 additions and 0 deletions

View File

@ -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";

View File

@ -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 (
<Flex
alignItems="center"
flexDirection={"column"}
gap="spaces-5"
padding="spaces-7"
>
<Text color="var(--ads-v2-color-gray-500)">
{createMessage(NO_SEARCH_COMMAND_FOUND_EXTERNAL_SAAS)}
</Text>
<Button
data-testid="t--select-custom--action"
kind="secondary"
onClick={onClick}
size="sm"
startIcon="plus"
>
{createMessage(ADD_CUSTOM_ACTION)}
</Button>
</Flex>
);
}
return <>{createMessage(NOT_FOUND)}</>;
}

View File

@ -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 (
<TabbedWrapper defaultValue={CUSTOM_ACTION_TABS.HEADERS}>
<TabsList>
{Object.values(CUSTOM_ACTION_TABS).map((tab) => (
<Tab data-testid={`t--api-editor-${tab}`} key={tab} value={tab}>
{createMessage(API_EDITOR_TAB_TITLES[tab])}
</Tab>
))}
</TabsList>
<TabPanel value={CUSTOM_ACTION_TABS.HEADERS}>
<FormControl
config={{
controlType: "KEYVALUE_ARRAY",
configProperty: `${props.configProperty}.headers`,
formName: props.formName,
id: `${props.configProperty}.headers`,
isValid: true,
// @ts-expect-error FormControl component has incomplete TypeScript definitions for some valid properties
showHeader: true,
}}
formName={props.formName}
/>
</TabPanel>
<TabPanel value={CUSTOM_ACTION_TABS.PARAMS}>
<FormControl
config={{
controlType: "KEYVALUE_ARRAY",
configProperty: `${props.configProperty}.params`,
formName: props.formName,
id: `${props.configProperty}.params`,
// @ts-expect-error FormControl component has incomplete TypeScript definitions for some valid properties
showHeader: true,
isValid: true,
}}
formName={props.formName}
/>
</TabPanel>
<TabPanel value={CUSTOM_ACTION_TABS.BODY}>
<FormControl
config={{
controlType: "QUERY_DYNAMIC_TEXT",
configProperty: `${props.configProperty}.body`,
formName: props.formName,
id: `${props.configProperty}.body`,
label: "",
isValid: true,
}}
formName={props.formName}
/>
</TabPanel>
</TabbedWrapper>
);
};
/**
* 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<ControlProps> {
getControlType(): ControlType {
return "CUSTOM_ACTIONS_CONFIG_FORM";
}
render() {
const { props } = this;
return (
<Flex flexDirection="column" gap="spaces-4">
<Grid gap="spaces-4" gridTemplateColumns="100px 1fr">
<FormControl
config={{
controlType: "DROP_DOWN",
configProperty: `${props.configProperty}.method`,
formName: props.formName,
id: `${props.configProperty}.method`,
label: "",
isValid: true,
// @ts-expect-error FormControl component has incomplete TypeScript definitions for some valid properties
options: Object.values(HTTP_METHOD).map((method) => ({
label: method,
value: method,
})),
}}
formName={props.formName}
/>
<FormControl
config={{
controlType: "QUERY_DYNAMIC_INPUT_TEXT",
configProperty: `${props.configProperty}.path`,
formName: props.formName,
id: `${props.configProperty}.path`,
label: "",
isValid: true,
placeholderText: "/v1/users",
}}
formName={props.formName}
/>
</Grid>
<TabbedControls {...props} />
</Flex>
);
}
}

View File

@ -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(
<Provider store={store}>
<ReduxFormDecorator>
<DropDownControl {...externalSaasDropdownprops} />
</ReduxFormDecorator>
</Provider>,
);
// 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(
<Provider store={store}>
<ReduxFormDecorator>
<DropDownControl {...dropDownPropsSingleSelect} />
</ReduxFormDecorator>
</Provider>,
);
// 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();
});
});

View File

@ -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={
<NoSearchCommandFound
configProperty={props.configProperty}
onSelectOptions={onSelectOptions}
options={options}
pluginId={get(props.formValues, "pluginId")}
/>
}
onClear={clearAllOptions}
onDeselect={onRemoveOptions}
onPopupScroll={handlePopupScroll}

View File

@ -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 <CustomActionsControl {...controlProps} />;
},
},
);
}
}

View File

@ -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",
};