Merge branch 'main' into feature/random-data-placeholders

This commit is contained in:
Rikhi Singh 2025-01-31 14:44:01 -05:00 committed by GitHub
commit 024b2b5c8c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
197 changed files with 11777 additions and 2636 deletions

View File

@ -2,6 +2,11 @@ name: Bru CLI Tests (npm)
on: on:
workflow_dispatch: workflow_dispatch:
inputs:
build:
description: 'Test Bru CLI (npm)'
required: true
default: 'true'
# Assign permissions for unit tests to be reported. # Assign permissions for unit tests to be reported.
# See https://github.com/dorny/test-reporter/issues/168 # See https://github.com/dorny/test-reporter/issues/168

View File

@ -52,6 +52,9 @@ jobs:
cli-test: cli-test:
name: CLI Tests name: CLI Tests
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
checks: write
pull-requests: write
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4

7974
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -30,11 +30,13 @@
"ts-jest": "^29.0.5" "ts-jest": "^29.0.5"
}, },
"scripts": { "scripts": {
"setup": "node ./scripts/setup.js",
"dev": "concurrently --kill-others \"npm run dev:web\" \"npm run dev:electron\"", "dev": "concurrently --kill-others \"npm run dev:web\" \"npm run dev:electron\"",
"dev:web": "npm run dev --workspace=packages/bruno-app", "dev:web": "npm run dev --workspace=packages/bruno-app",
"build:web": "npm run build --workspace=packages/bruno-app", "build:web": "npm run build --workspace=packages/bruno-app",
"prettier:web": "npm run prettier --workspace=packages/bruno-app", "prettier:web": "npm run prettier --workspace=packages/bruno-app",
"dev:electron": "npm run dev --workspace=packages/bruno-electron", "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-common": "npm run build --workspace=packages/bruno-common",
"build:bruno-query": "npm run build --workspace=packages/bruno-query", "build:bruno-query": "npm run build --workspace=packages/bruno-query",
"build:graphql-docs": "npm run build --workspace=packages/bruno-graphql-docs", "build:graphql-docs": "npm run build --workspace=packages/bruno-graphql-docs",
@ -51,6 +53,6 @@
"prepare": "husky install" "prepare": "husky install"
}, },
"overrides": { "overrides": {
"rollup":"3.29.5" "rollup": "3.29.5"
} }
} }

View File

@ -1,4 +1,4 @@
{ {
"presets": ["next/babel"], "presets": ["@babel/preset-env"],
"plugins": [["styled-components", { "ssr": true }]] "plugins": [["styled-components", { "ssr": true }]]
} }

View File

@ -31,6 +31,6 @@ yarn-error.log*
# next.js # next.js
.next/ .next/
out/ dist/
.env .env

View File

@ -1,22 +0,0 @@
module.exports = {
output: 'export',
reactStrictMode: false,
publicRuntimeConfig: {
CI: process.env.CI,
PLAYWRIGHT: process.env.PLAYWRIGHT,
ENV: process.env.ENV
},
webpack: (config, { isServer }) => {
// Fixes npm packages that depend on `fs` module
if (!isServer) {
config.resolve.fallback.fs = false;
}
Object.defineProperty(config, 'devtool', {
get() {
return 'source-map';
},
set() {},
});
return config;
},
};

View File

@ -3,15 +3,15 @@
"version": "0.3.0", "version": "0.3.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "cross-env ENV=dev next dev -p 3000", "dev": "rsbuild dev",
"build": "next build", "build": "rsbuild build -m production",
"start": "next start", "preview": "rsbuild preview",
"lint": "next lint",
"test": "jest", "test": "jest",
"test:prettier": "prettier --check \"./src/**/*.{js,jsx,json,ts,tsx}\"", "test:prettier": "prettier --check \"./src/**/*.{js,jsx,json,ts,tsx}\"",
"prettier": "prettier --write \"./src/**/*.{js,jsx,json,ts,tsx}\"" "prettier": "prettier --write \"./src/**/*.{js,jsx,json,ts,tsx}\""
}, },
"dependencies": { "dependencies": {
"@babel/preset-env": "^7.26.0",
"@fontsource/inter": "^5.0.15", "@fontsource/inter": "^5.0.15",
"@prantlf/jsonlint": "^16.0.0", "@prantlf/jsonlint": "^16.0.0",
"@reduxjs/toolkit": "^1.8.0", "@reduxjs/toolkit": "^1.8.0",
@ -20,7 +20,6 @@
"@usebruno/common": "0.1.0", "@usebruno/common": "0.1.0",
"@usebruno/graphql-docs": "0.1.0", "@usebruno/graphql-docs": "0.1.0",
"@usebruno/schema": "0.7.0", "@usebruno/schema": "0.7.0",
"axios": "1.7.5",
"classnames": "^2.3.1", "classnames": "^2.3.1",
"codemirror": "5.65.2", "codemirror": "5.65.2",
"codemirror-graphql": "2.1.1", "codemirror-graphql": "2.1.1",
@ -35,21 +34,20 @@
"graphql": "^16.6.0", "graphql": "^16.6.0",
"graphql-request": "^3.7.0", "graphql-request": "^3.7.0",
"httpsnippet": "^3.0.6", "httpsnippet": "^3.0.6",
"i18next": "^23.14.0", "i18next": "24.1.2",
"idb": "^7.0.0", "idb": "^7.0.0",
"immer": "^9.0.15", "immer": "^9.0.15",
"jsesc": "^3.0.2", "jsesc": "^3.0.2",
"jshint": "^2.13.6", "jshint": "^2.13.6",
"json5": "^2.2.3", "json5": "^2.2.3",
"jsonc-parser": "^3.2.1", "jsonc-parser": "^3.2.1",
"jsonpath-plus": "10.1.0", "jsonpath-plus": "10.2.0",
"know-your-http-well": "^0.5.0", "know-your-http-well": "^0.5.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"markdown-it": "^13.0.2", "markdown-it": "^13.0.2",
"markdown-it-replace-link": "^1.2.0", "markdown-it-replace-link": "^1.2.0",
"mousetrap": "^1.6.5", "mousetrap": "^1.6.5",
"nanoid": "3.3.4", "nanoid": "3.3.8",
"next": "14.2.16",
"path": "^0.12.7", "path": "^0.12.7",
"pdfjs-dist": "4.4.168", "pdfjs-dist": "4.4.168",
"platform": "^1.3.6", "platform": "^1.3.6",
@ -57,17 +55,17 @@
"prettier": "^2.7.1", "prettier": "^2.7.1",
"qs": "^6.11.0", "qs": "^6.11.0",
"query-string": "^7.0.1", "query-string": "^7.0.1",
"react": "18.2.0", "react": "19.0.0",
"react-copy-to-clipboard": "^5.1.0", "react-copy-to-clipboard": "^5.1.0",
"react-dnd": "^16.0.1", "react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1", "react-dnd-html5-backend": "^16.0.1",
"react-dom": "18.2.0", "react-dom": "19.0.0",
"react-hot-toast": "^2.4.0", "react-hot-toast": "^2.4.0",
"react-i18next": "^15.0.1", "react-i18next": "^15.0.1",
"react-inspector": "^6.0.2", "react-inspector": "^6.0.2",
"react-pdf": "9.1.1", "react-pdf": "9.1.1",
"react-player": "^2.16.0", "react-player": "^2.16.0",
"react-redux": "^7.2.6", "react-redux": "^7.2.9",
"react-tooltip": "^5.5.2", "react-tooltip": "^5.5.2",
"sass": "^1.46.0", "sass": "^1.46.0",
"strip-json-comments": "^5.0.1", "strip-json-comments": "^5.0.1",
@ -79,13 +77,14 @@
"yup": "^0.32.11" "yup": "^0.32.11"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.16.0", "@rsbuild/core": "^1.1.2",
"@babel/plugin-transform-spread": "^7.16.7", "@rsbuild/plugin-babel": "^1.0.3",
"@babel/preset-env": "^7.16.4", "@rsbuild/plugin-node-polyfill": "^1.2.0",
"@babel/preset-react": "^7.16.0", "@rsbuild/plugin-react": "^1.0.7",
"@babel/runtime": "^7.16.3", "@rsbuild/plugin-sass": "^1.1.0",
"@rsbuild/plugin-styled-components": "1.1.0",
"autoprefixer": "10.4.20", "autoprefixer": "10.4.20",
"babel-loader": "^8.2.3", "babel-plugin-react-compiler": "19.0.0-beta-a7bf2bd-20241110",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"css-loader": "7.1.2", "css-loader": "7.1.2",
"file-loader": "^6.2.0", "file-loader": "^6.2.0",

View File

@ -0,0 +1,39 @@
import { defineConfig } from '@rsbuild/core';
import { pluginReact } from '@rsbuild/plugin-react';
import { pluginBabel } from '@rsbuild/plugin-babel';
import { pluginStyledComponents } from '@rsbuild/plugin-styled-components';
import { pluginSass } from '@rsbuild/plugin-sass';
import { pluginNodePolyfill } from '@rsbuild/plugin-node-polyfill'
export default defineConfig({
plugins: [
pluginNodePolyfill(),
pluginReact(),
pluginStyledComponents(),
pluginSass(),
pluginBabel({
include: /\.(?:js|jsx|tsx)$/,
babelLoaderOptions(opts) {
opts.plugins?.unshift('babel-plugin-react-compiler');
}
})
],
source: {
tsconfigPath: './jsconfig.json', // Specifies the path to the JavaScript/TypeScript configuration file
},
html: {
title: 'Bruno'
},
tools: {
rspack: {
module: {
parser: {
javascript: {
// This loads the JavaScript contents from a library along with the main JavaScript bundle.
dynamicImportMode: "eager",
},
},
},
},
}
});

View File

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

View File

@ -7,12 +7,12 @@
import React from 'react'; import React from 'react';
import { isEqual, escapeRegExp } from 'lodash'; import { isEqual, escapeRegExp } from 'lodash';
import { getEnvironmentVariables } from 'utils/collections';
import { defineCodeMirrorBrunoVariablesMode } from 'utils/common/codemirror'; import { defineCodeMirrorBrunoVariablesMode } from 'utils/common/codemirror';
import StyledWrapper from './StyledWrapper'; import StyledWrapper from './StyledWrapper';
import * as jsonlint from '@prantlf/jsonlint'; import * as jsonlint from '@prantlf/jsonlint';
import { JSHINT } from 'jshint'; import { JSHINT } from 'jshint';
import stripJsonComments from 'strip-json-comments'; import stripJsonComments from 'strip-json-comments';
import { getAllVariables } from 'utils/collections';
let CodeMirror; let CodeMirror;
const SERVER_RENDERED = typeof window === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true; const SERVER_RENDERED = typeof window === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true;
@ -74,9 +74,16 @@ if (!SERVER_RENDERED) {
'bru.setNextRequest(requestName)', 'bru.setNextRequest(requestName)',
'req.disableParsingResponseJson()', 'req.disableParsingResponseJson()',
'bru.getRequestVar(key)', 'bru.getRequestVar(key)',
'bru.runRequest(requestPathName)',
'bru.getAssertionResults()',
'bru.getTestResults()',
'bru.sleep(ms)', 'bru.sleep(ms)',
'bru.getGlobalEnvVar(key)', 'bru.getGlobalEnvVar(key)',
'bru.setGlobalEnvVar(key, value)' 'bru.setGlobalEnvVar(key, value)',
'bru.runner',
'bru.runner.setNextRequest(requestName)',
'bru.runner.skipRequest()',
'bru.runner.stopExecution()',
]; ];
CodeMirror.registerHelper('hint', 'brunoJS', (editor, options) => { CodeMirror.registerHelper('hint', 'brunoJS', (editor, options) => {
const cursor = editor.getCursor(); const cursor = editor.getCursor();
@ -98,7 +105,7 @@ if (!SERVER_RENDERED) {
if (curWordBru) { if (curWordBru) {
hintWords.forEach((h) => { hintWords.forEach((h) => {
if (h.includes('.') == curWordBru.includes('.') && h.startsWith(curWordBru)) { if (h.includes('.') == curWordBru.includes('.') && h.startsWith(curWordBru)) {
result.list.push(curWordBru.includes('.') ? h.split('.')[1] : h); result.list.push(curWordBru.includes('.') ? h.split('.')?.at(-1) : h);
} }
}); });
result.list?.sort(); result.list?.sort();
@ -190,8 +197,20 @@ export default class CodeEditor extends React.Component {
'Cmd-Y': 'foldAll', 'Cmd-Y': 'foldAll',
'Ctrl-I': 'unfoldAll', 'Ctrl-I': 'unfoldAll',
'Cmd-I': 'unfoldAll', 'Cmd-I': 'unfoldAll',
'Ctrl-/': 'toggleComment', 'Ctrl-/': () => {
'Cmd-/': 'toggleComment' 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();
}
}
}, },
foldOptions: { foldOptions: {
widget: (from, to) => { widget: (from, to) => {
@ -289,7 +308,7 @@ export default class CodeEditor extends React.Component {
} }
if (this.editor) { if (this.editor) {
let variables = getEnvironmentVariables(this.props.collection); let variables = getAllVariables(this.props.collection, this.props.item);
if (!isEqual(variables, this.variables)) { if (!isEqual(variables, this.variables)) {
this.addOverlay(); this.addOverlay();
} }
@ -329,7 +348,7 @@ export default class CodeEditor extends React.Component {
addOverlay = () => { addOverlay = () => {
const mode = this.props.mode || 'application/ld+json'; const mode = this.props.mode || 'application/ld+json';
let variables = getEnvironmentVariables(this.props.collection); let variables = getAllVariables(this.props.collection, this.props.item);
this.variables = variables; this.variables = variables;
defineCodeMirrorBrunoVariablesMode(variables, mode); defineCodeMirrorBrunoVariablesMode(variables, mode);

View File

@ -79,6 +79,15 @@ const AuthMode = ({ collection }) => {
> >
Digest Auth Digest Auth
</div> </div>
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
onModeChange('ntlm');
}}
>
NTLM Auth
</div>
<div <div
className="dropdown-item" className="dropdown-item"
onClick={() => { onClick={() => {

View File

@ -0,0 +1,17 @@
import styled from 'styled-components';
const Wrapper = styled.div`
label {
font-size: 0.8125rem;
}
.single-line-editor-wrapper {
max-width: 400px;
padding: 0.15rem 0.4rem;
border-radius: 3px;
border: solid 1px ${(props) => props.theme.input.border};
background-color: ${(props) => props.theme.input.bg};
}
`;
export default Wrapper;

View File

@ -0,0 +1,110 @@
import React from 'react';
import get from 'lodash/get';
import { useTheme } from 'providers/Theme';
import { useDispatch } from 'react-redux';
import SingleLineEditor from 'components/SingleLineEditor';
import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections';
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';
const NTLMAuth = ({ collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const ntlmAuth = get(collection, 'root.request.auth.ntlm', {});
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
const handleUsernameChange = (username) => {
dispatch(
updateCollectionAuth({
mode: 'ntlm',
collectionUid: collection.uid,
content: {
username: username,
password: ntlmAuth.password,
domain: ntlmAuth.domain
}
})
);
};
const handlePasswordChange = (password) => {
dispatch(
updateCollectionAuth({
mode: 'ntlm',
collectionUid: collection.uid,
content: {
username: ntlmAuth.username,
password: password,
domain: ntlmAuth.domain
}
})
);
};
const handleDomainChange = (domain) => {
dispatch(
updateCollectionAuth({
mode: 'ntlm',
collectionUid: collection.uid,
content: {
username: ntlmAuth.username,
password: ntlmAuth.password,
domain: domain
}
})
);
};
return (
<StyledWrapper className="mt-2 w-full">
<label className="block font-medium mb-2">Username</label>
<div className="single-line-editor-wrapper mb-2">
<SingleLineEditor
value={ntlmAuth.username || ''}
theme={storedTheme}
onSave={handleSave}
onChange={(val) => handleUsernameChange(val)}
collection={collection}
/>
</div>
<label className="block font-medium mb-2">Password</label>
<div className="single-line-editor-wrapper">
<SingleLineEditor
value={ntlmAuth.password || ''}
theme={storedTheme}
onSave={handleSave}
onChange={(val) => handlePasswordChange(val)}
collection={collection}
isSecret={true}
/>
</div>
<label className="block font-medium mb-2">Domain</label>
<div className="single-line-editor-wrapper">
<SingleLineEditor
value={ntlmAuth.domain || ''}
theme={storedTheme}
onSave={handleSave}
onChange={(val) => handleDomainChange(val)}
collection={collection}
/>
</div>
</StyledWrapper>
);
};
export default NTLMAuth;

View File

@ -1,5 +1,7 @@
import styled from 'styled-components'; import styled from 'styled-components';
const Wrapper = styled.div``; const Wrapper = styled.div`
max-width: 800px;
`;
export default Wrapper; export default Wrapper;

View File

@ -11,6 +11,8 @@ import ApiKeyAuth from './ApiKeyAuth/';
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions'; import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper'; import StyledWrapper from './StyledWrapper';
import OAuth2 from './OAuth2'; import OAuth2 from './OAuth2';
import NTLMAuth from './NTLMAuth';
const Auth = ({ collection }) => { const Auth = ({ collection }) => {
const authMode = get(collection, 'root.request.auth.mode'); const authMode = get(collection, 'root.request.auth.mode');
@ -32,6 +34,9 @@ const Auth = ({ collection }) => {
case 'digest': { case 'digest': {
return <DigestAuth collection={collection} />; return <DigestAuth collection={collection} />;
} }
case 'ntlm': {
return <NTLMAuth collection={collection} />;
}
case 'oauth2': { case 'oauth2': {
return <OAuth2 collection={collection} />; return <OAuth2 collection={collection} />;
} }

View File

@ -68,12 +68,13 @@ const ClientCertSettings = ({ root, clientCertConfig, onUpdate, onRemove }) => {
}); });
const getFile = (e) => { const getFile = (e) => {
if (e.files?.[0]?.path) { const filePath = window?.ipcRenderer?.getFilePath(e?.files?.[0]);
if (filePath) {
let relativePath; let relativePath;
if (isWindowsOS()) { if (isWindowsOS()) {
relativePath = slash(path.win32.relative(root, e.files[0].path)); relativePath = slash(path.win32.relative(root, filePath));
} else { } else {
relativePath = path.posix.relative(root, e.files[0].path); relativePath = path.posix.relative(root, filePath);
} }
formik.setFieldValue(e.name, relativePath); formik.setFieldValue(e.name, relativePath);
} }

View File

@ -2,16 +2,12 @@ import styled from 'styled-components';
const StyledWrapper = styled.div` const StyledWrapper = styled.div`
div.CodeMirror { div.CodeMirror {
/* todo: find a better way */
height: calc(100vh - 240px);
.CodeMirror-scroll { .CodeMirror-scroll {
padding-bottom: 0px; padding-bottom: 0px;
} }
} }
.editing-mode { .editing-mode {
cursor: pointer; cursor: pointer;
color: ${(props) => props.theme.colors.text.yellow};
} }
`; `;

View File

@ -8,6 +8,7 @@ import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/acti
import Markdown from 'components/MarkDown'; import Markdown from 'components/MarkDown';
import CodeEditor from 'components/CodeEditor'; import CodeEditor from 'components/CodeEditor';
import StyledWrapper from './StyledWrapper'; import StyledWrapper from './StyledWrapper';
import { IconEdit, IconX, IconFileText } from '@tabler/icons';
const Docs = ({ collection }) => { const Docs = ({ collection }) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
@ -29,19 +30,50 @@ const Docs = ({ collection }) => {
); );
}; };
const onSave = () => dispatch(saveCollectionRoot(collection.uid)); const handleDiscardChanges = () => {
dispatch(
updateCollectionDocs({
collectionUid: collection.uid,
docs: docs
})
);
toggleViewMode();
}
const onSave = () => {
dispatch(saveCollectionRoot(collection.uid));
toggleViewMode();
}
return ( return (
<StyledWrapper className="mt-1 h-full w-full relative"> <StyledWrapper className="mt-1 h-full w-full relative flex flex-col">
<div className="editing-mode mb-2" role="tab" onClick={toggleViewMode}> <div className='flex flex-row w-full justify-between items-center mb-4'>
{isEditing ? 'Preview' : 'Edit'} <div className='text-lg font-medium flex items-center gap-2'>
<IconFileText size={20} strokeWidth={1.5} />
Documentation
</div>
<div className='flex flex-row gap-2 items-center justify-center'>
{isEditing ? (
<>
<div className="editing-mode" role="tab" onClick={handleDiscardChanges}>
<IconX className="cursor-pointer" size={20} strokeWidth={1.5} />
</div>
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={onSave}>
Save
</button>
</>
) : (
<div className="editing-mode" role="tab" onClick={toggleViewMode}>
<IconEdit className="cursor-pointer" size={20} strokeWidth={1.5} />
</div>
)}
</div>
</div> </div>
{isEditing ? ( {isEditing ? (
<CodeEditor <CodeEditor
collection={collection} collection={collection}
theme={displayedTheme} theme={displayedTheme}
value={docs || ''} value={docs}
onEdit={onEdit} onEdit={onEdit}
onSave={onSave} onSave={onSave}
mode="application/text" mode="application/text"
@ -49,10 +81,44 @@ const Docs = ({ collection }) => {
fontSize={get(preferences, 'font.codeFontSize')} fontSize={get(preferences, 'font.codeFontSize')}
/> />
) : ( ) : (
<div className='h-full overflow-auto pl-1'>
<div className='h-[1px] min-h-[500px]'>
{
docs?.length > 0 ?
<Markdown collectionPath={collection.pathname} onDoubleClick={toggleViewMode} content={docs} /> <Markdown collectionPath={collection.pathname} onDoubleClick={toggleViewMode} content={docs} />
:
<Markdown collectionPath={collection.pathname} onDoubleClick={toggleViewMode} content={documentationPlaceholder} />
}
</div>
</div>
)} )}
</StyledWrapper> </StyledWrapper>
); );
}; };
export default Docs; export default Docs;
const documentationPlaceholder = `
Welcome to your collection documentation! This space is designed to help you document your API collection effectively.
## Overview
Use this section to provide a high-level overview of your collection. You can describe:
- The purpose of these API endpoints
- Key features and functionalities
- Target audience or users
## Best Practices
- Keep documentation up to date
- Include request/response examples
- Document error scenarios
- Add relevant links and references
## Markdown Support
This documentation supports Markdown formatting! You can use:
- **Bold** and *italic* text
- \`code blocks\` and syntax highlighting
- Tables and lists
- [Links](https://example.com)
- And more!
`;

View File

@ -1,6 +1,8 @@
import styled from 'styled-components'; import styled from 'styled-components';
const Wrapper = styled.div` const Wrapper = styled.div`
max-width: 800px;
table { table {
width: 100%; width: 100%;
border-collapse: collapse; border-collapse: collapse;

View File

@ -1,39 +0,0 @@
import React from 'react';
import StyledWrapper from './StyledWrapper';
import { getTotalRequestCountInCollection } from 'utils/collections/';
const Info = ({ collection }) => {
const totalRequestsInCollection = getTotalRequestCountInCollection(collection);
return (
<StyledWrapper className="w-full flex flex-col h-full">
<div className="text-xs mb-4 text-muted">General information about the collection.</div>
<table className="w-full border-collapse">
<tbody>
<tr className="">
<td className="py-2 px-2 text-right">Name&nbsp;:</td>
<td className="py-2 px-2">{collection.name}</td>
</tr>
<tr className="">
<td className="py-2 px-2 text-right">Location&nbsp;:</td>
<td className="py-2 px-2 break-all">{collection.pathname}</td>
</tr>
<tr className="">
<td className="py-2 px-2 text-right">Ignored files&nbsp;:</td>
<td className="py-2 px-2 break-all">{collection.brunoConfig?.ignore?.map((x) => `'${x}'`).join(', ')}</td>
</tr>
<tr className="">
<td className="py-2 px-2 text-right">Environments&nbsp;:</td>
<td className="py-2 px-2">{collection.environments?.length || 0}</td>
</tr>
<tr className="">
<td className="py-2 px-2 text-right">Requests&nbsp;:</td>
<td className="py-2 px-2">{totalRequestsInCollection}</td>
</tr>
</tbody>
</table>
</StyledWrapper>
);
};
export default Info;

View File

@ -0,0 +1,56 @@
import React from 'react';
import { getTotalRequestCountInCollection } from 'utils/collections/';
import { IconFolder, IconFileOff, IconWorld, IconApi } from '@tabler/icons';
const Info = ({ collection }) => {
const totalRequestsInCollection = getTotalRequestCountInCollection(collection);
return (
<div className="w-full flex flex-col h-fit">
<div className="rounded-lg py-6">
<div className="grid gap-6">
{/* Location Row */}
<div className="flex items-start">
<div className="flex-shrink-0 p-3 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
<IconFolder className="w-5 h-5 text-blue-500" stroke={1.5} />
</div>
<div className="ml-4">
<div className="font-semibold text-sm">Location</div>
<div className="mt-1 text-sm text-muted break-all">
{collection.pathname}
</div>
</div>
</div>
{/* Environments Row */}
<div className="flex items-start">
<div className="flex-shrink-0 p-3 bg-green-50 dark:bg-green-900/20 rounded-lg">
<IconWorld className="w-5 h-5 text-green-500" stroke={1.5} />
</div>
<div className="ml-4">
<div className="font-semibold text-sm">Environments</div>
<div className="mt-1 text-sm text-muted">
{collection.environments?.length || 0} environment{collection.environments?.length !== 1 ? 's' : ''} configured
</div>
</div>
</div>
{/* Requests Row */}
<div className="flex items-start">
<div className="flex-shrink-0 p-3 bg-purple-50 dark:bg-purple-900/20 rounded-lg">
<IconApi className="w-5 h-5 text-purple-500" stroke={1.5} />
</div>
<div className="ml-4">
<div className="font-semibold text-sm">Requests</div>
<div className="mt-1 text-sm text-muted">
{totalRequestsInCollection} request{totalRequestsInCollection !== 1 ? 's' : ''} in collection
</div>
</div>
</div>
</div>
</div>
</div>
);
};
export default Info;

View File

@ -0,0 +1,25 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
&.card {
background-color: ${(props) => props.theme.requestTabPanel.card.bg};
.title {
border-top: 1px solid ${(props) => props.theme.requestTabPanel.cardTable.border};
border-left: 1px solid ${(props) => props.theme.requestTabPanel.cardTable.border};
border-right: 1px solid ${(props) => props.theme.requestTabPanel.cardTable.border};
border-top-left-radius: 3px;
border-top-right-radius: 3px;
}
.table {
thead {
background-color: ${(props) => props.theme.requestTabPanel.cardTable.table.thead.bg};
color: ${(props) => props.theme.requestTabPanel.cardTable.table.thead.color};
}
}
}
`;
export default StyledWrapper;

View File

@ -0,0 +1,50 @@
import React from 'react';
import { flattenItems } from "utils/collections";
import { IconAlertTriangle } from '@tabler/icons';
import StyledWrapper from "./StyledWrapper";
const RequestsNotLoaded = ({ collection }) => {
const flattenedItems = flattenItems(collection.items);
const itemsFailedLoading = flattenedItems?.filter(item => item?.partial && !item?.loading);
if (!itemsFailedLoading?.length) {
return null;
}
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">
<IconAlertTriangle size={16} className="text-yellow-500" />
<span className="font-medium">Following requests were not loaded</span>
</div>
<table className="w-full border-collapse">
<thead>
<tr>
<th className="py-2 px-3 text-left font-medium">
Pathname
</th>
<th className="py-2 px-3 text-left font-medium">
Size
</th>
</tr>
</thead>
<tbody>
{flattenedItems?.map((item, index) => (
item?.partial && !item?.loading ? (
<tr key={index}>
<td className="py-1.5 px-3">
{item?.pathname?.split(`${collection?.pathname}/`)?.[1]}
</td>
<td className="py-1.5 px-3">
{item?.size?.toFixed?.(2)}&nbsp;MB
</td>
</tr>
) : null
))}
</tbody>
</table>
</StyledWrapper>
);
};
export default RequestsNotLoaded;

View File

@ -0,0 +1,25 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
.partial {
color: ${(props) => props.theme.colors.text.yellow};
opacity: 0.8;
}
.loading {
color: ${(props) => props.theme.colors.text.muted};
opacity: 0.8;
}
.completed {
color: ${(props) => props.theme.colors.text.green};
opacity: 0.8;
}
.failed {
color: ${(props) => props.theme.colors.text.danger};
opacity: 0.8;
}
`;
export default StyledWrapper;

View File

@ -0,0 +1,27 @@
import StyledWrapper from "./StyledWrapper";
import Docs from "../Docs";
import Info from "./Info";
import { IconBox } from '@tabler/icons';
import RequestsNotLoaded from "./RequestsNotLoaded";
const Overview = ({ collection }) => {
return (
<div className="h-full">
<div className="grid grid-cols-5 gap-4 h-full">
<div className="col-span-2">
<div className="text-xl font-semibold flex items-center gap-2">
<IconBox size={24} stroke={1.5} />
{collection?.name}
</div>
<Info collection={collection} />
<RequestsNotLoaded collection={collection} />
</div>
<div className="col-span-3">
<Docs collection={collection} />
</div>
</div>
</div>
);
}
export default Overview;

View File

@ -1,6 +1,8 @@
import styled from 'styled-components'; import styled from 'styled-components';
const StyledWrapper = styled.div` const StyledWrapper = styled.div`
max-width: 800px;
.settings-label { .settings-label {
width: 110px; width: 110px;
} }

View File

@ -1,6 +1,8 @@
import styled from 'styled-components'; import styled from 'styled-components';
const StyledWrapper = styled.div` const StyledWrapper = styled.div`
max-width: 800px;
div.CodeMirror { div.CodeMirror {
height: inherit; height: inherit;
} }

View File

@ -1,8 +1,6 @@
import styled from 'styled-components'; import styled from 'styled-components';
const StyledWrapper = styled.div` const StyledWrapper = styled.div`
max-width: 800px;
div.tabs { div.tabs {
div.tab { div.tab {
padding: 6px 0px; padding: 6px 0px;

View File

@ -1,5 +1,7 @@
import styled from 'styled-components'; import styled from 'styled-components';
const StyledWrapper = styled.div``; const StyledWrapper = styled.div`
max-width: 800px;
`;
export default StyledWrapper; export default StyledWrapper;

View File

@ -1,6 +1,8 @@
import styled from 'styled-components'; import styled from 'styled-components';
const StyledWrapper = styled.div` const StyledWrapper = styled.div`
max-width: 800px;
div.title { div.title {
color: var(--color-tab-inactive); color: var(--color-tab-inactive);
} }

View File

@ -12,12 +12,11 @@ import Headers from './Headers';
import Auth from './Auth'; import Auth from './Auth';
import Script from './Script'; import Script from './Script';
import Test from './Tests'; import Test from './Tests';
import Docs from './Docs';
import Presets from './Presets'; import Presets from './Presets';
import Info from './Info';
import StyledWrapper from './StyledWrapper'; import StyledWrapper from './StyledWrapper';
import Vars from './Vars/index'; import Vars from './Vars/index';
import DotIcon from 'components/Icons/Dot'; import DotIcon from 'components/Icons/Dot';
import Overview from './Overview/index';
const ContentIndicator = () => { const ContentIndicator = () => {
return ( return (
@ -97,6 +96,9 @@ const CollectionSettings = ({ collection }) => {
const getTabPanel = (tab) => { const getTabPanel = (tab) => {
switch (tab) { switch (tab) {
case 'overview': {
return <Overview collection={collection} />;
}
case 'headers': { case 'headers': {
return <Headers collection={collection} />; return <Headers collection={collection} />;
} }
@ -128,12 +130,6 @@ const CollectionSettings = ({ collection }) => {
/> />
); );
} }
case 'docs': {
return <Docs collection={collection} />;
}
case 'info': {
return <Info collection={collection} />;
}
} }
}; };
@ -146,6 +142,9 @@ const CollectionSettings = ({ collection }) => {
return ( return (
<StyledWrapper className="flex flex-col h-full relative px-4 py-4"> <StyledWrapper className="flex flex-col h-full relative px-4 py-4">
<div className="flex flex-wrap items-center tabs" role="tablist"> <div className="flex flex-wrap items-center tabs" role="tablist">
<div className={getTabClassname('overview')} role="tab" onClick={() => setTab('overview')}>
Overview
</div>
<div className={getTabClassname('headers')} role="tab" onClick={() => setTab('headers')}> <div className={getTabClassname('headers')} role="tab" onClick={() => setTab('headers')}>
Headers Headers
{activeHeadersCount > 0 && <sup className="ml-1 font-medium">{activeHeadersCount}</sup>} {activeHeadersCount > 0 && <sup className="ml-1 font-medium">{activeHeadersCount}</sup>}
@ -177,13 +176,6 @@ const CollectionSettings = ({ collection }) => {
Client Certificates Client Certificates
{clientCertConfig.length > 0 && <ContentIndicator />} {clientCertConfig.length > 0 && <ContentIndicator />}
</div> </div>
<div className={getTabClassname('docs')} role="tab" onClick={() => setTab('docs')}>
Docs
{hasDocs && <ContentIndicator />}
</div>
<div className={getTabClassname('info')} role="tab" onClick={() => setTab('info')}>
Info
</div>
</div> </div>
<section className="mt-4 h-full">{getTabPanel(tab)}</section> <section className="mt-4 h-full">{getTabPanel(tab)}</section>
</StyledWrapper> </StyledWrapper>

View File

@ -3,7 +3,6 @@ import styled from 'styled-components';
const StyledWrapper = styled.div` const StyledWrapper = styled.div`
.editing-mode { .editing-mode {
cursor: pointer; cursor: pointer;
color: ${(props) => props.theme.colors.text.yellow};
} }
`; `;

View File

@ -19,7 +19,7 @@ const EnvironmentSelector = ({ collection }) => {
const Icon = forwardRef((props, ref) => { const Icon = forwardRef((props, ref) => {
return ( return (
<div ref={ref} className="current-environment flex items-center justify-center pl-3 pr-2 py-1 select-none"> <div ref={ref} className="current-environment flex items-center justify-center pl-3 pr-2 py-1 select-none">
{activeEnvironment ? activeEnvironment.name : 'No Environment'} <p className="text-nowrap truncate max-w-32">{activeEnvironment ? activeEnvironment.name : 'No Environment'}</p>
<IconCaretDown className="caret" size={14} strokeWidth={2} /> <IconCaretDown className="caret" size={14} strokeWidth={2} />
</div> </div>
); );
@ -78,7 +78,10 @@ const EnvironmentSelector = ({ collection }) => {
<IconDatabaseOff size={18} strokeWidth={1.5} /> <IconDatabaseOff size={18} strokeWidth={1.5} />
<span className="ml-2">No Environment</span> <span className="ml-2">No Environment</span>
</div> </div>
<div className="dropdown-item border-top" onClick={handleSettingsIconClick}> <div className="dropdown-item border-top" onClick={() => {
handleSettingsIconClick();
dropdownTippyRef.current.hide();
}}>
<div className="pr-2 text-gray-600"> <div className="pr-2 text-gray-600">
<IconSettings size={18} strokeWidth={1.5} /> <IconSettings size={18} strokeWidth={1.5} />
</div> </div>

View File

@ -11,7 +11,6 @@ const CreateEnvironment = ({ collection, onClose }) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const inputRef = useRef(); const inputRef = useRef();
// todo: Add this to global env too.
const validateEnvironmentName = (name) => { const validateEnvironmentName = (name) => {
return !collection?.environments?.some((env) => env?.name?.toLowerCase().trim() === name?.toLowerCase().trim()); return !collection?.environments?.some((env) => env?.name?.toLowerCase().trim() === name?.toLowerCase().trim());
}; };

View File

@ -1,8 +1,9 @@
import React, { useRef, useEffect } from 'react'; import React, { useRef, useEffect } from 'react';
import cloneDeep from 'lodash/cloneDeep'; import cloneDeep from 'lodash/cloneDeep';
import { IconTrash, IconAlertCircle } from '@tabler/icons'; import { IconTrash, IconAlertCircle, IconDeviceFloppy, IconRefresh, IconCircleCheck } from '@tabler/icons';
import { useTheme } from 'providers/Theme'; import { useTheme } from 'providers/Theme';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import { selectEnvironment } from 'providers/ReduxStore/slices/collections/actions';
import SingleLineEditor from 'components/SingleLineEditor'; import SingleLineEditor from 'components/SingleLineEditor';
import StyledWrapper from './StyledWrapper'; import StyledWrapper from './StyledWrapper';
import { uuid } from 'utils/common'; import { uuid } from 'utils/common';
@ -13,7 +14,7 @@ import { saveEnvironment } from 'providers/ReduxStore/slices/collections/actions
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
import { Tooltip } from 'react-tooltip'; import { Tooltip } from 'react-tooltip';
const EnvironmentVariables = ({ environment, collection, setIsModified, originalEnvironmentVariables }) => { const EnvironmentVariables = ({ environment, collection, setIsModified, originalEnvironmentVariables, onClose }) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const { storedTheme } = useTheme(); const { storedTheme } = useTheme();
const addButtonRef = useRef(null); const addButtonRef = useRef(null);
@ -84,6 +85,19 @@ const EnvironmentVariables = ({ environment, collection, setIsModified, original
formik.setFieldValue(formik.values.length, newVariable, false); formik.setFieldValue(formik.values.length, newVariable, false);
}; };
const onActivate = () => {
dispatch(selectEnvironment(environment ? environment.uid : null, collection.uid))
.then(() => {
if (environment) {
toast.success(`Environment changed to ${environment.name}`);
onClose();
} else {
toast.success(`No Environments are active now`);
}
})
.catch((err) => console.log(err) && toast.error('An error occurred while selecting the environment'));
};
const handleRemoveVar = (id) => { const handleRemoveVar = (id) => {
formik.setValues(formik.values.filter((variable) => variable.uid !== id)); formik.setValues(formik.values.filter((variable) => variable.uid !== id));
}; };
@ -183,13 +197,19 @@ const EnvironmentVariables = ({ environment, collection, setIsModified, original
</div> </div>
</div> </div>
<div> <div className="flex items-center">
<button type="submit" className="submit btn btn-md btn-secondary mt-2" onClick={formik.handleSubmit}> <button type="submit" className="submit btn btn-sm btn-secondary mt-2 flex items-center" onClick={formik.handleSubmit}>
<IconDeviceFloppy size={16} strokeWidth={1.5} className="mr-1" />
Save Save
</button> </button>
<button type="submit" className="ml-2 px-1 submit btn btn-md btn-secondary mt-2" onClick={handleReset}> <button type="submit" className="ml-2 px-1 submit btn btn-sm btn-close mt-2 flex items-center" onClick={handleReset}>
<IconRefresh size={16} strokeWidth={1.5} className="mr-1" />
Reset Reset
</button> </button>
<button type="submit" className="submit btn btn-sm btn-close mt-2 flex items-center" onClick={onActivate}>
<IconCircleCheck size={16} strokeWidth={1.5} className="mr-1" />
Activate
</button>
</div> </div>
</StyledWrapper> </StyledWrapper>
); );

View File

@ -5,7 +5,7 @@ import DeleteEnvironment from '../../DeleteEnvironment';
import RenameEnvironment from '../../RenameEnvironment'; import RenameEnvironment from '../../RenameEnvironment';
import EnvironmentVariables from './EnvironmentVariables'; import EnvironmentVariables from './EnvironmentVariables';
const EnvironmentDetails = ({ environment, collection, setIsModified }) => { const EnvironmentDetails = ({ environment, collection, setIsModified, onClose }) => {
const [openEditModal, setOpenEditModal] = useState(false); const [openEditModal, setOpenEditModal] = useState(false);
const [openDeleteModal, setOpenDeleteModal] = useState(false); const [openDeleteModal, setOpenDeleteModal] = useState(false);
const [openCopyModal, setOpenCopyModal] = useState(false); const [openCopyModal, setOpenCopyModal] = useState(false);
@ -38,7 +38,7 @@ const EnvironmentDetails = ({ environment, collection, setIsModified }) => {
</div> </div>
<div> <div>
<EnvironmentVariables environment={environment} collection={collection} setIsModified={setIsModified} /> <EnvironmentVariables environment={environment} collection={collection} setIsModified={setIsModified} onClose={onClose} />
</div> </div>
</div> </div>
); );

View File

@ -23,6 +23,10 @@ const StyledWrapper = styled.div`
padding: 8px 10px; padding: 8px 10px;
border-left: solid 2px transparent; border-left: solid 2px transparent;
text-decoration: none; text-decoration: none;
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
&:hover { &:hover {
text-decoration: none; text-decoration: none;

View File

@ -8,8 +8,10 @@ import ImportEnvironment from '../ImportEnvironment';
import ManageSecrets from '../ManageSecrets'; import ManageSecrets from '../ManageSecrets';
import StyledWrapper from './StyledWrapper'; import StyledWrapper from './StyledWrapper';
import ConfirmSwitchEnv from './ConfirmSwitchEnv'; import ConfirmSwitchEnv from './ConfirmSwitchEnv';
import ToolHint from 'components/ToolHint';
import { isEqual } from 'lodash';
const EnvironmentList = ({ selectedEnvironment, setSelectedEnvironment, collection, isModified, setIsModified }) => { const EnvironmentList = ({ selectedEnvironment, setSelectedEnvironment, collection, isModified, setIsModified, onClose }) => {
const { environments } = collection; const { environments } = collection;
const [openCreateModal, setOpenCreateModal] = useState(false); const [openCreateModal, setOpenCreateModal] = useState(false);
const [openImportModal, setOpenImportModal] = useState(false); const [openImportModal, setOpenImportModal] = useState(false);
@ -23,6 +25,11 @@ const EnvironmentList = ({ selectedEnvironment, setSelectedEnvironment, collecti
useEffect(() => { useEffect(() => {
if (selectedEnvironment) { if (selectedEnvironment) {
const _selectedEnvironment = environments?.find(env => env?.uid === selectedEnvironment?.uid);
const hasSelectedEnvironmentChanged = !isEqual(selectedEnvironment, _selectedEnvironment);
if (hasSelectedEnvironmentChanged) {
setSelectedEnvironment(_selectedEnvironment);
}
setOriginalEnvironmentVariables(selectedEnvironment.variables); setOriginalEnvironmentVariables(selectedEnvironment.variables);
return; return;
} }
@ -103,13 +110,15 @@ const EnvironmentList = ({ selectedEnvironment, setSelectedEnvironment, collecti
{environments && {environments &&
environments.length && environments.length &&
environments.map((env) => ( environments.map((env) => (
<ToolHint key={env.uid} text={env.name} toolhintId={env.uid} place="right">
<div <div
key={env.uid} id={env.uid}
className={selectedEnvironment.uid === env.uid ? 'environment-item active' : 'environment-item'} className={selectedEnvironment.uid === env.uid ? 'environment-item active' : 'environment-item'}
onClick={() => handleEnvironmentClick(env)} // Use handleEnvironmentClick to handle clicks onClick={() => handleEnvironmentClick(env)} // Use handleEnvironmentClick to handle clicks
> >
<span className="break-all">{env.name}</span> <span className="break-all">{env.name}</span>
</div> </div>
</ToolHint>
))} ))}
<div className="btn-create-environment" onClick={() => handleCreateEnvClick()}> <div className="btn-create-environment" onClick={() => handleCreateEnvClick()}>
+ <span>Create</span> + <span>Create</span>
@ -132,6 +141,7 @@ const EnvironmentList = ({ selectedEnvironment, setSelectedEnvironment, collecti
collection={collection} collection={collection}
setIsModified={setIsModified} setIsModified={setIsModified}
originalEnvironmentVariables={originalEnvironmentVariables} originalEnvironmentVariables={originalEnvironmentVariables}
onClose={onClose}
/> />
</div> </div>
</StyledWrapper> </StyledWrapper>

View File

@ -72,6 +72,7 @@ const EnvironmentSettings = ({ collection, onClose }) => {
collection={collection} collection={collection}
isModified={isModified} isModified={isModified}
setIsModified={setIsModified} setIsModified={setIsModified}
onClose={onClose}
/> />
</Modal> </Modal>
); );

View File

@ -1,12 +1,9 @@
import styled from 'styled-components'; import styled from 'styled-components';
const StyledWrapper = styled.div` const StyledWrapper = styled.div`
table { .editing-mode {
td { cursor: pointer;
&:first-child { color: ${(props) => props.theme.colors.text.yellow};
width: 120px;
}
}
} }
`; `;

View File

@ -0,0 +1,66 @@
import 'github-markdown-css/github-markdown.css';
import get from 'lodash/get';
import { updateFolderDocs } from 'providers/ReduxStore/slices/collections';
import { useTheme } from 'providers/Theme';
import { useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { saveFolderRoot } from 'providers/ReduxStore/slices/collections/actions';
import Markdown from 'components/MarkDown';
import CodeEditor from 'components/CodeEditor';
import StyledWrapper from './StyledWrapper';
const Documentation = ({ collection, folder }) => {
const dispatch = useDispatch();
const { displayedTheme } = useTheme();
const preferences = useSelector((state) => state.app.preferences);
const [isEditing, setIsEditing] = useState(false);
const docs = get(folder, 'root.docs', '');
const toggleViewMode = () => {
setIsEditing((prev) => !prev);
};
const onEdit = (value) => {
dispatch(
updateFolderDocs({
folderUid: folder.uid,
collectionUid: collection.uid,
docs: value
})
);
};
const onSave = () => dispatch(saveFolderRoot(collection.uid, folder.uid));
if (!folder) {
return null;
}
return (
<StyledWrapper className="mt-1 h-full w-full relative flex flex-col">
<div className="editing-mode flex justify-between items-center" role="tab" onClick={toggleViewMode}>
{isEditing ? 'Preview' : 'Edit'}
</div>
{isEditing ? (
<div className="mt-2 flex-1 max-h-[70vh]">
<CodeEditor
collection={collection}
theme={displayedTheme}
value={docs || ''}
onEdit={onEdit}
onSave={onSave}
mode="application/text"
/>
<button type="submit" className="submit btn btn-sm btn-secondary my-6" onClick={onSave}>
Save
</button>
</div>
) : (
<Markdown collectionPath={collection.pathname} onDoubleClick={toggleViewMode} content={docs} />
)}
</StyledWrapper>
);
};
export default Documentation;

View File

@ -7,6 +7,7 @@ import Script from './Script';
import Tests from './Tests'; import Tests from './Tests';
import StyledWrapper from './StyledWrapper'; import StyledWrapper from './StyledWrapper';
import Vars from './Vars'; import Vars from './Vars';
import Documentation from './Documentation';
import DotIcon from 'components/Icons/Dot'; import DotIcon from 'components/Icons/Dot';
const ContentIndicator = () => { const ContentIndicator = () => {
@ -60,6 +61,9 @@ const FolderSettings = ({ collection, folder }) => {
case 'vars': { case 'vars': {
return <Vars collection={collection} folder={folder} />; return <Vars collection={collection} folder={folder} />;
} }
case 'docs': {
return <Documentation collection={collection} folder={folder} />;
}
} }
}; };
@ -89,6 +93,9 @@ const FolderSettings = ({ collection, folder }) => {
Vars Vars
{activeVarsCount > 0 && <sup className="ml-1 font-medium">{activeVarsCount}</sup>} {activeVarsCount > 0 && <sup className="ml-1 font-medium">{activeVarsCount}</sup>}
</div> </div>
<div className={getTabClassname('docs')} role="tab" onClick={() => setTab('docs')}>
Docs
</div>
</div> </div>
<section className={`flex mt-4 h-full`}>{getTabPanel(tab)}</section> <section className={`flex mt-4 h-full`}>{getTabPanel(tab)}</section>
</div> </div>

View File

@ -23,7 +23,7 @@ const EnvironmentSelector = () => {
<ToolHint text="Global Environments" toolhintId="GlobalEnvironmentsToolhintId" className='flex flex-row'> <ToolHint text="Global Environments" toolhintId="GlobalEnvironmentsToolhintId" className='flex flex-row'>
<IconWorld className="globe" size={16} strokeWidth={1.5} /> <IconWorld className="globe" size={16} strokeWidth={1.5} />
{ {
activeEnvironment ? <div>{activeEnvironment?.name}</div> : null activeEnvironment ? <div className='text-nowrap truncate max-w-32'>{activeEnvironment?.name}</div> : null
} }
</ToolHint> </ToolHint>
</div> </div>

View File

@ -2,12 +2,19 @@ import React, { useEffect, useRef } from 'react';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
import { useFormik } from 'formik'; import { useFormik } from 'formik';
import * as Yup from 'yup'; import * as Yup from 'yup';
import { useDispatch } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import Portal from 'components/Portal'; import Portal from 'components/Portal';
import Modal from 'components/Modal'; import Modal from 'components/Modal';
import { addGlobalEnvironment } from 'providers/ReduxStore/slices/global-environments'; import { addGlobalEnvironment } from 'providers/ReduxStore/slices/global-environments';
const CreateEnvironment = ({ onClose }) => { const CreateEnvironment = ({ onClose }) => {
const globalEnvs = useSelector((state) => state?.globalEnvironments?.globalEnvironments);
const validateEnvironmentName = (name) => {
const trimmedName = name?.toLowerCase().trim();
return globalEnvs.every((env) => env?.name?.toLowerCase().trim() !== trimmedName);
};
const dispatch = useDispatch(); const dispatch = useDispatch();
const inputRef = useRef(); const inputRef = useRef();
const formik = useFormik({ const formik = useFormik({
@ -17,9 +24,10 @@ const CreateEnvironment = ({ onClose }) => {
}, },
validationSchema: Yup.object({ validationSchema: Yup.object({
name: Yup.string() name: Yup.string()
.min(1, 'must be at least 1 character') .min(1, 'Must be at least 1 character')
.max(50, 'must be 50 characters or less') .max(50, 'Must be 50 characters or less')
.required('name is required') .required('Name is required')
.test('duplicate-name', 'Global Environment already exists', validateEnvironmentName)
}), }),
onSubmit: (values) => { onSubmit: (values) => {
dispatch(addGlobalEnvironment({ name: values.name })) dispatch(addGlobalEnvironment({ name: values.name }))

View File

@ -23,6 +23,10 @@ const StyledWrapper = styled.div`
padding: 8px 10px; padding: 8px 10px;
border-left: solid 2px transparent; border-left: solid 2px transparent;
text-decoration: none; text-decoration: none;
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
&:hover { &:hover {
text-decoration: none; text-decoration: none;

View File

@ -8,6 +8,7 @@ import ConfirmSwitchEnv from './ConfirmSwitchEnv';
import ManageSecrets from 'components/Environments/EnvironmentSettings/ManageSecrets/index'; import ManageSecrets from 'components/Environments/EnvironmentSettings/ManageSecrets/index';
import ImportEnvironment from '../ImportEnvironment'; import ImportEnvironment from '../ImportEnvironment';
import { isEqual } from 'lodash'; import { isEqual } from 'lodash';
import ToolHint from 'components/ToolHint/index';
const EnvironmentList = ({ environments, activeEnvironmentUid, selectedEnvironment, setSelectedEnvironment, isModified, setIsModified }) => { const EnvironmentList = ({ environments, activeEnvironmentUid, selectedEnvironment, setSelectedEnvironment, isModified, setIsModified }) => {
const [openCreateModal, setOpenCreateModal] = useState(false); const [openCreateModal, setOpenCreateModal] = useState(false);
@ -112,13 +113,15 @@ const EnvironmentList = ({ environments, activeEnvironmentUid, selectedEnvironme
{environments && {environments &&
environments.length && environments.length &&
environments.map((env) => ( environments.map((env) => (
<ToolHint key={env.uid} text={env.name} toolhintId={env.uid} place="right">
<div <div
key={env.uid} id={env.uid}
className={selectedEnvironment.uid === env.uid ? 'environment-item active' : 'environment-item'} className={selectedEnvironment.uid === env.uid ? 'environment-item active' : 'environment-item'}
onClick={() => handleEnvironmentClick(env)} // Use handleEnvironmentClick to handle clicks onClick={() => handleEnvironmentClick(env)} // Use handleEnvironmentClick to handle click
> >
<span className="break-all">{env.name}</span> <span className="break-all">{env.name}</span>
</div> </div>
</ToolHint>
))} ))}
<div className="btn-create-environment" onClick={() => handleCreateEnvClick()}> <div className="btn-create-environment" onClick={() => handleCreateEnvClick()}>
+ <span>Create</span> + <span>Create</span>

View File

@ -9,7 +9,6 @@ const StyledMarkdownBodyWrapper = styled.div`
box-sizing: border-box; box-sizing: border-box;
height: 100%; height: 100%;
margin: 0 auto; margin: 0 auto;
padding-top: 0.5rem;
font-size: 0.875rem; font-size: 0.875rem;
h1 { h1 {
@ -55,7 +54,7 @@ const StyledMarkdownBodyWrapper = styled.div`
height: 1px; height: 1px;
padding: 0; padding: 0;
margin: 24px 0; margin: 24px 0;
background-color: var(--color-border-default); background-color: var(--color-sidebar-collection-item-active-indent-border);
border: 0; border: 0;
} }
@ -80,12 +79,6 @@ const StyledMarkdownBodyWrapper = styled.div`
} }
} }
} }
@media (max-width: 767px) {
.markdown-body {
padding: 15px;
}
}
`; `;
export default StyledMarkdownBodyWrapper; export default StyledMarkdownBodyWrapper;

View File

@ -62,7 +62,7 @@ const Modal = ({
confirmText, confirmText,
cancelText, cancelText,
handleCancel, handleCancel,
handleConfirm, handleConfirm = () => {},
children, children,
confirmDisabled, confirmDisabled,
hideCancel, hideCancel,
@ -103,7 +103,7 @@ const Modal = ({
return () => { return () => {
document.removeEventListener('keydown', handleKeydown); document.removeEventListener('keydown', handleKeydown);
}; };
}, [disableEscapeKey, document]); }, [disableEscapeKey, document, handleConfirm]);
let classes = 'bruno-modal'; let classes = 'bruno-modal';
if (isClosing) { if (isClosing) {

View File

@ -13,9 +13,14 @@ const StyledWrapper = styled.div`
line-height: 30px; line-height: 30px;
overflow: hidden; overflow: hidden;
pre.CodeMirror-placeholder {
color: ${(props) => props.theme.text};
padding-left: 0;
opacity: 0.5;
}
.CodeMirror-scroll { .CodeMirror-scroll {
overflow: hidden !important; overflow: visible !important;
${'' /* padding-bottom: 50px !important; */}
position: relative; position: relative;
display: block; display: block;
margin: 0px; margin: 0px;

View File

@ -30,6 +30,7 @@ class MultiLineEditor extends Component {
lineWrapping: false, lineWrapping: false,
lineNumbers: false, lineNumbers: false,
theme: this.props.theme === 'dark' ? 'monokai' : 'default', theme: this.props.theme === 'dark' ? 'monokai' : 'default',
placeholder: this.props.placeholder,
mode: 'brunovariables', mode: 'brunovariables',
brunoVarInfo: { brunoVarInfo: {
variables variables

View File

@ -1,8 +1,6 @@
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
function Portal({ children, wrapperId }) { function Portal({ children }) {
wrapperId = wrapperId || 'bruno-app-body'; return createPortal(children, document.body);
return createPortal(children, document.getElementById(wrapperId));
} }
export default Portal; export default Portal;

View File

@ -90,7 +90,10 @@ const General = ({ close }) => {
}; };
const addCaCertificate = (e) => { const addCaCertificate = (e) => {
formik.setFieldValue('customCaCertificate.filePath', e.target.files[0]?.path); const filePath = window?.ipcRenderer?.getFilePath(e?.target?.files?.[0]);
if (filePath) {
formik.setFieldValue('customCaCertificate.filePath', filePath);
}
}; };
const deleteCaCertificate = () => { const deleteCaCertificate = () => {

View File

@ -20,6 +20,7 @@ import React from 'react';
* endsWith : ends with * endsWith : ends with
* between : between * between : between
* isEmpty : is empty * isEmpty : is empty
* isNotEmpty : is not empty
* isNull : is null * isNull : is null
* isUndefined : is undefined * isUndefined : is undefined
* isDefined : is defined * isDefined : is defined
@ -51,6 +52,7 @@ const AssertionOperator = ({ operator, onChange }) => {
'endsWith', 'endsWith',
'between', 'between',
'isEmpty', 'isEmpty',
'isNotEmpty',
'isNull', 'isNull',
'isUndefined', 'isUndefined',
'isDefined', 'isDefined',

View File

@ -24,6 +24,7 @@ import { useTheme } from 'providers/Theme';
* endsWith : ends with * endsWith : ends with
* between : between * between : between
* isEmpty : is empty * isEmpty : is empty
* isNotEmpty : is not empty
* isNull : is null * isNull : is null
* isUndefined : is undefined * isUndefined : is undefined
* isDefined : is defined * isDefined : is defined
@ -61,6 +62,7 @@ const parseAssertionOperator = (str = '') => {
'endsWith', 'endsWith',
'between', 'between',
'isEmpty', 'isEmpty',
'isNotEmpty',
'isNull', 'isNull',
'isUndefined', 'isUndefined',
'isDefined', 'isDefined',
@ -75,6 +77,7 @@ const parseAssertionOperator = (str = '') => {
const unaryOperators = [ const unaryOperators = [
'isEmpty', 'isEmpty',
'isNotEmpty',
'isNull', 'isNull',
'isUndefined', 'isUndefined',
'isDefined', 'isDefined',
@ -87,7 +90,7 @@ const parseAssertionOperator = (str = '') => {
'isArray' 'isArray'
]; ];
const [operator, ...rest] = str.trim().split(' '); const [operator, ...rest] = str.split(' ');
const value = rest.join(' '); const value = rest.join(' ');
if (unaryOperators.includes(operator)) { if (unaryOperators.includes(operator)) {
@ -113,6 +116,7 @@ const parseAssertionOperator = (str = '') => {
const isUnaryOperator = (operator) => { const isUnaryOperator = (operator) => {
const unaryOperators = [ const unaryOperators = [
'isEmpty', 'isEmpty',
'isNotEmpty',
'isNull', 'isNull',
'isUndefined', 'isUndefined',
'isDefined', 'isDefined',
@ -142,19 +146,8 @@ const AssertionRow = ({
const { operator, value } = parseAssertionOperator(assertion.value); const { operator, value } = parseAssertionOperator(assertion.value);
return ( return (
<tr key={assertion.uid}> <>
<td>
<input
type="text"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={assertion.name}
className="mousetrap"
onChange={(e) => handleAssertionChange(e, assertion, 'name')}
/>
</td>
<td> <td>
<AssertionOperator <AssertionOperator
operator={operator} operator={operator}
@ -162,7 +155,7 @@ const AssertionRow = ({
handleAssertionChange( handleAssertionChange(
{ {
target: { target: {
value: `${op} ${value}` value: isUnaryOperator(op) ? op : `${op} ${value}`
} }
}, },
assertion, assertion,
@ -178,7 +171,7 @@ const AssertionRow = ({
theme={storedTheme} theme={storedTheme}
readOnly={true} readOnly={true}
onSave={onSave} onSave={onSave}
onChange={(newValue) => onChange={(newValue) => {
handleAssertionChange( handleAssertionChange(
{ {
target: { target: {
@ -189,6 +182,7 @@ const AssertionRow = ({
'value' 'value'
) )
} }
}
onRun={handleRun} onRun={handleRun}
collection={collection} collection={collection}
item={item} item={item}
@ -211,7 +205,7 @@ const AssertionRow = ({
</button> </button>
</div> </div>
</td> </td>
</tr> </>
); );
}; };

View File

@ -4,6 +4,7 @@ const Wrapper = styled.div`
table { table {
width: 100%; width: 100%;
border-collapse: collapse; border-collapse: collapse;
font-weight: 600;
table-layout: fixed; table-layout: fixed;
thead, thead,
@ -15,24 +16,15 @@ const Wrapper = styled.div`
color: ${(props) => props.theme.table.thead.color}; color: ${(props) => props.theme.table.thead.color};
font-size: 0.8125rem; font-size: 0.8125rem;
user-select: none; user-select: none;
font-weight: 600;
} }
td { td {
padding: 6px 10px; padding: 6px 10px;
&:nth-child(1) {
width: 30%;
}
&:nth-child(4) {
width: 70px;
} }
select { select {
background-color: transparent; background-color: transparent;
} }
} }
}
.btn-add-assertion { .btn-add-assertion {
font-size: 0.8125rem; font-size: 0.8125rem;
@ -42,7 +34,8 @@ const Wrapper = styled.div`
width: 100%; width: 100%;
border: solid 1px transparent; border: solid 1px transparent;
outline: none !important; outline: none !important;
background-color: inherit; color: ${(props) => props.theme.table.input.color};
background: transparent;
&:focus { &:focus {
outline: none !important; outline: none !important;

View File

@ -6,6 +6,9 @@ import { addAssertion, updateAssertion, deleteAssertion } from 'providers/ReduxS
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions'; import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import AssertionRow from './AssertionRow'; import AssertionRow from './AssertionRow';
import StyledWrapper from './StyledWrapper'; import StyledWrapper from './StyledWrapper';
import Table from 'components/Table/index';
import ReorderTable from 'components/ReorderTable/index';
import { moveAssertion } from 'providers/ReduxStore/slices/collections/index';
const Assertions = ({ item, collection }) => { const Assertions = ({ item, collection }) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
@ -57,21 +60,43 @@ const Assertions = ({ item, collection }) => {
); );
}; };
const handleAssertionDrag = ({ updateReorderedItem }) => {
dispatch(
moveAssertion({
collectionUid: collection.uid,
itemUid: item.uid,
updateReorderedItem
})
);
};
return ( return (
<StyledWrapper className="w-full"> <StyledWrapper className="w-full">
<table> <Table
<thead> headers={[
<tr> { name: 'Expr', accessor: 'expr', width: '30%' },
<td>Expr</td> { name: 'Operator', accessor: 'operator', width: '120px' },
<td>Operator</td> { name: 'Value', accessor: 'value', width: '30%' },
<td>Value</td> { name: '', accessor: '', width: '15%' }
<td></td> ]}
</tr> >
</thead> <ReorderTable updateReorderedItem={handleAssertionDrag}>
<tbody>
{assertions && assertions.length {assertions && assertions.length
? assertions.map((assertion) => { ? assertions.map((assertion) => {
return ( return (
<tr key={assertion.uid} data-uid={assertion.uid}>
<td className='flex relative'>
<input
type="text"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={assertion.name}
className="mousetrap"
onChange={(e) => handleAssertionChange(e, assertion, 'name')}
/>
</td>
<AssertionRow <AssertionRow
key={assertion.uid} key={assertion.uid}
assertion={assertion} assertion={assertion}
@ -82,11 +107,12 @@ const Assertions = ({ item, collection }) => {
onSave={onSave} onSave={onSave}
handleRun={handleRun} handleRun={handleRun}
/> />
</tr>
); );
}) })
: null} : null}
</tbody> </ReorderTable>
</table> </Table>
<button className="btn-add-assertion text-link pr-2 py-3 mt-2 select-none" onClick={handleAddAssertion}> <button className="btn-add-assertion text-link pr-2 py-3 mt-2 select-none" onClick={handleAddAssertion}>
+ Add Assertion + Add Assertion
</button> </button>

View File

@ -70,6 +70,15 @@ const AuthMode = ({ item, collection }) => {
> >
Digest Auth Digest Auth
</div> </div>
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef?.current?.hide();
onModeChange('ntlm');
}}
>
NTLM Auth
</div>
<div <div
className="dropdown-item" className="dropdown-item"
onClick={() => { onClick={() => {

View File

@ -0,0 +1,17 @@
import styled from 'styled-components';
const Wrapper = styled.div`
label {
font-size: 0.8125rem;
}
.single-line-editor-wrapper {
max-width: 400px;
padding: 0.15rem 0.4rem;
border-radius: 3px;
border: solid 1px ${(props) => props.theme.input.border};
background-color: ${(props) => props.theme.input.bg};
}
`;
export default Wrapper;

View File

@ -0,0 +1,110 @@
import React from 'react';
import get from 'lodash/get';
import { useTheme } from 'providers/Theme';
import { useDispatch } from 'react-redux';
import SingleLineEditor from 'components/SingleLineEditor';
import { updateAuth } from 'providers/ReduxStore/slices/collections';
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';
const NTLMAuth = ({ item, collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const ntlmAuth = item.draft ? get(item, 'draft.request.auth.ntlm', {}) : get(item, 'request.auth.ntlm', {});
const handleRun = () => dispatch(sendRequest(item, collection.uid));
const handleSave = () => dispatch(saveRequest(item.uid, collection.uid));
const handleUsernameChange = (username) => {
dispatch(
updateAuth({
mode: 'ntlm',
collectionUid: collection.uid,
itemUid: item.uid,
content: {
username: username,
password: ntlmAuth.password,
domain: ntlmAuth.domain
}
})
);
};
const handlePasswordChange = (password) => {
dispatch(
updateAuth({
mode: 'ntlm',
collectionUid: collection.uid,
itemUid: item.uid,
content: {
username: ntlmAuth.username,
password: password,
domain: ntlmAuth.domain
}
})
);
};
const handleDomainChange = (domain) => {
dispatch(
updateAuth({
mode: 'ntlm',
collectionUid: collection.uid,
itemUid: item.uid,
content: {
username: ntlmAuth.username,
password: ntlmAuth.password,
domain: domain
}
})
);
};
return (
<StyledWrapper className="mt-2 w-full">
<label className="block font-medium mb-2">Username</label>
<div className="single-line-editor-wrapper mb-2">
<SingleLineEditor
value={ntlmAuth.username || ''}
theme={storedTheme}
onSave={handleSave}
onChange={(val) => handleUsernameChange(val)}
onRun={handleRun}
collection={collection}
item={item}
/>
</div>
<label className="block font-medium mb-2">Password</label>
<div className="single-line-editor-wrapper">
<SingleLineEditor
value={ntlmAuth.password || ''}
theme={storedTheme}
onSave={handleSave}
onChange={(val) => handlePasswordChange(val)}
onRun={handleRun}
collection={collection}
item={item}
isSecret={true}
/>
</div>
<label className="block font-medium mb-2">Domain</label>
<div className="single-line-editor-wrapper">
<SingleLineEditor
value={ntlmAuth.domain || ''}
theme={storedTheme}
onSave={handleSave}
onChange={(val) => handleDomainChange(val)}
onRun={handleRun}
collection={collection}
item={item}
/>
</div>
</StyledWrapper>
);
};
export default NTLMAuth;

View File

@ -6,6 +6,8 @@ import BearerAuth from './BearerAuth';
import BasicAuth from './BasicAuth'; import BasicAuth from './BasicAuth';
import DigestAuth from './DigestAuth'; import DigestAuth from './DigestAuth';
import WsseAuth from './WsseAuth'; import WsseAuth from './WsseAuth';
import NTLMAuth from './NTLMAuth';
import ApiKeyAuth from './ApiKeyAuth'; import ApiKeyAuth from './ApiKeyAuth';
import StyledWrapper from './StyledWrapper'; import StyledWrapper from './StyledWrapper';
import { humanizeRequestAuthMode } from 'utils/collections/index'; import { humanizeRequestAuthMode } from 'utils/collections/index';
@ -31,6 +33,9 @@ const Auth = ({ item, collection }) => {
case 'digest': { case 'digest': {
return <DigestAuth collection={collection} item={item} />; return <DigestAuth collection={collection} item={item} />;
} }
case 'ntlm': {
return <NTLMAuth collection={collection} item={item} />;
}
case 'oauth2': { case 'oauth2': {
return <OAuth2 collection={collection} item={item} />; return <OAuth2 collection={collection} item={item} />;
} }

View File

@ -19,14 +19,6 @@ const Wrapper = styled.div`
} }
td { td {
padding: 6px 10px; padding: 6px 10px;
&:nth-child(1) {
width: 30%;
}
&:nth-child(3) {
width: 70px;
}
} }
} }

View File

@ -7,11 +7,14 @@ import { useTheme } from 'providers/Theme';
import { import {
addFormUrlEncodedParam, addFormUrlEncodedParam,
updateFormUrlEncodedParam, updateFormUrlEncodedParam,
deleteFormUrlEncodedParam deleteFormUrlEncodedParam,
moveFormUrlEncodedParam
} from 'providers/ReduxStore/slices/collections'; } from 'providers/ReduxStore/slices/collections';
import MultiLineEditor from 'components/MultiLineEditor'; import MultiLineEditor from 'components/MultiLineEditor';
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions'; import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper'; import StyledWrapper from './StyledWrapper';
import ReorderTable from 'components/ReorderTable/index';
import Table from 'components/Table/index';
const FormUrlEncodedParams = ({ item, collection }) => { const FormUrlEncodedParams = ({ item, collection }) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
@ -64,22 +67,31 @@ const FormUrlEncodedParams = ({ item, collection }) => {
); );
}; };
const handleParamDrag = ({ updateReorderedItem }) => {
dispatch(
moveFormUrlEncodedParam({
collectionUid: collection.uid,
itemUid: item.uid,
updateReorderedItem
})
);
};
return ( return (
<StyledWrapper className="w-full"> <StyledWrapper className="w-full">
<table> <Table
<thead> headers={[
<tr> { name: 'Key', accessor: 'key', width: '40%' },
<td>Key</td> { name: 'Value', accessor: 'value', width: '46%' },
<td>Value</td> { name: '', accessor: '', width: '14%' }
<td></td> ]}
</tr> >
</thead> <ReorderTable updateReorderedItem={handleParamDrag}>
<tbody>
{params && params.length {params && params.length
? params.map((param, index) => { ? params.map((param, index) => {
return ( return (
<tr key={param.uid}> <tr key={param.uid} data-uid={param.uid}>
<td> <td className='flex relative'>
<input <input
type="text" type="text"
autoComplete="off" autoComplete="off"
@ -131,8 +143,8 @@ const FormUrlEncodedParams = ({ item, collection }) => {
); );
}) })
: null} : null}
</tbody> </ReorderTable>
</table> </Table>
<button className="btn-add-param text-link pr-2 py-3 mt-2 select-none" onClick={addParam}> <button className="btn-add-param text-link pr-2 py-3 mt-2 select-none" onClick={addParam}>
+ Add Param + Add Param
</button> </button>

View File

@ -19,14 +19,6 @@ const Wrapper = styled.div`
} }
td { td {
padding: 6px 10px; padding: 6px 10px;
&:nth-child(1) {
width: 30%;
}
&:nth-child(3) {
width: 70px;
}
} }
} }

View File

@ -7,12 +7,15 @@ import { useTheme } from 'providers/Theme';
import { import {
addMultipartFormParam, addMultipartFormParam,
updateMultipartFormParam, updateMultipartFormParam,
deleteMultipartFormParam deleteMultipartFormParam,
moveMultipartFormParam
} from 'providers/ReduxStore/slices/collections'; } from 'providers/ReduxStore/slices/collections';
import MultiLineEditor from 'components/MultiLineEditor'; import MultiLineEditor from 'components/MultiLineEditor';
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions'; import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper'; import StyledWrapper from './StyledWrapper';
import FilePickerEditor from 'components/FilePickerEditor'; import FilePickerEditor from 'components/FilePickerEditor';
import Table from 'components/Table/index';
import ReorderTable from 'components/ReorderTable/index';
const MultipartFormParams = ({ item, collection }) => { const MultipartFormParams = ({ item, collection }) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
@ -54,6 +57,10 @@ const MultipartFormParams = ({ item, collection }) => {
param.value = e.target.value; param.value = e.target.value;
break; break;
} }
case 'contentType': {
param.contentType = e.target.value;
break;
}
case 'enabled': { case 'enabled': {
param.enabled = e.target.checked; param.enabled = e.target.checked;
break; break;
@ -78,22 +85,32 @@ const MultipartFormParams = ({ item, collection }) => {
); );
}; };
const handleParamDrag = ({ updateReorderedItem }) => {
dispatch(
moveMultipartFormParam({
collectionUid: collection.uid,
itemUid: item.uid,
updateReorderedItem
})
);
};
return ( return (
<StyledWrapper className="w-full"> <StyledWrapper className="w-full">
<table> <Table
<thead> headers={[
<tr> { name: 'Key', accessor: 'key', width: '29%' },
<td>Key</td> { name: 'Value', accessor: 'value', width: '29%' },
<td>Value</td> { name: 'Content-Type', accessor: 'content-type', width: '28%' },
<td></td> { name: '', accessor: '', width: '14%' }
</tr> ]}
</thead> >
<tbody> <ReorderTable updateReorderedItem={handleParamDrag}>
{params && params.length {params && params.length
? params.map((param, index) => { ? params.map((param, index) => {
return ( return (
<tr key={param.uid}> <tr key={param.uid} className='w-full' data-uid={param.uid}>
<td> <td className="flex relative">
<input <input
type="text" type="text"
autoComplete="off" autoComplete="off"
@ -145,6 +162,27 @@ const MultipartFormParams = ({ item, collection }) => {
/> />
)} )}
</td> </td>
<td>
<MultiLineEditor
onSave={onSave}
theme={storedTheme}
placeholder="Auto"
value={param.contentType}
onChange={(newValue) =>
handleParamChange(
{
target: {
value: newValue
}
},
param,
'contentType'
)
}
onRun={handleRun}
collection={collection}
/>
</td>
<td> <td>
<div className="flex items-center"> <div className="flex items-center">
<input <input
@ -163,8 +201,8 @@ const MultipartFormParams = ({ item, collection }) => {
); );
}) })
: null} : null}
</tbody> </ReorderTable>
</table> </Table>
<div> <div>
<button className="btn-add-param text-link pr-2 pt-3 mt-2 select-none" onClick={addParam}> <button className="btn-add-param text-link pr-2 pt-3 mt-2 select-none" onClick={addParam}>
+ Add Param + Add Param

View File

@ -103,7 +103,7 @@ const QueryParams = ({ item, collection }) => {
); );
}; };
const handleParamDrag = ({ updateReorderedItem }) => { const handleQueryParamDrag = ({ updateReorderedItem }) => {
dispatch( dispatch(
moveQueryParam({ moveQueryParam({
collectionUid: collection.uid, collectionUid: collection.uid,
@ -117,7 +117,6 @@ const QueryParams = ({ item, collection }) => {
<StyledWrapper className="w-full flex flex-col absolute"> <StyledWrapper className="w-full flex flex-col absolute">
<div className="flex-1 mt-2"> <div className="flex-1 mt-2">
<div className="mb-1 title text-xs">Query</div> <div className="mb-1 title text-xs">Query</div>
<Table <Table
headers={[ headers={[
{ name: 'Name', accessor: 'name', width: '31%' }, { name: 'Name', accessor: 'name', width: '31%' },
@ -125,7 +124,7 @@ const QueryParams = ({ item, collection }) => {
{ name: '', accessor: '', width: '13%' } { name: '', accessor: '', width: '13%' }
]} ]}
> >
<ReorderTable updateReorderedItem={handleParamDrag}> <ReorderTable updateReorderedItem={handleQueryParamDrag}>
{queryParams && queryParams.length {queryParams && queryParams.length
? queryParams.map((param, index) => ( ? queryParams.map((param, index) => (
<tr key={param.uid} data-uid={param.uid}> <tr key={param.uid} data-uid={param.uid}>
@ -153,7 +152,7 @@ const QueryParams = ({ item, collection }) => {
/> />
</td> </td>
<td> <td>
<div className="flex items-center"> <div className="flex items-center justify-center">
<input <input
type="checkbox" type="checkbox"
checked={param.enabled} checked={param.enabled}
@ -241,11 +240,7 @@ const QueryParams = ({ item, collection }) => {
: null} : null}
</tbody> </tbody>
</table> </table>
{!(pathParams && pathParams.length) ? {!(pathParams && pathParams.length) ? <div className="title pr-2 py-3 mt-2 text-xs"></div> : null}
<div className="title pr-2 py-3 mt-2 text-xs">
</div>
: null}
</div> </div>
</StyledWrapper> </StyledWrapper>
); );

View File

@ -70,7 +70,7 @@ const QueryUrl = ({ item, collection, handleRun }) => {
const handleGenerateCode = (e) => { const handleGenerateCode = (e) => {
e.stopPropagation(); e.stopPropagation();
if (item.request.url !== '' || (item.draft?.request.url !== undefined && item.draft?.request.url !== '')) { if (item?.request?.url !== '' || (item.draft?.request?.url !== undefined && item.draft?.request?.url !== '')) {
setGenerateCodeItemModalOpen(true); setGenerateCodeItemModalOpen(true);
} else { } else {
toast.error('URL is required'); toast.error('URL is required');

View File

@ -48,6 +48,7 @@ const RequestBody = ({ item, collection }) => {
<StyledWrapper className="w-full"> <StyledWrapper className="w-full">
<CodeEditor <CodeEditor
collection={collection} collection={collection}
item={item}
theme={displayedTheme} theme={displayedTheme}
font={get(preferences, 'font.codeFont', 'default')} font={get(preferences, 'font.codeFont', 'default')}
fontSize={get(preferences, 'font.codeFontSize')} fontSize={get(preferences, 'font.codeFontSize')}

View File

@ -19,14 +19,6 @@ const Wrapper = styled.div`
} }
td { td {
padding: 6px 10px; padding: 6px 10px;
&:nth-child(1) {
width: 30%;
}
&:nth-child(3) {
width: 70px;
}
} }
} }

View File

@ -4,12 +4,14 @@ import cloneDeep from 'lodash/cloneDeep';
import { IconTrash } from '@tabler/icons'; import { IconTrash } from '@tabler/icons';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import { useTheme } from 'providers/Theme'; import { useTheme } from 'providers/Theme';
import { addRequestHeader, updateRequestHeader, deleteRequestHeader } from 'providers/ReduxStore/slices/collections'; import { addRequestHeader, updateRequestHeader, deleteRequestHeader, moveRequestHeader } from 'providers/ReduxStore/slices/collections';
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions'; import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import SingleLineEditor from 'components/SingleLineEditor'; import SingleLineEditor from 'components/SingleLineEditor';
import StyledWrapper from './StyledWrapper'; import StyledWrapper from './StyledWrapper';
import { headers as StandardHTTPHeaders } from 'know-your-http-well'; import { headers as StandardHTTPHeaders } from 'know-your-http-well';
import { MimeTypes } from 'utils/codemirror/autocompleteConstants'; import { MimeTypes } from 'utils/codemirror/autocompleteConstants';
import Table from 'components/Table/index';
import ReorderTable from 'components/ReorderTable/index';
const headerAutoCompleteList = StandardHTTPHeaders.map((e) => e.header); const headerAutoCompleteList = StandardHTTPHeaders.map((e) => e.header);
const RequestHeaders = ({ item, collection }) => { const RequestHeaders = ({ item, collection }) => {
@ -63,22 +65,31 @@ const RequestHeaders = ({ item, collection }) => {
); );
}; };
const handleHeaderDrag = ({ updateReorderedItem }) => {
dispatch(
moveRequestHeader({
collectionUid: collection.uid,
itemUid: item.uid,
updateReorderedItem
})
);
};
return ( return (
<StyledWrapper className="w-full"> <StyledWrapper className="w-full">
<table> <Table
<thead> headers={[
<tr> { name: 'Key', accessor: 'key', width: '34%' },
<td>Name</td> { name: 'Value', accessor: 'value', width: '46%' },
<td>Value</td> { name: '', accessor: '', width: '20%' }
<td></td> ]}
</tr> >
</thead> <ReorderTable updateReorderedItem={handleHeaderDrag}>
<tbody>
{headers && headers.length {headers && headers.length
? headers.map((header) => { ? headers.map((header) => {
return ( return (
<tr key={header.uid}> <tr key={header.uid} data-uid={header.uid}>
<td> <td className='flex relative'>
<SingleLineEditor <SingleLineEditor
value={header.name} value={header.name}
theme={storedTheme} theme={storedTheme}
@ -140,8 +151,8 @@ const RequestHeaders = ({ item, collection }) => {
); );
}) })
: null} : null}
</tbody> </ReorderTable>
</table> </Table>
<button className="btn-add-header text-link pr-2 py-3 mt-2 select-none" onClick={addHeader}> <button className="btn-add-header text-link pr-2 py-3 mt-2 select-none" onClick={addHeader}>
+ Add Header + Add Header
</button> </button>

View File

@ -19,14 +19,6 @@ const Wrapper = styled.div`
} }
td { td {
padding: 6px 10px; padding: 6px 10px;
&:nth-child(1) {
width: 30%;
}
&:nth-child(3) {
width: 70px;
}
} }
} }
@ -38,7 +30,8 @@ const Wrapper = styled.div`
width: 100%; width: 100%;
border: solid 1px transparent; border: solid 1px transparent;
outline: none !important; outline: none !important;
background-color: inherit; color: ${(props) => props.theme.table.input.color};
background: transparent;
&:focus { &:focus {
outline: none !important; outline: none !important;

View File

@ -3,13 +3,15 @@ import cloneDeep from 'lodash/cloneDeep';
import { IconTrash } from '@tabler/icons'; import { IconTrash } from '@tabler/icons';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import { useTheme } from 'providers/Theme'; import { useTheme } from 'providers/Theme';
import { addVar, updateVar, deleteVar } from 'providers/ReduxStore/slices/collections'; import { addVar, updateVar, deleteVar, moveVar } from 'providers/ReduxStore/slices/collections';
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions'; import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import SingleLineEditor from 'components/SingleLineEditor'; import SingleLineEditor from 'components/SingleLineEditor';
import InfoTip from 'components/InfoTip'; import InfoTip from 'components/InfoTip';
import StyledWrapper from './StyledWrapper'; import StyledWrapper from './StyledWrapper';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
import { variableNameRegex } from 'utils/common/regex'; import { variableNameRegex } from 'utils/common/regex';
import Table from 'components/Table/index';
import ReorderTable from 'components/ReorderTable/index';
const VarsTable = ({ item, collection, vars, varType }) => { const VarsTable = ({ item, collection, vars, varType }) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
@ -73,35 +75,41 @@ const VarsTable = ({ item, collection, vars, varType }) => {
); );
}; };
const handleVarDrag = ({ updateReorderedItem }) => {
dispatch(
moveVar({
type: varType,
collectionUid: collection.uid,
itemUid: item.uid,
updateReorderedItem
})
);
};
return ( return (
<StyledWrapper className="w-full"> <StyledWrapper className="w-full">
<table> <Table
<thead> headers={[
<tr> { name: 'Name', accessor: 'name', width: '40%' },
<td>Name</td> { name: varType === 'request' ? (
{varType === 'request' ? (
<td>
<div className="flex items-center"> <div className="flex items-center">
<span>Value</span> <span>Value</span>
</div> </div>
</td>
) : ( ) : (
<td>
<div className="flex items-center"> <div className="flex items-center">
<span>Expr</span> <span>Expr</span>
<InfoTip text="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> </div>
</td> ), accessor: 'value', width: '46%' },
)} { name: '', accessor: '', width: '14%' }
<td></td> ]}
</tr> >
</thead> <ReorderTable updateReorderedItem={handleVarDrag}>
<tbody>
{vars && vars.length {vars && vars.length
? vars.map((_var) => { ? vars.map((_var) => {
return ( return (
<tr key={_var.uid}> <tr key={_var.uid} data-uid={_var.uid}>
<td> <td className='flex relative'>
<input <input
type="text" type="text"
autoComplete="off" autoComplete="off"
@ -152,8 +160,8 @@ const VarsTable = ({ item, collection, vars, varType }) => {
); );
}) })
: null} : null}
</tbody> </ReorderTable>
</table> </Table>
<button className="btn-add-var text-link pr-2 py-3 mt-2 select-none" onClick={handleAddVar}> <button className="btn-add-var text-link pr-2 py-3 mt-2 select-none" onClick={handleAddVar}>
+ Add + Add
</button> </button>

View File

@ -0,0 +1,19 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
div.card {
background: ${(props) => props.theme.requestTabPanel.card.bg};
border: 1px solid ${(props) => props.theme.requestTabPanel.card.border};
div.hr {
border-bottom: 1px solid ${(props) => props.theme.requestTabPanel.card.hr};
height: 1px;
}
div.border-top {
border-top: 1px solid ${(props) => props.theme.requestTabPanel.card.border};
}
}
`;
export default StyledWrapper;

View File

@ -0,0 +1,47 @@
import { IconLoader2, IconFile } from '@tabler/icons';
import StyledWrapper from './StyledWrapper';
const RequestIsLoading = ({ item }) => {
return <StyledWrapper>
<div className='flex flex-col p-4'>
<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='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>
<div className='hr'/>
<div className='flex items-center gap-2 mt-4'>
<IconLoader2 className="animate-spin" size={16} strokeWidth={2} />
<span>Loading...</span>
</div>
</div>
</div>
</div>
</StyledWrapper>
}
export default RequestIsLoading;

View File

@ -0,0 +1,19 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
div.card {
background: ${(props) => props.theme.requestTabPanel.card.bg};
border: 1px solid ${(props) => props.theme.requestTabPanel.card.border};
div.hr {
border-bottom: 1px solid ${(props) => props.theme.requestTabPanel.card.hr};
height: 1px;
}
div.border-top {
border-top: 1px solid ${(props) => props.theme.requestTabPanel.card.border};
}
}
`;
export default StyledWrapper;

View File

@ -0,0 +1,89 @@
import { IconLoader2, IconFile } from '@tabler/icons';
import { loadRequest, loadRequestViaWorker } from 'providers/ReduxStore/slices/collections/actions';
import { useDispatch } from 'react-redux';
import StyledWrapper from './StyledWrapper';
const RequestNotLoaded = ({ collection, item }) => {
const dispatch = useDispatch();
const handleLoadRequestViaWorker = () => {
!item?.loading && dispatch(loadRequestViaWorker({ collectionUid: collection?.uid, pathname: item?.pathname }));
}
const handleLoadRequest = () => {
!item?.loading && dispatch(loadRequest({ collectionUid: collection?.uid, pathname: item?.pathname }));
}
return <StyledWrapper>
<div className='flex flex-col p-4'>
<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='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='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-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}
>
Load Request
</button>
<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>
</>
)}
{item?.loading && (
<>
<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>
</div>
</>
)}
</div>
</div>
</div>
</StyledWrapper>
}
export default RequestNotLoaded;

View File

@ -20,10 +20,13 @@ import { DocExplorer } from '@usebruno/graphql-docs';
import StyledWrapper from './StyledWrapper'; import StyledWrapper from './StyledWrapper';
import SecuritySettings from 'components/SecuritySettings'; import SecuritySettings from 'components/SecuritySettings';
import FolderSettings from 'components/FolderSettings'; import FolderSettings from 'components/FolderSettings';
import { getGlobalEnvironmentVariables } from 'utils/collections/index'; import { getGlobalEnvironmentVariables, getGlobalEnvironmentVariablesMasked } from 'utils/collections/index';
import { produce } from 'immer'; import { produce } from 'immer';
import get from 'lodash/get'; import get from 'lodash/get';
import { replacePlaceholders } from 'utils/common/variable-replacer'; import { replacePlaceholders } from 'utils/common/variable-replacer';
import CollectionOverview from 'components/CollectionSettings/Overview';
import RequestNotLoaded from './RequestNotLoaded';
import RequestIsLoading from './RequestIsLoading';
const MIN_LEFT_PANE_WIDTH = 300; const MIN_LEFT_PANE_WIDTH = 300;
const MIN_RIGHT_PANE_WIDTH = 350; const MIN_RIGHT_PANE_WIDTH = 350;
@ -41,13 +44,18 @@ const RequestTabPanel = () => {
const _collections = useSelector((state) => state.collections.collections); const _collections = useSelector((state) => state.collections.collections);
// merge `globalEnvironmentVariables` into the active collection and rebuild `collections` immer proxy object // merge `globalEnvironmentVariables` into the active collection and rebuild `collections` immer proxy object
let collections = produce(_collections, draft => { let collections = produce(_collections, (draft) => {
let collection = find(draft, (c) => c.uid === focusedTab?.collectionUid); let collection = find(draft, (c) => c.uid === focusedTab?.collectionUid);
if (collection) { if (collection) {
// add selected global env variables to the collection object // add selected global env variables to the collection object
const globalEnvironmentVariables = getGlobalEnvironmentVariables({ globalEnvironments, activeGlobalEnvironmentUid }); const globalEnvironmentVariables = getGlobalEnvironmentVariables({
globalEnvironments,
activeGlobalEnvironmentUid
});
const globalEnvSecrets = getGlobalEnvironmentVariablesMasked({ globalEnvironments, activeGlobalEnvironmentUid });
collection.globalEnvironmentVariables = globalEnvironmentVariables; collection.globalEnvironmentVariables = globalEnvironmentVariables;
collection.globalEnvSecrets = globalEnvSecrets;
} }
}); });
@ -150,6 +158,11 @@ const RequestTabPanel = () => {
if (focusedTab.type === 'collection-settings') { if (focusedTab.type === 'collection-settings') {
return <CollectionSettings collection={collection} />; return <CollectionSettings collection={collection} />;
} }
if (focusedTab.type === 'collection-overview') {
return <CollectionOverview collection={collection} />;
}
if (focusedTab.type === 'folder-settings') { if (focusedTab.type === 'folder-settings') {
const folder = findItemInCollection(collection, focusedTab.folderUid); const folder = findItemInCollection(collection, focusedTab.folderUid);
return <FolderSettings collection={collection} folder={folder} />; return <FolderSettings collection={collection} folder={folder} />;
@ -164,6 +177,14 @@ const RequestTabPanel = () => {
return <RequestNotFound itemUid={activeTabUid} />; return <RequestNotFound itemUid={activeTabUid} />;
} }
if (item?.partial) {
return <RequestNotLoaded item={item} collection={collection} />
}
if (item?.loading) {
return <RequestIsLoading item={item} />
}
const handleRun = async () => { const handleRun = async () => {
let newItem = JSON.parse(JSON.stringify(item)); let newItem = JSON.parse(JSON.stringify(item));

View File

@ -13,6 +13,14 @@ const SpecialTab = ({ handleCloseClick, type, tabName }) => {
</> </>
); );
} }
case 'collection-overview': {
return (
<>
<IconSettings size={18} strokeWidth={1.5} className="text-yellow-600" />
<span className="ml-1 leading-6">Collection</span>
</>
);
}
case 'security-settings': { case 'security-settings': {
return ( return (
<> <>

View File

@ -70,7 +70,7 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi
}; };
const folder = folderUid ? findItemInCollection(collection, folderUid) : null; const folder = folderUid ? findItemInCollection(collection, folderUid) : null;
if (['collection-settings', 'folder-settings', 'variables', 'collection-runner', 'security-settings'].includes(tab.type)) { if (['collection-settings', 'collection-overview', 'folder-settings', 'variables', 'collection-runner', 'security-settings'].includes(tab.type)) {
return ( return (
<StyledWrapper <StyledWrapper
className="flex items-center justify-between tab-container px-1" className="flex items-center justify-between tab-container px-1"

View File

@ -20,14 +20,14 @@ const formatResponse = (data, mode, filter) => {
} }
if (data === null) { if (data === null) {
return data; return 'null';
} }
if (mode.includes('json')) { if (mode.includes('json')) {
let isValidJSON = false; let isValidJSON = false;
try { try {
isValidJSON = typeof JSON.parse(JSON.stringify(data)) === 'object'; isValidJSON = typeof JSON.parse(JSON.stringify(data)) === 'object'
} catch (error) { } catch (error) {
console.log('Error parsing JSON: ', error.message); console.log('Error parsing JSON: ', error.message);
} }

View File

@ -11,7 +11,7 @@ const ResponseSave = ({ item }) => {
const saveResponseToFile = () => { const saveResponseToFile = () => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
ipcRenderer ipcRenderer
.invoke('renderer:save-response-to-file', response, item.requestSent.url) .invoke('renderer:save-response-to-file', response, item?.requestSent?.url)
.then(resolve) .then(resolve)
.catch((err) => { .catch((err) => {
toast.error(get(err, 'error.message') || 'Something went wrong!'); toast.error(get(err, 'error.message') || 'Something went wrong!');

View File

@ -43,7 +43,7 @@ const Timeline = ({ request, response }) => {
<div className="mt-4"> <div className="mt-4">
<pre className="line response font-bold"> <pre className="line response font-bold">
<span className="arrow">{'<'}</span> {response.status} {response.statusText} <span className="arrow">{'<'}</span> {response.status} - {response.statusText}
</pre> </pre>
{responseHeaders.map((h) => { {responseHeaders.map((h) => {

View File

@ -9,6 +9,7 @@ import { IconRefresh, IconCircleCheck, IconCircleX, IconCheck, IconX, IconRun }
import slash from 'utils/common/slash'; import slash from 'utils/common/slash';
import ResponsePane from './ResponsePane'; import ResponsePane from './ResponsePane';
import StyledWrapper from './StyledWrapper'; import StyledWrapper from './StyledWrapper';
import { areItemsLoading } from 'utils/collections';
const getRelativePath = (fullPath, pathname) => { const getRelativePath = (fullPath, pathname) => {
// convert to unix style path // convert to unix style path
@ -59,7 +60,7 @@ export default function RunnerResults({ collection }) {
pathname: info.pathname, pathname: info.pathname,
relativePath: getRelativePath(collection.pathname, info.pathname) relativePath: getRelativePath(collection.pathname, info.pathname)
}; };
if (newItem.status !== 'error') { if (newItem.status !== 'error' && newItem.status !== 'skipped') {
if (newItem.testResults) { if (newItem.testResults) {
const failed = newItem.testResults.filter((result) => result.status === 'fail'); const failed = newItem.testResults.filter((result) => result.status === 'fail');
newItem.testStatus = failed.length ? 'fail' : 'pass'; newItem.testStatus = failed.length ? 'fail' : 'pass';
@ -106,6 +107,8 @@ export default function RunnerResults({ collection }) {
return (item.status !== 'error' && item.testStatus === 'fail') || item.assertionStatus === 'fail'; return (item.status !== 'error' && item.testStatus === 'fail') || item.assertionStatus === 'fail';
}); });
let isCollectionLoading = areItemsLoading(collection);
if (!items || !items.length) { if (!items || !items.length) {
return ( return (
<StyledWrapper className="px-4 pb-4"> <StyledWrapper className="px-4 pb-4">
@ -116,7 +119,7 @@ export default function RunnerResults({ collection }) {
<div className="mt-6"> <div className="mt-6">
You have <span className="font-medium">{totalRequestsInCollection}</span> requests in this collection. You have <span className="font-medium">{totalRequestsInCollection}</span> requests in this collection.
</div> </div>
{isCollectionLoading ? <div className='my-1 danger'>Requests in this collection are still loading.</div> : null}
<div className="mt-6"> <div className="mt-6">
<label>Delay (in ms)</label> <label>Delay (in ms)</label>
<input <input
@ -163,29 +166,35 @@ export default function RunnerResults({ collection }) {
<div className="pb-2 font-medium test-summary"> <div className="pb-2 font-medium test-summary">
Total Requests: {items.length}, Passed: {passedRequests.length}, Failed: {failedRequests.length} Total Requests: {items.length}, Passed: {passedRequests.length}, Failed: {failedRequests.length}
</div> </div>
{runnerInfo?.statusText ?
<div className="pb-2 font-medium danger">
{runnerInfo?.statusText}
</div>
: null}
{items.map((item) => { {items.map((item) => {
return ( return (
<div key={item.uid}> <div key={item.uid}>
<div className="item-path mt-2"> <div className="item-path mt-2">
<div className="flex items-center"> <div className="flex items-center">
<span> <span>
{item.status !== 'error' && item.testStatus === 'pass' ? ( {item.status !== 'error' && item.testStatus === 'pass' && item.status !== 'skipped' ? (
<IconCircleCheck className="test-success" size={20} strokeWidth={1.5} /> <IconCircleCheck className="test-success" size={20} strokeWidth={1.5} />
) : ( ) : (
<IconCircleX className="test-failure" size={20} strokeWidth={1.5} /> <IconCircleX className="test-failure" size={20} strokeWidth={1.5} />
)} )}
</span> </span>
<span <span
className={`mr-1 ml-2 ${item.status == 'error' || item.testStatus == 'fail' ? 'danger' : ''}`} className={`mr-1 ml-2 ${item.status == 'error' || item.status == 'skipped' || item.testStatus == 'fail' ? 'danger' : ''}`}
> >
{item.relativePath} {item.relativePath}
</span> </span>
{item.status !== 'error' && item.status !== 'completed' ? ( {item.status !== 'error' && item.status !== 'skipped' && item.status !== 'completed' ? (
<IconRefresh className="animate-spin ml-1" size={18} strokeWidth={1.5} /> <IconRefresh className="animate-spin ml-1" size={18} strokeWidth={1.5} />
) : item.responseReceived?.status ? ( ) : item.responseReceived?.status ? (
<span className="text-xs link cursor-pointer" onClick={() => setSelectedItem(item)}> <span className="text-xs link cursor-pointer" onClick={() => setSelectedItem(item)}>
(<span className="mr-1">{item.responseReceived?.status}</span> <span className="mr-1">{item.responseReceived?.status}</span>
<span>{item.responseReceived?.statusText}</span>) -&nbsp;
<span>{item.responseReceived?.statusText}</span>
</span> </span>
) : ( ) : (
<span className="danger text-xs cursor-pointer" onClick={() => setSelectedItem(item)}> <span className="danger text-xs cursor-pointer" onClick={() => setSelectedItem(item)}>

View File

@ -0,0 +1,12 @@
import styled from 'styled-components';
const Wrapper = styled.div`
.partial {
color: ${(props) => props.theme.colors.text.yellow};
}
.error {
color: ${(props) => props.theme.colors.text.danger};
}
`;
export default Wrapper;

View File

@ -0,0 +1,21 @@
import RequestMethod from "../RequestMethod";
import { IconLoader2, IconAlertTriangle, IconAlertCircle } from '@tabler/icons';
import StyledWrapper from "./StyledWrapper";
const CollectionItemIcon = ({ item }) => {
if (item?.error) {
return <StyledWrapper><IconAlertCircle className="w-fit mr-2 error" size={18} strokeWidth={1.5} /></StyledWrapper>;
}
if (item?.loading) {
return <IconLoader2 className="animate-spin w-fit mr-2" size={18} strokeWidth={1.5} />;
}
if (item?.partial) {
return <StyledWrapper><IconAlertTriangle size={18} className="w-fit mr-2 partial" strokeWidth={1.5} /></StyledWrapper>;
}
return <RequestMethod item={item} />;
};
export default CollectionItemIcon;

View File

@ -12,10 +12,15 @@ const DeleteCollectionItem = ({ onClose, item, collection }) => {
const isFolder = isItemAFolder(item); const isFolder = isItemAFolder(item);
const onConfirm = () => { const onConfirm = () => {
dispatch(deleteItem(item.uid, collection.uid)).then(() => { dispatch(deleteItem(item.uid, collection.uid)).then(() => {
if (isFolder) { if (isFolder) {
// close all tabs that belong to the folder
// including the folder itself and its children
const tabUids = [...recursivelyGetAllItemUids(item.items), item.uid]
dispatch( dispatch(
closeTabs({ closeTabs({
tabUids: recursivelyGetAllItemUids(item.items) tabUids: tabUids
}) })
); );
} else { } else {

View File

@ -35,6 +35,28 @@ const StyledWrapper = styled.div`
background-color: ${(props) => props.theme.collection.environment.settings.item.active.hoverBg} !important; background-color: ${(props) => props.theme.collection.environment.settings.item.active.hoverBg} !important;
} }
} }
.flexible-container {
width: 100%;
}
@media (max-width: 600px) {
.flexible-container {
width: 500px;
}
}
@media (min-width: 601px) and (max-width: 1200px) {
.flexible-container {
width: 800px;
}
}
@media (min-width: 1201px) {
.flexible-container {
width: 900px;
}
}
`; `;
export default StyledWrapper; export default StyledWrapper;

View File

@ -48,7 +48,7 @@ const GenerateCodeItem = ({ collection, item, onClose }) => {
return ( return (
<Modal size="lg" title="Generate Code" handleCancel={onClose} hideFooter={true}> <Modal size="lg" title="Generate Code" handleCancel={onClose} hideFooter={true}>
<StyledWrapper> <StyledWrapper>
<div className="flex w-full"> <div className="flex w-full flexible-container">
<div> <div>
<div className="generate-code-sidebar"> <div className="generate-code-sidebar">
{languages && {languages &&
@ -59,7 +59,26 @@ const GenerateCodeItem = ({ collection, item, onClose }) => {
className={ className={
language.name === selectedLanguage.name ? 'generate-code-item active' : 'generate-code-item' language.name === selectedLanguage.name ? 'generate-code-item active' : 'generate-code-item'
} }
role="button"
tabIndex={0}
onClick={() => setSelectedLanguage(language)} onClick={() => setSelectedLanguage(language)}
onKeyDown={(e) => {
if (e.key === 'Tab' || (e.shiftKey && e.key === 'Tab')) {
e.preventDefault();
const currentIndex = languages.findIndex((lang) => lang.name === selectedLanguage.name);
const nextIndex = e.shiftKey
? (currentIndex - 1 + languages.length) % languages.length
: (currentIndex + 1) % languages.length;
setSelectedLanguage(languages[nextIndex]);
// Explicitly focus on the new active element
const nextElement = document.querySelector(`[data-language="${languages[nextIndex].name}"]`);
nextElement?.focus();
}
}}
data-language={language.name}
aria-pressed={language.name === selectedLanguage.name}
> >
<span className="capitalize">{language.name}</span> <span className="capitalize">{language.name}</span>
</div> </div>
@ -69,6 +88,7 @@ const GenerateCodeItem = ({ collection, item, onClose }) => {
<div className="flex-grow p-4"> <div className="flex-grow p-4">
{isValidUrl(finalUrl) ? ( {isValidUrl(finalUrl) ? (
<CodeView <CodeView
tabIndex={-1}
language={selectedLanguage} language={selectedLanguage}
item={{ item={{
...item, ...item,

View File

@ -6,6 +6,7 @@ import { useDispatch } from 'react-redux';
import { isItemAFolder } from 'utils/tabs'; import { isItemAFolder } from 'utils/tabs';
import { renameItem, saveRequest } from 'providers/ReduxStore/slices/collections/actions'; import { renameItem, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
import { closeTabs } from 'providers/ReduxStore/slices/tabs';
const RenameCollectionItem = ({ collection, item, onClose }) => { const RenameCollectionItem = ({ collection, item, onClose }) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
@ -33,7 +34,8 @@ const RenameCollectionItem = ({ collection, item, onClose }) => {
} }
dispatch(renameItem(values.name, item.uid, collection.uid)) dispatch(renameItem(values.name, item.uid, collection.uid))
.then(() => { .then(() => {
toast.success('Request renamed'); isFolder && dispatch(closeTabs({ tabUids: [item.uid] }));
toast.success(isFolder ? 'Folder renamed' : 'Request renamed');
onClose(); onClose();
}) })
.catch((err) => { .catch((err) => {

View File

@ -4,6 +4,9 @@ const Wrapper = styled.div`
.bruno-modal-content { .bruno-modal-content {
padding-bottom: 1rem; padding-bottom: 1rem;
} }
.warning {
color: ${(props) => props.theme.colors.text.danger};
}
`; `;
export default Wrapper; export default Wrapper;

View File

@ -7,6 +7,7 @@ import { addTab } from 'providers/ReduxStore/slices/tabs';
import { runCollectionFolder } from 'providers/ReduxStore/slices/collections/actions'; import { runCollectionFolder } from 'providers/ReduxStore/slices/collections/actions';
import { flattenItems } from 'utils/collections'; import { flattenItems } from 'utils/collections';
import StyledWrapper from './StyledWrapper'; import StyledWrapper from './StyledWrapper';
import { areItemsLoading } from 'utils/collections';
const RunCollectionItem = ({ collection, item, onClose }) => { const RunCollectionItem = ({ collection, item, onClose }) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
@ -32,6 +33,10 @@ const RunCollectionItem = ({ collection, item, onClose }) => {
const flattenedItems = flattenItems(item ? item.items : collection.items); const flattenedItems = flattenItems(item ? item.items : collection.items);
const recursiveRunLength = getRequestsCount(flattenedItems); const recursiveRunLength = getRequestsCount(flattenedItems);
const isFolderLoading = areItemsLoading(item);
console.log(item);
console.log(isFolderLoading);
return ( return (
<StyledWrapper> <StyledWrapper>
<Modal size="md" title="Collection Runner" hideFooter={true} handleCancel={onClose}> <Modal size="md" title="Collection Runner" hideFooter={true} handleCancel={onClose}>
@ -44,13 +49,12 @@ const RunCollectionItem = ({ collection, item, onClose }) => {
<span className="ml-1 text-xs">({runLength} requests)</span> <span className="ml-1 text-xs">({runLength} requests)</span>
</div> </div>
<div className="mb-8">This will only run the requests in this folder.</div> <div className="mb-8">This will only run the requests in this folder.</div>
<div className="mb-1"> <div className="mb-1">
<span className="font-medium">Recursive Run</span> <span className="font-medium">Recursive Run</span>
<span className="ml-1 text-xs">({recursiveRunLength} requests)</span> <span className="ml-1 text-xs">({recursiveRunLength} requests)</span>
</div> </div>
<div className="mb-8">This will run all the requests in this folder and all its subfolders.</div> <div className={isFolderLoading ? "mb-2" : "mb-8"}>This will run all the requests in this folder and all its subfolders.</div>
{isFolderLoading ? <div className='mb-8 warning'>Requests in this folder are still loading.</div> : null}
<div className="flex justify-end bruno-modal-footer"> <div className="flex justify-end bruno-modal-footer">
<span className="mr-3"> <span className="mr-3">
<button type="button" onClick={onClose} className="btn btn-md btn-close"> <button type="button" onClick={onClose} className="btn btn-md btn-close">

View File

@ -6,12 +6,11 @@ import { useDrag, useDrop } from 'react-dnd';
import { IconChevronRight, IconDots } from '@tabler/icons'; import { IconChevronRight, IconDots } from '@tabler/icons';
import { useSelector, useDispatch } from 'react-redux'; import { useSelector, useDispatch } from 'react-redux';
import { addTab, focusTab } from 'providers/ReduxStore/slices/tabs'; import { addTab, focusTab } from 'providers/ReduxStore/slices/tabs';
import { moveItem, sendRequest } from 'providers/ReduxStore/slices/collections/actions'; import { moveItem, showInFolder, sendRequest } from 'providers/ReduxStore/slices/collections/actions';
import { collectionFolderClicked } from 'providers/ReduxStore/slices/collections'; import { collectionFolderClicked } from 'providers/ReduxStore/slices/collections';
import Dropdown from 'components/Dropdown'; import Dropdown from 'components/Dropdown';
import NewRequest from 'components/Sidebar/NewRequest'; import NewRequest from 'components/Sidebar/NewRequest';
import NewFolder from 'components/Sidebar/NewFolder'; import NewFolder from 'components/Sidebar/NewFolder';
import RequestMethod from './RequestMethod';
import RenameCollectionItem from './RenameCollectionItem'; import RenameCollectionItem from './RenameCollectionItem';
import CloneCollectionItem from './CloneCollectionItem'; import CloneCollectionItem from './CloneCollectionItem';
import DeleteCollectionItem from './DeleteCollectionItem'; import DeleteCollectionItem from './DeleteCollectionItem';
@ -24,7 +23,7 @@ import { hideHomePage } from 'providers/ReduxStore/slices/app';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
import StyledWrapper from './StyledWrapper'; import StyledWrapper from './StyledWrapper';
import NetworkError from 'components/ResponsePane/NetworkError/index'; import NetworkError from 'components/ResponsePane/NetworkError/index';
import { uuid } from 'utils/common'; import CollectionItemIcon from './CollectionItemIcon/index';
const CollectionItem = ({ item, collection, searchText }) => { const CollectionItem = ({ item, collection, searchText }) => {
const tabs = useSelector((state) => state.tabs.tabs); const tabs = useSelector((state) => state.tabs.tabs);
@ -39,7 +38,9 @@ const CollectionItem = ({ item, collection, searchText }) => {
const [newRequestModalOpen, setNewRequestModalOpen] = useState(false); const [newRequestModalOpen, setNewRequestModalOpen] = useState(false);
const [newFolderModalOpen, setNewFolderModalOpen] = useState(false); const [newFolderModalOpen, setNewFolderModalOpen] = useState(false);
const [runCollectionModalOpen, setRunCollectionModalOpen] = useState(false); const [runCollectionModalOpen, setRunCollectionModalOpen] = useState(false);
const [itemIsCollapsed, setItemisCollapsed] = useState(item.collapsed);
const hasSearchText = searchText && searchText?.trim()?.length;
const itemIsCollapsed = hasSearchText ? false : item.collapsed;
const [{ isDragging }, drag] = useDrag({ const [{ isDragging }, drag] = useDrag({
type: `COLLECTION_ITEM_${collection.uid}`, type: `COLLECTION_ITEM_${collection.uid}`,
@ -64,14 +65,6 @@ const CollectionItem = ({ item, collection, searchText }) => {
}) })
}); });
useEffect(() => {
if (searchText && searchText.length) {
setItemisCollapsed(false);
} else {
setItemisCollapsed(item.collapsed);
}
}, [searchText, item]);
const dropdownTippyRef = useRef(); const dropdownTippyRef = useRef();
const MenuIcon = forwardRef((props, ref) => { const MenuIcon = forwardRef((props, ref) => {
return ( return (
@ -128,6 +121,13 @@ const CollectionItem = ({ item, collection, searchText }) => {
); );
return; return;
} }
dispatch(
addTab({
uid: item.uid,
collectionUid: collection.uid,
type: 'folder-settings'
})
);
dispatch( dispatch(
collectionFolderClicked({ collectionFolderClicked({
itemUid: item.uid, itemUid: item.uid,
@ -136,6 +136,15 @@ const CollectionItem = ({ item, collection, searchText }) => {
); );
}; };
const handleFolderCollapse = () => {
dispatch(
collectionFolderClicked({
itemUid: item.uid,
collectionUid: collection.uid
})
);
}
const handleRightClick = (event) => { const handleRightClick = (event) => {
const _menuDropdown = dropdownTippyRef.current; const _menuDropdown = dropdownTippyRef.current;
if (_menuDropdown) { if (_menuDropdown) {
@ -183,7 +192,7 @@ const CollectionItem = ({ item, collection, searchText }) => {
const handleGenerateCode = (e) => { const handleGenerateCode = (e) => {
e.stopPropagation(); e.stopPropagation();
dropdownTippyRef.current.hide(); dropdownTippyRef.current.hide();
if (item.request.url !== '' || (item.draft?.request.url !== undefined && item.draft?.request.url !== '')) { if (item?.request?.url !== '' || (item?.draft?.request?.url !== undefined && item?.draft?.request?.url !== '')) {
setGenerateCodeItemModalOpen(true); setGenerateCodeItemModalOpen(true);
} else { } else {
toast.error('URL is required'); toast.error('URL is required');
@ -211,6 +220,13 @@ 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 requestItems = sortRequestItems(filter(item.items, (i) => isItemARequest(i)));
const folderItems = sortFolderItems(filter(item.items, (i) => isItemAFolder(i))); const folderItems = sortFolderItems(filter(item.items, (i) => isItemAFolder(i)));
@ -260,9 +276,6 @@ const CollectionItem = ({ item, collection, searchText }) => {
}) })
: null} : null}
<div <div
onClick={handleClick}
onContextMenu={handleRightClick}
onDoubleClick={handleDoubleClick}
className="flex flex-grow items-center h-full overflow-hidden" className="flex flex-grow items-center h-full overflow-hidden"
style={{ style={{
paddingLeft: 8 paddingLeft: 8
@ -275,12 +288,18 @@ const CollectionItem = ({ item, collection, searchText }) => {
strokeWidth={2} strokeWidth={2}
className={iconClassName} className={iconClassName}
style={{ color: 'rgb(160 160 160)' }} style={{ color: 'rgb(160 160 160)' }}
onClick={handleFolderCollapse}
/> />
) : null} ) : null}
</div> </div>
<div className="ml-1 flex items-center overflow-hidden"> <div
<RequestMethod item={item} /> 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}> <span className="item-name" title={item.name}>
{item.name} {item.name}
</span> </span>
@ -359,6 +378,15 @@ const CollectionItem = ({ item, collection, searchText }) => {
Generate Code Generate Code
</div> </div>
)} )}
<div
className="dropdown-item"
onClick={(e) => {
dropdownTippyRef.current.hide();
handleShowInFolder();
}}
>
Show in Folder
</div>
<div <div
className="dropdown-item delete-item" className="dropdown-item delete-item"
onClick={(e) => { onClick={(e) => {

View File

@ -3,10 +3,10 @@ import classnames from 'classnames';
import { uuid } from 'utils/common'; import { uuid } from 'utils/common';
import filter from 'lodash/filter'; import filter from 'lodash/filter';
import { useDrop } from 'react-dnd'; import { useDrop } from 'react-dnd';
import { IconChevronRight, IconDots } from '@tabler/icons'; import { IconChevronRight, IconDots, IconLoader2 } from '@tabler/icons';
import Dropdown from 'components/Dropdown'; import Dropdown from 'components/Dropdown';
import { collectionClicked } from 'providers/ReduxStore/slices/collections'; import { collapseCollection } from 'providers/ReduxStore/slices/collections';
import { moveItemToRootOfCollection } from 'providers/ReduxStore/slices/collections/actions'; import { mountCollection, moveItemToRootOfCollection } from 'providers/ReduxStore/slices/collections/actions';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import { addTab } from 'providers/ReduxStore/slices/tabs'; import { addTab } from 'providers/ReduxStore/slices/tabs';
import NewRequest from 'components/Sidebar/NewRequest'; import NewRequest from 'components/Sidebar/NewRequest';
@ -15,12 +15,12 @@ import CollectionItem from './CollectionItem';
import RemoveCollection from './RemoveCollection'; import RemoveCollection from './RemoveCollection';
import ExportCollection from './ExportCollection'; import ExportCollection from './ExportCollection';
import { doesCollectionHaveItemsMatchingSearchText } from 'utils/collections/search'; import { doesCollectionHaveItemsMatchingSearchText } from 'utils/collections/search';
import { isItemAFolder, isItemARequest, transformCollectionToSaveToExportAsFile } from 'utils/collections'; import { isItemAFolder, isItemARequest } from 'utils/collections';
import exportCollection from 'utils/collections/export';
import RenameCollection from './RenameCollection'; import RenameCollection from './RenameCollection';
import StyledWrapper from './StyledWrapper'; import StyledWrapper from './StyledWrapper';
import CloneCollection from './CloneCollection/index'; import CloneCollection from './CloneCollection';
import { areItemsLoading } from 'utils/collections';
const Collection = ({ collection, searchText }) => { const Collection = ({ collection, searchText }) => {
const [showNewFolderModal, setShowNewFolderModal] = useState(false); const [showNewFolderModal, setShowNewFolderModal] = useState(false);
@ -29,8 +29,8 @@ const Collection = ({ collection, searchText }) => {
const [showCloneCollectionModalOpen, setShowCloneCollectionModalOpen] = useState(false); const [showCloneCollectionModalOpen, setShowCloneCollectionModalOpen] = useState(false);
const [showExportCollectionModal, setShowExportCollectionModal] = useState(false); const [showExportCollectionModal, setShowExportCollectionModal] = useState(false);
const [showRemoveCollectionModal, setShowRemoveCollectionModal] = useState(false); const [showRemoveCollectionModal, setShowRemoveCollectionModal] = useState(false);
const [collectionIsCollapsed, setCollectionIsCollapsed] = useState(collection.collapsed);
const dispatch = useDispatch(); const dispatch = useDispatch();
const isLoading = areItemsLoading(collection);
const menuDropdownTippyRef = useRef(); const menuDropdownTippyRef = useRef();
const onMenuDropdownCreate = (ref) => (menuDropdownTippyRef.current = ref); const onMenuDropdownCreate = (ref) => (menuDropdownTippyRef.current = ref);
@ -52,20 +52,36 @@ const Collection = ({ collection, searchText }) => {
); );
}; };
useEffect(() => { const hasSearchText = searchText && searchText?.trim()?.length;
if (searchText && searchText.length) { const collectionIsCollapsed = hasSearchText ? false : collection.collapsed;
setCollectionIsCollapsed(false);
} else {
setCollectionIsCollapsed(collection.collapsed);
}
}, [searchText, collection]);
const iconClassName = classnames({ const iconClassName = classnames({
'rotate-90': !collectionIsCollapsed 'rotate-90': !collectionIsCollapsed
}); });
const handleClick = (event) => { const handleClick = (event) => {
dispatch(collectionClicked(collection.uid)); // Check if the click came from the chevron icon
const isChevronClick = event.target.closest('svg')?.classList.contains('chevron-icon');
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: uuid(),
collectionUid: collection.uid,
type: 'collection-settings'
})
);
}
}; };
const handleRightClick = (event) => { const handleRightClick = (event) => {
@ -147,12 +163,13 @@ const Collection = ({ collection, searchText }) => {
<IconChevronRight <IconChevronRight
size={16} size={16}
strokeWidth={2} strokeWidth={2}
className={iconClassName} className={`chevron-icon ${iconClassName}`}
style={{ width: 16, minWidth: 16, color: 'rgb(160 160 160)' }} style={{ width: 16, minWidth: 16, color: 'rgb(160 160 160)' }}
/> />
<div className="ml-1" id="sidebar-collection-name"> <div className="ml-1" id="sidebar-collection-name">
{collection.name} {collection.name}
</div> </div>
{isLoading ? <IconLoader2 className="animate-spin mx-1" size={18} strokeWidth={1.5} /> : null}
</div> </div>
<div className="collection-actions"> <div className="collection-actions">
<Dropdown onCreate={onMenuDropdownCreate} icon={<MenuIcon />} placement="bottom-start"> <Dropdown onCreate={onMenuDropdownCreate} icon={<MenuIcon />} placement="bottom-start">

View File

@ -68,7 +68,7 @@ const ImportCollection = ({ onClose, handleSubmit }) => {
); );
}; };
return ( return (
<Modal size="sm" title="Import Collection" hideFooter={true} handleConfirm={onClose} handleCancel={onClose}> <Modal size="sm" title="Import Collection" hideFooter={true} handleCancel={onClose}>
<div className="flex flex-col"> <div className="flex flex-col">
<h3 className="text-sm">Select the type of your existing collection :</h3> <h3 className="text-sm">Select the type of your existing collection :</h3>
<div className="mt-4 grid grid-rows-2 grid-flow-col gap-2"> <div className="mt-4 grid grid-rows-2 grid-flow-col gap-2">

View File

@ -184,7 +184,7 @@ const Sidebar = () => {
Star Star
</GitHubButton> */} </GitHubButton> */}
</div> </div>
<div className="flex flex-grow items-center justify-end text-xs mr-2">v1.36.0</div> <div className="flex flex-grow items-center justify-end text-xs mr-2">v1.36.1</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -63,16 +63,16 @@ const Table = ({ minColumnWidth = 1, headers = [], children }) => {
[activeColumnIndex, columns, minColumnWidth] [activeColumnIndex, columns, minColumnWidth]
); );
const handleMouseUp = useCallback(() => {
setActiveColumnIndex(null);
removeListeners();
}, [removeListeners]);
const removeListeners = useCallback(() => { const removeListeners = useCallback(() => {
window.removeEventListener('mousemove', handleMouseMove); window.removeEventListener('mousemove', handleMouseMove);
window.removeEventListener('mouseup', removeListeners); window.removeEventListener('mouseup', removeListeners);
}, [handleMouseMove]); }, [handleMouseMove]);
const handleMouseUp = useCallback(() => {
setActiveColumnIndex(null);
removeListeners?.();
}, [removeListeners]);
useEffect(() => { useEffect(() => {
if (activeColumnIndex !== null) { if (activeColumnIndex !== null) {
window.addEventListener('mousemove', handleMouseMove); window.addEventListener('mousemove', handleMouseMove);

View File

@ -15,7 +15,7 @@ const KeyValueExplorer = ({ data = [], theme }) => {
<SecretToggle showSecret={showSecret} onClick={() => setShowSecret(!showSecret)} /> <SecretToggle showSecret={showSecret} onClick={() => setShowSecret(!showSecret)} />
<table className="border-collapse"> <table className="border-collapse">
<tbody> <tbody>
{data.map((envVar) => ( {data.toSorted((a, b) => a.name.localeCompare(b.name)).map((envVar) => (
<tr key={envVar.name}> <tr key={envVar.name}>
<td className="px-2 py-1">{envVar.name}</td> <td className="px-2 py-1">{envVar.name}</td>
<td className="px-2 py-1"> <td className="px-2 py-1">

View File

@ -0,0 +1,14 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './pages/index';
const rootElement = document.getElementById('root');
if (rootElement) {
const root = ReactDOM.createRoot(rootElement);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
}

View File

@ -31,6 +31,7 @@ if (!SERVER_RENDERED) {
require('codemirror/addon/search/jump-to-line'); require('codemirror/addon/search/jump-to-line');
require('codemirror/addon/search/search'); require('codemirror/addon/search/search');
require('codemirror/addon/search/searchcursor'); require('codemirror/addon/search/searchcursor');
require('codemirror/addon/display/placeholder');
require('codemirror/keymap/sublime'); require('codemirror/keymap/sublime');
require('codemirror-graphql/hint'); require('codemirror-graphql/hint');

View File

@ -25,31 +25,7 @@ import '@fontsource/inter/900.css';
import { setupPolyfills } from 'utils/common/setupPolyfills'; import { setupPolyfills } from 'utils/common/setupPolyfills';
setupPolyfills(); setupPolyfills();
function SafeHydrate({ children }) { function Main({ children }) {
return <div suppressHydrationWarning>{typeof window === 'undefined' ? null : children}</div>;
}
function NoSsr({ children }) {
const SERVER_RENDERED = typeof window === 'undefined';
if (SERVER_RENDERED) {
return null;
}
return <>{children}</>;
}
function MyApp({ Component, pageProps }) {
const [domLoaded, setDomLoaded] = useState(false);
useEffect(() => {
setDomLoaded(true);
}, []);
if (!domLoaded) {
return null;
}
if (!window.ipcRenderer) { if (!window.ipcRenderer) {
return ( return (
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 mx-10 my-10 rounded relative" role="alert"> <div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 mx-10 my-10 rounded relative" role="alert">
@ -65,23 +41,21 @@ function MyApp({ Component, pageProps }) {
return ( return (
<ErrorBoundary> <ErrorBoundary>
<SafeHydrate>
<NoSsr>
<Provider store={ReduxStore}> <Provider store={ReduxStore}>
<ThemeProvider> <ThemeProvider>
<ToastProvider> <ToastProvider>
<AppProvider> <AppProvider>
<HotkeysProvider> <HotkeysProvider>
<Component {...pageProps} /> {children}
</HotkeysProvider> </HotkeysProvider>
</AppProvider> </AppProvider>
</ToastProvider> </ToastProvider>
</ThemeProvider> </ThemeProvider>
</Provider> </Provider>
</NoSsr>
</SafeHydrate>
</ErrorBoundary> </ErrorBoundary>
); );
} }
export default MyApp; export default Main;

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