diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/ExportCollection/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/ExportCollection/index.js new file mode 100644 index 000000000..0a1c7cd8a --- /dev/null +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/ExportCollection/index.js @@ -0,0 +1,37 @@ +import React from 'react'; +import exportBrunoCollection from 'utils/collections/export'; +import exportPostmanCollection from 'utils/exporters/postman-collection'; +import { toastError } from 'utils/common/error'; +import cloneDeep from 'lodash/cloneDeep'; +import Modal from 'components/Modal'; +import { transformCollectionToSaveToExportAsFile } from 'utils/collections/index'; + +const ExportCollection = ({ onClose, collection }) => { + const handleExportBrunoCollection = () => { + const collectionCopy = cloneDeep(collection); + exportBrunoCollection(transformCollectionToSaveToExportAsFile(collectionCopy)); + onClose(); + }; + + const handleExportPostmanCollection = () => { + const collectionCopy = cloneDeep(collection); + exportPostmanCollection(collectionCopy); + // exportPostmanCollection(transformCollectionToSaveToExportAsFile(collectionCopy)); + onClose(); + }; + + return ( + +
+
+ Bruno Collection +
+
+ Postman Collection +
+
+
+ ); +}; + +export default ExportCollection; diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js index 41274753b..37683d6ee 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js @@ -14,6 +14,7 @@ import NewRequest from 'components/Sidebar/NewRequest'; import NewFolder from 'components/Sidebar/NewFolder'; import CollectionItem from './CollectionItem'; import RemoveCollection from './RemoveCollection'; +import ExportCollection from './ExportCollection'; import CollectionProperties from './CollectionProperties'; import { doesCollectionHaveItemsMatchingSearchText } from 'utils/collections/search'; import { isItemAFolder, isItemARequest, transformCollectionToSaveToExportAsFile } from 'utils/collections'; @@ -26,6 +27,7 @@ const Collection = ({ collection, searchText }) => { const [showNewFolderModal, setShowNewFolderModal] = useState(false); const [showNewRequestModal, setShowNewRequestModal] = useState(false); const [showRenameCollectionModal, setShowRenameCollectionModal] = useState(false); + const [showExportCollectionModal, setShowExportCollectionModal] = useState(false); const [showRemoveCollectionModal, setShowRemoveCollectionModal] = useState(false); const [collectionPropertiesModal, setCollectionPropertiesModal] = useState(false); const [collectionIsCollapsed, setCollectionIsCollapsed] = useState(collection.collapsed); @@ -129,6 +131,9 @@ const Collection = ({ collection, searchText }) => { {showRemoveCollectionModal && ( setShowRemoveCollectionModal(false)} /> )} + {showExportCollectionModal && ( + setShowExportCollectionModal(false)} /> + )} {collectionPropertiesModal && ( setCollectionPropertiesModal(false)} /> )} @@ -186,7 +191,7 @@ const Collection = ({ collection, searchText }) => { className="dropdown-item" onClick={(e) => { menuDropdownTippyRef.current.hide(); - handleExportClick(true); + setShowExportCollectionModal(true); }} > Export diff --git a/packages/bruno-app/src/utils/collections/export.js b/packages/bruno-app/src/utils/collections/export.js index 64fc0da91..e5a68bc66 100644 --- a/packages/bruno-app/src/utils/collections/export.js +++ b/packages/bruno-app/src/utils/collections/export.js @@ -2,7 +2,7 @@ import * as FileSaver from 'file-saver'; import get from 'lodash/get'; import each from 'lodash/each'; -const deleteUidsInItems = (items) => { +export const deleteUidsInItems = (items) => { each(items, (item) => { delete item.uid; @@ -26,7 +26,7 @@ const deleteUidsInItems = (items) => { * Some of the models in the app are not consistent with the Collection Json format * This function is used to transform the models to the Collection Json format */ -const transformItem = (items = []) => { +export const transformItem = (items = []) => { each(items, (item) => { if (['http-request', 'graphql-request'].includes(item.type)) { item.request.query = item.request.params; @@ -47,14 +47,14 @@ const transformItem = (items = []) => { }); }; -const deleteUidsInEnvs = (envs) => { +export const deleteUidsInEnvs = (envs) => { each(envs, (env) => { delete env.uid; each(env.variables, (variable) => delete variable.uid); }); }; -const deleteSecretsInEnvs = (envs) => { +export const deleteSecretsInEnvs = (envs) => { each(envs, (env) => { each(env.variables, (variable) => { if (variable.secret) { @@ -64,7 +64,7 @@ const deleteSecretsInEnvs = (envs) => { }); }; -const exportCollection = (collection) => { +export const exportCollection = (collection) => { // delete uids delete collection.uid; deleteUidsInItems(collection.items); diff --git a/packages/bruno-app/src/utils/exporters/postman-collection.js b/packages/bruno-app/src/utils/exporters/postman-collection.js new file mode 100644 index 000000000..36bbff747 --- /dev/null +++ b/packages/bruno-app/src/utils/exporters/postman-collection.js @@ -0,0 +1,214 @@ +import map from 'lodash/map'; +import * as FileSaver from 'file-saver'; +import { deleteSecretsInEnvs, deleteUidsInEnvs, deleteUidsInItems } from 'utils/collections/export'; + +export const exportCollection = (collection) => { + delete collection.uid; + deleteUidsInItems(collection.items); + deleteUidsInEnvs(collection.environments); + deleteSecretsInEnvs(collection.environments); + + const generateInfoSection = () => { + return { + name: collection.name, + schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json' + }; + }; + + const generateCollectionVars = (collection) => { + const pattern = /{{[^{}]+}}/g; + let listOfVars = []; + + const findOccurrences = (obj, results) => { + if (typeof obj === 'object') { + if (Array.isArray(obj)) { + obj.forEach((item) => findOccurrences(item, results)); + } else { + for (const key in obj) { + findOccurrences(obj[key], results); + } + } + } else if (typeof obj === 'string') { + obj.replace(pattern, (match) => { + results.push(match.replace(/{{|}}/g, '')); + }); + } + }; + + findOccurrences(collection, listOfVars); + + const finalArrayOfVars = [...new Set(listOfVars)]; + + return finalArrayOfVars.map((variable) => ({ + key: variable, + value: '', + type: 'default' + })); + }; + + const generateEventSection = (item) => { + const eventArray = []; + if (item.request.tests.length) { + eventArray.push({ + listen: 'test', + script: { + exec: item.request.tests.split('\n') + // type: 'text/javascript' + } + }); + } + if (item.request.script.req) { + eventArray.push({ + listen: 'prerequest', + script: { + exec: item.request.script.req.split('\n') + // type: 'text/javascript' + } + }); + } + return eventArray; + }; + + const generateHeaders = (headersArray) => { + return map(headersArray, (item) => { + return { + key: item.name, + value: item.value, + disabled: !item.enabled, + type: 'default' + }; + }); + }; + + const generateBody = (body) => { + switch (body.mode) { + case 'formUrlEncoded': + return { + mode: 'urlencoded', + urlencoded: map(body.formUrlEncoded, (bodyItem) => { + return { + key: bodyItem.name, + value: bodyItem.value, + disabled: !bodyItem.enabled, + type: 'default' + }; + }) + }; + case 'multipartForm': + return { + mode: 'formdata', + formdata: map(body.multipartForm, (bodyItem) => { + return { + key: bodyItem.name, + value: bodyItem.value, + disabled: !bodyItem.enabled, + type: 'default' + }; + }) + }; + case 'json': + return { + mode: 'raw', + raw: body.json, + options: { + raw: { + language: 'json' + } + } + }; + case 'xml': + return { + mode: 'raw', + raw: body.xml, + options: { + raw: { + language: 'xml' + } + } + }; + case 'text': + return { + mode: 'raw', + raw: body.text, + options: { + raw: { + language: 'text' + } + } + }; + } + }; + + const generateAuth = (itemAuth) => { + switch (itemAuth) { + case 'bearer': + return { + type: 'bearer', + bearer: { + key: 'token', + value: itemAuth.bearer.token, + type: 'string' + } + }; + case 'basic': { + return { + type: 'basic', + basic: [ + { + key: 'password', + value: itemAuth.basic.password, + type: 'string' + }, + { + key: 'username', + value: itemAuth.basic.username, + type: 'string' + } + ] + }; + } + } + }; + + const generateRequestSection = (itemRequest) => { + const requestObject = { + method: itemRequest.method, + header: generateHeaders(itemRequest.headers), + url: itemRequest.url, + auth: generateAuth(itemRequest.auth) + }; + + if (itemRequest.body.mode != 'none') { + requestObject.body = generateBody(itemRequest.body); + } + return requestObject; + }; + + const generateItemSection = (itemsArray) => { + return map(itemsArray, (item) => { + if (item.type === 'folder') { + return { + name: item.name, + item: item.items.length ? generateItemSection(item.items) : [] + }; + } else { + return { + name: item.name, + event: generateEventSection(item), + request: generateRequestSection(item.request) + }; + } + }); + }; + const collectionToExport = {}; + collectionToExport.info = generateInfoSection(); + collectionToExport.item = generateItemSection(collection.items); + collectionToExport.variable = generateCollectionVars(collection); + + const fileName = `${collection.name}.json`; + const fileBlob = new Blob([JSON.stringify(collection, null, 2)], { type: 'application/json' }); + + FileSaver.saveAs(fileBlob, fileName); +}; + +export default exportCollection;