mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-05 02:18:32 +00:00
Compare commits
16 Commits
fix/json-s
...
feat/colle
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5713e19c23 | ||
|
|
93080de2a8 | ||
|
|
36ef38be6a | ||
|
|
4726f5008e | ||
|
|
091b02c2c3 | ||
|
|
600940226c | ||
|
|
ee7f886c03 | ||
|
|
682c7bd1b1 | ||
|
|
e1aa5b4eb5 | ||
|
|
6bd9d4c480 | ||
|
|
a38d09a117 | ||
|
|
82985d1b43 | ||
|
|
d34d3a45ff | ||
|
|
25ccb38202 | ||
|
|
5f6a5f59b1 | ||
|
|
9e5148f032 |
@@ -39,7 +39,7 @@ Bruno ist ein reines Offline-Tool. Es gibt keine Pläne, Bruno um eine Cloud-Syn
|
||||
|
||||
[Download Bruno](https://www.usebruno.com/downloads)
|
||||
|
||||
📢 Sehen Sie sich unseren Vortrag auf der India FOSS 3.0 Conference [hier](https://www.youtube.com/watch?v=7bSMFpbcPiY) an.
|
||||
📢 Sieh Dir unseren Vortrag auf der India FOSS 3.0 Conference [hier](https://www.youtube.com/watch?v=7bSMFpbcPiY) an.
|
||||
|
||||
 <br /><br />
|
||||
|
||||
@@ -48,13 +48,13 @@ Bruno ist ein reines Offline-Tool. Es gibt keine Pläne, Bruno um eine Cloud-Syn
|
||||
Die meisten unserer Funktionen sind kostenlos und quelloffen.
|
||||
Wir bemühen uns um ein Gleichgewicht zwischen [Open-Source-Prinzipien und Nachhaltigkeit](https://github.com/usebruno/bruno/discussions/269)
|
||||
|
||||
Sie können die [Golden Edition](https://www.usebruno.com/pricing) vorbestellen ~~$19~~ **$9** ! <br/>
|
||||
Du kannst die [Golden Edition](https://www.usebruno.com/pricing) bestellen **$19**! <br/>
|
||||
|
||||
### Installation
|
||||
|
||||
Bruno ist als Download [auf unserer Website](https://www.usebruno.com/downloads) für Mac, Windows und Linux verfügbar.
|
||||
|
||||
Sie können Bruno auch über Paketmanager wie Homebrew, Chocolatey, Scoop, Snap, Flatpak und Apt installieren.
|
||||
Du kannst Bruno auch über Paketmanager wie Homebrew, Chocolatey, Scoop, Snap, Flatpak und Apt installieren.
|
||||
|
||||
```sh
|
||||
# Auf Mac via Homebrew
|
||||
@@ -123,11 +123,11 @@ Oder einer Versionskontrolle deiner Wahl
|
||||
|
||||
### Unterstützung ❤️
|
||||
|
||||
Wuff! Wenn du dieses Projekt magst, klick den ⭐ Button !!
|
||||
Wuff! Wenn du dieses Projekt magst, klick auf den ⭐ Button !!
|
||||
|
||||
### Teile Erfahrungsberichte 📣
|
||||
|
||||
Wenn Bruno dir und in deinen Teams bei der Arbeit geholfen hat, vergiss bitte nicht, deine [Erfahrungsberichte auf unserer GitHub-Diskussion](https://github.com/usebruno/bruno/discussions/343) zu teilen.
|
||||
Wenn Bruno dir und in deinem Team bei der Arbeit geholfen hat, vergiss bitte nicht, deine [Erfahrungsberichte in unserer GitHub-Diskussion](https://github.com/usebruno/bruno/discussions/343) zu teilen.
|
||||
|
||||
### Bereitstellung in neuen Paket-Managern
|
||||
|
||||
|
||||
53
package-lock.json
generated
53
package-lock.json
generated
@@ -50,6 +50,7 @@
|
||||
},
|
||||
"node_modules/@ampproject/remapping": {
|
||||
"version": "2.3.0",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@jridgewell/gen-mapping": "^0.3.5",
|
||||
@@ -670,6 +671,7 @@
|
||||
},
|
||||
"node_modules/@babel/compat-data": {
|
||||
"version": "7.25.2",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -677,6 +679,7 @@
|
||||
},
|
||||
"node_modules/@babel/core": {
|
||||
"version": "7.25.2",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "^2.2.0",
|
||||
@@ -740,6 +743,7 @@
|
||||
},
|
||||
"node_modules/@babel/helper-compilation-targets": {
|
||||
"version": "7.25.2",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/compat-data": "^7.25.2",
|
||||
@@ -754,6 +758,7 @@
|
||||
},
|
||||
"node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": {
|
||||
"version": "5.1.1",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"yallist": "^3.0.2"
|
||||
@@ -761,6 +766,7 @@
|
||||
},
|
||||
"node_modules/@babel/helper-compilation-targets/node_modules/yallist": {
|
||||
"version": "3.1.1",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/@babel/helper-create-class-features-plugin": {
|
||||
@@ -839,6 +845,7 @@
|
||||
},
|
||||
"node_modules/@babel/helper-module-transforms": {
|
||||
"version": "7.25.2",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-module-imports": "^7.24.7",
|
||||
@@ -905,6 +912,7 @@
|
||||
},
|
||||
"node_modules/@babel/helper-simple-access": {
|
||||
"version": "7.24.7",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/traverse": "^7.24.7",
|
||||
@@ -942,6 +950,7 @@
|
||||
},
|
||||
"node_modules/@babel/helper-validator-option": {
|
||||
"version": "7.24.8",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -962,6 +971,7 @@
|
||||
},
|
||||
"node_modules/@babel/helpers": {
|
||||
"version": "7.25.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/template": "^7.25.0",
|
||||
@@ -3644,37 +3654,6 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@n8n/vm2": {
|
||||
"version": "3.9.25",
|
||||
"resolved": "https://registry.npmjs.org/@n8n/vm2/-/vm2-3.9.25.tgz",
|
||||
"integrity": "sha512-qoGLFzyHBW7HKpwXkl05QKsIh3GkDw6lOiTOWYlUDnOIQ1b7EgM+O5EMjrMGy7r+kz52+Q7o6GLxBIcxVI8rEg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"acorn": "^8.7.0",
|
||||
"acorn-walk": "^8.2.0"
|
||||
},
|
||||
"bin": {
|
||||
"vm2": "bin/vm2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.10",
|
||||
"pnpm": ">=9.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@n8n/vm2/node_modules/acorn": {
|
||||
"version": "8.12.1",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz",
|
||||
"integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/env": {
|
||||
"version": "12.3.3",
|
||||
"license": "MIT"
|
||||
@@ -4751,6 +4730,7 @@
|
||||
},
|
||||
"node_modules/@types/linkify-it": {
|
||||
"version": "5.0.0",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/lodash": {
|
||||
@@ -4759,6 +4739,7 @@
|
||||
},
|
||||
"node_modules/@types/markdown-it": {
|
||||
"version": "12.2.3",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/linkify-it": "*",
|
||||
@@ -4767,6 +4748,7 @@
|
||||
},
|
||||
"node_modules/@types/mdurl": {
|
||||
"version": "2.0.0",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/minimatch": {
|
||||
@@ -6240,6 +6222,7 @@
|
||||
},
|
||||
"node_modules/browserslist": {
|
||||
"version": "4.23.3",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -7238,6 +7221,7 @@
|
||||
},
|
||||
"node_modules/convert-source-map": {
|
||||
"version": "2.0.0",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cookie": {
|
||||
@@ -8477,6 +8461,7 @@
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.11",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/electron-util": {
|
||||
@@ -9438,6 +9423,7 @@
|
||||
},
|
||||
"node_modules/gensync": {
|
||||
"version": "1.0.0-beta.2",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -13186,6 +13172,7 @@
|
||||
},
|
||||
"node_modules/node-releases": {
|
||||
"version": "2.0.18",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/node-vault": {
|
||||
@@ -16201,6 +16188,7 @@
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "6.3.1",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
@@ -17663,6 +17651,7 @@
|
||||
},
|
||||
"node_modules/update-browserslist-db": {
|
||||
"version": "1.1.0",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -18633,7 +18622,7 @@
|
||||
},
|
||||
"packages/bruno-electron": {
|
||||
"name": "bruno",
|
||||
"version": "v1.26.1",
|
||||
"version": "v1.27.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/credential-providers": "3.525.0",
|
||||
"@usebruno/common": "0.1.0",
|
||||
|
||||
@@ -5,6 +5,7 @@ const StyledWrapper = styled.div`
|
||||
background: ${(props) => props.theme.codemirror.bg};
|
||||
border: solid 1px ${(props) => props.theme.codemirror.border};
|
||||
font-family: ${(props) => (props.font ? props.font : 'default')};
|
||||
font-size: ${(props) => (props.fontSize ? `${props.fontSize}px` : 'inherit')};
|
||||
line-break: anywhere;
|
||||
flex: 1 1 0;
|
||||
}
|
||||
|
||||
@@ -67,6 +67,7 @@ if (!SERVER_RENDERED) {
|
||||
'bru.setVar(key,value)',
|
||||
'bru.deleteVar(key)',
|
||||
'bru.setNextRequest(requestName)',
|
||||
'req.disableParsingResponseJson()',
|
||||
'bru.getRequestVar(key)',
|
||||
'bru.sleep(ms)'
|
||||
];
|
||||
@@ -332,6 +333,7 @@ export default class CodeEditor extends React.Component {
|
||||
className="h-full w-full flex flex-col relative"
|
||||
aria-label="Code Editor"
|
||||
font={this.props.font}
|
||||
fontSize={this.props.fontSize}
|
||||
ref={(node) => {
|
||||
this._node = node;
|
||||
}}
|
||||
|
||||
@@ -10,8 +10,9 @@ import StyledWrapper from './StyledWrapper';
|
||||
import { useRef } from 'react';
|
||||
import path from 'path';
|
||||
import slash from 'utils/common/slash';
|
||||
import { isWindowsOS } from 'utils/common/platform';
|
||||
|
||||
const ClientCertSettings = ({ clientCertConfig, onUpdate, onRemove }) => {
|
||||
const ClientCertSettings = ({ root, clientCertConfig, onUpdate, onRemove }) => {
|
||||
const certFilePathInputRef = useRef();
|
||||
const keyFilePathInputRef = useRef();
|
||||
const pfxFilePathInputRef = useRef();
|
||||
@@ -67,7 +68,15 @@ const ClientCertSettings = ({ clientCertConfig, onUpdate, onRemove }) => {
|
||||
});
|
||||
|
||||
const getFile = (e) => {
|
||||
e.files?.[0]?.path && formik.setFieldValue(e.name, e.files?.[0]?.path);
|
||||
if (e.files?.[0]?.path) {
|
||||
let relativePath;
|
||||
if (isWindowsOS()) {
|
||||
relativePath = slash(path.win32.relative(root, e.files[0].path));
|
||||
} else {
|
||||
relativePath = path.posix.relative(root, e.files[0].path);
|
||||
}
|
||||
formik.setFieldValue(e.name, relativePath);
|
||||
}
|
||||
};
|
||||
|
||||
const resetFileInputFields = () => {
|
||||
@@ -102,10 +111,14 @@ const ClientCertSettings = ({ clientCertConfig, onUpdate, onRemove }) => {
|
||||
: clientCertConfig.map((clientCert) => (
|
||||
<li key={uuid()} className="flex items-center available-certificates p-2 rounded-lg mb-2">
|
||||
<div className="flex items-center w-full justify-between">
|
||||
<div className="flex items-center">
|
||||
<div className="flex w-full items-center">
|
||||
<IconWorld className="mr-2" size={18} strokeWidth={1.5} />
|
||||
{clientCert.domain}
|
||||
</div>
|
||||
<div className="flex w-full items-center">
|
||||
<IconCertificate className="mr-2 flex-shrink-0" size={18} strokeWidth={1.5} />
|
||||
{clientCert.type === 'cert' ? clientCert.certFilePath : clientCert.pfxFilePath}
|
||||
</div>
|
||||
<button onClick={() => onRemove(clientCert)} className="remove-certificate ml-2">
|
||||
<IconTrash size={18} strokeWidth={1.5} />
|
||||
</button>
|
||||
|
||||
@@ -46,6 +46,7 @@ const Docs = ({ collection }) => {
|
||||
onSave={onSave}
|
||||
mode="application/text"
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
fontSize={get(preferences, 'font.codeFontSize')}
|
||||
/>
|
||||
) : (
|
||||
<Markdown collectionPath={collection.pathname} onDoubleClick={toggleViewMode} content={docs} />
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useFormik } from 'formik';
|
||||
import Tooltip from 'components/Tooltip';
|
||||
import InfoTip from 'components/InfoTip';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import * as Yup from 'yup';
|
||||
import toast from 'react-hot-toast';
|
||||
@@ -104,7 +104,7 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label flex items-center" htmlFor="enabled">
|
||||
Config
|
||||
<Tooltip
|
||||
<InfoTip
|
||||
text={`
|
||||
<div>
|
||||
<ul>
|
||||
@@ -114,7 +114,7 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
|
||||
</ul>
|
||||
</div>
|
||||
`}
|
||||
tooltipId="request-var"
|
||||
infotipId="request-var"
|
||||
/>
|
||||
</label>
|
||||
<div className="flex items-center">
|
||||
|
||||
@@ -52,6 +52,7 @@ const Script = ({ collection }) => {
|
||||
mode="javascript"
|
||||
onSave={handleSave}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
fontSize={get(preferences, 'font.codeFontSize')}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 mt-6">
|
||||
@@ -64,6 +65,7 @@ const Script = ({ collection }) => {
|
||||
mode="javascript"
|
||||
onSave={handleSave}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
fontSize={get(preferences, 'font.codeFontSize')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ const Tests = ({ collection }) => {
|
||||
mode="javascript"
|
||||
onSave={handleSave}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
fontSize={get(preferences, 'font.codeFontSize')}
|
||||
/>
|
||||
|
||||
<div className="mt-6">
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
div.title {
|
||||
color: var(--color-tab-inactive);
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,56 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-weight: 600;
|
||||
table-layout: fixed;
|
||||
|
||||
thead,
|
||||
td {
|
||||
border: 1px solid ${(props) => props.theme.table.border};
|
||||
}
|
||||
|
||||
thead {
|
||||
color: ${(props) => props.theme.table.thead.color};
|
||||
font-size: 0.8125rem;
|
||||
user-select: none;
|
||||
}
|
||||
td {
|
||||
padding: 6px 10px;
|
||||
|
||||
&:nth-child(1) {
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
width: 70px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btn-add-var {
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
input[type='text'] {
|
||||
width: 100%;
|
||||
border: solid 1px transparent;
|
||||
outline: none !important;
|
||||
background-color: inherit;
|
||||
|
||||
&:focus {
|
||||
outline: none !important;
|
||||
border: solid 1px transparent;
|
||||
}
|
||||
}
|
||||
|
||||
input[type='checkbox'] {
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
top: 1px;
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
@@ -0,0 +1,162 @@
|
||||
import React from 'react';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { IconTrash } from '@tabler/icons';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { saveCollectionRoot } 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 {
|
||||
addCollectionVar,
|
||||
deleteCollectionVar,
|
||||
updateCollectionVar
|
||||
} from 'providers/ReduxStore/slices/collections/index';
|
||||
|
||||
const VarsTable = ({ collection, vars, varType }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const addVar = () => {
|
||||
dispatch(
|
||||
addCollectionVar({
|
||||
collectionUid: collection.uid,
|
||||
type: varType
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const onSave = () => dispatch(saveCollectionRoot(collection.uid));
|
||||
const handleVarChange = (e, v, type) => {
|
||||
const _var = cloneDeep(v);
|
||||
switch (type) {
|
||||
case 'name': {
|
||||
const value = e.target.value;
|
||||
|
||||
if (variableNameRegex.test(value) === false) {
|
||||
toast.error(
|
||||
'Variable contains invalid characters! Variables must only contain alpha-numeric characters, "-", "_", "."'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
_var.name = value;
|
||||
break;
|
||||
}
|
||||
case 'value': {
|
||||
_var.value = e.target.value;
|
||||
break;
|
||||
}
|
||||
case 'enabled': {
|
||||
_var.enabled = e.target.checked;
|
||||
break;
|
||||
}
|
||||
}
|
||||
dispatch(
|
||||
updateCollectionVar({
|
||||
type: varType,
|
||||
var: _var,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handleRemoveVar = (_var) => {
|
||||
dispatch(
|
||||
deleteCollectionVar({
|
||||
type: varType,
|
||||
varUid: _var.uid,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
{varType === 'request' ? (
|
||||
<td>
|
||||
<div className="flex items-center">
|
||||
<span>Value</span>
|
||||
<InfoTip text="You can write any valid JS Template Literal here" infotipId="request-var" />
|
||||
</div>
|
||||
</td>
|
||||
) : (
|
||||
<td>
|
||||
<div className="flex items-center">
|
||||
<span>Expr</span>
|
||||
<InfoTip text="You can write any valid JS Template Literal here" infotipId="request-var" />
|
||||
</div>
|
||||
</td>
|
||||
)}
|
||||
<td></td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{vars && vars.length
|
||||
? vars.map((_var) => {
|
||||
return (
|
||||
<tr key={_var.uid}>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={_var.name}
|
||||
className="mousetrap"
|
||||
onChange={(e) => handleVarChange(e, _var, 'name')}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<SingleLineEditor
|
||||
value={_var.value}
|
||||
theme={storedTheme}
|
||||
onSave={onSave}
|
||||
onChange={(newValue) =>
|
||||
handleVarChange(
|
||||
{
|
||||
target: {
|
||||
value: newValue
|
||||
}
|
||||
},
|
||||
_var,
|
||||
'value'
|
||||
)
|
||||
}
|
||||
collection={collection}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={_var.enabled}
|
||||
tabIndex="-1"
|
||||
className="mr-3 mousetrap"
|
||||
onChange={(e) => handleVarChange(e, _var, 'enabled')}
|
||||
/>
|
||||
<button tabIndex="-1" onClick={() => handleRemoveVar(_var)}>
|
||||
<IconTrash strokeWidth={1.5} size={20} />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
: null}
|
||||
</tbody>
|
||||
</table>
|
||||
<button className="btn-add-var text-link pr-2 py-3 mt-2 select-none" onClick={addVar}>
|
||||
+ Add
|
||||
</button>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
export default VarsTable;
|
||||
@@ -0,0 +1,32 @@
|
||||
import React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import VarsTable from './VarsTable';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
const Vars = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const requestVars = get(collection, 'root.request.vars.req', []);
|
||||
const responseVars = get(collection, 'root.request.vars.res', []);
|
||||
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
|
||||
return (
|
||||
<StyledWrapper className="w-full flex flex-col">
|
||||
<div className="flex-1 mt-2">
|
||||
<div className="mb-1 title text-xs">Pre Request</div>
|
||||
<VarsTable collection={collection} vars={requestVars} varType="request" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="mt-1 mb-1 title text-xs">Post Response</div>
|
||||
<VarsTable collection={collection} vars={responseVars} varType="response" />
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Vars;
|
||||
@@ -16,6 +16,7 @@ import Docs from './Docs';
|
||||
import Presets from './Presets';
|
||||
import Info from './Info';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import Vars from './Vars/index';
|
||||
|
||||
const CollectionSettings = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -77,6 +78,9 @@ const CollectionSettings = ({ collection }) => {
|
||||
case 'headers': {
|
||||
return <Headers collection={collection} />;
|
||||
}
|
||||
case 'vars': {
|
||||
return <Vars collection={collection} />;
|
||||
}
|
||||
case 'auth': {
|
||||
return <Auth collection={collection} />;
|
||||
}
|
||||
@@ -95,6 +99,7 @@ const CollectionSettings = ({ collection }) => {
|
||||
case 'clientCert': {
|
||||
return (
|
||||
<ClientCertSettings
|
||||
root={collection.pathname}
|
||||
clientCertConfig={clientCertConfig}
|
||||
onUpdate={onClientCertSettingsUpdate}
|
||||
onRemove={onClientCertSettingsRemove}
|
||||
@@ -122,6 +127,9 @@ const CollectionSettings = ({ collection }) => {
|
||||
<div className={getTabClassname('headers')} role="tab" onClick={() => setTab('headers')}>
|
||||
Headers
|
||||
</div>
|
||||
<div className={getTabClassname('vars')} role="tab" onClick={() => setTab('vars')}>
|
||||
Vars
|
||||
</div>
|
||||
<div className={getTabClassname('auth')} role="tab" onClick={() => setTab('auth')}>
|
||||
Auth
|
||||
</div>
|
||||
|
||||
@@ -47,6 +47,7 @@ const Documentation = ({ item, collection }) => {
|
||||
collection={collection}
|
||||
theme={displayedTheme}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
fontSize={get(preferences, 'font.codeFontSize')}
|
||||
value={docs || ''}
|
||||
onEdit={onEdit}
|
||||
onSave={onSave}
|
||||
|
||||
@@ -116,6 +116,7 @@ const Headers = ({ collection, folder }) => {
|
||||
)
|
||||
}
|
||||
collection={collection}
|
||||
item={folder}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@@ -54,6 +54,7 @@ const Script = ({ collection, folder }) => {
|
||||
mode="javascript"
|
||||
onSave={handleSave}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
fontSize={get(preferences, 'font.codeFontSize')}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col flex-1 mt-2 gap-y-2">
|
||||
@@ -66,6 +67,7 @@ const Script = ({ collection, folder }) => {
|
||||
mode="javascript"
|
||||
onSave={handleSave}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
fontSize={get(preferences, 'font.codeFontSize')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ const Tests = ({ collection, folder }) => {
|
||||
mode="javascript"
|
||||
onSave={handleSave}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
fontSize={get(preferences, 'font.codeFontSize')}
|
||||
/>
|
||||
|
||||
<div className="mt-6">
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useDispatch } from 'react-redux';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { saveFolderRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import Tooltip from 'components/Tooltip';
|
||||
import InfoTip from 'components/InfoTip';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import toast from 'react-hot-toast';
|
||||
import { variableNameRegex } from 'utils/common/regex';
|
||||
@@ -82,14 +82,14 @@ const VarsTable = ({ folder, collection, vars, varType }) => {
|
||||
<td>
|
||||
<div className="flex items-center">
|
||||
<span>Value</span>
|
||||
<Tooltip text="You can write any valid JS Template Literal here" tooltipId="request-var" />
|
||||
<InfoTip text="You can write any valid JS Template Literal here" infotipId="request-var" />
|
||||
</div>
|
||||
</td>
|
||||
) : (
|
||||
<td>
|
||||
<div className="flex items-center">
|
||||
<span>Expr</span>
|
||||
<Tooltip text="You can write any valid JS expression here" tooltipId="response-var" />
|
||||
<InfoTip text="You can write any valid JS expression here" infotipId="response-var" />
|
||||
</div>
|
||||
</td>
|
||||
)}
|
||||
@@ -130,6 +130,7 @@ const VarsTable = ({ folder, collection, vars, varType }) => {
|
||||
)
|
||||
}
|
||||
collection={collection}
|
||||
item={folder}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import React from 'react';
|
||||
import { Tooltip as ReactTooltip } from 'react-tooltip';
|
||||
import { Tooltip as ReactInfoTip } from 'react-tooltip';
|
||||
|
||||
const Tooltip = ({ text, tooltipId }) => {
|
||||
const InfoTip = ({ text, infotipId }) => {
|
||||
return (
|
||||
<>
|
||||
<svg
|
||||
tabIndex="-1"
|
||||
id={tooltipId}
|
||||
id={infotipId}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="14"
|
||||
height="14"
|
||||
@@ -17,9 +17,9 @@ const Tooltip = ({ text, tooltipId }) => {
|
||||
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z" />
|
||||
<path d="M5.255 5.786a.237.237 0 0 0 .241.247h.825c.138 0 .248-.113.266-.25.09-.656.54-1.134 1.342-1.134.686 0 1.314.343 1.314 1.168 0 .635-.374.927-.965 1.371-.673.489-1.206 1.06-1.168 1.987l.003.217a.25.25 0 0 0 .25.246h.811a.25.25 0 0 0 .25-.25v-.105c0-.718.273-.927 1.01-1.486.609-.463 1.244-.977 1.244-2.056 0-1.511-1.276-2.241-2.673-2.241-1.267 0-2.655.59-2.75 2.286zm1.557 5.763c0 .533.425.927 1.01.927.609 0 1.028-.394 1.028-.927 0-.552-.42-.94-1.029-.94-.584 0-1.009.388-1.009.94z" />
|
||||
</svg>
|
||||
<ReactTooltip anchorId={tooltipId} html={text} />
|
||||
<ReactInfoTip anchorId={infotipId} html={text} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Tooltip;
|
||||
export default InfoTip;
|
||||
@@ -10,6 +10,8 @@ import {
|
||||
} from 'providers/ReduxStore/slices/notifications';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { humanizeDate, relativeDate } from 'utils/common';
|
||||
import ToolHint from 'components/ToolHint';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
|
||||
const PAGE_SIZE = 5;
|
||||
|
||||
@@ -20,6 +22,7 @@ const Notifications = () => {
|
||||
const [showNotificationsModal, setShowNotificationsModal] = useState(false);
|
||||
const [selectedNotification, setSelectedNotification] = useState(null);
|
||||
const [pageNumber, setPageNumber] = useState(1);
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const notificationsStartIndex = (pageNumber - 1) * PAGE_SIZE;
|
||||
const notificationsEndIndex = pageNumber * PAGE_SIZE;
|
||||
@@ -85,21 +88,22 @@ const Notifications = () => {
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<a
|
||||
title="Notifications"
|
||||
className="relative cursor-pointer"
|
||||
onClick={() => {
|
||||
dispatch(fetchNotifications());
|
||||
setShowNotificationsModal(true);
|
||||
}}
|
||||
>
|
||||
<IconBell
|
||||
size={18}
|
||||
strokeWidth={1.5}
|
||||
className={`mr-2 hover:text-gray-700 ${unreadNotifications?.length > 0 ? 'bell' : ''}`}
|
||||
/>
|
||||
{unreadNotifications.length > 0 && (
|
||||
<span className="notification-count text-xs">{unreadNotifications.length}</span>
|
||||
)}
|
||||
<ToolHint text="Notifications" toolhintId="Notifications" offset={8} >
|
||||
<IconBell
|
||||
size={18}
|
||||
strokeWidth={1.5}
|
||||
className={`mr-2 ${unreadNotifications?.length > 0 ? 'bell' : ''}`}
|
||||
/>
|
||||
{unreadNotifications.length > 0 && (
|
||||
<span className="notification-count text-xs">{unreadNotifications.length}</span>
|
||||
)}
|
||||
</ToolHint>
|
||||
</a>
|
||||
|
||||
{showNotificationsModal && (
|
||||
@@ -129,9 +133,8 @@ const Notifications = () => {
|
||||
{notifications?.slice(notificationsStartIndex, notificationsEndIndex)?.map((notification) => (
|
||||
<li
|
||||
key={notification.id}
|
||||
className={`p-4 flex flex-col justify-center ${
|
||||
selectedNotification?.id == notification.id ? 'active' : notification.read ? 'read' : ''
|
||||
}`}
|
||||
className={`p-4 flex flex-col justify-center ${selectedNotification?.id == notification.id ? 'active' : notification.read ? 'read' : ''
|
||||
}`}
|
||||
onClick={handleNotificationItemClick(notification)}
|
||||
>
|
||||
<div className="notification-title w-full">{notification?.title}</div>
|
||||
@@ -141,9 +144,8 @@ const Notifications = () => {
|
||||
</ul>
|
||||
<div className="w-full pagination flex flex-row gap-4 justify-center p-2 items-center text-xs">
|
||||
<button
|
||||
className={`pl-2 pr-2 py-3 select-none ${
|
||||
pageNumber <= 1 ? 'opacity-50' : 'text-link cursor-pointer hover:underline'
|
||||
}`}
|
||||
className={`pl-2 pr-2 py-3 select-none ${pageNumber <= 1 ? 'opacity-50' : 'text-link cursor-pointer hover:underline'
|
||||
}`}
|
||||
onClick={handlePrev}
|
||||
>
|
||||
{'Prev'}
|
||||
@@ -159,9 +161,8 @@ const Notifications = () => {
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
className={`pl-2 pr-2 py-3 select-none ${
|
||||
pageNumber == totalPages ? 'opacity-50' : 'text-link cursor-pointer hover:underline'
|
||||
}`}
|
||||
className={`pl-2 pr-2 py-3 select-none ${pageNumber == totalPages ? 'opacity-50' : 'text-link cursor-pointer hover:underline'
|
||||
}`}
|
||||
onClick={handleNext}
|
||||
>
|
||||
{'Next'}
|
||||
|
||||
@@ -9,17 +9,25 @@ const Font = ({ close }) => {
|
||||
const preferences = useSelector((state) => state.app.preferences);
|
||||
|
||||
const [codeFont, setCodeFont] = useState(get(preferences, 'font.codeFont', 'default'));
|
||||
const [codeFontSize, setCodeFontSize] = useState(get(preferences, 'font.codeFontSize', '14'));
|
||||
|
||||
const handleInputChange = (event) => {
|
||||
const handleCodeFontChange = (event) => {
|
||||
setCodeFont(event.target.value);
|
||||
};
|
||||
|
||||
const handleCodeFontSizeChange = (event) => {
|
||||
// Restrict to min/max value
|
||||
const clampedSize = Math.max(1, Math.min(event.target.value, 32));
|
||||
setCodeFontSize(clampedSize);
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
dispatch(
|
||||
savePreferences({
|
||||
...preferences,
|
||||
font: {
|
||||
codeFont
|
||||
codeFont,
|
||||
codeFontSize
|
||||
}
|
||||
})
|
||||
).then(() => {
|
||||
@@ -29,17 +37,33 @@ const Font = ({ close }) => {
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<label className="block font-medium">Code Editor Font</label>
|
||||
<input
|
||||
type="text"
|
||||
className="block textbox mt-2 w-full"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
onChange={handleInputChange}
|
||||
defaultValue={codeFont}
|
||||
/>
|
||||
<div className="flex flex-row gap-2 w-full">
|
||||
<div className="w-4/5">
|
||||
<label className="block font-medium">Code Editor Font</label>
|
||||
<input
|
||||
type="text"
|
||||
className="block textbox mt-2 w-full"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
onChange={handleCodeFontChange}
|
||||
defaultValue={codeFont}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-1/5">
|
||||
<label className="block font-medium">Font Size</label>
|
||||
<input
|
||||
type="number"
|
||||
className="block textbox mt-2 w-full"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
inputMode="numeric"
|
||||
onChange={handleCodeFontSizeChange}
|
||||
defaultValue={codeFontSize}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-10">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
|
||||
|
||||
@@ -191,6 +191,7 @@ const AssertionRow = ({
|
||||
}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
item={item}
|
||||
/>
|
||||
) : (
|
||||
<input type="text" className="cursor-default" disabled />
|
||||
|
||||
@@ -31,6 +31,7 @@ const GraphQLRequestPane = ({ item, collection, leftPaneWidth, onSchemaLoad, tog
|
||||
: get(item, 'request.body.graphql.variables');
|
||||
const { displayedTheme } = useTheme();
|
||||
const [schema, setSchema] = useState(null);
|
||||
const preferences = useSelector((state) => state.app.preferences);
|
||||
|
||||
useEffect(() => {
|
||||
onSchemaLoad(schema);
|
||||
@@ -71,6 +72,8 @@ const GraphQLRequestPane = ({ item, collection, leftPaneWidth, onSchemaLoad, tog
|
||||
onRun={onRun}
|
||||
onEdit={onQueryChange}
|
||||
onClickReference={handleGqlClickReference}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
fontSize={get(preferences, 'font.codeFontSize')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ const GraphQLVariables = ({ variables, item, collection }) => {
|
||||
value={variables || ''}
|
||||
theme={displayedTheme}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
fontSize={get(preferences, 'font.codeFontSize')}
|
||||
onEdit={onEdit}
|
||||
mode="javascript"
|
||||
onRun={onRun}
|
||||
|
||||
@@ -4,6 +4,8 @@ const StyledWrapper = styled.div`
|
||||
div.CodeMirror {
|
||||
background: ${(props) => props.theme.codemirror.bg};
|
||||
border: solid 1px ${(props) => props.theme.codemirror.border};
|
||||
font-family: ${(props) => (props.font ? props.font : 'default')};
|
||||
font-size: ${(props) => (props.fontSize ? `${props.fontSize}px` : 'inherit')};
|
||||
flex: 1 1 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -211,6 +211,8 @@ export default class QueryEditor extends React.Component {
|
||||
<StyledWrapper
|
||||
className="h-full w-full flex flex-col relative"
|
||||
aria-label="Query Editor"
|
||||
font={this.props.font}
|
||||
fontSize={this.props.fontSize}
|
||||
ref={(node) => {
|
||||
this._node = node;
|
||||
}}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import Tooltip from 'components/Tooltip';
|
||||
import InfoTip from 'components/InfoTip';
|
||||
import { IconTrash } from '@tabler/icons';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
@@ -175,7 +175,7 @@ const QueryParams = ({ item, collection }) => {
|
||||
</button>
|
||||
<div className="mb-2 title text-xs flex items-stretch">
|
||||
<span>Path</span>
|
||||
<Tooltip
|
||||
<InfoTip
|
||||
text={`
|
||||
<div>
|
||||
Path variables are automatically added whenever the
|
||||
@@ -186,7 +186,7 @@ const QueryParams = ({ item, collection }) => {
|
||||
</code>
|
||||
</div>
|
||||
`}
|
||||
tooltipId="path-param-tooltip"
|
||||
infotipId="path-param-InfoTip"
|
||||
/>
|
||||
</div>
|
||||
<table>
|
||||
|
||||
@@ -33,18 +33,18 @@ const Wrapper = styled.div`
|
||||
top: 1px;
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
.infotip {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tooltip:hover .tooltiptext {
|
||||
.infotip:hover .infotiptext {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.tooltiptext {
|
||||
.infotiptext {
|
||||
visibility: hidden;
|
||||
width: auto;
|
||||
background-color: ${(props) => props.theme.requestTabs.active.bg};
|
||||
@@ -62,7 +62,7 @@ const Wrapper = styled.div`
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tooltiptext::after {
|
||||
.infotiptext::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
|
||||
@@ -74,7 +74,7 @@ const QueryUrl = ({ item, collection, handleRun }) => {
|
||||
/>
|
||||
<div className="flex items-center h-full mr-2 cursor-pointer" id="send-request" onClick={handleRun}>
|
||||
<div
|
||||
className="tooltip mx-3"
|
||||
className="infotip mr-3"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (!item.draft) return;
|
||||
@@ -87,7 +87,7 @@ const QueryUrl = ({ item, collection, handleRun }) => {
|
||||
size={22}
|
||||
className={`${item.draft ? 'cursor-pointer' : 'cursor-default'}`}
|
||||
/>
|
||||
<span className="tooltiptext text-xs">
|
||||
<span className="infotiptext text-xs">
|
||||
Save <span className="shortcut">({saveShortcut})</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -50,6 +50,7 @@ const RequestBody = ({ item, collection }) => {
|
||||
collection={collection}
|
||||
theme={displayedTheme}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
fontSize={get(preferences, 'font.codeFontSize')}
|
||||
value={bodyContent[bodyMode] || ''}
|
||||
onEdit={onEdit}
|
||||
onRun={onRun}
|
||||
|
||||
@@ -47,6 +47,7 @@ const Script = ({ item, collection }) => {
|
||||
value={requestScript || ''}
|
||||
theme={displayedTheme}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
fontSize={get(preferences, 'font.codeFontSize')}
|
||||
onEdit={onRequestScriptEdit}
|
||||
mode="javascript"
|
||||
onRun={onRun}
|
||||
@@ -60,6 +61,7 @@ const Script = ({ item, collection }) => {
|
||||
value={responseScript || ''}
|
||||
theme={displayedTheme}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
fontSize={get(preferences, 'font.codeFontSize')}
|
||||
onEdit={onResponseScriptEdit}
|
||||
mode="javascript"
|
||||
onRun={onRun}
|
||||
|
||||
@@ -34,6 +34,7 @@ const Tests = ({ item, collection }) => {
|
||||
value={tests || ''}
|
||||
theme={displayedTheme}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
fontSize={get(preferences, 'font.codeFontSize')}
|
||||
onEdit={onEdit}
|
||||
mode="javascript"
|
||||
onRun={onRun}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useTheme } from 'providers/Theme';
|
||||
import { addVar, updateVar, deleteVar } from 'providers/ReduxStore/slices/collections';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import Tooltip from 'components/Tooltip';
|
||||
import InfoTip from 'components/InfoTip';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import toast from 'react-hot-toast';
|
||||
import { variableNameRegex } from 'utils/common/regex';
|
||||
@@ -83,14 +83,14 @@ const VarsTable = ({ item, collection, vars, varType }) => {
|
||||
<td>
|
||||
<div className="flex items-center">
|
||||
<span>Value</span>
|
||||
<Tooltip text="You can write any valid JS Template Literal here" tooltipId="request-var" />
|
||||
<InfoTip text="You can write any valid JS Template Literal here" infotipId="request-var" />
|
||||
</div>
|
||||
</td>
|
||||
) : (
|
||||
<td>
|
||||
<div className="flex items-center">
|
||||
<span>Expr</span>
|
||||
<Tooltip text="You can write any valid JS expression here" tooltipId="response-var" />
|
||||
<InfoTip text="You can write any valid JS expression here" infotipId="response-var" />
|
||||
</div>
|
||||
</td>
|
||||
)}
|
||||
@@ -132,6 +132,7 @@ const VarsTable = ({ item, collection, vars, varType }) => {
|
||||
}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
item={item}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@@ -2,4 +2,4 @@ import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div``;
|
||||
|
||||
export default StyledWrapper;
|
||||
export default StyledWrapper;
|
||||
@@ -4,6 +4,7 @@ import { IconFiles, IconRun, IconEye, IconSettings } from '@tabler/icons';
|
||||
import EnvironmentSelector from 'components/Environments/EnvironmentSelector';
|
||||
import { addTab } from 'providers/ReduxStore/slices/tabs';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import ToolHint from 'components/ToolHint';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import JsSandboxMode from 'components/SecuritySettings/JsSandboxMode';
|
||||
|
||||
@@ -51,14 +52,20 @@ const CollectionToolBar = ({ collection }) => {
|
||||
<span className="mr-2">
|
||||
<JsSandboxMode collection={collection} />
|
||||
</span>
|
||||
<span className="mr-2">
|
||||
<IconRun className="cursor-pointer" size={20} strokeWidth={1.5} onClick={handleRun} />
|
||||
<span className="mr-3">
|
||||
<ToolHint text="Runner" toolhintId="RunnnerToolhintId" place='bottom'>
|
||||
<IconRun className="cursor-pointer" size={18} strokeWidth={1.5} onClick={handleRun} />
|
||||
</ToolHint>
|
||||
</span>
|
||||
<span className="mr-3">
|
||||
<IconEye className="cursor-pointer" size={18} strokeWidth={1.5} onClick={viewVariables} />
|
||||
<ToolHint text="Variables" toolhintId="VariablesToolhintId">
|
||||
<IconEye className="cursor-pointer" size={18} strokeWidth={1.5} onClick={viewVariables} />
|
||||
</ToolHint>
|
||||
</span>
|
||||
<span className="mr-3">
|
||||
<IconSettings className="cursor-pointer" size={18} strokeWidth={1.5} onClick={viewCollectionSettings} />
|
||||
<ToolHint text="Collection Settings" toolhintId="CollectionSettingsToolhintId">
|
||||
<IconSettings className="cursor-pointer" size={18} strokeWidth={1.5} onClick={viewCollectionSettings} />
|
||||
</ToolHint>
|
||||
</span>
|
||||
<EnvironmentSelector collection={collection} />
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { IconFilter, IconX } from '@tabler/icons';
|
||||
import React, { useMemo } from 'react';
|
||||
import { useRef } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { Tooltip as ReactTooltip } from 'react-tooltip';
|
||||
import { Tooltip as ReactInfotip } from 'react-tooltip';
|
||||
|
||||
const QueryResultFilter = ({ filter, onChange, mode }) => {
|
||||
const inputRef = useRef(null);
|
||||
@@ -19,7 +19,7 @@ const QueryResultFilter = ({ filter, onChange, mode }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const tooltipText = useMemo(() => {
|
||||
const infotipText = useMemo(() => {
|
||||
if (mode.includes('json')) {
|
||||
return 'Filter with JSONPath';
|
||||
}
|
||||
@@ -49,7 +49,7 @@ const QueryResultFilter = ({ filter, onChange, mode }) => {
|
||||
'response-filter absolute bottom-2 w-full justify-end right-0 flex flex-row items-center gap-2 py-4 px-2 pointer-events-none'
|
||||
}
|
||||
>
|
||||
{tooltipText && !isExpanded && <ReactTooltip anchorId={'request-filter-icon'} html={tooltipText} />}
|
||||
{infotipText && !isExpanded && <ReactInfotip anchorId={'request-filter-icon'} html={infotipText} />}
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
|
||||
@@ -81,6 +81,7 @@ const QueryResultPreview = ({
|
||||
<CodeEditor
|
||||
collection={collection}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
fontSize={get(preferences, 'font.codeFontSize')}
|
||||
theme={displayedTheme}
|
||||
onRun={onRun}
|
||||
value={formattedData}
|
||||
|
||||
@@ -17,8 +17,6 @@ const Timeline = ({ request, response }) => {
|
||||
});
|
||||
});
|
||||
|
||||
let requestData = safeStringifyJSON(request.data);
|
||||
|
||||
return (
|
||||
<StyledWrapper className="pb-4 w-full">
|
||||
<div>
|
||||
@@ -33,9 +31,10 @@ const Timeline = ({ request, response }) => {
|
||||
);
|
||||
})}
|
||||
|
||||
{requestData ? (
|
||||
{request.data ? (
|
||||
<pre className="line request">
|
||||
<span className="arrow">{'>'}</span> data {requestData}
|
||||
<span className="arrow">{'>'}</span> data{' '}
|
||||
<pre className="text-sm flex flex-wrap whitespace-break-spaces">{request.data}</pre>
|
||||
</pre>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@ import * as Yup from 'yup';
|
||||
import { browseDirectory } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { cloneCollection } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import toast from 'react-hot-toast';
|
||||
import Tooltip from 'components/Tooltip';
|
||||
import InfoTip from 'components/InfoTip';
|
||||
import Modal from 'components/Modal';
|
||||
|
||||
const CloneCollection = ({ onClose, collection }) => {
|
||||
@@ -126,9 +126,9 @@ const CloneCollection = ({ onClose, collection }) => {
|
||||
|
||||
<label htmlFor="collection-folder-name" className="flex items-center mt-3">
|
||||
<span className="font-semibold">Folder Name</span>
|
||||
<Tooltip
|
||||
<InfoTip
|
||||
text="This folder will be created under the selected location"
|
||||
tooltipId="collection-folder-name-tooltip"
|
||||
infotipId="collection-folder-name-infotip"
|
||||
/>
|
||||
</label>
|
||||
<input
|
||||
|
||||
@@ -53,6 +53,7 @@ const CodeView = ({ language, item }) => {
|
||||
collection={collection}
|
||||
value={snippet}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
fontSize={get(preferences, 'font.codeFontSize')}
|
||||
theme={displayedTheme}
|
||||
mode={lang}
|
||||
/>
|
||||
|
||||
@@ -65,7 +65,7 @@ const Wrapper = styled.div`
|
||||
div.dropdown-item.delete-item {
|
||||
color: ${(props) => props.theme.colors.danger};
|
||||
&:hover {
|
||||
background-color: ${(props) => props.theme.colors.bg.danger};
|
||||
background-color: ${(props) => props.theme.colors.bg.danger} !important;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import * as Yup from 'yup';
|
||||
import { browseDirectory } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { createCollection } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import toast from 'react-hot-toast';
|
||||
import Tooltip from 'components/Tooltip';
|
||||
import InfoTip from 'components/InfoTip';
|
||||
import Modal from 'components/Modal';
|
||||
|
||||
const CreateCollection = ({ onClose }) => {
|
||||
@@ -119,9 +119,9 @@ const CreateCollection = ({ onClose }) => {
|
||||
|
||||
<label htmlFor="collection-folder-name" className="flex items-center mt-3">
|
||||
<span className="font-semibold">Folder Name</span>
|
||||
<Tooltip
|
||||
<InfoTip
|
||||
text="This folder will be created under the selected location"
|
||||
tooltipId="collection-folder-name-tooltip"
|
||||
infotipId="collection-folder-name-infotip"
|
||||
/>
|
||||
</label>
|
||||
<input
|
||||
|
||||
@@ -4,6 +4,7 @@ import StyledWrapper from './StyledWrapper';
|
||||
import GitHubButton from 'react-github-btn';
|
||||
import Preferences from 'components/Preferences';
|
||||
import Cookies from 'components/Cookies';
|
||||
import ToolHint from 'components/ToolHint';
|
||||
import GoldenEdition from './GoldenEdition';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
@@ -95,28 +96,30 @@ const Sidebar = () => {
|
||||
|
||||
<div className="footer flex px-1 py-2 absolute bottom-0 left-0 right-0 items-center select-none">
|
||||
<div className="flex items-center ml-1 text-xs ">
|
||||
<a
|
||||
title="Preferences"
|
||||
className="mr-2 cursor-pointer hover:text-gray-700"
|
||||
onClick={() => dispatch(showPreferences(true))}
|
||||
>
|
||||
<IconSettings size={18} strokeWidth={1.5} />
|
||||
<a className="mr-2 cursor-pointer" onClick={() => dispatch(showPreferences(true))}>
|
||||
<ToolHint text="Preferences" toolhintId="Preferences" effect='float' place='top-start' offset={8}>
|
||||
<IconSettings size={18} strokeWidth={1.5} />
|
||||
</ToolHint>
|
||||
</a>
|
||||
<a
|
||||
title="Cookies"
|
||||
className="mr-2 cursor-pointer hover:text-gray-700"
|
||||
className="mr-2 cursor-pointer"
|
||||
onClick={() => setCookiesOpen(true)}
|
||||
>
|
||||
<IconCookie size={18} strokeWidth={1.5} />
|
||||
<ToolHint text="Cookies" toolhintId="Cookies" offset={8}>
|
||||
<IconCookie size={18} strokeWidth={1.5} />
|
||||
</ToolHint>
|
||||
</a>
|
||||
<a
|
||||
title="Golden Edition"
|
||||
className="mr-2 cursor-pointer hover:text-gray-700"
|
||||
className="mr-2 cursor-pointer"
|
||||
onClick={() => setGoldenEditonOpen(true)}
|
||||
>
|
||||
<IconHeart size={18} strokeWidth={1.5} />
|
||||
<ToolHint text="Golden Edition" toolhintId="Golden Edition" offset={8} >
|
||||
<IconHeart size={18} strokeWidth={1.5} />
|
||||
</ToolHint>
|
||||
</a>
|
||||
<a>
|
||||
<Notifications />
|
||||
</a>
|
||||
<Notifications />
|
||||
</div>
|
||||
<div className="pl-1" style={{ position: 'relative', top: '3px' }}>
|
||||
{/* This will get moved to home page */}
|
||||
@@ -126,10 +129,10 @@ const Sidebar = () => {
|
||||
data-show-count="true"
|
||||
aria-label="Star usebruno/bruno on GitHub"
|
||||
>
|
||||
Star
|
||||
Star
|
||||
</GitHubButton> */}
|
||||
</div>
|
||||
<div className="flex flex-grow items-center justify-end text-xs mr-2">v1.26.2</div>
|
||||
<div className="flex flex-grow items-center justify-end text-xs mr-2">v1.27.0</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -137,7 +140,7 @@ const Sidebar = () => {
|
||||
<div className="absolute drag-sidebar h-full" onMouseDown={handleDragbarMouseDown}>
|
||||
<div className="drag-request-border" />
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
</StyledWrapper >
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
background-color: ${(props) => props.theme.sidebar.badge};
|
||||
color: ${(props) => props.theme.text};
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
47
packages/bruno-app/src/components/ToolHint/index.js
Normal file
47
packages/bruno-app/src/components/ToolHint/index.js
Normal file
@@ -0,0 +1,47 @@
|
||||
import React from 'react';
|
||||
import { Tooltip as ReactToolHint } from 'react-tooltip';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
|
||||
const ToolHint = ({
|
||||
text,
|
||||
toolhintId,
|
||||
children,
|
||||
tooltipStyle = {},
|
||||
place = 'top',
|
||||
offset,
|
||||
theme = null
|
||||
}) => {
|
||||
const { theme: contextTheme } = useTheme();
|
||||
const appliedTheme = theme || contextTheme;
|
||||
|
||||
const toolhintBackgroundColor = appliedTheme?.sidebar.badge.bg || 'black';
|
||||
const toolhintTextColor = appliedTheme?.text || 'white';
|
||||
|
||||
const combinedToolhintStyle = {
|
||||
...tooltipStyle,
|
||||
fontSize: '0.75rem',
|
||||
padding: '0.25rem 0.5rem',
|
||||
backgroundColor: toolhintBackgroundColor,
|
||||
color: toolhintTextColor
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<span id={toolhintId}>{children}</span>
|
||||
<StyledWrapper theme={appliedTheme}>
|
||||
<ReactToolHint
|
||||
anchorId={toolhintId}
|
||||
html={text}
|
||||
className="toolhint"
|
||||
offset={offset}
|
||||
place={place}
|
||||
noArrow={true}
|
||||
style={combinedToolhintStyle}
|
||||
/>
|
||||
</StyledWrapper>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ToolHint;
|
||||
@@ -60,7 +60,7 @@ const trackStart = () => {
|
||||
event: 'start',
|
||||
properties: {
|
||||
os: platformLib.os.family,
|
||||
version: '1.26.2'
|
||||
version: '1.27.0'
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -9,7 +9,7 @@ import NetworkError from 'components/ResponsePane/NetworkError';
|
||||
import NewRequest from 'components/Sidebar/NewRequest';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { findCollectionByUid, findItemInCollection } from 'utils/collections';
|
||||
import { closeTabs } from 'providers/ReduxStore/slices/tabs';
|
||||
import { closeTabs, switchTab } from 'providers/ReduxStore/slices/tabs';
|
||||
|
||||
export const HotkeysContext = React.createContext();
|
||||
|
||||
@@ -154,7 +154,41 @@ export const HotkeysProvider = (props) => {
|
||||
};
|
||||
}, [activeTabUid]);
|
||||
|
||||
// close all tabs
|
||||
// Switch to the previous tab
|
||||
useEffect(() => {
|
||||
Mousetrap.bind(['command+pageup', 'ctrl+pageup'], (e) => {
|
||||
dispatch(
|
||||
switchTab({
|
||||
direction: 'pageup'
|
||||
})
|
||||
);
|
||||
|
||||
return false; // this stops the event bubbling
|
||||
});
|
||||
|
||||
return () => {
|
||||
Mousetrap.unbind(['command+pageup', 'ctrl+pageup']);
|
||||
};
|
||||
}, [dispatch]);
|
||||
|
||||
// Switch to the next tab
|
||||
useEffect(() => {
|
||||
Mousetrap.bind(['command+pagedown', 'ctrl+pagedown'], (e) => {
|
||||
dispatch(
|
||||
switchTab({
|
||||
direction: 'pagedown'
|
||||
})
|
||||
);
|
||||
|
||||
return false; // this stops the event bubbling
|
||||
});
|
||||
|
||||
return () => {
|
||||
Mousetrap.unbind(['command+pagedown', 'ctrl+pagedown']);
|
||||
};
|
||||
}, [dispatch]);
|
||||
|
||||
// Close all tabs
|
||||
useEffect(() => {
|
||||
Mousetrap.bind(['command+shift+w', 'ctrl+shift+w'], (e) => {
|
||||
const activeTab = find(tabs, (t) => t.uid === activeTabUid);
|
||||
|
||||
@@ -1302,6 +1302,71 @@ export const collectionsSlice = createSlice({
|
||||
set(collection, 'root.request.headers', headers);
|
||||
}
|
||||
},
|
||||
addCollectionVar: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
const type = action.payload.type;
|
||||
if (collection) {
|
||||
if (type === 'request') {
|
||||
const vars = get(collection, 'root.request.vars.req', []);
|
||||
vars.push({
|
||||
uid: uuid(),
|
||||
name: '',
|
||||
value: '',
|
||||
enabled: true
|
||||
});
|
||||
set(collection, 'root.request.vars.req', vars);
|
||||
} else if (type === 'response') {
|
||||
const vars = get(collection, 'root.request.vars.res', []);
|
||||
vars.push({
|
||||
uid: uuid(),
|
||||
name: '',
|
||||
value: '',
|
||||
enabled: true
|
||||
});
|
||||
set(collection, 'root.request.vars.res', vars);
|
||||
}
|
||||
}
|
||||
},
|
||||
updateCollectionVar: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
const type = action.payload.type;
|
||||
if (type === 'request') {
|
||||
let vars = get(collection, 'root.request.vars.req', []);
|
||||
const _var = find(vars, (h) => h.uid === action.payload.var.uid);
|
||||
if (_var) {
|
||||
_var.name = action.payload.var.name;
|
||||
_var.value = action.payload.var.value;
|
||||
_var.description = action.payload.var.description;
|
||||
_var.enabled = action.payload.var.enabled;
|
||||
}
|
||||
set(collection, 'root.request.vars.req', vars);
|
||||
} else if (type === 'response') {
|
||||
let vars = get(collection, 'root.request.vars.res', []);
|
||||
const _var = find(vars, (h) => h.uid === action.payload.var.uid);
|
||||
if (_var) {
|
||||
_var.name = action.payload.var.name;
|
||||
_var.value = action.payload.var.value;
|
||||
_var.description = action.payload.var.description;
|
||||
_var.enabled = action.payload.var.enabled;
|
||||
}
|
||||
set(collection, 'root.request.vars.res', vars);
|
||||
}
|
||||
},
|
||||
deleteCollectionVar: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
const type = action.payload.type;
|
||||
if (collection) {
|
||||
if (type === 'request') {
|
||||
let vars = get(collection, 'root.request.vars.req', []);
|
||||
vars = filter(vars, (h) => h.uid !== action.payload.varUid);
|
||||
set(collection, 'root.request.vars.req', vars);
|
||||
} else if (type === 'response') {
|
||||
let vars = get(collection, 'root.request.vars.res', []);
|
||||
vars = filter(vars, (h) => h.uid !== action.payload.varUid);
|
||||
set(collection, 'root.request.vars.res', vars);
|
||||
}
|
||||
}
|
||||
},
|
||||
collectionAddFileEvent: (state, action) => {
|
||||
const file = action.payload.file;
|
||||
const isCollectionRoot = file.meta.collectionRoot ? true : false;
|
||||
@@ -1694,6 +1759,9 @@ export const {
|
||||
addCollectionHeader,
|
||||
updateCollectionHeader,
|
||||
deleteCollectionHeader,
|
||||
addCollectionVar,
|
||||
updateCollectionVar,
|
||||
deleteCollectionVar,
|
||||
updateCollectionAuthMode,
|
||||
updateCollectionAuth,
|
||||
updateCollectionRequestScript,
|
||||
|
||||
@@ -48,6 +48,26 @@ export const tabsSlice = createSlice({
|
||||
focusTab: (state, action) => {
|
||||
state.activeTabUid = action.payload.uid;
|
||||
},
|
||||
switchTab: (state, action) => {
|
||||
if (!state.tabs || !state.tabs.length) {
|
||||
state.activeTabUid = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const direction = action.payload.direction;
|
||||
|
||||
const activeTabIndex = state.tabs.findIndex((t) => t.uid === state.activeTabUid);
|
||||
|
||||
let toBeActivatedTabIndex = 0;
|
||||
|
||||
if (direction == 'pageup') {
|
||||
toBeActivatedTabIndex = (activeTabIndex - 1 + state.tabs.length) % state.tabs.length;
|
||||
} else if (direction == 'pagedown') {
|
||||
toBeActivatedTabIndex = (activeTabIndex + 1) % state.tabs.length;
|
||||
}
|
||||
|
||||
state.activeTabUid = state.tabs[toBeActivatedTabIndex].uid;
|
||||
},
|
||||
updateRequestPaneTabWidth: (state, action) => {
|
||||
const tab = find(state.tabs, (t) => t.uid === action.payload.uid);
|
||||
|
||||
@@ -111,6 +131,7 @@ export const tabsSlice = createSlice({
|
||||
export const {
|
||||
addTab,
|
||||
focusTab,
|
||||
switchTab,
|
||||
updateRequestPaneTabWidth,
|
||||
updateRequestPaneTab,
|
||||
updateResponsePaneTab,
|
||||
|
||||
@@ -787,24 +787,25 @@ export const getTotalRequestCountInCollection = (collection) => {
|
||||
};
|
||||
|
||||
export const getAllVariables = (collection, item) => {
|
||||
const environmentVariables = getEnvironmentVariables(collection);
|
||||
let requestVariables = {};
|
||||
if (item?.request) {
|
||||
const requestTreePath = getTreePathFromCollectionToItem(collection, item);
|
||||
requestVariables = mergeFolderLevelVars(item?.request, requestTreePath);
|
||||
}
|
||||
const envVariables = getEnvironmentVariables(collection);
|
||||
const requestTreePath = getTreePathFromCollectionToItem(collection, item);
|
||||
let { collectionVariables, folderVariables, requestVariables } = mergeVars(collection, requestTreePath);
|
||||
const pathParams = getPathParams(item);
|
||||
|
||||
const { processEnvVariables = {}, runtimeVariables = {} } = collection;
|
||||
|
||||
return {
|
||||
...environmentVariables,
|
||||
...collectionVariables,
|
||||
...envVariables,
|
||||
...folderVariables,
|
||||
...requestVariables,
|
||||
...collection.runtimeVariables,
|
||||
...runtimeVariables,
|
||||
pathParams: {
|
||||
...pathParams
|
||||
},
|
||||
process: {
|
||||
env: {
|
||||
...collection.processEnvVariables
|
||||
...processEnvVariables
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -831,14 +832,22 @@ const getTreePathFromCollectionToItem = (collection, _item) => {
|
||||
return path;
|
||||
};
|
||||
|
||||
const mergeFolderLevelVars = (request, requestTreePath = []) => {
|
||||
const mergeVars = (collection, requestTreePath = []) => {
|
||||
let collectionVariables = {};
|
||||
let folderVariables = {};
|
||||
let requestVariables = {};
|
||||
let collectionRequestVars = get(collection, 'root.request.vars.req', []);
|
||||
collectionRequestVars.forEach((_var) => {
|
||||
if (_var.enabled) {
|
||||
collectionVariables[_var.name] = _var.value;
|
||||
}
|
||||
});
|
||||
for (let i of requestTreePath) {
|
||||
if (i.type === 'folder') {
|
||||
let vars = get(i, 'root.request.vars.req', []);
|
||||
vars.forEach((_var) => {
|
||||
if (_var.enabled) {
|
||||
requestVariables[_var.name] = _var.value;
|
||||
folderVariables[_var.name] = _var.value;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
@@ -850,6 +859,9 @@ const mergeFolderLevelVars = (request, requestTreePath = []) => {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return requestVariables;
|
||||
return {
|
||||
collectionVariables,
|
||||
folderVariables,
|
||||
requestVariables
|
||||
};
|
||||
};
|
||||
|
||||
@@ -149,7 +149,9 @@ export const relativeDate = (dateString) => {
|
||||
};
|
||||
|
||||
export const humanizeDate = (dateString) => {
|
||||
const date = new Date(dateString);
|
||||
// See this discussion for why .split is necessary
|
||||
// https://stackoverflow.com/questions/7556591/is-the-javascript-date-object-always-one-day-off
|
||||
const date = new Date(dateString.split('-'));
|
||||
return date.toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const { describe, it, expect } = require('@jest/globals');
|
||||
|
||||
import { normalizeFileName, startsWith } from './index';
|
||||
import { normalizeFileName, startsWith, humanizeDate, relativeDate } from './index';
|
||||
|
||||
describe('common utils', () => {
|
||||
describe('normalizeFileName', () => {
|
||||
@@ -49,4 +49,50 @@ describe('common utils', () => {
|
||||
expect(startsWith('foo', 'foo')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('humanizeDate', () => {
|
||||
it('should return a date string in the en-US locale', () => {
|
||||
expect(humanizeDate('2024-03-17')).toBe('March 17, 2024');
|
||||
});
|
||||
|
||||
it('should return invalid date if the date is invalid', () => {
|
||||
expect(humanizeDate('9999-99-99')).toBe('Invalid Date');
|
||||
});
|
||||
});
|
||||
|
||||
describe('relativeDate', () => {
|
||||
it('should return few seconds ago', () => {
|
||||
expect(relativeDate(new Date())).toBe('Few seconds ago');
|
||||
});
|
||||
|
||||
it('should return minutes ago', () => {
|
||||
let date = new Date();
|
||||
date.setMinutes(date.getMinutes() - 30);
|
||||
expect(relativeDate(date)).toBe('30 minutes ago');
|
||||
});
|
||||
|
||||
it('should return hours ago', () => {
|
||||
let date = new Date();
|
||||
date.setHours(date.getHours() - 10);
|
||||
expect(relativeDate(date)).toBe('10 hours ago');
|
||||
});
|
||||
|
||||
it('should return days ago', () => {
|
||||
let date = new Date();
|
||||
date.setDate(date.getDate() - 5);
|
||||
expect(relativeDate(date)).toBe('5 days ago');
|
||||
});
|
||||
|
||||
it('should return weeks ago', () => {
|
||||
let date = new Date();
|
||||
date.setDate(date.getDate() - 8);
|
||||
expect(relativeDate(date)).toBe('1 week ago');
|
||||
});
|
||||
|
||||
it('should return months ago', () => {
|
||||
let date = new Date();
|
||||
date.setDate(date.getDate() - 60);
|
||||
expect(relativeDate(date)).toBe('2 months ago');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,16 +18,17 @@ const hasLength = (str) => {
|
||||
};
|
||||
|
||||
export const parseQueryParams = (query) => {
|
||||
if (!query || !query.length) {
|
||||
try {
|
||||
if (!query || !query.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return Array.from(new URLSearchParams(query.split('#')[0]).entries())
|
||||
.map(([name, value]) => ({ name, value }));
|
||||
} catch (error) {
|
||||
console.error('Error parsing query params:', error);
|
||||
return [];
|
||||
}
|
||||
|
||||
let params = query.split('&').map((param) => {
|
||||
let [name, value = ''] = param.split('=');
|
||||
return { name, value };
|
||||
});
|
||||
|
||||
return filter(params, (p) => hasLength(p.name));
|
||||
};
|
||||
|
||||
export const parsePathParams = (url) => {
|
||||
|
||||
@@ -49,6 +49,23 @@ describe('Url Utils - parseQueryParams', () => {
|
||||
{ name: 'b', value: '2' }
|
||||
]);
|
||||
});
|
||||
|
||||
it('should parse query with "=" character - case 9', () => {
|
||||
const params = parseQueryParams('a=1&b={color=red,size=large}&c=3');
|
||||
expect(params).toEqual([
|
||||
{ name: 'a', value: '1' },
|
||||
{ name: 'b', value: '{color=red,size=large}' },
|
||||
{ name: 'c', value: '3' }
|
||||
]);
|
||||
});
|
||||
|
||||
it('should parse query with fragment - case 10', () => {
|
||||
const params = parseQueryParams('a=1&b=2#I-AM-FRAGMENT');
|
||||
expect(params).toEqual([
|
||||
{ name: 'a', value: '1' },
|
||||
{ name: 'b', value: '2' }
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Url Utils - parsePathParams', () => {
|
||||
|
||||
@@ -60,20 +60,6 @@ const runSingleRequest = async function (
|
||||
request.data = form;
|
||||
}
|
||||
|
||||
// run pre-request vars
|
||||
const preRequestVars = get(bruJson, 'request.vars.req');
|
||||
if (preRequestVars?.length) {
|
||||
const varsRuntime = new VarsRuntime({ runtime: scriptingConfig?.runtime });
|
||||
varsRuntime.runPreRequestVars(
|
||||
preRequestVars,
|
||||
request,
|
||||
envVariables,
|
||||
runtimeVariables,
|
||||
collectionPath,
|
||||
processEnvVars
|
||||
);
|
||||
}
|
||||
|
||||
// run pre request script
|
||||
const requestScriptFile = compact([
|
||||
get(collectionRoot, 'request.script.req'),
|
||||
@@ -276,7 +262,7 @@ const runSingleRequest = async function (
|
||||
|
||||
console.log(
|
||||
chalk.green(stripExtension(filename)) +
|
||||
chalk.dim(` (${response.status} ${response.statusText}) - ${responseTime} ms`)
|
||||
chalk.dim(` (${response.status} ${response.statusText}) - ${responseTime} ms`)
|
||||
);
|
||||
|
||||
// run post-response vars
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "v1.26.2",
|
||||
"version": "v1.27.0",
|
||||
"name": "bruno",
|
||||
"description": "Opensource API Client for Exploring and Testing APIs",
|
||||
"homepage": "https://www.usebruno.com",
|
||||
|
||||
@@ -3,7 +3,7 @@ const path = require('path');
|
||||
const isDev = require('electron-is-dev');
|
||||
|
||||
if (isDev) {
|
||||
if (!fs.existsSync('./src/sandbox/bundle-browser-rollup.js')) {
|
||||
if(!fs.existsSync(path.join(__dirname, '../../bruno-js/src/sandbox/bundle-browser-rollup.js'))) {
|
||||
console.log('JS Sandbox libraries have not been bundled yet');
|
||||
console.log('Please run the below command \nnpm run sandbox:bundle-libraries --workspace=packages/bruno-js');
|
||||
throw new Error('JS Sandbox libraries have not been bundled yet');
|
||||
|
||||
@@ -272,7 +272,7 @@ const configureRequest = async (
|
||||
return axiosInstance;
|
||||
};
|
||||
|
||||
const parseDataFromResponse = (response) => {
|
||||
const parseDataFromResponse = (response, disableParsingResponseJson = false) => {
|
||||
// Parse the charset from content type: https://stackoverflow.com/a/33192813
|
||||
const charsetMatch = /charset=([^()<>@,;:"/[\]?.=\s]*)/i.exec(response.headers['content-type'] || '');
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/exec#using_exec_with_regexp_literals
|
||||
@@ -290,8 +290,10 @@ const parseDataFromResponse = (response) => {
|
||||
// Filter out ZWNBSP character
|
||||
// https://gist.github.com/antic183/619f42b559b78028d1fe9e7ae8a1352d
|
||||
data = data.replace(/^\uFEFF/, '');
|
||||
data = JSON.parse(data);
|
||||
} catch {}
|
||||
if (!disableParsingResponseJson) {
|
||||
data = JSON.parse(data);
|
||||
}
|
||||
} catch { }
|
||||
|
||||
return { data, dataBuffer };
|
||||
};
|
||||
@@ -317,20 +319,6 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
processEnvVars,
|
||||
scriptingConfig
|
||||
) => {
|
||||
// run pre-request vars
|
||||
const preRequestVars = get(request, 'vars.req', []);
|
||||
if (preRequestVars?.length) {
|
||||
const varsRuntime = new VarsRuntime({ runtime: scriptingConfig?.runtime });
|
||||
varsRuntime.runPreRequestVars(
|
||||
preRequestVars,
|
||||
request,
|
||||
envVars,
|
||||
runtimeVariables,
|
||||
collectionPath,
|
||||
processEnvVars
|
||||
);
|
||||
}
|
||||
|
||||
// run pre-request script
|
||||
let scriptResult;
|
||||
const requestScript = compact([get(collectionRoot, 'request.script.req'), get(request, 'script.req')]).join(os.EOL);
|
||||
@@ -540,7 +528,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
|
||||
// Continue with the rest of the request lifecycle - post response vars, script, assertions, tests
|
||||
|
||||
const { data, dataBuffer } = parseDataFromResponse(response);
|
||||
const { data, dataBuffer } = parseDataFromResponse(response, request.__brunoDisableParsingResponseJson);
|
||||
response.data = data;
|
||||
|
||||
response.responseTime = responseTime;
|
||||
@@ -701,7 +689,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
}
|
||||
}
|
||||
|
||||
const { data } = parseDataFromResponse(response);
|
||||
const { data } = parseDataFromResponse(response, request.__brunoDisableParsingResponseJson);
|
||||
response.data = data;
|
||||
|
||||
await runPostResponse(
|
||||
@@ -969,7 +957,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
response = await axiosInstance(request);
|
||||
timeEnd = Date.now();
|
||||
|
||||
const { data, dataBuffer } = parseDataFromResponse(response);
|
||||
const { data, dataBuffer } = parseDataFromResponse(response, request.__brunoDisableParsingResponseJson);
|
||||
response.data = data;
|
||||
response.responseTime = response.headers.get('request-duration');
|
||||
|
||||
@@ -1158,7 +1146,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
try {
|
||||
const disposition = contentDispositionParser.parse(contentDisposition);
|
||||
return disposition && disposition.parameters['filename'];
|
||||
} catch (error) {}
|
||||
} catch (error) { }
|
||||
};
|
||||
|
||||
const getFileNameFromUrlPath = () => {
|
||||
|
||||
@@ -12,15 +12,17 @@ const getContentType = (headers = {}) => {
|
||||
return contentType;
|
||||
};
|
||||
|
||||
const interpolateVars = (request, envVars = {}, runtimeVariables = {}, processEnvVars = {}) => {
|
||||
const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, processEnvVars = {}) => {
|
||||
const collectionVariables = request?.collectionVariables || {};
|
||||
const folderVariables = request?.folderVariables || {};
|
||||
const requestVariables = request?.requestVariables || {};
|
||||
// we clone envVars because we don't want to modify the original object
|
||||
envVars = cloneDeep(envVars);
|
||||
envVariables = cloneDeep(envVariables);
|
||||
|
||||
// envVars can inturn have values as {{process.env.VAR_NAME}}
|
||||
// so we need to interpolate envVars first with processEnvVars
|
||||
forOwn(envVars, (value, key) => {
|
||||
envVars[key] = interpolate(value, {
|
||||
forOwn(envVariables, (value, key) => {
|
||||
envVariables[key] = interpolate(value, {
|
||||
process: {
|
||||
env: {
|
||||
...processEnvVars
|
||||
@@ -36,7 +38,9 @@ const interpolateVars = (request, envVars = {}, runtimeVariables = {}, processEn
|
||||
|
||||
// runtimeVariables take precedence over envVars
|
||||
const combinedVars = {
|
||||
...envVars,
|
||||
...collectionVariables,
|
||||
...envVariables,
|
||||
...folderVariables,
|
||||
...requestVariables,
|
||||
...runtimeVariables,
|
||||
process: {
|
||||
@@ -59,14 +63,6 @@ const interpolateVars = (request, envVars = {}, runtimeVariables = {}, processEn
|
||||
const contentType = getContentType(request.headers);
|
||||
|
||||
if (contentType.includes('json')) {
|
||||
if (typeof request.data === 'object') {
|
||||
try {
|
||||
let parsed = JSON.stringify(request.data);
|
||||
parsed = _interpolate(parsed);
|
||||
request.data = JSON.parse(parsed);
|
||||
} catch (err) {}
|
||||
}
|
||||
|
||||
if (typeof request.data === 'string') {
|
||||
if (request.data.length) {
|
||||
request.data = _interpolate(request.data);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
const os = require('os');
|
||||
const { get, each, filter, extend, compact } = require('lodash');
|
||||
const decomment = require('decomment');
|
||||
var JSONbig = require('json-bigint');
|
||||
const FormData = require('form-data');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
@@ -45,73 +44,75 @@ const mergeFolderLevelHeaders = (request, requestTreePath) => {
|
||||
request.headers = Array.from(requestHeadersMap, ([name, value]) => ({ name, value, enabled: true }));
|
||||
};
|
||||
|
||||
const mergeFolderLevelVars = (request, requestTreePath) => {
|
||||
let folderReqVars = new Map();
|
||||
const mergeVars = (collection, request, requestTreePath) => {
|
||||
let reqVars = new Map();
|
||||
let collectionRequestVars = get(collection, 'root.request.vars.req', []);
|
||||
let collectionVariables = {};
|
||||
collectionRequestVars.forEach((_var) => {
|
||||
if (_var.enabled) {
|
||||
reqVars.set(_var.name, _var.value);
|
||||
collectionVariables[_var.name] = _var.value;
|
||||
}
|
||||
});
|
||||
let folderVariables = {};
|
||||
let requestVariables = {};
|
||||
for (let i of requestTreePath) {
|
||||
if (i.type === 'folder') {
|
||||
let vars = get(i, 'root.request.vars.req', []);
|
||||
vars.forEach((_var) => {
|
||||
if (_var.enabled) {
|
||||
folderReqVars.set(_var.name, _var.value);
|
||||
reqVars.set(_var.name, _var.value);
|
||||
folderVariables[_var.name] = _var.value;
|
||||
}
|
||||
});
|
||||
} else if (i.uid === request.uid) {
|
||||
} else {
|
||||
const vars = i?.draft ? get(i, 'draft.request.vars.req', []) : get(i, 'request.vars.req', []);
|
||||
vars.forEach((_var) => {
|
||||
if (_var.enabled) {
|
||||
folderReqVars.set(_var.name, _var.value);
|
||||
reqVars.set(_var.name, _var.value);
|
||||
requestVariables[_var.name] = _var.value;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
let mergedFolderReqVars = Array.from(folderReqVars, ([name, value]) => ({ name, value, enabled: true }));
|
||||
let requestReqVars = request?.vars?.req || [];
|
||||
let requestReqVarsMap = new Map();
|
||||
for (let _var of requestReqVars) {
|
||||
if (_var.enabled) {
|
||||
requestReqVarsMap.set(_var.name, _var.value);
|
||||
}
|
||||
}
|
||||
mergedFolderReqVars.forEach((_var) => {
|
||||
requestReqVarsMap.set(_var.name, _var.value);
|
||||
});
|
||||
request.vars.req = Array.from(requestReqVarsMap, ([name, value]) => ({
|
||||
|
||||
request.collectionVariables = collectionVariables;
|
||||
request.folderVariables = folderVariables;
|
||||
request.requestVariables = requestVariables;
|
||||
|
||||
request.vars.req = Array.from(reqVars, ([name, value]) => ({
|
||||
name,
|
||||
value,
|
||||
enabled: true,
|
||||
type: 'request'
|
||||
}));
|
||||
|
||||
let folderResVars = new Map();
|
||||
let resVars = new Map();
|
||||
let collectionResponseVars = get(collection, 'root.request.vars.res', []);
|
||||
collectionResponseVars.forEach((_var) => {
|
||||
if (_var.enabled) {
|
||||
resVars.set(_var.name, _var.value);
|
||||
}
|
||||
});
|
||||
for (let i of requestTreePath) {
|
||||
if (i.type === 'folder') {
|
||||
let vars = get(i, 'root.request.vars.res', []);
|
||||
vars.forEach((_var) => {
|
||||
if (_var.enabled) {
|
||||
folderResVars.set(_var.name, _var.value);
|
||||
resVars.set(_var.name, _var.value);
|
||||
}
|
||||
});
|
||||
} else if (i.uid === request.uid) {
|
||||
} else {
|
||||
const vars = i?.draft ? get(i, 'draft.request.vars.res', []) : get(i, 'request.vars.res', []);
|
||||
vars.forEach((_var) => {
|
||||
if (_var.enabled) {
|
||||
folderResVars.set(_var.name, _var.value);
|
||||
resVars.set(_var.name, _var.value);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
let mergedFolderResVars = Array.from(folderResVars, ([name, value]) => ({ name, value, enabled: true }));
|
||||
let requestResVars = request?.vars?.res || [];
|
||||
let requestResVarsMap = new Map();
|
||||
for (let _var of requestResVars) {
|
||||
if (_var.enabled) {
|
||||
requestResVarsMap.set(_var.name, _var.value);
|
||||
}
|
||||
}
|
||||
mergedFolderResVars.forEach((_var) => {
|
||||
requestResVarsMap.set(_var.name, _var.value);
|
||||
});
|
||||
request.vars.res = Array.from(requestResVarsMap, ([name, value]) => ({
|
||||
|
||||
request.vars.res = Array.from(resVars, ([name, value]) => ({
|
||||
name,
|
||||
value,
|
||||
enabled: true,
|
||||
@@ -315,7 +316,7 @@ const prepareRequest = (item, collection) => {
|
||||
if (requestTreePath && requestTreePath.length > 0) {
|
||||
mergeFolderLevelHeaders(request, requestTreePath);
|
||||
mergeFolderLevelScripts(request, requestTreePath, scriptFlow);
|
||||
mergeFolderLevelVars(request, requestTreePath);
|
||||
mergeVars(collection, request, requestTreePath);
|
||||
}
|
||||
|
||||
each(request.headers, (h) => {
|
||||
@@ -342,16 +343,10 @@ const prepareRequest = (item, collection) => {
|
||||
if (!contentTypeDefined) {
|
||||
axiosRequest.headers['content-type'] = 'application/json';
|
||||
}
|
||||
let jsonBody;
|
||||
try {
|
||||
jsonBody = decomment(request?.body?.json);
|
||||
axiosRequest.data = decomment(request?.body?.json);
|
||||
} catch (error) {
|
||||
jsonBody = request?.body?.json;
|
||||
}
|
||||
try {
|
||||
axiosRequest.data = JSONbig.parse(jsonBody);
|
||||
} catch (error) {
|
||||
axiosRequest.data = jsonBody;
|
||||
axiosRequest.data = request?.body?.json;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -408,6 +403,9 @@ const prepareRequest = (item, collection) => {
|
||||
}
|
||||
|
||||
axiosRequest.vars = request.vars;
|
||||
axiosRequest.collectionVariables = request.collectionVariables;
|
||||
axiosRequest.folderVariables = request.folderVariables;
|
||||
axiosRequest.requestVariables = request.requestVariables;
|
||||
axiosRequest.assertions = request.assertions;
|
||||
|
||||
return axiosRequest;
|
||||
|
||||
@@ -23,7 +23,8 @@ const defaultPreferences = {
|
||||
timeout: 0
|
||||
},
|
||||
font: {
|
||||
codeFont: 'default'
|
||||
codeFont: 'default',
|
||||
codeFontSize: 14
|
||||
},
|
||||
proxy: {
|
||||
enabled: false,
|
||||
@@ -54,7 +55,8 @@ const preferencesSchema = Yup.object().shape({
|
||||
timeout: Yup.number()
|
||||
}),
|
||||
font: Yup.object().shape({
|
||||
codeFont: Yup.string().nullable()
|
||||
codeFont: Yup.string().nullable(),
|
||||
codeFontSize: Yup.number().min(1).max(32).nullable()
|
||||
}),
|
||||
proxy: Yup.object({
|
||||
enabled: Yup.boolean(),
|
||||
|
||||
@@ -6,7 +6,7 @@ describe('prepare-request: prepareRequest', () => {
|
||||
describe('Decomments request body', () => {
|
||||
it('If request body is valid JSON', async () => {
|
||||
const body = { mode: 'json', json: '{\n"test": "{{someVar}}" // comment\n}' };
|
||||
const expected = { test: '{{someVar}}' };
|
||||
const expected = '{\n"test": "{{someVar}}" \n}';
|
||||
const result = prepareRequest({ request: { body } }, {});
|
||||
expect(result.data).toEqual(expected);
|
||||
});
|
||||
|
||||
@@ -4,10 +4,12 @@ const { interpolate } = require('@usebruno/common');
|
||||
const variableNameRegex = /^[\w-.]*$/;
|
||||
|
||||
class Bru {
|
||||
constructor(envVariables, runtimeVariables, processEnvVars, collectionPath, requestVariables) {
|
||||
constructor(envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables) {
|
||||
this.envVariables = envVariables || {};
|
||||
this.runtimeVariables = runtimeVariables || {};
|
||||
this.processEnvVars = cloneDeep(processEnvVars || {});
|
||||
this.collectionVariables = collectionVariables || {};
|
||||
this.folderVariables = folderVariables || {};
|
||||
this.requestVariables = requestVariables || {};
|
||||
this.collectionPath = collectionPath;
|
||||
}
|
||||
@@ -18,7 +20,9 @@ class Bru {
|
||||
}
|
||||
|
||||
const combinedVars = {
|
||||
...this.collectionVariables,
|
||||
...this.envVariables,
|
||||
...this.folderVariables,
|
||||
...this.requestVariables,
|
||||
...this.runtimeVariables,
|
||||
process: {
|
||||
@@ -71,7 +75,7 @@ class Bru {
|
||||
if (variableNameRegex.test(key) === false) {
|
||||
throw new Error(
|
||||
`Variable name: "${key}" contains invalid characters!` +
|
||||
' Names must only contain alpha-numeric characters, "-", "_", "."'
|
||||
' Names must only contain alpha-numeric characters, "-", "_", "."'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -82,7 +86,7 @@ class Bru {
|
||||
if (variableNameRegex.test(key) === false) {
|
||||
throw new Error(
|
||||
`Variable name: "${key}" contains invalid characters!` +
|
||||
' Names must only contain alpha-numeric characters, "-", "_", "."'
|
||||
' Names must only contain alpha-numeric characters, "-", "_", "."'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -93,6 +97,14 @@ class Bru {
|
||||
delete this.runtimeVariables[key];
|
||||
}
|
||||
|
||||
getCollectionVar(key) {
|
||||
return this._interpolate(this.collectionVariables[key]);
|
||||
}
|
||||
|
||||
getFolderVar(key) {
|
||||
return this._interpolate(this.folderVariables[key]);
|
||||
}
|
||||
|
||||
getRequestVar(key) {
|
||||
return this._interpolate(this.requestVariables[key]);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,34 @@
|
||||
class BrunoRequest {
|
||||
/**
|
||||
* The following properties are available as shorthand:
|
||||
* - req.url
|
||||
* - req.method
|
||||
* - req.headers
|
||||
* - req.timeout
|
||||
* - req.body
|
||||
*
|
||||
* Above shorthands are useful for accessing the request properties directly in the scripts
|
||||
* It must be noted that the user cannot set these properties directly.
|
||||
* They should use the respective setter methods to set these properties.
|
||||
*/
|
||||
constructor(req) {
|
||||
this.req = req;
|
||||
this.url = req.url;
|
||||
this.method = req.method;
|
||||
this.headers = req.headers;
|
||||
this.body = req.data;
|
||||
this.timeout = req.timeout;
|
||||
|
||||
/**
|
||||
* We automatically parse the JSON body if the content type is JSON
|
||||
* This is to make it easier for the user to access the body directly
|
||||
*
|
||||
* It must be noted that the request data is always a string and is what gets sent over the network
|
||||
* If the user wants to access the raw data, they can use getBody({raw: true}) method
|
||||
*/
|
||||
const isJson = this.hasJSONContentType(this.req.headers);
|
||||
if (isJson) {
|
||||
this.body = this.__safeParseJSON(req.data);
|
||||
}
|
||||
}
|
||||
|
||||
getUrl() {
|
||||
@@ -13,6 +36,7 @@ class BrunoRequest {
|
||||
}
|
||||
|
||||
setUrl(url) {
|
||||
this.url = url;
|
||||
this.req.url = url;
|
||||
}
|
||||
|
||||
@@ -37,6 +61,7 @@ class BrunoRequest {
|
||||
}
|
||||
|
||||
setMethod(method) {
|
||||
this.method = method;
|
||||
this.req.method = method;
|
||||
}
|
||||
|
||||
@@ -45,6 +70,7 @@ class BrunoRequest {
|
||||
}
|
||||
|
||||
setHeaders(headers) {
|
||||
this.headers = headers;
|
||||
this.req.headers = headers;
|
||||
}
|
||||
|
||||
@@ -53,15 +79,60 @@ class BrunoRequest {
|
||||
}
|
||||
|
||||
setHeader(name, value) {
|
||||
this.headers[name] = value;
|
||||
this.req.headers[name] = value;
|
||||
}
|
||||
|
||||
getBody() {
|
||||
hasJSONContentType(headers) {
|
||||
const contentType = headers?.['Content-Type'] || headers?.['content-type'] || '';
|
||||
return contentType.includes('json');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the body of the request
|
||||
*
|
||||
* We automatically parse and return the JSON body if the content type is JSON
|
||||
* If the user wants the raw body, they can pass the raw option as true
|
||||
*/
|
||||
getBody(options = {}) {
|
||||
if (options.raw) {
|
||||
return this.req.data;
|
||||
}
|
||||
|
||||
const isJson = this.hasJSONContentType(this.req.headers);
|
||||
if (isJson) {
|
||||
return this.__safeParseJSON(this.req.data);
|
||||
}
|
||||
|
||||
return this.req.data;
|
||||
}
|
||||
|
||||
setBody(data) {
|
||||
/**
|
||||
* If the content type is JSON and if the data is an object
|
||||
* - We set the body property as the object itself
|
||||
* - We set the request data as the stringified JSON as it is what gets sent over the network
|
||||
* Otherwise
|
||||
* - We set the request data as the data itself
|
||||
* - We set the body property as the data itself
|
||||
*
|
||||
* If the user wants to override this behavior, they can pass the raw option as true
|
||||
*/
|
||||
setBody(data, options = {}) {
|
||||
if (options.raw) {
|
||||
this.req.data = data;
|
||||
this.body = data;
|
||||
return;
|
||||
}
|
||||
|
||||
const isJson = this.hasJSONContentType(this.req.headers);
|
||||
if (isJson && this.__isObject(data)) {
|
||||
this.body = data;
|
||||
this.req.data = this.__safeStringifyJSON(data);
|
||||
return;
|
||||
}
|
||||
|
||||
this.req.data = data;
|
||||
this.body = data;
|
||||
}
|
||||
|
||||
setMaxRedirects(maxRedirects) {
|
||||
@@ -73,8 +144,34 @@ class BrunoRequest {
|
||||
}
|
||||
|
||||
setTimeout(timeout) {
|
||||
this.timeout = timeout;
|
||||
this.req.timeout = timeout;
|
||||
}
|
||||
|
||||
__safeParseJSON(str) {
|
||||
try {
|
||||
return JSON.parse(str);
|
||||
} catch (e) {
|
||||
return str;
|
||||
}
|
||||
}
|
||||
|
||||
__safeStringifyJSON(obj) {
|
||||
try {
|
||||
return JSON.stringify(obj);
|
||||
} catch (e) {
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
|
||||
__isObject(obj) {
|
||||
return obj !== null && typeof obj === 'object';
|
||||
}
|
||||
|
||||
|
||||
disableParsingResponseJson() {
|
||||
this.req.__brunoDisableParsingResponseJson = true;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = BrunoRequest;
|
||||
|
||||
@@ -2,14 +2,16 @@ const { interpolate } = require('@usebruno/common');
|
||||
|
||||
const interpolateString = (
|
||||
str,
|
||||
{ envVariables = {}, runtimeVariables = {}, processEnvVars = {}, requestVariables = {} }
|
||||
{ envVariables = {}, runtimeVariables = {}, processEnvVars = {}, collectionVariables = {}, folderVariables = {}, requestVariables = {} }
|
||||
) => {
|
||||
if (!str || !str.length || typeof str !== 'string') {
|
||||
return str;
|
||||
}
|
||||
|
||||
const combinedVars = {
|
||||
...collectionVariables,
|
||||
...envVariables,
|
||||
...folderVariables,
|
||||
...requestVariables,
|
||||
...runtimeVariables,
|
||||
process: {
|
||||
|
||||
@@ -192,6 +192,8 @@ const evaluateRhsOperand = (rhsOperand, operator, context, runtime) => {
|
||||
}
|
||||
|
||||
const interpolationContext = {
|
||||
collectionVariables: context.bru.collectionVariables,
|
||||
folderVariables: context.bru.folderVariables,
|
||||
requestVariables: context.bru.requestVariables,
|
||||
runtimeVariables: context.bru.runtimeVariables,
|
||||
envVariables: context.bru.envVariables,
|
||||
@@ -238,13 +240,23 @@ class AssertRuntime {
|
||||
}
|
||||
|
||||
runAssertions(assertions, request, response, envVariables, runtimeVariables, processEnvVars) {
|
||||
const collectionVariables = request?.collectionVariables || {};
|
||||
const folderVariables = request?.folderVariables || {};
|
||||
const requestVariables = request?.requestVariables || {};
|
||||
const enabledAssertions = _.filter(assertions, (a) => a.enabled);
|
||||
if (!enabledAssertions.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const bru = new Bru(envVariables, runtimeVariables, processEnvVars, undefined, requestVariables);
|
||||
const bru = new Bru(
|
||||
envVariables,
|
||||
runtimeVariables,
|
||||
processEnvVars,
|
||||
undefined,
|
||||
collectionVariables,
|
||||
folderVariables,
|
||||
requestVariables
|
||||
);
|
||||
const req = new BrunoRequest(request);
|
||||
const res = createResponseParser(response);
|
||||
|
||||
@@ -255,7 +267,9 @@ class AssertRuntime {
|
||||
};
|
||||
|
||||
const context = {
|
||||
...collectionVariables,
|
||||
...envVariables,
|
||||
...folderVariables,
|
||||
...requestVariables,
|
||||
...runtimeVariables,
|
||||
...processEnvVars,
|
||||
|
||||
@@ -47,8 +47,10 @@ class ScriptRuntime {
|
||||
processEnvVars,
|
||||
scriptingConfig
|
||||
) {
|
||||
const collectionVariables = request?.collectionVariables || {};
|
||||
const folderVariables = request?.folderVariables || {};
|
||||
const requestVariables = request?.requestVariables || {};
|
||||
const bru = new Bru(envVariables, runtimeVariables, processEnvVars, collectionPath, requestVariables);
|
||||
const bru = new Bru(envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables);
|
||||
const req = new BrunoRequest(request);
|
||||
const allowScriptFilesystemAccess = get(scriptingConfig, 'filesystemAccess.allow', false);
|
||||
const moduleWhitelist = get(scriptingConfig, 'moduleWhitelist', []);
|
||||
@@ -162,8 +164,10 @@ class ScriptRuntime {
|
||||
processEnvVars,
|
||||
scriptingConfig
|
||||
) {
|
||||
const collectionVariables = request?.collectionVariables || {};
|
||||
const folderVariables = request?.folderVariables || {};
|
||||
const requestVariables = request?.requestVariables || {};
|
||||
const bru = new Bru(envVariables, runtimeVariables, processEnvVars, collectionPath, requestVariables);
|
||||
const bru = new Bru(envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables);
|
||||
const req = new BrunoRequest(request);
|
||||
const res = new BrunoResponse(response);
|
||||
const allowScriptFilesystemAccess = get(scriptingConfig, 'filesystemAccess.allow', false);
|
||||
|
||||
@@ -48,8 +48,10 @@ class TestRuntime {
|
||||
processEnvVars,
|
||||
scriptingConfig
|
||||
) {
|
||||
const collectionVariables = request?.collectionVariables || {};
|
||||
const folderVariables = request?.folderVariables || {};
|
||||
const requestVariables = request?.requestVariables || {};
|
||||
const bru = new Bru(envVariables, runtimeVariables, processEnvVars, collectionPath, requestVariables);
|
||||
const bru = new Bru(envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables);
|
||||
const req = new BrunoRequest(request);
|
||||
const res = new BrunoResponse(response);
|
||||
const allowScriptFilesystemAccess = get(scriptingConfig, 'filesystemAccess.allow', false);
|
||||
|
||||
@@ -1,22 +1,10 @@
|
||||
const _ = require('lodash');
|
||||
const Bru = require('../bru');
|
||||
const BrunoRequest = require('../bruno-request');
|
||||
const { evaluateJsTemplateLiteral, evaluateJsExpression, createResponseParser } = require('../utils');
|
||||
const { evaluateJsExpression, createResponseParser } = require('../utils');
|
||||
|
||||
const { executeQuickJsVm } = require('../sandbox/quickjs');
|
||||
|
||||
const evaluateJsTemplateLiteralBasedOnRuntime = (literal, context, runtime) => {
|
||||
if (runtime === 'quickjs') {
|
||||
return executeQuickJsVm({
|
||||
script: literal,
|
||||
context,
|
||||
scriptType: 'template-literal'
|
||||
});
|
||||
}
|
||||
|
||||
return evaluateJsTemplateLiteral(literal, context);
|
||||
};
|
||||
|
||||
const evaluateJsExpressionBasedOnRuntime = (expr, context, runtime, mode) => {
|
||||
if (runtime === 'quickjs') {
|
||||
return executeQuickJsVm({
|
||||
@@ -35,35 +23,6 @@ class VarsRuntime {
|
||||
this.mode = props?.mode || 'developer';
|
||||
}
|
||||
|
||||
runPreRequestVars(vars, request, envVariables, runtimeVariables, collectionPath, processEnvVars) {
|
||||
if (!request?.requestVariables) {
|
||||
request.requestVariables = {};
|
||||
}
|
||||
const enabledVars = _.filter(vars, (v) => v.enabled);
|
||||
if (!enabledVars.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bru = new Bru(envVariables, runtimeVariables, processEnvVars);
|
||||
const req = new BrunoRequest(request);
|
||||
|
||||
const bruContext = {
|
||||
bru,
|
||||
req
|
||||
};
|
||||
|
||||
const context = {
|
||||
...envVariables,
|
||||
...runtimeVariables,
|
||||
...bruContext
|
||||
};
|
||||
|
||||
_.each(enabledVars, (v) => {
|
||||
const value = evaluateJsTemplateLiteralBasedOnRuntime(v.value, context, this.runtime);
|
||||
request?.requestVariables && (request.requestVariables[v.name] = value);
|
||||
});
|
||||
}
|
||||
|
||||
runPostResponseVars(vars, request, response, envVariables, runtimeVariables, collectionPath, processEnvVars) {
|
||||
const requestVariables = request?.requestVariables || {};
|
||||
const enabledVars = _.filter(vars, (v) => v.enabled);
|
||||
|
||||
@@ -69,6 +69,18 @@ const addBruShimToContext = (vm, bru) => {
|
||||
vm.setProp(bruObject, 'getRequestVar', getRequestVar);
|
||||
getRequestVar.dispose();
|
||||
|
||||
let getFolderVar = vm.newFunction('getFolderVar', function (key) {
|
||||
return marshallToVm(bru.getFolderVar(vm.dump(key)), vm);
|
||||
});
|
||||
vm.setProp(bruObject, 'getFolderVar', getFolderVar);
|
||||
getFolderVar.dispose();
|
||||
|
||||
let getCollectionVar = vm.newFunction('getCollectionVar', function (key) {
|
||||
return marshallToVm(bru.getCollectionVar(vm.dump(key)), vm);
|
||||
});
|
||||
vm.setProp(bruObject, 'getCollectionVar', getCollectionVar);
|
||||
getCollectionVar.dispose();
|
||||
|
||||
const sleep = vm.newFunction('sleep', (timer) => {
|
||||
const t = vm.getString(timer);
|
||||
const promise = vm.newPromise();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
headers {
|
||||
check: again
|
||||
token: {{collection_pre_var_token}}
|
||||
}
|
||||
|
||||
auth {
|
||||
@@ -10,6 +11,11 @@ auth:bearer {
|
||||
token: {{bearer_auth_token}}
|
||||
}
|
||||
|
||||
vars:pre-request {
|
||||
collection_pre_var: collection_pre_var_value
|
||||
collection_pre_var_token: {{request_pre_var_token}}
|
||||
}
|
||||
|
||||
docs {
|
||||
# bruno-testbench 🐶
|
||||
|
||||
|
||||
42
packages/bruno-tests/collection/echo/echo bigint.bru
Normal file
42
packages/bruno-tests/collection/echo/echo bigint.bru
Normal file
@@ -0,0 +1,42 @@
|
||||
meta {
|
||||
name: echo bigint
|
||||
type: http
|
||||
seq: 6
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{host}}/api/echo/json
|
||||
body: json
|
||||
auth: none
|
||||
}
|
||||
|
||||
headers {
|
||||
foo: bar
|
||||
}
|
||||
|
||||
auth:basic {
|
||||
username: asd
|
||||
password: j
|
||||
}
|
||||
|
||||
auth:bearer {
|
||||
token:
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"hello": 990531470713421825,
|
||||
"decimal": 1.0,
|
||||
"decimal2": 1.00,
|
||||
"decimal3": 1.00200,
|
||||
"decimal4": 0.00
|
||||
}
|
||||
}
|
||||
|
||||
assert {
|
||||
res.status: eq 200
|
||||
}
|
||||
|
||||
tests {
|
||||
// todo: add tests once lossless json echo server is ready
|
||||
}
|
||||
@@ -25,16 +25,6 @@ body:json {
|
||||
}
|
||||
}
|
||||
|
||||
vars:pre-request {
|
||||
boolean: false
|
||||
undefined: undefined
|
||||
null: null
|
||||
string: foo
|
||||
number_1: 1
|
||||
number_2: 0
|
||||
number_3: -1
|
||||
}
|
||||
|
||||
assert {
|
||||
req.body.boolean: isBoolean false
|
||||
req.body.number_1: isNumber 1
|
||||
@@ -51,35 +41,4 @@ assert {
|
||||
req.body.number_3: eq -1
|
||||
req.body.number_2: isNumber
|
||||
req.body.number_3: isNumber
|
||||
boolean: eq false
|
||||
undefined: eq undefined
|
||||
null: eq null
|
||||
string: eq foo
|
||||
number_1: eq 1
|
||||
number_2: eq 0
|
||||
number_3: eq -1
|
||||
}
|
||||
|
||||
tests {
|
||||
test("boolean pre var", function() {
|
||||
expect(bru.getRequestVar('boolean')).to.eql(false);
|
||||
});
|
||||
|
||||
test("number pre var", function() {
|
||||
expect(bru.getRequestVar('number_1')).to.eql(1);
|
||||
expect(bru.getRequestVar('number_2')).to.eql(0);
|
||||
expect(bru.getRequestVar('number_3')).to.eql(-1);
|
||||
});
|
||||
|
||||
test("null pre var", function() {
|
||||
expect(bru.getRequestVar('null')).to.eql(null);
|
||||
});
|
||||
|
||||
test("undefined pre var", function() {
|
||||
expect(bru.getRequestVar('undefined')).to.eql(undefined);
|
||||
});
|
||||
|
||||
test("string pre var", function() {
|
||||
expect(bru.getRequestVar('string')).to.eql('foo');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
meta {
|
||||
name: string interpolation
|
||||
}
|
||||
|
||||
vars:pre-request {
|
||||
folder_pre_var: folder_pre_var_value
|
||||
folder_pre_var_2: {{env.var1}}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
meta {
|
||||
name: pre-vars
|
||||
type: http
|
||||
seq: 5
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{host}}/ping
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
headers {
|
||||
request_pre_var: {{request_pre_var}}
|
||||
}
|
||||
|
||||
vars:pre-request {
|
||||
request_pre_var: {{folder_pre_var}}
|
||||
request_pre_var_token: request_pre_var_token_value
|
||||
request_pre_var_1: request_pre_var_1_value
|
||||
request_pre_var_2: {{request_pre_var_1}}
|
||||
}
|
||||
|
||||
assert {
|
||||
collection_pre_var: eq collection_pre_var_value
|
||||
folder_pre_var: eq folder_pre_var_value
|
||||
}
|
||||
|
||||
tests {
|
||||
test("collection pre var bru function", function() {
|
||||
expect(bru.getCollectionVar('collection_pre_var')).to.eql('collection_pre_var_value');
|
||||
});
|
||||
|
||||
|
||||
test("folder pre var bru function", function() {
|
||||
expect(bru.getFolderVar('folder_pre_var')).to.eql('folder_pre_var_value');
|
||||
});
|
||||
|
||||
|
||||
test("request pre var bru function", function() {
|
||||
expect(bru.getRequestVar('request_pre_var')).to.eql('folder_pre_var_value');
|
||||
});
|
||||
|
||||
test("request pre var self-interpoaltion", function() {
|
||||
expect(bru.getRequestVar('request_pre_var_2')).to.eql('request_pre_var_1_value');
|
||||
});
|
||||
|
||||
}
|
||||
48
readme.md
48
readme.md
@@ -43,14 +43,34 @@ Bruno is offline-only. There are no plans to add cloud-sync to Bruno, ever. We v
|
||||
|
||||
 <br /><br />
|
||||
|
||||
### Golden Edition ✨
|
||||
## Golden Edition ✨
|
||||
|
||||
Majority of our features are free and open source.
|
||||
We strive to strike a harmonious balance between [open-source principles and sustainability](https://github.com/usebruno/bruno/discussions/269)
|
||||
|
||||
You can buy the [Golden Edition](https://www.usebruno.com/pricing) for a one-time payment of **$19**! <br/>
|
||||
|
||||
### Installation
|
||||
## Table of Contents
|
||||
- [Installation](#installation)
|
||||
- [Features](#features)
|
||||
- [Run across multiple platforms 🖥️](#run-across-multiple-platforms-%EF%B8%8F)
|
||||
- [Collaborate via Git 👩💻🧑💻](#collaborate-via-git-)
|
||||
- [Sponsors](#sponsors)
|
||||
- [Gold Sponsors](#gold-sponsors)
|
||||
- [Silver Sponsors](#silver-sponsors)
|
||||
- [Bronze Sponsors](#bronze-sponsors)
|
||||
- [Important Links 📌](#important-links-)
|
||||
- [Showcase 🎥](#showcase-)
|
||||
- [Support ❤️](#support-%EF%B8%8F)
|
||||
- [Share Testimonials 📣](#share-testimonials-)
|
||||
- [Publishing to New Package Managers](#publishing-to-new-package-managers)
|
||||
- [Stay in touch 🌐](#stay-in-touch-)
|
||||
- [Trademark](#trademark)
|
||||
- [Contribute 👩💻🧑💻](#contribute-)
|
||||
- [Authors](#authors)
|
||||
- [License 📄](#license-)
|
||||
|
||||
## Installation
|
||||
|
||||
Bruno is available as binary download [on our website](https://www.usebruno.com/downloads) for Mac, Windows and Linux.
|
||||
|
||||
@@ -86,6 +106,8 @@ sudo apt update
|
||||
sudo apt install bruno
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
### Run across multiple platforms 🖥️
|
||||
|
||||
 <br /><br />
|
||||
@@ -96,7 +118,7 @@ Or any version control system of your choice
|
||||
|
||||
 <br /><br />
|
||||
|
||||
### Sponsors
|
||||
## Sponsors
|
||||
|
||||
#### Gold Sponsors
|
||||
|
||||
@@ -112,7 +134,7 @@ Or any version control system of your choice
|
||||
<img src="assets/images/sponsors/zuplo.png" width="120"/>
|
||||
</a>
|
||||
|
||||
### Important Links 📌
|
||||
## Important Links 📌
|
||||
|
||||
- [Our Long Term Vision](https://github.com/usebruno/bruno/discussions/269)
|
||||
- [Roadmap](https://github.com/usebruno/bruno/discussions/384)
|
||||
@@ -123,32 +145,32 @@ Or any version control system of your choice
|
||||
- [Download](https://www.usebruno.com/downloads)
|
||||
- [GitHub Sponsors](https://github.com/sponsors/helloanoop).
|
||||
|
||||
### Showcase 🎥
|
||||
## Showcase 🎥
|
||||
|
||||
- [Testimonials](https://github.com/usebruno/bruno/discussions/343)
|
||||
- [Knowledge Hub](https://github.com/usebruno/bruno/discussions/386)
|
||||
- [Scriptmania](https://github.com/usebruno/bruno/discussions/385)
|
||||
|
||||
### Support ❤️
|
||||
## Support ❤️
|
||||
|
||||
If you like Bruno and want to support our opensource work, consider sponsoring us via [GitHub Sponsors](https://github.com/sponsors/helloanoop).
|
||||
|
||||
### Share Testimonials 📣
|
||||
## Share Testimonials 📣
|
||||
|
||||
If Bruno has helped you at work and your teams, please don't forget to share your [testimonials on our GitHub discussion](https://github.com/usebruno/bruno/discussions/343)
|
||||
|
||||
### Publishing to New Package Managers
|
||||
## Publishing to New Package Managers
|
||||
|
||||
Please see [here](publishing.md) for more information.
|
||||
|
||||
### Stay in touch 🌐
|
||||
## Stay in touch 🌐
|
||||
|
||||
[𝕏 (Twitter)](https://twitter.com/use_bruno) <br />
|
||||
[Website](https://www.usebruno.com) <br />
|
||||
[Discord](https://discord.com/invite/KgcZUncpjq) <br />
|
||||
[LinkedIn](https://www.linkedin.com/company/usebruno)
|
||||
|
||||
### Trademark
|
||||
## Trademark
|
||||
|
||||
**Name**
|
||||
|
||||
@@ -158,13 +180,13 @@ Please see [here](publishing.md) for more information.
|
||||
|
||||
The logo is sourced from [OpenMoji](https://openmoji.org/library/emoji-1F436/). License: CC [BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/)
|
||||
|
||||
### Contribute 👩💻🧑💻
|
||||
## Contribute 👩💻🧑💻
|
||||
|
||||
I am happy that you are looking to improve bruno. Please check out the [contributing guide](contributing.md)
|
||||
|
||||
Even if you are not able to make contributions via code, please don't hesitate to file bugs and feature requests that needs to be implemented to solve your use case.
|
||||
|
||||
### Authors
|
||||
## Authors
|
||||
|
||||
<div align="center">
|
||||
<a href="https://github.com/usebruno/bruno/graphs/contributors">
|
||||
@@ -172,6 +194,6 @@ Even if you are not able to make contributions via code, please don't hesitate t
|
||||
</a>
|
||||
</div>
|
||||
|
||||
### License 📄
|
||||
## License 📄
|
||||
|
||||
[MIT](license.md)
|
||||
|
||||
Reference in New Issue
Block a user