diff --git a/packages/bruno-app/src/components/Sidebar/NewRequest/StyledWrapper.js b/packages/bruno-app/src/components/Sidebar/NewRequest/StyledWrapper.js index 9845bd2ef..f7d7e914d 100644 --- a/packages/bruno-app/src/components/Sidebar/NewRequest/StyledWrapper.js +++ b/packages/bruno-app/src/components/Sidebar/NewRequest/StyledWrapper.js @@ -39,6 +39,14 @@ const StyledWrapper = styled.div` textarea.curl-command { min-height: 150px; } + + .dropdown { + width: fit-content; + + .dropdown-item { + padding: 0.2rem 0.6rem !important; + } + } `; export default StyledWrapper; diff --git a/packages/bruno-app/src/components/Sidebar/NewRequest/index.js b/packages/bruno-app/src/components/Sidebar/NewRequest/index.js index 48b871af3..f95b3efcc 100644 --- a/packages/bruno-app/src/components/Sidebar/NewRequest/index.js +++ b/packages/bruno-app/src/components/Sidebar/NewRequest/index.js @@ -1,4 +1,4 @@ -import React, { useRef, useEffect, useCallback } from 'react'; +import React, { useRef, useEffect, useCallback, forwardRef, useState } from 'react'; import { useFormik } from 'formik'; import * as Yup from 'yup'; import toast from 'react-hot-toast'; @@ -12,6 +12,8 @@ import HttpMethodSelector from 'components/RequestPane/QueryUrl/HttpMethodSelect import { getDefaultRequestPaneTab } from 'utils/collections'; import StyledWrapper from './StyledWrapper'; import { getRequestFromCurlCommand } from 'utils/curl'; +import Dropdown from 'components/Dropdown'; +import { IconCaretDown } from '@tabler/icons'; const NewRequest = ({ collection, item, isEphemeral, onClose }) => { const dispatch = useDispatch(); @@ -19,6 +21,39 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => { const { brunoConfig: { presets: collectionPresets = {} } } = collection; + const [curlRequestTypeDetected, setCurlRequestTypeDetected] = useState(null); + + const dropdownTippyRef = useRef(); + const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref); + + const Icon = forwardRef((props, ref) => { + return ( +
+ {curlRequestTypeDetected === 'http-request' ? "HTTP" : "GraphQL"} + +
+ ); + }); + + // This function analyzes a given cURL command string and determines whether the request is a GraphQL or HTTP request. + const identifyCurlRequestType = (url, headers, body) => { + if (url.endsWith('/graphql')) { + setCurlRequestTypeDetected('graphql-request'); + return; + } + + const contentType = headers?.find((h) => h.name.toLowerCase() === 'content-type')?.value; + if (contentType && contentType.includes('application/graphql')) { + setCurlRequestTypeDetected('graphql-request'); + return; + } + + setCurlRequestTypeDetected('http-request'); + }; + + const curlRequestTypeChange = (type) => { + setCurlRequestTypeDetected(type); + }; const getRequestType = (collectionPresets) => { if (!collectionPresets || !collectionPresets.requestType) { @@ -99,11 +134,11 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => { }) .catch((err) => toast.error(err ? err.message : 'An error occurred while adding the request')); } else if (values.requestType === 'from-curl') { - const request = getRequestFromCurlCommand(values.curlCommand); + const request = getRequestFromCurlCommand(values.curlCommand, curlRequestTypeDetected); dispatch( newHttpRequest({ requestName: values.requestName, - requestType: 'http-request', + requestType: curlRequestTypeDetected, requestUrl: request.url, requestMethod: request.method, collectionUid: collection.uid, @@ -158,6 +193,12 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => { formik.setFieldValue('requestType', 'from-curl'); formik.setFieldValue('curlCommand', pastedData); + // Identify the request type + const request = getRequestFromCurlCommand(pastedData); + if (request) { + identifyCurlRequestType(request.url, request.headers, request.body); + } + // Prevent the default paste behavior to avoid pasting into the textarea event.preventDefault(); } @@ -165,6 +206,18 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => { [formik] ); + const handleCurlCommandChange = (event) => { + formik.handleChange(event); + + if (event.target.name === 'curlCommand') { + const curlCommand = event.target.value; + const request = getRequestFromCurlCommand(curlCommand); + if (request) { + identifyCurlRequestType(request.url, request.headers, request.body); + } + } + }; + return ( @@ -279,15 +332,37 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => { ) : (
- +
+ + } placement="bottom-end"> +
{ + dropdownTippyRef.current.hide(); + curlRequestTypeChange('http-request'); + }} + > + HTTP +
+
{ + dropdownTippyRef.current.hide(); + curlRequestTypeChange('graphql-request'); + }} + > + GraphQL +
+
+
{formik.touched.curlCommand && formik.errors.curlCommand ? (
{formik.errors.curlCommand}
diff --git a/packages/bruno-app/src/utils/codegenerator/har.js b/packages/bruno-app/src/utils/codegenerator/har.js index 9bbd0eea9..479fcd67a 100644 --- a/packages/bruno-app/src/utils/codegenerator/har.js +++ b/packages/bruno-app/src/utils/codegenerator/har.js @@ -19,16 +19,23 @@ const createContentType = (mode) => { } }; +/** + * Creates a list of enabled headers for the request, ensuring no duplicate content-type headers. + * + * @param {Object} request - The request object. + * @param {Object[]} headers - The array of header objects, each containing name, value, and enabled properties. + * @returns {Object[]} - An array of enabled headers with normalized names and values. + */ const createHeaders = (request, headers) => { const enabledHeaders = headers .filter((header) => header.enabled) .map((header) => ({ - name: header.name, + name: header.name.toLowerCase(), value: header.value })); const contentType = createContentType(request.body?.mode); - if (contentType !== '') { + if (contentType !== '' && !enabledHeaders.some((header) => header.name === 'content-type')) { enabledHeaders.push({ name: 'content-type', value: contentType }); } diff --git a/packages/bruno-app/src/utils/curl/curl-to-json.js b/packages/bruno-app/src/utils/curl/curl-to-json.js index e76f4014a..44d9c4b2b 100644 --- a/packages/bruno-app/src/utils/curl/curl-to-json.js +++ b/packages/bruno-app/src/utils/curl/curl-to-json.js @@ -36,6 +36,12 @@ function getQueries(request) { return queries; } +/** + * Converts request data to a string based on its content type. + * + * @param {Object} request - The request object containing data and headers. + * @returns {Object} An object containing the data string. + */ function getDataString(request) { if (typeof request.data === 'number') { request.data = request.data.toString(); @@ -44,7 +50,13 @@ function getDataString(request) { const contentType = getContentType(request.headers); if (contentType && contentType.includes('application/json')) { - return { data: request.data.toString() }; + try { + const parsedData = JSON.parse(request.data); + return { data: JSON.stringify(parsedData) }; + } catch (error) { + console.error('Failed to parse JSON data:', error); + return { data: request.data.toString() }; + } } const parsedQueryString = querystring.parse(request.data, { sort: false }); diff --git a/packages/bruno-app/src/utils/curl/index.js b/packages/bruno-app/src/utils/curl/index.js index e16dc68a5..e478a8e7e 100644 --- a/packages/bruno-app/src/utils/curl/index.js +++ b/packages/bruno-app/src/utils/curl/index.js @@ -2,7 +2,7 @@ import { forOwn } from 'lodash'; import { convertToCodeMirrorJson } from 'utils/common'; import curlToJson from './curl-to-json'; -export const getRequestFromCurlCommand = (curlCommand) => { +export const getRequestFromCurlCommand = (curlCommand, requestType = 'http-request') => { const parseFormData = (parsedBody) => { const formData = []; forOwn(parsedBody, (value, key) => { @@ -12,6 +12,22 @@ export const getRequestFromCurlCommand = (curlCommand) => { return formData; }; + const parseGraphQL = (text) => { + try { + const graphql = JSON.parse(text); + + return { + query: graphql.query, + variables: JSON.stringify(graphql.variables, null, 2) + }; + } catch (e) { + return { + query: '', + variables: '' + }; + } + }; + try { if (!curlCommand || typeof curlCommand !== 'string' || curlCommand.length === 0) { return null; @@ -24,6 +40,8 @@ export const getRequestFromCurlCommand = (curlCommand) => { Object.keys(parsedHeaders).map((key) => ({ name: key, value: parsedHeaders[key], enabled: true })); const contentType = headers?.find((h) => h.name.toLowerCase() === 'content-type')?.value; + const parsedBody = request.data; + const body = { mode: 'none', json: null, @@ -31,11 +49,15 @@ export const getRequestFromCurlCommand = (curlCommand) => { xml: null, sparql: null, multipartForm: null, - formUrlEncoded: null + formUrlEncoded: null, + graphql: null }; - const parsedBody = request.data; + if (parsedBody && contentType && typeof contentType === 'string') { - if (contentType.includes('application/json')) { + if (requestType === 'graphql-request' && (contentType.includes('application/json') || contentType.includes('application/graphql'))) { + body.mode = 'graphql'; + body.graphql = parseGraphQL(parsedBody); + } else if (contentType.includes('application/json')) { body.mode = 'json'; body.json = convertToCodeMirrorJson(parsedBody); } else if (contentType.includes('text/xml')) {