From b8b0d57504d863dc0fd38b87158d158c28e0868b Mon Sep 17 00:00:00 2001 From: Ganesh Kumar <40178541+ganesh8056@users.noreply.github.com> Date: Fri, 8 Aug 2025 22:28:57 +0530 Subject: [PATCH] Fix: ApiEndpointInput component used for rendering OpenAPI spec where values are stored in cache for respective operation. (#13732) * apiendpoint component used for rendering openapi spec value are persisted * Fix/stripe old component (#13737) * fix: integrated old component * removed log --------- Co-authored-by: Devanshu Gupta --- frontend/src/_components/ApiEndpointInput.jsx | 50 ++- .../src/_components/ApiEndpointInputOld.jsx | 361 ++++++++++++++++++ frontend/src/_components/DynamicForm.jsx | 16 +- plugins/packages/stripe/lib/operations.json | 2 +- 4 files changed, 414 insertions(+), 15 deletions(-) create mode 100644 frontend/src/_components/ApiEndpointInputOld.jsx diff --git a/frontend/src/_components/ApiEndpointInput.jsx b/frontend/src/_components/ApiEndpointInput.jsx index 063093f4a2..b58a2c9fe8 100644 --- a/frontend/src/_components/ApiEndpointInput.jsx +++ b/frontend/src/_components/ApiEndpointInput.jsx @@ -59,6 +59,7 @@ const ApiEndpointInput = (props) => { const [loadingSpec, setLoadingSpec] = useState(true); const [options, setOptions] = useState(props.options); const [specJson, setSpecJson] = useState(null); + const [operationParams, setOperationParams] = useState({}); // Check if specUrl is an object (multiple specs) or string (single spec) const isMultiSpec = typeof props.specUrl === 'object' && !Array.isArray(props.specUrl); @@ -78,21 +79,25 @@ const ApiEndpointInput = (props) => { setSpecJson(data); if (isMultiSpec) { - // Clear all parameters when switching specs - const queryParams = { + // MODIFIED: Retain existing values instead of clearing them + const currentParams = options?.params || { path: {}, query: {}, request: {}, }; - let newOperation = null; - let newPath = null; + // Keep existing values if the operation/path still exists in the new spec + let newOperation = options?.operation; + let newPath = options?.path; let newSelectedOperation = null; - if (options?.path && options?.operation && data?.paths?.[options.path]?.[options.operation]) { - newOperation = options.operation; - newPath = options.path; - newSelectedOperation = data.paths[options.path][options.operation]; + // Validate if the current operation/path exists in the new spec + if (newPath && newOperation && data?.paths?.[newPath]?.[newOperation]) { + newSelectedOperation = data.paths[newPath][newOperation]; + } else { + // Only clear if the operation/path doesn't exist in the new spec + newOperation = null; + newPath = null; } const newOptions = { @@ -100,7 +105,8 @@ const ApiEndpointInput = (props) => { operation: newOperation, path: newPath, selectedOperation: newSelectedOperation, - params: queryParams, + params: currentParams, // Retain existing params + specType: specUrlOrType, }; setOptions(newOptions); @@ -112,12 +118,24 @@ const ApiEndpointInput = (props) => { }); }; + const getOperationKey = (operation, path) => { + return `${operation}_${path}`; + }; + const changeOperation = (value) => { const operation = value.split('/', 2)[0]; const path = value.substring(value.indexOf('/')); - // Clear all params when changing operation - const queryParams = { + if (options.operation && options.path) { + const currentOperationKey = getOperationKey(options.operation, options.path); + setOperationParams((prevState) => ({ + ...prevState, + [currentOperationKey]: options.params, + })); + } + + const newOperationKey = getOperationKey(operation, path); + const savedParams = operationParams[newOperationKey] || { path: {}, query: {}, request: {}, @@ -128,7 +146,7 @@ const ApiEndpointInput = (props) => { path, operation, selectedOperation: specJson.paths[path][operation], - params: queryParams, + params: savedParams, }; setOptions(newOptions); @@ -167,7 +185,13 @@ const ApiEndpointInput = (props) => { const removeParam = (paramType, paramName) => { const newOptions = JSON.parse(JSON.stringify(options)); - delete newOptions['params'][paramType][paramName]; + const newParams = { ...newOptions.params }; + const newParamType = { ...newParams[paramType] }; + + delete newParamType[paramName]; + + newParams[paramType] = newParamType; + newOptions.params = newParams; setOptions(newOptions); props.optionsChanged(newOptions); }; diff --git a/frontend/src/_components/ApiEndpointInputOld.jsx b/frontend/src/_components/ApiEndpointInputOld.jsx new file mode 100644 index 0000000000..193413ee07 --- /dev/null +++ b/frontend/src/_components/ApiEndpointInputOld.jsx @@ -0,0 +1,361 @@ +import React, { useEffect, useState } from 'react'; +import { openapiService } from '@/_services'; +import Select from '@/_ui/Select'; +import { queryManagerSelectComponentStyle } from '@/_ui/Select/styles'; +import DOMPurify from 'dompurify'; +import { ToolTip } from '@/_components'; +import CodeHinter from '@/AppBuilder/CodeEditor'; +import { withTranslation } from 'react-i18next'; +import { isEmpty } from 'lodash'; +import PropTypes from 'prop-types'; +import SolidIcons from '@/_ui/Icon/SolidIcons'; + +const operationColorMapping = { + get: 'azure', + post: 'green', + delete: 'red', + put: 'yellow', +}; + +const ApiEndpointInput = (props) => { + const [loadingSpec, setLoadingSpec] = useState(true); + const [options, setOptions] = useState(props.options); + const [specJson, setSpecJson] = useState(null); + + const fetchOpenApiSpec = () => { + setLoadingSpec(true); + openapiService + .fetchSpecFromUrl(props.specUrl) + .then((response) => response.text()) + .then((text) => { + const data = JSON.parse(text); + setSpecJson(data); + setLoadingSpec(false); + }); + }; + + const changeOperation = (value) => { + const operation = value.split('/', 2)[0]; + const path = value.substring(value.indexOf('/')); + const newOptions = { ...options, path, operation, selectedOperation: specJson.paths[path][operation] }; + setOptions(newOptions); + props.optionsChanged(newOptions); + }; + + const changeParam = (paramType, paramName, value) => { + if (value === '') { + removeParam(paramType, paramName); + } else { + const newOptions = { + ...options, + params: { + ...options.params, + [paramType]: { + ...options.params[paramType], + [paramName]: value, + }, + }, + }; + setOptions(newOptions); + props.optionsChanged(newOptions); + } + }; + + const removeParam = (paramType, paramName) => { + const newOptions = JSON.parse(JSON.stringify(options)); + delete newOptions['params'][paramType][paramName]; + setOptions(newOptions); + props.optionsChanged(newOptions); + }; + + const renderOperationOption = (data) => { + const path = data.value.substring(data.value.indexOf('/')); + const operation = data.operation; + if (path && operation) { + return ( +
+
+ {operation} +
+
+ {path} +
+
+ ); + } else { + return 'Select an operation'; + } + }; + + const categorizeOperations = (operation, path, acc, category) => { + const option = { + value: `${operation}${path}`, + label: `${path}`, + name: path, + operation: operation, + }; + const existingCategory = acc.find((obj) => obj.label === category); + if (existingCategory) { + existingCategory.options.push(option); + } else { + acc.push({ + label: category, + options: [option], + }); + } + }; + + const computeOperationSelectionOptions = () => { + const paths = specJson?.paths; + if (isEmpty(paths)) return []; + + const pathGroups = Object.keys(paths).reduce((acc, path) => { + const operations = Object.keys(paths[path]); + const category = path.split('/')[2]; + operations.forEach((operation) => categorizeOperations(operation, path, acc, category)); + return acc; + }, []); + + return pathGroups; + }; + + useEffect(() => { + const queryParams = { + path: props.options?.params?.path ?? {}, + query: props.options?.params?.query ?? {}, + request: props.options?.params?.request ?? {}, + }; + setLoadingSpec(true); + setOptions({ ...props.options, params: queryParams }); + fetchOpenApiSpec(); + }, []); + + return ( +
+ {loadingSpec && ( +
+
+ {props.t('stripe', 'Please wait while we load the OpenAPI specification.')} +
+ )} + {options && !loadingSpec && ( +
+
+
+ +
+
+ +
+ + ); + }; + + const paramLabelWithoutDescription = (param) => { + return ( + + ); + }; + + const paramType = (param) => { + return ( +
+ {type === 'query' && + param?.schema?.anyOf && + param?.schema?.anyOf.map((type, i) => + i < param.schema?.anyOf.length - 1 + ? type.type.substring(0, 3).toUpperCase() + '|' + : type.type.substring(0, 3).toUpperCase() + )} + {(type === 'path' || (type === 'query' && !param?.schema?.anyOf)) && + param?.schema?.type?.substring(0, 3).toUpperCase()} + {type === 'request' && parameters[param].type?.substring(0, 3).toUpperCase()} +
+ ); + }; + + const paramDetails = (param) => { + return ( +
+ {(type === 'request' && parameters[param].description) || param?.description + ? paramLabelWithDescription(param) + : paramLabelWithoutDescription(param)} + {param.required && *} + {paramType(param)} +
+ ); + }; + + const inputField = (param) => { + return ( + { + if (type === 'request') { + changeParam(type, param, value); + } else { + changeParam(type, param.name, value); + } + }} + height={'32px'} + /> + ); + }; + + const clearButton = (param) => { + const handleClear = () => { + if (type === 'request') { + removeParam(type, param); + } else { + removeParam(type, param.name); + } + }; + + return ( + { + if (e.key === 'Enter') { + handleClear(); + } + }} + tabIndex="0" + > + + + ); + }; + + return ( + filteredParams?.length > 0 && ( +
+
{label}
+
+ {filteredParams.map((param) => ( +
+
+ {paramDetails(param)} +
{inputField(param)}
+ {((type === 'request' && options['params'][type][param]) || options['params'][type][param.name]) && + clearButton(param)} +
+
+ ))} +
+
+ ) + ); +}; + +RenderParameterFields.propTypes = { + parameters: PropTypes.any, + type: PropTypes.string, + label: PropTypes.string, + options: PropTypes.object, + changeParam: PropTypes.func, + removeParam: PropTypes.func, + darkMode: PropTypes.bool, +}; diff --git a/frontend/src/_components/DynamicForm.jsx b/frontend/src/_components/DynamicForm.jsx index 5e0aa29afb..b5a7e10b1d 100644 --- a/frontend/src/_components/DynamicForm.jsx +++ b/frontend/src/_components/DynamicForm.jsx @@ -14,6 +14,7 @@ import GoogleSheets from '@/_components/Googlesheets'; import Slack from '@/_components/Slack'; import Zendesk from '@/_components/Zendesk'; import ApiEndpointInput from '@/_components/ApiEndpointInput'; +import ApiEndpointInputOld from './ApiEndpointInputOld'; import { ConditionFilter, CondtionSort, MultiColumn } from '@/_components/MultiConditions'; import ToolJetDbOperations from '@/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/ToolJetDbOperations'; import { orgEnvironmentVariableService, orgEnvironmentConstantService } from '../_services'; @@ -210,6 +211,8 @@ const DynamicForm = ({ return CondtionSort; case 'react-component-api-endpoint': return ApiEndpointInput; + case 'react-component-api-endpoint-old': + return ApiEndpointInputOld; case 'react-component-sharepoint': return Sharepoint; case 'react-component-oauth': @@ -502,6 +505,13 @@ const DynamicForm = ({ options, darkMode, }; + case 'react-component-api-endpoint-old': + return { + specUrl: spec_url, + optionsChanged, + options, + darkMode, + }; default: return {}; } @@ -580,7 +590,11 @@ const DynamicForm = ({ {Object.keys(obj).map((key) => { const { label, type, encrypted, className, key: propertyKey, shouldRenderTheProperty = '' } = obj[key]; const Element = getElement(type); - const isSpecificComponent = ['tooljetdb-operations', 'react-component-api-endpoint'].includes(type); + const isSpecificComponent = [ + 'tooljetdb-operations', + 'react-component-api-endpoint', + 'react-component-api-endpoint-old', + ].includes(type); // shouldRenderTheProperty - key is used for Dynamic connection parameters const enabled = shouldRenderTheProperty ? selectedDataSource?.options?.[shouldRenderTheProperty]?.value ?? false diff --git a/plugins/packages/stripe/lib/operations.json b/plugins/packages/stripe/lib/operations.json index 4532254d99..3fb22283d7 100644 --- a/plugins/packages/stripe/lib/operations.json +++ b/plugins/packages/stripe/lib/operations.json @@ -8,7 +8,7 @@ "operation": { "label": "", "key": "stripe_operation", - "type": "react-component-api-endpoint", + "type": "react-component-api-endpoint-old", "description": "Single select dropdown for operation", "spec_url": "https://raw.githubusercontent.com/stripe/openapi/master/openapi/spec3.json" }