feat: support for sending multipart form data (resolves #12)

This commit is contained in:
Anoop M D 2022-10-05 19:06:13 +05:30
parent bd153bf849
commit 3bf18a1127
9 changed files with 11154 additions and 1570 deletions

View File

@ -1,10 +1,24 @@
const axios = require('axios');
const FormData = require('form-data');
const { ipcMain } = require('electron');
const { forOwn, extend } = require('lodash');
const registerIpc = () => {
// handler for sending http request
ipcMain.handle('send-http-request', async (event, request) => {
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);
return {

12446
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -32,6 +32,7 @@
"electron-store": "^8.0.1",
"electron-util": "^0.17.2",
"escape-html": "^1.0.3",
"form-data": "^4.0.0",
"formik": "^2.2.9",
"fs-extra": "^10.0.1",
"graphiql": "^1.5.9",

View File

@ -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;

View 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;

View File

@ -2,6 +2,7 @@ import React from 'react';
import get from 'lodash/get';
import CodeEditor from 'components/CodeEditor';
import FormUrlEncodedParams from 'components/RequestPane/FormUrlEncodedParams';
import MultipartFormParams from 'components/RequestPane/MultipartFormParams';
import { useDispatch } from 'react-redux';
import { updateRequestBody, sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections';
import StyledWrapper from './StyledWrapper';
@ -52,6 +53,10 @@ const RequestBody = ({item, collection}) => {
return <FormUrlEncodedParams item={item} collection={collection}/>;
}
if(bodyMode === 'multipartForm') {
return <MultipartFormParams item={item} collection={collection}/>;
}
return(
<StyledWrapper className="w-full">
No Body

View File

@ -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) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
@ -487,6 +542,9 @@ export const {
addFormUrlEncodedParam,
updateFormUrlEncodedParam,
deleteFormUrlEncodedParam,
addMultipartFormParam,
updateMultipartFormParam,
deleteMultipartFormParam,
updateRequestBodyMode,
updateRequestBody,
updateRequestMethod

View File

@ -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) => {
each(sourceItems, (si) => {
const di = {
@ -163,7 +175,8 @@ export const transformCollectionToSaveToIdb = (collection, options = {}) => {
text: si.draft.request.body.text,
xml: si.draft.request.body.xml,
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,
xml: si.request.body.xml,
multipartForm: si.request.body.multipartForm,
formUrlEncoded: copyFormUrlEncodedParams(si.request.body.formUrlEncoded)
formUrlEncoded: copyFormUrlEncodedParams(si.request.body.formUrlEncoded),
multipartForm: copyMultipartFormParams(si.request.body.multipartForm)
}
}
};

View File

@ -42,7 +42,7 @@ const sendHttpRequest = async (request) => {
};
if(request.body.mode === 'json') {
options.headers['Content-Type'] = 'application/json';
options.headers['content-type'] = 'application/json';
try {
options.data = JSON.parse(request.body.json);
} catch (ex) {
@ -51,23 +51,31 @@ const sendHttpRequest = async (request) => {
}
if(request.body.mode === 'text') {
options.headers['Content-Type'] = 'text/plain';
options.headers['content-type'] = 'text/plain';
options.data = request.body.text;
}
if(request.body.mode === 'xml') {
options.headers['Content-Type'] = 'text/xml';
options.headers['content-type'] = 'text/xml';
options.data = request.body.xml;
}
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 enabledParams = filter(request.body.formUrlEncoded, p => p.enabled);
each(enabledParams, (p) => params[p.name] = p.value);
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(options);