From 7460078fd6d9ad5d693b3711c8faa327797af9b3 Mon Sep 17 00:00:00 2001 From: Abhishek S Lal Date: Thu, 12 Feb 2026 00:42:16 +0530 Subject: [PATCH] fix: enhance tag handling and validation in collection import/export (#7107) - Added collection format handling in Tags component. - Updated convertCollection function to accept collectionFormat parameter. - Improved tag validation logic in TagList component based on collection format. - Adjusted OpenAPI transformation functions to support collection format options. - Enhanced schema validation for tags to allow spaces and underscores. --- .../RequestPane/Settings/Tags/index.js | 1 + .../Sidebar/ImportCollectionLocation/index.js | 6 +++--- .../bruno-app/src/components/TagList/index.js | 14 +++++++++---- .../src/openapi/openapi-to-bruno.js | 21 +++++++++++++------ .../bruno-schema/src/collections/index.js | 2 +- 5 files changed, 30 insertions(+), 14 deletions(-) diff --git a/packages/bruno-app/src/components/RequestPane/Settings/Tags/index.js b/packages/bruno-app/src/components/RequestPane/Settings/Tags/index.js index f2a732a8f..e22130220 100644 --- a/packages/bruno-app/src/components/RequestPane/Settings/Tags/index.js +++ b/packages/bruno-app/src/components/RequestPane/Settings/Tags/index.js @@ -58,6 +58,7 @@ const Tags = ({ item, collection }) => { handleRemoveTag={handleRemove} tags={tags} onSave={handleRequestSave} + collectionFormat={collection.format} /> ); diff --git a/packages/bruno-app/src/components/Sidebar/ImportCollectionLocation/index.js b/packages/bruno-app/src/components/Sidebar/ImportCollectionLocation/index.js index b8e18e3db..4efd3c54c 100644 --- a/packages/bruno-app/src/components/Sidebar/ImportCollectionLocation/index.js +++ b/packages/bruno-app/src/components/Sidebar/ImportCollectionLocation/index.js @@ -51,13 +51,13 @@ const getCollectionName = (format, rawData) => { }; // Convert raw data to Bruno collection format -const convertCollection = async (format, rawData, groupingType) => { +const convertCollection = async (format, rawData, groupingType, collectionFormat) => { try { let collection; switch (format) { case 'openapi': - collection = convertOpenapiToBruno(rawData, { groupBy: groupingType }); + collection = convertOpenapiToBruno(rawData, { groupBy: groupingType, collectionFormat }); break; case 'wsdl': collection = await wsdlToBruno(rawData); @@ -127,7 +127,7 @@ const ImportCollectionLocation = ({ onClose, handleSubmit, rawData, format }) => .required('Location is required') }), onSubmit: async (values) => { - const convertedCollection = await convertCollection(format, rawData, groupingType); + const convertedCollection = await convertCollection(format, rawData, groupingType, collectionFormat); handleSubmit(convertedCollection, values.collectionLocation, { format: collectionFormat }); } }); diff --git a/packages/bruno-app/src/components/TagList/index.js b/packages/bruno-app/src/components/TagList/index.js index 94403225d..309aae366 100644 --- a/packages/bruno-app/src/components/TagList/index.js +++ b/packages/bruno-app/src/components/TagList/index.js @@ -4,9 +4,10 @@ import StyledWrapper from './StyledWrapper'; import SingleLineEditor from 'components/SingleLineEditor/index'; import { useTheme } from 'providers/Theme/index'; -const TagList = ({ tagsHintList = [], handleAddTag, tags, handleRemoveTag, onSave, handleValidation }) => { +const TagList = ({ tagsHintList = [], handleAddTag, tags, handleRemoveTag, onSave, handleValidation, collectionFormat }) => { const { displayedTheme } = useTheme(); - const tagNameRegex = /^[\w-]+$/; + const isBruFormat = collectionFormat === 'bru'; + const tagNameRegex = isBruFormat ? /^[\w-]+$/ : /^[\w-][\w\s-]*[\w-]$|^[\w-]+$/; const [text, setText] = useState(''); const [error, setError] = useState(''); @@ -16,8 +17,14 @@ const TagList = ({ tagsHintList = [], handleAddTag, tags, handleRemoveTag, onSav }; const handleKeyDown = (e) => { + if (!text.trim()) { + return; + } if (!tagNameRegex.test(text)) { - setError('Tags must only contain alpha-numeric characters, "-", "_"'); + setError(isBruFormat + ? 'Tags in BRU format must only contain alpha-numeric characters, "-", "_".' + : 'Tags must only contain alpha-numeric characters, spaces, "-", "_"' + ); return; } if (tags.includes(text)) { @@ -28,7 +35,6 @@ const TagList = ({ tagsHintList = [], handleAddTag, tags, handleRemoveTag, onSav const error = handleValidation(text); if (error) { setError(error); - setText(''); return; } } diff --git a/packages/bruno-converters/src/openapi/openapi-to-bruno.js b/packages/bruno-converters/src/openapi/openapi-to-bruno.js index f91129845..6041a179a 100644 --- a/packages/bruno-converters/src/openapi/openapi-to-bruno.js +++ b/packages/bruno-converters/src/openapi/openapi-to-bruno.js @@ -494,7 +494,7 @@ const createBrunoExample = ({ brunoRequestItem, exampleValue, exampleName, examp return brunoExample; }; -const transformOpenapiRequestItem = (request, usedNames = new Set()) => { +const transformOpenapiRequestItem = (request, usedNames = new Set(), options = {}) => { let _operationObject = request.operationObject; let operationName = _operationObject.summary || _operationObject.operationId || _operationObject.description; @@ -530,6 +530,15 @@ const transformOpenapiRequestItem = (request, usedNames = new Set()) => { uid: uuid(), name: operationName, type: 'http-request', + tags: [...new Set( + (request.operationObject.tags || []).map((tag) => { + let sanitized = tag.trim(); + if (options.collectionFormat !== 'yml') { + sanitized = sanitized.replace(/\s+/g, '_'); + } + return sanitized; + }).filter((tag) => tag.trim()) + )], request: { url: ensureUrl(request.global.server + path), method: request.method.toUpperCase(), @@ -1050,7 +1059,7 @@ const groupRequestsByTags = (requests) => { return [groups, ungrouped]; }; -const groupRequestsByPath = (requests) => { +const groupRequestsByPath = (requests, options = {}) => { const pathGroups = {}; // Group requests by their path segments @@ -1110,7 +1119,7 @@ const groupRequestsByPath = (requests) => { const buildFolderStructure = (group) => { // Create a new usedNames set for each folder/subfolder scope const localUsedNames = new Set(); - const items = group.requests.map((req) => transformOpenapiRequestItem(req, localUsedNames)); + const items = group.requests.map((req) => transformOpenapiRequestItem(req, localUsedNames, options)); // Add sub-folders const subFolders = []; @@ -1258,7 +1267,7 @@ export const parseOpenApiCollection = (data, options = {}) => { const groupingType = options.groupBy || 'tags'; if (groupingType === 'path') { - brunoCollection.items = groupRequestsByPath(allRequests); + brunoCollection.items = groupRequestsByPath(allRequests, options); } else { // Default tag-based grouping let [groups, ungroupedRequests] = groupRequestsByTags(allRequests); @@ -1282,11 +1291,11 @@ export const parseOpenApiCollection = (data, options = {}) => { name: group.name } }, - items: group.requests.map((req) => transformOpenapiRequestItem(req, usedNames)) + items: group.requests.map((req) => transformOpenapiRequestItem(req, usedNames, options)) }; }); - let ungroupedItems = ungroupedRequests.map((req) => transformOpenapiRequestItem(req, usedNames)); + let ungroupedItems = ungroupedRequests.map((req) => transformOpenapiRequestItem(req, usedNames, options)); let brunoCollectionItems = brunoFolders.concat(ungroupedItems); brunoCollection.items = brunoCollectionItems; } diff --git a/packages/bruno-schema/src/collections/index.js b/packages/bruno-schema/src/collections/index.js index d17fa9f6c..3d679cf5f 100644 --- a/packages/bruno-schema/src/collections/index.js +++ b/packages/bruno-schema/src/collections/index.js @@ -545,7 +545,7 @@ const itemSchema = Yup.object({ type: Yup.string().oneOf(['http-request', 'graphql-request', 'folder', 'js', 'grpc-request', 'ws-request']).required('type is required'), seq: Yup.number().min(1), name: Yup.string().min(1, 'name must be at least 1 character').required('name is required'), - tags: Yup.array().of(Yup.string().matches(/^[\w-]+$/, 'tag must be alphanumeric')), + tags: Yup.array().of(Yup.string().matches(/^[\w-][\w\s-]*[\w-]$|^[\w-]+$/, 'tag must contain only alphanumeric characters, spaces, hyphens, or underscores')), request: Yup.mixed().when('type', { is: (type) => type === 'grpc-request', then: grpcRequestSchema.required('request is required when item-type is grpc-request'),