diff --git a/packages/bruno-app/package.json b/packages/bruno-app/package.json index 4845116a5..625e9dc0d 100644 --- a/packages/bruno-app/package.json +++ b/packages/bruno-app/package.json @@ -30,6 +30,8 @@ "graphiql": "^1.5.9", "graphql": "^16.6.0", "graphql-request": "^3.7.0", + "handlebars": "^4.7.8", + "httpsnippet": "^3.0.1", "idb": "^7.0.0", "immer": "^9.0.15", "know-your-http-well": "^0.5.0", diff --git a/packages/bruno-app/src/components/CodeEditor/index.js b/packages/bruno-app/src/components/CodeEditor/index.js index bcc13b5c2..96d5bb48a 100644 --- a/packages/bruno-app/src/components/CodeEditor/index.js +++ b/packages/bruno-app/src/components/CodeEditor/index.js @@ -119,7 +119,7 @@ export default class CodeEditor extends React.Component { render() { return ( { this._node = node; diff --git a/packages/bruno-app/src/components/ResponsePane/Placeholder/index.js b/packages/bruno-app/src/components/ResponsePane/Placeholder/index.js index 0879ba520..2e7cd8621 100644 --- a/packages/bruno-app/src/components/ResponsePane/Placeholder/index.js +++ b/packages/bruno-app/src/components/ResponsePane/Placeholder/index.js @@ -13,13 +13,11 @@ const Placeholder = () => {
Send Request
New Request
Edit Environments
-
Help
Cmd + Enter
Cmd + B
Cmd + E
-
Cmd + H
diff --git a/packages/bruno-app/src/components/ResponsePane/QueryResult/index.js b/packages/bruno-app/src/components/ResponsePane/QueryResult/index.js index f6698df22..dd4c8a502 100644 --- a/packages/bruno-app/src/components/ResponsePane/QueryResult/index.js +++ b/packages/bruno-app/src/components/ResponsePane/QueryResult/index.js @@ -4,15 +4,56 @@ import { useTheme } from 'providers/Theme'; import { useDispatch } from 'react-redux'; import { sendRequest } from 'providers/ReduxStore/slices/collections/actions'; import classnames from 'classnames'; +import { getContentType, safeStringifyJSON, safeParseXML } from 'utils/common'; +import { getCodeMirrorModeBasedOnContentType } from 'utils/common/codemirror'; import StyledWrapper from './StyledWrapper'; import { useState } from 'react'; import { useMemo } from 'react'; -const QueryResult = ({ item, collection, value, width, disableRunEventListener, mode }) => { +const QueryResult = ({ item, collection, data, width, disableRunEventListener, headers }) => { const { storedTheme } = useTheme(); const [tab, setTab] = useState('raw'); const dispatch = useDispatch(); + const contentType = getContentType(headers); + const mode = getCodeMirrorModeBasedOnContentType(contentType); + + const formatResponse = (data, mode) => { + if (!data) { + return ''; + } + + if (mode.includes('json')) { + return safeStringifyJSON(data, true); + } + + if (mode.includes('xml')) { + let parsed = safeParseXML(data, { collapseContent: true }); + + if (typeof parsed === 'string') { + return parsed; + } + + return safeStringifyJSON(parsed, true); + } + + if (['text', 'html'].includes(mode)) { + if (typeof data === 'string') { + return data; + } + + return safeStringifyJSON(data); + } + + // final fallback + if (typeof data === 'string') { + return data; + } + + return safeStringifyJSON(data); + }; + + const value = formatResponse(data, mode); const onRun = () => { if (disableRunEventListener) { @@ -32,7 +73,7 @@ const QueryResult = ({ item, collection, value, width, disableRunEventListener, Raw )]; - if (mode.includes('text/html')) { + if (mode.includes('html')) { tabs.push(
setTab('preview')}> Preview @@ -43,7 +84,7 @@ const QueryResult = ({ item, collection, value, width, disableRunEventListener, const activeResult = useMemo(() => { if (tab === 'preview') { // Add the Base tag to the head so content loads proparly. This also needs the correct CSP settings - const webViewSrc = value.replace('', ``); + const webViewSrc = data.replace('', ``); return ( diff --git a/packages/bruno-app/src/components/ResponsePane/index.js b/packages/bruno-app/src/components/ResponsePane/index.js index 64ff9eb25..21c4ca16f 100644 --- a/packages/bruno-app/src/components/ResponsePane/index.js +++ b/packages/bruno-app/src/components/ResponsePane/index.js @@ -2,7 +2,6 @@ import React from 'react'; import find from 'lodash/find'; import classnames from 'classnames'; import { useDispatch, useSelector } from 'react-redux'; -import { getContentType, formatResponse } from 'utils/common'; import { updateResponsePaneTab } from 'providers/ReduxStore/slices/tabs'; import QueryResult from './QueryResult'; import Overlay from './Overlay'; @@ -41,8 +40,8 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => { item={item} collection={collection} width={rightPaneWidth} - value={response.data ? formatResponse(response) : ''} - mode={getContentType(response.headers)} + data={response.data} + headers={response.headers} /> ); } diff --git a/packages/bruno-app/src/components/RunnerResults/ResponsePane/index.js b/packages/bruno-app/src/components/RunnerResults/ResponsePane/index.js index ba87fcaf9..2c4f28b20 100644 --- a/packages/bruno-app/src/components/RunnerResults/ResponsePane/index.js +++ b/packages/bruno-app/src/components/RunnerResults/ResponsePane/index.js @@ -33,7 +33,8 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => { collection={collection} width={rightPaneWidth} disableRunEventListener={true} - value={responseReceived && responseReceived.data ? safeStringifyJSON(responseReceived.data, true) : ''} + data={responseReceived.data} + headers={responseReceived.headers} /> ); } diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/CodeView/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/CodeView/index.js new file mode 100644 index 000000000..79d636daf --- /dev/null +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/CodeView/index.js @@ -0,0 +1,21 @@ +import CodeEditor from 'components/CodeEditor/index'; +import { HTTPSnippet } from 'httpsnippet'; +import { useTheme } from 'providers/Theme/index'; +import { buildHarRequest } from 'utils/codegenerator/har'; + +const CodeView = ({ language, item }) => { + const { storedTheme } = useTheme(); + const { target, client, language: lang } = language; + let snippet = ''; + + try { + snippet = new HTTPSnippet(buildHarRequest(item.request)).convert(target, client); + } catch (e) { + console.error(e); + snippet = 'Error generating code snippet'; + } + + return ; +}; + +export default CodeView; diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/StyledWrapper.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/StyledWrapper.js new file mode 100644 index 000000000..f1c1c33e4 --- /dev/null +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/StyledWrapper.js @@ -0,0 +1,38 @@ +import styled from 'styled-components'; + +const StyledWrapper = styled.div` + margin-inline: -1rem; + margin-block: -1.5rem; + background-color: ${(props) => props.theme.collection.environment.settings.bg}; + + .generate-code-sidebar { + background-color: ${(props) => props.theme.collection.environment.settings.sidebar.bg}; + border-right: solid 1px ${(props) => props.theme.collection.environment.settings.sidebar.borderRight}; + min-height: 400px; + } + + .generate-code-item { + min-width: 150px; + display: block; + position: relative; + cursor: pointer; + padding: 8px 10px; + border-left: solid 2px transparent; + text-decoration: none; + + &:hover { + text-decoration: none; + background-color: ${(props) => props.theme.collection.environment.settings.item.hoverBg}; + } + } + + .active { + background-color: ${(props) => props.theme.collection.environment.settings.item.active.bg} !important; + border-left: solid 2px ${(props) => props.theme.collection.environment.settings.item.border}; + &:hover { + background-color: ${(props) => props.theme.collection.environment.settings.item.active.hoverBg} !important; + } + } +`; + +export default StyledWrapper; diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/index.js new file mode 100644 index 000000000..509a529bd --- /dev/null +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/index.js @@ -0,0 +1,145 @@ +import Modal from 'components/Modal/index'; +import { useState } from 'react'; +import CodeView from './CodeView'; +import StyledWrapper from './StyledWrapper'; +import { isValidUrl } from 'utils/url/index'; +import get from 'lodash/get'; +import handlebars from 'handlebars'; +import { findEnvironmentInCollection } from 'utils/collections'; + +const interpolateUrl = ({ url, envVars, collectionVariables, processEnvVars }) => { + if (!url || !url.length || typeof url !== 'string') { + return str; + } + + const template = handlebars.compile(url, { noEscape: true }); + + return template({ + ...envVars, + ...collectionVariables, + process: { + env: { + ...processEnvVars + } + } + }); +}; + +const languages = [ + { + name: 'HTTP', + target: 'http', + client: 'http1.1' + }, + { + name: 'JavaScript-Fetch', + target: 'javascript', + client: 'fetch' + }, + { + name: 'Javascript-jQuery', + target: 'javascript', + client: 'jquery' + }, + { + name: 'Javascript-axios', + target: 'javascript', + client: 'axios' + }, + { + name: 'Python-Python3', + target: 'python', + client: 'python3' + }, + { + name: 'Python-Requests', + target: 'python', + client: 'requests' + }, + { + name: 'PHP', + target: 'php', + client: 'curl' + }, + { + name: 'Shell-curl', + target: 'shell', + client: 'curl' + }, + { + name: 'Shell-httpie', + target: 'shell', + client: 'httpie' + } +]; + +const GenerateCodeItem = ({ collection, item, onClose }) => { + const url = get(item, 'request.url') || ''; + const environment = findEnvironmentInCollection(collection, collection.activeEnvironmentUid); + + let envVars = {}; + if (environment) { + const vars = get(environment, 'variables', []); + envVars = vars.reduce((acc, curr) => { + acc[curr.name] = curr.value; + return acc; + }, {}); + } + + const interpolatedUrl = interpolateUrl({ + url, + envVars, + collectionVariables: collection.collectionVariables, + processEnvVars: collection.processEnvVariables + }); + + const [selectedLanguage, setSelectedLanguage] = useState(languages[0]); + return ( + + +
+
+
+ {languages && + languages.length && + languages.map((language) => ( +
setSelectedLanguage(language)} + > + {language.name} +
+ ))} +
+
+
+ {isValidUrl(interpolatedUrl) ? ( + + ) : ( +
+
+

Invalid URL: {interpolatedUrl}

+

Please check the URL and try again

+
+
+ )} +
+
+
+
+ ); +}; + +export default GenerateCodeItem; diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js index 3916cc2e6..daed44279 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js @@ -16,6 +16,7 @@ import RenameCollectionItem from './RenameCollectionItem'; import CloneCollectionItem from './CloneCollectionItem'; import DeleteCollectionItem from './DeleteCollectionItem'; import RunCollectionItem from './RunCollectionItem'; +import GenerateCodeItem from './GenerateCodeItem'; import { isItemARequest, isItemAFolder, itemIsOpenedInTabs } from 'utils/tabs'; import { doesRequestMatchSearchText, doesFolderHaveItemsMatchSearchText } from 'utils/collections/search'; import { getDefaultRequestPaneTab } from 'utils/collections'; @@ -32,6 +33,7 @@ const CollectionItem = ({ item, collection, searchText }) => { const [renameItemModalOpen, setRenameItemModalOpen] = useState(false); const [cloneItemModalOpen, setCloneItemModalOpen] = useState(false); const [deleteItemModalOpen, setDeleteItemModalOpen] = useState(false); + const [generateCodeItemModalOpen, setGenerateCodeItemModalOpen] = useState(false); const [newRequestModalOpen, setNewRequestModalOpen] = useState(false); const [newFolderModalOpen, setNewFolderModalOpen] = useState(false); const [runCollectionModalOpen, setRunCollectionModalOpen] = useState(false); @@ -166,6 +168,9 @@ const CollectionItem = ({ item, collection, searchText }) => { {runCollectionModalOpen && ( setRunCollectionModalOpen(false)} /> )} + {generateCodeItemModalOpen && ( + setGenerateCodeItemModalOpen(false)} /> + )}
drag(drop(node))}>
{indents && indents.length @@ -264,6 +269,18 @@ const CollectionItem = ({ item, collection, searchText }) => { Clone
)} + {!isFolder && item.type === 'http-request' && ( +
{ + e.stopPropagation(); + dropdownTippyRef.current.hide(); + setGenerateCodeItemModalOpen(true); + }} + > + Generate Code +
+ )}
{ diff --git a/packages/bruno-app/src/components/Sidebar/Collections/index.js b/packages/bruno-app/src/components/Sidebar/Collections/index.js index 57b14a0d7..af54350e4 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/index.js @@ -1,6 +1,12 @@ import React, { useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import { IconSearch, IconFolders, IconSortAZ } from '@tabler/icons'; +import { + IconSearch, + IconFolders, + IconArrowsSort, + IconSortAscendingLetters, + IconSortDescendingLetters +} from '@tabler/icons'; import Collection from '../Collections/Collection'; import CreateCollection from '../CreateCollection'; import StyledWrapper from './StyledWrapper'; @@ -9,20 +15,47 @@ import { DndProvider } from 'react-dnd'; import { HTML5Backend } from 'react-dnd-html5-backend'; import { sortCollections } from 'providers/ReduxStore/slices/collections/actions'; +// todo: move this to a separate folder +// the coding convention is to keep all the components in a folder named after the component const CollectionsBadge = () => { - const dispatch = useDispatch() + const dispatch = useDispatch(); + const { collections } = useSelector((state) => state.collections); + const { collectionSortOrder } = useSelector((state) => state.collections); + const sortCollectionOrder = () => { + let order; + switch (collectionSortOrder) { + case 'default': + order = 'alphabetical'; + break; + case 'alphabetical': + order = 'reverseAlphabetical'; + break; + case 'reverseAlphabetical': + order = 'default'; + break; + } + dispatch(sortCollections({ order })); + }; return (
-
+
Collections
- + {collections.length >= 1 && ( + + )}
); @@ -71,12 +104,12 @@ const Collections = () => {
{collections && collections.length ? collections.map((c) => { - return ( - - - - ); - }) + return ( + + + + ); + }) : null}
diff --git a/packages/bruno-app/src/components/Sidebar/index.js b/packages/bruno-app/src/components/Sidebar/index.js index 14d92d42f..cb0f0ffb5 100644 --- a/packages/bruno-app/src/components/Sidebar/index.js +++ b/packages/bruno-app/src/components/Sidebar/index.js @@ -116,7 +116,7 @@ const Sidebar = () => { )}
-
v0.16.5
+
v0.16.6
diff --git a/packages/bruno-app/src/providers/Hotkeys/index.js b/packages/bruno-app/src/providers/Hotkeys/index.js index a50e71dfb..522fa0d46 100644 --- a/packages/bruno-app/src/providers/Hotkeys/index.js +++ b/packages/bruno-app/src/providers/Hotkeys/index.js @@ -7,7 +7,6 @@ import SaveRequest from 'components/RequestPane/SaveRequest'; import EnvironmentSettings from 'components/Environments/EnvironmentSettings'; import NetworkError from 'components/ResponsePane/NetworkError'; import NewRequest from 'components/Sidebar/NewRequest'; -import BrunoSupport from 'components/BrunoSupport'; import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions'; import { findCollectionByUid, findItemInCollection } from 'utils/collections'; import { closeTabs } from 'providers/ReduxStore/slices/tabs'; @@ -22,7 +21,6 @@ export const HotkeysProvider = (props) => { const [showSaveRequestModal, setShowSaveRequestModal] = useState(false); const [showEnvSettingsModal, setShowEnvSettingsModal] = useState(false); const [showNewRequestModal, setShowNewRequestModal] = useState(false); - const [showBrunoSupportModal, setShowBrunoSupportModal] = useState(false); const getCurrentCollectionItems = () => { const activeTab = find(tabs, (t) => t.uid === activeTabUid); @@ -133,18 +131,6 @@ export const HotkeysProvider = (props) => { }; }, [activeTabUid, tabs, collections, setShowNewRequestModal]); - // help (ctrl/cmd + h) - useEffect(() => { - Mousetrap.bind(['command+h', 'ctrl+h'], (e) => { - setShowBrunoSupportModal(true); - return false; // this stops the event bubbling - }); - - return () => { - Mousetrap.unbind(['command+h', 'ctrl+h']); - }; - }, [setShowNewRequestModal]); - // close tab hotkey useEffect(() => { Mousetrap.bind(['command+w', 'ctrl+w'], (e) => { @@ -164,7 +150,6 @@ export const HotkeysProvider = (props) => { return ( - {showBrunoSupportModal && setShowBrunoSupportModal(false)} />} {showSaveRequestModal && ( setShowSaveRequestModal(false)} /> )} diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js index 9cd588e31..0c6945ae9 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js @@ -22,7 +22,7 @@ import { } from 'utils/collections'; import { collectionSchema, itemSchema, environmentSchema, environmentsSchema } from '@usebruno/schema'; import { waitForNextTick } from 'utils/common'; -import { getDirectoryName } from 'utils/common/platform'; +import { getDirectoryName, isWindowsOS } from 'utils/common/platform'; import { sendNetworkRequest, cancelNetworkRequest } from 'utils/network'; import { @@ -34,12 +34,12 @@ import { renameItem as _renameItem, cloneItem as _cloneItem, deleteItem as _deleteItem, - sortCollections as _sortCollections, saveRequest as _saveRequest, selectEnvironment as _selectEnvironment, createCollection as _createCollection, renameCollection as _renameCollection, removeCollection as _removeCollection, + sortCollections as _sortCollections, collectionAddEnvFileEvent as _collectionAddEnvFileEvent } from './index'; @@ -146,6 +146,11 @@ export const cancelRequest = (cancelTokenUid, item, collection) => (dispatch) => .catch((err) => console.log(err)); }; +// todo: this can be directly put inside the collections/index.js file +// the coding convention is to put only actions that need ipc in this file +export const sortCollections = (order) => (dispatch) => { + dispatch(_sortCollections(order)); +}; export const runCollectionFolder = (collectionUid, folderUid, recursive) => (dispatch, getState) => { const state = getState(); const collection = findCollectionByUid(state.collections.collections, collectionUid); @@ -263,7 +268,19 @@ export const renameItem = (newName, itemUid, collectionUid) => (dispatch, getSta } const { ipcRenderer } = window; - ipcRenderer.invoke('renderer:rename-item', item.pathname, newPathname, newName).then(resolve).catch(reject); + ipcRenderer + .invoke('renderer:rename-item', item.pathname, newPathname, newName) + .then(() => { + // In case of Mac and Linux, we get the unlinkDir and addDir IPC events from electron which takes care of updating the state + // But in windows we don't get those events, so we need to update the state manually + // This looks like an issue in our watcher library chokidar + // GH: https://github.com/usebruno/bruno/issues/251 + if (isWindowsOS()) { + dispatch(_renameItem({ newName, itemUid, collectionUid })); + } + resolve(); + }) + .catch(reject); }); }; @@ -347,16 +364,22 @@ export const deleteItem = (itemUid, collectionUid) => (dispatch, getState) => { ipcRenderer .invoke('renderer:delete-item', item.pathname, item.type) - .then(() => resolve()) + .then(() => { + // In case of Mac and Linux, we get the unlinkDir IPC event from electron which takes care of updating the state + // But in windows we don't get those events, so we need to update the state manually + // This looks like an issue in our watcher library chokidar + // GH: https://github.com/usebruno/bruno/issues/265 + if (isWindowsOS()) { + dispatch(_deleteItem({ itemUid, collectionUid })); + } + resolve(); + }) .catch((error) => reject(error)); } return; }); }; -export const sortCollections = () => (dispatch) => { - dispatch(_sortCollections()) -} export const moveItem = (collectionUid, draggedItemUid, targetItemUid) => (dispatch, getState) => { const state = getState(); const collection = findCollectionByUid(state.collections.collections, collectionUid); diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js index 8227efc6b..213761029 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js @@ -28,7 +28,8 @@ import { getSubdirectoriesFromRoot, getDirectoryName } from 'utils/common/platfo const PATH_SEPARATOR = path.sep; const initialState = { - collections: [] + collections: [], + collectionSortOrder: 'default' }; export const collectionsSlice = createSlice({ @@ -38,12 +39,12 @@ export const collectionsSlice = createSlice({ createCollection: (state, action) => { const collectionUids = map(state.collections, (c) => c.uid); const collection = action.payload; - // last action is used to track the last action performed on the collection // this is optional // this is used in scenarios where we want to know the last action performed on the collection // and take some extra action based on that // for example, when a env is created, we want to auto select it the env modal + collection.importedAt = new Date().getTime(); collection.lastAction = null; collapseCollection(collection); @@ -70,8 +71,19 @@ export const collectionsSlice = createSlice({ removeCollection: (state, action) => { state.collections = filter(state.collections, (c) => c.uid !== action.payload.collectionUid); }, - sortCollections: (state) => { - state.collections = state.collections.sort((a, b) => a.name.localeCompare(b.name)) + sortCollections: (state, action) => { + state.collectionSortOrder = action.payload.order; + switch (action.payload.order) { + case 'default': + state.collections = state.collections.sort((a, b) => a.importedAt - b.importedAt); + break; + case 'alphabetical': + state.collections = state.collections.sort((a, b) => a.name.localeCompare(b.name)); + break; + case 'reverseAlphabetical': + state.collections = state.collections.sort((a, b) => b.name.localeCompare(a.name)); + break; + } }, updateLastAction: (state, action) => { const { collectionUid, lastAction } = action.payload; diff --git a/packages/bruno-app/src/utils/codegenerator/har.js b/packages/bruno-app/src/utils/codegenerator/har.js new file mode 100644 index 000000000..b48fbc3c7 --- /dev/null +++ b/packages/bruno-app/src/utils/codegenerator/har.js @@ -0,0 +1,71 @@ +const createContentType = (mode) => { + switch (mode) { + case 'json': + return 'application/json'; + case 'xml': + return 'application/xml'; + case 'formUrlEncoded': + return 'application/x-www-form-urlencoded'; + case 'multipartForm': + return 'multipart/form-data'; + default: + return 'application/json'; + } +}; + +const createHeaders = (headers, mode) => { + const contentType = createContentType(mode); + const headersArray = headers + .filter((header) => header.enabled) + .map((header) => { + return { + name: header.name, + value: header.value + }; + }); + const headerNames = headersArray.map((header) => header.name); + if (!headerNames.includes('Content-Type')) { + return [...headersArray, { name: 'Content-Type', value: contentType }]; + } + return headersArray; +}; + +const createQuery = (queryParams = []) => { + return queryParams.map((param) => { + return { + name: param.name, + value: param.value + }; + }); +}; + +const createPostData = (body) => { + const contentType = createContentType(body.mode); + if (body.mode === 'formUrlEncoded' || body.mode === 'multipartForm') { + return { + mimeType: contentType, + params: body[body.mode] + .filter((param) => param.enabled) + .map((param) => ({ name: param.name, value: param.value })) + }; + } else { + return { + mimeType: contentType, + text: body[body.mode] + }; + } +}; + +export const buildHarRequest = (request) => { + return { + method: request.method, + url: request.url, + httpVersion: 'HTTP/1.1', + cookies: [], + headers: createHeaders(request.headers, request.body.mode), + queryString: createQuery(request.params), + postData: createPostData(request.body), + headersSize: 0, + bodySize: 0 + }; +}; diff --git a/packages/bruno-app/src/utils/collections/index.js b/packages/bruno-app/src/utils/collections/index.js index 80fe41dd3..0a20cb448 100644 --- a/packages/bruno-app/src/utils/collections/index.js +++ b/packages/bruno-app/src/utils/collections/index.js @@ -129,9 +129,11 @@ export const moveCollectionItem = (collection, draggedItem, targetItem) => { let draggedItemParent = findParentItemInCollection(collection, draggedItem.uid); if (draggedItemParent) { + draggedItemParent.items = sortBy(draggedItemParent.items, (item) => item.seq); draggedItemParent.items = filter(draggedItemParent.items, (i) => i.uid !== draggedItem.uid); draggedItem.pathname = path.join(draggedItemParent.pathname, draggedItem.filename); } else { + collection.items = sortBy(collection.items, (item) => item.seq); collection.items = filter(collection.items, (i) => i.uid !== draggedItem.uid); } @@ -143,10 +145,12 @@ export const moveCollectionItem = (collection, draggedItem, targetItem) => { let targetItemParent = findParentItemInCollection(collection, targetItem.uid); if (targetItemParent) { + targetItemParent.items = sortBy(targetItemParent.items, (item) => item.seq); let targetItemIndex = findIndex(targetItemParent.items, (i) => i.uid === targetItem.uid); targetItemParent.items.splice(targetItemIndex + 1, 0, draggedItem); draggedItem.pathname = path.join(targetItemParent.pathname, draggedItem.filename); } else { + collection.items = sortBy(collection.items, (item) => item.seq); let targetItemIndex = findIndex(collection.items, (i) => i.uid === targetItem.uid); collection.items.splice(targetItemIndex + 1, 0, draggedItem); draggedItem.pathname = path.join(collection.pathname, draggedItem.filename); diff --git a/packages/bruno-app/src/utils/common/codemirror.js b/packages/bruno-app/src/utils/common/codemirror.js index b1b60568c..aa4ba0519 100644 --- a/packages/bruno-app/src/utils/common/codemirror.js +++ b/packages/bruno-app/src/utils/common/codemirror.js @@ -42,3 +42,25 @@ export const defineCodeMirrorBrunoVariablesMode = (variables, mode) => { return CodeMirror.overlayMode(CodeMirror.getMode(config, parserConfig.backdrop || mode), variablesOverlay); }); }; + +export const getCodeMirrorModeBasedOnContentType = (contentType) => { + if (!contentType || typeof contentType !== 'string') { + return 'application/text'; + } + + if (contentType.includes('json')) { + return 'application/ld+json'; + } else if (contentType.includes('xml')) { + return 'application/xml'; + } else if (contentType.includes('html')) { + return 'application/html'; + } else if (contentType.includes('text')) { + return 'application/text'; + } else if (contentType.includes('application/edn')) { + return 'application/xml'; + } else if (mimeType.includes('yaml')) { + return 'application/yaml'; + } else { + return 'application/text'; + } +}; diff --git a/packages/bruno-app/src/utils/common/index.js b/packages/bruno-app/src/utils/common/index.js index 84725332f..c5eaa93ab 100644 --- a/packages/bruno-app/src/utils/common/index.js +++ b/packages/bruno-app/src/utils/common/index.js @@ -51,6 +51,17 @@ export const safeStringifyJSON = (obj, indent = false) => { } }; +export const safeParseXML = (str) => { + if (!str || !str.length || typeof str !== 'string') { + return str; + } + try { + return xmlFormat(str); + } catch (e) { + return str; + } +}; + // Remove any characters that are not alphanumeric, spaces, hyphens, or underscores export const normalizeFileName = (name) => { if (!name) { @@ -80,16 +91,6 @@ export const getContentType = (headers) => { return contentType[0]; } } + return ''; }; - -export const formatResponse = (response) => { - let type = getContentType(response.headers); - if (type.includes('json')) { - return safeStringifyJSON(response.data, true); - } - if (type.includes('xml')) { - return xmlFormat(response.data, { collapseContent: true }); - } - return response.data; -}; diff --git a/packages/bruno-app/src/utils/common/platform.js b/packages/bruno-app/src/utils/common/platform.js index d144796e7..e49a66ec9 100644 --- a/packages/bruno-app/src/utils/common/platform.js +++ b/packages/bruno-app/src/utils/common/platform.js @@ -1,6 +1,7 @@ import trim from 'lodash/trim'; import path from 'path'; import slash from './slash'; +import platform from 'platform'; export const isElectron = () => { if (!window) { @@ -33,3 +34,10 @@ export const getDirectoryName = (pathname) => { return path.dirname(pathname); }; + +export const isWindowsOS = () => { + const os = platform.os; + const osFamily = os.family.toLowerCase(); + + return osFamily.includes('windows'); +}; diff --git a/packages/bruno-app/src/utils/url/index.js b/packages/bruno-app/src/utils/url/index.js index b28cc019e..7f5a8e825 100644 --- a/packages/bruno-app/src/utils/url/index.js +++ b/packages/bruno-app/src/utils/url/index.js @@ -53,3 +53,12 @@ export const splitOnFirst = (str, char) => { return [str.slice(0, index), str.slice(index + 1)]; }; + +export const isValidUrl = (url) => { + try { + new URL(url); + return true; + } catch (err) { + return false; + } +}; diff --git a/packages/bruno-electron/package.json b/packages/bruno-electron/package.json index ce46ee3c3..2807e2f97 100644 --- a/packages/bruno-electron/package.json +++ b/packages/bruno-electron/package.json @@ -1,5 +1,5 @@ { - "version": "v0.16.5", + "version": "v0.16.6", "name": "bruno", "description": "Opensource API Client for Exploring and Testing APIs", "homepage": "https://www.usebruno.com", diff --git a/readme.md b/readme.md index 6f902b14d..af60a1888 100644 --- a/readme.md +++ b/readme.md @@ -1,7 +1,7 @@
-### Bruno - Opensource IDE for exploring and testing APIs. +### Bruno - Opensource IDE for exploring and testing APIs. [![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%bruno) [![CI](https://github.com/usebruno/bruno/actions/workflows/unit-tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/workflows/unit-tests.yml) @@ -10,36 +10,42 @@ [![Website](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com) [![Download](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads) - Bruno is a new and innovative API client, aimed at revolutionizing the status quo represented by Postman and similar tools out there. Bruno stores your collections directly in a folder on your filesystem. We use a plain text markup language, Bru, to save information about API requests. You can use git or any version control of your choice to collaborate over your API collections. +Bruno is offline-only. There are no plans to add cloud-sync to Bruno, ever. We value your data privacy and believe it should stay on your device. Read our long-term vision [here](https://github.com/usebruno/bruno/discussions/269) ![bruno](assets/images/landing-2.png)

### Run across multiple platforms 🖥️ + ![bruno](assets/images/run-anywhere.png)

### Collaborate via Git 👩‍💻🧑‍💻 + Or any version control system of your choice ![bruno](assets/images/version-control.png)

### Website 📄 + Please visit [here](https://www.usebruno.com) to checkout our website and download the app ### Documentation 📄 + Please visit [here](https://docs.usebruno.com) for documentation ### Contribute 👩‍💻🧑‍💻 + I am happy that you are looking to improve bruno. Please checkout the [contributing guide](contributing.md) Even if you are not able to make contributions via code, please don't hesitate to file bugs and feature requests that needs to be implemented to solve your use case. -### Support ❤️ +### Support ❤️ + Woof! If you like project, hit that ⭐ button !! ### Authors @@ -51,9 +57,11 @@ Woof! If you like project, hit that ⭐ button !! ### Stay in touch 🌐 + [Twitter](https://twitter.com/use_bruno)
[Website](https://www.usebruno.com)
[Discord](https://discord.com/invite/KgcZUncpjq) ### License 📄 + [MIT](license.md)