mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-15 11:51:30 +00:00
Merge remote-tracking branch 'upstream/main' into feat/oauth2-improvements
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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 :</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'} :</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 :</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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -810,7 +810,7 @@ export const newHttpRequest = (params) => (dispatch, getState) => {
|
||||
file: null
|
||||
},
|
||||
auth: auth ?? {
|
||||
mode: 'none'
|
||||
mode: 'inherit'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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.";
|
||||
}
|
||||
|
||||
176
packages/bruno-electron/src/app/about-bruno.js
Normal file
176
packages/bruno-electron/src/app/about-bruno.js
Normal 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>
|
||||
`;
|
||||
};
|
||||
@@ -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') }
|
||||
]
|
||||
|
||||
@@ -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'",
|
||||
|
||||
@@ -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 = '') => {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user