Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions app/client/src/ce/constants/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Original file line number Diff line number Diff line change
@@ -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)}</>;
}
Original file line number Diff line number Diff line change
@@ -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";
Comment on lines +1 to +9
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Verify import path for API_EDITOR_TAB_TITLES

The import statement uses ee/constants/messages for API_EDITOR_TAB_TITLES, but the provided context shows this constant is defined in app/client/src/ce/constants/messages.ts. Ensure the import path is correct.


🏁 Script executed:

#!/bin/bash
# Check where API_EDITOR_TAB_TITLES is actually defined
rg -p "API_EDITOR_TAB_TITLES.*=.*{" --type ts

Length of output: 165


Below is a revised review comment after noting that the initial regex search failed. I've generated an updated shell script to re-check the definition (please run it to obtain additional details), and based on the inconclusive automated output, I'm flagging this for manual verification.


Action Required: Verify the Import Path for API_EDITOR_TAB_TITLES

The current code imports API_EDITOR_TAB_TITLES from ee/constants/messages. However, context suggests it may be defined in app/client/src/ce/constants/messages.ts. Please verify the actual definition location and update the import path if necessary.

To assist further, please run the script below:

  • Verify if API_EDITOR_TAB_TITLES is actually defined in app/client/src/ce/constants/messages.ts.
  • Update the import in app/client/src/components/formControls/CustomActionsConfigControl/index.tsx accordingly if needed.

🏁 Script executed:

#!/bin/bash
echo "Searching for API_EDITOR_TAB_TITLES definition across the repository:"
rg "API_EDITOR_TAB_TITLES\s*=" --type ts || true

echo ""
echo "Searching in app/client/src/ce/constants/messages.ts for API_EDITOR_TAB_TITLES:"
grep -R "API_EDITOR_TAB_TITLES" app/client/src/ce/constants/messages.ts || true

Length of output: 683


Action Required: Correct Import Path for API_EDITOR_TAB_TITLES

  • The constant is defined in app/client/src/ce/constants/messages.ts (confirmed via grep).
  • The current import in app/client/src/components/formControls/CustomActionsConfigControl/index.tsx uses "ee/constants/messages". Update this to reference the correct path.


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,
Comment on lines +59 to +60
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Address TypeScript errors properly

There are multiple @ts-expect-error comments indicating incomplete type definitions for FormControl. Consider creating proper type interfaces or contributing to the existing ones instead of suppressing these errors.

Also applies to: 72-74


🏁 Script executed:

#!/bin/bash
# Check FormControl type definitions to understand what's missing
fd FormControl --type file --exec grep -l "interface.*FormControlProps" {}

Length of output: 124


Update FormControl types to remove the need for error suppression

It looks like the FormControl component’s TypeScript definitions (defined in app/client/src/pages/Editor/FormControl.tsx) are missing the showHeader property. Please update the FormControlProps interface to include showHeader (and any other missing props), then remove the accompanying @ts-expect-error comments in:

  • app/client/src/components/formControls/CustomActionsConfigControl/index.tsx (lines 59–60)
  • app/client/src/components/formControls/CustomActionsConfigControl/index.tsx (lines 72–74)

This change will ensure TypeScript accurately validates the component without suppressing errors.

}}
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>
);
}
}
73 changes: 73 additions & 0 deletions app/client/src/components/formControls/DropDownControl.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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([]);

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -524,6 +527,7 @@ describe("DropdownControl Single select tests", () => {
actionConfiguration: {
testPath: "option1",
},
pluginId: EXTERNAL_SAAS_PLUGIN_ID,
};

const mockActionSingleSelect = {
Expand Down Expand Up @@ -564,6 +568,16 @@ describe("DropdownControl Single select tests", () => {
values: initialValuesSingleSelect,
},
},
entities: {
plugins: {
list: [
{
id: EXTERNAL_SAAS_PLUGIN_ID,
type: PluginType.EXTERNAL_SAAS,
},
],
},
},
appState: {},
});
});
Expand Down Expand Up @@ -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();
});
});
9 changes: 9 additions & 0 deletions app/client/src/components/formControls/DropDownControl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import type {
ConditionalOutput,
DynamicValues,
} from "reducers/evaluationReducers/formEvaluationReducer";
import NoSearchCommandFound from "./CustomActionsConfigControl/NoSearchCommandFound";

export interface DropDownGroupedOptions {
label: string;
Expand Down Expand Up @@ -462,6 +463,14 @@ function renderDropdown(
isLoading={props.isLoading}
isMultiSelect={isMultiSelect}
maxTagCount={props.maxTagCount}
notFoundContent={
<NoSearchCommandFound
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@AmanAgarwal041 Is it possible to make this config driven? Asking because this notContentFound is not very generic, it's very specific to external saas plugin but DropdownControl is actually generic

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sneha122 If you see the implementation of this : https://github.com/appsmithorg/appsmith/pull/39764/files#diff-dec54d51495715bd2faf9ea73dc09419fc8e14da8577fadab3fa09878df6bdc5R40
It is specific to external saas plugin. Any ideas on how we can do it via config ? Because it requires some functionalities as well.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah that's true, even passing this component in notContentFound is specific to external saas plugin. Doing it via config would also be tricky, we can probably have component name in the config but the options and what happens on select etc can't be passed through config.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hence, going ahead with the current implementation. @sneha122

configProperty={props.configProperty}
onSelectOptions={onSelectOptions}
options={options}
pluginId={get(props.formValues, "pluginId")}
/>
}
onClear={clearAllOptions}
onDeselect={onRemoveOptions}
onPopupScroll={handlePopupScroll}
Expand Down
10 changes: 10 additions & 0 deletions app/client/src/utils/formControl/FormControlRegistry.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -254,6 +255,15 @@ class FormControlRegistry {
},
},
);

FormControlFactory.registerControlBuilder(
formControlTypes.CUSTOM_ACTIONS_CONFIG_FORM,
{
buildPropertyControl(controlProps): JSX.Element {
return <CustomActionsControl {...controlProps} />;
},
},
);
}
}

Expand Down
1 change: 1 addition & 0 deletions app/client/src/utils/formControl/formControlTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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