feat: added custom action config control type for paragon integrations (#39764)
This commit is contained in:
parent
f27afff281
commit
59f2688575
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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)}</>;
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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} />;
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user