mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-04 01:48:33 +00:00
Compare commits
73 Commits
feat/folde
...
fix/lossle
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
103e0a2444 | ||
|
|
72c3aaa5ba | ||
|
|
eb1c10fd6e | ||
|
|
911e3aa589 | ||
|
|
2358aa4cdc | ||
|
|
800dbcfdbc | ||
|
|
d7ec3d1cc5 | ||
|
|
92073e7573 | ||
|
|
3e2a3b65a4 | ||
|
|
aa4bcdca9b | ||
|
|
12fdbbb291 | ||
|
|
7cafed6c93 | ||
|
|
8a9df14e16 | ||
|
|
b2b41fec1a | ||
|
|
a8aa54cf1b | ||
|
|
60a8647e7c | ||
|
|
7ca59656f2 | ||
|
|
4598bb1bdd | ||
|
|
adb843faa7 | ||
|
|
2b0d55ce6b | ||
|
|
c5ec7eea34 | ||
|
|
741250068f | ||
|
|
7c33fd413e | ||
|
|
8f920a90c7 | ||
|
|
640623b39a | ||
|
|
37bec70fe6 | ||
|
|
98c53cf443 | ||
|
|
6fe96a8194 | ||
|
|
f2ba351f0d | ||
|
|
2e2c60d90e | ||
|
|
1d2e06d419 | ||
|
|
c99da3a581 | ||
|
|
073c1aae12 | ||
|
|
398c833393 | ||
|
|
47724b1b1e | ||
|
|
2804ce1eb3 | ||
|
|
9892f7cd40 | ||
|
|
7194998b0e | ||
|
|
ab9bcbe5ed | ||
|
|
e60aaf2ea9 | ||
|
|
81497d8397 | ||
|
|
34a961967e | ||
|
|
1b0495c7b0 | ||
|
|
73214107c7 | ||
|
|
f159f73340 | ||
|
|
c2e6dee2da | ||
|
|
f64dca16a7 | ||
|
|
b5b9e547c9 | ||
|
|
1239baf687 | ||
|
|
b2038c7cc2 | ||
|
|
9f76834b2f | ||
|
|
8094149fbe | ||
|
|
240d2d03f7 | ||
|
|
58c8085a64 | ||
|
|
e5425299a2 | ||
|
|
f1e0b112ae | ||
|
|
0988f2b86e | ||
|
|
e71e38f62e | ||
|
|
589e173256 | ||
|
|
e462eb6ecd | ||
|
|
40f7be534a | ||
|
|
c8f95a34e9 | ||
|
|
2aa7d26a89 | ||
|
|
71353b0404 | ||
|
|
01605f6f2a | ||
|
|
0d204694a6 | ||
|
|
0d3765ad66 | ||
|
|
bd61e453ee | ||
|
|
02e23df349 | ||
|
|
61e0ac03fa | ||
|
|
c895d7f357 | ||
|
|
45ff36d394 | ||
|
|
fd57b2ce94 |
16
.github/workflows/tests.yml
vendored
16
.github/workflows/tests.yml
vendored
@@ -15,6 +15,8 @@ jobs:
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: './package-lock.json'
|
||||
- name: Install dependencies
|
||||
run: npm ci --legacy-peer-deps
|
||||
|
||||
@@ -50,6 +52,8 @@ jobs:
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: './package-lock.json'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci --legacy-peer-deps
|
||||
@@ -71,15 +75,3 @@ jobs:
|
||||
with:
|
||||
files: packages/bruno-tests/collection/junit.xml
|
||||
comment_mode: always
|
||||
prettier:
|
||||
name: Prettier
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
- name: Install dependencies
|
||||
run: npm ci --legacy-peer-deps
|
||||
- name: Run Prettier
|
||||
run: npm run test:prettier:web
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
npx pretty-quick --staged
|
||||
@@ -62,6 +62,9 @@ npm run build:graphql-docs
|
||||
# construction de bruno query
|
||||
npm run build:bruno-query
|
||||
|
||||
# construction de bruno common
|
||||
npm run build:bruno-common
|
||||
|
||||
# démarrage de next (terminal 1)
|
||||
npm run dev:web
|
||||
|
||||
|
||||
84
docs/contributing/contributing_sk.md
Normal file
84
docs/contributing/contributing_sk.md
Normal file
@@ -0,0 +1,84 @@
|
||||
## Urobme bruno lepším, spoločne !!
|
||||
|
||||
Sme radi, že chcete zlepšiť bruno. Nižšie sú uvedené pokyny, ako začať s výchovou bruno na vašom počítači.
|
||||
|
||||
### Technologický zásobník
|
||||
|
||||
Bruno je vytvorené pomocou Next.js a React. Na dodávanie desktopovej verzie (ktorá podporuje lokálne kolekcie) používame aj electron.
|
||||
|
||||
Balíčky, ktoré používame:
|
||||
|
||||
- CSS - Tailwind
|
||||
- Editory kódu - Codemirror
|
||||
- Správa stavu - Redux
|
||||
- Ikony - Tabler Icons
|
||||
- Formuláre - formik
|
||||
- Overovanie schém - Yup
|
||||
- Klient požiadaviek - axios
|
||||
- Sledovač súborového systému - chokidar
|
||||
|
||||
### Závislosti
|
||||
|
||||
Budete potrebovať [NodeJS v18.x alebo najnovšiu verziu LTS](https://nodejs.org/en/) a npm versiu 8.x. V projekte používame pracovné priestory npm
|
||||
|
||||
## Vývoj
|
||||
|
||||
Bruno sa vyvíja ako desktopová aplikácia. Aplikáciu je potrebné načítať spustením aplikácie Next.js v jednom termináli a potom spustiť aplikáciu electron v inom termináli.
|
||||
|
||||
### Závislosti
|
||||
|
||||
- NodeJS v18
|
||||
|
||||
### Miestny vývoj
|
||||
|
||||
```bash
|
||||
# použite verziu nodejs 18
|
||||
nvm use
|
||||
|
||||
# nainštalovať balíčky
|
||||
npm i --legacy-peer-deps
|
||||
|
||||
# zostaviť balíčky
|
||||
npm run build:graphql-docs
|
||||
npm run build:bruno-query
|
||||
npm run build:bruno-common
|
||||
|
||||
# spustite ďalšiu aplikáciu (terminál 1)
|
||||
npm run dev:web
|
||||
|
||||
# spustite aplikáciu electron (terminál 2)
|
||||
npm run dev:electron
|
||||
```
|
||||
|
||||
### Riešenie problémov
|
||||
|
||||
Pri spustení `npm install` sa môžete stretnúť s chybou `Unsupported platform`. Ak chcete túto chybu odstrániť, musíte odstrániť súbory `node_modules`, `package-lock.json` a spustiť `npm install`. Tým by sa mali nainštalovať všetky potrebné balíky potrebné na spustenie aplikácie.
|
||||
|
||||
```shell
|
||||
# Odstrániť node_modules v podadresároch
|
||||
find ./ -type d -name "node_modules" -print0 | while read -d $'\0' dir; do
|
||||
rm -rf "$dir"
|
||||
done
|
||||
|
||||
# Odstráňte package-lock v podadresároch
|
||||
find . -type f -name "package-lock.json" -delete
|
||||
```
|
||||
|
||||
### Testovanie
|
||||
|
||||
````bash
|
||||
# bruno-schema
|
||||
npm test --workspace=packages/bruno-schema
|
||||
|
||||
# bruno-lang
|
||||
npm test --workspace=packages/bruno-lang
|
||||
```
|
||||
|
||||
### Vyrobenie Pull Request
|
||||
|
||||
- Prosím, aby PR boli malé a zamerané na jednu vec
|
||||
- Prosím, dodržujte formát vytvárania vetiev
|
||||
- feature/[názov funkcie]: Táto vetva by mala obsahovať zmeny pre konkrétnu funkciu
|
||||
- Príklad: feature/dark-mode
|
||||
- bugfix/[názov chyby]: Táto vetva by mala obsahovať iba opravy konkrétnej chyby
|
||||
- Príklad: bugfix/bug-1
|
||||
@@ -103,6 +103,12 @@ Ou qualquer sistema de controle de versão de sua escolha.
|
||||
|
||||
<img src="../../assets/images/sponsors/commit-company.png" width="70"/>
|
||||
|
||||
#### Apoiadores Bronze
|
||||
|
||||
<a href="https://zuplo.link/bruno">
|
||||
<img src="../../assets/images/sponsors/zuplo.png" width="120"/>
|
||||
</a>
|
||||
|
||||
### Links Importantes 📌
|
||||
|
||||
- [Nossa Visão de Longo Prazo](https://github.com/usebruno/bruno/discussions/269)
|
||||
|
||||
895
package-lock.json
generated
895
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -6,6 +6,7 @@ const StyledWrapper = styled.div`
|
||||
border: solid 1px ${(props) => props.theme.codemirror.border};
|
||||
font-family: ${(props) => (props.font ? props.font : 'default')};
|
||||
line-break: anywhere;
|
||||
flex: 1 1 0;
|
||||
}
|
||||
|
||||
.CodeMirror-overlayscroll-horizontal div,
|
||||
|
||||
@@ -16,6 +16,7 @@ import stripJsonComments from 'strip-json-comments';
|
||||
|
||||
let CodeMirror;
|
||||
const SERVER_RENDERED = typeof navigator === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true;
|
||||
const TAB_SIZE = 2;
|
||||
|
||||
if (!SERVER_RENDERED) {
|
||||
CodeMirror = require('codemirror');
|
||||
@@ -58,10 +59,13 @@ if (!SERVER_RENDERED) {
|
||||
'bru.cwd()',
|
||||
'bru.getEnvName(key)',
|
||||
'bru.getProcessEnv(key)',
|
||||
'bru.hasEnvVar(key)',
|
||||
'bru.getEnvVar(key)',
|
||||
'bru.setEnvVar(key,value)',
|
||||
'bru.hasVar(key)',
|
||||
'bru.getVar(key)',
|
||||
'bru.setVar(key,value)',
|
||||
'bru.deleteVar(key)',
|
||||
'bru.setNextRequest(requestName)'
|
||||
];
|
||||
CodeMirror.registerHelper('hint', 'brunoJS', (editor, options) => {
|
||||
@@ -118,7 +122,7 @@ export default class CodeEditor extends React.Component {
|
||||
value: this.props.value || '',
|
||||
lineNumbers: true,
|
||||
lineWrapping: true,
|
||||
tabSize: 2,
|
||||
tabSize: TAB_SIZE,
|
||||
mode: this.props.mode || 'application/ld+json',
|
||||
keyMap: 'sublime',
|
||||
autoCloseBrackets: true,
|
||||
@@ -166,7 +170,33 @@ export default class CodeEditor extends React.Component {
|
||||
'Ctrl-Y': 'foldAll',
|
||||
'Cmd-Y': 'foldAll',
|
||||
'Ctrl-I': 'unfoldAll',
|
||||
'Cmd-I': 'unfoldAll'
|
||||
'Cmd-I': 'unfoldAll',
|
||||
'Cmd-/': (cm) => {
|
||||
// comment/uncomment every selected line(s)
|
||||
const selections = cm.listSelections();
|
||||
selections.forEach((range) => {
|
||||
for (let i = range.from().line; i <= range.to().line; i++) {
|
||||
const selectedLine = cm.getLine(i);
|
||||
// if commented line, remove comment
|
||||
if (selectedLine.trim().startsWith('//')) {
|
||||
cm.replaceRange(
|
||||
selectedLine.replace(/^(\s*)\/\/\s?/, '$1'),
|
||||
{ line: i, ch: 0 },
|
||||
{ line: i, ch: selectedLine.length }
|
||||
);
|
||||
continue;
|
||||
}
|
||||
// otherwise add comment
|
||||
cm.replaceRange(
|
||||
selectedLine.search(/\S|$/) >= TAB_SIZE
|
||||
? ' '.repeat(TAB_SIZE) + '// ' + selectedLine.trim()
|
||||
: '// ' + selectedLine,
|
||||
{ line: i, ch: 0 },
|
||||
{ line: i, ch: selectedLine.length }
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
foldOptions: {
|
||||
widget: (from, to) => {
|
||||
@@ -286,7 +316,7 @@ export default class CodeEditor extends React.Component {
|
||||
}
|
||||
return (
|
||||
<StyledWrapper
|
||||
className="h-full w-full"
|
||||
className="h-full w-full flex flex-col relative"
|
||||
aria-label="Code Editor"
|
||||
font={this.props.font}
|
||||
ref={(node) => {
|
||||
|
||||
@@ -138,6 +138,7 @@ const AwsV4Auth = ({ collection }) => {
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleSecretAccessKeyChange(val)}
|
||||
collection={collection}
|
||||
isSecret={true}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -62,6 +62,7 @@ const BasicAuth = ({ collection }) => {
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handlePasswordChange(val)}
|
||||
collection={collection}
|
||||
isSecret={true}
|
||||
/>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
|
||||
@@ -37,6 +37,7 @@ const BearerAuth = ({ collection }) => {
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleTokenChange(val)}
|
||||
collection={collection}
|
||||
isSecret={true}
|
||||
/>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
|
||||
@@ -62,6 +62,7 @@ const DigestAuth = ({ collection }) => {
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handlePasswordChange(val)}
|
||||
collection={collection}
|
||||
isSecret={true}
|
||||
/>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
|
||||
@@ -78,7 +78,7 @@ const OAuth2AuthorizationCode = ({ collection }) => {
|
||||
return (
|
||||
<StyledWrapper className="mt-2 flex w-full gap-4 flex-col">
|
||||
{inputsConfig.map((input) => {
|
||||
const { key, label } = input;
|
||||
const { key, label, isSecret } = input;
|
||||
return (
|
||||
<div className="flex flex-col w-full gap-1" key={`input-${key}`}>
|
||||
<label className="block font-medium">{label}</label>
|
||||
@@ -90,6 +90,7 @@ const OAuth2AuthorizationCode = ({ collection }) => {
|
||||
onChange={(val) => handleChange(key, val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
isSecret={isSecret}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -17,7 +17,8 @@ const inputsConfig = [
|
||||
},
|
||||
{
|
||||
key: 'clientSecret',
|
||||
label: 'Client Secret'
|
||||
label: 'Client Secret',
|
||||
isSecret: true
|
||||
},
|
||||
{
|
||||
key: 'scope',
|
||||
|
||||
@@ -42,7 +42,7 @@ const OAuth2ClientCredentials = ({ collection }) => {
|
||||
return (
|
||||
<StyledWrapper className="mt-2 flex w-full gap-4 flex-col">
|
||||
{inputsConfig.map((input) => {
|
||||
const { key, label } = input;
|
||||
const { key, label, isSecret } = input;
|
||||
return (
|
||||
<div className="flex flex-col w-full gap-1" key={`input-${key}`}>
|
||||
<label className="block font-medium">{label}</label>
|
||||
@@ -54,6 +54,7 @@ const OAuth2ClientCredentials = ({ collection }) => {
|
||||
onChange={(val) => handleChange(key, val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
isSecret={isSecret}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,7 +9,8 @@ const inputsConfig = [
|
||||
},
|
||||
{
|
||||
key: 'clientSecret',
|
||||
label: 'Client Secret'
|
||||
label: 'Client Secret',
|
||||
isSecret: true
|
||||
},
|
||||
{
|
||||
key: 'scope',
|
||||
|
||||
@@ -44,7 +44,7 @@ const OAuth2AuthorizationCode = ({ item, collection }) => {
|
||||
return (
|
||||
<StyledWrapper className="mt-2 flex w-full gap-4 flex-col">
|
||||
{inputsConfig.map((input) => {
|
||||
const { key, label } = input;
|
||||
const { key, label, isSecret } = input;
|
||||
return (
|
||||
<div className="flex flex-col w-full gap-1" key={`input-${key}`}>
|
||||
<label className="block font-medium">{label}</label>
|
||||
@@ -56,6 +56,7 @@ const OAuth2AuthorizationCode = ({ item, collection }) => {
|
||||
onChange={(val) => handleChange(key, val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
isSecret={isSecret}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -17,7 +17,8 @@ const inputsConfig = [
|
||||
},
|
||||
{
|
||||
key: 'clientSecret',
|
||||
label: 'Client Secret'
|
||||
label: 'Client Secret',
|
||||
isSecret: true
|
||||
},
|
||||
{
|
||||
key: 'scope',
|
||||
|
||||
@@ -13,6 +13,7 @@ import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/acti
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { headers as StandardHTTPHeaders } from 'know-your-http-well';
|
||||
import { MimeTypes } from 'utils/codemirror/autocompleteConstants';
|
||||
const headerAutoCompleteList = StandardHTTPHeaders.map((e) => e.header);
|
||||
|
||||
const Headers = ({ collection }) => {
|
||||
@@ -117,6 +118,7 @@ const Headers = ({ collection }) => {
|
||||
)
|
||||
}
|
||||
collection={collection}
|
||||
autocomplete={MimeTypes}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@@ -1,14 +1,6 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
div.CodeMirror {
|
||||
/* todo: find a better way */
|
||||
height: calc(100vh - 240px);
|
||||
|
||||
.CodeMirror-scroll {
|
||||
padding-bottom: 0px;
|
||||
}
|
||||
}
|
||||
.editing-mode {
|
||||
cursor: pointer;
|
||||
color: ${(props) => props.theme.colors.text.yellow};
|
||||
|
||||
@@ -37,8 +37,8 @@ const Documentation = ({ item, collection }) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledWrapper className="mt-1 h-full w-full relative">
|
||||
<div className="editing-mode mb-2" role="tab" onClick={toggleViewMode}>
|
||||
<StyledWrapper className="flex flex-col gap-y-1 h-full w-full relative">
|
||||
<div className="editing-mode" role="tab" onClick={toggleViewMode}>
|
||||
{isEditing ? 'Preview' : 'Edit'}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import Portal from 'components/Portal';
|
||||
import Modal from 'components/Modal';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useFormik } from 'formik';
|
||||
import { addEnvironment } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import * as Yup from 'yup';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import Portal from 'components/Portal';
|
||||
import Modal from 'components/Modal';
|
||||
|
||||
const CreateEnvironment = ({ collection, onClose }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -27,7 +27,7 @@ const CreateEnvironment = ({ collection, onClose }) => {
|
||||
toast.success('Environment created in collection');
|
||||
onClose();
|
||||
})
|
||||
.catch(() => toast.error('An error occurred while created the environment'));
|
||||
.catch(() => toast.error('An error occurred while creating the environment'));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -55,19 +55,21 @@ const CreateEnvironment = ({ collection, onClose }) => {
|
||||
<label htmlFor="name" className="block font-semibold">
|
||||
Environment Name
|
||||
</label>
|
||||
<input
|
||||
id="environment-name"
|
||||
type="text"
|
||||
name="name"
|
||||
ref={inputRef}
|
||||
className="block textbox mt-2 w-full"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.name || ''}
|
||||
/>
|
||||
<div className="flex items-center mt-2">
|
||||
<input
|
||||
id="environment-name"
|
||||
type="text"
|
||||
name="name"
|
||||
ref={inputRef}
|
||||
className="block textbox w-full"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.name || ''}
|
||||
/>
|
||||
</div>
|
||||
{formik.touched.name && formik.errors.name ? (
|
||||
<div className="text-red-500">{formik.errors.name}</div>
|
||||
) : null}
|
||||
|
||||
@@ -5,7 +5,6 @@ import { useDispatch } from 'react-redux';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { uuid } from 'utils/common';
|
||||
import { maskInputValue } from 'utils/collections';
|
||||
import { useFormik } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
import { variableNameRegex } from 'utils/common/regex';
|
||||
@@ -96,10 +95,10 @@ const EnvironmentVariables = ({ environment, collection, setIsModified, original
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Enabled</td>
|
||||
<td className="text-center">Enabled</td>
|
||||
<td>Name</td>
|
||||
<td>Value</td>
|
||||
<td>Secret</td>
|
||||
<td className="text-center">Secret</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -109,7 +108,7 @@ const EnvironmentVariables = ({ environment, collection, setIsModified, original
|
||||
<td className="text-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="mr-3 mousetrap"
|
||||
className="mousetrap"
|
||||
name={`${index}.enabled`}
|
||||
checked={variable.enabled}
|
||||
onChange={formik.handleChange}
|
||||
@@ -130,23 +129,22 @@ const EnvironmentVariables = ({ environment, collection, setIsModified, original
|
||||
/>
|
||||
<ErrorMessage name={`${index}.name`} />
|
||||
</td>
|
||||
<td>
|
||||
{variable.secret ? (
|
||||
<div className="overflow-hidden text-ellipsis">{maskInputValue(variable.value)}</div>
|
||||
) : (
|
||||
<td className="flex flex-row flex-nowrap">
|
||||
<div className="overflow-hidden grow w-full relative">
|
||||
<SingleLineEditor
|
||||
theme={storedTheme}
|
||||
collection={collection}
|
||||
name={`${index}.value`}
|
||||
value={variable.value}
|
||||
isSecret={variable.secret}
|
||||
onChange={(newValue) => formik.setFieldValue(`${index}.value`, newValue, true)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<td className="text-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="mr-3 mousetrap"
|
||||
className="mousetrap"
|
||||
name={`${index}.secret`}
|
||||
checked={variable.secret}
|
||||
onChange={formik.handleChange}
|
||||
|
||||
@@ -1,24 +1,38 @@
|
||||
import React from 'react';
|
||||
import Portal from 'components/Portal';
|
||||
import Modal from 'components/Modal';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import importPostmanEnvironment from 'utils/importers/postman-environment';
|
||||
import { importEnvironment } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { toastError } from 'utils/common/error';
|
||||
import Modal from 'components/Modal';
|
||||
import { IconDatabaseImport } from '@tabler/icons';
|
||||
|
||||
const ImportEnvironment = ({ onClose, collection }) => {
|
||||
const ImportEnvironment = ({ collection, onClose }) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleImportPostmanEnvironment = () => {
|
||||
importPostmanEnvironment()
|
||||
.then((environment) => {
|
||||
dispatch(importEnvironment(environment.name, environment.variables, collection.uid))
|
||||
.then(() => {
|
||||
toast.success('Environment imported successfully');
|
||||
onClose();
|
||||
})
|
||||
.catch(() => toast.error('An error occurred while importing the environment'));
|
||||
.then((environments) => {
|
||||
environments
|
||||
.filter((env) =>
|
||||
env.name && env.name !== 'undefined'
|
||||
? true
|
||||
: () => {
|
||||
toast.error('Failed to import environment: env has no name');
|
||||
return false;
|
||||
}
|
||||
)
|
||||
.map((environment) => {
|
||||
dispatch(importEnvironment(environment.name, environment.variables, collection.uid))
|
||||
.then(() => {
|
||||
toast.success('Environment imported successfully');
|
||||
})
|
||||
.catch(() => toast.error('An error occurred while importing the environment'));
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
onClose();
|
||||
})
|
||||
.catch((err) => toastError(err, 'Postman Import environment failed'));
|
||||
};
|
||||
@@ -26,11 +40,14 @@ const ImportEnvironment = ({ onClose, collection }) => {
|
||||
return (
|
||||
<Portal>
|
||||
<Modal size="sm" title="Import Environment" hideFooter={true} handleConfirm={onClose} handleCancel={onClose}>
|
||||
<div>
|
||||
<div className="text-link hover:underline cursor-pointer" onClick={handleImportPostmanEnvironment}>
|
||||
Postman Environment
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleImportPostmanEnvironment}
|
||||
className="flex justify-center flex-col items-center w-full dark:bg-zinc-700 rounded-lg border-2 border-dashed border-zinc-300 dark:border-zinc-400 p-12 text-center hover:border-zinc-400 focus:outline-none focus:ring-2 focus:ring-amber-500 focus:ring-offset-2"
|
||||
>
|
||||
<IconDatabaseImport size={64} />
|
||||
<span className="mt-2 block text-sm font-semibold">Import your Postman environments</span>
|
||||
</button>
|
||||
</Modal>
|
||||
</Portal>
|
||||
);
|
||||
|
||||
@@ -4,45 +4,61 @@ import CreateEnvironment from './CreateEnvironment';
|
||||
import EnvironmentList from './EnvironmentList';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import ImportEnvironment from './ImportEnvironment';
|
||||
import { IconFileAlert } from '@tabler/icons';
|
||||
|
||||
export const SharedButton = ({ children, className, onClick }) => {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className={`rounded bg-transparent px-2.5 py-2 w-fit text-xs font-semibold text-zinc-900 dark:text-zinc-50 shadow-sm ring-1 ring-inset ring-zinc-300 dark:ring-zinc-500 hover:bg-gray-50 dark:hover:bg-zinc-700
|
||||
${className}`}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
const DefaultTab = ({ setTab }) => {
|
||||
return (
|
||||
<div className="text-center items-center flex flex-col">
|
||||
<IconFileAlert size={64} strokeWidth={1} />
|
||||
<span className="font-semibold mt-2">No environments found</span>
|
||||
<span className="font-extralight mt-2 text-zinc-500 dark:text-zinc-400">
|
||||
Get started by using the following buttons :
|
||||
</span>
|
||||
<div className="flex items-center justify-center mt-6">
|
||||
<SharedButton onClick={() => setTab('create')}>
|
||||
<span>Create Environment</span>
|
||||
</SharedButton>
|
||||
|
||||
<span className="mx-4">Or</span>
|
||||
|
||||
<SharedButton onClick={() => setTab('import')}>
|
||||
<span>Import Environment</span>
|
||||
</SharedButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const EnvironmentSettings = ({ collection, onClose }) => {
|
||||
const [isModified, setIsModified] = useState(false);
|
||||
const { environments } = collection;
|
||||
const [openCreateModal, setOpenCreateModal] = useState(false);
|
||||
const [openImportModal, setOpenImportModal] = useState(false);
|
||||
const [selectedEnvironment, setSelectedEnvironment] = useState(null);
|
||||
|
||||
const [tab, setTab] = useState('default');
|
||||
if (!environments || !environments.length) {
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<Modal
|
||||
size="md"
|
||||
title="Environments"
|
||||
confirmText={'Close'}
|
||||
handleConfirm={onClose}
|
||||
handleCancel={onClose}
|
||||
hideCancel={true}
|
||||
>
|
||||
{openCreateModal && <CreateEnvironment collection={collection} onClose={() => setOpenCreateModal(false)} />}
|
||||
{openImportModal && <ImportEnvironment collection={collection} onClose={() => setOpenImportModal(false)} />}
|
||||
<div className="text-center flex flex-col">
|
||||
<p>No environments found!</p>
|
||||
<button
|
||||
className="btn-create-environment text-link pr-2 py-3 mt-2 select-none"
|
||||
onClick={() => setOpenCreateModal(true)}
|
||||
>
|
||||
<span>Create Environment</span>
|
||||
</button>
|
||||
|
||||
<span>Or</span>
|
||||
|
||||
<button
|
||||
className="btn-import-environment text-link pl-2 pr-2 py-3 select-none"
|
||||
onClick={() => setOpenImportModal(true)}
|
||||
>
|
||||
<span>Import Environment</span>
|
||||
</button>
|
||||
</div>
|
||||
<Modal size="md" title="Environments" handleCancel={onClose} hideCancel={true} hideFooter={true}>
|
||||
{tab === 'create' ? (
|
||||
<CreateEnvironment collection={collection} onClose={() => setTab('default')} />
|
||||
) : tab === 'import' ? (
|
||||
<ImportEnvironment collection={collection} onClose={() => setTab('default')} />
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
<DefaultTab setTab={setTab} />
|
||||
</Modal>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -42,7 +42,7 @@ const FilePickerEditor = ({ value, onChange, collection }) => {
|
||||
};
|
||||
|
||||
const clear = () => {
|
||||
onChange('');
|
||||
onChange([]);
|
||||
};
|
||||
|
||||
const renderButtonText = (filenames) => {
|
||||
|
||||
@@ -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-header {
|
||||
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,153 @@
|
||||
import React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { IconTrash } from '@tabler/icons';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { addFolderHeader, updateFolderHeader, deleteFolderHeader } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveFolderRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { headers as StandardHTTPHeaders } from 'know-your-http-well';
|
||||
const headerAutoCompleteList = StandardHTTPHeaders.map((e) => e.header);
|
||||
|
||||
const Headers = ({ collection, folder }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
const headers = get(folder, 'root.request.headers', []);
|
||||
|
||||
const addHeader = () => {
|
||||
dispatch(
|
||||
addFolderHeader({
|
||||
collectionUid: collection.uid,
|
||||
folderUid: folder.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handleSave = () => dispatch(saveFolderRoot(collection.uid, folder.uid));
|
||||
const handleHeaderValueChange = (e, _header, type) => {
|
||||
const header = cloneDeep(_header);
|
||||
switch (type) {
|
||||
case 'name': {
|
||||
header.name = e.target.value;
|
||||
break;
|
||||
}
|
||||
case 'value': {
|
||||
header.value = e.target.value;
|
||||
break;
|
||||
}
|
||||
case 'enabled': {
|
||||
header.enabled = e.target.checked;
|
||||
break;
|
||||
}
|
||||
}
|
||||
dispatch(
|
||||
updateFolderHeader({
|
||||
header: header,
|
||||
collectionUid: collection.uid,
|
||||
folderUid: folder.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handleRemoveHeader = (header) => {
|
||||
dispatch(
|
||||
deleteFolderHeader({
|
||||
headerUid: header.uid,
|
||||
collectionUid: collection.uid,
|
||||
folderUid: folder.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full">
|
||||
<div className="text-xs mb-4 text-muted">
|
||||
Request headers that will be sent with every request inside this folder.
|
||||
</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>Value</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{headers && headers.length
|
||||
? headers.map((header) => {
|
||||
return (
|
||||
<tr key={header.uid}>
|
||||
<td>
|
||||
<SingleLineEditor
|
||||
value={header.name}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(newValue) =>
|
||||
handleHeaderValueChange(
|
||||
{
|
||||
target: {
|
||||
value: newValue
|
||||
}
|
||||
},
|
||||
header,
|
||||
'name'
|
||||
)
|
||||
}
|
||||
autocomplete={headerAutoCompleteList}
|
||||
collection={collection}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<SingleLineEditor
|
||||
value={header.value}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(newValue) =>
|
||||
handleHeaderValueChange(
|
||||
{
|
||||
target: {
|
||||
value: newValue
|
||||
}
|
||||
},
|
||||
header,
|
||||
'value'
|
||||
)
|
||||
}
|
||||
collection={collection}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={header.enabled}
|
||||
tabIndex="-1"
|
||||
className="mr-3 mousetrap"
|
||||
onChange={(e) => handleHeaderValueChange(e, header, 'enabled')}
|
||||
/>
|
||||
<button tabIndex="-1" onClick={() => handleRemoveHeader(header)}>
|
||||
<IconTrash strokeWidth={1.5} size={20} />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
: null}
|
||||
</tbody>
|
||||
</table>
|
||||
<button className="btn-add-header text-link pr-2 py-3 mt-2 select-none" onClick={addHeader}>
|
||||
+ Add Header
|
||||
</button>
|
||||
|
||||
<div className="mt-6">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
export default Headers;
|
||||
@@ -0,0 +1,13 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
div.CodeMirror {
|
||||
height: inherit;
|
||||
}
|
||||
|
||||
div.title {
|
||||
color: var(--color-tab-inactive);
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,81 @@
|
||||
import React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import CodeEditor from 'components/CodeEditor';
|
||||
import { updateFolderRequestScript, updateFolderResponseScript } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveFolderRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const Script = ({ collection, folder }) => {
|
||||
const dispatch = useDispatch();
|
||||
const requestScript = get(folder, 'root.request.script.req', '');
|
||||
const responseScript = get(folder, 'root.request.script.res', '');
|
||||
|
||||
const { displayedTheme } = useTheme();
|
||||
const preferences = useSelector((state) => state.app.preferences);
|
||||
|
||||
const onRequestScriptEdit = (value) => {
|
||||
dispatch(
|
||||
updateFolderRequestScript({
|
||||
script: value,
|
||||
collectionUid: collection.uid,
|
||||
folderUid: folder.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const onResponseScriptEdit = (value) => {
|
||||
dispatch(
|
||||
updateFolderResponseScript({
|
||||
script: value,
|
||||
collectionUid: collection.uid,
|
||||
folderUid: folder.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
dispatch(saveFolderRoot(collection.uid, folder.uid));
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full flex flex-col h-full">
|
||||
<div className="text-xs mb-4 text-muted">
|
||||
Pre and post-request scripts that will run before and after any request inside this folder is sent.
|
||||
</div>
|
||||
<div className="flex-1 mt-2">
|
||||
<div className="mb-1 title text-xs">Pre Request</div>
|
||||
<CodeEditor
|
||||
collection={collection}
|
||||
value={requestScript || ''}
|
||||
theme={displayedTheme}
|
||||
onEdit={onRequestScriptEdit}
|
||||
mode="javascript"
|
||||
onSave={handleSave}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 mt-6">
|
||||
<div className="mt-1 mb-1 title text-xs">Post Response</div>
|
||||
<CodeEditor
|
||||
collection={collection}
|
||||
value={responseScript || ''}
|
||||
theme={displayedTheme}
|
||||
onEdit={onResponseScriptEdit}
|
||||
mode="javascript"
|
||||
onSave={handleSave}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-12">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Script;
|
||||
@@ -0,0 +1,46 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
max-width: 800px;
|
||||
|
||||
div.tabs {
|
||||
div.tab {
|
||||
padding: 6px 0px;
|
||||
border: none;
|
||||
border-bottom: solid 2px transparent;
|
||||
margin-right: 1.25rem;
|
||||
color: var(--color-tab-inactive);
|
||||
cursor: pointer;
|
||||
|
||||
&:focus,
|
||||
&:active,
|
||||
&:focus-within,
|
||||
&:focus-visible,
|
||||
&:target {
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: ${(props) => props.theme.tabs.active.color} !important;
|
||||
border-bottom: solid 2px ${(props) => props.theme.tabs.active.border} !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
table {
|
||||
thead,
|
||||
td {
|
||||
border: 1px solid ${(props) => props.theme.table.border};
|
||||
|
||||
li {
|
||||
background-color: ${(props) => props.theme.bg} !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.muted {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,5 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div``;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,51 @@
|
||||
import React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import CodeEditor from 'components/CodeEditor';
|
||||
import { updateFolderTests } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveFolderRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const Tests = ({ collection, folder }) => {
|
||||
const dispatch = useDispatch();
|
||||
const tests = get(folder, 'root.request.tests', '');
|
||||
|
||||
const { displayedTheme } = useTheme();
|
||||
const preferences = useSelector((state) => state.app.preferences);
|
||||
|
||||
const onEdit = (value) => {
|
||||
dispatch(
|
||||
updateFolderTests({
|
||||
tests: value,
|
||||
collectionUid: collection.uid,
|
||||
folderUid: folder.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handleSave = () => dispatch(saveFolderRoot(collection.uid, folder.uid));
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full flex flex-col h-full">
|
||||
<div className="text-xs mb-4 text-muted">These tests will run any time a request in this collection is sent.</div>
|
||||
<CodeEditor
|
||||
collection={collection}
|
||||
value={tests || ''}
|
||||
theme={displayedTheme}
|
||||
onEdit={onEdit}
|
||||
mode="javascript"
|
||||
onSave={handleSave}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
/>
|
||||
|
||||
<div className="mt-6">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Tests;
|
||||
@@ -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,161 @@
|
||||
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 { saveFolderRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import Tooltip from 'components/Tooltip';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import toast from 'react-hot-toast';
|
||||
import { variableNameRegex } from 'utils/common/regex';
|
||||
import { addFolderVar, deleteFolderVar, updateFolderVar } from 'providers/ReduxStore/slices/collections/index';
|
||||
|
||||
const VarsTable = ({ folder, collection, vars, varType }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const addVar = () => {
|
||||
dispatch(
|
||||
addFolderVar({
|
||||
collectionUid: collection.uid,
|
||||
folderUid: folder.uid,
|
||||
type: varType
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const onSave = () => dispatch(saveFolderRoot(collection.uid, folder.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(
|
||||
updateFolderVar({
|
||||
type: varType,
|
||||
var: _var,
|
||||
folderUid: folder.uid,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handleRemoveVar = (_var) => {
|
||||
dispatch(
|
||||
deleteFolderVar({
|
||||
type: varType,
|
||||
varUid: _var.uid,
|
||||
folderUid: folder.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>
|
||||
<Tooltip text="You can write any valid JS Template Literal here" tooltipId="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" />
|
||||
</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 { saveFolderRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
const Vars = ({ collection, folder }) => {
|
||||
const dispatch = useDispatch();
|
||||
const requestVars = get(folder, 'root.request.vars.req', []);
|
||||
const responseVars = get(folder, 'root.request.vars.res', []);
|
||||
const handleSave = () => dispatch(saveFolderRoot(collection.uid, folder.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 folder={folder} collection={collection} vars={requestVars} varType="request" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="mt-1 mb-1 title text-xs">Post Response</div>
|
||||
<VarsTable folder={folder} 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;
|
||||
75
packages/bruno-app/src/components/FolderSettings/index.js
Normal file
75
packages/bruno-app/src/components/FolderSettings/index.js
Normal file
@@ -0,0 +1,75 @@
|
||||
import React from 'react';
|
||||
import classnames from 'classnames';
|
||||
import { updatedFolderSettingsSelectedTab } from 'providers/ReduxStore/slices/collections';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import Headers from './Headers';
|
||||
import Script from './Script';
|
||||
import Tests from './Tests';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import Vars from './Vars';
|
||||
|
||||
const FolderSettings = ({ collection, folder }) => {
|
||||
const dispatch = useDispatch();
|
||||
let tab = 'headers';
|
||||
const { folderLevelSettingsSelectedTab } = collection;
|
||||
if (folderLevelSettingsSelectedTab?.[folder?.uid]) {
|
||||
tab = folderLevelSettingsSelectedTab[folder?.uid];
|
||||
}
|
||||
|
||||
const setTab = (tab) => {
|
||||
dispatch(
|
||||
updatedFolderSettingsSelectedTab({
|
||||
collectionUid: collection.uid,
|
||||
folderUid: folder.uid,
|
||||
tab
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const getTabPanel = (tab) => {
|
||||
switch (tab) {
|
||||
case 'headers': {
|
||||
return <Headers collection={collection} folder={folder} />;
|
||||
}
|
||||
case 'script': {
|
||||
return <Script collection={collection} folder={folder} />;
|
||||
}
|
||||
case 'test': {
|
||||
return <Tests collection={collection} folder={folder} />;
|
||||
}
|
||||
case 'vars': {
|
||||
return <Vars collection={collection} folder={folder} />;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getTabClassname = (tabName) => {
|
||||
return classnames(`tab select-none ${tabName}`, {
|
||||
active: tabName === tab
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="flex flex-col h-full relative px-4 py-4">
|
||||
<div className="flex flex-wrap items-center tabs" role="tablist">
|
||||
<div className={getTabClassname('headers')} role="tab" onClick={() => setTab('headers')}>
|
||||
Headers
|
||||
</div>
|
||||
<div className={getTabClassname('script')} role="tab" onClick={() => setTab('script')}>
|
||||
Script
|
||||
</div>
|
||||
<div className={getTabClassname('test')} role="tab" onClick={() => setTab('test')}>
|
||||
Test
|
||||
</div>
|
||||
<div className={getTabClassname('vars')} role="tab" onClick={() => setTab('vars')}>
|
||||
Vars
|
||||
</div>
|
||||
</div>
|
||||
<section className={`flex mt-4 h-full`}>{getTabPanel(tab)}</section>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default FolderSettings;
|
||||
@@ -2,7 +2,6 @@ import styled from 'styled-components';
|
||||
|
||||
const StyledMarkdownBodyWrapper = styled.div`
|
||||
background: transparent;
|
||||
height: inherit;
|
||||
.markdown-body {
|
||||
background: transparent;
|
||||
overflow-y: auto;
|
||||
|
||||
@@ -1,15 +1,28 @@
|
||||
import MarkdownIt from 'markdown-it';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import * as React from 'react';
|
||||
import React from 'react';
|
||||
|
||||
const md = new MarkdownIt();
|
||||
|
||||
const Markdown = ({ onDoubleClick, content }) => {
|
||||
const handleOnClick = (event) => {
|
||||
const target = event.target;
|
||||
if (target.tagName === 'A') {
|
||||
event.preventDefault();
|
||||
const href = target.getAttribute('href');
|
||||
if (href) {
|
||||
window.open(href, '_blank');
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleOnDoubleClick = (event) => {
|
||||
if (event?.detail === 2) {
|
||||
if (event.detail === 2) {
|
||||
onDoubleClick();
|
||||
}
|
||||
};
|
||||
|
||||
const htmlFromMarkdown = md.render(content || '');
|
||||
|
||||
return (
|
||||
@@ -17,7 +30,8 @@ const Markdown = ({ onDoubleClick, content }) => {
|
||||
<div
|
||||
className="markdown-body"
|
||||
dangerouslySetInnerHTML={{ __html: htmlFromMarkdown }}
|
||||
onClick={handleOnDoubleClick}
|
||||
onClick={handleOnClick}
|
||||
onDoubleClick={handleOnDoubleClick}
|
||||
/>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -24,13 +24,15 @@ class MultiLineEditor extends Component {
|
||||
componentDidMount() {
|
||||
// Initialize CodeMirror as a single line editor
|
||||
/** @type {import("codemirror").Editor} */
|
||||
const variables = getAllVariables(this.props.collection, this.props.item);
|
||||
|
||||
this.editor = CodeMirror(this.editorRef.current, {
|
||||
lineWrapping: false,
|
||||
lineNumbers: false,
|
||||
theme: this.props.theme === 'dark' ? 'monokai' : 'default',
|
||||
mode: 'brunovariables',
|
||||
brunoVarInfo: {
|
||||
variables: getAllVariables(this.props.collection)
|
||||
variables
|
||||
},
|
||||
scrollbarStyle: null,
|
||||
tabindex: 0,
|
||||
@@ -85,7 +87,7 @@ class MultiLineEditor extends Component {
|
||||
}
|
||||
this.editor.setValue(String(this.props.value) || '');
|
||||
this.editor.on('change', this._onEdit);
|
||||
this.addOverlay();
|
||||
this.addOverlay(variables);
|
||||
}
|
||||
|
||||
_onEdit = () => {
|
||||
@@ -103,10 +105,10 @@ class MultiLineEditor extends Component {
|
||||
// event loop.
|
||||
this.ignoreChangeEvent = true;
|
||||
|
||||
let variables = getAllVariables(this.props.collection);
|
||||
let variables = getAllVariables(this.props.collection, this.props.item);
|
||||
if (!isEqual(variables, this.variables)) {
|
||||
this.editor.options.brunoVarInfo.variables = variables;
|
||||
this.addOverlay();
|
||||
this.addOverlay(variables);
|
||||
}
|
||||
if (this.props.theme !== prevProps.theme && this.editor) {
|
||||
this.editor.setOption('theme', this.props.theme === 'dark' ? 'monokai' : 'default');
|
||||
@@ -125,10 +127,8 @@ class MultiLineEditor extends Component {
|
||||
this.editor.getWrapperElement().remove();
|
||||
}
|
||||
|
||||
addOverlay = () => {
|
||||
let variables = getAllVariables(this.props.collection);
|
||||
addOverlay = (variables) => {
|
||||
this.variables = variables;
|
||||
|
||||
defineCodeMirrorBrunoVariablesMode(variables, 'text/plain');
|
||||
this.editor.setOption('mode', 'brunovariables');
|
||||
};
|
||||
|
||||
@@ -136,6 +136,7 @@ const AwsV4Auth = ({ onTokenChange, item, collection }) => {
|
||||
onChange={(val) => handleAccessKeyIdChange(val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
item={item}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -148,6 +149,8 @@ const AwsV4Auth = ({ onTokenChange, item, collection }) => {
|
||||
onChange={(val) => handleSecretAccessKeyChange(val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
item={item}
|
||||
isSecret={true}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -160,6 +163,7 @@ const AwsV4Auth = ({ onTokenChange, item, collection }) => {
|
||||
onChange={(val) => handleSessionTokenChange(val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
item={item}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -172,6 +176,7 @@ const AwsV4Auth = ({ onTokenChange, item, collection }) => {
|
||||
onChange={(val) => handleServiceChange(val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
item={item}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -184,6 +189,7 @@ const AwsV4Auth = ({ onTokenChange, item, collection }) => {
|
||||
onChange={(val) => handleRegionChange(val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
item={item}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -196,6 +202,7 @@ const AwsV4Auth = ({ onTokenChange, item, collection }) => {
|
||||
onChange={(val) => handleProfileNameChange(val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
item={item}
|
||||
/>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
|
||||
@@ -55,6 +55,7 @@ const BasicAuth = ({ item, collection }) => {
|
||||
onChange={(val) => handleUsernameChange(val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
item={item}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -67,6 +68,8 @@ const BasicAuth = ({ item, collection }) => {
|
||||
onChange={(val) => handlePasswordChange(val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
item={item}
|
||||
isSecret={true}
|
||||
/>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
|
||||
@@ -42,6 +42,8 @@ const BearerAuth = ({ item, collection }) => {
|
||||
onChange={(val) => handleTokenChange(val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
item={item}
|
||||
isSecret={true}
|
||||
/>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
|
||||
@@ -55,6 +55,7 @@ const DigestAuth = ({ item, collection }) => {
|
||||
onChange={(val) => handleUsernameChange(val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
item={item}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -67,6 +68,8 @@ const DigestAuth = ({ item, collection }) => {
|
||||
onChange={(val) => handlePasswordChange(val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
item={item}
|
||||
isSecret={true}
|
||||
/>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
|
||||
@@ -80,7 +80,7 @@ const OAuth2AuthorizationCode = ({ item, collection }) => {
|
||||
return (
|
||||
<StyledWrapper className="mt-2 flex w-full gap-4 flex-col">
|
||||
{inputsConfig.map((input) => {
|
||||
const { key, label } = input;
|
||||
const { key, label, isSecret } = input;
|
||||
return (
|
||||
<div className="flex flex-col w-full gap-1" key={`input-${key}`}>
|
||||
<label className="block font-medium">{label}</label>
|
||||
@@ -92,6 +92,8 @@ const OAuth2AuthorizationCode = ({ item, collection }) => {
|
||||
onChange={(val) => handleChange(key, val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
item={item}
|
||||
isSecret={isSecret}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -17,7 +17,8 @@ const inputsConfig = [
|
||||
},
|
||||
{
|
||||
key: 'clientSecret',
|
||||
label: 'Client Secret'
|
||||
label: 'Client Secret',
|
||||
isSecret: true
|
||||
},
|
||||
{
|
||||
key: 'scope',
|
||||
|
||||
@@ -43,7 +43,7 @@ const OAuth2ClientCredentials = ({ item, collection }) => {
|
||||
return (
|
||||
<StyledWrapper className="mt-2 flex w-full gap-4 flex-col">
|
||||
{inputsConfig.map((input) => {
|
||||
const { key, label } = input;
|
||||
const { key, label, isSecret } = input;
|
||||
return (
|
||||
<div className="flex flex-col w-full gap-1" key={`input-${key}`}>
|
||||
<label className="block font-medium">{label}</label>
|
||||
@@ -55,6 +55,8 @@ const OAuth2ClientCredentials = ({ item, collection }) => {
|
||||
onChange={(val) => handleChange(key, val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
item={item}
|
||||
isSecret={isSecret}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,7 +9,8 @@ const inputsConfig = [
|
||||
},
|
||||
{
|
||||
key: 'clientSecret',
|
||||
label: 'Client Secret'
|
||||
label: 'Client Secret',
|
||||
isSecret: true
|
||||
},
|
||||
{
|
||||
key: 'scope',
|
||||
|
||||
@@ -45,7 +45,7 @@ const OAuth2AuthorizationCode = ({ item, collection }) => {
|
||||
return (
|
||||
<StyledWrapper className="mt-2 flex w-full gap-4 flex-col">
|
||||
{inputsConfig.map((input) => {
|
||||
const { key, label } = input;
|
||||
const { key, label, isSecret } = input;
|
||||
return (
|
||||
<div className="flex flex-col w-full gap-1" key={`input-${key}`}>
|
||||
<label className="block font-medium">{label}</label>
|
||||
@@ -57,6 +57,8 @@ const OAuth2AuthorizationCode = ({ item, collection }) => {
|
||||
onChange={(val) => handleChange(key, val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
item={item}
|
||||
isSecret={isSecret}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,7 +9,8 @@ const inputsConfig = [
|
||||
},
|
||||
{
|
||||
key: 'password',
|
||||
label: 'Password'
|
||||
label: 'Password',
|
||||
isSecret: true
|
||||
},
|
||||
{
|
||||
key: 'clientId',
|
||||
@@ -17,7 +18,8 @@ const inputsConfig = [
|
||||
},
|
||||
{
|
||||
key: 'clientSecret',
|
||||
label: 'Client Secret'
|
||||
label: 'Client Secret',
|
||||
isSecret: true
|
||||
},
|
||||
{
|
||||
key: 'scope',
|
||||
|
||||
@@ -110,6 +110,7 @@ const FormUrlEncodedParams = ({ item, collection }) => {
|
||||
allowNewlines={true}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
item={item}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@@ -151,7 +151,7 @@ const GraphQLRequestPane = ({ item, collection, leftPaneWidth, onSchemaLoad, tog
|
||||
</div>
|
||||
<GraphQLSchemaActions item={item} collection={collection} onSchemaLoad={setSchema} toggleDocs={toggleDocs} />
|
||||
</div>
|
||||
<section className="flex w-full mt-5">{getTabPanel(focusedTab.requestPaneTab)}</section>
|
||||
<section className="flex w-full mt-5 flex-1">{getTabPanel(focusedTab.requestPaneTab)}</section>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -137,7 +137,7 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
|
||||
) : null}
|
||||
</div>
|
||||
<section
|
||||
className={classnames('flex w-full', {
|
||||
className={classnames('flex w-full flex-1', {
|
||||
'mt-5': !isMultipleContentTab
|
||||
})}
|
||||
>
|
||||
|
||||
@@ -24,7 +24,8 @@ const MultipartFormParams = ({ item, collection }) => {
|
||||
addMultipartFormParam({
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid,
|
||||
type: 'text'
|
||||
type: 'text',
|
||||
value: ''
|
||||
})
|
||||
);
|
||||
};
|
||||
@@ -34,7 +35,8 @@ const MultipartFormParams = ({ item, collection }) => {
|
||||
addMultipartFormParam({
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid,
|
||||
type: 'file'
|
||||
type: 'file',
|
||||
value: []
|
||||
})
|
||||
);
|
||||
};
|
||||
@@ -139,6 +141,7 @@ const MultipartFormParams = ({ item, collection }) => {
|
||||
onRun={handleRun}
|
||||
allowNewlines={true}
|
||||
collection={collection}
|
||||
item={item}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
|
||||
@@ -4,8 +4,7 @@ const StyledWrapper = styled.div`
|
||||
div.CodeMirror {
|
||||
background: ${(props) => props.theme.codemirror.bg};
|
||||
border: solid 1px ${(props) => props.theme.codemirror.border};
|
||||
/* todo: find a better way */
|
||||
height: calc(100vh - 220px);
|
||||
flex: 1 1 0;
|
||||
}
|
||||
|
||||
textarea.cm-editor {
|
||||
|
||||
@@ -209,7 +209,7 @@ export default class QueryEditor extends React.Component {
|
||||
return (
|
||||
<>
|
||||
<StyledWrapper
|
||||
className="h-full w-full relative"
|
||||
className="h-full w-full flex flex-col relative"
|
||||
aria-label="Query Editor"
|
||||
ref={(node) => {
|
||||
this._node = node;
|
||||
|
||||
@@ -147,6 +147,7 @@ const QueryParams = ({ item, collection }) => {
|
||||
}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
item={item}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
@@ -214,6 +215,7 @@ const QueryParams = ({ item, collection }) => {
|
||||
}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
item={item}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -69,6 +69,7 @@ const QueryUrl = ({ item, collection, handleRun }) => {
|
||||
onChange={(newValue) => onUrlChange(newValue)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
highlightPathParams={true}
|
||||
item={item}
|
||||
/>
|
||||
<div className="flex items-center h-full mr-2 cursor-pointer" id="send-request" onClick={handleRun}>
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
div.CodeMirror {
|
||||
/* todo: find a better way */
|
||||
height: calc(100vh - 220px);
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
|
||||
@@ -9,6 +9,7 @@ import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collection
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { headers as StandardHTTPHeaders } from 'know-your-http-well';
|
||||
import { MimeTypes } from 'utils/codemirror/autocompleteConstants';
|
||||
const headerAutoCompleteList = StandardHTTPHeaders.map((e) => e.header);
|
||||
|
||||
const RequestHeaders = ({ item, collection }) => {
|
||||
@@ -115,8 +116,10 @@ const RequestHeaders = ({ item, collection }) => {
|
||||
)
|
||||
}
|
||||
onRun={handleRun}
|
||||
autocomplete={MimeTypes}
|
||||
allowNewlines={true}
|
||||
collection={collection}
|
||||
item={item}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@@ -40,8 +40,8 @@ const Script = ({ item, collection }) => {
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full flex flex-col">
|
||||
<div className="flex-1 mt-2">
|
||||
<div className="mb-1 title text-xs">Pre Request</div>
|
||||
<div className="flex flex-col flex-1 mt-2 gap-y-2">
|
||||
<div className="title text-xs">Pre Request</div>
|
||||
<CodeEditor
|
||||
collection={collection}
|
||||
value={requestScript || ''}
|
||||
@@ -53,8 +53,8 @@ const Script = ({ item, collection }) => {
|
||||
onSave={onSave}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 mt-6">
|
||||
<div className="mt-1 mb-1 title text-xs">Post Response</div>
|
||||
<div className="flex flex-col flex-1 mt-2 gap-y-2">
|
||||
<div className="title text-xs">Post Response</div>
|
||||
<CodeEditor
|
||||
collection={collection}
|
||||
value={responseScript || ''}
|
||||
|
||||
@@ -9,11 +9,11 @@ const Vars = ({ item, collection }) => {
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full flex flex-col">
|
||||
<div className="flex-1 mt-2">
|
||||
<div className="mt-2">
|
||||
<div className="mb-1 title text-xs">Pre Request</div>
|
||||
<VarsTable item={item} collection={collection} vars={requestVars} varType="request" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div>
|
||||
<div className="mt-1 mb-1 title text-xs">Post Response</div>
|
||||
<VarsTable item={item} collection={collection} vars={responseVars} varType="response" />
|
||||
</div>
|
||||
|
||||
@@ -18,6 +18,7 @@ import CollectionSettings from 'components/CollectionSettings';
|
||||
import { DocExplorer } from '@usebruno/graphql-docs';
|
||||
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import FolderSettings from 'components/FolderSettings';
|
||||
|
||||
const MIN_LEFT_PANE_WIDTH = 300;
|
||||
const MIN_RIGHT_PANE_WIDTH = 350;
|
||||
@@ -131,6 +132,10 @@ const RequestTabPanel = () => {
|
||||
if (focusedTab.type === 'collection-settings') {
|
||||
return <CollectionSettings collection={collection} />;
|
||||
}
|
||||
if (focusedTab.type === 'folder-settings') {
|
||||
const folder = findItemInCollection(collection, focusedTab.folderUid);
|
||||
return <FolderSettings collection={collection} folder={folder} />;
|
||||
}
|
||||
|
||||
const item = findItemInCollection(collection, activeTabUid);
|
||||
if (!item || !item.uid) {
|
||||
@@ -153,10 +158,9 @@ const RequestTabPanel = () => {
|
||||
<section className="main flex flex-grow pb-4 relative">
|
||||
<section className="request-pane">
|
||||
<div
|
||||
className="px-4"
|
||||
className="px-4 h-full"
|
||||
style={{
|
||||
width: `${Math.max(leftPaneWidth, MIN_LEFT_PANE_WIDTH)}px`,
|
||||
height: `calc(100% - ${DEFAULT_PADDING}px)`
|
||||
width: `${Math.max(leftPaneWidth, MIN_LEFT_PANE_WIDTH)}px`
|
||||
}}
|
||||
>
|
||||
{item.type === 'graphql-request' ? (
|
||||
|
||||
@@ -44,7 +44,7 @@ const CollectionToolBar = ({ collection }) => {
|
||||
<div className="flex items-center p-2">
|
||||
<div className="flex flex-1 items-center cursor-pointer hover:underline" onClick={viewCollectionSettings}>
|
||||
<IconFiles size={18} strokeWidth={1.5} />
|
||||
<span className="ml-2 mr-4 font-semibold">{collection.name}</span>
|
||||
<span className="ml-2 mr-4 font-semibold">{collection?.name}</span>
|
||||
</div>
|
||||
<div className="flex flex-1 items-center justify-end">
|
||||
<span className="mr-2">
|
||||
|
||||
@@ -1,22 +1,30 @@
|
||||
import React from 'react';
|
||||
import { IconVariable, IconSettings, IconRun } from '@tabler/icons';
|
||||
import { IconVariable, IconSettings, IconRun, IconFolder } from '@tabler/icons';
|
||||
|
||||
const SpecialTab = ({ handleCloseClick, type }) => {
|
||||
const getTabInfo = (type) => {
|
||||
const SpecialTab = ({ handleCloseClick, type, tabName }) => {
|
||||
const getTabInfo = (type, tabName) => {
|
||||
switch (type) {
|
||||
case 'collection-settings': {
|
||||
return (
|
||||
<>
|
||||
<IconSettings size={18} strokeWidth={1.5} className="text-yellow-600" />
|
||||
<span className="ml-1">Collection</span>
|
||||
<span className="ml-1 leading-6">Collection</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
case 'folder-settings': {
|
||||
return (
|
||||
<div className="flex items-center flex-nowrap overflow-hidden">
|
||||
<IconFolder size={18} strokeWidth={1.5} className="text-yellow-600 min-w-[18px]" />
|
||||
<span className="ml-1 leading-6 truncate">{tabName || 'Folder'}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
case 'variables': {
|
||||
return (
|
||||
<>
|
||||
<IconVariable size={18} strokeWidth={1.5} className="text-yellow-600" />
|
||||
<span className="ml-1">Variables</span>
|
||||
<span className="ml-1 leading-6">Variables</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -24,7 +32,7 @@ const SpecialTab = ({ handleCloseClick, type }) => {
|
||||
return (
|
||||
<>
|
||||
<IconRun size={18} strokeWidth={1.5} className="text-yellow-600" />
|
||||
<span className="ml-1">Runner</span>
|
||||
<span className="ml-1 leading-6">Runner</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -33,7 +41,7 @@ const SpecialTab = ({ handleCloseClick, type }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center tab-label pl-2">{getTabInfo(type)}</div>
|
||||
<div className="flex items-center tab-label pl-2">{getTabInfo(type, tabName)}</div>
|
||||
<div className="flex px-2 close-icon-container" onClick={(e) => handleCloseClick(e)}>
|
||||
<svg focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512" className="close-icon">
|
||||
<path
|
||||
|
||||
@@ -13,7 +13,7 @@ import RequestTabNotFound from './RequestTabNotFound';
|
||||
import SpecialTab from './SpecialTab';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const RequestTab = ({ tab, collection }) => {
|
||||
const RequestTab = ({ tab, collection, folderUid }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
const [showConfirmClose, setShowConfirmClose] = useState(false);
|
||||
@@ -80,11 +80,15 @@ const RequestTab = ({ tab, collection }) => {
|
||||
|
||||
return color;
|
||||
};
|
||||
|
||||
if (['collection-settings', 'variables', 'collection-runner'].includes(tab.type)) {
|
||||
const folder = folderUid ? findItemInCollection(collection, folderUid) : null;
|
||||
if (['collection-settings', 'folder-settings', 'variables', 'collection-runner'].includes(tab.type)) {
|
||||
return (
|
||||
<StyledWrapper className="flex items-center justify-between tab-container px-1">
|
||||
<SpecialTab handleCloseClick={handleCloseClick} type={tab.type} />
|
||||
{tab.type === 'folder-settings' ? (
|
||||
<SpecialTab handleCloseClick={handleCloseClick} type={tab.type} tabName={folder?.name} />
|
||||
) : (
|
||||
<SpecialTab handleCloseClick={handleCloseClick} type={tab.type} />
|
||||
)}
|
||||
</StyledWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@ const Wrapper = styled.div`
|
||||
display: none;
|
||||
}
|
||||
|
||||
scrollbar-width: none;
|
||||
|
||||
li {
|
||||
display: inline-flex;
|
||||
max-width: 150px;
|
||||
|
||||
@@ -75,7 +75,6 @@ const RequestTabs = () => {
|
||||
'has-chevrons': showChevrons
|
||||
});
|
||||
};
|
||||
|
||||
// Todo: Must support ephemeral requests
|
||||
return (
|
||||
<StyledWrapper className={getRootClassname()}>
|
||||
@@ -111,7 +110,7 @@ const RequestTabs = () => {
|
||||
role="tab"
|
||||
onClick={() => handleClick(tab)}
|
||||
>
|
||||
<RequestTab key={tab.uid} tab={tab} collection={activeCollection} />
|
||||
<RequestTab key={tab.uid} tab={tab} collection={activeCollection} folderUid={tab.folderUid} />
|
||||
</li>
|
||||
);
|
||||
})
|
||||
|
||||
@@ -50,6 +50,7 @@ const CodeView = ({ language, item }) => {
|
||||
</CopyToClipboard>
|
||||
<CodeEditor
|
||||
readOnly
|
||||
collection={collection}
|
||||
value={snippet}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
theme={displayedTheme}
|
||||
|
||||
@@ -73,7 +73,7 @@ const GenerateCodeItem = ({ collection, item, onClose }) => {
|
||||
const interpolatedUrl = interpolateUrl({
|
||||
url: requestUrl,
|
||||
envVars,
|
||||
collectionVariables: collection.collectionVariables,
|
||||
runtimeVariables: collection.runtimeVariables,
|
||||
processEnvVars: collection.processEnvVariables
|
||||
});
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ import { hideHomePage } from 'providers/ReduxStore/slices/app';
|
||||
import toast from 'react-hot-toast';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import NetworkError from 'components/ResponsePane/NetworkError/index';
|
||||
import { uuid } from 'utils/common';
|
||||
|
||||
const CollectionItem = ({ item, collection, searchText }) => {
|
||||
const tabs = useSelector((state) => state.tabs.tabs);
|
||||
@@ -188,6 +189,16 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
toast.error('URL is required');
|
||||
}
|
||||
};
|
||||
const viewFolderSettings = () => {
|
||||
dispatch(
|
||||
addTab({
|
||||
uid: uuid(),
|
||||
collectionUid: collection.uid,
|
||||
folderUid: item.uid,
|
||||
type: 'folder-settings'
|
||||
})
|
||||
);
|
||||
};
|
||||
const requestItems = sortRequestItems(filter(item.items, (i) => isItemARequest(i)));
|
||||
const folderItems = sortFolderItems(filter(item.items, (i) => isItemAFolder(i)));
|
||||
|
||||
@@ -345,6 +356,17 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
>
|
||||
Delete
|
||||
</div>
|
||||
{isFolder && (
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={(e) => {
|
||||
dropdownTippyRef.current.hide();
|
||||
viewFolderSettings();
|
||||
}}
|
||||
>
|
||||
Settings
|
||||
</div>
|
||||
)}
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -60,7 +60,7 @@ const ImportCollection = ({ onClose, handleSubmit }) => {
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className={`rounded bg-transparent px-2.5 py-1 text-xs font-semibold text-slate-900 dark:text-slate-50 shadow-sm ring-1 ring-inset ring-zinc-300 dark:ring-zinc-500 hover:bg-gray-50 dark:hover:bg-zinc-700
|
||||
className={`rounded bg-transparent px-2.5 py-1 text-xs font-semibold text-zinc-900 dark:text-zinc-50 shadow-sm ring-1 ring-inset ring-zinc-300 dark:ring-zinc-500 hover:bg-gray-50 dark:hover:bg-zinc-700
|
||||
${className}`}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -161,7 +161,16 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<Modal size="md" title="New Request" confirmText="Create" handleConfirm={onSubmit} handleCancel={onClose}>
|
||||
<form className="bruno-form" onSubmit={formik.handleSubmit}>
|
||||
<form
|
||||
className="bruno-form"
|
||||
onSubmit={formik.handleSubmit}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
formik.handleSubmit();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<label htmlFor="requestName" className="block font-semibold">
|
||||
Type
|
||||
|
||||
@@ -129,7 +129,7 @@ const Sidebar = () => {
|
||||
Star
|
||||
</GitHubButton> */}
|
||||
</div>
|
||||
<div className="flex flex-grow items-center justify-end text-xs mr-2">v1.18.0</div>
|
||||
<div className="flex flex-grow items-center justify-end text-xs mr-2">v1.24.0</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import React, { Component } from 'react';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import { getAllVariables } from 'utils/collections';
|
||||
import { defineCodeMirrorBrunoVariablesMode } from 'utils/common/codemirror';
|
||||
import { defineCodeMirrorBrunoVariablesMode, MaskedEditor } from 'utils/common/codemirror';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { IconEye, IconEyeOff } from '@tabler/icons';
|
||||
|
||||
let CodeMirror;
|
||||
const SERVER_RENDERED = typeof navigator === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true;
|
||||
@@ -20,12 +21,28 @@ class SingleLineEditor extends Component {
|
||||
this.cachedValue = props.value || '';
|
||||
this.editorRef = React.createRef();
|
||||
this.variables = {};
|
||||
|
||||
this.state = {
|
||||
maskInput: props.isSecret || false // Always mask the input by default (if it's a secret)
|
||||
};
|
||||
}
|
||||
componentDidMount() {
|
||||
// Initialize CodeMirror as a single line editor
|
||||
/** @type {import("codemirror").Editor} */
|
||||
const variables = getAllVariables(this.props.collection, this.props.item);
|
||||
|
||||
const runHandler = () => {
|
||||
if (this.props.onRun) {
|
||||
this.props.onRun();
|
||||
}
|
||||
};
|
||||
const saveHandler = () => {
|
||||
if (this.props.onSave) {
|
||||
this.props.onSave();
|
||||
}
|
||||
};
|
||||
const noopHandler = () => {};
|
||||
|
||||
this.editor = CodeMirror(this.editorRef.current, {
|
||||
lineWrapping: false,
|
||||
lineNumbers: false,
|
||||
@@ -37,21 +54,9 @@ class SingleLineEditor extends Component {
|
||||
scrollbarStyle: null,
|
||||
tabindex: 0,
|
||||
extraKeys: {
|
||||
Enter: () => {
|
||||
if (this.props.onRun) {
|
||||
this.props.onRun();
|
||||
}
|
||||
},
|
||||
'Ctrl-Enter': () => {
|
||||
if (this.props.onRun) {
|
||||
this.props.onRun();
|
||||
}
|
||||
},
|
||||
'Cmd-Enter': () => {
|
||||
if (this.props.onRun) {
|
||||
this.props.onRun();
|
||||
}
|
||||
},
|
||||
Enter: runHandler,
|
||||
'Ctrl-Enter': runHandler,
|
||||
'Cmd-Enter': runHandler,
|
||||
'Alt-Enter': () => {
|
||||
if (this.props.allowNewlines) {
|
||||
this.editor.setValue(this.editor.getValue() + '\n');
|
||||
@@ -60,23 +65,11 @@ class SingleLineEditor extends Component {
|
||||
this.props.onRun();
|
||||
}
|
||||
},
|
||||
'Shift-Enter': () => {
|
||||
if (this.props.onRun) {
|
||||
this.props.onRun();
|
||||
}
|
||||
},
|
||||
'Cmd-S': () => {
|
||||
if (this.props.onSave) {
|
||||
this.props.onSave();
|
||||
}
|
||||
},
|
||||
'Ctrl-S': () => {
|
||||
if (this.props.onSave) {
|
||||
this.props.onSave();
|
||||
}
|
||||
},
|
||||
'Cmd-F': () => {},
|
||||
'Ctrl-F': () => {},
|
||||
'Shift-Enter': runHandler,
|
||||
'Cmd-S': saveHandler,
|
||||
'Ctrl-S': saveHandler,
|
||||
'Cmd-F': noopHandler,
|
||||
'Ctrl-F': noopHandler,
|
||||
// Tabbing disabled to make tabindex work
|
||||
Tab: false,
|
||||
'Shift-Tab': false
|
||||
@@ -93,8 +86,24 @@ class SingleLineEditor extends Component {
|
||||
this.editor.setValue(String(this.props.value) || '');
|
||||
this.editor.on('change', this._onEdit);
|
||||
this.addOverlay(variables);
|
||||
this._enableMaskedEditor(this.props.isSecret);
|
||||
this.setState({ maskInput: this.props.isSecret });
|
||||
}
|
||||
|
||||
/** Enable or disable masking the rendered content of the editor */
|
||||
_enableMaskedEditor = (enabled) => {
|
||||
if (typeof enabled !== 'boolean') return;
|
||||
|
||||
console.log('Enabling masked editor: ' + enabled);
|
||||
if (enabled == true) {
|
||||
if (!this.maskedEditor) this.maskedEditor = new MaskedEditor(this.editor, '*');
|
||||
this.maskedEditor.enable();
|
||||
} else {
|
||||
this.maskedEditor?.disable();
|
||||
this.maskedEditor = null;
|
||||
}
|
||||
};
|
||||
|
||||
_onEdit = () => {
|
||||
if (!this.ignoreChangeEvent && this.editor) {
|
||||
this.cachedValue = this.editor.getValue();
|
||||
@@ -122,6 +131,12 @@ class SingleLineEditor extends Component {
|
||||
this.cachedValue = String(this.props.value);
|
||||
this.editor.setValue(String(this.props.value) || '');
|
||||
}
|
||||
if (!isEqual(this.props.isSecret, prevProps.isSecret)) {
|
||||
// If the secret flag has changed, update the editor to reflect the change
|
||||
this._enableMaskedEditor(this.props.isSecret);
|
||||
// also set the maskInput flag to the new value
|
||||
this.setState({ maskInput: this.props.isSecret });
|
||||
}
|
||||
this.ignoreChangeEvent = false;
|
||||
}
|
||||
|
||||
@@ -131,12 +146,39 @@ class SingleLineEditor extends Component {
|
||||
|
||||
addOverlay = (variables) => {
|
||||
this.variables = variables;
|
||||
defineCodeMirrorBrunoVariablesMode(variables, 'text/plain');
|
||||
this.editor.setOption('mode', 'combinedmode');
|
||||
defineCodeMirrorBrunoVariablesMode(variables, 'text/plain', this.props.highlightPathParams);
|
||||
this.editor.setOption('mode', 'brunovariables');
|
||||
};
|
||||
|
||||
toggleVisibleSecret = () => {
|
||||
const isVisible = !this.state.maskInput;
|
||||
this.setState({ maskInput: isVisible });
|
||||
this._enableMaskedEditor(isVisible);
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Eye icon to show/hide the secret value
|
||||
* @returns ReactComponent The eye icon
|
||||
*/
|
||||
secretEye = (isSecret) => {
|
||||
return isSecret === true ? (
|
||||
<button className="mx-2" onClick={() => this.toggleVisibleSecret()}>
|
||||
{this.state.maskInput === true ? (
|
||||
<IconEyeOff size={18} strokeWidth={2} />
|
||||
) : (
|
||||
<IconEye size={18} strokeWidth={2} />
|
||||
)}
|
||||
</button>
|
||||
) : null;
|
||||
};
|
||||
|
||||
render() {
|
||||
return <StyledWrapper ref={this.editorRef} className="single-line-editor"></StyledWrapper>;
|
||||
return (
|
||||
<div className="flex flex-row justify-between w-full">
|
||||
<StyledWrapper ref={this.editorRef} className="single-line-editor grow" />
|
||||
{this.secretEye(this.props.isSecret)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
export default SingleLineEditor;
|
||||
|
||||
@@ -62,10 +62,10 @@ const EnvVariables = ({ collection, theme }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const CollectionVariables = ({ collection, theme }) => {
|
||||
const collectionVariablesFound = Object.keys(collection.collectionVariables).length > 0;
|
||||
const RuntimeVariables = ({ collection, theme }) => {
|
||||
const runtimeVariablesFound = Object.keys(collection.runtimeVariables).length > 0;
|
||||
|
||||
const collectionVariableArray = Object.entries(collection.collectionVariables).map(([name, value]) => ({
|
||||
const runtimeVariableArray = Object.entries(collection.runtimeVariables).map(([name, value]) => ({
|
||||
name,
|
||||
value,
|
||||
secret: false
|
||||
@@ -73,11 +73,11 @@ const CollectionVariables = ({ collection, theme }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1 className="font-semibold mb-2">Collection Variables</h1>
|
||||
{collectionVariablesFound ? (
|
||||
<KeyValueExplorer data={collectionVariableArray} theme={theme} />
|
||||
<h1 className="font-semibold mb-2">Runtime Variables</h1>
|
||||
{runtimeVariablesFound ? (
|
||||
<KeyValueExplorer data={runtimeVariableArray} theme={theme} />
|
||||
) : (
|
||||
<div className="muted text-xs">No collection variables found</div>
|
||||
<div className="muted text-xs">No runtime variables found</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
@@ -90,13 +90,13 @@ const VariablesEditor = ({ collection }) => {
|
||||
|
||||
return (
|
||||
<StyledWrapper className="px-4 py-4">
|
||||
<CollectionVariables collection={collection} theme={reactInspectorTheme} />
|
||||
<RuntimeVariables collection={collection} theme={reactInspectorTheme} />
|
||||
<EnvVariables collection={collection} theme={reactInspectorTheme} />
|
||||
|
||||
<div className="mt-8 muted text-xs">
|
||||
Note: As of today, collection variables can only be set via the API -{' '}
|
||||
<span className="font-medium">getVar()</span> and <span className="font-medium">setVar()</span>. <br />
|
||||
In the next release, we will add a UI to set and modify collection variables.
|
||||
Note: As of today, runtime variables can only be set via the API - <span className="font-medium">getVar()</span>{' '}
|
||||
and <span className="font-medium">setVar()</span>. <br />
|
||||
In the next release, we will add a UI to set and modify runtime variables.
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -168,7 +168,6 @@ const GlobalStyle = createGlobalStyle`
|
||||
// (macos scrollbar styling is the ideal style reference)
|
||||
@media not all and (pointer: coarse) {
|
||||
* {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: ${(props) => props.theme.scrollbar.color};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { get } from 'lodash';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { refreshScreenWidth } from 'providers/ReduxStore/slices/app';
|
||||
import ConfirmAppClose from './ConfirmAppClose';
|
||||
@@ -18,6 +19,13 @@ export const AppProvider = (props) => {
|
||||
dispatch(refreshScreenWidth());
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const platform = get(navigator, 'platform', '');
|
||||
if(platform && platform.toLowerCase().indexOf('mac') > -1) {
|
||||
document.body.classList.add('os-mac');
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
dispatch(refreshScreenWidth());
|
||||
|
||||
@@ -60,7 +60,7 @@ const trackStart = () => {
|
||||
event: 'start',
|
||||
properties: {
|
||||
os: platformLib.os.family,
|
||||
version: '1.18.0'
|
||||
version: '1.24.0'
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -14,10 +14,10 @@ import {
|
||||
findParentItemInCollection,
|
||||
getItemsToResequence,
|
||||
isItemAFolder,
|
||||
refreshUidsInItem,
|
||||
isItemARequest,
|
||||
moveCollectionItem,
|
||||
moveCollectionItemToRootOfCollection,
|
||||
refreshUidsInItem,
|
||||
transformRequestToSaveToFilesystem
|
||||
} from 'utils/collections';
|
||||
import { uuid, waitForNextTick } from 'utils/common';
|
||||
@@ -41,6 +41,7 @@ import { closeAllCollectionTabs } from 'providers/ReduxStore/slices/tabs';
|
||||
import { resolveRequestFilename } from 'utils/common/platform';
|
||||
import { parseQueryParams, splitOnFirst } from 'utils/url/index';
|
||||
import { sendCollectionOauth2Request as _sendCollectionOauth2Request } from 'utils/network/index';
|
||||
import { name } from 'file-loader';
|
||||
|
||||
export const renameCollection = (newName, collectionUid) => (dispatch, getState) => {
|
||||
const state = getState();
|
||||
@@ -143,7 +144,42 @@ export const saveCollectionRoot = (collectionUid) => (dispatch, getState) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const sendCollectionOauth2Request = (collectionUid) => (dispatch, getState) => {
|
||||
export const saveFolderRoot = (collectionUid, folderUid) => (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const collection = findCollectionByUid(state.collections.collections, collectionUid);
|
||||
const folder = findItemInCollection(collection, folderUid);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!collection) {
|
||||
return reject(new Error('Collection not found'));
|
||||
}
|
||||
|
||||
if (!folder) {
|
||||
return reject(new Error('Folder not found'));
|
||||
}
|
||||
console.log(collection);
|
||||
|
||||
const { ipcRenderer } = window;
|
||||
|
||||
const folderData = {
|
||||
name: folder.name,
|
||||
pathname: folder.pathname,
|
||||
root: folder.root
|
||||
};
|
||||
console.log(folderData);
|
||||
|
||||
ipcRenderer
|
||||
.invoke('renderer:save-folder-root', folderData)
|
||||
.then(() => toast.success('Folder Settings saved successfully'))
|
||||
.then(resolve)
|
||||
.catch((err) => {
|
||||
toast.error('Failed to save folder settings!');
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const sendCollectionOauth2Request = (collectionUid, itemUid) => (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const collection = findCollectionByUid(state.collections.collections, collectionUid);
|
||||
|
||||
@@ -156,7 +192,7 @@ export const sendCollectionOauth2Request = (collectionUid) => (dispatch, getStat
|
||||
|
||||
const environment = findEnvironmentInCollection(collectionCopy, collection.activeEnvironmentUid);
|
||||
|
||||
_sendCollectionOauth2Request(collection, environment, collectionCopy.collectionVariables)
|
||||
_sendCollectionOauth2Request(collection, environment, collectionCopy.runtimeVariables)
|
||||
.then((response) => {
|
||||
if (response?.data?.error) {
|
||||
toast.error(response?.data?.error);
|
||||
@@ -184,9 +220,8 @@ export const sendRequest = (item, collectionUid) => (dispatch, getState) => {
|
||||
const itemCopy = cloneDeep(item || {});
|
||||
const collectionCopy = cloneDeep(collection);
|
||||
|
||||
const environment = findEnvironmentInCollection(collectionCopy, collection.activeEnvironmentUid);
|
||||
|
||||
sendNetworkRequest(itemCopy, collection, environment, collectionCopy.collectionVariables)
|
||||
const environment = findEnvironmentInCollection(collectionCopy, collectionCopy.activeEnvironmentUid);
|
||||
sendNetworkRequest(itemCopy, collectionCopy, environment, collectionCopy.runtimeVariables)
|
||||
.then((response) => {
|
||||
return dispatch(
|
||||
responseReceived({
|
||||
@@ -276,7 +311,7 @@ export const runCollectionFolder = (collectionUid, folderUid, recursive) => (dis
|
||||
folder,
|
||||
collectionCopy,
|
||||
environment,
|
||||
collectionCopy.collectionVariables,
|
||||
collectionCopy.runtimeVariables,
|
||||
recursive
|
||||
)
|
||||
.then(resolve)
|
||||
@@ -998,7 +1033,7 @@ export const openCollectionEvent = (uid, pathname, brunoConfig) => (dispatch, ge
|
||||
name: brunoConfig.name,
|
||||
pathname: pathname,
|
||||
items: [],
|
||||
collectionVariables: {},
|
||||
runtimeVariables: {},
|
||||
brunoConfig: brunoConfig
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { uuid } from 'utils/common';
|
||||
import path from 'path';
|
||||
import { find, map, forOwn, concat, filter, each, cloneDeep, get, set, debounce } from 'lodash';
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import {
|
||||
@@ -33,6 +34,8 @@ export const collectionsSlice = createSlice({
|
||||
|
||||
collection.settingsSelectedTab = 'headers';
|
||||
|
||||
collection.folderLevelSettingsSelectedTab = {};
|
||||
|
||||
// TODO: move this to use the nextAction approach
|
||||
// last action is used to track the last action performed on the collection
|
||||
// this is optional
|
||||
@@ -89,7 +92,7 @@ export const collectionsSlice = createSlice({
|
||||
}
|
||||
},
|
||||
updateSettingsSelectedTab: (state, action) => {
|
||||
const { collectionUid, tab } = action.payload;
|
||||
const { collectionUid, folderUid, tab } = action.payload;
|
||||
|
||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||
|
||||
@@ -97,6 +100,19 @@ export const collectionsSlice = createSlice({
|
||||
collection.settingsSelectedTab = tab;
|
||||
}
|
||||
},
|
||||
updatedFolderSettingsSelectedTab: (state, action) => {
|
||||
const { collectionUid, folderUid, tab } = action.payload;
|
||||
|
||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||
|
||||
if (collection) {
|
||||
const folder = findItemInCollection(collection, folderUid);
|
||||
|
||||
if (folder) {
|
||||
collection.folderLevelSettingsSelectedTab[folderUid] = tab;
|
||||
}
|
||||
}
|
||||
},
|
||||
collectionUnlinkEnvFileEvent: (state, action) => {
|
||||
const { data: environment, meta } = action.payload;
|
||||
const collection = findCollectionByUid(state.collections, meta.collectionUid);
|
||||
@@ -184,7 +200,7 @@ export const collectionsSlice = createSlice({
|
||||
}
|
||||
},
|
||||
scriptEnvironmentUpdateEvent: (state, action) => {
|
||||
const { collectionUid, envVariables, collectionVariables } = action.payload;
|
||||
const { collectionUid, envVariables, runtimeVariables } = action.payload;
|
||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||
|
||||
if (collection) {
|
||||
@@ -214,7 +230,7 @@ export const collectionsSlice = createSlice({
|
||||
});
|
||||
}
|
||||
|
||||
collection.collectionVariables = collectionVariables;
|
||||
collection.runtimeVariables = runtimeVariables;
|
||||
}
|
||||
},
|
||||
processEnvUpdateEvent: (state, action) => {
|
||||
@@ -701,7 +717,7 @@ export const collectionsSlice = createSlice({
|
||||
uid: uuid(),
|
||||
type: action.payload.type,
|
||||
name: '',
|
||||
value: '',
|
||||
value: action.payload.value,
|
||||
description: '',
|
||||
enabled: true
|
||||
});
|
||||
@@ -1114,6 +1130,135 @@ export const collectionsSlice = createSlice({
|
||||
set(collection, 'root.docs', action.payload.docs);
|
||||
}
|
||||
},
|
||||
addFolderHeader: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
const folder = collection ? findItemInCollection(collection, action.payload.folderUid) : null;
|
||||
if (folder) {
|
||||
const headers = get(folder, 'root.request.headers', []);
|
||||
headers.push({
|
||||
uid: uuid(),
|
||||
name: '',
|
||||
value: '',
|
||||
description: '',
|
||||
enabled: true
|
||||
});
|
||||
set(folder, 'root.request.headers', headers);
|
||||
}
|
||||
},
|
||||
updateFolderHeader: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
const folder = collection ? findItemInCollection(collection, action.payload.folderUid) : null;
|
||||
if (folder) {
|
||||
const headers = get(folder, 'root.request.headers', []);
|
||||
const header = find(headers, (h) => h.uid === action.payload.header.uid);
|
||||
if (header) {
|
||||
header.name = action.payload.header.name;
|
||||
header.value = action.payload.header.value;
|
||||
header.description = action.payload.header.description;
|
||||
header.enabled = action.payload.header.enabled;
|
||||
}
|
||||
}
|
||||
},
|
||||
deleteFolderHeader: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
const folder = collection ? findItemInCollection(collection, action.payload.folderUid) : null;
|
||||
if (folder) {
|
||||
let headers = get(folder, 'root.request.headers', []);
|
||||
headers = filter(headers, (h) => h.uid !== action.payload.headerUid);
|
||||
set(folder, 'root.request.headers', headers);
|
||||
}
|
||||
},
|
||||
addFolderVar: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
const folder = collection ? findItemInCollection(collection, action.payload.folderUid) : null;
|
||||
const type = action.payload.type;
|
||||
if (folder) {
|
||||
if (type === 'request') {
|
||||
const vars = get(folder, 'root.request.vars.req', []);
|
||||
vars.push({
|
||||
uid: uuid(),
|
||||
name: '',
|
||||
value: '',
|
||||
enabled: true
|
||||
});
|
||||
set(folder, 'root.request.vars.req', vars);
|
||||
} else if (type === 'response') {
|
||||
const vars = get(folder, 'root.request.vars.res', []);
|
||||
vars.push({
|
||||
uid: uuid(),
|
||||
name: '',
|
||||
value: '',
|
||||
enabled: true
|
||||
});
|
||||
set(folder, 'root.request.vars.res', vars);
|
||||
}
|
||||
}
|
||||
},
|
||||
updateFolderVar: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
const folder = collection ? findItemInCollection(collection, action.payload.folderUid) : null;
|
||||
const type = action.payload.type;
|
||||
if (folder) {
|
||||
if (type === 'request') {
|
||||
let vars = get(folder, '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(folder, 'root.request.vars.req', vars);
|
||||
} else if (type === 'response') {
|
||||
let vars = get(folder, '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(folder, 'root.request.vars.res', vars);
|
||||
}
|
||||
}
|
||||
},
|
||||
deleteFolderVar: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
const folder = collection ? findItemInCollection(collection, action.payload.folderUid) : null;
|
||||
const type = action.payload.type;
|
||||
if (folder) {
|
||||
if (type === 'request') {
|
||||
let vars = get(folder, 'root.request.vars.req', []);
|
||||
vars = filter(vars, (h) => h.uid !== action.payload.varUid);
|
||||
set(folder, 'root.request.vars.req', vars);
|
||||
} else if (type === 'response') {
|
||||
let vars = get(folder, 'root.request.vars.res', []);
|
||||
vars = filter(vars, (h) => h.uid !== action.payload.varUid);
|
||||
set(folder, 'root.request.vars.res', vars);
|
||||
}
|
||||
}
|
||||
},
|
||||
updateFolderRequestScript: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
const folder = collection ? findItemInCollection(collection, action.payload.folderUid) : null;
|
||||
if (folder) {
|
||||
set(folder, 'root.request.script.req', action.payload.script);
|
||||
}
|
||||
},
|
||||
updateFolderResponseScript: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
const folder = collection ? findItemInCollection(collection, action.payload.folderUid) : null;
|
||||
if (folder) {
|
||||
set(folder, 'root.request.script.res', action.payload.script);
|
||||
}
|
||||
},
|
||||
updateFolderTests: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
const folder = collection ? findItemInCollection(collection, action.payload.folderUid) : null;
|
||||
if (folder) {
|
||||
set(folder, 'root.request.tests', action.payload.tests);
|
||||
}
|
||||
},
|
||||
addCollectionHeader: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
|
||||
@@ -1155,8 +1300,8 @@ export const collectionsSlice = createSlice({
|
||||
collectionAddFileEvent: (state, action) => {
|
||||
const file = action.payload.file;
|
||||
const isCollectionRoot = file.meta.collectionRoot ? true : false;
|
||||
const isFolderRoot = file.meta.folderRoot ? true : false;
|
||||
const collection = findCollectionByUid(state.collections, file.meta.collectionUid);
|
||||
|
||||
if (isCollectionRoot) {
|
||||
if (collection) {
|
||||
collection.root = file.data;
|
||||
@@ -1164,6 +1309,15 @@ export const collectionsSlice = createSlice({
|
||||
return;
|
||||
}
|
||||
|
||||
if (isFolderRoot) {
|
||||
const folderPath = getDirectoryName(file.meta.pathname);
|
||||
const folderItem = findItemInCollectionByPathname(collection, folderPath);
|
||||
if (folderItem) {
|
||||
folderItem.root = file.data;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (collection) {
|
||||
const dirname = getDirectoryName(file.meta.pathname);
|
||||
const subDirectories = getSubdirectoriesFromRoot(collection.pathname, dirname);
|
||||
@@ -1187,7 +1341,7 @@ export const collectionsSlice = createSlice({
|
||||
currentSubItems = childItem.items;
|
||||
}
|
||||
|
||||
if (!currentSubItems.find((f) => f.name === file.meta.name)) {
|
||||
if (file.meta.name != 'folder.bru' && !currentSubItems.find((f) => f.name === file.meta.name)) {
|
||||
// this happens when you rename a file
|
||||
// the add event might get triggered first, before the unlink event
|
||||
// this results in duplicate uids causing react renderer to go mad
|
||||
@@ -1412,29 +1566,29 @@ export const collectionsSlice = createSlice({
|
||||
}
|
||||
|
||||
if (type === 'request-sent') {
|
||||
const item = collection.runnerResult.items.find((i) => i.uid === request.uid);
|
||||
const item = collection.runnerResult.items.findLast((i) => i.uid === request.uid);
|
||||
item.status = 'running';
|
||||
item.requestSent = action.payload.requestSent;
|
||||
}
|
||||
|
||||
if (type === 'response-received') {
|
||||
const item = collection.runnerResult.items.find((i) => i.uid === request.uid);
|
||||
const item = collection.runnerResult.items.findLast((i) => i.uid === request.uid);
|
||||
item.status = 'completed';
|
||||
item.responseReceived = action.payload.responseReceived;
|
||||
}
|
||||
|
||||
if (type === 'test-results') {
|
||||
const item = collection.runnerResult.items.find((i) => i.uid === request.uid);
|
||||
const item = collection.runnerResult.items.findLast((i) => i.uid === request.uid);
|
||||
item.testResults = action.payload.testResults;
|
||||
}
|
||||
|
||||
if (type === 'assertion-results') {
|
||||
const item = collection.runnerResult.items.find((i) => i.uid === request.uid);
|
||||
const item = collection.runnerResult.items.findLast((i) => i.uid === request.uid);
|
||||
item.assertionResults = action.payload.assertionResults;
|
||||
}
|
||||
|
||||
if (type === 'error') {
|
||||
const item = collection.runnerResult.items.find((i) => i.uid === request.uid);
|
||||
const item = collection.runnerResult.items.findLast((i) => i.uid === request.uid);
|
||||
item.error = action.payload.error;
|
||||
item.responseReceived = action.payload.responseReceived;
|
||||
item.status = 'error';
|
||||
@@ -1474,6 +1628,7 @@ export const {
|
||||
sortCollections,
|
||||
updateLastAction,
|
||||
updateSettingsSelectedTab,
|
||||
updatedFolderSettingsSelectedTab,
|
||||
collectionUnlinkEnvFileEvent,
|
||||
saveEnvironment,
|
||||
selectEnvironment,
|
||||
@@ -1521,6 +1676,15 @@ export const {
|
||||
addVar,
|
||||
updateVar,
|
||||
deleteVar,
|
||||
addFolderHeader,
|
||||
updateFolderHeader,
|
||||
deleteFolderHeader,
|
||||
addFolderVar,
|
||||
updateFolderVar,
|
||||
deleteFolderVar,
|
||||
updateFolderRequestScript,
|
||||
updateFolderResponseScript,
|
||||
updateFolderTests,
|
||||
addCollectionHeader,
|
||||
updateCollectionHeader,
|
||||
deleteCollectionHeader,
|
||||
|
||||
@@ -38,7 +38,8 @@ export const tabsSlice = createSlice({
|
||||
requestPaneWidth: null,
|
||||
requestPaneTab: action.payload.requestPaneTab || 'params',
|
||||
responsePaneTab: 'response',
|
||||
type: action.payload.type || 'request'
|
||||
type: action.payload.type || 'request',
|
||||
...(action.payload.folderUid ? { folderUid: action.payload.folderUid } : {})
|
||||
});
|
||||
state.activeTabUid = action.payload.uid;
|
||||
},
|
||||
|
||||
@@ -58,6 +58,15 @@ body::-webkit-scrollbar-thumb,
|
||||
border-radius: 5rem;
|
||||
}
|
||||
|
||||
/*
|
||||
* Mac-specific scrollbar styling
|
||||
* This ensures that scrollbars are only visible when the user starts to scroll,
|
||||
* providing a cleaner and more minimalistic appearance.
|
||||
*/
|
||||
body.os-mac * {
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
/*
|
||||
* todo: this will be supported in the future to be changed via applying a theme
|
||||
* making all the checkboxes and radios bigger
|
||||
|
||||
@@ -2,10 +2,16 @@ const createContentType = (mode) => {
|
||||
switch (mode) {
|
||||
case 'json':
|
||||
return 'application/json';
|
||||
case 'text':
|
||||
return 'text/plain';
|
||||
case 'xml':
|
||||
return 'application/xml';
|
||||
case 'sparql':
|
||||
return 'application/sparql-query';
|
||||
case 'formUrlEncoded':
|
||||
return 'application/x-www-form-urlencoded';
|
||||
case 'graphql':
|
||||
return 'application/json';
|
||||
case 'multipartForm':
|
||||
return 'multipart/form-data';
|
||||
default:
|
||||
@@ -13,13 +19,19 @@ const createContentType = (mode) => {
|
||||
}
|
||||
};
|
||||
|
||||
const createHeaders = (headers) => {
|
||||
return headers
|
||||
const createHeaders = (request, headers) => {
|
||||
const enabledHeaders = headers
|
||||
.filter((header) => header.enabled)
|
||||
.map((header) => ({
|
||||
name: header.name,
|
||||
value: header.value
|
||||
}));
|
||||
|
||||
const contentType = createContentType(request.body?.mode);
|
||||
if (contentType !== '') {
|
||||
enabledHeaders.push({ name: 'content-type', value: contentType });
|
||||
}
|
||||
return enabledHeaders;
|
||||
};
|
||||
|
||||
const createQuery = (queryParams = []) => {
|
||||
@@ -54,7 +66,7 @@ export const buildHarRequest = ({ request, headers }) => {
|
||||
url: encodeURI(request.url),
|
||||
httpVersion: 'HTTP/1.1',
|
||||
cookies: [],
|
||||
headers: createHeaders(headers),
|
||||
headers: createHeaders(request, headers),
|
||||
queryString: createQuery(request.params),
|
||||
postData: createPostData(request.body),
|
||||
headersSize: 0,
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
export const MimeTypes = [
|
||||
'application/atom+xml',
|
||||
'application/ecmascript',
|
||||
'application/json',
|
||||
'application/vnd.api+json',
|
||||
'application/javascript',
|
||||
'application/octet-stream',
|
||||
'application/ogg',
|
||||
'application/pdf',
|
||||
'application/postscript',
|
||||
'application/rdf+xml',
|
||||
'application/rss+xml',
|
||||
'application/soap+xml',
|
||||
'application/font-woff',
|
||||
'application/x-yaml',
|
||||
'application/xhtml+xml',
|
||||
'application/xml',
|
||||
'application/xml-dtd',
|
||||
'application/xop+xml',
|
||||
'application/zip',
|
||||
'application/gzip',
|
||||
'application/graphql',
|
||||
'application/x-www-form-urlencoded',
|
||||
'audio/basic',
|
||||
'audio/L24',
|
||||
'audio/mp4',
|
||||
'audio/mpeg',
|
||||
'audio/ogg',
|
||||
'audio/vorbis',
|
||||
'audio/vnd.rn-realaudio',
|
||||
'audio/vnd.wave',
|
||||
'audio/webm',
|
||||
'image/gif',
|
||||
'image/jpeg',
|
||||
'image/pjpeg',
|
||||
'image/png',
|
||||
'image/svg+xml',
|
||||
'image/tiff',
|
||||
'message/http',
|
||||
'message/imdn+xml',
|
||||
'message/partial',
|
||||
'message/rfc822',
|
||||
'multipart/mixed',
|
||||
'multipart/alternative',
|
||||
'multipart/related',
|
||||
'multipart/form-data',
|
||||
'multipart/signed',
|
||||
'multipart/encrypted',
|
||||
'text/cmd',
|
||||
'text/css',
|
||||
'text/csv',
|
||||
'text/html',
|
||||
'text/plain',
|
||||
'text/vcard',
|
||||
'text/xml'
|
||||
];
|
||||
@@ -31,8 +31,8 @@ if (!SERVER_RENDERED) {
|
||||
if (str.startsWith('{{')) {
|
||||
variableName = str.replace('{{', '').replace('}}', '').trim();
|
||||
variableValue = interpolate(get(options.variables, variableName), options.variables);
|
||||
} else if (str.startsWith(':')) {
|
||||
variableName = str.replace(':', '').trim();
|
||||
} else if (str.startsWith('/:')) {
|
||||
variableName = str.replace('/:', '').trim();
|
||||
variableValue =
|
||||
options.variables && options.variables.pathParams ? options.variables.pathParams[variableName] : undefined;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import isEqual from 'lodash/isEqual';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { uuid } from 'utils/common';
|
||||
import path from 'path';
|
||||
import slash from 'utils/common/slash';
|
||||
|
||||
const replaceTabsWithSpaces = (str, numSpaces = 2) => {
|
||||
if (!str || !str.length || !isString(str)) {
|
||||
@@ -98,7 +99,7 @@ export const findCollectionByItemUid = (collections, itemUid) => {
|
||||
};
|
||||
|
||||
export const findItemByPathname = (items = [], pathname) => {
|
||||
return find(items, (i) => i.pathname === pathname);
|
||||
return find(items, (i) => slash(i.pathname) === slash(pathname));
|
||||
};
|
||||
|
||||
export const findItemInCollectionByPathname = (collection, pathname) => {
|
||||
@@ -380,6 +381,55 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {}
|
||||
}
|
||||
}
|
||||
|
||||
if (si.type == 'folder' && si?.root) {
|
||||
di.root = {
|
||||
request: {}
|
||||
};
|
||||
|
||||
let { request, meta } = si?.root || {};
|
||||
let { headers, script = {}, vars = {}, tests } = request || {};
|
||||
|
||||
// folder level headers
|
||||
if (headers?.length) {
|
||||
di.root.request.headers = headers;
|
||||
}
|
||||
// folder level script
|
||||
if (Object.keys(script)?.length) {
|
||||
di.root.request.script = {};
|
||||
if (script?.req?.length) {
|
||||
di.root.request.script.req = script?.req;
|
||||
}
|
||||
if (script?.res?.length) {
|
||||
di.root.request.script.res = script?.res;
|
||||
}
|
||||
}
|
||||
// folder level vars
|
||||
if (Object.keys(vars)?.length) {
|
||||
di.root.request.vars = {};
|
||||
if (vars?.req?.length) {
|
||||
di.root.request.vars.req = vars?.req;
|
||||
}
|
||||
if (vars?.res?.length) {
|
||||
di.root.request.vars.res = vars?.res;
|
||||
}
|
||||
}
|
||||
// folder level tests
|
||||
if (tests?.length) {
|
||||
di.root.request.tests = tests;
|
||||
}
|
||||
|
||||
if (meta?.name) {
|
||||
di.root.meta = {};
|
||||
di.root.meta.name = meta?.name;
|
||||
}
|
||||
if (!Object.keys(di.root.request)?.length) {
|
||||
delete di.root.request;
|
||||
}
|
||||
if (!Object.keys(di.root)?.length) {
|
||||
delete di.root;
|
||||
}
|
||||
}
|
||||
|
||||
if (si.type === 'js') {
|
||||
di.fileContent = si.raw;
|
||||
}
|
||||
@@ -403,6 +453,60 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {}
|
||||
collectionToSave.activeEnvironmentUid = collection.activeEnvironmentUid;
|
||||
collectionToSave.environments = collection.environments || [];
|
||||
|
||||
collectionToSave.root = {
|
||||
request: {}
|
||||
};
|
||||
|
||||
let { request, docs, meta } = collection?.root || {};
|
||||
let { auth, headers, script = {}, vars = {}, tests } = request || {};
|
||||
|
||||
// collection level auth
|
||||
if (auth?.mode) {
|
||||
collectionToSave.root.request.auth = auth;
|
||||
}
|
||||
// collection level headers
|
||||
if (headers?.length) {
|
||||
collectionToSave.root.request.headers = headers;
|
||||
}
|
||||
// collection level script
|
||||
if (Object.keys(script)?.length) {
|
||||
collectionToSave.root.request.script = {};
|
||||
if (script?.req?.length) {
|
||||
collectionToSave.root.request.script.req = script?.req;
|
||||
}
|
||||
if (script?.res?.length) {
|
||||
collectionToSave.root.request.script.res = script?.res;
|
||||
}
|
||||
}
|
||||
// collection level vars
|
||||
if (Object.keys(vars)?.length) {
|
||||
collectionToSave.root.request.vars = {};
|
||||
if (vars?.req?.length) {
|
||||
collectionToSave.root.request.vars.req = vars?.req;
|
||||
}
|
||||
if (vars?.res?.length) {
|
||||
collectionToSave.root.request.vars.res = vars?.res;
|
||||
}
|
||||
}
|
||||
// collection level tests
|
||||
if (tests?.length) {
|
||||
collectionToSave.root.request.tests = tests;
|
||||
}
|
||||
// collection level docs
|
||||
if (docs?.length) {
|
||||
collectionToSave.root.docs = docs;
|
||||
}
|
||||
if (meta?.name) {
|
||||
collectionToSave.root.meta = {};
|
||||
collectionToSave.root.meta.name = meta?.name;
|
||||
}
|
||||
if (!Object.keys(collectionToSave.root.request)?.length) {
|
||||
delete collectionToSave.root.request;
|
||||
}
|
||||
if (!Object.keys(collectionToSave.root)?.length) {
|
||||
delete collectionToSave.root;
|
||||
}
|
||||
|
||||
collectionToSave.brunoConfig = cloneDeep(collection?.brunoConfig);
|
||||
|
||||
// delete proxy password if present
|
||||
@@ -684,11 +788,17 @@ 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 pathParams = getPathParams(item);
|
||||
|
||||
return {
|
||||
...environmentVariables,
|
||||
...collection.collectionVariables,
|
||||
...requestVariables,
|
||||
...collection.runtimeVariables,
|
||||
pathParams: {
|
||||
...pathParams
|
||||
},
|
||||
@@ -710,3 +820,36 @@ export const maskInputValue = (value) => {
|
||||
.map(() => '*')
|
||||
.join('');
|
||||
};
|
||||
|
||||
const getTreePathFromCollectionToItem = (collection, _item) => {
|
||||
let path = [];
|
||||
let item = findItemInCollection(collection, _item?.uid);
|
||||
while (item) {
|
||||
path.unshift(item);
|
||||
item = findParentItemInCollection(collection, item?.uid);
|
||||
}
|
||||
return path;
|
||||
};
|
||||
|
||||
const mergeFolderLevelVars = (request, requestTreePath = []) => {
|
||||
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) {
|
||||
requestVariables[_var.name] = _var.value;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
let vars = get(i, 'request.vars.req', []);
|
||||
vars.forEach((_var) => {
|
||||
if (_var.enabled) {
|
||||
requestVariables[_var.name] = _var.value;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return requestVariables;
|
||||
};
|
||||
|
||||
@@ -12,8 +12,67 @@ const pathFoundInVariables = (path, obj) => {
|
||||
return value !== undefined;
|
||||
};
|
||||
|
||||
export const defineCodeMirrorBrunoVariablesMode = (variables, mode) => {
|
||||
CodeMirror.defineMode('combinedmode', function (config, parserConfig) {
|
||||
/**
|
||||
* Changes the render behaviour for a given CodeMirror editor.
|
||||
* Replaces all **rendered** characters, not the actual value, with the provided character.
|
||||
*/
|
||||
export class MaskedEditor {
|
||||
/**
|
||||
* @param {import('codemirror').Editor} editor CodeMirror editor instance
|
||||
* @param {string} maskChar Target character being applied to all content
|
||||
*/
|
||||
constructor(editor, maskChar) {
|
||||
this.editor = editor;
|
||||
this.maskChar = maskChar;
|
||||
this.enabled = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set and apply new masking character
|
||||
*/
|
||||
enable = () => {
|
||||
this.enabled = true;
|
||||
this.editor.setValue(this.editor.getValue());
|
||||
this.editor.on('inputRead', this.maskContent);
|
||||
this.update();
|
||||
};
|
||||
|
||||
/** Disables masking of the editor field. */
|
||||
disable = () => {
|
||||
this.enabled = false;
|
||||
this.editor.off('inputRead', this.maskContent);
|
||||
this.editor.setValue(this.editor.getValue());
|
||||
};
|
||||
|
||||
/** Updates the rendered content if enabled. */
|
||||
update = () => {
|
||||
if (this.enabled) this.maskContent();
|
||||
};
|
||||
|
||||
/** Replaces all rendered characters, with the provided character. */
|
||||
maskContent = () => {
|
||||
const content = this.editor.getValue();
|
||||
this.editor.operation(() => {
|
||||
// Clear previous masked text
|
||||
this.editor.getAllMarks().forEach((mark) => mark.clear());
|
||||
// Apply new masked text
|
||||
for (let i = 0; i < content.length; i++) {
|
||||
if (content[i] !== '\n') {
|
||||
const maskedNode = document.createTextNode(this.maskChar);
|
||||
this.editor.markText(
|
||||
{ line: this.editor.posFromIndex(i).line, ch: this.editor.posFromIndex(i).ch },
|
||||
{ line: this.editor.posFromIndex(i + 1).line, ch: this.editor.posFromIndex(i + 1).ch },
|
||||
{ replacedWith: maskedNode, handleMouseEvents: true }
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export const defineCodeMirrorBrunoVariablesMode = (_variables, mode, highlightPathParams) => {
|
||||
CodeMirror.defineMode('brunovariables', function (config, parserConfig) {
|
||||
const { pathParams = {}, ...variables } = _variables || {};
|
||||
const variablesOverlay = {
|
||||
token: function (stream) {
|
||||
if (stream.match('{{', true)) {
|
||||
@@ -37,13 +96,13 @@ export const defineCodeMirrorBrunoVariablesMode = (variables, mode) => {
|
||||
|
||||
const urlPathParamsOverlay = {
|
||||
token: function (stream) {
|
||||
if (stream.match(':', true)) {
|
||||
if (stream.match('/:', true)) {
|
||||
let ch;
|
||||
let word = '';
|
||||
while ((ch = stream.next()) != null) {
|
||||
if (ch === '/' || ch === '?' || ch === '&' || ch === '=') {
|
||||
stream.backUp(1);
|
||||
const found = pathFoundInVariables(word, variables?.pathParams);
|
||||
const found = pathFoundInVariables(word, pathParams);
|
||||
const status = found ? 'valid' : 'invalid';
|
||||
const randomClass = `random-${(Math.random() + 1).toString(36).substring(9)}`;
|
||||
return `variable-${status} ${randomClass}`;
|
||||
@@ -53,21 +112,24 @@ export const defineCodeMirrorBrunoVariablesMode = (variables, mode) => {
|
||||
|
||||
// If we've consumed all characters and the word is not empty, it might be a path parameter at the end of the URL.
|
||||
if (word) {
|
||||
const found = pathFoundInVariables(word, variables?.pathParams);
|
||||
const found = pathFoundInVariables(word, pathParams);
|
||||
const status = found ? 'valid' : 'invalid';
|
||||
const randomClass = `random-${(Math.random() + 1).toString(36).substring(9)}`;
|
||||
return `variable-${status} ${randomClass}`;
|
||||
}
|
||||
}
|
||||
stream.skipTo(':') || stream.skipToEnd();
|
||||
stream.skipTo('/:') || stream.skipToEnd();
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return CodeMirror.overlayMode(
|
||||
CodeMirror.overlayMode(CodeMirror.getMode(config, parserConfig.backdrop || mode), variablesOverlay),
|
||||
urlPathParamsOverlay
|
||||
);
|
||||
let baseMode = CodeMirror.overlayMode(CodeMirror.getMode(config, parserConfig.backdrop || mode), variablesOverlay);
|
||||
|
||||
if (highlightPathParams) {
|
||||
return CodeMirror.overlayMode(baseMode, urlPathParamsOverlay);
|
||||
} else {
|
||||
return baseMode;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -72,11 +72,10 @@ const parseCurlCommand = (curlCommand) => {
|
||||
parsedArguments.header.forEach((header) => {
|
||||
if (header.indexOf('Cookie') !== -1) {
|
||||
cookieString = header;
|
||||
} else {
|
||||
const components = header.split(/:(.*)/);
|
||||
if (components[1]) {
|
||||
headers[components[0]] = components[1].trim();
|
||||
}
|
||||
}
|
||||
const components = header.split(/:(.*)/);
|
||||
if (components[1]) {
|
||||
headers[components[0]] = components[1].trim();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -119,15 +118,16 @@ const parseCurlCommand = (curlCommand) => {
|
||||
cookies = cookie.parse(cookieString.replace(/^Cookie: /gi, ''), cookieParseOptions);
|
||||
}
|
||||
let method;
|
||||
if (parsedArguments.X === 'POST') {
|
||||
let parsedMethodArgument = parsedArguments.X || parsedArguments.request || parsedArguments.T;
|
||||
if (parsedMethodArgument === 'POST') {
|
||||
method = 'post';
|
||||
} else if (parsedArguments.X === 'PUT' || parsedArguments.T) {
|
||||
} else if (parsedMethodArgument === 'PUT') {
|
||||
method = 'put';
|
||||
} else if (parsedArguments.X === 'PATCH') {
|
||||
} else if (parsedMethodArgument === 'PATCH') {
|
||||
method = 'patch';
|
||||
} else if (parsedArguments.X === 'DELETE') {
|
||||
} else if (parsedMethodArgument === 'DELETE') {
|
||||
method = 'delete';
|
||||
} else if (parsedArguments.X === 'OPTIONS') {
|
||||
} else if (parsedMethodArgument === 'OPTIONS') {
|
||||
method = 'options';
|
||||
} else if (
|
||||
(parsedArguments.d ||
|
||||
|
||||
@@ -57,10 +57,20 @@ const parsePostmanEnvironment = (str) => {
|
||||
|
||||
const importEnvironment = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
fileDialog({ accept: 'application/json' })
|
||||
.then(readFile)
|
||||
.then(parsePostmanEnvironment)
|
||||
.then((environment) => resolve(environment))
|
||||
fileDialog({ multiple: true, accept: 'application/json' })
|
||||
.then((files) => {
|
||||
return Promise.all(
|
||||
Object.values(files ?? {}).map((file) =>
|
||||
readFile([file])
|
||||
.then(parsePostmanEnvironment)
|
||||
.catch((err) => {
|
||||
console.error(`Error processing file: ${file.name || 'undefined'}`, err);
|
||||
throw err;
|
||||
})
|
||||
)
|
||||
);
|
||||
})
|
||||
.then((environments) => resolve(environments))
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
reject(new BrunoError('Import Environment failed'));
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { safeStringifyJSON } from 'utils/common';
|
||||
|
||||
export const sendNetworkRequest = async (item, collection, environment, collectionVariables) => {
|
||||
export const sendNetworkRequest = async (item, collection, environment, runtimeVariables) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (['http-request', 'graphql-request'].includes(item.type)) {
|
||||
sendHttpRequest(item, collection, environment, collectionVariables)
|
||||
sendHttpRequest(item, collection, environment, runtimeVariables)
|
||||
.then((response) => {
|
||||
resolve({
|
||||
state: 'success',
|
||||
@@ -22,22 +22,22 @@ export const sendNetworkRequest = async (item, collection, environment, collecti
|
||||
});
|
||||
};
|
||||
|
||||
const sendHttpRequest = async (item, collection, environment, collectionVariables) => {
|
||||
const sendHttpRequest = async (item, collection, environment, runtimeVariables) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const { ipcRenderer } = window;
|
||||
|
||||
ipcRenderer
|
||||
.invoke('send-http-request', item, collection, environment, collectionVariables)
|
||||
.invoke('send-http-request', item, collection, environment, runtimeVariables)
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
});
|
||||
};
|
||||
|
||||
export const sendCollectionOauth2Request = async (collection, environment, collectionVariables) => {
|
||||
export const sendCollectionOauth2Request = async (collection, environment, runtimeVariables) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const { ipcRenderer } = window;
|
||||
ipcRenderer
|
||||
.invoke('send-collection-oauth2-request', collection, environment, collectionVariables)
|
||||
.invoke('send-collection-oauth2-request', collection, environment, runtimeVariables)
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
});
|
||||
|
||||
@@ -107,14 +107,14 @@ export const isValidUrl = (url) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const interpolateUrl = ({ url, envVars, collectionVariables, processEnvVars }) => {
|
||||
export const interpolateUrl = ({ url, envVars, runtimeVariables, processEnvVars }) => {
|
||||
if (!url || !url.length || typeof url !== 'string') {
|
||||
return;
|
||||
}
|
||||
|
||||
return interpolate(url, {
|
||||
...envVars,
|
||||
...collectionVariables,
|
||||
...runtimeVariables,
|
||||
process: {
|
||||
env: {
|
||||
...processEnvVars
|
||||
|
||||
@@ -129,10 +129,10 @@ describe('Url Utils - interpolateUrl, interpolateUrlPathParams', () => {
|
||||
const expectedUrl = 'https://example.com/api/:id/path?foo=foo_value&bar=bar_value&baz=baz_value';
|
||||
|
||||
const envVars = { host: 'https://example.com', foo: 'foo_value' };
|
||||
const collectionVariables = { bar: 'bar_value' };
|
||||
const runtimeVariables = { bar: 'bar_value' };
|
||||
const processEnvVars = { baz: 'baz_value' };
|
||||
|
||||
const result = interpolateUrl({ url, envVars, collectionVariables, processEnvVars });
|
||||
const result = interpolateUrl({ url, envVars, runtimeVariables, processEnvVars });
|
||||
|
||||
expect(result).toEqual(expectedUrl);
|
||||
});
|
||||
@@ -153,10 +153,10 @@ describe('Url Utils - interpolateUrl, interpolateUrlPathParams', () => {
|
||||
const expectedUrl = 'https://example.com/api/123/path?foo=foo_value&bar=bar_value&baz=baz_value';
|
||||
|
||||
const envVars = { host: 'https://example.com', foo: 'foo_value' };
|
||||
const collectionVariables = { bar: 'bar_value' };
|
||||
const runtimeVariables = { bar: 'bar_value' };
|
||||
const processEnvVars = { baz: 'baz_value' };
|
||||
|
||||
const intermediateResult = interpolateUrl({ url, envVars, collectionVariables, processEnvVars });
|
||||
const intermediateResult = interpolateUrl({ url, envVars, runtimeVariables, processEnvVars });
|
||||
const result = interpolateUrlPathParams(intermediateResult, params);
|
||||
|
||||
expect(result).toEqual(expectedUrl);
|
||||
|
||||
@@ -64,13 +64,13 @@ Bruno cli returns the following exit status codes:
|
||||
- `1` -- an assertion, test, or request in the executed collection failed
|
||||
- `2` -- the specified output directory does not exist
|
||||
- `3` -- the request chain seems to loop endlessly
|
||||
- `4` -- bru was called outside of a colection root directory
|
||||
- `4` -- bru was called outside of a collection root directory
|
||||
- `5` -- the specified input file does not exist
|
||||
- `6` -- the specified environment does not exist
|
||||
- `7` -- the environment override was not a string or object
|
||||
- `8` -- an environment override is malformed
|
||||
- `9` -- an invalid output format was requested
|
||||
- `255` -- another error occured
|
||||
- `255` -- another error occurred
|
||||
|
||||
## Demo
|
||||
|
||||
|
||||
@@ -179,6 +179,17 @@ const getCollectionRoot = (dir) => {
|
||||
return collectionBruToJson(content);
|
||||
};
|
||||
|
||||
const getFolderRoot = (dir) => {
|
||||
const folderRootPath = path.join(dir, 'folder.bru');
|
||||
const exists = fs.existsSync(folderRootPath);
|
||||
if (!exists) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(folderRootPath, 'utf8');
|
||||
return collectionBruToJson(content);
|
||||
};
|
||||
|
||||
const builder = async (yargs) => {
|
||||
yargs
|
||||
.option('r', {
|
||||
@@ -301,7 +312,7 @@ const handler = async function (argv) {
|
||||
recursive = true;
|
||||
}
|
||||
|
||||
const collectionVariables = {};
|
||||
const runtimeVariables = {};
|
||||
let envVars = {};
|
||||
|
||||
if (env) {
|
||||
@@ -406,7 +417,7 @@ const handler = async function (argv) {
|
||||
if (!recursive) {
|
||||
console.log(chalk.yellow('Running Folder \n'));
|
||||
const files = fs.readdirSync(filename);
|
||||
const bruFiles = files.filter((file) => file.endsWith('.bru'));
|
||||
const bruFiles = files.filter((file) => !['folder.bru'].includes(file) && file.endsWith('.bru'));
|
||||
|
||||
for (const bruFile of bruFiles) {
|
||||
const bruFilepath = path.join(filename, bruFile);
|
||||
@@ -451,7 +462,7 @@ const handler = async function (argv) {
|
||||
bruFilepath,
|
||||
bruJson,
|
||||
collectionPath,
|
||||
collectionVariables,
|
||||
runtimeVariables,
|
||||
envVars,
|
||||
processEnvVars,
|
||||
brunoConfig,
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
const { forOwn, cloneDeep } = require('lodash');
|
||||
const { interpolate } = require('@usebruno/common');
|
||||
|
||||
const interpolateString = (str, { envVars, collectionVariables, processEnvVars }) => {
|
||||
const interpolateString = (str, { envVars, runtimeVariables, processEnvVars }) => {
|
||||
if (!str || !str.length || typeof str !== 'string') {
|
||||
return str;
|
||||
}
|
||||
|
||||
processEnvVars = processEnvVars || {};
|
||||
collectionVariables = collectionVariables || {};
|
||||
runtimeVariables = runtimeVariables || {};
|
||||
|
||||
// we clone envVars because we don't want to modify the original object
|
||||
envVars = envVars ? cloneDeep(envVars) : {};
|
||||
@@ -24,10 +24,10 @@ const interpolateString = (str, { envVars, collectionVariables, processEnvVars }
|
||||
});
|
||||
});
|
||||
|
||||
// collectionVariables take precedence over envVars
|
||||
// runtimeVariables take precedence over envVars
|
||||
const combinedVars = {
|
||||
...envVars,
|
||||
...collectionVariables,
|
||||
...runtimeVariables,
|
||||
process: {
|
||||
env: {
|
||||
...processEnvVars
|
||||
|
||||
@@ -12,7 +12,7 @@ const getContentType = (headers = {}) => {
|
||||
return contentType;
|
||||
};
|
||||
|
||||
const interpolateVars = (request, envVars = {}, collectionVariables = {}, processEnvVars = {}) => {
|
||||
const interpolateVars = (request, envVars = {}, runtimeVariables = {}, processEnvVars = {}) => {
|
||||
// we clone envVars because we don't want to modify the original object
|
||||
envVars = cloneDeep(envVars);
|
||||
|
||||
@@ -33,10 +33,10 @@ const interpolateVars = (request, envVars = {}, collectionVariables = {}, proces
|
||||
return str;
|
||||
}
|
||||
|
||||
// collectionVariables take precedence over envVars
|
||||
// runtimeVariables take precedence over envVars
|
||||
const combinedVars = {
|
||||
...envVars,
|
||||
...collectionVariables,
|
||||
...runtimeVariables,
|
||||
process: {
|
||||
env: {
|
||||
...processEnvVars
|
||||
@@ -82,11 +82,11 @@ const interpolateVars = (request, envVars = {}, collectionVariables = {}, proces
|
||||
request.data = _interpolate(request.data);
|
||||
}
|
||||
|
||||
each(request.params, (param) => {
|
||||
each(request?.pathParams, (param) => {
|
||||
param.value = _interpolate(param.value);
|
||||
});
|
||||
|
||||
if (request?.params?.length) {
|
||||
if (request?.pathParams?.length) {
|
||||
let url = request.url;
|
||||
|
||||
if (!url.startsWith('http://') && !url.startsWith('https://')) {
|
||||
@@ -107,7 +107,7 @@ const interpolateVars = (request, envVars = {}, collectionVariables = {}, proces
|
||||
return '/' + path;
|
||||
} else {
|
||||
const name = path.slice(1);
|
||||
const existingPathParam = request.params.find((param) => param.type === 'path' && param.name === name);
|
||||
const existingPathParam = request?.pathParams?.find((param) => param.type === 'path' && param.name === name);
|
||||
return existingPathParam ? '/' + existingPathParam.value : '';
|
||||
}
|
||||
})
|
||||
|
||||
@@ -29,7 +29,8 @@ const prepareRequest = (request, collectionRoot) => {
|
||||
let axiosRequest = {
|
||||
method: request.method,
|
||||
url: request.url,
|
||||
headers: headers
|
||||
headers: headers,
|
||||
pathParams: request?.params?.filter((param) => param.type === 'path')
|
||||
};
|
||||
|
||||
const collectionAuth = get(collectionRoot, 'request.auth');
|
||||
|
||||
@@ -25,7 +25,7 @@ const runSingleRequest = async function (
|
||||
filename,
|
||||
bruJson,
|
||||
collectionPath,
|
||||
collectionVariables,
|
||||
runtimeVariables,
|
||||
envVariables,
|
||||
processEnvVars,
|
||||
brunoConfig,
|
||||
@@ -62,7 +62,7 @@ const runSingleRequest = async function (
|
||||
preRequestVars,
|
||||
request,
|
||||
envVariables,
|
||||
collectionVariables,
|
||||
runtimeVariables,
|
||||
collectionPath,
|
||||
processEnvVars
|
||||
);
|
||||
@@ -79,7 +79,7 @@ const runSingleRequest = async function (
|
||||
decomment(requestScriptFile),
|
||||
request,
|
||||
envVariables,
|
||||
collectionVariables,
|
||||
runtimeVariables,
|
||||
collectionPath,
|
||||
null,
|
||||
processEnvVars,
|
||||
@@ -91,7 +91,7 @@ const runSingleRequest = async function (
|
||||
}
|
||||
|
||||
// interpolate variables inside request
|
||||
interpolateVars(request, envVariables, collectionVariables, processEnvVars);
|
||||
interpolateVars(request, envVariables, runtimeVariables, processEnvVars);
|
||||
|
||||
if (!protocolRegex.test(request.url)) {
|
||||
request.url = `http://${request.url}`;
|
||||
@@ -120,7 +120,7 @@ const runSingleRequest = async function (
|
||||
|
||||
const interpolationOptions = {
|
||||
envVars: envVariables,
|
||||
collectionVariables,
|
||||
runtimeVariables,
|
||||
processEnvVars
|
||||
};
|
||||
|
||||
@@ -282,7 +282,7 @@ const runSingleRequest = async function (
|
||||
request,
|
||||
response,
|
||||
envVariables,
|
||||
collectionVariables,
|
||||
runtimeVariables,
|
||||
collectionPath,
|
||||
processEnvVars
|
||||
);
|
||||
@@ -300,7 +300,7 @@ const runSingleRequest = async function (
|
||||
request,
|
||||
response,
|
||||
envVariables,
|
||||
collectionVariables,
|
||||
runtimeVariables,
|
||||
collectionPath,
|
||||
null,
|
||||
processEnvVars,
|
||||
@@ -321,7 +321,7 @@ const runSingleRequest = async function (
|
||||
request,
|
||||
response,
|
||||
envVariables,
|
||||
collectionVariables,
|
||||
runtimeVariables,
|
||||
processEnvVars
|
||||
);
|
||||
|
||||
@@ -345,13 +345,17 @@ const runSingleRequest = async function (
|
||||
request,
|
||||
response,
|
||||
envVariables,
|
||||
collectionVariables,
|
||||
runtimeVariables,
|
||||
collectionPath,
|
||||
null,
|
||||
processEnvVars,
|
||||
scriptingConfig
|
||||
);
|
||||
testResults = get(result, 'results', []);
|
||||
|
||||
if (result?.nextRequestName !== undefined) {
|
||||
nextRequestName = result.nextRequestName;
|
||||
}
|
||||
}
|
||||
|
||||
if (testResults?.length) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user