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.
This commit is contained in:
Abhishek S Lal
2026-02-12 00:42:16 +05:30
committed by GitHub
parent e4b6f7a28b
commit 7460078fd6
5 changed files with 30 additions and 14 deletions

View File

@@ -58,6 +58,7 @@ const Tags = ({ item, collection }) => {
handleRemoveTag={handleRemove}
tags={tags}
onSave={handleRequestSave}
collectionFormat={collection.format}
/>
</div>
);

View File

@@ -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 });
}
});

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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'),