diff --git a/packages/bruno-app/src/components/CodeEditor/StyledWrapper.js b/packages/bruno-app/src/components/CodeEditor/StyledWrapper.js
index c77749cb8..4d47186c0 100644
--- a/packages/bruno-app/src/components/CodeEditor/StyledWrapper.js
+++ b/packages/bruno-app/src/components/CodeEditor/StyledWrapper.js
@@ -10,6 +10,12 @@ const StyledWrapper = styled.div`
flex: 1 1 0;
}
+ /* Removes the glow outline around the folded json */
+ .CodeMirror-foldmarker {
+ text-shadow: none;
+ color: ${(props) => props.theme.textLink};
+ }
+
.CodeMirror-overlayscroll-horizontal div,
.CodeMirror-overlayscroll-vertical div {
background: #d2d7db;
diff --git a/packages/bruno-app/src/components/CodeEditor/index.js b/packages/bruno-app/src/components/CodeEditor/index.js
index 364f399c5..b822babcb 100644
--- a/packages/bruno-app/src/components/CodeEditor/index.js
+++ b/packages/bruno-app/src/components/CodeEditor/index.js
@@ -69,6 +69,7 @@ if (!SERVER_RENDERED) {
'bru.getVar(key)',
'bru.setVar(key,value)',
'bru.deleteVar(key)',
+ 'bru.deleteAllVars()',
'bru.setNextRequest(requestName)',
'req.disableParsingResponseJson()',
'bru.getRequestVar(key)',
diff --git a/packages/bruno-app/src/components/Preferences/Keybindings/StyledWrapper.js b/packages/bruno-app/src/components/Preferences/Keybindings/StyledWrapper.js
new file mode 100644
index 000000000..1a1fe7f01
--- /dev/null
+++ b/packages/bruno-app/src/components/Preferences/Keybindings/StyledWrapper.js
@@ -0,0 +1,46 @@
+import styled from 'styled-components';
+
+const StyledWrapper = styled.div`
+ table {
+ width: 100%;
+ border-collapse: collapse;
+
+ thead,
+ td {
+ border: 2px solid ${(props) => props.theme.table.border};
+ }
+
+ thead {
+ color: ${(props) => props.theme.table.thead.color};
+ font-size: 1rem;
+ user-select: none;
+ }
+
+ td {
+ padding: 4px 8px;
+ }
+
+ thead th {
+ font-weight: 600;
+ padding: 10px;
+ text-align: left;
+ }
+ }
+
+ .table-container {
+ max-height: 400px;
+ overflow-y: scroll;
+ }
+
+ .key-button {
+ display: inline-block;
+ color: ${(props) => props.theme.table.input.color};
+ border-radius: 4px;
+ padding: 1px 5px;
+ font-family: monospace;
+ margin-right: 8px;
+ border: 1px solid #ccc;
+ }
+`;
+
+export default StyledWrapper;
diff --git a/packages/bruno-app/src/components/Preferences/Keybindings/index.js b/packages/bruno-app/src/components/Preferences/Keybindings/index.js
new file mode 100644
index 000000000..d2bc918aa
--- /dev/null
+++ b/packages/bruno-app/src/components/Preferences/Keybindings/index.js
@@ -0,0 +1,45 @@
+import StyledWrapper from './StyledWrapper';
+import React from 'react';
+import { getKeyBindingsForOS } from 'providers/Hotkeys/keyMappings';
+import { isMacOS } from 'utils/common/platform';
+
+const Keybindings = ({ close }) => {
+ const keyMapping = getKeyBindingsForOS(isMacOS() ? 'mac' : 'windows');
+
+ return (
+
+
+
+
+
+ Command |
+ Keybinding |
+
+
+
+ {keyMapping ? (
+ Object.entries(keyMapping).map(([action, { name, keys }], index) => (
+
+ {name} |
+
+ {keys.split('+').map((key, i) => (
+
+ {key}
+
+ ))}
+ |
+
+ ))
+ ) : (
+
+ No key bindings available |
+
+ )}
+
+
+
+
+ );
+};
+
+export default Keybindings;
diff --git a/packages/bruno-app/src/components/Preferences/index.js b/packages/bruno-app/src/components/Preferences/index.js
index 03b1d9ef8..3635ca5a9 100644
--- a/packages/bruno-app/src/components/Preferences/index.js
+++ b/packages/bruno-app/src/components/Preferences/index.js
@@ -1,11 +1,14 @@
import Modal from 'components/Modal/index';
import classnames from 'classnames';
import React, { useState } from 'react';
+
import Support from './Support';
import General from './General';
import Proxy from './ProxySettings';
+import Display from './Display';
+import Keybindings from './Keybindings';
+
import StyledWrapper from './StyledWrapper';
-import Display from './Display/index';
const Preferences = ({ onClose }) => {
const [tab, setTab] = useState('general');
@@ -30,6 +33,10 @@ const Preferences = ({ onClose }) => {
return ;
}
+ case 'keybindings': {
+ return ;
+ }
+
case 'support': {
return ;
}
@@ -50,6 +57,9 @@ const Preferences = ({ onClose }) => {
setTab('proxy')}>
Proxy
+ setTab('keybindings')}>
+ Keybindings
+
setTab('support')}>
Support
diff --git a/packages/bruno-app/src/components/ReorderTable/index.js b/packages/bruno-app/src/components/ReorderTable/index.js
index 9d8c11088..b5ea369a2 100644
--- a/packages/bruno-app/src/components/ReorderTable/index.js
+++ b/packages/bruno-app/src/components/ReorderTable/index.js
@@ -1,4 +1,4 @@
-import React, { useEffect, useRef, useState, useCallback } from 'react';
+import React, { useEffect, useRef, useState, useMemo } from 'react';
import { IconGripVertical, IconMinusVertical } from '@tabler/icons';
/**
@@ -13,17 +13,17 @@ import { IconGripVertical, IconMinusVertical } from '@tabler/icons';
const ReorderTable = ({ children, updateReorderedItem }) => {
const tbodyRef = useRef();
- const [rowsOrder, setRowsOrder] = useState(React.Children.toArray(children));
const [hoveredRow, setHoveredRow] = useState(null);
const [dragStart, setDragStart] = useState(null);
+ const rowsOrder = useMemo(() => React.Children.toArray(children), [children]);
+
/**
- * useEffect hook to update the rows order and handle row hover states
+ * useEffect hook to handle row hover states
*/
useEffect(() => {
- setRowsOrder(React.Children.toArray(children));
handleRowHover(null, false);
- }, [children, dragStart]);
+ }, [children]);
const handleRowHover = (index, hoverstatus = true) => {
setHoveredRow(hoverstatus ? index : null);
@@ -48,7 +48,6 @@ const ReorderTable = ({ children, updateReorderedItem }) => {
const updatedRowsOrder = [...rowsOrder];
const [movedRow] = updatedRowsOrder.splice(fromIndex, 1);
updatedRowsOrder.splice(toIndex, 0, movedRow);
- setRowsOrder(updatedRowsOrder);
updateReorderedItem({
updateReorderedItem: updatedRowsOrder.map((row) => row.props['data-uid'])
diff --git a/packages/bruno-app/src/components/Sidebar/index.js b/packages/bruno-app/src/components/Sidebar/index.js
index cc878bcc9..09fd4c5fa 100644
--- a/packages/bruno-app/src/components/Sidebar/index.js
+++ b/packages/bruno-app/src/components/Sidebar/index.js
@@ -185,7 +185,7 @@ const Sidebar = () => {
Star
*/}
- v1.30.1
+ v1.32.1
diff --git a/packages/bruno-app/src/providers/App/useIpcEvents.js b/packages/bruno-app/src/providers/App/useIpcEvents.js
index ba0670285..80ea83283 100644
--- a/packages/bruno-app/src/providers/App/useIpcEvents.js
+++ b/packages/bruno-app/src/providers/App/useIpcEvents.js
@@ -19,7 +19,7 @@ import {
runRequestEvent,
scriptEnvironmentUpdateEvent
} from 'providers/ReduxStore/slices/collections';
-import { collectionAddEnvFileEvent, openCollectionEvent } from 'providers/ReduxStore/slices/collections/actions';
+import { collectionAddEnvFileEvent, openCollectionEvent, hydrateCollectionWithUiStateSnapshot } from 'providers/ReduxStore/slices/collections/actions';
import toast from 'react-hot-toast';
import { useDispatch } from 'react-redux';
import { isElectron } from 'utils/common/platform';
@@ -158,6 +158,10 @@ const useIpcEvents = () => {
dispatch(updateGlobalEnvironments(val));
});
+ const removeSnapshotHydrationListener = ipcRenderer.on('main:hydrate-app-with-ui-state-snapshot', (val) => {
+ dispatch(hydrateCollectionWithUiStateSnapshot(val));
+ })
+
return () => {
removeCollectionTreeUpdateListener();
removeOpenCollectionListener();
@@ -176,6 +180,7 @@ const useIpcEvents = () => {
removeCookieUpdateListener();
removeSystemProxyEnvUpdatesListener();
removeGlobalEnvironmentsUpdatesListener();
+ removeSnapshotHydrationListener();
};
}, [isElectron]);
};
diff --git a/packages/bruno-app/src/providers/App/useTelemetry.js b/packages/bruno-app/src/providers/App/useTelemetry.js
index 55b0bbdad..1087e8508 100644
--- a/packages/bruno-app/src/providers/App/useTelemetry.js
+++ b/packages/bruno-app/src/providers/App/useTelemetry.js
@@ -60,7 +60,7 @@ const trackStart = () => {
event: 'start',
properties: {
os: platformLib.os.family,
- version: '1.30.1'
+ version: '1.32.1'
}
});
};
diff --git a/packages/bruno-app/src/providers/Hotkeys/index.js b/packages/bruno-app/src/providers/Hotkeys/index.js
index 5b6bf1c00..41e71b4a2 100644
--- a/packages/bruno-app/src/providers/Hotkeys/index.js
+++ b/packages/bruno-app/src/providers/Hotkeys/index.js
@@ -10,6 +10,7 @@ import NewRequest from 'components/Sidebar/NewRequest';
import { sendRequest, saveRequest, saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
import { findCollectionByUid, findItemInCollection } from 'utils/collections';
import { closeTabs, switchTab } from 'providers/ReduxStore/slices/tabs';
+import { getKeyBindingsForActionAllOS } from './keyMappings';
export const HotkeysContext = React.createContext();
@@ -43,7 +44,7 @@ export const HotkeysProvider = (props) => {
// save hotkey
useEffect(() => {
- Mousetrap.bind(['command+s', 'ctrl+s'], (e) => {
+ Mousetrap.bind([...getKeyBindingsForActionAllOS('save')], (e) => {
if (isEnvironmentSettingsModalOpen) {
console.log('todo: save environment settings');
} else {
@@ -68,13 +69,13 @@ export const HotkeysProvider = (props) => {
});
return () => {
- Mousetrap.unbind(['command+s', 'ctrl+s']);
+ Mousetrap.unbind([...getKeyBindingsForActionAllOS('save')]);
};
}, [activeTabUid, tabs, saveRequest, collections, isEnvironmentSettingsModalOpen]);
// send request (ctrl/cmd + enter)
useEffect(() => {
- Mousetrap.bind(['command+enter', 'ctrl+enter'], (e) => {
+ Mousetrap.bind([...getKeyBindingsForActionAllOS('sendRequest')], (e) => {
const activeTab = find(tabs, (t) => t.uid === activeTabUid);
if (activeTab) {
const collection = findCollectionByUid(collections, activeTab.collectionUid);
@@ -95,13 +96,13 @@ export const HotkeysProvider = (props) => {
});
return () => {
- Mousetrap.unbind(['command+enter', 'ctrl+enter']);
+ Mousetrap.unbind([...getKeyBindingsForActionAllOS('sendRequest')]);
};
}, [activeTabUid, tabs, saveRequest, collections]);
// edit environments (ctrl/cmd + e)
useEffect(() => {
- Mousetrap.bind(['command+e', 'ctrl+e'], (e) => {
+ Mousetrap.bind([...getKeyBindingsForActionAllOS('editEnvironment')], (e) => {
const activeTab = find(tabs, (t) => t.uid === activeTabUid);
if (activeTab) {
const collection = findCollectionByUid(collections, activeTab.collectionUid);
@@ -115,13 +116,13 @@ export const HotkeysProvider = (props) => {
});
return () => {
- Mousetrap.unbind(['command+e', 'ctrl+e']);
+ Mousetrap.unbind([...getKeyBindingsForActionAllOS('editEnvironment')]);
};
}, [activeTabUid, tabs, collections, setShowEnvSettingsModal]);
// new request (ctrl/cmd + b)
useEffect(() => {
- Mousetrap.bind(['command+b', 'ctrl+b'], (e) => {
+ Mousetrap.bind([...getKeyBindingsForActionAllOS('newRequest')], (e) => {
const activeTab = find(tabs, (t) => t.uid === activeTabUid);
if (activeTab) {
const collection = findCollectionByUid(collections, activeTab.collectionUid);
@@ -135,13 +136,13 @@ export const HotkeysProvider = (props) => {
});
return () => {
- Mousetrap.unbind(['command+b', 'ctrl+b']);
+ Mousetrap.unbind([...getKeyBindingsForActionAllOS('newRequest')]);
};
}, [activeTabUid, tabs, collections, setShowNewRequestModal]);
// close tab hotkey
useEffect(() => {
- Mousetrap.bind(['command+w', 'ctrl+w'], (e) => {
+ Mousetrap.bind([...getKeyBindingsForActionAllOS('closeTab')], (e) => {
dispatch(
closeTabs({
tabUids: [activeTabUid]
@@ -152,13 +153,13 @@ export const HotkeysProvider = (props) => {
});
return () => {
- Mousetrap.unbind(['command+w', 'ctrl+w']);
+ Mousetrap.unbind([...getKeyBindingsForActionAllOS('closeTab')]);
};
}, [activeTabUid]);
// Switch to the previous tab
useEffect(() => {
- Mousetrap.bind(['command+pageup', 'ctrl+pageup'], (e) => {
+ Mousetrap.bind([...getKeyBindingsForActionAllOS('switchToPreviousTab')], (e) => {
dispatch(
switchTab({
direction: 'pageup'
@@ -169,13 +170,13 @@ export const HotkeysProvider = (props) => {
});
return () => {
- Mousetrap.unbind(['command+pageup', 'ctrl+pageup']);
+ Mousetrap.unbind([...getKeyBindingsForActionAllOS('switchToPreviousTab')]);
};
}, [dispatch]);
// Switch to the next tab
useEffect(() => {
- Mousetrap.bind(['command+pagedown', 'ctrl+pagedown'], (e) => {
+ Mousetrap.bind([...getKeyBindingsForActionAllOS('switchToNextTab')], (e) => {
dispatch(
switchTab({
direction: 'pagedown'
@@ -186,13 +187,13 @@ export const HotkeysProvider = (props) => {
});
return () => {
- Mousetrap.unbind(['command+pagedown', 'ctrl+pagedown']);
+ Mousetrap.unbind([...getKeyBindingsForActionAllOS('switchToNextTab')]);
};
}, [dispatch]);
// Close all tabs
useEffect(() => {
- Mousetrap.bind(['command+shift+w', 'ctrl+shift+w'], (e) => {
+ Mousetrap.bind([...getKeyBindingsForActionAllOS('closeAllTabs')], (e) => {
const activeTab = find(tabs, (t) => t.uid === activeTabUid);
if (activeTab) {
const collection = findCollectionByUid(collections, activeTab.collectionUid);
@@ -211,7 +212,7 @@ export const HotkeysProvider = (props) => {
});
return () => {
- Mousetrap.unbind(['command+shift+w', 'ctrl+shift+w']);
+ Mousetrap.unbind([...getKeyBindingsForActionAllOS('closeAllTabs')]);
};
}, [activeTabUid, tabs, collections, dispatch]);
diff --git a/packages/bruno-app/src/providers/Hotkeys/keyMappings.js b/packages/bruno-app/src/providers/Hotkeys/keyMappings.js
new file mode 100644
index 000000000..05ad4531b
--- /dev/null
+++ b/packages/bruno-app/src/providers/Hotkeys/keyMappings.js
@@ -0,0 +1,60 @@
+const KeyMapping = {
+ save: { mac: 'command+s', windows: 'ctrl+s', name: 'Save' },
+ sendRequest: { mac: 'command+enter', windows: 'ctrl+enter', name: 'Send Request' },
+ editEnvironment: { mac: 'command+e', windows: 'ctrl+e', name: 'Edit Environment' },
+ newRequest: { mac: 'command+b', windows: 'ctrl+b', name: 'New Request' },
+ closeTab: { mac: 'command+w', windows: 'ctrl+w', name: 'Close Tab' },
+ openPreferences: { mac: 'command+,', windows: 'ctrl+,', name: 'Open Preferences' },
+ minimizeWindow: {
+ mac: 'command+Shift+Q',
+ windows: 'control+Shift+Q',
+ name: 'Minimize Window'
+ },
+ switchToPreviousTab: {
+ mac: 'command+pageup',
+ windows: 'ctrl+pageup',
+ name: 'Switch to Previous Tab'
+ },
+ switchToNextTab: {
+ mac: 'command+pagedown',
+ windows: 'ctrl+pagedown',
+ name: 'Switch to Next Tab'
+ },
+ closeAllTabs: { mac: 'command+shift+w', windows: 'ctrl+shift+w', name: 'Close All Tabs' }
+};
+
+/**
+ * Retrieves the key bindings for a specific operating system.
+ *
+ * @param {string} os - The operating system (e.g., 'mac', 'windows').
+ * @returns {Object} An object containing the key bindings for the specified OS.
+ */
+export const getKeyBindingsForOS = (os) => {
+ const keyBindings = {};
+ for (const [action, { name, ...keys }] of Object.entries(KeyMapping)) {
+ if (keys[os]) {
+ keyBindings[action] = {
+ keys: keys[os],
+ name
+ };
+ }
+ }
+ return keyBindings;
+};
+
+/**
+ * Retrieves the key bindings for a specific action across all operating systems.
+ *
+ * @param {string} action - The action for which to retrieve key bindings.
+ * @returns {Object|null} An object containing the key bindings for macOS, Windows, or null if the action is not found.
+ */
+export const getKeyBindingsForActionAllOS = (action) => {
+ const actionBindings = KeyMapping[action];
+
+ if (!actionBindings) {
+ console.warn(`Action "${action}" not found in KeyMapping.`);
+ return null;
+ }
+
+ return [actionBindings.mac, actionBindings.windows];
+};
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 4e83b89df..066889d68 100644
--- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js
+++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js
@@ -45,6 +45,7 @@ import { sendCollectionOauth2Request as _sendCollectionOauth2Request } from 'uti
import { name } from 'file-loader';
import slash from 'utils/common/slash';
import { getGlobalEnvironmentVariables } from 'utils/collections/index';
+import { findCollectionByPathname, findEnvironmentInCollectionByName } from 'utils/collections/index';
export const renameCollection = (newName, collectionUid) => (dispatch, getState) => {
const state = getState();
@@ -987,12 +988,16 @@ export const selectEnvironment = (environmentUid, collectionUid) => (dispatch, g
}
const collectionCopy = cloneDeep(collection);
- if (environmentUid) {
- const environment = findEnvironmentInCollection(collectionCopy, environmentUid);
- if (!environment) {
- return reject(new Error('Environment not found'));
- }
- }
+
+ const environmentName = environmentUid
+ ? findEnvironmentInCollection(collectionCopy, environmentUid)?.name
+ : null;
+
+ if (environmentUid && !environmentName) {
+ return reject(new Error('Environment not found'));
+ }
+
+ ipcRenderer.invoke('renderer:update-ui-state-snapshot', { type: 'COLLECTION_ENVIRONMENT', data: { collectionPath: collection?.pathname, environmentName }});
dispatch(_selectEnvironment({ environmentUid, collectionUid }));
resolve();
@@ -1158,3 +1163,33 @@ export const saveCollectionSecurityConfig = (collectionUid, securityConfig) => (
.catch(reject);
});
};
+
+
+export const hydrateCollectionWithUiStateSnapshot = (payload) => (dispatch, getState) => {
+ const collectionSnapshotData = payload;
+ return new Promise((resolve, reject) => {
+ const state = getState();
+ try {
+ if(!collectionSnapshotData) resolve();
+ const { pathname, selectedEnvironment } = collectionSnapshotData;
+ const collection = findCollectionByPathname(state.collections.collections, pathname);
+ const collectionCopy = cloneDeep(collection);
+ const collectionUid = collectionCopy?.uid;
+
+ // update selected environment
+ if (selectedEnvironment) {
+ const environment = findEnvironmentInCollectionByName(collectionCopy, selectedEnvironment);
+ if (environment) {
+ dispatch(_selectEnvironment({ environmentUid: environment?.uid, collectionUid }));
+ }
+ }
+
+ // todo: add any other redux state that you want to save
+
+ resolve();
+ }
+ catch(error) {
+ reject(error);
+ }
+ });
+ };
\ No newline at end of file
diff --git a/packages/bruno-app/src/utils/collections/index.js b/packages/bruno-app/src/utils/collections/index.js
index c39a097fb..cd925054d 100644
--- a/packages/bruno-app/src/utils/collections/index.js
+++ b/packages/bruno-app/src/utils/collections/index.js
@@ -132,6 +132,10 @@ export const findEnvironmentInCollection = (collection, envUid) => {
return find(collection.environments, (e) => e.uid === envUid);
};
+export const findEnvironmentInCollectionByName = (collection, name) => {
+ return find(collection.environments, (e) => e.name === name);
+};
+
export const moveCollectionItem = (collection, draggedItem, targetItem) => {
let draggedItemParent = findParentItemInCollection(collection, draggedItem.uid);
diff --git a/packages/bruno-cli/src/runner/interpolate-vars.js b/packages/bruno-cli/src/runner/interpolate-vars.js
index 0253c10cd..2b727e671 100644
--- a/packages/bruno-cli/src/runner/interpolate-vars.js
+++ b/packages/bruno-cli/src/runner/interpolate-vars.js
@@ -74,17 +74,17 @@ const interpolateVars = (request, envVars = {}, runtimeVariables = {}, processEn
} else if (contentType === 'application/x-www-form-urlencoded') {
if (typeof request.data === 'object') {
try {
- let parsed = JSON.stringify(request.data);
- parsed = _interpolate(parsed);
- request.data = JSON.parse(parsed);
+ forOwn(request?.data, (value, key) => {
+ request.data[key] = _interpolate(value);
+ });
} catch (err) {}
}
} else if (contentType === 'multipart/form-data') {
if (typeof request.data === 'object' && !(request?.data instanceof FormData)) {
try {
- let parsed = JSON.stringify(request.data);
- parsed = _interpolate(parsed);
- request.data = JSON.parse(parsed);
+ forOwn(request?.data, (value, key) => {
+ request.data[key] = _interpolate(value);
+ });
} catch (err) {}
}
} else {
diff --git a/packages/bruno-cli/src/runner/prepare-request.js b/packages/bruno-cli/src/runner/prepare-request.js
index d6688a1ff..bc2b22886 100644
--- a/packages/bruno-cli/src/runner/prepare-request.js
+++ b/packages/bruno-cli/src/runner/prepare-request.js
@@ -76,17 +76,17 @@ const prepareRequest = (request, collectionRoot) => {
const password = get(request, 'auth.wsse.password', '');
const ts = new Date().toISOString();
- const nonce = crypto.randomBytes(16).toString('base64');
+ const nonce = crypto.randomBytes(16).toString('hex');
- // Create the password digest using SHA-256
- const hash = crypto.createHash('sha256');
+ // Create the password digest using SHA-1 as required for WSSE
+ const hash = crypto.createHash('sha1');
hash.update(nonce + ts + password);
- const digest = hash.digest('base64');
+ const digest = Buffer.from(hash.digest('hex').toString('utf8')).toString('base64');
// Construct the WSSE header
axiosRequest.headers[
'X-WSSE'
- ] = `UsernameToken Username="${username}", PasswordDigest="${digest}", Created="${ts}", Nonce="${nonce}"`;
+ ] = `UsernameToken Username="${username}", PasswordDigest="${digest}", Nonce="${nonce}", Created="${ts}"`;
}
}
diff --git a/packages/bruno-electron/package.json b/packages/bruno-electron/package.json
index b158989aa..8e2084828 100644
--- a/packages/bruno-electron/package.json
+++ b/packages/bruno-electron/package.json
@@ -1,5 +1,5 @@
{
- "version": "v1.30.1",
+ "version": "v1.32.1",
"name": "bruno",
"description": "Opensource API Client for Exploring and Testing APIs",
"homepage": "https://www.usebruno.com",
diff --git a/packages/bruno-electron/src/app/watcher.js b/packages/bruno-electron/src/app/watcher.js
index 589cd29d8..82d116d81 100644
--- a/packages/bruno-electron/src/app/watcher.js
+++ b/packages/bruno-electron/src/app/watcher.js
@@ -12,6 +12,7 @@ const { decryptString } = require('../utils/encryption');
const { setDotEnvVars } = require('../store/process-env');
const { setBrunoConfig } = require('../store/bruno-config');
const EnvironmentSecretsStore = require('../store/env-secrets');
+const UiStateSnapshot = require('../store/ui-state-snapshot');
const environmentSecretsStore = new EnvironmentSecretsStore();
@@ -201,7 +202,6 @@ const add = async (win, pathname, collectionUid, collectionPath) => {
const payload = {
collectionUid,
processEnvVariables: {
- ...process.env,
...jsonData
}
};
@@ -331,7 +331,6 @@ const change = async (win, pathname, collectionUid, collectionPath) => {
const payload = {
collectionUid,
processEnvVariables: {
- ...process.env,
...jsonData
}
};
@@ -423,6 +422,13 @@ const unlinkDir = (win, pathname, collectionUid, collectionPath) => {
win.webContents.send('main:collection-tree-updated', 'unlinkDir', directory);
};
+const onWatcherSetupComplete = (win, collectionPath) => {
+ const UiStateSnapshotStore = new UiStateSnapshot();
+ const collectionsSnapshotState = UiStateSnapshotStore.getCollections();
+ const collectionSnapshotState = collectionsSnapshotState?.find(c => c?.pathname == collectionPath);
+ win.webContents.send('main:hydrate-app-with-ui-state-snapshot', collectionSnapshotState);
+};
+
class Watcher {
constructor() {
this.watchers = {};
@@ -458,6 +464,7 @@ class Watcher {
let startedNewWatcher = false;
watcher
+ .on('ready', () => onWatcherSetupComplete(win, watchPath))
.on('add', (pathname) => add(win, pathname, collectionUid, watchPath))
.on('addDir', (pathname) => addDirectory(win, pathname, collectionUid, watchPath))
.on('change', (pathname) => change(win, pathname, collectionUid, watchPath))
diff --git a/packages/bruno-electron/src/ipc/collection.js b/packages/bruno-electron/src/ipc/collection.js
index 950902ece..0421060dd 100644
--- a/packages/bruno-electron/src/ipc/collection.js
+++ b/packages/bruno-electron/src/ipc/collection.js
@@ -25,9 +25,11 @@ const { moveRequestUid, deleteRequestUid } = require('../cache/requestUids');
const { deleteCookiesForDomain, getDomainsWithCookies } = require('../utils/cookies');
const EnvironmentSecretsStore = require('../store/env-secrets');
const CollectionSecurityStore = require('../store/collection-security');
+const UiStateSnapshotStore = require('../store/ui-state-snapshot');
const environmentSecretsStore = new EnvironmentSecretsStore();
const collectionSecurityStore = new CollectionSecurityStore();
+const uiStateSnapshotStore = new UiStateSnapshotStore();
const envHasSecrets = (environment = {}) => {
const secrets = _.filter(environment.variables, (v) => v.secret);
@@ -695,6 +697,14 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
return Promise.reject(error);
}
});
+
+ ipcMain.handle('renderer:update-ui-state-snapshot', (event, { type, data }) => {
+ try {
+ uiStateSnapshotStore.update({ type, data });
+ } catch (error) {
+ throw new Error(error.message);
+ }
+ });
};
const registerMainEventHandlers = (mainWindow, watcher, lastOpenedCollections) => {
diff --git a/packages/bruno-electron/src/ipc/network/interpolate-vars.js b/packages/bruno-electron/src/ipc/network/interpolate-vars.js
index 70150130a..59f494416 100644
--- a/packages/bruno-electron/src/ipc/network/interpolate-vars.js
+++ b/packages/bruno-electron/src/ipc/network/interpolate-vars.js
@@ -70,21 +70,27 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc
if (request.data.length) {
request.data = _interpolate(request.data);
}
- }
- } else if (contentType === 'application/x-www-form-urlencoded') {
- if (typeof request.data === 'object') {
+ } else if (typeof request.data === 'object') {
try {
let parsed = JSON.stringify(request.data);
parsed = _interpolate(parsed);
request.data = JSON.parse(parsed);
} catch (err) {}
}
+ } else if (contentType === 'application/x-www-form-urlencoded') {
+ if (typeof request.data === 'object') {
+ try {
+ forOwn(request?.data, (value, key) => {
+ request.data[key] = _interpolate(value);
+ });
+ } catch (err) {}
+ }
} else if (contentType === 'multipart/form-data') {
if (typeof request.data === 'object' && !(request.data instanceof FormData)) {
try {
- let parsed = JSON.stringify(request.data);
- parsed = _interpolate(parsed);
- request.data = JSON.parse(parsed);
+ forOwn(request?.data, (value, key) => {
+ request.data[key] = _interpolate(value);
+ });
} catch (err) {}
}
} else {
diff --git a/packages/bruno-electron/src/ipc/network/prepare-request.js b/packages/bruno-electron/src/ipc/network/prepare-request.js
index 1ba52895a..c8b36bb89 100644
--- a/packages/bruno-electron/src/ipc/network/prepare-request.js
+++ b/packages/bruno-electron/src/ipc/network/prepare-request.js
@@ -224,17 +224,17 @@ const setAuthHeaders = (axiosRequest, request, collectionRoot) => {
const password = get(request, 'auth.wsse.password', '');
const ts = new Date().toISOString();
- const nonce = crypto.randomBytes(16).toString('base64');
+ const nonce = crypto.randomBytes(16).toString('hex');
- // Create the password digest using SHA-256
- const hash = crypto.createHash('sha256');
+ // Create the password digest using SHA-1 as required for WSSE
+ const hash = crypto.createHash('sha1');
hash.update(nonce + ts + password);
- const digest = hash.digest('base64');
+ const digest = Buffer.from(hash.digest('hex').toString('utf8')).toString('base64');
// Construct the WSSE header
axiosRequest.headers[
'X-WSSE'
- ] = `UsernameToken Username="${username}", PasswordDigest="${digest}", Created="${ts}", Nonce="${nonce}"`;
+ ] = `UsernameToken Username="${username}", PasswordDigest="${digest}", Nonce="${nonce}", Created="${ts}"`;
break;
case 'apikey':
const apiKeyAuth = get(collectionAuth, 'apikey');
@@ -318,17 +318,17 @@ const setAuthHeaders = (axiosRequest, request, collectionRoot) => {
const password = get(request, 'auth.wsse.password', '');
const ts = new Date().toISOString();
- const nonce = crypto.randomBytes(16).toString('base64');
+ const nonce = crypto.randomBytes(16).toString('hex');
- // Create the password digest using SHA-256
- const hash = crypto.createHash('sha256');
+ // Create the password digest using SHA-1 as required for WSSE
+ const hash = crypto.createHash('sha1');
hash.update(nonce + ts + password);
- const digest = hash.digest('base64');
+ const digest = Buffer.from(hash.digest('hex').toString('utf8')).toString('base64');
// Construct the WSSE header
axiosRequest.headers[
'X-WSSE'
- ] = `UsernameToken Username="${username}", PasswordDigest="${digest}", Created="${ts}", Nonce="${nonce}"`;
+ ] = `UsernameToken Username="${username}", PasswordDigest="${digest}", Nonce="${nonce}", Created="${ts}"`;
break;
case 'apikey':
const apiKeyAuth = get(request, 'auth.apikey');
diff --git a/packages/bruno-electron/src/store/ui-state-snapshot.js b/packages/bruno-electron/src/store/ui-state-snapshot.js
new file mode 100644
index 000000000..a130c36de
--- /dev/null
+++ b/packages/bruno-electron/src/store/ui-state-snapshot.js
@@ -0,0 +1,60 @@
+const Store = require('electron-store');
+
+class UiStateSnapshotStore {
+ constructor() {
+ this.store = new Store({
+ name: 'ui-state-snapshot',
+ clearInvalidConfig: true
+ });
+ }
+
+ getCollections() {
+ return this.store.get('collections') || [];
+ }
+
+ saveCollections(collections) {
+ this.store.set('collections', collections);
+ }
+
+ getCollectionByPathname({ pathname }) {
+ let collections = this.getCollections();
+
+ let collection = collections.find(c => c?.pathname === pathname);
+ if (!collection) {
+ collection = { pathname };
+ collections.push(collection);
+ this.saveCollections(collections);
+ }
+
+ return collection;
+ }
+
+ setCollectionByPathname({ collection }) {
+ let collections = this.getCollections();
+
+ collections = collections.filter(c => c?.pathname !== collection.pathname);
+ collections.push({ ...collection });
+ this.saveCollections(collections);
+
+ return collection;
+ }
+
+ updateCollectionEnvironment({ collectionPath, environmentName }) {
+ const collection = this.getCollectionByPathname({ pathname: collectionPath });
+ collection.selectedEnvironment = environmentName;
+ this.setCollectionByPathname({ collection });
+ }
+
+ update({ type, data }) {
+ switch(type) {
+ case 'COLLECTION_ENVIRONMENT':
+ const { collectionPath, environmentName } = data;
+ this.updateCollectionEnvironment({ collectionPath, environmentName });
+ break;
+ default:
+ break;
+ }
+ }
+}
+
+module.exports = UiStateSnapshotStore;
diff --git a/packages/bruno-js/src/bru.js b/packages/bruno-js/src/bru.js
index 46c6231ba..fc6f81378 100644
--- a/packages/bruno-js/src/bru.js
+++ b/packages/bruno-js/src/bru.js
@@ -111,6 +111,14 @@ class Bru {
delete this.runtimeVariables[key];
}
+ deleteAllVars() {
+ for (let key in this.runtimeVariables) {
+ if (this.runtimeVariables.hasOwnProperty(key)) {
+ delete this.runtimeVariables[key];
+ }
+ }
+ }
+
getCollectionVar(key) {
return this._interpolate(this.collectionVariables[key]);
}
diff --git a/packages/bruno-js/src/runtime/vars-runtime.js b/packages/bruno-js/src/runtime/vars-runtime.js
index 1ed806000..94e45f46e 100644
--- a/packages/bruno-js/src/runtime/vars-runtime.js
+++ b/packages/bruno-js/src/runtime/vars-runtime.js
@@ -50,7 +50,9 @@ class VarsRuntime {
_.each(enabledVars, (v) => {
try {
const value = evaluateJsExpressionBasedOnRuntime(v.value, context, this.runtime);
- bru.setVar(v.name, value);
+ if (v.name) {
+ bru.setVar(v.name, value);
+ }
} catch (error) {
errors.set(v.name, error);
}
diff --git a/packages/bruno-js/src/sandbox/quickjs/shims/bru.js b/packages/bruno-js/src/sandbox/quickjs/shims/bru.js
index 0f8fbc39c..d55c37439 100644
--- a/packages/bruno-js/src/sandbox/quickjs/shims/bru.js
+++ b/packages/bruno-js/src/sandbox/quickjs/shims/bru.js
@@ -21,6 +21,12 @@ const addBruShimToContext = (vm, bru) => {
vm.setProp(bruObject, 'getProcessEnv', getProcessEnv);
getProcessEnv.dispose();
+ let hasEnvVar = vm.newFunction('hasEnvVar', function (key) {
+ return marshallToVm(bru.hasEnvVar(vm.dump(key)), vm);
+ });
+ vm.setProp(bruObject, 'hasEnvVar', hasEnvVar);
+ hasEnvVar.dispose();
+
let getEnvVar = vm.newFunction('getEnvVar', function (key) {
return marshallToVm(bru.getEnvVar(vm.dump(key)), vm);
});
@@ -45,6 +51,12 @@ const addBruShimToContext = (vm, bru) => {
vm.setProp(bruObject, 'setGlobalEnvVar', setGlobalEnvVar);
setGlobalEnvVar.dispose();
+ let hasVar = vm.newFunction('hasVar', function (key) {
+ return marshallToVm(bru.hasVar(vm.dump(key)), vm);
+ });
+ vm.setProp(bruObject, 'hasVar', hasVar);
+ hasVar.dispose();
+
let getVar = vm.newFunction('getVar', function (key) {
return marshallToVm(bru.getVar(vm.dump(key)), vm);
});
@@ -57,6 +69,18 @@ const addBruShimToContext = (vm, bru) => {
vm.setProp(bruObject, 'setVar', setVar);
setVar.dispose();
+ let deleteVar = vm.newFunction('deleteVar', function (key) {
+ bru.deleteVar(vm.dump(key));
+ });
+ vm.setProp(bruObject, 'deleteVar', deleteVar);
+ deleteVar.dispose();
+
+ let deleteAllVars = vm.newFunction('deleteAllVars', function () {
+ bru.deleteAllVars();
+ });
+ vm.setProp(bruObject, 'deleteAllVars', deleteAllVars);
+ deleteAllVars.dispose();
+
let setNextRequest = vm.newFunction('setNextRequest', function (nextRequest) {
bru.setNextRequest(vm.dump(nextRequest));
});
diff --git a/packages/bruno-tests/collection/echo/echo form-url-encoded.bru b/packages/bruno-tests/collection/echo/echo form-url-encoded.bru
index a0d2f0afb..7c0ce77eb 100644
--- a/packages/bruno-tests/collection/echo/echo form-url-encoded.bru
+++ b/packages/bruno-tests/collection/echo/echo form-url-encoded.bru
@@ -12,12 +12,15 @@ post {
body:form-urlencoded {
form-data-key: {{form-data-key}}
-}
-
-script:pre-request {
- bru.setVar('form-data-key', 'form-data-value');
+ form-data-stringified-object: {{form-data-stringified-object}}
}
assert {
- res.body: eq form-data-key=form-data-value
+ res.body: eq form-data-key=form-data-value&form-data-stringified-object=%7B%22foo%22%3A123%7D
+}
+
+script:pre-request {
+ let obj = JSON.stringify({foo:123});
+ bru.setVar('form-data-key', 'form-data-value');
+ bru.setVar('form-data-stringified-object', obj);
}
diff --git a/packages/bruno-tests/collection/echo/echo multipart.bru b/packages/bruno-tests/collection/echo/echo multipart.bru
index b8fd8abf7..1edb2ca8a 100644
--- a/packages/bruno-tests/collection/echo/echo multipart.bru
+++ b/packages/bruno-tests/collection/echo/echo multipart.bru
@@ -11,14 +11,18 @@ post {
}
body:multipart-form {
- foo: {{form-data-key}}
+ form-data-key: {{form-data-key}}
+ form-data-stringified-object: {{form-data-stringified-object}}
file: @file(bruno.png)
}
assert {
res.body: contains form-data-value
+ res.body: contains {"foo":123}
}
script:pre-request {
+ let obj = JSON.stringify({foo:123});
bru.setVar('form-data-key', 'form-data-value');
+ bru.setVar('form-data-stringified-object', obj);
}