mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-05 02:18:32 +00:00
Compare commits
16 Commits
fix/json-s
...
feat/ctrl-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fd92df7bf9 | ||
|
|
93080de2a8 | ||
|
|
36ef38be6a | ||
|
|
4726f5008e | ||
|
|
091b02c2c3 | ||
|
|
600940226c | ||
|
|
ee7f886c03 | ||
|
|
682c7bd1b1 | ||
|
|
e1aa5b4eb5 | ||
|
|
6bd9d4c480 | ||
|
|
a38d09a117 | ||
|
|
82985d1b43 | ||
|
|
d34d3a45ff | ||
|
|
25ccb38202 | ||
|
|
5f6a5f59b1 | ||
|
|
9e5148f032 |
@@ -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.
|
||||
|
||||
 <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
|
||||
|
||||
|
||||
15
packages/bruno-app/jest.config.js
Normal file
15
packages/bruno-app/jest.config.js
Normal 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;
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -95,6 +95,7 @@ const CollectionSettings = ({ collection }) => {
|
||||
case 'clientCert': {
|
||||
return (
|
||||
<ClientCertSettings
|
||||
root={collection.pathname}
|
||||
clientCertConfig={clientCertConfig}
|
||||
onUpdate={onClientCertSettingsUpdate}
|
||||
onRemove={onClientCertSettingsRemove}
|
||||
|
||||
@@ -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;
|
||||
81
packages/bruno-app/src/components/CtrlTabPopup/index.js
Normal file
81
packages/bruno-app/src/components/CtrlTabPopup/index.js
Normal 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>
|
||||
);
|
||||
}
|
||||
59
packages/bruno-app/src/components/CtrlTabPopup/index.spec.js
Normal file
59
packages/bruno-app/src/components/CtrlTabPopup/index.spec.js
Normal 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' }
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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;
|
||||
@@ -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'}
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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%;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -2,4 +2,4 @@ import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div``;
|
||||
|
||||
export default StyledWrapper;
|
||||
export default StyledWrapper;
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 >
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
47
packages/bruno-app/src/components/ToolHint/index.js
Normal file
47
packages/bruno-app/src/components/ToolHint/index.js
Normal 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;
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -60,7 +60,7 @@ const trackStart = () => {
|
||||
event: 'start',
|
||||
properties: {
|
||||
os: platformLib.os.family,
|
||||
version: '1.26.2'
|
||||
version: '1.27.0'
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -174,7 +174,11 @@ const darkTheme = {
|
||||
opacity: 0.2
|
||||
}
|
||||
},
|
||||
|
||||
ctrlTabPopup: {
|
||||
bg: '#1f2937',
|
||||
text: '#ccc',
|
||||
highlightBg: '#374151'
|
||||
},
|
||||
button: {
|
||||
secondary: {
|
||||
color: 'rgb(204, 204, 204)',
|
||||
|
||||
@@ -178,7 +178,11 @@ const lightTheme = {
|
||||
opacity: 0.4
|
||||
}
|
||||
},
|
||||
|
||||
ctrlTabPopup: {
|
||||
bg: '#fff8f7',
|
||||
text: 'rgb(52, 52, 52)',
|
||||
highlightBg: '#ffb63154'
|
||||
},
|
||||
button: {
|
||||
secondary: {
|
||||
color: '#212529',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
48
readme.md
48
readme.md
@@ -43,14 +43,34 @@ Bruno is offline-only. There are no plans to add cloud-sync to Bruno, ever. We v
|
||||
|
||||
 <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 🖥️
|
||||
|
||||
 <br /><br />
|
||||
@@ -96,7 +118,7 @@ Or any version control system of your choice
|
||||
|
||||
 <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)
|
||||
|
||||
Reference in New Issue
Block a user