mirror of
https://github.com/usebruno/bruno.git
synced 2024-11-28 19:03:15 +01:00
feat: support for sending multipart form data (resolves #12)
This commit is contained in:
parent
bd153bf849
commit
3bf18a1127
14
main/ipc.js
14
main/ipc.js
@ -1,10 +1,24 @@
|
|||||||
const axios = require('axios');
|
const axios = require('axios');
|
||||||
|
const FormData = require('form-data');
|
||||||
const { ipcMain } = require('electron');
|
const { ipcMain } = require('electron');
|
||||||
|
const { forOwn, extend } = require('lodash');
|
||||||
|
|
||||||
|
|
||||||
const registerIpc = () => {
|
const registerIpc = () => {
|
||||||
// handler for sending http request
|
// handler for sending http request
|
||||||
ipcMain.handle('send-http-request', async (event, request) => {
|
ipcMain.handle('send-http-request', async (event, request) => {
|
||||||
try {
|
try {
|
||||||
|
// make axios work in node using form data
|
||||||
|
// reference: https://github.com/axios/axios/issues/1006#issuecomment-320165427
|
||||||
|
if(request.headers && request.headers['content-type'] === 'multipart/form-data') {
|
||||||
|
const form = new FormData();
|
||||||
|
forOwn(request.data, (value, key) => {
|
||||||
|
form.append(key, value);
|
||||||
|
});
|
||||||
|
extend(request.headers, form.getHeaders());
|
||||||
|
request.data = form;
|
||||||
|
}
|
||||||
|
|
||||||
const result = await axios(request);
|
const result = await axios(request);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
12446
package-lock.json
generated
12446
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -32,6 +32,7 @@
|
|||||||
"electron-store": "^8.0.1",
|
"electron-store": "^8.0.1",
|
||||||
"electron-util": "^0.17.2",
|
"electron-util": "^0.17.2",
|
||||||
"escape-html": "^1.0.3",
|
"escape-html": "^1.0.3",
|
||||||
|
"form-data": "^4.0.0",
|
||||||
"formik": "^2.2.9",
|
"formik": "^2.2.9",
|
||||||
"fs-extra": "^10.0.1",
|
"fs-extra": "^10.0.1",
|
||||||
"graphiql": "^1.5.9",
|
"graphiql": "^1.5.9",
|
||||||
|
@ -0,0 +1,45 @@
|
|||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
const Wrapper = styled.div`
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-weight: 600;
|
||||||
|
|
||||||
|
thead, td {
|
||||||
|
border: 1px solid #efefef;
|
||||||
|
}
|
||||||
|
|
||||||
|
thead {
|
||||||
|
color: #616161;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
td {
|
||||||
|
padding: 6px 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-add-param {
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"] {
|
||||||
|
width: 100%;
|
||||||
|
border: solid 1px transparent;
|
||||||
|
outline: none !important;
|
||||||
|
|
||||||
|
&:focus{
|
||||||
|
outline: none !important;
|
||||||
|
border: solid 1px transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="checkbox"] {
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
top: 1px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default Wrapper;
|
119
renderer/components/RequestPane/MultipartFormParams/index.js
Normal file
119
renderer/components/RequestPane/MultipartFormParams/index.js
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import get from 'lodash/get';
|
||||||
|
import cloneDeep from 'lodash/cloneDeep';
|
||||||
|
import { IconTrash } from '@tabler/icons';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
import { addMultipartFormParam, updateMultipartFormParam, deleteMultipartFormParam } from 'providers/ReduxStore/slices/collections';
|
||||||
|
import StyledWrapper from './StyledWrapper';
|
||||||
|
|
||||||
|
const MultipartFormParams = ({item, collection}) => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const params = item.draft ? get(item, 'draft.request.body.multipartForm') : get(item, 'request.body.multipartForm');
|
||||||
|
|
||||||
|
const addParam = () => {
|
||||||
|
dispatch(addMultipartFormParam({
|
||||||
|
itemUid: item.uid,
|
||||||
|
collectionUid: collection.uid,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleParamChange = (e, _param, type) => {
|
||||||
|
const param = cloneDeep(_param);
|
||||||
|
switch(type) {
|
||||||
|
case 'name' : {
|
||||||
|
param.name = e.target.value;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'value' : {
|
||||||
|
param.value = e.target.value;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'description' : {
|
||||||
|
param.description = e.target.value;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'enabled' : {
|
||||||
|
param.enabled = e.target.checked;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dispatch(updateMultipartFormParam({
|
||||||
|
param: param,
|
||||||
|
itemUid: item.uid,
|
||||||
|
collectionUid: collection.uid
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveParams = (param) => {
|
||||||
|
dispatch(deleteMultipartFormParam({
|
||||||
|
paramUid: param.uid,
|
||||||
|
itemUid: item.uid,
|
||||||
|
collectionUid: collection.uid
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledWrapper className="w-full">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<td>Key</td>
|
||||||
|
<td>Value</td>
|
||||||
|
<td>Description</td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{params && params.length ? params.map((param, index) => {
|
||||||
|
return (
|
||||||
|
<tr key={param.uid}>
|
||||||
|
<td>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
autoComplete="off" autoCorrect="off" autoCapitalize="off" spellCheck="false"
|
||||||
|
value={param.name}
|
||||||
|
className="mousetrap"
|
||||||
|
onChange={(e) => handleParamChange(e, param, 'name')}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
autoComplete="off" autoCorrect="off" autoCapitalize="off" spellCheck="false"
|
||||||
|
value={param.value}
|
||||||
|
className="mousetrap"
|
||||||
|
onChange={(e) => handleParamChange(e, param, 'value')}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
autoComplete="off" autoCorrect="off" autoCapitalize="off" spellCheck="false"
|
||||||
|
value={param.description}
|
||||||
|
className="mousetrap"
|
||||||
|
onChange={(e) => handleParamChange(e, param, 'description')}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={param.enabled}
|
||||||
|
className="mr-3 mousetrap"
|
||||||
|
onChange={(e) => handleParamChange(e, param, 'enabled')}
|
||||||
|
/>
|
||||||
|
<button onClick={() => handleRemoveParams(param)}>
|
||||||
|
<IconTrash strokeWidth={1.5} size={20}/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}) : null}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<button className="btn-add-param text-link pr-2 py-3 mt-2 select-none" onClick={addParam}>+ Add Param</button>
|
||||||
|
</StyledWrapper>
|
||||||
|
)
|
||||||
|
};
|
||||||
|
export default MultipartFormParams;
|
@ -2,6 +2,7 @@ import React from 'react';
|
|||||||
import get from 'lodash/get';
|
import get from 'lodash/get';
|
||||||
import CodeEditor from 'components/CodeEditor';
|
import CodeEditor from 'components/CodeEditor';
|
||||||
import FormUrlEncodedParams from 'components/RequestPane/FormUrlEncodedParams';
|
import FormUrlEncodedParams from 'components/RequestPane/FormUrlEncodedParams';
|
||||||
|
import MultipartFormParams from 'components/RequestPane/MultipartFormParams';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import { updateRequestBody, sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections';
|
import { updateRequestBody, sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections';
|
||||||
import StyledWrapper from './StyledWrapper';
|
import StyledWrapper from './StyledWrapper';
|
||||||
@ -52,6 +53,10 @@ const RequestBody = ({item, collection}) => {
|
|||||||
return <FormUrlEncodedParams item={item} collection={collection}/>;
|
return <FormUrlEncodedParams item={item} collection={collection}/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(bodyMode === 'multipartForm') {
|
||||||
|
return <MultipartFormParams item={item} collection={collection}/>;
|
||||||
|
}
|
||||||
|
|
||||||
return(
|
return(
|
||||||
<StyledWrapper className="w-full">
|
<StyledWrapper className="w-full">
|
||||||
No Body
|
No Body
|
||||||
|
@ -398,6 +398,61 @@ export const collectionsSlice = createSlice({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
addMultipartFormParam: (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.multipartForm = item.draft.request.body.multipartForm || [];
|
||||||
|
item.draft.request.body.multipartForm.push({
|
||||||
|
uid: uuid(),
|
||||||
|
name: '',
|
||||||
|
value: '',
|
||||||
|
description: '',
|
||||||
|
enabled: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updateMultipartFormParam: (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.multipartForm, (p) => p.uid === action.payload.param.uid);
|
||||||
|
if(param) {
|
||||||
|
param.name = action.payload.param.name;
|
||||||
|
param.value = action.payload.param.value;
|
||||||
|
param.description = action.payload.param.description;
|
||||||
|
param.enabled = action.payload.param.enabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
deleteMultipartFormParam: (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.multipartForm = filter(item.draft.request.body.multipartForm, (p) => p.uid !== action.payload.paramUid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
updateRequestBodyMode: (state, action) => {
|
updateRequestBodyMode: (state, action) => {
|
||||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||||
|
|
||||||
@ -487,6 +542,9 @@ export const {
|
|||||||
addFormUrlEncodedParam,
|
addFormUrlEncodedParam,
|
||||||
updateFormUrlEncodedParam,
|
updateFormUrlEncodedParam,
|
||||||
deleteFormUrlEncodedParam,
|
deleteFormUrlEncodedParam,
|
||||||
|
addMultipartFormParam,
|
||||||
|
updateMultipartFormParam,
|
||||||
|
deleteMultipartFormParam,
|
||||||
updateRequestBodyMode,
|
updateRequestBodyMode,
|
||||||
updateRequestBody,
|
updateRequestBody,
|
||||||
updateRequestMethod
|
updateRequestMethod
|
||||||
|
@ -140,6 +140,18 @@ export const transformCollectionToSaveToIdb = (collection, options = {}) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const copyMultipartFormParams = (params = []) => {
|
||||||
|
return map(params, (param) => {
|
||||||
|
return {
|
||||||
|
uid: param.uid,
|
||||||
|
name: param.name,
|
||||||
|
value: param.value,
|
||||||
|
description: param.description,
|
||||||
|
enabled: param.enabled
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const copyItems = (sourceItems, destItems) => {
|
const copyItems = (sourceItems, destItems) => {
|
||||||
each(sourceItems, (si) => {
|
each(sourceItems, (si) => {
|
||||||
const di = {
|
const di = {
|
||||||
@ -163,7 +175,8 @@ export const transformCollectionToSaveToIdb = (collection, options = {}) => {
|
|||||||
text: si.draft.request.body.text,
|
text: si.draft.request.body.text,
|
||||||
xml: si.draft.request.body.xml,
|
xml: si.draft.request.body.xml,
|
||||||
multipartForm: si.draft.request.body.multipartForm,
|
multipartForm: si.draft.request.body.multipartForm,
|
||||||
formUrlEncoded: copyFormUrlEncodedParams(si.draft.request.body.formUrlEncoded)
|
formUrlEncoded: copyFormUrlEncodedParams(si.draft.request.body.formUrlEncoded),
|
||||||
|
multipartForm: copyMultipartFormParams(si.draft.request.body.multipartForm)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -180,7 +193,8 @@ export const transformCollectionToSaveToIdb = (collection, options = {}) => {
|
|||||||
text: si.request.body.text,
|
text: si.request.body.text,
|
||||||
xml: si.request.body.xml,
|
xml: si.request.body.xml,
|
||||||
multipartForm: si.request.body.multipartForm,
|
multipartForm: si.request.body.multipartForm,
|
||||||
formUrlEncoded: copyFormUrlEncodedParams(si.request.body.formUrlEncoded)
|
formUrlEncoded: copyFormUrlEncodedParams(si.request.body.formUrlEncoded),
|
||||||
|
multipartForm: copyMultipartFormParams(si.request.body.multipartForm)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -42,7 +42,7 @@ const sendHttpRequest = async (request) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if(request.body.mode === 'json') {
|
if(request.body.mode === 'json') {
|
||||||
options.headers['Content-Type'] = 'application/json';
|
options.headers['content-type'] = 'application/json';
|
||||||
try {
|
try {
|
||||||
options.data = JSON.parse(request.body.json);
|
options.data = JSON.parse(request.body.json);
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
@ -51,23 +51,31 @@ const sendHttpRequest = async (request) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if(request.body.mode === 'text') {
|
if(request.body.mode === 'text') {
|
||||||
options.headers['Content-Type'] = 'text/plain';
|
options.headers['content-type'] = 'text/plain';
|
||||||
options.data = request.body.text;
|
options.data = request.body.text;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(request.body.mode === 'xml') {
|
if(request.body.mode === 'xml') {
|
||||||
options.headers['Content-Type'] = 'text/xml';
|
options.headers['content-type'] = 'text/xml';
|
||||||
options.data = request.body.xml;
|
options.data = request.body.xml;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(request.body.mode === 'formUrlEncoded') {
|
if(request.body.mode === 'formUrlEncoded') {
|
||||||
options.headers['Content-Type'] = 'application/x-www-form-urlencoded';
|
options.headers['content-type'] = 'application/x-www-form-urlencoded';
|
||||||
const params = {};
|
const params = {};
|
||||||
const enabledParams = filter(request.body.formUrlEncoded, p => p.enabled);
|
const enabledParams = filter(request.body.formUrlEncoded, p => p.enabled);
|
||||||
each(enabledParams, (p) => params[p.name] = p.value);
|
each(enabledParams, (p) => params[p.name] = p.value);
|
||||||
options.data = qs.stringify(params);
|
options.data = qs.stringify(params);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(request.body.mode === 'multipartForm') {
|
||||||
|
const params = {};
|
||||||
|
const enabledParams = filter(request.body.multipartForm, p => p.enabled);
|
||||||
|
each(enabledParams, (p) => params[p.name] = p.value);
|
||||||
|
options.headers['content-type'] = 'multipart/form-data';
|
||||||
|
options.data = params;
|
||||||
|
}
|
||||||
|
|
||||||
console.log('>>> Sending Request');
|
console.log('>>> Sending Request');
|
||||||
console.log(options);
|
console.log(options);
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user