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 df9103891..ea63602df 100644 --- a/packages/bruno-app/src/components/CodeEditor/index.js +++ b/packages/bruno-app/src/components/CodeEditor/index.js @@ -55,6 +55,7 @@ if (!SERVER_RENDERED) { 'req.setMaxRedirects(maxRedirects)', 'req.getTimeout()', 'req.setTimeout(timeout)', + 'req.getExecutionMode()', 'bru', 'bru.cwd()', 'bru.getEnvName(key)', @@ -68,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/CollectionSettings/Vars/VarsTable/index.js b/packages/bruno-app/src/components/CollectionSettings/Vars/VarsTable/index.js index 950076b60..ebc6a2fe7 100644 --- a/packages/bruno-app/src/components/CollectionSettings/Vars/VarsTable/index.js +++ b/packages/bruno-app/src/components/CollectionSettings/Vars/VarsTable/index.js @@ -83,7 +83,6 @@ const VarsTable = ({ collection, vars, varType }) => {
Value -
) : ( diff --git a/packages/bruno-app/src/components/Environments/EnvironmentSettings/RenameEnvironment/index.js b/packages/bruno-app/src/components/Environments/EnvironmentSettings/RenameEnvironment/index.js index ac2d2623a..3ebcadca1 100644 --- a/packages/bruno-app/src/components/Environments/EnvironmentSettings/RenameEnvironment/index.js +++ b/packages/bruno-app/src/components/Environments/EnvironmentSettings/RenameEnvironment/index.js @@ -22,6 +22,9 @@ const RenameEnvironment = ({ onClose, environment, collection }) => { .required('name is required') }), onSubmit: (values) => { + if (values.name === environment.name) { + return; + } dispatch(renameEnvironment(values.name, environment.uid, collection.uid)) .then(() => { toast.success('Environment renamed successfully'); diff --git a/packages/bruno-app/src/components/FolderSettings/Vars/VarsTable/index.js b/packages/bruno-app/src/components/FolderSettings/Vars/VarsTable/index.js index d0a77de44..17d79629e 100644 --- a/packages/bruno-app/src/components/FolderSettings/Vars/VarsTable/index.js +++ b/packages/bruno-app/src/components/FolderSettings/Vars/VarsTable/index.js @@ -82,7 +82,6 @@ const VarsTable = ({ folder, collection, vars, varType }) => {
Value -
) : ( 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/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/RequestPane/Vars/VarsTable/index.js b/packages/bruno-app/src/components/RequestPane/Vars/VarsTable/index.js index 84f040c6e..0f94f35bb 100644 --- a/packages/bruno-app/src/components/RequestPane/Vars/VarsTable/index.js +++ b/packages/bruno-app/src/components/RequestPane/Vars/VarsTable/index.js @@ -83,7 +83,6 @@ const VarsTable = ({ item, collection, vars, varType }) => {
Value -
) : ( 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 index 78977cabb..9d5648907 100644 --- 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 @@ -32,7 +32,10 @@ const CodeView = ({ language, item }) => { let snippet = ''; try { - snippet = new HTTPSnippet(buildHarRequest({ request: item.request, headers })).convert(target, client); + snippet = new HTTPSnippet(buildHarRequest({ request: item.request, headers, type: item.type })).convert( + target, + client + ); } catch (e) { console.error(e); snippet = 'Error generating code snippet'; diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RenameCollectionItem/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RenameCollectionItem/index.js index 5711cdbcd..6cf8cb21a 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RenameCollectionItem/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RenameCollectionItem/index.js @@ -28,9 +28,12 @@ const RenameCollectionItem = ({ collection, item, onClose }) => { if (!isFolder && item.draft) { await dispatch(saveRequest(item.uid, collection.uid, true)); } + if (item.name === values.name) { + return; + } dispatch(renameItem(values.name, item.uid, collection.uid)) .then(() => { - toast.success('Request renamed!'); + toast.success('Request renamed'); onClose(); }) .catch((err) => { @@ -55,7 +58,7 @@ const RenameCollectionItem = ({ collection, item, onClose }) => { handleConfirm={onSubmit} handleCancel={onClose} > -
e.preventDefault()}> + e.preventDefault()}>
)} - {!isFolder && item.type === 'http-request' && ( + {!isFolder && (item.type === 'http-request' || item.type === 'graphql-request') && (
{ diff --git a/packages/bruno-app/src/components/Sidebar/index.js b/packages/bruno-app/src/components/Sidebar/index.js index cc878bcc9..fe02a8ae2 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.0
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/App/useTelemetry.js b/packages/bruno-app/src/providers/App/useTelemetry.js index 55b0bbdad..0e709abcb 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.0' } }); }; 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 772be2699..4176aa2d5 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js @@ -43,6 +43,8 @@ import { resolveRequestFilename } from 'utils/common/platform'; 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(); @@ -401,7 +403,7 @@ 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', slash(item.pathname), newPathname, newName).then(resolve).catch(reject); }); }; @@ -972,13 +974,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(); }); }; @@ -1151,3 +1155,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/codegenerator/har.js b/packages/bruno-app/src/utils/codegenerator/har.js index fa0738503..9bbd0eea9 100644 --- a/packages/bruno-app/src/utils/codegenerator/har.js +++ b/packages/bruno-app/src/utils/codegenerator/har.js @@ -31,6 +31,7 @@ const createHeaders = (request, headers) => { if (contentType !== '') { enabledHeaders.push({ name: 'content-type', value: contentType }); } + return enabledHeaders; }; @@ -43,7 +44,14 @@ const createQuery = (queryParams = []) => { })); }; -const createPostData = (body) => { +const createPostData = (body, type) => { + if (type === 'graphql-request') { + return { + mimeType: 'application/json', + text: JSON.stringify(body[body.mode]) + }; + } + const contentType = createContentType(body.mode); if (body.mode === 'formUrlEncoded' || body.mode === 'multipartForm') { return { @@ -64,7 +72,7 @@ const createPostData = (body) => { } }; -export const buildHarRequest = ({ request, headers }) => { +export const buildHarRequest = ({ request, headers, type }) => { return { method: request.method, url: encodeURI(request.url), @@ -72,7 +80,7 @@ export const buildHarRequest = ({ request, headers }) => { cookies: [], headers: createHeaders(request, headers), queryString: createQuery(request.params), - postData: createPostData(request.body), + postData: createPostData(request.body, type), 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 1d69332a1..710f0e8b9 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-app/src/utils/importers/openapi-collection.js b/packages/bruno-app/src/utils/importers/openapi-collection.js index 3bfe8394f..fd25b7f51 100644 --- a/packages/bruno-app/src/utils/importers/openapi-collection.js +++ b/packages/bruno-app/src/utils/importers/openapi-collection.js @@ -32,7 +32,7 @@ const readFile = (files) => { const ensureUrl = (url) => { // emoving multiple slashes after the protocol if it exists, or after the beginning of the string otherwise - return url.replace(/(^\w+:|^)\/{2,}/, '$1/'); + return url.replace(/([^:])\/{2,}/g, '$1/'); }; const buildEmptyJsonBody = (bodySchema) => { diff --git a/packages/bruno-cli/src/runner/interpolate-vars.js b/packages/bruno-cli/src/runner/interpolate-vars.js index 39e92a6ec..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 { @@ -122,7 +122,8 @@ const interpolateVars = (request, envVars = {}, runtimeVariables = {}, processEn }) .join(''); - request.url = url.origin + interpolatedUrlPath + url.search; + const trailingSlash = url.pathname.endsWith('/') ? '/' : ''; + request.url = url.origin + interpolatedUrlPath + trailingSlash + url.search; } if (request.proxy) { diff --git a/packages/bruno-cli/src/runner/run-single-request.js b/packages/bruno-cli/src/runner/run-single-request.js index c36a9b97f..cb59c78ba 100644 --- a/packages/bruno-cli/src/runner/run-single-request.js +++ b/packages/bruno-cli/src/runner/run-single-request.js @@ -43,6 +43,8 @@ const runSingleRequest = async function ( request = prepareRequest(bruJson.request, collectionRoot); + request.__bruno__executionMode = 'cli'; + const scriptingConfig = get(brunoConfig, 'scripts', {}); scriptingConfig.runtime = runtime; diff --git a/packages/bruno-electron/package.json b/packages/bruno-electron/package.json index b158989aa..d71156a4a 100644 --- a/packages/bruno-electron/package.json +++ b/packages/bruno-electron/package.json @@ -1,5 +1,5 @@ { - "version": "v1.30.1", + "version": "v1.32.0", "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 3d7d54423..3911f4349 100644 --- a/packages/bruno-electron/src/ipc/collection.js +++ b/packages/bruno-electron/src/ipc/collection.js @@ -17,6 +17,8 @@ const { sanitizeDirectoryName, isWSLPath, normalizeWslPath, + normalizeAndResolvePath, + safeToRename } = require('../utils/filesystem'); const { openCollectionDialog } = require('../app/collections'); const { generateUidBasedOnHash, stringifyJson, safeParseJSON, safeStringifyJSON } = require('../utils/common'); @@ -24,9 +26,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); @@ -308,7 +312,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection } const newEnvFilePath = path.join(envDirPath, `${newName}.bru`); - if (fs.existsSync(newEnvFilePath)) { + if (!safeToRename(envFilePath, newEnvFilePath)) { throw new Error(`environment: ${newEnvFilePath} already exists`); } @@ -341,21 +345,18 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection ipcMain.handle('renderer:rename-item', async (event, oldPath, newPath, newName) => { try { // Normalize paths if they are WSL paths - if (isWSLPath(oldPath)) { - oldPath = normalizeWslPath(oldPath); - } - if (isWSLPath(newPath)) { - newPath = normalizeWslPath(newPath); - } + oldPath = isWSLPath(oldPath) ? normalizeWslPath(oldPath) : normalizeAndResolvePath(oldPath); + newPath = isWSLPath(newPath) ? normalizeWslPath(newPath) : normalizeAndResolvePath(newPath); + // Check if the old path exists if (!fs.existsSync(oldPath)) { throw new Error(`path: ${oldPath} does not exist`); } - if (fs.existsSync(newPath)) { - throw new Error(`path: ${oldPath} already exists`); + + if (!safeToRename(oldPath, newPath)) { + throw new Error(`path: ${newPath} already exists`); } - // if its directory, rename and return if (isDirectory(oldPath)) { const bruFilesAtSource = await searchForBruFiles(oldPath); @@ -376,12 +377,13 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection const jsonData = bruToJson(data); jsonData.name = newName; - moveRequestUid(oldPath, newPath); const content = jsonToBru(jsonData); - await writeFile(newPath, content); await fs.unlinkSync(oldPath); + await writeFile(newPath, content); + + return newPath; } catch (error) { return Promise.reject(error); } @@ -707,6 +709,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/index.js b/packages/bruno-electron/src/ipc/network/index.js index b0dfbfb68..328232e31 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -525,6 +525,7 @@ const registerNetworkIpc = (mainWindow) => { const collectionRoot = get(collection, 'root', {}); const request = prepareRequest(item, collection); + request.__bruno__executionMode = 'standalone'; const envVars = getEnvVars(environment); const processEnvVars = getProcessEnvVars(collectionUid); const brunoConfig = getBrunoConfig(collectionUid); @@ -717,6 +718,7 @@ const registerNetworkIpc = (mainWindow) => { const collectionRoot = get(collection, 'root', {}); const _request = collectionRoot?.request; const request = prepareCollectionRequest(_request, collectionRoot, collectionPath); + request.__bruno__executionMode = 'standalone'; const envVars = getEnvVars(environment); const processEnvVars = getProcessEnvVars(collectionUid); const brunoConfig = getBrunoConfig(collectionUid); @@ -960,6 +962,8 @@ const registerNetworkIpc = (mainWindow) => { }); const request = prepareRequest(item, collection); + request.__bruno__executionMode = 'runner'; + const requestUid = uuid(); const processEnvVars = getProcessEnvVars(collectionUid); diff --git a/packages/bruno-electron/src/ipc/network/interpolate-vars.js b/packages/bruno-electron/src/ipc/network/interpolate-vars.js index 90b072658..99b0191cd 100644 --- a/packages/bruno-electron/src/ipc/network/interpolate-vars.js +++ b/packages/bruno-electron/src/ipc/network/interpolate-vars.js @@ -68,21 +68,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 { @@ -120,7 +126,8 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc }) .join(''); - request.url = url.origin + urlPathnameInterpolatedWithPathParams + url.search; + const trailingSlash = url.pathname.endsWith('/') ? '/' : ''; + request.url = url.origin + urlPathnameInterpolatedWithPathParams + trailingSlash + url.search; } if (request.proxy) { 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; diff --git a/packages/bruno-electron/src/utils/filesystem.js b/packages/bruno-electron/src/utils/filesystem.js index e4773f5ef..dabace00c 100644 --- a/packages/bruno-electron/src/utils/filesystem.js +++ b/packages/bruno-electron/src/utils/filesystem.js @@ -3,6 +3,7 @@ const fs = require('fs-extra'); const fsPromises = require('fs/promises'); const { dialog } = require('electron'); const isValidPathname = require('is-valid-path'); +const os = require('os'); const exists = async (p) => { try { @@ -169,12 +170,34 @@ const searchForBruFiles = (dir) => { return searchForFiles(dir, '.bru'); }; -// const isW - const sanitizeDirectoryName = (name) => { return name.replace(/[<>:"/\\|?*\x00-\x1F]+/g, '-'); }; +const safeToRename = (oldPath, newPath) => { + try { + // If the new path doesn't exist, it's safe to rename + if (!fs.existsSync(newPath)) { + return true; + } + + const oldStat = fs.statSync(oldPath); + const newStat = fs.statSync(newPath); + + if (os.platform() === 'win32') { + // Windows-specific comparison: + // Check if both files have the same birth time, size (Since, Win FAT-32 doesn't use inodes) + + return oldStat.birthtimeMs === newStat.birthtimeMs && oldStat.size === newStat.size; + } + // Unix/Linux/MacOS: Check inode to see if they are the same file + return oldStat.ino === newStat.ino; + } catch (error) { + console.error(`Error checking file rename safety for ${oldPath} and ${newPath}:`, error); + return false; + } +}; + module.exports = { isValidPathname, exists, @@ -195,5 +218,6 @@ module.exports = { chooseFileToSave, searchForFiles, searchForBruFiles, - sanitizeDirectoryName + sanitizeDirectoryName, + safeToRename }; diff --git a/packages/bruno-js/src/bru.js b/packages/bruno-js/src/bru.js index 7f24cea14..30f016e9f 100644 --- a/packages/bruno-js/src/bru.js +++ b/packages/bruno-js/src/bru.js @@ -97,6 +97,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/bruno-request.js b/packages/bruno-js/src/bruno-request.js index b0d22b6ac..32e40c19e 100644 --- a/packages/bruno-js/src/bruno-request.js +++ b/packages/bruno-js/src/bruno-request.js @@ -173,6 +173,10 @@ class BrunoRequest { disableParsingResponseJson() { this.req.__brunoDisableParsingResponseJson = true; } + + getExecutionMode() { + return this.req.__bruno__executionMode; + } } module.exports = BrunoRequest; diff --git a/packages/bruno-js/src/sandbox/quickjs/shims/bru.js b/packages/bruno-js/src/sandbox/quickjs/shims/bru.js index f045b134b..0e489265c 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); }); @@ -33,6 +39,12 @@ const addBruShimToContext = (vm, bru) => { vm.setProp(bruObject, 'setEnvVar', setEnvVar); setEnvVar.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); }); @@ -45,6 +57,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-js/src/sandbox/quickjs/shims/bruno-request.js b/packages/bruno-js/src/sandbox/quickjs/shims/bruno-request.js index 1edfaaadb..e3f364fe7 100644 --- a/packages/bruno-js/src/sandbox/quickjs/shims/bruno-request.js +++ b/packages/bruno-js/src/sandbox/quickjs/shims/bruno-request.js @@ -111,6 +111,12 @@ const addBrunoRequestShimToContext = (vm, req) => { vm.setProp(reqObject, 'disableParsingResponseJson', disableParsingResponseJson); disableParsingResponseJson.dispose(); + let getExecutionMode = vm.newFunction('getExecutionMode', function () { + return marshallToVm(req.getExecutionMode(), vm); + }); + vm.setProp(reqObject, 'getExecutionMode', getExecutionMode); + getExecutionMode.dispose(); + vm.setProp(vm.global, 'req', reqObject); reqObject.dispose(); }; 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); }