mirror of
https://github.com/ToolJet/ToolJet
synced 2026-04-21 21:47:17 +00:00
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 <devanshuguptaknp@gmail.com>
This commit is contained in:
parent
f05eb802a9
commit
b8b0d57504
4 changed files with 414 additions and 15 deletions
|
|
@ -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);
|
||||
};
|
||||
|
|
|
|||
361
frontend/src/_components/ApiEndpointInputOld.jsx
Normal file
361
frontend/src/_components/ApiEndpointInputOld.jsx
Normal file
|
|
@ -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 (
|
||||
<div className="row">
|
||||
<div className="col-auto" style={{ width: '60px' }}>
|
||||
<span className={`badge bg-${operationColorMapping[operation]}`}>{operation}</span>
|
||||
</div>
|
||||
<div className="col">
|
||||
<span>{path}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} 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 (
|
||||
<div>
|
||||
{loadingSpec && (
|
||||
<div className="p-3">
|
||||
<div className="spinner-border spinner-border-sm text-azure mx-2" role="status"></div>
|
||||
{props.t('stripe', 'Please wait while we load the OpenAPI specification.')}
|
||||
</div>
|
||||
)}
|
||||
{options && !loadingSpec && (
|
||||
<div>
|
||||
<div className="d-flex g-2">
|
||||
<div className="col-12 form-label">
|
||||
<label className="form-label">{props.t('globals.operation', 'Operation')}</label>
|
||||
</div>
|
||||
<div className="col stripe-operation-options flex-grow-1" style={{ width: '90px', marginTop: 0 }}>
|
||||
<Select
|
||||
options={computeOperationSelectionOptions()}
|
||||
value={{
|
||||
operation: options?.operation,
|
||||
value: `${options?.operation}${options?.path}`,
|
||||
}}
|
||||
onChange={(value) => changeOperation(value)}
|
||||
width={'100%'}
|
||||
useMenuPortal={true}
|
||||
customOption={renderOperationOption}
|
||||
styles={queryManagerSelectComponentStyle(props.darkMode, '100%')}
|
||||
useCustomStyles={true}
|
||||
/>
|
||||
{options?.selectedOperation && (
|
||||
<small
|
||||
style={{ margintTop: '10px' }}
|
||||
className="my-2"
|
||||
dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(options?.selectedOperation?.description) }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{options?.selectedOperation && (
|
||||
<div className={`row stripe-fields-row ${props.darkMode && 'theme-dark'}`}>
|
||||
<RenderParameterFields
|
||||
parameters={options?.selectedOperation?.parameters}
|
||||
type="path"
|
||||
label={props.t('globals.path', 'PATH')}
|
||||
options={options}
|
||||
changeParam={changeParam}
|
||||
removeParam={removeParam}
|
||||
darkMode={props.darkMode}
|
||||
/>
|
||||
<RenderParameterFields
|
||||
parameters={options?.selectedOperation?.parameters}
|
||||
type="query"
|
||||
label={props.t('globals.query'.toUpperCase(), 'Query')}
|
||||
options={options}
|
||||
changeParam={changeParam}
|
||||
removeParam={removeParam}
|
||||
darkMode={props.darkMode}
|
||||
/>
|
||||
<RenderParameterFields
|
||||
parameters={
|
||||
options?.selectedOperation?.requestBody?.content[
|
||||
Object.keys(options?.selectedOperation?.requestBody?.content)[0]
|
||||
]?.schema?.properties ?? {}
|
||||
}
|
||||
type="request"
|
||||
label={props.t('globals.requestBody', 'REQUEST BODY')}
|
||||
options={options}
|
||||
changeParam={changeParam}
|
||||
removeParam={removeParam}
|
||||
darkMode={props.darkMode}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default withTranslation()(ApiEndpointInput);
|
||||
|
||||
ApiEndpointInput.propTypes = {
|
||||
options: PropTypes.object,
|
||||
specUrl: PropTypes.string,
|
||||
optionsChanged: PropTypes.func,
|
||||
darkMode: PropTypes.bool,
|
||||
t: PropTypes.func,
|
||||
};
|
||||
|
||||
const RenderParameterFields = ({ parameters, type, label, options, changeParam, removeParam, darkMode }) => {
|
||||
let filteredParams;
|
||||
if (type === 'request') {
|
||||
filteredParams = Object.keys(parameters);
|
||||
} else {
|
||||
filteredParams = parameters?.filter((param) => param.in === type);
|
||||
}
|
||||
|
||||
const paramLabelWithDescription = (param) => {
|
||||
return (
|
||||
<ToolTip message={type === 'request' ? DOMPurify.sanitize(parameters[param].description) : param.description}>
|
||||
<div className="cursor-help">
|
||||
<input
|
||||
type="text"
|
||||
value={type === 'request' ? param : param.name}
|
||||
className="form-control form-control-underline"
|
||||
placeholder="key"
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
</ToolTip>
|
||||
);
|
||||
};
|
||||
|
||||
const paramLabelWithoutDescription = (param) => {
|
||||
return (
|
||||
<input
|
||||
type="text"
|
||||
value={type === 'request' ? param : param.name}
|
||||
className="form-control"
|
||||
placeholder="key"
|
||||
disabled
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const paramType = (param) => {
|
||||
return (
|
||||
<div className="p-2 text-muted">
|
||||
{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()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const paramDetails = (param) => {
|
||||
return (
|
||||
<div className="col-auto d-flex field field-width-179 align-items-center">
|
||||
{(type === 'request' && parameters[param].description) || param?.description
|
||||
? paramLabelWithDescription(param)
|
||||
: paramLabelWithoutDescription(param)}
|
||||
{param.required && <span className="text-danger fw-bold">*</span>}
|
||||
{paramType(param)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const inputField = (param) => {
|
||||
return (
|
||||
<CodeHinter
|
||||
initialValue={(type === 'request' ? options?.params[type][param] : options?.params[type][param.name]) ?? ''}
|
||||
mode="text"
|
||||
placeholder={'Value'}
|
||||
theme={darkMode ? 'monokai' : 'duotone-light'}
|
||||
lineNumbers={false}
|
||||
onChange={(value) => {
|
||||
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 (
|
||||
<span
|
||||
className="code-hinter-clear-btn"
|
||||
role="button"
|
||||
onClick={handleClear}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
handleClear();
|
||||
}
|
||||
}}
|
||||
tabIndex="0"
|
||||
>
|
||||
<SolidIcons name="removerectangle" width="20" fill="#ACB2B9" />
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
filteredParams?.length > 0 && (
|
||||
<div className={`${type === 'request' ? 'request-body' : type}-fields d-flex`}>
|
||||
<h5 className="text-heading form-label">{label}</h5>
|
||||
<div className="flex-grow-1 input-group-parent-container">
|
||||
{filteredParams.map((param) => (
|
||||
<div className="input-group-wrapper" key={type === 'request' ? param : param.name}>
|
||||
<div className="input-group">
|
||||
{paramDetails(param)}
|
||||
<div className="col field overflow-hidden code-hinter-borderless">{inputField(param)}</div>
|
||||
{((type === 'request' && options['params'][type][param]) || options['params'][type][param.name]) &&
|
||||
clearButton(param)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
RenderParameterFields.propTypes = {
|
||||
parameters: PropTypes.any,
|
||||
type: PropTypes.string,
|
||||
label: PropTypes.string,
|
||||
options: PropTypes.object,
|
||||
changeParam: PropTypes.func,
|
||||
removeParam: PropTypes.func,
|
||||
darkMode: PropTypes.bool,
|
||||
};
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue