Compare commits

..

6 Commits

Author SHA1 Message Date
Anoop M D
758ef9fc96 feat: async parser workers (#3834) 2025-01-29 02:53:10 +05:30
Anoop M D
ced9d38abc feat: async parser workers (#3834) 2025-01-29 02:44:32 +05:30
lohit
98f3a524dc feat: updated the bru async parsing logic (#3898) 2025-01-28 21:26:46 +05:30
Anoop M D
a06a339d0c feat: async parser workers (#3834) 2025-01-27 23:44:17 +05:30
Anoop M D
e34ac3de7c feat: async parser workers (#3834) 2025-01-27 15:33:05 +05:30
lohit
074c6be5f4 feat: async parser workers (#3834)
* feat: async parser workers (#3834)
---------
Co-authored-by: Anoop M D <anoop.md1421@gmail.com>
2025-01-27 12:26:32 +05:30
233 changed files with 1547 additions and 10226 deletions

View File

@@ -6,58 +6,26 @@ body:
attributes:
value: |
Thanks for taking the time to fill out this bug report!
Before submitting, please make sure you've searched existing issues:
👉 [Search existing issues](https://github.com/usebruno/bruno/issues?q=is%3Aissue)
- type: checkboxes
attributes:
label: 'I have checked the following:'
options:
- label: "I have searched existing issues and found nothing related to my issue."
- label: I use the newest version of bruno.
required: true
- label: I've searched existing issues and found nothing related to my issue.
required: true
- type: checkboxes
attributes:
label: 'This bug is:'
options:
- label: making Bruno unusable for me
required: false
- label: slowing me down but I'm able to continue working
required: false
- label: annoying
required: false
- type: input
attributes:
label: Bruno version
description: Please specify the version of Bruno you are using in which the issue occurs.
placeholder: 1.38.1
validations:
required: true
- type: input
attributes:
label: Operating System
description: Information about the operating system the issue occurs on.
placeholder: Windows 11 26100.3037 / macOS 15.1 (24B83) / Linux 6.13.1
validations:
required: true
- type: textarea
attributes:
label: Describe the bug
description: A clear and concise description of the bug and how it's effecting your work along with steps to reproduce.
description: A clear and concise description of the bug.
validations:
required: true
- type: textarea
attributes:
label: .bru file to reproduce the bug
description: Attach your .bru file here that can reproduce the problem.
description: Attach your .bru file here that can reqroduce the problem.
validations:
required: false
- type: textarea
attributes:
label: Screenshots/Live demo link

View File

@@ -8,23 +8,13 @@ body:
options:
- label: I've searched existing issues and found nothing related to my issue.
required: true
- type: checkboxes
attributes:
label: 'This feature'
options:
- label: blocks me from using Bruno
required: false
- label: would improve my quality of life in Bruno
required: false
- label: is something I've never seen an API client do before
required: false
- type: markdown
attributes:
value: |
Suggest an idea for this project.
- type: textarea
attributes:
label: Describe the feature you want to add, and how it would change your usage of Bruno
label: Describe the feature you want to add
description: A clear and concise description of the feature you want to be added.
validations:
required: true
@@ -33,4 +23,4 @@ body:
label: Mockups or Images of the feature
description: Add some images to support your feature.
validations:
required: false
required: true

View File

@@ -1,31 +0,0 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: weekly
- package-ecosystem: npm
directory: "/"
schedule:
interval: weekly
groups:
bruno-dependencies:
patterns:
- "*usebruno*"
babel-dependencies:
patterns:
- "*babel*"
fortawesome-dependencies:
patterns:
- "*fortawesome*"
electron-dependencies:
patterns:
- "*electron*"
rollup-dependencies:
patterns:
- "*rollup*"
jest-dependencies:
patterns:
- "*jest*"

View File

@@ -26,7 +26,7 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- uses: actions/setup-node@v3
with:
node-version-file: '.nvmrc'
@@ -43,7 +43,7 @@ jobs:
bru run --env Prod --output junit.xml --format junit
- name: Publish Test Report
uses: dorny/test-reporter@v2
uses: dorny/test-reporter@v1
if: success() || failure()
with:
name: Test Report

View File

@@ -5,13 +5,14 @@ on:
pull_request:
branches: [main]
permissions:
contents: read
jobs:
unit-test:
name: Unit Tests
timeout-minutes: 60
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
@@ -51,10 +52,6 @@ jobs:
cli-test:
name: CLI Tests
runs-on: ubuntu-latest
permissions:
checks: write
pull-requests: write
contents: read
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4

View File

@@ -86,11 +86,11 @@ find . -type f -name "package-lock.json" -delete
### Testing
```bash
# run bruno-schema tests
# bruno-schema
npm test --workspace=packages/bruno-schema
# run tests over all workspaces
npm test --workspaces --if-present
# bruno-lang
npm test --workspace=packages/bruno-lang
```
### Raising Pull Requests

View File

@@ -70,11 +70,11 @@ find . -type f -name "package-lock.json" -delete
### Testing (পরীক্ষা)
```bash
# ব্রুনো-স্কিমা পরীক্ষা চালান
# bruno-schema
npm test --workspace=packages/bruno-schema
# সমস্ত কর্মক্ষেত্রে পরীক্ষা চালান
npm test --workspaces --if-present
# bruno-lang
npm test --workspace=packages/bruno-lang
```
### Raising Pull Request (পুল অনুরোধ উত্থাপন)

View File

@@ -70,11 +70,11 @@ find . -type f -name "package-lock.json" -delete
### 测试
```bash
# 运行 bruno-schema 测试
# bruno-schema
npm test --workspace=packages/bruno-schema
# 在所有工作区上运行测试
npm test --workspaces --if-present
# bruno-lang
npm test --workspace=packages/bruno-lang
```
### 提交 Pull Request

View File

@@ -83,9 +83,9 @@ find . -type f -name "package-lock.json" -delete
### Testen
```bash
# Führen Sie Bruno-Schema-Tests aus
# bruno-schema
npm test --workspace=packages/bruno-schema
# Führen Sie Tests für alle Arbeitsbereiche durch
npm test --workspaces --if-present
# bruno-lang
npm test --workspace=packages/bruno-lang
```

View File

@@ -70,11 +70,11 @@ find . -type f -name "package-lock.json" -delete
### Pruebas
```bash
# ejecutar pruebas de esquema bruno
# bruno-schema
npm test --workspace=packages/bruno-schema
# ejecutar pruebas en todos los espacios de trabajo
npm test --workspaces --if-present
# bruno-lang
npm test --workspace=packages/bruno-lang
```
### Crea un Pull Request

View File

@@ -73,11 +73,11 @@ find . -type f -name "package-lock.json" -delete
### Tests
```bash
# exécuter des tests de schéma bruno
# bruno-schema
npm test --workspace=packages/bruno-schema
# exécuter des tests sur tous les espaces de travail
npm test --workspaces --if-present
# bruno-lang
npm test --workspace=packages/bruno-lang
```
### Ouvrir une Pull Request

View File

@@ -65,11 +65,11 @@ find . -type f -name "package-lock.json" -delete
### परिक्षण
```bash
# ब्रूनो-स्कीमा परीक्षण चलाएँ
# bruno-schema
npm test --workspace=packages/bruno-schema
# सभी कार्यस्थानों पर परीक्षण चलाएँ
npm test --workspaces --if-present
# bruno-lang
npm test --workspace=packages/bruno-lang
```
### पुल अनुरोध प्रक्रिया

View File

@@ -83,9 +83,9 @@ find . -type f -name "package-lock.json" -delete
### Tests
```bash
# esegui i test dello schema bruno
# bruno-schema
npm test --workspace=packages/bruno-schema
# esegui test su tutti gli spazi di lavoro
npm test --workspaces --if-present
# bruno-lang
npm test --workspace=packages/bruno-lang
```

View File

@@ -65,11 +65,11 @@ find . -type f -name "package-lock.json" -delete
### テストを動かすには
```bash
# ブルーノスキーマのテストを実行します
# bruno-schema
npm test --workspace=packages/bruno-schema
# すべてのワークスペースでテストを実行します
npm test --workspaces --if-present
# bruno-lang
npm test --workspace=packages/bruno-lang
```
### プルリクエストの手順

View File

@@ -66,11 +66,11 @@ find . -type f -name "package-lock.json" -delete
### 테스팅
```bash
# bruno-schema 테스트 실행
# bruno-schema
npm test --workspace=packages/bruno-schema
# 모든 작업 공간에서 테스트 실행
npm test --workspaces --if-present
# bruno-lang
npm test --workspace=packages/bruno-lang
```
### Pull Requests 요청

View File

@@ -65,11 +65,11 @@ find . -type f -name "package-lock.json" -delete
### Testen
```bash
# voer bruno-schema tests uit
# bruno-schema
npm test --workspace=packages/bruno-schema
# voer tests uit over alle werkruimten
npm test --workspaces --if-present
# bruno-lang
npm test --workspace=packages/bruno-lang
```
### Pull Requests indienen

View File

@@ -71,11 +71,11 @@ find . -type f -name "package-lock.json" -delete
### Testowanie
```bash
# uruchom testy bruno-schema
# bruno-schema
npm test --workspace=packages/bruno-schema
# uruchom testy we wszystkich przestrzeniach roboczych
npm test --workspaces --if-present
# bruno-lang
npm test --workspace=packages/bruno-lang
```
### Tworzenie Pull Request

View File

@@ -70,11 +70,11 @@ find . -type f -name "package-lock.json" -delete
### Testando
```bash
# executar testes do bruno-schema
# bruno-schema
npm test --workspace=packages/bruno-schema
# executar testes em todos os ambientes de trabalho
npm test --workspaces --if-present
# bruno-lang
npm test --workspace=packages/bruno-lang
```
### Envio de Pull Request

View File

@@ -64,11 +64,11 @@ find . -type f -name "package-lock.json" -delete
### Testarea
```shell
# executați teste bruno-schema
# bruno-schema
npm test --workspace=packages/bruno-schema
# executați teste peste toate spațiile de lucru
npm test --workspaces --if-present
# bruno-lang
npm test --workspace=packages/bruno-lang
```
### Crearea unui Pull Request

View File

@@ -83,9 +83,9 @@ find . -type f -name "package-lock.json" -delete
### Тестирование
```bash
# запустите тесты bruno-schema
# bruno-schema
npm test --workspace=packages/bruno-schema
# запустите тесты во всех рабочих пространствах
npm test --workspaces --if-present
# bruno-lang
npm test --workspace=packages/bruno-lang
```

View File

@@ -67,11 +67,11 @@ find . -type f -name "package-lock.json" -delete
### Testovanie
````bash
# spustiť bruno-schema testy
# bruno-schema
npm test --workspace=packages/bruno-schema
# spustiť testy vo všetkých pracovných priestoroch
npm test --workspaces --if-present
# bruno-lang
npm test --workspace=packages/bruno-lang
```
### Vyrobenie Pull Request

View File

@@ -70,11 +70,11 @@ find . -type f -name "package-lock.json" -delete
### Test
```bash
# bruno-schema testlerini çalıştır
# bruno-schema
npm test --workspace=packages/bruno-schema
# tüm çalışma alanlarında testleri çalıştır
npm test --workspaces --if-present
# bruno-lang
npm test --workspace=packages/bruno-lang
```
### Pull Request Oluşturma

View File

@@ -83,9 +83,9 @@ find . -type f -name "package-lock.json" -delete
### Тестування
```bash
# запустити тести bruno-schema
# bruno-schema
npm test --workspace=packages/bruno-schema
# запустити тести у всіх робочих просторах
npm test --workspaces --if-present
# bruno-lang
npm test --workspace=packages/bruno-lang
```

View File

@@ -70,11 +70,11 @@ find . -type f -name "package-lock.json" -delete
### 測試
```bash
# 執行布魯諾架構測試
# bruno-schema
npm test --workspace=packages/bruno-schema
# 對所有工作區執行測試
npm test --workspaces --if-present
# bruno-lang
npm test --workspace=packages/bruno-lang
```
### 發送 Pull Request

View File

@@ -29,13 +29,13 @@
| [日本語](./readme_ja.md)
| [ქართული](./readme_ka.md)
Bruno це новий та іноваційний API клієнт, націлений на революційну зміну статусy кво, запровадженого інструментами на кшталт Postman.
Bruno це новий та іноваційний API клієнт, націлений на революційну зміну статус кво, запровадженого інструментами на кшталт Postman.
Bruno зберігає ваші колекції напряму у теці на вашому диску. Він використовує текстову мову розмітки Bru для збереження інформації про ваші API запити.
Ви можете використовувати git або будь-яку іншу систему контролю версій щоб спільно працювати над вашими колекціями API запитів.
Bruno є повністю автономним. Немає жодних планів додавати будь-які синхронізації через хмару, ніколи. Ми цінуємо приватність ваших даних, і вважаєм, що вони мають залишитись лише на вашому комп'ютері. Дізнатись більше про наше бачення у довготривалій перспективі можна [тут](https://github.com/usebruno/bruno/discussions/269)
Bruno є повністю автономним. Немає жодних планів додавати будь-які синхронізації через хмару, ніколи. Ми цінуємо приватність ваших даних, і вважаєм, що вони мають залишитись лише на вашому комп'ютері. Взнати більше про наше бачення у довготривалій перспективі можна [тут](https://github.com/usebruno/bruno/discussions/269)
![bruno](/assets/images/landing-2.png) <br /><br />
@@ -69,13 +69,13 @@ Bruno є повністю автономним. Немає жодних план
### Поділитись відгуками 📣
Якщо Bruno допоміг у роботі вам або вашій команді, будь ласка не забудьте поділитись вашими [відгуками у github дискусії](https://github.com/usebruno/bruno/discussions/343)
Якщо Bruno допоміг вам у вашій роботі і вашим командам, будь ласка не забудьте поділитись вашими [відгуками у github дискусії](https://github.com/usebruno/bruno/discussions/343)
### Зробити свій внесок 👩‍💻🧑‍💻
Я радий що ви бажаєте покращити Bruno. Будь ласка переглянте [інструкцію по контрибуції](../contributing/contributing_ua.md)
Навіть якщо ви не можете зробити свій внесок пишучи код, будь ласка не соромтесь рапортувати про помилки і писати запити на новий функціонал, який потрібен вам у вашій роботі.
Навіть якщо ви не можете зробити свій внесок пишучи програмний код, будь ласка не соромтесь рапортувати про помилки і писати запити на новий функціонал, який потрібен вам у вашій роботі.
### Автори

3659
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -27,7 +27,7 @@
"pretty-quick": "^3.1.3",
"randomstring": "^1.2.2",
"rimraf": "^6.0.1",
"ts-jest": "^29.2.6"
"ts-jest": "^29.0.5"
},
"scripts": {
"setup": "node ./scripts/setup.js",
@@ -36,7 +36,6 @@
"build:web": "npm run build --workspace=packages/bruno-app",
"prettier:web": "npm run prettier --workspace=packages/bruno-app",
"dev:electron": "npm run dev --workspace=packages/bruno-electron",
"dev:electron:debug": "npm run debug --workspace=packages/bruno-electron",
"build:bruno-common": "npm run build --workspace=packages/bruno-common",
"build:bruno-query": "npm run build --workspace=packages/bruno-query",
"build:graphql-docs": "npm run build --workspace=packages/bruno-graphql-docs",

View File

@@ -1,6 +1,6 @@
{
"name": "@usebruno/app",
"version": "1.39.0",
"version": "0.3.0",
"private": true,
"scripts": {
"dev": "rsbuild dev",
@@ -20,11 +20,11 @@
"@usebruno/common": "0.1.0",
"@usebruno/graphql-docs": "0.1.0",
"@usebruno/schema": "0.7.0",
"axios": "1.7.5",
"classnames": "^2.3.1",
"codemirror": "5.65.2",
"codemirror-graphql": "2.1.1",
"cookie": "0.7.1",
"dompurify": "^3.2.4",
"escape-html": "^1.0.3",
"file": "^0.2.2",
"file-dialog": "^0.0.8",
@@ -34,22 +34,19 @@
"graphiql": "3.7.1",
"graphql": "^16.6.0",
"graphql-request": "^3.7.0",
"httpsnippet": "^3.0.9",
"httpsnippet": "^3.0.6",
"i18next": "24.1.2",
"iconv-lite": "^0.6.3",
"idb": "^7.0.0",
"immer": "^9.0.15",
"jsesc": "^3.0.2",
"jshint": "^2.13.6",
"json5": "^2.2.3",
"jsonc-parser": "^3.2.1",
"jsonpath-plus": "^10.3.0",
"jsonpath-plus": "10.2.0",
"know-your-http-well": "^0.5.0",
"lodash": "^4.17.21",
"markdown-it": "^13.0.2",
"markdown-it-replace-link": "^1.2.0",
"moment": "^2.30.1",
"moment-timezone": "^0.5.47",
"mousetrap": "^1.6.5",
"nanoid": "3.3.8",
"path": "^0.12.7",
@@ -72,7 +69,6 @@
"react-redux": "^7.2.9",
"react-tooltip": "^5.5.2",
"sass": "^1.46.0",
"semver": "^7.7.1",
"strip-json-comments": "^5.0.1",
"styled-components": "^5.3.3",
"system": "^2.0.1",

View File

@@ -1,62 +0,0 @@
import React, { createContext, useContext, useState } from 'react';
import { IconChevronDown } from '@tabler/icons';
import { AccordionItem, AccordionHeader, AccordionContent } from './styledWrapper';
const AccordionContext = createContext();
const Accordion = ({ children, defaultIndex }) => {
const [openIndex, setOpenIndex] = useState(defaultIndex);
const toggleItem = (index) => {
setOpenIndex(openIndex === index ? null : index);
};
return (
<AccordionContext.Provider value={{ openIndex, toggleItem }}>
<div>{children}</div>
</AccordionContext.Provider>
);
};
const Item = ({ index, children, ...props }) => {
return (
<AccordionItem {...props}>
{React.Children.map(children, (child) => React.cloneElement(child, { index }))}
</AccordionItem>
);
};
export const Header = ({ index, children, ...props }) => {
const { openIndex, toggleItem } = useContext(AccordionContext);
const isOpen = openIndex === index;
return (
<AccordionHeader onClick={() => toggleItem(index)} {...props} className={isOpen ? 'open' : ''}>
<div className="w-full">{children}</div>
<IconChevronDown
className="w-5 h-5 ml-auto"
style={{
transform: `rotate(${isOpen ? '180deg' : '0deg'})`,
transition: 'transform 0.3s ease-in-out'
}}
/>
</AccordionHeader>
);
};
const Content = ({ index, children, ...props }) => {
const { openIndex } = useContext(AccordionContext);
const isOpen = openIndex === index;
return (
<AccordionContent isOpen={isOpen} {...props}>
{children}
</AccordionContent>
);
};
Accordion.Item = Item;
Accordion.Header = Header;
Accordion.Content = Content;
export default Accordion;

View File

@@ -1,28 +0,0 @@
import styled from 'styled-components';
const AccordionItem = styled.div`
border: 1px solid ${(props) => props.theme.input.border};
border-radius: 4px;
overflow: hidden;
margin-bottom: 1rem;
`;
const AccordionHeader = styled.button`
width: 100%;
display: flex;
padding: 0.75rem 1rem;
background: transparent;
cursor: pointer;
font-weight: 500;
&.open, &:hover {
background-color: ${(props) => props.theme.plainGrid.hoverBg};
}
`;
const AccordionContent = styled.div`
padding: ${(props) => (props.isOpen ? '1rem' : '0')};
max-height: ${(props) => (props.isOpen ? 'auto' : '0')};
`;
export { AccordionItem, AccordionHeader, AccordionContent };

View File

@@ -8,8 +8,6 @@ const StyledWrapper = styled.div`
font-size: ${(props) => (props.fontSize ? `${props.fontSize}px` : 'inherit')};
line-break: anywhere;
flex: 1 1 0;
display: flex;
flex-direction: column-reverse;
}
/* Removes the glow outline around the folded json */
@@ -28,10 +26,6 @@ const StyledWrapper = styled.div`
.CodeMirror-dialog {
overflow: visible;
position: relative;
top: unset;
left: unset;
input {
background: transparent;
border: 1px solid #d3d6db;

View File

@@ -31,7 +31,6 @@ if (!SERVER_RENDERED) {
'res.body',
'res.responseTime',
'res.getStatus()',
'res.getStatusText()',
'res.getHeader(name)',
'res.getHeaders()',
'res.getBody()',
@@ -75,9 +74,6 @@ if (!SERVER_RENDERED) {
'bru.setNextRequest(requestName)',
'req.disableParsingResponseJson()',
'bru.getRequestVar(key)',
'bru.runRequest(requestPathName)',
'bru.getAssertionResults()',
'bru.getTestResults()',
'bru.sleep(ms)',
'bru.getGlobalEnvVar(key)',
'bru.setGlobalEnvVar(key, value)',
@@ -175,21 +171,11 @@ export default class CodeEditor extends React.Component {
}
},
'Cmd-F': (cm) => {
if (this._isSearchOpen()) {
// replace the older search component with the new one
const search = document.querySelector('.CodeMirror-dialog.CodeMirror-dialog-top');
search && search.remove();
}
cm.execCommand('findPersistent');
this._bindSearchHandler();
this._appendSearchResultsCount();
},
'Ctrl-F': (cm) => {
if (this._isSearchOpen()) {
// replace the older search component with the new one
const search = document.querySelector('.CodeMirror-dialog.CodeMirror-dialog-top');
search && search.remove();
}
cm.execCommand('findPersistent');
this._bindSearchHandler();
this._appendSearchResultsCount();
@@ -208,20 +194,8 @@ export default class CodeEditor extends React.Component {
'Cmd-Y': 'foldAll',
'Ctrl-I': 'unfoldAll',
'Cmd-I': 'unfoldAll',
'Ctrl-/': () => {
if (['application/ld+json', 'application/json'].includes(this.props.mode)) {
this.editor.toggleComment({ lineComment: '//', blockComment: '/*' });
} else {
this.editor.toggleComment();
}
},
'Cmd-/': () => {
if (['application/ld+json', 'application/json'].includes(this.props.mode)) {
this.editor.toggleComment({ lineComment: '//', blockComment: '/*' });
} else {
this.editor.toggleComment();
}
}
'Ctrl-/': 'toggleComment',
'Cmd-/': 'toggleComment'
},
foldOptions: {
widget: (from, to) => {
@@ -376,10 +350,6 @@ export default class CodeEditor extends React.Component {
}
};
_isSearchOpen = () => {
return document.querySelector('.CodeMirror-dialog.CodeMirror-dialog-top');
};
/**
* Bind handler to search input to count number of search results
*/

View File

@@ -8,7 +8,9 @@ import { useState } from 'react';
import StyledWrapper from './StyledWrapper';
import { useRef } from 'react';
import path from 'utils/common/path';
import path from 'path';
import slash from 'utils/common/slash';
import { isWindowsOS } from 'utils/common/platform';
const ClientCertSettings = ({ root, clientCertConfig, onUpdate, onRemove }) => {
const certFilePathInputRef = useRef();
@@ -68,7 +70,12 @@ const ClientCertSettings = ({ root, clientCertConfig, onUpdate, onRemove }) => {
const getFile = (e) => {
const filePath = window?.ipcRenderer?.getFilePath(e?.files?.[0]);
if (filePath) {
let relativePath = path.relative(root, filePath);
let relativePath;
if (isWindowsOS()) {
relativePath = slash(path.win32.relative(root, filePath));
} else {
relativePath = path.posix.relative(root, filePath);
}
formik.setFieldValue(e.name, relativePath);
}
};
@@ -102,23 +109,23 @@ const ClientCertSettings = ({ root, clientCertConfig, onUpdate, onRemove }) => {
<ul className="mt-4">
{!clientCertConfig.length
? 'No client certificates added'
: clientCertConfig.map((clientCert, index) => (
<li key={`client-cert-${index}`} className="flex items-center available-certificates p-2 rounded-lg mb-2">
<div className="flex items-center w-full justify-between">
<div className="flex w-full items-center">
<IconWorld className="mr-2" size={18} strokeWidth={1.5} />
{clientCert.domain}
: clientCertConfig.map((clientCert) => (
<li key={uuid()} className="flex items-center available-certificates p-2 rounded-lg mb-2">
<div className="flex items-center w-full justify-between">
<div className="flex w-full items-center">
<IconWorld className="mr-2" size={18} strokeWidth={1.5} />
{clientCert.domain}
</div>
<div className="flex w-full items-center">
<IconCertificate className="mr-2 flex-shrink-0" size={18} strokeWidth={1.5} />
{clientCert.type === 'cert' ? clientCert.certFilePath : clientCert.pfxFilePath}
</div>
<button onClick={() => onRemove(clientCert)} className="remove-certificate ml-2">
<IconTrash size={18} strokeWidth={1.5} />
</button>
</div>
<div className="flex w-full items-center">
<IconCertificate className="mr-2 flex-shrink-0" size={18} strokeWidth={1.5} />
{clientCert.type === 'cert' ? clientCert.certFilePath : clientCert.pfxFilePath}
</div>
<button onClick={() => onRemove(clientCert)} className="remove-certificate ml-2">
<IconTrash size={18} strokeWidth={1.5} />
</button>
</div>
</li>
))}
</li>
))}
</ul>
<h1 className="font-semibold mt-8 mb-2">Add Client Certificate</h1>
@@ -191,9 +198,9 @@ const ClientCertSettings = ({ root, clientCertConfig, onUpdate, onRemove }) => {
<div className="flex flex-row gap-2 items-center">
<div
className="my-[3px] overflow-hidden text-ellipsis whitespace-nowrap max-w-[300px]"
title={path.basename(formik.values.certFilePath)}
title={path.basename(slash(formik.values.certFilePath))}
>
{path.basename(formik.values.certFilePath)}
{path.basename(slash(formik.values.certFilePath))}
</div>
<IconTrash
size={18}
@@ -231,9 +238,9 @@ const ClientCertSettings = ({ root, clientCertConfig, onUpdate, onRemove }) => {
<div className="flex flex-row gap-2 items-center">
<div
className="my-[3px] overflow-hidden text-ellipsis whitespace-nowrap max-w-[300px]"
title={path.basename(formik.values.keyFilePath)}
title={path.basename(slash(formik.values.keyFilePath))}
>
{path.basename(formik.values.keyFilePath)}
{path.basename(slash(formik.values.keyFilePath))}
</div>
<IconTrash
size={18}
@@ -274,9 +281,9 @@ const ClientCertSettings = ({ root, clientCertConfig, onUpdate, onRemove }) => {
<div className="flex flex-row gap-2 items-center">
<div
className="my-[3px] overflow-hidden text-ellipsis whitespace-nowrap max-w-[300px]"
title={path.basename(formik.values.pfxFilePath)}
title={path.basename(slash(formik.values.pfxFilePath))}
>
{path.basename(formik.values.pfxFilePath)}
{path.basename(slash(formik.values.pfxFilePath))}
</div>
<IconTrash
size={18}

View File

@@ -1,7 +1,11 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
div.CodeMirror {
.CodeMirror-scroll {
padding-bottom: 0px;
}
}
.editing-mode {
cursor: pointer;
}

View File

@@ -1,21 +1,10 @@
import React from "react";
import React from 'react';
import { getTotalRequestCountInCollection } from 'utils/collections/';
import { IconFolder, IconWorld, IconApi, IconShare } from '@tabler/icons';
import { areItemsLoading, getItemsLoadStats } from "utils/collections/index";
import { useState } from "react";
import ShareCollection from "components/ShareCollection/index";
import { IconFolder, IconFileOff, IconWorld, IconApi } from '@tabler/icons';
const Info = ({ collection }) => {
const totalRequestsInCollection = getTotalRequestCountInCollection(collection);
const isCollectionLoading = areItemsLoading(collection);
const { loading: itemsLoadingCount, total: totalItems } = getItemsLoadStats(collection);
const [showShareCollectionModal, toggleShowShareCollectionModal] = useState(false);
const handleToggleShowShareCollectionModal = (value) => (e) => {
toggleShowShareCollectionModal(value);
}
return (
<div className="w-full flex flex-col h-fit">
<div className="rounded-lg py-6">
@@ -53,30 +42,15 @@ const Info = ({ collection }) => {
</div>
<div className="ml-4">
<div className="font-semibold text-sm">Requests</div>
<div className="mt-1 text-sm text-muted font-mono">
{
isCollectionLoading? `${totalItems - itemsLoadingCount} out of ${totalItems} requests in the collection loaded` : `${totalRequestsInCollection} request${totalRequestsInCollection !== 1 ? 's' : ''} in collection`
}
<div className="mt-1 text-sm text-muted">
{totalRequestsInCollection} request{totalRequestsInCollection !== 1 ? 's' : ''} in collection
</div>
</div>
</div>
<div className="flex items-start group cursor-pointer" onClick={handleToggleShowShareCollectionModal(true)}>
<div className="flex-shrink-0 p-3 bg-indigo-50 dark:bg-indigo-900/20 rounded-lg">
<IconShare className="w-5 h-5 text-indigo-500" stroke={1.5} />
</div>
<div className="ml-4 h-full flex flex-col justify-start">
<div className="font-semibold text-sm h-fit my-auto">Share</div>
<div className="mt-1 text-sm group-hover:underline text-link">
Share Collection
</div>
</div>
</div>
{showShareCollectionModal && <ShareCollection collection={collection} onClose={handleToggleShowShareCollectionModal(false)} />}
</div>
</div>
</div>
);
};
export default Info;
export default Info;

View File

@@ -2,15 +2,8 @@ import React from 'react';
import { flattenItems } from "utils/collections";
import { IconAlertTriangle } from '@tabler/icons';
import StyledWrapper from "./StyledWrapper";
import { useDispatch, useSelector } from 'react-redux';
import { isItemARequest, itemIsOpenedInTabs } from 'utils/tabs/index';
import { getDefaultRequestPaneTab } from 'utils/collections/index';
import { addTab, focusTab } from 'providers/ReduxStore/slices/tabs';
import { hideHomePage } from 'providers/ReduxStore/slices/app';
const RequestsNotLoaded = ({ collection }) => {
const dispatch = useDispatch();
const tabs = useSelector((state) => state.tabs.tabs);
const flattenedItems = flattenItems(collection.items);
const itemsFailedLoading = flattenedItems?.filter(item => item?.partial && !item?.loading);
@@ -18,29 +11,6 @@ const RequestsNotLoaded = ({ collection }) => {
return null;
}
const handleRequestClick = (item) => e => {
e.preventDefault();
if (isItemARequest(item)) {
dispatch(hideHomePage());
if (itemIsOpenedInTabs(item, tabs)) {
dispatch(
focusTab({
uid: item.uid
})
);
return;
}
dispatch(
addTab({
uid: item.uid,
collectionUid: collection.uid,
requestPaneTab: getDefaultRequestPaneTab(item)
})
);
return;
}
}
return (
<StyledWrapper className="w-full card my-2">
<div className="flex items-center gap-2 px-3 py-2 title bg-yellow-50 dark:bg-yellow-900/20">
@@ -61,7 +31,7 @@ const RequestsNotLoaded = ({ collection }) => {
<tbody>
{flattenedItems?.map((item, index) => (
item?.partial && !item?.loading ? (
<tr key={index} className='cursor-pointer' onClick={handleRequestClick(item)}>
<tr key={index}>
<td className="py-1.5 px-3">
{item?.pathname?.split(`${collection?.pathname}/`)?.[1]}
</td>

View File

@@ -104,15 +104,18 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
<div className="mb-3 flex items-center">
<label className="settings-label flex items-center" htmlFor="enabled">
Config
<InfoTip infotipId="request-var">
<InfoTip
text={`
<div>
<ul>
<li><span style={{width: "50px", display: "inline-block"}}>global</span> - use global proxy config</li>
<li><span style={{width: "50px", display: "inline-block"}}>enabled</span> - use collection proxy config</li>
<li><span style={{width: "50px", display: "inline-block"}}>disable</span> - disable proxy</li>
<li><span style="width: 50px;display:inline-block;">global</span> - use global proxy config</li>
<li><span style="width: 50px;display:inline-block;">enabled</span> - use collection proxy config</li>
<li><span style="width: 50px;display:inline-block;">disable</span> - disable proxy</li>
</ul>
</div>
</InfoTip>
`}
infotipId="request-var"
/>
</label>
<div className="flex items-center">
<label className="flex items-center">

View File

@@ -89,7 +89,7 @@ const VarsTable = ({ collection, vars, varType }) => {
<td>
<div className="flex items-center">
<span>Expr</span>
<InfoTip content="You can write any valid JS Template Literal here" infotipId="request-var" />
<InfoTip text="You can write any valid JS Template Literal here" infotipId="request-var" />
</div>
</td>
)}

View File

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

View File

@@ -1,371 +0,0 @@
import React, { useState, useRef, useEffect } from 'react';
import { useFormik } from 'formik';
import * as Yup from 'yup';
import Modal from 'components/Modal/index';
import { modifyCookie, addCookie, getParsedCookie, createCookieString } from 'providers/ReduxStore/slices/app';
import { useDispatch } from 'react-redux';
import toast from 'react-hot-toast';
import ToggleSwitch from 'components/ToggleSwitch/index';
import { IconInfoCircle } from '@tabler/icons';
import moment from 'moment';
import 'moment-timezone';
import { Tooltip } from 'react-tooltip';
import { isEmpty } from 'lodash';
const removeEmptyValues = (obj) => {
return Object.fromEntries(Object.entries(obj).filter(([_, value]) => value !== null && value !== undefined));
};
const ModifyCookieModal = ({ onClose, domain, cookie }) => {
const dispatch = useDispatch();
const [isRawMode, setIsRawMode] = useState(false);
const [cookieString, setCookieString] = useState('');
const initialParseRef = useRef(false);
const formik = useFormik({
enableReinitialize: true,
initialValues: {
...(cookie ? cookie : {}),
key: cookie?.key || '',
value: cookie?.value || '',
path: cookie?.path || '/',
domain: cookie?.domain || domain || '',
expires: cookie?.expires ? moment(cookie.expires).format(moment.HTML5_FMT.DATETIME_LOCAL) : '',
secure: cookie?.secure || false,
httpOnly: cookie?.httpOnly || false
},
validationSchema: Yup.object({
key: Yup.string().required('Key is required'),
value: Yup.string().required('Value is required'),
domain: Yup.string().required('Domain is required'),
secure: Yup.boolean(),
httpOnly: Yup.boolean(),
expires: Yup.mixed()
.nullable()
.transform((value) => {
if (!value || value === '') return null;
return moment(value).isValid() ? moment(value).toDate() : null;
})
.test('future-date', 'Expiration date must be in the future', (value) => {
if (!value) return true;
return moment(value).isAfter(moment());
})
}),
onSubmit: (values) => {
const modValues = removeEmptyValues({
...(cookie ? cookie : {}),
...values,
expires: values.expires
? moment(values.expires).isValid()
? moment(values.expires).toDate()
: Infinity
: Infinity
});
handleCookieDispatch(cookie, domain, modValues, onClose);
}
});
const title = cookie ? 'Modify Cookie' : 'Add Cookie';
const handleCookieDispatch = (cookie, domain, modValues, onClose) => {
if (cookie) {
dispatch(modifyCookie(domain, cookie, modValues))
.then(() => {
toast.success('Cookie modified successfully');
onClose();
})
.catch((err) => {
toast.error('An error occurred while modifying cookie');
console.error(err);
});
} else {
dispatch(addCookie(domain, modValues))
.then(() => {
toast.success('Cookie added successfully');
onClose();
})
.catch((err) => {
toast.error('An error occurred while adding cookie');
console.error(err);
});
}
};
const onSubmit = async () => {
try {
if (isRawMode) {
const cookieObj = await dispatch(getParsedCookie(cookieString));
const modifiedCookie = removeEmptyValues({
...formik.values,
...cookieObj,
expires: cookieObj?.expires
? moment(cookieObj.expires).isValid()
? moment(cookieObj.expires).toDate()
: Infinity
: Infinity
});
if (!cookieObj) {
toast.error('Please enter a valid cookie string');
return;
}
const validationErrors = await formik.setValues(
(values) => ({
...values,
...modifiedCookie,
expires:
modifiedCookie?.expires && moment(modifiedCookie.expires).isValid()
? moment(new Date(modifiedCookie.expires)).format(moment.HTML5_FMT.DATETIME_LOCAL)
: ''
}),
true
);
if (!isEmpty(validationErrors)) {
toast.error(Object.values(validationErrors).join("\n"));
return;
}
handleCookieDispatch(cookie, domain, modifiedCookie, onClose);
} else {
formik.handleSubmit();
}
} catch (error) {
const errMsg = error.message || 'An error occurred while parsing cookie string';
toast.error(errMsg);
}
};
useEffect(() => {
if (!isRawMode) return;
const loadCookieString = async () => {
if (cookie) {
const str = await dispatch(createCookieString(cookie));
setCookieString(str);
}
return '';
};
loadCookieString();
}, [cookie, isRawMode]);
// create the cookieString when raw mode is enabled
useEffect(() => {
if (isRawMode) {
const createCookieStr = async () => {
const str = await dispatch(createCookieString(formik.values));
setCookieString(str);
};
createCookieStr();
}
}, [isRawMode, formik.values]);
useEffect(() => {
// Reset the ref when raw mode changes
if (isRawMode) {
initialParseRef.current = false;
return;
}
const setParsedCookie = async () => {
if (!isRawMode && cookieString && !initialParseRef.current) {
initialParseRef.current = true;
try {
const cookieObj = await dispatch(getParsedCookie(cookieString));
if (!cookieObj) return;
formik.setValues(
(values) => ({
...values,
...removeEmptyValues(cookieObj),
expires:
cookieObj?.expires && moment(cookieObj.expires).isValid()
? moment(new Date(cookieObj.expires)).format(moment.HTML5_FMT.DATETIME_LOCAL)
: ''
}),
true
);
} catch (error) {
const errMsg = error.message || 'An error occurred while parsing cookie string';
toast.error(errMsg);
}
}
};
setParsedCookie();
}, [isRawMode, cookieString, dispatch, formik]);
return (
<Modal
size="lg"
title={title}
onClose={onClose}
handleCancel={onClose}
handleConfirm={onSubmit}
customHeader={
<div className="flex items-center justify-between w-full">
<h2 className="text-sm font-bold">{title}</h2>
<div className="ml-auto flex items-center ">
<ToggleSwitch
className="mr-2"
isOn={isRawMode}
size="2xs"
handleToggle={(e) => {
setIsRawMode(e.target.checked);
}}
/>
<label className="text-sm font-normal mr-4 normal-case">Edit Raw</label>
</div>
</div>
}
>
<form onSubmit={(e) => e.preventDefault()} className="px-2">
{isRawMode ? (
<div>
<div className="flex items-center gap-2 mb-1">
<label className="block text-sm">Set-Cookie String</label>
<IconInfoCircle id="cookie-raw-info" size={16} strokeWidth={1.5} className="text-gray-400" />
<Tooltip
anchorId="cookie-raw-info"
className="tooltip-mod"
html="Key, Path, and Domain are immutable properties and cannot be modified for existing cookies"
/>
</div>
<textarea
value={cookieString}
onChange={(e) => setCookieString(e.target.value)}
className="block textbox w-full h-24"
placeholder="key=value; key2=value2"
/>
</div>
) : (
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm mb-1">
Domain<span className="text-red-600">*</span>{' '}
</label>
<input
type="text"
name="domain"
// Auto-focus if its add-new i.e. when domain prop is empty
autoFocus={!domain && !formik.values.domain}
value={formik.values.domain}
onChange={formik.handleChange}
className="block textbox non-passphrase-input w-full disabled:opacity-50"
disabled={!!cookie}
/>
{formik.touched.domain && formik.errors.domain && (
<div className="text-red-500 text-sm mt-1">{formik.errors.domain}</div>
)}
</div>
<div>
<label className="block text-sm mb-1">Path</label>
<input
type="text"
name="path"
value={formik.values.path}
onChange={formik.handleChange}
className="block textbox non-passphrase-input w-full disabled:opacity-50"
disabled={!!cookie}
/>
{formik.touched.path && formik.errors.path && (
<div className="text-red-500 text-sm mt-1">{formik.errors.path}</div>
)}
</div>
<div>
<label className="block text-sm mb-1">
Key<span className="text-red-600">*</span>{' '}
</label>
<input
type="text"
name="key"
// Auto focus when add-for-domain i.e. if domain is already prefilled
autoFocus={!!domain && !formik.values.key}
value={formik.values.key}
onChange={formik.handleChange}
className="block textbox non-passphrase-input w-full disabled:opacity-50"
disabled={!!cookie}
/>
{formik.touched.key && formik.errors.key && (
<div className="text-red-500 text-sm mt-1">{formik.errors.key}</div>
)}
</div>
<div>
<label className="block text-sm mb-1">
Value<span className="text-red-600">*</span>{' '}
</label>
<input
type="text"
name="value"
// Auto-focus when its in edit mode i.e. cookie prop is present
autoFocus={!!cookie}
value={formik.values.value}
onChange={formik.handleChange}
className="block textbox non-passphrase-input w-full"
/>
{formik.touched.value && formik.errors.value && (
<div className="text-red-500 text-sm mt-1">{formik.errors.value}</div>
)}
</div>
</div>
{/* Date Picker */}
<div className="w-full flex items-end">
<div>
<label className="block text-sm mb-1">Expiration ({moment.tz.guess()})</label>
<input
type="datetime-local"
name="expires"
value={formik.values.expires}
onChange={(e) => {
formik.handleChange(e);
}}
className="block textbox non-passphrase-input w-full"
min={moment().format(moment.HTML5_FMT.DATETIME_LOCAL)}
/>
{formik.touched.expires && formik.errors.expires && (
<div className="text-red-500 text-sm mt-1">{formik.errors.expires}</div>
)}
</div>
{/* Checkboxes */}
<div className="flex space-x-4 ml-auto">
<label className="flex items-center">
<input
type="checkbox"
name="secure"
checked={formik.values.secure}
onChange={formik.handleChange}
className="mr-2"
/>
<span className="text-sm">Secure</span>
</label>
<label className="flex items-center">
<input
type="checkbox"
name="httpOnly"
checked={formik.values.httpOnly}
onChange={formik.handleChange}
className="mr-2"
/>
<span className="text-sm">HTTP Only</span>
</label>
</div>
</div>
</div>
)}
</form>
</Modal>
);
};
export default ModifyCookieModal;

View File

@@ -11,65 +11,6 @@ const Wrapper = styled.div`
user-select: none;
}
}
&.header {
input {
padding: 0.3rem 0.5rem;
}
}
.textbox {
line-height: 1.42857143;
border: 1px solid #ccc;
padding: 0.45rem;
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};
&:focus {
border: solid 1px ${(props) => props.theme.modal.input.focusBorder} !important;
outline: none !important;
}
}
.scroll-box {
max-height: 500px;
overflow-y: auto;
background:
/* Shadow Cover TOP */
linear-gradient(
${(props) => props.theme.modal.body.bg} 20%,
rgba(255, 255, 255, 0)
) center top,
/* Shadow Cover BOTTOM */
linear-gradient(
rgba(255, 255, 255, 0),
${(props) => props.theme.modal.body.bg} 80%
) center bottom,
/* Shadow TOP */
linear-gradient(
rgba(0, 0, 0, 0.1) 0%,
rgba(0, 0, 0, 0) 100%
) center top,
/* Shadow BOTTOM */
linear-gradient(
rgba(0, 0, 0, 0) 0%,
rgba(0, 0, 0, 0.1) 100%
) center bottom;
background-repeat: no-repeat;
background-size: 100% 30px, 100% 30px, 100% 10px, 100% 10px;
background-attachment: local, local, scroll, scroll;
}
`;
export default Wrapper;

View File

@@ -1,331 +1,53 @@
import React, { useState, useRef, useEffect, useMemo } from 'react';
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import Modal from 'components/Modal';
import Accordion from 'components/Accordion/index';
import { IconTrash, IconEdit, IconCirclePlus, IconCookieOff, IconAlertTriangle, IconSearch } from '@tabler/icons';
import { deleteCookiesForDomain, deleteCookie } from 'providers/ReduxStore/slices/app';
import { IconTrash } from '@tabler/icons';
import { deleteCookiesForDomain } from 'providers/ReduxStore/slices/app';
import toast from 'react-hot-toast';
import ModifyCookieModal from 'components/Cookies/ModifyCookieModal/index';
import StyledWrapper from './StyledWrapper';
import moment from 'moment';
import { Tooltip } from 'react-tooltip';
const ClearDomainCookiesModal = ({ onClose, domain, onClear }) => (
<Modal onClose={onClose} handleCancel={onClose} title="Clear Domain Cookies" hideFooter={true}>
<div className="flex items-center font-normal">
<IconAlertTriangle size={32} strokeWidth={1.5} className="text-yellow-600" />
<h1 className="ml-2 text-lg font-semibold">Hold on..</h1>
</div>
<div className="font-normal mt-4">
Are you sure you want to clear all cookies for the domain {domain}?
</div>
<div className="flex justify-between mt-6">
<div>
<button className="btn btn-sm btn-close" onClick={onClose}>
Close
</button>
</div>
<div>
<button className="btn btn-sm btn-danger" onClick={onClear}>
Clear All
</button>
</div>
</div>
</Modal>
);
const DeleteCookieModal = ({ onClose, cookieName, onDelete }) => (
<Modal onClose={onClose} handleCancel={onClose} title="Delete Cookie" hideFooter={true}>
<div className="flex items-center font-normal">
<IconAlertTriangle size={32} strokeWidth={1.5} className="text-yellow-600" />
<h1 className="ml-2 text-lg font-semibold">Hold on..</h1>
</div>
<div className="font-normal mt-4">
Are you sure you want to delete the cookie {cookieName}?
</div>
<div className="flex justify-between mt-6">
<div>
<button className="btn btn-sm btn-close" onClick={onClose}>
Close
</button>
</div>
<div>
<button className="btn btn-sm btn-danger" onClick={onDelete}>
Delete
</button>
</div>
</div>
</Modal>
);
const CollectionProperties = ({ onClose }) => {
const dispatch = useDispatch();
const cookies = useSelector((state) => state.app.cookies) || [];
const [isModifyCookieModalOpen, setIsModifyCookieModalOpen] = useState(false);
const [currentDomain, setCurrentDomain] = useState(null);
const [cookieToEdit, setCookieToEdit] = useState(null);
const [domainToClear, setDomainToClear] = useState(null);
const [cookieToDelete, setCookieToDelete] = useState(null);
const [searchText, setSearchText] = useState(null);
const handleAddCookie = (domain) => {
if(domain) setCurrentDomain(domain);
setIsModifyCookieModalOpen(true);
};
const handleEditCookie = (domain, cookie) => {
setCurrentDomain(domain);
setCookieToEdit(cookie);
setIsModifyCookieModalOpen(true);
};
const handleClearDomainCookies = (domain) => {
setDomainToClear(domain);
};
const clearDomainCookiesAction = () => {
dispatch(deleteCookiesForDomain(domainToClear))
const handleDeleteDomain = (domain) => {
dispatch(deleteCookiesForDomain(domain))
.then(() => {
toast.success('Domain cookies cleared successfully');
toast.success('Domain deleted successfully');
})
.catch((err) => console.log(err) && toast.error('Failed to clear domain cookies'));
setDomainToClear(null);
.catch((err) => console.log(err) && toast.error('Failed to delete domain'));
};
const handleDeleteCookie = (domain, path, key) => {
setCookieToDelete({ key, domain, path });
};
const deleteCookieAction = () => {
if (cookieToDelete) {
const { domain, path, key } = cookieToDelete;
dispatch(deleteCookie(domain, path, key))
.then(() => {
toast.success('Cookie deleted successfully');
})
.catch((err) => console.log(err) && toast.error('Failed to delete cookie'));
}
setCookieToDelete(null);
};
const filteredCookies = useMemo(() => {
if (!searchText) return cookies;
return cookies.filter((cookie) =>
cookie.domain.toLowerCase().includes(searchText.toLowerCase())
);
}, [cookies, searchText]);
const shouldShowHeader = cookies && cookies.length > 0;
return (
<>
<Modal
size="xl"
title="Cookies"
hideFooter={true}
handleCancel={onClose}
customHeader={shouldShowHeader ? (
<StyledWrapper className="header flex items-center justify-between w-full">
<h2 className="text-xs font-medium">Cookies</h2>
<input
type="search"
placeholder="Search by domain"
value={searchText || ''}
onChange={(e) => setSearchText(e.target.value)}
className="block textbox non-passphrase-input ml-auto font-normal"
/>
<button
type="submit"
className="submit btn btn-sm btn-secondary flex items-center gap-1 mx-4 font-medium"
onClick={(e) => {
e.stopPropagation();
handleAddCookie();
}}
>
<IconCirclePlus strokeWidth={1.5} size={16} />
<span>Add Cookie</span>
</button>
</StyledWrapper>
) : null}
>
<StyledWrapper>
{!cookies || !cookies.length ? (
// No cookies found
<div className="flex items-center justify-center flex-col">
<IconCookieOff size={48} strokeWidth={1.5} className="text-gray-500" />
<h2 className="text-lg font-semibold mt-4">No cookies found</h2>
<p className="text-gray-500 mt-2">Add cookies to get started</p>
<button
type="submit"
className="submit btn btn-sm btn-secondary flex items-center gap-1 mt-8"
onClick={(e) => {
e.stopPropagation();
handleAddCookie();
}}
>
<IconCirclePlus strokeWidth={1.5} size={16} />
<span>Add Cookie</span>
</button>
</div>
) : cookies.length && !filteredCookies.length ? (
// No search results
<div className="flex items-center justify-center flex-col">
<IconSearch size={48} />
<h2 className="text-lg font-semibold mt-4">No search results</h2>
<p className="text-gray-500 mt-2">Try a different search term</p>
</div>
) : (
// Show cookies list
<div className="scroll-box">
<Accordion defaultIndex={0}>
{filteredCookies.map((domainWithCookies, i) => (
<Accordion.Item key={i} index={i}>
<Accordion.Header index={i} className="flex items-center">
<div className="flex items-center">
<span>{domainWithCookies.domain}</span>
<span className="ml-2 text-xs dark:text-gray-300 text-gray-500">
({domainWithCookies.cookies.length}{' '}
{domainWithCookies.cookies.length === 1 ? 'cookie' : 'cookies'})
</span>
<div className="ml-auto flex items-center gap-2">
<button
type="submit"
className="flex items-center gap-1 text-gray-500 hover:text-gray-950 dark:text-white dark:hover:text-gray-300"
onClick={(e) => {
e.stopPropagation();
handleAddCookie(domainWithCookies.domain);
}}
>
<IconCirclePlus strokeWidth={1.5} size={16} />
</button>
<button
onClick={(e) => {
e.stopPropagation();
handleClearDomainCookies(domainWithCookies.domain);
}}
className="text-gray-950 dark:text-white dark:hover:hover:text-red-600 hover:text-red-600 mr-2"
>
<IconTrash strokeWidth={1.5} size={16} />
</button>
</div>
</div>
</Accordion.Header>
<Accordion.Content index={i}>
<div className="flex items-center justify-between">
<table className="w-full">
<thead>
<tr className="text-left border-b border-gray-200 dark:border-neutral-600 text-gray-700 dark:text-gray-300">
<th className="py-2 px-4 font-semibold w-32">Name</th>
<th className="py-2 px-4 font-semibold w-52">Value</th>
<th className="py-2 px-4 font-semibold">Path</th>
<th className="py-2 px-4 font-semibold">Expires</th>
<th className="py-2 px-4 font-semibold text-center">Secure</th>
<th className="py-2 px-4 font-semibold text-center">HTTP Only</th>
<th className="py-2 px-4 font-semibold text-right w-24">Actions</th>
</tr>
</thead>
<tbody>
{domainWithCookies.cookies.map((cookie) => (
<tr key={cookie.key} className="border-b border-gray-200 dark:border-neutral-600 last:border-none">
<td className="py-2 px-4 truncate">
<span id={`cookie-key-${cookie.key}`}>{cookie.key}</span>
<Tooltip
anchorId={`cookie-key-${cookie.key}`}
className="tooltip-mod"
html={cookie.key}
/>
</td>
<td className="py-2 px-4 truncate">
<span id={`cookie-value-${cookie.key}`}>{cookie.value}</span>
<Tooltip
anchorId={`cookie-value-${cookie.key}`}
className="tooltip-mod"
html={cookie.value}
/>
</td>
<td className="py-2 px-4 truncate">{cookie.path || '/'}</td>
<td className="py-2 px-4 truncate">
<span id={`cookie-expires-${cookie.key}`}>
{cookie.expires && moment(cookie.expires).isValid()
? new Date(cookie.expires).toLocaleString()
: 'Session'}
</span>
{cookie.expires && moment(cookie.expires).isValid() && (
<Tooltip
anchorId={`cookie-expires-${cookie.key}`}
className="tooltip-mod"
html={new Date(cookie.expires).toLocaleString()}
/>
)}
</td>
<td className="py-2 px-4 text-center">{cookie.secure ? '✓' : ''}</td>
<td className="py-2 px-4 text-center">{cookie.httpOnly ? '✓' : ''}</td>
<td className="py-2 px-4">
<div className="flex items-center justify-end gap-2">
<button
onClick={(e) => {
e.stopPropagation();
handleEditCookie(domainWithCookies.domain, cookie);
}}
className="text-gray-700 hover:text-gray-950
dark:text-white dark:hover:text-gray-300"
>
<IconEdit strokeWidth={1.5} size={16} />
</button>
<button
onClick={(e) => {
e.stopPropagation();
handleDeleteCookie(domainWithCookies.domain, cookie.path, cookie.key);
}}
className="text-gray-950 dark:text-white dark:hover:hover:text-red-600 hover:text-red-600"
>
<IconTrash strokeWidth={1.5} size={16} />
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</Accordion.Content>
</Accordion.Item>
))}
</Accordion>
</div>
)}
</StyledWrapper>
</Modal>
{isModifyCookieModalOpen && (
<ModifyCookieModal
onClose={() => {
setCookieToEdit(null);
setCurrentDomain(null);
setIsModifyCookieModalOpen(false);
}}
domain={currentDomain}
cookie={cookieToEdit}
/>
)}
{domainToClear ? (
<ClearDomainCookiesModal
onClose={() => setDomainToClear(null)}
domain={domainToClear}
onClear={clearDomainCookiesAction}
/>
) : null}
{cookieToDelete ? (
<DeleteCookieModal
onClose={() => setCookieToDelete(null)}
cookieName={cookieToDelete.key}
onDelete={deleteCookieAction}
/>
) : null}
</>
<Modal size="md" title="Cookies" hideFooter={true} handleCancel={onClose}>
<StyledWrapper>
<table className="w-full border-collapse" style={{ marginTop: '-1rem' }}>
<thead>
<tr>
<th className="py-2 px-2 text-left">Domain</th>
<th className="py-2 px-2 text-left">Cookie</th>
<th className="py-2 px-2 text-center" style={{ width: 80 }}>
Actions
</th>
</tr>
</thead>
<tbody>
{cookies.map((cookie) => (
<tr key={cookie.domain}>
<td className="py-2 px-2">{cookie.domain}</td>
<td className="py-2 px-2 break-all">{cookie.cookieString}</td>
<td className="text-center">
<button tabIndex="-1" onClick={() => handleDeleteDomain(cookie.domain)}>
<IconTrash strokeWidth={1.5} size={20} />
</button>
</td>
</tr>
))}
</tbody>
</table>
</StyledWrapper>
</Modal>
);
};

View File

@@ -6,7 +6,6 @@ import * as Yup from 'yup';
import { useDispatch } from 'react-redux';
import Portal from 'components/Portal';
import Modal from 'components/Modal';
import { validateName, validateNameError } from 'utils/common/regex';
const CreateEnvironment = ({ collection, onClose }) => {
const dispatch = useDispatch();
@@ -24,11 +23,7 @@ const CreateEnvironment = ({ collection, onClose }) => {
validationSchema: Yup.object({
name: Yup.string()
.min(1, 'Must be at least 1 character')
.max(255, 'Must be 255 characters or less')
.test('is-valid-filename', function(value) {
const isValid = validateName(value);
return isValid ? true : this.createError({ message: validateNameError(value) });
})
.max(50, 'Must be 50 characters or less')
.required('Name is required')
.test('duplicate-name', 'Environment already exists', validateEnvironmentName)
}),

View File

@@ -6,7 +6,6 @@ import { useFormik } from 'formik';
import { renameEnvironment } from 'providers/ReduxStore/slices/collections/actions';
import * as Yup from 'yup';
import { useDispatch } from 'react-redux';
import { validateName, validateNameError } from 'utils/common/regex';
const RenameEnvironment = ({ onClose, environment, collection }) => {
const dispatch = useDispatch();
@@ -19,11 +18,7 @@ const RenameEnvironment = ({ onClose, environment, collection }) => {
validationSchema: Yup.object({
name: Yup.string()
.min(1, 'must be at least 1 character')
.max(255, 'Must be 255 characters or less')
.test('is-valid-filename', function(value) {
const isValid = validateName(value);
return isValid ? true : this.createError({ message: validateNameError(value) });
})
.max(50, 'must be 50 characters or less')
.required('name is required')
}),
onSubmit: (values) => {

View File

@@ -1,13 +1,15 @@
import React from 'react';
import path from 'utils/common/path';
import path from 'path';
import { useDispatch } from 'react-redux';
import { browseFiles } from 'providers/ReduxStore/slices/collections/actions';
import { IconX } from '@tabler/icons';
import { isWindowsOS } from 'utils/common/platform';
import slash from 'utils/common/slash';
const FilePickerEditor = ({ value, onChange, collection, isSingleFilePicker = false }) => {
const FilePickerEditor = ({ value, onChange, collection }) => {
value = value || [];
const dispatch = useDispatch();
const filenames = (isSingleFilePicker ? [value] : value || [])
const filenames = value
.filter((v) => v != null && v != '')
.map((v) => {
const separator = isWindowsOS() ? '\\' : '/';
@@ -18,7 +20,7 @@ const FilePickerEditor = ({ value, onChange, collection, isSingleFilePicker = fa
const title = filenames.map((v) => `- ${v}`).join('\n');
const browse = () => {
dispatch(browseFiles([], [!isSingleFilePicker ? "multiSelections": ""]))
dispatch(browseFiles())
.then((filePaths) => {
// If file is in the collection's directory, then we use relative path
// Otherwise, we use the absolute path
@@ -26,13 +28,13 @@ const FilePickerEditor = ({ value, onChange, collection, isSingleFilePicker = fa
const collectionDir = collection.pathname;
if (filePath.startsWith(collectionDir)) {
return path.relative(collectionDir, filePath);
return path.relative(slash(collectionDir), slash(filePath));
}
return filePath;
});
onChange(isSingleFilePicker ? filePaths[0] : filePaths);
onChange(filePaths);
})
.catch((error) => {
console.error(error);
@@ -40,14 +42,14 @@ const FilePickerEditor = ({ value, onChange, collection, isSingleFilePicker = fa
};
const clear = () => {
onChange(isSingleFilePicker ? '' : []);
onChange([]);
};
const renderButtonText = (filenames) => {
if (filenames.length == 1) {
return filenames[0];
}
return filenames.length + ' file(s) selected';
return filenames.length + ' files selected';
};
return filenames.length > 0 ? (
@@ -64,9 +66,9 @@ const FilePickerEditor = ({ value, onChange, collection, isSingleFilePicker = fa
</div>
) : (
<button className="btn btn-secondary px-1" style={{ width: '100%' }} onClick={browse}>
{isSingleFilePicker ? 'Select File' : 'Select Files'}
Select Files
</button>
);
};
export default FilePickerEditor;
export default FilePickerEditor;

View File

@@ -88,7 +88,7 @@ const VarsTable = ({ folder, collection, vars, varType }) => {
<td>
<div className="flex items-center">
<span>Expr</span>
<InfoTip content="You can write any valid JS expression here" infotipId="response-var" />
<InfoTip text="You can write any valid JS expression here" infotipId="response-var" />
</div>
</td>
)}

View File

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

View File

@@ -6,7 +6,6 @@ import { useDispatch, useSelector } from 'react-redux';
import Portal from 'components/Portal';
import Modal from 'components/Modal';
import { addGlobalEnvironment } from 'providers/ReduxStore/slices/global-environments';
import { validateName, validateNameError } from 'utils/common/regex';
const CreateEnvironment = ({ onClose }) => {
const globalEnvs = useSelector((state) => state?.globalEnvironments?.globalEnvironments);
@@ -26,11 +25,7 @@ const CreateEnvironment = ({ onClose }) => {
validationSchema: Yup.object({
name: Yup.string()
.min(1, 'Must be at least 1 character')
.max(255, 'Must be 255 characters or less')
.test('is-valid-filename', function(value) {
const isValid = validateName(value);
return isValid ? true : this.createError({ message: validateNameError(value) });
})
.max(50, 'Must be 50 characters or less')
.required('Name is required')
.test('duplicate-name', 'Global Environment already exists', validateEnvironmentName)
}),

View File

@@ -3,10 +3,10 @@ import Portal from 'components/Portal/index';
import Modal from 'components/Modal/index';
import toast from 'react-hot-toast';
import { useFormik } from 'formik';
import { renameEnvironment } from 'providers/ReduxStore/slices/collections/actions';
import * as Yup from 'yup';
import { useDispatch } from 'react-redux';
import { renameGlobalEnvironment } from 'providers/ReduxStore/slices/global-environments';
import { validateName, validateNameError } from 'utils/common/regex';
const RenameEnvironment = ({ onClose, environment }) => {
const dispatch = useDispatch();
@@ -19,11 +19,7 @@ const RenameEnvironment = ({ onClose, environment }) => {
validationSchema: Yup.object({
name: Yup.string()
.min(1, 'must be at least 1 character')
.max(255, 'Must be 255 characters or less')
.test('is-valid-filename', function(value) {
const isValid = validateName(value);
return isValid ? true : this.createError({ message: validateNameError(value) });
})
.max(50, 'must be 50 characters or less')
.required('name is required')
}),
onSubmit: (values) => {

View File

@@ -1,11 +0,0 @@
import styled from 'styled-components';
const Wrapper = styled.div`
font-weight: 400;
font-size: 0.75rem;
background-color: ${props => props.theme.infoTip.bg};
border: 1px solid ${props => props.theme.infoTip.border};
box-shadow: ${props => props.theme.infoTip.boxShadow};
`;
export default Wrapper;

View File

@@ -1,40 +0,0 @@
/**
* The InfoTip components needs to be nuked
* This component will be the future replacement
* We should allow icon and placement props to be passed in
*/
import React, { useState } from 'react';
import HelpIcon from 'components/Icons/Help';
import StyledWrapper from './StyledWrapper';
const Help = ({ children, width = 200 }) => {
const [showTooltip, setShowTooltip] = useState(false);
return (
<div className="flex items-center relative">
<span
className="flex items-center"
onMouseEnter={() => setShowTooltip(true)}
onMouseLeave={() => setShowTooltip(false)}
>
<HelpIcon size={14}/>
</span>
{showTooltip && (
<StyledWrapper
className="absolute z-50 rounded-md p-3"
style={{
top: '50%',
left: 'calc(100% + 8px)',
transform: 'translateY(-50%)',
width: `${width}px`
}}
>
{children}
</StyledWrapper>
)}
</div>
);
};
export default Help;

View File

@@ -1,20 +0,0 @@
import React from 'react';
const HelpIcon = ({ size = 14 }) => {
return (
<svg
tabIndex="-1"
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
fill="currentColor"
className="inline-block ml-2 cursor-pointer"
viewBox="0 0 16 16"
>
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z" />
<path d="M5.255 5.786a.237.237 0 0 0 .241.247h.825c.138 0 .248-.113.266-.25.09-.656.54-1.134 1.342-1.134.686 0 1.314.343 1.314 1.168 0 .635-.374.927-.965 1.371-.673.489-1.206 1.06-1.168 1.987l.003.217a.25.25 0 0 0 .25.246h.811a.25.25 0 0 0 .25-.25v-.105c0-.718.273-.927 1.01-1.486.609-.463 1.244-.977 1.244-2.056 0-1.511-1.276-2.241-2.673-2.241-1.267 0-2.655.59-2.75 2.286zm1.557 5.763c0 .533.425.927 1.01.927.609 0 1.028-.394 1.028-.927 0-.552-.42-.94-1.029-.94-.584 0-1.009.388-1.009.94z" />
</svg>
)
}
export default HelpIcon;

View File

@@ -1,104 +0,0 @@
const OpenApiLogo = () => {
return (
<svg width="28" height="28" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg">
<path
style={{
fill: "#91d400",
fillOpacity: 1,
fillRule: "nonzero",
stroke: "none"
}} d="M43.125 51.148H20.781l.012.325c.012.21.027.418.039.625.004.09.008.18.016.27a41.442 41.442 0 0 0 .164 1.687c0 .027.004.05.008.078.035.285.07.574.113.86 0 .003 0 .007.004.01a36.98 36.98 0 0 0 1.152 5.255c.004.008.008.012.008.02a27.978 27.978 0 0 0 .265.859c.004.015.012.031.016.047.078.242.164.484.246.73.024.059.043.121.067.184.074.207.148.418.226.629.04.093.074.187.11.285.07.172.136.343.203.52.054.128.11.257.164.39.054.133.113.27.168.406.074.164.148.328.218.492.047.102.09.2.133.297.09.195.184.395.278.59l.093.191c.11.227.223.45.336.672.02.035.035.07.051.102a36.344 36.344 0 0 0 .41.773c.028.051.059.102.086.149L44.45 56.156l.07-.043a15.031 15.031 0 0 1-1.394-4.965Zm0 0" transform="translate(-26.793 -.606) scale(1.44332)"
/>
<path
style={{
fill: "#91d400",
fillOpacity: 1,
fillRule: "nonzero",
stroke: "none"
}} d="m22.563 61.137-.727.207.008.02zm0 0" transform="translate(-26.793 -.606) scale(1.44332)"
/>
<path
style={{
fill: "#4c5930",
fillOpacity: 1,
fillRule: "nonzero",
stroke: "none"
}} d="m48.613 61.355-.05.055-15.739 15.664c.082.074.16.149.242.223.149.133.297.266.446.394.078.067.152.137.23.204.18.152.36.3.54.449.046.043.097.082.144.12a21.669 21.669 0 0 0 .691.556c.227.175.45.343.676.515.012.008.02.012.027.02.95.707 1.93 1.367 2.946 1.98.035.024.07.043.105.067l.578.34c.117.066.239.132.356.203.113.062.222.125.336.187.207.11.41.223.617.328a35.567 35.567 0 0 0 1.824.887l.559-1.348 7.918-19.133.027-.07a15.337 15.337 0 0 1-2.473-1.64Zm0 0" transform="translate(-26.793 -.606) scale(1.44332)"
/>
<path
style={{
fill: "#68a338",
fillOpacity: 1,
fillRule: "nonzero",
stroke: "none"
}} d="M46.977 59.797a16.778 16.778 0 0 1-.899-1.102c-.152-.203-.3-.406-.437-.617a15.93 15.93 0 0 1-.41-.633L26.124 68.902c.297.485.602.957.914 1.422.012.016.02.035.031.051l.012.016c.008.015.02.03.027.046.004.004.004.004.004.008.028.035.051.07.078.11 0 .004 0 .004.004.007v.004a35.121 35.121 0 0 0 1.051 1.465c.008.012.016.02.02.031.156.2.308.403.464.602.024.027.043.05.063.078.164.203.328.406.496.61.04.046.078.093.117.144.153.18.305.36.457.535.067.078.137.153.203.227.13.148.262.297.395.445.074.082.152.16.226.242.032.035.067.075.102.11l.293.316.121.121c.176.184.352.363.531.54l15.758-15.684a22.18 22.18 0 0 1-.515-.551Zm0 0" transform="translate(-26.793 -.606) scale(1.44332)"
/>
<path
style={{
fill: "#4c5930",
fillOpacity: 1,
fillRule: "nonzero",
stroke: "none"
}} d="M67.867 61.348c-.176.136-.347.273-.527.406l.039.066L78.87 80.805a38.54 38.54 0 0 0 1.57-1.078 38.099 38.099 0 0 0 3.227-2.653L67.93 61.41Zm0 0" transform="translate(-26.793 -.606) scale(1.44332)"
/>
<path
style={{
fill: "#91d400",
fillOpacity: 1,
fillRule: "nonzero",
stroke: "none"
}} d="m77.418 81.707.023-.012-.023.012zm.023-.012c.051-.027.102-.054.153-.086l-.004-.004c-.05.032-.098.06-.149.09zm-.023.012-.008.004zm-.039-.039.027.047zm.039.039.023-.012-.023.012zm-.016.012.004-.004zm.008-.009-.004.005c.004-.004.008-.004.012-.008-.004.004-.004.004-.008.004zm0 0" transform="translate(-26.793 -.606) scale(1.44332)"
/>
<path
style={{
fill: "#91d400",
fillOpacity: 1,
fillRule: "nonzero",
stroke: "none"
}} d="M77.441 81.695c.051-.03.102-.054.153-.086-.051.032-.102.059-.153.086zm.149-.09.004.004zm-.2.118.005-.004zm-.19-.763-.391-.644-10.727-17.722c-.215.133-.437.25-.66.367-.223.121-.45.23-.676.34a15.303 15.303 0 0 1-6.523 1.469c-1.465 0-2.93-.211-4.344-.63-.238-.074-.473-.167-.711-.25-.238-.085-.48-.156-.715-.253l-7.91 19.117-.309.75-.265.644h-.004l.062.024c.024.012.043.016.067.027h.004c.004 0 .007.004.011.004.188.078.375.145.563.215.238.094.473.184.707.27.121.042.238.097.36.136a38.13 38.13 0 0 0 7.648 1.824c.105.012.207.028.308.04l.32.035c.2.023.4.047.602.066l.149.012c.25.023.496.043.742.062.082.004.168.008.25.016.219.016.433.027.648.039.133.004.266.008.399.016.172.004.343.011.515.015.246.008.496.008.746.012h.176c2.082 0 4.164-.172 6.223-.516l.105-.015c.215-.04.434-.078.653-.117.125-.024.246-.047.37-.075.126-.023.255-.05.384-.078.21-.043.421-.09.632-.14l.118-.024a37.8 37.8 0 0 0 8.992-3.34c.187-.097.367-.207.554-.308.22-.121.438-.246.657-.371.152-.086.308-.164.457-.254l.004-.004h.004l.003-.004c.004 0 .004 0 .004-.004l-.027-.047.027.047h.004c.004-.004.008-.004.008-.004.008-.008.016-.012.023-.016.051-.03.102-.058.149-.09zM48.625 37.938c.172-.141.348-.274.523-.407l-.039-.066L37.621 18.48c-.535.348-1.062.704-1.578 1.082a37.213 37.213 0 0 0-3.219 2.649l15.739 15.664Zm0 0" transform="translate(-26.793 -.606) scale(1.44332)"
/>
<path
style={{
fill: "#4c5930",
fillOpacity: 1,
fillRule: "nonzero",
stroke: "none"
}} d="M31.73 23.254c-.18.18-.347.363-.523.543-.172.18-.352.36-.523.543a38.163 38.163 0 0 0-3.32 4.125c-.106.156-.216.312-.321.469-.11.164-.219.328-.324.496-.04.058-.078.12-.117.18a37.099 37.099 0 0 0-5.82 18.527c-.009.25-.016.504-.02.754s-.012.5-.012.75h22.29c0-.25.023-.5.034-.75.012-.254.016-.504.043-.754a15.002 15.002 0 0 1 3.36-8.078c.16-.192.34-.375.507-.559.172-.188.329-.379.508-.559zm45.993-5.508a46.43 46.43 0 0 0-.684-.402c-.117-.067-.23-.133-.348-.196-.117-.066-.23-.128-.347-.195-.2-.11-.403-.219-.606-.324-.031-.016-.062-.031-.093-.05a38.014 38.014 0 0 0-4.02-1.798c-.035-.015-.074-.027-.11-.039a37.527 37.527 0 0 0-8.41-2.102c-.101-.015-.207-.027-.312-.042l-.313-.036a31.754 31.754 0 0 0-.777-.078c-.238-.023-.48-.043-.719-.062-.093-.004-.187-.012-.28-.016-.204-.015-.415-.027-.618-.039l-.328-.012v22.239c1.144.117 2.281.363 3.383.734l16.445-16.367a41.117 41.117 0 0 0-1.863-1.215zm0 0" transform="translate(-26.793 -.606) scale(1.44332)"
/>
<path
style={{
fill: "#68a338",
fillOpacity: 1,
fillRule: "nonzero",
stroke: "none"
}} d="m38.898 17.68.391.644zm18.59-5.34c-.25.004-.504.004-.754.015a38.117 38.117 0 0 0-4.71.48c-.036.009-.07.013-.106.02-.219.036-.434.079-.652.118l-.371.07c-.13.027-.258.05-.383.082a36.99 36.99 0 0 0-.75.164 37.925 37.925 0 0 0-8.996 3.336c-.184.098-.368.21-.551.312-.219.118-.438.243-.66.368-.16.093-.325.18-.489.277h-.003a.162.162 0 0 1-.036.02c-.043.027-.086.046-.129.074l.004.004.387.644 11.117 18.367c.215-.132.438-.25.66-.367a15.15 15.15 0 0 1 5.668-1.73c.25-.028.5-.047.754-.063.25-.011.504-.023.758-.023V12.324c-.254 0-.504.008-.758.016zm0 0" transform="translate(-26.793 -.606) scale(1.44332)"
/>
<path
style={{
fill: "#4c5930",
fillOpacity: 1,
fillRule: "nonzero",
stroke: "none"
}} d="m95.695 47.809-.035-.598c-.008-.102-.012-.2-.02-.3l-.058-.704c-.008-.059-.012-.121-.016-.18a53.501 53.501 0 0 0-.086-.785c-.003-.023-.003-.043-.007-.062 0-.012 0-.02-.004-.032-.035-.28-.07-.562-.114-.843 0-.012 0-.02-.003-.028a36.772 36.772 0 0 0-1.153-5.246 47.929 47.929 0 0 0-.258-.832c-.011-.035-.023-.07-.03-.105-.083-.242-.161-.48-.247-.719-.023-.063-.043-.129-.066-.195a39.302 39.302 0 0 0-.34-.91c-.067-.172-.133-.344-.2-.512-.054-.137-.109-.27-.163-.403-.055-.132-.114-.261-.168-.394l-.223-.504-.129-.285c-.09-.2-.188-.402-.281-.602-.032-.058-.059-.12-.086-.18-.113-.23-.227-.456-.34-.683-.016-.031-.031-.062-.05-.094-.13-.25-.259-.5-.395-.746l-.012-.023a36.958 36.958 0 0 0-2.133-3.446L72.628 44.77c.376 1.097.618 2.23.735 3.37h22.344l-.012-.331zm0 0" transform="translate(-26.793 -.606) scale(1.44332)"
/>
<path
style={{
fill: "#68a338",
fillOpacity: 1,
fillRule: "nonzero",
stroke: "none"
}} d="M73.45 49.64c0 .255-.024.505-.036.755-.012.25-.016.503-.043.753a15.002 15.002 0 0 1-3.36 8.079c-.16.191-.335.375-.507.558-.168.188-.328.38-.508.559L84.758 76.03c.18-.18.347-.363.523-.543.172-.183.352-.363.52-.547a36.965 36.965 0 0 0 3.195-3.937c.04-.055.074-.11.11-.16.117-.168.234-.34.347-.508.102-.152.203-.3.3-.453.048-.074.095-.153.142-.223a37.055 37.055 0 0 0 5.808-18.515c.012-.25.016-.5.024-.75.003-.25.011-.5.011-.754zm0 0" transform="translate(-26.793 -.606) scale(1.44332)"
/>
<path
style={{
fill: "#3f3f42",
fillOpacity: 1,
fillRule: "nonzero",
stroke: "none"
}} d="M102.36 5.734c-4.079-4.058-10.688-4.058-14.766 0-3.254 3.235-3.903 8.075-1.965 11.961l-22.746 22.64c-3.906-1.925-8.77-1.28-12.024 1.958-4.078 4.059-4.074 10.64 0 14.7 4.082 4.058 10.692 4.054 14.77 0a10.356 10.356 0 0 0 1.965-11.966l22.746-22.64c3.906 1.93 8.765 1.281 12.02-1.957a10.355 10.355 0 0 0 0-14.696zm0 0" transform="translate(-26.793 -.606) scale(1.44332)"
/>
</svg>
);
};
export default OpenApiLogo;

View File

@@ -1,7 +1,7 @@
import React from 'react';
import { Tooltip as ReactInfoTip } from 'react-tooltip';
const InfoTip = ({ html: _ignored, infotipId, ...props }) => {
const InfoTip = ({ text, infotipId }) => {
return (
<>
<svg
@@ -17,7 +17,7 @@ const InfoTip = ({ html: _ignored, infotipId, ...props }) => {
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z" />
<path d="M5.255 5.786a.237.237 0 0 0 .241.247h.825c.138 0 .248-.113.266-.25.09-.656.54-1.134 1.342-1.134.686 0 1.314.343 1.314 1.168 0 .635-.374.927-.965 1.371-.673.489-1.206 1.06-1.168 1.987l.003.217a.25.25 0 0 0 .25.246h.811a.25.25 0 0 0 .25-.25v-.105c0-.718.273-.927 1.01-1.486.609-.463 1.244-.977 1.244-2.056 0-1.511-1.276-2.241-2.673-2.241-1.267 0-2.655.59-2.75 2.286zm1.557 5.763c0 .533.425.927 1.01.927.609 0 1.028-.394 1.028-.927 0-.552-.42-.94-1.029-.94-.584 0-1.009.388-1.009.94z" />
</svg>
<ReactInfoTip anchorId={infotipId} {...props} />
<ReactInfoTip anchorId={infotipId} html={text} />
</>
);
};

View File

@@ -15,14 +15,14 @@ const StyledMarkdownBodyWrapper = styled.div`
margin: 0.67em 0;
font-weight: var(--base-text-weight-semibold, 600);
padding-bottom: 0.3em;
font-size: 1.3em;
font-size: 1.3;
border-bottom: 1px solid var(--color-border-muted);
}
h2 {
font-weight: var(--base-text-weight-semibold, 600);
padding-bottom: 0.3em;
font-size: 1.2em;
font-size: 1.2;
border-bottom: 1px solid var(--color-border-muted);
}

View File

@@ -16,7 +16,7 @@ const ModalHeader = ({ title, handleCancel, customHeader, hideClose }) => (
</div>
);
const ModalContent = ({ children }) => <div className="bruno-modal-content px-4 py-4">{children}</div>;
const ModalContent = ({ children }) => <div className="bruno-modal-content px-4 py-6">{children}</div>;
const ModalFooter = ({
confirmText,

View File

@@ -3,7 +3,6 @@ import { useState } from 'react';
import StyledWrapper from './StyleWrapper';
import Modal from 'components/Modal/index';
import { useEffect } from 'react';
import { useApp } from 'providers/App';
import {
fetchNotifications,
markAllNotificationsAsRead,
@@ -12,18 +11,18 @@ import {
import { useDispatch, useSelector } from 'react-redux';
import { humanizeDate, relativeDate } from 'utils/common';
import ToolHint from 'components/ToolHint';
import DOMPurify from 'dompurify';
import { useTheme } from 'providers/Theme';
const PAGE_SIZE = 5;
const Notifications = () => {
const dispatch = useDispatch();
const { version } = useApp();
const notifications = useSelector((state) => state.notifications.notifications);
const [showNotificationsModal, setShowNotificationsModal] = useState(false);
const [selectedNotification, setSelectedNotification] = useState(null);
const [pageNumber, setPageNumber] = useState(1);
const { storedTheme } = useTheme();
const notificationsStartIndex = (pageNumber - 1) * PAGE_SIZE;
const notificationsEndIndex = pageNumber * PAGE_SIZE;
@@ -31,9 +30,7 @@ const Notifications = () => {
const unreadNotifications = notifications.filter((notification) => !notification.read);
useEffect(() => {
dispatch(fetchNotifications({
currentVersion: version
}));
dispatch(fetchNotifications());
}, []);
useEffect(() => {
@@ -69,13 +66,6 @@ const Notifications = () => {
dispatch(markNotificationAsRead({ notificationId: notification?.id }));
};
const getSanitizedDescription = (description) => {
return DOMPurify.sanitize(encodeURIComponent(description), {
ALLOWED_TAGS: ['a', 'ul', 'img', 'li', 'div', 'span', 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'],
ALLOWED_ATTR: ['href', 'style', 'target', 'src', 'alt']
});
};
const modalCustomHeader = (
<div className="flex flex-row gap-8">
<div>NOTIFICATIONS</div>
@@ -100,9 +90,7 @@ const Notifications = () => {
<a
className="relative cursor-pointer"
onClick={() => {
dispatch(fetchNotifications({
currentVersion: version
}));
dispatch(fetchNotifications());
setShowNotificationsModal(true);
}}
aria-label="Check all Notifications"
@@ -191,11 +179,10 @@ const Notifications = () => {
<div className="w-full notification-date text-xs mb-4">
{humanizeDate(selectedNotification?.date)}
</div>
<iframe
src={`data:text/html,${getSanitizedDescription(selectedNotification?.description)}`}
sandbox="allow-popups"
style={{ width: '100%', height: '100%' }}
></iframe>
<div
className="flex w-full flex-col flex-wrap h-fit"
dangerouslySetInnerHTML={{ __html: selectedNotification?.description }}
></div>
</div>
</div>
) : (

View File

@@ -1,39 +0,0 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
width: 100%;
.path-display {
background: ${(props) => props.theme.requestTabPanel.url.bg};
border-radius: 4px;
padding: 8px 12px;
font-size: 0.8125rem;
border: 1px solid rgba(0, 0, 0, 0.08);
.icon-column {
padding-right: 8px;
align-items: flex-start;
padding-top: 2px;
}
.path-container {
flex-wrap: wrap;
}
.path-segment {
white-space: nowrap;
}
.name-container, .file-extension {
color: ${(props) => props.theme.colors.text.yellow};
}
.separator {
color: ${(props) => props.theme.text};
opacity: 0.6;
margin: 0 2px;
}
}
`;
export default StyledWrapper;

View File

@@ -1,26 +0,0 @@
import React from 'react';
import { IconFolder, IconFile } from '@tabler/icons';
import path from 'utils/common/path';
import StyledWrapper from './StyledWrapper';
const PathDisplay = ({
baseName = '',
iconType = 'file'
}) => {
return (
<StyledWrapper>
<div className="path-display mt-2">
<div className="path-layout flex font-mono">
<div className="icon-column flex">
{iconType === 'file' ? <IconFile size={16} /> : <IconFolder size={16} />}
</div>
<span className="name-container">
{baseName}
</span>
</div>
</div>
</StyledWrapper>
);
};
export default PathDisplay;

View File

@@ -6,7 +6,8 @@ import { savePreferences } from 'providers/ReduxStore/slices/app';
import StyledWrapper from './StyledWrapper';
import * as Yup from 'yup';
import toast from 'react-hot-toast';
import path from 'utils/common/path';
import path from 'path';
import slash from 'utils/common/slash';
import { IconTrash } from '@tabler/icons';
const General = ({ close }) => {
@@ -133,7 +134,7 @@ const General = ({ close }) => {
className={`flex items-center mt-2 pl-6 ${formik.values.customCaCertificate.enabled ? '' : 'opacity-25'}`}
>
<span className="flex items-center border px-2 rounded-md">
{path.basename(formik.values.customCaCertificate.filePath)}
{path.basename(slash(formik.values.customCaCertificate.filePath))}
<button
type="button"
tabIndex="-1"

View File

@@ -46,7 +46,7 @@ const Preferences = ({ onClose }) => {
return (
<StyledWrapper>
<Modal size="lg" title="Preferences" handleCancel={onClose} hideFooter={true}>
<div className='flex flex-row gap-2 mx-[-1rem] !my-[-1.5rem] py-2'>
<div className='flex flex-row gap-2 mx-[-1rem] !my-[-1.5rem]'>
<div className="flex flex-col items-center tabs" role="tablist">
<div className={getTabClassname('general')} role="tab" onClick={() => setTab('general')}>
General

View File

@@ -1,65 +0,0 @@
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(2) {
width: 45%;
}
&:nth-child(3) {
width: 25%;
}
&:nth-child(4) {
width: 70px;
}
}
}
.btn-add-param {
font-size: 0.8125rem;
}
input[type='text'] {
width: 100%;
border: solid 1px transparent;
outline: none !important;
color: ${(props) => props.theme.table.input.color};
background: transparent;
&:focus {
outline: none !important;
border: solid 1px transparent;
}
}
input[type='radio'] {
cursor: pointer;
position: relative;
top: 1px;
}
`;
export default Wrapper;

View File

@@ -1,164 +0,0 @@
import React, { useState, useEffect } from 'react';
import { get, cloneDeep, isArray } from 'lodash';
import { IconTrash } from '@tabler/icons';
import { useDispatch } from 'react-redux';
import { useTheme } from 'providers/Theme';
import { addFile as _addFile, updateFile, deleteFile } from 'providers/ReduxStore/slices/collections/index';
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';
import FilePickerEditor from 'components/FilePickerEditor/index';
import SingleLineEditor from 'components/SingleLineEditor/index';
const FileBody = ({ item, collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const params = item.draft ? get(item, 'draft.request.body.file') : get(item, 'request.body.file');
const [enabledFileUid, setEnableFileUid] = useState(params && params.length ? params[0].uid : '');
const addFile = () => {
dispatch(
_addFile({
itemUid: item.uid,
collectionUid: collection.uid,
})
);
};
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
const handleRun = () => dispatch(sendRequest(item, collection.uid));
const handleParamChange = (e, _param, type) => {
const param = cloneDeep(_param);
switch (type) {
case 'filePath': {
param.filePath = e.target.filePath;
param.contentType = "";
break;
}
case 'contentType': {
param.contentType = e.target.contentType;
break;
}
case 'selected': {
param.selected = e.target.selected;
setEnableFileUid(param.uid)
break;
}
}
dispatch(
updateFile({
param: param,
itemUid: item.uid,
collectionUid: collection.uid
})
);
};
const handleRemoveParams = (param) => {
dispatch(
deleteFile({
paramUid: param.uid,
itemUid: item.uid,
collectionUid: collection.uid
})
);
};
return (
<StyledWrapper className="w-full">
<table>
<thead>
<tr>
<td>
<div className="flex items-center justify-center">File</div>
</td>
<td>
<div className="flex items-center justify-center">Content-Type</div>
</td>
<td>
<div className="flex items-center justify-center">Selected</div>
</td>
<td></td>
</tr>
</thead>
<tbody>
{params && params.length
? params.map((param, index) => {
return (
<tr key={param.uid}>
<td>
<FilePickerEditor
isSingleFilePicker={true}
value={param.filePath}
onChange={(path) =>
handleParamChange(
{
target: {
filePath: path
}
},
param,
'filePath'
)
}
collection={collection}
/>
</td>
<td>
<SingleLineEditor
className="flex items-center justify-center"
onSave={onSave}
theme={storedTheme}
placeholder="Auto"
value={param.contentType}
onChange={(newValue) =>
handleParamChange(
{
target: {
contentType: newValue
}
},
param,
'contentType'
)
}
onRun={handleRun}
collection={collection}
/>
</td>
<td>
<div className="flex items-center justify-center">
<input
key={param.uid}
type="radio"
name="selected"
checked={enabledFileUid === param.uid || param.selected}
tabIndex="-1"
className="mr-1 mousetrap"
onChange={(e) => handleParamChange(e, param, 'selected')}
/>
</div>
</td>
<td>
<div className="flex items-center justify-center">
<button tabIndex="-1" onClick={() => handleRemoveParams(param)}>
<IconTrash strokeWidth={1.5} size={20} />
</button>
</div>
</td>
</tr>
);
})
: null}
</tbody>
</table>
<div>
<button className="btn-add-param text-link pr-2 pt-3 select-none" onClick={addFile}>
+ Add File
</button>
</div>
</StyledWrapper>
);
};
export default FileBody;

View File

@@ -154,7 +154,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 flex-1 relative">{getTabPanel(focusedTab.requestPaneTab)}</section>
<section className="flex w-full mt-5 flex-1">{getTabPanel(focusedTab.requestPaneTab)}</section>
</StyledWrapper>
);
};

View File

@@ -49,7 +49,7 @@ const GraphQLVariables = ({ variables, item, collection }) => {
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
return (
<>
<StyledWrapper className="w-full relative">
<button
className="btn-add-param text-link px-4 py-4 select-none absolute top-0 right-0 z-10"
onClick={onPrettify}
@@ -68,7 +68,7 @@ const GraphQLVariables = ({ variables, item, collection }) => {
onRun={onRun}
onSave={onSave}
/>
</>
</StyledWrapper>
);
};

View File

@@ -15,7 +15,6 @@ import Tests from 'components/RequestPane/Tests';
import StyledWrapper from './StyledWrapper';
import { find, get } from 'lodash';
import Documentation from 'components/Documentation/index';
import { useEffect } from 'react';
const ContentIndicator = () => {
return (
@@ -25,14 +24,6 @@ const ContentIndicator = () => {
);
};
const ErrorIndicator = () => {
return (
<sup className="ml-[.125rem] opacity-80 font-medium text-red-500">
<DotIcon width="10" ></DotIcon>
</sup>
);
};
const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
const dispatch = useDispatch();
const tabs = useSelector((state) => state.tabs.tabs);
@@ -120,12 +111,6 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
requestVars.filter((request) => request.enabled).length +
responseVars.filter((response) => response.enabled).length;
useEffect(() => {
if (activeParamsLength === 0 && body.mode !== 'none') {
selectTab('body');
}
}, []);
return (
<StyledWrapper className="flex flex-col h-full relative">
<div className="flex flex-wrap items-center tabs" role="tablist">
@@ -151,11 +136,7 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
</div>
<div className={getTabClassname('script')} role="tab" onClick={() => selectTab('script')}>
Script
{(script.req || script.res) && (
item.preScriptResponseErrorMessage || item.postResponseScriptErrorMessage ?
<ErrorIndicator /> :
<ContentIndicator />
)}
{(script.req || script.res) && <ContentIndicator />}
</div>
<div className={getTabClassname('assert')} role="tab" onClick={() => selectTab('assert')}>
Assert

View File

@@ -109,7 +109,7 @@ export default class QueryEditor extends React.Component {
this.props.onPrettifyQuery();
}
},
/* Shift-Ctrl-P is hard coded in Firefox for private browsing so adding an alternative to Prettify */
/* Shift-Ctrl-P is hard coded in Firefox for private browsing so adding an alternative to Pretiffy */
'Shift-Ctrl-F': () => {
if (this.props.onPrettifyQuery) {
this.props.onPrettifyQuery();

View File

@@ -176,7 +176,8 @@ const QueryParams = ({ item, collection }) => {
</button>
<div className="mb-2 title text-xs flex items-stretch">
<span>Path</span>
<InfoTip infotipId="path-param-InfoTip">
<InfoTip
text={`
<div>
Path variables are automatically added whenever the
<code className="font-mono mx-2">:name</code>
@@ -185,7 +186,9 @@ const QueryParams = ({ item, collection }) => {
https://example.com/v1/users/<span>:id</span>
</code>
</div>
</InfoTip>
`}
infotipId="path-param-InfoTip"
/>
</div>
<table>
<thead>

View File

@@ -128,15 +128,6 @@ const RequestBodyMode = ({ item, collection }) => {
SPARQL
</div>
<div className="label-item font-medium">Other</div>
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
onModeChange('file');
}}
>
File / Binary
</div>
<div
className="dropdown-item"
onClick={() => {

View File

@@ -8,7 +8,6 @@ import { useTheme } from 'providers/Theme';
import { updateRequestBody } from 'providers/ReduxStore/slices/collections';
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';
import FileBody from '../FileBody/index';
const RequestBody = ({ item, collection }) => {
const dispatch = useDispatch();
@@ -63,10 +62,6 @@ const RequestBody = ({ item, collection }) => {
);
}
if (bodyMode === 'file') {
return <FileBody item={item} collection={collection}/>
}
if (bodyMode === 'formUrlEncoded') {
return <FormUrlEncodedParams item={item} collection={collection} />;
}
@@ -77,4 +72,4 @@ const RequestBody = ({ item, collection }) => {
return <StyledWrapper className="w-full">No Body</StyledWrapper>;
};
export default RequestBody;
export default RequestBody;

View File

@@ -0,0 +1,10 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
div.CodeMirror {
/* todo: find a better way */
height: calc(100vh - 220px);
}
`;
export default StyledWrapper;

View File

@@ -5,6 +5,7 @@ import CodeEditor from 'components/CodeEditor';
import { updateRequestTests } from 'providers/ReduxStore/slices/collections';
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import { useTheme } from 'providers/Theme';
import StyledWrapper from './StyledWrapper';
const Tests = ({ item, collection }) => {
const dispatch = useDispatch();
@@ -27,17 +28,19 @@ const Tests = ({ item, collection }) => {
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
return (
<CodeEditor
collection={collection}
value={tests || ''}
theme={displayedTheme}
font={get(preferences, 'font.codeFont', 'default')}
fontSize={get(preferences, 'font.codeFontSize')}
onEdit={onEdit}
mode="javascript"
onRun={onRun}
onSave={onSave}
/>
<StyledWrapper className="w-full">
<CodeEditor
collection={collection}
value={tests || ''}
theme={displayedTheme}
font={get(preferences, 'font.codeFont', 'default')}
fontSize={get(preferences, 'font.codeFontSize')}
onEdit={onEdit}
mode="javascript"
onRun={onRun}
onSave={onSave}
/>
</StyledWrapper>
);
};

View File

@@ -98,7 +98,7 @@ const VarsTable = ({ item, collection, vars, varType }) => {
) : (
<div className="flex items-center">
<span>Expr</span>
<InfoTip content="You can write any valid JS expression here" infotipId="response-var" />
<InfoTip text="You can write any valid JS expression here" infotipId="response-var" />
</div>
), accessor: 'value', width: '46%' },
{ name: '', accessor: '', width: '14%' }

View File

@@ -1,4 +1,4 @@
import { IconLoader2, IconFile, IconAlertTriangle } from '@tabler/icons';
import { IconLoader2, IconFile } from '@tabler/icons';
import { loadRequest, loadRequestViaWorker } from 'providers/ReduxStore/slices/collections/actions';
import { useDispatch } from 'react-redux';
import StyledWrapper from './StyledWrapper';
@@ -15,59 +15,65 @@ const RequestNotLoaded = ({ collection, item }) => {
return <StyledWrapper>
<div className='flex flex-col p-4'>
<div className='card shadow-sm rounded-md p-4 w-[685px]'>
<div className='card shadow-sm rounded-md p-4 w-[600px]'>
<div>
<div className='font-medium flex items-center gap-2 pb-4'>
<IconFile size={16} strokeWidth={1.5} className="text-gray-400" />
File Info
</div>
<div className='hr' />
<div className='hr'/>
<div className='flex items-center mt-2'>
<span className='w-12 mr-2 text-muted'>Name:</span>
<div>{item?.name}</div>
</div>
<div className='flex items-center mt-1'>
<span className='w-12 mr-2 text-muted'>Path:</span>
<div className='break-all'>{item?.pathname}</div>
</div>
<div className='flex items-center mt-1 pb-4'>
<span className='w-12 mr-2 text-muted'>Size:</span>
<div>{item?.size?.toFixed?.(2)} MB</div>
</div>
{!item?.error && (
<div className='flex flex-col'>
<div className='flex items-center gap-2 px-3 py-2 title bg-yellow-50 dark:bg-yellow-900/20'>
<IconAlertTriangle size={16} className="text-yellow-500" />
<span>The request wasn't loaded due to its large size. Please try again with the following options:</span>
<>
<div className='hr'/>
<div className='text-muted text-xs mt-4 mb-2'>
Due to its large size, this request wasn't loaded automatically.
</div>
<div className='flex flex-row mt-6 gap-2 items-center w-full'>
<button
className={`submit btn btn-sm btn-secondary w-fit h-fit flex flex-row gap-2 ${item?.loading? 'opacity-50 cursor-blocked': ''}`}
onClick={handleLoadRequestViaWorker}
>
Load in background
</button>
<p>(Runs in background)</p>
</div>
<div className='flex flex-row mt-6 items-center gap-2 w-full'>
<div className='flex flex-col gap-6 mt-4'>
<div className='flex flex-col'>
<button
className={`submit btn btn-sm btn-secondary w-fit h-fit flex flex-row gap-2 ${item?.loading? 'opacity-50 cursor-blocked': ''}`}
onClick={handleLoadRequest}
>
Force load
Load Request
</button>
<p>(May cause the app to freeze temporarily while it runs)</p>
<small className='text-muted mt-1'>
May cause the app to freeze temporarily while it runs.
</small>
</div>
<div className='flex flex-col'>
<button
className={`submit btn btn-sm btn-secondary w-fit h-fit flex flex-row gap-2 ${item?.loading? 'opacity-50 cursor-blocked': ''}`}
onClick={handleLoadRequestViaWorker}
>
Load Request in Background
</button>
<small className='text-muted mt-1'>
Runs in background.
</small>
</div>
</div>
</div>
</>
)}
{item?.loading && (
<>
<div className='hr mt-4' />
<div className='hr mt-4'/>
<div className='flex items-center gap-2 mt-4'>
<IconLoader2 className="animate-spin" size={16} strokeWidth={2} />
<span>Loading...</span>

View File

@@ -35,7 +35,7 @@ const CollectionToolBar = ({ collection }) => {
const viewCollectionSettings = () => {
dispatch(
addTab({
uid: collection.uid,
uid: uuid(),
collectionUid: collection.uid,
type: 'collection-settings'
})

View File

@@ -2,15 +2,15 @@ import React from 'react';
import CloseTabIcon from './CloseTabIcon';
import { IconVariable, IconSettings, IconRun, IconFolder, IconShieldLock } from '@tabler/icons';
const SpecialTab = ({ handleCloseClick, type, tabName, handleDoubleClick }) => {
const SpecialTab = ({ handleCloseClick, type, tabName }) => {
const getTabInfo = (type, tabName) => {
switch (type) {
case 'collection-settings': {
return (
<div onDoubleClick={handleDoubleClick} className="flex items-center flex-nowrap overflow-hidden">
<>
<IconSettings size={18} strokeWidth={1.5} className="text-yellow-600" />
<span className="ml-1 leading-6">Collection</span>
</div>
</>
);
}
case 'collection-overview': {
@@ -31,7 +31,7 @@ const SpecialTab = ({ handleCloseClick, type, tabName, handleDoubleClick }) => {
}
case 'folder-settings': {
return (
<div onDoubleClick={handleDoubleClick} className="flex items-center flex-nowrap overflow-hidden">
<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>

View File

@@ -1,6 +1,6 @@
import React, { useState, useRef, Fragment } from 'react';
import get from 'lodash/get';
import { closeTabs, makeTabPermanent } from 'providers/ReduxStore/slices/tabs';
import { closeTabs } from 'providers/ReduxStore/slices/tabs';
import { saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import { deleteRequestDraft } from 'providers/ReduxStore/slices/collections';
import { useTheme } from 'providers/Theme';
@@ -73,13 +73,13 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi
if (['collection-settings', 'collection-overview', 'folder-settings', 'variables', 'collection-runner', 'security-settings'].includes(tab.type)) {
return (
<StyledWrapper
className={`flex items-center justify-between tab-container px-1 ${tab.preview ? "italic" : ""}`}
className="flex items-center justify-between tab-container px-1"
onMouseUp={handleMouseUp} // Add middle-click behavior here
>
{tab.type === 'folder-settings' ? (
<SpecialTab handleCloseClick={handleCloseClick} handleDoubleClick={() => dispatch(makeTabPermanent({ uid: tab.uid }))} type={tab.type} tabName={folder?.name} />
<SpecialTab handleCloseClick={handleCloseClick} type={tab.type} tabName={folder?.name} />
) : (
<SpecialTab handleCloseClick={handleCloseClick} handleDoubleClick={() => dispatch(makeTabPermanent({ uid: tab.uid }))} type={tab.type} />
<SpecialTab handleCloseClick={handleCloseClick} type={tab.type} />
)}
</StyledWrapper>
);
@@ -144,9 +144,8 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi
/>
)}
<div
className={`flex items-baseline tab-label pl-2 ${tab.preview ? "italic" : ""}`}
className="flex items-baseline tab-label pl-2"
onContextMenu={handleRightClick}
onDoubleClick={() => dispatch(makeTabPermanent({ uid: tab.uid }))}
onMouseUp={(e) => {
if (!item.draft) return handleMouseUp(e);

View File

@@ -3,49 +3,48 @@ import QueryResultFilter from './QueryResultFilter';
import { JSONPath } from 'jsonpath-plus';
import React from 'react';
import classnames from 'classnames';
import iconv from 'iconv-lite';
import { getContentType, safeStringifyJSON, safeParseXML } from 'utils/common';
import { getCodeMirrorModeBasedOnContentType } from 'utils/common/codemirror';
import QueryResultPreview from './QueryResultPreview';
import StyledWrapper from './StyledWrapper';
import { useState, useMemo, useEffect } from 'react';
import { useTheme } from 'providers/Theme/index';
import { getEncoding, prettifyJson, uuid } from 'utils/common/index';
const formatResponse = (data, dataBuffer, encoding, mode, filter) => {
if (data === undefined || !dataBuffer) {
import StyledWrapper from './StyledWrapper';
import { useState } from 'react';
import { useMemo } from 'react';
import { useEffect } from 'react';
import { useTheme } from 'providers/Theme/index';
import { uuid } from 'utils/common/index';
const formatResponse = (data, mode, filter) => {
if (data === undefined) {
return '';
}
// TODO: We need a better way to get the raw response-data here instead
// of using this dataBuffer param.
// Also, we only need the raw response-data and content-type to show the preview.
const rawData = iconv.decode(
Buffer.from(dataBuffer, "base64"),
iconv.encodingExists(encoding) ? encoding : "utf-8"
);
if (data === null) {
return 'null';
}
if (mode.includes('json')) {
let isValidJSON = false;
try {
JSON.parse(rawData);
isValidJSON = typeof JSON.parse(JSON.stringify(data)) === 'object'
} catch (error) {
// If the response content-type is JSON and it fails parsing, its an invalid JSON.
// In that case, just show the response as it is in the preview.
return rawData;
console.log('Error parsing JSON: ', error.message);
}
if (!isValidJSON && typeof data === 'string') {
return data;
}
if (filter) {
try {
data = JSONPath({ path: filter, json: data });
return prettifyJson(JSON.stringify(data));
} catch (e) {
console.warn('Could not apply JSONPath filter:', e.message);
}
}
// Prettify the JSON string directly instead of parse->stringify to avoid
// issues like rounding numbers bigger than Number.MAX_SAFE_INTEGER etc.
return prettifyJson(rawData);
return safeStringifyJSON(data, true);
}
if (mode.includes('xml')) {
@@ -60,27 +59,14 @@ const formatResponse = (data, dataBuffer, encoding, mode, filter) => {
return data;
}
return prettifyJson(rawData);
};
const formatErrorMessage = (error) => {
if (!error) return 'Something went wrong';
const remoteMethodError = "Error invoking remote method 'send-http-request':";
if (error.includes(remoteMethodError)) {
const parts = error.split(remoteMethodError);
return parts[1]?.trim() || error;
}
return error;
return safeStringifyJSON(data, true);
};
const QueryResult = ({ item, collection, data, dataBuffer, width, disableRunEventListener, headers, error }) => {
const contentType = getContentType(headers);
const mode = getCodeMirrorModeBasedOnContentType(contentType, data);
const [filter, setFilter] = useState(null);
const formattedData = formatResponse(data, dataBuffer, getEncoding(headers), mode, filter);
const formattedData = formatResponse(data, mode, filter);
const { displayedTheme } = useTheme();
const debouncedResultFilterOnChange = debounce((e) => {
@@ -135,7 +121,6 @@ const QueryResult = ({ item, collection, data, dataBuffer, width, disableRunEven
}, [allowedPreviewModes, previewTab]);
const queryFilterEnabled = useMemo(() => mode.includes('json'), [mode]);
const hasScriptError = item.preRequestScriptErrorMessage || item.postResponseScriptErrorMessage;
return (
<StyledWrapper
@@ -148,7 +133,7 @@ const QueryResult = ({ item, collection, data, dataBuffer, width, disableRunEven
</div>
{error ? (
<div>
{hasScriptError ? null : <div className="text-red-500">{formatErrorMessage(error)}</div>}
<div className="text-red-500">{error}</div>
{error && typeof error === 'string' && error.toLowerCase().includes('self signed certificate') ? (
<div className="mt-6 muted text-xs">
@@ -158,26 +143,24 @@ const QueryResult = ({ item, collection, data, dataBuffer, width, disableRunEven
) : null}
</div>
) : (
<div className="h-full flex flex-col">
<div className="flex-1 relative">
<QueryResultPreview
previewTab={previewTab}
data={data}
dataBuffer={dataBuffer}
formattedData={formattedData}
item={item}
contentType={contentType}
mode={mode}
collection={collection}
allowedPreviewModes={allowedPreviewModes}
disableRunEventListener={disableRunEventListener}
displayedTheme={displayedTheme}
/>
{queryFilterEnabled && (
<QueryResultFilter filter={filter} onChange={debouncedResultFilterOnChange} mode={mode} />
)}
</div>
</div>
<>
<QueryResultPreview
previewTab={previewTab}
data={data}
dataBuffer={dataBuffer}
formattedData={formattedData}
item={item}
contentType={contentType}
mode={mode}
collection={collection}
allowedPreviewModes={allowedPreviewModes}
disableRunEventListener={disableRunEventListener}
displayedTheme={displayedTheme}
/>
{queryFilterEnabled && (
<QueryResultFilter filter={filter} onChange={debouncedResultFilterOnChange} mode={mode} />
)}
</>
)}
</StyledWrapper>
);

View File

@@ -1,55 +0,0 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
border-left: 4px solid ${(props) => props.theme.colors.text.danger};
border-top: 1px solid transparent;
border-right: 1px solid transparent;
border-bottom: 1px solid transparent;
border-radius: 0.375rem;
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
max-height: 200px;
min-height: 70px;
overflow-y: auto;
background-color: ${(props) => props.theme.bg === '#1e1e1e' ? 'rgba(40, 40, 40, 0.5)' : 'rgba(250, 250, 250, 0.9)'};
.error-icon-container {
margin-top: 0.125rem;
padding: 0.375rem;
border-radius: 9999px;
background-color: ${(props) => props.theme.bg === '#1e1e1e' ? 'rgba(40, 40, 40, 0.8)' : 'rgba(240, 240, 240, 0.8)'};
svg {
color: ${(props) => props.theme.colors.text.danger};
}
}
.close-button {
opacity: 0.7;
transition: opacity 0.2s;
&:hover {
opacity: 1;
}
svg {
color: ${(props) => props.theme.text};
}
}
.error-title {
font-weight: 600;
margin-bottom: 0.375rem;
color: ${(props) => props.theme.colors.text.danger};
}
.error-message {
font-family: monospace;
font-size: 0.6875rem;
line-height: 1.25rem;
white-space: pre-wrap;
word-break: break-all;
color: ${(props) => props.theme.text};
}
`;
export default StyledWrapper;

View File

@@ -1,37 +0,0 @@
import React from 'react';
import { IconX } from '@tabler/icons';
import StyledWrapper from './StyledWrapper';
const ScriptError = ({ item, onClose }) => {
const preRequestError = item?.preRequestScriptErrorMessage;
const postResponseError = item?.postResponseScriptErrorMessage;
if (!preRequestError && !postResponseError) return null;
const errorMessage = preRequestError || postResponseError;
const errorTitle = preRequestError ? 'Pre-Request Script Error' : 'Post-Response Script Error';
return (
<StyledWrapper className="mt-4 mb-2">
<div className="flex items-start gap-3 px-4 py-3">
<div className="flex-1 min-w-0">
<div className="error-title">
{errorTitle}
</div>
<div className="error-message">
{errorMessage}
</div>
</div>
<div
className="close-button flex-shrink-0 cursor-pointer"
onClick={onClose}
>
<IconX size={16} strokeWidth={1.5} />
</div>
</div>
</StyledWrapper>
);
};
export default ScriptError;

View File

@@ -1,28 +0,0 @@
import React from 'react';
import { IconAlertCircle } from '@tabler/icons';
import ToolHint from 'components/ToolHint';
const ScriptErrorIcon = ({ itemUid, onClick }) => {
const toolhintId = `script-error-icon-${itemUid}`;
return (
<>
<div
id={toolhintId}
className="cursor-pointer ml-2"
onClick={onClick}
>
<div className="flex items-center text-red-400">
<IconAlertCircle size={16} strokeWidth={1.5} className="stroke-current" />
</div>
</div>
<ToolHint
toolhintId={toolhintId}
text="Script execution error occurred"
place="bottom"
/>
</>
);
};
export default ScriptErrorIcon;

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react';
import React from 'react';
import find from 'lodash/find';
import classnames from 'classnames';
import { useDispatch, useSelector } from 'react-redux';
@@ -13,8 +13,6 @@ import ResponseSize from './ResponseSize';
import Timeline from './Timeline';
import TestResults from './TestResults';
import TestResultsLabel from './TestResultsLabel';
import ScriptError from './ScriptError';
import ScriptErrorIcon from './ScriptErrorIcon';
import StyledWrapper from './StyledWrapper';
import ResponseSave from 'src/components/ResponsePane/ResponseSave';
import ResponseClear from 'src/components/ResponsePane/ResponseClear';
@@ -24,13 +22,6 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
const tabs = useSelector((state) => state.tabs.tabs);
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
const isLoading = ['queued', 'sending'].includes(item.requestState);
const [showScriptErrorCard, setShowScriptErrorCard] = useState(false);
useEffect(() => {
if (item?.preRequestScriptErrorMessage || item?.postResponseScriptErrorMessage) {
setShowScriptErrorCard(true);
}
}, [item?.preRequestScriptErrorMessage, item?.postResponseScriptErrorMessage]);
const selectTab = (tab) => {
dispatch(
@@ -107,8 +98,6 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
};
const responseHeadersCount = typeof response.headers === 'object' ? Object.entries(response.headers).length : 0;
const hasScriptError = item?.preRequestScriptErrorMessage || item?.postResponseScriptErrorMessage;
return (
<StyledWrapper className="flex flex-col h-full relative">
@@ -128,12 +117,6 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
</div>
{!isLoading ? (
<div className="flex flex-grow justify-end items-center">
{hasScriptError && !showScriptErrorCard && (
<ScriptErrorIcon
itemUid={item.uid}
onClick={() => setShowScriptErrorCard(true)}
/>
)}
<ResponseClear item={item} collection={collection} />
<ResponseSave item={item} />
<StatusCode status={response.status} />
@@ -143,15 +126,9 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
) : null}
</div>
<section
className={`flex flex-col flex-grow relative pl-3 pr-4 ${focusedTab.responsePaneTab === 'response' ? '' : 'mt-4'}`}
className={`flex flex-grow relative pl-3 pr-4 ${focusedTab.responsePaneTab === 'response' ? '' : 'mt-4'}`}
>
{isLoading ? <Overlay item={item} collection={collection} /> : null}
{hasScriptError && showScriptErrorCard && (
<ScriptError
item={item}
onClose={() => setShowScriptErrorCard(false)}
/>
)}
{getTabPanel(focusedTab.responsePaneTab)}
</section>
</StyledWrapper>

View File

@@ -1,18 +1,23 @@
import React, { useState, useRef, useEffect } from 'react';
import path from 'utils/common/path';
import path from 'path';
import { useDispatch } from 'react-redux';
import { get, cloneDeep } from 'lodash';
import { runCollectionFolder, cancelRunnerExecution } from 'providers/ReduxStore/slices/collections/actions';
import { resetCollectionRunner } from 'providers/ReduxStore/slices/collections';
import { findItemInCollection, getTotalRequestCountInCollection } from 'utils/collections';
import { IconRefresh, IconCircleCheck, IconCircleX, IconCheck, IconX, IconRun } from '@tabler/icons';
import slash from 'utils/common/slash';
import ResponsePane from './ResponsePane';
import StyledWrapper from './StyledWrapper';
import { areItemsLoading } from 'utils/collections';
const getDisplayName = (fullPath, pathname, name = '') => {
const getRelativePath = (fullPath, pathname) => {
// convert to unix style path
fullPath = slash(fullPath);
pathname = slash(pathname);
let relativePath = path.relative(fullPath, pathname);
const { dir = '' } = path.parse(relativePath);
const { dir, name } = path.parse(relativePath);
return path.join(dir, name);
};
@@ -53,7 +58,7 @@ export default function RunnerResults({ collection }) {
type: info.type,
filename: info.filename,
pathname: info.pathname,
displayName: getDisplayName(collection.pathname, info.pathname, info.name)
relativePath: getRelativePath(collection.pathname, info.pathname)
};
if (newItem.status !== 'error' && newItem.status !== 'skipped') {
if (newItem.testResults) {
@@ -181,7 +186,7 @@ export default function RunnerResults({ collection }) {
<span
className={`mr-1 ml-2 ${item.status == 'error' || item.status == 'skipped' || item.testStatus == 'fail' ? 'danger' : ''}`}
>
{item.displayName}
{item.relativePath}
</span>
{item.status !== 'error' && item.status !== 'skipped' && item.status !== 'completed' ? (
<IconRefresh className="animate-spin ml-1" size={18} strokeWidth={1.5} />
@@ -261,7 +266,7 @@ export default function RunnerResults({ collection }) {
<div className="flex flex-1 w-[50%]">
<div className="flex flex-col w-full overflow-auto">
<div className="flex items-center px-3 mb-4 font-medium">
<span className="mr-2">{selectedItem.displayName}</span>
<span className="mr-2">{selectedItem.relativePath}</span>
<span>
{selectedItem.testStatus === 'pass' ? (
<IconCircleCheck className="test-success" size={20} strokeWidth={1.5} />
@@ -270,6 +275,7 @@ export default function RunnerResults({ collection }) {
)}
</span>
</div>
{/* <div className='px-3 mb-4 font-medium'>{selectedItem.relativePath}</div> */}
<ResponsePane item={selectedItem} collection={collection} />
</div>
</div>

View File

@@ -1,30 +0,0 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
.tabs {
.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;
}
}
}
`;
export default StyledWrapper;

View File

@@ -1,60 +0,0 @@
import React from 'react';
import Modal from 'components/Modal';
import { IconDownload } from '@tabler/icons';
import StyledWrapper from './StyledWrapper';
import Bruno from 'components/Bruno';
import exportBrunoCollection from 'utils/collections/export';
import exportPostmanCollection from 'utils/exporters/postman-collection';
import { cloneDeep } from 'lodash';
import { transformCollectionToSaveToExportAsFile } from 'utils/collections/index';
const ShareCollection = ({ onClose, collection }) => {
const handleExportBrunoCollection = () => {
const collectionCopy = cloneDeep(collection);
exportBrunoCollection(transformCollectionToSaveToExportAsFile(collectionCopy));
onClose();
};
const handleExportPostmanCollection = () => {
const collectionCopy = cloneDeep(collection);
exportPostmanCollection(collectionCopy);
onClose();
};
return (
<Modal
size="md"
title="Share Collection"
confirmText="Close"
handleConfirm={onClose}
handleCancel={onClose}
hideCancel
>
<StyledWrapper className="flex flex-col h-full w-[500px]">
<div className="space-y-2">
<div className="flex border border-gray-200 dark:border-gray-600 hover:bg-gray-100 dark:hover:bg-gray-500/10 items-center p-3 rounded-lg transition-colors cursor-pointer" onClick={handleExportBrunoCollection}>
<div className="mr-3 p-1 rounded-full">
<Bruno width={28} />
</div>
<div className="flex-1">
<div className="font-medium">Bruno Collection</div>
<div className="text-xs">Export in Bruno format</div>
</div>
</div>
<div className="flex border border-gray-200 dark:border-gray-600 hover:bg-gray-100 dark:hover:bg-gray-500/10 items-center p-3 rounded-lg transition-colors cursor-pointer" onClick={handleExportPostmanCollection}>
<div className="mr-3 p-1 rounded-full">
<IconDownload size={28} strokeWidth={1} className="" />
</div>
<div className="flex-1">
<div className="font-medium">Postman Collection</div>
<div className="text-xs">Export in Postman format</div>
</div>
</div>
</div>
</StyledWrapper>
</Modal>
);
};
export default ShareCollection;

View File

@@ -5,38 +5,29 @@ import * as Yup from 'yup';
import { browseDirectory } from 'providers/ReduxStore/slices/collections/actions';
import { cloneCollection } from 'providers/ReduxStore/slices/collections/actions';
import toast from 'react-hot-toast';
import InfoTip from 'components/InfoTip';
import Modal from 'components/Modal';
import { sanitizeName, validateName, validateNameError } from 'utils/common/regex';
import Help from 'components/Help';
import PathDisplay from 'components/PathDisplay';
import { useState } from 'react';
import { IconArrowBackUp, IconEdit } from "@tabler/icons";
const CloneCollection = ({ onClose, collection }) => {
const inputRef = useRef();
const dispatch = useDispatch();
const [isEditing, toggleEditing] = useState(false);
const { name } = collection;
const formik = useFormik({
enableReinitialize: true,
initialValues: {
collectionName: `${name} copy`,
collectionFolderName: `${sanitizeName(name)} copy`,
collectionName: '',
collectionFolderName: '',
collectionLocation: ''
},
validationSchema: Yup.object({
collectionName: Yup.string()
.min(1, 'must be at least 1 character')
.max(255, 'must be 255 characters or less')
.max(50, 'must be 50 characters or less')
.required('collection name is required'),
collectionFolderName: Yup.string()
.min(1, 'must be at least 1 character')
.max(255, 'must be 255 characters or less')
.test('is-valid-collection-name', function(value) {
const isValid = validateName(value);
return isValid ? true : this.createError({ message: validateNameError(value) });
})
.max(50, 'must be 50 characters or less')
.matches(/^[\w\-. ]+$/, 'Folder name contains invalid characters')
.required('folder name is required'),
collectionLocation: Yup.string().min(1, 'location is required').required('location is required')
}),
@@ -60,7 +51,7 @@ const CloneCollection = ({ onClose, collection }) => {
const browse = () => {
dispatch(browseDirectory())
.then((dirPath) => {
// When the user closes the dialog without selecting anything dirPath will be false
// When the user closes the diolog without selecting anything dirPath will be false
if (typeof dirPath === 'string') {
formik.setFieldValue('collectionLocation', dirPath);
}
@@ -94,7 +85,9 @@ const CloneCollection = ({ onClose, collection }) => {
className="block textbox mt-2 w-full"
onChange={(e) => {
formik.handleChange(e);
!isEditing && formik.setFieldValue('collectionFolderName', sanitizeName(e.target.value));
if (formik.values.collectionName === formik.values.collectionFolderName) {
formik.setFieldValue('collectionFolderName', e.target.value);
}
}}
autoComplete="off"
autoCorrect="off"
@@ -126,70 +119,33 @@ const CloneCollection = ({ onClose, collection }) => {
<div className="text-red-500">{formik.errors.collectionLocation}</div>
) : null}
<div className="mt-1">
<span
className="text-link cursor-pointer hover:underline" onClick={browse}
style={{
fontSize: '0.8125rem'
}}
>
<span className="text-link cursor-pointer hover:underline" onClick={browse}>
Browse
</span>
</div>
<div className="mt-4">
<div className="flex items-center justify-between">
<label htmlFor="filename" className="flex items-center font-semibold">
Folder Name
<Help width="300">
<p>
The name of the folder used to store the collection.
</p>
<p className="mt-2">
You can choose a folder name different from your collection's name or one compatible with filesystem rules.
</p>
</Help>
</label>
{isEditing ? (
<IconArrowBackUp
className="cursor-pointer opacity-50 hover:opacity-80"
size={16}
strokeWidth={1.5}
onClick={() => toggleEditing(false)}
/>
) : (
<IconEdit
className="cursor-pointer opacity-50 hover:opacity-80"
size={16}
strokeWidth={1.5}
onClick={() => toggleEditing(true)}
/>
)}
</div>
{isEditing ? (
<input
id="collection-folder-name"
type="text"
name="collectionFolderName"
className="block textbox mt-2 w-full"
onChange={formik.handleChange}
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={formik.values.collectionFolderName || ''}
/>
) : (
<div className='relative flex flex-row gap-1 items-center justify-between'>
<PathDisplay
baseName={formik.values.collectionFolderName}
/>
</div>
)}
{formik.touched.collectionFolderName && formik.errors.collectionFolderName ? (
<div className="text-red-500">{formik.errors.collectionFolderName}</div>
) : null}
</div>
<label htmlFor="collection-folder-name" className="flex items-center mt-3">
<span className="font-semibold">Folder Name</span>
<InfoTip
text="This folder will be created under the selected location"
infotipId="collection-folder-name-infotip"
/>
</label>
<input
id="collection-folder-name"
type="text"
name="collectionFolderName"
className="block textbox mt-2 w-full"
onChange={formik.handleChange}
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={formik.values.collectionFolderName || ''}
/>
{formik.touched.collectionFolderName && formik.errors.collectionFolderName ? (
<div className="text-red-500">{formik.errors.collectionFolderName}</div>
) : null}
</div>
</form>
</Modal>

View File

@@ -1,12 +0,0 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
.advanced-options {
.caret {
color: ${(props) => props.theme.textLink};
fill: ${(props) => props.theme.textLink};
}
}
`;
export default StyledWrapper;

View File

@@ -1,4 +1,4 @@
import React, { useState, useRef, useEffect, forwardRef } from 'react';
import React, { useRef, useEffect } from 'react';
import toast from 'react-hot-toast';
import { useFormik } from 'formik';
import * as Yup from 'yup';
@@ -6,50 +6,24 @@ import Modal from 'components/Modal';
import { useDispatch } from 'react-redux';
import { isItemAFolder } from 'utils/tabs';
import { cloneItem } from 'providers/ReduxStore/slices/collections/actions';
import { IconArrowBackUp, IconEdit, IconCaretDown } from "@tabler/icons";
import { sanitizeName, validateName, validateNameError } from 'utils/common/regex';
import Help from 'components/Help';
import PathDisplay from 'components/PathDisplay/index';
import path from 'utils/common/path';
import Portal from 'components/Portal';
import Dropdown from 'components/Dropdown';
import StyledWrapper from './StyledWrapper';
const CloneCollectionItem = ({ collection, item, onClose }) => {
const dispatch = useDispatch();
const isFolder = isItemAFolder(item);
const inputRef = useRef();
const [isEditing, toggleEditing] = useState(false);
const itemName = item?.name;
const itemType = item?.type;
const [showFilesystemName, toggleShowFilesystemName] = useState(false);
const dropdownTippyRef = useRef();
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
const formik = useFormik({
enableReinitialize: true,
initialValues: {
name: `${itemName} copy`,
filename: `${sanitizeName(itemName)} copy`
name: item.name
},
validationSchema: Yup.object({
name: Yup.string()
.min(1, 'must be at least 1 character')
.max(255, 'must be 255 characters or less')
.required('name is required'),
filename: Yup.string()
.min(1, 'must be at least 1 character')
.max(255, 'must be 255 characters or less')
.max(50, 'must be 50 characters or less')
.required('name is required')
.test('is-valid-name', function(value) {
const isValid = validateName(value);
return isValid ? true : this.createError({ message: validateNameError(value) });
})
.test('not-reserved', `The file names "collection" and "folder" are reserved in bruno`, value => !['collection', 'folder'].includes(value))
}),
onSubmit: (values) => {
dispatch(cloneItem(values.name, values.filename, item.uid, collection.uid))
dispatch(cloneItem(values.name, item.uid, collection.uid))
.then(() => {
toast.success('Request cloned!');
onClose();
@@ -66,159 +40,39 @@ const CloneCollectionItem = ({ collection, item, onClose }) => {
}
}, [inputRef]);
const AdvancedOptions = forwardRef((props, ref) => {
return (
<div ref={ref} className="flex mr-2 text-link cursor-pointer items-center">
<button
className="btn-advanced"
type="button"
>
Options
</button>
<IconCaretDown className="caret ml-1" size={14} strokeWidth={2}/>
</div>
);
});
const onSubmit = () => formik.handleSubmit();
return (
<Portal>
<StyledWrapper>
<Modal
size="md"
title={`Clone ${isFolder ? 'Folder' : 'Request'}`}
handleCancel={onClose}
hideFooter
>
<form className="bruno-form" onSubmit={formik.handleSubmit}>
<div>
<label htmlFor="name" className="block font-semibold">
{isFolder ? 'Folder' : 'Request'} Name
</label>
<input
id="collection-item-name"
type="text"
name="name"
placeholder='Enter Item name'
ref={inputRef}
className="block textbox mt-2 w-full"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
onChange={e => {
formik.setFieldValue('name', e.target.value);
!isEditing && formik.setFieldValue('filename', sanitizeName(e.target.value));
}}
value={formik.values.name || ''}
/>
{formik.touched.name && formik.errors.name ? <div className="text-red-500">{formik.errors.name}</div> : null}
</div>
{showFilesystemName && (
<div className="mt-4">
<div className="flex items-center justify-between">
<label htmlFor="filename" className="flex items-center font-semibold">
{isFolder ? 'Folder' : 'File'} Name <small className='font-normal text-muted ml-1'>(on filesystem)</small>
{ isFolder ? (
<Help width="300">
<p>
You can choose to save the folder as a different name on your file system versus what is displayed in the app.
</p>
</Help>
) : (
<Help width="300">
<p>
Bruno saves each request as a file in your collection's folder.
</p>
<p className="mt-2">
You can choose a file name different from your request's name or one compatible with filesystem rules.
</p>
</Help>
)}
</label>
{isEditing ? (
<IconArrowBackUp
className="cursor-pointer opacity-50 hover:opacity-80"
size={16}
strokeWidth={1.5}
onClick={() => toggleEditing(false)}
/>
) : (
<IconEdit
className="cursor-pointer opacity-50 hover:opacity-80"
size={16}
strokeWidth={1.5}
onClick={() => toggleEditing(true)}
/>
)}
</div>
{isEditing ? (
<div className='relative flex flex-row gap-1 items-center justify-between'>
<input
id="file-name"
type="text"
name="filename"
placeholder={isFolder ? 'Folder Name' : 'File Name'}
className={`!pr-10 block textbox mt-2 w-full`}
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
onChange={formik.handleChange}
value={formik.values.filename || ''}
/>
{itemType !== 'folder' && <span className='absolute right-2 top-4 flex justify-center items-center file-extension'>.bru</span>}
</div>
) : (
<div className='relative flex flex-row gap-1 items-center justify-between'>
<PathDisplay
collection={collection}
dirName={path.relative(collection?.pathname, path.dirname(item?.pathname))}
baseName={formik.values.filename}
/>
</div>
)}
{formik.touched.filename && formik.errors.filename ? (
<div className="text-red-500">{formik.errors.filename}</div>
) : null}
</div>
)}
<div className="flex justify-between items-center mt-8 bruno-modal-footer">
<div className='flex advanced-options'>
<Dropdown onCreate={onDropdownCreate} icon={<AdvancedOptions />} placement="bottom-start">
<div
className="dropdown-item"
key="show-filesystem-name"
onClick={(e) => {
dropdownTippyRef.current.hide();
toggleShowFilesystemName(!showFilesystemName);
}}
>
{showFilesystemName ? 'Hide Filesystem Name' : 'Show Filesystem Name'}
</div>
</Dropdown>
</div>
<div className='flex justify-end'>
<span className='mr-2'>
<button type="button" onClick={onClose} className="btn btn-md btn-close">
Cancel
</button>
</span>
<span>
<button
type="submit"
className="submit btn btn-md btn-secondary"
>
Clone
</button>
</span>
</div>
</div>
</form>
</Modal>
</StyledWrapper>
</Portal>
<Modal
size="sm"
title={`Clone ${isFolder ? 'Folder' : 'Request'}`}
confirmText="Clone"
handleConfirm={onSubmit}
handleCancel={onClose}
>
<form className="bruno-form" onSubmit={e => e.preventDefault()}>
<div>
<label htmlFor="name" className="block font-semibold">
{isFolder ? 'Folder' : 'Request'} Name
</label>
<input
id="collection-item-name"
type="text"
name="name"
placeholder='Enter Item 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 || ''}
/>
{formik.touched.name && formik.errors.name ? <div className="text-red-500">{formik.errors.name}</div> : null}
</div>
</form>
</Modal>
);
};

View File

@@ -1,57 +0,0 @@
import React from 'react';
import Modal from 'components/Modal';
import Help from 'components/Help';
const CollectionItemInfo = ({ item, onClose }) => {
const { name, filename, type } = item;
return (
<Modal
size="md"
title={`Info`}
handleCancel={onClose}
hideCancel={true}
hideFooter={true}
>
<div className="w-fit flex flex-col h-full">
<table className="w-full border-collapse">
<tbody>
<tr className="">
<td className="py-2 px-2 text-left text-muted ">
{type=='folder' ? 'Folder Name' : 'Request Name'}
</td>
<td className="py-2 px-2 text-nowrap truncate max-w-[500px]" title={name}>
<span className="mr-2">:</span>{name}
</td>
</tr>
<tr className="">
<td className="py-2 px-2 text-left text-muted flex items-center">
{type == 'folder' ? 'Folder Name' : 'File Name'}
<small className='font-normal text-muted ml-1'>(on filesystem)</small>
{type == 'folder' ? (
<Help width="300">
<p>
The name of the folder on your filesystem.
</p>
</Help>
) : (
<Help width="300">
<p>
Bruno saves each request as a file in your collection's folder.
</p>
</Help>
)}
</td>
<td className="py-2 px-2 break-all text-nowrap truncate max-w-[500px]" title={filename}>
<span className="mr-2">:</span>
{filename}
</td>
</tr>
</tbody>
</table>
</div>
</Modal>
);
};
export default CollectionItemInfo;

View File

@@ -1,12 +0,0 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
.advanced-options {
.caret {
color: ${(props) => props.theme.textLink};
fill: ${(props) => props.theme.textLink};
}
}
`;
export default StyledWrapper;

View File

@@ -1,82 +1,46 @@
import React, { useRef, useEffect, useState, forwardRef } from 'react';
import React, { useRef, useEffect } from 'react';
import { useFormik } from 'formik';
import * as Yup from 'yup';
import Modal from 'components/Modal';
import { useDispatch } from 'react-redux';
import { isItemAFolder } from 'utils/tabs';
import { renameItem, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import path from 'utils/common/path';
import { IconArrowBackUp, IconEdit, IconCaretDown } from '@tabler/icons';
import { sanitizeName, validateName, validateNameError } from 'utils/common/regex';
import toast from 'react-hot-toast';
import { closeTabs } from 'providers/ReduxStore/slices/tabs';
import Help from 'components/Help';
import PathDisplay from 'components/PathDisplay';
import Portal from 'components/Portal';
import Dropdown from 'components/Dropdown';
import StyledWrapper from './StyledWrapper';
const RenameCollectionItem = ({ collection, item, onClose }) => {
const dispatch = useDispatch();
const isFolder = isItemAFolder(item);
const inputRef = useRef();
const [isEditing, toggleEditing] = useState(false);
const itemName = item?.name;
const itemType = item?.type;
const itemFilename = item?.filename ? path.parse(item?.filename).name : '';
const [showFilesystemName, toggleShowFilesystemName] = useState(false);
const dropdownTippyRef = useRef();
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
const formik = useFormik({
enableReinitialize: true,
initialValues: {
name: itemName,
filename: sanitizeName(itemFilename)
name: item.name
},
validationSchema: Yup.object({
name: Yup.string()
.min(1, 'must be at least 1 character')
.max(255, 'must be 255 characters or less')
.required('name is required'),
filename: Yup.string()
.min(1, 'must be at least 1 character')
.max(255, 'must be 255 characters or less')
.max(50, 'must be 50 characters or less')
.required('name is required')
.test('is-valid-name', function(value) {
const isValid = validateName(value);
return isValid ? true : this.createError({ message: validateNameError(value) });
})
.test('not-reserved', `The file names "collection" and "folder" are reserved in bruno`, value => !['collection', 'folder'].includes(value))
}),
onSubmit: async (values) => {
// if there is unsaved changes in the request,
// save them before renaming the request
if ((item.name === values.name) && (itemFilename === values.filename)) {
return;
}
if (!isFolder && item.draft) {
await dispatch(saveRequest(item.uid, collection.uid, true));
}
const { name: newName, filename: newFilename } = values;
try {
let renameConfig = {
itemUid: item.uid,
collectionUid: collection.uid,
};
renameConfig['newName'] = newName;
if (itemFilename !== newFilename) {
renameConfig['newFilename'] = newFilename;
}
await dispatch(renameItem(renameConfig));
if (isFolder) {
dispatch(closeTabs({ tabUids: [item.uid] }));
}
onClose();
} catch (error) {
toast.error(error.message || 'An error occurred while renaming');
if (item.name === values.name) {
return;
}
dispatch(renameItem(values.name, item.uid, collection.uid))
.then(() => {
isFolder && dispatch(closeTabs({ tabUids: [item.uid] }));
toast.success(isFolder ? 'Folder renamed' : 'Request renamed');
onClose();
})
.catch((err) => {
toast.error(err ? err.message : 'An error occurred while renaming the request');
});
}
});
@@ -86,157 +50,38 @@ const RenameCollectionItem = ({ collection, item, onClose }) => {
}
}, [inputRef]);
const AdvancedOptions = forwardRef((props, ref) => {
return (
<div ref={ref} className="flex mr-2 text-link cursor-pointer items-center">
<button
className="btn-advanced"
type="button"
>
Options
</button>
<IconCaretDown className="caret ml-1" size={14} strokeWidth={2}/>
</div>
);
});
const onSubmit = () => formik.handleSubmit();
return (
<Portal>
<StyledWrapper>
<Modal
size="md"
title={`Rename ${isFolder ? 'Folder' : 'Request'}`}
handleCancel={onClose}
hideFooter
>
<form className="bruno-form" onSubmit={formik.handleSubmit}>
<div className='flex flex-col mt-2'>
<label htmlFor="name" className="block font-semibold">
{isFolder ? 'Folder' : 'Request'} Name
</label>
<input
id="collection-item-name"
type="text"
name="name"
ref={inputRef}
className="block textbox mt-2 w-full"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
onChange={e => {
formik.setFieldValue('name', e.target.value);
!isEditing && formik.setFieldValue('filename', sanitizeName(e.target.value));
}}
value={formik.values.name || ''}
/>
{formik.touched.name && formik.errors.name ? <div className="text-red-500">{formik.errors.name}</div> : null}
</div>
{showFilesystemName && (
<div className="mt-4">
<div className="flex items-center justify-between">
<label htmlFor="filename" className="flex items-center font-semibold">
{isFolder ? 'Folder' : 'File'} Name <small className='font-normal text-muted ml-1'>(on filesystem)</small>
{ isFolder ? (
<Help width="300">
<p>
You can choose to save the folder as a different name on your file system versus what is displayed in the app.
</p>
</Help>
) : (
<Help width="300">
<p>
Bruno saves each request as a file in your collection's folder.
</p>
<p className="mt-2">
You can choose a file name different from your request's name or one compatible with filesystem rules.
</p>
</Help>
)}
</label>
{isEditing ? (
<IconArrowBackUp
className="cursor-pointer opacity-50 hover:opacity-80"
size={16}
strokeWidth={1.5}
onClick={() => toggleEditing(false)}
/>
) : (
<IconEdit
className="cursor-pointer opacity-50 hover:opacity-80"
size={16}
strokeWidth={1.5}
onClick={() => toggleEditing(true)}
/>
)}
</div>
{isEditing ? (
<div className='relative flex flex-row gap-1 items-center justify-between'>
<input
id="file-name"
type="text"
name="filename"
placeholder={isFolder ? 'Folder Name' : 'File Name'}
className={`!pr-10 block textbox mt-2 w-full`}
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
onChange={formik.handleChange}
value={formik.values.filename || ''}
/>
{itemType !== 'folder' && <span className='absolute right-2 top-4 flex justify-center items-center file-extension'>.bru</span>}
</div>
) : (
<div className='relative flex flex-row gap-1 items-center justify-between'>
<PathDisplay
collection={collection}
dirName={path.relative(collection?.pathname, path.dirname(item?.pathname))}
baseName={formik.values.filename}
/>
</div>
)}
{formik.touched.filename && formik.errors.filename ? (
<div className="text-red-500">{formik.errors.filename}</div>
) : null}
</div>
)}
<div className="flex justify-between items-center mt-8 bruno-modal-footer">
<div className='flex advanced-options'>
<Dropdown onCreate={onDropdownCreate} icon={<AdvancedOptions />} placement="bottom-start">
<div
className="dropdown-item"
key="show-filesystem-name"
onClick={(e) => {
dropdownTippyRef.current.hide();
toggleShowFilesystemName(!showFilesystemName);
}}
>
{showFilesystemName ? 'Hide Filesystem Name' : 'Show Filesystem Name'}
</div>
</Dropdown>
</div>
<div className='flex justify-end'>
<span className='mr-2'>
<button type="button" onClick={onClose} className="btn btn-md btn-close">
Cancel
</button>
</span>
<span>
<button
type="submit"
className="submit btn btn-md btn-secondary"
>
Rename
</button>
</span>
</div>
</div>
</form>
</Modal>
</StyledWrapper>
</Portal>
<Modal
size="sm"
title={`Rename ${isFolder ? 'Folder' : 'Request'}`}
confirmText="Rename"
handleConfirm={onSubmit}
handleCancel={onClose}
>
<form className="bruno-form" onSubmit={(e) => e.preventDefault()}>
<div>
<label htmlFor="name" className="block font-semibold">
{isFolder ? 'Folder' : 'Request'} Name
</label>
<input
id="collection-item-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 || ''}
/>
{formik.touched.name && formik.errors.name ? <div className="text-red-500">{formik.errors.name}</div> : null}
</div>
</form>
</Modal>
);
};

View File

@@ -5,8 +5,8 @@ import classnames from 'classnames';
import { useDrag, useDrop } from 'react-dnd';
import { IconChevronRight, IconDots } from '@tabler/icons';
import { useSelector, useDispatch } from 'react-redux';
import { addTab, focusTab, makeTabPermanent } from 'providers/ReduxStore/slices/tabs';
import { moveItem, showInFolder, sendRequest } from 'providers/ReduxStore/slices/collections/actions';
import { addTab, focusTab } from 'providers/ReduxStore/slices/tabs';
import { moveItem, sendRequest } from 'providers/ReduxStore/slices/collections/actions';
import { collectionFolderClicked } from 'providers/ReduxStore/slices/collections';
import Dropdown from 'components/Dropdown';
import NewRequest from 'components/Sidebar/NewRequest';
@@ -23,16 +23,13 @@ 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 CollectionItemInfo from './CollectionItemInfo/index';
import CollectionItemIcon from './CollectionItemIcon';
import { scrollToTheActiveTab } from 'utils/tabs';
import CollectionItemIcon from './CollectionItemIcon/index';
const CollectionItem = ({ item, collection, searchText }) => {
const tabs = useSelector((state) => state.tabs.tabs);
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
const isSidebarDragging = useSelector((state) => state.app.isDragging);
const dispatch = useDispatch();
const collectionItemRef = useRef(null);
const [renameItemModalOpen, setRenameItemModalOpen] = useState(false);
const [cloneItemModalOpen, setCloneItemModalOpen] = useState(false);
@@ -41,36 +38,33 @@ const CollectionItem = ({ item, collection, searchText }) => {
const [newRequestModalOpen, setNewRequestModalOpen] = useState(false);
const [newFolderModalOpen, setNewFolderModalOpen] = useState(false);
const [runCollectionModalOpen, setRunCollectionModalOpen] = useState(false);
const [itemInfoModalOpen, setItemInfoModalOpen] = useState(false);
const hasSearchText = searchText && searchText?.trim()?.length;
const itemIsCollapsed = hasSearchText ? false : item.collapsed;
const [{ isDragging }, drag] = useDrag({
type: `collection-item-${collection.uid}`,
type: `COLLECTION_ITEM_${collection.uid}`,
item: item,
collect: (monitor) => ({
isDragging: monitor.isDragging()
}),
options: {
dropEffect: "move"
}
})
});
const [{ isOver }, drop] = useDrop({
accept: `collection-item-${collection.uid}`,
accept: `COLLECTION_ITEM_${collection.uid}`,
drop: (draggedItem) => {
dispatch(moveItem(collection.uid, draggedItem.uid, item.uid));
if (draggedItem.uid !== item.uid) {
dispatch(moveItem(collection.uid, draggedItem.uid, item.uid));
}
},
canDrop: (draggedItem) => {
return draggedItem.uid !== item.uid;
},
collect: (monitor) => ({
isOver: monitor.isOver(),
}),
isOver: monitor.isOver()
})
});
drag(drop(collectionItemRef));
const dropdownTippyRef = useRef();
const MenuIcon = forwardRef((props, ref) => {
return (
@@ -89,6 +83,13 @@ const CollectionItem = ({ item, collection, searchText }) => {
'item-hovered': isOver
});
const scrollToTheActiveTab = () => {
const activeTab = document.querySelector('.request-tab.active');
if (activeTab) {
activeTab.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
};
const handleRun = async () => {
dispatch(sendRequest(item, collection.uid)).catch((err) =>
toast.custom((t) => <NetworkError onClose={() => toast.dismiss(t.id)} />, {
@@ -98,13 +99,10 @@ const CollectionItem = ({ item, collection, searchText }) => {
};
const handleClick = (event) => {
if (event.detail != 1) return;
//scroll to the active tab
setTimeout(scrollToTheActiveTab, 50);
const isRequest = isItemARequest(item);
if (isRequest) {
if (isItemARequest(item)) {
dispatch(hideHomePage());
if (itemIsOpenedInTabs(item, tabs)) {
dispatch(
@@ -114,21 +112,20 @@ const CollectionItem = ({ item, collection, searchText }) => {
);
return;
}
dispatch(
addTab({
uid: item.uid,
collectionUid: collection.uid,
requestPaneTab: getDefaultRequestPaneTab(item),
type: 'request',
requestPaneTab: getDefaultRequestPaneTab(item)
})
);
} else {
return;
}
dispatch(
addTab({
uid: item.uid,
collectionUid: collection.uid,
type: 'folder-settings',
type: 'folder-settings'
})
);
dispatch(
@@ -137,12 +134,9 @@ const CollectionItem = ({ item, collection, searchText }) => {
collectionUid: collection.uid
})
);
}
};
const handleFolderCollapse = (e) => {
e.stopPropagation();
e.preventDefault();
const handleFolderCollapse = () => {
dispatch(
collectionFolderClicked({
itemUid: item.uid,
@@ -162,6 +156,10 @@ const CollectionItem = ({ item, collection, searchText }) => {
}
};
const handleDoubleClick = (event) => {
setRenameItemModalOpen(true);
};
let indents = range(item.depth);
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
const isFolder = isItemAFolder(item);
@@ -182,10 +180,6 @@ const CollectionItem = ({ item, collection, searchText }) => {
}
}
const handleDoubleClick = (event) => {
dispatch(makeTabPermanent({ uid: item.uid }))
};
// we need to sort request items by seq property
const sortRequestItems = (items = []) => {
return items.sort((a, b) => a.seq - b.seq);
@@ -226,13 +220,6 @@ const CollectionItem = ({ item, collection, searchText }) => {
}
};
const handleShowInFolder = () => {
dispatch(showInFolder(item.pathname)).catch((error) => {
console.error('Error opening the folder', error);
toast.error('Error opening the folder');
});
};
const requestItems = sortRequestItems(filter(item.items, (i) => isItemARequest(i)));
const folderItems = sortFolderItems(filter(item.items, (i) => isItemAFolder(i)));
@@ -259,10 +246,7 @@ const CollectionItem = ({ item, collection, searchText }) => {
{generateCodeItemModalOpen && (
<GenerateCodeItem collection={collection} item={item} onClose={() => setGenerateCodeItemModalOpen(false)} />
)}
{itemInfoModalOpen && (
<CollectionItemInfo item={item} collection={collection} onClose={() => setItemInfoModalOpen(false)} />
)}
<div className={itemRowClassName} ref={collectionItemRef}>
<div className={itemRowClassName} ref={(node) => drag(drop(node))}>
<div className="flex items-center h-full w-full">
{indents && indents.length
? indents.map((i) => {
@@ -289,9 +273,6 @@ const CollectionItem = ({ item, collection, searchText }) => {
style={{
paddingLeft: 8
}}
onClick={handleClick}
onContextMenu={handleRightClick}
onDoubleClick={handleDoubleClick}
>
<div style={{ width: 16, minWidth: 16 }}>
{isFolder ? (
@@ -307,6 +288,9 @@ const CollectionItem = ({ item, collection, searchText }) => {
<div
className="ml-1 flex w-full h-full items-center overflow-hidden"
onClick={handleClick}
onContextMenu={handleRightClick}
onDoubleClick={handleDoubleClick}
>
<CollectionItemIcon item={item} />
<span className="item-name" title={item.name}>
@@ -387,15 +371,6 @@ const CollectionItem = ({ item, collection, searchText }) => {
Generate Code
</div>
)}
<div
className="dropdown-item"
onClick={(e) => {
dropdownTippyRef.current.hide();
handleShowInFolder();
}}
>
Show in Folder
</div>
<div
className="dropdown-item delete-item"
onClick={(e) => {
@@ -416,15 +391,6 @@ const CollectionItem = ({ item, collection, searchText }) => {
Settings
</div>
)}
<div
className="dropdown-item item-info"
onClick={(e) => {
dropdownTippyRef.current.hide();
setItemInfoModalOpen(true);
}}
>
Info
</div>
</Dropdown>
</div>
</div>

View File

@@ -12,17 +12,6 @@ const Wrapper = styled.div`
transform: rotateZ(90deg);
}
&.item-hovered {
background: ${(props) => props.theme.sidebar.collection.item.hoverBg};
.collection-actions {
.dropdown {
div[aria-expanded='false'] {
visibility: visible;
}
}
}
}
.collection-actions {
.dropdown {
div[aria-expanded='true'] {

View File

@@ -2,37 +2,35 @@ import React, { useState, forwardRef, useRef, useEffect } from 'react';
import classnames from 'classnames';
import { uuid } from 'utils/common';
import filter from 'lodash/filter';
import { useDrop, useDrag } from 'react-dnd';
import { useDrop } from 'react-dnd';
import { IconChevronRight, IconDots, IconLoader2 } from '@tabler/icons';
import Dropdown from 'components/Dropdown';
import { collapseCollection } from 'providers/ReduxStore/slices/collections';
import { mountCollection, moveItemToRootOfCollection, moveCollectionAndPersist } from 'providers/ReduxStore/slices/collections/actions';
import { useDispatch, useSelector } from 'react-redux';
import { addTab, makeTabPermanent } from 'providers/ReduxStore/slices/tabs';
import { mountCollection, moveItemToRootOfCollection } from 'providers/ReduxStore/slices/collections/actions';
import { useDispatch } from 'react-redux';
import { addTab } from 'providers/ReduxStore/slices/tabs';
import NewRequest from 'components/Sidebar/NewRequest';
import NewFolder from 'components/Sidebar/NewFolder';
import CollectionItem from './CollectionItem';
import RemoveCollection from './RemoveCollection';
import ExportCollection from './ExportCollection';
import { doesCollectionHaveItemsMatchingSearchText } from 'utils/collections/search';
import { isItemAFolder, isItemARequest } from 'utils/collections';
import RenameCollection from './RenameCollection';
import StyledWrapper from './StyledWrapper';
import CloneCollection from './CloneCollection';
import { areItemsLoading, findItemInCollection } from 'utils/collections';
import { scrollToTheActiveTab } from 'utils/tabs';
import ShareCollection from 'components/ShareCollection/index';
import { areItemsLoading } from 'utils/collections';
const Collection = ({ collection, searchText }) => {
const [showNewFolderModal, setShowNewFolderModal] = useState(false);
const [showNewRequestModal, setShowNewRequestModal] = useState(false);
const [showRenameCollectionModal, setShowRenameCollectionModal] = useState(false);
const [showCloneCollectionModalOpen, setShowCloneCollectionModalOpen] = useState(false);
const [showShareCollectionModal, setShowShareCollectionModal] = useState(false);
const [showExportCollectionModal, setShowExportCollectionModal] = useState(false);
const [showRemoveCollectionModal, setShowRemoveCollectionModal] = useState(false);
const dispatch = useDispatch();
const isLoading = areItemsLoading(collection);
const collectionRef = useRef(null);
const menuDropdownTippyRef = useRef();
const onMenuDropdownCreate = (ref) => (menuDropdownTippyRef.current = ref);
@@ -54,16 +52,6 @@ const Collection = ({ collection, searchText }) => {
);
};
const ensureCollectionIsMounted = () => {
if (collection.mountStatus === 'unmounted') {
dispatch(mountCollection({
collectionUid: collection.uid,
collectionPathname: collection.pathname,
brunoConfig: collection.brunoConfig
}));
}
}
const hasSearchText = searchText && searchText?.trim()?.length;
const collectionIsCollapsed = hasSearchText ? false : collection.collapsed;
@@ -72,37 +60,30 @@ const Collection = ({ collection, searchText }) => {
});
const handleClick = (event) => {
if (event.detail != 1) return;
// Check if the click came from the chevron icon
const isChevronClick = event.target.closest('svg')?.classList.contains('chevron-icon');
setTimeout(scrollToTheActiveTab, 50);
ensureCollectionIsMounted();
if (collection.mountStatus === 'unmounted') {
dispatch(mountCollection({
collectionUid: collection.uid,
collectionPathname: collection.pathname,
brunoConfig: collection.brunoConfig
}));
}
dispatch(collapseCollection(collection.uid));
// Only open collection settings if not clicking the chevron
if(!isChevronClick) {
dispatch(
addTab({
uid: collection.uid,
uid: uuid(),
collectionUid: collection.uid,
type: 'collection-settings',
type: 'collection-settings'
})
);
}
};
const handleDoubleClick = (event) => {
dispatch(makeTabPermanent({ uid: collection.uid }))
};
const handleCollectionCollapse = (e) => {
e.stopPropagation();
e.preventDefault();
ensureCollectionIsMounted();
dispatch(collapseCollection(collection.uid));
}
const handleRightClick = (event) => {
const _menuDropdown = menuDropdownTippyRef.current;
if (_menuDropdown) {
@@ -117,58 +98,33 @@ const Collection = ({ collection, searchText }) => {
const viewCollectionSettings = () => {
dispatch(
addTab({
uid: collection.uid,
uid: uuid(),
collectionUid: collection.uid,
type: 'collection-settings'
})
);
};
const isCollectionItem = (itemType) => {
return itemType.startsWith('collection-item');
};
const [{ isDragging }, drag] = useDrag({
type: "collection",
item: collection,
collect: (monitor) => ({
isDragging: monitor.isDragging(),
}),
options: {
dropEffect: "move"
}
});
const [{ isOver }, drop] = useDrop({
accept: ["collection", `collection-item-${collection.uid}`],
drop: (draggedItem, monitor) => {
const itemType = monitor.getItemType();
if (isCollectionItem(itemType)) {
dispatch(moveItemToRootOfCollection(collection.uid, draggedItem.uid))
} else {
dispatch(moveCollectionAndPersist({draggedItem, targetItem: collection}));
}
accept: `COLLECTION_ITEM_${collection.uid}`,
drop: (draggedItem) => {
dispatch(moveItemToRootOfCollection(collection.uid, draggedItem.uid));
},
canDrop: (draggedItem) => {
return draggedItem.uid !== collection.uid;
// todo need to make sure that draggedItem belongs to the collection
return true;
},
collect: (monitor) => ({
isOver: monitor.isOver(),
}),
isOver: monitor.isOver()
})
});
drag(drop(collectionRef));
if (searchText && searchText.length) {
if (!doesCollectionHaveItemsMatchingSearchText(collection, searchText)) {
return null;
}
}
const collectionRowClassName = classnames('flex py-1 collection-name items-center', {
'item-hovered': isOver
});
// we need to sort request items by seq property
const sortRequestItems = (items = []) => {
return items.sort((a, b) => a.seq - b.seq);
@@ -192,19 +148,16 @@ const Collection = ({ collection, searchText }) => {
{showRemoveCollectionModal && (
<RemoveCollection collection={collection} onClose={() => setShowRemoveCollectionModal(false)} />
)}
{showShareCollectionModal && (
<ShareCollection collection={collection} onClose={() => setShowShareCollectionModal(false)} />
{showExportCollectionModal && (
<ExportCollection collection={collection} onClose={() => setShowExportCollectionModal(false)} />
)}
{showCloneCollectionModalOpen && (
<CloneCollection collection={collection} onClose={() => setShowCloneCollectionModalOpen(false)} />
)}
<div className={collectionRowClassName}
ref={collectionRef}
>
<div className="flex py-1 collection-name items-center" ref={drop}>
<div
className="flex flex-grow items-center overflow-hidden"
onClick={handleClick}
onDoubleClick={handleDoubleClick}
onContextMenu={handleRightClick}
>
<IconChevronRight
@@ -212,9 +165,8 @@ const Collection = ({ collection, searchText }) => {
strokeWidth={2}
className={`chevron-icon ${iconClassName}`}
style={{ width: 16, minWidth: 16, color: 'rgb(160 160 160)' }}
onClick={handleCollectionCollapse}
/>
<div className="ml-1 w-full" id="sidebar-collection-name">
<div className="ml-1" id="sidebar-collection-name">
{collection.name}
</div>
{isLoading ? <IconLoader2 className="animate-spin mx-1" size={18} strokeWidth={1.5} /> : null}
@@ -270,10 +222,10 @@ const Collection = ({ collection, searchText }) => {
className="dropdown-item"
onClick={(e) => {
menuDropdownTippyRef.current.hide();
setShowShareCollectionModal(true);
setShowExportCollectionModal(true);
}}
>
Share
Export
</div>
<div
className="dropdown-item"

View File

@@ -8,10 +8,12 @@ import {
IconSortDescendingLetters,
IconX
} from '@tabler/icons';
import Collection from './Collection';
import Collection from '../Collections/Collection';
import CreateCollection from '../CreateCollection';
import StyledWrapper from './StyledWrapper';
import CreateOrOpenCollection from './CreateOrOpenCollection';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import { sortCollections } from 'providers/ReduxStore/slices/collections/actions';
// todo: move this to a separate folder
@@ -89,7 +91,7 @@ const Collections = () => {
<input
type="text"
name="search"
placeholder="Search requests …"
placeholder="search"
id="search"
autoComplete="off"
autoCorrect="off"
@@ -117,7 +119,9 @@ const Collections = () => {
{collections && collections.length
? collections.map((c) => {
return (
<Collection searchText={searchText} collection={c} key={c.uid} />
<DndProvider backend={HTML5Backend} key={c.uid}>
<Collection searchText={searchText} collection={c} key={c.uid} />
</DndProvider>
);
})
: null}

View File

@@ -5,17 +5,12 @@ import * as Yup from 'yup';
import { browseDirectory } from 'providers/ReduxStore/slices/collections/actions';
import { createCollection } from 'providers/ReduxStore/slices/collections/actions';
import toast from 'react-hot-toast';
import InfoTip from 'components/InfoTip';
import Modal from 'components/Modal';
import { sanitizeName, validateName, validateNameError } from 'utils/common/regex';
import PathDisplay from 'components/PathDisplay/index';
import { useState } from 'react';
import { IconArrowBackUp, IconEdit } from '@tabler/icons';
import Help from 'components/Help';
const CreateCollection = ({ onClose }) => {
const inputRef = useRef();
const dispatch = useDispatch();
const [isEditing, toggleEditing] = useState(false);
const formik = useFormik({
enableReinitialize: true,
@@ -27,15 +22,12 @@ const CreateCollection = ({ onClose }) => {
validationSchema: Yup.object({
collectionName: Yup.string()
.min(1, 'must be at least 1 character')
.max(255, 'must be 255 characters or less')
.max(50, 'must be 50 characters or less')
.required('collection name is required'),
collectionFolderName: Yup.string()
.min(1, 'must be at least 1 character')
.max(255, 'must be 255 characters or less')
.test('is-valid-collection-name', function(value) {
const isValid = validateName(value);
return isValid ? true : this.createError({ message: validateNameError(value) });
})
.max(50, 'must be 50 characters or less')
.matches(/^[\w\-. ]+$/, 'Folder name contains invalid characters')
.required('folder name is required'),
collectionLocation: Yup.string().min(1, 'location is required').required('location is required')
}),
@@ -52,7 +44,7 @@ const CreateCollection = ({ onClose }) => {
const browse = () => {
dispatch(browseDirectory())
.then((dirPath) => {
// When the user closes the dialog without selecting anything dirPath will be false
// When the user closes the diolog without selecting anything dirPath will be false
if (typeof dirPath === 'string') {
formik.setFieldValue('collectionLocation', dirPath);
}
@@ -86,7 +78,9 @@ const CreateCollection = ({ onClose }) => {
className="block textbox mt-2 w-full"
onChange={(e) => {
formik.handleChange(e);
!isEditing && formik.setFieldValue('collectionFolderName', sanitizeName(e.target.value));
if (formik.values.collectionName === formik.values.collectionFolderName) {
formik.setFieldValue('collectionFolderName', e.target.value);
}
}}
autoComplete="off"
autoCorrect="off"
@@ -98,16 +92,8 @@ const CreateCollection = ({ onClose }) => {
<div className="text-red-500">{formik.errors.collectionName}</div>
) : null}
<label htmlFor="collection-location" className="block font-semibold mt-3 flex items-center">
<label htmlFor="collection-location" className="block font-semibold mt-3">
Location
<Help>
<p>
Bruno stores your collections on your computer's filesystem.
</p>
<p className="mt-2">
Choose the location where you want to store this collection.
</p>
</Help>
</label>
<input
id="collection-location"
@@ -126,70 +112,33 @@ const CreateCollection = ({ onClose }) => {
<div className="text-red-500">{formik.errors.collectionLocation}</div>
) : null}
<div className="mt-1">
<span
className="text-link cursor-pointer hover:underline" onClick={browse}
style={{
fontSize: '0.8125rem'
}}
>
<span className="text-link cursor-pointer hover:underline" onClick={browse}>
Browse
</span>
</div>
{formik.values.collectionName?.trim()?.length > 0 && (
<div className="mt-4">
<div className="flex items-center justify-between">
<label htmlFor="filename" className="flex items-center font-semibold">
Folder Name
<Help width="300">
<p>
The name of the folder used to store the collection.
</p>
<p className="mt-2">
You can choose a folder name different from your collection's name or one compatible with filesystem rules.
</p>
</Help>
</label>
{isEditing ? (
<IconArrowBackUp
className="cursor-pointer opacity-50 hover:opacity-80"
size={16}
strokeWidth={1.5}
onClick={() => toggleEditing(false)}
/>
) : (
<IconEdit
className="cursor-pointer opacity-50 hover:opacity-80"
size={16}
strokeWidth={1.5}
onClick={() => toggleEditing(true)}
/>
)}
</div>
{isEditing ? (
<input
id="collection-folder-name"
type="text"
name="collectionFolderName"
className="block textbox mt-2 w-full"
onChange={formik.handleChange}
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={formik.values.collectionFolderName || ''}
/>
) : (
<div className='relative flex flex-row gap-1 items-center justify-between'>
<PathDisplay
baseName={formik.values.collectionFolderName}
/>
</div>
)}
{formik.touched.collectionFolderName && formik.errors.collectionFolderName ? (
<div className="text-red-500">{formik.errors.collectionFolderName}</div>
) : null}
</div>
)}
<label htmlFor="collection-folder-name" className="flex items-center mt-3">
<span className="font-semibold">Folder Name</span>
<InfoTip
text="This folder will be created under the selected location"
infotipId="collection-folder-name-infotip"
/>
</label>
<input
id="collection-folder-name"
type="text"
name="collectionFolderName"
className="block textbox mt-2 w-full"
onChange={formik.handleChange}
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={formik.values.collectionFolderName || ''}
/>
{formik.touched.collectionFolderName && formik.errors.collectionFolderName ? (
<div className="text-red-500">{formik.errors.collectionFolderName}</div>
) : null}
</div>
</form>
</Modal>

View File

@@ -85,7 +85,7 @@ const GoldenEdition = ({ onClose }) => {
});
};
const goldenEditionIndividuals = [
const goldenEditonIndividuals = [
'Inbuilt Bru File Explorer',
'Visual Git (Like Gitlens for Vscode)',
'GRPC, Websocket, SocketIO, MQTT',
@@ -97,7 +97,7 @@ const GoldenEdition = ({ onClose }) => {
'Custom Themes'
];
const goldenEditionOrganizations = [
const goldenEditonOrganizations = [
'Centralized License Management',
'Integration with Secret Managers',
'Private Collection Registry',
@@ -179,7 +179,7 @@ const GoldenEdition = ({ onClose }) => {
</li>
{pricingOption === 'individuals' ? (
<>
{goldenEditionIndividuals.map((item, index) => (
{goldenEditonIndividuals.map((item, index) => (
<li className="flex items-center space-x-3" key={index}>
<CheckIcon />
<span>{item}</span>
@@ -192,7 +192,7 @@ const GoldenEdition = ({ onClose }) => {
<IconPlus size={16} strokeWidth={1.5} style={{ marginLeft: '2px' }} />
<span>Everything in the Individual Plan</span>
</li>
{goldenEditionOrganizations.map((item, index) => (
{goldenEditonOrganizations.map((item, index) => (
<li className="flex items-center space-x-3" key={index}>
<CheckIcon />
<span>{item}</span>

View File

@@ -1,12 +0,0 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
.advanced-options {
.caret {
color: ${(props) => props.theme.textLink};
fill: ${(props) => props.theme.textLink};
}
}
`;
export default StyledWrapper;

View File

@@ -1,61 +1,40 @@
import React, { useRef, useEffect, useState, forwardRef } from 'react';
import React, { useRef, useEffect } from 'react';
import { useFormik } from 'formik';
import toast from 'react-hot-toast';
import * as Yup from 'yup';
import Portal from 'components/Portal';
import Modal from 'components/Modal';
import { useDispatch } from 'react-redux';
import { newFolder } from 'providers/ReduxStore/slices/collections/actions';
import { IconArrowBackUp, IconEdit} from '@tabler/icons';
import { sanitizeName, validateName, validateNameError } from 'utils/common/regex';
import PathDisplay from 'components/PathDisplay/index';
import Help from 'components/Help';
import Dropdown from "components/Dropdown";
import { IconCaretDown } from "@tabler/icons";
import StyledWrapper from './StyledWrapper';
const NewFolder = ({ collection, item, onClose }) => {
const dispatch = useDispatch();
const inputRef = useRef();
const [isEditing, toggleEditing] = useState(false);
const [showFilesystemName, toggleShowFilesystemName] = useState(false);
const dropdownTippyRef = useRef();
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
const formik = useFormik({
enableReinitialize: true,
initialValues: {
folderName: '',
directoryName: ''
folderName: ''
},
validationSchema: Yup.object({
folderName: Yup.string()
.trim()
.min(1, 'must be at least 1 character')
.required('name is required'),
directoryName: Yup.string()
.trim()
.min(1, 'must be at least 1 character')
.required('foldername is required')
.test('is-valid-folder-name', function(value) {
const isValid = validateName(value);
return isValid ? true : this.createError({ message: validateNameError(value) });
})
.required('name is required')
.test({
name: 'folderName',
message: 'The folder name "environments" at the root of the collection is reserved in bruno',
test: (value) => {
if (item?.uid) return true;
if (item && item.uid) {
return true;
}
return value && !value.trim().toLowerCase().includes('environments');
}
})
}),
onSubmit: (values) => {
dispatch(newFolder(values.folderName, values.directoryName, collection.uid, item ? item.uid : null))
dispatch(newFolder(values.folderName, collection.uid, item ? item.uid : null))
.then(() => {
toast.success('New folder created!');
onClose();
onClose()
})
.catch((err) => toast.error(err ? err.message : 'An error occurred while adding the folder'));
}
@@ -67,139 +46,34 @@ const NewFolder = ({ collection, item, onClose }) => {
}
}, [inputRef]);
const AdvancedOptions = forwardRef((props, ref) => {
return (
<div ref={ref} className="flex mr-2 text-link cursor-pointer items-center">
<button
className="btn-advanced"
type="button"
>
Options
</button>
<IconCaretDown className="caret ml-1" size={14} strokeWidth={2}/>
</div>
);
});
const onSubmit = () => formik.handleSubmit();
return (
<Portal>
<StyledWrapper>
<Modal size="md" title="New Folder" hideFooter={true} handleCancel={onClose}>
<form className="bruno-form" onSubmit={formik.handleSubmit}>
<label htmlFor="folderName" className="block font-semibold">
Folder Name
</label>
<input
id="collection-name"
type="text"
name="folderName"
ref={inputRef}
className="block textbox mt-2 w-full"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
onChange={e => {
formik.setFieldValue('folderName', e.target.value);
!isEditing && formik.setFieldValue('directoryName', sanitizeName(e.target.value));
}}
value={formik.values.folderName || ''}
/>
{formik.touched.folderName && formik.errors.folderName ? (
<div className="text-red-500">{formik.errors.folderName}</div>
) : null}
{showFilesystemName && (
<div className="mt-4">
<div className="flex items-center justify-between">
<label htmlFor="directoryName" className="flex items-center font-semibold">
Folder Name <small className='font-normal text-muted ml-1'>(on filesystem)</small>
<Help width="300">
<p>
You can choose to save the folder as a different name on your file system versus what is displayed in the app.
</p>
</Help>
</label>
{isEditing ? (
<IconArrowBackUp
className="cursor-pointer opacity-50 hover:opacity-80"
size={16}
strokeWidth={1.5}
onClick={() => toggleEditing(false)}
/>
): (
<IconEdit
className="cursor-pointer opacity-50 hover:opacity-80"
size={16}
strokeWidth={1.5}
onClick={() => toggleEditing(true)}
/>
)}
</div>
{isEditing ? (
<div className='relative flex flex-row gap-1 items-center justify-between'>
<input
id="file-name"
type="text"
name="directoryName"
placeholder="Folder Name"
className={`block textbox mt-2 w-full`}
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
onChange={formik.handleChange}
value={formik.values.directoryName || ''}
/>
</div>
) : (
<div className='relative flex flex-row gap-1 items-center justify-between'>
<PathDisplay
iconType="folder"
baseName={formik.values.directoryName}
/>
</div>
)}
{formik.touched.directoryName && formik.errors.directoryName ? (
<div className="text-red-500">{formik.errors.directoryName}</div>
) : null}
</div>
)}
<div className="flex justify-between items-center mt-8 bruno-modal-footer">
<div className='flex advanced-options'>
<Dropdown onCreate={onDropdownCreate} icon={<AdvancedOptions />} placement="bottom-start">
<div
className="dropdown-item"
key="show-filesystem-name"
onClick={(e) => {
dropdownTippyRef.current.hide();
toggleShowFilesystemName(!showFilesystemName);
}}
>
{showFilesystemName ? 'Hide Filesystem Name' : 'Show Filesystem Name'}
</div>
</Dropdown>
</div>
<div className='flex justify-end'>
<span className='mr-2'>
<button type="button" onClick={onClose} className="btn btn-md btn-close">
Cancel
</button>
</span>
<span>
<button
type="submit"
className="submit btn btn-md btn-secondary"
>
Create
</button>
</span>
</div>
</div>
</form>
</Modal>
</StyledWrapper>
</Portal>
<Modal size="sm" title="New Folder" confirmText="Create" handleConfirm={onSubmit} handleCancel={onClose}>
<form className="bruno-form" onSubmit={e => e.preventDefault()}>
<div>
<label htmlFor="folderName" className="block font-semibold">
Folder Name
</label>
<input
id="collection-name"
type="text"
name="folderName"
ref={inputRef}
className="block textbox mt-2 w-full"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
onChange={formik.handleChange}
value={formik.values.folderName || ''}
/>
{formik.touched.folderName && formik.errors.folderName ? (
<div className="text-red-500">{formik.errors.folderName}</div>
) : null}
</div>
</form>
</Modal>
);
};

Some files were not shown because too many files have changed in this diff Show More