Merge branch 'main' of https://github.com/usebruno/bruno into fix/API-key-auth-in-cli

This commit is contained in:
Sanjai Kumar 2025-01-18 13:36:15 +05:30
commit fcd63d91cb
36 changed files with 531 additions and 357 deletions

2
package-lock.json generated
View File

@ -24199,7 +24199,7 @@
},
"packages/bruno-electron": {
"name": "bruno",
"version": "v1.36.0",
"version": "v1.38.1",
"dependencies": {
"@aws-sdk/credential-providers": "3.658.1",
"@usebruno/common": "0.1.0",

View File

@ -24,4 +24,16 @@ export default defineConfig({
html: {
title: 'Bruno'
},
tools: {
rspack: {
module: {
parser: {
javascript: {
// This loads the JavaScript contents from a library along with the main JavaScript bundle.
dynamicImportMode: "eager",
},
},
},
},
}
});

View File

@ -7,12 +7,12 @@
import React from 'react';
import { isEqual, escapeRegExp } from 'lodash';
import { getEnvironmentVariables } from 'utils/collections';
import { defineCodeMirrorBrunoVariablesMode } from 'utils/common/codemirror';
import StyledWrapper from './StyledWrapper';
import * as jsonlint from '@prantlf/jsonlint';
import { JSHINT } from 'jshint';
import stripJsonComments from 'strip-json-comments';
import { getAllVariables } from 'utils/collections';
let CodeMirror;
const SERVER_RENDERED = typeof window === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true;
@ -293,7 +293,7 @@ export default class CodeEditor extends React.Component {
}
if (this.editor) {
let variables = getEnvironmentVariables(this.props.collection);
let variables = getAllVariables(this.props.collection, this.props.item);
if (!isEqual(variables, this.variables)) {
this.addOverlay();
}
@ -333,7 +333,7 @@ export default class CodeEditor extends React.Component {
addOverlay = () => {
const mode = this.props.mode || 'application/ld+json';
let variables = getEnvironmentVariables(this.props.collection);
let variables = getAllVariables(this.props.collection, this.props.item);
this.variables = variables;
defineCodeMirrorBrunoVariablesMode(variables, mode);

View File

@ -2,9 +2,6 @@ import styled from 'styled-components';
const StyledWrapper = styled.div`
div.CodeMirror {
/* todo: find a better way */
height: calc(100vh - 240px);
.CodeMirror-scroll {
padding-bottom: 0px;
}

View File

@ -32,22 +32,27 @@ const Docs = ({ collection }) => {
const onSave = () => dispatch(saveCollectionRoot(collection.uid));
return (
<StyledWrapper className="mt-1 h-full w-full relative">
<div className="editing-mode mb-2" role="tab" onClick={toggleViewMode}>
<StyledWrapper className="mt-1 h-full w-full relative flex flex-col">
<div className="editing-mode flex justify-between items-center" role="tab" onClick={toggleViewMode}>
{isEditing ? 'Preview' : 'Edit'}
</div>
{isEditing ? (
<CodeEditor
collection={collection}
theme={displayedTheme}
value={docs || ''}
onEdit={onEdit}
onSave={onSave}
mode="application/text"
font={get(preferences, 'font.codeFont', 'default')}
fontSize={get(preferences, 'font.codeFontSize')}
/>
<div className="mt-2 flex-1 max-h-[70vh]">
<CodeEditor
collection={collection}
theme={displayedTheme}
value={docs || ''}
onEdit={onEdit}
onSave={onSave}
mode="application/text"
font={get(preferences, 'font.codeFont', 'default')}
fontSize={get(preferences, 'font.codeFontSize')}
/>
<button type="submit" className="submit btn btn-sm btn-secondary my-6" onClick={onSave}>
Save
</button>
</div>
) : (
<Markdown collectionPath={collection.pathname} onDoubleClick={toggleViewMode} content={docs} />
)}

View File

@ -78,7 +78,10 @@ const EnvironmentSelector = ({ collection }) => {
<IconDatabaseOff size={18} strokeWidth={1.5} />
<span className="ml-2">No Environment</span>
</div>
<div className="dropdown-item border-top" onClick={handleSettingsIconClick}>
<div className="dropdown-item border-top" onClick={() => {
handleSettingsIconClick();
dropdownTippyRef.current.hide();
}}>
<div className="pr-2 text-gray-600">
<IconSettings size={18} strokeWidth={1.5} />
</div>

View File

@ -1,8 +1,9 @@
import React, { useRef, useEffect } from 'react';
import cloneDeep from 'lodash/cloneDeep';
import { IconTrash, IconAlertCircle } from '@tabler/icons';
import { IconTrash, IconAlertCircle, IconDeviceFloppy, IconRefresh, IconCircleCheck } from '@tabler/icons';
import { useTheme } from 'providers/Theme';
import { useDispatch } from 'react-redux';
import { selectEnvironment } from 'providers/ReduxStore/slices/collections/actions';
import SingleLineEditor from 'components/SingleLineEditor';
import StyledWrapper from './StyledWrapper';
import { uuid } from 'utils/common';
@ -13,7 +14,7 @@ import { saveEnvironment } from 'providers/ReduxStore/slices/collections/actions
import toast from 'react-hot-toast';
import { Tooltip } from 'react-tooltip';
const EnvironmentVariables = ({ environment, collection, setIsModified, originalEnvironmentVariables }) => {
const EnvironmentVariables = ({ environment, collection, setIsModified, originalEnvironmentVariables, onClose }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const addButtonRef = useRef(null);
@ -84,6 +85,19 @@ const EnvironmentVariables = ({ environment, collection, setIsModified, original
formik.setFieldValue(formik.values.length, newVariable, false);
};
const onActivate = () => {
dispatch(selectEnvironment(environment ? environment.uid : null, collection.uid))
.then(() => {
if (environment) {
toast.success(`Environment changed to ${environment.name}`);
onClose();
} else {
toast.success(`No Environments are active now`);
}
})
.catch((err) => console.log(err) && toast.error('An error occurred while selecting the environment'));
};
const handleRemoveVar = (id) => {
formik.setValues(formik.values.filter((variable) => variable.uid !== id));
};
@ -183,13 +197,19 @@ const EnvironmentVariables = ({ environment, collection, setIsModified, original
</div>
</div>
<div>
<button type="submit" className="submit btn btn-md btn-secondary mt-2" onClick={formik.handleSubmit}>
<div className="flex items-center">
<button type="submit" className="submit btn btn-sm btn-secondary mt-2 flex items-center" onClick={formik.handleSubmit}>
<IconDeviceFloppy size={16} strokeWidth={1.5} className="mr-1" />
Save
</button>
<button type="submit" className="ml-2 px-1 submit btn btn-md btn-secondary mt-2" onClick={handleReset}>
<button type="submit" className="ml-2 px-1 submit btn btn-sm btn-close mt-2 flex items-center" onClick={handleReset}>
<IconRefresh size={16} strokeWidth={1.5} className="mr-1" />
Reset
</button>
<button type="submit" className="submit btn btn-sm btn-close mt-2 flex items-center" onClick={onActivate}>
<IconCircleCheck size={16} strokeWidth={1.5} className="mr-1" />
Activate
</button>
</div>
</StyledWrapper>
);

View File

@ -5,7 +5,7 @@ import DeleteEnvironment from '../../DeleteEnvironment';
import RenameEnvironment from '../../RenameEnvironment';
import EnvironmentVariables from './EnvironmentVariables';
const EnvironmentDetails = ({ environment, collection, setIsModified }) => {
const EnvironmentDetails = ({ environment, collection, setIsModified, onClose }) => {
const [openEditModal, setOpenEditModal] = useState(false);
const [openDeleteModal, setOpenDeleteModal] = useState(false);
const [openCopyModal, setOpenCopyModal] = useState(false);
@ -38,7 +38,7 @@ const EnvironmentDetails = ({ environment, collection, setIsModified }) => {
</div>
<div>
<EnvironmentVariables environment={environment} collection={collection} setIsModified={setIsModified} />
<EnvironmentVariables environment={environment} collection={collection} setIsModified={setIsModified} onClose={onClose} />
</div>
</div>
);

View File

@ -9,8 +9,9 @@ import ManageSecrets from '../ManageSecrets';
import StyledWrapper from './StyledWrapper';
import ConfirmSwitchEnv from './ConfirmSwitchEnv';
import ToolHint from 'components/ToolHint';
import { isEqual } from 'lodash';
const EnvironmentList = ({ selectedEnvironment, setSelectedEnvironment, collection, isModified, setIsModified }) => {
const EnvironmentList = ({ selectedEnvironment, setSelectedEnvironment, collection, isModified, setIsModified, onClose }) => {
const { environments } = collection;
const [openCreateModal, setOpenCreateModal] = useState(false);
const [openImportModal, setOpenImportModal] = useState(false);
@ -24,6 +25,11 @@ const EnvironmentList = ({ selectedEnvironment, setSelectedEnvironment, collecti
useEffect(() => {
if (selectedEnvironment) {
const _selectedEnvironment = environments?.find(env => env?.uid === selectedEnvironment?.uid);
const hasSelectedEnvironmentChanged = !isEqual(selectedEnvironment, _selectedEnvironment);
if (hasSelectedEnvironmentChanged) {
setSelectedEnvironment(_selectedEnvironment);
}
setOriginalEnvironmentVariables(selectedEnvironment.variables);
return;
}
@ -135,6 +141,7 @@ const EnvironmentList = ({ selectedEnvironment, setSelectedEnvironment, collecti
collection={collection}
setIsModified={setIsModified}
originalEnvironmentVariables={originalEnvironmentVariables}
onClose={onClose}
/>
</div>
</StyledWrapper>

View File

@ -72,6 +72,7 @@ const EnvironmentSettings = ({ collection, onClose }) => {
collection={collection}
isModified={isModified}
setIsModified={setIsModified}
onClose={onClose}
/>
</Modal>
);

View File

@ -37,22 +37,25 @@ const Documentation = ({ collection, folder }) => {
}
return (
<StyledWrapper className="flex flex-col gap-y-1 h-full w-full relative">
<div className="editing-mode" role="tab" onClick={toggleViewMode}>
<StyledWrapper className="mt-1 h-full w-full relative flex flex-col">
<div className="editing-mode flex justify-between items-center" role="tab" onClick={toggleViewMode}>
{isEditing ? 'Preview' : 'Edit'}
</div>
{isEditing ? (
<CodeEditor
collection={collection}
theme={displayedTheme}
font={get(preferences, 'font.codeFont', 'default')}
fontSize={get(preferences, 'font.codeFontSize')}
value={docs || ''}
onEdit={onEdit}
onSave={onSave}
mode="application/text"
/>
<div className="mt-2 flex-1 max-h-[70vh]">
<CodeEditor
collection={collection}
theme={displayedTheme}
value={docs || ''}
onEdit={onEdit}
onSave={onSave}
mode="application/text"
/>
<button type="submit" className="submit btn btn-sm btn-secondary my-6" onClick={onSave}>
Save
</button>
</div>
) : (
<Markdown collectionPath={collection.pathname} onDoubleClick={toggleViewMode} content={docs} />
)}

View File

@ -55,7 +55,7 @@ const StyledMarkdownBodyWrapper = styled.div`
height: 1px;
padding: 0;
margin: 24px 0;
background-color: var(--color-border-default);
background-color: var(--color-sidebar-collection-item-active-indent-border);
border: 0;
}

View File

@ -62,7 +62,7 @@ const Modal = ({
confirmText,
cancelText,
handleCancel,
handleConfirm,
handleConfirm = () => {},
children,
confirmDisabled,
hideCancel,
@ -92,7 +92,7 @@ const Modal = ({
};
useFocusTrap(modalRef);
const closeModal = (args) => {
setIsClosing(true);
setTimeout(() => handleCancel(args), closeModalFadeTimeout);
@ -103,7 +103,7 @@ const Modal = ({
return () => {
document.removeEventListener('keydown', handleKeydown);
};
}, [disableEscapeKey, document]);
}, [disableEscapeKey, document, handleConfirm]);
let classes = 'bruno-modal';
if (isClosing) {

View File

@ -19,9 +19,8 @@ const StyledWrapper = styled.div`
opacity: 0.5;
}
.CodeMirror-scroll {
overflow: hidden !important;
${'' /* padding-bottom: 50px !important; */}
.CodeMirror-scroll {
overflow: visible !important;
position: relative;
display: block;
margin: 0px;

View File

@ -146,19 +146,8 @@ const AssertionRow = ({
const { operator, value } = parseAssertionOperator(assertion.value);
return (
<tr key={assertion.uid}>
<td>
<input
type="text"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={assertion.name}
className="mousetrap"
onChange={(e) => handleAssertionChange(e, assertion, 'name')}
/>
</td>
<>
<td>
<AssertionOperator
operator={operator}
@ -216,7 +205,7 @@ const AssertionRow = ({
</button>
</div>
</td>
</tr>
</>
);
};

View File

@ -4,6 +4,7 @@ const Wrapper = styled.div`
table {
width: 100%;
border-collapse: collapse;
font-weight: 600;
table-layout: fixed;
thead,
@ -15,24 +16,15 @@ const Wrapper = styled.div`
color: ${(props) => props.theme.table.thead.color};
font-size: 0.8125rem;
user-select: none;
font-weight: 600;
}
td {
padding: 6px 10px;
&:nth-child(2) {
width: 130px;
}
&:nth-child(4) {
width: 70px;
}
select {
select {
background-color: transparent;
}
}
}
.btn-add-assertion {
font-size: 0.8125rem;
@ -42,7 +34,8 @@ const Wrapper = styled.div`
width: 100%;
border: solid 1px transparent;
outline: none !important;
background-color: inherit;
color: ${(props) => props.theme.table.input.color};
background: transparent;
&:focus {
outline: none !important;

View File

@ -6,6 +6,9 @@ import { addAssertion, updateAssertion, deleteAssertion } from 'providers/ReduxS
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import AssertionRow from './AssertionRow';
import StyledWrapper from './StyledWrapper';
import Table from 'components/Table/index';
import ReorderTable from 'components/ReorderTable/index';
import { moveAssertion } from 'providers/ReduxStore/slices/collections/index';
const Assertions = ({ item, collection }) => {
const dispatch = useDispatch();
@ -57,21 +60,43 @@ const Assertions = ({ item, collection }) => {
);
};
const handleAssertionDrag = ({ updateReorderedItem }) => {
dispatch(
moveAssertion({
collectionUid: collection.uid,
itemUid: item.uid,
updateReorderedItem
})
);
};
return (
<StyledWrapper className="w-full">
<table>
<thead>
<tr>
<td>Expr</td>
<td>Operator</td>
<td>Value</td>
<td></td>
</tr>
</thead>
<tbody>
<Table
headers={[
{ name: 'Expr', accessor: 'expr', width: '30%' },
{ name: 'Operator', accessor: 'operator', width: '120px' },
{ name: 'Value', accessor: 'value', width: '30%' },
{ name: '', accessor: '', width: '15%' }
]}
>
<ReorderTable updateReorderedItem={handleAssertionDrag}>
{assertions && assertions.length
? assertions.map((assertion) => {
return (
return (
<tr key={assertion.uid} data-uid={assertion.uid}>
<td className='flex relative'>
<input
type="text"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={assertion.name}
className="mousetrap"
onChange={(e) => handleAssertionChange(e, assertion, 'name')}
/>
</td>
<AssertionRow
key={assertion.uid}
assertion={assertion}
@ -82,11 +107,12 @@ const Assertions = ({ item, collection }) => {
onSave={onSave}
handleRun={handleRun}
/>
);
})
</tr>
);
})
: null}
</tbody>
</table>
</ReorderTable>
</Table>
<button className="btn-add-assertion text-link pr-2 py-3 mt-2 select-none" onClick={handleAddAssertion}>
+ Add Assertion
</button>

View File

@ -19,16 +19,8 @@ const Wrapper = styled.div`
}
td {
padding: 6px 10px;
&:nth-child(1) {
width: 30%;
}
&:nth-child(3) {
width: 70px;
}
}
}
.btn-add-param {
font-size: 0.8125rem;

View File

@ -7,11 +7,14 @@ import { useTheme } from 'providers/Theme';
import {
addFormUrlEncodedParam,
updateFormUrlEncodedParam,
deleteFormUrlEncodedParam
deleteFormUrlEncodedParam,
moveFormUrlEncodedParam
} from 'providers/ReduxStore/slices/collections';
import MultiLineEditor from 'components/MultiLineEditor';
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';
import ReorderTable from 'components/ReorderTable/index';
import Table from 'components/Table/index';
const FormUrlEncodedParams = ({ item, collection }) => {
const dispatch = useDispatch();
@ -64,75 +67,84 @@ const FormUrlEncodedParams = ({ item, collection }) => {
);
};
const handleParamDrag = ({ updateReorderedItem }) => {
dispatch(
moveFormUrlEncodedParam({
collectionUid: collection.uid,
itemUid: item.uid,
updateReorderedItem
})
);
};
return (
<StyledWrapper className="w-full">
<table>
<thead>
<tr>
<td>Key</td>
<td>Value</td>
<td></td>
</tr>
</thead>
<tbody>
<Table
headers={[
{ name: 'Key', accessor: 'key', width: '40%' },
{ name: 'Value', accessor: 'value', width: '46%' },
{ name: '', accessor: '', width: '14%' }
]}
>
<ReorderTable updateReorderedItem={handleParamDrag}>
{params && params.length
? params.map((param, index) => {
return (
<tr key={param.uid}>
<td>
return (
<tr key={param.uid} data-uid={param.uid}>
<td className='flex relative'>
<input
type="text"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={param.name}
className="mousetrap"
onChange={(e) => handleParamChange(e, param, 'name')}
/>
</td>
<td>
<MultiLineEditor
value={param.value}
theme={storedTheme}
onSave={onSave}
onChange={(newValue) =>
handleParamChange(
{
target: {
value: newValue
}
},
param,
'value'
)
}
allowNewlines={true}
onRun={handleRun}
collection={collection}
item={item}
/>
</td>
<td>
<div className="flex items-center">
<input
type="text"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={param.name}
className="mousetrap"
onChange={(e) => handleParamChange(e, param, 'name')}
type="checkbox"
checked={param.enabled}
tabIndex="-1"
className="mr-3 mousetrap"
onChange={(e) => handleParamChange(e, param, 'enabled')}
/>
</td>
<td>
<MultiLineEditor
value={param.value}
theme={storedTheme}
onSave={onSave}
onChange={(newValue) =>
handleParamChange(
{
target: {
value: newValue
}
},
param,
'value'
)
}
allowNewlines={true}
onRun={handleRun}
collection={collection}
item={item}
/>
</td>
<td>
<div className="flex items-center">
<input
type="checkbox"
checked={param.enabled}
tabIndex="-1"
className="mr-3 mousetrap"
onChange={(e) => handleParamChange(e, param, 'enabled')}
/>
<button tabIndex="-1" onClick={() => handleRemoveParams(param)}>
<IconTrash strokeWidth={1.5} size={20} />
</button>
</div>
</td>
</tr>
);
})
<button tabIndex="-1" onClick={() => handleRemoveParams(param)}>
<IconTrash strokeWidth={1.5} size={20} />
</button>
</div>
</td>
</tr>
);
})
: null}
</tbody>
</table>
</ReorderTable>
</Table>
<button className="btn-add-param text-link pr-2 py-3 mt-2 select-none" onClick={addParam}>
+ Add Param
</button>

View File

@ -19,23 +19,7 @@ const Wrapper = styled.div`
}
td {
padding: 6px 10px;
&:nth-child(1) {
width: 30%;
}
&:nth-child(2) {
width: 45%;
}
&:nth-child(3) {
width: 25%;
}
&:nth-child(4) {
width: 70px;
}
}
}
.btn-add-param {

View File

@ -7,12 +7,15 @@ import { useTheme } from 'providers/Theme';
import {
addMultipartFormParam,
updateMultipartFormParam,
deleteMultipartFormParam
deleteMultipartFormParam,
moveMultipartFormParam
} from 'providers/ReduxStore/slices/collections';
import MultiLineEditor from 'components/MultiLineEditor';
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';
import FilePickerEditor from 'components/FilePickerEditor';
import Table from 'components/Table/index';
import ReorderTable from 'components/ReorderTable/index';
const MultipartFormParams = ({ item, collection }) => {
const dispatch = useDispatch();
@ -82,80 +85,47 @@ const MultipartFormParams = ({ item, collection }) => {
);
};
const handleParamDrag = ({ updateReorderedItem }) => {
dispatch(
moveMultipartFormParam({
collectionUid: collection.uid,
itemUid: item.uid,
updateReorderedItem
})
);
};
return (
<StyledWrapper className="w-full">
<table>
<thead>
<tr>
<td>Key</td>
<td>Value</td>
<td>Content-Type</td>
<td></td>
</tr>
</thead>
<tbody>
<Table
headers={[
{ name: 'Key', accessor: 'key', width: '29%' },
{ name: 'Value', accessor: 'value', width: '29%' },
{ name: 'Content-Type', accessor: 'content-type', width: '28%' },
{ name: '', accessor: '', width: '14%' }
]}
>
<ReorderTable updateReorderedItem={handleParamDrag}>
{params && params.length
? params.map((param, index) => {
return (
<tr key={param.uid}>
<td>
<input
type="text"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={param.name}
className="mousetrap"
onChange={(e) => handleParamChange(e, param, 'name')}
/>
</td>
<td>
{param.type === 'file' ? (
<FilePickerEditor
value={param.value}
onChange={(newValue) =>
handleParamChange(
{
target: {
value: newValue
}
},
param,
'value'
)
}
collection={collection}
/>
) : (
<MultiLineEditor
onSave={onSave}
theme={storedTheme}
value={param.value}
onChange={(newValue) =>
handleParamChange(
{
target: {
value: newValue
}
},
param,
'value'
)
}
onRun={handleRun}
allowNewlines={true}
collection={collection}
item={item}
/>
)}
</td>
<td>
<MultiLineEditor
onSave={onSave}
theme={storedTheme}
placeholder="Auto"
value={param.contentType}
return (
<tr key={param.uid} className='w-full' data-uid={param.uid}>
<td className="flex relative">
<input
type="text"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={param.name}
className="mousetrap"
onChange={(e) => handleParamChange(e, param, 'name')}
/>
</td>
<td>
{param.type === 'file' ? (
<FilePickerEditor
value={param.value}
onChange={(newValue) =>
handleParamChange(
{
@ -164,33 +134,75 @@ const MultipartFormParams = ({ item, collection }) => {
}
},
param,
'contentType'
'value'
)
}
collection={collection}
/>
) : (
<MultiLineEditor
onSave={onSave}
theme={storedTheme}
value={param.value}
onChange={(newValue) =>
handleParamChange(
{
target: {
value: newValue
}
},
param,
'value'
)
}
onRun={handleRun}
allowNewlines={true}
collection={collection}
item={item}
/>
</td>
<td>
<div className="flex items-center">
<input
type="checkbox"
checked={param.enabled}
tabIndex="-1"
className="mr-3 mousetrap"
onChange={(e) => handleParamChange(e, param, 'enabled')}
/>
<button tabIndex="-1" onClick={() => handleRemoveParams(param)}>
<IconTrash strokeWidth={1.5} size={20} />
</button>
</div>
</td>
</tr>
);
})
)}
</td>
<td>
<MultiLineEditor
onSave={onSave}
theme={storedTheme}
placeholder="Auto"
value={param.contentType}
onChange={(newValue) =>
handleParamChange(
{
target: {
value: newValue
}
},
param,
'contentType'
)
}
onRun={handleRun}
collection={collection}
/>
</td>
<td>
<div className="flex items-center">
<input
type="checkbox"
checked={param.enabled}
tabIndex="-1"
className="mr-3 mousetrap"
onChange={(e) => handleParamChange(e, param, 'enabled')}
/>
<button tabIndex="-1" onClick={() => handleRemoveParams(param)}>
<IconTrash strokeWidth={1.5} size={20} />
</button>
</div>
</td>
</tr>
);
})
: null}
</tbody>
</table>
</ReorderTable>
</Table>
<div>
<button className="btn-add-param text-link pr-2 pt-3 mt-2 select-none" onClick={addParam}>
+ Add Param

View File

@ -103,7 +103,7 @@ const QueryParams = ({ item, collection }) => {
);
};
const handleParamDrag = ({ updateReorderedItem }) => {
const handleQueryParamDrag = ({ updateReorderedItem }) => {
dispatch(
moveQueryParam({
collectionUid: collection.uid,
@ -124,7 +124,7 @@ const QueryParams = ({ item, collection }) => {
{ name: '', accessor: '', width: '13%' }
]}
>
<ReorderTable updateReorderedItem={handleParamDrag}>
<ReorderTable updateReorderedItem={handleQueryParamDrag}>
{queryParams && queryParams.length
? queryParams.map((param, index) => (
<tr key={param.uid} data-uid={param.uid}>

View File

@ -48,6 +48,7 @@ const RequestBody = ({ item, collection }) => {
<StyledWrapper className="w-full">
<CodeEditor
collection={collection}
item={item}
theme={displayedTheme}
font={get(preferences, 'font.codeFont', 'default')}
fontSize={get(preferences, 'font.codeFontSize')}

View File

@ -19,15 +19,7 @@ const Wrapper = styled.div`
}
td {
padding: 6px 10px;
&:nth-child(1) {
width: 30%;
}
&:nth-child(3) {
width: 70px;
}
}
}
.btn-add-header {

View File

@ -4,12 +4,14 @@ import cloneDeep from 'lodash/cloneDeep';
import { IconTrash } from '@tabler/icons';
import { useDispatch } from 'react-redux';
import { useTheme } from 'providers/Theme';
import { addRequestHeader, updateRequestHeader, deleteRequestHeader } from 'providers/ReduxStore/slices/collections';
import { addRequestHeader, updateRequestHeader, deleteRequestHeader, moveRequestHeader } from 'providers/ReduxStore/slices/collections';
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import SingleLineEditor from 'components/SingleLineEditor';
import StyledWrapper from './StyledWrapper';
import { headers as StandardHTTPHeaders } from 'know-your-http-well';
import { MimeTypes } from 'utils/codemirror/autocompleteConstants';
import Table from 'components/Table/index';
import ReorderTable from 'components/ReorderTable/index';
const headerAutoCompleteList = StandardHTTPHeaders.map((e) => e.header);
const RequestHeaders = ({ item, collection }) => {
@ -63,22 +65,31 @@ const RequestHeaders = ({ item, collection }) => {
);
};
const handleHeaderDrag = ({ updateReorderedItem }) => {
dispatch(
moveRequestHeader({
collectionUid: collection.uid,
itemUid: item.uid,
updateReorderedItem
})
);
};
return (
<StyledWrapper className="w-full">
<table>
<thead>
<tr>
<td>Name</td>
<td>Value</td>
<td></td>
</tr>
</thead>
<tbody>
{headers && headers.length
<Table
headers={[
{ name: 'Key', accessor: 'key', width: '34%' },
{ name: 'Value', accessor: 'value', width: '46%' },
{ name: '', accessor: '', width: '20%' }
]}
>
<ReorderTable updateReorderedItem={handleHeaderDrag}>
{headers && headers.length
? headers.map((header) => {
return (
<tr key={header.uid}>
<td>
<tr key={header.uid} data-uid={header.uid}>
<td className='flex relative'>
<SingleLineEditor
value={header.name}
theme={storedTheme}
@ -140,8 +151,8 @@ const RequestHeaders = ({ item, collection }) => {
);
})
: null}
</tbody>
</table>
</ReorderTable>
</Table>
<button className="btn-add-header text-link pr-2 py-3 mt-2 select-none" onClick={addHeader}>
+ Add Header
</button>

View File

@ -19,16 +19,8 @@ const Wrapper = styled.div`
}
td {
padding: 6px 10px;
&:nth-child(1) {
width: 30%;
}
&:nth-child(3) {
width: 70px;
}
}
}
.btn-add-var {
font-size: 0.8125rem;
@ -38,7 +30,8 @@ const Wrapper = styled.div`
width: 100%;
border: solid 1px transparent;
outline: none !important;
background-color: inherit;
color: ${(props) => props.theme.table.input.color};
background: transparent;
&:focus {
outline: none !important;

View File

@ -3,13 +3,15 @@ import cloneDeep from 'lodash/cloneDeep';
import { IconTrash } from '@tabler/icons';
import { useDispatch } from 'react-redux';
import { useTheme } from 'providers/Theme';
import { addVar, updateVar, deleteVar } from 'providers/ReduxStore/slices/collections';
import { addVar, updateVar, deleteVar, moveVar } from 'providers/ReduxStore/slices/collections';
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import SingleLineEditor from 'components/SingleLineEditor';
import InfoTip from 'components/InfoTip';
import StyledWrapper from './StyledWrapper';
import toast from 'react-hot-toast';
import { variableNameRegex } from 'utils/common/regex';
import Table from 'components/Table/index';
import ReorderTable from 'components/ReorderTable/index';
const VarsTable = ({ item, collection, vars, varType }) => {
const dispatch = useDispatch();
@ -73,35 +75,41 @@ const VarsTable = ({ item, collection, vars, varType }) => {
);
};
const handleVarDrag = ({ updateReorderedItem }) => {
dispatch(
moveVar({
type: varType,
collectionUid: collection.uid,
itemUid: item.uid,
updateReorderedItem
})
);
};
return (
<StyledWrapper className="w-full">
<table>
<thead>
<tr>
<td>Name</td>
{varType === 'request' ? (
<td>
<div className="flex items-center">
<span>Value</span>
</div>
</td>
) : (
<td>
<div className="flex items-center">
<span>Expr</span>
<InfoTip text="You can write any valid JS expression here" infotipId="response-var" />
</div>
</td>
)}
<td></td>
</tr>
</thead>
<tbody>
{vars && vars.length
<Table
headers={[
{ name: 'Name', accessor: 'name', width: '40%' },
{ name: varType === 'request' ? (
<div className="flex items-center">
<span>Value</span>
</div>
) : (
<div className="flex items-center">
<span>Expr</span>
<InfoTip text="You can write any valid JS expression here" infotipId="response-var" />
</div>
), accessor: 'value', width: '46%' },
{ name: '', accessor: '', width: '14%' }
]}
>
<ReorderTable updateReorderedItem={handleVarDrag}>
{vars && vars.length
? vars.map((_var) => {
return (
<tr key={_var.uid}>
<td>
<tr key={_var.uid} data-uid={_var.uid}>
<td className='flex relative'>
<input
type="text"
autoComplete="off"
@ -152,8 +160,8 @@ const VarsTable = ({ item, collection, vars, varType }) => {
);
})
: null}
</tbody>
</table>
</ReorderTable>
</Table>
<button className="btn-add-var text-link pr-2 py-3 mt-2 select-none" onClick={handleAddVar}>
+ Add
</button>

View File

@ -152,6 +152,8 @@ const Collection = ({ collection, searchText }) => {
<div className="flex py-1 collection-name items-center" ref={drop}>
<div
className="flex flex-grow items-center overflow-hidden"
onClick={handleCollapseCollection}
onContextMenu={handleRightClick}
>
<IconChevronRight
size={16}
@ -160,9 +162,7 @@ const Collection = ({ collection, searchText }) => {
style={{ width: 16, minWidth: 16, color: 'rgb(160 160 160)' }}
onClick={handleClick}
/>
<div className="ml-1" id="sidebar-collection-name"
onClick={handleCollapseCollection}
onContextMenu={handleRightClick}>
<div className="ml-1" id="sidebar-collection-name">
{collection.name}
</div>
</div>

View File

@ -68,7 +68,7 @@ const ImportCollection = ({ onClose, handleSubmit }) => {
);
};
return (
<Modal size="sm" title="Import Collection" hideFooter={true} handleConfirm={onClose} handleCancel={onClose}>
<Modal size="sm" title="Import Collection" hideFooter={true} handleCancel={onClose}>
<div className="flex flex-col">
<h3 className="text-sm">Select the type of your existing collection :</h3>
<div className="mt-4 grid grid-rows-2 grid-flow-col gap-2">

View File

@ -58,7 +58,7 @@ const trackStart = () => {
event: 'start',
properties: {
os: platformLib.os.family,
version: '1.36.0'
version: '1.38.1'
}
});
};

View File

@ -532,11 +532,16 @@ export const collectionsSlice = createSlice({
const { updateReorderedItem } = action.payload;
const params = item.draft.request.params;
item.draft.request.params = updateReorderedItem.map((uid) => {
return params.find((param) => param.uid === uid);
const queryParams = params.filter((param) => param.type === 'query');
const pathParams = params.filter((param) => param.type === 'path');
// Reorder only query params based on updateReorderedItem
const reorderedQueryParams = updateReorderedItem.map((uid) => {
return queryParams.find((param) => param.uid === uid);
});
// update request url
item.draft.request.params = [...reorderedQueryParams, ...pathParams];
// Update request URL
const parts = splitOnFirst(item.draft.request.url, '?');
const query = stringifyQueryParams(filter(item.draft.request.params, (p) => p.enabled && p.type === 'query'));
if (query && query.length) {
@ -694,6 +699,28 @@ export const collectionsSlice = createSlice({
}
}
},
moveRequestHeader: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
if (collection) {
const item = findItemInCollection(collection, action.payload.itemUid);
if (item && isItemARequest(item)) {
// Ensure item.draft is a deep clone of item if not already present
if (!item.draft) {
item.draft = cloneDeep(item);
}
// Extract payload data
const { updateReorderedItem } = action.payload;
const params = item.draft.request.headers;
item.draft.request.headers = updateReorderedItem.map((uid) => {
return params.find((param) => param.uid === uid);
});
}
}
},
addFormUrlEncodedParam: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
@ -752,6 +779,28 @@ export const collectionsSlice = createSlice({
}
}
},
moveFormUrlEncodedParam: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
if (collection) {
const item = findItemInCollection(collection, action.payload.itemUid);
if (item && isItemARequest(item)) {
// Ensure item.draft is a deep clone of item if not already present
if (!item.draft) {
item.draft = cloneDeep(item);
}
// Extract payload data
const { updateReorderedItem } = action.payload;
const params = item.draft.request.body.formUrlEncoded;
item.draft.request.body.formUrlEncoded = updateReorderedItem.map((uid) => {
return params.find((param) => param.uid === uid);
});
}
}
},
addMultipartFormParam: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
@ -814,6 +863,28 @@ export const collectionsSlice = createSlice({
}
}
},
moveMultipartFormParam: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
if (collection) {
const item = findItemInCollection(collection, action.payload.itemUid);
if (item && isItemARequest(item)) {
// Ensure item.draft is a deep clone of item if not already present
if (!item.draft) {
item.draft = cloneDeep(item);
}
// Extract payload data
const { updateReorderedItem } = action.payload;
const params = item.draft.request.body.multipartForm;
item.draft.request.body.multipartForm = updateReorderedItem.map((uid) => {
return params.find((param) => param.uid === uid);
});
}
}
},
updateRequestAuthMode: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
@ -1027,6 +1098,28 @@ export const collectionsSlice = createSlice({
}
}
},
moveAssertion: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
if (collection) {
const item = findItemInCollection(collection, action.payload.itemUid);
if (item && isItemARequest(item)) {
// Ensure item.draft is a deep clone of item if not already present
if (!item.draft) {
item.draft = cloneDeep(item);
}
// Extract payload data
const { updateReorderedItem } = action.payload;
const params = item.draft.request.assertions;
item.draft.request.assertions = updateReorderedItem.map((uid) => {
return params.find((param) => param.uid === uid);
});
}
}
},
addVar: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
const type = action.payload.type;
@ -1121,6 +1214,37 @@ export const collectionsSlice = createSlice({
}
}
},
moveVar: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
const type = action.payload.type;
if (collection) {
const item = findItemInCollection(collection, action.payload.itemUid);
if (item && isItemARequest(item)) {
// Ensure item.draft is a deep clone of item if not already present
if (!item.draft) {
item.draft = cloneDeep(item);
}
// Extract payload data
const { updateReorderedItem } = action.payload;
if(type == "request"){
const params = item.draft.request.vars.req;
item.draft.request.vars.req = updateReorderedItem.map((uid) => {
return params.find((param) => param.uid === uid);
});
} else if (type === 'response') {
const params = item.draft.request.vars.res;
item.draft.request.vars.res = updateReorderedItem.map((uid) => {
return params.find((param) => param.uid === uid);
});
}
}
}
},
updateCollectionAuthMode: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
@ -1801,12 +1925,15 @@ export const {
addRequestHeader,
updateRequestHeader,
deleteRequestHeader,
moveRequestHeader,
addFormUrlEncodedParam,
updateFormUrlEncodedParam,
deleteFormUrlEncodedParam,
moveFormUrlEncodedParam,
addMultipartFormParam,
updateMultipartFormParam,
deleteMultipartFormParam,
moveMultipartFormParam,
updateRequestAuthMode,
updateRequestBodyMode,
updateRequestBody,
@ -1819,9 +1946,11 @@ export const {
addAssertion,
updateAssertion,
deleteAssertion,
moveAssertion,
addVar,
updateVar,
deleteVar,
moveVar,
addFolderHeader,
updateFolderHeader,
deleteFolderHeader,

View File

@ -97,8 +97,8 @@ const prepareRequest = (item = {}, collection = {}) => {
if (request.auth.mode === 'bearer') {
axiosRequest.headers['Authorization'] = `Bearer ${get(request, 'auth.bearer.token')}`;
}
if (request.auth?.mode === 'apikey') {
if (request.auth.mode === 'apikey') {
const placement = get(request, "auth.apikey.placement");
const key = get(request, "auth.apikey.key");
const value = get(request, "auth.apikey.value");

View File

@ -23,6 +23,7 @@ describe('prepare-request: prepareRequest', () => {
});
describe('Properly maps inherited auth from collectionRoot', () => {
// Initialize Test Fixtures
let collection, item;
beforeEach(() => {
@ -160,4 +161,4 @@ describe('prepare-request: prepareRequest', () => {
});
});
});
});
});

View File

@ -1,5 +1,5 @@
{
"version": "v1.36.0",
"version": "v1.38.1",
"name": "bruno",
"description": "Opensource API Client for Exploring and Testing APIs",
"homepage": "https://www.usebruno.com",

View File

@ -22,12 +22,6 @@ const toNumber = (value) => {
return Number.isInteger(num) ? parseInt(value, 10) : parseFloat(value);
};
const removeQuotes = (str) => {
if ((str.startsWith('"') && str.endsWith('"')) || (str.startsWith("'") && str.endsWith("'"))) {
return str.slice(1, -1);
}
return str;
};
const executeQuickJsVm = ({ script: externalScript, context: externalContext, scriptType = 'template-literal' }) => {
if (!externalScript?.length || typeof externalScript !== 'string') {
@ -44,7 +38,6 @@ const executeQuickJsVm = ({ script: externalScript, context: externalContext, sc
if (externalScript === 'null') return null;
if (externalScript === 'undefined') return undefined;
externalScript = removeQuotes(externalScript);
const vm = QuickJSSyncContext;
@ -94,7 +87,6 @@ const executeQuickJsVmAsync = async ({ script: externalScript, context: external
if (externalScript === 'null') return null;
if (externalScript === 'undefined') return undefined;
externalScript = removeQuotes(externalScript);
try {
const module = await newQuickJSWASMModule();

View File

@ -85,14 +85,6 @@ const evaluateJsTemplateLiteral = (templateLiteral, context) => {
return undefined;
}
if (templateLiteral.startsWith('"') && templateLiteral.endsWith('"')) {
return templateLiteral.slice(1, -1);
}
if (templateLiteral.startsWith("'") && templateLiteral.endsWith("'")) {
return templateLiteral.slice(1, -1);
}
if (!isNaN(templateLiteral)) {
const number = Number(templateLiteral);
// Check if the number is too high. Too high number might get altered, see #1000