mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-04 01:48:33 +00:00
Compare commits
20 Commits
fix/websoc
...
fix/playwr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
33439b3840 | ||
|
|
2446301e41 | ||
|
|
67903f26bc | ||
|
|
1b8eece173 | ||
|
|
1f05ffd469 | ||
|
|
c2acc25461 | ||
|
|
dc9df80638 | ||
|
|
c5abe4122b | ||
|
|
3081c06964 | ||
|
|
8c7ed3fe51 | ||
|
|
ce33cee03d | ||
|
|
d93d1eacdb | ||
|
|
aeb6b12b06 | ||
|
|
41ed51b4e3 | ||
|
|
b85f60e1d6 | ||
|
|
49ffdd1b8f | ||
|
|
f1961a8988 | ||
|
|
4831434e37 | ||
|
|
87c8934c45 | ||
|
|
01d4d3dc2a |
@@ -117,6 +117,18 @@ module.exports = runESMImports().then(() => defineConfig([
|
||||
'no-undef': 'error'
|
||||
}
|
||||
},
|
||||
{
|
||||
// Storybook config files use CommonJS with __dirname and module.exports
|
||||
files: ['packages/bruno-app/storybook/**/*.js'],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node
|
||||
}
|
||||
},
|
||||
rules: {
|
||||
'no-undef': 'error'
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ['packages/bruno-cli/**/*.js'],
|
||||
ignores: ['**/*.config.js'],
|
||||
|
||||
2478
package-lock.json
generated
2478
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -20,6 +20,11 @@
|
||||
],
|
||||
"homepage": "https://usebruno.com",
|
||||
"devDependencies": {
|
||||
"@storybook/addon-webpack5-compiler-babel": "^4.0.0",
|
||||
"@storybook/builder-webpack5": "^10.1.10",
|
||||
"@storybook/react": "^10.1.10",
|
||||
"@storybook/react-webpack5": "^10.1.10",
|
||||
"storybook": "^10.1.10",
|
||||
"@eslint/compat": "^1.3.2",
|
||||
"@faker-js/faker": "^7.6.0",
|
||||
"@jest/globals": "^29.2.0",
|
||||
|
||||
1
packages/bruno-app/.gitignore
vendored
1
packages/bruno-app/.gitignore
vendored
@@ -22,6 +22,7 @@ build
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
*.log
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
|
||||
@@ -9,7 +9,9 @@
|
||||
"preview": "rsbuild preview",
|
||||
"test": "jest",
|
||||
"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}\"",
|
||||
"storybook": "storybook dev -p 6006 --config-dir storybook",
|
||||
"build-storybook": "storybook build --config-dir storybook"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource/inter": "^5.0.15",
|
||||
@@ -63,6 +65,7 @@
|
||||
"path": "^0.12.7",
|
||||
"pdfjs-dist": "4.4.168",
|
||||
"platform": "^1.3.6",
|
||||
"polished": "^4.3.1",
|
||||
"posthog-node": "4.2.1",
|
||||
"prettier": "^2.7.1",
|
||||
"qs": "^6.11.0",
|
||||
|
||||
@@ -201,6 +201,15 @@ const Wrapper = styled.div`
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
&.os-linux .titlebar-content {
|
||||
padding-right: 0px;
|
||||
padding-left: 0px;
|
||||
}
|
||||
|
||||
&.os-linux .titlebar-left {
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
/* Custom window control buttons for Windows - always interactive, above modal overlay */
|
||||
.window-controls {
|
||||
display: flex;
|
||||
|
||||
@@ -20,11 +20,12 @@ import IconBottombarToggle from 'components/Icons/IconBottombarToggle/index';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { toTitleCase } from 'utils/common/index';
|
||||
import ResponseLayoutToggle from 'components/ResponsePane/ResponseLayoutToggle';
|
||||
import { isMacOS, isWindowsOS } from 'utils/common/platform';
|
||||
import { isMacOS, isWindowsOS, isLinuxOS } from 'utils/common/platform';
|
||||
|
||||
const getOsClass = () => {
|
||||
if (isMacOS()) return 'os-mac';
|
||||
if (isWindowsOS()) return 'os-windows';
|
||||
if (isLinuxOS()) return 'os-linux';
|
||||
return 'os-other';
|
||||
};
|
||||
|
||||
@@ -34,6 +35,8 @@ const AppTitleBar = () => {
|
||||
const [isMaximized, setIsMaximized] = useState(false);
|
||||
const osClass = getOsClass();
|
||||
const isWindows = osClass === 'os-windows';
|
||||
const isLinux = osClass === 'os-linux';
|
||||
const showWindowControls = isWindows || isLinux;
|
||||
|
||||
// Listen for fullscreen changes
|
||||
useEffect(() => {
|
||||
@@ -54,13 +57,11 @@ const AppTitleBar = () => {
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Check initial maximized state and listen for changes (Windows only)
|
||||
useEffect(() => {
|
||||
if (!isWindows) return;
|
||||
if (!showWindowControls) return;
|
||||
const { ipcRenderer } = window;
|
||||
if (!ipcRenderer) return;
|
||||
|
||||
// Get initial state
|
||||
ipcRenderer.invoke('renderer:window-is-maximized')
|
||||
.then((maximized) => {
|
||||
setIsMaximized(maximized);
|
||||
@@ -69,7 +70,6 @@ const AppTitleBar = () => {
|
||||
console.error('Error getting initial maximized state:', error);
|
||||
});
|
||||
|
||||
// Listen for maximize/unmaximize events from main process
|
||||
const removeMaximizedListener = ipcRenderer.on('main:window-maximized', () => {
|
||||
setIsMaximized(true);
|
||||
});
|
||||
@@ -82,9 +82,8 @@ const AppTitleBar = () => {
|
||||
removeMaximizedListener();
|
||||
removeUnmaximizedListener();
|
||||
};
|
||||
}, [isWindows]);
|
||||
}, [showWindowControls]);
|
||||
|
||||
// Window control handlers (Windows only) - these always work, even with modals open
|
||||
const handleMinimize = useCallback(() => {
|
||||
window.ipcRenderer?.send('renderer:window-minimize');
|
||||
}, []);
|
||||
@@ -300,7 +299,7 @@ const AppTitleBar = () => {
|
||||
<ResponseLayoutToggle />
|
||||
</div>
|
||||
|
||||
{isWindows && (
|
||||
{showWindowControls && (
|
||||
<div className="window-controls">
|
||||
<button
|
||||
className="window-control-btn minimize"
|
||||
|
||||
@@ -228,8 +228,10 @@ export default class CodeEditor extends React.Component {
|
||||
CodeMirror.signal(this.editor, 'change', this.editor);
|
||||
}
|
||||
if (this.props.value !== prevProps.value && this.props.value !== this.cachedValue && this.editor) {
|
||||
const cursor = this.editor.getCursor();
|
||||
this.cachedValue = this.props.value;
|
||||
this.editor.setValue(this.props.value);
|
||||
this.editor.setCursor(cursor);
|
||||
}
|
||||
|
||||
if (this.editor) {
|
||||
|
||||
@@ -12,6 +12,7 @@ import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import OAuth2 from './OAuth2';
|
||||
import NTLMAuth from './NTLMAuth';
|
||||
import Button from 'ui/Button';
|
||||
|
||||
const Auth = ({ collection }) => {
|
||||
const authMode = collection.draft?.root ? get(collection, 'draft.root.request.auth.mode') : get(collection, 'root.request.auth.mode');
|
||||
@@ -59,9 +60,9 @@ const Auth = ({ collection }) => {
|
||||
</div>
|
||||
{getAuthView()}
|
||||
<div className="mt-6">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
|
||||
<Button type="submit" size="sm" onClick={handleSave}>
|
||||
Save
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -13,6 +13,7 @@ import { useDispatch } from 'react-redux';
|
||||
import { updateCollectionClientCertificates } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import get from 'lodash/get';
|
||||
import Button from 'ui/Button';
|
||||
|
||||
const ClientCertSettings = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -374,13 +375,13 @@ const ClientCertSettings = ({ collection }) => {
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mt-6 flex flex-row gap-2 items-center">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary" data-testid="add-client-cert">
|
||||
<Button type="submit" size="sm" data-testid="add-client-cert">
|
||||
Add
|
||||
</button>
|
||||
</Button>
|
||||
<div className="h-4 border-l border-gray-600"></div>
|
||||
<button type="button" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
|
||||
<Button type="button" size="sm" onClick={handleSave}>
|
||||
Save
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</StyledWrapper>
|
||||
|
||||
@@ -10,6 +10,7 @@ import StyledWrapper from './StyledWrapper';
|
||||
import { headers as StandardHTTPHeaders } from 'know-your-http-well';
|
||||
import { MimeTypes } from 'utils/codemirror/autocompleteConstants';
|
||||
import BulkEditor from 'components/BulkEditor/index';
|
||||
import Button from 'ui/Button';
|
||||
|
||||
const headerAutoCompleteList = StandardHTTPHeaders.map((e) => e.header);
|
||||
|
||||
@@ -107,9 +108,9 @@ const Headers = ({ collection }) => {
|
||||
</button>
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
|
||||
<Button type="submit" size="sm" onClick={handleSave}>
|
||||
Save
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -6,7 +6,6 @@ import { useDispatch, useSelector } from 'react-redux';
|
||||
import { isItemARequest, itemIsOpenedInTabs } from 'utils/tabs/index';
|
||||
import { getDefaultRequestPaneTab } from 'utils/collections/index';
|
||||
import { addTab, focusTab } from 'providers/ReduxStore/slices/tabs';
|
||||
import { hideHomePage } from 'providers/ReduxStore/slices/app';
|
||||
|
||||
const RequestsNotLoaded = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -21,7 +20,6 @@ const RequestsNotLoaded = ({ collection }) => {
|
||||
const handleRequestClick = (item) => (e) => {
|
||||
e.preventDefault();
|
||||
if (isItemARequest(item)) {
|
||||
dispatch(hideHomePage());
|
||||
if (itemIsOpenedInTabs(item, tabs)) {
|
||||
dispatch(
|
||||
focusTab({
|
||||
|
||||
@@ -4,6 +4,7 @@ import StyledWrapper from './StyledWrapper';
|
||||
import { updateCollectionPresets } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { get } from 'lodash';
|
||||
import Button from 'ui/Button';
|
||||
|
||||
const PresetsSettings = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -122,9 +123,9 @@ const PresetsSettings = ({ collection }) => {
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
<button type="button" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
|
||||
<Button type="button" size="sm" onClick={handleSave}>
|
||||
Save
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
|
||||
@@ -12,6 +12,7 @@ import { getBasename } from 'utils/common/path';
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
import useProtoFileManagement from '../../../hooks/useProtoFileManagement';
|
||||
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import Button from 'ui/Button';
|
||||
|
||||
const ProtobufSettings = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -335,9 +336,9 @@ const ProtobufSettings = ({ collection }) => {
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
<button type="button" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
|
||||
<Button type="button" size="sm" onClick={handleSave}>
|
||||
Save
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
</StyledWrapper>
|
||||
|
||||
@@ -8,6 +8,7 @@ import { updateCollectionProxy } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { get } from 'lodash';
|
||||
import toast from 'react-hot-toast';
|
||||
import Button from 'ui/Button';
|
||||
|
||||
const ProxySettings = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -358,9 +359,9 @@ const ProxySettings = ({ collection }) => {
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
|
||||
<Button type="submit" size="sm" onClick={handleSave}>
|
||||
Save
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
|
||||
@@ -7,6 +7,7 @@ import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from 'components/Tabs';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import Button from 'ui/Button';
|
||||
|
||||
const Script = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -98,9 +99,9 @@ const Script = ({ collection }) => {
|
||||
</Tabs>
|
||||
|
||||
<div className="mt-12">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
|
||||
<Button type="submit" size="sm" onClick={handleSave}>
|
||||
Save
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -6,6 +6,7 @@ import { updateCollectionTests } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import Button from 'ui/Button';
|
||||
|
||||
const Tests = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -41,9 +42,9 @@ const Tests = ({ collection }) => {
|
||||
/>
|
||||
|
||||
<div className="mt-6">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
|
||||
<Button type="submit" size="sm" onClick={handleSave}>
|
||||
Save
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -4,6 +4,7 @@ import VarsTable from './VarsTable';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import Button from 'ui/Button';
|
||||
|
||||
const Vars = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -22,9 +23,9 @@ const Vars = ({ collection }) => {
|
||||
<VarsTable collection={collection} vars={responseVars} varType="response" />
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
|
||||
<Button type="submit" size="sm" onClick={handleSave}>
|
||||
Save
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -9,6 +9,7 @@ import ModifyCookieModal from 'components/Cookies/ModifyCookieModal/index';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import moment from 'moment';
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
import Button from 'ui/Button';
|
||||
|
||||
const ClearDomainCookiesModal = ({ onClose, domain, onClear }) => (
|
||||
<Modal onClose={onClose} handleCancel={onClose} title="Clear Domain Cookies" hideFooter={true}>
|
||||
@@ -22,14 +23,14 @@ const ClearDomainCookiesModal = ({ onClose, domain, onClear }) => (
|
||||
|
||||
<div className="flex justify-between mt-6">
|
||||
<div>
|
||||
<button className="btn btn-sm btn-close" onClick={onClose}>
|
||||
<Button size="sm" color="secondary" variant="ghost" onClick={onClose}>
|
||||
Close
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<button className="btn btn-sm btn-danger" onClick={onClear}>
|
||||
<Button size="sm" color="danger" onClick={onClear}>
|
||||
Clear All
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
@@ -47,14 +48,14 @@ const DeleteCookieModal = ({ onClose, cookieName, onDelete }) => (
|
||||
|
||||
<div className="flex justify-between mt-6">
|
||||
<div>
|
||||
<button className="btn btn-sm btn-close" onClick={onClose}>
|
||||
<Button size="sm" color="secondary" variant="ghost" onClick={onClose}>
|
||||
Close
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<button className="btn btn-sm btn-danger" onClick={onDelete}>
|
||||
<Button size="sm" color="danger" onClick={onDelete}>
|
||||
Delete
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
@@ -139,17 +140,18 @@ const CollectionProperties = ({ onClose }) => {
|
||||
className="block textbox non-passphrase-input ml-auto font-normal"
|
||||
autoFocus
|
||||
/>
|
||||
<button
|
||||
<Button
|
||||
type="submit"
|
||||
className="submit btn btn-sm btn-secondary flex items-center gap-1 mx-4 font-medium"
|
||||
size="sm"
|
||||
className="mx-4"
|
||||
icon={<IconCirclePlus strokeWidth={1.5} size={16} />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleAddCookie();
|
||||
}}
|
||||
>
|
||||
<IconCirclePlus strokeWidth={1.5} size={16} />
|
||||
<span>Add Cookie</span>
|
||||
</button>
|
||||
</Button>
|
||||
</StyledWrapper>
|
||||
) : null}
|
||||
>
|
||||
|
||||
@@ -1,153 +1,165 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
.dropdown-toggle {
|
||||
&:hover {
|
||||
color: black;
|
||||
min-width: 160px;
|
||||
font-size: ${(props) => props.theme.font.size.base};
|
||||
color: ${(props) => props.theme.dropdown.color};
|
||||
background-color: ${(props) => props.theme.dropdown.bg};
|
||||
box-shadow: ${(props) => props.theme.shadow.sm};
|
||||
border-radius: ${(props) => props.theme.border.radius.base};
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
max-width: unset !important;
|
||||
padding: 0.25rem;
|
||||
|
||||
[role="menu"] {
|
||||
outline: none;
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
.tippy-box {
|
||||
min-width: 160px;
|
||||
font-size: ${(props) => props.theme.font.size.base};
|
||||
.label-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.375rem 0.625rem 0.25rem 0.625rem;
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.025em;
|
||||
color: ${(props) => props.theme.dropdown.color};
|
||||
background-color: ${(props) => props.theme.dropdown.bg};
|
||||
box-shadow: ${(props) => props.theme.shadow.sm};
|
||||
border-radius: ${(props) => props.theme.border.radius.base};
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
max-width: unset !important;
|
||||
padding: 0.25rem;
|
||||
opacity: 0.6;
|
||||
margin-top: 0.25rem;
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.tippy-content {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
.dropdown-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.275rem 0.625rem;
|
||||
cursor: pointer;
|
||||
border-radius: 6px;
|
||||
margin: 0.0625rem 0;
|
||||
font-size: 0.8125rem;
|
||||
|
||||
[role="menu"] {
|
||||
outline: none;
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
.label-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.375rem 0.625rem 0.25rem 0.625rem;
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.025em;
|
||||
color: ${(props) => props.theme.dropdown.color};
|
||||
opacity: 0.6;
|
||||
margin-top: 0.25rem;
|
||||
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.275rem 0.625rem;
|
||||
cursor: pointer;
|
||||
border-radius: 6px;
|
||||
margin: 0.0625rem 0;
|
||||
font-size: 0.8125rem;
|
||||
|
||||
&.active {
|
||||
color: ${(props) => props.theme.colors.text.yellow} !important;
|
||||
.dropdown-icon {
|
||||
color: ${(props) => props.theme.colors.text.yellow} !important;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-label {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.dropdown-icon {
|
||||
flex-shrink: 0;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: ${(props) => props.theme.dropdown.iconColor};
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.dropdown-right-section {
|
||||
margin-left: auto;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: ${(props) => props.theme.dropdown.hoverBg};
|
||||
}
|
||||
|
||||
&.selected-focused:not(:disabled) {
|
||||
background-color: ${(props) => props.theme.dropdown.hoverBg};
|
||||
}
|
||||
|
||||
&:focus-visible:not(:disabled) {
|
||||
outline: none;
|
||||
background-color: ${(props) => props.theme.dropdown.hoverBg};
|
||||
}
|
||||
|
||||
&:focus:not(:focus-visible) {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&.delete-item {
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
.dropdown-icon {
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
}
|
||||
&:hover {
|
||||
background-color: ${({ theme }) => {
|
||||
const hex = theme.colors.text.danger.replace('#', '');
|
||||
const r = parseInt(hex.substring(0, 2), 16);
|
||||
const g = parseInt(hex.substring(2, 4), 16);
|
||||
const b = parseInt(hex.substring(4, 6), 16);
|
||||
return `rgba(${r}, ${g}, ${b}, 0.04)`; // 4% opacity
|
||||
}} !important;
|
||||
|
||||
color: ${(props) => props.theme.colors.text.danger} !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.border-top {
|
||||
border-top: solid 1px ${(props) => props.theme.dropdown.separator};
|
||||
margin-top: 0.25rem;
|
||||
padding-top: 0.375rem;
|
||||
}
|
||||
|
||||
&.dropdown-item-select {
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-separator {
|
||||
height: 1px;
|
||||
background-color: ${(props) => props.theme.dropdown.separator};
|
||||
margin: 0.25rem 0;
|
||||
&.active {
|
||||
color: ${(props) => props.theme.colors.text.yellow} !important;
|
||||
.dropdown-icon {
|
||||
color: ${(props) => props.theme.colors.text.yellow} !important;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-label {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.dropdown-icon {
|
||||
flex-shrink: 0;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: ${(props) => props.theme.dropdown.iconColor};
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.dropdown-right-section {
|
||||
margin-left: auto;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&:hover:not(:disabled):not(.disabled) {
|
||||
background-color: ${(props) => props.theme.dropdown.hoverBg};
|
||||
}
|
||||
|
||||
&.selected-focused:not(:disabled):not(.disabled) {
|
||||
background-color: ${(props) => props.theme.dropdown.hoverBg};
|
||||
}
|
||||
|
||||
&:focus-visible:not(:disabled):not(.disabled) {
|
||||
outline: none;
|
||||
background-color: ${(props) => props.theme.dropdown.hoverBg};
|
||||
}
|
||||
|
||||
&:focus:not(:focus-visible) {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&:disabled,
|
||||
&.disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&.delete-item {
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
.dropdown-icon {
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
}
|
||||
&:hover {
|
||||
background-color: ${({ theme }) => {
|
||||
const hex = theme.colors.text.danger.replace('#', '');
|
||||
const r = parseInt(hex.substring(0, 2), 16);
|
||||
const g = parseInt(hex.substring(2, 4), 16);
|
||||
const b = parseInt(hex.substring(4, 6), 16);
|
||||
return `rgba(${r}, ${g}, ${b}, 0.04)`; // 4% opacity
|
||||
}} !important;
|
||||
|
||||
color: ${(props) => props.theme.colors.text.danger} !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.border-top {
|
||||
border-top: solid 1px ${(props) => props.theme.dropdown.separator};
|
||||
margin-top: 0.25rem;
|
||||
padding-top: 0.375rem;
|
||||
}
|
||||
|
||||
&.dropdown-item-select {
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
|
||||
/* Focused state - applied during keyboard navigation */
|
||||
&.dropdown-item-focused {
|
||||
background-color: ${({ theme }) => theme.dropdown.hoverBg};
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* Active/selected state - applied to the currently selected item */
|
||||
&.dropdown-item-active {
|
||||
color: ${({ theme }) => theme.colors.text.yellow};
|
||||
background-color: ${({ theme }) => theme.dropdown.activeBg};
|
||||
font-weight: 500;
|
||||
.dropdown-icon {
|
||||
color: ${({ theme }) => theme.colors.text.yellow};
|
||||
}
|
||||
}
|
||||
|
||||
/* Combined state - when active item is also focused */
|
||||
&.dropdown-item-active.dropdown-item-focused {
|
||||
background-color: ${({ theme }) => theme.dropdown.activeHoverBg};
|
||||
}
|
||||
|
||||
/* Focus visible for accessibility */
|
||||
&:focus-visible {
|
||||
outline: 2px solid ${({ theme }) => theme.dropdown.focusRing};
|
||||
outline-offset: -2px;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-separator {
|
||||
height: 1px;
|
||||
background-color: ${(props) => props.theme.dropdown.separator};
|
||||
margin: 0.25rem 0;
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -2,25 +2,27 @@ import React from 'react';
|
||||
import Tippy from '@tippyjs/react';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const Dropdown = ({ icon, children, onCreate, placement, transparent, visible, ...props }) => {
|
||||
const Dropdown = ({ icon, children, onCreate, placement, transparent, visible, appendTo, ...props }) => {
|
||||
// When in controlled mode (visible prop is provided), don't use trigger prop
|
||||
const tippyProps = visible !== undefined
|
||||
? { ...props, visible, interactive: true, appendTo: 'parent' }
|
||||
: { ...props, trigger: 'click', interactive: true, appendTo: 'parent' };
|
||||
? { ...props, visible, interactive: true, appendTo: appendTo || 'parent' }
|
||||
: { ...props, trigger: 'click', interactive: true, appendTo: appendTo || 'parent' };
|
||||
|
||||
return (
|
||||
<StyledWrapper className="dropdown" transparent={transparent}>
|
||||
<Tippy
|
||||
content={children}
|
||||
placement={placement || 'bottom-end'}
|
||||
animation={false}
|
||||
arrow={false}
|
||||
onCreate={onCreate}
|
||||
{...tippyProps}
|
||||
>
|
||||
{icon}
|
||||
</Tippy>
|
||||
</StyledWrapper>
|
||||
<Tippy
|
||||
render={(attrs) => (
|
||||
<StyledWrapper className="tippy-box dropdown" transparent={transparent} tabIndex={-1} {...attrs}>
|
||||
{children}
|
||||
</StyledWrapper>
|
||||
)}
|
||||
placement={placement || 'bottom-end'}
|
||||
animation={false}
|
||||
arrow={false}
|
||||
onCreate={onCreate}
|
||||
{...tippyProps}
|
||||
>
|
||||
{icon}
|
||||
</Tippy>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import React from 'react';
|
||||
import { IconAlertTriangle } from '@tabler/icons';
|
||||
import Modal from 'components/Modal';
|
||||
import Portal from 'components/Portal';
|
||||
import Button from 'ui/Button';
|
||||
|
||||
const ConfirmCloseEnvironment = ({ onCancel, onCloseWithoutSave, onSaveAndClose, isGlobal }) => {
|
||||
return (
|
||||
@@ -25,17 +26,17 @@ const ConfirmCloseEnvironment = ({ onCancel, onCloseWithoutSave, onSaveAndClose,
|
||||
|
||||
<div className="flex justify-between mt-6">
|
||||
<div>
|
||||
<button className="btn btn-sm btn-danger" onClick={onCloseWithoutSave}>
|
||||
<Button size="sm" color="danger" onClick={onCloseWithoutSave}>
|
||||
Don't Save
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<button className="btn btn-close btn-sm mr-2" onClick={onCancel}>
|
||||
<Button size="sm" color="secondary" variant="ghost" onClick={onCancel}>
|
||||
Cancel
|
||||
</button>
|
||||
<button className="btn btn-secondary btn-sm" onClick={onSaveAndClose}>
|
||||
</Button>
|
||||
<Button size="sm" onClick={onSaveAndClose}>
|
||||
Save
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
@@ -18,6 +18,7 @@ import WsseAuth from 'components/RequestPane/Auth/WsseAuth';
|
||||
import ApiKeyAuth from 'components/RequestPane/Auth/ApiKeyAuth';
|
||||
import AwsV4Auth from 'components/RequestPane/Auth/AwsV4Auth';
|
||||
import { humanizeRequestAuthMode, getTreePathFromCollectionToItem } from 'utils/collections/index';
|
||||
import Button from 'ui/Button';
|
||||
|
||||
const GrantTypeComponentMap = ({ collection, folder }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -211,9 +212,9 @@ const Auth = ({ collection, folder }) => {
|
||||
</div>
|
||||
{getAuthView()}
|
||||
<div className="mt-6">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
|
||||
<Button type="submit" size="sm" onClick={handleSave}>
|
||||
Save
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -10,6 +10,7 @@ import StyledWrapper from './StyledWrapper';
|
||||
import { headers as StandardHTTPHeaders } from 'know-your-http-well';
|
||||
import { MimeTypes } from 'utils/codemirror/autocompleteConstants';
|
||||
import BulkEditor from 'components/BulkEditor/index';
|
||||
import Button from 'ui/Button';
|
||||
|
||||
const headerAutoCompleteList = StandardHTTPHeaders.map((e) => e.header);
|
||||
|
||||
@@ -112,9 +113,9 @@ const Headers = ({ collection, folder }) => {
|
||||
</button>
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
|
||||
<Button type="submit" size="sm" onClick={handleSave}>
|
||||
Save
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -7,6 +7,7 @@ import { saveFolderRoot } from 'providers/ReduxStore/slices/collections/actions'
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from 'components/Tabs';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import Button from 'ui/Button';
|
||||
|
||||
const Script = ({ collection, folder }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -100,9 +101,9 @@ const Script = ({ collection, folder }) => {
|
||||
</Tabs>
|
||||
|
||||
<div className="mt-12">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
|
||||
<Button type="submit" size="sm" onClick={handleSave}>
|
||||
Save
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -6,6 +6,7 @@ import { updateFolderTests } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveFolderRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import Button from 'ui/Button';
|
||||
|
||||
const Tests = ({ collection, folder }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -42,9 +43,9 @@ const Tests = ({ collection, folder }) => {
|
||||
/>
|
||||
|
||||
<div className="mt-6">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
|
||||
<Button type="submit" size="sm" onClick={handleSave}>
|
||||
Save
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -4,6 +4,7 @@ import VarsTable from './VarsTable';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { saveFolderRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import Button from 'ui/Button';
|
||||
|
||||
const Vars = ({ collection, folder }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -22,9 +23,9 @@ const Vars = ({ collection, folder }) => {
|
||||
<VarsTable folder={folder} collection={collection} vars={responseVars} varType="response" />
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
|
||||
<Button type="submit" size="sm" onClick={handleSave}>
|
||||
Save
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
} from '@tabler/icons';
|
||||
import { flattenItems, isItemARequest, isItemAFolder, findParentItemInCollection } from 'utils/collections';
|
||||
import { addTab, focusTab } from 'providers/ReduxStore/slices/tabs';
|
||||
import { hideHomePage } from 'providers/ReduxStore/slices/app';
|
||||
import { toggleCollectionItem, toggleCollection } from 'providers/ReduxStore/slices/collections';
|
||||
import { mountCollection } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { getDefaultRequestPaneTab } from 'utils/collections';
|
||||
@@ -246,8 +245,6 @@ const GlobalSearchModal = ({ isOpen, onClose }) => {
|
||||
expandItemPath(result);
|
||||
|
||||
if (result.type === SEARCH_TYPES.REQUEST) {
|
||||
dispatch(hideHomePage());
|
||||
|
||||
const existingTab = tabs.find((tab) => tab.uid === result.item.uid);
|
||||
|
||||
if (existingTab) {
|
||||
|
||||
@@ -32,7 +32,7 @@ const DeleteWorkspace = ({ onClose, workspace }) => {
|
||||
handleConfirm={onConfirm}
|
||||
handleCancel={onClose}
|
||||
confirmDisabled={isDeleting}
|
||||
confirmButtonClass="btn-danger"
|
||||
confirmButtonColor="danger"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<IconFolder size={18} strokeWidth={1.5} />
|
||||
|
||||
@@ -13,6 +13,7 @@ import RenameWorkspace from './RenameWorkspace';
|
||||
import DeleteWorkspace from './DeleteWorkspace';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import MenuDropdown from 'ui/MenuDropdown/index';
|
||||
import Button from 'ui/Button';
|
||||
|
||||
const ManageWorkspace = () => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -84,10 +85,9 @@ const ManageWorkspace = () => {
|
||||
</div>
|
||||
<span className="header-title">Manage Workspace</span>
|
||||
</div>
|
||||
<button className="create-workspace-btn" onClick={() => setCreateWorkspaceModalOpen(true)}>
|
||||
<IconPlus size={14} strokeWidth={2} />
|
||||
<span>Create Workspace</span>
|
||||
</button>
|
||||
<Button size="sm" onClick={() => setCreateWorkspaceModalOpen(true)} icon={<IconPlus size={14} strokeWidth={2} />}>
|
||||
Create Workspace
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="workspace-list">
|
||||
|
||||
@@ -28,8 +28,8 @@ const Wrapper = styled.div`
|
||||
.bruno-modal-card {
|
||||
animation-duration: 0.85s;
|
||||
animation-delay: 0.1s;
|
||||
background: var(--color-background-top);
|
||||
border-radius: var(--border-radius);
|
||||
background: ${(props) => props.theme.modal.body.bg};
|
||||
border-radius: ${(props) => props.theme.border.radius.base};
|
||||
position: relative;
|
||||
z-index: 11;
|
||||
max-width: calc(100% - var(--spacing-base-unit));
|
||||
@@ -68,25 +68,37 @@ const Wrapper = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
text-transform: uppercase;
|
||||
color: ${(props) => props.theme.modal.title.color};
|
||||
background-color: ${(props) => props.theme.modal.title.bg};
|
||||
font-size: ${(props) => props.theme.font.size.sm};
|
||||
padding: 12px;
|
||||
font-size: ${(props) => props.theme.font.size.md};
|
||||
padding: 0.5rem 1rem;
|
||||
font-weight: 500;
|
||||
border-top-left-radius: 4px;
|
||||
border-top-right-radius: 4px;
|
||||
border-top-left-radius: ${(props) => props.theme.border.radius.base};
|
||||
border-top-right-radius: ${(props) => props.theme.border.radius.base};
|
||||
|
||||
.bruno-modal-header-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.close {
|
||||
font-size: 1.3rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin-right: -0.5rem;
|
||||
font-size: 1.125rem;
|
||||
line-height: 1;
|
||||
color: ${(props) => props.theme.modal.iconColor};
|
||||
text-shadow: 0 1px 0 #fff;
|
||||
opacity: 0.5;
|
||||
margin-top: -2px;
|
||||
color: ${(props) => props.theme.modal.title.color};
|
||||
border-radius: ${(props) => props.theme.border.radius.sm};
|
||||
opacity: 0.7;
|
||||
transition: opacity 0.2s ease, background-color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
opacity: 1;
|
||||
background-color: ${(props) => props.theme.modal.closeButton.hoverBg};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -104,7 +116,7 @@ const Wrapper = styled.div`
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
transition: border-color ease-in-out 0.1s;
|
||||
border-radius: 3px;
|
||||
border-radius: ${(props) => props.theme.border.radius.sm};
|
||||
background-color: ${(props) => props.theme.modal.input.bg};
|
||||
border: 1px solid ${(props) => props.theme.modal.input.border};
|
||||
|
||||
@@ -144,14 +156,14 @@ const Wrapper = styled.div`
|
||||
|
||||
.bruno-modal-footer {
|
||||
background-color: ${(props) => props.theme.modal.body.bg};
|
||||
border-bottom-left-radius: 3px;
|
||||
border-bottom-right-radius: 3px;
|
||||
border-bottom-left-radius: ${(props) => props.theme.border.radius.base};
|
||||
border-bottom-right-radius: ${(props) => props.theme.border.radius.base};
|
||||
}
|
||||
|
||||
&.modal-footer-none {
|
||||
.bruno-modal-content {
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
border-bottom-left-radius: ${(props) => props.theme.border.radius.base};
|
||||
border-bottom-right-radius: ${(props) => props.theme.border.radius.base};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import useFocusTrap from 'hooks/useFocusTrap';
|
||||
import Button from 'ui/Button';
|
||||
|
||||
const ESC_KEY_CODE = 27;
|
||||
const ENTER_KEY_CODE = 13;
|
||||
@@ -10,7 +11,7 @@ const ModalHeader = ({ title, handleCancel, customHeader, hideClose }) => (
|
||||
{customHeader ? customHeader : <>{title ? <div className="bruno-modal-header-title">{title}</div> : null}</>}
|
||||
{handleCancel && !hideClose ? (
|
||||
// TODO: Remove data-test-id and use data-testid instead across the codebase.
|
||||
<div className="close cursor-pointer" onClick={handleCancel ? () => handleCancel() : null} data-test-id="modal-close-button" data-testid="modal-close-button">
|
||||
<div className="close cursor-pointer" onClick={handleCancel ? () => handleCancel() : null} data-testid="modal-close-button">
|
||||
×
|
||||
</div>
|
||||
) : null}
|
||||
@@ -27,7 +28,7 @@ const ModalFooter = ({
|
||||
confirmDisabled,
|
||||
hideCancel,
|
||||
hideFooter,
|
||||
confirmButtonClass = 'btn-secondary'
|
||||
confirmButtonColor = 'primary'
|
||||
}) => {
|
||||
confirmText = confirmText || 'Save';
|
||||
cancelText = cancelText || 'Cancel';
|
||||
@@ -39,19 +40,20 @@ const ModalFooter = ({
|
||||
return (
|
||||
<div className="flex justify-end p-4 bruno-modal-footer">
|
||||
<span className={hideCancel ? 'hidden' : 'mr-2'}>
|
||||
<button type="button" onClick={handleCancel} className="btn btn-md btn-close">
|
||||
<Button type="button" color="secondary" variant="ghost" onClick={handleCancel}>
|
||||
{cancelText}
|
||||
</button>
|
||||
</Button>
|
||||
</span>
|
||||
<span>
|
||||
<button
|
||||
<Button
|
||||
type="submit"
|
||||
className={`submit btn btn-md ${confirmButtonClass}`}
|
||||
color={confirmButtonColor}
|
||||
disabled={confirmDisabled}
|
||||
onClick={handleSubmit}
|
||||
className="submit"
|
||||
>
|
||||
{confirmText}
|
||||
</button>
|
||||
</Button>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
@@ -75,7 +77,7 @@ const Modal = ({
|
||||
onClick,
|
||||
closeModalFadeTimeout = 500,
|
||||
dataTestId,
|
||||
confirmButtonClass
|
||||
confirmButtonColor = 'primary'
|
||||
}) => {
|
||||
const modalRef = useRef(null);
|
||||
const [isClosing, setIsClosing] = useState(false);
|
||||
@@ -149,7 +151,7 @@ const Modal = ({
|
||||
confirmDisabled={confirmDisabled}
|
||||
hideCancel={hideCancel}
|
||||
hideFooter={hideFooter}
|
||||
confirmButtonClass={confirmButtonClass}
|
||||
confirmButtonColor={confirmButtonColor}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -154,8 +154,10 @@ class MultiLineEditor extends Component {
|
||||
this.editor.setOption('readOnly', this.props.readOnly);
|
||||
}
|
||||
if (this.props.value !== prevProps.value && this.props.value !== this.cachedValue && this.editor) {
|
||||
const cursor = this.editor.getCursor();
|
||||
this.cachedValue = String(this.props.value);
|
||||
this.editor.setValue(String(this.props.value) || '');
|
||||
this.editor.setCursor(cursor);
|
||||
}
|
||||
if (!isEqual(this.props.isSecret, prevProps.isSecret)) {
|
||||
// If the secret flag has changed, update the editor to reflect the change
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import React from 'react';
|
||||
import React, { useEffect, useCallback } from 'react';
|
||||
import { useFormik } from 'formik';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { savePreferences } from 'providers/ReduxStore/slices/app';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import * as Yup from 'yup';
|
||||
import debounce from 'lodash/debounce';
|
||||
import toast from 'react-hot-toast';
|
||||
import { IconFlask } from '@tabler/icons';
|
||||
import get from 'lodash/get';
|
||||
@@ -56,19 +57,37 @@ const Beta = ({ close }) => {
|
||||
}
|
||||
});
|
||||
|
||||
const handleSave = (newBetaPreferences) => {
|
||||
const handleSave = useCallback((newBetaPreferences) => {
|
||||
dispatch(
|
||||
savePreferences({
|
||||
...preferences,
|
||||
beta: newBetaPreferences
|
||||
})
|
||||
)
|
||||
.then(() => {
|
||||
toast.success('Beta preferences saved successfully');
|
||||
close();
|
||||
})
|
||||
.catch((err) => console.log(err) && toast.error('Failed to update beta preferences'));
|
||||
};
|
||||
}, [dispatch, preferences]);
|
||||
|
||||
const debouncedSave = useCallback(
|
||||
debounce((values) => {
|
||||
betaSchema.validate(values, { abortEarly: true })
|
||||
.then((validatedValues) => {
|
||||
handleSave(validatedValues);
|
||||
})
|
||||
.catch((error) => {
|
||||
});
|
||||
}, 500),
|
||||
[handleSave, betaSchema]
|
||||
);
|
||||
|
||||
// Auto-save when form values change
|
||||
useEffect(() => {
|
||||
if (formik.dirty && formik.isValid) {
|
||||
debouncedSave(formik.values);
|
||||
}
|
||||
return () => {
|
||||
debouncedSave.cancel();
|
||||
};
|
||||
}, [formik.values, formik.dirty, formik.isValid, debouncedSave]);
|
||||
|
||||
const hasAnyBetaFeatures = BETA_FEATURES.length > 0;
|
||||
|
||||
@@ -113,12 +132,6 @@ const Beta = ({ close }) => {
|
||||
<p>No beta features are currently available</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-10">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary">
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import debounce from 'lodash/debounce';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { savePreferences } from 'providers/ReduxStore/slices/app';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
@@ -8,6 +9,7 @@ import toast from 'react-hot-toast';
|
||||
const Font = ({ close }) => {
|
||||
const dispatch = useDispatch();
|
||||
const preferences = useSelector((state) => state.app.preferences);
|
||||
const isInitialMount = useRef(true);
|
||||
|
||||
const [codeFont, setCodeFont] = useState(get(preferences, 'font.codeFont', 'default'));
|
||||
const [codeFontSize, setCodeFontSize] = useState(get(preferences, 'font.codeFontSize', '13'));
|
||||
@@ -22,22 +24,37 @@ const Font = ({ close }) => {
|
||||
setCodeFontSize(clampedSize);
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
const handleSave = useCallback((font, fontSize) => {
|
||||
dispatch(
|
||||
savePreferences({
|
||||
...preferences,
|
||||
font: {
|
||||
codeFont,
|
||||
codeFontSize
|
||||
codeFont: font,
|
||||
codeFontSize: fontSize
|
||||
}
|
||||
})
|
||||
).then(() => {
|
||||
toast.success('Preferences saved successfully');
|
||||
close();
|
||||
}).catch(() => {
|
||||
).catch(() => {
|
||||
toast.error('Failed to save preferences');
|
||||
});
|
||||
};
|
||||
}, [dispatch, preferences]);
|
||||
|
||||
const debouncedSave = useCallback(
|
||||
debounce((font, fontSize) => {
|
||||
handleSave(font, fontSize);
|
||||
}, 500),
|
||||
[handleSave]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (isInitialMount.current) {
|
||||
isInitialMount.current = false;
|
||||
return;
|
||||
}
|
||||
debouncedSave(codeFont, codeFontSize);
|
||||
return () => {
|
||||
debouncedSave.cancel();
|
||||
};
|
||||
}, [codeFont, codeFontSize, debouncedSave]);
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
@@ -68,12 +85,6 @@ const Font = ({ close }) => {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-10">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useRef } from 'react';
|
||||
import React, { useRef, useEffect, useCallback } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import debounce from 'lodash/debounce';
|
||||
import { useFormik } from 'formik';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { savePreferences } from 'providers/ReduxStore/slices/app';
|
||||
@@ -95,7 +96,7 @@ const General = ({ close }) => {
|
||||
}
|
||||
});
|
||||
|
||||
const handleSave = (newPreferences) => {
|
||||
const handleSave = useCallback((newPreferences) => {
|
||||
dispatch(
|
||||
savePreferences({
|
||||
...preferences,
|
||||
@@ -123,12 +124,29 @@ const General = ({ close }) => {
|
||||
defaultCollectionLocation: newPreferences.defaultCollectionLocation
|
||||
}
|
||||
}))
|
||||
.then(() => {
|
||||
toast.success('Preferences saved successfully');
|
||||
close();
|
||||
})
|
||||
.catch((err) => console.log(err) && toast.error('Failed to update preferences'));
|
||||
};
|
||||
}, [dispatch, preferences]);
|
||||
|
||||
const debouncedSave = useCallback(
|
||||
debounce((values) => {
|
||||
preferencesSchema.validate(values, { abortEarly: true })
|
||||
.then((validatedValues) => {
|
||||
handleSave(validatedValues);
|
||||
})
|
||||
.catch((error) => {
|
||||
});
|
||||
}, 500),
|
||||
[handleSave]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (formik.dirty && formik.isValid) {
|
||||
debouncedSave(formik.values);
|
||||
}
|
||||
return () => {
|
||||
debouncedSave.cancel();
|
||||
};
|
||||
}, [formik.values, formik.dirty, formik.isValid, debouncedSave]);
|
||||
|
||||
const addCaCertificate = (e) => {
|
||||
const filePath = window?.ipcRenderer?.getFilePath(e?.target?.files?.[0]);
|
||||
@@ -366,11 +384,6 @@ const General = ({ close }) => {
|
||||
{formik.touched.defaultCollectionLocation && formik.errors.defaultCollectionLocation ? (
|
||||
<div className="text-red-500">{formik.errors.defaultCollectionLocation}</div>
|
||||
) : null}
|
||||
<div className="mt-10">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary">
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import React, { useEffect, useCallback } from 'react';
|
||||
import { useFormik } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
import debounce from 'lodash/debounce';
|
||||
import toast from 'react-hot-toast';
|
||||
import { savePreferences } from 'providers/ReduxStore/slices/app';
|
||||
|
||||
@@ -74,7 +75,7 @@ const ProxySettings = ({ close }) => {
|
||||
}
|
||||
});
|
||||
|
||||
const onUpdate = (values) => {
|
||||
const onUpdate = useCallback((values) => {
|
||||
proxySchema
|
||||
.validate(values, { abortEarly: true })
|
||||
.then((validatedProxy) => {
|
||||
@@ -83,18 +84,20 @@ const ProxySettings = ({ close }) => {
|
||||
...preferences,
|
||||
proxy: validatedProxy
|
||||
})
|
||||
).then(() => {
|
||||
toast.success('Preferences saved successfully');
|
||||
close();
|
||||
}).catch(() => {
|
||||
).catch(() => {
|
||||
toast.error('Failed to save preferences');
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
let errMsg = error.message || 'Preferences validation error';
|
||||
toast.error(errMsg);
|
||||
});
|
||||
};
|
||||
}, [dispatch, preferences, proxySchema]);
|
||||
|
||||
const debouncedSave = useCallback(
|
||||
debounce((values) => {
|
||||
onUpdate(values);
|
||||
}, 500),
|
||||
[onUpdate]
|
||||
);
|
||||
|
||||
const [passwordVisible, setPasswordVisible] = useState(false);
|
||||
|
||||
@@ -113,6 +116,15 @@ const ProxySettings = ({ close }) => {
|
||||
});
|
||||
}, [preferences]);
|
||||
|
||||
useEffect(() => {
|
||||
if (formik.dirty) {
|
||||
debouncedSave(formik.values);
|
||||
}
|
||||
return () => {
|
||||
debouncedSave.cancel();
|
||||
};
|
||||
}, [formik.values, formik.dirty, debouncedSave]);
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<form className="bruno-form" onSubmit={formik.handleSubmit}>
|
||||
@@ -365,11 +377,6 @@ const ProxySettings = ({ close }) => {
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
<div className="mt-6">
|
||||
<button type="submit" className="submit btn btn-md btn-secondary">
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -2,14 +2,20 @@ import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
div.tabs {
|
||||
padding: 8px;
|
||||
min-width: 160px;
|
||||
|
||||
div.tab {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
min-width: 120px;
|
||||
padding: 7px 10px;
|
||||
padding: 6px 10px;
|
||||
border: none;
|
||||
border-bottom: solid 2px transparent;
|
||||
color: var(--color-tab-inactive);
|
||||
border-radius: ${(props) => props.theme.border.radius.sm};
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
cursor: pointer;
|
||||
transition: background-color 0.15s ease;
|
||||
|
||||
&:focus,
|
||||
&:active,
|
||||
@@ -21,18 +27,36 @@ const StyledWrapper = styled.div`
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: ${(props) => props.theme.sidebar.color} !important;
|
||||
background: ${(props) => props.theme.sidebar.collection.item.bg};
|
||||
color: ${(props) => props.theme.text} !important;
|
||||
background: ${(props) => props.theme.modal.title.bg};
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.sidebar.collection.item.bg} !important;
|
||||
background: ${(props) => props.theme.modal.title.bg} !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
section.tab-panel {
|
||||
min-height: 300px;
|
||||
min-height: 70vh;
|
||||
max-height: 70vh;
|
||||
overflow-y: auto;
|
||||
max-width: 50vw;
|
||||
}
|
||||
|
||||
input[type="checkbox"],
|
||||
input[type="radio"] {
|
||||
accent-color: ${(props) => props.theme.workspace.accent};
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
font-size: ${(props) => props.theme.font.size.sm};
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
font-weight: 500;
|
||||
margin-bottom: 8px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -78,7 +78,7 @@ const WsseAuth = ({ item, collection, updateAuth, request, save }) => {
|
||||
item={item}
|
||||
isSecret={true}
|
||||
/>
|
||||
{showWarning && <SensitiveFieldWarning fieldName="wsse-password" message={warningMessage} />}
|
||||
{showWarning && <SensitiveFieldWarning fieldName="wsse-password" warningMessage={warningMessage} />}
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { closeTabs } from 'providers/ReduxStore/slices/tabs';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import Button from 'ui/Button';
|
||||
|
||||
const ExampleNotFound = ({ exampleUid }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -30,9 +31,9 @@ const ExampleNotFound = ({ exampleUid }) => {
|
||||
This can occur when the example definition in your local file has been deleted or updated.
|
||||
</div>
|
||||
</div>
|
||||
<button className="btn btn-md btn-secondary mt-6" onClick={closeTab}>
|
||||
<Button size="md" color="secondary" variant="ghost" onClick={closeTab}>
|
||||
Close Tab
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import { closeTabs } from 'providers/ReduxStore/slices/tabs';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import Button from 'ui/Button';
|
||||
|
||||
const FolderNotFound = ({ folderUid }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -32,9 +33,9 @@ const FolderNotFound = ({ folderUid }) => {
|
||||
This can happen when the folder was renamed or deleted on your filesystem.
|
||||
</div>
|
||||
</div>
|
||||
<button className="btn btn-md btn-secondary mt-6" onClick={closeTab}>
|
||||
<Button size="md" color="secondary" variant="ghost" onClick={closeTab}>
|
||||
Close Tab
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { closeTabs } from 'providers/ReduxStore/slices/tabs';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import Button from 'ui/Button';
|
||||
|
||||
const RequestNotFound = ({ itemUid }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -36,9 +37,9 @@ const RequestNotFound = ({ itemUid }) => {
|
||||
This can happen when the .bru file associated with this request was deleted on your filesystem.
|
||||
</div>
|
||||
</div>
|
||||
<button className="btn btn-md btn-secondary mt-6" onClick={closeTab}>
|
||||
<Button size="md" color="secondary" variant="ghost" onClick={closeTab}>
|
||||
Close Tab
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -38,7 +38,7 @@ import EnvironmentSettings from 'components/Environments/EnvironmentSettings';
|
||||
import GlobalEnvironmentSettings from 'components/Environments/GlobalEnvironmentSettings';
|
||||
|
||||
const MIN_LEFT_PANE_WIDTH = 300;
|
||||
const MIN_RIGHT_PANE_WIDTH = 480;
|
||||
const MIN_RIGHT_PANE_WIDTH = 490;
|
||||
const MIN_TOP_PANE_HEIGHT = 150;
|
||||
const MIN_BOTTOM_PANE_HEIGHT = 150;
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import { IconAlertTriangle } from '@tabler/icons';
|
||||
import Modal from 'components/Modal';
|
||||
import Button from 'ui/Button';
|
||||
|
||||
const ConfirmCollectionClose = ({ collection, onCancel, onCloseWithoutSave, onSaveAndClose }) => {
|
||||
return (
|
||||
@@ -29,17 +30,17 @@ const ConfirmCollectionClose = ({ collection, onCancel, onCloseWithoutSave, onSa
|
||||
|
||||
<div className="flex justify-between mt-6">
|
||||
<div>
|
||||
<button className="btn btn-sm btn-danger" onClick={onCloseWithoutSave}>
|
||||
<Button size="sm" color="danger" onClick={onCloseWithoutSave}>
|
||||
Don't Save
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<button className="btn btn-close btn-sm mr-2" onClick={onCancel}>
|
||||
<Button size="sm" color="secondary" variant="ghost" onClick={onCancel}>
|
||||
Cancel
|
||||
</button>
|
||||
<button className="btn btn-secondary btn-sm" onClick={onSaveAndClose}>
|
||||
</Button>
|
||||
<Button size="sm" onClick={onSaveAndClose}>
|
||||
Save
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import { IconAlertTriangle } from '@tabler/icons';
|
||||
import Modal from 'components/Modal';
|
||||
import Button from 'ui/Button';
|
||||
|
||||
const ConfirmFolderClose = ({ folder, onCancel, onCloseWithoutSave, onSaveAndClose }) => {
|
||||
return (
|
||||
@@ -29,17 +30,17 @@ const ConfirmFolderClose = ({ folder, onCancel, onCloseWithoutSave, onSaveAndClo
|
||||
|
||||
<div className="flex justify-between mt-6">
|
||||
<div>
|
||||
<button className="btn btn-sm btn-danger" onClick={onCloseWithoutSave}>
|
||||
<Button size="sm" color="danger" onClick={onCloseWithoutSave}>
|
||||
Don't Save
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<button className="btn btn-close btn-sm mr-2" onClick={onCancel}>
|
||||
<Button size="sm" color="secondary" variant="ghost" onClick={onCancel}>
|
||||
Cancel
|
||||
</button>
|
||||
<button className="btn btn-secondary btn-sm" onClick={onSaveAndClose}>
|
||||
</Button>
|
||||
<Button size="sm" onClick={onSaveAndClose}>
|
||||
Save
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import { IconAlertTriangle } from '@tabler/icons';
|
||||
import Modal from 'components/Modal';
|
||||
import Button from 'ui/Button';
|
||||
|
||||
const ConfirmRequestClose = ({ item, example, onCancel, onCloseWithoutSave, onSaveAndClose }) => {
|
||||
const isExample = !!example;
|
||||
@@ -33,17 +34,17 @@ const ConfirmRequestClose = ({ item, example, onCancel, onCloseWithoutSave, onSa
|
||||
|
||||
<div className="flex justify-between mt-6">
|
||||
<div>
|
||||
<button className="btn btn-sm btn-danger" onClick={onCloseWithoutSave}>
|
||||
<Button size="sm" color="danger" onClick={onCloseWithoutSave}>
|
||||
Don't Save
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<button className="btn btn-close btn-sm mr-2" onClick={onCancel}>
|
||||
<Button size="sm" color="secondary" variant="ghost" onClick={onCancel}>
|
||||
Cancel
|
||||
</button>
|
||||
<button className="btn btn-secondary btn-sm" onClick={onSaveAndClose}>
|
||||
</Button>
|
||||
<Button size="sm" onClick={onSaveAndClose}>
|
||||
Save
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
@@ -17,7 +17,7 @@ import ConfirmCloseEnvironment from 'components/Environments/ConfirmCloseEnviron
|
||||
import RequestTabNotFound from './RequestTabNotFound';
|
||||
import SpecialTab from './SpecialTab';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import MenuDropdown from 'ui/MenuDropdown';
|
||||
import CloneCollectionItem from 'components/Sidebar/Collections/Collection/CollectionItem/CloneCollectionItem/index';
|
||||
import NewRequest from 'components/Sidebar/NewRequest/index';
|
||||
import GradientCloseButton from './GradientCloseButton';
|
||||
@@ -26,11 +26,12 @@ import { closeWsConnection } from 'utils/network/index';
|
||||
import ExampleTab from '../ExampleTab';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUid, hasOverflow, setHasOverflow }) => {
|
||||
const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUid, hasOverflow, setHasOverflow, dropdownContainerRef }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
const theme = storedTheme === 'dark' ? darkTheme : lightTheme;
|
||||
const tabNameRef = useRef(null);
|
||||
const tabLabelRef = useRef(null);
|
||||
const lastOverflowStateRef = useRef(null);
|
||||
const [showConfirmClose, setShowConfirmClose] = useState(false);
|
||||
const [showConfirmCollectionClose, setShowConfirmCollectionClose] = useState(false);
|
||||
@@ -38,8 +39,7 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi
|
||||
const [showConfirmEnvironmentClose, setShowConfirmEnvironmentClose] = useState(false);
|
||||
const [showConfirmGlobalEnvironmentClose, setShowConfirmGlobalEnvironmentClose] = useState(false);
|
||||
|
||||
const dropdownTippyRef = useRef();
|
||||
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
|
||||
const menuDropdownRef = useRef();
|
||||
|
||||
const item = findItemInCollection(collection, tab.uid);
|
||||
|
||||
@@ -99,17 +99,10 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi
|
||||
);
|
||||
};
|
||||
|
||||
const handleRightClick = (_event) => {
|
||||
const menuDropdown = dropdownTippyRef.current;
|
||||
if (!menuDropdown) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (menuDropdown.state.isShown) {
|
||||
menuDropdown.hide();
|
||||
} else {
|
||||
menuDropdown.show();
|
||||
}
|
||||
const handleRightClick = (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
menuDropdownRef.current?.show();
|
||||
};
|
||||
|
||||
const handleMouseUp = (e) => {
|
||||
@@ -383,6 +376,7 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
ref={tabLabelRef}
|
||||
className={`flex items-baseline tab-label ${tab.preview ? 'italic' : ''}`}
|
||||
onContextMenu={handleRightClick}
|
||||
onDoubleClick={() => dispatch(makeTabPermanent({ uid: tab.uid }))}
|
||||
@@ -403,13 +397,13 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi
|
||||
{item.name}
|
||||
</span>
|
||||
<RequestTabMenu
|
||||
onDropdownCreate={onDropdownCreate}
|
||||
menuDropdownRef={menuDropdownRef}
|
||||
tabLabelRef={tabLabelRef}
|
||||
tabIndex={tabIndex}
|
||||
collectionRequestTabs={collectionRequestTabs}
|
||||
tabItem={item}
|
||||
collection={collection}
|
||||
dropdownTippyRef={dropdownTippyRef}
|
||||
dispatch={dispatch}
|
||||
dropdownContainerRef={dropdownContainerRef}
|
||||
/>
|
||||
</div>
|
||||
<GradientCloseButton
|
||||
@@ -429,10 +423,19 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi
|
||||
);
|
||||
};
|
||||
|
||||
function RequestTabMenu({ onDropdownCreate, collectionRequestTabs, tabIndex, collection, dropdownTippyRef, dispatch }) {
|
||||
function RequestTabMenu({ menuDropdownRef, tabLabelRef, collectionRequestTabs, tabIndex, collection, dispatch, dropdownContainerRef }) {
|
||||
const [showCloneRequestModal, setShowCloneRequestModal] = useState(false);
|
||||
const [showAddNewRequestModal, setShowAddNewRequestModal] = useState(false);
|
||||
|
||||
// Returns the tab-label's position for dropdown positioning.
|
||||
// Returns zero-sized rect if element isn't mounted yet (prevents Tippy errors).
|
||||
const getTabLabelRect = () => {
|
||||
if (!tabLabelRef.current) {
|
||||
return { width: 0, height: 0, top: 0, bottom: 0, left: 0, right: 0 };
|
||||
}
|
||||
return tabLabelRef.current.getBoundingClientRect();
|
||||
};
|
||||
|
||||
const totalTabs = collectionRequestTabs.length || 0;
|
||||
const currentTabUid = collectionRequestTabs[tabIndex]?.uid;
|
||||
const currentTabItem = findItemInCollection(collection, currentTabUid);
|
||||
@@ -442,10 +445,7 @@ function RequestTabMenu({ onDropdownCreate, collectionRequestTabs, tabIndex, col
|
||||
const hasRightTabs = totalTabs > tabIndex + 1;
|
||||
const hasOtherTabs = totalTabs > 1;
|
||||
|
||||
async function handleCloseTab(event, tabUid) {
|
||||
event.stopPropagation();
|
||||
dropdownTippyRef.current.hide();
|
||||
|
||||
async function handleCloseTab(tabUid) {
|
||||
if (!tabUid) {
|
||||
return;
|
||||
}
|
||||
@@ -461,10 +461,7 @@ function RequestTabMenu({ onDropdownCreate, collectionRequestTabs, tabIndex, col
|
||||
} catch (err) { }
|
||||
}
|
||||
|
||||
function handleRevertChanges(event) {
|
||||
event.stopPropagation();
|
||||
dropdownTippyRef.current.hide();
|
||||
|
||||
function handleRevertChanges() {
|
||||
if (!currentTabUid) {
|
||||
return;
|
||||
}
|
||||
@@ -480,40 +477,96 @@ function RequestTabMenu({ onDropdownCreate, collectionRequestTabs, tabIndex, col
|
||||
} catch (err) { }
|
||||
}
|
||||
|
||||
function handleCloseOtherTabs(event) {
|
||||
dropdownTippyRef.current.hide();
|
||||
|
||||
async function handleCloseOtherTabs() {
|
||||
const otherTabs = collectionRequestTabs.filter((_, index) => index !== tabIndex);
|
||||
otherTabs.forEach((tab) => handleCloseTab(event, tab.uid));
|
||||
await Promise.all(otherTabs.map((tab) => handleCloseTab(tab.uid)));
|
||||
}
|
||||
|
||||
function handleCloseTabsToTheLeft(event) {
|
||||
dropdownTippyRef.current.hide();
|
||||
|
||||
async function handleCloseTabsToTheLeft() {
|
||||
const leftTabs = collectionRequestTabs.filter((_, index) => index < tabIndex);
|
||||
leftTabs.forEach((tab) => handleCloseTab(event, tab.uid));
|
||||
await Promise.all(leftTabs.map((tab) => handleCloseTab(tab.uid)));
|
||||
}
|
||||
|
||||
function handleCloseTabsToTheRight(event) {
|
||||
dropdownTippyRef.current.hide();
|
||||
|
||||
async function handleCloseTabsToTheRight() {
|
||||
const rightTabs = collectionRequestTabs.filter((_, index) => index > tabIndex);
|
||||
rightTabs.forEach((tab) => handleCloseTab(event, tab.uid));
|
||||
await Promise.all(rightTabs.map((tab) => handleCloseTab(tab.uid)));
|
||||
}
|
||||
|
||||
function handleCloseSavedTabs(event) {
|
||||
event.stopPropagation();
|
||||
|
||||
function handleCloseSavedTabs() {
|
||||
const items = flattenItems(collection?.items);
|
||||
const savedTabs = items?.filter?.((item) => !hasRequestChanges(item));
|
||||
const savedTabIds = savedTabs?.map((item) => item.uid) || [];
|
||||
dispatch(closeTabs({ tabUids: savedTabIds }));
|
||||
}
|
||||
|
||||
function handleCloseAllTabs(event) {
|
||||
collectionRequestTabs.forEach((tab) => handleCloseTab(event, tab.uid));
|
||||
async function handleCloseAllTabs() {
|
||||
await Promise.all(collectionRequestTabs.map((tab) => handleCloseTab(tab.uid)));
|
||||
}
|
||||
|
||||
const menuItems = useMemo(() => [
|
||||
{
|
||||
id: 'new-request',
|
||||
label: 'New Request',
|
||||
onClick: () => setShowAddNewRequestModal(true)
|
||||
},
|
||||
{
|
||||
id: 'clone-request',
|
||||
label: 'Clone Request',
|
||||
onClick: () => setShowCloneRequestModal(true)
|
||||
},
|
||||
{
|
||||
id: 'revert-changes',
|
||||
label: 'Revert Changes',
|
||||
onClick: handleRevertChanges,
|
||||
disabled: !currentTabItem?.draft
|
||||
},
|
||||
{
|
||||
id: 'close',
|
||||
label: 'Close',
|
||||
onClick: () => handleCloseTab(currentTabUid)
|
||||
},
|
||||
{
|
||||
id: 'close-others',
|
||||
label: 'Close Others',
|
||||
onClick: handleCloseOtherTabs,
|
||||
disabled: !hasOtherTabs
|
||||
},
|
||||
{
|
||||
id: 'close-left',
|
||||
label: 'Close to the Left',
|
||||
onClick: handleCloseTabsToTheLeft,
|
||||
disabled: !hasLeftTabs
|
||||
},
|
||||
{
|
||||
id: 'close-right',
|
||||
label: 'Close to the Right',
|
||||
onClick: handleCloseTabsToTheRight,
|
||||
disabled: !hasRightTabs
|
||||
},
|
||||
{
|
||||
id: 'close-saved',
|
||||
label: 'Close Saved',
|
||||
onClick: handleCloseSavedTabs
|
||||
},
|
||||
{
|
||||
id: 'close-all',
|
||||
label: 'Close All',
|
||||
onClick: handleCloseAllTabs
|
||||
}
|
||||
], [currentTabUid, currentTabItem, hasOtherTabs, hasLeftTabs, hasRightTabs, collection, collectionRequestTabs, tabIndex, dispatch]);
|
||||
|
||||
const menuDropdown = (
|
||||
<MenuDropdown
|
||||
ref={menuDropdownRef}
|
||||
items={menuItems}
|
||||
placement="bottom-start"
|
||||
appendTo={dropdownContainerRef?.current || document.body}
|
||||
getReferenceClientRect={getTabLabelRect}
|
||||
>
|
||||
<span></span>
|
||||
</MenuDropdown>
|
||||
);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{showAddNewRequestModal && (
|
||||
@@ -528,51 +581,7 @@ function RequestTabMenu({ onDropdownCreate, collectionRequestTabs, tabIndex, col
|
||||
/>
|
||||
)}
|
||||
|
||||
<Dropdown onCreate={onDropdownCreate} icon={<span></span>} placement="bottom-start">
|
||||
<button
|
||||
className="dropdown-item w-full"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
setShowAddNewRequestModal(true);
|
||||
}}
|
||||
>
|
||||
New Request
|
||||
</button>
|
||||
<button
|
||||
className="dropdown-item w-full"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
setShowCloneRequestModal(true);
|
||||
}}
|
||||
>
|
||||
Clone Request
|
||||
</button>
|
||||
<button
|
||||
className="dropdown-item w-full"
|
||||
onClick={handleRevertChanges}
|
||||
disabled={!currentTabItem?.draft}
|
||||
>
|
||||
Revert Changes
|
||||
</button>
|
||||
<button className="dropdown-item w-full" onClick={(e) => handleCloseTab(e, currentTabUid)}>
|
||||
Close
|
||||
</button>
|
||||
<button disabled={!hasOtherTabs} className="dropdown-item w-full" onClick={handleCloseOtherTabs}>
|
||||
Close Others
|
||||
</button>
|
||||
<button disabled={!hasLeftTabs} className="dropdown-item w-full" onClick={handleCloseTabsToTheLeft}>
|
||||
Close to the Left
|
||||
</button>
|
||||
<button disabled={!hasRightTabs} className="dropdown-item w-full" onClick={handleCloseTabsToTheRight}>
|
||||
Close to the Right
|
||||
</button>
|
||||
<button className="dropdown-item w-full" onClick={handleCloseSavedTabs}>
|
||||
Close Saved
|
||||
</button>
|
||||
<button className="dropdown-item w-full" onClick={handleCloseAllTabs}>
|
||||
Close All
|
||||
</button>
|
||||
</Dropdown>
|
||||
{menuDropdown}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ const RequestTabs = () => {
|
||||
const dispatch = useDispatch();
|
||||
const tabsRef = useRef();
|
||||
const scrollContainerRef = useRef();
|
||||
const collectionTabsRef = useRef();
|
||||
const [newRequestModalOpen, setNewRequestModalOpen] = useState(false);
|
||||
const [tabOverflowStates, setTabOverflowStates] = useState({});
|
||||
const [showChevrons, setShowChevrons] = useState(false);
|
||||
@@ -115,7 +116,7 @@ const RequestTabs = () => {
|
||||
{collectionRequestTabs && collectionRequestTabs.length ? (
|
||||
<>
|
||||
<CollectionToolBar collection={activeCollection} />
|
||||
<div className="flex items-center pl-2">
|
||||
<div className="flex items-center pl-2" ref={collectionTabsRef}>
|
||||
<ul role="tablist">
|
||||
{showChevrons ? (
|
||||
<li className="select-none short-tab" onClick={leftSlide}>
|
||||
@@ -158,6 +159,7 @@ const RequestTabs = () => {
|
||||
folderUid={tab.folderUid}
|
||||
hasOverflow={tabOverflowStates[tab.uid]}
|
||||
setHasOverflow={createSetHasOverflow(tab.uid)}
|
||||
dropdownContainerRef={collectionTabsRef}
|
||||
/>
|
||||
</DraggableTab>
|
||||
);
|
||||
|
||||
@@ -2,16 +2,14 @@ import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
.caret {
|
||||
fill: currentColor;
|
||||
color: ${(props) => props.theme.app.collection.toolbar.environmentSelector.caret};
|
||||
fill: ${(props) => props.theme.app.collection.toolbar.environmentSelector.caret};
|
||||
}
|
||||
|
||||
.button-dropdown-button {
|
||||
color: ${(props) => props.theme.dropdown.primaryText};
|
||||
border-color: ${(props) => props.theme.workspace.border};
|
||||
|
||||
&:hover {
|
||||
background-color: ${(props) => props.theme.dropdown.hoverBg};
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-divider {
|
||||
@@ -24,6 +22,10 @@ const StyledWrapper = styled.div`
|
||||
color: ${(props) => props.theme.colors.text.yellow};
|
||||
}
|
||||
|
||||
.icon-muted {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
}
|
||||
|
||||
.preview-response-tab-label {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
}
|
||||
|
||||
@@ -1,11 +1,22 @@
|
||||
import React, { forwardRef } from 'react';
|
||||
import { IconEye, IconCaretDown } from '@tabler/icons';
|
||||
import React, { forwardRef, useState } from 'react';
|
||||
import { IconEye, IconCaretDown, IconBraces, IconCode, IconFileCode, IconBrandJavascript, IconFileText, IconHexagons, IconBinaryTree } from '@tabler/icons';
|
||||
import classnames from 'classnames';
|
||||
import MenuDropdown from 'ui/MenuDropdown';
|
||||
import ToggleSwitch from 'components/ToggleSwitch';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const ButtonIcon = forwardRef(({ disabled, className, style, prefix, selectedLabel, suffix, ...props }, ref) => {
|
||||
// Icon mapping for format options
|
||||
const FORMAT_ICONS = {
|
||||
json: IconBraces,
|
||||
html: IconCode,
|
||||
xml: IconFileCode,
|
||||
javascript: IconBrandJavascript,
|
||||
raw: IconFileText,
|
||||
hex: IconHexagons,
|
||||
base64: IconBinaryTree
|
||||
};
|
||||
|
||||
const ButtonIcon = forwardRef(({ disabled, className, style, prefix, selectedLabel, suffix, isActive, ...props }, ref) => {
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
@@ -20,10 +31,10 @@ const ButtonIcon = forwardRef(({ disabled, className, style, prefix, selectedLab
|
||||
role="button"
|
||||
{...props}
|
||||
>
|
||||
{prefix && <span>{prefix}</span>}
|
||||
<span className="active">{selectedLabel}</span>
|
||||
{prefix && <span className={isActive ? 'active' : 'icon-muted'}>{prefix}</span>}
|
||||
<span>{selectedLabel}</span>
|
||||
{suffix && <span>{suffix}</span>}
|
||||
<IconCaretDown className="caret ml-1" size={14} strokeWidth={2} />
|
||||
{isActive && <IconCaretDown className="caret ml-0.5" size={12} strokeWidth={2} />}
|
||||
</button>
|
||||
);
|
||||
});
|
||||
@@ -34,8 +45,21 @@ const QueryResultTypeSelector = ({
|
||||
formatValue,
|
||||
onFormatChange,
|
||||
onPreviewTabSelect,
|
||||
selectedTab
|
||||
selectedTab,
|
||||
isActiveTab,
|
||||
onTabSelect
|
||||
}) => {
|
||||
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||
|
||||
// Handle dropdown state change - only allow opening when active tab
|
||||
const handleDropdownChange = (open) => {
|
||||
if (!isActiveTab && open) {
|
||||
// First click when not active - select this tab, don't open dropdown
|
||||
onTabSelect?.();
|
||||
return;
|
||||
}
|
||||
setDropdownOpen(open);
|
||||
};
|
||||
// Find the selected item's label
|
||||
const findSelectedLabel = () => {
|
||||
if (formatValue != null) {
|
||||
@@ -47,10 +71,26 @@ const QueryResultTypeSelector = ({
|
||||
|
||||
const selectedLabel = findSelectedLabel();
|
||||
|
||||
// Enhance items with onChange handler
|
||||
// Get the icon for the currently selected format
|
||||
const SelectedFormatIcon = FORMAT_ICONS[formatValue];
|
||||
|
||||
// Determine the prefix icon - eye icon when in preview mode, format icon otherwise
|
||||
const getPrefixIcon = () => {
|
||||
if (selectedTab === 'preview') {
|
||||
return <IconEye size={14} strokeWidth={2} />;
|
||||
}
|
||||
if (SelectedFormatIcon) {
|
||||
return <SelectedFormatIcon size={14} strokeWidth={1.5} />;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// Enhance items with onChange handler and icons
|
||||
const enhancedItems = formatOptions.map((item) => {
|
||||
const IconComponent = FORMAT_ICONS[item.id];
|
||||
return {
|
||||
...item,
|
||||
leftSection: IconComponent ? <IconComponent size={14} strokeWidth={1.5} /> : null,
|
||||
onClick: () => {
|
||||
if (onFormatChange) {
|
||||
onFormatChange(item.id);
|
||||
@@ -67,7 +107,7 @@ const QueryResultTypeSelector = ({
|
||||
handleToggle={(e) => {
|
||||
e.preventDefault();
|
||||
// e.stopPropagation();
|
||||
onPreviewTabSelect();
|
||||
onPreviewTabSelect(selectedTab === 'preview' ? 'editor' : 'preview');
|
||||
}}
|
||||
size="2xs"
|
||||
data-testid="preview-response-tab"
|
||||
@@ -77,7 +117,7 @@ const QueryResultTypeSelector = ({
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<StyledWrapper className={isActiveTab ? 'tab-active' : ''}>
|
||||
<MenuDropdown
|
||||
items={enhancedItems}
|
||||
header={header}
|
||||
@@ -85,12 +125,15 @@ const QueryResultTypeSelector = ({
|
||||
showTickMark={true}
|
||||
placement="bottom-end"
|
||||
data-testid="format-response-tab"
|
||||
opened={dropdownOpen}
|
||||
onChange={handleDropdownChange}
|
||||
>
|
||||
<ButtonIcon
|
||||
selectedLabel={selectedLabel}
|
||||
suffix={selectedTab === 'preview' ? <IconEye size={14} strokeWidth={2} className="active mr-[2px]" /> : null}
|
||||
prefix={getPrefixIcon()}
|
||||
isActive={isActiveTab}
|
||||
disabled={false}
|
||||
className="h-[20px] text-[11px]"
|
||||
className="h-[22px] text-[10px]"
|
||||
data-testid="format-response-tab"
|
||||
/>
|
||||
</MenuDropdown>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect, forwardRef, useImperativeHandle, useRef, useMemo } from 'react';
|
||||
import React, { useState, useEffect, forwardRef, useImperativeHandle, useRef, useCallback } from 'react';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import toast from 'react-hot-toast';
|
||||
import { IconCopy, IconCheck } from '@tabler/icons';
|
||||
@@ -6,22 +6,23 @@ import classnames from 'classnames';
|
||||
import ActionIcon from 'ui/ActionIcon/index';
|
||||
import { formatResponse } from 'utils/common';
|
||||
|
||||
// Helper function to get text to copy
|
||||
const getTextToCopy = (selectedTab, selectedFormat, data, dataBuffer) => {
|
||||
// If preview is on, copy raw data (what's shown in TextPreview)
|
||||
if (selectedTab === 'preview') {
|
||||
return typeof data === 'string' ? data : JSON.stringify(data, null, 2);
|
||||
}
|
||||
// If editor is on, copy formatted data based on selected format
|
||||
if (selectedFormat && data && dataBuffer) {
|
||||
return formatResponse(data, dataBuffer, selectedFormat, null);
|
||||
}
|
||||
return typeof data === 'string' ? data : JSON.stringify(data, null, 2);
|
||||
};
|
||||
|
||||
// Hook to get copy response function
|
||||
export const useResponseCopy = (item, selectedFormat, selectedTab, data, dataBuffer) => {
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const textToCopy = useMemo(() => {
|
||||
// If preview is on, copy raw data (what's shown in TextPreview)
|
||||
if (selectedTab === 'preview') {
|
||||
return typeof data === 'string' ? data : JSON.stringify(data, null, 2);
|
||||
}
|
||||
// If editor is on, copy formatted data based on selected format
|
||||
if (selectedFormat && data && dataBuffer) {
|
||||
return formatResponse(data, dataBuffer, selectedFormat, null);
|
||||
}
|
||||
return typeof data === 'string' ? data : JSON.stringify(data, null, 2);
|
||||
}, [data, dataBuffer, selectedFormat, selectedTab]);
|
||||
|
||||
useEffect(() => {
|
||||
if (copied) {
|
||||
const timer = setTimeout(() => {
|
||||
@@ -31,15 +32,16 @@ export const useResponseCopy = (item, selectedFormat, selectedTab, data, dataBuf
|
||||
}
|
||||
}, [copied]);
|
||||
|
||||
const copyResponse = async () => {
|
||||
const copyResponse = useCallback(async () => {
|
||||
try {
|
||||
const textToCopy = getTextToCopy(selectedTab, selectedFormat, data, dataBuffer);
|
||||
await navigator.clipboard.writeText(textToCopy);
|
||||
toast.success('Response copied to clipboard');
|
||||
setCopied(true);
|
||||
} catch (error) {
|
||||
toast.error('Failed to copy response');
|
||||
}
|
||||
};
|
||||
}, [selectedTab, selectedFormat, data, dataBuffer]);
|
||||
|
||||
return { copyResponse, copied, hasData: !!data };
|
||||
};
|
||||
|
||||
@@ -80,6 +80,33 @@ const StyledWrapper = styled.div`
|
||||
border-left: 1px solid ${(props) => props.theme.preferences.sidebar.border};
|
||||
margin: 0 8px;
|
||||
}
|
||||
|
||||
.result-view-tabs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
padding: 3px;
|
||||
border-radius: 8px;
|
||||
|
||||
.button-dropdown-button {
|
||||
border: 1px solid transparent !important;
|
||||
background-color: transparent;
|
||||
border-radius: 5px;
|
||||
font-size: ${(props) => props.theme.font.size.sm};
|
||||
|
||||
&:hover {
|
||||
border-color: ${(props) => props.theme.app.collection.toolbar.environmentSelector.border} !important;
|
||||
}
|
||||
}
|
||||
|
||||
.tab-active .button-dropdown-button {
|
||||
border-color: ${(props) => props.theme.app.collection.toolbar.environmentSelector.border} !important;
|
||||
|
||||
&:hover {
|
||||
border-color: ${(props) => props.theme.app.collection.toolbar.environmentSelector.hoverBorder} !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
|
||||
@@ -26,7 +26,7 @@ import WSMessagesList from './WsResponsePane/WSMessagesList';
|
||||
import ResponsiveTabs from 'ui/ResponsiveTabs';
|
||||
|
||||
// Width threshold for expanded right-side action buttons
|
||||
const RIGHT_CONTENT_EXPANDED_WIDTH = 375;
|
||||
const RIGHT_CONTENT_EXPANDED_WIDTH = 135;
|
||||
|
||||
const ResponsePane = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -68,10 +68,9 @@ const ResponsePane = ({ item, collection }) => {
|
||||
dispatch(updateResponseFormat({ uid: item.uid, responseFormat: newFormat }));
|
||||
}, [dispatch, item.uid]);
|
||||
|
||||
const handleViewTabToggle = useCallback(() => {
|
||||
const newViewTab = selectedViewTab === 'editor' ? 'preview' : 'editor';
|
||||
const handleViewTabChange = useCallback((newViewTab) => {
|
||||
dispatch(updateResponseViewTab({ uid: item.uid, responseViewTab: newViewTab }));
|
||||
}, [dispatch, item.uid, selectedViewTab]);
|
||||
}, [dispatch, item.uid]);
|
||||
|
||||
const requestTimeline = ([...(collection.timeline || [])]).filter((obj) => {
|
||||
if (obj.itemUid === item.uid) return true;
|
||||
@@ -226,15 +225,24 @@ const ResponsePane = ({ item, collection }) => {
|
||||
onClick={() => setShowScriptErrorCard(true)}
|
||||
/>
|
||||
)}
|
||||
{focusedTab?.responsePaneTab === 'response' ? (
|
||||
{focusedTab?.responsePaneTab === 'response' && item?.response ? (
|
||||
<>
|
||||
<QueryResultTypeSelector
|
||||
formatOptions={previewFormatOptions}
|
||||
formatValue={selectedFormat}
|
||||
onFormatChange={handleFormatChange}
|
||||
onPreviewTabSelect={handleViewTabToggle}
|
||||
selectedTab={selectedViewTab}
|
||||
/>
|
||||
{/* Result View Tabs (Visualizations + Response Format) */}
|
||||
<div className="result-view-tabs">
|
||||
|
||||
{/* Response Format */}
|
||||
<QueryResultTypeSelector
|
||||
formatOptions={previewFormatOptions}
|
||||
formatValue={selectedFormat}
|
||||
onFormatChange={handleFormatChange}
|
||||
onPreviewTabSelect={handleViewTabChange}
|
||||
selectedTab={selectedViewTab}
|
||||
isActiveTab={selectedViewTab === 'editor' || selectedViewTab === 'preview'}
|
||||
onTabSelect={() => {
|
||||
handleViewTabChange('editor');
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
<div className="flex items-center response-pane-status">
|
||||
|
||||
@@ -3,6 +3,7 @@ import { saveCollectionSecurityConfig } from 'providers/ReduxStore/slices/collec
|
||||
import toast from 'react-hot-toast';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import Button from 'ui/Button';
|
||||
|
||||
const SecuritySettings = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -71,9 +72,9 @@ const SecuritySettings = ({ collection }) => {
|
||||
JavaScript code has access to the filesystem, can execute system commands and access sensitive information.
|
||||
</p>
|
||||
</div>
|
||||
<button onClick={handleSave} className="submit btn btn-sm btn-secondary w-fit mt-6">
|
||||
<Button size="sm" onClick={handleSave} className="w-fit mt-6">
|
||||
Save
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -26,6 +26,21 @@ const StyledWrapper = styled.div`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.share-button {
|
||||
display: flex;
|
||||
border-radius: ${(props) => props.theme.border.radius.base};
|
||||
padding: 10px;
|
||||
border: 1px solid ${(props) => props.theme.sidebar.collection.item.indentBorder};
|
||||
background-color: ${(props) => props.theme.sidebar.bg};
|
||||
color: ${(props) => props.theme.text};
|
||||
cursor: pointer;
|
||||
transition: all 0.1s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: ${(props) => props.theme.listItem.hoverBg};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import Modal from 'components/Modal';
|
||||
import { IconUpload, IconLoader2, IconAlertTriangle } from '@tabler/icons';
|
||||
import { IconUpload, IconLoader2, IconAlertTriangle, IconFileExport } from '@tabler/icons';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import Bruno from 'components/Bruno';
|
||||
import exportBrunoCollection from 'utils/collections/export';
|
||||
import exportPostmanCollection from 'utils/exporters/postman-collection';
|
||||
import exportOpenCollection from 'utils/exporters/opencollection';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { transformCollectionToSaveToExportAsFile } from 'utils/collections/index';
|
||||
import { useSelector } from 'react-redux';
|
||||
@@ -48,6 +49,12 @@ const ShareCollection = ({ onClose, collectionUid }) => {
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleExportOpenCollection = () => {
|
||||
const collectionCopy = cloneDeep(collection);
|
||||
exportOpenCollection(transformCollectionToSaveToExportAsFile(collectionCopy));
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
size="md"
|
||||
@@ -60,10 +67,10 @@ const ShareCollection = ({ onClose, collectionUid }) => {
|
||||
<StyledWrapper className="flex flex-col h-full w-[500px]">
|
||||
<div className="space-y-2">
|
||||
<div
|
||||
className={`flex border border-gray-200 dark:border-gray-600 items-center p-3 rounded-lg transition-colors ${
|
||||
className={`share-button ${
|
||||
isCollectionLoading
|
||||
? 'opacity-50 cursor-not-allowed'
|
||||
: 'hover:bg-gray-100 dark:hover:bg-gray-500/10 cursor-pointer'
|
||||
: 'cursor-pointer'
|
||||
}`}
|
||||
onClick={isCollectionLoading ? undefined : handleExportBrunoCollection}
|
||||
>
|
||||
@@ -77,10 +84,31 @@ const ShareCollection = ({ onClose, collectionUid }) => {
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`flex flex-col border border-gray-200 dark:border-gray-600 items-center rounded-lg transition-colors ${
|
||||
className={`share-button ${
|
||||
isCollectionLoading
|
||||
? 'opacity-50 cursor-not-allowed'
|
||||
: 'hover:bg-gray-100 dark:hover:bg-gray-500/10 cursor-pointer'
|
||||
: 'cursor-pointer'
|
||||
}`}
|
||||
onClick={isCollectionLoading ? undefined : handleExportOpenCollection}
|
||||
>
|
||||
<div className="mr-3 p-1 rounded-full">
|
||||
{isCollectionLoading ? (
|
||||
<IconLoader2 size={28} className="animate-spin" />
|
||||
) : (
|
||||
<IconFileExport size={28} strokeWidth={1} />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="font-medium">OpenCollection</div>
|
||||
<div className="text-xs">{isCollectionLoading ? 'Loading collection...' : 'Export in OpenCollection format'}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`flex !flex-col share-button ${
|
||||
isCollectionLoading
|
||||
? 'opacity-50 cursor-not-allowed'
|
||||
: 'cursor-pointer'
|
||||
}`}
|
||||
onClick={isCollectionLoading ? undefined : handleExportPostmanCollection}
|
||||
>
|
||||
|
||||
@@ -26,7 +26,7 @@ const DeleteResponseExampleModal = ({ onClose, example, item, collection }) => {
|
||||
confirmText="Delete"
|
||||
handleConfirm={onConfirm}
|
||||
handleCancel={onClose}
|
||||
confirmButtonClass="btn-danger"
|
||||
confirmButtonColor="danger"
|
||||
>
|
||||
Are you sure you want to delete the example <span className="font-medium">{example.name}</span>?
|
||||
</Modal>
|
||||
|
||||
@@ -37,7 +37,6 @@ import GenerateCodeItem from './GenerateCodeItem';
|
||||
import { isItemARequest, isItemAFolder } from 'utils/tabs';
|
||||
import { doesRequestMatchSearchText, doesFolderHaveItemsMatchSearchText } from 'utils/collections/search';
|
||||
import { getDefaultRequestPaneTab } from 'utils/collections';
|
||||
import { hideHomePage, hideApiSpecPage } from 'providers/ReduxStore/slices/app';
|
||||
import toast from 'react-hot-toast';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import NetworkError from 'components/ResponsePane/NetworkError/index';
|
||||
@@ -48,14 +47,16 @@ import ExampleIcon from 'components/Icons/ExampleIcon';
|
||||
import { scrollToTheActiveTab } from 'utils/tabs';
|
||||
import { isTabForItemActive as isTabForItemActiveSelector, isTabForItemPresent as isTabForItemPresentSelector } from 'src/selectors/tab';
|
||||
import { isEqual } from 'lodash';
|
||||
import { calculateDraggedItemNewPathname, getInitialExampleName } from 'utils/collections/index';
|
||||
import { calculateDraggedItemNewPathname, getInitialExampleName, findParentItemInCollection } from 'utils/collections/index';
|
||||
import { sortByNameThenSequence } from 'utils/common/index';
|
||||
import CreateExampleModal from 'components/ResponseExample/CreateExampleModal';
|
||||
import { openDevtoolsAndSwitchToTerminal } from 'utils/terminal';
|
||||
import ActionIcon from 'ui/ActionIcon';
|
||||
import MenuDropdown from 'ui/MenuDropdown';
|
||||
import { useSidebarAccordion } from 'components/Sidebar/SidebarAccordionContext';
|
||||
|
||||
const CollectionItem = ({ item, collectionUid, collectionPathname, searchText }) => {
|
||||
const { dropdownContainerRef } = useSidebarAccordion();
|
||||
const _isTabForItemActiveSelector = isTabForItemActiveSelector({ itemUid: item.uid });
|
||||
const isTabForItemActive = useSelector(_isTabForItemActiveSelector, isEqual);
|
||||
|
||||
@@ -214,8 +215,6 @@ const CollectionItem = ({ item, collectionUid, collectionPathname, searchText })
|
||||
setTimeout(scrollToTheActiveTab, 50);
|
||||
const isRequest = isItemARequest(item);
|
||||
if (isRequest) {
|
||||
dispatch(hideHomePage());
|
||||
dispatch(hideApiSpecPage());
|
||||
if (isTabForItemPresent) {
|
||||
dispatch(
|
||||
focusTab({
|
||||
@@ -233,8 +232,6 @@ const CollectionItem = ({ item, collectionUid, collectionPathname, searchText })
|
||||
})
|
||||
);
|
||||
} else {
|
||||
dispatch(hideHomePage());
|
||||
dispatch(hideApiSpecPage());
|
||||
dispatch(
|
||||
addTab({
|
||||
uid: item.uid,
|
||||
@@ -538,13 +535,14 @@ const CollectionItem = ({ item, collectionUid, collectionPathname, searchText })
|
||||
};
|
||||
|
||||
const handlePasteItem = () => {
|
||||
// Only allow paste into folders
|
||||
// Determine target folder: if item is a folder, paste into it; otherwise paste into parent folder
|
||||
let targetFolderUid = item.uid;
|
||||
if (!isFolder) {
|
||||
toast.error('Paste is only available for folders');
|
||||
return;
|
||||
const parentFolder = findParentItemInCollection(collection, item.uid);
|
||||
targetFolderUid = parentFolder ? parentFolder.uid : null;
|
||||
}
|
||||
|
||||
dispatch(pasteItem(collectionUid, item.uid))
|
||||
dispatch(pasteItem(collectionUid, targetFolderUid))
|
||||
.then(() => {
|
||||
toast.success('Item pasted successfully');
|
||||
})
|
||||
@@ -644,8 +642,9 @@ const CollectionItem = ({ item, collectionUid, collectionPathname, searchText })
|
||||
onClick={handleClick}
|
||||
onDoubleClick={handleDoubleClick}
|
||||
>
|
||||
<ActionIcon style={{ width: 16, minWidth: 16 }}>
|
||||
{isFolder ? (
|
||||
|
||||
{isFolder ? (
|
||||
<ActionIcon style={{ width: 16, minWidth: 16 }}>
|
||||
<IconChevronRight
|
||||
size={16}
|
||||
strokeWidth={2}
|
||||
@@ -655,7 +654,9 @@ const CollectionItem = ({ item, collectionUid, collectionPathname, searchText })
|
||||
onDoubleClick={handleFolderDoubleClick}
|
||||
data-testid="folder-chevron"
|
||||
/>
|
||||
) : hasExamples ? (
|
||||
</ActionIcon>
|
||||
) : hasExamples ? (
|
||||
<ActionIcon style={{ width: 16, minWidth: 16 }}>
|
||||
<IconChevronRight
|
||||
size={16}
|
||||
strokeWidth={2}
|
||||
@@ -665,8 +666,9 @@ const CollectionItem = ({ item, collectionUid, collectionPathname, searchText })
|
||||
onDoubleClick={handleExamplesDoubleClick}
|
||||
data-testid="request-item-chevron"
|
||||
/>
|
||||
) : null}
|
||||
</ActionIcon>
|
||||
</ActionIcon>
|
||||
) : null}
|
||||
|
||||
<div className="ml-1 flex w-full h-full items-center overflow-hidden">
|
||||
<CollectionItemIcon item={item} />
|
||||
<span className="item-name" title={item.name}>
|
||||
@@ -680,6 +682,8 @@ const CollectionItem = ({ item, collectionUid, collectionPathname, searchText })
|
||||
items={buildMenuItems()}
|
||||
placement="bottom-start"
|
||||
data-testid="collection-item-menu"
|
||||
popperOptions={{ strategy: 'fixed' }}
|
||||
appendTo={dropdownContainerRef?.current || document.body}
|
||||
>
|
||||
<ActionIcon className="menu-icon">
|
||||
<IconDots size={18} className="collection-item-menu-icon" />
|
||||
|
||||
@@ -9,6 +9,7 @@ import { removeCollection } from 'providers/ReduxStore/slices/collections/action
|
||||
import { IconAlertTriangle } from '@tabler/icons';
|
||||
import Modal from 'components/Modal';
|
||||
import toast from 'react-hot-toast';
|
||||
import Button from 'ui/Button';
|
||||
|
||||
const ConfirmCollectionCloseDrafts = ({ onClose, collection, collectionUid }) => {
|
||||
const MAX_UNSAVED_REQUESTS_TO_SHOW = 5;
|
||||
@@ -102,17 +103,17 @@ const ConfirmCollectionCloseDrafts = ({ onClose, collection, collectionUid }) =>
|
||||
|
||||
<div className="flex justify-between mt-6">
|
||||
<div>
|
||||
<button className="btn btn-sm btn-danger" onClick={handleDiscardAll}>
|
||||
<Button size="sm" color="danger" onClick={handleDiscardAll}>
|
||||
Discard and Remove
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<button className="btn btn-close btn-sm mr-2" onClick={onClose}>
|
||||
<Button size="sm" color="secondary" variant="ghost" onClick={onClose}>
|
||||
Cancel
|
||||
</button>
|
||||
<button className="btn btn-secondary btn-sm" onClick={handleSaveAll}>
|
||||
</Button>
|
||||
<Button size="sm" onClick={handleSaveAll}>
|
||||
{currentDrafts.length > 1 ? 'Save All and Remove' : 'Save and Remove'}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
.collection-info-card {
|
||||
background-color: ${(props) => props.theme.modal.title.bg};
|
||||
border-radius: 4px;
|
||||
padding: 12px;
|
||||
}
|
||||
.collection-name {
|
||||
font-weight: 500;
|
||||
padding-left: 0 !important;
|
||||
color: ${(props) => props.theme.text};
|
||||
margin-bottom: 4px;
|
||||
cursor: default !important;
|
||||
&:hover {
|
||||
background: none !important;
|
||||
}
|
||||
}
|
||||
.collection-path {
|
||||
font-size: ${(props) => props.theme.font.size.sm};
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
word-break: break-all;
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -2,11 +2,12 @@ import React, { useMemo } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import Modal from 'components/Modal';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { IconFiles } from '@tabler/icons';
|
||||
import { IconAlertCircle } from '@tabler/icons';
|
||||
import { removeCollection } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { findCollectionByUid, flattenItems, isItemARequest, hasRequestChanges } from 'utils/collections/index';
|
||||
import filter from 'lodash/filter';
|
||||
import ConfirmCollectionCloseDrafts from './ConfirmCollectionCloseDrafts';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const RemoveCollection = ({ onClose, collectionUid }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -42,21 +43,35 @@ const RemoveCollection = ({ onClose, collectionUid }) => {
|
||||
return <ConfirmCollectionCloseDrafts onClose={onClose} collection={collection} collectionUid={collectionUid} />;
|
||||
}
|
||||
|
||||
const customHeader = (
|
||||
<div className="flex items-center gap-2" data-testid="close-collection-modal-title">
|
||||
<IconAlertCircle size={18} strokeWidth={1.5} className="text-red-500" />
|
||||
<span>Remove Collection</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
// Otherwise, show the standard remove confirmation modal
|
||||
return (
|
||||
<Modal size="sm" title="Remove Collection" confirmText="Remove" handleConfirm={onConfirm} handleCancel={onClose}>
|
||||
<div className="flex items-center">
|
||||
<IconFiles size={18} strokeWidth={1.5} />
|
||||
<span className="ml-2 mr-4 font-medium">{collection.name}</span>
|
||||
</div>
|
||||
<div className="break-words text-xs mt-1">{collection.pathname}</div>
|
||||
<div className="mt-4">
|
||||
Are you sure you want to remove collection <span className="font-medium">{collection.name}</span> from this workspace?
|
||||
</div>
|
||||
<div className="mt-4 text-muted">
|
||||
The collection files will remain on disk and can be re-added to this or another workspace later.
|
||||
</div>
|
||||
</Modal>
|
||||
<StyledWrapper>
|
||||
<Modal
|
||||
size="sm"
|
||||
title="Remove Collection"
|
||||
customHeader={customHeader}
|
||||
confirmText="Remove"
|
||||
confirmButtonColor="danger"
|
||||
handleConfirm={onConfirm}
|
||||
handleCancel={onClose}
|
||||
>
|
||||
<p className="mb-4">Are you sure you want to close following collection in Bruno?</p>
|
||||
<div className="collection-info-card">
|
||||
<div className="collection-name">{collection.name}</div>
|
||||
<div className="collection-path">{collection.pathname}</div>
|
||||
</div>
|
||||
<p className="mt-4 text-muted text-sm">
|
||||
It will still be available in the filesystem at the above location and can be re-opened later.
|
||||
</p>
|
||||
</Modal>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -24,7 +24,6 @@ import {
|
||||
import { toggleCollection, collapseFullCollection } from 'providers/ReduxStore/slices/collections';
|
||||
import { mountCollection, moveCollectionAndPersist, handleCollectionItemDrop, pasteItem, showInFolder, saveCollectionSecurityConfig } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { hideApiSpecPage, hideHomePage } from 'providers/ReduxStore/slices/app';
|
||||
import { addTab, makeTabPermanent } from 'providers/ReduxStore/slices/tabs';
|
||||
import toast from 'react-hot-toast';
|
||||
import NewRequest from 'components/Sidebar/NewRequest';
|
||||
@@ -46,8 +45,10 @@ import { sortByNameThenSequence } from 'utils/common/index';
|
||||
import { openDevtoolsAndSwitchToTerminal } from 'utils/terminal';
|
||||
import ActionIcon from 'ui/ActionIcon';
|
||||
import MenuDropdown from 'ui/MenuDropdown';
|
||||
import { useSidebarAccordion } from 'components/Sidebar/SidebarAccordionContext';
|
||||
|
||||
const Collection = ({ collection, searchText }) => {
|
||||
const { dropdownContainerRef } = useSidebarAccordion();
|
||||
const [showNewFolderModal, setShowNewFolderModal] = useState(false);
|
||||
const [showNewRequestModal, setShowNewRequestModal] = useState(false);
|
||||
const [showRenameCollectionModal, setShowRenameCollectionModal] = useState(false);
|
||||
@@ -111,8 +112,6 @@ const Collection = ({ collection, searchText }) => {
|
||||
}
|
||||
|
||||
if (!isChevronClick) {
|
||||
dispatch(hideHomePage()); // @TODO Playwright tests are often stuck on home page, rather than collection settings tab. Revisit for a proper fix.
|
||||
dispatch(hideApiSpecPage());
|
||||
dispatch(
|
||||
addTab({
|
||||
uid: collection.uid,
|
||||
@@ -437,6 +436,8 @@ const Collection = ({ collection, searchText }) => {
|
||||
ref={menuDropdownRef}
|
||||
items={menuItems}
|
||||
placement="bottom-start"
|
||||
appendTo={dropdownContainerRef?.current || document.body}
|
||||
popperOptions={{ strategy: 'fixed' }}
|
||||
data-testid="collection-actions"
|
||||
>
|
||||
<ActionIcon className="collection-actions">
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
} from 'utils/collections/index';
|
||||
import { IconAlertTriangle } from '@tabler/icons';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import Button from 'ui/Button';
|
||||
|
||||
const MAX_COLLECTIONS_WIDTH = 530;
|
||||
const CHARACTER_WIDTH = 8;
|
||||
@@ -226,17 +227,17 @@ const RemoveCollectionsModal = ({ collectionUids, onClose }) => {
|
||||
|
||||
<div className="flex justify-between mt-6">
|
||||
<div>
|
||||
<button className="btn btn-sm btn-danger" onClick={handleDiscard}>
|
||||
<Button size="sm" color="danger" onClick={handleDiscard}>
|
||||
Discard and Close
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<button className="btn btn-close btn-sm mr-2" onClick={handleCancel}>
|
||||
<Button size="sm" color="secondary" variant="ghost" onClick={handleCancel}>
|
||||
Cancel
|
||||
</button>
|
||||
<button className="btn btn-secondary btn-sm" onClick={handleSave}>
|
||||
</Button>
|
||||
<Button size="sm" onClick={handleSave}>
|
||||
Save and Close
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
@@ -255,7 +256,7 @@ const RemoveCollectionsModal = ({ collectionUids, onClose }) => {
|
||||
Collections will be removed from the current workspace but will still be available in the file system and can be re-opened later.
|
||||
</div>
|
||||
<div className="flex justify-end mt-6">
|
||||
<button className="btn btn-close btn-sm mr-2" onClick={handleCancel}>
|
||||
<button className="btn btn-close btn-sm mr-2" data-testid="modal-close-button" onClick={handleCancel}>
|
||||
Cancel
|
||||
</button>
|
||||
<button className="btn btn-secondary btn-sm" onClick={handleCloseAllCollections}>
|
||||
|
||||
@@ -18,6 +18,7 @@ import { toggleSidebarCollapse } from 'providers/ReduxStore/slices/app';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import get from 'lodash/get';
|
||||
import Button from 'ui/Button';
|
||||
|
||||
const CreateCollection = ({ onClose, defaultLocation: propDefaultLocation }) => {
|
||||
const inputRef = useRef();
|
||||
@@ -319,17 +320,14 @@ const CreateCollection = ({ onClose, defaultLocation: propDefaultLocation }) =>
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
<span className="mr-2">
|
||||
<button type="button" onClick={onClose} className="btn btn-md btn-close">
|
||||
<Button type="button" size="sm" color="secondary" variant="ghost" onClick={onClose}>
|
||||
Cancel
|
||||
</button>
|
||||
</Button>
|
||||
</span>
|
||||
<span>
|
||||
<button
|
||||
type="submit"
|
||||
className="submit btn btn-md btn-secondary"
|
||||
>
|
||||
<Button type="submit" size="sm">
|
||||
Create
|
||||
</button>
|
||||
</Button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,6 +8,7 @@ import { isInsomniaCollection } from 'utils/importers/insomnia-collection';
|
||||
import { isOpenApiSpec } from 'utils/importers/openapi-collection';
|
||||
import { isWSDLCollection } from 'utils/importers/wsdl-collection';
|
||||
import { isBrunoCollection } from 'utils/importers/bruno-collection';
|
||||
import { isOpenCollection } from 'utils/importers/opencollection';
|
||||
import FullscreenLoader from './FullscreenLoader/index';
|
||||
|
||||
const convertFileToObject = async (file) => {
|
||||
@@ -72,6 +73,8 @@ const ImportCollection = ({ onClose, handleSubmit }) => {
|
||||
type = 'postman';
|
||||
} else if (isInsomniaCollection(data)) {
|
||||
type = 'insomnia';
|
||||
} else if (isOpenCollection(data)) {
|
||||
type = 'opencollection';
|
||||
} else if (isBrunoCollection(data)) {
|
||||
type = 'bruno';
|
||||
} else {
|
||||
@@ -159,7 +162,7 @@ const ImportCollection = ({ onClose, handleSubmit }) => {
|
||||
</button>
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
Supports Bruno, Postman, Insomnia, OpenAPI v3, and WSDL formats
|
||||
Supports Bruno, OpenCollection, Postman, Insomnia, OpenAPI v3, and WSDL formats
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,6 +9,7 @@ import { postmanToBruno } from 'utils/importers/postman-collection';
|
||||
import { convertInsomniaToBruno } from 'utils/importers/insomnia-collection';
|
||||
import { convertOpenapiToBruno } from 'utils/importers/openapi-collection';
|
||||
import { processBrunoCollection } from 'utils/importers/bruno-collection';
|
||||
import { processOpenCollection } from 'utils/importers/opencollection';
|
||||
import { wsdlToBruno } from '@usebruno/converters';
|
||||
import { toastError } from 'utils/common/error';
|
||||
import Modal from 'components/Modal';
|
||||
@@ -37,6 +38,8 @@ const getCollectionName = (format, rawData) => {
|
||||
return rawData.name || 'Insomnia Collection';
|
||||
case 'bruno':
|
||||
return rawData.name || 'Bruno Collection';
|
||||
case 'opencollection':
|
||||
return rawData.info?.name || 'OpenCollection';
|
||||
case 'wsdl':
|
||||
return 'WSDL Collection';
|
||||
default:
|
||||
@@ -65,6 +68,9 @@ const convertCollection = async (format, rawData, groupingType) => {
|
||||
case 'bruno':
|
||||
collection = await processBrunoCollection(rawData);
|
||||
break;
|
||||
case 'opencollection':
|
||||
collection = await processOpenCollection(rawData);
|
||||
break;
|
||||
default:
|
||||
throw new Error('Unknown collection format');
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { createContext, useContext, useState, useCallback } from 'react';
|
||||
import React, { createContext, useContext, useState, useCallback, useRef } from 'react';
|
||||
|
||||
const SidebarAccordionContext = createContext();
|
||||
|
||||
@@ -12,6 +12,7 @@ export const useSidebarAccordion = () => {
|
||||
|
||||
export const SidebarAccordionProvider = ({ children, defaultExpanded = ['collections'] }) => {
|
||||
const [expandedSections, setExpandedSections] = useState(new Set(defaultExpanded));
|
||||
const dropdownContainerRef = useRef(null);
|
||||
|
||||
const toggleSection = useCallback((sectionId) => {
|
||||
setExpandedSections((prev) => {
|
||||
@@ -52,10 +53,13 @@ export const SidebarAccordionProvider = ({ children, defaultExpanded = ['collect
|
||||
toggleSection,
|
||||
setSectionExpanded,
|
||||
isExpanded,
|
||||
getExpandedCount
|
||||
getExpandedCount,
|
||||
dropdownContainerRef
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
<div ref={dropdownContainerRef}>
|
||||
{children}
|
||||
</div>
|
||||
</SidebarAccordionContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -169,8 +169,10 @@ class SingleLineEditor extends Component {
|
||||
this.editor.setOption('theme', this.props.theme === 'dark' ? 'monokai' : 'default');
|
||||
}
|
||||
if (this.props.value !== prevProps.value && this.props.value !== this.cachedValue && this.editor) {
|
||||
const cursor = this.editor.getCursor();
|
||||
this.cachedValue = String(this.props.value);
|
||||
this.editor.setValue(String(this.props.value ?? ''));
|
||||
this.editor.setCursor(cursor);
|
||||
|
||||
// Update newline markers after value change
|
||||
if (this.props.showNewlineArrow) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import React from 'react';
|
||||
import { IconAlertTriangle } from '@tabler/icons';
|
||||
import Modal from 'components/Modal';
|
||||
import { createPortal } from 'react-dom';
|
||||
import Button from 'ui/Button';
|
||||
|
||||
const ConfirmSwitchEnv = ({ onCancel }) => {
|
||||
const modalContent = (
|
||||
@@ -26,9 +27,9 @@ const ConfirmSwitchEnv = ({ onCancel }) => {
|
||||
|
||||
<div className="flex justify-between mt-6">
|
||||
<div>
|
||||
<button className="btn btn-sm btn-danger" onClick={onCancel}>
|
||||
<Button size="sm" color="danger" onClick={onCancel}>
|
||||
Close
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
<div></div>
|
||||
</div>
|
||||
|
||||
@@ -74,26 +74,6 @@ const Wrapper = styled.div`
|
||||
}
|
||||
}
|
||||
|
||||
.btn-add-param {
|
||||
font-size: 12px;
|
||||
color: ${(props) => props.theme.textLink};
|
||||
font-weight: 500;
|
||||
padding: 7px 14px;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
border-radius: 6px;
|
||||
border: ${(props) => props.theme.sidebar.collection.item.indentBorder};
|
||||
background: transparent;
|
||||
transition: all 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.listItem.hoverBg};
|
||||
border-color: ${(props) => props.theme.textLink};
|
||||
}
|
||||
}
|
||||
|
||||
.tooltip-mod {
|
||||
font-size: 11px !important;
|
||||
max-width: 200px !important;
|
||||
@@ -123,65 +103,11 @@ const Wrapper = styled.div`
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 4px;
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
transition: color 0.15s ease, background 0.15s ease;
|
||||
}
|
||||
|
||||
.button-container {
|
||||
padding: 12px 2px;
|
||||
background: ${(props) => props.theme.bg};
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.submit {
|
||||
padding: 6px 16px;
|
||||
font-size: ${(props) => props.theme.font.size.sm};
|
||||
border-radius: ${(props) => props.theme.border.radius.base};
|
||||
border: none;
|
||||
background: ${(props) => props.theme.brand};
|
||||
color: ${(props) => props.theme.bg};
|
||||
cursor: pointer;
|
||||
transition: opacity 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
|
||||
.reset {
|
||||
background: transparent;
|
||||
padding: 6px 16px;
|
||||
color: ${(props) => props.theme.brand};
|
||||
&:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
|
||||
.discard {
|
||||
padding: 6px 16px;
|
||||
font-size: ${(props) => props.theme.font.size.sm};
|
||||
border-radius: ${(props) => props.theme.border.radius.base};
|
||||
background: transparent;
|
||||
color: ${(props) => props.theme.text};
|
||||
border: ${(props) => props.theme.sidebar.collection.item.indentBorder};
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.sidebar.collection.item.hoverBg};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
} from 'providers/ReduxStore/slices/global-environments';
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
import { getGlobalEnvironmentVariables } from 'utils/collections';
|
||||
import Button from 'ui/Button';
|
||||
|
||||
const EnvironmentVariables = ({ environment, setIsModified, originalEnvironmentVariables, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -425,14 +426,14 @@ const EnvironmentVariables = ({ environment, setIsModified, originalEnvironmentV
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div className="button-container">
|
||||
<div className="flex items-center">
|
||||
<button type="button" className="submit" onClick={handleSave} data-testid="save-env">
|
||||
<div className="button-container mt-5">
|
||||
<div className="flex items-center gap-2">
|
||||
<Button type="submit" size="sm" onClick={handleSave} data-testid="save-env">
|
||||
Save
|
||||
</button>
|
||||
<button type="button" className="submit reset ml-2" onClick={handleReset} data-testid="reset-env">
|
||||
</Button>
|
||||
<Button type="reset" size="sm" color="secondary" variant="ghost" onClick={handleReset} data-testid="reset-env">
|
||||
Reset
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
|
||||
@@ -3,7 +3,6 @@ import { useSelector, useDispatch } from 'react-redux';
|
||||
import { IconBox, IconTrash, IconEdit, IconShare, IconDots } from '@tabler/icons';
|
||||
import { removeCollectionFromWorkspaceAction } from 'providers/ReduxStore/slices/workspaces/actions';
|
||||
import { addTab } from 'providers/ReduxStore/slices/tabs';
|
||||
import { hideHomePage } from 'providers/ReduxStore/slices/app';
|
||||
import { mountCollection } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { normalizePath } from 'utils/common/path';
|
||||
import toast from 'react-hot-toast';
|
||||
@@ -117,8 +116,6 @@ const CollectionsList = ({ workspace }) => {
|
||||
})
|
||||
);
|
||||
|
||||
dispatch(hideHomePage());
|
||||
|
||||
dispatch(
|
||||
addTab({
|
||||
uid: collection.uid,
|
||||
|
||||
@@ -12,6 +12,7 @@ import { browseDirectory } from 'providers/ReduxStore/slices/collections/actions
|
||||
import { multiLineMsg } from 'utils/common/index';
|
||||
import { formatIpcError } from 'utils/common/error';
|
||||
import { sanitizeName, validateName, validateNameError } from 'utils/common/regex';
|
||||
import Button from 'ui/Button';
|
||||
|
||||
const CreateWorkspace = ({ onClose }) => {
|
||||
const inputRef = useRef();
|
||||
@@ -188,7 +189,7 @@ const CreateWorkspace = ({ onClose }) => {
|
||||
</p>
|
||||
</Help>
|
||||
</label>
|
||||
<div className="flex gap-2">
|
||||
<div className="flex gap-2 items-center">
|
||||
<input
|
||||
id="workspace-location"
|
||||
type="text"
|
||||
@@ -202,9 +203,9 @@ const CreateWorkspace = ({ onClose }) => {
|
||||
value={formik.values.workspaceLocation || ''}
|
||||
onClick={browse}
|
||||
/>
|
||||
<button type="button" className="btn btn-sm btn-secondary" onClick={browse}>
|
||||
<Button type="button" onClick={browse}>
|
||||
Browse
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
{formik.touched.workspaceLocation && formik.errors.workspaceLocation ? (
|
||||
<div className="text-red-500 text-sm mt-1">{formik.errors.workspaceLocation}</div>
|
||||
|
||||
@@ -10,6 +10,7 @@ import { importWorkspaceAction } from 'providers/ReduxStore/slices/workspaces/ac
|
||||
import { formatIpcError } from 'utils/common/error';
|
||||
import { multiLineMsg } from 'utils/common/index';
|
||||
import Help from 'components/Help';
|
||||
import Button from 'ui/Button';
|
||||
|
||||
const ImportWorkspace = ({ onClose }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -209,7 +210,7 @@ const ImportWorkspace = ({ onClose }) => {
|
||||
</p>
|
||||
</Help>
|
||||
</label>
|
||||
<div className="flex gap-2 mt-2">
|
||||
<div className="flex gap-2 mt-2 items-center">
|
||||
<input
|
||||
id="workspace-location"
|
||||
type="text"
|
||||
@@ -224,9 +225,9 @@ const ImportWorkspace = ({ onClose }) => {
|
||||
value={formik.values.workspaceLocation || ''}
|
||||
onClick={browse}
|
||||
/>
|
||||
<button type="button" className="btn btn-sm btn-secondary" onClick={browse}>
|
||||
<Button type="button" onClick={browse}>
|
||||
Browse
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
{formik.touched.workspaceLocation && formik.errors.workspaceLocation ? (
|
||||
<div className="text-red-500 text-sm mt-1">{formik.errors.workspaceLocation}</div>
|
||||
|
||||
@@ -10,6 +10,7 @@ import { completeQuitFlow } from 'providers/ReduxStore/slices/app';
|
||||
import { saveMultipleRequests, saveMultipleCollections, saveMultipleFolders } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { IconAlertTriangle } from '@tabler/icons';
|
||||
import Modal from 'components/Modal';
|
||||
import Button from 'ui/Button';
|
||||
|
||||
const SaveRequestsModal = ({ onClose }) => {
|
||||
const MAX_UNSAVED_ITEMS_TO_SHOW = 5;
|
||||
@@ -157,17 +158,17 @@ const SaveRequestsModal = ({ onClose }) => {
|
||||
|
||||
<div className="flex justify-between mt-6">
|
||||
<div>
|
||||
<button className="btn btn-sm btn-danger" onClick={closeWithoutSave}>
|
||||
<Button size="sm" color="danger" onClick={closeWithoutSave}>
|
||||
Don't Save
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<button className="btn btn-close btn-sm mr-2" onClick={onClose}>
|
||||
<Button size="sm" color="secondary" variant="ghost" onClick={onClose}>
|
||||
Cancel
|
||||
</button>
|
||||
<button className="btn btn-secondary btn-sm" onClick={closeWithSave}>
|
||||
</Button>
|
||||
<Button size="sm" onClick={closeWithSave}>
|
||||
{totalDraftsCount > 1 ? 'Save All' : 'Save'}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
@@ -9,19 +9,23 @@ const actionsToIntercept = [
|
||||
'collections/moveQueryParam',
|
||||
'collections/updateQueryParam',
|
||||
'collections/deleteQueryParam',
|
||||
'collections/setQueryParams',
|
||||
'collections/updatePathParam',
|
||||
'collections/addRequestHeader',
|
||||
'collections/updateRequestHeader',
|
||||
'collections/deleteRequestHeader',
|
||||
'collections/moveRequestHeader',
|
||||
'collections/setRequestHeaders',
|
||||
'collections/addFormUrlEncodedParam',
|
||||
'collections/updateFormUrlEncodedParam',
|
||||
'collections/deleteFormUrlEncodedParam',
|
||||
'collections/moveFormUrlEncodedParam',
|
||||
'collections/setFormUrlEncodedParams',
|
||||
'collections/addMultipartFormParam',
|
||||
'collections/updateMultipartFormParam',
|
||||
'collections/deleteMultipartFormParam',
|
||||
'collections/moveMultipartFormParam',
|
||||
'collections/setMultipartFormParams',
|
||||
'collections/updateRequestAuthMode',
|
||||
'collections/updateRequestBodyMode',
|
||||
'collections/updateRequestBody',
|
||||
@@ -47,9 +51,11 @@ const actionsToIntercept = [
|
||||
'collections/addFolderHeader',
|
||||
'collections/updateFolderHeader',
|
||||
'collections/deleteFolderHeader',
|
||||
'collections/setFolderHeaders',
|
||||
'collections/addFolderVar',
|
||||
'collections/updateFolderVar',
|
||||
'collections/deleteFolderVar',
|
||||
'collections/setFolderVars',
|
||||
'collections/updateFolderRequestScript',
|
||||
'collections/updateFolderResponseScript',
|
||||
'collections/updateFolderTests',
|
||||
@@ -61,9 +67,11 @@ const actionsToIntercept = [
|
||||
'collections/addCollectionHeader',
|
||||
'collections/updateCollectionHeader',
|
||||
'collections/deleteCollectionHeader',
|
||||
'collections/setCollectionHeaders',
|
||||
'collections/addCollectionVar',
|
||||
'collections/updateCollectionVar',
|
||||
'collections/deleteCollectionVar',
|
||||
'collections/setCollectionVars',
|
||||
'collections/updateCollectionAuth',
|
||||
'collections/updateCollectionAuthMode',
|
||||
'collections/updateCollectionRequestScript',
|
||||
|
||||
@@ -8,19 +8,23 @@ const actionsToIntercept = [
|
||||
'collections/moveQueryParam',
|
||||
'collections/updateQueryParam',
|
||||
'collections/deleteQueryParam',
|
||||
'collections/setQueryParams',
|
||||
'collections/updatePathParam',
|
||||
'collections/addRequestHeader',
|
||||
'collections/updateRequestHeader',
|
||||
'collections/deleteRequestHeader',
|
||||
'collections/moveRequestHeader',
|
||||
'collections/setRequestHeaders',
|
||||
'collections/addFormUrlEncodedParam',
|
||||
'collections/updateFormUrlEncodedParam',
|
||||
'collections/deleteFormUrlEncodedParam',
|
||||
'collections/moveFormUrlEncodedParam',
|
||||
'collections/setFormUrlEncodedParams',
|
||||
'collections/addMultipartFormParam',
|
||||
'collections/updateMultipartFormParam',
|
||||
'collections/deleteMultipartFormParam',
|
||||
'collections/moveMultipartFormParam',
|
||||
'collections/setMultipartFormParams',
|
||||
'collections/updateRequestAuthMode',
|
||||
'collections/updateRequestBodyMode',
|
||||
'collections/updateRequestBody',
|
||||
@@ -45,9 +49,11 @@ const actionsToIntercept = [
|
||||
'collections/addFolderHeader',
|
||||
'collections/updateFolderHeader',
|
||||
'collections/deleteFolderHeader',
|
||||
'collections/setFolderHeaders',
|
||||
'collections/addFolderVar',
|
||||
'collections/updateFolderVar',
|
||||
'collections/deleteFolderVar',
|
||||
'collections/setFolderVars',
|
||||
'collections/updateFolderRequestScript',
|
||||
'collections/updateFolderResponseScript',
|
||||
'collections/updateFolderTests',
|
||||
@@ -59,9 +65,11 @@ const actionsToIntercept = [
|
||||
'collections/addCollectionHeader',
|
||||
'collections/updateCollectionHeader',
|
||||
'collections/deleteCollectionHeader',
|
||||
'collections/setCollectionHeaders',
|
||||
'collections/addCollectionVar',
|
||||
'collections/updateCollectionVar',
|
||||
'collections/deleteCollectionVar',
|
||||
'collections/setCollectionVars',
|
||||
'collections/updateCollectionAuth',
|
||||
'collections/updateCollectionAuthMode',
|
||||
'collections/updateCollectionRequestScript',
|
||||
|
||||
@@ -2,7 +2,7 @@ import get from 'lodash/get';
|
||||
import each from 'lodash/each';
|
||||
import filter from 'lodash/filter';
|
||||
import { createListenerMiddleware } from '@reduxjs/toolkit';
|
||||
import { removeTaskFromQueue, hideHomePage } from 'providers/ReduxStore/slices/app';
|
||||
import { removeTaskFromQueue } from 'providers/ReduxStore/slices/app';
|
||||
import { addTab } from 'providers/ReduxStore/slices/tabs';
|
||||
import { collectionAddFileEvent, collectionChangeFileEvent } from 'providers/ReduxStore/slices/collections';
|
||||
import { findCollectionByUid, findItemInCollectionByPathname, getDefaultRequestPaneTab, findItemInCollectionByItemUid } from 'utils/collections/index';
|
||||
@@ -37,7 +37,6 @@ taskMiddleware.startListening({
|
||||
requestPaneTab: getDefaultRequestPaneTab(item)
|
||||
})
|
||||
);
|
||||
listenerApi.dispatch(hideHomePage());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,7 +79,6 @@ taskMiddleware.startListening({
|
||||
type: 'response-example',
|
||||
itemUid: item.uid
|
||||
}));
|
||||
listenerApi.dispatch(hideHomePage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import filter from 'lodash/filter';
|
||||
import brunoClipboard from 'utils/bruno-clipboard';
|
||||
import { addTab, focusTab } from './tabs';
|
||||
|
||||
const initialState = {
|
||||
isDragging: false,
|
||||
@@ -127,6 +128,20 @@ export const appSlice = createSlice({
|
||||
// Update clipboard UI state
|
||||
state.clipboard.hasCopiedItems = action.payload.hasCopiedItems;
|
||||
}
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
// Automatically hide special pages when any tab is added or focused
|
||||
builder
|
||||
.addCase(addTab, (state) => {
|
||||
state.showHomePage = false;
|
||||
state.showApiSpecPage = false;
|
||||
state.showManageWorkspacePage = false;
|
||||
})
|
||||
.addCase(focusTab, (state) => {
|
||||
state.showHomePage = false;
|
||||
state.showApiSpecPage = false;
|
||||
state.showManageWorkspacePage = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -2683,7 +2683,12 @@ export const collectionsSlice = createSlice({
|
||||
item.examples = file.data.examples;
|
||||
item.filename = file.meta.name;
|
||||
item.pathname = file.meta.pathname;
|
||||
item.draft = null;
|
||||
|
||||
// Only clear draft if it matches the file content
|
||||
// This preserves characters typed during autosave
|
||||
if (item.draft && areItemsTheSameExceptSeqUpdate(item.draft, file.data)) {
|
||||
item.draft = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,10 @@ const transformCollection = async (collection, type) => {
|
||||
const { convertOpenapiToBruno } = await import('utils/importers/openapi-collection');
|
||||
return convertOpenapiToBruno(collection);
|
||||
}
|
||||
case 'opencollection': {
|
||||
const { processOpenCollection } = await import('utils/importers/opencollection');
|
||||
return processOpenCollection(collection);
|
||||
}
|
||||
case 'wsdl': {
|
||||
const { wsdlToBruno } = await import('@usebruno/converters');
|
||||
return wsdlToBruno(collection);
|
||||
|
||||
@@ -2,11 +2,13 @@ const colors = {
|
||||
BRAND: '#d9a342',
|
||||
TEXT: '#d4d4d4',
|
||||
TEXT_MUTED: '#858585',
|
||||
TEXT_LINK: '#569cd6',
|
||||
TEXT_LINK: '#8BB7E0',
|
||||
BG: '#1e1e1e',
|
||||
|
||||
GREEN: '#4ec9b0',
|
||||
YELLOW: '#d9a342',
|
||||
WHITE: '#fff',
|
||||
BLACK: '#000',
|
||||
|
||||
GRAY_1: '#252526',
|
||||
GRAY_2: '#3D3D3D',
|
||||
@@ -313,6 +315,9 @@ const darkTheme = {
|
||||
},
|
||||
backdrop: {
|
||||
opacity: 0.2
|
||||
},
|
||||
closeButton: {
|
||||
hoverBg: 'rgba(255, 255, 255, 0.1)'
|
||||
}
|
||||
},
|
||||
|
||||
@@ -340,6 +345,30 @@ const darkTheme = {
|
||||
border: '#dc3545'
|
||||
}
|
||||
},
|
||||
button2: {
|
||||
color: {
|
||||
primary: {
|
||||
bg: colors.BRAND,
|
||||
text: colors.BLACK
|
||||
},
|
||||
secondary: {
|
||||
bg: colors.GRAY_4,
|
||||
text: '#fff'
|
||||
},
|
||||
success: {
|
||||
bg: '#059669',
|
||||
text: '#fff'
|
||||
},
|
||||
warning: {
|
||||
bg: '#f59e0b',
|
||||
text: '#1e1e1e'
|
||||
},
|
||||
danger: {
|
||||
bg: '#f43f5e',
|
||||
text: '#fff'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
tabs: {
|
||||
marginRight: '1.2rem',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const colors = {
|
||||
BRAND: '#cf8730',
|
||||
BRAND: '#c7822e',
|
||||
TEXT: 'rgb(52, 52, 52)',
|
||||
TEXT_MUTED: '#838383',
|
||||
TEXT_LINK: '#1663bb',
|
||||
@@ -9,7 +9,7 @@ const colors = {
|
||||
BLACK: '#000',
|
||||
SLATE_BLACK: '#343434',
|
||||
GREEN: '#047857',
|
||||
YELLOW: '#cf8730',
|
||||
YELLOW: '#c7822e',
|
||||
|
||||
GRAY_1: '#f8f8f8',
|
||||
GRAY_2: '#f3f3f3',
|
||||
@@ -318,6 +318,9 @@ const lightTheme = {
|
||||
},
|
||||
backdrop: {
|
||||
opacity: 0.4
|
||||
},
|
||||
closeButton: {
|
||||
hoverBg: 'rgba(0, 0, 0, 0.08)'
|
||||
}
|
||||
},
|
||||
|
||||
@@ -345,13 +348,36 @@ const lightTheme = {
|
||||
border: '#dc3545'
|
||||
}
|
||||
},
|
||||
|
||||
button2: {
|
||||
color: {
|
||||
primary: {
|
||||
bg: colors.BRAND,
|
||||
text: '#fff'
|
||||
},
|
||||
secondary: {
|
||||
bg: '#e5e7eb',
|
||||
text: colors.TEXT
|
||||
},
|
||||
success: {
|
||||
bg: '#4f9a7d',
|
||||
text: '#fff'
|
||||
},
|
||||
warning: {
|
||||
bg: '#c98b2b',
|
||||
text: '#fff'
|
||||
},
|
||||
danger: {
|
||||
bg: '#d14f5b',
|
||||
text: '#fff'
|
||||
}
|
||||
}
|
||||
},
|
||||
tabs: {
|
||||
marginRight: '1.2rem',
|
||||
active: {
|
||||
fontWeight: 400,
|
||||
color: colors.SLATE_BLACK,
|
||||
border: '#cf8730'
|
||||
border: '#c7822e'
|
||||
},
|
||||
secondary: {
|
||||
active: {
|
||||
|
||||
421
packages/bruno-app/src/ui/Button/Button.stories.jsx
Normal file
421
packages/bruno-app/src/ui/Button/Button.stories.jsx
Normal file
@@ -0,0 +1,421 @@
|
||||
import React from 'react';
|
||||
import Button from './index';
|
||||
|
||||
export default {
|
||||
title: 'Components/Button',
|
||||
component: Button,
|
||||
parameters: {
|
||||
layout: 'centered'
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
argTypes: {
|
||||
size: {
|
||||
control: 'select',
|
||||
options: ['xs', 'sm', 'base', 'md', 'lg'],
|
||||
description: 'The size of the button'
|
||||
},
|
||||
variant: {
|
||||
control: 'select',
|
||||
options: ['filled', 'outline', 'ghost'],
|
||||
description: 'The visual style variant of the button'
|
||||
},
|
||||
color: {
|
||||
control: 'select',
|
||||
options: ['primary', 'secondary', 'success', 'warning', 'danger'],
|
||||
description: 'The color of the button'
|
||||
},
|
||||
fontWeight: {
|
||||
control: 'select',
|
||||
options: ['regular', 'medium'],
|
||||
description: 'Font weight (default: regular for filled/ghost, medium for outline)'
|
||||
},
|
||||
rounded: {
|
||||
control: 'select',
|
||||
options: ['sm', 'base', 'md', 'lg', 'full'],
|
||||
description: 'Border radius style'
|
||||
},
|
||||
disabled: {
|
||||
control: 'boolean',
|
||||
description: 'Whether the button is disabled'
|
||||
},
|
||||
loading: {
|
||||
control: 'boolean',
|
||||
description: 'Whether the button is in loading state'
|
||||
},
|
||||
fullWidth: {
|
||||
control: 'boolean',
|
||||
description: 'Whether the button takes full width'
|
||||
},
|
||||
iconPosition: {
|
||||
control: 'select',
|
||||
options: ['left', 'right'],
|
||||
description: 'Position of the icon relative to text'
|
||||
},
|
||||
children: {
|
||||
control: 'text',
|
||||
description: 'Button text content'
|
||||
},
|
||||
onClick: { action: 'clicked' },
|
||||
onDoubleClick: { action: 'double-clicked' },
|
||||
onMouseEnter: { action: 'mouse-entered' },
|
||||
onMouseLeave: { action: 'mouse-left' }
|
||||
}
|
||||
};
|
||||
|
||||
// Sample icon component for stories
|
||||
const PlusIcon = () => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<line x1="12" y1="5" x2="12" y2="19"></line>
|
||||
<line x1="5" y1="12" x2="19" y2="12"></line>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const SendIcon = () => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<line x1="22" y1="2" x2="11" y2="13"></line>
|
||||
<polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const TrashIcon = () => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<polyline points="3 6 5 6 21 6"></polyline>
|
||||
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
|
||||
</svg>
|
||||
);
|
||||
|
||||
// Default story
|
||||
export const Default = {
|
||||
args: {
|
||||
children: 'Button'
|
||||
}
|
||||
};
|
||||
|
||||
// Variants
|
||||
export const Filled = {
|
||||
render: () => (
|
||||
<div style={{ display: 'flex', gap: '12px', flexWrap: 'wrap' }}>
|
||||
<Button variant="filled" color="primary">Primary</Button>
|
||||
<Button variant="filled" color="secondary">Secondary</Button>
|
||||
<Button variant="filled" color="success">Success</Button>
|
||||
<Button variant="filled" color="warning">Warning</Button>
|
||||
<Button variant="filled" color="danger">Danger</Button>
|
||||
</div>
|
||||
)
|
||||
};
|
||||
|
||||
export const Outline = {
|
||||
render: () => (
|
||||
<div style={{ display: 'flex', gap: '12px', flexWrap: 'wrap' }}>
|
||||
<Button variant="outline" color="primary">Primary</Button>
|
||||
<Button variant="outline" color="secondary">Secondary</Button>
|
||||
<Button variant="outline" color="success">Success</Button>
|
||||
<Button variant="outline" color="warning">Warning</Button>
|
||||
<Button variant="outline" color="danger">Danger</Button>
|
||||
</div>
|
||||
)
|
||||
};
|
||||
|
||||
export const Ghost = {
|
||||
render: () => (
|
||||
<div style={{ display: 'flex', gap: '12px', flexWrap: 'wrap' }}>
|
||||
<Button variant="ghost" color="primary">Primary</Button>
|
||||
<Button variant="ghost" color="secondary">Secondary</Button>
|
||||
<Button variant="ghost" color="success">Success</Button>
|
||||
<Button variant="ghost" color="warning">Warning</Button>
|
||||
<Button variant="ghost" color="danger">Danger</Button>
|
||||
</div>
|
||||
)
|
||||
};
|
||||
|
||||
// With Icons
|
||||
export const WithIconLeft = {
|
||||
render: () => (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '24px' }}>
|
||||
<div>
|
||||
<h3 style={{ marginBottom: '12px', fontSize: '14px', fontWeight: 600 }}>Filled</h3>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
||||
<Button variant="filled" size="xs" icon={<PlusIcon />} iconPosition="left">Add</Button>
|
||||
<Button variant="filled" size="sm" icon={<PlusIcon />} iconPosition="left">Add Item</Button>
|
||||
<Button variant="filled" size="base" icon={<PlusIcon />} iconPosition="left">Add Item</Button>
|
||||
<Button variant="filled" size="md" icon={<PlusIcon />} iconPosition="left">Add Item</Button>
|
||||
<Button variant="filled" size="lg" icon={<PlusIcon />} iconPosition="left">Add Item</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 style={{ marginBottom: '12px', fontSize: '14px', fontWeight: 600 }}>Outline</h3>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
||||
<Button variant="outline" size="xs" icon={<PlusIcon />} iconPosition="left">Add</Button>
|
||||
<Button variant="outline" size="sm" icon={<PlusIcon />} iconPosition="left">Add Item</Button>
|
||||
<Button variant="outline" size="base" icon={<PlusIcon />} iconPosition="left">Add Item</Button>
|
||||
<Button variant="outline" size="md" icon={<PlusIcon />} iconPosition="left">Add Item</Button>
|
||||
<Button variant="outline" size="lg" icon={<PlusIcon />} iconPosition="left">Add Item</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 style={{ marginBottom: '12px', fontSize: '14px', fontWeight: 600 }}>Ghost</h3>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
||||
<Button variant="ghost" size="xs" icon={<PlusIcon />} iconPosition="left">Add</Button>
|
||||
<Button variant="ghost" size="sm" icon={<PlusIcon />} iconPosition="left">Add Item</Button>
|
||||
<Button variant="ghost" size="base" icon={<PlusIcon />} iconPosition="left">Add Item</Button>
|
||||
<Button variant="ghost" size="md" icon={<PlusIcon />} iconPosition="left">Add Item</Button>
|
||||
<Button variant="ghost" size="lg" icon={<PlusIcon />} iconPosition="left">Add Item</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
};
|
||||
|
||||
export const WithIconRight = {
|
||||
render: () => (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '24px' }}>
|
||||
<div>
|
||||
<h3 style={{ marginBottom: '12px', fontSize: '14px', fontWeight: 600 }}>Filled</h3>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
||||
<Button variant="filled" size="xs" icon={<SendIcon />} iconPosition="right">Send</Button>
|
||||
<Button variant="filled" size="sm" icon={<SendIcon />} iconPosition="right">Send</Button>
|
||||
<Button variant="filled" size="base" icon={<SendIcon />} iconPosition="right">Send</Button>
|
||||
<Button variant="filled" size="md" icon={<SendIcon />} iconPosition="right">Send</Button>
|
||||
<Button variant="filled" size="lg" icon={<SendIcon />} iconPosition="right">Send</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 style={{ marginBottom: '12px', fontSize: '14px', fontWeight: 600 }}>Outline</h3>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
||||
<Button variant="outline" size="xs" icon={<SendIcon />} iconPosition="right">Send</Button>
|
||||
<Button variant="outline" size="sm" icon={<SendIcon />} iconPosition="right">Send</Button>
|
||||
<Button variant="outline" size="base" icon={<SendIcon />} iconPosition="right">Send</Button>
|
||||
<Button variant="outline" size="md" icon={<SendIcon />} iconPosition="right">Send</Button>
|
||||
<Button variant="outline" size="lg" icon={<SendIcon />} iconPosition="right">Send</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 style={{ marginBottom: '12px', fontSize: '14px', fontWeight: 600 }}>Ghost</h3>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
||||
<Button variant="ghost" size="xs" icon={<SendIcon />} iconPosition="right">Send</Button>
|
||||
<Button variant="ghost" size="sm" icon={<SendIcon />} iconPosition="right">Send</Button>
|
||||
<Button variant="ghost" size="base" icon={<SendIcon />} iconPosition="right">Send</Button>
|
||||
<Button variant="ghost" size="md" icon={<SendIcon />} iconPosition="right">Send</Button>
|
||||
<Button variant="ghost" size="lg" icon={<SendIcon />} iconPosition="right">Send</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
};
|
||||
|
||||
export const IconOnly = {
|
||||
args: {
|
||||
icon: <PlusIcon />,
|
||||
rounded: 'full',
|
||||
size: 'base'
|
||||
}
|
||||
};
|
||||
|
||||
// States
|
||||
export const Disabled = {
|
||||
render: () => (
|
||||
<div style={{ display: 'flex', gap: '48px' }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
||||
<h3 style={{ marginBottom: '4px', fontSize: '14px', fontWeight: 600 }}>Enabled</h3>
|
||||
<Button variant="filled" color="primary">Primary</Button>
|
||||
<Button variant="filled" color="secondary">Secondary</Button>
|
||||
<Button variant="filled" color="success">Success</Button>
|
||||
<Button variant="filled" color="warning">Warning</Button>
|
||||
<Button variant="filled" color="danger">Danger</Button>
|
||||
<Button variant="outline" color="primary">Outline</Button>
|
||||
<Button variant="ghost" color="primary">Ghost</Button>
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
||||
<h3 style={{ marginBottom: '4px', fontSize: '14px', fontWeight: 600 }}>Disabled</h3>
|
||||
<Button variant="filled" color="primary" disabled>Primary</Button>
|
||||
<Button variant="filled" color="secondary" disabled>Secondary</Button>
|
||||
<Button variant="filled" color="success" disabled>Success</Button>
|
||||
<Button variant="filled" color="warning" disabled>Warning</Button>
|
||||
<Button variant="filled" color="danger" disabled>Danger</Button>
|
||||
<Button variant="outline" color="primary" disabled>Outline</Button>
|
||||
<Button variant="ghost" color="primary" disabled>Ghost</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
};
|
||||
|
||||
export const Loading = {
|
||||
render: () => (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '24px' }}>
|
||||
<div>
|
||||
<h3 style={{ marginBottom: '12px', fontSize: '14px', fontWeight: 600 }}>Filled</h3>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
||||
<Button variant="filled" size="xs" loading>Loading</Button>
|
||||
<Button variant="filled" size="sm" loading>Loading</Button>
|
||||
<Button variant="filled" size="base" loading>Loading</Button>
|
||||
<Button variant="filled" size="md" loading>Loading</Button>
|
||||
<Button variant="filled" size="lg" loading>Loading</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 style={{ marginBottom: '12px', fontSize: '14px', fontWeight: 600 }}>Outline</h3>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
||||
<Button variant="outline" size="xs" loading>Loading</Button>
|
||||
<Button variant="outline" size="sm" loading>Loading</Button>
|
||||
<Button variant="outline" size="base" loading>Loading</Button>
|
||||
<Button variant="outline" size="md" loading>Loading</Button>
|
||||
<Button variant="outline" size="lg" loading>Loading</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 style={{ marginBottom: '12px', fontSize: '14px', fontWeight: 600 }}>Ghost</h3>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
||||
<Button variant="ghost" size="xs" loading>Loading</Button>
|
||||
<Button variant="ghost" size="sm" loading>Loading</Button>
|
||||
<Button variant="ghost" size="base" loading>Loading</Button>
|
||||
<Button variant="ghost" size="md" loading>Loading</Button>
|
||||
<Button variant="ghost" size="lg" loading>Loading</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
};
|
||||
|
||||
// Rounded variants
|
||||
export const Rounded = {
|
||||
render: () => (
|
||||
<div style={{ display: 'flex', gap: '12px', alignItems: 'center' }}>
|
||||
<Button rounded="sm">Small</Button>
|
||||
<Button rounded="base">Base</Button>
|
||||
<Button rounded="md">Medium</Button>
|
||||
<Button rounded="lg">Large</Button>
|
||||
<Button rounded="full">Full</Button>
|
||||
</div>
|
||||
)
|
||||
};
|
||||
|
||||
// Full Width
|
||||
export const FullWidth = {
|
||||
args: {
|
||||
children: 'Full Width Button',
|
||||
fullWidth: true
|
||||
},
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div style={{ width: '300px' }}>
|
||||
<Story />
|
||||
</div>
|
||||
)
|
||||
]
|
||||
};
|
||||
|
||||
// Combined Examples
|
||||
export const DangerWithIcon = {
|
||||
args: {
|
||||
children: 'Delete',
|
||||
variant: 'filled',
|
||||
color: 'danger',
|
||||
icon: <TrashIcon />
|
||||
}
|
||||
};
|
||||
|
||||
// All Colors Showcase
|
||||
export const AllColors = {
|
||||
render: () => (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '24px' }}>
|
||||
<div>
|
||||
<h3 style={{ marginBottom: '12px', fontSize: '14px', fontWeight: 600 }}>Filled Variant</h3>
|
||||
<div style={{ display: 'flex', gap: '12px', flexWrap: 'wrap' }}>
|
||||
<Button variant="filled" color="primary">Primary</Button>
|
||||
<Button variant="filled" color="secondary">Secondary</Button>
|
||||
<Button variant="filled" color="success">Success</Button>
|
||||
<Button variant="filled" color="warning">Warning</Button>
|
||||
<Button variant="filled" color="danger">Danger</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 style={{ marginBottom: '12px', fontSize: '14px', fontWeight: 600 }}>Outline Variant</h3>
|
||||
<div style={{ display: 'flex', gap: '12px', flexWrap: 'wrap' }}>
|
||||
<Button variant="outline" color="primary">Primary</Button>
|
||||
<Button variant="outline" color="secondary">Secondary</Button>
|
||||
<Button variant="outline" color="success">Success</Button>
|
||||
<Button variant="outline" color="warning">Warning</Button>
|
||||
<Button variant="outline" color="danger">Danger</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 style={{ marginBottom: '12px', fontSize: '14px', fontWeight: 600 }}>Ghost Variant</h3>
|
||||
<div style={{ display: 'flex', gap: '12px', flexWrap: 'wrap' }}>
|
||||
<Button variant="ghost" color="primary">Primary</Button>
|
||||
<Button variant="ghost" color="secondary">Secondary</Button>
|
||||
<Button variant="ghost" color="success">Success</Button>
|
||||
<Button variant="ghost" color="warning">Warning</Button>
|
||||
<Button variant="ghost" color="danger">Danger</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
};
|
||||
|
||||
// All Sizes Showcase
|
||||
export const AllSizes = {
|
||||
render: () => (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '24px' }}>
|
||||
<div>
|
||||
<h3 style={{ marginBottom: '12px', fontSize: '14px', fontWeight: 600 }}>Filled</h3>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
||||
<Button variant="filled" size="xs">Extra Small</Button>
|
||||
<Button variant="filled" size="sm">Small</Button>
|
||||
<Button variant="filled" size="base">Base</Button>
|
||||
<Button variant="filled" size="md">Medium</Button>
|
||||
<Button variant="filled" size="lg">Large</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 style={{ marginBottom: '12px', fontSize: '14px', fontWeight: 600 }}>Outline</h3>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
||||
<Button variant="outline" size="xs">Extra Small</Button>
|
||||
<Button variant="outline" size="sm">Small</Button>
|
||||
<Button variant="outline" size="base">Base</Button>
|
||||
<Button variant="outline" size="md">Medium</Button>
|
||||
<Button variant="outline" size="lg">Large</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 style={{ marginBottom: '12px', fontSize: '14px', fontWeight: 600 }}>Ghost</h3>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
||||
<Button variant="ghost" size="xs">Extra Small</Button>
|
||||
<Button variant="ghost" size="sm">Small</Button>
|
||||
<Button variant="ghost" size="base">Base</Button>
|
||||
<Button variant="ghost" size="md">Medium</Button>
|
||||
<Button variant="ghost" size="lg">Large</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
};
|
||||
274
packages/bruno-app/src/ui/Button/StyledWrapper.js
Normal file
274
packages/bruno-app/src/ui/Button/StyledWrapper.js
Normal file
@@ -0,0 +1,274 @@
|
||||
import styled, { css, keyframes } from 'styled-components';
|
||||
import { darken, rgba } from 'polished';
|
||||
|
||||
const spin = keyframes`
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
`;
|
||||
|
||||
const sizeStyles = {
|
||||
xs: css`
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: ${(props) => props.theme.font.size.xs};
|
||||
gap: 0.25rem;
|
||||
|
||||
.button-icon {
|
||||
width: 0.75rem;
|
||||
height: 0.75rem;
|
||||
}
|
||||
|
||||
.spinner-icon {
|
||||
width: 0.75rem;
|
||||
height: 0.75rem;
|
||||
}
|
||||
`,
|
||||
sm: css`
|
||||
padding: 0.375rem 0.75rem;
|
||||
font-size: ${(props) => props.theme.font.size.sm};
|
||||
gap: 0.375rem;
|
||||
|
||||
.button-icon {
|
||||
width: 0.875rem;
|
||||
height: 0.875rem;
|
||||
}
|
||||
|
||||
.spinner-icon {
|
||||
width: 0.875rem;
|
||||
height: 0.875rem;
|
||||
}
|
||||
`,
|
||||
base: css`
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: ${(props) => props.theme.font.size.base};
|
||||
gap: 0.5rem;
|
||||
|
||||
.button-icon {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
.spinner-icon {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
`,
|
||||
md: css`
|
||||
padding: 0.625rem 1.125rem;
|
||||
font-size: ${(props) => props.theme.font.size.md};
|
||||
gap: 0.5rem;
|
||||
|
||||
.button-icon {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
.spinner-icon {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
`,
|
||||
lg: css`
|
||||
padding: 0.75rem 1.5rem;
|
||||
font-size: ${(props) => props.theme.font.size.md};
|
||||
gap: 0.75rem;
|
||||
|
||||
.button-icon {
|
||||
width: 1.125rem;
|
||||
height: 1.125rem;
|
||||
}
|
||||
|
||||
.spinner-icon {
|
||||
width: 1.125rem;
|
||||
height: 1.125rem;
|
||||
}
|
||||
`
|
||||
};
|
||||
|
||||
const roundedStyles = {
|
||||
sm: css`
|
||||
border-radius: ${(props) => props.theme.border.radius.sm};
|
||||
`,
|
||||
base: css`
|
||||
border-radius: ${(props) => props.theme.border.radius.base};
|
||||
`,
|
||||
md: css`
|
||||
border-radius: ${(props) => props.theme.border.radius.md};
|
||||
`,
|
||||
lg: css`
|
||||
border-radius: ${(props) => props.theme.border.radius.lg};
|
||||
`,
|
||||
full: css`
|
||||
border-radius: 9999px;
|
||||
`
|
||||
};
|
||||
|
||||
const fontWeightStyles = {
|
||||
regular: 400,
|
||||
medium: 500
|
||||
};
|
||||
|
||||
// For secondary, use text color for outline/ghost; for others, use bg
|
||||
const getDisplayColor = (colorConfig, colorName) => {
|
||||
return colorName === 'secondary' ? colorConfig.text : colorConfig.bg;
|
||||
};
|
||||
|
||||
const getVariantStyles = (variant, color) => {
|
||||
if (variant === 'filled') {
|
||||
return css`
|
||||
background-color: ${(props) => props.theme.button2.color[color].bg};
|
||||
color: ${(props) => props.theme.button2.color[color].text};
|
||||
border: 1px solid ${(props) => props.theme.button2.color[color].bg};
|
||||
|
||||
&:disabled {
|
||||
color: ${(props) => props.theme.button2.color[color].text} !important;
|
||||
}
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
${(props) => {
|
||||
const bg = props.theme.button2.color[color].bg;
|
||||
return css`
|
||||
background-color: ${darken(0.03, bg)};
|
||||
border-color: ${darken(0.03, bg)};
|
||||
`;
|
||||
}}
|
||||
}
|
||||
|
||||
&:active:not(:disabled) {
|
||||
${(props) => {
|
||||
const bg = props.theme.button2.color[color].bg;
|
||||
return css`
|
||||
background-color: ${darken(0.07, bg)};
|
||||
`;
|
||||
}}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
if (variant === 'outline') {
|
||||
return css`
|
||||
background-color: transparent;
|
||||
color: ${(props) => getDisplayColor(props.theme.button2.color[color], color)};
|
||||
border: 1px solid ${(props) => getDisplayColor(props.theme.button2.color[color], color)};
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
${(props) => {
|
||||
const displayColor = getDisplayColor(props.theme.button2.color[color], color);
|
||||
return css`
|
||||
background-color: ${rgba(displayColor, 0.05)};
|
||||
`;
|
||||
}}
|
||||
}
|
||||
|
||||
&:active:not(:disabled) {
|
||||
${(props) => {
|
||||
const displayColor = getDisplayColor(props.theme.button2.color[color], color);
|
||||
return css`
|
||||
background-color: ${rgba(displayColor, 0.1)};
|
||||
`;
|
||||
}}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
if (variant === 'ghost') {
|
||||
return css`
|
||||
background-color: transparent;
|
||||
color: ${(props) => getDisplayColor(props.theme.button2.color[color], color)};
|
||||
border: 1px solid transparent;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
${(props) => {
|
||||
const displayColor = getDisplayColor(props.theme.button2.color[color], color);
|
||||
return css`
|
||||
background-color: ${rgba(displayColor, 0.1)};
|
||||
`;
|
||||
}}
|
||||
}
|
||||
|
||||
&:active:not(:disabled) {
|
||||
${(props) => {
|
||||
const displayColor = getDisplayColor(props.theme.button2.color[color], color);
|
||||
return css`
|
||||
background-color: ${rgba(displayColor, 0.15)};
|
||||
`;
|
||||
}}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
return css``;
|
||||
};
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
display: ${(props) => (props.$fullWidth ? 'block' : 'inline-block')};
|
||||
width: ${(props) => (props.$fullWidth ? '100%' : 'auto')};
|
||||
|
||||
button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: ${(props) => (props.$fullWidth ? '100%' : 'auto')};
|
||||
font-family: inherit;
|
||||
font-weight: ${(props) => fontWeightStyles[props.$fontWeight] || 400};
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
outline: none;
|
||||
white-space: nowrap;
|
||||
user-select: none;
|
||||
|
||||
${(props) => sizeStyles[props.$size]}
|
||||
${(props) => roundedStyles[props.$rounded]}
|
||||
${(props) => getVariantStyles(props.$variant, props.$color)}
|
||||
|
||||
&:focus-visible {
|
||||
${(props) => {
|
||||
const colorConfig = props.theme.button2.color[props.$color];
|
||||
const focusColor = props.$color === 'secondary' ? colorConfig.text : colorConfig.bg;
|
||||
return css`
|
||||
box-shadow: 0 0 0 2px ${rgba(focusColor, 0.4)};
|
||||
`;
|
||||
}}
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
pointer-events: none;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.button-content {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.button-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
|
||||
svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.button-spinner {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.spinner-icon {
|
||||
animation: ${spin} 0.75s linear infinite;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
81
packages/bruno-app/src/ui/Button/index.js
Normal file
81
packages/bruno-app/src/ui/Button/index.js
Normal file
@@ -0,0 +1,81 @@
|
||||
import React from 'react';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const Button = ({
|
||||
children,
|
||||
size = 'base',
|
||||
variant = 'filled',
|
||||
color = 'primary',
|
||||
disabled = false,
|
||||
loading = false,
|
||||
icon,
|
||||
iconPosition = 'left',
|
||||
fullWidth = false,
|
||||
type = 'button',
|
||||
rounded = 'base',
|
||||
fontWeight,
|
||||
onClick,
|
||||
onDoubleClick,
|
||||
className = '',
|
||||
...rest
|
||||
}) => {
|
||||
const handleClick = (e) => {
|
||||
if (disabled || loading) return;
|
||||
onClick?.(e);
|
||||
};
|
||||
|
||||
const handleDoubleClick = (e) => {
|
||||
if (disabled || loading) return;
|
||||
onDoubleClick?.(e);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper
|
||||
$size={size}
|
||||
$variant={variant}
|
||||
$color={color}
|
||||
$disabled={disabled}
|
||||
$loading={loading}
|
||||
$fullWidth={fullWidth}
|
||||
$rounded={rounded}
|
||||
$fontWeight={fontWeight}
|
||||
$hasIcon={!!icon}
|
||||
$iconPosition={iconPosition}
|
||||
className={className}
|
||||
>
|
||||
<button
|
||||
type={type}
|
||||
disabled={disabled || loading}
|
||||
onClick={handleClick}
|
||||
onDoubleClick={handleDoubleClick}
|
||||
{...rest}
|
||||
>
|
||||
{loading && (
|
||||
<span className="button-spinner">
|
||||
<svg className="spinner-icon" viewBox="0 0 24 24">
|
||||
<circle
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
strokeWidth="3"
|
||||
fill="none"
|
||||
strokeLinecap="round"
|
||||
strokeDasharray="31.4 31.4"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
)}
|
||||
{icon && iconPosition === 'left' && !loading && (
|
||||
<span className="button-icon button-icon-left">{icon}</span>
|
||||
)}
|
||||
{children && <span className="button-content">{children}</span>}
|
||||
{icon && iconPosition === 'right' && !loading && (
|
||||
<span className="button-icon button-icon-right">{icon}</span>
|
||||
)}
|
||||
</button>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Button;
|
||||
@@ -1,150 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
.tippy-box {
|
||||
.tippy-content {
|
||||
.label-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.375rem 0.625rem 0.25rem 0.625rem;
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.025em;
|
||||
color: ${(props) => props.theme.dropdown.color};
|
||||
opacity: 0.6;
|
||||
margin-top: 0.25rem;
|
||||
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.275rem 0.625rem;
|
||||
cursor: pointer;
|
||||
border-radius: 6px;
|
||||
margin: 0.0625rem 0;
|
||||
font-size: 0.8125rem;
|
||||
|
||||
&.active {
|
||||
color: ${(props) => props.theme.colors.text.yellow} !important;
|
||||
.dropdown-icon {
|
||||
color: ${(props) => props.theme.colors.text.yellow} !important;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-label {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.dropdown-icon {
|
||||
flex-shrink: 0;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: ${(props) => props.theme.dropdown.iconColor};
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.dropdown-right-section {
|
||||
margin-left: auto;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&:hover:not(:disabled):not(.disabled) {
|
||||
background-color: ${(props) => props.theme.dropdown.hoverBg};
|
||||
}
|
||||
|
||||
&.selected-focused:not(:disabled):not(.disabled) {
|
||||
background-color: ${(props) => props.theme.dropdown.hoverBg};
|
||||
}
|
||||
|
||||
&:focus-visible:not(:disabled):not(.disabled) {
|
||||
outline: none;
|
||||
background-color: ${(props) => props.theme.dropdown.hoverBg};
|
||||
}
|
||||
|
||||
&:focus:not(:focus-visible) {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&:disabled,
|
||||
&.disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&.delete-item {
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
.dropdown-icon {
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
}
|
||||
&:hover {
|
||||
background-color: ${({ theme }) => {
|
||||
const hex = theme.colors.text.danger.replace('#', '');
|
||||
const r = parseInt(hex.substring(0, 2), 16);
|
||||
const g = parseInt(hex.substring(2, 4), 16);
|
||||
const b = parseInt(hex.substring(4, 6), 16);
|
||||
return `rgba(${r}, ${g}, ${b}, 0.04)`; // 4% opacity
|
||||
}} !important;
|
||||
|
||||
color: ${(props) => props.theme.colors.text.danger} !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.border-top {
|
||||
border-top: solid 1px ${(props) => props.theme.dropdown.separator};
|
||||
margin-top: 0.25rem;
|
||||
padding-top: 0.375rem;
|
||||
}
|
||||
|
||||
&.dropdown-item-select {
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
|
||||
/* Focused state - applied during keyboard navigation */
|
||||
&.dropdown-item-focused {
|
||||
background-color: ${({ theme }) => theme.dropdown.hoverBg};
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* Active/selected state - applied to the currently selected item */
|
||||
&.dropdown-item-active {
|
||||
color: ${({ theme }) => theme.colors.text.yellow};
|
||||
background-color: ${({ theme }) => theme.dropdown.activeBg};
|
||||
font-weight: 500;
|
||||
.dropdown-icon {
|
||||
color: ${({ theme }) => theme.colors.text.yellow};
|
||||
}
|
||||
}
|
||||
|
||||
/* Combined state - when active item is also focused */
|
||||
&.dropdown-item-active.dropdown-item-focused {
|
||||
background-color: ${({ theme }) => theme.dropdown.activeHoverBg};
|
||||
}
|
||||
|
||||
/* Focus visible for accessibility */
|
||||
&:focus-visible {
|
||||
outline: 2px solid ${({ theme }) => theme.dropdown.focusRing};
|
||||
outline-offset: -2px;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-separator {
|
||||
height: 1px;
|
||||
background-color: ${(props) => props.theme.dropdown.separator};
|
||||
margin: 0.25rem 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -1,6 +1,5 @@
|
||||
import React, { forwardRef, useRef, useCallback, useState, useImperativeHandle, useEffect, useMemo } from 'react';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
// Constants
|
||||
const NAVIGATION_KEYS = ['ArrowDown', 'ArrowUp', 'Home', 'End', 'Escape'];
|
||||
@@ -432,37 +431,35 @@ const MenuDropdown = forwardRef(({
|
||||
: <div onClick={handleTriggerClick} data-testid={testId}>{children}</div>;
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<Dropdown
|
||||
onCreate={onDropdownCreate}
|
||||
icon={triggerElement}
|
||||
placement={placement}
|
||||
className={className}
|
||||
visible={isOpen}
|
||||
onClickOutside={handleClickOutside}
|
||||
{...dropdownProps}
|
||||
>
|
||||
<div {...(testId && { 'data-testid': testId + '-dropdown' })}>
|
||||
{header && (
|
||||
<div className="dropdown-header-container" onClick={handleClickOutside}>
|
||||
{header}
|
||||
<div className="dropdown-divider"></div>
|
||||
</div>
|
||||
)}
|
||||
<div role="menu" tabIndex={-1} onKeyDown={handleMenuKeyDown}>
|
||||
{renderMenuContent()}
|
||||
<Dropdown
|
||||
onCreate={onDropdownCreate}
|
||||
icon={triggerElement}
|
||||
placement={placement}
|
||||
className={className}
|
||||
visible={isOpen}
|
||||
onClickOutside={handleClickOutside}
|
||||
{...dropdownProps}
|
||||
>
|
||||
<div {...(testId && { 'data-testid': testId + '-dropdown' })}>
|
||||
{header && (
|
||||
<div className="dropdown-header-container" onClick={handleClickOutside}>
|
||||
{header}
|
||||
<div className="dropdown-divider"></div>
|
||||
</div>
|
||||
{footer && (
|
||||
<>
|
||||
<div className="dropdown-divider"></div>
|
||||
<div className="dropdown-footer-container">
|
||||
{footer}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
)}
|
||||
<div role="menu" tabIndex={-1} onKeyDown={handleMenuKeyDown}>
|
||||
{renderMenuContent()}
|
||||
</div>
|
||||
</Dropdown>
|
||||
</StyledWrapper>
|
||||
{footer && (
|
||||
<>
|
||||
<div className="dropdown-divider"></div>
|
||||
<div className="dropdown-footer-container">
|
||||
{footer}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Dropdown>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ const DROPDOWN_WIDTH = 60;
|
||||
const CALCULATION_DELAY_DEFAULT = 20;
|
||||
const CALCULATION_DELAY_EXTENDED = 150;
|
||||
const GAP_BETWEEN_LEFT_AND_RIGHT_CONTENT = 80;
|
||||
const EXPANDABLE_HYSTERESIS = 20; // Buffer to prevent flickering at boundary
|
||||
|
||||
// Compare two tab arrays by their keys
|
||||
const areTabArraysEqual = (a, b) => {
|
||||
@@ -22,7 +23,8 @@ const ResponsiveTabs = ({
|
||||
rightContent,
|
||||
rightContentRef,
|
||||
delayedTabs = [],
|
||||
rightContentExpandedWidth // Optional: width of the right content when expanded(used when right content's elements are collapsible)
|
||||
rightContentExpandedWidth, // Optional: width of the expandable element when expanded
|
||||
expandableElementIndex = -1 // Optional: index of the expandable child element (-1 means last child)
|
||||
}) => {
|
||||
const [visibleTabs, setVisibleTabs] = useState([]);
|
||||
const [overflowTabs, setOverflowTabs] = useState([]);
|
||||
@@ -82,12 +84,47 @@ const ResponsiveTabs = ({
|
||||
setOverflowTabs((prev) => (areTabArraysEqual(prev, overflow) ? prev : overflow));
|
||||
|
||||
// Only calculate expandibility if rightContentExpandedWidth is provided
|
||||
if (rightContentExpandedWidth) {
|
||||
if (rightContentExpandedWidth && rightContentRef?.current) {
|
||||
const leftContentWidth = currentWidth + (overflow.length ? DROPDOWN_WIDTH : 0);
|
||||
const expandable = containerWidth - leftContentWidth - GAP_BETWEEN_LEFT_AND_RIGHT_CONTENT > rightContentExpandedWidth;
|
||||
setRightSideExpandable((prev) => (prev === expandable ? prev : expandable));
|
||||
|
||||
// Calculate total expanded width by summing children widths
|
||||
// and replacing the expandable element's current width with its expanded width
|
||||
const children = rightContentRef.current.children;
|
||||
const childrenCount = children.length;
|
||||
|
||||
if (childrenCount > 0) {
|
||||
// Resolve the expandable element index (-1 means last child)
|
||||
const targetIndex = expandableElementIndex < 0 ? childrenCount + expandableElementIndex : expandableElementIndex;
|
||||
const validTargetIndex = Math.max(0, Math.min(targetIndex, childrenCount - 1));
|
||||
|
||||
let totalExpandedWidth = 0;
|
||||
for (let i = 0; i < childrenCount; i++) {
|
||||
if (i === validTargetIndex) {
|
||||
// Use the expanded width for the expandable element
|
||||
totalExpandedWidth += rightContentExpandedWidth;
|
||||
} else {
|
||||
// Use the current width for other elements
|
||||
totalExpandedWidth += children[i].offsetWidth;
|
||||
}
|
||||
}
|
||||
|
||||
const availableSpace = containerWidth - leftContentWidth - GAP_BETWEEN_LEFT_AND_RIGHT_CONTENT;
|
||||
|
||||
// Use hysteresis to prevent flickering at boundary
|
||||
// When expanded: only collapse if significantly less space available
|
||||
// When collapsed: expand when there's enough space
|
||||
setRightSideExpandable((prev) => {
|
||||
if (prev) {
|
||||
// Currently expanded - only collapse if space drops below threshold minus hysteresis
|
||||
return availableSpace > totalExpandedWidth - EXPANDABLE_HYSTERESIS;
|
||||
} else {
|
||||
// Currently collapsed - expand if there's enough space
|
||||
return availableSpace > totalExpandedWidth;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [tabs, activeTab, rightContentRef, rightContentExpandedWidth]);
|
||||
}, [tabs, activeTab, rightContentRef, rightContentExpandedWidth, expandableElementIndex]);
|
||||
|
||||
// Recalculate on tab/activeTab changes
|
||||
useEffect(() => {
|
||||
|
||||
@@ -33,6 +33,13 @@ export const isMacOS = () => {
|
||||
return osFamily.includes('os x');
|
||||
};
|
||||
|
||||
export const isLinuxOS = () => {
|
||||
const os = platform.os;
|
||||
const osFamily = os.family.toLowerCase();
|
||||
|
||||
return osFamily.includes('linux') || osFamily.includes('ubuntu') || osFamily.includes('debian') || osFamily.includes('fedora') || osFamily.includes('centos') || osFamily.includes('arch');
|
||||
};
|
||||
|
||||
export const getAppInstallDate = () => {
|
||||
let dateString = localStorage.getItem('bruno.installedOn');
|
||||
|
||||
|
||||
23
packages/bruno-app/src/utils/exporters/opencollection.js
Normal file
23
packages/bruno-app/src/utils/exporters/opencollection.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import * as FileSaver from 'file-saver';
|
||||
import jsyaml from 'js-yaml';
|
||||
import { brunoToOpenCollection } from '@usebruno/converters';
|
||||
import { sanitizeName } from 'utils/common/regex';
|
||||
|
||||
export const exportCollection = (collection) => {
|
||||
const openCollection = brunoToOpenCollection(collection);
|
||||
|
||||
const yamlContent = jsyaml.dump(openCollection, {
|
||||
indent: 2,
|
||||
lineWidth: -1,
|
||||
noRefs: true,
|
||||
sortKeys: false
|
||||
});
|
||||
|
||||
const sanitizedName = sanitizeName(collection.name);
|
||||
const fileName = `${sanitizedName}.yml`;
|
||||
const fileBlob = new Blob([yamlContent], { type: 'application/x-yaml' });
|
||||
|
||||
FileSaver.saveAs(fileBlob, fileName);
|
||||
};
|
||||
|
||||
export default exportCollection;
|
||||
81
packages/bruno-app/src/utils/importers/opencollection.js
Normal file
81
packages/bruno-app/src/utils/importers/opencollection.js
Normal file
@@ -0,0 +1,81 @@
|
||||
import each from 'lodash/each';
|
||||
import { uuid } from 'utils/common';
|
||||
import { BrunoError } from 'utils/common/error';
|
||||
import { validateSchema, updateUidsInCollection, hydrateSeqInCollection } from './common';
|
||||
import { openCollectionToBruno } from '@usebruno/converters';
|
||||
|
||||
const addUidsToRoot = (collection) => {
|
||||
if (collection.root?.request?.headers) {
|
||||
each(collection.root.request.headers, (header) => {
|
||||
header.uid = uuid();
|
||||
});
|
||||
}
|
||||
if (collection.root?.request?.vars?.req) {
|
||||
each(collection.root.request.vars.req, (v) => {
|
||||
v.uid = uuid();
|
||||
});
|
||||
}
|
||||
if (collection.root?.request?.vars?.res) {
|
||||
each(collection.root.request.vars.res, (v) => {
|
||||
v.uid = uuid();
|
||||
});
|
||||
}
|
||||
|
||||
const addUidsToFolderRoot = (items) => {
|
||||
each(items, (item) => {
|
||||
if (item.type === 'folder') {
|
||||
if (item.root?.request?.headers) {
|
||||
each(item.root.request.headers, (header) => {
|
||||
header.uid = uuid();
|
||||
});
|
||||
}
|
||||
if (item.root?.request?.vars?.req) {
|
||||
each(item.root.request.vars.req, (v) => {
|
||||
v.uid = uuid();
|
||||
});
|
||||
}
|
||||
if (item.root?.request?.vars?.res) {
|
||||
each(item.root.request.vars.res, (v) => {
|
||||
v.uid = uuid();
|
||||
});
|
||||
}
|
||||
if (item.items?.length) {
|
||||
addUidsToFolderRoot(item.items);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
addUidsToFolderRoot(collection.items);
|
||||
return collection;
|
||||
};
|
||||
|
||||
export const processOpenCollection = async (jsonData) => {
|
||||
try {
|
||||
let collection = openCollectionToBruno(jsonData);
|
||||
collection = hydrateSeqInCollection(collection);
|
||||
collection = updateUidsInCollection(collection);
|
||||
collection = addUidsToRoot(collection);
|
||||
await validateSchema(collection);
|
||||
return collection;
|
||||
} catch (err) {
|
||||
console.error('Error processing OpenCollection:', err);
|
||||
throw new BrunoError('Import OpenCollection failed');
|
||||
}
|
||||
};
|
||||
|
||||
export const isOpenCollection = (data) => {
|
||||
if (typeof data !== 'object' || data === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof data.opencollection !== 'string' || !data.opencollection.trim()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof data.info !== 'object' || data.info === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
35
packages/bruno-app/storybook/main.js
Normal file
35
packages/bruno-app/storybook/main.js
Normal file
@@ -0,0 +1,35 @@
|
||||
const path = require('path');
|
||||
|
||||
/** @type { import('@storybook/react-webpack5').StorybookConfig } */
|
||||
const config = {
|
||||
stories: ['../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
|
||||
addons: [
|
||||
'@storybook/addon-webpack5-compiler-babel'
|
||||
],
|
||||
framework: {
|
||||
name: '@storybook/react-webpack5',
|
||||
options: {}
|
||||
},
|
||||
docs: {
|
||||
autodocs: true
|
||||
},
|
||||
webpackFinal: async (config) => {
|
||||
// Add path aliases to match jsconfig.json
|
||||
config.resolve.alias = {
|
||||
...config.resolve.alias,
|
||||
assets: path.resolve(__dirname, '../src/assets'),
|
||||
ui: path.resolve(__dirname, '../src/ui'),
|
||||
components: path.resolve(__dirname, '../src/components'),
|
||||
hooks: path.resolve(__dirname, '../src/hooks'),
|
||||
themes: path.resolve(__dirname, '../src/themes'),
|
||||
api: path.resolve(__dirname, '../src/api'),
|
||||
pageComponents: path.resolve(__dirname, '../src/pageComponents'),
|
||||
providers: path.resolve(__dirname, '../src/providers'),
|
||||
utils: path.resolve(__dirname, '../src/utils')
|
||||
};
|
||||
|
||||
return config;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = config;
|
||||
73
packages/bruno-app/storybook/preview.jsx
Normal file
73
packages/bruno-app/storybook/preview.jsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import React from 'react';
|
||||
import { ThemeProvider as SCThemeProvider, createGlobalStyle } from 'styled-components';
|
||||
import themes from 'themes/index';
|
||||
import '@fontsource/inter/400.css';
|
||||
import '@fontsource/inter/500.css';
|
||||
import '@fontsource/inter/600.css';
|
||||
import '@fontsource/inter/700.css';
|
||||
|
||||
const GlobalStyle = createGlobalStyle`
|
||||
* {
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
||||
}
|
||||
`;
|
||||
|
||||
/** @type { import('@storybook/react').Preview } */
|
||||
const preview = {
|
||||
parameters: {
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/i
|
||||
}
|
||||
},
|
||||
backgrounds: {
|
||||
default: 'light',
|
||||
values: [
|
||||
{ name: 'light', value: '#ffffff' },
|
||||
{ name: 'dark', value: '#1e1e1e' }
|
||||
]
|
||||
}
|
||||
},
|
||||
globalTypes: {
|
||||
theme: {
|
||||
description: 'Global theme for components',
|
||||
toolbar: {
|
||||
title: 'Theme',
|
||||
icon: 'paintbrush',
|
||||
items: [
|
||||
{ value: 'light', title: 'Light' },
|
||||
{ value: 'dark', title: 'Dark' }
|
||||
],
|
||||
dynamicTitle: true
|
||||
}
|
||||
}
|
||||
},
|
||||
initialGlobals: {
|
||||
theme: 'light'
|
||||
},
|
||||
decorators: [
|
||||
(Story, context) => {
|
||||
const themeName = context.globals.theme || 'light';
|
||||
const theme = themes[themeName];
|
||||
|
||||
// Update background and text color based on theme
|
||||
const isDark = themeName === 'dark';
|
||||
const backgroundColor = isDark ? '#1e1e1e' : '#ffffff';
|
||||
const textColor = isDark ? '#d4d4d4' : '#333333';
|
||||
document.body.style.backgroundColor = backgroundColor;
|
||||
document.body.style.color = textColor;
|
||||
|
||||
return (
|
||||
<SCThemeProvider theme={theme}>
|
||||
<GlobalStyle />
|
||||
<div style={{ padding: '1rem', color: textColor }}>
|
||||
<Story />
|
||||
</div>
|
||||
</SCThemeProvider>
|
||||
);
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
export default preview;
|
||||
@@ -4,7 +4,6 @@
|
||||
"license": "MIT",
|
||||
"main": "dist/cjs/index.js",
|
||||
"module": "dist/esm/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"files": [
|
||||
"dist",
|
||||
"src",
|
||||
@@ -29,6 +28,8 @@
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.25.2",
|
||||
"@babel/preset-env": "^7.25.4",
|
||||
"@opencollection/types": "0.3.0",
|
||||
"@usebruno/schema-types": "0.0.1",
|
||||
"@rollup/plugin-alias": "^5.1.0",
|
||||
"@rollup/plugin-commonjs": "^23.0.2",
|
||||
"@rollup/plugin-node-resolve": "^15.0.1",
|
||||
|
||||
@@ -1,43 +1,89 @@
|
||||
const { nodeResolve } = require('@rollup/plugin-node-resolve');
|
||||
const commonjs = require('@rollup/plugin-commonjs');
|
||||
const typescript = require('@rollup/plugin-typescript');
|
||||
const { terser } = require('rollup-plugin-terser');
|
||||
const peerDepsExternal = require('rollup-plugin-peer-deps-external');
|
||||
const { copy } = require('@web/rollup-plugin-copy');
|
||||
const path = require('path');
|
||||
|
||||
const packageJson = require('./package.json');
|
||||
const alias = require('@rollup/plugin-alias');
|
||||
const path = require('path');
|
||||
|
||||
const externalDeps = [
|
||||
'@usebruno/schema',
|
||||
'@usebruno/schema-types',
|
||||
/@usebruno\/schema-types\/.*/,
|
||||
'@opencollection/types',
|
||||
/@opencollection\/types\/.*/,
|
||||
// Runtime dependencies
|
||||
'lodash',
|
||||
'lodash/each',
|
||||
'lodash/get',
|
||||
'lodash/cloneDeep',
|
||||
'lodash/map',
|
||||
'js-yaml',
|
||||
'jscodeshift',
|
||||
'nanoid',
|
||||
'xml2js',
|
||||
// Node built-ins
|
||||
'path',
|
||||
'fs'
|
||||
];
|
||||
|
||||
module.exports = [
|
||||
{
|
||||
input: 'src/index.js',
|
||||
output: [
|
||||
{
|
||||
dir: path.dirname(packageJson.main),
|
||||
format: 'cjs',
|
||||
sourcemap: true
|
||||
},
|
||||
{
|
||||
dir: path.dirname(packageJson.module),
|
||||
format: 'esm',
|
||||
sourcemap: true
|
||||
}
|
||||
],
|
||||
output: {
|
||||
dir: path.dirname(packageJson.main),
|
||||
format: 'cjs',
|
||||
sourcemap: true,
|
||||
exports: 'named',
|
||||
entryFileNames: 'index.js'
|
||||
},
|
||||
plugins: [
|
||||
peerDepsExternal(),
|
||||
nodeResolve({
|
||||
preferBuiltins: true,
|
||||
extensions: ['.js', '.css'] // Resolve .js files
|
||||
extensions: ['.js', '.ts', '.tsx', '.json']
|
||||
}),
|
||||
commonjs(),
|
||||
terser(),
|
||||
alias({
|
||||
entries: [{ find: 'src', replacement: path.resolve(__dirname, 'src') }]
|
||||
typescript({
|
||||
tsconfig: './tsconfig.json',
|
||||
sourceMap: true,
|
||||
outDir: path.dirname(packageJson.main)
|
||||
}),
|
||||
terser(),
|
||||
copy({
|
||||
patterns: 'src/workers/scripts/**/*',
|
||||
rootDir: '.'
|
||||
})
|
||||
]
|
||||
],
|
||||
external: externalDeps
|
||||
},
|
||||
{
|
||||
input: 'src/index.js',
|
||||
output: {
|
||||
dir: path.dirname(packageJson.module),
|
||||
format: 'esm',
|
||||
sourcemap: true,
|
||||
exports: 'named',
|
||||
entryFileNames: 'index.js'
|
||||
},
|
||||
plugins: [
|
||||
peerDepsExternal(),
|
||||
nodeResolve({
|
||||
extensions: ['.js', '.ts', '.tsx', '.json']
|
||||
}),
|
||||
commonjs(),
|
||||
typescript({
|
||||
tsconfig: './tsconfig.json',
|
||||
sourceMap: true,
|
||||
outDir: path.dirname(packageJson.module)
|
||||
}),
|
||||
terser(),
|
||||
copy({
|
||||
patterns: 'src/workers/scripts/**/*',
|
||||
rootDir: '.'
|
||||
})
|
||||
],
|
||||
external: externalDeps
|
||||
}
|
||||
];
|
||||
|
||||
@@ -5,3 +5,5 @@ export { default as openApiToBruno } from './openapi/openapi-to-bruno.js';
|
||||
export { default as insomniaToBruno } from './insomnia/insomnia-to-bruno.js';
|
||||
export { default as wsdlToBruno } from './wsdl/wsdl-to-bruno.js';
|
||||
export { default as postmanTranslation } from './postman/postman-translations.js';
|
||||
export { openCollectionToBruno } from './opencollection/opencollection-to-bruno.js';
|
||||
export { brunoToOpenCollection } from './opencollection/bruno-to-opencollection.js';
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
import { toOpenCollectionAuth, toOpenCollectionHeaders, toOpenCollectionScripts, toOpenCollectionVariables } from "./common";
|
||||
import { toOpenCollectionEnvironments } from "./environment";
|
||||
import { toOpenCollectionFolder } from "./folder";
|
||||
import { toOpenCollectionItems } from "./items";
|
||||
import { BrunoCollection, BrunoCollectionRoot, BrunoConfig, ClientCertificate, CollectionConfig, OpenCollection, PemCertificate, Pkcs12Certificate, Protobuf } from "./types";
|
||||
|
||||
const toOpenCollectionConfig = (brunoConfig: BrunoConfig | undefined): CollectionConfig | undefined => {
|
||||
if (!brunoConfig) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const config: CollectionConfig = {};
|
||||
|
||||
if (brunoConfig.protobuf?.protoFiles?.length || brunoConfig.protobuf?.importPaths?.length) {
|
||||
config.protobuf = {} as Protobuf;
|
||||
|
||||
if (brunoConfig.protobuf.protoFiles?.length) {
|
||||
config.protobuf.protoFiles = brunoConfig.protobuf.protoFiles.map((f) => ({
|
||||
type: 'file' as const,
|
||||
path: f.path
|
||||
}));
|
||||
}
|
||||
|
||||
if (brunoConfig.protobuf.importPaths?.length) {
|
||||
config.protobuf.importPaths = brunoConfig.protobuf.importPaths.map((p) => {
|
||||
const importPath: { path: string; disabled?: boolean } = { path: p.path };
|
||||
if (p.disabled) {
|
||||
importPath.disabled = true;
|
||||
}
|
||||
return importPath;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (brunoConfig.proxy?.enabled) {
|
||||
if (brunoConfig.proxy.enabled === 'global') {
|
||||
config.proxy = 'inherit';
|
||||
} else {
|
||||
config.proxy = {
|
||||
protocol: brunoConfig.proxy.protocol || 'http',
|
||||
hostname: brunoConfig.proxy.hostname || '',
|
||||
port: brunoConfig.proxy.port || 0
|
||||
};
|
||||
|
||||
if (brunoConfig.proxy.auth?.enabled) {
|
||||
config.proxy.auth = {
|
||||
username: brunoConfig.proxy.auth.username || '',
|
||||
password: brunoConfig.proxy.auth.password || ''
|
||||
};
|
||||
}
|
||||
|
||||
if (brunoConfig.proxy.bypassProxy) {
|
||||
config.proxy.bypassProxy = brunoConfig.proxy.bypassProxy;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (brunoConfig.clientCertificates?.certs?.length) {
|
||||
config.clientCertificates = brunoConfig.clientCertificates.certs
|
||||
.map((cert): ClientCertificate | null => {
|
||||
if (cert.type === 'pem') {
|
||||
const pemCert: PemCertificate = {
|
||||
domain: cert.domain || '',
|
||||
type: 'pem',
|
||||
certificateFilePath: cert.certFilePath || '',
|
||||
privateKeyFilePath: cert.keyFilePath || ''
|
||||
};
|
||||
if (cert.passphrase) {
|
||||
pemCert.passphrase = cert.passphrase;
|
||||
}
|
||||
return pemCert;
|
||||
} else if (cert.type === 'pkcs12') {
|
||||
const pkcs12Cert: Pkcs12Certificate = {
|
||||
domain: cert.domain || '',
|
||||
type: 'pkcs12',
|
||||
pkcs12FilePath: cert.pfxFilePath || ''
|
||||
};
|
||||
if (cert.passphrase) {
|
||||
pkcs12Cert.passphrase = cert.passphrase;
|
||||
}
|
||||
return pkcs12Cert;
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter((cert): cert is ClientCertificate => cert !== null);
|
||||
}
|
||||
|
||||
return Object.keys(config).length > 0 ? config : undefined;
|
||||
};
|
||||
|
||||
const hasRequestDefaults = (root: BrunoCollectionRoot | undefined): boolean => {
|
||||
const request = root?.request;
|
||||
return Boolean(
|
||||
request?.headers?.length ||
|
||||
request?.vars?.req?.length ||
|
||||
request?.script?.req ||
|
||||
request?.script?.res ||
|
||||
request?.tests ||
|
||||
(request?.auth && request.auth.mode !== 'none')
|
||||
);
|
||||
};
|
||||
|
||||
export const brunoToOpenCollection = (collection: BrunoCollection): OpenCollection => {
|
||||
const openCollection: OpenCollection = {
|
||||
opencollection: '1.0.0',
|
||||
info: {
|
||||
name: collection.name || 'Untitled Collection'
|
||||
}
|
||||
};
|
||||
|
||||
const config = toOpenCollectionConfig(collection.brunoConfig as BrunoConfig);
|
||||
if (config) {
|
||||
openCollection.config = config;
|
||||
}
|
||||
|
||||
const environments = toOpenCollectionEnvironments(collection.environments ?? undefined);
|
||||
if (environments?.length) {
|
||||
if (!openCollection.config) {
|
||||
openCollection.config = {};
|
||||
}
|
||||
openCollection.config.environments = environments;
|
||||
}
|
||||
|
||||
const items = toOpenCollectionItems(collection.items, toOpenCollectionFolder);
|
||||
if (items.length) {
|
||||
openCollection.items = items as OpenCollection['items'];
|
||||
}
|
||||
|
||||
if (hasRequestDefaults(collection.root as BrunoCollectionRoot)) {
|
||||
const request = (collection.root as BrunoCollectionRoot)?.request;
|
||||
openCollection.request = {};
|
||||
|
||||
const headers = toOpenCollectionHeaders(request?.headers);
|
||||
if (headers) {
|
||||
openCollection.request.headers = headers;
|
||||
}
|
||||
|
||||
const auth = toOpenCollectionAuth(request?.auth);
|
||||
if (auth) {
|
||||
openCollection.request.auth = auth;
|
||||
}
|
||||
|
||||
const variables = toOpenCollectionVariables(request?.vars);
|
||||
if (variables) {
|
||||
openCollection.request.variables = variables;
|
||||
}
|
||||
|
||||
const scripts = toOpenCollectionScripts(request as any);
|
||||
if (scripts) {
|
||||
openCollection.request.scripts = scripts;
|
||||
}
|
||||
}
|
||||
|
||||
if ((collection.root as BrunoCollectionRoot)?.docs) {
|
||||
openCollection.docs = {
|
||||
content: (collection.root as BrunoCollectionRoot).docs!,
|
||||
type: 'text/markdown'
|
||||
};
|
||||
}
|
||||
|
||||
openCollection.bundled = true;
|
||||
|
||||
const extensions: { ignore?: string[] } = {};
|
||||
if ((collection.brunoConfig as BrunoConfig)?.ignore?.length) {
|
||||
extensions.ignore = (collection.brunoConfig as BrunoConfig).ignore;
|
||||
}
|
||||
if (Object.keys(extensions).length > 0) {
|
||||
openCollection.extensions = extensions;
|
||||
}
|
||||
|
||||
return openCollection;
|
||||
};
|
||||
@@ -0,0 +1,83 @@
|
||||
import { uuid } from '../../common/index.js';
|
||||
import type {
|
||||
Action,
|
||||
ActionSetVariable,
|
||||
ActionVariableScope,
|
||||
BrunoVariable,
|
||||
BrunoVariables
|
||||
} from '../types';
|
||||
|
||||
/**
|
||||
* Convert Bruno post-response variables to OpenCollection actions.
|
||||
* Post-response variables in Bruno are converted to 'set-variable' actions
|
||||
* with phase 'after-response'.
|
||||
*/
|
||||
export const toOpenCollectionActions = (resVariables: BrunoVariables | null | undefined): Action[] | undefined => {
|
||||
if (!resVariables?.length) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const actions: Action[] = resVariables.map((v: BrunoVariable): ActionSetVariable => {
|
||||
const action: ActionSetVariable = {
|
||||
type: 'set-variable',
|
||||
phase: 'after-response',
|
||||
selector: {
|
||||
expression: v.value || '',
|
||||
method: 'jsonq'
|
||||
},
|
||||
variable: {
|
||||
name: v.name || '',
|
||||
scope: v.local ? 'request' : 'runtime' as ActionVariableScope
|
||||
}
|
||||
};
|
||||
|
||||
if (v.description && typeof v.description === 'string' && v.description.trim().length) {
|
||||
action.description = v.description;
|
||||
}
|
||||
|
||||
if (v.enabled === false) {
|
||||
action.disabled = true;
|
||||
}
|
||||
|
||||
return action;
|
||||
});
|
||||
|
||||
return actions.length > 0 ? actions : undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert OpenCollection actions to Bruno post-response variables.
|
||||
* Only 'set-variable' actions with phase 'after-response' are converted.
|
||||
*/
|
||||
export const fromOpenCollectionActions = (actions: Action[] | null | undefined): BrunoVariables => {
|
||||
if (!actions?.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const resVars: BrunoVariables = [];
|
||||
|
||||
actions.forEach((action: Action) => {
|
||||
// Only process 'set-variable' actions with 'after-response' phase
|
||||
if (action.type === 'set-variable' && action.phase === 'after-response') {
|
||||
const setVarAction = action as ActionSetVariable;
|
||||
|
||||
const variable: BrunoVariable = {
|
||||
uid: uuid(),
|
||||
name: setVarAction.variable?.name || '',
|
||||
value: setVarAction.selector?.expression || '',
|
||||
enabled: setVarAction.disabled !== true,
|
||||
local: setVarAction.variable?.scope === 'request'
|
||||
};
|
||||
|
||||
if (setVarAction.description) {
|
||||
variable.description = typeof setVarAction.description === 'string'
|
||||
? setVarAction.description
|
||||
: (setVarAction.description as { content?: string })?.content || '';
|
||||
}
|
||||
|
||||
resVars.push(variable);
|
||||
}
|
||||
});
|
||||
|
||||
return resVars;
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user