diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/CollectionItemDragPreview/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/CollectionItemDragPreview/index.js index 1ad4065a8..fa1fb960b 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/CollectionItemDragPreview/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/CollectionItemDragPreview/index.js @@ -30,8 +30,9 @@ export const CollectionItemDragPreview = () => { clientOffset: monitor.getClientOffset(), })); if (!isDragging) return null; + if (!item.type) return null; const { x, y } = clientOffset || {}; - const shouldShowFolderIcon = !item.type || item.type === 'folder'; + const shouldShowFolderIcon = item.type === 'folder'; return (
diff --git a/packages/bruno-app/src/components/Sidebar/ImportCollection/index.js b/packages/bruno-app/src/components/Sidebar/ImportCollection/index.js index 0b71125c8..7d1c6599d 100644 --- a/packages/bruno-app/src/components/Sidebar/ImportCollection/index.js +++ b/packages/bruno-app/src/components/Sidebar/ImportCollection/index.js @@ -1,126 +1,204 @@ -import React, { useState, useEffect } from 'react'; -import { IconLoader2 } from '@tabler/icons'; -import importBrunoCollection from 'utils/importers/bruno-collection'; -import { postmanToBruno, readFile } from 'utils/importers/postman-collection'; -import importInsomniaCollection from 'utils/importers/insomnia-collection'; -import importOpenapiCollection from 'utils/importers/openapi-collection'; +import React, { useState, useEffect, useRef } from 'react'; +import { IconLoader2, IconFileImport } from '@tabler/icons'; import { toastError } from 'utils/common/error'; import Modal from 'components/Modal'; -import fileDialog from 'file-dialog'; +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 { processBrunoCollection } from 'utils/importers/bruno-collection'; + +const convertFileToObject = async (file) => { + const text = await file.text(); + + try { + if (file.type === 'application/json' || file.name.endsWith('.json')) { + return JSON.parse(text); + } + + const parsed = jsyaml.load(text); + if (typeof parsed !== 'object' || parsed === null) { + throw new Error(); + } + return parsed; + } catch { + throw new Error('Failed to parse the file – ensure it is valid JSON or YAML'); + } +}; + +const FullscreenLoader = ({ isLoading }) => { + const [loadingMessage, setLoadingMessage] = useState(''); + + // Messages to cycle through while loading + const loadingMessages = [ + 'Processing collection...', + 'Analyzing requests...', + 'Translating scripts...', + 'Preparing collection...', + 'Almost done...' + ]; + + useEffect(() => { + if (!isLoading) return; + + let messageIndex = 0; + const interval = setInterval(() => { + messageIndex = (messageIndex + 1) % loadingMessages.length; + setLoadingMessage(loadingMessages[messageIndex]); + }, 2000); + + setLoadingMessage(loadingMessages[0]); + + return () => clearInterval(interval); + }, [isLoading]); + + return ( +
+
+ +

+ {loadingMessage} +

+

+ This may take a moment depending on the collection size +

+
+
+ ); +}; const ImportCollection = ({ onClose, handleSubmit }) => { - const [isLoading, setIsLoading] = useState(false) + const [isLoading, setIsLoading] = useState(false); + const [dragActive, setDragActive] = useState(false); + const fileInputRef = useRef(null); - const handleImportBrunoCollection = () => { - importBrunoCollection() - .then(({ collection }) => { - handleSubmit({ collection }); - }) - .catch((err) => toastError(err, 'Import collection failed')) + const handleDrag = (e) => { + e.preventDefault(); + e.stopPropagation(); + + if (e.dataTransfer) { + e.dataTransfer.dropEffect = 'copy'; + } + + if (e.type === 'dragenter' || e.type === 'dragover') { + setDragActive(true); + } else if (e.type === 'dragleave') { + setDragActive(false); + } }; + const processFile = async (file) => { + setIsLoading(true); + try { + const data = await convertFileToObject(file); + + if (!data) { + throw new Error('Failed to parse file content'); + } + + let collection; + + if (isPostmanCollection(data)) { + collection = await postmanToBruno(data); + } + else if (isInsomniaCollection(data)) { + collection = convertInsomniaToBruno(data); + } + else if (isOpenApiSpec(data)) { + collection = convertOpenapiToBruno(data); + } + else { + collection = await processBrunoCollection(data); + } + + handleSubmit({ collection }); + } catch (err) { + toastError(err, 'Import collection failed'); + } finally { + setIsLoading(false); + } + }; - const handleImportPostmanCollection = () => { - fileDialog({ accept: 'application/json' }) - .then((...args) => { - setIsLoading(true); - return readFile(...args); - }) - .then((collection) => postmanToBruno(collection)) - .then((collection) => handleSubmit({ collection })) - .catch((err) => toastError(err, 'Postman Import collection failed')) - .finally(() => setIsLoading(false)); + const handleDrop = async (e) => { + e.preventDefault(); + e.stopPropagation(); + setDragActive(false); + + if (e.dataTransfer.files && e.dataTransfer.files[0]) { + await processFile(e.dataTransfer.files[0]); + } + }; + + const handleBrowseFiles = () => { + fileInputRef.current.click(); + }; + + const handleFileInputChange = async (e) => { + if (e.target.files && e.target.files[0]) { + await processFile(e.target.files[0]); + } + }; + + if (isLoading) { + return ; } - const handleImportInsomniaCollection = () => { - importInsomniaCollection() - .then(({ collection }) => { - handleSubmit({ collection }); - }) - .catch((err) => toastError(err, 'Insomnia Import collection failed')) - }; + const acceptedFileTypes = [ + '.json', + '.yaml', + '.yml', + 'application/json', + 'application/yaml', + 'application/x-yaml' + ] - const handleImportOpenapiCollection = () => { - importOpenapiCollection() - .then(({ collection }) => { - handleSubmit({ collection }); - }) - .catch((err) => toastError(err, 'OpenAPI v3 Import collection failed')) - }; - - const CollectionButton = ({ children, className, onClick }) => { - return ( - - ); - }; - - const FullscreenLoader = () => { - const [loadingMessage, setLoadingMessage] = useState(''); - - // Messages to cycle through while loading - const loadingMessages = [ - 'Processing collection...', - 'Analyzing requests...', - 'Translating scripts...', - 'Preparing collection...', - 'Almost done...' - ]; - - - // Cycle through loading messages for better UX - useEffect(() => { - if (!isLoading) return; - - let messageIndex = 0; - const interval = setInterval(() => { - messageIndex = (messageIndex + 1) % loadingMessages.length; - setLoadingMessage(loadingMessages[messageIndex]); - }, 2000); - - setLoadingMessage(loadingMessages[0]); - - return () => clearInterval(interval); - }, [isLoading]); - - return ( -
-
- -

- {loadingMessage} -

-

- This may take a moment depending on the collection size -

-
-
- ); - }; - return ( - <> - {isLoading && } - {!isLoading && ( - -
-

Select the type of your existing collection :

-
- Bruno Collection - Postman Collection - Insomnia Collection - OpenAPI V3 Spec + +
+
+

Import from file

+
+
+ + +

+ Drop file to import or{' '} + +

+

+ Supports Bruno, Postman, Insomnia, and OpenAPI v3 formats +

- - )} - +
+
+
); }; diff --git a/packages/bruno-app/src/utils/importers/bruno-collection.js b/packages/bruno-app/src/utils/importers/bruno-collection.js index d96802c39..b6327b41d 100644 --- a/packages/bruno-app/src/utils/importers/bruno-collection.js +++ b/packages/bruno-app/src/utils/importers/bruno-collection.js @@ -1,43 +1,16 @@ -import fileDialog from 'file-dialog'; import { BrunoError } from 'utils/common/error'; import { validateSchema, transformItemsInCollection, updateUidsInCollection, hydrateSeqInCollection } from './common'; -const readFile = (files) => { - return new Promise((resolve, reject) => { - const fileReader = new FileReader(); - fileReader.onload = (e) => resolve(e.target.result); - fileReader.onerror = (err) => reject(err); - fileReader.readAsText(files[0]); - }); -}; -const parseJsonCollection = (str) => { - return new Promise((resolve, reject) => { - try { - let parsed = JSON.parse(str); - return resolve(parsed); - } catch (err) { - console.log(err); - reject(new BrunoError('Unable to parse the collection json file')); - } - }); +export const processBrunoCollection = async (jsonData) => { + try { + let collection = hydrateSeqInCollection(jsonData); + collection = updateUidsInCollection(collection); + collection = transformItemsInCollection(collection); + await validateSchema(collection); + return collection; + } catch (err) { + console.error('Error processing Bruno collection:', err); + throw new BrunoError('Import collection failed'); + } }; - -const importCollection = () => { - return new Promise((resolve, reject) => { - fileDialog({ accept: 'application/json' }) - .then(readFile) - .then(parseJsonCollection) - .then(hydrateSeqInCollection) - .then(updateUidsInCollection) - .then(transformItemsInCollection) - .then(validateSchema) - .then((collection) => resolve({ collection })) - .catch((err) => { - console.log(err); - reject(new BrunoError('Import collection failed')); - }); - }); -}; - -export default importCollection; diff --git a/packages/bruno-app/src/utils/importers/insomnia-collection.js b/packages/bruno-app/src/utils/importers/insomnia-collection.js index c81efaee7..5e41b5832 100644 --- a/packages/bruno-app/src/utils/importers/insomnia-collection.js +++ b/packages/bruno-app/src/utils/importers/insomnia-collection.js @@ -1,43 +1,26 @@ -import jsyaml from 'js-yaml'; -import fileDialog from 'file-dialog'; import { BrunoError } from 'utils/common/error'; import { insomniaToBruno } from '@usebruno/converters'; -const readFile = (files) => { - return new Promise((resolve, reject) => { - const fileReader = new FileReader(); - fileReader.onload = (e) => { - try { - // try to load JSON - const parsedData = JSON.parse(e.target.result); - resolve(parsedData); - } catch (jsonError) { - // not a valid JSOn, try yaml - try { - const parsedData = jsyaml.load(e.target.result, { schema: jsyaml.CORE_SCHEMA }); - resolve(parsedData); - } catch (yamlError) { - console.error('Error parsing the file :', jsonError, yamlError); - reject(new BrunoError('Import collection failed')); - } - } - }; - fileReader.onerror = (err) => reject(err); - fileReader.readAsText(files[0]); - }); + +export const convertInsomniaToBruno = (data) => { + try { + return insomniaToBruno(data); + } catch (err) { + console.error('Error converting Insomnia to Bruno:', err); + throw new BrunoError('Import collection failed: ' + err.message); + } }; -const importCollection = () => { - return new Promise((resolve, reject) => { - fileDialog({ accept: '.json, .yaml, .yml, application/json, application/yaml, application/x-yaml' }) - .then(readFile) - .then((collection) => insomniaToBruno(collection)) - .then((collection) => resolve({ collection })) - .catch((err) => { - console.error(err); - reject(new BrunoError('Import collection failed: ' + err.message)); - }); - }); -}; +export const isInsomniaCollection = (data) => { + // Check for Insomnia v5 collection format – collection array must be present + if (typeof data.type === 'string' && data.type.startsWith('collection.insomnia.rest/5')) { + return Array.isArray(data.collection); + } -export default importCollection; + // Check for Insomnia v4 export format – must have __export_format and resources array + if (data._type === 'export') { + return Array.isArray(data.resources) && typeof data.__export_format === 'number'; + } + + return false; +}; diff --git a/packages/bruno-app/src/utils/importers/openapi-collection.js b/packages/bruno-app/src/utils/importers/openapi-collection.js index 70cbe918c..5e619d97b 100644 --- a/packages/bruno-app/src/utils/importers/openapi-collection.js +++ b/packages/bruno-app/src/utils/importers/openapi-collection.js @@ -1,43 +1,27 @@ -import jsyaml from 'js-yaml'; -import fileDialog from 'file-dialog'; import { BrunoError } from 'utils/common/error'; import { openApiToBruno } from '@usebruno/converters'; -const readFile = (files) => { - return new Promise((resolve, reject) => { - const fileReader = new FileReader(); - fileReader.onload = (e) => { - try { - // try to load JSON - const parsedData = JSON.parse(e.target.result); - resolve(parsedData); - } catch (jsonError) { - // not a valid JSOn, try yaml - try { - const parsedData = jsyaml.load(e.target.result); - resolve(parsedData); - } catch (yamlError) { - console.error('Error parsing the file :', jsonError, yamlError); - reject(new BrunoError('Import collection failed')); - } - } - }; - fileReader.onerror = (err) => reject(err); - fileReader.readAsText(files[0]); - }); +export const convertOpenapiToBruno = (data) => { + try { + return openApiToBruno(data); + } catch (err) { + console.error('Error converting OpenAPI to Bruno:', err); + throw new BrunoError('Import collection failed: ' + err.message); + } }; -const importCollection = () => { - return new Promise((resolve, reject) => { - fileDialog({ accept: '.json, .yaml, .yml, application/json, application/yaml, application/x-yaml' }) - .then(readFile) - .then((collection) => openApiToBruno(collection)) - .then((collection) => resolve({ collection })) - .catch((err) => { - console.error(err); - reject(new BrunoError('Import collection failed: ' + err.message)); - }); - }); -}; +export const isOpenApiSpec = (data) => { + if (typeof data.info !== 'object' || data.info === null) { + return false; + } -export default importCollection; + if (typeof data.openapi === 'string' && data.openapi.trim().length) { + return true; + } + + if (typeof data.swagger === 'string' && data.swagger.trim().length) { + return true; + } + + return false; +}; diff --git a/packages/bruno-app/src/utils/importers/postman-collection.js b/packages/bruno-app/src/utils/importers/postman-collection.js index b9cceed65..4622c2bb5 100644 --- a/packages/bruno-app/src/utils/importers/postman-collection.js +++ b/packages/bruno-app/src/utils/importers/postman-collection.js @@ -22,4 +22,20 @@ const postmanToBruno = (collection) => { }); }; -export { postmanToBruno, readFile }; +const isPostmanCollection = (data) => { + const info = data.info; + if (!info || typeof info !== 'object') { + return false; + } + + 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; + } + + return false; +}; + +export { postmanToBruno, readFile, isPostmanCollection };