Compare commits

...

12 Commits

Author SHA1 Message Date
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
51 changed files with 468 additions and 142 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

@@ -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,6 +67,7 @@ if (!SERVER_RENDERED) {
'bru.setVar(key,value)',
'bru.deleteVar(key)',
'bru.setNextRequest(requestName)',
'req.disableParsingResponseJson()',
'bru.getRequestVar(key)',
'bru.sleep(ms)'
];
@@ -332,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

@@ -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

@@ -17,8 +17,6 @@ const Timeline = ({ request, response }) => {
});
});
let requestData = safeStringifyJSON(request.data);
return (
<StyledWrapper className="pb-4 w-full">
<div>
@@ -33,9 +31,10 @@ const Timeline = ({ request, response }) => {
);
})}
{requestData ? (
{request.data ? (
<pre className="line request">
<span className="arrow">{'>'}</span> data {requestData}
<span className="arrow">{'>'}</span> data{' '}
<pre className="text-sm flex flex-wrap whitespace-break-spaces">{request.data}</pre>
</pre>
) : null}
</div>

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

@@ -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

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

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

@@ -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

@@ -272,7 +272,7 @@ const configureRequest = async (
return axiosInstance;
};
const parseDataFromResponse = (response) => {
const parseDataFromResponse = (response, disableParsingResponseJson = false) => {
// Parse the charset from content type: https://stackoverflow.com/a/33192813
const charsetMatch = /charset=([^()<>@,;:"/[\]?.=\s]*)/i.exec(response.headers['content-type'] || '');
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/exec#using_exec_with_regexp_literals
@@ -290,7 +290,9 @@ const parseDataFromResponse = (response) => {
// Filter out ZWNBSP character
// https://gist.github.com/antic183/619f42b559b78028d1fe9e7ae8a1352d
data = data.replace(/^\uFEFF/, '');
data = JSON.parse(data);
if(!disableParsingResponseJson) {
data = JSON.parse(data);
}
} catch {}
return { data, dataBuffer };
@@ -540,7 +542,7 @@ const registerNetworkIpc = (mainWindow) => {
// Continue with the rest of the request lifecycle - post response vars, script, assertions, tests
const { data, dataBuffer } = parseDataFromResponse(response);
const { data, dataBuffer } = parseDataFromResponse(response, request.__brunoDisableParsingResponseJson);
response.data = data;
response.responseTime = responseTime;
@@ -701,7 +703,7 @@ const registerNetworkIpc = (mainWindow) => {
}
}
const { data } = parseDataFromResponse(response);
const { data } = parseDataFromResponse(response, request.__brunoDisableParsingResponseJson);
response.data = data;
await runPostResponse(
@@ -969,7 +971,7 @@ const registerNetworkIpc = (mainWindow) => {
response = await axiosInstance(request);
timeEnd = Date.now();
const { data, dataBuffer } = parseDataFromResponse(response);
const { data, dataBuffer } = parseDataFromResponse(response, request.__brunoDisableParsingResponseJson);
response.data = data;
response.responseTime = response.headers.get('request-duration');

View File

@@ -59,14 +59,6 @@ const interpolateVars = (request, envVars = {}, runtimeVariables = {}, processEn
const contentType = getContentType(request.headers);
if (contentType.includes('json')) {
if (typeof request.data === 'object') {
try {
let parsed = JSON.stringify(request.data);
parsed = _interpolate(parsed);
request.data = JSON.parse(parsed);
} catch (err) {}
}
if (typeof request.data === 'string') {
if (request.data.length) {
request.data = _interpolate(request.data);

View File

@@ -1,7 +1,6 @@
const os = require('os');
const { get, each, filter, extend, compact } = require('lodash');
const decomment = require('decomment');
var JSONbig = require('json-bigint');
const FormData = require('form-data');
const fs = require('fs');
const path = require('path');
@@ -342,16 +341,10 @@ const prepareRequest = (item, collection) => {
if (!contentTypeDefined) {
axiosRequest.headers['content-type'] = 'application/json';
}
let jsonBody;
try {
jsonBody = decomment(request?.body?.json);
axiosRequest.data = decomment(request?.body?.json);
} catch (error) {
jsonBody = request?.body?.json;
}
try {
axiosRequest.data = JSONbig.parse(jsonBody);
} catch (error) {
axiosRequest.data = jsonBody;
axiosRequest.data = request?.body?.json;
}
}

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

@@ -6,7 +6,7 @@ describe('prepare-request: prepareRequest', () => {
describe('Decomments request body', () => {
it('If request body is valid JSON', async () => {
const body = { mode: 'json', json: '{\n"test": "{{someVar}}" // comment\n}' };
const expected = { test: '{{someVar}}' };
const expected = '{\n"test": "{{someVar}}" \n}';
const result = prepareRequest({ request: { body } }, {});
expect(result.data).toEqual(expected);
});

View File

@@ -1,11 +1,34 @@
class BrunoRequest {
/**
* The following properties are available as shorthand:
* - req.url
* - req.method
* - req.headers
* - req.timeout
* - req.body
*
* Above shorthands are useful for accessing the request properties directly in the scripts
* It must be noted that the user cannot set these properties directly.
* They should use the respective setter methods to set these properties.
*/
constructor(req) {
this.req = req;
this.url = req.url;
this.method = req.method;
this.headers = req.headers;
this.body = req.data;
this.timeout = req.timeout;
/**
* We automatically parse the JSON body if the content type is JSON
* This is to make it easier for the user to access the body directly
*
* It must be noted that the request data is always a string and is what gets sent over the network
* If the user wants to access the raw data, they can use getBody({raw: true}) method
*/
const isJson = this.hasJSONContentType(this.req.headers);
if (isJson) {
this.body = this.__safeParseJSON(req.data);
}
}
getUrl() {
@@ -13,6 +36,7 @@ class BrunoRequest {
}
setUrl(url) {
this.url = url;
this.req.url = url;
}
@@ -37,6 +61,7 @@ class BrunoRequest {
}
setMethod(method) {
this.method = method;
this.req.method = method;
}
@@ -45,6 +70,7 @@ class BrunoRequest {
}
setHeaders(headers) {
this.headers = headers;
this.req.headers = headers;
}
@@ -53,15 +79,60 @@ class BrunoRequest {
}
setHeader(name, value) {
this.headers[name] = value;
this.req.headers[name] = value;
}
getBody() {
hasJSONContentType(headers) {
const contentType = headers?.['Content-Type'] || headers?.['content-type'] || '';
return contentType.includes('json');
}
/**
* Get the body of the request
*
* We automatically parse and return the JSON body if the content type is JSON
* If the user wants the raw body, they can pass the raw option as true
*/
getBody(options = {}) {
if (options.raw) {
return this.req.data;
}
const isJson = this.hasJSONContentType(this.req.headers);
if (isJson) {
return this.__safeParseJSON(this.req.data);
}
return this.req.data;
}
setBody(data) {
/**
* If the content type is JSON and if the data is an object
* - We set the body property as the object itself
* - We set the request data as the stringified JSON as it is what gets sent over the network
* Otherwise
* - We set the request data as the data itself
* - We set the body property as the data itself
*
* If the user wants to override this behavior, they can pass the raw option as true
*/
setBody(data, options = {}) {
if (options.raw) {
this.req.data = data;
this.body = data;
return;
}
const isJson = this.hasJSONContentType(this.req.headers);
if (isJson && this.__isObject(data)) {
this.body = data;
this.req.data = this.__safeStringifyJSON(data);
return;
}
this.req.data = data;
this.body = data;
}
setMaxRedirects(maxRedirects) {
@@ -73,8 +144,34 @@ class BrunoRequest {
}
setTimeout(timeout) {
this.timeout = timeout;
this.req.timeout = timeout;
}
__safeParseJSON(str) {
try {
return JSON.parse(str);
} catch (e) {
return str;
}
}
__safeStringifyJSON(obj) {
try {
return JSON.stringify(obj);
} catch (e) {
return obj;
}
}
__isObject(obj) {
return obj !== null && typeof obj === 'object';
}
disableParsingResponseJson() {
this.req.__brunoDisableParsingResponseJson = true;
}
}
module.exports = BrunoRequest;

View File

@@ -0,0 +1,42 @@
meta {
name: echo bigint
type: http
seq: 6
}
post {
url: {{host}}/api/echo/json
body: json
auth: none
}
headers {
foo: bar
}
auth:basic {
username: asd
password: j
}
auth:bearer {
token:
}
body:json {
{
"hello": 990531470713421825,
"decimal": 1.0,
"decimal2": 1.00,
"decimal3": 1.00200,
"decimal4": 0.00
}
}
assert {
res.status: eq 200
}
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)