raw file body - fixes (#3913)

---------

Co-authored-by: lohit <lohit@usebruno.com>
Co-authored-by: Sanjai Kumar <161328623+sanjaikumar-bruno@users.noreply.github.com>
This commit is contained in:
lohit
2025-02-04 12:27:45 +05:30
committed by GitHub
parent 364b0f523a
commit a075cc71d4
41 changed files with 612 additions and 176 deletions

View File

@@ -6,10 +6,10 @@ import { IconX } from '@tabler/icons';
import { isWindowsOS } from 'utils/common/platform';
import slash from 'utils/common/slash';
const FilePickerEditor = ({ value, onChange, collection, isSingleFilePicker = false}) => {
const FilePickerEditor = ({ value, onChange, collection, isSingleFilePicker = false }) => {
value = value || [];
const dispatch = useDispatch();
const filenames = value
const filenames = (isSingleFilePicker ? [value] : value || [])
.filter((v) => v != null && v != '')
.map((v) => {
const separator = isWindowsOS() ? '\\' : '/';
@@ -20,7 +20,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 +34,7 @@ const FilePickerEditor = ({ value, onChange, collection, isSingleFilePicker = fa
return filePath;
});
onChange(filePaths);
onChange(isSingleFilePicker ? filePaths[0] : filePaths);
})
.catch((error) => {
console.error(error);
@@ -42,7 +42,7 @@ const FilePickerEditor = ({ value, onChange, collection, isSingleFilePicker = fa
};
const clear = () => {
onChange([]);
onChange(isSingleFilePicker ? '' : []);
};
const renderButtonText = (filenames) => {
@@ -66,9 +66,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

@@ -0,0 +1,65 @@
import styled from 'styled-components';
const Wrapper = styled.div`
table {
width: 100%;
border-collapse: collapse;
font-weight: 600;
table-layout: fixed;
thead,
td {
border: 1px solid ${(props) => props.theme.table.border};
}
thead {
color: ${(props) => props.theme.table.thead.color};
font-size: 0.8125rem;
user-select: none;
}
td {
padding: 6px 10px;
&:nth-child(1) {
width: 30%;
}
&:nth-child(2) {
width: 45%;
}
&:nth-child(3) {
width: 25%;
}
&:nth-child(4) {
width: 70px;
}
}
}
.btn-add-param {
font-size: 0.8125rem;
}
input[type='text'] {
width: 100%;
border: solid 1px transparent;
outline: none !important;
color: ${(props) => props.theme.table.input.color};
background: transparent;
&:focus {
outline: none !important;
border: solid 1px transparent;
}
}
input[type='radio'] {
cursor: pointer;
position: relative;
top: 1px;
}
`;
export default Wrapper;

View File

@@ -0,0 +1,164 @@
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 { addFileParam, updateFileParam, deleteFileParam } from 'providers/ReduxStore/slices/collections/index';
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';
import FilePickerEditor from 'components/FilePickerEditor/index';
import SingleLineEditor from 'components/SingleLineEditor/index';
const FileBody = ({ item, collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const params = item.draft ? get(item, 'draft.request.body.file') : get(item, 'request.body.file');
const [enabledFileUid, setEnableFileUid] = useState(params && params.length ? params[0].uid : '');
const addFile = () => {
dispatch(
addFileParam({
itemUid: item.uid,
collectionUid: collection.uid,
})
);
};
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 'filePath': {
param.filePath = e.target.filePath;
param.contentType = "";
break;
}
case 'contentType': {
param.contentType = e.target.contentType;
break;
}
case 'selected': {
param.selected = e.target.selected;
setEnableFileUid(param.uid)
break;
}
}
dispatch(
updateFileParam({
param: param,
itemUid: item.uid,
collectionUid: collection.uid
})
);
};
const handleRemoveParams = (param) => {
dispatch(
deleteFileParam({
paramUid: param.uid,
itemUid: item.uid,
collectionUid: collection.uid
})
);
};
return (
<StyledWrapper className="w-full">
<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">Selected</div>
</td>
<td></td>
</tr>
</thead>
<tbody>
{params && params.length
? params.map((param, index) => {
return (
<tr key={param.uid}>
<td>
<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"
onSave={onSave}
theme={storedTheme}
placeholder="Auto"
value={param.contentType}
onChange={(newValue) =>
handleParamChange(
{
target: {
contentType: newValue
}
},
param,
'contentType'
)
}
onRun={handleRun}
collection={collection}
/>
</td>
<td>
<div className="flex items-center justify-center">
<input
key={param.uid}
type="radio"
name="selected"
checked={enabledFileUid === param.uid || param.selected}
tabIndex="-1"
className="mr-1 mousetrap"
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>
</td>
</tr>
);
})
: null}
</tbody>
</table>
<div>
<button className="btn-add-param text-link pr-2 pt-3 select-none" onClick={addFile}>
+ Add File
</button>
</div>
</StyledWrapper>
);
};
export default FileBody;

View File

@@ -132,10 +132,10 @@ const RequestBodyMode = ({ item, collection }) => {
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
onModeChange('binaryFile');
onModeChange('file');
}}
>
Binary File
File / Binary
</div>
<div
className="dropdown-item"

View File

@@ -8,7 +8,7 @@ import { useTheme } from 'providers/Theme';
import { updateRequestBody } from 'providers/ReduxStore/slices/collections';
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';
import Binary from '../Binary/index';
import FileBody from '../FileBody/index';
const RequestBody = ({ item, collection }) => {
const dispatch = useDispatch();
@@ -63,8 +63,8 @@ const RequestBody = ({ item, collection }) => {
);
}
if (bodyMode === 'binaryFile') {
return <Binary item={item} collection={collection}/>
if (bodyMode === 'file') {
return <FileBody item={item} collection={collection}/>
}
if (bodyMode === 'formUrlEncoded') {

View File

@@ -759,7 +759,7 @@ export const newHttpRequest = (params) => (dispatch, getState) => {
sparql: null,
multipartForm: null,
formUrlEncoded: null,
binaryFile: null
file: null
},
auth: auth ?? {
mode: 'none'
@@ -1040,13 +1040,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

@@ -961,6 +961,76 @@ export const collectionsSlice = createSlice({
}
}
},
addFileParam: (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.file = item.draft.request.body.file || [];
item.draft.request.body.file.push({
uid: uuid(),
filePath: '',
contentType: '',
selected: false
});
}
}
},
updateFileParam: (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);
}
const param = find(item.draft.request.body.file, (p) => p.uid === action.payload.param.uid);
if (param) {
const contentType = mime.contentType(path.extname(action.payload.param.filePath));
param.filePath = action.payload.param.filePath;
param.contentType = action.payload.param.contentType || contentType || '';
param.selected = action.payload.param.selected;
item.draft.request.body.file = item.draft.request.body.file.map((p) => {
p.selected = p.uid === param.uid;
return p;
});
}
}
}
},
deleteFileParam: (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.file = filter(
item.draft.request.body.file,
(p) => p.uid !== action.payload.paramUid
);
if (item.draft.request.body.file.length > 0) {
item.draft.request.body.file[0].selected = true;
}
}
}
},
updateRequestAuthMode: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
@@ -1017,8 +1087,8 @@ export const collectionsSlice = createSlice({
item.draft.request.body.sparql = action.payload.content;
break;
}
case 'binaryFile': {
item.draft.request.body.binaryFile = action.payload.content;
case 'file': {
item.draft.request.body.file = action.payload.content;
break;
}
case 'formUrlEncoded': {
@@ -2022,9 +2092,9 @@ export const {
addMultipartFormParam,
updateMultipartFormParam,
deleteMultipartFormParam,
addBinaryFile,
updateBinaryFile,
deleteBinaryFile,
addFileParam,
updateFileParam,
deleteFileParam,
moveMultipartFormParam,
updateRequestAuthMode,
updateRequestBodyMode,

View File

@@ -14,7 +14,7 @@ const createContentType = (mode) => {
return 'application/json';
case 'multipartForm':
return 'multipart/form-data';
case 'binaryFile':
case 'file':
return 'application/octet-stream';
default:
return '';
@@ -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,
@@ -76,20 +93,15 @@ const createPostData = (body, type) => {
...(param.type === 'file' && { fileName: param.value })
}))
};
case 'binaryFile':
case 'file':
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 {

View File

@@ -14,7 +14,7 @@ export const deleteUidsInItems = (items) => {
each(get(item, 'request.vars.assertions'), (a) => delete a.uid);
each(get(item, 'request.body.multipartForm'), (param) => delete param.uid);
each(get(item, 'request.body.formUrlEncoded'), (param) => delete param.uid);
each(get(item, 'request.body.binaryFile'), (param) => delete param.uid);
each(get(item, 'request.body.file'), (param) => delete param.uid);
}
if (item.items && item.items.length) {

View File

@@ -281,15 +281,13 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {}
});
};
const copyBinaryFileParams = (params = []) => {
const copyFileParams = (params = []) => {
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
}
});
}
@@ -322,7 +320,7 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {}
sparql: si.request.body.sparql,
formUrlEncoded: copyFormUrlEncodedParams(si.request.body.formUrlEncoded),
multipartForm: copyMultipartFormParams(si.request.body.multipartForm),
binaryFile: copyBinaryFileParams(si.request.body.binaryFile)
file: copyFileParams(si.request.body.file)
},
script: si.request.script,
vars: si.request.vars,
@@ -675,8 +673,8 @@ export const humanizeRequestBodyMode = (mode) => {
label = 'SPARQL';
break;
}
case 'binaryFile': {
label = 'Binary File';
case 'file': {
label = 'File / Binary';
break;
}
case 'formUrlEncoded': {
@@ -779,7 +777,7 @@ export const refreshUidsInItem = (item) => {
each(get(item, 'request.params'), (param) => (param.uid = uuid()));
each(get(item, 'request.body.multipartForm'), (param) => (param.uid = uuid()));
each(get(item, 'request.body.formUrlEncoded'), (param) => (param.uid = uuid()));
each(get(item, 'request.body.binaryFile'), (param) => (param.uid = uuid()));
each(get(item, 'request.body.file'), (param) => (param.uid = uuid()));
return item;
};
@@ -790,13 +788,13 @@ export const deleteUidsInItem = (item) => {
const headers = get(item, 'request.headers', []);
const bodyFormUrlEncoded = get(item, 'request.body.formUrlEncoded', []);
const bodyMultipartForm = get(item, 'request.body.multipartForm', []);
const binaryFile = get(item, 'request.body.binaryFile', []);
const file = get(item, 'request.body.file', []);
params.forEach((param) => delete param.uid);
headers.forEach((header) => delete header.uid);
bodyFormUrlEncoded.forEach((param) => delete param.uid);
bodyMultipartForm.forEach((param) => delete param.uid);
binaryFile.forEach((param) => delete param.uid);
file.forEach((param) => delete param.uid);
return item;
};

View File

@@ -101,31 +101,24 @@ function getFilesString(request) {
const data = {};
data.data = {};
if (request.isDataBinary) {
let filePath = '';
if (request.isDataBinary){
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 +183,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;

View File

@@ -117,7 +117,7 @@ describe('curlToJson', () => {
value: ['/path/to/file'],
enabled: true,
contentType: 'application/json;charset=utf-8',
type: 'binaryFile'
type: 'file'
}
]
});

View File

@@ -51,7 +51,7 @@ export const getRequestFromCurlCommand = (curlCommand, requestType = 'http-reque
multipartForm: null,
formUrlEncoded: null,
graphql: null,
binaryFile: null
file: null
};
if (parsedBody && contentType && typeof contentType === 'string') {
@@ -59,8 +59,8 @@ export const getRequestFromCurlCommand = (curlCommand, requestType = 'http-reque
body.mode = 'graphql';
body.graphql = parseGraphQL(parsedBody);
} else if (requestType === 'http-request' && request.isDataBinary) {
body.mode = 'binaryFile';
body.binaryFile = parsedBody;
body.mode = 'file';
body.file = parsedBody;
}else if (contentType.includes('application/json')) {
body.mode = 'json';
body.json = convertToCodeMirrorJson(parsedBody);

View File

@@ -35,7 +35,7 @@ export const updateUidsInCollection = (_collection) => {
each(get(item, 'request.assertions'), (a) => (a.uid = uuid()));
each(get(item, 'request.body.multipartForm'), (param) => (param.uid = uuid()));
each(get(item, 'request.body.formUrlEncoded'), (param) => (param.uid = uuid()));
each(get(item, 'request.body.binaryFile'), (param) => (param.uid = uuid()));
each(get(item, 'request.body.file'), (param) => (param.uid = uuid()));
if (item.items && item.items.length) {
updateItemUids(item.items);