Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { renameItem, saveRequest, closeTabs } from 'providers/ReduxStore/slices/
import path from 'utils/common/path';
import { IconArrowBackUp, IconEdit, IconCaretDown } from '@tabler/icons';
import { sanitizeName, validateName, validateNameError } from 'utils/common/regex';
import { getHttpRequestFilenameBase } from 'utils/common/requestFilename';
import toast from 'react-hot-toast';
import Help from 'components/Help';
import PathDisplay from 'components/PathDisplay';
Expand All @@ -25,6 +26,10 @@ const RenameCollectionItem = ({ collectionUid, item, onClose }) => {
const itemName = item?.name;
const itemType = item?.type;
const itemFilename = item?.filename ? path.parse(item?.filename).name : '';
const isHttpRequest = itemType === 'http-request';
const expectedHttpFilename = isHttpRequest ? getHttpRequestFilenameBase(itemName, item?.request?.method) : null;
const hasCustomFilename = isHttpRequest && itemFilename !== expectedHttpFilename;
const [isFilenameManuallyEdited, setFilenameManuallyEdited] = useState(hasCustomFilename);
const [showFilesystemName, toggleShowFilesystemName] = useState(false);

const dropdownTippyRef = useRef();
Expand Down Expand Up @@ -127,7 +132,12 @@ const RenameCollectionItem = ({ collectionUid, item, onClose }) => {
spellCheck="false"
onChange={(e) => {
formik.setFieldValue('name', e.target.value);
!isEditing && formik.setFieldValue('filename', sanitizeName(e.target.value));
if (!isFilenameManuallyEdited) {
const filename = isHttpRequest
? getHttpRequestFilenameBase(e.target.value, item?.request?.method)
: sanitizeName(e.target.value);
formik.setFieldValue('filename', filename);
}
}}
value={formik.values.name || ''}
/>
Expand Down Expand Up @@ -185,7 +195,10 @@ const RenameCollectionItem = ({ collectionUid, item, onClose }) => {
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
onChange={formik.handleChange}
onChange={(e) => {
setFilenameManuallyEdited(true);
formik.handleChange(e);
}}
value={formik.values.filename || ''}
/>
{itemType !== 'folder' && <span className="absolute right-2 top-4 flex justify-center items-center file-extension">.{collection?.format || 'bru'}</span>}
Expand Down
97 changes: 77 additions & 20 deletions packages/bruno-app/src/components/Sidebar/NewRequest/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import get from 'lodash/get';
import { useFormik } from 'formik';
import * as Yup from 'yup';
import toast from 'react-hot-toast';
import path from 'utils/common/path';
import { uuid } from 'utils/common';
import Modal from 'components/Modal';
import { useDispatch, useSelector } from 'react-redux';
Expand All @@ -14,7 +13,8 @@ import HttpMethodSelector from 'components/RequestPane/QueryUrl/HttpMethodSelect
import { getDefaultRequestPaneTab } from 'utils/collections';
import { getRequestFromCurlCommand } from 'utils/curl';
import { IconArrowBackUp, IconCaretDown, IconEdit } from '@tabler/icons';
import { sanitizeName, validateName, validateNameError } from 'utils/common/regex';
import { validateName, validateNameError } from 'utils/common/regex';
import { getRequestFilenameBase } from 'utils/common/requestFilename';
import Dropdown from 'components/Dropdown';
import PathDisplay from 'components/PathDisplay';
import Portal from 'components/Portal';
Expand Down Expand Up @@ -55,26 +55,31 @@ const NewRequest = ({ collectionUid, item, isEphemeral, onClose }) => {
});

// This function analyzes a given cURL command string and determines whether the request is a GraphQL or HTTP request.
const identifyCurlRequestType = (url, headers, body) => {
const getCurlRequestType = (url, headers, body) => {
if (url.endsWith('/graphql')) {
setCurlRequestTypeDetected('graphql-request');
return;
return 'graphql-request';
}

const contentType = headers?.find((h) => h.name.toLowerCase() === 'content-type')?.value;
if (contentType && contentType.includes('application/graphql')) {
setCurlRequestTypeDetected('graphql-request');
return;
return 'graphql-request';
}

setCurlRequestTypeDetected('http-request');
return 'http-request';
};

const identifyCurlRequestType = (url, headers, body) => {
const requestType = getCurlRequestType(url, headers, body);
setCurlRequestTypeDetected(requestType);
return requestType;
};

const curlRequestTypeChange = (type) => {
setCurlRequestTypeDetected(type);
};
Comment on lines 77 to 79
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Resync auto filename when cURL type is toggled from the dropdown.

curlRequestTypeChange updates only curlRequestTypeDetected; when filename is still auto-managed, it can stay stale after switching HTTP ↔ GraphQL.

Proposed fix
 const curlRequestTypeChange = (type) => {
   setCurlRequestTypeDetected(type);
+  if (!isFilenameManuallyEdited) {
+    formik.setFieldValue('filename', getRequestFilenameBase({
+      requestName: formik.values.requestName,
+      requestMethod: formik.values.requestMethod,
+      requestType: type
+    }));
+  }
 };

Also applies to: 603-615

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/bruno-app/src/components/Sidebar/NewRequest/index.js` around lines
77 - 79, curlRequestTypeChange currently only calls setCurlRequestTypeDetected,
leaving an auto-generated filename stale when the user toggles between HTTP and
GraphQL; update curlRequestTypeChange to also detect if the filename is in
auto-mode (the auto flag, e.g., isFilenameAuto or filenameAuto) and when true
compute the new auto filename (use the existing helper that derives the auto
name or replicate its logic) and call setFilename with that value (and ensure
the auto flag remains true via setIsFilenameAuto if applicable); apply the same
change pattern to the other handler(s) referenced around lines 603-615 so
toggling the cURL type always resyncs the auto filename.


const [isEditing, toggleEditing] = useState(false);
const [isFilenameManuallyEdited, setFilenameManuallyEdited] = useState(false);

const getRequestType = (collectionPresets) => {
if (!collectionPresets || !collectionPresets.requestType) {
Expand Down Expand Up @@ -149,7 +154,7 @@ const NewRequest = ({ collectionUid, item, isEphemeral, onClose }) => {
onSubmit: (values) => {
const isGrpcRequest = values.requestType === 'grpc-request';
const isWsRequest = values.requestType === 'ws-request';
const filename = values.filename;
let filename = values.filename;

if (isGrpcRequest) {
dispatch(
Expand Down Expand Up @@ -211,6 +216,13 @@ const NewRequest = ({ collectionUid, item, isEphemeral, onClose }) => {
} else if (values.requestType === 'from-curl') {
const request = getRequestFromCurlCommand(values.curlCommand, curlRequestTypeDetected);
const settings = { encodeUrl: false };
filename = isFilenameManuallyEdited
? values.filename
: getRequestFilenameBase({
requestName: values.requestName,
requestMethod: request.method,
requestType: curlRequestTypeDetected || 'http-request'
});

dispatch(
newHttpRequest({
Expand Down Expand Up @@ -261,6 +273,25 @@ const NewRequest = ({ collectionUid, item, isEphemeral, onClose }) => {

const onSubmit = () => formik.handleSubmit();

const setAutoFilename = ({
requestName = formik.values.requestName,
requestMethod = formik.values.requestMethod,
requestType = formik.values.requestType
}) => {
if (isFilenameManuallyEdited) {
return;
}

const filenameRequestType = requestType === 'from-curl' ? curlRequestTypeDetected || 'http-request' : requestType;
formik.setFieldValue('filename', getRequestFilenameBase({ requestName, requestMethod, requestType: filenameRequestType }));
};

const handleRequestTypeChange = (event) => {
const requestType = event.target.value;
formik.setFieldValue('requestType', requestType);
setAutoFilename({ requestType });
};

const handlePaste = useCallback(
(event) => {
const clipboardData = event.clipboardData || window.clipboardData;
Expand All @@ -276,14 +307,24 @@ const NewRequest = ({ collectionUid, item, isEphemeral, onClose }) => {
// Identify the request type
const request = getRequestFromCurlCommand(pastedData);
if (request) {
identifyCurlRequestType(request.url, request.headers, request.body);
const requestType = identifyCurlRequestType(request.url, request.headers, request.body);
if (!isFilenameManuallyEdited) {
formik.setFieldValue(
'filename',
getRequestFilenameBase({
requestName: formik.values.requestName,
requestMethod: request.method,
requestType
})
);
}
}

// Prevent the default paste behavior to avoid pasting into the textarea
event.preventDefault();
}
},
[formik]
[formik, isFilenameManuallyEdited, curlRequestTypeDetected]
);

const handleCurlCommandChange = (event) => {
Expand All @@ -293,7 +334,17 @@ const NewRequest = ({ collectionUid, item, isEphemeral, onClose }) => {
const curlCommand = event.target.value;
const request = getRequestFromCurlCommand(curlCommand);
if (request) {
identifyCurlRequestType(request.url, request.headers, request.body);
const requestType = identifyCurlRequestType(request.url, request.headers, request.body);
if (!isFilenameManuallyEdited) {
formik.setFieldValue(
'filename',
getRequestFilenameBase({
requestName: formik.values.requestName,
requestMethod: request.method,
requestType
})
);
}
}
}
};
Expand Down Expand Up @@ -331,7 +382,7 @@ const NewRequest = ({ collectionUid, item, isEphemeral, onClose }) => {
name="requestType"
value="http-request"
checked={formik.values.requestType === 'http-request'}
onChange={formik.handleChange}
onChange={handleRequestTypeChange}
data-testid="http-request"
/>
<label htmlFor="http-request" className="ml-1 cursor-pointer select-none">
Expand All @@ -345,7 +396,7 @@ const NewRequest = ({ collectionUid, item, isEphemeral, onClose }) => {
name="requestType"
value="graphql-request"
checked={formik.values.requestType === 'graphql-request'}
onChange={formik.handleChange}
onChange={handleRequestTypeChange}
data-testid="graphql-request"
/>
<label htmlFor="graphql-request" className="ml-1 cursor-pointer select-none">
Expand All @@ -362,7 +413,7 @@ const NewRequest = ({ collectionUid, item, isEphemeral, onClose }) => {
name="requestType"
value="grpc-request"
checked={formik.values.requestType === 'grpc-request'}
onChange={formik.handleChange}
onChange={handleRequestTypeChange}
data-testid="grpc-request"
/>
<label htmlFor="grpc-request" className="ml-1 cursor-pointer select-none">
Expand All @@ -377,7 +428,7 @@ const NewRequest = ({ collectionUid, item, isEphemeral, onClose }) => {
name="requestType"
value="ws-request"
checked={formik.values.requestType === 'ws-request'}
onChange={formik.handleChange}
onChange={handleRequestTypeChange}
data-testid="ws-request"
/>
<label htmlFor="ws-request" className="ml-1 cursor-pointer select-none">
Expand All @@ -394,7 +445,7 @@ const NewRequest = ({ collectionUid, item, isEphemeral, onClose }) => {
name="requestType"
value="from-curl"
checked={formik.values.requestType === 'from-curl'}
onChange={formik.handleChange}
onChange={handleRequestTypeChange}
data-testid="from-curl"
/>
<label htmlFor="from-curl" className="ml-1 cursor-pointer select-none">
Expand All @@ -421,7 +472,7 @@ const NewRequest = ({ collectionUid, item, isEphemeral, onClose }) => {
spellCheck="false"
onChange={(e) => {
formik.setFieldValue('requestName', e.target.value);
!isEditing && formik.setFieldValue('filename', sanitizeName(e.target.value));
setAutoFilename({ requestName: e.target.value });
}}
value={formik.values.requestName || ''}
data-testid="request-name"
Expand Down Expand Up @@ -471,7 +522,10 @@ const NewRequest = ({ collectionUid, item, isEphemeral, onClose }) => {
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
onChange={formik.handleChange}
onChange={(e) => {
setFilenameManuallyEdited(true);
formik.handleChange(e);
}}
value={formik.values.filename || ''}
data-testid="file-name"
/>
Expand Down Expand Up @@ -500,7 +554,10 @@ const NewRequest = ({ collectionUid, item, isEphemeral, onClose }) => {
<div className="flex items-center h-full method-selector-container">
<HttpMethodSelector
method={formik.values.requestMethod}
onMethodSelect={(val) => formik.setFieldValue('requestMethod', val)}
onMethodSelect={(val) => {
formik.setFieldValue('requestMethod', val);
setAutoFilename({ requestMethod: val });
}}
showCaret
/>
</div>
Expand Down
26 changes: 26 additions & 0 deletions packages/bruno-app/src/utils/common/requestFilename.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import path from './path';
import { sanitizeName } from './regex';

export const getHttpRequestFilenameBase = (requestName, method = 'GET') => {
const sanitizedRequestName = sanitizeName(requestName || '') || 'request';
const normalizedMethod = String(method || 'GET').trim().toUpperCase() || 'GET';
return sanitizeName(`${normalizedMethod} ${sanitizedRequestName}`);
};

export const getRequestFilenameBase = ({ requestName, requestMethod, requestType }) => {
if (requestType === 'http-request') {
return getHttpRequestFilenameBase(requestName, requestMethod);
}

return sanitizeName(requestName || '');
};

export const normalizeRequestFilename = (filename, format = 'bru') => {
const targetExtension = format === 'yml' ? 'yml' : 'bru';
const extension = path.extname(filename || '').toLowerCase();
const baseName = ['.bru', '.yml', '.yaml'].includes(extension)
? path.basename(filename, extension)
: filename;
Comment on lines +20 to +23
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Handle extension stripping with original-case suffix, not the lowercased variant.

path.basename(filename, extension) may fail when the actual suffix casing differs (e.g. .BRU), so normalization can produce incorrect base names.

Proposed fix
 export const normalizeRequestFilename = (filename, format = 'bru') => {
   const targetExtension = format === 'yml' ? 'yml' : 'bru';
-  const extension = path.extname(filename || '').toLowerCase();
-  const baseName = ['.bru', '.yml', '.yaml'].includes(extension)
-    ? path.basename(filename, extension)
-    : filename;
+  const originalExtension = path.extname(filename || '');
+  const extension = originalExtension.toLowerCase();
+  const baseName = ['.bru', '.yml', '.yaml'].includes(extension)
+    ? path.basename(filename, originalExtension)
+    : filename;
 
   return `${sanitizeName(baseName || 'request')}.${targetExtension}`;
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const extension = path.extname(filename || '').toLowerCase();
const baseName = ['.bru', '.yml', '.yaml'].includes(extension)
? path.basename(filename, extension)
: filename;
const extension = path.extname(filename || '');
const extension = extension.toLowerCase();
const baseName = ['.bru', '.yml', '.yaml'].includes(extension)
? path.basename(filename, extension)
: filename;
Suggested change
const extension = path.extname(filename || '').toLowerCase();
const baseName = ['.bru', '.yml', '.yaml'].includes(extension)
? path.basename(filename, extension)
: filename;
export const normalizeRequestFilename = (filename, format = 'bru') => {
const targetExtension = format === 'yml' ? 'yml' : 'bru';
const originalExtension = path.extname(filename || '');
const extension = originalExtension.toLowerCase();
const baseName = ['.bru', '.yml', '.yaml'].includes(extension)
? path.basename(filename, originalExtension)
: filename;
return `${sanitizeName(baseName || 'request')}.${targetExtension}`;
};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/bruno-app/src/utils/common/requestFilename.js` around lines 20 - 23,
The code lowercases the extension into the variable extension and then passes
that lowercased value to path.basename, which breaks when the file's actual
suffix has different casing; change the logic to keep a lowercased copy for the
includes check but obtain the original-case suffix (e.g. const origExt =
path.extname(filename)) and pass origExt to path.basename when stripping (use
extensionLower = origExt.toLowerCase() for the includes check and call
path.basename(filename, origExt) when creating baseName), ensuring filename,
extension (origExt), and baseName references are updated accordingly.


return `${sanitizeName(baseName || 'request')}.${targetExtension}`;
};
14 changes: 14 additions & 0 deletions packages/bruno-app/src/utils/common/requestFilename.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { getHttpRequestFilenameBase, normalizeRequestFilename } from './requestFilename';

describe('request filename helpers', () => {
it('generates method-aware HTTP request filenames from display names', () => {
expect(getHttpRequestFilenameBase('/projects', 'GET')).toBe('GET projects');
expect(getHttpRequestFilenameBase('/projects', 'POST')).toBe('POST projects');
expect(getHttpRequestFilenameBase('/projects/{id}', 'PUT')).toBe('PUT projects-{id}');
});

it('normalizes request filename extensions for collection format', () => {
expect(normalizeRequestFilename('GET projects.bru', 'yml')).toBe('GET projects.yml');
expect(normalizeRequestFilename('POST projects', 'bru')).toBe('POST projects.bru');
});
});
20 changes: 7 additions & 13 deletions packages/bruno-cli/src/utils/collection.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const os = require('os');
const fs = require('fs');
const path = require('path');
const { sanitizeName } = require('./filesystem');
const { getUniqueRequestFilename } = require('./request-filename');
const { parseRequest, parseCollection, parseFolder, stringifyCollection, stringifyFolder, stringifyEnvironment, stringifyRequest } = require('@usebruno/filestore');
const constants = require('../constants');
const chalk = require('chalk');
Expand All @@ -11,7 +12,7 @@ const FORMAT_CONFIG = {
yml: { ext: '.yml', collectionFile: 'opencollection.yml', folderFile: 'folder.yml' },
bru: { ext: '.bru', collectionFile: 'collection.bru', folderFile: 'folder.bru' }
};
const REQUEST_ITEM_TYPES = ['http-request', 'graphql-request'];
const REQUEST_ITEM_TYPES = ['http-request', 'graphql-request', 'grpc-request', 'ws-request'];

const getCollectionFormat = (collectionPath) => {
if (fs.existsSync(path.join(collectionPath, 'opencollection.yml'))) return 'yml';
Expand Down Expand Up @@ -596,6 +597,7 @@ const createCollectionFromBrunoObject = async (collection, dirPath, options = {}
*/
const processCollectionItems = async (items = [], currentPath, options = {}) => {
const { format = 'bru' } = options;
const filenamesInFolder = new Set();
for (const item of items) {
if (item.type === 'folder') {
// Create folder
Expand All @@ -620,18 +622,10 @@ const processCollectionItems = async (items = [], currentPath, options = {}) =>
}
} else if (REQUEST_ITEM_TYPES.includes(item.type)) {
// Create request file
let sanitizedFilename;
if (format == 'yml') {
sanitizedFilename = sanitizeName(item?.filename || `${item.name}.yml`);
if (!sanitizedFilename.endsWith('.yml')) {
sanitizedFilename += '.yml';
}
} else {
sanitizedFilename = sanitizeName(item?.filename || `${item.name}.bru`);
if (!sanitizedFilename.endsWith('.bru')) {
sanitizedFilename += '.bru';
}
}
let sanitizedFilename = getUniqueRequestFilename(item, format, (filename) => {
return filenamesInFolder.has(filename) || fs.existsSync(path.join(currentPath, filename));
});
filenamesInFolder.add(sanitizedFilename);

// Convert to YML format
const itemJson = {
Expand Down
Loading