diff --git a/packages/bruno-app/src/components/FilePickerEditor/index.js b/packages/bruno-app/src/components/FilePickerEditor/index.js
new file mode 100644
index 000000000..a8b33c653
--- /dev/null
+++ b/packages/bruno-app/src/components/FilePickerEditor/index.js
@@ -0,0 +1,66 @@
+import React from 'react';
+import { useDispatch } from 'react-redux';
+import { browseFiles } from 'providers/ReduxStore/slices/collections/actions';
+import { IconX } from '@tabler/icons';
+
+const FilePickerEditor = ({ value, onChange, collection }) => {
+ const dispatch = useDispatch();
+ const filnames = value
+ .split('|')
+ .filter((v) => v != null && v != '')
+ .map((v) => v.split('\\').pop());
+ const title = filnames.map((v) => `- ${v}`).join('\n');
+
+ const browse = () => {
+ dispatch(browseFiles())
+ .then((filePaths) => {
+ // If file is in the collection's directory, then we use relative path
+ // Otherwise, we use the absolute path
+ filePaths = filePaths.map((filePath) => {
+ const collectionDir = collection.pathname;
+
+ if (filePath.startsWith(collectionDir)) {
+ return filePath.substring(collectionDir.length + 1);
+ }
+
+ return filePath;
+ });
+
+ onChange(filePaths.join('|'));
+ })
+ .catch((error) => {
+ console.error(error);
+ });
+ };
+
+ const clear = () => {
+ onChange('');
+ };
+
+ const renderButtonText = (filnames) => {
+ if (filnames.length == 1) {
+ return filnames[0];
+ }
+ return filnames.length + ' files selected';
+ };
+
+ return filnames.length > 0 ? (
+
+
+
+ {renderButtonText(filnames)}
+
+ ) : (
+
+ );
+};
+
+export default FilePickerEditor;
diff --git a/packages/bruno-app/src/components/RequestPane/MultipartFormParams/index.js b/packages/bruno-app/src/components/RequestPane/MultipartFormParams/index.js
index f8bd7ebbd..13464c6c9 100644
--- a/packages/bruno-app/src/components/RequestPane/MultipartFormParams/index.js
+++ b/packages/bruno-app/src/components/RequestPane/MultipartFormParams/index.js
@@ -12,6 +12,7 @@ import {
import SingleLineEditor from 'components/SingleLineEditor';
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';
+import FilePickerEditor from 'components/FilePickerEditor/index';
const MultipartFormParams = ({ item, collection }) => {
const dispatch = useDispatch();
@@ -27,6 +28,16 @@ const MultipartFormParams = ({ item, collection }) => {
);
};
+ const addFile = () => {
+ dispatch(
+ addMultipartFormParam({
+ itemUid: item.uid,
+ collectionUid: collection.uid,
+ isFile: true
+ })
+ );
+ };
+
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
const handleRun = () => dispatch(sendRequest(item, collection.uid));
const handleParamChange = (e, _param, type) => {
@@ -92,24 +103,42 @@ const MultipartFormParams = ({ item, collection }) => {
/>
-
- handleParamChange(
- {
- target: {
- value: newValue
- }
- },
- param,
- 'value'
- )
- }
- onRun={handleRun}
- collection={collection}
- />
+ {param.isFile === true ? (
+
+ handleParamChange(
+ {
+ target: {
+ value: newValue
+ }
+ },
+ param,
+ 'value'
+ )
+ }
+ collection={collection}
+ />
+ ) : (
+
+ handleParamChange(
+ {
+ target: {
+ value: newValue
+ }
+ },
+ param,
+ 'value'
+ )
+ }
+ onRun={handleRun}
+ collection={collection}
+ />
+ )}
|
@@ -131,9 +160,16 @@ const MultipartFormParams = ({ item, collection }) => {
: null}
-
+
+
+
+
+
+
);
};
diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js
index e8d7093eb..cdf29e4c8 100644
--- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js
+++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js
@@ -909,6 +909,16 @@ export const browseDirectory = () => (dispatch, getState) => {
});
};
+export const browseFiles =
+ (filters = []) =>
+ (dispatch, getState) => {
+ const { ipcRenderer } = window;
+
+ return new Promise((resolve, reject) => {
+ ipcRenderer.invoke('renderer:browse-files', filters).then(resolve).catch(reject);
+ });
+ };
+
export const updateBrunoConfig = (brunoConfig, collectionUid) => (dispatch, getState) => {
const state = getState();
diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js
index 09bf07061..18102ab07 100644
--- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js
+++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js
@@ -617,6 +617,7 @@ export const collectionsSlice = createSlice({
item.draft.request.body.multipartForm = item.draft.request.body.multipartForm || [];
item.draft.request.body.multipartForm.push({
uid: uuid(),
+ isFile: action.payload.isFile ?? false,
name: '',
value: '',
description: '',
@@ -637,6 +638,7 @@ export const collectionsSlice = createSlice({
}
const param = find(item.draft.request.body.multipartForm, (p) => p.uid === action.payload.param.uid);
if (param) {
+ param.isFile = action.payload.param.isFile;
param.name = action.payload.param.name;
param.value = action.payload.param.value;
param.description = action.payload.param.description;
diff --git a/packages/bruno-cli/src/runner/prepare-request.js b/packages/bruno-cli/src/runner/prepare-request.js
index ace3b3101..957013b11 100644
--- a/packages/bruno-cli/src/runner/prepare-request.js
+++ b/packages/bruno-cli/src/runner/prepare-request.js
@@ -109,6 +109,7 @@ const prepareRequest = (request, collectionRoot) => {
each(enabledParams, (p) => (params[p.name] = p.value));
axiosRequest.headers['content-type'] = 'multipart/form-data';
axiosRequest.data = params;
+ // TODO is it needed here as well ?
}
if (request.body.mode === 'graphql') {
diff --git a/packages/bruno-cli/src/runner/run-single-request.js b/packages/bruno-cli/src/runner/run-single-request.js
index 3a94daa44..0390a99d2 100644
--- a/packages/bruno-cli/src/runner/run-single-request.js
+++ b/packages/bruno-cli/src/runner/run-single-request.js
@@ -40,6 +40,7 @@ const runSingleRequest = async function (
// 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') {
+ // TODO remove ?
const form = new FormData();
forOwn(request.data, (value, key) => {
form.append(key, value);
diff --git a/packages/bruno-electron/src/ipc/collection.js b/packages/bruno-electron/src/ipc/collection.js
index e6a1c2c37..3b53b9839 100644
--- a/packages/bruno-electron/src/ipc/collection.js
+++ b/packages/bruno-electron/src/ipc/collection.js
@@ -10,6 +10,7 @@ const {
hasBruExtension,
isDirectory,
browseDirectory,
+ browseFiles,
createDirectory,
searchForBruFiles,
sanitizeDirectoryName
@@ -38,6 +39,17 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
}
});
+ // browse directory for file
+ ipcMain.handle('renderer:browse-files', async (event, pathname, request, filters) => {
+ try {
+ const filePaths = await browseFiles(mainWindow, filters);
+
+ return filePaths;
+ } catch (error) {
+ return Promise.reject(error);
+ }
+ });
+
// create collection
ipcMain.handle(
'renderer:create-collection',
diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js
index 2cd076732..89d198457 100644
--- a/packages/bruno-electron/src/ipc/network/index.js
+++ b/packages/bruno-electron/src/ipc/network/index.js
@@ -393,7 +393,7 @@ const registerNetworkIpc = (mainWindow) => {
const collectionRoot = get(collection, 'root', {});
const _request = item.draft ? item.draft.request : item.request;
- const request = prepareRequest(_request, collectionRoot);
+ const request = prepareRequest(_request, collectionRoot, collectionPath);
const envVars = getEnvVars(environment);
const processEnvVars = getProcessEnvVars(collectionUid);
const brunoConfig = getBrunoConfig(collectionUid);
@@ -735,7 +735,7 @@ const registerNetworkIpc = (mainWindow) => {
});
const _request = item.draft ? item.draft.request : item.request;
- const request = prepareRequest(_request, collectionRoot);
+ const request = prepareRequest(_request, collectionRoot, collectionPath);
const requestUid = uuid();
const processEnvVars = getProcessEnvVars(collectionUid);
diff --git a/packages/bruno-electron/src/ipc/network/prepare-request.js b/packages/bruno-electron/src/ipc/network/prepare-request.js
index 761984e65..9d90efb6a 100644
--- a/packages/bruno-electron/src/ipc/network/prepare-request.js
+++ b/packages/bruno-electron/src/ipc/network/prepare-request.js
@@ -1,6 +1,35 @@
const { get, each, filter, forOwn, extend } = require('lodash');
const decomment = require('decomment');
const FormData = require('form-data');
+const fs = require('fs');
+const path = require('path');
+
+const parseFormData = (datas, collectionPath) => {
+ const form = new FormData();
+ datas.forEach((item) => {
+ const value = item.value;
+ const name = item.name;
+ if (item.isFile === true) {
+ const filePaths = value
+ .toString()
+ .replace(/^@file\(/, '')
+ .replace(/\)$/, '')
+ .split('|');
+
+ filePaths.forEach((filePath) => {
+ let trimmedFilePath = filePath.trim();
+ if (!path.isAbsolute(trimmedFilePath)) {
+ trimmedFilePath = path.join(collectionPath, trimmedFilePath);
+ }
+ const file = fs.readFileSync(trimmedFilePath);
+ form.append(name, file, path.basename(trimmedFilePath));
+ });
+ } else {
+ form.append(name, value);
+ }
+ });
+ return form;
+};
// Authentication
// A request can override the collection auth with another auth
@@ -70,7 +99,7 @@ const setAuthHeaders = (axiosRequest, request, collectionRoot) => {
return axiosRequest;
};
-const prepareRequest = (request, collectionRoot) => {
+const prepareRequest = (request, collectionRoot, collectionPath) => {
const headers = {};
let contentTypeDefined = false;
let url = request.url;
@@ -146,18 +175,10 @@ const prepareRequest = (request, collectionRoot) => {
}
if (request.body.mode === 'multipartForm') {
- const params = {};
- const enabledParams = filter(request.body.multipartForm, (p) => p.enabled);
- each(enabledParams, (p) => (params[p.name] = p.value));
- axiosRequest.headers['content-type'] = 'multipart/form-data';
- axiosRequest.data = params;
-
// make axios work in node using form data
// reference: https://github.com/axios/axios/issues/1006#issuecomment-320165427
- const form = new FormData();
- forOwn(axiosRequest.data, (value, key) => {
- form.append(key, value);
- });
+ const enabledParams = filter(request.body.multipartForm, (p) => p.enabled);
+ const form = parseFormData(enabledParams, collectionPath);
extend(axiosRequest.headers, form.getHeaders());
axiosRequest.data = form;
}
diff --git a/packages/bruno-electron/src/utils/filesystem.js b/packages/bruno-electron/src/utils/filesystem.js
index 4f3ea980b..8216bd9c9 100644
--- a/packages/bruno-electron/src/utils/filesystem.js
+++ b/packages/bruno-electron/src/utils/filesystem.js
@@ -103,6 +103,19 @@ const browseDirectory = async (win) => {
return isDirectory(resolvedPath) ? resolvedPath : false;
};
+const browseFiles = async (win, filters) => {
+ const { filePaths } = await dialog.showOpenDialog(win, {
+ properties: ['openFile', 'multiSelections'],
+ filters
+ });
+
+ if (!filePaths) {
+ return [];
+ }
+
+ return filePaths.map((path) => normalizeAndResolvePath(path)).filter((path) => isFile(path));
+};
+
const chooseFileToSave = async (win, preferredFileName = '') => {
const { filePath } = await dialog.showSaveDialog(win, {
defaultPath: preferredFileName
@@ -147,6 +160,7 @@ module.exports = {
hasBruExtension,
createDirectory,
browseDirectory,
+ browseFiles,
chooseFileToSave,
searchForFiles,
searchForBruFiles,
diff --git a/packages/bruno-lang/v2/src/bruToJson.js b/packages/bruno-lang/v2/src/bruToJson.js
index fbe289974..ddb54743b 100644
--- a/packages/bruno-lang/v2/src/bruToJson.js
+++ b/packages/bruno-lang/v2/src/bruToJson.js
@@ -128,6 +128,18 @@ const mapPairListToKeyValPairs = (pairList = [], parseEnabled = true) => {
});
};
+const mapPairListToKeyValPairsMultipart = (pairList = [], parseEnabled = true) => {
+ const pairs = mapPairListToKeyValPairs(pairList, parseEnabled);
+
+ return pairs.map((pair) => {
+ if (pair.value.startsWith('@file(') && pair.value.endsWith(')')) {
+ pair.isFile = true;
+ pair.value = pair.value.replace(/^@file\(/, '').replace(/\)$/, '');
+ }
+ return pair;
+ });
+};
+
const concatArrays = (objValue, srcValue) => {
if (_.isArray(objValue) && _.isArray(srcValue)) {
return objValue.concat(srcValue);
@@ -376,7 +388,7 @@ const sem = grammar.createSemantics().addAttribute('ast', {
bodymultipart(_1, dictionary) {
return {
body: {
- multipartForm: mapPairListToKeyValPairs(dictionary.ast)
+ multipartForm: mapPairListToKeyValPairsMultipart(dictionary.ast)
}
};
},
diff --git a/packages/bruno-lang/v2/src/jsonToBru.js b/packages/bruno-lang/v2/src/jsonToBru.js
index f4959500a..d94a0e8f4 100644
--- a/packages/bruno-lang/v2/src/jsonToBru.js
+++ b/packages/bruno-lang/v2/src/jsonToBru.js
@@ -181,18 +181,17 @@ ${indentString(body.sparql)}
if (body && body.multipartForm && body.multipartForm.length) {
bru += `body:multipart-form {`;
- if (enabled(body.multipartForm).length) {
- bru += `\n${indentString(
- enabled(body.multipartForm)
- .map((item) => `${item.name}: ${item.value}`)
- .join('\n')
- )}`;
- }
+ const multipartForms = enabled(body.multipartForm).concat(disabled(body.multipartForm));
- if (disabled(body.multipartForm).length) {
+ if (multipartForms.length) {
bru += `\n${indentString(
- disabled(body.multipartForm)
- .map((item) => `~${item.name}: ${item.value}`)
+ multipartForms
+ .map((item) => {
+ const enabled = item.enabled ? '' : '~';
+ const value = item.isFile ? `@file(${item.value})` : item.value;
+
+ return `${enabled}${item.name}: ${value}`;
+ })
.join('\n')
)}`;
}
diff --git a/packages/bruno-schema/src/collections/index.js b/packages/bruno-schema/src/collections/index.js
index 37e6629af..10db08767 100644
--- a/packages/bruno-schema/src/collections/index.js
+++ b/packages/bruno-schema/src/collections/index.js
@@ -24,6 +24,7 @@ const environmentsSchema = Yup.array().of(environmentSchema);
const keyValueSchema = Yup.object({
uid: uidSchema,
+ isFile: Yup.boolean().nullable(),
name: Yup.string().nullable(),
value: Yup.string().nullable(),
description: Yup.string().nullable(),
|