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.
[](https://badge.fury.io/gh/usebruno%bruno)
[](https://github.com/usebruno/bruno/workflows/unit-tests.yml)
@@ -10,36 +10,42 @@
[](https://www.usebruno.com)
[](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)

### Run across multiple platforms 🖥️
+

### Collaborate via Git 👩💻🧑💻
+
Or any version control system of your choice

### 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)