Compare commits

..

16 Commits

Author SHA1 Message Date
jeremiec
fd92df7bf9 Feat(#736): ctrl tab shortcut (#990)
* feat: update settings collection tab name

* refact: special tab name in its own helper

* feat: add ctrl tab

* chore: add jest config for module name mapping

* feat: add path to ctrlTab

---------

Co-authored-by: Anoop M D <anoop.md1421@gmail.com>
2024-08-30 10:34:31 +05:30
Mateusz Pietryga
93080de2a8 fix: Issue with Parameters Passed in the URL(#2124) (#2139)
* fix: Issue with Parameters Passed in the URL(#2124)

The '=' should be allowed within query parameter value. While first equals sign separates parameter name from its value, any subsequent occurrences are valid and should not be discarded.
The '#' in URL always indicates the start of URI Fragment component, and should not be treated as part of any parameter value.

* chore: gracefully fail when URLSearchParams throws error

---------

Co-authored-by: Anoop M D <anoop.md1421@gmail.com>
2024-08-29 12:46:20 +05:30
dev.paramjot
36ef38be6a feat(#736): Switch tabs with keyboard shortcut (#812)
* feat(#736): Switch tabs with keyboard shortcut

1. Registered keyboard events in Hotkeys/index.js
2. Added logic for replacing `state.activeTabUid` to switch active tab as per keyboard event.
3. Maintained a stack `recentUsedTabsStack` for tab visit history and pop out on `Ctrl+Tab` keyboard event.

* feat(#736): Switch tabs with keyboard shortcut

Keeping this feature request only limited to CTRL+PGUP and CTRL_PGDN button clicks functionality. Hence removing logic for CTRL+TAB click functionality.

* feat(#736): Switch tabs with keyboard shortcut

clean up

* feate(#827): Switch tabs with keyboard shortcut

* Implimented logic of cyclic traversal of tabs array with % opreator.

---------

Co-authored-by: Anoop M D <anoop.md1421@gmail.com>
2024-08-29 11:12:28 +05:30
Pragadesh-45
4726f5008e style chore: make delete div bg important (#2949) 2024-08-29 10:43:32 +05:30
Anoop M D
091b02c2c3 release: v1.27.0 2024-08-28 15:10:28 +05:30
Pragadesh-45
600940226c Feat/icon tooltip (#2812)
* comp-rename Tooltip to InfoTip (fbu)

* fix: additional func InfoTip

* ToolHint component

* toolhint intg collectiontoolbar

* toolhint intg notifications

* toolhint intg sidebar

* chore: update infotip for path params

* chore: update infotip for path params
2024-08-27 18:36:59 +05:30
Max Bauer
ee7f886c03 feat: adjust code editor font size (#2204)
* add font-size setting for code editor

* add code font size to remaining editors

* align font-size after font-family

* changed default font size to 14

* fixed className typo

* set inherit mode if unset

* add code font size schema validation

* add font size to folder settings

---------

Co-authored-by: Anoop M D <anoop.md1421@gmail.com>
2024-08-27 18:26:58 +05:30
Daniel Kocot
682c7bd1b1 Aligned the correct form of address to make it easier to read (#2176)
* Aligned the correct form of address to make it easier to read

* typo

Co-authored-by: Andreas Siegel <mail@andreassiegel.de>

---------

Co-authored-by: Andreas Siegel <mail@andreassiegel.de>
2024-08-27 17:33:06 +05:30
KameronKeller
e1aa5b4eb5 Feat (#2284): Feature: Add Table of Contents to the Readme (#2285)
* Add table of contents and change heading types

* Revert language section

* Move table of contents and delete first two entries

* Fix broken links
2024-08-27 16:49:43 +05:30
KameronKeller
6bd9d4c480 Fix/2377 Fix humanizeDate so that it always returns the date it is passed (#2378)
* Add tests for humanizeDate and relativeDate

* Add fix to humanizeDate

* Fix test description
2024-08-27 16:16:35 +05:30
Mateusz Pietryga
a38d09a117 feat: Store client certificate paths in collection settings as relative to collection and display them the UI. (#2421)
#2420
2024-08-27 16:09:19 +05:30
Anoop M D
82985d1b43 chore: updated tests 2024-08-27 14:38:44 +05:30
Anoop M D
d34d3a45ff chore: updated test 2024-08-27 14:28:20 +05:30
Anoop M D
25ccb38202 chore: fix lint issue 2024-08-27 14:21:41 +05:30
Anoop M D
5f6a5f59b1 chore: updated check for js sandbox libs 2024-08-27 14:20:50 +05:30
Anoop M D
9e5148f032 fix(#2767): Fix serilization issues of bigint in json body (#2773)
Fix serilization issues of bigint in json body
---------

Co-authored-by: lohit <lohit.jiddimani@gmail.com>
2024-08-27 14:12:56 +05:30
58 changed files with 726 additions and 143 deletions

View File

@@ -39,7 +39,7 @@ Bruno ist ein reines Offline-Tool. Es gibt keine Pläne, Bruno um eine Cloud-Syn
[Download Bruno](https://www.usebruno.com/downloads)
📢 Sehen Sie sich unseren Vortrag auf der India FOSS 3.0 Conference [hier](https://www.youtube.com/watch?v=7bSMFpbcPiY) an.
📢 Sieh Dir unseren Vortrag auf der India FOSS 3.0 Conference [hier](https://www.youtube.com/watch?v=7bSMFpbcPiY) an.
![bruno](/assets/images/landing-2.png) <br /><br />
@@ -48,13 +48,13 @@ Bruno ist ein reines Offline-Tool. Es gibt keine Pläne, Bruno um eine Cloud-Syn
Die meisten unserer Funktionen sind kostenlos und quelloffen.
Wir bemühen uns um ein Gleichgewicht zwischen [Open-Source-Prinzipien und Nachhaltigkeit](https://github.com/usebruno/bruno/discussions/269)
Sie können die [Golden Edition](https://www.usebruno.com/pricing) vorbestellen ~~$19~~ **$9** ! <br/>
Du kannst die [Golden Edition](https://www.usebruno.com/pricing) bestellen **$19**! <br/>
### Installation
Bruno ist als Download [auf unserer Website](https://www.usebruno.com/downloads) für Mac, Windows und Linux verfügbar.
Sie können Bruno auch über Paketmanager wie Homebrew, Chocolatey, Scoop, Snap, Flatpak und Apt installieren.
Du kannst Bruno auch über Paketmanager wie Homebrew, Chocolatey, Scoop, Snap, Flatpak und Apt installieren.
```sh
# Auf Mac via Homebrew
@@ -123,11 +123,11 @@ Oder einer Versionskontrolle deiner Wahl
### Unterstützung ❤️
Wuff! Wenn du dieses Projekt magst, klick den ⭐ Button !!
Wuff! Wenn du dieses Projekt magst, klick auf den ⭐ Button !!
### Teile Erfahrungsberichte 📣
Wenn Bruno dir und in deinen Teams bei der Arbeit geholfen hat, vergiss bitte nicht, deine [Erfahrungsberichte auf unserer GitHub-Diskussion](https://github.com/usebruno/bruno/discussions/343) zu teilen.
Wenn Bruno dir und in deinem Team bei der Arbeit geholfen hat, vergiss bitte nicht, deine [Erfahrungsberichte in unserer GitHub-Diskussion](https://github.com/usebruno/bruno/discussions/343) zu teilen.
### Bereitstellung in neuen Paket-Managern

View File

@@ -0,0 +1,15 @@
/** @type {import('jest').Config} */
const config = {
moduleNameMapper: {
'^assets/(.*)$': '<rootDir>/src/assets/$1',
'^components/(.*)$': '<rootDir>/src/components/$1',
'^hooks/(.*)$': '<rootDir>/src/hooks/$1',
'^themes/(.*)$': '<rootDir>/src/themes/$1',
'^api/(.*)$': '<rootDir>/src/api/$1',
'^pageComponents/(.*)$': '<rootDir>/src/pageComponents/$1',
'^providers/(.*)$': '<rootDir>/src/providers/$1',
'^utils/(.*)$': '<rootDir>/src/utils/$1'
}
};
module.exports = config;

View File

@@ -5,6 +5,7 @@ const StyledWrapper = styled.div`
background: ${(props) => props.theme.codemirror.bg};
border: solid 1px ${(props) => props.theme.codemirror.border};
font-family: ${(props) => (props.font ? props.font : 'default')};
font-size: ${(props) => (props.fontSize ? `${props.fontSize}px` : 'inherit')};
line-break: anywhere;
flex: 1 1 0;
}

View File

@@ -67,7 +67,7 @@ if (!SERVER_RENDERED) {
'bru.setVar(key,value)',
'bru.deleteVar(key)',
'bru.setNextRequest(requestName)',
'req.disableParsingResponseJson()'
'req.disableParsingResponseJson()',
'bru.getRequestVar(key)',
'bru.sleep(ms)'
];
@@ -333,6 +333,7 @@ export default class CodeEditor extends React.Component {
className="h-full w-full flex flex-col relative"
aria-label="Code Editor"
font={this.props.font}
fontSize={this.props.fontSize}
ref={(node) => {
this._node = node;
}}

View File

@@ -10,8 +10,9 @@ import StyledWrapper from './StyledWrapper';
import { useRef } from 'react';
import path from 'path';
import slash from 'utils/common/slash';
import { isWindowsOS } from 'utils/common/platform';
const ClientCertSettings = ({ clientCertConfig, onUpdate, onRemove }) => {
const ClientCertSettings = ({ root, clientCertConfig, onUpdate, onRemove }) => {
const certFilePathInputRef = useRef();
const keyFilePathInputRef = useRef();
const pfxFilePathInputRef = useRef();
@@ -67,7 +68,15 @@ const ClientCertSettings = ({ clientCertConfig, onUpdate, onRemove }) => {
});
const getFile = (e) => {
e.files?.[0]?.path && formik.setFieldValue(e.name, e.files?.[0]?.path);
if (e.files?.[0]?.path) {
let relativePath;
if (isWindowsOS()) {
relativePath = slash(path.win32.relative(root, e.files[0].path));
} else {
relativePath = path.posix.relative(root, e.files[0].path);
}
formik.setFieldValue(e.name, relativePath);
}
};
const resetFileInputFields = () => {
@@ -102,10 +111,14 @@ const ClientCertSettings = ({ clientCertConfig, onUpdate, onRemove }) => {
: clientCertConfig.map((clientCert) => (
<li key={uuid()} className="flex items-center available-certificates p-2 rounded-lg mb-2">
<div className="flex items-center w-full justify-between">
<div className="flex items-center">
<div className="flex w-full items-center">
<IconWorld className="mr-2" size={18} strokeWidth={1.5} />
{clientCert.domain}
</div>
<div className="flex w-full items-center">
<IconCertificate className="mr-2 flex-shrink-0" size={18} strokeWidth={1.5} />
{clientCert.type === 'cert' ? clientCert.certFilePath : clientCert.pfxFilePath}
</div>
<button onClick={() => onRemove(clientCert)} className="remove-certificate ml-2">
<IconTrash size={18} strokeWidth={1.5} />
</button>

View File

@@ -46,6 +46,7 @@ const Docs = ({ collection }) => {
onSave={onSave}
mode="application/text"
font={get(preferences, 'font.codeFont', 'default')}
fontSize={get(preferences, 'font.codeFontSize')}
/>
) : (
<Markdown collectionPath={collection.pathname} onDoubleClick={toggleViewMode} content={docs} />

View File

@@ -1,6 +1,6 @@
import React, { useEffect } from 'react';
import { useFormik } from 'formik';
import Tooltip from 'components/Tooltip';
import InfoTip from 'components/InfoTip';
import StyledWrapper from './StyledWrapper';
import * as Yup from 'yup';
import toast from 'react-hot-toast';
@@ -104,7 +104,7 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
<div className="mb-3 flex items-center">
<label className="settings-label flex items-center" htmlFor="enabled">
Config
<Tooltip
<InfoTip
text={`
<div>
<ul>
@@ -114,7 +114,7 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
</ul>
</div>
`}
tooltipId="request-var"
infotipId="request-var"
/>
</label>
<div className="flex items-center">

View File

@@ -52,6 +52,7 @@ const Script = ({ collection }) => {
mode="javascript"
onSave={handleSave}
font={get(preferences, 'font.codeFont', 'default')}
fontSize={get(preferences, 'font.codeFontSize')}
/>
</div>
<div className="flex-1 mt-6">
@@ -64,6 +65,7 @@ const Script = ({ collection }) => {
mode="javascript"
onSave={handleSave}
font={get(preferences, 'font.codeFont', 'default')}
fontSize={get(preferences, 'font.codeFontSize')}
/>
</div>

View File

@@ -36,6 +36,7 @@ const Tests = ({ collection }) => {
mode="javascript"
onSave={handleSave}
font={get(preferences, 'font.codeFont', 'default')}
fontSize={get(preferences, 'font.codeFontSize')}
/>
<div className="mt-6">

View File

@@ -95,6 +95,7 @@ const CollectionSettings = ({ collection }) => {
case 'clientCert': {
return (
<ClientCertSettings
root={collection.pathname}
clientCertConfig={clientCertConfig}
onUpdate={onClientCertSettingsUpdate}
onRemove={onClientCertSettingsRemove}

View File

@@ -0,0 +1,12 @@
import styled from 'styled-components';
const Wrapper = styled.dialog`
background-color: ${(props) => props.theme.ctrlTabPopup.bg};
color: ${(props) => props.theme.ctrlTabPopup.text};
button:focus {
outline: none;
background-color: ${(props) => props.theme.ctrlTabPopup.highlightBg};
}
`;
export default Wrapper;

View File

@@ -0,0 +1,81 @@
import React from 'react';
import { findItemInCollection, findCollectionByUid } from 'utils/collections';
import { useDispatch, useSelector } from 'react-redux';
import classnames from 'classnames';
import StyledWrapper from './StyledWrapper';
import reverse from 'lodash/reverse';
import { getSpecialTabName, isSpecialTab, isItemAFolder } from 'utils/tabs';
import { selectCtrlTabAction } from 'providers/ReduxStore/slices/tabs';
export const tabStackToPopupTabs = (collections, ctrlTabStack) => {
if (!collections) {
return [];
}
return ctrlTabStack
.map((tab) => {
const collection = findCollectionByUid(collections, tab.collectionUid);
return { collection, tab: findItemInCollection(collection, tab.uid) ?? tab };
})
.map(({ collection, tab }) => ({
tabName: isSpecialTab(tab) ? getSpecialTabName(tab.type) : tab.name,
path: getItemPath(collection, tab),
uid: tab.uid
}));
};
const getItemPath = (collection, item) => {
if (isSpecialTab(item)) {
return collection.name;
}
if (collection.items.find((i) => i.uid === item.uid)) {
return collection.name;
}
return (
collection.name + '/' + collection.items.map((i) => (isItemAFolder(i) ? getItemPath(i, item) : null)).find(Boolean)
);
};
// required in cases where we remove a tab from the stack but the user is still holding ctrl
const tabStackToUniqueId = (ctrlTabStack) => ctrlTabStack.map((tab) => tab.uid).join('-');
export default function CtrlTabPopup() {
const ctrlTabIndex = useSelector((state) => state.tabs.ctrlTabIndex);
const ctrlTabStack = useSelector((state) => state.tabs.ctrlTabStack);
const collections = useSelector((state) => state.collections.collections);
const dispatch = useDispatch();
if (ctrlTabIndex === null) {
return null;
}
const popupTabs = tabStackToPopupTabs(collections, ctrlTabStack);
const currentTabbedTab = popupTabs.at(ctrlTabIndex);
return (
<div className="absolute flex justify-center top-1 w-full">
<StyledWrapper
key={'dialog' + ctrlTabIndex + tabStackToUniqueId(ctrlTabStack)}
className="flex flex-col rounded isolate z-10 p-1 overflow-y-auto max-h-80 w-96"
>
{reverse(popupTabs).map((popupTab) => (
<button
key={popupTab.uid}
autoFocus={currentTabbedTab === popupTab}
onClick={() => dispatch(selectCtrlTabAction(popupTab.uid))}
type="button"
className={classnames('py-0.5 px-5 rounded text-left truncate', {
'is-active': currentTabbedTab === popupTab
})}
>
<strong className="font-medium">{popupTab.tabName}</strong>
{' '}
<span className="text-xs font-extralight">{popupTab.path}</span>
</button>
))}
</StyledWrapper>
</div>
);
}

View File

@@ -0,0 +1,59 @@
import { tabStackToPopupTabs } from './index';
describe('CtrlTabPopup', () => {
describe('tabStackToPopupTabs', () => {
it('should return an empty array if collections is falsy', () => {
const collections = null;
const ctrlTabStack = [];
const result = tabStackToPopupTabs(collections, ctrlTabStack);
expect(result).toEqual([]);
});
it('should return an array of popup tabs', () => {
const collections = [
{
name: 'Collection 1',
uid: 'collection1',
items: [{ name: 'Request 1', uid: 'aaa', type: 'http-request', collectionUid: 'collection1' }]
},
{
name: 'Collection 2',
uid: 'collection2',
items: [
{ name: 'Request 2', uid: 'ccc', type: 'http-request', collectionUid: 'collection2' },
{
name: 'Folder 1',
type: 'folder',
items: [
{ name: 'Request 3', uid: 'ddd', type: 'http-request', collectionUid: 'collection2' },
{
name: 'Folder 2',
type: 'folder',
items: [{ name: 'Request 4', uid: 'eee', type: 'http-request', collectionUid: 'collection2' }]
}
]
}
]
}
];
const ctrlTabStack = [
{ collectionUid: 'collection1', uid: 'aaa', type: 'http-request' },
{ collectionUid: 'collection2', uid: 'ddd', type: 'http-request' },
{ collectionUid: 'collection2', uid: 'eee', type: 'http-request' },
{ collectionUid: 'collection2', uid: 'yyy', type: 'collection-settings' }
];
const result = tabStackToPopupTabs(collections, ctrlTabStack);
expect(result).toEqual([
{ tabName: 'Request 1', path: 'Collection 1', uid: 'aaa' },
{ tabName: 'Request 3', path: 'Collection 2/Folder 1', uid: 'ddd' },
{ tabName: 'Request 4', path: 'Collection 2/Folder 1/Folder 2', uid: 'eee' },
{ tabName: 'Settings', path: 'Collection 2', uid: 'yyy' }
]);
});
});
});

View File

@@ -47,6 +47,7 @@ const Documentation = ({ item, collection }) => {
collection={collection}
theme={displayedTheme}
font={get(preferences, 'font.codeFont', 'default')}
fontSize={get(preferences, 'font.codeFontSize')}
value={docs || ''}
onEdit={onEdit}
onSave={onSave}

View File

@@ -54,6 +54,7 @@ const Script = ({ collection, folder }) => {
mode="javascript"
onSave={handleSave}
font={get(preferences, 'font.codeFont', 'default')}
fontSize={get(preferences, 'font.codeFontSize')}
/>
</div>
<div className="flex flex-col flex-1 mt-2 gap-y-2">
@@ -66,6 +67,7 @@ const Script = ({ collection, folder }) => {
mode="javascript"
onSave={handleSave}
font={get(preferences, 'font.codeFont', 'default')}
fontSize={get(preferences, 'font.codeFontSize')}
/>
</div>

View File

@@ -37,6 +37,7 @@ const Tests = ({ collection, folder }) => {
mode="javascript"
onSave={handleSave}
font={get(preferences, 'font.codeFont', 'default')}
fontSize={get(preferences, 'font.codeFontSize')}
/>
<div className="mt-6">

View File

@@ -5,7 +5,7 @@ import { useDispatch } from 'react-redux';
import { useTheme } from 'providers/Theme';
import { saveFolderRoot } from 'providers/ReduxStore/slices/collections/actions';
import SingleLineEditor from 'components/SingleLineEditor';
import Tooltip from 'components/Tooltip';
import InfoTip from 'components/InfoTip';
import StyledWrapper from './StyledWrapper';
import toast from 'react-hot-toast';
import { variableNameRegex } from 'utils/common/regex';
@@ -82,14 +82,14 @@ const VarsTable = ({ folder, collection, vars, varType }) => {
<td>
<div className="flex items-center">
<span>Value</span>
<Tooltip text="You can write any valid JS Template Literal here" tooltipId="request-var" />
<InfoTip text="You can write any valid JS Template Literal here" infotipId="request-var" />
</div>
</td>
) : (
<td>
<div className="flex items-center">
<span>Expr</span>
<Tooltip text="You can write any valid JS expression here" tooltipId="response-var" />
<InfoTip text="You can write any valid JS expression here" infotipId="response-var" />
</div>
</td>
)}

View File

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

View File

@@ -10,6 +10,8 @@ import {
} from 'providers/ReduxStore/slices/notifications';
import { useDispatch, useSelector } from 'react-redux';
import { humanizeDate, relativeDate } from 'utils/common';
import ToolHint from 'components/ToolHint';
import { useTheme } from 'providers/Theme';
const PAGE_SIZE = 5;
@@ -20,6 +22,7 @@ const Notifications = () => {
const [showNotificationsModal, setShowNotificationsModal] = useState(false);
const [selectedNotification, setSelectedNotification] = useState(null);
const [pageNumber, setPageNumber] = useState(1);
const { storedTheme } = useTheme();
const notificationsStartIndex = (pageNumber - 1) * PAGE_SIZE;
const notificationsEndIndex = pageNumber * PAGE_SIZE;
@@ -85,21 +88,22 @@ const Notifications = () => {
return (
<StyledWrapper>
<a
title="Notifications"
className="relative cursor-pointer"
onClick={() => {
dispatch(fetchNotifications());
setShowNotificationsModal(true);
}}
>
<IconBell
size={18}
strokeWidth={1.5}
className={`mr-2 hover:text-gray-700 ${unreadNotifications?.length > 0 ? 'bell' : ''}`}
/>
{unreadNotifications.length > 0 && (
<span className="notification-count text-xs">{unreadNotifications.length}</span>
)}
<ToolHint text="Notifications" toolhintId="Notifications" offset={8} >
<IconBell
size={18}
strokeWidth={1.5}
className={`mr-2 ${unreadNotifications?.length > 0 ? 'bell' : ''}`}
/>
{unreadNotifications.length > 0 && (
<span className="notification-count text-xs">{unreadNotifications.length}</span>
)}
</ToolHint>
</a>
{showNotificationsModal && (
@@ -129,9 +133,8 @@ const Notifications = () => {
{notifications?.slice(notificationsStartIndex, notificationsEndIndex)?.map((notification) => (
<li
key={notification.id}
className={`p-4 flex flex-col justify-center ${
selectedNotification?.id == notification.id ? 'active' : notification.read ? 'read' : ''
}`}
className={`p-4 flex flex-col justify-center ${selectedNotification?.id == notification.id ? 'active' : notification.read ? 'read' : ''
}`}
onClick={handleNotificationItemClick(notification)}
>
<div className="notification-title w-full">{notification?.title}</div>
@@ -141,9 +144,8 @@ const Notifications = () => {
</ul>
<div className="w-full pagination flex flex-row gap-4 justify-center p-2 items-center text-xs">
<button
className={`pl-2 pr-2 py-3 select-none ${
pageNumber <= 1 ? 'opacity-50' : 'text-link cursor-pointer hover:underline'
}`}
className={`pl-2 pr-2 py-3 select-none ${pageNumber <= 1 ? 'opacity-50' : 'text-link cursor-pointer hover:underline'
}`}
onClick={handlePrev}
>
{'Prev'}
@@ -159,9 +161,8 @@ const Notifications = () => {
</div>
</div>
<button
className={`pl-2 pr-2 py-3 select-none ${
pageNumber == totalPages ? 'opacity-50' : 'text-link cursor-pointer hover:underline'
}`}
className={`pl-2 pr-2 py-3 select-none ${pageNumber == totalPages ? 'opacity-50' : 'text-link cursor-pointer hover:underline'
}`}
onClick={handleNext}
>
{'Next'}

View File

@@ -9,17 +9,25 @@ const Font = ({ close }) => {
const preferences = useSelector((state) => state.app.preferences);
const [codeFont, setCodeFont] = useState(get(preferences, 'font.codeFont', 'default'));
const [codeFontSize, setCodeFontSize] = useState(get(preferences, 'font.codeFontSize', '14'));
const handleInputChange = (event) => {
const handleCodeFontChange = (event) => {
setCodeFont(event.target.value);
};
const handleCodeFontSizeChange = (event) => {
// Restrict to min/max value
const clampedSize = Math.max(1, Math.min(event.target.value, 32));
setCodeFontSize(clampedSize);
};
const handleSave = () => {
dispatch(
savePreferences({
...preferences,
font: {
codeFont
codeFont,
codeFontSize
}
})
).then(() => {
@@ -29,17 +37,33 @@ const Font = ({ close }) => {
return (
<StyledWrapper>
<label className="block font-medium">Code Editor Font</label>
<input
type="text"
className="block textbox mt-2 w-full"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
onChange={handleInputChange}
defaultValue={codeFont}
/>
<div className="flex flex-row gap-2 w-full">
<div className="w-4/5">
<label className="block font-medium">Code Editor Font</label>
<input
type="text"
className="block textbox mt-2 w-full"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
onChange={handleCodeFontChange}
defaultValue={codeFont}
/>
</div>
<div className="w-1/5">
<label className="block font-medium">Font Size</label>
<input
type="number"
className="block textbox mt-2 w-full"
autoComplete="off"
autoCorrect="off"
inputMode="numeric"
onChange={handleCodeFontSizeChange}
defaultValue={codeFontSize}
/>
</div>
</div>
<div className="mt-10">
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>

View File

@@ -31,6 +31,7 @@ const GraphQLRequestPane = ({ item, collection, leftPaneWidth, onSchemaLoad, tog
: get(item, 'request.body.graphql.variables');
const { displayedTheme } = useTheme();
const [schema, setSchema] = useState(null);
const preferences = useSelector((state) => state.app.preferences);
useEffect(() => {
onSchemaLoad(schema);
@@ -71,6 +72,8 @@ const GraphQLRequestPane = ({ item, collection, leftPaneWidth, onSchemaLoad, tog
onRun={onRun}
onEdit={onQueryChange}
onClickReference={handleGqlClickReference}
font={get(preferences, 'font.codeFont', 'default')}
fontSize={get(preferences, 'font.codeFontSize')}
/>
);
}

View File

@@ -33,6 +33,7 @@ const GraphQLVariables = ({ variables, item, collection }) => {
value={variables || ''}
theme={displayedTheme}
font={get(preferences, 'font.codeFont', 'default')}
fontSize={get(preferences, 'font.codeFontSize')}
onEdit={onEdit}
mode="javascript"
onRun={onRun}

View File

@@ -4,6 +4,8 @@ const StyledWrapper = styled.div`
div.CodeMirror {
background: ${(props) => props.theme.codemirror.bg};
border: solid 1px ${(props) => props.theme.codemirror.border};
font-family: ${(props) => (props.font ? props.font : 'default')};
font-size: ${(props) => (props.fontSize ? `${props.fontSize}px` : 'inherit')};
flex: 1 1 0;
}

View File

@@ -211,6 +211,8 @@ export default class QueryEditor extends React.Component {
<StyledWrapper
className="h-full w-full flex flex-col relative"
aria-label="Query Editor"
font={this.props.font}
fontSize={this.props.fontSize}
ref={(node) => {
this._node = node;
}}

View File

@@ -1,7 +1,7 @@
import React from 'react';
import get from 'lodash/get';
import cloneDeep from 'lodash/cloneDeep';
import Tooltip from 'components/Tooltip';
import InfoTip from 'components/InfoTip';
import { IconTrash } from '@tabler/icons';
import { useDispatch } from 'react-redux';
import { useTheme } from 'providers/Theme';
@@ -175,7 +175,7 @@ const QueryParams = ({ item, collection }) => {
</button>
<div className="mb-2 title text-xs flex items-stretch">
<span>Path</span>
<Tooltip
<InfoTip
text={`
<div>
Path variables are automatically added whenever the
@@ -186,7 +186,7 @@ const QueryParams = ({ item, collection }) => {
</code>
</div>
`}
tooltipId="path-param-tooltip"
infotipId="path-param-InfoTip"
/>
</div>
<table>

View File

@@ -33,18 +33,18 @@ const Wrapper = styled.div`
top: 1px;
}
.tooltip {
.infotip {
position: relative;
display: inline-block;
cursor: pointer;
}
.tooltip:hover .tooltiptext {
.infotip:hover .infotiptext {
visibility: visible;
opacity: 1;
}
.tooltiptext {
.infotiptext {
visibility: hidden;
width: auto;
background-color: ${(props) => props.theme.requestTabs.active.bg};
@@ -62,7 +62,7 @@ const Wrapper = styled.div`
white-space: nowrap;
}
.tooltiptext::after {
.infotiptext::after {
content: '';
position: absolute;
top: 100%;

View File

@@ -74,7 +74,7 @@ const QueryUrl = ({ item, collection, handleRun }) => {
/>
<div className="flex items-center h-full mr-2 cursor-pointer" id="send-request" onClick={handleRun}>
<div
className="tooltip mx-3"
className="infotip mr-3"
onClick={(e) => {
e.stopPropagation();
if (!item.draft) return;
@@ -87,7 +87,7 @@ const QueryUrl = ({ item, collection, handleRun }) => {
size={22}
className={`${item.draft ? 'cursor-pointer' : 'cursor-default'}`}
/>
<span className="tooltiptext text-xs">
<span className="infotiptext text-xs">
Save <span className="shortcut">({saveShortcut})</span>
</span>
</div>

View File

@@ -50,6 +50,7 @@ const RequestBody = ({ item, collection }) => {
collection={collection}
theme={displayedTheme}
font={get(preferences, 'font.codeFont', 'default')}
fontSize={get(preferences, 'font.codeFontSize')}
value={bodyContent[bodyMode] || ''}
onEdit={onEdit}
onRun={onRun}

View File

@@ -47,6 +47,7 @@ const Script = ({ item, collection }) => {
value={requestScript || ''}
theme={displayedTheme}
font={get(preferences, 'font.codeFont', 'default')}
fontSize={get(preferences, 'font.codeFontSize')}
onEdit={onRequestScriptEdit}
mode="javascript"
onRun={onRun}
@@ -60,6 +61,7 @@ const Script = ({ item, collection }) => {
value={responseScript || ''}
theme={displayedTheme}
font={get(preferences, 'font.codeFont', 'default')}
fontSize={get(preferences, 'font.codeFontSize')}
onEdit={onResponseScriptEdit}
mode="javascript"
onRun={onRun}

View File

@@ -34,6 +34,7 @@ const Tests = ({ item, collection }) => {
value={tests || ''}
theme={displayedTheme}
font={get(preferences, 'font.codeFont', 'default')}
fontSize={get(preferences, 'font.codeFontSize')}
onEdit={onEdit}
mode="javascript"
onRun={onRun}

View File

@@ -6,7 +6,7 @@ import { useTheme } from 'providers/Theme';
import { addVar, updateVar, deleteVar } from 'providers/ReduxStore/slices/collections';
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import SingleLineEditor from 'components/SingleLineEditor';
import Tooltip from 'components/Tooltip';
import InfoTip from 'components/InfoTip';
import StyledWrapper from './StyledWrapper';
import toast from 'react-hot-toast';
import { variableNameRegex } from 'utils/common/regex';
@@ -83,14 +83,14 @@ const VarsTable = ({ item, collection, vars, varType }) => {
<td>
<div className="flex items-center">
<span>Value</span>
<Tooltip text="You can write any valid JS Template Literal here" tooltipId="request-var" />
<InfoTip text="You can write any valid JS Template Literal here" infotipId="request-var" />
</div>
</td>
) : (
<td>
<div className="flex items-center">
<span>Expr</span>
<Tooltip text="You can write any valid JS expression here" tooltipId="response-var" />
<InfoTip text="You can write any valid JS expression here" infotipId="response-var" />
</div>
</td>
)}

View File

@@ -2,4 +2,4 @@ import styled from 'styled-components';
const StyledWrapper = styled.div``;
export default StyledWrapper;
export default StyledWrapper;

View File

@@ -4,6 +4,7 @@ import { IconFiles, IconRun, IconEye, IconSettings } from '@tabler/icons';
import EnvironmentSelector from 'components/Environments/EnvironmentSelector';
import { addTab } from 'providers/ReduxStore/slices/tabs';
import { useDispatch } from 'react-redux';
import ToolHint from 'components/ToolHint';
import StyledWrapper from './StyledWrapper';
import JsSandboxMode from 'components/SecuritySettings/JsSandboxMode';
@@ -51,14 +52,20 @@ const CollectionToolBar = ({ collection }) => {
<span className="mr-2">
<JsSandboxMode collection={collection} />
</span>
<span className="mr-2">
<IconRun className="cursor-pointer" size={20} strokeWidth={1.5} onClick={handleRun} />
<span className="mr-3">
<ToolHint text="Runner" toolhintId="RunnnerToolhintId" place='bottom'>
<IconRun className="cursor-pointer" size={18} strokeWidth={1.5} onClick={handleRun} />
</ToolHint>
</span>
<span className="mr-3">
<IconEye className="cursor-pointer" size={18} strokeWidth={1.5} onClick={viewVariables} />
<ToolHint text="Variables" toolhintId="VariablesToolhintId">
<IconEye className="cursor-pointer" size={18} strokeWidth={1.5} onClick={viewVariables} />
</ToolHint>
</span>
<span className="mr-3">
<IconSettings className="cursor-pointer" size={18} strokeWidth={1.5} onClick={viewCollectionSettings} />
<ToolHint text="Collection Settings" toolhintId="CollectionSettingsToolhintId">
<IconSettings className="cursor-pointer" size={18} strokeWidth={1.5} onClick={viewCollectionSettings} />
</ToolHint>
</span>
<EnvironmentSelector collection={collection} />
</div>

View File

@@ -2,7 +2,7 @@ import { IconFilter, IconX } from '@tabler/icons';
import React, { useMemo } from 'react';
import { useRef } from 'react';
import { useState } from 'react';
import { Tooltip as ReactTooltip } from 'react-tooltip';
import { Tooltip as ReactInfotip } from 'react-tooltip';
const QueryResultFilter = ({ filter, onChange, mode }) => {
const inputRef = useRef(null);
@@ -19,7 +19,7 @@ const QueryResultFilter = ({ filter, onChange, mode }) => {
}
};
const tooltipText = useMemo(() => {
const infotipText = useMemo(() => {
if (mode.includes('json')) {
return 'Filter with JSONPath';
}
@@ -49,7 +49,7 @@ const QueryResultFilter = ({ filter, onChange, mode }) => {
'response-filter absolute bottom-2 w-full justify-end right-0 flex flex-row items-center gap-2 py-4 px-2 pointer-events-none'
}
>
{tooltipText && !isExpanded && <ReactTooltip anchorId={'request-filter-icon'} html={tooltipText} />}
{infotipText && !isExpanded && <ReactInfotip anchorId={'request-filter-icon'} html={infotipText} />}
<input
ref={inputRef}
type="text"

View File

@@ -81,6 +81,7 @@ const QueryResultPreview = ({
<CodeEditor
collection={collection}
font={get(preferences, 'font.codeFont', 'default')}
fontSize={get(preferences, 'font.codeFontSize')}
theme={displayedTheme}
onRun={onRun}
value={formattedData}

View File

@@ -5,7 +5,7 @@ import * as Yup from 'yup';
import { browseDirectory } from 'providers/ReduxStore/slices/collections/actions';
import { cloneCollection } from 'providers/ReduxStore/slices/collections/actions';
import toast from 'react-hot-toast';
import Tooltip from 'components/Tooltip';
import InfoTip from 'components/InfoTip';
import Modal from 'components/Modal';
const CloneCollection = ({ onClose, collection }) => {
@@ -126,9 +126,9 @@ const CloneCollection = ({ onClose, collection }) => {
<label htmlFor="collection-folder-name" className="flex items-center mt-3">
<span className="font-semibold">Folder Name</span>
<Tooltip
<InfoTip
text="This folder will be created under the selected location"
tooltipId="collection-folder-name-tooltip"
infotipId="collection-folder-name-infotip"
/>
</label>
<input

View File

@@ -53,6 +53,7 @@ const CodeView = ({ language, item }) => {
collection={collection}
value={snippet}
font={get(preferences, 'font.codeFont', 'default')}
fontSize={get(preferences, 'font.codeFontSize')}
theme={displayedTheme}
mode={lang}
/>

View File

@@ -65,7 +65,7 @@ const Wrapper = styled.div`
div.dropdown-item.delete-item {
color: ${(props) => props.theme.colors.danger};
&:hover {
background-color: ${(props) => props.theme.colors.bg.danger};
background-color: ${(props) => props.theme.colors.bg.danger} !important;
color: white;
}
}

View File

@@ -5,7 +5,7 @@ import * as Yup from 'yup';
import { browseDirectory } from 'providers/ReduxStore/slices/collections/actions';
import { createCollection } from 'providers/ReduxStore/slices/collections/actions';
import toast from 'react-hot-toast';
import Tooltip from 'components/Tooltip';
import InfoTip from 'components/InfoTip';
import Modal from 'components/Modal';
const CreateCollection = ({ onClose }) => {
@@ -119,9 +119,9 @@ const CreateCollection = ({ onClose }) => {
<label htmlFor="collection-folder-name" className="flex items-center mt-3">
<span className="font-semibold">Folder Name</span>
<Tooltip
<InfoTip
text="This folder will be created under the selected location"
tooltipId="collection-folder-name-tooltip"
infotipId="collection-folder-name-infotip"
/>
</label>
<input

View File

@@ -4,6 +4,7 @@ import StyledWrapper from './StyledWrapper';
import GitHubButton from 'react-github-btn';
import Preferences from 'components/Preferences';
import Cookies from 'components/Cookies';
import ToolHint from 'components/ToolHint';
import GoldenEdition from './GoldenEdition';
import { useState, useEffect } from 'react';
@@ -95,28 +96,30 @@ const Sidebar = () => {
<div className="footer flex px-1 py-2 absolute bottom-0 left-0 right-0 items-center select-none">
<div className="flex items-center ml-1 text-xs ">
<a
title="Preferences"
className="mr-2 cursor-pointer hover:text-gray-700"
onClick={() => dispatch(showPreferences(true))}
>
<IconSettings size={18} strokeWidth={1.5} />
<a className="mr-2 cursor-pointer" onClick={() => dispatch(showPreferences(true))}>
<ToolHint text="Preferences" toolhintId="Preferences" effect='float' place='top-start' offset={8}>
<IconSettings size={18} strokeWidth={1.5} />
</ToolHint>
</a>
<a
title="Cookies"
className="mr-2 cursor-pointer hover:text-gray-700"
className="mr-2 cursor-pointer"
onClick={() => setCookiesOpen(true)}
>
<IconCookie size={18} strokeWidth={1.5} />
<ToolHint text="Cookies" toolhintId="Cookies" offset={8}>
<IconCookie size={18} strokeWidth={1.5} />
</ToolHint>
</a>
<a
title="Golden Edition"
className="mr-2 cursor-pointer hover:text-gray-700"
className="mr-2 cursor-pointer"
onClick={() => setGoldenEditonOpen(true)}
>
<IconHeart size={18} strokeWidth={1.5} />
<ToolHint text="Golden Edition" toolhintId="Golden Edition" offset={8} >
<IconHeart size={18} strokeWidth={1.5} />
</ToolHint>
</a>
<a>
<Notifications />
</a>
<Notifications />
</div>
<div className="pl-1" style={{ position: 'relative', top: '3px' }}>
{/* This will get moved to home page */}
@@ -126,10 +129,10 @@ const Sidebar = () => {
data-show-count="true"
aria-label="Star usebruno/bruno on GitHub"
>
Star
Star
</GitHubButton> */}
</div>
<div className="flex flex-grow items-center justify-end text-xs mr-2">v1.26.2</div>
<div className="flex flex-grow items-center justify-end text-xs mr-2">v1.27.0</div>
</div>
</div>
</div>
@@ -137,7 +140,7 @@ const Sidebar = () => {
<div className="absolute drag-sidebar h-full" onMouseDown={handleDragbarMouseDown}>
<div className="drag-request-border" />
</div>
</StyledWrapper>
</StyledWrapper >
);
};

View File

@@ -0,0 +1,8 @@
import styled from 'styled-components';
const Wrapper = styled.div`
background-color: ${(props) => props.theme.sidebar.badge};
color: ${(props) => props.theme.text};
`;
export default Wrapper;

View File

@@ -0,0 +1,47 @@
import React from 'react';
import { Tooltip as ReactToolHint } from 'react-tooltip';
import StyledWrapper from './StyledWrapper';
import { useTheme } from 'providers/Theme';
const ToolHint = ({
text,
toolhintId,
children,
tooltipStyle = {},
place = 'top',
offset,
theme = null
}) => {
const { theme: contextTheme } = useTheme();
const appliedTheme = theme || contextTheme;
const toolhintBackgroundColor = appliedTheme?.sidebar.badge.bg || 'black';
const toolhintTextColor = appliedTheme?.text || 'white';
const combinedToolhintStyle = {
...tooltipStyle,
fontSize: '0.75rem',
padding: '0.25rem 0.5rem',
backgroundColor: toolhintBackgroundColor,
color: toolhintTextColor
};
return (
<>
<span id={toolhintId}>{children}</span>
<StyledWrapper theme={appliedTheme}>
<ReactToolHint
anchorId={toolhintId}
html={text}
className="toolhint"
offset={offset}
place={place}
noArrow={true}
style={combinedToolhintStyle}
/>
</StyledWrapper>
</>
);
};
export default ToolHint;

View File

@@ -9,6 +9,7 @@ import StyledWrapper from './StyledWrapper';
import 'codemirror/theme/material.css';
import 'codemirror/theme/monokai.css';
import 'codemirror/addon/scroll/simplescrollbars.css';
import CtrlTabPopup from 'components/CtrlTabPopup';
const SERVER_RENDERED = typeof navigator === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true;
if (!SERVER_RENDERED) {
@@ -60,6 +61,7 @@ export default function Main() {
return (
<div>
<StyledWrapper className={className}>
<CtrlTabPopup />
<Sidebar />
<section className="flex flex-grow flex-col overflow-auto">
{showHomePage ? (

View File

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

View File

@@ -9,7 +9,7 @@ import NetworkError from 'components/ResponsePane/NetworkError';
import NewRequest from 'components/Sidebar/NewRequest';
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import { findCollectionByUid, findItemInCollection } from 'utils/collections';
import { closeTabs } from 'providers/ReduxStore/slices/tabs';
import { CTRL_TAB_ACTIONS, closeTabs, switchTab } from 'providers/ReduxStore/slices/tabs';
export const HotkeysContext = React.createContext();
@@ -154,7 +154,83 @@ export const HotkeysProvider = (props) => {
};
}, [activeTabUid]);
// close all tabs
useEffect(() => {
const shortcuts = ['mod+tab'];
const shiftedShortcuts = ['shift+mod+tab'];
const bindCtrlTabShortcut = () => {
Mousetrap.bind(shortcuts, () => {
dispatch(ctrlTab(CTRL_TAB_ACTIONS.ENTER));
Mousetrap.unbind(shortcuts);
Mousetrap.bind(shortcuts, () => {
dispatch(ctrlTab(CTRL_TAB_ACTIONS.PLUS));
return false; // this stops the event bubbling
});
Mousetrap.bind(shiftedShortcuts, () => {
dispatch(ctrlTab(CTRL_TAB_ACTIONS.MINUS));
return false; // this stops the event bubbling
});
Mousetrap.bind(
['mod'],
() => {
dispatch(ctrlTab(CTRL_TAB_ACTIONS.SWITCH));
Mousetrap.unbind(['mod'], 'keyup');
Mousetrap.unbind(shortcuts);
Mousetrap.unbind(shiftedShortcuts);
bindCtrlTabShortcut();
return false; // this stops the event bubbling
},
'keyup'
);
return false; // this stops the event bubbling
});
};
bindCtrlTabShortcut();
return () => {
Mousetrap.unbind(shortcuts);
};
}, []);
// Switch to the previous tab
useEffect(() => {
Mousetrap.bind(['command+pageup', 'ctrl+pageup'], (e) => {
dispatch(
switchTab({
direction: 'pageup'
})
);
return false; // this stops the event bubbling
});
return () => {
Mousetrap.unbind(['command+pageup', 'ctrl+pageup']);
};
}, [dispatch]);
// Switch to the next tab
useEffect(() => {
Mousetrap.bind(['command+pagedown', 'ctrl+pagedown'], (e) => {
dispatch(
switchTab({
direction: 'pagedown'
})
);
return false; // this stops the event bubbling
});
return () => {
Mousetrap.unbind(['command+pagedown', 'ctrl+pagedown']);
};
}, [dispatch]);
// Close all tabs
useEffect(() => {
Mousetrap.bind(['command+shift+w', 'ctrl+shift+w'], (e) => {
const activeTab = find(tabs, (t) => t.uid === activeTabUid);

View File

@@ -7,13 +7,48 @@ import last from 'lodash/last';
const initialState = {
tabs: [],
activeTabUid: null
activeTabUid: null,
ctrlTabStack: [],
ctrlTabIndex: null
};
const tabTypeAlreadyExists = (tabs, collectionUid, type) => {
return find(tabs, (tab) => tab.collectionUid === collectionUid && tab.type === type);
};
const uidToTab = (state, uid) => find(state.tabs, (tab) => tab.uid === uid);
const focusTabWithStack = (state, uid) => {
if (state.activeTabUid === uid) {
return;
}
if (state.activeTabUid) {
const previousTab = uidToTab(state, state.activeTabUid);
const currentTab = uidToTab(state, uid);
state.ctrlTabStack = [
...filter(state.ctrlTabStack, (tab) => tab.uid !== state.activeTabUid && tab.uid !== uid),
...(previousTab ? [previousTab] : []), // if previousTab is undefined, it means the tab was closed while focused
currentTab
];
}
state.activeTabUid = uid;
};
const removeClosedTabs = (state, filterFunction) => {
state.tabs = filter(state.tabs, filterFunction);
state.ctrlTabStack = filter(state.ctrlTabStack, filterFunction);
if (state.ctrlTabStack.length < 2) {
state.ctrlTabIndex = null;
}
};
export const CTRL_TAB_ACTIONS = Object.freeze({
ENTER: 'enter',
PLUS: 'plus',
MINUS: 'minus',
SWITCH: 'switch'
});
export const tabsSlice = createSlice({
name: 'tabs',
initialState,
@@ -29,7 +64,7 @@ export const tabsSlice = createSlice({
) {
const tab = tabTypeAlreadyExists(state.tabs, action.payload.collectionUid, action.payload.type);
if (tab) {
state.activeTabUid = tab.uid;
focusTabWithStack(state, tab.uid);
return;
}
}
@@ -43,10 +78,58 @@ export const tabsSlice = createSlice({
type: action.payload.type || 'request',
...(action.payload.uid ? { folderUid: action.payload.uid } : {})
});
state.activeTabUid = action.payload.uid;
focusTabWithStack(state, action.payload.uid);
},
focusTab: (state, action) => {
state.activeTabUid = action.payload.uid;
focusTabWithStack(state, action.payload.uid);
},
focusCtrlTab: (state, action) => {
focusTabWithStack(state, action.payload.uid);
state.ctrlTabIndex = null;
},
ctrlTab: (state, action) => {
if (state.ctrlTabStack.length < 2) {
return;
}
switch (action.payload) {
case CTRL_TAB_ACTIONS.ENTER:
state.ctrlTabIndex = -2;
return;
case CTRL_TAB_ACTIONS.PLUS:
state.ctrlTabIndex = (state.ctrlTabIndex - 1) % state.ctrlTabStack.length;
return;
case CTRL_TAB_ACTIONS.MINUS:
state.ctrlTabIndex = (state.ctrlTabIndex + 1) % state.ctrlTabStack.length;
return;
case CTRL_TAB_ACTIONS.SWITCH:
if (state.ctrlTabIndex === null) {
// if already switched (eg, from click), do nothing
return;
}
focusTabWithStack(state, state.ctrlTabStack.at(state.ctrlTabIndex).uid);
state.ctrlTabIndex = null;
return;
}
},
switchTab: (state, action) => {
if (!state.tabs || !state.tabs.length) {
state.activeTabUid = null;
return;
}
const direction = action.payload.direction;
const activeTabIndex = state.tabs.findIndex((t) => t.uid === state.activeTabUid);
let toBeActivatedTabIndex = 0;
if (direction == 'pageup') {
toBeActivatedTabIndex = (activeTabIndex - 1 + state.tabs.length) % state.tabs.length;
} else if (direction == 'pagedown') {
toBeActivatedTabIndex = (activeTabIndex + 1) % state.tabs.length;
}
state.activeTabUid = state.tabs[toBeActivatedTabIndex].uid;
},
updateRequestPaneTabWidth: (state, action) => {
const tab = find(state.tabs, (t) => t.uid === action.payload.uid);
@@ -74,7 +157,7 @@ export const tabsSlice = createSlice({
const tabUids = action.payload.tabUids || [];
// remove the tabs from the state
state.tabs = filter(state.tabs, (t) => !tabUids.includes(t.uid));
removeClosedTabs(state, (t) => !tabUids.includes(t.uid));
if (activeTab && state.tabs.length) {
const { collectionUid } = activeTab;
@@ -89,9 +172,9 @@ export const tabsSlice = createSlice({
// if there are sibling tabs, set the active tab to the last sibling tab
// otherwise, set the active tab to the last tab in the list
if (siblingTabs && siblingTabs.length) {
state.activeTabUid = last(siblingTabs).uid;
focusTabWithStack(state, last(siblingTabs).uid);
} else {
state.activeTabUid = last(state.tabs).uid;
focusTabWithStack(state, last(state.tabs).uid);
}
}
}
@@ -102,15 +185,25 @@ export const tabsSlice = createSlice({
},
closeAllCollectionTabs: (state, action) => {
const collectionUid = action.payload.collectionUid;
state.tabs = filter(state.tabs, (t) => t.collectionUid !== collectionUid);
removeClosedTabs(state, (t) => t.collectionUid !== collectionUid);
state.activeTabUid = null;
}
}
});
export const selectCtrlTabAction = (uid) => (dispatch) => {
dispatch(
tabsSlice.actions.focusCtrlTab({
uid
})
);
};
export const {
addTab,
focusTab,
ctrlTab,
switchTab,
updateRequestPaneTabWidth,
updateRequestPaneTab,
updateResponsePaneTab,

View File

@@ -174,7 +174,11 @@ const darkTheme = {
opacity: 0.2
}
},
ctrlTabPopup: {
bg: '#1f2937',
text: '#ccc',
highlightBg: '#374151'
},
button: {
secondary: {
color: 'rgb(204, 204, 204)',

View File

@@ -178,7 +178,11 @@ const lightTheme = {
opacity: 0.4
}
},
ctrlTabPopup: {
bg: '#fff8f7',
text: 'rgb(52, 52, 52)',
highlightBg: '#ffb63154'
},
button: {
secondary: {
color: '#212529',

View File

@@ -149,7 +149,9 @@ export const relativeDate = (dateString) => {
};
export const humanizeDate = (dateString) => {
const date = new Date(dateString);
// See this discussion for why .split is necessary
// https://stackoverflow.com/questions/7556591/is-the-javascript-date-object-always-one-day-off
const date = new Date(dateString.split('-'));
return date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',

View File

@@ -1,6 +1,6 @@
const { describe, it, expect } = require('@jest/globals');
import { normalizeFileName, startsWith } from './index';
import { normalizeFileName, startsWith, humanizeDate, relativeDate } from './index';
describe('common utils', () => {
describe('normalizeFileName', () => {
@@ -49,4 +49,50 @@ describe('common utils', () => {
expect(startsWith('foo', 'foo')).toBe(true);
});
});
describe('humanizeDate', () => {
it('should return a date string in the en-US locale', () => {
expect(humanizeDate('2024-03-17')).toBe('March 17, 2024');
});
it('should return invalid date if the date is invalid', () => {
expect(humanizeDate('9999-99-99')).toBe('Invalid Date');
});
});
describe('relativeDate', () => {
it('should return few seconds ago', () => {
expect(relativeDate(new Date())).toBe('Few seconds ago');
});
it('should return minutes ago', () => {
let date = new Date();
date.setMinutes(date.getMinutes() - 30);
expect(relativeDate(date)).toBe('30 minutes ago');
});
it('should return hours ago', () => {
let date = new Date();
date.setHours(date.getHours() - 10);
expect(relativeDate(date)).toBe('10 hours ago');
});
it('should return days ago', () => {
let date = new Date();
date.setDate(date.getDate() - 5);
expect(relativeDate(date)).toBe('5 days ago');
});
it('should return weeks ago', () => {
let date = new Date();
date.setDate(date.getDate() - 8);
expect(relativeDate(date)).toBe('1 week ago');
});
it('should return months ago', () => {
let date = new Date();
date.setDate(date.getDate() - 60);
expect(relativeDate(date)).toBe('2 months ago');
});
});
});

View File

@@ -11,3 +11,24 @@ export const isItemAFolder = (item) => {
export const itemIsOpenedInTabs = (item, tabs) => {
return find(tabs, (t) => t.uid === item.uid);
};
export const isSpecialTab = ({ type }) => {
if (!type) {
return false;
}
return ['variables', 'collection-settings', 'collection-runner'].includes(type);
};
export const getSpecialTabName = (type) => {
switch (type) {
case 'variables':
return 'Variables';
case 'collection-settings':
return 'Settings';
case 'collection-runner':
return 'Runner';
default:
console.error('Unknown special tab type', type);
return type;
}
};

View File

@@ -18,16 +18,17 @@ const hasLength = (str) => {
};
export const parseQueryParams = (query) => {
if (!query || !query.length) {
try {
if (!query || !query.length) {
return [];
}
return Array.from(new URLSearchParams(query.split('#')[0]).entries())
.map(([name, value]) => ({ name, value }));
} catch (error) {
console.error('Error parsing query params:', error);
return [];
}
let params = query.split('&').map((param) => {
let [name, value = ''] = param.split('=');
return { name, value };
});
return filter(params, (p) => hasLength(p.name));
};
export const parsePathParams = (url) => {

View File

@@ -49,6 +49,23 @@ describe('Url Utils - parseQueryParams', () => {
{ name: 'b', value: '2' }
]);
});
it('should parse query with "=" character - case 9', () => {
const params = parseQueryParams('a=1&b={color=red,size=large}&c=3');
expect(params).toEqual([
{ name: 'a', value: '1' },
{ name: 'b', value: '{color=red,size=large}' },
{ name: 'c', value: '3' }
]);
});
it('should parse query with fragment - case 10', () => {
const params = parseQueryParams('a=1&b=2#I-AM-FRAGMENT');
expect(params).toEqual([
{ name: 'a', value: '1' },
{ name: 'b', value: '2' }
]);
});
});
describe('Url Utils - parsePathParams', () => {

View File

@@ -1,5 +1,5 @@
{
"version": "v1.26.2",
"version": "v1.27.0",
"name": "bruno",
"description": "Opensource API Client for Exploring and Testing APIs",
"homepage": "https://www.usebruno.com",

View File

@@ -3,7 +3,7 @@ const path = require('path');
const isDev = require('electron-is-dev');
if (isDev) {
if (!fs.existsSync('./src/sandbox/bundle-browser-rollup.js')) {
if(!fs.existsSync(path.join(__dirname, '../../bruno-js/src/sandbox/bundle-browser-rollup.js'))) {
console.log('JS Sandbox libraries have not been bundled yet');
console.log('Please run the below command \nnpm run sandbox:bundle-libraries --workspace=packages/bruno-js');
throw new Error('JS Sandbox libraries have not been bundled yet');

View File

@@ -23,7 +23,8 @@ const defaultPreferences = {
timeout: 0
},
font: {
codeFont: 'default'
codeFont: 'default',
codeFontSize: 14
},
proxy: {
enabled: false,
@@ -54,7 +55,8 @@ const preferencesSchema = Yup.object().shape({
timeout: Yup.number()
}),
font: Yup.object().shape({
codeFont: Yup.string().nullable()
codeFont: Yup.string().nullable(),
codeFontSize: Yup.number().min(1).max(32).nullable()
}),
proxy: Yup.object({
enabled: Yup.boolean(),

View File

@@ -25,13 +25,11 @@ auth:bearer {
body:json {
{
"hello": 990531470713421825
}
}
body:text {
{
"hello": 990531470713421825
"hello": 990531470713421825,
"decimal": 1.0,
"decimal2": 1.00,
"decimal3": 1.00200,
"decimal4": 0.00
}
}
@@ -39,6 +37,6 @@ assert {
res.status: eq 200
}
script:pre-request {
bru.setVar("foo", "foo-world-2");
tests {
// todo: add tests once lossless json echo server is ready
}

View File

@@ -43,14 +43,34 @@ Bruno is offline-only. There are no plans to add cloud-sync to Bruno, ever. We v
![bruno](assets/images/landing-2.png) <br /><br />
### Golden Edition ✨
## Golden Edition ✨
Majority of our features are free and open source.
We strive to strike a harmonious balance between [open-source principles and sustainability](https://github.com/usebruno/bruno/discussions/269)
You can buy the [Golden Edition](https://www.usebruno.com/pricing) for a one-time payment of **$19**! <br/>
### Installation
## Table of Contents
- [Installation](#installation)
- [Features](#features)
- [Run across multiple platforms 🖥️](#run-across-multiple-platforms-%EF%B8%8F)
- [Collaborate via Git 👩‍💻🧑‍💻](#collaborate-via-git-)
- [Sponsors](#sponsors)
- [Gold Sponsors](#gold-sponsors)
- [Silver Sponsors](#silver-sponsors)
- [Bronze Sponsors](#bronze-sponsors)
- [Important Links 📌](#important-links-)
- [Showcase 🎥](#showcase-)
- [Support ❤️](#support-%EF%B8%8F)
- [Share Testimonials 📣](#share-testimonials-)
- [Publishing to New Package Managers](#publishing-to-new-package-managers)
- [Stay in touch 🌐](#stay-in-touch-)
- [Trademark](#trademark)
- [Contribute 👩‍💻🧑‍💻](#contribute-)
- [Authors](#authors)
- [License 📄](#license-)
## Installation
Bruno is available as binary download [on our website](https://www.usebruno.com/downloads) for Mac, Windows and Linux.
@@ -86,6 +106,8 @@ sudo apt update
sudo apt install bruno
```
## Features
### Run across multiple platforms 🖥️
![bruno](assets/images/run-anywhere.png) <br /><br />
@@ -96,7 +118,7 @@ Or any version control system of your choice
![bruno](assets/images/version-control.png) <br /><br />
### Sponsors
## Sponsors
#### Gold Sponsors
@@ -112,7 +134,7 @@ Or any version control system of your choice
<img src="assets/images/sponsors/zuplo.png" width="120"/>
</a>
### Important Links 📌
## Important Links 📌
- [Our Long Term Vision](https://github.com/usebruno/bruno/discussions/269)
- [Roadmap](https://github.com/usebruno/bruno/discussions/384)
@@ -123,32 +145,32 @@ Or any version control system of your choice
- [Download](https://www.usebruno.com/downloads)
- [GitHub Sponsors](https://github.com/sponsors/helloanoop).
### Showcase 🎥
## Showcase 🎥
- [Testimonials](https://github.com/usebruno/bruno/discussions/343)
- [Knowledge Hub](https://github.com/usebruno/bruno/discussions/386)
- [Scriptmania](https://github.com/usebruno/bruno/discussions/385)
### Support ❤️
## Support ❤️
If you like Bruno and want to support our opensource work, consider sponsoring us via [GitHub Sponsors](https://github.com/sponsors/helloanoop).
### Share Testimonials 📣
## Share Testimonials 📣
If Bruno has helped you at work and your teams, please don't forget to share your [testimonials on our GitHub discussion](https://github.com/usebruno/bruno/discussions/343)
### Publishing to New Package Managers
## Publishing to New Package Managers
Please see [here](publishing.md) for more information.
### Stay in touch 🌐
## Stay in touch 🌐
[𝕏 (Twitter)](https://twitter.com/use_bruno) <br />
[Website](https://www.usebruno.com) <br />
[Discord](https://discord.com/invite/KgcZUncpjq) <br />
[LinkedIn](https://www.linkedin.com/company/usebruno)
### Trademark
## Trademark
**Name**
@@ -158,13 +180,13 @@ Please see [here](publishing.md) for more information.
The logo is sourced from [OpenMoji](https://openmoji.org/library/emoji-1F436/). License: CC [BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/)
### Contribute 👩‍💻🧑‍💻
## Contribute 👩‍💻🧑‍💻
I am happy that you are looking to improve bruno. Please check out the [contributing guide](contributing.md)
Even if you are not able to make contributions via code, please don't hesitate to file bugs and feature requests that needs to be implemented to solve your use case.
### Authors
## Authors
<div align="center">
<a href="https://github.com/usebruno/bruno/graphs/contributors">
@@ -172,6 +194,6 @@ Even if you are not able to make contributions via code, please don't hesitate t
</a>
</div>
### License 📄
## License 📄
[MIT](license.md)