diff --git a/.gitignore b/.gitignore index 33e25480..990068b0 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,7 @@ chrome-extension chrome-extension.pem chrome-extension.crx bruno.zip +*.zip # misc .DS_Store diff --git a/packages/bruno-app/src/components/Modal/StyledWrapper.js b/packages/bruno-app/src/components/Modal/StyledWrapper.js index 0b140347..b8d0b5c2 100644 --- a/packages/bruno-app/src/components/Modal/StyledWrapper.js +++ b/packages/bruno-app/src/components/Modal/StyledWrapper.js @@ -144,6 +144,13 @@ const Wrapper = styled.div` border-bottom-left-radius: 3px; border-bottom-right-radius: 3px; } + + &.modal-footer-none { + .bruno-modal-content { + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; + } + } `; export default Wrapper; diff --git a/packages/bruno-app/src/components/Modal/index.js b/packages/bruno-app/src/components/Modal/index.js index bc7d9079..0e888cda 100644 --- a/packages/bruno-app/src/components/Modal/index.js +++ b/packages/bruno-app/src/components/Modal/index.js @@ -64,6 +64,9 @@ const Modal = ({ size, title, confirmText, cancelText, handleCancel, handleConfi if (isClosing) { classes += ' modal--animate-out'; } + if(hideFooter) { + classes += ' modal-footer-none'; + } return (
diff --git a/packages/bruno-app/src/components/Sidebar/ImportCollection/index.js b/packages/bruno-app/src/components/Sidebar/ImportCollection/index.js new file mode 100644 index 00000000..e922d796 --- /dev/null +++ b/packages/bruno-app/src/components/Sidebar/ImportCollection/index.js @@ -0,0 +1,57 @@ +import React from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { collectionImported } from 'providers/ReduxStore/slices/collections'; +import importBrunoCollection from 'utils/importers/bruno-collection'; +import importPostmanCollection from 'utils/importers/postman-collection'; +import { addCollectionToWorkspace } from 'providers/ReduxStore/slices/workspaces/actions'; +import { toastError } from 'utils/common/error'; +import toast from 'react-hot-toast'; +import Modal from 'components/Modal'; + +const ImportCollection = ({ onClose }) => { + const dispatch = useDispatch(); + const { activeWorkspaceUid } = useSelector((state) => state.workspaces); + + const handleImportBrunoCollection = () => { + importBrunoCollection() + .then((collection) => { + dispatch(collectionImported({ collection: collection })); + dispatch(addCollectionToWorkspace(activeWorkspaceUid, collection.uid)); + toast.success('Collection imported successfully'); + onClose(); + }) + .catch((err) => toastError(err, 'Import collection failed')); + }; + + const handleImportPostmanCollection = () => { + importPostmanCollection() + .then((collection) => { + dispatch(collectionImported({ collection: collection })); + dispatch(addCollectionToWorkspace(activeWorkspaceUid, collection.uid)); + toast.success('Postman Collection imported successfully'); + onClose(); + }) + .catch((err) => toastError(err, 'Postman Import collection failed')); + }; + + return ( + +
+
+ Bruno Collection +
+
+ Postman Collection +
+
+
+ ); +}; + +export default ImportCollection; diff --git a/packages/bruno-app/src/components/Sidebar/TitleBar/index.js b/packages/bruno-app/src/components/Sidebar/TitleBar/index.js index fbce081b..189d9fc3 100644 --- a/packages/bruno-app/src/components/Sidebar/TitleBar/index.js +++ b/packages/bruno-app/src/components/Sidebar/TitleBar/index.js @@ -2,8 +2,8 @@ import toast from 'react-hot-toast'; import Bruno from 'components/Bruno'; import Dropdown from 'components/Dropdown'; import CreateCollection from '../CreateCollection'; -import importCollection from 'utils/collections/import'; import SelectCollection from 'components/Sidebar/Collections/SelectCollection'; +import ImportCollection from 'components/Sidebar/ImportCollection'; import { IconDots } from '@tabler/icons'; import { IconFolders } from '@tabler/icons'; @@ -11,13 +11,13 @@ import { isElectron } from 'utils/common/platform'; import { useState, forwardRef, useRef } from 'react'; import { useSelector, useDispatch } from 'react-redux'; import { showHomePage } from 'providers/ReduxStore/slices/app'; -import { collectionImported } from 'providers/ReduxStore/slices/collections'; import { openLocalCollection } from 'providers/ReduxStore/slices/collections/actions'; import { addCollectionToWorkspace } from 'providers/ReduxStore/slices/workspaces/actions'; import StyledWrapper from './StyledWrapper'; const TitleBar = () => { const [createCollectionModalOpen, setCreateCollectionModalOpen] = useState(false); + const [importCollectionModalOpen, setImportCollectionModalOpen] = useState(false); const [addCollectionToWSModalOpen, setAddCollectionToWSModalOpen] = useState(false); const { activeWorkspaceUid } = useSelector((state) => state.workspaces); const isPlatformElectron = isElectron(); @@ -48,18 +48,10 @@ const TitleBar = () => { .catch(() => toast.error('An error occured while adding collection to workspace')); }; - const handleImportCollection = () => { - importCollection() - .then((collection) => { - dispatch(collectionImported({ collection: collection })); - dispatch(addCollectionToWorkspace(activeWorkspaceUid, collection.uid)); - }) - .catch((err) => console.log(err)); - }; - return ( {createCollectionModalOpen ? setCreateCollectionModalOpen(false)} /> : null} + {importCollectionModalOpen ? setImportCollectionModalOpen(false)} /> : null} {addCollectionToWSModalOpen ? ( setAddCollectionToWSModalOpen(false)} onSelect={handleAddCollectionToWorkspace} /> @@ -91,7 +83,7 @@ const TitleBar = () => { className="dropdown-item" onClick={(e) => { menuDropdownTippyRef.current.hide(); - handleImportCollection(); + setImportCollectionModalOpen(true); }} > Import Collection diff --git a/packages/bruno-app/src/components/Welcome/index.js b/packages/bruno-app/src/components/Welcome/index.js index 09801843..33a7eaed 100644 --- a/packages/bruno-app/src/components/Welcome/index.js +++ b/packages/bruno-app/src/components/Welcome/index.js @@ -10,13 +10,15 @@ import { IconBrandGithub, IconPlus, IconUpload, IconFiles, IconFolders, IconPlay import Bruno from 'components/Bruno'; import CreateCollection from 'components/Sidebar/CreateCollection'; import SelectCollection from 'components/Sidebar/Collections/SelectCollection'; -import importCollection, { importSampleCollection } from 'utils/collections/import'; +import { importSampleCollection } from 'utils/importers/bruno-collection'; +import ImportCollection from 'components/Sidebar/ImportCollection'; import StyledWrapper from './StyledWrapper'; const Welcome = () => { const dispatch = useDispatch(); const [createCollectionModalOpen, setCreateCollectionModalOpen] = useState(false); const [addCollectionToWSModalOpen, setAddCollectionToWSModalOpen] = useState(false); + const [importCollectionModalOpen, setImportCollectionModalOpen] = useState(false); const { activeWorkspaceUid } = useSelector((state) => state.workspaces); const isPlatformElectron = isElectron(); @@ -29,15 +31,6 @@ const Welcome = () => { .catch(() => toast.error('An error occured while adding collection to workspace')); }; - const handleImportCollection = () => { - importCollection() - .then((collection) => { - dispatch(collectionImported({ collection: collection })); - dispatch(addCollectionToWorkspace(activeWorkspaceUid, collection.uid)); - }) - .catch((err) => console.log(err)); - }; - const handleImportSampleCollection = () => { importSampleCollection() .then((collection) => { @@ -58,6 +51,7 @@ const Welcome = () => { return ( {createCollectionModalOpen ? setCreateCollectionModalOpen(false)} /> : null} + {importCollectionModalOpen ? setImportCollectionModalOpen(false)} /> : null} {addCollectionToWSModalOpen ? ( setAddCollectionToWSModalOpen(false)} onSelect={handleAddCollectionToWorkspace} /> @@ -83,7 +77,7 @@ const Welcome = () => { Add Collection to Workspace
-
+
setImportCollectionModalOpen(true)}> Import Collection
diff --git a/packages/bruno-app/src/utils/collections/import.js b/packages/bruno-app/src/utils/collections/import.js deleted file mode 100644 index 3b8b0198..00000000 --- a/packages/bruno-app/src/utils/collections/import.js +++ /dev/null @@ -1,99 +0,0 @@ -import each from 'lodash/each'; -import get from 'lodash/get'; -import fileDialog from 'file-dialog'; -import toast from 'react-hot-toast'; -import cloneDeep from 'lodash/cloneDeep'; -import { uuid } from 'utils/common'; -import { collectionSchema } from '@usebruno/schema'; -import { saveCollectionToIdb } from 'utils/idb'; -import sampleCollection from './samples/sample-collection.json'; - -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) { - toast.error('Unable to parse the collection json file'); - reject(err); - } - }); -}; - -const validateSchema = (collection = {}) => { - return new Promise((resolve, reject) => { - collectionSchema - .validate(collection) - .then(() => resolve(collection)) - .catch((err) => { - toast.error('The Collection file is corrupted'); - reject(err); - }); - }); -}; - -const updateUidsInCollection = (_collection) => { - const collection = cloneDeep(_collection); - - collection.uid = uuid(); - - const updateItemUids = (items = []) => { - each(items, (item) => { - item.uid = uuid(); - - each(get(item, 'request.headers'), (header) => (header.uid = uuid())); - each(get(item, 'request.params'), (param) => (param.uid = uuid())); - each(get(item, 'request.body.multipartForm'), (param) => (param.uid = uuid())); - each(get(item, 'request.body.formUrlEncoded'), (param) => (param.uid = uuid())); - - if (item.items && item.items.length) { - updateItemUids(item.items); - } - }); - }; - updateItemUids(collection.items); - - return collection; -}; - -const importCollection = () => { - return new Promise((resolve, reject) => { - fileDialog({ accept: 'application/json' }) - .then(readFile) - .then(parseJsonCollection) - .then(validateSchema) - .then(updateUidsInCollection) - .then(validateSchema) - .then((collection) => saveCollectionToIdb(window.__idb, collection)) - .then((collection) => { - toast.success('Collection imported successfully'); - resolve(collection); - }) - .catch((err) => { - toast.error('Import collection failed'); - reject(err); - }); - }); -}; - -export const importSampleCollection = () => { - return new Promise((resolve, reject) => { - validateSchema(sampleCollection) - .then(updateUidsInCollection) - .then(validateSchema) - .then((collection) => saveCollectionToIdb(window.__idb, collection)) - .then(resolve) - .catch(reject); - }); -}; - -export default importCollection; diff --git a/packages/bruno-app/src/utils/importers/bruno-collection.js b/packages/bruno-app/src/utils/importers/bruno-collection.js new file mode 100644 index 00000000..847b2f61 --- /dev/null +++ b/packages/bruno-app/src/utils/importers/bruno-collection.js @@ -0,0 +1,56 @@ +import fileDialog from 'file-dialog'; +import { saveCollectionToIdb } from 'utils/idb'; +import { BrunoError } from 'utils/common/error'; +import { validateSchema, updateUidsInCollection } from './common'; +import sampleCollection from './samples/sample-collection.json'; + +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')); + } + }); +}; + +const importCollection = () => { + return new Promise((resolve, reject) => { + fileDialog({ accept: 'application/json' }) + .then(readFile) + .then(parseJsonCollection) + .then(validateSchema) + .then(updateUidsInCollection) + .then(validateSchema) + .then((collection) => saveCollectionToIdb(window.__idb, collection)) + .then((collection) => resolve(collection)) + .catch((err) => { + console.log(err); + reject(new BrunoError('Import collection failed')); + }); + }); +}; + +export const importSampleCollection = () => { + return new Promise((resolve, reject) => { + validateSchema(sampleCollection) + .then(updateUidsInCollection) + .then(validateSchema) + .then((collection) => saveCollectionToIdb(window.__idb, collection)) + .then(resolve) + .catch(reject); + }); +}; + +export default importCollection; diff --git a/packages/bruno-app/src/utils/importers/common.js b/packages/bruno-app/src/utils/importers/common.js new file mode 100644 index 00000000..eee7108f --- /dev/null +++ b/packages/bruno-app/src/utils/importers/common.js @@ -0,0 +1,44 @@ + +import each from 'lodash/each'; +import get from 'lodash/get'; + +import cloneDeep from 'lodash/cloneDeep'; +import { uuid } from 'utils/common'; +import { collectionSchema } from '@usebruno/schema'; +import { BrunoError } from 'utils/common/error'; + +export const validateSchema = (collection = {}) => { + return new Promise((resolve, reject) => { + collectionSchema + .validate(collection) + .then(() => resolve(collection)) + .catch((err) => { + console.log(err); + reject(new BrunoError('The Collection file is corrupted')); + }); + }); +}; + +export const updateUidsInCollection = (_collection) => { + const collection = cloneDeep(_collection); + + collection.uid = uuid(); + + const updateItemUids = (items = []) => { + each(items, (item) => { + item.uid = uuid(); + + each(get(item, 'request.headers'), (header) => (header.uid = uuid())); + each(get(item, 'request.params'), (param) => (param.uid = uuid())); + each(get(item, 'request.body.multipartForm'), (param) => (param.uid = uuid())); + each(get(item, 'request.body.formUrlEncoded'), (param) => (param.uid = uuid())); + + if (item.items && item.items.length) { + updateItemUids(item.items); + } + }); + }; + updateItemUids(collection.items); + + return collection; +}; diff --git a/packages/bruno-app/src/utils/importers/postman-collection.js b/packages/bruno-app/src/utils/importers/postman-collection.js new file mode 100644 index 00000000..f910ff74 --- /dev/null +++ b/packages/bruno-app/src/utils/importers/postman-collection.js @@ -0,0 +1,187 @@ +import each from 'lodash/each'; +import get from 'lodash/get'; +import fileDialog from 'file-dialog'; +import { uuid } from 'utils/common'; +import { saveCollectionToIdb } from 'utils/idb'; +import { BrunoError } from 'utils/common/error'; +import { validateSchema, updateUidsInCollection } 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 isItemAFolder = (item) => { + return !item.request; +}; + +const importPostmanV2CollectionItem = (brunoParent, item) => { + brunoParent.items = brunoParent.items || []; + + each(item, (i) => { + if(isItemAFolder(i)) { + const brunoFolderItem = { + uid: uuid(), + name: i.name, + type: 'folder', + items: [] + }; + brunoParent.items.push(brunoFolderItem); + if(i.item && i.item.length) { + importPostmanV2CollectionItem(brunoFolderItem, i.item); + } + } else { + if(i.request) { + const brunoRequestItem = { + uid: uuid(), + name: i.name, + type: 'http-request', + request: { + url: get(i, 'request.url.raw'), + method: i.request.method, + headers: [], + params: [], + body: { + mode: 'none', + json: null, + text: null, + xml: null, + formUrlEncoded: [], + multipartForm: [] + } + } + }; + + const bodyMode = get(i, 'request.body.mode'); + if(bodyMode) { + if(bodyMode === 'formdata') { + brunoRequestItem.request.body.mode = 'multipartForm'; + each(i.request.body.formdata, (param) => { + brunoRequestItem.request.body.formUrlEncoded.push({ + uid: uuid(), + name: param.key, + value: param.value, + description: param.description, + enabled: !param.disabled + }); + }); + } + + if(bodyMode === 'urlencoded') { + brunoRequestItem.request.body.mode = 'formUrlEncoded'; + each(i.request.body.urlencoded, (param) => { + brunoRequestItem.request.body.formUrlEncoded.push({ + uid: uuid(), + name: param.key, + value: param.value, + description: param.description, + enabled: !param.disabled + }); + }); + } + + if(bodyMode === 'raw') { + const language = get(i, 'request.body.options.raw.language'); + if(language === 'json') { + brunoRequestItem.request.body.mode = 'json'; + brunoRequestItem.request.body.json = i.request.body.raw; + } else if (language === 'xml') { + brunoRequestItem.request.body.mode = 'xml'; + brunoRequestItem.request.body.xml = i.request.body.raw; + } else { + brunoRequestItem.request.body.mode = 'text'; + brunoRequestItem.request.body.text = i.request.body.raw; + } + } + } + + each(i.request.header, (header) => { + brunoRequestItem.request.headers.push({ + uid: uuid(), + name: header.key, + value: header.value, + description: header.description, + enabled: !header.disabled + }); + }); + + each(get(i, 'request.url.query'), (param) => { + brunoRequestItem.request.params.push({ + uid: uuid(), + name: param.key, + value: param.value, + description: param.description, + enabled: !param.disabled + }); + }); + + brunoParent.items.push(brunoRequestItem); + } + } + }); +}; + +const importPostmanV2Collection = (collection) => { + const brunoCollection = { + name: collection.info.name, + uid: uuid(), + version: "1", + items: [], + environments: [] + }; + + importPostmanV2CollectionItem(brunoCollection, collection.item); + + return brunoCollection; +}; + + +const parsePostmanCollection = (str) => { + return new Promise((resolve, reject) => { + try { + let collection = JSON.parse(str); + let schema = get(collection, 'info.schema'); + + let v2Schemas = [ + 'https://schema.getpostman.com/json/collection/v2.0.0/collection.json', + 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json' + ]; + + if(v2Schemas.includes(schema)) { + return resolve(importPostmanV2Collection(collection)); + } + + throw new BrunoError('Unknown postman schema'); + } catch (err) { + console.log(err); + if(err instanceof BrunoError) { + return reject(err); + } + + return reject(new BrunoError('Unable to parse the postman collection json file')); + } + }); +}; + +const importCollection = () => { + return new Promise((resolve, reject) => { + fileDialog({ accept: 'application/json' }) + .then(readFile) + .then(parsePostmanCollection) + .then(validateSchema) + .then(updateUidsInCollection) + .then(validateSchema) + .then((collection) => saveCollectionToIdb(window.__idb, collection)) + .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/collections/samples/sample-collection.json b/packages/bruno-app/src/utils/importers/samples/sample-collection.json similarity index 100% rename from packages/bruno-app/src/utils/collections/samples/sample-collection.json rename to packages/bruno-app/src/utils/importers/samples/sample-collection.json