From 1adfad63162c0b9d4f4bc82cc57e867ea84e26f6 Mon Sep 17 00:00:00 2001 From: Anoop M D Date: Mon, 30 Sep 2024 11:01:30 +0530 Subject: [PATCH 1/7] release: v1.31.0 --- packages/bruno-app/src/components/Sidebar/index.js | 2 +- packages/bruno-app/src/providers/App/useTelemetry.js | 2 +- packages/bruno-electron/package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/bruno-app/src/components/Sidebar/index.js b/packages/bruno-app/src/components/Sidebar/index.js index cc878bcc9..2423bce68 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.31.0
diff --git a/packages/bruno-app/src/providers/App/useTelemetry.js b/packages/bruno-app/src/providers/App/useTelemetry.js index 55b0bbdad..279b96a2c 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.31.0' } }); }; diff --git a/packages/bruno-electron/package.json b/packages/bruno-electron/package.json index b158989aa..b2b382754 100644 --- a/packages/bruno-electron/package.json +++ b/packages/bruno-electron/package.json @@ -1,5 +1,5 @@ { - "version": "v1.30.1", + "version": "v1.31.0", "name": "bruno", "description": "Opensource API Client for Exploring and Testing APIs", "homepage": "https://www.usebruno.com", From 727fa26e44dd6461d41b4c4e27d48f02781337b4 Mon Sep 17 00:00:00 2001 From: Sanjai Kumar <84461672+sanjai0py@users.noreply.github.com> Date: Mon, 30 Sep 2024 11:18:02 +0530 Subject: [PATCH 2/7] Refactor CodeMirror styling to remove glow outline around folded JSON (#3208) * Refactor CodeMirror styling to remove glow outline around folded JSON * Improved font color for better legibility. * chore: used colot from theme for codemirror fold count --------- Co-authored-by: Anoop M D --- .../bruno-app/src/components/CodeEditor/StyledWrapper.js | 6 ++++++ 1 file changed, 6 insertions(+) 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; From f35b715c6f2af0b019e8afc295fc8b1d00bc26b9 Mon Sep 17 00:00:00 2001 From: lohit Date: Mon, 30 Sep 2024 16:39:05 +0530 Subject: [PATCH 3/7] feat: restrict access to system process env vars (#3226) --- packages/bruno-electron/src/app/watcher.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/bruno-electron/src/app/watcher.js b/packages/bruno-electron/src/app/watcher.js index 589cd29d8..b93b01a55 100644 --- a/packages/bruno-electron/src/app/watcher.js +++ b/packages/bruno-electron/src/app/watcher.js @@ -201,7 +201,6 @@ const add = async (win, pathname, collectionUid, collectionPath) => { const payload = { collectionUid, processEnvVariables: { - ...process.env, ...jsonData } }; @@ -331,7 +330,6 @@ const change = async (win, pathname, collectionUid, collectionPath) => { const payload = { collectionUid, processEnvVariables: { - ...process.env, ...jsonData } }; From d448599a531ce29b9a8c0feb661b8cfa99557789 Mon Sep 17 00:00:00 2001 From: lohit Date: Mon, 30 Sep 2024 16:51:49 +0530 Subject: [PATCH 4/7] feat: ui-state-snapshot (#3215) * wip: save env * feat: updates * feat: updates --- .../src/providers/App/useIpcEvents.js | 7 ++- .../ReduxStore/slices/collections/actions.js | 41 +++++++++++-- .../bruno-app/src/utils/collections/index.js | 4 ++ packages/bruno-electron/src/app/watcher.js | 9 +++ packages/bruno-electron/src/ipc/collection.js | 10 ++++ .../src/store/ui-state-snapshot.js | 60 +++++++++++++++++++ 6 files changed, 126 insertions(+), 5 deletions(-) create mode 100644 packages/bruno-electron/src/store/ui-state-snapshot.js diff --git a/packages/bruno-app/src/providers/App/useIpcEvents.js b/packages/bruno-app/src/providers/App/useIpcEvents.js index f4a04030f..b885ad74d 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, hydrateCollectionsWithUiStateSnapshot } from 'providers/ReduxStore/slices/collections/actions'; import toast from 'react-hot-toast'; import { useDispatch } from 'react-redux'; import { isElectron } from 'utils/common/platform'; @@ -149,6 +149,10 @@ const useIpcEvents = () => { dispatch(updateCookies(val)); }); + const removeSnapshotHydrationListener = ipcRenderer.on('main:hydrate-app-with-ui-state-snapshot', (val) => { + dispatch(hydrateCollectionsWithUiStateSnapshot(val)); + }) + return () => { removeCollectionTreeUpdateListener(); removeOpenCollectionListener(); @@ -165,6 +169,7 @@ const useIpcEvents = () => { removePreferencesUpdatesListener(); removeCookieUpdateListener(); removeSystemProxyEnvUpdatesListener(); + removeSnapshotHydrationListener(); }; }, [isElectron]); }; 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 777a194ab..e582b0f1f 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js @@ -44,6 +44,7 @@ import { parsePathParams, parseQueryParams, splitOnFirst } from 'utils/url/index import { sendCollectionOauth2Request as _sendCollectionOauth2Request } from 'utils/network/index'; import { name } from 'file-loader'; import slash from 'utils/common/slash'; +import { findCollectionByPathname, findEnvironmentInCollectionByName } from 'utils/collections/index'; export const renameCollection = (newName, collectionUid) => (dispatch, getState) => { const state = getState(); @@ -972,13 +973,15 @@ export const selectEnvironment = (environmentUid, collectionUid) => (dispatch, g const collectionCopy = cloneDeep(collection); if (environmentUid) { const environment = findEnvironmentInCollection(collectionCopy, environmentUid); - if (!environment) { + if (environment) { + ipcRenderer.invoke('renderer:update-ui-state-snapshot', { type: 'COLLECTION_ENVIRONMENT', data: { collectionPath: collection?.pathname, environmentName: environment?.name } }) + dispatch(_selectEnvironment({ environmentUid, collectionUid })); + resolve(); + } + else { return reject(new Error('Environment not found')); } } - - dispatch(_selectEnvironment({ environmentUid, collectionUid })); - resolve(); }); }; @@ -1141,3 +1144,33 @@ export const saveCollectionSecurityConfig = (collectionUid, securityConfig) => ( .catch(reject); }); }; + + +export const hydrateCollectionsWithUiStateSnapshot = (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 ea8712be5..ddb98f531 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-electron/src/app/watcher.js b/packages/bruno-electron/src/app/watcher.js index b93b01a55..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(); @@ -421,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 = {}; @@ -456,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..0aa2058c2 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 UiStateSnapshot = require('../store/ui-state-snapshot'); const environmentSecretsStore = new EnvironmentSecretsStore(); const collectionSecurityStore = new CollectionSecurityStore(); +const UiStateSnapshotStore = new UiStateSnapshot(); 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/store/ui-state-snapshot.js b/packages/bruno-electron/src/store/ui-state-snapshot.js new file mode 100644 index 000000000..b3d1909b6 --- /dev/null +++ b/packages/bruno-electron/src/store/ui-state-snapshot.js @@ -0,0 +1,60 @@ +const Store = require('electron-store'); + +class UiStateSnapshot { + 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 = UiStateSnapshot; From e2baed6724ae49ea9ded78dc3918f86f3365a36a Mon Sep 17 00:00:00 2001 From: lohit Date: Mon, 30 Sep 2024 17:14:58 +0530 Subject: [PATCH 5/7] fix: interpolate json body for type object -- graphql variables (#3212) --- packages/bruno-electron/src/ipc/network/interpolate-vars.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/bruno-electron/src/ipc/network/interpolate-vars.js b/packages/bruno-electron/src/ipc/network/interpolate-vars.js index 5ea2bf7f4..e60d52428 100644 --- a/packages/bruno-electron/src/ipc/network/interpolate-vars.js +++ b/packages/bruno-electron/src/ipc/network/interpolate-vars.js @@ -68,6 +68,12 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc if (request.data.length) { request.data = _interpolate(request.data); } + } 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') { From 02a82c5371d347a8148cac3be7b221b46593c4e2 Mon Sep 17 00:00:00 2001 From: Pragadesh-45 <54320162+Pragadesh-45@users.noreply.github.com> Date: Mon, 30 Sep 2024 17:19:18 +0530 Subject: [PATCH 6/7] refactor: `ReorderTable` component to use `useMemo` for rowsOrder (#3227) --- .../bruno-app/src/components/ReorderTable/index.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) 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']) From 95e56cd9c950ad6bca80760dcb8ea508764f0a86 Mon Sep 17 00:00:00 2001 From: Sanjai Kumar <84461672+sanjai0py@users.noreply.github.com> Date: Mon, 30 Sep 2024 17:20:46 +0530 Subject: [PATCH 7/7] Added Keybindings tab. (#3204) * Added Keybindings tab. * Minor Refactoring --- .../Preferences/Keybindings/StyledWrapper.js | 46 ++++++++++++++ .../Preferences/Keybindings/index.js | 45 ++++++++++++++ .../src/components/Preferences/index.js | 12 +++- .../bruno-app/src/providers/Hotkeys/index.js | 33 +++++----- .../src/providers/Hotkeys/keyMappings.js | 60 +++++++++++++++++++ 5 files changed, 179 insertions(+), 17 deletions(-) create mode 100644 packages/bruno-app/src/components/Preferences/Keybindings/StyledWrapper.js create mode 100644 packages/bruno-app/src/components/Preferences/Keybindings/index.js create mode 100644 packages/bruno-app/src/providers/Hotkeys/keyMappings.js 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..e12969388 --- /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.colors.text.white}; + 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 ( + +
+ + + + + + + + + {keyMapping ? ( + Object.entries(keyMapping).map(([action, { name, keys }], index) => ( + + + + + )) + ) : ( + + + + )} + +
CommandKeybinding
{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/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]; +};