mirror of
https://github.com/usebruno/bruno.git
synced 2024-12-03 21:34:36 +01:00
Merge branch 'usebruno:main' into feature/add-raw-file-request-body-option
This commit is contained in:
commit
e58f54d34a
@ -10,6 +10,12 @@ const StyledWrapper = styled.div`
|
|||||||
flex: 1 1 0;
|
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-horizontal div,
|
||||||
.CodeMirror-overlayscroll-vertical div {
|
.CodeMirror-overlayscroll-vertical div {
|
||||||
background: #d2d7db;
|
background: #d2d7db;
|
||||||
|
@ -55,6 +55,7 @@ if (!SERVER_RENDERED) {
|
|||||||
'req.setMaxRedirects(maxRedirects)',
|
'req.setMaxRedirects(maxRedirects)',
|
||||||
'req.getTimeout()',
|
'req.getTimeout()',
|
||||||
'req.setTimeout(timeout)',
|
'req.setTimeout(timeout)',
|
||||||
|
'req.getExecutionMode()',
|
||||||
'bru',
|
'bru',
|
||||||
'bru.cwd()',
|
'bru.cwd()',
|
||||||
'bru.getEnvName(key)',
|
'bru.getEnvName(key)',
|
||||||
@ -68,6 +69,7 @@ if (!SERVER_RENDERED) {
|
|||||||
'bru.getVar(key)',
|
'bru.getVar(key)',
|
||||||
'bru.setVar(key,value)',
|
'bru.setVar(key,value)',
|
||||||
'bru.deleteVar(key)',
|
'bru.deleteVar(key)',
|
||||||
|
'bru.deleteAllVars()',
|
||||||
'bru.setNextRequest(requestName)',
|
'bru.setNextRequest(requestName)',
|
||||||
'req.disableParsingResponseJson()',
|
'req.disableParsingResponseJson()',
|
||||||
'bru.getRequestVar(key)',
|
'bru.getRequestVar(key)',
|
||||||
|
@ -83,7 +83,6 @@ const VarsTable = ({ collection, vars, varType }) => {
|
|||||||
<td>
|
<td>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<span>Value</span>
|
<span>Value</span>
|
||||||
<InfoTip text="You can write any valid JS Template Literal here" infotipId="request-var" />
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
) : (
|
) : (
|
||||||
|
@ -22,6 +22,9 @@ const RenameEnvironment = ({ onClose, environment, collection }) => {
|
|||||||
.required('name is required')
|
.required('name is required')
|
||||||
}),
|
}),
|
||||||
onSubmit: (values) => {
|
onSubmit: (values) => {
|
||||||
|
if (values.name === environment.name) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
dispatch(renameEnvironment(values.name, environment.uid, collection.uid))
|
dispatch(renameEnvironment(values.name, environment.uid, collection.uid))
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success('Environment renamed successfully');
|
toast.success('Environment renamed successfully');
|
||||||
|
@ -82,7 +82,6 @@ const VarsTable = ({ folder, collection, vars, varType }) => {
|
|||||||
<td>
|
<td>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<span>Value</span>
|
<span>Value</span>
|
||||||
<InfoTip text="You can write any valid JS Template Literal here" infotipId="request-var" />
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
) : (
|
) : (
|
||||||
|
@ -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;
|
@ -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 (
|
||||||
|
<StyledWrapper className="w-full">
|
||||||
|
<div className="table-container">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Command</th>
|
||||||
|
<th>Keybinding</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{keyMapping ? (
|
||||||
|
Object.entries(keyMapping).map(([action, { name, keys }], index) => (
|
||||||
|
<tr key={index}>
|
||||||
|
<td>{name}</td>
|
||||||
|
<td>
|
||||||
|
{keys.split('+').map((key, i) => (
|
||||||
|
<div className="key-button" key={i}>
|
||||||
|
{key}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<tr>
|
||||||
|
<td colSpan="2">No key bindings available</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</StyledWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Keybindings;
|
@ -1,11 +1,14 @@
|
|||||||
import Modal from 'components/Modal/index';
|
import Modal from 'components/Modal/index';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
import Support from './Support';
|
import Support from './Support';
|
||||||
import General from './General';
|
import General from './General';
|
||||||
import Proxy from './ProxySettings';
|
import Proxy from './ProxySettings';
|
||||||
|
import Display from './Display';
|
||||||
|
import Keybindings from './Keybindings';
|
||||||
|
|
||||||
import StyledWrapper from './StyledWrapper';
|
import StyledWrapper from './StyledWrapper';
|
||||||
import Display from './Display/index';
|
|
||||||
|
|
||||||
const Preferences = ({ onClose }) => {
|
const Preferences = ({ onClose }) => {
|
||||||
const [tab, setTab] = useState('general');
|
const [tab, setTab] = useState('general');
|
||||||
@ -30,6 +33,10 @@ const Preferences = ({ onClose }) => {
|
|||||||
return <Display close={onClose} />;
|
return <Display close={onClose} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'keybindings': {
|
||||||
|
return <Keybindings close={onClose} />;
|
||||||
|
}
|
||||||
|
|
||||||
case 'support': {
|
case 'support': {
|
||||||
return <Support />;
|
return <Support />;
|
||||||
}
|
}
|
||||||
@ -50,6 +57,9 @@ const Preferences = ({ onClose }) => {
|
|||||||
<div className={getTabClassname('proxy')} role="tab" onClick={() => setTab('proxy')}>
|
<div className={getTabClassname('proxy')} role="tab" onClick={() => setTab('proxy')}>
|
||||||
Proxy
|
Proxy
|
||||||
</div>
|
</div>
|
||||||
|
<div className={getTabClassname('keybindings')} role="tab" onClick={() => setTab('keybindings')}>
|
||||||
|
Keybindings
|
||||||
|
</div>
|
||||||
<div className={getTabClassname('support')} role="tab" onClick={() => setTab('support')}>
|
<div className={getTabClassname('support')} role="tab" onClick={() => setTab('support')}>
|
||||||
Support
|
Support
|
||||||
</div>
|
</div>
|
||||||
|
@ -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';
|
import { IconGripVertical, IconMinusVertical } from '@tabler/icons';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -13,17 +13,17 @@ import { IconGripVertical, IconMinusVertical } from '@tabler/icons';
|
|||||||
|
|
||||||
const ReorderTable = ({ children, updateReorderedItem }) => {
|
const ReorderTable = ({ children, updateReorderedItem }) => {
|
||||||
const tbodyRef = useRef();
|
const tbodyRef = useRef();
|
||||||
const [rowsOrder, setRowsOrder] = useState(React.Children.toArray(children));
|
|
||||||
const [hoveredRow, setHoveredRow] = useState(null);
|
const [hoveredRow, setHoveredRow] = useState(null);
|
||||||
const [dragStart, setDragStart] = 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(() => {
|
useEffect(() => {
|
||||||
setRowsOrder(React.Children.toArray(children));
|
|
||||||
handleRowHover(null, false);
|
handleRowHover(null, false);
|
||||||
}, [children, dragStart]);
|
}, [children]);
|
||||||
|
|
||||||
const handleRowHover = (index, hoverstatus = true) => {
|
const handleRowHover = (index, hoverstatus = true) => {
|
||||||
setHoveredRow(hoverstatus ? index : null);
|
setHoveredRow(hoverstatus ? index : null);
|
||||||
@ -48,7 +48,6 @@ const ReorderTable = ({ children, updateReorderedItem }) => {
|
|||||||
const updatedRowsOrder = [...rowsOrder];
|
const updatedRowsOrder = [...rowsOrder];
|
||||||
const [movedRow] = updatedRowsOrder.splice(fromIndex, 1);
|
const [movedRow] = updatedRowsOrder.splice(fromIndex, 1);
|
||||||
updatedRowsOrder.splice(toIndex, 0, movedRow);
|
updatedRowsOrder.splice(toIndex, 0, movedRow);
|
||||||
setRowsOrder(updatedRowsOrder);
|
|
||||||
|
|
||||||
updateReorderedItem({
|
updateReorderedItem({
|
||||||
updateReorderedItem: updatedRowsOrder.map((row) => row.props['data-uid'])
|
updateReorderedItem: updatedRowsOrder.map((row) => row.props['data-uid'])
|
||||||
|
@ -83,7 +83,6 @@ const VarsTable = ({ item, collection, vars, varType }) => {
|
|||||||
<td>
|
<td>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<span>Value</span>
|
<span>Value</span>
|
||||||
<InfoTip text="You can write any valid JS Template Literal here" infotipId="request-var" />
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
) : (
|
) : (
|
||||||
|
@ -32,7 +32,10 @@ const CodeView = ({ language, item }) => {
|
|||||||
|
|
||||||
let snippet = '';
|
let snippet = '';
|
||||||
try {
|
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) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
snippet = 'Error generating code snippet';
|
snippet = 'Error generating code snippet';
|
||||||
|
@ -28,9 +28,12 @@ const RenameCollectionItem = ({ collection, item, onClose }) => {
|
|||||||
if (!isFolder && item.draft) {
|
if (!isFolder && item.draft) {
|
||||||
await dispatch(saveRequest(item.uid, collection.uid, true));
|
await dispatch(saveRequest(item.uid, collection.uid, true));
|
||||||
}
|
}
|
||||||
|
if (item.name === values.name) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
dispatch(renameItem(values.name, item.uid, collection.uid))
|
dispatch(renameItem(values.name, item.uid, collection.uid))
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success('Request renamed!');
|
toast.success('Request renamed');
|
||||||
onClose();
|
onClose();
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
@ -55,7 +58,7 @@ const RenameCollectionItem = ({ collection, item, onClose }) => {
|
|||||||
handleConfirm={onSubmit}
|
handleConfirm={onSubmit}
|
||||||
handleCancel={onClose}
|
handleCancel={onClose}
|
||||||
>
|
>
|
||||||
<form className="bruno-form" onSubmit={e => e.preventDefault()}>
|
<form className="bruno-form" onSubmit={(e) => e.preventDefault()}>
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="name" className="block font-semibold">
|
<label htmlFor="name" className="block font-semibold">
|
||||||
{isFolder ? 'Folder' : 'Request'} Name
|
{isFolder ? 'Folder' : 'Request'} Name
|
||||||
|
@ -349,7 +349,7 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
|||||||
Run
|
Run
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!isFolder && item.type === 'http-request' && (
|
{!isFolder && (item.type === 'http-request' || item.type === 'graphql-request') && (
|
||||||
<div
|
<div
|
||||||
className="dropdown-item"
|
className="dropdown-item"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
|
@ -185,7 +185,7 @@ const Sidebar = () => {
|
|||||||
Star
|
Star
|
||||||
</GitHubButton> */}
|
</GitHubButton> */}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-grow items-center justify-end text-xs mr-2">v1.30.1</div>
|
<div className="flex flex-grow items-center justify-end text-xs mr-2">v1.32.0</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -19,7 +19,7 @@ import {
|
|||||||
runRequestEvent,
|
runRequestEvent,
|
||||||
scriptEnvironmentUpdateEvent
|
scriptEnvironmentUpdateEvent
|
||||||
} from 'providers/ReduxStore/slices/collections';
|
} 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 toast from 'react-hot-toast';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import { isElectron } from 'utils/common/platform';
|
import { isElectron } from 'utils/common/platform';
|
||||||
@ -149,6 +149,10 @@ const useIpcEvents = () => {
|
|||||||
dispatch(updateCookies(val));
|
dispatch(updateCookies(val));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const removeSnapshotHydrationListener = ipcRenderer.on('main:hydrate-app-with-ui-state-snapshot', (val) => {
|
||||||
|
dispatch(hydrateCollectionsWithUiStateSnapshot(val));
|
||||||
|
})
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
removeCollectionTreeUpdateListener();
|
removeCollectionTreeUpdateListener();
|
||||||
removeOpenCollectionListener();
|
removeOpenCollectionListener();
|
||||||
@ -165,6 +169,7 @@ const useIpcEvents = () => {
|
|||||||
removePreferencesUpdatesListener();
|
removePreferencesUpdatesListener();
|
||||||
removeCookieUpdateListener();
|
removeCookieUpdateListener();
|
||||||
removeSystemProxyEnvUpdatesListener();
|
removeSystemProxyEnvUpdatesListener();
|
||||||
|
removeSnapshotHydrationListener();
|
||||||
};
|
};
|
||||||
}, [isElectron]);
|
}, [isElectron]);
|
||||||
};
|
};
|
||||||
|
@ -60,7 +60,7 @@ const trackStart = () => {
|
|||||||
event: 'start',
|
event: 'start',
|
||||||
properties: {
|
properties: {
|
||||||
os: platformLib.os.family,
|
os: platformLib.os.family,
|
||||||
version: '1.30.1'
|
version: '1.32.0'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -10,6 +10,7 @@ import NewRequest from 'components/Sidebar/NewRequest';
|
|||||||
import { sendRequest, saveRequest, saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
|
import { sendRequest, saveRequest, saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||||
import { findCollectionByUid, findItemInCollection } from 'utils/collections';
|
import { findCollectionByUid, findItemInCollection } from 'utils/collections';
|
||||||
import { closeTabs, switchTab } from 'providers/ReduxStore/slices/tabs';
|
import { closeTabs, switchTab } from 'providers/ReduxStore/slices/tabs';
|
||||||
|
import { getKeyBindingsForActionAllOS } from './keyMappings';
|
||||||
|
|
||||||
export const HotkeysContext = React.createContext();
|
export const HotkeysContext = React.createContext();
|
||||||
|
|
||||||
@ -43,7 +44,7 @@ export const HotkeysProvider = (props) => {
|
|||||||
|
|
||||||
// save hotkey
|
// save hotkey
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
Mousetrap.bind(['command+s', 'ctrl+s'], (e) => {
|
Mousetrap.bind([...getKeyBindingsForActionAllOS('save')], (e) => {
|
||||||
if (isEnvironmentSettingsModalOpen) {
|
if (isEnvironmentSettingsModalOpen) {
|
||||||
console.log('todo: save environment settings');
|
console.log('todo: save environment settings');
|
||||||
} else {
|
} else {
|
||||||
@ -68,13 +69,13 @@ export const HotkeysProvider = (props) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
Mousetrap.unbind(['command+s', 'ctrl+s']);
|
Mousetrap.unbind([...getKeyBindingsForActionAllOS('save')]);
|
||||||
};
|
};
|
||||||
}, [activeTabUid, tabs, saveRequest, collections, isEnvironmentSettingsModalOpen]);
|
}, [activeTabUid, tabs, saveRequest, collections, isEnvironmentSettingsModalOpen]);
|
||||||
|
|
||||||
// send request (ctrl/cmd + enter)
|
// send request (ctrl/cmd + enter)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
Mousetrap.bind(['command+enter', 'ctrl+enter'], (e) => {
|
Mousetrap.bind([...getKeyBindingsForActionAllOS('sendRequest')], (e) => {
|
||||||
const activeTab = find(tabs, (t) => t.uid === activeTabUid);
|
const activeTab = find(tabs, (t) => t.uid === activeTabUid);
|
||||||
if (activeTab) {
|
if (activeTab) {
|
||||||
const collection = findCollectionByUid(collections, activeTab.collectionUid);
|
const collection = findCollectionByUid(collections, activeTab.collectionUid);
|
||||||
@ -95,13 +96,13 @@ export const HotkeysProvider = (props) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
Mousetrap.unbind(['command+enter', 'ctrl+enter']);
|
Mousetrap.unbind([...getKeyBindingsForActionAllOS('sendRequest')]);
|
||||||
};
|
};
|
||||||
}, [activeTabUid, tabs, saveRequest, collections]);
|
}, [activeTabUid, tabs, saveRequest, collections]);
|
||||||
|
|
||||||
// edit environments (ctrl/cmd + e)
|
// edit environments (ctrl/cmd + e)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
Mousetrap.bind(['command+e', 'ctrl+e'], (e) => {
|
Mousetrap.bind([...getKeyBindingsForActionAllOS('editEnvironment')], (e) => {
|
||||||
const activeTab = find(tabs, (t) => t.uid === activeTabUid);
|
const activeTab = find(tabs, (t) => t.uid === activeTabUid);
|
||||||
if (activeTab) {
|
if (activeTab) {
|
||||||
const collection = findCollectionByUid(collections, activeTab.collectionUid);
|
const collection = findCollectionByUid(collections, activeTab.collectionUid);
|
||||||
@ -115,13 +116,13 @@ export const HotkeysProvider = (props) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
Mousetrap.unbind(['command+e', 'ctrl+e']);
|
Mousetrap.unbind([...getKeyBindingsForActionAllOS('editEnvironment')]);
|
||||||
};
|
};
|
||||||
}, [activeTabUid, tabs, collections, setShowEnvSettingsModal]);
|
}, [activeTabUid, tabs, collections, setShowEnvSettingsModal]);
|
||||||
|
|
||||||
// new request (ctrl/cmd + b)
|
// new request (ctrl/cmd + b)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
Mousetrap.bind(['command+b', 'ctrl+b'], (e) => {
|
Mousetrap.bind([...getKeyBindingsForActionAllOS('newRequest')], (e) => {
|
||||||
const activeTab = find(tabs, (t) => t.uid === activeTabUid);
|
const activeTab = find(tabs, (t) => t.uid === activeTabUid);
|
||||||
if (activeTab) {
|
if (activeTab) {
|
||||||
const collection = findCollectionByUid(collections, activeTab.collectionUid);
|
const collection = findCollectionByUid(collections, activeTab.collectionUid);
|
||||||
@ -135,13 +136,13 @@ export const HotkeysProvider = (props) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
Mousetrap.unbind(['command+b', 'ctrl+b']);
|
Mousetrap.unbind([...getKeyBindingsForActionAllOS('newRequest')]);
|
||||||
};
|
};
|
||||||
}, [activeTabUid, tabs, collections, setShowNewRequestModal]);
|
}, [activeTabUid, tabs, collections, setShowNewRequestModal]);
|
||||||
|
|
||||||
// close tab hotkey
|
// close tab hotkey
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
Mousetrap.bind(['command+w', 'ctrl+w'], (e) => {
|
Mousetrap.bind([...getKeyBindingsForActionAllOS('closeTab')], (e) => {
|
||||||
dispatch(
|
dispatch(
|
||||||
closeTabs({
|
closeTabs({
|
||||||
tabUids: [activeTabUid]
|
tabUids: [activeTabUid]
|
||||||
@ -152,13 +153,13 @@ export const HotkeysProvider = (props) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
Mousetrap.unbind(['command+w', 'ctrl+w']);
|
Mousetrap.unbind([...getKeyBindingsForActionAllOS('closeTab')]);
|
||||||
};
|
};
|
||||||
}, [activeTabUid]);
|
}, [activeTabUid]);
|
||||||
|
|
||||||
// Switch to the previous tab
|
// Switch to the previous tab
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
Mousetrap.bind(['command+pageup', 'ctrl+pageup'], (e) => {
|
Mousetrap.bind([...getKeyBindingsForActionAllOS('switchToPreviousTab')], (e) => {
|
||||||
dispatch(
|
dispatch(
|
||||||
switchTab({
|
switchTab({
|
||||||
direction: 'pageup'
|
direction: 'pageup'
|
||||||
@ -169,13 +170,13 @@ export const HotkeysProvider = (props) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
Mousetrap.unbind(['command+pageup', 'ctrl+pageup']);
|
Mousetrap.unbind([...getKeyBindingsForActionAllOS('switchToPreviousTab')]);
|
||||||
};
|
};
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
|
||||||
// Switch to the next tab
|
// Switch to the next tab
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
Mousetrap.bind(['command+pagedown', 'ctrl+pagedown'], (e) => {
|
Mousetrap.bind([...getKeyBindingsForActionAllOS('switchToNextTab')], (e) => {
|
||||||
dispatch(
|
dispatch(
|
||||||
switchTab({
|
switchTab({
|
||||||
direction: 'pagedown'
|
direction: 'pagedown'
|
||||||
@ -186,13 +187,13 @@ export const HotkeysProvider = (props) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
Mousetrap.unbind(['command+pagedown', 'ctrl+pagedown']);
|
Mousetrap.unbind([...getKeyBindingsForActionAllOS('switchToNextTab')]);
|
||||||
};
|
};
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
|
||||||
// Close all tabs
|
// Close all tabs
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
Mousetrap.bind(['command+shift+w', 'ctrl+shift+w'], (e) => {
|
Mousetrap.bind([...getKeyBindingsForActionAllOS('closeAllTabs')], (e) => {
|
||||||
const activeTab = find(tabs, (t) => t.uid === activeTabUid);
|
const activeTab = find(tabs, (t) => t.uid === activeTabUid);
|
||||||
if (activeTab) {
|
if (activeTab) {
|
||||||
const collection = findCollectionByUid(collections, activeTab.collectionUid);
|
const collection = findCollectionByUid(collections, activeTab.collectionUid);
|
||||||
@ -211,7 +212,7 @@ export const HotkeysProvider = (props) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
Mousetrap.unbind(['command+shift+w', 'ctrl+shift+w']);
|
Mousetrap.unbind([...getKeyBindingsForActionAllOS('closeAllTabs')]);
|
||||||
};
|
};
|
||||||
}, [activeTabUid, tabs, collections, dispatch]);
|
}, [activeTabUid, tabs, collections, dispatch]);
|
||||||
|
|
||||||
|
60
packages/bruno-app/src/providers/Hotkeys/keyMappings.js
Normal file
60
packages/bruno-app/src/providers/Hotkeys/keyMappings.js
Normal file
@ -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];
|
||||||
|
};
|
@ -43,6 +43,8 @@ import { resolveRequestFilename } from 'utils/common/platform';
|
|||||||
import { parsePathParams, parseQueryParams, splitOnFirst } from 'utils/url/index';
|
import { parsePathParams, parseQueryParams, splitOnFirst } from 'utils/url/index';
|
||||||
import { sendCollectionOauth2Request as _sendCollectionOauth2Request } from 'utils/network/index';
|
import { sendCollectionOauth2Request as _sendCollectionOauth2Request } from 'utils/network/index';
|
||||||
import { name } from 'file-loader';
|
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) => {
|
export const renameCollection = (newName, collectionUid) => (dispatch, getState) => {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
@ -401,7 +403,7 @@ export const renameItem = (newName, itemUid, collectionUid) => (dispatch, getSta
|
|||||||
}
|
}
|
||||||
const { ipcRenderer } = window;
|
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);
|
const collectionCopy = cloneDeep(collection);
|
||||||
if (environmentUid) {
|
if (environmentUid) {
|
||||||
const environment = findEnvironmentInCollection(collectionCopy, 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'));
|
return reject(new Error('Environment not found'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch(_selectEnvironment({ environmentUid, collectionUid }));
|
|
||||||
resolve();
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1151,3 +1155,33 @@ export const saveCollectionSecurityConfig = (collectionUid, securityConfig) => (
|
|||||||
.catch(reject);
|
.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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
@ -31,6 +31,7 @@ const createHeaders = (request, headers) => {
|
|||||||
if (contentType !== '') {
|
if (contentType !== '') {
|
||||||
enabledHeaders.push({ name: 'content-type', value: contentType });
|
enabledHeaders.push({ name: 'content-type', value: contentType });
|
||||||
}
|
}
|
||||||
|
|
||||||
return enabledHeaders;
|
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);
|
const contentType = createContentType(body.mode);
|
||||||
if (body.mode === 'formUrlEncoded' || body.mode === 'multipartForm') {
|
if (body.mode === 'formUrlEncoded' || body.mode === 'multipartForm') {
|
||||||
return {
|
return {
|
||||||
@ -64,7 +72,7 @@ const createPostData = (body) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const buildHarRequest = ({ request, headers }) => {
|
export const buildHarRequest = ({ request, headers, type }) => {
|
||||||
return {
|
return {
|
||||||
method: request.method,
|
method: request.method,
|
||||||
url: encodeURI(request.url),
|
url: encodeURI(request.url),
|
||||||
@ -72,7 +80,7 @@ export const buildHarRequest = ({ request, headers }) => {
|
|||||||
cookies: [],
|
cookies: [],
|
||||||
headers: createHeaders(request, headers),
|
headers: createHeaders(request, headers),
|
||||||
queryString: createQuery(request.params),
|
queryString: createQuery(request.params),
|
||||||
postData: createPostData(request.body),
|
postData: createPostData(request.body, type),
|
||||||
headersSize: 0,
|
headersSize: 0,
|
||||||
bodySize: 0
|
bodySize: 0
|
||||||
};
|
};
|
||||||
|
@ -132,6 +132,10 @@ export const findEnvironmentInCollection = (collection, envUid) => {
|
|||||||
return find(collection.environments, (e) => e.uid === 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) => {
|
export const moveCollectionItem = (collection, draggedItem, targetItem) => {
|
||||||
let draggedItemParent = findParentItemInCollection(collection, draggedItem.uid);
|
let draggedItemParent = findParentItemInCollection(collection, draggedItem.uid);
|
||||||
|
|
||||||
|
@ -32,7 +32,7 @@ const readFile = (files) => {
|
|||||||
|
|
||||||
const ensureUrl = (url) => {
|
const ensureUrl = (url) => {
|
||||||
// emoving multiple slashes after the protocol if it exists, or after the beginning of the string otherwise
|
// 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) => {
|
const buildEmptyJsonBody = (bodySchema) => {
|
||||||
|
@ -74,17 +74,17 @@ const interpolateVars = (request, envVars = {}, runtimeVariables = {}, processEn
|
|||||||
} else if (contentType === 'application/x-www-form-urlencoded') {
|
} else if (contentType === 'application/x-www-form-urlencoded') {
|
||||||
if (typeof request.data === 'object') {
|
if (typeof request.data === 'object') {
|
||||||
try {
|
try {
|
||||||
let parsed = JSON.stringify(request.data);
|
forOwn(request?.data, (value, key) => {
|
||||||
parsed = _interpolate(parsed);
|
request.data[key] = _interpolate(value);
|
||||||
request.data = JSON.parse(parsed);
|
});
|
||||||
} catch (err) {}
|
} catch (err) {}
|
||||||
}
|
}
|
||||||
} else if (contentType === 'multipart/form-data') {
|
} else if (contentType === 'multipart/form-data') {
|
||||||
if (typeof request.data === 'object' && !(request?.data instanceof FormData)) {
|
if (typeof request.data === 'object' && !(request?.data instanceof FormData)) {
|
||||||
try {
|
try {
|
||||||
let parsed = JSON.stringify(request.data);
|
forOwn(request?.data, (value, key) => {
|
||||||
parsed = _interpolate(parsed);
|
request.data[key] = _interpolate(value);
|
||||||
request.data = JSON.parse(parsed);
|
});
|
||||||
} catch (err) {}
|
} catch (err) {}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -122,7 +122,8 @@ const interpolateVars = (request, envVars = {}, runtimeVariables = {}, processEn
|
|||||||
})
|
})
|
||||||
.join('');
|
.join('');
|
||||||
|
|
||||||
request.url = url.origin + interpolatedUrlPath + url.search;
|
const trailingSlash = url.pathname.endsWith('/') ? '/' : '';
|
||||||
|
request.url = url.origin + interpolatedUrlPath + trailingSlash + url.search;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (request.proxy) {
|
if (request.proxy) {
|
||||||
|
@ -43,6 +43,8 @@ const runSingleRequest = async function (
|
|||||||
|
|
||||||
request = prepareRequest(bruJson.request, collectionRoot);
|
request = prepareRequest(bruJson.request, collectionRoot);
|
||||||
|
|
||||||
|
request.__bruno__executionMode = 'cli';
|
||||||
|
|
||||||
const scriptingConfig = get(brunoConfig, 'scripts', {});
|
const scriptingConfig = get(brunoConfig, 'scripts', {});
|
||||||
scriptingConfig.runtime = runtime;
|
scriptingConfig.runtime = runtime;
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"version": "v1.30.1",
|
"version": "v1.32.0",
|
||||||
"name": "bruno",
|
"name": "bruno",
|
||||||
"description": "Opensource API Client for Exploring and Testing APIs",
|
"description": "Opensource API Client for Exploring and Testing APIs",
|
||||||
"homepage": "https://www.usebruno.com",
|
"homepage": "https://www.usebruno.com",
|
||||||
|
@ -12,6 +12,7 @@ const { decryptString } = require('../utils/encryption');
|
|||||||
const { setDotEnvVars } = require('../store/process-env');
|
const { setDotEnvVars } = require('../store/process-env');
|
||||||
const { setBrunoConfig } = require('../store/bruno-config');
|
const { setBrunoConfig } = require('../store/bruno-config');
|
||||||
const EnvironmentSecretsStore = require('../store/env-secrets');
|
const EnvironmentSecretsStore = require('../store/env-secrets');
|
||||||
|
const UiStateSnapshot = require('../store/ui-state-snapshot');
|
||||||
|
|
||||||
const environmentSecretsStore = new EnvironmentSecretsStore();
|
const environmentSecretsStore = new EnvironmentSecretsStore();
|
||||||
|
|
||||||
@ -201,7 +202,6 @@ const add = async (win, pathname, collectionUid, collectionPath) => {
|
|||||||
const payload = {
|
const payload = {
|
||||||
collectionUid,
|
collectionUid,
|
||||||
processEnvVariables: {
|
processEnvVariables: {
|
||||||
...process.env,
|
|
||||||
...jsonData
|
...jsonData
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -331,7 +331,6 @@ const change = async (win, pathname, collectionUid, collectionPath) => {
|
|||||||
const payload = {
|
const payload = {
|
||||||
collectionUid,
|
collectionUid,
|
||||||
processEnvVariables: {
|
processEnvVariables: {
|
||||||
...process.env,
|
|
||||||
...jsonData
|
...jsonData
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -423,6 +422,13 @@ const unlinkDir = (win, pathname, collectionUid, collectionPath) => {
|
|||||||
win.webContents.send('main:collection-tree-updated', 'unlinkDir', directory);
|
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 {
|
class Watcher {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.watchers = {};
|
this.watchers = {};
|
||||||
@ -458,6 +464,7 @@ class Watcher {
|
|||||||
|
|
||||||
let startedNewWatcher = false;
|
let startedNewWatcher = false;
|
||||||
watcher
|
watcher
|
||||||
|
.on('ready', () => onWatcherSetupComplete(win, watchPath))
|
||||||
.on('add', (pathname) => add(win, pathname, collectionUid, watchPath))
|
.on('add', (pathname) => add(win, pathname, collectionUid, watchPath))
|
||||||
.on('addDir', (pathname) => addDirectory(win, pathname, collectionUid, watchPath))
|
.on('addDir', (pathname) => addDirectory(win, pathname, collectionUid, watchPath))
|
||||||
.on('change', (pathname) => change(win, pathname, collectionUid, watchPath))
|
.on('change', (pathname) => change(win, pathname, collectionUid, watchPath))
|
||||||
|
@ -17,6 +17,8 @@ const {
|
|||||||
sanitizeDirectoryName,
|
sanitizeDirectoryName,
|
||||||
isWSLPath,
|
isWSLPath,
|
||||||
normalizeWslPath,
|
normalizeWslPath,
|
||||||
|
normalizeAndResolvePath,
|
||||||
|
safeToRename
|
||||||
} = require('../utils/filesystem');
|
} = require('../utils/filesystem');
|
||||||
const { openCollectionDialog } = require('../app/collections');
|
const { openCollectionDialog } = require('../app/collections');
|
||||||
const { generateUidBasedOnHash, stringifyJson, safeParseJSON, safeStringifyJSON } = require('../utils/common');
|
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 { deleteCookiesForDomain, getDomainsWithCookies } = require('../utils/cookies');
|
||||||
const EnvironmentSecretsStore = require('../store/env-secrets');
|
const EnvironmentSecretsStore = require('../store/env-secrets');
|
||||||
const CollectionSecurityStore = require('../store/collection-security');
|
const CollectionSecurityStore = require('../store/collection-security');
|
||||||
|
const UiStateSnapshot = require('../store/ui-state-snapshot');
|
||||||
|
|
||||||
const environmentSecretsStore = new EnvironmentSecretsStore();
|
const environmentSecretsStore = new EnvironmentSecretsStore();
|
||||||
const collectionSecurityStore = new CollectionSecurityStore();
|
const collectionSecurityStore = new CollectionSecurityStore();
|
||||||
|
const UiStateSnapshotStore = new UiStateSnapshot();
|
||||||
|
|
||||||
const envHasSecrets = (environment = {}) => {
|
const envHasSecrets = (environment = {}) => {
|
||||||
const secrets = _.filter(environment.variables, (v) => v.secret);
|
const secrets = _.filter(environment.variables, (v) => v.secret);
|
||||||
@ -308,7 +312,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
|||||||
}
|
}
|
||||||
|
|
||||||
const newEnvFilePath = path.join(envDirPath, `${newName}.bru`);
|
const newEnvFilePath = path.join(envDirPath, `${newName}.bru`);
|
||||||
if (fs.existsSync(newEnvFilePath)) {
|
if (!safeToRename(envFilePath, newEnvFilePath)) {
|
||||||
throw new Error(`environment: ${newEnvFilePath} already exists`);
|
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) => {
|
ipcMain.handle('renderer:rename-item', async (event, oldPath, newPath, newName) => {
|
||||||
try {
|
try {
|
||||||
// Normalize paths if they are WSL paths
|
// Normalize paths if they are WSL paths
|
||||||
if (isWSLPath(oldPath)) {
|
oldPath = isWSLPath(oldPath) ? normalizeWslPath(oldPath) : normalizeAndResolvePath(oldPath);
|
||||||
oldPath = normalizeWslPath(oldPath);
|
newPath = isWSLPath(newPath) ? normalizeWslPath(newPath) : normalizeAndResolvePath(newPath);
|
||||||
}
|
|
||||||
if (isWSLPath(newPath)) {
|
|
||||||
newPath = normalizeWslPath(newPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Check if the old path exists
|
||||||
if (!fs.existsSync(oldPath)) {
|
if (!fs.existsSync(oldPath)) {
|
||||||
throw new Error(`path: ${oldPath} does not exist`);
|
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)) {
|
if (isDirectory(oldPath)) {
|
||||||
const bruFilesAtSource = await searchForBruFiles(oldPath);
|
const bruFilesAtSource = await searchForBruFiles(oldPath);
|
||||||
|
|
||||||
@ -376,12 +377,13 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
|||||||
const jsonData = bruToJson(data);
|
const jsonData = bruToJson(data);
|
||||||
|
|
||||||
jsonData.name = newName;
|
jsonData.name = newName;
|
||||||
|
|
||||||
moveRequestUid(oldPath, newPath);
|
moveRequestUid(oldPath, newPath);
|
||||||
|
|
||||||
const content = jsonToBru(jsonData);
|
const content = jsonToBru(jsonData);
|
||||||
await writeFile(newPath, content);
|
|
||||||
await fs.unlinkSync(oldPath);
|
await fs.unlinkSync(oldPath);
|
||||||
|
await writeFile(newPath, content);
|
||||||
|
|
||||||
|
return newPath;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
}
|
}
|
||||||
@ -707,6 +709,14 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
|||||||
return Promise.reject(error);
|
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) => {
|
const registerMainEventHandlers = (mainWindow, watcher, lastOpenedCollections) => {
|
||||||
|
@ -525,6 +525,7 @@ const registerNetworkIpc = (mainWindow) => {
|
|||||||
|
|
||||||
const collectionRoot = get(collection, 'root', {});
|
const collectionRoot = get(collection, 'root', {});
|
||||||
const request = prepareRequest(item, collection);
|
const request = prepareRequest(item, collection);
|
||||||
|
request.__bruno__executionMode = 'standalone';
|
||||||
const envVars = getEnvVars(environment);
|
const envVars = getEnvVars(environment);
|
||||||
const processEnvVars = getProcessEnvVars(collectionUid);
|
const processEnvVars = getProcessEnvVars(collectionUid);
|
||||||
const brunoConfig = getBrunoConfig(collectionUid);
|
const brunoConfig = getBrunoConfig(collectionUid);
|
||||||
@ -717,6 +718,7 @@ const registerNetworkIpc = (mainWindow) => {
|
|||||||
const collectionRoot = get(collection, 'root', {});
|
const collectionRoot = get(collection, 'root', {});
|
||||||
const _request = collectionRoot?.request;
|
const _request = collectionRoot?.request;
|
||||||
const request = prepareCollectionRequest(_request, collectionRoot, collectionPath);
|
const request = prepareCollectionRequest(_request, collectionRoot, collectionPath);
|
||||||
|
request.__bruno__executionMode = 'standalone';
|
||||||
const envVars = getEnvVars(environment);
|
const envVars = getEnvVars(environment);
|
||||||
const processEnvVars = getProcessEnvVars(collectionUid);
|
const processEnvVars = getProcessEnvVars(collectionUid);
|
||||||
const brunoConfig = getBrunoConfig(collectionUid);
|
const brunoConfig = getBrunoConfig(collectionUid);
|
||||||
@ -960,6 +962,8 @@ const registerNetworkIpc = (mainWindow) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const request = prepareRequest(item, collection);
|
const request = prepareRequest(item, collection);
|
||||||
|
request.__bruno__executionMode = 'runner';
|
||||||
|
|
||||||
const requestUid = uuid();
|
const requestUid = uuid();
|
||||||
const processEnvVars = getProcessEnvVars(collectionUid);
|
const processEnvVars = getProcessEnvVars(collectionUid);
|
||||||
|
|
||||||
|
@ -68,21 +68,27 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc
|
|||||||
if (request.data.length) {
|
if (request.data.length) {
|
||||||
request.data = _interpolate(request.data);
|
request.data = _interpolate(request.data);
|
||||||
}
|
}
|
||||||
}
|
} else if (typeof request.data === 'object') {
|
||||||
} else if (contentType === 'application/x-www-form-urlencoded') {
|
|
||||||
if (typeof request.data === 'object') {
|
|
||||||
try {
|
try {
|
||||||
let parsed = JSON.stringify(request.data);
|
let parsed = JSON.stringify(request.data);
|
||||||
parsed = _interpolate(parsed);
|
parsed = _interpolate(parsed);
|
||||||
request.data = JSON.parse(parsed);
|
request.data = JSON.parse(parsed);
|
||||||
} catch (err) {}
|
} 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') {
|
} else if (contentType === 'multipart/form-data') {
|
||||||
if (typeof request.data === 'object' && !(request.data instanceof FormData)) {
|
if (typeof request.data === 'object' && !(request.data instanceof FormData)) {
|
||||||
try {
|
try {
|
||||||
let parsed = JSON.stringify(request.data);
|
forOwn(request?.data, (value, key) => {
|
||||||
parsed = _interpolate(parsed);
|
request.data[key] = _interpolate(value);
|
||||||
request.data = JSON.parse(parsed);
|
});
|
||||||
} catch (err) {}
|
} catch (err) {}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -120,7 +126,8 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc
|
|||||||
})
|
})
|
||||||
.join('');
|
.join('');
|
||||||
|
|
||||||
request.url = url.origin + urlPathnameInterpolatedWithPathParams + url.search;
|
const trailingSlash = url.pathname.endsWith('/') ? '/' : '';
|
||||||
|
request.url = url.origin + urlPathnameInterpolatedWithPathParams + trailingSlash + url.search;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (request.proxy) {
|
if (request.proxy) {
|
||||||
|
60
packages/bruno-electron/src/store/ui-state-snapshot.js
Normal file
60
packages/bruno-electron/src/store/ui-state-snapshot.js
Normal file
@ -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;
|
@ -3,6 +3,7 @@ const fs = require('fs-extra');
|
|||||||
const fsPromises = require('fs/promises');
|
const fsPromises = require('fs/promises');
|
||||||
const { dialog } = require('electron');
|
const { dialog } = require('electron');
|
||||||
const isValidPathname = require('is-valid-path');
|
const isValidPathname = require('is-valid-path');
|
||||||
|
const os = require('os');
|
||||||
|
|
||||||
const exists = async (p) => {
|
const exists = async (p) => {
|
||||||
try {
|
try {
|
||||||
@ -169,12 +170,34 @@ const searchForBruFiles = (dir) => {
|
|||||||
return searchForFiles(dir, '.bru');
|
return searchForFiles(dir, '.bru');
|
||||||
};
|
};
|
||||||
|
|
||||||
// const isW
|
|
||||||
|
|
||||||
const sanitizeDirectoryName = (name) => {
|
const sanitizeDirectoryName = (name) => {
|
||||||
return name.replace(/[<>:"/\\|?*\x00-\x1F]+/g, '-');
|
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 = {
|
module.exports = {
|
||||||
isValidPathname,
|
isValidPathname,
|
||||||
exists,
|
exists,
|
||||||
@ -195,5 +218,6 @@ module.exports = {
|
|||||||
chooseFileToSave,
|
chooseFileToSave,
|
||||||
searchForFiles,
|
searchForFiles,
|
||||||
searchForBruFiles,
|
searchForBruFiles,
|
||||||
sanitizeDirectoryName
|
sanitizeDirectoryName,
|
||||||
|
safeToRename
|
||||||
};
|
};
|
||||||
|
@ -97,6 +97,14 @@ class Bru {
|
|||||||
delete this.runtimeVariables[key];
|
delete this.runtimeVariables[key];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deleteAllVars() {
|
||||||
|
for (let key in this.runtimeVariables) {
|
||||||
|
if (this.runtimeVariables.hasOwnProperty(key)) {
|
||||||
|
delete this.runtimeVariables[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
getCollectionVar(key) {
|
getCollectionVar(key) {
|
||||||
return this._interpolate(this.collectionVariables[key]);
|
return this._interpolate(this.collectionVariables[key]);
|
||||||
}
|
}
|
||||||
|
@ -173,6 +173,10 @@ class BrunoRequest {
|
|||||||
disableParsingResponseJson() {
|
disableParsingResponseJson() {
|
||||||
this.req.__brunoDisableParsingResponseJson = true;
|
this.req.__brunoDisableParsingResponseJson = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getExecutionMode() {
|
||||||
|
return this.req.__bruno__executionMode;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = BrunoRequest;
|
module.exports = BrunoRequest;
|
||||||
|
@ -21,6 +21,12 @@ const addBruShimToContext = (vm, bru) => {
|
|||||||
vm.setProp(bruObject, 'getProcessEnv', getProcessEnv);
|
vm.setProp(bruObject, 'getProcessEnv', getProcessEnv);
|
||||||
getProcessEnv.dispose();
|
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) {
|
let getEnvVar = vm.newFunction('getEnvVar', function (key) {
|
||||||
return marshallToVm(bru.getEnvVar(vm.dump(key)), vm);
|
return marshallToVm(bru.getEnvVar(vm.dump(key)), vm);
|
||||||
});
|
});
|
||||||
@ -33,6 +39,12 @@ const addBruShimToContext = (vm, bru) => {
|
|||||||
vm.setProp(bruObject, 'setEnvVar', setEnvVar);
|
vm.setProp(bruObject, 'setEnvVar', setEnvVar);
|
||||||
setEnvVar.dispose();
|
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) {
|
let getVar = vm.newFunction('getVar', function (key) {
|
||||||
return marshallToVm(bru.getVar(vm.dump(key)), vm);
|
return marshallToVm(bru.getVar(vm.dump(key)), vm);
|
||||||
});
|
});
|
||||||
@ -45,6 +57,18 @@ const addBruShimToContext = (vm, bru) => {
|
|||||||
vm.setProp(bruObject, 'setVar', setVar);
|
vm.setProp(bruObject, 'setVar', setVar);
|
||||||
setVar.dispose();
|
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) {
|
let setNextRequest = vm.newFunction('setNextRequest', function (nextRequest) {
|
||||||
bru.setNextRequest(vm.dump(nextRequest));
|
bru.setNextRequest(vm.dump(nextRequest));
|
||||||
});
|
});
|
||||||
|
@ -111,6 +111,12 @@ const addBrunoRequestShimToContext = (vm, req) => {
|
|||||||
vm.setProp(reqObject, 'disableParsingResponseJson', disableParsingResponseJson);
|
vm.setProp(reqObject, 'disableParsingResponseJson', disableParsingResponseJson);
|
||||||
disableParsingResponseJson.dispose();
|
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);
|
vm.setProp(vm.global, 'req', reqObject);
|
||||||
reqObject.dispose();
|
reqObject.dispose();
|
||||||
};
|
};
|
||||||
|
@ -12,12 +12,15 @@ post {
|
|||||||
|
|
||||||
body:form-urlencoded {
|
body:form-urlencoded {
|
||||||
form-data-key: {{form-data-key}}
|
form-data-key: {{form-data-key}}
|
||||||
}
|
form-data-stringified-object: {{form-data-stringified-object}}
|
||||||
|
|
||||||
script:pre-request {
|
|
||||||
bru.setVar('form-data-key', 'form-data-value');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
assert {
|
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);
|
||||||
}
|
}
|
||||||
|
@ -11,14 +11,18 @@ post {
|
|||||||
}
|
}
|
||||||
|
|
||||||
body:multipart-form {
|
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)
|
file: @file(bruno.png)
|
||||||
}
|
}
|
||||||
|
|
||||||
assert {
|
assert {
|
||||||
res.body: contains form-data-value
|
res.body: contains form-data-value
|
||||||
|
res.body: contains {"foo":123}
|
||||||
}
|
}
|
||||||
|
|
||||||
script:pre-request {
|
script:pre-request {
|
||||||
|
let obj = JSON.stringify({foo:123});
|
||||||
bru.setVar('form-data-key', 'form-data-value');
|
bru.setVar('form-data-key', 'form-data-value');
|
||||||
|
bru.setVar('form-data-stringified-object', obj);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user