mirror of
https://github.com/usebruno/bruno.git
synced 2025-03-18 08:48:55 +01:00
Merge remote-tracking branch 'origin' into feature/add-raw-file-request-body-option
This commit is contained in:
commit
e7a10939ef
16
.github/workflows/tests.yml
vendored
16
.github/workflows/tests.yml
vendored
@ -15,6 +15,8 @@ jobs:
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: './package-lock.json'
|
||||
- name: Install dependencies
|
||||
run: npm ci --legacy-peer-deps
|
||||
|
||||
@ -50,6 +52,8 @@ jobs:
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: './package-lock.json'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci --legacy-peer-deps
|
||||
@ -71,15 +75,3 @@ jobs:
|
||||
with:
|
||||
files: packages/bruno-tests/collection/junit.xml
|
||||
comment_mode: always
|
||||
prettier:
|
||||
name: Prettier
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
- name: Install dependencies
|
||||
run: npm ci --legacy-peer-deps
|
||||
- name: Run Prettier
|
||||
run: npm run test:prettier:web
|
||||
|
@ -62,6 +62,9 @@ npm run build:graphql-docs
|
||||
# construction de bruno query
|
||||
npm run build:bruno-query
|
||||
|
||||
# construction de bruno common
|
||||
npm run build:bruno-common
|
||||
|
||||
# démarrage de next (terminal 1)
|
||||
npm run dev:web
|
||||
|
||||
|
84
docs/contributing/contributing_sk.md
Normal file
84
docs/contributing/contributing_sk.md
Normal file
@ -0,0 +1,84 @@
|
||||
## Urobme bruno lepším, spoločne !!
|
||||
|
||||
Sme radi, že chcete zlepšiť bruno. Nižšie sú uvedené pokyny, ako začať s výchovou bruno na vašom počítači.
|
||||
|
||||
### Technologický zásobník
|
||||
|
||||
Bruno je vytvorené pomocou Next.js a React. Na dodávanie desktopovej verzie (ktorá podporuje lokálne kolekcie) používame aj electron.
|
||||
|
||||
Balíčky, ktoré používame:
|
||||
|
||||
- CSS - Tailwind
|
||||
- Editory kódu - Codemirror
|
||||
- Správa stavu - Redux
|
||||
- Ikony - Tabler Icons
|
||||
- Formuláre - formik
|
||||
- Overovanie schém - Yup
|
||||
- Klient požiadaviek - axios
|
||||
- Sledovač súborového systému - chokidar
|
||||
|
||||
### Závislosti
|
||||
|
||||
Budete potrebovať [NodeJS v18.x alebo najnovšiu verziu LTS](https://nodejs.org/en/) a npm versiu 8.x. V projekte používame pracovné priestory npm
|
||||
|
||||
## Vývoj
|
||||
|
||||
Bruno sa vyvíja ako desktopová aplikácia. Aplikáciu je potrebné načítať spustením aplikácie Next.js v jednom termináli a potom spustiť aplikáciu electron v inom termináli.
|
||||
|
||||
### Závislosti
|
||||
|
||||
- NodeJS v18
|
||||
|
||||
### Miestny vývoj
|
||||
|
||||
```bash
|
||||
# použite verziu nodejs 18
|
||||
nvm use
|
||||
|
||||
# nainštalovať balíčky
|
||||
npm i --legacy-peer-deps
|
||||
|
||||
# zostaviť balíčky
|
||||
npm run build:graphql-docs
|
||||
npm run build:bruno-query
|
||||
npm run build:bruno-common
|
||||
|
||||
# spustite ďalšiu aplikáciu (terminál 1)
|
||||
npm run dev:web
|
||||
|
||||
# spustite aplikáciu electron (terminál 2)
|
||||
npm run dev:electron
|
||||
```
|
||||
|
||||
### Riešenie problémov
|
||||
|
||||
Pri spustení `npm install` sa môžete stretnúť s chybou `Unsupported platform`. Ak chcete túto chybu odstrániť, musíte odstrániť súbory `node_modules`, `package-lock.json` a spustiť `npm install`. Tým by sa mali nainštalovať všetky potrebné balíky potrebné na spustenie aplikácie.
|
||||
|
||||
```shell
|
||||
# Odstrániť node_modules v podadresároch
|
||||
find ./ -type d -name "node_modules" -print0 | while read -d $'\0' dir; do
|
||||
rm -rf "$dir"
|
||||
done
|
||||
|
||||
# Odstráňte package-lock v podadresároch
|
||||
find . -type f -name "package-lock.json" -delete
|
||||
```
|
||||
|
||||
### Testovanie
|
||||
|
||||
````bash
|
||||
# bruno-schema
|
||||
npm test --workspace=packages/bruno-schema
|
||||
|
||||
# bruno-lang
|
||||
npm test --workspace=packages/bruno-lang
|
||||
```
|
||||
|
||||
### Vyrobenie Pull Request
|
||||
|
||||
- Prosím, aby PR boli malé a zamerané na jednu vec
|
||||
- Prosím, dodržujte formát vytvárania vetiev
|
||||
- feature/[názov funkcie]: Táto vetva by mala obsahovať zmeny pre konkrétnu funkciu
|
||||
- Príklad: feature/dark-mode
|
||||
- bugfix/[názov chyby]: Táto vetva by mala obsahovať iba opravy konkrétnej chyby
|
||||
- Príklad: bugfix/bug-1
|
@ -103,6 +103,12 @@ Ou qualquer sistema de controle de versão de sua escolha.
|
||||
|
||||
<img src="../../assets/images/sponsors/commit-company.png" width="70"/>
|
||||
|
||||
#### Apoiadores Bronze
|
||||
|
||||
<a href="https://zuplo.link/bruno">
|
||||
<img src="../../assets/images/sponsors/zuplo.png" width="120"/>
|
||||
</a>
|
||||
|
||||
### Links Importantes 📌
|
||||
|
||||
- [Nossa Visão de Longo Prazo](https://github.com/usebruno/bruno/discussions/269)
|
||||
|
2
package-lock.json
generated
2
package-lock.json
generated
@ -19668,7 +19668,7 @@
|
||||
},
|
||||
"packages/bruno-electron": {
|
||||
"name": "bruno",
|
||||
"version": "v1.21.0",
|
||||
"version": "v1.23.1",
|
||||
"dependencies": {
|
||||
"@aws-sdk/credential-providers": "3.525.0",
|
||||
"@usebruno/common": "0.1.0",
|
||||
|
@ -6,6 +6,7 @@ const StyledWrapper = styled.div`
|
||||
border: solid 1px ${(props) => props.theme.codemirror.border};
|
||||
font-family: ${(props) => (props.font ? props.font : 'default')};
|
||||
line-break: anywhere;
|
||||
flex: 1 1 0;
|
||||
}
|
||||
|
||||
.CodeMirror-overlayscroll-horizontal div,
|
||||
|
@ -16,6 +16,7 @@ import stripJsonComments from 'strip-json-comments';
|
||||
|
||||
let CodeMirror;
|
||||
const SERVER_RENDERED = typeof navigator === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true;
|
||||
const TAB_SIZE = 2;
|
||||
|
||||
if (!SERVER_RENDERED) {
|
||||
CodeMirror = require('codemirror');
|
||||
@ -121,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,
|
||||
@ -169,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) => {
|
||||
@ -289,7 +316,7 @@ export default class CodeEditor extends React.Component {
|
||||
}
|
||||
return (
|
||||
<StyledWrapper
|
||||
className="h-full w-full"
|
||||
className="h-full w-full flex flex-col relative"
|
||||
aria-label="Code Editor"
|
||||
font={this.props.font}
|
||||
ref={(node) => {
|
||||
|
@ -138,6 +138,7 @@ const AwsV4Auth = ({ collection }) => {
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleSecretAccessKeyChange(val)}
|
||||
collection={collection}
|
||||
isSecret={true}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
@ -62,6 +62,7 @@ const BasicAuth = ({ collection }) => {
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handlePasswordChange(val)}
|
||||
collection={collection}
|
||||
isSecret={true}
|
||||
/>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
|
@ -37,6 +37,7 @@ const BearerAuth = ({ collection }) => {
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleTokenChange(val)}
|
||||
collection={collection}
|
||||
isSecret={true}
|
||||
/>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
|
@ -62,6 +62,7 @@ const DigestAuth = ({ collection }) => {
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handlePasswordChange(val)}
|
||||
collection={collection}
|
||||
isSecret={true}
|
||||
/>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
|
@ -78,7 +78,7 @@ const OAuth2AuthorizationCode = ({ collection }) => {
|
||||
return (
|
||||
<StyledWrapper className="mt-2 flex w-full gap-4 flex-col">
|
||||
{inputsConfig.map((input) => {
|
||||
const { key, label } = input;
|
||||
const { key, label, isSecret } = input;
|
||||
return (
|
||||
<div className="flex flex-col w-full gap-1" key={`input-${key}`}>
|
||||
<label className="block font-medium">{label}</label>
|
||||
@ -90,6 +90,7 @@ const OAuth2AuthorizationCode = ({ collection }) => {
|
||||
onChange={(val) => handleChange(key, val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
isSecret={isSecret}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -17,7 +17,8 @@ const inputsConfig = [
|
||||
},
|
||||
{
|
||||
key: 'clientSecret',
|
||||
label: 'Client Secret'
|
||||
label: 'Client Secret',
|
||||
isSecret: true
|
||||
},
|
||||
{
|
||||
key: 'scope',
|
||||
|
@ -42,7 +42,7 @@ const OAuth2ClientCredentials = ({ collection }) => {
|
||||
return (
|
||||
<StyledWrapper className="mt-2 flex w-full gap-4 flex-col">
|
||||
{inputsConfig.map((input) => {
|
||||
const { key, label } = input;
|
||||
const { key, label, isSecret } = input;
|
||||
return (
|
||||
<div className="flex flex-col w-full gap-1" key={`input-${key}`}>
|
||||
<label className="block font-medium">{label}</label>
|
||||
@ -54,6 +54,7 @@ const OAuth2ClientCredentials = ({ collection }) => {
|
||||
onChange={(val) => handleChange(key, val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
isSecret={isSecret}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -9,7 +9,8 @@ const inputsConfig = [
|
||||
},
|
||||
{
|
||||
key: 'clientSecret',
|
||||
label: 'Client Secret'
|
||||
label: 'Client Secret',
|
||||
isSecret: true
|
||||
},
|
||||
{
|
||||
key: 'scope',
|
||||
|
@ -44,7 +44,7 @@ const OAuth2AuthorizationCode = ({ item, collection }) => {
|
||||
return (
|
||||
<StyledWrapper className="mt-2 flex w-full gap-4 flex-col">
|
||||
{inputsConfig.map((input) => {
|
||||
const { key, label } = input;
|
||||
const { key, label, isSecret } = input;
|
||||
return (
|
||||
<div className="flex flex-col w-full gap-1" key={`input-${key}`}>
|
||||
<label className="block font-medium">{label}</label>
|
||||
@ -56,6 +56,7 @@ const OAuth2AuthorizationCode = ({ item, collection }) => {
|
||||
onChange={(val) => handleChange(key, val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
isSecret={isSecret}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -17,7 +17,8 @@ const inputsConfig = [
|
||||
},
|
||||
{
|
||||
key: 'clientSecret',
|
||||
label: 'Client Secret'
|
||||
label: 'Client Secret',
|
||||
isSecret: true
|
||||
},
|
||||
{
|
||||
key: 'scope',
|
||||
|
@ -74,6 +74,7 @@ const PresetsSettings = ({ collection }) => {
|
||||
id="request-url"
|
||||
type="text"
|
||||
name="requestUrl"
|
||||
placeholder='Request URL'
|
||||
className="block textbox"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
|
@ -1,14 +1,6 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
div.CodeMirror {
|
||||
/* todo: find a better way */
|
||||
height: calc(100vh - 240px);
|
||||
|
||||
.CodeMirror-scroll {
|
||||
padding-bottom: 0px;
|
||||
}
|
||||
}
|
||||
.editing-mode {
|
||||
cursor: pointer;
|
||||
color: ${(props) => props.theme.colors.text.yellow};
|
||||
|
@ -37,8 +37,8 @@ const Documentation = ({ item, collection }) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledWrapper className="mt-1 h-full w-full relative">
|
||||
<div className="editing-mode mb-2" role="tab" onClick={toggleViewMode}>
|
||||
<StyledWrapper className="flex flex-col gap-y-1 h-full w-full relative">
|
||||
<div className="editing-mode" role="tab" onClick={toggleViewMode}>
|
||||
{isEditing ? 'Preview' : 'Edit'}
|
||||
</div>
|
||||
|
||||
|
@ -40,10 +40,15 @@ const Wrapper = styled.div`
|
||||
color: ${(props) => props.theme.dropdown.iconColor};
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&:hover:not(:disabled) {
|
||||
background-color: ${(props) => props.theme.dropdown.hoverBg};
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
color: gray;
|
||||
}
|
||||
|
||||
&.border-top {
|
||||
border-top: solid 1px ${(props) => props.theme.dropdown.separator};
|
||||
}
|
||||
|
@ -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}
|
||||
|
@ -44,8 +44,8 @@ const Script = ({ collection, folder }) => {
|
||||
<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>
|
||||
<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 || ''}
|
||||
@ -56,8 +56,8 @@ const Script = ({ collection, folder }) => {
|
||||
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>
|
||||
<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 || ''}
|
||||
|
@ -50,7 +50,7 @@ const FolderSettings = ({ collection, folder }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<StyledWrapper className="flex flex-col h-full">
|
||||
<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')}>
|
||||
|
@ -2,7 +2,6 @@ import styled from 'styled-components';
|
||||
|
||||
const StyledMarkdownBodyWrapper = styled.div`
|
||||
background: transparent;
|
||||
height: inherit;
|
||||
.markdown-body {
|
||||
background: transparent;
|
||||
overflow-y: auto;
|
||||
|
@ -1,39 +1,42 @@
|
||||
import React from 'react';
|
||||
import { IconSpeakerphone, IconBrandTwitter, IconBrandGithub, IconBrandDiscord, IconBook } from '@tabler/icons';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { useDictionary } from 'providers/Dictionary/index';
|
||||
|
||||
const Support = () => {
|
||||
const { dictionary } = useDictionary();
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="rows">
|
||||
<div className="mt-2">
|
||||
<a href="https://docs.usebruno.com" target="_blank" className="flex items-end">
|
||||
<IconBook size={18} strokeWidth={2} />
|
||||
<span className="label ml-2">Documentation</span>
|
||||
<span className="label ml-2">{dictionary.documentation}</span>
|
||||
</a>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<a href="https://github.com/usebruno/bruno/issues" target="_blank" className="flex items-end">
|
||||
<IconSpeakerphone size={18} strokeWidth={2} />
|
||||
<span className="label ml-2">Report Issues</span>
|
||||
<span className="label ml-2">{dictionary.reportIssues}</span>
|
||||
</a>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<a href="https://discord.com/invite/KgcZUncpjq" target="_blank" className="flex items-end">
|
||||
<IconBrandDiscord size={18} strokeWidth={2} />
|
||||
<span className="label ml-2">Discord</span>
|
||||
<span className="label ml-2">{dictionary.discord}</span>
|
||||
</a>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<a href="https://github.com/usebruno/bruno" target="_blank" className="flex items-end">
|
||||
<IconBrandGithub size={18} strokeWidth={2} />
|
||||
<span className="label ml-2">GitHub</span>
|
||||
<span className="label ml-2">{dictionary.gitHub}</span>
|
||||
</a>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<a href="https://twitter.com/use_bruno" target="_blank" className="flex items-end">
|
||||
<IconBrandTwitter size={18} strokeWidth={2} />
|
||||
<span className="label ml-2">Twitter</span>
|
||||
<span className="label ml-2">{dictionary.twitter}</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -150,6 +150,7 @@ const AwsV4Auth = ({ onTokenChange, item, collection }) => {
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
item={item}
|
||||
isSecret={true}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
@ -69,6 +69,7 @@ const BasicAuth = ({ item, collection }) => {
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
item={item}
|
||||
isSecret={true}
|
||||
/>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
|
@ -43,6 +43,7 @@ const BearerAuth = ({ item, collection }) => {
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
item={item}
|
||||
isSecret={true}
|
||||
/>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
|
@ -69,6 +69,7 @@ const DigestAuth = ({ item, collection }) => {
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
item={item}
|
||||
isSecret={true}
|
||||
/>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
|
@ -80,7 +80,7 @@ const OAuth2AuthorizationCode = ({ item, collection }) => {
|
||||
return (
|
||||
<StyledWrapper className="mt-2 flex w-full gap-4 flex-col">
|
||||
{inputsConfig.map((input) => {
|
||||
const { key, label } = input;
|
||||
const { key, label, isSecret } = input;
|
||||
return (
|
||||
<div className="flex flex-col w-full gap-1" key={`input-${key}`}>
|
||||
<label className="block font-medium">{label}</label>
|
||||
@ -93,6 +93,7 @@ const OAuth2AuthorizationCode = ({ item, collection }) => {
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
item={item}
|
||||
isSecret={isSecret}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -17,7 +17,8 @@ const inputsConfig = [
|
||||
},
|
||||
{
|
||||
key: 'clientSecret',
|
||||
label: 'Client Secret'
|
||||
label: 'Client Secret',
|
||||
isSecret: true
|
||||
},
|
||||
{
|
||||
key: 'scope',
|
||||
|
@ -43,7 +43,7 @@ const OAuth2ClientCredentials = ({ item, collection }) => {
|
||||
return (
|
||||
<StyledWrapper className="mt-2 flex w-full gap-4 flex-col">
|
||||
{inputsConfig.map((input) => {
|
||||
const { key, label } = input;
|
||||
const { key, label, isSecret } = input;
|
||||
return (
|
||||
<div className="flex flex-col w-full gap-1" key={`input-${key}`}>
|
||||
<label className="block font-medium">{label}</label>
|
||||
@ -56,6 +56,7 @@ const OAuth2ClientCredentials = ({ item, collection }) => {
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
item={item}
|
||||
isSecret={isSecret}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -9,7 +9,8 @@ const inputsConfig = [
|
||||
},
|
||||
{
|
||||
key: 'clientSecret',
|
||||
label: 'Client Secret'
|
||||
label: 'Client Secret',
|
||||
isSecret: true
|
||||
},
|
||||
{
|
||||
key: 'scope',
|
||||
|
@ -45,7 +45,7 @@ const OAuth2AuthorizationCode = ({ item, collection }) => {
|
||||
return (
|
||||
<StyledWrapper className="mt-2 flex w-full gap-4 flex-col">
|
||||
{inputsConfig.map((input) => {
|
||||
const { key, label } = input;
|
||||
const { key, label, isSecret } = input;
|
||||
return (
|
||||
<div className="flex flex-col w-full gap-1" key={`input-${key}`}>
|
||||
<label className="block font-medium">{label}</label>
|
||||
@ -58,6 +58,7 @@ const OAuth2AuthorizationCode = ({ item, collection }) => {
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
item={item}
|
||||
isSecret={isSecret}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -9,7 +9,8 @@ const inputsConfig = [
|
||||
},
|
||||
{
|
||||
key: 'password',
|
||||
label: 'Password'
|
||||
label: 'Password',
|
||||
isSecret: true
|
||||
},
|
||||
{
|
||||
key: 'clientId',
|
||||
@ -17,7 +18,8 @@ const inputsConfig = [
|
||||
},
|
||||
{
|
||||
key: 'clientSecret',
|
||||
label: 'Client Secret'
|
||||
label: 'Client Secret',
|
||||
isSecret: true
|
||||
},
|
||||
{
|
||||
key: 'scope',
|
||||
|
@ -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
|
||||
})}
|
||||
>
|
||||
|
@ -4,6 +4,7 @@ const StyledWrapper = styled.div`
|
||||
div.CodeMirror {
|
||||
background: ${(props) => props.theme.codemirror.bg};
|
||||
border: solid 1px ${(props) => props.theme.codemirror.border};
|
||||
flex: 1 1 0;
|
||||
}
|
||||
|
||||
textarea.cm-editor {
|
||||
|
@ -74,7 +74,7 @@ const QueryUrl = ({ item, collection, handleRun }) => {
|
||||
/>
|
||||
<div className="flex items-center h-full mr-2 cursor-pointer" id="send-request" onClick={handleRun}>
|
||||
<div
|
||||
className="tooltip mr-3"
|
||||
className="tooltip mx-3"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (!item.draft) return;
|
||||
|
@ -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;
|
||||
|
@ -40,8 +40,8 @@ const Script = ({ item, collection }) => {
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full flex flex-col">
|
||||
<div className="flex-1 mt-2">
|
||||
<div className="mb-1 title text-xs">Pre Request</div>
|
||||
<div className="flex flex-col flex-1 mt-2 gap-y-2">
|
||||
<div className="title text-xs">Pre Request</div>
|
||||
<CodeEditor
|
||||
collection={collection}
|
||||
value={requestScript || ''}
|
||||
@ -53,8 +53,8 @@ const Script = ({ item, collection }) => {
|
||||
onSave={onSave}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 mt-6">
|
||||
<div className="mt-1 mb-1 title text-xs">Post Response</div>
|
||||
<div className="flex flex-col flex-1 mt-2 gap-y-2">
|
||||
<div className="title text-xs">Post Response</div>
|
||||
<CodeEditor
|
||||
collection={collection}
|
||||
value={responseScript || ''}
|
||||
|
@ -9,11 +9,11 @@ const Vars = ({ item, collection }) => {
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full flex flex-col">
|
||||
<div className="flex-1 mt-2">
|
||||
<div className="mt-2">
|
||||
<div className="mb-1 title text-xs">Pre Request</div>
|
||||
<VarsTable item={item} collection={collection} vars={requestVars} varType="request" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div>
|
||||
<div className="mt-1 mb-1 title text-xs">Post Response</div>
|
||||
<VarsTable item={item} collection={collection} vars={responseVars} varType="response" />
|
||||
</div>
|
||||
|
@ -30,7 +30,7 @@ const RequestNotFound = ({ itemUid }) => {
|
||||
|
||||
return (
|
||||
<div className="mt-6 px-6">
|
||||
<div className="p-4 bg-orange-100 border-l-4 border-yellow-500 text-yellow-700 bg-yellow-100 p-4">
|
||||
<div className="p-4 bg-orange-100 border-l-4 border-yellow-500 text-yellow-700">
|
||||
<div>Request no longer exists.</div>
|
||||
<div className="mt-2">
|
||||
This can happen when the .bru file associated with this request was deleted on your filesystem.
|
||||
|
@ -158,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' ? (
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useRef, Fragment } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { closeTabs } from 'providers/ReduxStore/slices/tabs';
|
||||
import { saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
@ -12,12 +12,18 @@ import ConfirmRequestClose from './ConfirmRequestClose';
|
||||
import RequestTabNotFound from './RequestTabNotFound';
|
||||
import SpecialTab from './SpecialTab';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import CloneCollectionItem from 'components/Sidebar/Collections/Collection/CollectionItem/CloneCollectionItem/index';
|
||||
import NewRequest from 'components/Sidebar/NewRequest/index';
|
||||
|
||||
const RequestTab = ({ tab, collection, folderUid }) => {
|
||||
const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUid }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
const [showConfirmClose, setShowConfirmClose] = useState(false);
|
||||
|
||||
const dropdownTippyRef = useRef();
|
||||
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
|
||||
|
||||
const handleCloseClick = (event) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
@ -28,6 +34,19 @@ const RequestTab = ({ tab, collection, folderUid }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const handleRightClick = (_event) => {
|
||||
const menuDropdown = dropdownTippyRef.current;
|
||||
if (!menuDropdown) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (menuDropdown.state.isShown) {
|
||||
menuDropdown.hide();
|
||||
} else {
|
||||
menuDropdown.show();
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseUp = (e) => {
|
||||
if (e.button === 1) {
|
||||
e.stopPropagation();
|
||||
@ -143,6 +162,7 @@ const RequestTab = ({ tab, collection, folderUid }) => {
|
||||
)}
|
||||
<div
|
||||
className="flex items-baseline tab-label pl-2"
|
||||
onContextMenu={handleRightClick}
|
||||
onMouseUp={(e) => {
|
||||
if (!item.draft) return handleMouseUp(e);
|
||||
|
||||
@ -159,6 +179,15 @@ const RequestTab = ({ tab, collection, folderUid }) => {
|
||||
<span className="ml-1 tab-name" title={item.name}>
|
||||
{item.name}
|
||||
</span>
|
||||
<RequestTabMenu
|
||||
onDropdownCreate={onDropdownCreate}
|
||||
tabIndex={tabIndex}
|
||||
collectionRequestTabs={collectionRequestTabs}
|
||||
tabItem={item}
|
||||
collection={collection}
|
||||
dropdownTippyRef={dropdownTippyRef}
|
||||
dispatch={dispatch}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="flex px-2 close-icon-container"
|
||||
@ -195,4 +224,124 @@ const RequestTab = ({ tab, collection, folderUid }) => {
|
||||
);
|
||||
};
|
||||
|
||||
function RequestTabMenu({ onDropdownCreate, collectionRequestTabs, tabIndex, collection, dropdownTippyRef, dispatch }) {
|
||||
const [showCloneRequestModal, setShowCloneRequestModal] = useState(false);
|
||||
const [showAddNewRequestModal, setShowAddNewRequestModal] = useState(false);
|
||||
|
||||
const totalTabs = collectionRequestTabs.length || 0;
|
||||
const currentTabUid = collectionRequestTabs[tabIndex]?.uid;
|
||||
const currentTabItem = findItemInCollection(collection, currentTabUid);
|
||||
|
||||
const hasLeftTabs = tabIndex !== 0;
|
||||
const hasRightTabs = totalTabs > tabIndex + 1;
|
||||
const hasOtherTabs = totalTabs > 1;
|
||||
|
||||
async function handleCloseTab(event, tabUid) {
|
||||
event.stopPropagation();
|
||||
dropdownTippyRef.current.hide();
|
||||
|
||||
if (!tabUid) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const item = findItemInCollection(collection, tabUid);
|
||||
// silently save unsaved changes before closing the tab
|
||||
if (item.draft) {
|
||||
await dispatch(saveRequest(item.uid, collection.uid, true));
|
||||
}
|
||||
|
||||
dispatch(closeTabs({ tabUids: [tabUid] }));
|
||||
} catch (err) {}
|
||||
}
|
||||
|
||||
function handleCloseOtherTabs(event) {
|
||||
dropdownTippyRef.current.hide();
|
||||
|
||||
const otherTabs = collectionRequestTabs.filter((_, index) => index !== tabIndex);
|
||||
otherTabs.forEach((tab) => handleCloseTab(event, tab.uid));
|
||||
}
|
||||
|
||||
function handleCloseTabsToTheLeft(event) {
|
||||
dropdownTippyRef.current.hide();
|
||||
|
||||
const leftTabs = collectionRequestTabs.filter((_, index) => index < tabIndex);
|
||||
leftTabs.forEach((tab) => handleCloseTab(event, tab.uid));
|
||||
}
|
||||
|
||||
function handleCloseTabsToTheRight(event) {
|
||||
dropdownTippyRef.current.hide();
|
||||
|
||||
const rightTabs = collectionRequestTabs.filter((_, index) => index > tabIndex);
|
||||
rightTabs.forEach((tab) => handleCloseTab(event, tab.uid));
|
||||
}
|
||||
|
||||
function handleCloseSavedTabs(event) {
|
||||
event.stopPropagation();
|
||||
|
||||
const savedTabs = collection.items.filter((item) => !item.draft);
|
||||
const savedTabIds = savedTabs.map((item) => item.uid) || [];
|
||||
dispatch(closeTabs({ tabUids: savedTabIds }));
|
||||
}
|
||||
|
||||
function handleCloseAllTabs(event) {
|
||||
collectionRequestTabs.forEach((tab) => handleCloseTab(event, tab.uid));
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{showAddNewRequestModal && (
|
||||
<NewRequest collection={collection} onClose={() => setShowAddNewRequestModal(false)} />
|
||||
)}
|
||||
|
||||
{showCloneRequestModal && (
|
||||
<CloneCollectionItem
|
||||
item={currentTabItem}
|
||||
collection={collection}
|
||||
onClose={() => setShowCloneRequestModal(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Dropdown onCreate={onDropdownCreate} icon={<span></span>} placement="bottom-start">
|
||||
<button
|
||||
className="dropdown-item w-full"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
setShowAddNewRequestModal(true);
|
||||
}}
|
||||
>
|
||||
New Request
|
||||
</button>
|
||||
<button
|
||||
className="dropdown-item w-full"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
setShowCloneRequestModal(true);
|
||||
}}
|
||||
>
|
||||
Clone Request
|
||||
</button>
|
||||
<button className="dropdown-item w-full" onClick={(e) => handleCloseTab(e, currentTabUid)}>
|
||||
Close
|
||||
</button>
|
||||
<button disabled={!hasOtherTabs} className="dropdown-item w-full" onClick={handleCloseOtherTabs}>
|
||||
Close Others
|
||||
</button>
|
||||
<button disabled={!hasLeftTabs} className="dropdown-item w-full" onClick={handleCloseTabsToTheLeft}>
|
||||
Close to the Left
|
||||
</button>
|
||||
<button disabled={!hasRightTabs} className="dropdown-item w-full" onClick={handleCloseTabsToTheRight}>
|
||||
Close to the Right
|
||||
</button>
|
||||
<button className="dropdown-item w-full" onClick={handleCloseSavedTabs}>
|
||||
Close Saved
|
||||
</button>
|
||||
<button className="dropdown-item w-full" onClick={handleCloseAllTabs}>
|
||||
Close All
|
||||
</button>
|
||||
</Dropdown>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
export default RequestTab;
|
||||
|
@ -7,13 +7,14 @@ const Wrapper = styled.div`
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
position: relative;
|
||||
overflow: scroll;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
scrollbar-width: none;
|
||||
|
||||
li {
|
||||
display: inline-flex;
|
||||
max-width: 150px;
|
||||
|
@ -110,7 +110,14 @@ const RequestTabs = () => {
|
||||
role="tab"
|
||||
onClick={() => handleClick(tab)}
|
||||
>
|
||||
<RequestTab key={tab.uid} tab={tab} collection={activeCollection} folderUid={tab.folderUid} />
|
||||
<RequestTab
|
||||
collectionRequestTabs={collectionRequestTabs}
|
||||
tabIndex={index}
|
||||
key={tab.uid}
|
||||
tab={tab}
|
||||
collection={activeCollection}
|
||||
folderUid={tab.folderUid}
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
})
|
||||
|
@ -1,6 +1,19 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
.textbox {
|
||||
border: 1px solid #ccc;
|
||||
padding: 0.2rem 0.5rem;
|
||||
box-shadow: none;
|
||||
border-radius: 0px;
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
transition: border-color ease-in-out 0.1s;
|
||||
border-radius: 3px;
|
||||
background-color: ${(props) => props.theme.modal.input.bg};
|
||||
border: 1px solid ${(props) => props.theme.modal.input.border};
|
||||
}
|
||||
|
||||
.item-path {
|
||||
.link {
|
||||
color: ${(props) => props.theme.textLink};
|
||||
|
@ -23,6 +23,7 @@ const getRelativePath = (fullPath, pathname) => {
|
||||
export default function RunnerResults({ collection }) {
|
||||
const dispatch = useDispatch();
|
||||
const [selectedItem, setSelectedItem] = useState(null);
|
||||
const [delay, setDelay] = useState(null);
|
||||
|
||||
// ref for the runner output body
|
||||
const runnerBodyRef = useRef();
|
||||
@ -78,11 +79,11 @@ export default function RunnerResults({ collection }) {
|
||||
.filter(Boolean);
|
||||
|
||||
const runCollection = () => {
|
||||
dispatch(runCollectionFolder(collection.uid, null, true));
|
||||
dispatch(runCollectionFolder(collection.uid, null, true, Number(delay)));
|
||||
};
|
||||
|
||||
const runAgain = () => {
|
||||
dispatch(runCollectionFolder(collection.uid, runnerInfo.folderUid, runnerInfo.isRecursive));
|
||||
dispatch(runCollectionFolder(collection.uid, runnerInfo.folderUid, runnerInfo.isRecursive, Number(delay)));
|
||||
};
|
||||
|
||||
const resetRunner = () => {
|
||||
@ -116,6 +117,20 @@ export default function RunnerResults({ collection }) {
|
||||
You have <span className="font-medium">{totalRequestsInCollection}</span> requests in this collection.
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
<label>Delay (in ms)</label>
|
||||
<input
|
||||
type="number"
|
||||
className="block textbox mt-2 py-5"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={delay}
|
||||
onChange={(e) => setDelay(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary mt-6" onClick={runCollection}>
|
||||
Run Collection
|
||||
</button>
|
||||
@ -167,10 +182,14 @@ export default function RunnerResults({ collection }) {
|
||||
</span>
|
||||
{item.status !== 'error' && item.status !== 'completed' ? (
|
||||
<IconRefresh className="animate-spin ml-1" size={18} strokeWidth={1.5} />
|
||||
) : (
|
||||
) : item.responseReceived?.status ? (
|
||||
<span className="text-xs link cursor-pointer" onClick={() => setSelectedItem(item)}>
|
||||
(<span className="mr-1">{get(item.responseReceived, 'status')}</span>
|
||||
<span>{get(item.responseReceived, 'statusText')}</span>)
|
||||
(<span className="mr-1">{item.responseReceived?.status}</span>
|
||||
<span>{item.responseReceived?.statusText}</span>)
|
||||
</span>
|
||||
) : (
|
||||
<span className="danger text-xs cursor-pointer" onClick={() => setSelectedItem(item)}>
|
||||
(request failed)
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
@ -58,6 +58,7 @@ const CloneCollectionItem = ({ collection, item, onClose }) => {
|
||||
id="collection-item-name"
|
||||
type="text"
|
||||
name="name"
|
||||
placeholder='Enter Item name'
|
||||
ref={inputRef}
|
||||
className="block textbox mt-2 w-full"
|
||||
autoComplete="off"
|
||||
|
@ -2,6 +2,7 @@ import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
position: relative;
|
||||
height: 100%;
|
||||
|
||||
.copy-to-clipboard {
|
||||
position: absolute;
|
||||
|
@ -91,13 +91,13 @@ const Collections = () => {
|
||||
<input
|
||||
type="text"
|
||||
name="search"
|
||||
placeholder="search"
|
||||
id="search"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
className="block w-full pl-7 py-1 sm:text-sm"
|
||||
placeholder="search"
|
||||
value={searchText}
|
||||
onChange={(e) => setSearchText(e.target.value.toLowerCase())}
|
||||
/>
|
||||
|
@ -115,7 +115,7 @@ const ImportCollectionLocation = ({ onClose, handleSubmit, collectionName, trans
|
||||
collectionLocation: Yup.string()
|
||||
.min(1, 'must be at least 1 character')
|
||||
.max(500, 'must be 500 characters or less')
|
||||
.required('name is required')
|
||||
.required('Location is required')
|
||||
}),
|
||||
onSubmit: (values) => {
|
||||
handleSubmit(values.collectionLocation);
|
||||
@ -124,7 +124,9 @@ const ImportCollectionLocation = ({ onClose, handleSubmit, collectionName, trans
|
||||
const browse = () => {
|
||||
dispatch(browseDirectory())
|
||||
.then((dirPath) => {
|
||||
formik.setFieldValue('collectionLocation', dirPath);
|
||||
if (typeof dirPath === 'string' && dirPath.length > 0) {
|
||||
formik.setFieldValue('collectionLocation', dirPath);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
formik.setFieldValue('collectionLocation', '');
|
||||
@ -160,7 +162,7 @@ const ImportCollectionLocation = ({ onClose, handleSubmit, collectionName, trans
|
||||
type="text"
|
||||
name="collectionLocation"
|
||||
readOnly={true}
|
||||
className="block textbox mt-2 w-full"
|
||||
className="block textbox mt-2 w-full cursor-pointer"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
|
@ -109,7 +109,8 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
|
||||
collectionUid: collection.uid,
|
||||
itemUid: item ? item.uid : null,
|
||||
headers: request.headers,
|
||||
body: request.body
|
||||
body: request.body,
|
||||
auth: request.auth
|
||||
})
|
||||
)
|
||||
.then(() => onClose())
|
||||
@ -161,7 +162,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
|
||||
@ -220,6 +230,7 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
|
||||
id="request-name"
|
||||
type="text"
|
||||
name="requestName"
|
||||
placeholder="Request Name"
|
||||
ref={inputRef}
|
||||
className="block textbox mt-2 w-full"
|
||||
autoComplete="off"
|
||||
@ -252,6 +263,7 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
|
||||
id="request-url"
|
||||
type="text"
|
||||
name="requestUrl"
|
||||
placeholder="Request URL"
|
||||
className="px-3 w-full "
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
|
@ -129,7 +129,7 @@ const Sidebar = () => {
|
||||
Star
|
||||
</GitHubButton> */}
|
||||
</div>
|
||||
<div className="flex flex-grow items-center justify-end text-xs mr-2">v1.22.0</div>
|
||||
<div className="flex flex-grow items-center justify-end text-xs mr-2">v1.25.0</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,8 +1,9 @@
|
||||
import React, { Component } from 'react';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import { getAllVariables } from 'utils/collections';
|
||||
import { defineCodeMirrorBrunoVariablesMode } from 'utils/common/codemirror';
|
||||
import { defineCodeMirrorBrunoVariablesMode, MaskedEditor } from 'utils/common/codemirror';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { IconEye, IconEyeOff } from '@tabler/icons';
|
||||
|
||||
let CodeMirror;
|
||||
const SERVER_RENDERED = typeof navigator === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true;
|
||||
@ -20,12 +21,28 @@ class SingleLineEditor extends Component {
|
||||
this.cachedValue = props.value || '';
|
||||
this.editorRef = React.createRef();
|
||||
this.variables = {};
|
||||
|
||||
this.state = {
|
||||
maskInput: props.isSecret || false // Always mask the input by default (if it's a secret)
|
||||
};
|
||||
}
|
||||
componentDidMount() {
|
||||
// Initialize CodeMirror as a single line editor
|
||||
/** @type {import("codemirror").Editor} */
|
||||
const variables = getAllVariables(this.props.collection, this.props.item);
|
||||
|
||||
const runHandler = () => {
|
||||
if (this.props.onRun) {
|
||||
this.props.onRun();
|
||||
}
|
||||
};
|
||||
const saveHandler = () => {
|
||||
if (this.props.onSave) {
|
||||
this.props.onSave();
|
||||
}
|
||||
};
|
||||
const noopHandler = () => {};
|
||||
|
||||
this.editor = CodeMirror(this.editorRef.current, {
|
||||
lineWrapping: false,
|
||||
lineNumbers: false,
|
||||
@ -37,21 +54,9 @@ class SingleLineEditor extends Component {
|
||||
scrollbarStyle: null,
|
||||
tabindex: 0,
|
||||
extraKeys: {
|
||||
Enter: () => {
|
||||
if (this.props.onRun) {
|
||||
this.props.onRun();
|
||||
}
|
||||
},
|
||||
'Ctrl-Enter': () => {
|
||||
if (this.props.onRun) {
|
||||
this.props.onRun();
|
||||
}
|
||||
},
|
||||
'Cmd-Enter': () => {
|
||||
if (this.props.onRun) {
|
||||
this.props.onRun();
|
||||
}
|
||||
},
|
||||
Enter: runHandler,
|
||||
'Ctrl-Enter': runHandler,
|
||||
'Cmd-Enter': runHandler,
|
||||
'Alt-Enter': () => {
|
||||
if (this.props.allowNewlines) {
|
||||
this.editor.setValue(this.editor.getValue() + '\n');
|
||||
@ -60,23 +65,11 @@ class SingleLineEditor extends Component {
|
||||
this.props.onRun();
|
||||
}
|
||||
},
|
||||
'Shift-Enter': () => {
|
||||
if (this.props.onRun) {
|
||||
this.props.onRun();
|
||||
}
|
||||
},
|
||||
'Cmd-S': () => {
|
||||
if (this.props.onSave) {
|
||||
this.props.onSave();
|
||||
}
|
||||
},
|
||||
'Ctrl-S': () => {
|
||||
if (this.props.onSave) {
|
||||
this.props.onSave();
|
||||
}
|
||||
},
|
||||
'Cmd-F': () => {},
|
||||
'Ctrl-F': () => {},
|
||||
'Shift-Enter': runHandler,
|
||||
'Cmd-S': saveHandler,
|
||||
'Ctrl-S': saveHandler,
|
||||
'Cmd-F': noopHandler,
|
||||
'Ctrl-F': noopHandler,
|
||||
// Tabbing disabled to make tabindex work
|
||||
Tab: false,
|
||||
'Shift-Tab': false
|
||||
@ -93,8 +86,24 @@ class SingleLineEditor extends Component {
|
||||
this.editor.setValue(String(this.props.value) || '');
|
||||
this.editor.on('change', this._onEdit);
|
||||
this.addOverlay(variables);
|
||||
this._enableMaskedEditor(this.props.isSecret);
|
||||
this.setState({ maskInput: this.props.isSecret });
|
||||
}
|
||||
|
||||
/** Enable or disable masking the rendered content of the editor */
|
||||
_enableMaskedEditor = (enabled) => {
|
||||
if (typeof enabled !== 'boolean') return;
|
||||
|
||||
console.log('Enabling masked editor: ' + enabled);
|
||||
if (enabled == true) {
|
||||
if (!this.maskedEditor) this.maskedEditor = new MaskedEditor(this.editor, '*');
|
||||
this.maskedEditor.enable();
|
||||
} else {
|
||||
this.maskedEditor?.disable();
|
||||
this.maskedEditor = null;
|
||||
}
|
||||
};
|
||||
|
||||
_onEdit = () => {
|
||||
if (!this.ignoreChangeEvent && this.editor) {
|
||||
this.cachedValue = this.editor.getValue();
|
||||
@ -122,6 +131,12 @@ class SingleLineEditor extends Component {
|
||||
this.cachedValue = String(this.props.value);
|
||||
this.editor.setValue(String(this.props.value) || '');
|
||||
}
|
||||
if (!isEqual(this.props.isSecret, prevProps.isSecret)) {
|
||||
// If the secret flag has changed, update the editor to reflect the change
|
||||
this._enableMaskedEditor(this.props.isSecret);
|
||||
// also set the maskInput flag to the new value
|
||||
this.setState({ maskInput: this.props.isSecret });
|
||||
}
|
||||
this.ignoreChangeEvent = false;
|
||||
}
|
||||
|
||||
@ -135,8 +150,35 @@ class SingleLineEditor extends Component {
|
||||
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 overflow-x-auto">
|
||||
<StyledWrapper ref={this.editorRef} className="single-line-editor grow" />
|
||||
{this.secretEye(this.props.isSecret)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
export default SingleLineEditor;
|
||||
|
@ -9,9 +9,11 @@ import CreateCollection from 'components/Sidebar/CreateCollection';
|
||||
import ImportCollection from 'components/Sidebar/ImportCollection';
|
||||
import ImportCollectionLocation from 'components/Sidebar/ImportCollectionLocation';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { useDictionary } from 'providers/Dictionary/index';
|
||||
|
||||
const Welcome = () => {
|
||||
const dispatch = useDispatch();
|
||||
const { dictionary } = useDictionary();
|
||||
const [importedCollection, setImportedCollection] = useState(null);
|
||||
const [importedTranslationLog, setImportedTranslationLog] = useState({});
|
||||
const [createCollectionModalOpen, setCreateCollectionModalOpen] = useState(false);
|
||||
@ -20,7 +22,7 @@ const Welcome = () => {
|
||||
|
||||
const handleOpenCollection = () => {
|
||||
dispatch(openCollection()).catch(
|
||||
(err) => console.log(err) && toast.error('An error occurred while opening the collection')
|
||||
(err) => console.log(err) && toast.error(dictionary.errorWhileOpeningCollection)
|
||||
);
|
||||
};
|
||||
|
||||
@ -38,12 +40,12 @@ const Welcome = () => {
|
||||
.then(() => {
|
||||
setImportCollectionLocationModalOpen(false);
|
||||
setImportedCollection(null);
|
||||
toast.success('Collection imported successfully');
|
||||
toast.success(dictionary.collectionImportedSuccessfully);
|
||||
})
|
||||
.catch((err) => {
|
||||
setImportCollectionLocationModalOpen(false);
|
||||
console.error(err);
|
||||
toast.error('An error occurred while importing the collection. Check the logs for more information.');
|
||||
toast.error(dictionary.errorWhileImportingCollection);
|
||||
});
|
||||
};
|
||||
|
||||
@ -66,46 +68,45 @@ const Welcome = () => {
|
||||
<Bruno width={50} />
|
||||
</div>
|
||||
<div className="text-xl font-semibold select-none">bruno</div>
|
||||
<div className="mt-4">Opensource IDE for exploring and testing APIs</div>
|
||||
<div className="mt-4">{dictionary.aboutBruno}</div>
|
||||
|
||||
<div className="uppercase font-semibold heading mt-10">Collections</div>
|
||||
<div className="uppercase font-semibold heading mt-10">{dictionary.collections}</div>
|
||||
<div className="mt-4 flex items-center collection-options select-none">
|
||||
<div className="flex items-center" onClick={() => setCreateCollectionModalOpen(true)}>
|
||||
<IconPlus size={18} strokeWidth={2} />
|
||||
<span className="label ml-2" id="create-collection">
|
||||
Create Collection
|
||||
{dictionary.createCollection}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center ml-6" onClick={handleOpenCollection}>
|
||||
<IconFolders size={18} strokeWidth={2} />
|
||||
<span className="label ml-2">Open Collection</span>
|
||||
<span className="label ml-2">{dictionary.openCollection}</span>
|
||||
</div>
|
||||
<div className="flex items-center ml-6" onClick={() => setImportCollectionModalOpen(true)}>
|
||||
<IconDownload size={18} strokeWidth={2} />
|
||||
<span className="label ml-2" id="import-collection">
|
||||
Import Collection
|
||||
{dictionary.importCollection}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="uppercase font-semibold heading mt-10 pt-6">Links</div>
|
||||
<div className="mt-4 flex flex-col collection-options select-none">
|
||||
<div className="flex items-center mt-2">
|
||||
<a href="https://docs.usebruno.com" target="_blank" className="inline-flex items-center">
|
||||
<IconBook size={18} strokeWidth={2} />
|
||||
<span className="label ml-2">Documentation</span>
|
||||
<span className="label ml-2">{dictionary.documentation}</span>
|
||||
</a>
|
||||
</div>
|
||||
<div className="flex items-center mt-2">
|
||||
<a href="https://github.com/usebruno/bruno/issues" target="_blank" className="inline-flex items-center">
|
||||
<IconSpeakerphone size={18} strokeWidth={2} />
|
||||
<span className="label ml-2">Report Issues</span>
|
||||
<span className="label ml-2">{dictionary.reportIssues}</span>
|
||||
</a>
|
||||
</div>
|
||||
<div className="flex items-center mt-2">
|
||||
<a href="https://github.com/usebruno/bruno" target="_blank" className="flex items-center">
|
||||
<IconBrandGithub size={18} strokeWidth={2} />
|
||||
<span className="label ml-2">GitHub</span>
|
||||
<span className="label ml-2">{dictionary.gitHub}</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
16
packages/bruno-app/src/dictionaries/en.js
Normal file
16
packages/bruno-app/src/dictionaries/en.js
Normal file
@ -0,0 +1,16 @@
|
||||
export default {
|
||||
aboutBruno: 'Opensource IDE for exploring and testing APIs',
|
||||
collections: 'Collections',
|
||||
createCollection: 'Create Collection',
|
||||
openCollection: 'Open Collection',
|
||||
importCollection: 'Import Collection',
|
||||
documentation: 'Documentation',
|
||||
reportIssues: 'Report Issues',
|
||||
gitHub: 'GitHub',
|
||||
collectionImportedSuccessfully: 'Collection imported successfully',
|
||||
errorWhileOpeningCollection: 'An error occurred while opening the collection',
|
||||
errorWhileImportingCollection:
|
||||
'An error occurred while importing the collection. Check the logs for more information.',
|
||||
discord: 'Discord',
|
||||
twitter: 'Twitter'
|
||||
};
|
5
packages/bruno-app/src/dictionaries/index.js
Normal file
5
packages/bruno-app/src/dictionaries/index.js
Normal file
@ -0,0 +1,5 @@
|
||||
import en from './en.js';
|
||||
|
||||
export const dictionaries = {
|
||||
en
|
||||
};
|
@ -100,6 +100,11 @@ const GlobalStyle = createGlobalStyle`
|
||||
}
|
||||
}
|
||||
|
||||
input::placeholder {
|
||||
color: ${(props) => props.theme.input.placeholder.color};
|
||||
opacity: ${(props) => props.theme.input.placeholder.opacity};
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
|
@ -14,6 +14,7 @@ import 'codemirror/lib/codemirror.css';
|
||||
import 'graphiql/graphiql.min.css';
|
||||
import 'react-tooltip/dist/react-tooltip.css';
|
||||
import '@usebruno/graphql-docs/dist/esm/index.css';
|
||||
import { DictionaryProvider } from 'providers/Dictionary/index';
|
||||
|
||||
function SafeHydrate({ children }) {
|
||||
return <div suppressHydrationWarning>{typeof window === 'undefined' ? null : children}</div>;
|
||||
@ -59,13 +60,15 @@ function MyApp({ Component, pageProps }) {
|
||||
<NoSsr>
|
||||
<Provider store={ReduxStore}>
|
||||
<ThemeProvider>
|
||||
<ToastProvider>
|
||||
<AppProvider>
|
||||
<HotkeysProvider>
|
||||
<Component {...pageProps} />
|
||||
</HotkeysProvider>
|
||||
</AppProvider>
|
||||
</ToastProvider>
|
||||
<DictionaryProvider>
|
||||
<ToastProvider>
|
||||
<AppProvider>
|
||||
<HotkeysProvider>
|
||||
<Component {...pageProps} />
|
||||
</HotkeysProvider>
|
||||
</AppProvider>
|
||||
</ToastProvider>
|
||||
</DictionaryProvider>
|
||||
</ThemeProvider>
|
||||
</Provider>
|
||||
</NoSsr>
|
||||
|
@ -1,4 +1,5 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { get } from 'lodash';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { refreshScreenWidth } from 'providers/ReduxStore/slices/app';
|
||||
import ConfirmAppClose from './ConfirmAppClose';
|
||||
@ -18,6 +19,13 @@ export const AppProvider = (props) => {
|
||||
dispatch(refreshScreenWidth());
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const platform = get(navigator, 'platform', '');
|
||||
if(platform && platform.toLowerCase().indexOf('mac') > -1) {
|
||||
document.body.classList.add('os-mac');
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
dispatch(refreshScreenWidth());
|
||||
|
@ -60,7 +60,7 @@ const trackStart = () => {
|
||||
event: 'start',
|
||||
properties: {
|
||||
os: platformLib.os.family,
|
||||
version: '1.22.0'
|
||||
version: '1.25.0'
|
||||
}
|
||||
});
|
||||
};
|
||||
|
28
packages/bruno-app/src/providers/Dictionary/index.js
Normal file
28
packages/bruno-app/src/providers/Dictionary/index.js
Normal file
@ -0,0 +1,28 @@
|
||||
import React from 'react';
|
||||
import { useState, useContext } from 'react';
|
||||
import { dictionaries } from 'src/dictionaries/index';
|
||||
|
||||
export const DictionaryContext = React.createContext();
|
||||
|
||||
const DictionaryProvider = (props) => {
|
||||
const [language, setLanguage] = useState('en');
|
||||
const dictionary = dictionaries[language] ?? dictionaries.en;
|
||||
|
||||
return (
|
||||
<DictionaryContext.Provider {...props} value={{ language, setLanguage, dictionary }}>
|
||||
<>{props.children}</>
|
||||
</DictionaryContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
const useDictionary = () => {
|
||||
const context = useContext(DictionaryContext);
|
||||
|
||||
if (context === undefined) {
|
||||
throw new Error(`useDictionary must be used within a DictionaryProvider`);
|
||||
}
|
||||
|
||||
return context;
|
||||
};
|
||||
|
||||
export { useDictionary, DictionaryProvider };
|
@ -154,6 +154,31 @@ export const HotkeysProvider = (props) => {
|
||||
};
|
||||
}, [activeTabUid]);
|
||||
|
||||
// close all tabs
|
||||
useEffect(() => {
|
||||
Mousetrap.bind(['command+shift+w', 'ctrl+shift+w'], (e) => {
|
||||
const activeTab = find(tabs, (t) => t.uid === activeTabUid);
|
||||
if (activeTab) {
|
||||
const collection = findCollectionByUid(collections, activeTab.collectionUid);
|
||||
|
||||
if (collection) {
|
||||
const tabUids = tabs.filter((tab) => tab.collectionUid === collection.uid).map((tab) => tab.uid);
|
||||
dispatch(
|
||||
closeTabs({
|
||||
tabUids: tabUids
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return false; // this stops the event bubbling
|
||||
});
|
||||
|
||||
return () => {
|
||||
Mousetrap.unbind(['command+shift+w', 'ctrl+shift+w']);
|
||||
};
|
||||
}, [activeTabUid, tabs, collections, dispatch]);
|
||||
|
||||
return (
|
||||
<HotkeysContext.Provider {...props} value="hotkey">
|
||||
{showSaveRequestModal && (
|
||||
|
@ -39,7 +39,7 @@ import {
|
||||
import { each } from 'lodash';
|
||||
import { closeAllCollectionTabs } from 'providers/ReduxStore/slices/tabs';
|
||||
import { resolveRequestFilename } from 'utils/common/platform';
|
||||
import { parseQueryParams, splitOnFirst } from 'utils/url/index';
|
||||
import { parsePathParams, parseQueryParams, splitOnFirst } from 'utils/url/index';
|
||||
import { sendCollectionOauth2Request as _sendCollectionOauth2Request } from 'utils/network/index';
|
||||
import { name } from 'file-loader';
|
||||
|
||||
@ -192,10 +192,7 @@ export const sendCollectionOauth2Request = (collectionUid, itemUid) => (dispatch
|
||||
|
||||
const environment = findEnvironmentInCollection(collectionCopy, collection.activeEnvironmentUid);
|
||||
|
||||
const externalSecrets = getExternalCollectionSecretsForActiveEnvironment({ collection });
|
||||
const secretVariables = getFormattedCollectionSecretVariables({ externalSecrets });
|
||||
|
||||
_sendCollectionOauth2Request(collection, environment, collectionCopy.runtimeVariables, itemUid, secretVariables)
|
||||
_sendCollectionOauth2Request(collection, environment, collectionCopy.runtimeVariables)
|
||||
.then((response) => {
|
||||
if (response?.data?.error) {
|
||||
toast.error(response?.data?.error);
|
||||
@ -284,7 +281,7 @@ export const cancelRunnerExecution = (cancelTokenUid) => (dispatch) => {
|
||||
cancelNetworkRequest(cancelTokenUid).catch((err) => console.log(err));
|
||||
};
|
||||
|
||||
export const runCollectionFolder = (collectionUid, folderUid, recursive) => (dispatch, getState) => {
|
||||
export const runCollectionFolder = (collectionUid, folderUid, recursive, delay) => (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const collection = findCollectionByUid(state.collections.collections, collectionUid);
|
||||
|
||||
@ -315,7 +312,8 @@ export const runCollectionFolder = (collectionUid, folderUid, recursive) => (dis
|
||||
collectionCopy,
|
||||
environment,
|
||||
collectionCopy.runtimeVariables,
|
||||
recursive
|
||||
recursive,
|
||||
delay
|
||||
)
|
||||
.then(resolve)
|
||||
.catch((err) => {
|
||||
@ -700,7 +698,7 @@ export const moveItemToRootOfCollection = (collectionUid, draggedItemUid) => (di
|
||||
};
|
||||
|
||||
export const newHttpRequest = (params) => (dispatch, getState) => {
|
||||
const { requestName, requestType, requestUrl, requestMethod, collectionUid, itemUid, headers, body } = params;
|
||||
const { requestName, requestType, requestUrl, requestMethod, collectionUid, itemUid, headers, body, auth } = params;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const state = getState();
|
||||
@ -710,11 +708,20 @@ export const newHttpRequest = (params) => (dispatch, getState) => {
|
||||
}
|
||||
|
||||
const parts = splitOnFirst(requestUrl, '?');
|
||||
const params = parseQueryParams(parts[1]);
|
||||
each(params, (urlParam) => {
|
||||
const queryParams = parseQueryParams(parts[1]);
|
||||
each(queryParams, (urlParam) => {
|
||||
urlParam.enabled = true;
|
||||
urlParam.type = 'query';
|
||||
});
|
||||
|
||||
const pathParams = parsePathParams(requestUrl);
|
||||
each(pathParams, (pathParm) => {
|
||||
pathParams.enabled = true;
|
||||
pathParm.type = 'path'
|
||||
});
|
||||
|
||||
const params = [...queryParams, ...pathParams];
|
||||
|
||||
const item = {
|
||||
uid: uuid(),
|
||||
type: requestType,
|
||||
@ -733,6 +740,9 @@ export const newHttpRequest = (params) => (dispatch, getState) => {
|
||||
multipartForm: null,
|
||||
formUrlEncoded: null,
|
||||
rawFile: null
|
||||
},
|
||||
auth: auth ?? {
|
||||
mode: 'none'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -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
|
||||
|
@ -20,7 +20,11 @@ const darkTheme = {
|
||||
input: {
|
||||
bg: 'rgb(65, 65, 65)',
|
||||
border: 'rgb(65, 65, 65)',
|
||||
focusBorder: 'rgb(65, 65, 65)'
|
||||
focusBorder: 'rgb(65, 65, 65)',
|
||||
placeholder: {
|
||||
color: '#a2a2a2',
|
||||
opacity: 0.75
|
||||
}
|
||||
},
|
||||
|
||||
variables: {
|
||||
@ -154,7 +158,7 @@ const darkTheme = {
|
||||
modal: {
|
||||
title: {
|
||||
color: '#ccc',
|
||||
bg: 'rgb(48, 48, 49)',
|
||||
bg: 'rgb(38, 38, 39)',
|
||||
iconColor: '#ccc'
|
||||
},
|
||||
body: {
|
||||
|
@ -20,7 +20,11 @@ const lightTheme = {
|
||||
input: {
|
||||
bg: 'white',
|
||||
border: '#ccc',
|
||||
focusBorder: '#8b8b8b'
|
||||
focusBorder: '#8b8b8b',
|
||||
placeholder: {
|
||||
color: '#a2a2a2',
|
||||
opacity: 0.8
|
||||
}
|
||||
},
|
||||
|
||||
menubar: {
|
||||
|
@ -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,
|
||||
|
@ -3,7 +3,7 @@ import filter from 'lodash/filter';
|
||||
import find from 'lodash/find';
|
||||
|
||||
export const doesRequestMatchSearchText = (request, searchText = '') => {
|
||||
return request.name.toLowerCase().includes(searchText.toLowerCase());
|
||||
return request?.name?.toLowerCase().includes(searchText.toLowerCase());
|
||||
};
|
||||
|
||||
export const doesFolderHaveItemsMatchSearchText = (item, searchText = '') => {
|
||||
|
@ -12,6 +12,64 @@ const pathFoundInVariables = (path, obj) => {
|
||||
return value !== undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* 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 || {};
|
||||
|
@ -123,7 +123,7 @@ const curlToJson = (curlCommand) => {
|
||||
request.urlWithoutQuery = 'http://' + request.urlWithoutQuery;
|
||||
}
|
||||
|
||||
requestJson.url = request.urlWithoutQuery.replace(/\/$/, '');
|
||||
requestJson.url = request.urlWithoutQuery;
|
||||
requestJson.raw_url = request.url;
|
||||
requestJson.method = request.method;
|
||||
|
||||
@ -160,14 +160,15 @@ const curlToJson = (curlCommand) => {
|
||||
}
|
||||
|
||||
if (request.auth) {
|
||||
const splitAuth = request.auth.split(':');
|
||||
const user = splitAuth[0] || '';
|
||||
const password = splitAuth[1] || '';
|
||||
|
||||
requestJson.auth = {
|
||||
user: repr(user),
|
||||
password: repr(password)
|
||||
};
|
||||
if(request.auth.mode === 'basic'){
|
||||
requestJson.auth = {
|
||||
mode: 'basic',
|
||||
basic: {
|
||||
username: repr(request.auth.basic?.username),
|
||||
password: repr(request.auth.basic?.password)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Object.keys(requestJson).length ? requestJson : {};
|
||||
|
@ -75,4 +75,15 @@ describe('curlToJson', () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should return and parse a simple curl command with a trailing slash', () => {
|
||||
const curlCommand = 'curl https://www.usebruno.com/';
|
||||
const result = curlToJson(curlCommand);
|
||||
|
||||
expect(result).toEqual({
|
||||
url: 'https://www.usebruno.com/',
|
||||
raw_url: 'https://www.usebruno.com/',
|
||||
method: 'get'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -56,7 +56,8 @@ export const getRequestFromCurlCommand = (curlCommand) => {
|
||||
url: request.url,
|
||||
method: request.method,
|
||||
body,
|
||||
headers: headers
|
||||
headers: headers,
|
||||
auth: request.auth
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
@ -36,7 +36,8 @@ const parseCurlCommand = (curlCommand) => {
|
||||
boolean: ['I', 'head', 'compressed', 'L', 'k', 'silent', 's', 'G', 'get'],
|
||||
alias: {
|
||||
H: 'header',
|
||||
A: 'user-agent'
|
||||
A: 'user-agent',
|
||||
u: 'user'
|
||||
}
|
||||
});
|
||||
|
||||
@ -72,11 +73,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();
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -188,10 +188,21 @@ const parseCurlCommand = (curlCommand) => {
|
||||
}
|
||||
|
||||
urlObject.search = null; // Clean out the search/query portion.
|
||||
|
||||
let urlWithoutQuery = URL.format(urlObject);
|
||||
let urlHost = urlObject?.host;
|
||||
if (!url?.includes(`${urlHost}/`)) {
|
||||
if (urlWithoutQuery && urlHost) {
|
||||
const [beforeHost, afterHost] = urlWithoutQuery.split(urlHost);
|
||||
urlWithoutQuery = beforeHost + urlHost + afterHost?.slice(1);
|
||||
}
|
||||
}
|
||||
|
||||
const request = {
|
||||
url: url,
|
||||
urlWithoutQuery: URL.format(urlObject)
|
||||
url,
|
||||
urlWithoutQuery
|
||||
};
|
||||
|
||||
if (compressed) {
|
||||
request.compressed = true;
|
||||
}
|
||||
@ -227,12 +238,19 @@ const parseCurlCommand = (curlCommand) => {
|
||||
request.data = parsedArguments['data-urlencode'];
|
||||
}
|
||||
|
||||
if (parsedArguments.u) {
|
||||
request.auth = parsedArguments.u;
|
||||
}
|
||||
if (parsedArguments.user) {
|
||||
request.auth = parsedArguments.user;
|
||||
if (parsedArguments.user && typeof parsedArguments.user === 'string') {
|
||||
const basicAuth = parsedArguments.user.split(':')
|
||||
const username = basicAuth[0] || ''
|
||||
const password = basicAuth[1] || ''
|
||||
request.auth = {
|
||||
mode: 'basic',
|
||||
basic: {
|
||||
username,
|
||||
password
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(request.data)) {
|
||||
request.dataArray = request.data;
|
||||
request.data = request.data.join('&');
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "v1.22.0",
|
||||
"version": "v1.25.0",
|
||||
"name": "bruno",
|
||||
"description": "Opensource API Client for Exploring and Testing APIs",
|
||||
"homepage": "https://www.usebruno.com",
|
||||
|
@ -428,17 +428,16 @@ class Watcher {
|
||||
this.watchers = {};
|
||||
}
|
||||
|
||||
addWatcher(win, watchPath, collectionUid, brunoConfig) {
|
||||
addWatcher(win, watchPath, collectionUid, brunoConfig, forcePolling = false) {
|
||||
if (this.watchers[watchPath]) {
|
||||
this.watchers[watchPath].close();
|
||||
}
|
||||
|
||||
const ignores = brunoConfig?.ignore || [];
|
||||
const self = this;
|
||||
setTimeout(() => {
|
||||
const watcher = chokidar.watch(watchPath, {
|
||||
ignoreInitial: false,
|
||||
usePolling: watchPath.startsWith('\\\\') ? true : false,
|
||||
usePolling: watchPath.startsWith('\\\\') || forcePolling ? true : false,
|
||||
ignored: (filepath) => {
|
||||
const normalizedPath = filepath.replace(/\\/g, '/');
|
||||
const relativePath = path.relative(watchPath, normalizedPath);
|
||||
@ -457,14 +456,36 @@ class Watcher {
|
||||
depth: 20
|
||||
});
|
||||
|
||||
let startedNewWatcher = false;
|
||||
watcher
|
||||
.on('add', (pathname) => add(win, pathname, collectionUid, watchPath))
|
||||
.on('addDir', (pathname) => addDirectory(win, pathname, collectionUid, watchPath))
|
||||
.on('change', (pathname) => change(win, pathname, collectionUid, watchPath))
|
||||
.on('unlink', (pathname) => unlink(win, pathname, collectionUid, watchPath))
|
||||
.on('unlinkDir', (pathname) => unlinkDir(win, pathname, collectionUid, watchPath));
|
||||
.on('unlinkDir', (pathname) => unlinkDir(win, pathname, collectionUid, watchPath))
|
||||
.on('error', (error) => {
|
||||
// `EMFILE` is an error code thrown when to many files are watched at the same time see: https://github.com/usebruno/bruno/issues/627
|
||||
// `ENOSPC` stands for "Error No space" but is also thrown if the file watcher limit is reached.
|
||||
// To prevent loops `!forcePolling` is checked.
|
||||
if ((error.code === 'ENOSPC' || error.code === 'EMFILE') && !startedNewWatcher && !forcePolling) {
|
||||
// This callback is called for every file the watcher is trying to watch. To prevent a spam of messages and
|
||||
// Multiple watcher being started `startedNewWatcher` is set to prevent this.
|
||||
startedNewWatcher = true;
|
||||
watcher.close();
|
||||
console.error(
|
||||
`\nCould not start watcher for ${watchPath}:`,
|
||||
'ENOSPC: System limit for number of file watchers reached!',
|
||||
'Trying again with polling, this will be slower!\n',
|
||||
'Update you system config to allow more concurrently watched files with:',
|
||||
'"echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p"'
|
||||
);
|
||||
this.addWatcher(win, watchPath, collectionUid, brunoConfig, true);
|
||||
} else {
|
||||
console.error(`An error occurred in the watcher for: ${watchPath}`, error);
|
||||
}
|
||||
});
|
||||
|
||||
self.watchers[watchPath] = watcher;
|
||||
this.watchers[watchPath] = watcher;
|
||||
}, 100);
|
||||
}
|
||||
|
||||
|
@ -50,6 +50,7 @@ app.on('ready', async () => {
|
||||
height,
|
||||
minWidth: 1000,
|
||||
minHeight: 640,
|
||||
show: false,
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
contextIsolation: true,
|
||||
@ -67,6 +68,9 @@ app.on('ready', async () => {
|
||||
mainWindow.maximize();
|
||||
}
|
||||
|
||||
mainWindow.once('ready-to-show', () => {
|
||||
mainWindow.show();
|
||||
})
|
||||
const url = isDev
|
||||
? 'http://localhost:3000'
|
||||
: format({
|
||||
|
@ -408,7 +408,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
}
|
||||
|
||||
// run post-response script
|
||||
const responseScript = compact(scriptingConfig.flow === 'natural' ? [
|
||||
const responseScript = compact(scriptingConfig.flow === 'sequential' ? [
|
||||
get(collectionRoot, 'request.script.res'), get(request, 'script.res')
|
||||
] : [
|
||||
get(request, 'script.res'), get(collectionRoot, 'request.script.res')
|
||||
@ -596,7 +596,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
|
||||
// run tests
|
||||
const testScript = item.draft ? get(item.draft, 'request.tests') : get(item, 'request.tests');
|
||||
const testFile = compact(scriptingConfig.flow === 'natural' ? [
|
||||
const testFile = compact(scriptingConfig.flow === 'sequential' ? [
|
||||
get(collectionRoot, 'request.tests'), testScript,
|
||||
] : [
|
||||
testScript, get(collectionRoot, 'request.tests')
|
||||
@ -825,7 +825,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
|
||||
ipcMain.handle(
|
||||
'renderer:run-collection-folder',
|
||||
async (event, folder, collection, environment, runtimeVariables, recursive) => {
|
||||
async (event, folder, collection, environment, runtimeVariables, recursive, delay) => {
|
||||
const collectionUid = collection.uid;
|
||||
const collectionPath = collection.pathname;
|
||||
const folderUid = folder ? folder.uid : null;
|
||||
@ -944,6 +944,18 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
timeStart = Date.now();
|
||||
let response, responseTime;
|
||||
try {
|
||||
if (delay && !Number.isNaN(delay) && delay > 0) {
|
||||
const delayPromise = new Promise((resolve) => setTimeout(resolve, delay));
|
||||
|
||||
const cancellationPromise = new Promise((_, reject) => {
|
||||
abortController.signal.addEventListener('abort', () => {
|
||||
reject(new Error('Cancelled'));
|
||||
});
|
||||
});
|
||||
|
||||
await Promise.race([delayPromise, cancellationPromise]);
|
||||
}
|
||||
|
||||
/** @type {import('axios').AxiosResponse} */
|
||||
response = await axiosInstance(request);
|
||||
timeEnd = Date.now();
|
||||
@ -1036,7 +1048,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
|
||||
// run tests
|
||||
const testScript = item.draft ? get(item.draft, 'request.tests') : get(item, 'request.tests');
|
||||
const testFile = compact(scriptingConfig.flow === 'natural' ? [
|
||||
const testFile = compact(scriptingConfig.flow === 'sequential' ? [
|
||||
get(collectionRoot, 'request.tests'), testScript
|
||||
] : [
|
||||
testScript, get(collectionRoot, 'request.tests')
|
||||
@ -1153,12 +1165,22 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
return `response.${extension}`;
|
||||
};
|
||||
|
||||
const fileName =
|
||||
getFileNameFromContentDispositionHeader() || getFileNameFromUrlPath() || getFileNameBasedOnContentTypeHeader();
|
||||
const getEncodingFormat = () => {
|
||||
const contentType = getHeaderValue('content-type');
|
||||
const extension = mime.extension(contentType) || 'txt';
|
||||
return ['json', 'xml', 'html', 'yml', 'yaml', 'txt'].includes(extension) ? 'utf-8' : 'base64';
|
||||
};
|
||||
|
||||
const determineFileName = () => {
|
||||
return (
|
||||
getFileNameFromContentDispositionHeader() || getFileNameFromUrlPath() || getFileNameBasedOnContentTypeHeader()
|
||||
);
|
||||
};
|
||||
|
||||
const fileName = determineFileName();
|
||||
const filePath = await chooseFileToSave(mainWindow, fileName);
|
||||
if (filePath) {
|
||||
await writeBinaryFile(filePath, Buffer.from(response.dataBuffer, 'base64'));
|
||||
await writeBinaryFile(filePath, Buffer.from(response.dataBuffer, getEncodingFormat()));
|
||||
}
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
|
@ -32,9 +32,6 @@ const resolveOAuth2AuthorizationCodeAccessToken = async (request, collectionUid)
|
||||
client_secret: clientSecret,
|
||||
state: state
|
||||
};
|
||||
if (scope) {
|
||||
data['scope'] = scope;
|
||||
}
|
||||
if (pkce) {
|
||||
data['code_verifier'] = codeVerifier;
|
||||
}
|
||||
|
@ -18,8 +18,8 @@ const mergeFolderLevelHeaders = (request, requestTreePath) => {
|
||||
folderHeaders.set(header.name, header.value);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
let headers = get(i, 'request.headers', []);
|
||||
} else if (i.uid === request.uid) {
|
||||
const headers = i?.draft ? get(i, 'draft.request.headers', []) : get(i, 'request.headers', []);
|
||||
headers.forEach((header) => {
|
||||
if (header.enabled) {
|
||||
folderHeaders.set(header.name, header.value);
|
||||
@ -55,8 +55,8 @@ const mergeFolderLevelVars = (request, requestTreePath) => {
|
||||
folderReqVars.set(_var.name, _var.value);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
let vars = get(i, 'request.vars.req', []);
|
||||
} else if (i.uid === request.uid) {
|
||||
const vars = i?.draft ? get(i, 'draft.request.vars.req', []) : get(i, 'request.vars.req', []);
|
||||
vars.forEach((_var) => {
|
||||
if (_var.enabled) {
|
||||
folderReqVars.set(_var.name, _var.value);
|
||||
@ -91,8 +91,8 @@ const mergeFolderLevelVars = (request, requestTreePath) => {
|
||||
folderResVars.set(_var.name, _var.value);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
let vars = get(i, 'request.vars.res', []);
|
||||
} else if (i.uid === request.uid) {
|
||||
const vars = i?.draft ? get(i, 'draft.request.vars.res', []) : get(i, 'request.vars.res', []);
|
||||
vars.forEach((_var) => {
|
||||
if (_var.enabled) {
|
||||
folderResVars.set(_var.name, _var.value);
|
||||
@ -147,7 +147,7 @@ const mergeFolderLevelScripts = (request, requestTreePath, scriptFlow) => {
|
||||
}
|
||||
|
||||
if (folderCombinedPostResScript.length) {
|
||||
if (scriptFlow === 'natural') {
|
||||
if (scriptFlow === 'sequential') {
|
||||
request.script.res = compact([...folderCombinedPostResScript, request?.script?.res || '']).join(os.EOL);
|
||||
} else {
|
||||
request.script.res = compact([request?.script?.res || '', ...folderCombinedPostResScript.reverse()]).join(os.EOL);
|
||||
@ -155,7 +155,7 @@ const mergeFolderLevelScripts = (request, requestTreePath, scriptFlow) => {
|
||||
}
|
||||
|
||||
if (folderCombinedTests.length) {
|
||||
if (scriptFlow === 'natural') {
|
||||
if (scriptFlow === 'sequential') {
|
||||
request.tests = compact([...folderCombinedTests, request?.tests || '']).join(os.EOL);
|
||||
} else {
|
||||
request.tests = compact([request?.tests || '', ...folderCombinedTests.reverse()]).join(os.EOL);
|
||||
@ -309,7 +309,7 @@ const prepareRequest = (item, collection) => {
|
||||
}
|
||||
});
|
||||
|
||||
// scriptFlow is either "sandwich" or "natural"
|
||||
// scriptFlow is either "sandwich" or "sequential"
|
||||
const scriptFlow = collection.brunoConfig?.scripts?.flow ?? 'sandwich';
|
||||
const requestTreePath = getTreePathFromCollectionToItem(collection, item);
|
||||
if (requestTreePath && requestTreePath.length > 0) {
|
||||
|
@ -13,7 +13,7 @@ const stripLastLine = (text) => {
|
||||
};
|
||||
|
||||
const getValueString = (value) => {
|
||||
const hasNewLines = value.includes('\n');
|
||||
const hasNewLines = value?.includes('\n');
|
||||
|
||||
if (!hasNewLines) {
|
||||
return value;
|
||||
@ -269,7 +269,6 @@ ${indentString(body.rawFile)}
|
||||
multipartForms
|
||||
.map((item) => {
|
||||
const enabled = item.enabled ? '' : '~';
|
||||
|
||||
if (item.type === 'text') {
|
||||
return `${enabled}${item.name}: ${getValueString(item.value)}`;
|
||||
}
|
||||
|
@ -48,7 +48,7 @@ const varsSchema = Yup.object({
|
||||
|
||||
const requestUrlSchema = Yup.string().min(0).defined();
|
||||
const requestMethodSchema = Yup.string()
|
||||
.oneOf(['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'])
|
||||
.oneOf(['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS', 'TRACE'])
|
||||
.required('method is required');
|
||||
|
||||
const graphqlBodySchema = Yup.object({
|
||||
|
@ -32,7 +32,7 @@ describe('Request Schema Validation', () => {
|
||||
return Promise.all([
|
||||
expect(requestSchema.validate(request)).rejects.toEqual(
|
||||
validationErrorWithMessages(
|
||||
'method must be one of the following values: GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS'
|
||||
'method must be one of the following values: GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS, TRACE'
|
||||
)
|
||||
)
|
||||
]);
|
||||
|
Loading…
Reference in New Issue
Block a user