mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-01 00:24:08 +00:00
move: import setting into import collection modal (#5929)
This commit is contained in:
@@ -3,13 +3,11 @@ import { IconFileImport } from '@tabler/icons';
|
||||
import { toastError } from 'utils/common/error';
|
||||
import Modal from 'components/Modal';
|
||||
import jsyaml from 'js-yaml';
|
||||
import { postmanToBruno, isPostmanCollection } from 'utils/importers/postman-collection';
|
||||
import { convertInsomniaToBruno, isInsomniaCollection } from 'utils/importers/insomnia-collection';
|
||||
import { convertOpenapiToBruno, isOpenApiSpec } from 'utils/importers/openapi-collection';
|
||||
import { isPostmanCollection } from 'utils/importers/postman-collection';
|
||||
import { isInsomniaCollection } from 'utils/importers/insomnia-collection';
|
||||
import { isOpenApiSpec } from 'utils/importers/openapi-collection';
|
||||
import { isWSDLCollection } from 'utils/importers/wsdl-collection';
|
||||
import { processBrunoCollection } from 'utils/importers/bruno-collection';
|
||||
import { wsdlToBruno } from '@usebruno/converters';
|
||||
import ImportSettings from 'components/Sidebar/ImportSettings';
|
||||
import { isBrunoCollection } from 'utils/importers/bruno-collection';
|
||||
import FullscreenLoader from './FullscreenLoader/index';
|
||||
|
||||
const convertFileToObject = async (file) => {
|
||||
@@ -38,9 +36,6 @@ const convertFileToObject = async (file) => {
|
||||
const ImportCollection = ({ onClose, handleSubmit }) => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [dragActive, setDragActive] = useState(false);
|
||||
const [showImportSettings, setShowImportSettings] = useState(false);
|
||||
const [openApiData, setOpenApiData] = useState(null);
|
||||
const [groupingType, setGroupingType] = useState('tags');
|
||||
const fileInputRef = useRef(null);
|
||||
|
||||
const handleDrag = (e) => {
|
||||
@@ -58,16 +53,6 @@ const ImportCollection = ({ onClose, handleSubmit }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleImportSettings = () => {
|
||||
try {
|
||||
const collection = convertOpenapiToBruno(openApiData, { groupBy: groupingType });
|
||||
handleSubmit({ collection });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
toastError(err, 'Failed to process OpenAPI specification');
|
||||
}
|
||||
};
|
||||
|
||||
const processFile = async (file) => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
@@ -77,26 +62,23 @@ const ImportCollection = ({ onClose, handleSubmit }) => {
|
||||
throw new Error('Failed to parse file content');
|
||||
}
|
||||
|
||||
// Check if it's an OpenAPI spec and show settings
|
||||
let type = null;
|
||||
|
||||
if (isOpenApiSpec(data)) {
|
||||
setOpenApiData(data);
|
||||
setIsLoading(false);
|
||||
setShowImportSettings(true);
|
||||
return;
|
||||
}
|
||||
|
||||
let collection;
|
||||
if (isWSDLCollection(data)) {
|
||||
collection = await wsdlToBruno(data);
|
||||
type = 'openapi';
|
||||
} else if (isWSDLCollection(data)) {
|
||||
type = 'wsdl';
|
||||
} else if (isPostmanCollection(data)) {
|
||||
collection = await postmanToBruno(data);
|
||||
type = 'postman';
|
||||
} else if (isInsomniaCollection(data)) {
|
||||
collection = convertInsomniaToBruno(data);
|
||||
type = 'insomnia';
|
||||
} else if (isBrunoCollection(data)) {
|
||||
type = 'bruno';
|
||||
} else {
|
||||
collection = await processBrunoCollection(data);
|
||||
throw new Error('Unsupported collection format');
|
||||
}
|
||||
|
||||
handleSubmit({ collection });
|
||||
handleSubmit({ rawData: data, type });
|
||||
} catch (err) {
|
||||
toastError(err, 'Import collection failed');
|
||||
} finally {
|
||||
@@ -140,17 +122,6 @@ const ImportCollection = ({ onClose, handleSubmit }) => {
|
||||
'application/xml'
|
||||
];
|
||||
|
||||
if (showImportSettings) {
|
||||
return (
|
||||
<ImportSettings
|
||||
groupingType={groupingType}
|
||||
setGroupingType={setGroupingType}
|
||||
onClose={onClose}
|
||||
onConfirm={handleImportSettings}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal size="sm" title="Import Collection" hideFooter={true} handleCancel={onClose} dataTestId="import-collection-modal">
|
||||
<div className="flex flex-col">
|
||||
|
||||
@@ -1,15 +1,94 @@
|
||||
import React, { useRef, useEffect, useState } from 'react';
|
||||
import React, { useRef, useEffect, useState, forwardRef } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useFormik } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
import { IconCaretDown } from '@tabler/icons';
|
||||
import { browseDirectory } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { postmanToBruno } from 'utils/importers/postman-collection';
|
||||
import { convertInsomniaToBruno } from 'utils/importers/insomnia-collection';
|
||||
import { convertOpenapiToBruno } from 'utils/importers/openapi-collection';
|
||||
import { processBrunoCollection } from 'utils/importers/bruno-collection';
|
||||
import { wsdlToBruno } from '@usebruno/converters';
|
||||
import { toastError } from 'utils/common/error';
|
||||
import Modal from 'components/Modal';
|
||||
import Help from 'components/Help';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
// Extract collection name from raw data
|
||||
const getCollectionName = (format, rawData) => {
|
||||
if (!rawData) return 'Collection';
|
||||
|
||||
const ImportCollectionLocation = ({ onClose, handleSubmit, collectionName }) => {
|
||||
switch (format) {
|
||||
case 'openapi':
|
||||
return rawData.info?.title || 'OpenAPI Collection';
|
||||
case 'postman':
|
||||
return rawData.info?.name || rawData.collection?.info?.name || 'Postman Collection';
|
||||
case 'insomnia':
|
||||
// For Insomnia v4 format, name is in the workspace resource
|
||||
if (rawData.resources && Array.isArray(rawData.resources)) {
|
||||
const workspace = rawData.resources.find((r) => r._type === 'workspace');
|
||||
if (workspace?.name) {
|
||||
return workspace.name;
|
||||
}
|
||||
}
|
||||
// Fallback to root name property
|
||||
return rawData.name || 'Insomnia Collection';
|
||||
case 'bruno':
|
||||
return rawData.name || 'Bruno Collection';
|
||||
case 'wsdl':
|
||||
return 'WSDL Collection';
|
||||
default:
|
||||
return 'Collection';
|
||||
}
|
||||
};
|
||||
|
||||
// Convert raw data to Bruno collection format
|
||||
const convertCollection = async (format, rawData, groupingType) => {
|
||||
try {
|
||||
let collection;
|
||||
|
||||
switch (format) {
|
||||
case 'openapi':
|
||||
collection = convertOpenapiToBruno(rawData, { groupBy: groupingType });
|
||||
break;
|
||||
case 'wsdl':
|
||||
collection = await wsdlToBruno(rawData);
|
||||
break;
|
||||
case 'postman':
|
||||
collection = await postmanToBruno(rawData);
|
||||
break;
|
||||
case 'insomnia':
|
||||
collection = convertInsomniaToBruno(rawData);
|
||||
break;
|
||||
case 'bruno':
|
||||
collection = await processBrunoCollection(rawData);
|
||||
break;
|
||||
default:
|
||||
throw new Error('Unknown collection format');
|
||||
}
|
||||
|
||||
return collection;
|
||||
} catch (err) {
|
||||
console.error('Conversion error:', err);
|
||||
toastError(err, 'Failed to convert collection');
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
const groupingOptions = [
|
||||
{ value: 'tags', label: 'Tags', description: 'Group requests by OpenAPI tags', testId: 'grouping-option-tags' },
|
||||
{ value: 'path', label: 'Paths', description: 'Group requests by URL path structure', testId: 'grouping-option-path' }
|
||||
];
|
||||
|
||||
const ImportCollectionLocation = ({ onClose, handleSubmit, rawData, format }) => {
|
||||
const inputRef = useRef();
|
||||
const dispatch = useDispatch();
|
||||
const [groupingType, setGroupingType] = useState('tags');
|
||||
const dropdownTippyRef = useRef();
|
||||
const isOpenApi = format === 'openapi';
|
||||
|
||||
const collectionName = getCollectionName(format, rawData);
|
||||
|
||||
const formik = useFormik({
|
||||
enableReinitialize: true,
|
||||
@@ -22,10 +101,27 @@ const ImportCollectionLocation = ({ onClose, handleSubmit, collectionName }) =>
|
||||
.max(500, 'must be 500 characters or less')
|
||||
.required('Location is required')
|
||||
}),
|
||||
onSubmit: (values) => {
|
||||
handleSubmit(values.collectionLocation);
|
||||
onSubmit: async (values) => {
|
||||
const convertedCollection = await convertCollection(format, rawData, groupingType);
|
||||
handleSubmit(convertedCollection, values.collectionLocation);
|
||||
}
|
||||
});
|
||||
|
||||
const onDropdownCreate = (ref) => {
|
||||
dropdownTippyRef.current = ref;
|
||||
};
|
||||
|
||||
const GroupingDropdownIcon = forwardRef((props, ref) => {
|
||||
const selectedOption = groupingOptions.find((option) => option.value === groupingType);
|
||||
return (
|
||||
<div ref={ref} className="flex items-center justify-between w-full current-group" data-testid="grouping-dropdown">
|
||||
<div>
|
||||
<div className="text-sm font-medium text-gray-900 dark:text-gray-100">{selectedOption.label}</div>
|
||||
</div>
|
||||
<IconCaretDown size={16} className="text-gray-400 ml-[0.25rem]" fill="currentColor" />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
const browse = () => {
|
||||
dispatch(browseDirectory())
|
||||
.then((dirPath) => {
|
||||
@@ -48,53 +144,89 @@ const ImportCollectionLocation = ({ onClose, handleSubmit, collectionName }) =>
|
||||
const onSubmit = () => formik.handleSubmit();
|
||||
|
||||
return (
|
||||
<Modal size="sm" title="Import Collection" confirmText="Import" handleConfirm={onSubmit} handleCancel={onClose} dataTestId="import-collection-location-modal">
|
||||
<form className="bruno-form" onSubmit={e => e.preventDefault()}>
|
||||
<div>
|
||||
<label htmlFor="collectionName" className="block font-semibold">
|
||||
Name
|
||||
</label>
|
||||
<div className="mt-2">{collectionName}</div>
|
||||
<>
|
||||
<label htmlFor="collectionLocation" className="block font-semibold mt-3 flex items-center">
|
||||
Location
|
||||
<Help>
|
||||
<p>
|
||||
Bruno stores your collections on your computer's filesystem.
|
||||
</p>
|
||||
<p className="mt-2">
|
||||
Choose the location where you want to store this collection.
|
||||
</p>
|
||||
</Help>
|
||||
<StyledWrapper>
|
||||
<Modal
|
||||
size="sm"
|
||||
title="Import Collection"
|
||||
confirmText="Import"
|
||||
handleConfirm={onSubmit}
|
||||
handleCancel={onClose}
|
||||
dataTestId="import-collection-location-modal"
|
||||
>
|
||||
<form className="bruno-form" onSubmit={(e) => e.preventDefault()}>
|
||||
<div>
|
||||
<label htmlFor="collectionName" className="block font-semibold">
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
id="collection-location"
|
||||
type="text"
|
||||
name="collectionLocation"
|
||||
className="block textbox mt-2 w-full cursor-pointer"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={formik.values.collectionLocation || ''}
|
||||
onClick={browse}
|
||||
onChange={e => {
|
||||
formik.setFieldValue('collectionLocation', e.target.value);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
{formik.touched.collectionLocation && formik.errors.collectionLocation ? (
|
||||
<div className="text-red-500">{formik.errors.collectionLocation}</div>
|
||||
) : null}
|
||||
<div className="mt-2">{collectionName}</div>
|
||||
|
||||
<div className="mt-1">
|
||||
<span className="text-link cursor-pointer hover:underline" onClick={browse}>
|
||||
Browse
|
||||
</span>
|
||||
<>
|
||||
<label htmlFor="collectionLocation" className="font-semibold mt-4 flex items-center">
|
||||
Location
|
||||
<Help>
|
||||
<p>Bruno stores your collections on your computer's filesystem.</p>
|
||||
<p className="mt-2">Choose the location where you want to store this collection.</p>
|
||||
</Help>
|
||||
</label>
|
||||
<input
|
||||
id="collection-location"
|
||||
type="text"
|
||||
name="collectionLocation"
|
||||
className="block textbox mt-2 w-full cursor-pointer"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={formik.values.collectionLocation || ''}
|
||||
onClick={browse}
|
||||
onChange={(e) => {
|
||||
formik.setFieldValue('collectionLocation', e.target.value);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
{formik.touched.collectionLocation && formik.errors.collectionLocation ? (
|
||||
<div className="text-red-500">{formik.errors.collectionLocation}</div>
|
||||
) : null}
|
||||
|
||||
<div className="mt-1">
|
||||
<span className="text-link cursor-pointer hover:underline" onClick={browse}>
|
||||
Browse
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
|
||||
{isOpenApi && (
|
||||
<div className="mt-4 flex gap-4 items-center">
|
||||
<div>
|
||||
<label htmlFor="groupingType" className="block font-semibold mt-4">
|
||||
Folder arrangement
|
||||
</label>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1 mb-2">
|
||||
Select whether to create folders according to the spec's paths or tags.
|
||||
</p>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<Dropdown onCreate={onDropdownCreate} icon={<GroupingDropdownIcon />} placement="bottom-start">
|
||||
{groupingOptions.map((option) => (
|
||||
<div
|
||||
key={option.value}
|
||||
className="dropdown-item"
|
||||
data-testid={option.testId}
|
||||
onClick={() => {
|
||||
dropdownTippyRef?.current?.hide();
|
||||
setGroupingType(option.value);
|
||||
}}
|
||||
>
|
||||
{option.label}
|
||||
</div>
|
||||
))}
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</Modal>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
import React, { useRef, forwardRef } from 'react';
|
||||
import { IconCaretDown } from '@tabler/icons';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import Modal from 'components/Modal';
|
||||
import Portal from 'components/Portal';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const groupingOptions = [
|
||||
{ value: 'tags', label: 'Tags', description: 'Group requests by OpenAPI tags', testId: 'grouping-option-tags' },
|
||||
{ value: 'path', label: 'Paths', description: 'Group requests by URL path structure', testId: 'grouping-option-path' }
|
||||
];
|
||||
|
||||
const ImportSettings = ({
|
||||
groupingType,
|
||||
setGroupingType,
|
||||
onClose,
|
||||
onConfirm
|
||||
}) => {
|
||||
const dropdownTippyRef = useRef();
|
||||
|
||||
const onDropdownCreate = (ref) => {
|
||||
dropdownTippyRef.current = ref;
|
||||
};
|
||||
|
||||
const GroupingDropdownIcon = forwardRef((props, ref) => {
|
||||
const selectedOption = groupingOptions.find((option) => option.value === groupingType);
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className="flex items-center justify-between w-full current-group"
|
||||
data-testid="grouping-dropdown"
|
||||
>
|
||||
<div>
|
||||
<div className="text-sm font-medium text-gray-900 dark:text-gray-100">{selectedOption.label}</div>
|
||||
</div>
|
||||
<IconCaretDown size={16} className="text-gray-400 ml-[0.25rem]" fill="currentColor" />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<Modal
|
||||
size="sm"
|
||||
title="OpenAPI Import Settings"
|
||||
handleCancel={onClose}
|
||||
handleConfirm={onConfirm}
|
||||
confirmText="Import"
|
||||
dataTestId="import-settings-modal"
|
||||
>
|
||||
<StyledWrapper>
|
||||
<div className="flex items-center">
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100 mb-2">Folder arrangement</h3>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3">
|
||||
Select whether to create folders according to the spec's paths or tags.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
<Dropdown onCreate={onDropdownCreate} icon={<GroupingDropdownIcon />} placement="bottom-start">
|
||||
{groupingOptions.map((option) => (
|
||||
<div
|
||||
key={option.value}
|
||||
className="dropdown-item"
|
||||
data-testid={option.testId}
|
||||
onClick={() => {
|
||||
dropdownTippyRef?.current?.hide();
|
||||
setGroupingType(option.value);
|
||||
}}
|
||||
>
|
||||
{option.label}
|
||||
</div>
|
||||
))}
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
</Modal>
|
||||
</Portal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImportSettings;
|
||||
@@ -15,24 +15,24 @@ import { multiLineMsg } from "utils/common";
|
||||
import { formatIpcError } from "utils/common/error";
|
||||
|
||||
const TitleBar = () => {
|
||||
const [importedCollection, setImportedCollection] = useState(null);
|
||||
const [createCollectionModalOpen, setCreateCollectionModalOpen] = useState(false);
|
||||
const [importCollectionModalOpen, setImportCollectionModalOpen] = useState(false);
|
||||
const [importCollectionLocationModalOpen, setImportCollectionLocationModalOpen] = useState(false);
|
||||
const [importData, setImportData] = useState(null);
|
||||
const dispatch = useDispatch();
|
||||
const { ipcRenderer } = window;
|
||||
|
||||
const handleImportCollection = ({ collection }) => {
|
||||
setImportedCollection(collection);
|
||||
const handleImportCollection = ({ rawData, type }) => {
|
||||
setImportData({ rawData, type });
|
||||
setImportCollectionModalOpen(false);
|
||||
setImportCollectionLocationModalOpen(true);
|
||||
};
|
||||
|
||||
const handleImportCollectionLocation = (collectionLocation) => {
|
||||
dispatch(importCollection(importedCollection, collectionLocation))
|
||||
const handleImportCollectionLocation = (convertedCollection, collectionLocation) => {
|
||||
dispatch(importCollection(convertedCollection, collectionLocation))
|
||||
.then(() => {
|
||||
setImportCollectionLocationModalOpen(false);
|
||||
setImportedCollection(null);
|
||||
setImportData(null);
|
||||
toast.success('Collection imported successfully');
|
||||
})
|
||||
.catch((err) => {
|
||||
@@ -72,9 +72,10 @@ const TitleBar = () => {
|
||||
{importCollectionModalOpen ? (
|
||||
<ImportCollection onClose={() => setImportCollectionModalOpen(false)} handleSubmit={handleImportCollection} />
|
||||
) : null}
|
||||
{importCollectionLocationModalOpen ? (
|
||||
{importCollectionLocationModalOpen && importData ? (
|
||||
<ImportCollectionLocation
|
||||
collectionName={importedCollection.name}
|
||||
rawData={importData.rawData}
|
||||
format={importData.type}
|
||||
onClose={() => setImportCollectionLocationModalOpen(false)}
|
||||
handleSubmit={handleImportCollectionLocation}
|
||||
/>
|
||||
|
||||
@@ -17,7 +17,7 @@ const Welcome = () => {
|
||||
const { t } = useTranslation();
|
||||
const sidebarCollapsed = useSelector((state) => state.app.sidebarCollapsed);
|
||||
const collections = useSelector((state) => state.collections.collections);
|
||||
const [importedCollection, setImportedCollection] = useState(null);
|
||||
const [importData, setImportData] = useState(null);
|
||||
const [createCollectionModalOpen, setCreateCollectionModalOpen] = useState(false);
|
||||
const [importCollectionModalOpen, setImportCollectionModalOpen] = useState(false);
|
||||
const [importCollectionLocationModalOpen, setImportCollectionLocationModalOpen] = useState(false);
|
||||
@@ -30,17 +30,17 @@ const Welcome = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const handleImportCollection = ({ collection }) => {
|
||||
setImportedCollection(collection);
|
||||
const handleImportCollection = ({ rawData, type }) => {
|
||||
setImportData({ rawData, type });
|
||||
setImportCollectionModalOpen(false);
|
||||
setImportCollectionLocationModalOpen(true);
|
||||
};
|
||||
|
||||
const handleImportCollectionLocation = (collectionLocation) => {
|
||||
dispatch(importCollection(importedCollection, collectionLocation))
|
||||
const handleImportCollectionLocation = (convertedCollection, collectionLocation) => {
|
||||
dispatch(importCollection(convertedCollection, collectionLocation))
|
||||
.then(() => {
|
||||
setImportCollectionLocationModalOpen(false);
|
||||
setImportedCollection(null);
|
||||
setImportData(null);
|
||||
toast.success(t('WELCOME.COLLECTION_IMPORT_SUCCESS'));
|
||||
})
|
||||
.catch((err) => {
|
||||
@@ -56,9 +56,10 @@ const Welcome = () => {
|
||||
{importCollectionModalOpen ? (
|
||||
<ImportCollection onClose={() => setImportCollectionModalOpen(false)} handleSubmit={handleImportCollection} />
|
||||
) : null}
|
||||
{importCollectionLocationModalOpen ? (
|
||||
{importCollectionLocationModalOpen && importData ? (
|
||||
<ImportCollectionLocation
|
||||
collectionName={importedCollection.name}
|
||||
rawData={importData.rawData}
|
||||
format={importData.type}
|
||||
onClose={() => setImportCollectionLocationModalOpen(false)}
|
||||
handleSubmit={handleImportCollectionLocation}
|
||||
/>
|
||||
|
||||
@@ -14,3 +14,27 @@ export const processBrunoCollection = async (jsonData) => {
|
||||
throw new BrunoError('Import collection failed');
|
||||
}
|
||||
};
|
||||
|
||||
export const isBrunoCollection = (data) => {
|
||||
// Check for Bruno collection format
|
||||
if (typeof data !== 'object' || data === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Must have a version field that is a non-empty string
|
||||
if (typeof data.version !== 'string' || !data.version.trim()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Must have a name field that is a non-empty string
|
||||
if (typeof data.name !== 'string' || !data.name.trim()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Must have an items array
|
||||
if (!Array.isArray(data.items)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -29,13 +29,19 @@ const isPostmanCollection = (data) => {
|
||||
}
|
||||
|
||||
const schema = info.schema;
|
||||
// Accept schemas hosted at schema.getpostman.com or schema.postman.com
|
||||
const schemaRegex = /^https:\/\/schema\.(?:getpostman|postman)\.com\//;
|
||||
if (typeof schema === 'string' && schemaRegex.test(schema)) {
|
||||
return true;
|
||||
if (typeof schema !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
// Only accept supported Postman v2.0 and v2.1 schemas
|
||||
const supportedSchemas = [
|
||||
'https://schema.getpostman.com/json/collection/v2.0.0/collection.json',
|
||||
'https://schema.getpostman.com/json/collection/v2.1.0/collection.json',
|
||||
'https://schema.postman.com/json/collection/v2.0.0/collection.json',
|
||||
'https://schema.postman.com/json/collection/v2.1.0/collection.json'
|
||||
];
|
||||
|
||||
return supportedSchemas.includes(schema);
|
||||
};
|
||||
|
||||
export { postmanToBruno, readFile, isPostmanCollection };
|
||||
|
||||
@@ -18,7 +18,7 @@ test.describe('Import Bruno Collection - Missing Required Schema Fields', () =>
|
||||
await page.locator('#import-collection-loader').waitFor({ state: 'hidden' });
|
||||
|
||||
// Check for schema validation error messages
|
||||
const hasImportError = await page.getByText('Import collection failed').first().isVisible();
|
||||
const hasImportError = await page.getByText('Unsupported collection format').first().isVisible();
|
||||
|
||||
expect(hasImportError).toBe(true);
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { test, expect } from '../../../playwright';
|
||||
import * as path from 'path';
|
||||
import { closeAllCollections } from '../../utils/page';
|
||||
|
||||
test.describe('Import Bruno Testbench Collection', () => {
|
||||
test.beforeAll(async ({ page }) => {
|
||||
@@ -7,7 +8,12 @@ test.describe('Import Bruno Testbench Collection', () => {
|
||||
await page.locator('.bruno-logo').click();
|
||||
});
|
||||
|
||||
test('Import Bruno Testbench collection successfully', async ({ page }) => {
|
||||
test.afterEach(async ({ page }) => {
|
||||
// cleanup: close all collections
|
||||
await closeAllCollections(page);
|
||||
});
|
||||
|
||||
test('Import Bruno Testbench collection successfully', async ({ page, createTmpDir }) => {
|
||||
const brunoFile = path.resolve(__dirname, 'fixtures', 'bruno-testbench.json');
|
||||
|
||||
await page.getByRole('button', { name: 'Import Collection' }).click();
|
||||
@@ -29,7 +35,9 @@ test.describe('Import Bruno Testbench Collection', () => {
|
||||
// Wait for collection to appear in the location modal
|
||||
await expect(locationModal.getByText('bruno-testbench')).toBeVisible();
|
||||
|
||||
// Cleanup: close any open modals
|
||||
await page.locator('[data-test-id="modal-close-button"]').click();
|
||||
await page.locator('#collection-location').fill(await createTmpDir('bruno-testbench-test'));
|
||||
await page.getByRole('button', { name: 'Import', exact: true }).click();
|
||||
|
||||
await expect(page.locator('#sidebar-collection-name').getByText('bruno-testbench')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import { test, expect } from '../../../playwright';
|
||||
import * as path from 'path';
|
||||
import { closeAllCollections } from '../../utils/page';
|
||||
|
||||
test.describe('Import Insomnia Collection v4', () => {
|
||||
test('Import Insomnia Collection v4 successfully', async ({ page }) => {
|
||||
test.afterEach(async ({ page }) => {
|
||||
// cleanup: close all collections
|
||||
await closeAllCollections(page);
|
||||
});
|
||||
|
||||
test('Import Insomnia Collection v4 successfully', async ({ page, createTmpDir }) => {
|
||||
const insomniaFile = path.resolve(__dirname, 'fixtures', 'insomnia-v4.json');
|
||||
|
||||
await page.getByRole('button', { name: 'Import Collection' }).click();
|
||||
@@ -21,10 +27,9 @@ test.describe('Import Insomnia Collection v4', () => {
|
||||
const locationModal = page.getByRole('dialog');
|
||||
await expect(locationModal.locator('.bruno-modal-header-title')).toContainText('Import Collection');
|
||||
|
||||
// Wait for collection to appear in the location modal
|
||||
await expect(locationModal.getByText('Test API Collection v4')).toBeVisible();
|
||||
await page.locator('#collection-location').fill(await createTmpDir('insomnia-v4-test'));
|
||||
await page.getByRole('button', { name: 'Import', exact: true }).click();
|
||||
|
||||
// Cleanup: close any open modals
|
||||
await page.locator('[data-test-id="modal-close-button"]').click();
|
||||
await expect(page.locator('#sidebar-collection-name').getByText('Test API Collection v4')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import { test, expect } from '../../../playwright';
|
||||
import * as path from 'path';
|
||||
import { closeAllCollections } from '../../utils/page';
|
||||
|
||||
test.describe('Import Insomnia Collection v5', () => {
|
||||
test('Import Insomnia Collection v5 successfully', async ({ page }) => {
|
||||
test.afterEach(async ({ page }) => {
|
||||
// cleanup: close all collections
|
||||
await closeAllCollections(page);
|
||||
});
|
||||
|
||||
test('Import Insomnia Collection v5 successfully', async ({ page, createTmpDir }) => {
|
||||
const insomniaFile = path.resolve(__dirname, 'fixtures', 'insomnia-v5.yaml');
|
||||
|
||||
await page.getByRole('button', { name: 'Import Collection' }).click();
|
||||
@@ -21,10 +27,9 @@ test.describe('Import Insomnia Collection v5', () => {
|
||||
const locationModal = page.getByRole('dialog');
|
||||
await expect(locationModal.locator('.bruno-modal-header-title')).toContainText('Import Collection');
|
||||
|
||||
// Wait for collection to appear in the location modal
|
||||
await expect(locationModal.getByText('Test API Collection v5')).toBeVisible();
|
||||
await page.locator('#collection-location').fill(await createTmpDir('insomnia-v5-test'));
|
||||
await page.getByRole('button', { name: 'Import', exact: true }).click();
|
||||
|
||||
// Cleanup: close any open modals
|
||||
await page.locator('[data-test-id="modal-close-button"]').click();
|
||||
await expect(page.locator('#sidebar-collection-name').getByText('Test API Collection v5')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,7 +18,7 @@ test.describe('Invalid Insomnia Collection - Missing Collection Array', () => {
|
||||
await page.locator('#import-collection-loader').waitFor({ state: 'hidden' });
|
||||
|
||||
// Check for error message
|
||||
const hasError = await page.getByText('Import collection failed').first().isVisible();
|
||||
const hasError = await page.getByText('Unsupported collection format').first().isVisible();
|
||||
expect(hasError).toBe(true);
|
||||
|
||||
// Cleanup: close any open modals
|
||||
|
||||
@@ -24,18 +24,6 @@ test.describe('OpenAPI Duplicate Names Handling', () => {
|
||||
// wait for the file processing to complete
|
||||
await page.locator('#import-collection-loader').waitFor({ state: 'hidden' });
|
||||
|
||||
// verify that the import settings modal appears
|
||||
const settingsModal = page.getByTestId('import-settings-modal');
|
||||
await expect(settingsModal.locator('.bruno-modal-header-title')).toContainText('OpenAPI Import Settings');
|
||||
|
||||
// click the Import button in the settings modal footer
|
||||
await settingsModal.getByRole('button', { name: 'Import' }).click();
|
||||
|
||||
// verify that the collection location modal appears (OpenAPI files go directly to location modal)
|
||||
const locationModal = page.getByTestId('import-collection-location-modal');
|
||||
// verify the collection name is correctly parsed despite duplicate operation names
|
||||
await expect(locationModal.getByText('Duplicate Test Collection')).toBeVisible();
|
||||
|
||||
// select a location
|
||||
await page.locator('#collection-location').fill(await createTmpDir('duplicate-test'));
|
||||
await page.getByRole('button', { name: 'Import', exact: true }).click();
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import { test, expect } from '../../../playwright';
|
||||
import * as path from 'path';
|
||||
import { closeAllCollections } from '../../utils/page';
|
||||
|
||||
test.describe('Import OpenAPI v3 JSON Collection', () => {
|
||||
test('Import simple OpenAPI v3 JSON successfully', async ({ page }) => {
|
||||
test.afterEach(async ({ page }) => {
|
||||
// cleanup: close all collections
|
||||
await closeAllCollections(page);
|
||||
});
|
||||
|
||||
test('Import simple OpenAPI v3 JSON successfully', async ({ page, createTmpDir }) => {
|
||||
const openApiFile = path.resolve(__dirname, 'fixtures', 'openapi-simple.json');
|
||||
|
||||
await page.getByRole('button', { name: 'Import Collection' }).click();
|
||||
@@ -17,13 +23,6 @@ test.describe('Import OpenAPI v3 JSON Collection', () => {
|
||||
// Wait for the loader to disappear
|
||||
await page.locator('#import-collection-loader').waitFor({ state: 'hidden' });
|
||||
|
||||
// verify that the import settings modal appears
|
||||
const settingsModal = page.getByTestId('import-settings-modal');
|
||||
await expect(settingsModal.locator('.bruno-modal-header-title')).toContainText('OpenAPI Import Settings');
|
||||
|
||||
// click the Import button in the settings modal footer
|
||||
await settingsModal.getByRole('button', { name: 'Import' }).click();
|
||||
|
||||
// Verify that the Import Collection modal is displayed (for location selection)
|
||||
const locationModal = page.getByRole('dialog');
|
||||
await expect(locationModal.locator('.bruno-modal-header-title')).toContainText('Import Collection');
|
||||
@@ -31,7 +30,11 @@ test.describe('Import OpenAPI v3 JSON Collection', () => {
|
||||
// Wait for collection to appear in the location modal
|
||||
await expect(locationModal.getByText('Simple Test API')).toBeVisible();
|
||||
|
||||
// Cleanup: close any open modals
|
||||
await page.locator('[data-test-id="modal-close-button"]').click();
|
||||
// Select a location and import
|
||||
await page.locator('#collection-location').fill(await createTmpDir('simple-test'));
|
||||
await page.getByRole('button', { name: 'Import', exact: true }).click();
|
||||
|
||||
// Verify the collection was imported successfully
|
||||
await expect(page.locator('#sidebar-collection-name').getByText('Simple Test API')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import { test, expect } from '../../../playwright';
|
||||
import * as path from 'path';
|
||||
import { closeAllCollections } from '../../utils/page';
|
||||
|
||||
test.describe('Import OpenAPI v3 YAML Collection', () => {
|
||||
test('Import comprehensive OpenAPI v3 YAML successfully', async ({ page }) => {
|
||||
test.afterEach(async ({ page }) => {
|
||||
// cleanup: close all collections
|
||||
await closeAllCollections(page);
|
||||
});
|
||||
|
||||
test('Import comprehensive OpenAPI v3 YAML successfully', async ({ page, createTmpDir }) => {
|
||||
const openApiFile = path.resolve(__dirname, 'fixtures', 'openapi-comprehensive.yaml');
|
||||
|
||||
await page.getByRole('button', { name: 'Import Collection' }).click();
|
||||
@@ -14,13 +20,6 @@ test.describe('Import OpenAPI v3 YAML Collection', () => {
|
||||
|
||||
await page.setInputFiles('input[type="file"]', openApiFile);
|
||||
|
||||
// verify that the import settings modal appears
|
||||
const settingsModal = page.getByTestId('import-settings-modal');
|
||||
await expect(settingsModal.locator('.bruno-modal-header-title')).toContainText('OpenAPI Import Settings');
|
||||
|
||||
// click the Import button in the settings modal footer
|
||||
await settingsModal.getByRole('button', { name: 'Import' }).click();
|
||||
|
||||
// Wait for the loader to disappear
|
||||
await page.locator('#import-collection-loader').waitFor({ state: 'hidden' });
|
||||
|
||||
@@ -31,7 +30,11 @@ test.describe('Import OpenAPI v3 YAML Collection', () => {
|
||||
// Wait for collection to appear in the location modal
|
||||
await expect(locationModal.getByText('Comprehensive API Test Collection')).toBeVisible();
|
||||
|
||||
// Cleanup: close any open modals
|
||||
await page.locator('[data-test-id="modal-close-button"]').click();
|
||||
// Select a location and import
|
||||
await page.locator('#collection-location').fill(await createTmpDir('comprehensive-test'));
|
||||
await page.getByRole('button', { name: 'Import', exact: true }).click();
|
||||
|
||||
// Verify the collection was imported successfully
|
||||
await expect(page.locator('#sidebar-collection-name').getByText('Comprehensive API Test Collection')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,7 +18,7 @@ test.describe('Invalid OpenAPI - Missing Info Section', () => {
|
||||
await page.locator('#import-collection-loader').waitFor({ state: 'hidden' });
|
||||
|
||||
// The OpenAPI parser might handle missing info gracefully with defaults
|
||||
const hasError = await page.getByText('Import collection failed').first().isVisible();
|
||||
const hasError = await page.getByText('Unsupported collection format').first().isVisible();
|
||||
|
||||
// Either should show an error or create an "Untitled Collection"
|
||||
expect(hasError).toBe(true);
|
||||
|
||||
@@ -21,16 +21,6 @@ test.describe('OpenAPI Newline Handling', () => {
|
||||
// upload the OpenAPI file with problematic operation names
|
||||
await page.setInputFiles('input[type="file"]', openApiFile);
|
||||
|
||||
// verify that the import settings modal appears
|
||||
const settingsModal = page.getByTestId('import-settings-modal');
|
||||
await expect(settingsModal.locator('.bruno-modal-header-title')).toContainText('OpenAPI Import Settings');
|
||||
|
||||
// click the Import button in the settings modal footer
|
||||
await settingsModal.getByRole('button', { name: 'Import' }).click();
|
||||
|
||||
// wait for the file processing to complete
|
||||
await page.locator('#import-collection-loader').waitFor({ state: 'hidden' });
|
||||
|
||||
// verify that the collection location modal appears (OpenAPI files go directly to location modal)
|
||||
const locationModal = page.getByTestId('import-collection-location-modal');
|
||||
await expect(locationModal.getByText('Newline Test Collection')).toBeVisible();
|
||||
|
||||
@@ -24,24 +24,14 @@ test.describe('OpenAPI Path-Based Grouping', () => {
|
||||
// Wait for the loader to disappear
|
||||
await page.locator('#import-collection-loader').waitFor({ state: 'hidden' });
|
||||
|
||||
// Verify that the import settings modal appears
|
||||
const settingsModal = page.getByTestId('import-settings-modal');
|
||||
await expect(settingsModal.locator('.bruno-modal-header-title')).toContainText('OpenAPI Import Settings');
|
||||
|
||||
// Select path-based grouping from the dropdown
|
||||
await settingsModal.getByTestId('grouping-dropdown').click();
|
||||
|
||||
// Wait for dropdown options to be visible (they might be rendered outside the modal)
|
||||
await page.getByTestId('grouping-option-path').waitFor({ state: 'visible' });
|
||||
await page.getByTestId('grouping-option-path').click();
|
||||
|
||||
// Now import the collection with path-based grouping
|
||||
await settingsModal.getByRole('button', { name: 'Import' }).click();
|
||||
|
||||
// Verify that the collection location modal appears
|
||||
const locationModal = page.getByTestId('import-collection-location-modal');
|
||||
await expect(locationModal.getByText('Path Grouping Test API')).toBeVisible();
|
||||
|
||||
// Select path-based grouping from dropdown
|
||||
await page.getByTestId('grouping-dropdown').click();
|
||||
await page.getByTestId('grouping-option-path').click();
|
||||
|
||||
// Select a location and import
|
||||
await page.locator('#collection-location').fill(await createTmpDir('path-grouping-test'));
|
||||
await page.getByRole('button', { name: 'Import', exact: true }).click();
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import { test, expect } from '../../../playwright';
|
||||
import * as path from 'path';
|
||||
import { closeAllCollections } from '../../utils/page';
|
||||
|
||||
test.describe('Import Postman Collection v2.0', () => {
|
||||
test('Import Postman Collection v2.0 successfully', async ({ page }) => {
|
||||
test.afterEach(async ({ page }) => {
|
||||
// cleanup: close all collections
|
||||
await closeAllCollections(page);
|
||||
});
|
||||
|
||||
test('Import Postman Collection v2.0 successfully', async ({ page, createTmpDir }) => {
|
||||
const postmanFile = path.resolve(__dirname, 'fixtures', 'postman-v20.json');
|
||||
|
||||
await page.getByRole('button', { name: 'Import Collection' }).click();
|
||||
@@ -24,7 +30,11 @@ test.describe('Import Postman Collection v2.0', () => {
|
||||
// Wait for collection to appear in the location modal
|
||||
await expect(locationModal.getByText('Postman v2.0 Collection')).toBeVisible();
|
||||
|
||||
// Cleanup: close any open modals
|
||||
await page.locator('[data-test-id="modal-close-button"]').click();
|
||||
// Select a location and import
|
||||
await page.locator('#collection-location').fill(await createTmpDir('postman-v20-test'));
|
||||
await page.getByRole('button', { name: 'Import', exact: true }).click();
|
||||
|
||||
// Verify the collection was imported successfully
|
||||
await expect(page.locator('#sidebar-collection-name').getByText('Postman v2.0 Collection')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import { test, expect } from '../../../playwright';
|
||||
import * as path from 'path';
|
||||
import { closeAllCollections } from '../../utils/page';
|
||||
|
||||
test.describe('Import Postman Collection v2.1', () => {
|
||||
test('Import Postman Collection v2.1 successfully', async ({ page }) => {
|
||||
test.afterEach(async ({ page }) => {
|
||||
// cleanup: close all collections
|
||||
await closeAllCollections(page);
|
||||
});
|
||||
|
||||
test('Import Postman Collection v2.1 successfully', async ({ page, createTmpDir }) => {
|
||||
const postmanFile = path.resolve(__dirname, 'fixtures', 'postman-v21.json');
|
||||
|
||||
await page.getByRole('button', { name: 'Import Collection' }).click();
|
||||
@@ -24,7 +30,11 @@ test.describe('Import Postman Collection v2.1', () => {
|
||||
// Wait for collection to appear in the location modal
|
||||
await expect(locationModal.getByText('Postman v2.1 Collection')).toBeVisible();
|
||||
|
||||
// Cleanup: close any open modals
|
||||
await page.locator('[data-test-id="modal-close-button"]').click();
|
||||
// Select a location and import
|
||||
await page.locator('#collection-location').fill(await createTmpDir('postman-v21-test'));
|
||||
await page.getByRole('button', { name: 'Import', exact: true }).click();
|
||||
|
||||
// Verify the collection was imported successfully
|
||||
await expect(page.locator('#sidebar-collection-name').getByText('Postman v2.1 Collection')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,7 +18,8 @@ test.describe('Invalid Postman Collection - Invalid JSON', () => {
|
||||
await page.locator('#import-collection-loader').waitFor({ state: 'hidden' });
|
||||
|
||||
// Check for error message
|
||||
const hasError = await page.getByText('Conversion failed').first().isVisible();
|
||||
const hasError = await page.getByText('Unsupported collection format').first().isVisible();
|
||||
|
||||
expect(hasError).toBe(true);
|
||||
|
||||
// Cleanup: close any open modals
|
||||
|
||||
@@ -18,7 +18,7 @@ test.describe('Invalid Postman Collection - Missing Info', () => {
|
||||
await page.locator('#import-collection-loader').waitFor({ state: 'hidden' });
|
||||
|
||||
// Check for error message
|
||||
const hasError = await page.getByText('Import collection failed').first().isVisible();
|
||||
const hasError = await page.getByText('Unsupported collection format').first().isVisible();
|
||||
expect(hasError).toBe(true);
|
||||
|
||||
// Cleanup: close any open modals
|
||||
|
||||
@@ -18,7 +18,7 @@ test.describe('Invalid Postman Collection - Invalid Schema', () => {
|
||||
await page.locator('#import-collection-loader').waitFor({ state: 'hidden' });
|
||||
|
||||
// Check for error message
|
||||
const hasError = await page.getByText('Conversion failed').first().isVisible();
|
||||
const hasError = await page.getByText('Unsupported collection format').first().isVisible();
|
||||
expect(hasError).toBe(true);
|
||||
|
||||
// Cleanup: close any open modals
|
||||
|
||||
@@ -18,7 +18,7 @@ test.describe('Invalid Postman Collection - Malformed Structure', () => {
|
||||
await page.locator('#import-collection-loader').waitFor({ state: 'hidden' });
|
||||
|
||||
// Check for error message
|
||||
const hasError = await page.getByText('Import collection failed').first().isVisible();
|
||||
const hasError = await page.getByText('Unsupported collection format').first().isVisible();
|
||||
expect(hasError).toBe(true);
|
||||
|
||||
// Cleanup: close any open modals
|
||||
|
||||
@@ -32,12 +32,10 @@ test.describe('Import WSDL Collection', () => {
|
||||
const locationModal = page.getByRole('dialog');
|
||||
await expect(locationModal.locator('.bruno-modal-header-title')).toContainText('Import Collection');
|
||||
|
||||
// Wait for collection to appear in the location modal
|
||||
await expect(locationModal.getByText('TestWSDLServiceXML')).toBeVisible();
|
||||
|
||||
// select a location
|
||||
await page.locator('#collection-location').fill(await createTmpDir('wsdl-xml-test'));
|
||||
await page.getByRole('button', { name: 'Import', exact: true }).click();
|
||||
await expect(page.locator('#sidebar-collection-name').getByText('TestWSDLServiceXML')).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('Verify that the collection was imported successfully', async () => {
|
||||
|
||||
Reference in New Issue
Block a user