feat: refined implementation for raw binary files handling (#3734) (#3829)

---------
Co-authored-by: marcosadsj <marcosadsj@gmail.com>
Co-authored-by: naman-bruno <naman@usebruno.com>
Co-authored-by: sanish chirayath <sanish@usebruno.com>
This commit is contained in:
Sanjai Kumar
2025-01-28 13:51:50 +05:30
committed by GitHub
parent ecf883ba0d
commit ed56584ba6
25 changed files with 231 additions and 210 deletions

View File

@@ -34,7 +34,7 @@
"graphiql": "3.7.1",
"graphql": "^16.6.0",
"graphql-request": "^3.7.0",
"httpsnippet": "^3.0.6",
"httpsnippet": "^3.0.9",
"i18next": "24.1.2",
"idb": "^7.0.0",
"immer": "^9.0.15",

View File

@@ -78,7 +78,10 @@ const EnvironmentSelector = ({ collection }) => {
<IconDatabaseOff size={18} strokeWidth={1.5} />
<span className="ml-2">No Environment</span>
</div>
<div className="dropdown-item border-top" onClick={handleSettingsIconClick}>
<div className="dropdown-item border-top" onClick={() => {
handleSettingsIconClick();
dropdownTippyRef.current.hide();
}}>
<div className="pr-2 text-gray-600">
<IconSettings size={18} strokeWidth={1.5} />
</div>

View File

@@ -6,10 +6,9 @@ import { IconX } from '@tabler/icons';
import { isWindowsOS } from 'utils/common/platform';
import slash from 'utils/common/slash';
const FilePickerEditor = ({ value, onChange, collection, isSingleFilePicker = false}) => {
value = value || [];
const FilePickerEditor = ({ value, onChange, collection, isSingleFilePicker = false }) => {
const dispatch = useDispatch();
const filenames = value
const filenames = (isSingleFilePicker ? [value] : value || [])
.filter((v) => v != null && v != '')
.map((v) => {
const separator = isWindowsOS() ? '\\' : '/';
@@ -20,7 +19,7 @@ const FilePickerEditor = ({ value, onChange, collection, isSingleFilePicker = fa
const title = filenames.map((v) => `- ${v}`).join('\n');
const browse = () => {
dispatch(browseFiles([],['']))
dispatch(browseFiles([], [!isSingleFilePicker ? "multiSelections": ""]))
.then((filePaths) => {
// If file is in the collection's directory, then we use relative path
// Otherwise, we use the absolute path
@@ -34,7 +33,7 @@ const FilePickerEditor = ({ value, onChange, collection, isSingleFilePicker = fa
return filePath;
});
onChange(filePaths);
onChange(isSingleFilePicker ? filePaths[0] : filePaths);
})
.catch((error) => {
console.error(error);
@@ -42,7 +41,7 @@ const FilePickerEditor = ({ value, onChange, collection, isSingleFilePicker = fa
};
const clear = () => {
onChange([]);
onChange(isSingleFilePicker ? '' : []);
};
const renderButtonText = (filenames) => {
@@ -66,9 +65,9 @@ const FilePickerEditor = ({ value, onChange, collection, isSingleFilePicker = fa
</div>
) : (
<button className="btn btn-secondary px-1" style={{ width: '100%' }} onClick={browse}>
{isSingleFilePicker? 'Select File' : 'Select Files'}
{isSingleFilePicker ? 'Select File' : 'Select Files'}
</button>
);
};
export default FilePickerEditor;
export default FilePickerEditor;

View File

@@ -55,7 +55,7 @@ const StyledMarkdownBodyWrapper = styled.div`
height: 1px;
padding: 0;
margin: 24px 0;
background-color: var(--color-border-default);
background-color: var(--color-sidebar-collection-item-active-indent-border);
border: 0;
}

View File

@@ -62,7 +62,7 @@ const Modal = ({
confirmText,
cancelText,
handleCancel,
handleConfirm,
handleConfirm = () => {},
children,
confirmDisabled,
hideCancel,
@@ -92,7 +92,7 @@ const Modal = ({
};
useFocusTrap(modalRef);
const closeModal = (args) => {
setIsClosing(true);
setTimeout(() => handleCancel(args), closeModalFadeTimeout);
@@ -103,7 +103,7 @@ const Modal = ({
return () => {
document.removeEventListener('keydown', handleKeydown);
};
}, [disableEscapeKey, document]);
}, [disableEscapeKey, document, handleConfirm]);
let classes = 'bruno-modal';
if (isClosing) {

View File

@@ -1,21 +1,13 @@
import React from 'react';
import get from 'lodash/get';
import cloneDeep from 'lodash/cloneDeep';
import React, { useState, useEffect } from 'react';
import { get, cloneDeep, isArray } from 'lodash';
import { IconTrash } from '@tabler/icons';
import { useDispatch } from 'react-redux';
import { useTheme } from 'providers/Theme';
import {
addBinaryFile,
updateBinaryFile,
deleteBinaryFile
} from 'providers/ReduxStore/slices/collections';
import { addBinaryFile, updateBinaryFile, deleteBinaryFile } from 'providers/ReduxStore/slices/collections/index';
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';
import FilePickerEditor from 'components/FilePickerEditor';
import FilePickerEditor from 'components/FilePickerEditor/index';
import SingleLineEditor from 'components/SingleLineEditor/index';
import { isArray } from 'lodash';
import path from 'node:path';
import { useState } from 'react';
const Binary = ({ item, collection }) => {
const dispatch = useDispatch();
@@ -29,35 +21,28 @@ const Binary = ({ item, collection }) => {
addBinaryFile({
itemUid: item.uid,
collectionUid: collection.uid,
type: 'binaryFile',
value: [''],
})
);
};
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
const handleRun = () => dispatch(sendRequest(item, collection.uid));
const handleParamChange = (e, _param, type) => {
const param = cloneDeep(_param);
switch (type) {
case 'value': {
param.value = isArray(e.target.value) && e.target.value.length > 0 ? e.target.value : [''];
param.name = param.value.length === 0 ? '': path.basename(param.value[0], path.extname(param.value[0]));
case 'filePath': {
param.filePath = e.target.filePath;
param.contentType = "";
break;
}
case 'contentType': {
param.contentType = e.target.value;
param.contentType = e.target.contentType;
break;
}
case 'enabled': {
param.enabled = e.target.checked;
setEnableFileUid(param.uid);
case 'selected': {
param.selected = e.target.selected;
setEnableFileUid(param.uid)
break;
}
}
@@ -85,9 +70,15 @@ const Binary = ({ item, collection }) => {
<table>
<thead>
<tr>
<td><div className="flex items-center justify-center">File</div></td>
<td><div className="flex items-center justify-center">Content-Type</div></td>
<td><div className="flex items-center justify-center">Enabled</div></td>
<td>
<div className="flex items-center justify-center">File</div>
</td>
<td>
<div className="flex items-center justify-center">Content-Type</div>
</td>
<td>
<div className="flex items-center justify-center">Selected</div>
</td>
<td></td>
</tr>
</thead>
@@ -97,26 +88,26 @@ const Binary = ({ item, collection }) => {
return (
<tr key={param.uid}>
<td>
<FilePickerEditor
isSingleFilePicker={true}
value={param.value}
onChange={(newValue) =>
handleParamChange(
{
target: {
value: newValue
}
},
param,
'value'
)
}
collection={collection}
/>
<FilePickerEditor
isSingleFilePicker={true}
value={param.filePath}
onChange={(path) =>
handleParamChange(
{
target: {
filePath: path
}
},
param,
'filePath'
)
}
collection={collection}
/>
</td>
<td>
<SingleLineEditor
className='flex items-center justify-center'
className="flex items-center justify-center"
onSave={onSave}
theme={storedTheme}
placeholder="Auto"
@@ -125,7 +116,7 @@ const Binary = ({ item, collection }) => {
handleParamChange(
{
target: {
value: newValue
contentType: newValue
}
},
param,
@@ -141,19 +132,19 @@ const Binary = ({ item, collection }) => {
<input
key={param.uid}
type="radio"
name="enabled"
checked={enabledFileUid === param.uid || param.enabled}
name="selected"
checked={enabledFileUid === param.uid || param.selected}
tabIndex="-1"
className="mr-1 mousetrap"
onChange={(e) => handleParamChange(e, param, 'enabled')}
onChange={(e) => handleParamChange(e, param, 'selected')}
/>
</div>
</td>
<td>
<div className="flex items-center justify-center">
<button tabIndex="-1" onClick={() => handleRemoveParams(param)}>
<IconTrash strokeWidth={1.5} size={20} />
</button>
<div className="flex items-center justify-center">
<button tabIndex="-1" onClick={() => handleRemoveParams(param)}>
<IconTrash strokeWidth={1.5} size={20} />
</button>
</div>
</td>
</tr>
@@ -170,4 +161,4 @@ const Binary = ({ item, collection }) => {
</StyledWrapper>
);
};
export default Binary;
export default Binary;

View File

@@ -135,7 +135,7 @@ const RequestBodyMode = ({ item, collection }) => {
onModeChange('binaryFile');
}}
>
Binary File
File / Binary
</div>
<div
className="dropdown-item"

View File

@@ -152,6 +152,8 @@ const Collection = ({ collection, searchText }) => {
<div className="flex py-1 collection-name items-center" ref={drop}>
<div
className="flex flex-grow items-center overflow-hidden"
onClick={handleCollapseCollection}
onContextMenu={handleRightClick}
>
<IconChevronRight
size={16}
@@ -160,9 +162,7 @@ const Collection = ({ collection, searchText }) => {
style={{ width: 16, minWidth: 16, color: 'rgb(160 160 160)' }}
onClick={handleClick}
/>
<div className="ml-1" id="sidebar-collection-name"
onClick={handleCollapseCollection}
onContextMenu={handleRightClick}>
<div className="ml-1" id="sidebar-collection-name">
{collection.name}
</div>
</div>

View File

@@ -68,7 +68,7 @@ const ImportCollection = ({ onClose, handleSubmit }) => {
);
};
return (
<Modal size="sm" title="Import Collection" hideFooter={true} handleConfirm={onClose} handleCancel={onClose}>
<Modal size="sm" title="Import Collection" hideFooter={true} handleCancel={onClose}>
<div className="flex flex-col">
<h3 className="text-sm">Select the type of your existing collection :</h3>
<div className="mt-4 grid grid-rows-2 grid-flow-col gap-2">

View File

@@ -1041,13 +1041,16 @@ export const browseDirectory = () => (dispatch, getState) => {
export const browseFiles =
(filters = [], properties = ['multiSelections']) =>
(dispatch, getState) => {
(_dispatch, _getState) => {
const { ipcRenderer } = window;
return new Promise((resolve, reject) => {
ipcRenderer.invoke('renderer:browse-files', undefined, undefined, undefined, filters, properties).then(resolve).catch(reject);
ipcRenderer
.invoke('renderer:browse-files', filters, properties)
.then(resolve)
.catch(reject);
});
};
};
export const updateBrunoConfig = (brunoConfig, collectionUid) => (dispatch, getState) => {
const state = getState();

View File

@@ -865,80 +865,82 @@ export const collectionsSlice = createSlice({
}
}
},
moveMultipartFormParam: (state, action) => {
// Ensure item.draft is a deep clone of item if not already present
if (!item.draft) {
item.draft = cloneDeep(item);
}
// Extract payload data
const { updateReorderedItem } = action.payload;
const params = item.draft.request.body.multipartForm;
item.draft.request.body.multipartForm = updateReorderedItem.map((uid) => {
return params.find((param) => param.uid === uid);
});
},
addBinaryFile: (state, action) => {
moveMultipartFormParam: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
if (collection) {
const item = findItemInCollection(collection, action.payload.itemUid);
if (item && isItemARequest(item)) {
// Ensure item.draft is a deep clone of item if not already present
if (!item.draft) {
item.draft = cloneDeep(item);
}
// Extract payload data
const { updateReorderedItem } = action.payload;
const params = item.draft.request.body.multipartForm;
item.draft.request.body.multipartForm = updateReorderedItem.map((uid) => {
return params.find((param) => param.uid === uid);
});
}
}
},
addBinaryFile: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
if (collection) {
const item = findItemInCollection(collection, action.payload.itemUid);
if (item && isItemARequest(item)) {
if (!item.draft) {
item.draft = cloneDeep(item);
}
item.draft.request.body.binaryFile = item.draft.request.body.binaryFile || [];
item.draft.request.body.binaryFile.push({
uid: uuid(),
type: action.payload.type,
name: '',
value: [''],
filePath: '',
contentType: '',
enabled: false
selected: false
});
}
}
},
updateBinaryFile: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
if (collection) {
const item = findItemInCollection(collection, action.payload.itemUid);
if (item && isItemARequest(item)) {
if (!item.draft) {
item.draft = cloneDeep(item);
}
item.draft.request.body.binaryFile = item.draft.request.body.binaryFile.map((p) => {
p.enabled = false;
return p;
});
const param = find(item.draft.request.body.binaryFile, (p) => p.uid === action.payload.param.uid);
if (param) {
const contentType = mime.contentType(path.extname(action.payload.param.value[0]));
param.type = action.payload.param.type;
param.name = action.payload.param.name;
param.value = action.payload.param.value;
const contentType = mime.contentType(path.extname(action.payload.param.filePath));
param.filePath = action.payload.param.filePath;
param.contentType = action.payload.param.contentType || contentType || '';
param.enabled = action.payload.param.enabled;
param.selected = action.payload.param.selected;
item.draft.request.body.binaryFile = item.draft.request.body.binaryFile.map((p) => {
p.selected = p.uid === param.uid;
return p;
});
}
}
}
},
deleteBinaryFile: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
if (collection) {
const item = findItemInCollection(collection, action.payload.itemUid);
if (item && isItemARequest(item)) {
if (!item.draft) {
item.draft = cloneDeep(item);
@@ -948,6 +950,10 @@ export const collectionsSlice = createSlice({
item.draft.request.body.binaryFile,
(p) => p.uid !== action.payload.paramUid
);
if (item.draft.request.body.binaryFile.length > 0) {
item.draft.request.body.binaryFile[0].selected = true;
}
}
}
},

View File

@@ -65,6 +65,23 @@ const createPostData = (body, type) => {
switch (body.mode) {
case 'formUrlEncoded':
return {
mimeType: contentType,
text: new URLSearchParams(
body[body.mode]
.filter((param) => param.enabled)
.reduce((acc, param) => {
acc[param.name] = param.value;
return acc;
}, {})
).toString(),
params: body[body.mode]
.filter((param) => param.enabled)
.map((param) => ({
name: param.name,
value: param.value
}))
};
case 'multipartForm':
return {
mimeType: contentType,
@@ -78,18 +95,13 @@ const createPostData = (body, type) => {
};
case 'binaryFile':
const binary = {
mimeType: 'application/octet-stream',
// mimeType: body[body.mode].filter((param) => param.enabled)[0].contentType,
mimeType: body[body.mode].filter((param) => param.enabled)[0].contentType,
params: body[body.mode]
.filter((param) => param.enabled)
.filter((param) => param.selected)
.map((param) => ({
name: param.name,
value: param.value,
fileName: param.value
value: param.filePath,
}))
};
console.log('curl-binary', binary);
return binary;
default:
return {
@@ -100,10 +112,6 @@ const createPostData = (body, type) => {
};
export const buildHarRequest = ({ request, headers, type }) => {
console.log('buildHarRequest', request, headers, type);
console.log('buildHarRequest-postData', createPostData(request.body, type));
return {
method: request.method,
url: encodeURI(request.url),

View File

@@ -275,11 +275,9 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {}
return map(params, (param) => {
return {
uid: param.uid,
type: param.type,
name: param.name,
value: param.value,
filePath: param.filePath,
contentType: param.contentType,
enabled: param.enabled
selected: param.selected
}
});
}
@@ -666,7 +664,7 @@ export const humanizeRequestBodyMode = (mode) => {
break;
}
case 'binaryFile': {
label = 'Binary File';
label = 'File / Binary';
break;
}
case 'formUrlEncoded': {

View File

@@ -102,30 +102,24 @@ function getFilesString(request) {
data.data = {};
if (request.isDataBinary){
if (request.isDataBinary) {
let filePath = '';
let filePath = ''
if(request.data.startsWith('@')){
if (request.data.startsWith('@')) {
filePath = request.data.slice(1);
}else{
} else {
filePath = request.data;
}
const fileName = path.basename(filePath);
data.data = [
{
name: repr(fileName),
value: [repr(filePath)],
enabled: true,
filePath: repr(filePath),
contentType: request.headers['Content-Type'],
type: 'binaryFile'
selected: true,
}
];
return data;
}
data.files = {};
@@ -190,13 +184,11 @@ const curlToJson = (curlCommand) => {
if (request.query) {
requestJson.queries = getQueries(request);
}
else if (request.multipartUploads || request.isDataBinary) {
} else if (request.multipartUploads || request.isDataBinary) {
Object.assign(requestJson, getFilesString(request));
} else if (typeof request.data === 'string' || typeof request.data === 'number') {
Object.assign(requestJson, getDataString(request));
}
}
if (request.insecure) {
requestJson.insecure = false;