Merge remote-tracking branch 'upstream/main' into feat/oauth2-improvements

This commit is contained in:
lohxt1
2025-03-20 19:38:54 +05:30
27 changed files with 1192 additions and 624 deletions

View File

@@ -1,6 +1,7 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
width: 100%;
.path-display {
background: ${(props) => props.theme.requestTabPanel.url.bg};
border-radius: 4px;
@@ -23,14 +24,14 @@ const StyledWrapper = styled.div`
}
.filename, .file-extension {
.name-container, .file-extension {
color: ${(props) => props.theme.colors.text.yellow};
}
.separator {
color: ${(props) => props.theme.text};
opacity: 0.6;
margin: 0 1px;
margin: 0 2px;
}
}
`;

View File

@@ -1,63 +1,22 @@
import React from 'react';
import { IconEdit, IconFolder, IconFile } from '@tabler/icons';
import { IconFolder, IconFile } from '@tabler/icons';
import path from 'utils/common/path';
import StyledWrapper from './StyledWrapper';
const PathDisplay = ({
collection,
item,
filename,
extension = '.bru',
showExtension = true,
toggleEditingFilename,
showDirectory = false
baseName = '',
iconType = 'file'
}) => {
const relativePath = item?.pathname && path.relative(collection?.pathname, showDirectory ? path.dirname(item?.pathname) : item?.pathname);
const pathSegments = relativePath?.split(path.sep).filter(Boolean);
return (
<StyledWrapper>
<div className="mt-4">
<div className="flex items-center justify-between mb-2">
<label className="block font-medium">Location</label>
<IconEdit
className="cursor-pointer opacity-50 hover:opacity-80"
size={16}
strokeWidth={1.5}
onClick={() => toggleEditingFilename(true)}
/>
</div>
<div className="path-display">
<div className="path-layout flex">
<div className="icon-column flex">
{showExtension ? <IconFile size={16} /> : <IconFolder size={16} />}
</div>
<div className="path-container flex font-mono items-center">
<div className="path-segment collection-segment">
{collection?.name}
</div>
{pathSegments?.length > 0 && pathSegments?.map((segment, index) => (
<React.Fragment key={index}>
<span className="separator">/</span>
<div className="path-segment">
{segment}
</div>
</React.Fragment>
))}
{collection && (
<span className="separator">/</span>
)}
<span className="filename">
{filename}
{showExtension && filename?.length ? (
<span className="file-extension">{extension}</span>
) : null}
</span>
</div>
<div className="path-display mt-2">
<div className="path-layout flex font-mono">
<div className="icon-column flex">
{iconType === 'file' ? <IconFile size={16} /> : <IconFolder size={16} />}
</div>
<span className="name-container">
{baseName}
</span>
</div>
</div>
</StyledWrapper>

View File

@@ -46,7 +46,7 @@ const Preferences = ({ onClose }) => {
return (
<StyledWrapper>
<Modal size="lg" title="Preferences" handleCancel={onClose} hideFooter={true}>
<div className='flex flex-row gap-2 mx-[-1rem] !my-[-1.5rem]'>
<div className='flex flex-row gap-2 mx-[-1rem] !my-[-1.5rem] py-2'>
<div className="flex flex-col items-center tabs" role="tablist">
<div className={getTabClassname('general')} role="tab" onClick={() => setTab('general')}>
General

View File

@@ -6,7 +6,7 @@ const BodyBlock = ({ collection, data, dataBuffer, headers, error, item, width }
return (
<div className="collapsible-section">
<div className="section-header" onClick={() => toggleBody(!isBodyCollapsed)}>
<pre className="flex flex-row items-center text-lg text-indigo-500/80 dark:text-indigo-500/80">
<pre className="flex flex-row items-center text-indigo-500/80 dark:text-indigo-500/80">
<div className="opacity-70">{isBodyCollapsed ? '▼' : '▶'}</div> Body
</pre>
</div>

View File

@@ -6,7 +6,7 @@ const HeadersBlock = ({ headers, type }) => {
return (
<div className="collapsible-section mt-2">
<div className="section-header" onClick={() => toggleHeaders(!areHeadersCollapsed)}>
<pre className="flex flex-row items-center text-lg text-indigo-500/80 dark:text-indigo-500/80">
<pre className="flex flex-row items-center text-indigo-500/80 dark:text-indigo-500/80">
<div className="opacity-70">{areHeadersCollapsed ? '▼' : '▶'}</div> Headers
{headers && Object.keys(headers).length > 0 &&
<div className="ml-1">({Object.keys(headers).length})</div>

View File

@@ -7,20 +7,22 @@ import { cloneCollection } from 'providers/ReduxStore/slices/collections/actions
import toast from 'react-hot-toast';
import Modal from 'components/Modal';
import { sanitizeName, validateName, validateNameError } from 'utils/common/regex';
import PathDisplay from 'components/PathDisplay/index';
import Help from 'components/Help';
import PathDisplay from 'components/PathDisplay';
import { useState } from 'react';
import { IconArrowBackUp } from "@tabler/icons";
import { IconArrowBackUp, IconEdit } from "@tabler/icons";
const CloneCollection = ({ onClose, collection }) => {
const inputRef = useRef();
const dispatch = useDispatch();
const [isEditingFilename, toggleEditingFilename] = useState(false);
const [isEditing, toggleEditing] = useState(false);
const { name } = collection;
const formik = useFormik({
enableReinitialize: true,
initialValues: {
collectionName: '',
collectionFolderName: '',
collectionName: `${name} copy`,
collectionFolderName: `${sanitizeName(name)} copy`,
collectionLocation: ''
},
validationSchema: Yup.object({
@@ -31,7 +33,7 @@ const CloneCollection = ({ onClose, collection }) => {
collectionFolderName: Yup.string()
.min(1, 'must be at least 1 character')
.max(255, 'must be 255 characters or less')
.test('is-valid-dir-name', function(value) {
.test('is-valid-collection-name', function(value) {
const isValid = validateName(value);
return isValid ? true : this.createError({ message: validateNameError(value) });
})
@@ -92,7 +94,7 @@ const CloneCollection = ({ onClose, collection }) => {
className="block textbox mt-2 w-full"
onChange={(e) => {
formik.handleChange(e);
!isEditingFilename && formik.setFieldValue('collectionFolderName', sanitizeName(e.target.value));
!isEditing && formik.setFieldValue('collectionFolderName', sanitizeName(e.target.value));
}}
autoComplete="off"
autoCorrect="off"
@@ -124,49 +126,70 @@ const CloneCollection = ({ onClose, collection }) => {
<div className="text-red-500">{formik.errors.collectionLocation}</div>
) : null}
<div className="mt-1">
<span className="text-link cursor-pointer hover:underline" onClick={browse}>
<span
className="text-link cursor-pointer hover:underline" onClick={browse}
style={{
fontSize: '0.8125rem'
}}
>
Browse
</span>
</div>
{isEditingFilename ?
<>
<div className="mt-4">
<div className="flex items-center justify-between">
<label htmlFor="filename" className="block font-semibold">
Directory Name
</label>
<IconArrowBackUp
className="cursor-pointer opacity-50 hover:opacity-80"
size={16}
strokeWidth={1.5}
onClick={() => toggleEditingFilename(false)}
/>
</div>
<input
id="collection-folder-name"
type="text"
name="collectionFolderName"
className="block textbox mt-2 w-full"
onChange={formik.handleChange}
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={formik.values.collectionFolderName || ''}
<div className="mt-4">
<div className="flex items-center justify-between">
<label htmlFor="filename" className="flex items-center font-semibold">
Folder Name
<Help width="300">
<p>
The name of the folder used to store the collection.
</p>
<p className="mt-2">
You can choose a folder name different from your collection's name or one compatible with filesystem rules.
</p>
</Help>
</label>
{isEditing ? (
<IconArrowBackUp
className="cursor-pointer opacity-50 hover:opacity-80"
size={16}
strokeWidth={1.5}
onClick={() => toggleEditing(false)}
/>
) : (
<IconEdit
className="cursor-pointer opacity-50 hover:opacity-80"
size={16}
strokeWidth={1.5}
onClick={() => toggleEditing(true)}
/>
)}
</div>
{isEditing ? (
<input
id="collection-folder-name"
type="text"
name="collectionFolderName"
className="block textbox mt-2 w-full"
onChange={formik.handleChange}
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={formik.values.collectionFolderName || ''}
/>
) : (
<div className='relative flex flex-row gap-1 items-center justify-between'>
<PathDisplay
baseName={formik.values.collectionFolderName}
/>
</div>
</>
:
<PathDisplay
filename={formik.values.collectionFolderName}
showExtension={false}
isEditingFilename={isEditingFilename}
toggleEditingFilename={toggleEditingFilename}
/>
}
{formik.touched.collectionFolderName && formik.errors.collectionFolderName ? (
<div className="text-red-500">{formik.errors.collectionFolderName}</div>
) : null}
)}
{formik.touched.collectionFolderName && formik.errors.collectionFolderName ? (
<div className="text-red-500">{formik.errors.collectionFolderName}</div>
) : null}
</div>
</div>
</form>
</Modal>

View File

@@ -0,0 +1,12 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
.advanced-options {
.caret {
color: ${(props) => props.theme.textLink};
fill: ${(props) => props.theme.textLink};
}
}
`;
export default StyledWrapper;

View File

@@ -1,4 +1,4 @@
import React, { useState, useRef, useEffect } from 'react';
import React, { useState, useRef, useEffect, forwardRef } from 'react';
import toast from 'react-hot-toast';
import { useFormik } from 'formik';
import * as Yup from 'yup';
@@ -6,24 +6,32 @@ import Modal from 'components/Modal';
import { useDispatch } from 'react-redux';
import { isItemAFolder } from 'utils/tabs';
import { cloneItem } from 'providers/ReduxStore/slices/collections/actions';
import { IconArrowBackUp } from '@tabler/icons';
import path from "utils/common/path";
import { IconArrowBackUp, IconEdit, IconCaretDown } from "@tabler/icons";
import { sanitizeName, validateName, validateNameError } from 'utils/common/regex';
import Help from 'components/Help';
import PathDisplay from 'components/PathDisplay/index';
import path from 'utils/common/path';
import Portal from 'components/Portal';
import Dropdown from 'components/Dropdown';
import StyledWrapper from './StyledWrapper';
const CloneCollectionItem = ({ collection, item, onClose }) => {
const dispatch = useDispatch();
const isFolder = isItemAFolder(item);
const inputRef = useRef();
const [isEditingFilename, toggleEditingFilename] = useState(false);
const [isEditing, toggleEditing] = useState(false);
const itemName = item?.name;
const itemType = item?.type;
const itemFilename = item?.filename ? path.parse(item?.filename).name : '';
const [showFilesystemName, toggleShowFilesystemName] = useState(false);
const dropdownTippyRef = useRef();
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
const formik = useFormik({
enableReinitialize: true,
initialValues: {
name: itemName,
filename: sanitizeName(itemFilename)
name: `${itemName} copy`,
filename: `${sanitizeName(itemName)} copy`
},
validationSchema: Yup.object({
name: Yup.string()
@@ -34,7 +42,7 @@ const CloneCollectionItem = ({ collection, item, onClose }) => {
.min(1, 'must be at least 1 character')
.max(255, 'must be 255 characters or less')
.required('name is required')
.test('is-valid-filename', function(value) {
.test('is-valid-name', function(value) {
const isValid = validateName(value);
return isValid ? true : this.createError({ message: validateNameError(value) });
})
@@ -58,86 +66,159 @@ const CloneCollectionItem = ({ collection, item, onClose }) => {
}
}, [inputRef]);
const onSubmit = () => formik.handleSubmit();
const AdvancedOptions = forwardRef((props, ref) => {
return (
<div ref={ref} className="flex mr-2 text-link cursor-pointer items-center">
<button
className="btn-advanced"
type="button"
>
Options
</button>
<IconCaretDown className="caret ml-1" size={14} strokeWidth={2}/>
</div>
);
});
return (
<Modal
size="md"
title={`Clone ${isFolder ? 'Folder' : 'Request'}`}
confirmText="Clone"
handleConfirm={onSubmit}
handleCancel={onClose}
>
<form className="bruno-form" onSubmit={e => e.preventDefault()}>
<div>
<label htmlFor="name" className="block font-semibold">
{isFolder ? 'Folder' : 'Request'} Name
</label>
<input
id="collection-item-name"
type="text"
name="name"
placeholder='Enter Item name'
ref={inputRef}
className="block textbox mt-2 w-full"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
onChange={e => {
formik.setFieldValue('name', e.target.value);
!isEditingFilename && formik.setFieldValue('filename', sanitizeName(e.target.value));
}}
value={formik.values.name || ''}
/>
{formik.touched.name && formik.errors.name ? <div className="text-red-500">{formik.errors.name}</div> : null}
</div>
{isEditingFilename ? (
<div className="mt-4">
<div className="flex items-center justify-between">
<label htmlFor="filename" className="block font-semibold">
{isFolder ? 'Directory' : 'File'} Name
</label>
<IconArrowBackUp
className="cursor-pointer opacity-50 hover:opacity-80"
size={16}
strokeWidth={1.5}
onClick={() => toggleEditingFilename(false)}
/>
<Portal>
<StyledWrapper>
<Modal
size="md"
title={`Clone ${isFolder ? 'Folder' : 'Request'}`}
handleCancel={onClose}
hideFooter
>
<form className="bruno-form" onSubmit={formik.handleSubmit}>
<div>
<label htmlFor="name" className="block font-semibold">
{isFolder ? 'Folder' : 'Request'} Name
</label>
<input
id="collection-item-name"
type="text"
name="name"
placeholder='Enter Item name'
ref={inputRef}
className="block textbox mt-2 w-full"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
onChange={e => {
formik.setFieldValue('name', e.target.value);
!isEditing && formik.setFieldValue('filename', sanitizeName(e.target.value));
}}
value={formik.values.name || ''}
/>
{formik.touched.name && formik.errors.name ? <div className="text-red-500">{formik.errors.name}</div> : null}
</div>
{showFilesystemName && (
<div className="mt-4">
<div className="flex items-center justify-between">
<label htmlFor="filename" className="flex items-center font-semibold">
{isFolder ? 'Folder' : 'File'} Name <small className='font-normal text-muted ml-1'>(on filesystem)</small>
{ isFolder ? (
<Help width="300">
<p>
You can choose to save the folder as a different name on your file system versus what is displayed in the app.
</p>
</Help>
) : (
<Help width="300">
<p>
Bruno saves each request as a file in your collection's folder.
</p>
<p className="mt-2">
You can choose a file name different from your request's name or one compatible with filesystem rules.
</p>
</Help>
)}
</label>
{isEditing ? (
<IconArrowBackUp
className="cursor-pointer opacity-50 hover:opacity-80"
size={16}
strokeWidth={1.5}
onClick={() => toggleEditing(false)}
/>
) : (
<IconEdit
className="cursor-pointer opacity-50 hover:opacity-80"
size={16}
strokeWidth={1.5}
onClick={() => toggleEditing(true)}
/>
)}
</div>
{isEditing ? (
<div className='relative flex flex-row gap-1 items-center justify-between'>
<input
id="file-name"
type="text"
name="filename"
placeholder={isFolder ? 'Folder Name' : 'File Name'}
className={`!pr-10 block textbox mt-2 w-full`}
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
onChange={formik.handleChange}
value={formik.values.filename || ''}
/>
{itemType !== 'folder' && <span className='absolute right-2 top-4 flex justify-center items-center file-extension'>.bru</span>}
</div>
) : (
<div className='relative flex flex-row gap-1 items-center justify-between'>
<PathDisplay
collection={collection}
dirName={path.relative(collection?.pathname, path.dirname(item?.pathname))}
baseName={formik.values.filename}
/>
</div>
)}
{formik.touched.filename && formik.errors.filename ? (
<div className="text-red-500">{formik.errors.filename}</div>
) : null}
</div>
<div className='relative flex flex-row gap-1 items-center justify-between'>
<input
id="file-name"
type="text"
name="filename"
placeholder="File Name"
className={`!pr-10 block textbox mt-2 w-full`}
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
onChange={formik.handleChange}
value={formik.values.filename || ''}
/>
{itemType !== 'folder' && <span className='absolute right-2 top-4 flex justify-center items-center file-extension'>.bru</span>}
)}
<div className="flex justify-between items-center mt-8 bruno-modal-footer">
<div className='flex advanced-options'>
<Dropdown onCreate={onDropdownCreate} icon={<AdvancedOptions />} placement="bottom-start">
<div
className="dropdown-item"
key="show-filesystem-name"
onClick={(e) => {
dropdownTippyRef.current.hide();
toggleShowFilesystemName(!showFilesystemName);
}}
>
{showFilesystemName ? 'Hide Filesystem Name' : 'Show Filesystem Name'}
</div>
</Dropdown>
</div>
<div className='flex justify-end'>
<span className='mr-2'>
<button type="button" onClick={onClose} className="btn btn-md btn-close">
Cancel
</button>
</span>
<span>
<button
type="submit"
className="submit btn btn-md btn-secondary"
>
Clone
</button>
</span>
</div>
</div>
) : (
<PathDisplay
collection={collection}
item={item}
filename={formik.values.filename}
showExtension={itemType !== 'folder'}
isEditingFilename={isEditingFilename}
toggleEditingFilename={toggleEditingFilename}
showDirectory={true}
/>
)}
{formik.touched.filename && formik.errors.filename ? (
<div className="text-red-500">{formik.errors.filename}</div>
) : null}
</form>
</Modal>
</form>
</Modal>
</StyledWrapper>
</Portal>
);
};

View File

@@ -1,11 +1,10 @@
import React from 'react';
import Modal from 'components/Modal';
import path from "utils/common/path";
import Help from 'components/Help';
const CollectionItemInfo = ({ item, onClose }) => {
const { name, filename, type } = item;
const CollectionItemInfo = ({ collection, item, onClose }) => {
const { pathname: collectionPathname } = collection;
const { name, filename, pathname, type } = item;
const relativePathname = path.relative(collectionPathname, pathname);
return (
<Modal
size="md"
@@ -14,24 +13,43 @@ const CollectionItemInfo = ({ collection, item, onClose }) => {
hideCancel={true}
hideFooter={true}
>
<div className="w-fit flex flex-col h-full">
<table className="w-full border-collapse">
<tbody>
<tr className="">
<td className="py-2 px-2 text-right opacity-50">Name&nbsp;:</td>
<td className="py-2 px-2 text-nowrap truncate max-w-[500px]" title={name}>{name}</td>
</tr>
<tr className="">
<td className="py-2 px-2 text-right opacity-50">{type=='folder' ? 'Directory Name' : 'File Name'}&nbsp;:</td>
<td className="py-2 px-2 break-all text-nowrap truncate max-w-[500px]" title={filename}>{filename}</td>
</tr>
<tr className="">
<td className="py-2 px-2 text-right opacity-50">Pathname&nbsp;:</td>
<td className="py-2 px-2 break-all text-nowrap truncate max-w-[500px]" title={relativePathname}>{relativePathname}</td>
</tr>
</tbody>
</table>
</div>
<div className="w-fit flex flex-col h-full">
<table className="w-full border-collapse">
<tbody>
<tr className="">
<td className="py-2 px-2 text-left text-muted ">
{type=='folder' ? 'Folder Name' : 'Request Name'}
</td>
<td className="py-2 px-2 text-nowrap truncate max-w-[500px]" title={name}>
<span className="mr-2">:</span>{name}
</td>
</tr>
<tr className="">
<td className="py-2 px-2 text-left text-muted flex items-center">
{type == 'folder' ? 'Folder Name' : 'File Name'}
<small className='font-normal text-muted ml-1'>(on filesystem)</small>
{type == 'folder' ? (
<Help width="300">
<p>
The name of the folder on your filesystem.
</p>
</Help>
) : (
<Help width="300">
<p>
Bruno saves each request as a file in your collection's folder.
</p>
</Help>
)}
</td>
<td className="py-2 px-2 break-all text-nowrap truncate max-w-[500px]" title={filename}>
<span className="mr-2">:</span>
{filename}
</td>
</tr>
</tbody>
</table>
</div>
</Modal>
);
};

View File

@@ -0,0 +1,12 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
.advanced-options {
.caret {
color: ${(props) => props.theme.textLink};
fill: ${(props) => props.theme.textLink};
}
}
`;
export default StyledWrapper;

View File

@@ -1,4 +1,4 @@
import React, { useRef, useEffect, useState } from 'react';
import React, { useRef, useEffect, useState, forwardRef } from 'react';
import { useFormik } from 'formik';
import * as Yup from 'yup';
import Modal from 'components/Modal';
@@ -6,20 +6,29 @@ import { useDispatch } from 'react-redux';
import { isItemAFolder } from 'utils/tabs';
import { renameItem, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import path from 'utils/common/path';
import { IconArrowBackUp } from '@tabler/icons';
import { IconArrowBackUp, IconEdit, IconCaretDown } from '@tabler/icons';
import { sanitizeName, validateName, validateNameError } from 'utils/common/regex';
import toast from 'react-hot-toast';
import { closeTabs } from 'providers/ReduxStore/slices/tabs';
import Help from 'components/Help';
import PathDisplay from 'components/PathDisplay';
import Portal from 'components/Portal';
import Dropdown from 'components/Dropdown';
import StyledWrapper from './StyledWrapper';
const RenameCollectionItem = ({ collection, item, onClose }) => {
const dispatch = useDispatch();
const isFolder = isItemAFolder(item);
const inputRef = useRef();
const [isEditingFilename, toggleEditingFilename] = useState(false);
const [isEditing, toggleEditing] = useState(false);
const itemName = item?.name;
const itemType = item?.type;
const itemFilename = item?.filename ? path.parse(item?.filename).name : '';
const [showFilesystemName, toggleShowFilesystemName] = useState(false);
const dropdownTippyRef = useRef();
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
const formik = useFormik({
enableReinitialize: true,
initialValues: {
@@ -35,7 +44,7 @@ const RenameCollectionItem = ({ collection, item, onClose }) => {
.min(1, 'must be at least 1 character')
.max(255, 'must be 255 characters or less')
.required('name is required')
.test('is-valid-filename', function(value) {
.test('is-valid-name', function(value) {
const isValid = validateName(value);
return isValid ? true : this.createError({ message: validateNameError(value) });
})
@@ -77,86 +86,157 @@ const RenameCollectionItem = ({ collection, item, onClose }) => {
}
}, [inputRef]);
const onSubmit = () => formik.handleSubmit();
const AdvancedOptions = forwardRef((props, ref) => {
return (
<div ref={ref} className="flex mr-2 text-link cursor-pointer items-center">
<button
className="btn-advanced"
type="button"
>
Options
</button>
<IconCaretDown className="caret ml-1" size={14} strokeWidth={2}/>
</div>
);
});
return (
<Modal
size="md"
title={`Rename ${isFolder ? 'Folder' : 'Request'}`}
confirmText="Rename"
handleConfirm={onSubmit}
handleCancel={onClose}
>
<form className="bruno-form" onSubmit={e => {e.preventDefault()}}>
<div className='flex flex-col mt-2'>
<label htmlFor="name" className="block font-semibold">
{isFolder ? 'Folder' : 'Request'} Name
</label>
<input
id="collection-item-name"
type="text"
name="name"
ref={inputRef}
className="block textbox mt-2 w-full"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
onChange={e => {
formik.setFieldValue('name', e.target.value);
!isEditingFilename && formik.setFieldValue('filename', sanitizeName(e.target.value));
}}
value={formik.values.name || ''}
/>
{formik.touched.name && formik.errors.name ? <div className="text-red-500">{formik.errors.name}</div> : null}
</div>
{isEditingFilename ? (
<div className="mt-4">
<div className="flex items-center justify-between">
<label htmlFor="filename" className="block font-semibold">
{isFolder ? 'Directory' : 'File'} Name
<Portal>
<StyledWrapper>
<Modal
size="md"
title={`Rename ${isFolder ? 'Folder' : 'Request'}`}
handleCancel={onClose}
hideFooter
>
<form className="bruno-form" onSubmit={formik.handleSubmit}>
<div className='flex flex-col mt-2'>
<label htmlFor="name" className="block font-semibold">
{isFolder ? 'Folder' : 'Request'} Name
</label>
<IconArrowBackUp
className="cursor-pointer opacity-50 hover:opacity-80"
size={16}
strokeWidth={1.5}
onClick={() => toggleEditingFilename(false)}
/>
</div>
<div className='relative flex flex-row gap-1 items-center justify-between'>
<input
id="file-name"
id="collection-item-name"
type="text"
name="filename"
placeholder="File Name"
className={`!pr-10 block textbox mt-2 w-full`}
name="name"
ref={inputRef}
className="block textbox mt-2 w-full"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
onChange={formik.handleChange}
value={formik.values.filename || ''}
onChange={e => {
formik.setFieldValue('name', e.target.value);
!isEditing && formik.setFieldValue('filename', sanitizeName(e.target.value));
}}
value={formik.values.name || ''}
/>
{itemType !== 'folder' && <span className='absolute right-2 top-4 flex justify-center items-center file-extension'>.bru</span>}
{formik.touched.name && formik.errors.name ? <div className="text-red-500">{formik.errors.name}</div> : null}
</div>
</div>
) : (
<PathDisplay
collection={collection}
item={item}
filename={formik.values.filename}
showExtension={itemType !== 'folder'}
isEditingFilename={isEditingFilename}
toggleEditingFilename={toggleEditingFilename}
showDirectory={true}
/>
)}
{formik.touched.filename && formik.errors.filename ? (
<div className="text-red-500">{formik.errors.filename}</div>
) : null}
</form>
</Modal>
{showFilesystemName && (
<div className="mt-4">
<div className="flex items-center justify-between">
<label htmlFor="filename" className="flex items-center font-semibold">
{isFolder ? 'Folder' : 'File'} Name <small className='font-normal text-muted ml-1'>(on filesystem)</small>
{ isFolder ? (
<Help width="300">
<p>
You can choose to save the folder as a different name on your file system versus what is displayed in the app.
</p>
</Help>
) : (
<Help width="300">
<p>
Bruno saves each request as a file in your collection's folder.
</p>
<p className="mt-2">
You can choose a file name different from your request's name or one compatible with filesystem rules.
</p>
</Help>
)}
</label>
{isEditing ? (
<IconArrowBackUp
className="cursor-pointer opacity-50 hover:opacity-80"
size={16}
strokeWidth={1.5}
onClick={() => toggleEditing(false)}
/>
) : (
<IconEdit
className="cursor-pointer opacity-50 hover:opacity-80"
size={16}
strokeWidth={1.5}
onClick={() => toggleEditing(true)}
/>
)}
</div>
{isEditing ? (
<div className='relative flex flex-row gap-1 items-center justify-between'>
<input
id="file-name"
type="text"
name="filename"
placeholder={isFolder ? 'Folder Name' : 'File Name'}
className={`!pr-10 block textbox mt-2 w-full`}
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
onChange={formik.handleChange}
value={formik.values.filename || ''}
/>
{itemType !== 'folder' && <span className='absolute right-2 top-4 flex justify-center items-center file-extension'>.bru</span>}
</div>
) : (
<div className='relative flex flex-row gap-1 items-center justify-between'>
<PathDisplay
collection={collection}
dirName={path.relative(collection?.pathname, path.dirname(item?.pathname))}
baseName={formik.values.filename}
/>
</div>
)}
{formik.touched.filename && formik.errors.filename ? (
<div className="text-red-500">{formik.errors.filename}</div>
) : null}
</div>
)}
<div className="flex justify-between items-center mt-8 bruno-modal-footer">
<div className='flex advanced-options'>
<Dropdown onCreate={onDropdownCreate} icon={<AdvancedOptions />} placement="bottom-start">
<div
className="dropdown-item"
key="show-filesystem-name"
onClick={(e) => {
dropdownTippyRef.current.hide();
toggleShowFilesystemName(!showFilesystemName);
}}
>
{showFilesystemName ? 'Hide Filesystem Name' : 'Show Filesystem Name'}
</div>
</Dropdown>
</div>
<div className='flex justify-end'>
<span className='mr-2'>
<button type="button" onClick={onClose} className="btn btn-md btn-close">
Cancel
</button>
</span>
<span>
<button
type="submit"
className="submit btn btn-md btn-secondary"
>
Rename
</button>
</span>
</div>
</div>
</form>
</Modal>
</StyledWrapper>
</Portal>
);
};

View File

@@ -24,7 +24,6 @@ import toast from 'react-hot-toast';
import StyledWrapper from './StyledWrapper';
import NetworkError from 'components/ResponsePane/NetworkError/index';
import CollectionItemInfo from './CollectionItemInfo/index';
import { findItemInCollection } from 'utils/collections';
import CollectionItemIcon from './CollectionItemIcon';
import { scrollToTheActiveTab } from 'utils/tabs';

View File

@@ -9,13 +9,13 @@ import Modal from 'components/Modal';
import { sanitizeName, validateName, validateNameError } from 'utils/common/regex';
import PathDisplay from 'components/PathDisplay/index';
import { useState } from 'react';
import { IconArrowBackUp } from '@tabler/icons';
import { IconArrowBackUp, IconEdit } from '@tabler/icons';
import Help from 'components/Help';
const CreateCollection = ({ onClose }) => {
const inputRef = useRef();
const dispatch = useDispatch();
const [isEditingFilename, toggleEditingFilename] = useState(false);
const [isEditing, toggleEditing] = useState(false);
const formik = useFormik({
enableReinitialize: true,
@@ -32,7 +32,7 @@ const CreateCollection = ({ onClose }) => {
collectionFolderName: Yup.string()
.min(1, 'must be at least 1 character')
.max(255, 'must be 255 characters or less')
.test('is-valid-dir-name', function(value) {
.test('is-valid-collection-name', function(value) {
const isValid = validateName(value);
return isValid ? true : this.createError({ message: validateNameError(value) });
})
@@ -86,7 +86,7 @@ const CreateCollection = ({ onClose }) => {
className="block textbox mt-2 w-full"
onChange={(e) => {
formik.handleChange(e);
!isEditingFilename && formik.setFieldValue('collectionFolderName', sanitizeName(e.target.value));
!isEditing && formik.setFieldValue('collectionFolderName', sanitizeName(e.target.value));
}}
autoComplete="off"
autoCorrect="off"
@@ -105,7 +105,7 @@ const CreateCollection = ({ onClose }) => {
Bruno stores your collections on your computer's filesystem.
</p>
<p className="mt-2">
Choose where you want to store this collection.
Choose the location where you want to store this collection.
</p>
</Help>
</label>
@@ -126,24 +126,46 @@ const CreateCollection = ({ onClose }) => {
<div className="text-red-500">{formik.errors.collectionLocation}</div>
) : null}
<div className="mt-1">
<span className="text-link cursor-pointer hover:underline" onClick={browse}>
<span
className="text-link cursor-pointer hover:underline" onClick={browse}
style={{
fontSize: '0.8125rem'
}}
>
Browse
</span>
</div>
{isEditingFilename ?
<>
<div className="mt-4">
<div className="flex items-center justify-between">
<label htmlFor="filename" className="block font-semibold">
Directory Name
</label>
{formik.values.collectionName?.trim()?.length > 0 && (
<div className="mt-4">
<div className="flex items-center justify-between">
<label htmlFor="filename" className="flex items-center font-semibold">
Folder Name
<Help width="300">
<p>
The name of the folder used to store the collection.
</p>
<p className="mt-2">
You can choose a folder name different from your collection's name or one compatible with filesystem rules.
</p>
</Help>
</label>
{isEditing ? (
<IconArrowBackUp
className="cursor-pointer opacity-50 hover:opacity-80"
size={16}
strokeWidth={1.5}
onClick={() => toggleEditingFilename(false)}
onClick={() => toggleEditing(false)}
/>
</div>
) : (
<IconEdit
className="cursor-pointer opacity-50 hover:opacity-80"
size={16}
strokeWidth={1.5}
onClick={() => toggleEditing(true)}
/>
)}
</div>
{isEditing ? (
<input
id="collection-folder-name"
type="text"
@@ -156,19 +178,18 @@ const CreateCollection = ({ onClose }) => {
spellCheck="false"
value={formik.values.collectionFolderName || ''}
/>
</div>
</>
:
<PathDisplay
filename={formik.values.collectionFolderName}
showExtension={false}
isEditingFilename={isEditingFilename}
toggleEditingFilename={toggleEditingFilename}
/>
}
{formik.touched.collectionFolderName && formik.errors.collectionFolderName ? (
<div className="text-red-500">{formik.errors.collectionFolderName}</div>
) : null}
) : (
<div className='relative flex flex-row gap-1 items-center justify-between'>
<PathDisplay
baseName={formik.values.collectionFolderName}
/>
</div>
)}
{formik.touched.collectionFolderName && formik.errors.collectionFolderName ? (
<div className="text-red-500">{formik.errors.collectionFolderName}</div>
) : null}
</div>
)}
</div>
</form>
</Modal>

View File

@@ -0,0 +1,12 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
.advanced-options {
.caret {
color: ${(props) => props.theme.textLink};
fill: ${(props) => props.theme.textLink};
}
}
`;
export default StyledWrapper;

View File

@@ -1,18 +1,27 @@
import React, { useRef, useEffect, useState } from 'react';
import React, { useRef, useEffect, useState, forwardRef } from 'react';
import { useFormik } from 'formik';
import toast from 'react-hot-toast';
import * as Yup from 'yup';
import Portal from 'components/Portal';
import Modal from 'components/Modal';
import { useDispatch } from 'react-redux';
import { newFolder } from 'providers/ReduxStore/slices/collections/actions';
import { IconArrowBackUp } from '@tabler/icons';
import { IconArrowBackUp, IconEdit} from '@tabler/icons';
import { sanitizeName, validateName, validateNameError } from 'utils/common/regex';
import PathDisplay from 'components/PathDisplay';
import PathDisplay from 'components/PathDisplay/index';
import Help from 'components/Help';
import Dropdown from "components/Dropdown";
import { IconCaretDown } from "@tabler/icons";
import StyledWrapper from './StyledWrapper';
const NewFolder = ({ collection, item, onClose }) => {
const dispatch = useDispatch();
const inputRef = useRef();
const [isEditingFilename, toggleEditingFilename] = useState(false);
const [isEditing, toggleEditing] = useState(false);
const [showFilesystemName, toggleShowFilesystemName] = useState(false);
const dropdownTippyRef = useRef();
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
const formik = useFormik({
enableReinitialize: true,
@@ -58,80 +67,139 @@ const NewFolder = ({ collection, item, onClose }) => {
}
}, [inputRef]);
const onSubmit = () => formik.handleSubmit();
const AdvancedOptions = forwardRef((props, ref) => {
return (
<div ref={ref} className="flex mr-2 text-link cursor-pointer items-center">
<button
className="btn-advanced"
type="button"
>
Options
</button>
<IconCaretDown className="caret ml-1" size={14} strokeWidth={2}/>
</div>
);
});
return (
<Modal size="md" title="New Folder" confirmText="Create" handleConfirm={onSubmit} handleCancel={onClose}>
<form className="bruno-form" onSubmit={formik.handleSubmit}>
<div>
<label htmlFor="folderName" className="block font-semibold">
Folder Name
</label>
<input
id="collection-name"
type="text"
name="folderName"
ref={inputRef}
className="block textbox mt-2 w-full"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
onChange={e => {
formik.setFieldValue('folderName', e.target.value);
!isEditingFilename && formik.setFieldValue('directoryName', sanitizeName(e.target.value));
}}
value={formik.values.folderName || ''}
/>
{formik.touched.folderName && formik.errors.folderName ? (
<div className="text-red-500">{formik.errors.folderName}</div>
) : null}
</div>
{isEditingFilename ? (
<div className="mt-4">
<div className="flex items-center justify-between">
<label htmlFor="directoryName" className="block font-semibold">
Directory Name
</label>
<IconArrowBackUp
className="cursor-pointer opacity-50 hover:opacity-80"
size={16}
strokeWidth={1.5}
onClick={() => toggleEditingFilename(false)}
/>
<Portal>
<StyledWrapper>
<Modal size="md" title="New Folder" hideFooter={true} handleCancel={onClose}>
<form className="bruno-form" onSubmit={formik.handleSubmit}>
<label htmlFor="folderName" className="block font-semibold">
Folder Name
</label>
<input
id="collection-name"
type="text"
name="folderName"
ref={inputRef}
className="block textbox mt-2 w-full"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
onChange={e => {
formik.setFieldValue('folderName', e.target.value);
!isEditing && formik.setFieldValue('directoryName', sanitizeName(e.target.value));
}}
value={formik.values.folderName || ''}
/>
{formik.touched.folderName && formik.errors.folderName ? (
<div className="text-red-500">{formik.errors.folderName}</div>
) : null}
{showFilesystemName && (
<div className="mt-4">
<div className="flex items-center justify-between">
<label htmlFor="directoryName" className="flex items-center font-semibold">
Folder Name <small className='font-normal text-muted ml-1'>(on filesystem)</small>
<Help width="300">
<p>
You can choose to save the folder as a different name on your file system versus what is displayed in the app.
</p>
</Help>
</label>
{isEditing ? (
<IconArrowBackUp
className="cursor-pointer opacity-50 hover:opacity-80"
size={16}
strokeWidth={1.5}
onClick={() => toggleEditing(false)}
/>
): (
<IconEdit
className="cursor-pointer opacity-50 hover:opacity-80"
size={16}
strokeWidth={1.5}
onClick={() => toggleEditing(true)}
/>
)}
</div>
{isEditing ? (
<div className='relative flex flex-row gap-1 items-center justify-between'>
<input
id="file-name"
type="text"
name="directoryName"
placeholder="Folder Name"
className={`block textbox mt-2 w-full`}
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
onChange={formik.handleChange}
value={formik.values.directoryName || ''}
/>
</div>
) : (
<div className='relative flex flex-row gap-1 items-center justify-between'>
<PathDisplay
iconType="folder"
baseName={formik.values.directoryName}
/>
</div>
)}
{formik.touched.directoryName && formik.errors.directoryName ? (
<div className="text-red-500">{formik.errors.directoryName}</div>
) : null}
</div>
)}
<div className="flex justify-between items-center mt-8 bruno-modal-footer">
<div className='flex advanced-options'>
<Dropdown onCreate={onDropdownCreate} icon={<AdvancedOptions />} placement="bottom-start">
<div
className="dropdown-item"
key="show-filesystem-name"
onClick={(e) => {
dropdownTippyRef.current.hide();
toggleShowFilesystemName(!showFilesystemName);
}}
>
{showFilesystemName ? 'Hide Filesystem Name' : 'Show Filesystem Name'}
</div>
</Dropdown>
</div>
<div className='flex justify-end'>
<span className='mr-2'>
<button type="button" onClick={onClose} className="btn btn-md btn-close">
Cancel
</button>
</span>
<span>
<button
type="submit"
className="submit btn btn-md btn-secondary"
>
Create
</button>
</span>
</div>
</div>
<div className='relative flex flex-row gap-1 items-center justify-between'>
<input
id="file-name"
type="text"
name="directoryName"
placeholder="Directory Name"
className={`block textbox mt-2 w-full`}
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
onChange={formik.handleChange}
value={formik.values.directoryName || ''}
/>
</div>
</div>
) : (
<PathDisplay
collection={collection}
item={item}
filename={formik.values.directoryName}
showExtension={false}
isEditingFilename={isEditingFilename}
toggleEditingFilename={toggleEditingFilename}
/>
)}
{formik.touched.directoryName && formik.errors.directoryName ? (
<div className="text-red-500">{formik.errors.directoryName}</div>
) : null}
</form>
</Modal>
</form>
</Modal>
</StyledWrapper>
</Portal>
);
};

View File

@@ -1,45 +1,53 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
div.method-selector-container {
border: solid 1px ${(props) => props.theme.modal.input.border};
border-right: none;
background-color: ${(props) => props.theme.modal.input.bg};
border-top-left-radius: 3px;
border-bottom-left-radius: 3px;
.method-selector {
min-width: 80px;
}
}
div.method-selector-container,
div.input-container {
background-color: ${(props) => props.theme.modal.input.bg};
height: 2.3rem;
}
div.input-container {
border: solid 1px ${(props) => props.theme.modal.input.border};
border-top-right-radius: 3px;
border-bottom-right-radius: 3px;
input {
background-color: ${(props) => props.theme.modal.input.bg};
outline: none;
box-shadow: none;
&:focus {
outline: none !important;
box-shadow: none !important;
}
}
}
textarea.curl-command {
min-height: 150px;
}
.dropdown {
width: fit-content;
.dropdown-item {
padding: 0.2rem 0.6rem !important;
}
}
`;
const StyledWrapper = styled.div`
div.method-selector-container {
border: solid 1px ${(props) => props.theme.modal.input.border};
border-right: none;
background-color: ${(props) => props.theme.modal.input.bg};
border-top-left-radius: 3px;
border-bottom-left-radius: 3px;
.method-selector {
min-width: 80px;
}
}
div.method-selector-container,
div.input-container {
background-color: ${(props) => props.theme.modal.input.bg};
height: 2.3rem;
}
div.input-container {
border: solid 1px ${(props) => props.theme.modal.input.border};
border-top-right-radius: 3px;
border-bottom-right-radius: 3px;
input {
background-color: ${(props) => props.theme.modal.input.bg};
outline: none;
box-shadow: none;
&:focus {
outline: none !important;
box-shadow: none !important;
}
}
}
export default StyledWrapper;
textarea.curl-command {
min-height: 150px;
}
.dropdown {
width: fit-content;
.dropdown-item {
padding: 0.2rem 0.6rem !important;
}
}
.advanced-options {
.caret {
color: ${(props) => props.theme.textLink};
fill: ${(props) => props.theme.textLink};
}
}
`;
export default StyledWrapper;

View File

@@ -2,6 +2,7 @@ import React, { useRef, useEffect, useCallback, forwardRef, useState } from 'rea
import { useFormik } from 'formik';
import * as Yup from 'yup';
import toast from 'react-hot-toast';
import path from 'utils/common/path';
import { uuid } from 'utils/common';
import Modal from 'components/Modal';
import { useDispatch } from 'react-redux';
@@ -11,10 +12,12 @@ import { addTab } from 'providers/ReduxStore/slices/tabs';
import HttpMethodSelector from 'components/RequestPane/QueryUrl/HttpMethodSelector';
import { getDefaultRequestPaneTab } from 'utils/collections';
import { getRequestFromCurlCommand } from 'utils/curl';
import { IconArrowBackUp, IconCaretDown } from '@tabler/icons';
import { IconArrowBackUp, IconCaretDown, IconEdit } from '@tabler/icons';
import { sanitizeName, validateName, validateNameError } from 'utils/common/regex';
import Dropdown from 'components/Dropdown';
import PathDisplay from 'components/PathDisplay';
import Portal from 'components/Portal';
import Help from 'components/Help';
import StyledWrapper from './StyledWrapper';
const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
@@ -24,10 +27,14 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
brunoConfig: { presets: collectionPresets = {} }
} = collection;
const [curlRequestTypeDetected, setCurlRequestTypeDetected] = useState(null);
const [showFilesystemName, toggleShowFilesystemName] = useState(false);
const dropdownTippyRef = useRef();
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
const advancedDropdownTippyRef = useRef();
const onAdvancedDropdownCreate = (ref) => (advancedDropdownTippyRef.current = ref);
const Icon = forwardRef((props, ref) => {
return (
<div ref={ref} className="flex items-center justify-end auth-type-label select-none">
@@ -57,7 +64,7 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
setCurlRequestTypeDetected(type);
};
const [isEditingFilename, toggleEditingFilename] = useState(false);
const [isEditing, toggleEditing] = useState(false);
const getRequestType = (collectionPresets) => {
if (!collectionPresets || !collectionPresets.requestType) {
@@ -229,213 +236,279 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
}
};
return (
<StyledWrapper>
<Modal size="md" title="New Request" confirmText="Create" handleConfirm={onSubmit} handleCancel={onClose}>
<form
className="bruno-form"
onSubmit={formik.handleSubmit}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
formik.handleSubmit();
}
}}
const AdvancedOptions = forwardRef((props, ref) => {
return (
<div ref={ref} className="flex mr-2 text-link cursor-pointer items-center">
<button
className="btn-advanced"
type="button"
>
<div>
<label htmlFor="requestName" className="block font-semibold">
Type
</label>
Options
</button>
<IconCaretDown className="caret ml-1" size={14} strokeWidth={2}/>
</div>
);
});
<div className="flex items-center mt-2">
<input
id="http-request"
className="cursor-pointer"
type="radio"
name="requestType"
onChange={formik.handleChange}
value="http-request"
checked={formik.values.requestType === 'http-request'}
/>
<label htmlFor="http-request" className="ml-1 cursor-pointer select-none">
HTTP
return (
<Portal>
<StyledWrapper>
<Modal size="md" title="New Request" hideFooter handleCancel={onClose}>
<form
className="bruno-form"
onSubmit={formik.handleSubmit}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
formik.handleSubmit();
}
}}
>
<div>
<label htmlFor="requestName" className="block font-semibold">
Type
</label>
<input
id="graphql-request"
className="ml-4 cursor-pointer"
type="radio"
name="requestType"
onChange={(event) => {
formik.setFieldValue('requestMethod', 'POST');
formik.handleChange(event);
}}
value="graphql-request"
checked={formik.values.requestType === 'graphql-request'}
/>
<label htmlFor="graphql-request" className="ml-1 cursor-pointer select-none">
GraphQL
</label>
<input
id="from-curl"
className="cursor-pointer ml-auto"
type="radio"
name="requestType"
onChange={formik.handleChange}
value="from-curl"
checked={formik.values.requestType === 'from-curl'}
/>
<label htmlFor="from-curl" className="ml-1 cursor-pointer select-none">
From cURL
</label>
</div>
</div>
<div className="mt-4">
<label htmlFor="requestName" className="block font-semibold">
Name
</label>
<input
id="request-name"
type="text"
name="requestName"
placeholder="Request Name"
ref={inputRef}
className="block textbox mt-2 w-full"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
onChange={e => {
formik.setFieldValue('requestName', e.target.value);
!isEditingFilename && formik.setFieldValue('filename', sanitizeName(e.target.value));
}}
value={formik.values.requestName || ''}
/>
{formik.touched.requestName && formik.errors.requestName ? (
<div className="text-red-500">{formik.errors.requestName}</div>
) : null}
</div>
{isEditingFilename ? (
<div className="mt-4">
<div className="flex items-center justify-between">
<label htmlFor="filename" className="block font-semibold">
File Name
</label>
<IconArrowBackUp
className="cursor-pointer opacity-50 hover:opacity-80"
size={16}
strokeWidth={1.5}
onClick={() => toggleEditingFilename(false)}
/>
</div>
<div className='relative flex flex-row gap-1 items-center justify-between'>
<div className="flex items-center mt-2">
<input
id="file-name"
type="text"
name="filename"
placeholder="File Name"
className={`!pr-10 block textbox mt-2 w-full`}
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
id="http-request"
className="cursor-pointer"
type="radio"
name="requestType"
onChange={formik.handleChange}
value={formik.values.filename || ''}
value="http-request"
checked={formik.values.requestType === 'http-request'}
/>
<span className='absolute right-2 top-4 flex justify-center items-center file-extension'>.bru</span>
<label htmlFor="http-request" className="ml-1 cursor-pointer select-none">
HTTP
</label>
<input
id="graphql-request"
className="ml-4 cursor-pointer"
type="radio"
name="requestType"
onChange={(event) => {
formik.setFieldValue('requestMethod', 'POST');
formik.handleChange(event);
}}
value="graphql-request"
checked={formik.values.requestType === 'graphql-request'}
/>
<label htmlFor="graphql-request" className="ml-1 cursor-pointer select-none">
GraphQL
</label>
<input
id="from-curl"
className="cursor-pointer ml-auto"
type="radio"
name="requestType"
onChange={formik.handleChange}
value="from-curl"
checked={formik.values.requestType === 'from-curl'}
/>
<label htmlFor="from-curl" className="ml-1 cursor-pointer select-none">
From cURL
</label>
</div>
</div>
) : (
<PathDisplay
collection={collection}
item={item}
filename={formik.values.filename}
isEditingFilename={isEditingFilename}
toggleEditingFilename={toggleEditingFilename}
/>
)}
{formik.touched.filename && formik.errors.filename ? (
<div className="text-red-500">{formik.errors.filename}</div>
) : null}
{formik.values.requestType !== 'from-curl' ? (
<>
<div className="mt-4">
<label htmlFor="requestName" className="block font-semibold">
Request Name
</label>
<input
id="request-name"
type="text"
name="requestName"
placeholder="Request Name"
ref={inputRef}
className="block textbox mt-2 w-full"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
onChange={e => {
formik.setFieldValue('requestName', e.target.value);
!isEditing && formik.setFieldValue('filename', sanitizeName(e.target.value));
}}
value={formik.values.requestName || ''}
/>
{formik.touched.requestName && formik.errors.requestName ? (
<div className="text-red-500">{formik.errors.requestName}</div>
) : null}
</div>
{showFilesystemName && (
<div className="mt-4">
<label htmlFor="request-url" className="block font-semibold">
URL
</label>
<div className="flex items-center mt-2 ">
<div className="flex items-center h-full method-selector-container">
<HttpMethodSelector
method={formik.values.requestMethod}
onMethodSelect={(val) => formik.setFieldValue('requestMethod', val)}
<div className="flex items-center justify-between">
<label htmlFor="filename" className="flex items-center font-semibold">
File Name <small className='font-normal text-muted ml-1'>(on filesystem)</small>
<Help width="300">
<p>
Bruno saves each request as a file in your collection's folder.
</p>
<p className="mt-2">
You can choose a file name different from your request's name or one compatible with filesystem rules.
</p>
</Help>
</label>
{isEditing ? (
<IconArrowBackUp
className="cursor-pointer opacity-50 hover:opacity-80"
size={16}
strokeWidth={1.5}
onClick={() => toggleEditing(false)}
/>
</div>
<div className="flex items-center flex-grow input-container h-full">
) : (
<IconEdit
className="cursor-pointer opacity-50 hover:opacity-80"
size={16}
strokeWidth={1.5}
onClick={() => toggleEditing(true)}
/>
)}
</div>
{isEditing ? (
<div className='relative flex flex-row gap-1 items-center justify-between'>
<input
id="request-url"
id="file-name"
type="text"
name="requestUrl"
placeholder="Request URL"
className="px-3 w-full "
name="filename"
placeholder="File Name"
className={`!pr-10 block textbox mt-2 w-full`}
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
onChange={formik.handleChange}
value={formik.values.requestUrl || ''}
onPaste={handlePaste}
value={formik.values.filename || ''}
/>
<span className='absolute right-2 top-4 flex justify-center items-center file-extension'>.bru</span>
</div>
) : (
<div className='relative flex flex-row gap-1 items-center justify-between'>
<PathDisplay
collection={collection}
dirName={path.relative(collection?.pathname, item?.pathname || collection?.pathname)}
baseName={formik.values.filename? `${formik.values.filename}.bru` : ''}
/>
</div>
</div>
{formik.touched.requestUrl && formik.errors.requestUrl ? (
<div className="text-red-500">{formik.errors.requestUrl}</div>
)}
{formik.touched.filename && formik.errors.filename ? (
<div className="text-red-500">{formik.errors.filename}</div>
) : null}
</div>
</>
) : (
<div className="mt-4">
<div className="flex justify-between">
<label htmlFor="request-url" className="block font-semibold">
cURL Command
</label>
<Dropdown className="dropdown" onCreate={onDropdownCreate} icon={<Icon />} placement="bottom-end">
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
curlRequestTypeChange('http-request');
}}
>
HTTP
)}
{formik.values.requestType !== 'from-curl' ? (
<>
<div className="mt-4">
<label htmlFor="request-url" className="block font-semibold">
URL
</label>
<div className="flex items-center mt-2 ">
<div className="flex items-center h-full method-selector-container">
<HttpMethodSelector
method={formik.values.requestMethod}
onMethodSelect={(val) => formik.setFieldValue('requestMethod', val)}
/>
</div>
<div className="flex items-center flex-grow input-container h-full">
<input
id="request-url"
type="text"
name="requestUrl"
placeholder="Request URL"
className="px-3 w-full "
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
onChange={formik.handleChange}
value={formik.values.requestUrl || ''}
onPaste={handlePaste}
/>
</div>
</div>
<div
{formik.touched.requestUrl && formik.errors.requestUrl ? (
<div className="text-red-500">{formik.errors.requestUrl}</div>
) : null}
</div>
</>
) : (
<div className="mt-4">
<div className="flex justify-between">
<label htmlFor="request-url" className="block font-semibold">
cURL Command
</label>
<Dropdown className="dropdown" onCreate={onDropdownCreate} icon={<Icon />} placement="bottom-end">
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
curlRequestTypeChange('http-request');
}}
>
HTTP
</div>
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
curlRequestTypeChange('graphql-request');
}}
>
GraphQL
</div>
</Dropdown>
</div>
<textarea
name="curlCommand"
placeholder="Enter cURL request here.."
className="block textbox w-full mt-4 curl-command"
value={formik.values.curlCommand}
onChange={handleCurlCommandChange}
></textarea>
{formik.touched.curlCommand && formik.errors.curlCommand ? (
<div className="text-red-500">{formik.errors.curlCommand}</div>
) : null}
</div>
)}
<div className="flex justify-between items-center mt-8 bruno-modal-footer">
<div className='flex advanced-options'>
<Dropdown onCreate={onAdvancedDropdownCreate} icon={<AdvancedOptions />} placement="bottom-start">
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
curlRequestTypeChange('graphql-request');
key="show-filesystem-name"
onClick={(e) => {
advancedDropdownTippyRef.current.hide();
toggleShowFilesystemName(!showFilesystemName);
}}
>
GraphQL
{showFilesystemName ? 'Hide Filesystem Name' : 'Show Filesystem Name'}
</div>
</Dropdown>
</div>
<textarea
name="curlCommand"
placeholder="Enter cURL request here.."
className="block textbox w-full mt-4 curl-command"
value={formik.values.curlCommand}
onChange={handleCurlCommandChange}
></textarea>
{formik.touched.curlCommand && formik.errors.curlCommand ? (
<div className="text-red-500">{formik.errors.curlCommand}</div>
) : null}
<div className='flex justify-end'>
<span className='mr-2'>
<button type="button" onClick={onClose} className="btn btn-md btn-close">
Cancel
</button>
</span>
<span>
<button
type="submit"
className="submit btn btn-md btn-secondary"
>
Create
</button>
</span>
</div>
</div>
)}
</form>
</Modal>
</StyledWrapper>
</form>
</Modal>
</StyledWrapper>
</Portal>
);
};

View File

@@ -810,7 +810,7 @@ export const newHttpRequest = (params) => (dispatch, getState) => {
file: null
},
auth: auth ?? {
mode: 'none'
mode: 'inherit'
}
}
};

View File

@@ -145,7 +145,7 @@ body {
font-kerning: none;
text-rendering: optimizeSpeed;
letter-spacing: normal;
font-family: Inter, sans-serif !important;
font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif !important;
overflow-x: hidden;
}

View File

@@ -2,7 +2,7 @@ const invalidCharacters = /[<>:"/\\|?*\x00-\x1F]/g; // replace invalid character
const reservedDeviceNames = /^(CON|PRN|AUX|NUL|COM[0-9]|LPT[0-9])$/i;
const firstCharacter = /^[^.\s\-\<>:"/\\|?*\x00-\x1F]/; // no dot, space, or hyphen at start
const middleCharacters = /^[^<>:"/\\|?*\x00-\x1F]*$/; // no invalid characters
const lastCharacter = /[^.\s]$/; // no dot or space at end, hyphen allowed
const lastCharacter = /^[^.\s\-\<>:"/\\|?*\x00-\x1F]/; // no dot or space at end, hyphen allowed
export const variableNameRegex = /^[\w-.]*$/;
@@ -29,6 +29,7 @@ export const validateName = (name) => {
export const validateNameError = (name) => {
if (!name) return "Name cannot be empty.";
if (name.length > 255) {
return "Name cannot exceed 255 characters.";
}

View File

@@ -0,0 +1,176 @@
module.exports = function aboutBruno({version}) {
return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1, user-scalable=yes">
<title>About Bruno</title>
<style>
body {
font-family: Arial, sans-serif;
text-align: center;
margin: 0;
padding: 10px;
background-color: #f4f4f4;
color: #333;
}
.logo {
margin-top: 0px;
}
.title {
font-size: 24px;
margin-top: 5px;
font-weight: bold;
color: #222;
}
.description {
font-size: 12px;
color: #222;
margin-top: 5px;
}
.buttons {
margin-top: 5px;
}
.footer {
margin-top: 5px;
padding: 5px;
font-size: 14px;
color: #555;
}
.link {
display: inline-block;
margin-top: 10px;
padding: 10px 15px;
background-color: #F4AA41;
color: white;
text-decoration: none;
border-radius: 5px;
cursor: pointer;
transition: background 0.3s;
}
.link:hover {
background-color: #F4AA41;
}
</style>
</head>
<body>
<div class="logo">
</div>
<svg id="emoji" width="100" viewBox="0 0 72 72" xmlns="http://www.w3.org/2000/svg">
<g id="color">
<path
fill="#F4AA41"
stroke="none"
d="M23.5,14.5855l-4.5,1.75l-7.25,8.5l-4.5,10.75l2,5.25c1.2554,3.7911,3.5231,7.1832,7.25,10l2.5-3.3333 c0,0,3.8218,7.7098,10.7384,8.9598c0,0,10.2616,1.936,15.5949-0.8765c3.4203-1.8037,4.4167-4.4167,4.4167-4.4167l3.4167-3.4167 l1.5833,2.3333l2.0833-0.0833l5.4167-7.25L64,37.3355l-0.1667-4.5l-2.3333-5.5l-4.8333-7.4167c0,0-2.6667-4.9167-8.1667-3.9167 c0,0-6.5-4.8333-11.8333-4.0833S32.0833,10.6688,23.5,14.5855z"
/>
<polygon
fill="#EA5A47"
stroke="none"
points="36,47.2521 32.9167,49.6688 30.4167,49.6688 30.3333,53.5021 31.0833,57.0021 32.1667,58.9188 35,60.4188 39.5833,59.8355 41.1667,58.0855 42.1667,53.8355 41.9167,49.8355 39.9167,50.0855"
/>
<polygon
fill="#3F3F3F"
stroke="none"
points="32.5,36.9188 30.9167,40.6688 33.0833,41.9188 34.3333,42.4188 38.6667,42.5855 41.5833,40.3355 39.8333,37.0855"
/>
</g>
<g id="hair" />
<g id="skin" />
<g id="skin-shadow" />
<g id="line">
<path
fill="#000000"
stroke="none"
d="M29.5059,30.1088c0,0-1.8051,1.2424-2.7484,0.6679c-0.9434-0.5745-1.2424-1.8051-0.6679-2.7484 s1.805-1.2424,2.7484-0.6679S29.5059,30.1088,29.5059,30.1088z"
/>
<path
fill="none"
stroke="#000000"
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit="10"
strokeWidth="2"
d="M33.1089,37.006h6.1457c0.4011,0,0.7634,0.2397,0.9203,0.6089l1.1579,2.7245l-2.1792,1.1456 c-0.6156,0.3236-1.3654-0.0645-1.4567-0.754"
/>
<path
fill="none"
stroke="#000000"
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit="10"
strokeWidth="2"
d="M34.7606,40.763c-0.1132,0.6268-0.7757,0.9895-1.3647,0.7471l-2.3132-0.952l1.0899-2.9035 c0.1465-0.3901,0.5195-0.6486,0.9362-0.6486"
/>
<path
fill="none"
stroke="#000000"
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit="10"
strokeWidth="2"
d="M30.4364,50.0268c0,0-0.7187,8.7934,3.0072,9.9375c2.6459,0.8125,5.1497,0.5324,6.0625-0.25 c0.875-0.75,2.6323-4.4741,1.8267-9.6875"
/>
<path
fill="#000000"
stroke="none"
d="M44.2636,30.1088c0,0,1.805,1.2424,2.7484,0.6679c0.9434-0.5745,1.2424-1.8051,0.6679-2.7484 c-0.5745-0.9434-1.805-1.2424-2.7484-0.6679C43.9881,27.9349,44.2636,30.1088,44.2636,30.1088z"
/>
<path
fill="none"
stroke="#000000"
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit="10"
strokeWidth="2"
d="M25.6245,42.8393c-0.475,3.6024,2.2343,5.7505,4.2847,6.8414c1.1968,0.6367,2.6508,0.5182,3.7176-0.3181l2.581-2.0233l2.581,2.0233 c1.0669,0.8363,2.5209,0.9548,3.7176,0.3181c2.0504-1.0909,4.7597-3.239,4.2847-6.8414"
/>
<path
fill="none"
stroke="#000000"
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit="10"
strokeWidth="2"
d="M19.9509,28.3572c-2.3166,5.1597-0.5084,13.0249,0.119,15.3759c0.122,0.4571,0.0755,0.9355-0.1271,1.3631l-1.9874,4.1937 c-0.623,1.3146-2.3934,1.5533-3.331,0.4409c-3.1921-3.7871-8.5584-11.3899-6.5486-16.686 c7.0625-18.6104,15.8677-18.1429,15.8677-18.1429c2.8453-1.9336,13.1042-6.9375,24.8125,0.875c0,0,8.6323-1.7175,14.9375,16.9375 c1.8036,5.3362-3.4297,12.8668-6.5506,16.6442c-0.9312,1.127-2.7162,0.8939-3.3423-0.4272l-1.9741-4.1656 c-0.2026-0.4275-0.2491-0.906-0.1271-1.3631c0.6275-2.3509,2.4356-10.2161,0.119-15.3759"
/>
<path
fill="none"
stroke="#000000"
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit="10"
strokeWidth="2"
d="M52.6309,46.4628c0,0-3.0781,6.7216-7.8049,8.2712"
/>
<path
fill="none"
stroke="#000000"
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit="10"
strokeWidth="2"
d="M19.437,46.969c0,0,3.0781,6.0823,7.8049,7.632"
/>
<line
x1="36.2078"
x2="36.2078"
y1="47.3393"
y2="44.3093"
fill="none"
stroke="#000000"
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit="10"
strokeWidth="2"
/>
</g>
</svg>
<h2 class="title">Bruno ${version}</h2>
<footer class="footer">
©2025 Bruno Software Inc
</footer>
</body>
</html>
`;
};

View File

@@ -1,7 +1,8 @@
const { ipcMain } = require('electron');
const os = require('os');
const openAboutWindow = require('about-window').default;
const { join } = require('path');
const { BrowserWindow } = require('electron');
const { version } = require('../../package.json');
const aboutBruno = require('./about-bruno');
const template = [
{
@@ -77,14 +78,16 @@ const template = [
submenu: [
{
label: 'About Bruno',
click: () =>
openAboutWindow({
product_name: 'Bruno',
icon_path: join(__dirname, '../about/256x256.png'),
css_path: join(__dirname, '../about/about.css'),
homepage: 'https://www.usebruno.com/',
package_json_dir: join(__dirname, '../..')
})
click: () => {
const aboutWindow = new BrowserWindow({
width: 350,
height: 250,
webPreferences: {
nodeIntegration: true,
},
});
aboutWindow.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(aboutBruno({version}))}`);
}
},
{ label: 'Documentation', click: () => ipcMain.emit('main:open-docs') }
]

View File

@@ -32,7 +32,7 @@ const lastOpenedCollections = new LastOpenedCollections();
const contentSecurityPolicy = [
"default-src 'self'",
"connect-src 'self' https://*.posthog.com",
"font-src 'self' https:",
"font-src 'self' https: data:;",
"frame-src data:",
// this has been commented out to make oauth2 work
// "form-action 'none'",

View File

@@ -131,7 +131,7 @@ const browseFiles = async (win, filters = [], properties = []) => {
return [];
}
return filePaths.map((path) => path.resolve(path)).filter((path) => isFile(path));
return filePaths.map((filePath) => path.resolve(filePath)).filter((filePath) => isFile(filePath));
};
const chooseFileToSave = async (win, preferredFileName = '') => {

View File

@@ -142,11 +142,13 @@ const getOAuth2TokenUsingAuthorizationCode = async ({ request, collectionUid, fo
const axiosInstance = makeAxiosInstance();
// Interceptor to capture request data
axiosInstance.interceptors.request.use((config) => {
const requestData = typeof config?.data === 'string' ? config?.data : safeStringifyJSON(config?.data);
axiosRequestInfo = {
method: config.method.toUpperCase(),
url: config.url,
headers: config.headers,
data: config.data,
data: requestData,
dataBuffer: Buffer.from(requestData),
timestamp: Date.now(),
};
return config;
@@ -195,7 +197,8 @@ const getOAuth2TokenUsingAuthorizationCode = async ({ request, collectionUid, fo
url: axiosRequestInfo?.url,
method: axiosRequestInfo?.method,
headers: axiosRequestInfo?.headers || {},
body: axiosRequestInfo?.data,
data: axiosRequestInfo?.data,
dataBuffer: axiosRequestInfo?.dataBuffer,
error: null
},
response: {

View File

@@ -2,6 +2,7 @@ const os = require('os');
const fs = require('fs-extra');
const util = require('util');
const spawn = util.promisify(require('child_process').spawn);
const path = require('path');
async function deleteFileIfExists(filePath) {
try {
@@ -80,7 +81,7 @@ async function main() {
// Copy build
await copyFolderIfExists('packages/bruno-app/dist', 'packages/bruno-electron/web');
// Change paths in next
// Update static paths
const files = await fs.readdir('packages/bruno-electron/web');
for (const file of files) {
if (file.endsWith('.html')) {
@@ -90,6 +91,22 @@ async function main() {
}
}
// update font load paths
const cssDir = path.join('packages/bruno-electron/web/static/css');
try {
const cssFiles = await fs.readdir(cssDir);
for (const file of cssFiles) {
if (file.endsWith('.css')) {
const filePath = path.join(cssDir, file);
let content = await fs.readFile(filePath, 'utf8');
content = content.replace(/\/static\/font/g, '../../static/font');
await fs.writeFile(filePath, content);
}
}
} catch (error) {
console.error(`Error updating font paths: ${error}`);
}
// Remove sourcemaps
await removeSourceMapFiles('packages/bruno-electron/web');

View File

@@ -13,8 +13,9 @@ mkdir packages/bruno-electron/web
cp -r packages/bruno-app/dist/* packages/bruno-electron/web
# Change paths in next
# Update static paths
sed -i'' -e 's@/static/@static/@g' packages/bruno-electron/web/**.html
sed -i'' -e 's@/static/font@../../static/font@g' packages/bruno-electron/web/static/css/**.**.css
# Remove sourcemaps
find packages/bruno-electron/web -name '*.map' -type f -delete