Compare commits

...

73 Commits

Author SHA1 Message Date
Anoop M D
103e0a2444 fix: lossless json serialization 2024-08-07 13:11:58 +05:30
Anoop M D
72c3aaa5ba chore: bumped version 2024-08-07 12:21:46 +05:30
Anoop M D
eb1c10fd6e feat: mac specific os styling 2024-08-06 18:49:35 +05:30
Sam Ho
911e3aa589 feat: comment with keybinding (cmd + /) in JSON payload interface (#2634)
Co-authored-by: Sam Ho <kwunting.ho@bt.com>
2024-08-06 17:36:51 +05:30
Pragadesh-45
2358aa4cdc Bugfix/window UI distortion electron (#2765)
* set initial window: false

* set window: true after loading the window state

* added ready-to-show event
2024-08-06 16:08:32 +05:30
Joel Wetzell
800dbcfdbc add cache to cli-test job in tests workflow (#2766) 2024-08-06 16:05:05 +05:30
Anoop M D
d7ec3d1cc5 Revert "fix: BigInters are now correctly shown in the response (#2736)" (#2768)
This reverts commit 3e2a3b65a4.
2024-08-06 16:00:49 +05:30
Vincenzo De Petris
92073e7573 fix: draft variables and headers (#2651)
* fix: extract variables and headers from the provided request

* fix: handling draft headers and vars

* fix: handling draft headers and vars

---------

Co-authored-by: Vincenzo De Petris <vincenzodepetris@gmail.it>
Co-authored-by: Anoop M D <anoop.md1421@gmail.com>
2024-08-06 14:32:54 +05:30
Timon
3e2a3b65a4 fix: BigInters are now correctly shown in the response (#2736)
Fixes: #1753
2024-08-06 13:01:30 +05:30
Chae Jeong Ah
aa4bcdca9b fix: type error in jsonToBru during import collection (#2761)
* fix: add optional chaining to prevent type error

* fix: remove debug code
2024-08-06 12:43:47 +05:30
Joel Wetzell
12fdbbb291 add cache to tests workflow (#2762) 2024-08-06 12:41:57 +05:30
Anoop M D
7cafed6c93 chore: temporarily reverting the change related to pr - #2713 2024-08-06 12:12:47 +05:30
Radovan Mihálik
8a9df14e16 Create contributing_sk.md (#1524) 2024-08-05 18:38:05 +05:30
Joel Wetzell
b2b41fec1a set content-type header during code generation (#2491)
* set content-type header during code generation

handles missing request body modes as well

* formatting
2024-08-05 18:22:17 +05:30
LucasVermersch
a8aa54cf1b Fix: contributing_fr.md (#2500)
* fix contributing_fr.md

* add space

---------

Co-authored-by: Lucas Vermersch <lvermersch@access-it.fr>
2024-08-05 18:20:03 +05:30
Rinku Chaudhari
60a8647e7c fix: enter key not submitting new request form (#2630) 2024-08-05 17:57:19 +05:30
Pragadesh-45
7ca59656f2 set inital window-show: false (#2713) 2024-08-05 17:55:06 +05:30
Anoop M D
4598bb1bdd fix(#2605): fix editor view height (#2758)
* fix(#2605): fix editor view height

* chore: disabled prettier on github actions
2024-08-05 17:49:04 +05:30
Joel Wetzell
adb843faa7 don't exclude cookies from request headers when importing from curl (#2748) 2024-08-05 12:27:00 +05:30
Bruno Grasselli
2b0d55ce6b Update missing sponsor in readme_pt_br.md (#2754) 2024-08-05 12:24:04 +05:30
Natalie Carey
c5ec7eea34 Feature: Add a show/hide privacy toggle to passwords and secrets in Auth options (#2750)
* mask support for SingleLineEditor

* add secret visibility toggle button

* move visibility toggle into SingleLineComponent

Co-authored-by: Liz MacLean <18120837+lizziemac@users.noreply.github.com>

* fix eye button focus state

* center enabled and secret toggle

* fix input field scales to 100% width

* Using a prvacy toggle for all sensitive auth details.

* Applied privacy toggle to Collection Auth settings.

---------

Co-authored-by: Max Bauer <krummbar@pm.me>
Co-authored-by: Liz MacLean <18120837+lizziemac@users.noreply.github.com>
2024-08-05 11:51:01 +05:30
Max Bauer
741250068f feat: masking support for SingleLineEditor (#2240)
* mask support for SingleLineEditor

* add secret visibility toggle button

* move visibility toggle into SingleLineComponent

Co-authored-by: Liz MacLean <18120837+lizziemac@users.noreply.github.com>

* fix eye button focus state

* center enabled and secret toggle

* fix input field scales to 100% width

---------

Co-authored-by: Liz MacLean <18120837+lizziemac@users.noreply.github.com>
2024-08-05 11:46:06 +05:30
Natalie Carey
7c33fd413e Refactored handlers into reusable functions for readability. (#2744) 2024-08-02 12:10:04 +05:30
Anoop M D
8f920a90c7 chore: bumped version 2024-08-01 18:16:54 +05:30
Anoop M D
640623b39a chore: version bumped to v1.23.0 2024-07-31 17:57:39 +05:30
lohit
37bec70fe6 fix: removed unused code (#2729) 2024-07-31 16:13:55 +05:30
lohit
98c53cf443 update natural to sequential (#2717) 2024-07-29 11:04:29 +05:30
Mateusz Pietryga
6fe96a8194 bugfix/test - update Jest configuration to fix unit tests (#2672) 2024-07-26 18:24:44 +05:30
Mateusz Pietryga
f2ba351f0d Fix: OAuth 2.0 Grant Type Authorization: "invalid_client" error / URL Encode of Client ID (#2129)
#2115
#1003
2024-07-26 18:17:38 +05:30
Timon
2e2c60d90e feat: Add flow option for "natural" flow in scripts (#2704)
- Adds a new key in the `bruno.json` under `scripts.flow`
- When concating post and tests scripts the flow will now be used
  to determine to correct order

Fixes: #2648 #2680 #2597 #2639
2024-07-26 18:10:10 +05:30
BruAlcaraz
1d2e06d419 Fix test results when the same request is executed more than 1 time (#2522) (#2551)
Co-authored-by: Alcaraz, Bruno <Bruno.Alcaraz@ulgroup.com>
2024-07-25 17:05:00 +05:30
BruAlcaraz
c99da3a581 Allow bru.setNextRequest() on Test Scripts (#2155) (#2552)
Co-authored-by: Alcaraz, Bruno <Bruno.Alcaraz@ulgroup.com>
2024-07-25 16:57:55 +05:30
Anoop M D
073c1aae12 chore: bumped version 2024-07-23 17:19:25 +05:30
Anoop M D
398c833393 chore: package-lock update 2024-07-19 18:45:01 +05:30
lohit
47724b1b1e scrollbar fix (#2670) 2024-07-19 18:43:51 +05:30
Sanjai Kumar
2804ce1eb3 Revert "fix: active enviroment after rename when there is single enviroment (…" (#2660)
This reverts commit 81497d8397.
2024-07-19 16:51:12 +05:30
lohit
9892f7cd40 Feat/electron bump (#2668)
* pr review changes

* collection root object in export json

* import environment updates

* tests run execution order fix for collection runs

* updated validations

* accept request flag in curl string for method type

* electron version bump to v31.2.1
2024-07-19 16:29:10 +05:30
lohit
7194998b0e update node-machine-id to @usebruno/node-machine-id (#2661)
* update node-machine-id to @usebruno/node-machine-id

* added lock file

* tests check

* tests check
2024-07-18 17:19:22 +05:30
lohit
ab9bcbe5ed feat/rename collectionVariables variable name to runtimeVariables (#2638)
* pr review changes

* collection root object in export json

* import environment updates

* tests run execution order fix for collection runs

* updated validations

* collectionVariables -> runtimeVariables

* removed husky, adjusted indentation

---------

Co-authored-by: Anoop M D <anoop.md1421@gmail.com>
2024-07-17 17:21:03 +05:30
Max Destors
e60aaf2ea9 Multipart request file - Fix init value and clear value (#2609) 2024-07-17 15:41:25 +05:30
Rinku Chaudhari
81497d8397 fix: active enviroment after rename when there is single enviroment (#2640) 2024-07-16 15:53:26 +05:30
Anoop M D
34a961967e chore: bumped version 2024-07-15 17:37:54 +05:30
Anoop M D
1b0495c7b0 Merge branch 'main' of github.com:usebruno/bruno 2024-07-15 17:37:10 +05:30
lohit
73214107c7 feat/request variables highlight (#2621)
* pr review changes

* collection root object in export json

* import environment updates

* tests run execution order fix for collection runs

* updated validations

* folder/request pre-vars green/red color highlight

* collection vars > request vars

* chore: removed unused logic

---------

Co-authored-by: Anoop M D <anoop.md1421@gmail.com>
2024-07-15 17:18:29 +05:30
Anoop M D
f159f73340 chore: bumped version 2024-07-10 16:28:58 +05:30
lohit
c2e6dee2da validations update for export collection (#2607)
* pr review changes

* collection root object in export json

* import environment updates

* tests run execution order fix for collection runs

* updated validations
2024-07-10 16:26:19 +05:30
Fabio GRANDE
f64dca16a7 Removed Underscores on variables (#2603)
Co-authored-by: Fabio Grande <fabio.grande@hdhome.it>
2024-07-10 12:58:19 +05:30
Anoop M D
b5b9e547c9 chore: bumped version 2024-07-10 12:57:05 +05:30
Anoop M D
1239baf687 chore: added graceful check while accessing path params 2024-07-10 11:55:12 +05:30
Fabio GRANDE
b2038c7cc2 CLI doesn't interpolate params on the URL #2587 (#2588)
Co-authored-by: Fabio Grande <fabio.grande@hdhome.it>
2024-07-10 11:52:18 +05:30
Fabio GRANDE
9f76834b2f Bruno CLI tries to execute folder.bru #2584 (#2585)
Co-authored-by: Fabio Grande <fabio.grande@hdhome.it>
2024-07-10 11:49:12 +05:30
Jorge Caridad
8094149fbe fixed typos in bruno-cli readme.md (#2600) 2024-07-10 11:29:23 +05:30
Fabio GRANDE
240d2d03f7 Cloning a dir now clone also folder.bru files recursively#2593 (#2596)
Co-authored-by: Fabio Grande <fabio.grande@hdhome.it>
2024-07-10 11:24:49 +05:30
lohit
58c8085a64 fix/collection export import (#2601)
* pr review changes

* collection root object in export json

* import environment updates

* tests run execution order fix for collection runs

* headers schema update, export only required parts of request

* update auth in object spread

* docs not present in folder level settings

* docs not present in folder level settings

---------

Co-authored-by: Anoop M D <anoop.md1421@gmail.com>
2024-07-10 11:21:58 +05:30
sfreeman422
e5425299a2 Import Bruno Collection fails - ValidationError: headers are required #2583 (#2598)
* Removed headers key on folderRootSchema as it seems unlikely that headers are actually required here

* Removed launch.json

* Added back package-locks

* reverted package-lock

* removed only the .required
2024-07-10 11:19:12 +05:30
lohit
f1e0b112ae fix/folder bru data loading issue in windows (#2595)
* pr review changes

* collection root object in export json

* import environment updates

* validations for folder.bru paths for windows
2024-07-10 11:04:55 +05:30
Anoop M D
0988f2b86e chore: bumped version 2024-07-07 14:52:22 +05:30
Timon
e71e38f62e fix(#2573): Fix broken resequenzing of requests (#2574) 2024-07-06 23:07:36 +05:30
Anoop M D
589e173256 chore: version bump 2024-07-04 16:28:43 +05:30
lohit
e462eb6ecd import environments ui and collection root object for json export - updates (#2565)
* pr review changes

* collection root object in export json

* import environment updates
2024-07-04 16:10:38 +05:30
lohit
40f7be534a pr review changes (#2563) 2024-07-04 14:47:46 +05:30
lohit
c8f95a34e9 feat: bru hasEnvVar, hasVar, deleteVar (#2531)
Co-authored-by: Anoop M D <anoop.md1421@gmail.com>
2024-07-04 13:21:27 +05:30
lohit
2aa7d26a89 add collection and folder data to exported bruno collection json (#2560)
* add collection and folder data to exported bruno collection json

* folder root data order
2024-07-04 13:18:40 +05:30
Baptiste Poulain
71353b0404 [Feature] : Bulk env import and UX/UI improvements (#2509)
* feat(bulk-env-import): bulk import working like a charm

* feat(bulk-env-import): refresh no env dialog's styling

* feat(bulk-env-import): group create and import env within initial modal, UI improvements

* feat(bulk-env-import): minor styling fixes

* feat(bulk-env-import): handle incorrect files in env importer

---------

Co-authored-by: bpoulaindev <bpoulainpro@gmail.com>
2024-07-04 12:01:24 +05:30
Sanjai Kumar
01605f6f2a Bugfix/links in docs (#2561)
* chore: fix markdown component

* Refactor MarkDown component to remove unnecessary useCallback hooks
2024-07-04 11:38:35 +05:30
Guillaume Leon
0d204694a6 Add autocompletion for headers value (#1142)
* feature: add autocompletion for headers value

* chore: rename file to autocompleteConstants and move it to codemirror utils
2024-07-04 11:28:46 +05:30
Nick Boyadjian
0d3765ad66 fix scrolling issue by setting the height of the CodeMirror using (#1058)
flex

Co-authored-by: Nick Boyadjian <nick.boyadjian@podium.com>
Co-authored-by: Anoop M D <anoop.md1421@gmail.com>
2024-07-04 11:26:34 +05:30
lohit
bd61e453ee fix/variables highlighting (#2502)
* js highlighting fix, only highlight path params pattern in url bar

* path param pattern matching validation update

* path param tooltip validation update
2024-07-01 19:25:55 +05:30
Anoop M D
02e23df349 chore: version bumped 2024-07-01 18:20:16 +05:30
lohit
61e0ac03fa updated testbench collection (#2542) 2024-07-01 15:40:31 +05:30
lohit
c895d7f357 feat: request vars, bru.getRequestVar function (#2541) 2024-07-01 14:15:25 +05:30
Anoop M D
45ff36d394 Folder level Headers, Scripts and Tests (#2529)
* [Feature] : Settings on folder level (#1334)

* feat(folder_settings): enable settings tab from folder, currently not using folder.bru

* feat(folder_settings): read and write in folder settings only in headers, ignore folder.bru file il requests list

* feat(folder_settings): merge collection and folder settings when sending network request

* feat(folder_settings): remove console, testing headers merging working fine

* feat(folder_settings): add missing endl for prettier check, remove redundant imports

---------

Co-authored-by: Baptiste POULAIN <baptistepoulain@MAC882.local>
Co-authored-by: Anoop M D <anoop.md1421@gmail.com>

* feat: folder level scripts and tests

* feat: folder level variables (#2530)

---------

Co-authored-by: Baptiste Poulain <64689165+bpoulaindev@users.noreply.github.com>
Co-authored-by: Baptiste POULAIN <baptistepoulain@MAC882.local>
Co-authored-by: lohit <lohit.jiddimani@gmail.com>
2024-07-01 12:52:56 +05:30
Jean Lethiec
fd57b2ce94 Add collection var to CodeEditor props (#2501)
Co-authored-by: jean_lethiec <Jean_LETHIEC@connect-tech.sncf>
2024-06-28 20:10:14 +05:30
134 changed files with 3087 additions and 784 deletions

View File

@@ -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

View File

@@ -1,4 +0,0 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx pretty-quick --staged

View File

@@ -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

View 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

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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,

View File

@@ -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) => {

View File

@@ -138,6 +138,7 @@ const AwsV4Auth = ({ collection }) => {
onSave={handleSave}
onChange={(val) => handleSecretAccessKeyChange(val)}
collection={collection}
isSecret={true}
/>
</div>

View File

@@ -62,6 +62,7 @@ const BasicAuth = ({ collection }) => {
onSave={handleSave}
onChange={(val) => handlePasswordChange(val)}
collection={collection}
isSecret={true}
/>
</div>
</StyledWrapper>

View File

@@ -37,6 +37,7 @@ const BearerAuth = ({ collection }) => {
onSave={handleSave}
onChange={(val) => handleTokenChange(val)}
collection={collection}
isSecret={true}
/>
</div>
</StyledWrapper>

View File

@@ -62,6 +62,7 @@ const DigestAuth = ({ collection }) => {
onSave={handleSave}
onChange={(val) => handlePasswordChange(val)}
collection={collection}
isSecret={true}
/>
</div>
</StyledWrapper>

View File

@@ -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>

View File

@@ -17,7 +17,8 @@ const inputsConfig = [
},
{
key: 'clientSecret',
label: 'Client Secret'
label: 'Client Secret',
isSecret: true
},
{
key: 'scope',

View File

@@ -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>

View File

@@ -9,7 +9,8 @@ const inputsConfig = [
},
{
key: 'clientSecret',
label: 'Client Secret'
label: 'Client Secret',
isSecret: true
},
{
key: 'scope',

View File

@@ -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>

View File

@@ -17,7 +17,8 @@ const inputsConfig = [
},
{
key: 'clientSecret',
label: 'Client Secret'
label: 'Client Secret',
isSecret: true
},
{
key: 'scope',

View File

@@ -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>

View File

@@ -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};

View File

@@ -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>

View File

@@ -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}

View File

@@ -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}

View File

@@ -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>
);

View File

@@ -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>
);

View File

@@ -42,7 +42,7 @@ const FilePickerEditor = ({ value, onChange, collection }) => {
};
const clear = () => {
onChange('');
onChange([]);
};
const renderButtonText = (filenames) => {

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -0,0 +1,5 @@
import styled from 'styled-components';
const StyledWrapper = styled.div``;
export default StyledWrapper;

View File

@@ -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;

View File

@@ -0,0 +1,9 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
div.title {
color: var(--color-tab-inactive);
}
`;
export default StyledWrapper;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View 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;

View File

@@ -2,7 +2,6 @@ import styled from 'styled-components';
const StyledMarkdownBodyWrapper = styled.div`
background: transparent;
height: inherit;
.markdown-body {
background: transparent;
overflow-y: auto;

View File

@@ -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>
);

View File

@@ -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');
};

View File

@@ -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>

View File

@@ -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>

View File

@@ -42,6 +42,8 @@ const BearerAuth = ({ item, collection }) => {
onChange={(val) => handleTokenChange(val)}
onRun={handleRun}
collection={collection}
item={item}
isSecret={true}
/>
</div>
</StyledWrapper>

View File

@@ -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>

View File

@@ -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>

View File

@@ -17,7 +17,8 @@ const inputsConfig = [
},
{
key: 'clientSecret',
label: 'Client Secret'
label: 'Client Secret',
isSecret: true
},
{
key: 'scope',

View File

@@ -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>

View File

@@ -9,7 +9,8 @@ const inputsConfig = [
},
{
key: 'clientSecret',
label: 'Client Secret'
label: 'Client Secret',
isSecret: true
},
{
key: 'scope',

View File

@@ -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>

View File

@@ -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',

View File

@@ -110,6 +110,7 @@ const FormUrlEncodedParams = ({ item, collection }) => {
allowNewlines={true}
onRun={handleRun}
collection={collection}
item={item}
/>
</td>
<td>

View File

@@ -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>
);
};

View File

@@ -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
})}
>

View File

@@ -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>

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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>

View File

@@ -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}>

View File

@@ -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;

View File

@@ -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>

View File

@@ -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 || ''}

View File

@@ -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>

View File

@@ -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' ? (

View File

@@ -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">

View File

@@ -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

View File

@@ -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>
);
}

View File

@@ -14,6 +14,8 @@ const Wrapper = styled.div`
display: none;
}
scrollbar-width: none;
li {
display: inline-flex;
max-width: 150px;

View File

@@ -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>
);
})

View File

@@ -50,6 +50,7 @@ const CodeView = ({ language, item }) => {
</CopyToClipboard>
<CodeEditor
readOnly
collection={collection}
value={snippet}
font={get(preferences, 'font.codeFont', 'default')}
theme={displayedTheme}

View File

@@ -73,7 +73,7 @@ const GenerateCodeItem = ({ collection, item, onClose }) => {
const interpolatedUrl = interpolateUrl({
url: requestUrl,
envVars,
collectionVariables: collection.collectionVariables,
runtimeVariables: collection.runtimeVariables,
processEnvVars: collection.processEnvVariables
});

View File

@@ -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>

View File

@@ -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}

View File

@@ -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

View File

@@ -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>

View File

@@ -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;

View File

@@ -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>
);

View File

@@ -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};
}

View File

@@ -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());

View File

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

View File

@@ -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
};

View File

@@ -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,

View File

@@ -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;
},

View File

@@ -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

View File

@@ -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,

View File

@@ -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'
];

View File

@@ -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;
}

View File

@@ -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;
};

View File

@@ -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;
}
});
};

View File

@@ -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 ||

View File

@@ -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'));

View File

@@ -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);
});

View File

@@ -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

View File

@@ -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);

View File

@@ -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

View File

@@ -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,

View File

@@ -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

View File

@@ -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 : '';
}
})

View File

@@ -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');

View File

@@ -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