From ec5a5c9b569c28d4081eaa8849d0611974733983 Mon Sep 17 00:00:00 2001 From: Pragadesh-45 Date: Tue, 4 Feb 2025 17:49:40 +0530 Subject: [PATCH 01/18] fix: correct variable used in collection name update --- packages/bruno-electron/src/ipc/collection.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/bruno-electron/src/ipc/collection.js b/packages/bruno-electron/src/ipc/collection.js index c7454c113..472a31bc5 100644 --- a/packages/bruno-electron/src/ipc/collection.js +++ b/packages/bruno-electron/src/ipc/collection.js @@ -141,7 +141,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection // Change new name of collection let brunoConfig = JSON.parse(content); brunoConfig.name = collectionName; - const cont = await stringifyJson(json); + const cont = await stringifyJson(brunoConfig); // write the bruno.json to new dir await writeFile(path.join(dirPath, 'bruno.json'), cont); From 8abf8ff9c84a75fe390aa2bac739246ce9b3d60f Mon Sep 17 00:00:00 2001 From: lohxt1 Date: Tue, 4 Feb 2025 19:52:58 +0530 Subject: [PATCH 02/18] skipped request should not be considered as errors in junit reports --- packages/bruno-cli/src/reporters/junit.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/bruno-cli/src/reporters/junit.js b/packages/bruno-cli/src/reporters/junit.js index 30fb51939..e4a622722 100644 --- a/packages/bruno-cli/src/reporters/junit.js +++ b/packages/bruno-cli/src/reporters/junit.js @@ -62,7 +62,10 @@ const makeJUnitOutput = async (results, outputPath) => { suite.testcase.push(testcase); }); - if (result.error) { + if (result?.skipped) { + suite['@skipped'] = 1; + } + else if (result.error) { suite['@errors'] = 1; suite['@tests'] = 1; suite.testcase = [ From 324b7cec51674062a35b2bd3d2065a8f8ecb5016 Mon Sep 17 00:00:00 2001 From: Marcos Adriano Date: Mon, 3 Feb 2025 10:52:45 -0300 Subject: [PATCH 03/18] feat: raw binary files handling in request body (#3734) -------------- Co-authored-by: lohit Co-authored-by: Anoop M D --- .../src/components/FilePickerEditor/index.js | 8 +- .../RequestPane/Binary/StyledWrapper.js | 65 +++++++ .../components/RequestPane/Binary/index.js | 173 ++++++++++++++++++ .../RequestBody/RequestBodyMode/index.js | 9 + .../RequestPane/RequestBody/index.js | 5 + .../ReduxStore/slices/collections/actions.js | 7 +- .../ReduxStore/slices/collections/index.js | 89 ++++++++- .../bruno-app/src/utils/codegenerator/har.js | 59 ++++-- .../bruno-app/src/utils/collections/export.js | 1 + .../bruno-app/src/utils/collections/index.js | 23 ++- .../bruno-app/src/utils/curl/curl-to-json.js | 39 +++- .../src/utils/curl/curl-to-json.spec.js | 36 ++++ packages/bruno-app/src/utils/curl/index.js | 8 +- .../bruno-app/src/utils/importers/common.js | 1 + packages/bruno-electron/src/ipc/collection.js | 5 +- .../bruno-electron/src/ipc/network/index.js | 16 +- .../src/ipc/network/prepare-request.js | 34 +++- .../bruno-electron/src/utils/filesystem.js | 4 +- packages/bruno-lang/v2/src/bruToJson.js | 42 ++++- packages/bruno-lang/v2/src/jsonToBru.js | 26 +++ .../bruno-lang/v2/tests/fixtures/request.bru | 5 + .../bruno-lang/v2/tests/fixtures/request.json | 16 ++ .../bruno-schema/src/collections/index.js | 17 +- .../binaryFile/binary-file-types.bru | 27 +++ .../collection/binaryFile/binary-file.json | 9 + 25 files changed, 669 insertions(+), 55 deletions(-) create mode 100644 packages/bruno-app/src/components/RequestPane/Binary/StyledWrapper.js create mode 100644 packages/bruno-app/src/components/RequestPane/Binary/index.js create mode 100644 packages/bruno-tests/collection/binaryFile/binary-file-types.bru create mode 100644 packages/bruno-tests/collection/binaryFile/binary-file.json diff --git a/packages/bruno-app/src/components/FilePickerEditor/index.js b/packages/bruno-app/src/components/FilePickerEditor/index.js index 797771bbb..d976a3e79 100644 --- a/packages/bruno-app/src/components/FilePickerEditor/index.js +++ b/packages/bruno-app/src/components/FilePickerEditor/index.js @@ -6,7 +6,7 @@ import { IconX } from '@tabler/icons'; import { isWindowsOS } from 'utils/common/platform'; import slash from 'utils/common/slash'; -const FilePickerEditor = ({ value, onChange, collection }) => { +const FilePickerEditor = ({ value, onChange, collection, isSingleFilePicker = false}) => { value = value || []; const dispatch = useDispatch(); const filenames = value @@ -20,7 +20,7 @@ const FilePickerEditor = ({ value, onChange, collection }) => { const title = filenames.map((v) => `- ${v}`).join('\n'); const browse = () => { - dispatch(browseFiles()) + dispatch(browseFiles([],[''])) .then((filePaths) => { // If file is in the collection's directory, then we use relative path // Otherwise, we use the absolute path @@ -49,7 +49,7 @@ const FilePickerEditor = ({ value, onChange, collection }) => { if (filenames.length == 1) { return filenames[0]; } - return filenames.length + ' files selected'; + return filenames.length + ' file(s) selected'; }; return filenames.length > 0 ? ( @@ -66,7 +66,7 @@ const FilePickerEditor = ({ value, onChange, collection }) => { ) : ( ); }; diff --git a/packages/bruno-app/src/components/RequestPane/Binary/StyledWrapper.js b/packages/bruno-app/src/components/RequestPane/Binary/StyledWrapper.js new file mode 100644 index 000000000..35adfcc1f --- /dev/null +++ b/packages/bruno-app/src/components/RequestPane/Binary/StyledWrapper.js @@ -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; diff --git a/packages/bruno-app/src/components/RequestPane/Binary/index.js b/packages/bruno-app/src/components/RequestPane/Binary/index.js new file mode 100644 index 000000000..77bbda8d5 --- /dev/null +++ b/packages/bruno-app/src/components/RequestPane/Binary/index.js @@ -0,0 +1,173 @@ +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 { useTheme } from 'providers/Theme'; +import { + addBinaryFile, + updateBinaryFile, + deleteBinaryFile +} from 'providers/ReduxStore/slices/collections'; +import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions'; +import StyledWrapper from './StyledWrapper'; +import FilePickerEditor from 'components/FilePickerEditor'; +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(); + const { storedTheme } = useTheme(); + const params = item.draft ? get(item, 'draft.request.body.binaryFile') : get(item, 'request.body.binaryFile'); + + const [enabledFileUid, setEnableFileUid] = useState(params && params.length ? params[0].uid : ''); + + const addFile = () => { + dispatch( + 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])); + break; + } + case 'contentType': { + param.contentType = e.target.value; + break; + } + case 'enabled': { + param.enabled = e.target.checked; + + setEnableFileUid(param.uid); + + break; + } + } + dispatch( + updateBinaryFile({ + param: param, + itemUid: item.uid, + collectionUid: collection.uid + }) + ); + }; + + const handleRemoveParams = (param) => { + dispatch( + deleteBinaryFile({ + paramUid: param.uid, + itemUid: item.uid, + collectionUid: collection.uid + }) + ); + }; + + return ( + + + + + + + + + + + + {params && params.length + ? params.map((param, index) => { + return ( + + + + + + + ); + }) + : null} + +
File
Content-Type
Enabled
+ + handleParamChange( + { + target: { + value: newValue + } + }, + param, + 'value' + ) + } + collection={collection} + /> + + + handleParamChange( + { + target: { + value: newValue + } + }, + param, + 'contentType' + ) + } + onRun={handleRun} + collection={collection} + /> + +
+ handleParamChange(e, param, 'enabled')} + /> +
+
+
+ +
+
+
+ +
+
+ ); +}; +export default Binary; diff --git a/packages/bruno-app/src/components/RequestPane/RequestBody/RequestBodyMode/index.js b/packages/bruno-app/src/components/RequestPane/RequestBody/RequestBodyMode/index.js index 29b66d58d..95b3b6a55 100644 --- a/packages/bruno-app/src/components/RequestPane/RequestBody/RequestBodyMode/index.js +++ b/packages/bruno-app/src/components/RequestPane/RequestBody/RequestBodyMode/index.js @@ -128,6 +128,15 @@ const RequestBodyMode = ({ item, collection }) => { SPARQL
Other
+
{ + dropdownTippyRef.current.hide(); + onModeChange('binaryFile'); + }} + > + Binary File +
{ diff --git a/packages/bruno-app/src/components/RequestPane/RequestBody/index.js b/packages/bruno-app/src/components/RequestPane/RequestBody/index.js index ca60c8662..9a71a4ac3 100644 --- a/packages/bruno-app/src/components/RequestPane/RequestBody/index.js +++ b/packages/bruno-app/src/components/RequestPane/RequestBody/index.js @@ -8,6 +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'; const RequestBody = ({ item, collection }) => { const dispatch = useDispatch(); @@ -62,6 +63,10 @@ const RequestBody = ({ item, collection }) => { ); } + if (bodyMode === 'binaryFile') { + return + } + if (bodyMode === 'formUrlEncoded') { return ; } 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 cc53b3339..f089dbddc 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js @@ -758,7 +758,8 @@ export const newHttpRequest = (params) => (dispatch, getState) => { xml: null, sparql: null, multipartForm: null, - formUrlEncoded: null + formUrlEncoded: null, + binaryFile: null }, auth: auth ?? { mode: 'none' @@ -1038,12 +1039,12 @@ export const browseDirectory = () => (dispatch, getState) => { }; export const browseFiles = - (filters = []) => + (filters = [], properties = ['multiSelections']) => (dispatch, getState) => { const { ipcRenderer } = window; return new Promise((resolve, reject) => { - ipcRenderer.invoke('renderer:browse-files', filters).then(resolve).catch(reject); + ipcRenderer.invoke('renderer:browse-files', undefined, undefined, undefined, filters, properties).then(resolve).catch(reject); }); }; 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 6a795171f..bdecc1543 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js @@ -18,6 +18,8 @@ import { import { parsePathParams, parseQueryParams, splitOnFirst, stringifyQueryParams } from 'utils/url'; import { getDirectoryName, getSubdirectoriesFromRoot, PATH_SEPARATOR } from 'utils/common/platform'; import toast from 'react-hot-toast'; +import mime from 'mime-types'; +import path from 'node:path'; const initialState = { collections: [], @@ -873,25 +875,89 @@ export const collectionsSlice = createSlice({ } } }, - moveMultipartFormParam: (state, action) => { + 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) => { + 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: [''], + contentType: '', + enabled: 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)) { - // 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); + 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; + param.contentType = action.payload.param.contentType || contentType || ''; + param.enabled = action.payload.param.enabled; + } + } + } + }, + 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); + } + + item.draft.request.body.binaryFile = filter( + item.draft.request.body.binaryFile, + (p) => p.uid !== action.payload.paramUid + ); } } }, @@ -951,6 +1017,10 @@ export const collectionsSlice = createSlice({ item.draft.request.body.sparql = action.payload.content; break; } + case 'binaryFile': { + item.draft.request.body.binaryFile = action.payload.content; + break; + } case 'formUrlEncoded': { item.draft.request.body.formUrlEncoded = action.payload.content; break; @@ -1952,6 +2022,9 @@ export const { addMultipartFormParam, updateMultipartFormParam, deleteMultipartFormParam, + addBinaryFile, + updateBinaryFile, + deleteBinaryFile, moveMultipartFormParam, updateRequestAuthMode, updateRequestBodyMode, diff --git a/packages/bruno-app/src/utils/codegenerator/har.js b/packages/bruno-app/src/utils/codegenerator/har.js index 479fcd67a..19e4ea4de 100644 --- a/packages/bruno-app/src/utils/codegenerator/har.js +++ b/packages/bruno-app/src/utils/codegenerator/har.js @@ -14,6 +14,8 @@ const createContentType = (mode) => { return 'application/json'; case 'multipartForm': return 'multipart/form-data'; + case 'binaryFile': + return 'application/octet-stream'; default: return ''; } @@ -60,26 +62,48 @@ const createPostData = (body, type) => { } const contentType = createContentType(body.mode); - if (body.mode === 'formUrlEncoded' || body.mode === 'multipartForm') { - return { - mimeType: contentType, - params: body[body.mode] - .filter((param) => param.enabled) - .map((param) => ({ - name: param.name, - value: param.value, - ...(param.type === 'file' && { fileName: param.value }) - })) - }; - } else { - return { - mimeType: contentType, - text: body[body.mode] - }; + + switch (body.mode) { + case 'formUrlEncoded': + case 'multipartForm': + return { + mimeType: contentType, + params: body[body.mode] + .filter((param) => param.enabled) + .map((param) => ({ + name: param.name, + value: param.value, + ...(param.type === 'file' && { fileName: param.value }) + })) + }; + case 'binaryFile': + const binary = { + mimeType: 'application/octet-stream', + // mimeType: body[body.mode].filter((param) => param.enabled)[0].contentType, + params: body[body.mode] + .filter((param) => param.enabled) + .map((param) => ({ + name: param.name, + value: param.value, + fileName: param.value + })) + }; + + console.log('curl-binary', binary); + return binary; + default: + return { + mimeType: contentType, + text: body[body.mode] + }; } }; 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), @@ -89,6 +113,7 @@ export const buildHarRequest = ({ request, headers, type }) => { queryString: createQuery(request.params), postData: createPostData(request.body, type), headersSize: 0, - bodySize: 0 + bodySize: 0, + binary: true }; }; diff --git a/packages/bruno-app/src/utils/collections/export.js b/packages/bruno-app/src/utils/collections/export.js index 5ef7b1b49..b7ca9f5ba 100644 --- a/packages/bruno-app/src/utils/collections/export.js +++ b/packages/bruno-app/src/utils/collections/export.js @@ -14,6 +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); } if (item.items && item.items.length) { diff --git a/packages/bruno-app/src/utils/collections/index.js b/packages/bruno-app/src/utils/collections/index.js index 956616710..309c245eb 100644 --- a/packages/bruno-app/src/utils/collections/index.js +++ b/packages/bruno-app/src/utils/collections/index.js @@ -281,6 +281,19 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {} }); }; + const copyBinaryFileParams = (params = []) => { + return map(params, (param) => { + return { + uid: param.uid, + type: param.type, + name: param.name, + value: param.value, + contentType: param.contentType, + enabled: param.enabled + } + }); + } + const copyItems = (sourceItems, destItems) => { each(sourceItems, (si) => { if (!isItemAFolder(si) && !isItemARequest(si) && si.type !== 'js') { @@ -308,7 +321,8 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {} graphql: si.request.body.graphql, sparql: si.request.body.sparql, formUrlEncoded: copyFormUrlEncodedParams(si.request.body.formUrlEncoded), - multipartForm: copyMultipartFormParams(si.request.body.multipartForm) + multipartForm: copyMultipartFormParams(si.request.body.multipartForm), + binaryFile: copyBinaryFileParams(si.request.body.binaryFile) }, script: si.request.script, vars: si.request.vars, @@ -661,6 +675,10 @@ export const humanizeRequestBodyMode = (mode) => { label = 'SPARQL'; break; } + case 'binaryFile': { + label = 'Binary File'; + break; + } case 'formUrlEncoded': { label = 'Form URL Encoded'; break; @@ -761,6 +779,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())); return item; }; @@ -771,11 +790,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', []); 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); return item; }; diff --git a/packages/bruno-app/src/utils/curl/curl-to-json.js b/packages/bruno-app/src/utils/curl/curl-to-json.js index c1398ab14..5a2c62022 100644 --- a/packages/bruno-app/src/utils/curl/curl-to-json.js +++ b/packages/bruno-app/src/utils/curl/curl-to-json.js @@ -9,6 +9,7 @@ import parseCurlCommand from './parse-curl'; import * as querystring from 'query-string'; import * as jsesc from 'jsesc'; +import * as path from 'path'; function getContentType(headers = {}) { const contentType = Object.keys(headers).find((key) => key.toLowerCase() === 'content-type'); @@ -99,9 +100,36 @@ function getMultipleDataString(request, parsedQueryString) { function getFilesString(request) { const data = {}; - data.files = {}; data.data = {}; + if (request.isDataBinary){ + + let filePath = '' + + if(request.data.startsWith('@')){ + filePath = request.data.slice(1); + }else{ + filePath = request.data; + } + + const fileName = path.basename(filePath); + + data.data = [ + { + name: repr(fileName), + value: [repr(filePath)], + enabled: true, + contentType: request.headers['Content-Type'], + type: 'binaryFile' + } + ]; + + return data; + + } + + data.files = {}; + for (const multipartKey in request.multipartUploads) { const multipartValue = request.multipartUploads[multipartKey]; if (multipartValue.startsWith('@')) { @@ -140,6 +168,7 @@ const curlToJson = (curlCommand) => { requestJson.url = request.urlWithoutQuery; requestJson.raw_url = request.url; requestJson.method = request.method; + requestJson.isDataBinary = request.isDataBinary; if (request.cookies) { const cookies = {}; @@ -163,11 +192,11 @@ const curlToJson = (curlCommand) => { requestJson.queries = getQueries(request); } - if (typeof request.data === 'string' || typeof request.data === 'number') { - Object.assign(requestJson, getDataString(request)); - } else if (request.multipartUploads) { + 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; diff --git a/packages/bruno-app/src/utils/curl/curl-to-json.spec.js b/packages/bruno-app/src/utils/curl/curl-to-json.spec.js index 2d9785154..6f8206139 100644 --- a/packages/bruno-app/src/utils/curl/curl-to-json.spec.js +++ b/packages/bruno-app/src/utils/curl/curl-to-json.spec.js @@ -86,4 +86,40 @@ describe('curlToJson', () => { method: 'get' }); }); + + it('should return a parse a curl with a post body with binary file type', () => { + const curlCommand = `curl 'https://www.usebruno.com' + -H 'Accept: application/json, text/plain, */*' + -H 'Accept-Language: en-US,en;q=0.9,hi;q=0.8' + -H 'Content-Type: application/json;charset=utf-8' + -H 'Origin: https://www.usebruno.com' + -H 'Referer: https://www.usebruno.com/' + --data-binary '@/path/to/file' + `; + + const result = curlToJson(curlCommand); + + expect(result).toEqual({ + url: 'https://www.usebruno.com', + raw_url: 'https://www.usebruno.com', + method: 'post', + headers: { + Accept: 'application/json, text/plain, */*', + 'Accept-Language': 'en-US,en;q=0.9,hi;q=0.8', + 'Content-Type': 'application/json;charset=utf-8', + Origin: 'https://www.usebruno.com', + Referer: 'https://www.usebruno.com/' + }, + isDataBinary: true, + data: [ + { + name: 'file', + value: ['/path/to/file'], + enabled: true, + contentType: 'application/json;charset=utf-8', + type: 'binaryFile' + } + ] + }); + }); }); diff --git a/packages/bruno-app/src/utils/curl/index.js b/packages/bruno-app/src/utils/curl/index.js index f486df56b..d91588178 100644 --- a/packages/bruno-app/src/utils/curl/index.js +++ b/packages/bruno-app/src/utils/curl/index.js @@ -50,14 +50,18 @@ export const getRequestFromCurlCommand = (curlCommand, requestType = 'http-reque sparql: null, multipartForm: null, formUrlEncoded: null, - graphql: null + graphql: null, + binaryFile: null }; if (parsedBody && contentType && typeof contentType === 'string') { if (requestType === 'graphql-request' && (contentType.includes('application/json') || contentType.includes('application/graphql'))) { body.mode = 'graphql'; body.graphql = parseGraphQL(parsedBody); - } else if (contentType.includes('application/json')) { + } else if (requestType === 'http-request' && request.isDataBinary) { + body.mode = 'binaryFile'; + body.binaryFile = parsedBody; + }else if (contentType.includes('application/json')) { body.mode = 'json'; body.json = convertToCodeMirrorJson(parsedBody); } else if (contentType.includes('xml')) { diff --git a/packages/bruno-app/src/utils/importers/common.js b/packages/bruno-app/src/utils/importers/common.js index 88c4c7872..af187cc82 100644 --- a/packages/bruno-app/src/utils/importers/common.js +++ b/packages/bruno-app/src/utils/importers/common.js @@ -35,6 +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())); if (item.items && item.items.length) { updateItemUids(item.items); diff --git a/packages/bruno-electron/src/ipc/collection.js b/packages/bruno-electron/src/ipc/collection.js index 472a31bc5..c1a05a49a 100644 --- a/packages/bruno-electron/src/ipc/collection.js +++ b/packages/bruno-electron/src/ipc/collection.js @@ -62,9 +62,10 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection }); // browse directory for file - ipcMain.handle('renderer:browse-files', async (event, pathname, request, filters) => { + ipcMain.handle('renderer:browse-files', async (event, pathname, request, filters, properties) => { try { - const filePaths = await browseFiles(mainWindow, filters); + + const filePaths = await browseFiles(mainWindow, filters, properties); return filePaths; } catch (error) { diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index 4a2b29c7d..dbc36d846 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -575,16 +575,20 @@ const registerNetworkIpc = (mainWindow) => { cancelTokenUid }); - const request = prepareRequest(item, collection); + const abortController = new AbortController(); + + const collectionRoot = get(collection, 'root', {}); + const request = await prepareRequest(item, collection, abortController); request.__bruno__executionMode = 'standalone'; + const brunoConfig = getBrunoConfig(collectionUid); const scriptingConfig = get(brunoConfig, 'scripts', {}); scriptingConfig.runtime = getJsSandboxRuntime(collection); try { - const controller = new AbortController(); - request.signal = controller.signal; - saveCancelToken(cancelTokenUid, controller); + request.signal = abortController.signal; + + saveCancelToken(cancelTokenUid, abortController); await runPreRequest( request, @@ -614,7 +618,7 @@ const registerNetworkIpc = (mainWindow) => { url: request.url, method: request.method, headers: request.headers, - data: safeParseJSON(safeStringifyJSON(request.data)), + data: request.mode == 'binaryFile'? undefined: safeParseJSON(safeStringifyJSON(request.data)) , timestamp: Date.now() }, collectionUid, @@ -1036,7 +1040,7 @@ const registerNetworkIpc = (mainWindow) => { ...eventData }); - const request = prepareRequest(item, collection); + const request = await prepareRequest(item, collection, abortController); request.__bruno__executionMode = 'runner'; const requestUid = uuid(); diff --git a/packages/bruno-electron/src/ipc/network/prepare-request.js b/packages/bruno-electron/src/ipc/network/prepare-request.js index 6c7672e7d..297033c6d 100644 --- a/packages/bruno-electron/src/ipc/network/prepare-request.js +++ b/packages/bruno-electron/src/ipc/network/prepare-request.js @@ -1,8 +1,10 @@ const { get, each, filter } = require('lodash'); const decomment = require('decomment'); const crypto = require('node:crypto'); +const fs = require('node:fs/promises'); const { getTreePathFromCollectionToItem, mergeHeaders, mergeScripts, mergeVars } = require('../../utils/collection'); const { buildFormUrlEncodedPayload, createFormData } = require('../../utils/form-data'); +const path = require('node:path'); const setAuthHeaders = (axiosRequest, request, collectionRoot) => { @@ -174,7 +176,7 @@ const setAuthHeaders = (axiosRequest, request, collectionRoot) => { return axiosRequest; }; -const prepareRequest = (item, collection) => { +const prepareRequest = async (item, collection, abortController) => { const request = item.draft ? item.draft.request : item.request; const collectionRoot = get(collection, 'root', {}); const collectionPath = collection.pathname; @@ -251,6 +253,36 @@ const prepareRequest = (item, collection) => { axiosRequest.data = request.body.sparql; } + if (request.body.mode === 'binaryFile') { + + if (!contentTypeDefined) { + axiosRequest.headers['content-type'] = 'application/octet-stream'; + } + + if (request.body.binaryFile && request.body.binaryFile.length > 0) { + + axiosRequest.headers['content-type'] = request.body.binaryFile[0].contentType; + + let filePath = request.body.binaryFile[0].value[0]; + + if (filePath && filePath !== '') { + + if (!path.isAbsolute(filePath)) { + + filePath = path.join(collectionPath, filePath); + } + + const file = await fs.readFile(filePath, abortController) + + axiosRequest.data = file + + if(axiosRequest.headers['content-type'].includes('application/json')) { + axiosRequest.data = JSON.parse(file) + } + } + } + } + if (request.body.mode === 'formUrlEncoded') { if (!contentTypeDefined) { axiosRequest.headers['content-type'] = 'application/x-www-form-urlencoded'; diff --git a/packages/bruno-electron/src/utils/filesystem.js b/packages/bruno-electron/src/utils/filesystem.js index 0ab6bbf0a..d37742be4 100644 --- a/packages/bruno-electron/src/utils/filesystem.js +++ b/packages/bruno-electron/src/utils/filesystem.js @@ -121,9 +121,9 @@ const browseDirectory = async (win) => { return isDirectory(resolvedPath) ? resolvedPath : false; }; -const browseFiles = async (win, filters) => { +const browseFiles = async (win, filters, properties) => { const { filePaths } = await dialog.showOpenDialog(win, { - properties: ['openFile', 'multiSelections'], + properties: ['openFile', ...properties], filters }); diff --git a/packages/bruno-lang/v2/src/bruToJson.js b/packages/bruno-lang/v2/src/bruToJson.js index 2fe5fb472..e7b0c5772 100644 --- a/packages/bruno-lang/v2/src/bruToJson.js +++ b/packages/bruno-lang/v2/src/bruToJson.js @@ -25,7 +25,7 @@ const grammar = ohm.grammar(`Bru { BruFile = (meta | http | query | params | headers | auths | bodies | varsandassert | script | tests | docs)* auths = authawsv4 | authbasic | authbearer | authdigest | authNTLM | authOAuth2 | authwsse | authapikey bodies = bodyjson | bodytext | bodyxml | bodysparql | bodygraphql | bodygraphqlvars | bodyforms | body - bodyforms = bodyformurlencoded | bodymultipart + bodyforms = bodyformurlencoded | bodymultipart | bodybinaryfile params = paramspath | paramsquery nl = "\\r"? "\\n" @@ -102,7 +102,8 @@ const grammar = ohm.grammar(`Bru { bodyformurlencoded = "body:form-urlencoded" dictionary bodymultipart = "body:multipart-form" dictionary - + bodybinaryfile = "body:binary-file" dictionary + script = scriptreq | scriptres scriptreq = "script:pre-request" st* "{" nl* textblock tagend scriptres = "script:post-response" st* "{" nl* textblock tagend @@ -173,6 +174,19 @@ const multipartExtractContentType = (pair) => { } }; +const binaryFileExtractContentType = (pair) => { + if (_.isString(pair.value)) { + const match = pair.value.match(/^(.*?)\s*@contentType\((.*?)\)\s*$/); + if (match != null && match.length > 2) { + pair.value = match[1]; + pair.contentType = match[2]; + } else { + pair.contentType = ''; + } + } +}; + + const mapPairListToKeyValPairsMultipart = (pairList = [], parseEnabled = true) => { const pairs = mapPairListToKeyValPairs(pairList, parseEnabled); @@ -190,6 +204,23 @@ const mapPairListToKeyValPairsMultipart = (pairList = [], parseEnabled = true) = }); }; +const mapPairListToKeyValPairsBinaryFile = (pairList = [], parseEnabled = true) => { + const pairs = mapPairListToKeyValPairs(pairList, parseEnabled); + + return pairs.map((pair) => { + binaryFileExtractContentType(pair); + + if (pair.value.startsWith('@file(') && pair.value.endsWith(')')) { + let filestr = pair.value.replace(/^@file\(/, '').replace(/\)$/, ''); + pair.type = 'binaryFile'; + pair.value = filestr != '' ? filestr.split('|') : ['']; + } + + return pair; + }); +}; + + const concatArrays = (objValue, srcValue) => { if (_.isArray(objValue) && _.isArray(srcValue)) { return objValue.concat(srcValue); @@ -574,6 +605,13 @@ const sem = grammar.createSemantics().addAttribute('ast', { } }; }, + bodybinaryfile(_1, dictionary) { + return { + body: { + binaryFile: mapPairListToKeyValPairsBinaryFile(dictionary.ast) + } + }; + }, body(_1, _2, _3, _4, textblock, _5) { return { http: { diff --git a/packages/bruno-lang/v2/src/jsonToBru.js b/packages/bruno-lang/v2/src/jsonToBru.js index 62b31c2f9..c4e2ba323 100644 --- a/packages/bruno-lang/v2/src/jsonToBru.js +++ b/packages/bruno-lang/v2/src/jsonToBru.js @@ -313,6 +313,32 @@ ${indentString(body.sparql)} bru += '\n}\n\n'; } + + if (body && body.binaryFile && body.binaryFile.length) { + bru += `body:binary-file {`; + const binaryFiles = enabled(body.binaryFile).concat(disabled(body.binaryFile)); + + if (binaryFiles.length) { + bru += `\n${indentString( + binaryFiles + .map((item) => { + const enabled = item.enabled ? '' : '~'; + const contentType = + item.contentType && item.contentType !== '' ? ' @contentType(' + item.contentType + ')' : ''; + + if (item.type === 'binaryFile') { + let filestr = item.value[0] || ''; + const value = `@file(${filestr})`; + return `${enabled}${item.name}: ${value}${contentType}`; + } + }) + .join('\n') + )}`; + } + + bru += '\n}\n\n'; + } + if (body && body.graphql && body.graphql.query) { bru += `body:graphql {\n`; bru += `${indentString(body.graphql.query)}`; diff --git a/packages/bruno-lang/v2/tests/fixtures/request.bru b/packages/bruno-lang/v2/tests/fixtures/request.bru index 1a3efeab7..5f7183f34 100644 --- a/packages/bruno-lang/v2/tests/fixtures/request.bru +++ b/packages/bruno-lang/v2/tests/fixtures/request.bru @@ -102,6 +102,11 @@ body:multipart-form { ~message: hello } +body:binary-file { + file: @file(path/to/file.json) @contentType(application/json) + ~file2: @file(path/to/file2.json) @contentType(application/json) +} + body:graphql { { launchesPast { diff --git a/packages/bruno-lang/v2/tests/fixtures/request.json b/packages/bruno-lang/v2/tests/fixtures/request.json index 9c8ed143d..ad7a45495 100644 --- a/packages/bruno-lang/v2/tests/fixtures/request.json +++ b/packages/bruno-lang/v2/tests/fixtures/request.json @@ -137,6 +137,22 @@ "enabled": false, "type": "text" } + ], + "binaryFile" : [ + { + "name": "file", + "value": ["path/to/file.json"], + "enabled": true, + "type": "binaryFile", + "contentType": "application/json" + }, + { + "name": "file2", + "value": ["path/to/file2.json"], + "enabled": false, + "type": "binaryFile", + "contentType": "application/json" + } ] }, "vars": { diff --git a/packages/bruno-schema/src/collections/index.js b/packages/bruno-schema/src/collections/index.js index b6e044ae4..19d98afbb 100644 --- a/packages/bruno-schema/src/collections/index.js +++ b/packages/bruno-schema/src/collections/index.js @@ -74,9 +74,21 @@ const multipartFormSchema = Yup.object({ .noUnknown(true) .strict(); + +const binaryFileSchema = Yup.object({ + uid: uidSchema, + type: Yup.string().oneOf(['binaryFile']).required('type is required'), + name: Yup.string().nullable(), + value: Yup.array().of(Yup.string().nullable()).nullable(), + contentType: Yup.string().nullable(), + enabled: Yup.boolean() +}) + .noUnknown(true) + .strict(); + const requestBodySchema = Yup.object({ mode: Yup.string() - .oneOf(['none', 'json', 'text', 'xml', 'formUrlEncoded', 'multipartForm', 'graphql', 'sparql']) + .oneOf(['none', 'json', 'text', 'xml', 'formUrlEncoded', 'multipartForm', 'graphql', 'sparql', 'binaryFile']) .required('mode is required'), json: Yup.string().nullable(), text: Yup.string().nullable(), @@ -84,7 +96,8 @@ const requestBodySchema = Yup.object({ sparql: Yup.string().nullable(), formUrlEncoded: Yup.array().of(keyValueSchema).nullable(), multipartForm: Yup.array().of(multipartFormSchema).nullable(), - graphql: graphqlBodySchema.nullable() + graphql: graphqlBodySchema.nullable(), + binaryFile: Yup.array().of(binaryFileSchema).nullable() }) .noUnknown(true) .strict(); diff --git a/packages/bruno-tests/collection/binaryFile/binary-file-types.bru b/packages/bruno-tests/collection/binaryFile/binary-file-types.bru new file mode 100644 index 000000000..93275971f --- /dev/null +++ b/packages/bruno-tests/collection/binaryFile/binary-file-types.bru @@ -0,0 +1,27 @@ +meta { + name: binary-files-types + type: http + seq: 1 +} + +post { + url: {{host}}/api/binaryFile/binary-file-types + body: binaryFile + auth: none +} + +body:binary-file { + file1: @file() @contentType() + file2: @file(binaryFile/binary-file.json) @contentType() + file3: @file(binaryFile/binary-file.json) @contentType(application/json) +} + +assert { + res.status: eq 200 + res.body.find(p=>p.name === 'file1').value[0]: isUndefined + res.body.find(p=>p.name === 'file1').contentType: isUndefined + res.body.find(p=>p.name === 'file2').value[0]: eq binaryFile/binary-file.json + res.body.find(p=>p.name === 'file2').contentType: eq isUndefined + res.body.find(p=>p.name === 'file3').value[0]: eq binaryFile/binary-file.json + res.body.find(p=>p.name === 'file3').contentType: eq application/json +} diff --git a/packages/bruno-tests/collection/binaryFile/binary-file.json b/packages/bruno-tests/collection/binaryFile/binary-file.json new file mode 100644 index 000000000..2ff269bff --- /dev/null +++ b/packages/bruno-tests/collection/binaryFile/binary-file.json @@ -0,0 +1,9 @@ +{ + "version": "1", + "name": "bruno-testing", + "type": "collection", + "ignore": [ + "node_modules", + ".git" + ] +} \ No newline at end of file From dd239629581e3ec549b528ec90185fc7c55817ef Mon Sep 17 00:00:00 2001 From: Sanjai Kumar <84461672+sanjai0py@users.noreply.github.com> Date: Tue, 4 Feb 2025 16:14:23 +0530 Subject: [PATCH 04/18] improvements (#3930) Co-authored-by: Sanjai Kumar --- packages/bruno-app/package.json | 2 +- .../src/components/FilePickerEditor/index.js | 15 ++- .../components/RequestPane/Binary/index.js | 103 ++++++++---------- .../RequestBody/RequestBodyMode/index.js | 2 +- .../ReduxStore/slices/collections/actions.js | 9 +- .../ReduxStore/slices/collections/index.js | 82 +++++++------- .../bruno-app/src/utils/codegenerator/har.js | 32 ++++-- .../bruno-app/src/utils/collections/index.js | 8 +- .../bruno-app/src/utils/curl/curl-to-json.js | 24 ++-- packages/bruno-electron/src/ipc/collection.js | 9 +- .../bruno-electron/src/ipc/network/index.js | 6 +- .../src/ipc/network/prepare-request.js | 35 +++--- .../bruno-electron/src/utils/collection.js | 2 + packages/bruno-lang/v2/src/bruToJson.js | 21 ++-- packages/bruno-lang/v2/src/jsonToBru.js | 18 ++- .../bruno-lang/v2/tests/fixtures/request.bru | 3 +- .../bruno-lang/v2/tests/fixtures/request.json | 21 ++-- .../bruno-schema/src/collections/index.js | 6 +- .../collection/echo/multiline/echo binary.bru | 15 +++ packages/bruno-tests/src/echo/index.js | 11 ++ packages/bruno-tests/src/index.js | 1 + 21 files changed, 221 insertions(+), 204 deletions(-) create mode 100644 packages/bruno-tests/collection/echo/multiline/echo binary.bru diff --git a/packages/bruno-app/package.json b/packages/bruno-app/package.json index c824f13a8..7203bb816 100644 --- a/packages/bruno-app/package.json +++ b/packages/bruno-app/package.json @@ -33,7 +33,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", diff --git a/packages/bruno-app/src/components/FilePickerEditor/index.js b/packages/bruno-app/src/components/FilePickerEditor/index.js index d976a3e79..be7d689a3 100644 --- a/packages/bruno-app/src/components/FilePickerEditor/index.js +++ b/packages/bruno-app/src/components/FilePickerEditor/index.js @@ -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
) : ( ); }; -export default FilePickerEditor; +export default FilePickerEditor; \ No newline at end of file diff --git a/packages/bruno-app/src/components/RequestPane/Binary/index.js b/packages/bruno-app/src/components/RequestPane/Binary/index.js index 77bbda8d5..f4a6eb007 100644 --- a/packages/bruno-app/src/components/RequestPane/Binary/index.js +++ b/packages/bruno-app/src/components/RequestPane/Binary/index.js @@ -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 }) => { - - - + + + @@ -97,26 +88,26 @@ const Binary = ({ item, collection }) => { return ( @@ -170,4 +161,4 @@ const Binary = ({ item, collection }) => { ); }; -export default Binary; +export default Binary; \ No newline at end of file diff --git a/packages/bruno-app/src/components/RequestPane/RequestBody/RequestBodyMode/index.js b/packages/bruno-app/src/components/RequestPane/RequestBody/RequestBodyMode/index.js index 95b3b6a55..6e5d575c4 100644 --- a/packages/bruno-app/src/components/RequestPane/RequestBody/RequestBodyMode/index.js +++ b/packages/bruno-app/src/components/RequestPane/RequestBody/RequestBodyMode/index.js @@ -135,7 +135,7 @@ const RequestBodyMode = ({ item, collection }) => { onModeChange('binaryFile'); }} > - Binary File + File / Binary
(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(); 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 bdecc1543..25bf4fa68 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js @@ -875,80 +875,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); @@ -958,6 +960,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; + } } } }, diff --git a/packages/bruno-app/src/utils/codegenerator/har.js b/packages/bruno-app/src/utils/codegenerator/har.js index 19e4ea4de..8514588ab 100644 --- a/packages/bruno-app/src/utils/codegenerator/har.js +++ b/packages/bruno-app/src/utils/codegenerator/har.js @@ -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), diff --git a/packages/bruno-app/src/utils/collections/index.js b/packages/bruno-app/src/utils/collections/index.js index 309c245eb..fa4a2acd4 100644 --- a/packages/bruno-app/src/utils/collections/index.js +++ b/packages/bruno-app/src/utils/collections/index.js @@ -285,11 +285,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 } }); } @@ -676,7 +674,7 @@ export const humanizeRequestBodyMode = (mode) => { break; } case 'binaryFile': { - label = 'Binary File'; + label = 'File / Binary'; break; } case 'formUrlEncoded': { diff --git a/packages/bruno-app/src/utils/curl/curl-to-json.js b/packages/bruno-app/src/utils/curl/curl-to-json.js index 5a2c62022..7683b5cda 100644 --- a/packages/bruno-app/src/utils/curl/curl-to-json.js +++ b/packages/bruno-app/src/utils/curl/curl-to-json.js @@ -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; diff --git a/packages/bruno-electron/src/ipc/collection.js b/packages/bruno-electron/src/ipc/collection.js index c1a05a49a..07f15926f 100644 --- a/packages/bruno-electron/src/ipc/collection.js +++ b/packages/bruno-electron/src/ipc/collection.js @@ -62,14 +62,11 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection }); // browse directory for file - ipcMain.handle('renderer:browse-files', async (event, pathname, request, filters, properties) => { + ipcMain.handle('renderer:browse-files', async (_, filters, properties) => { try { - - const filePaths = await browseFiles(mainWindow, filters, properties); - - return filePaths; + return await browseFiles(mainWindow, filters, properties); } catch (error) { - return Promise.reject(error); + throw error; } }); diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index dbc36d846..baa9f098b 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -576,18 +576,14 @@ const registerNetworkIpc = (mainWindow) => { }); const abortController = new AbortController(); - - const collectionRoot = get(collection, 'root', {}); const request = await prepareRequest(item, collection, abortController); request.__bruno__executionMode = 'standalone'; - const brunoConfig = getBrunoConfig(collectionUid); const scriptingConfig = get(brunoConfig, 'scripts', {}); scriptingConfig.runtime = getJsSandboxRuntime(collection); try { request.signal = abortController.signal; - saveCancelToken(cancelTokenUid, abortController); await runPreRequest( @@ -618,7 +614,7 @@ const registerNetworkIpc = (mainWindow) => { url: request.url, method: request.method, headers: request.headers, - data: request.mode == 'binaryFile'? undefined: safeParseJSON(safeStringifyJSON(request.data)) , + data: request.mode == 'binaryFile'? "": safeParseJSON(safeStringifyJSON(request.data)) , timestamp: Date.now() }, collectionUid, diff --git a/packages/bruno-electron/src/ipc/network/prepare-request.js b/packages/bruno-electron/src/ipc/network/prepare-request.js index 297033c6d..70d8017d1 100644 --- a/packages/bruno-electron/src/ipc/network/prepare-request.js +++ b/packages/bruno-electron/src/ipc/network/prepare-request.js @@ -1,4 +1,4 @@ -const { get, each, filter } = require('lodash'); +const { get, each, filter, find } = require('lodash'); const decomment = require('decomment'); const crypto = require('node:crypto'); const fs = require('node:fs/promises'); @@ -254,30 +254,25 @@ const prepareRequest = async (item, collection, abortController) => { } if (request.body.mode === 'binaryFile') { - if (!contentTypeDefined) { - axiosRequest.headers['content-type'] = 'application/octet-stream'; + axiosRequest.headers['content-type'] = 'application/octet-stream'; // Default headers for binary file uploads } - - if (request.body.binaryFile && request.body.binaryFile.length > 0) { - - axiosRequest.headers['content-type'] = request.body.binaryFile[0].contentType; - - let filePath = request.body.binaryFile[0].value[0]; - - if (filePath && filePath !== '') { - + + const binaryFile = find(request.body.binaryFile, (param) => param.selected); + if (binaryFile) { + let { filePath, contentType } = binaryFile; + + axiosRequest.headers['content-type'] = contentType; + if (filePath) { if (!path.isAbsolute(filePath)) { - filePath = path.join(collectionPath, filePath); } - - const file = await fs.readFile(filePath, abortController) - - axiosRequest.data = file - - if(axiosRequest.headers['content-type'].includes('application/json')) { - axiosRequest.data = JSON.parse(file) + + try { + const fileContent = await fs.readFile(filePath); + axiosRequest.data = fileContent; + } catch (error) { + console.error('Error reading file:', error); } } } diff --git a/packages/bruno-electron/src/utils/collection.js b/packages/bruno-electron/src/utils/collection.js index 96e75acae..46624677c 100644 --- a/packages/bruno-electron/src/utils/collection.js +++ b/packages/bruno-electron/src/utils/collection.js @@ -240,6 +240,7 @@ const hydrateRequestWithUuid = (request, pathname) => { const assertions = get(request, 'request.assertions', []); const bodyFormUrlEncoded = get(request, 'request.body.formUrlEncoded', []); const bodyMultipartForm = get(request, 'request.body.multipartForm', []); + const binaryFile = get(request, 'request.body.binaryFile', []); params.forEach((param) => (param.uid = uuid())); headers.forEach((header) => (header.uid = uuid())); @@ -248,6 +249,7 @@ const hydrateRequestWithUuid = (request, pathname) => { assertions.forEach((assertion) => (assertion.uid = uuid())); bodyFormUrlEncoded.forEach((param) => (param.uid = uuid())); bodyMultipartForm.forEach((param) => (param.uid = uuid())); + binaryFile.forEach((param) => (param.uid = uuid())); return request; }; diff --git a/packages/bruno-lang/v2/src/bruToJson.js b/packages/bruno-lang/v2/src/bruToJson.js index e7b0c5772..3b228036f 100644 --- a/packages/bruno-lang/v2/src/bruToJson.js +++ b/packages/bruno-lang/v2/src/bruToJson.js @@ -177,9 +177,9 @@ const multipartExtractContentType = (pair) => { const binaryFileExtractContentType = (pair) => { if (_.isString(pair.value)) { const match = pair.value.match(/^(.*?)\s*@contentType\((.*?)\)\s*$/); - if (match != null && match.length > 2) { - pair.value = match[1]; - pair.contentType = match[2]; + if (match && match.length > 2) { + pair.value = match[1].trim(); + pair.contentType = match[2].trim(); } else { pair.contentType = ''; } @@ -206,21 +206,25 @@ const mapPairListToKeyValPairsMultipart = (pairList = [], parseEnabled = true) = const mapPairListToKeyValPairsBinaryFile = (pairList = [], parseEnabled = true) => { const pairs = mapPairListToKeyValPairs(pairList, parseEnabled); - return pairs.map((pair) => { binaryFileExtractContentType(pair); if (pair.value.startsWith('@file(') && pair.value.endsWith(')')) { - let filestr = pair.value.replace(/^@file\(/, '').replace(/\)$/, ''); - pair.type = 'binaryFile'; - pair.value = filestr != '' ? filestr.split('|') : ['']; + let filePath = pair.value.replace(/^@file\(/, '').replace(/\)$/, ''); + pair.filePath = filePath; + pair.selected = pair.enabled + + // Remove pair.value as it only contains the file path reference + delete pair.value; + // Remove pair.name as it is auto-generated (e.g., file1, file2, file3, etc.) + delete pair.name; + delete pair.enabled; } return pair; }); }; - const concatArrays = (objValue, srcValue) => { if (_.isArray(objValue) && _.isArray(srcValue)) { return objValue.concat(srcValue); @@ -746,3 +750,4 @@ const parser = (input) => { }; module.exports = parser; + \ No newline at end of file diff --git a/packages/bruno-lang/v2/src/jsonToBru.js b/packages/bruno-lang/v2/src/jsonToBru.js index c4e2ba323..164ea6a35 100644 --- a/packages/bruno-lang/v2/src/jsonToBru.js +++ b/packages/bruno-lang/v2/src/jsonToBru.js @@ -2,8 +2,8 @@ const _ = require('lodash'); const { indentString } = require('../../v1/src/utils'); -const enabled = (items = []) => items.filter((item) => item.enabled); -const disabled = (items = []) => items.filter((item) => !item.enabled); +const enabled = (items = [], key = "enabled") => items.filter((item) => item[key]); +const disabled = (items = [], key = "enabled") => items.filter((item) => !item[key]); // remove the last line if two new lines are found const stripLastLine = (text) => { @@ -316,21 +316,19 @@ ${indentString(body.sparql)} if (body && body.binaryFile && body.binaryFile.length) { bru += `body:binary-file {`; - const binaryFiles = enabled(body.binaryFile).concat(disabled(body.binaryFile)); + const binaryFiles = enabled(body.binaryFile, "selected").concat(disabled(body.binaryFile, "selected")); if (binaryFiles.length) { bru += `\n${indentString( binaryFiles .map((item) => { - const enabled = item.enabled ? '' : '~'; + const selected = item.selected ? '' : '~'; const contentType = item.contentType && item.contentType !== '' ? ' @contentType(' + item.contentType + ')' : ''; - - if (item.type === 'binaryFile') { - let filestr = item.value[0] || ''; - const value = `@file(${filestr})`; - return `${enabled}${item.name}: ${value}${contentType}`; - } + const filePath = item.filePath || ''; + const value = `@file(${filePath})`; + const itemName = "file"; + return `${selected}${itemName}: ${value}${contentType}`; }) .join('\n') )}`; diff --git a/packages/bruno-lang/v2/tests/fixtures/request.bru b/packages/bruno-lang/v2/tests/fixtures/request.bru index 5f7183f34..59f37ac89 100644 --- a/packages/bruno-lang/v2/tests/fixtures/request.bru +++ b/packages/bruno-lang/v2/tests/fixtures/request.bru @@ -104,7 +104,8 @@ body:multipart-form { body:binary-file { file: @file(path/to/file.json) @contentType(application/json) - ~file2: @file(path/to/file2.json) @contentType(application/json) + file: @file(path/to/file.json) @contentType(application/json) + ~file: @file(path/to/file2.json) @contentType(application/json) } body:graphql { diff --git a/packages/bruno-lang/v2/tests/fixtures/request.json b/packages/bruno-lang/v2/tests/fixtures/request.json index ad7a45495..166040509 100644 --- a/packages/bruno-lang/v2/tests/fixtures/request.json +++ b/packages/bruno-lang/v2/tests/fixtures/request.json @@ -140,18 +140,19 @@ ], "binaryFile" : [ { - "name": "file", - "value": ["path/to/file.json"], - "enabled": true, - "type": "binaryFile", - "contentType": "application/json" + "filePath": "path/to/file.json", + "contentType": "application/json", + "selected": true }, { - "name": "file2", - "value": ["path/to/file2.json"], - "enabled": false, - "type": "binaryFile", - "contentType": "application/json" + "filePath": "path/to/file.json", + "contentType": "application/json", + "selected": true + }, + { + "filePath": "path/to/file2.json", + "contentType": "application/json", + "selected": false } ] }, diff --git a/packages/bruno-schema/src/collections/index.js b/packages/bruno-schema/src/collections/index.js index 19d98afbb..fdf88b38c 100644 --- a/packages/bruno-schema/src/collections/index.js +++ b/packages/bruno-schema/src/collections/index.js @@ -77,11 +77,9 @@ const multipartFormSchema = Yup.object({ const binaryFileSchema = Yup.object({ uid: uidSchema, - type: Yup.string().oneOf(['binaryFile']).required('type is required'), - name: Yup.string().nullable(), - value: Yup.array().of(Yup.string().nullable()).nullable(), + filePath: Yup.string().nullable(), contentType: Yup.string().nullable(), - enabled: Yup.boolean() + selected: Yup.boolean() }) .noUnknown(true) .strict(); diff --git a/packages/bruno-tests/collection/echo/multiline/echo binary.bru b/packages/bruno-tests/collection/echo/multiline/echo binary.bru new file mode 100644 index 000000000..d11b30413 --- /dev/null +++ b/packages/bruno-tests/collection/echo/multiline/echo binary.bru @@ -0,0 +1,15 @@ +meta { + name: echo binary + type: http + seq: 1 +} + +post { + url: {{echo-host}} + body: binaryFile + auth: none +} + +body:binary-file { + file: @file(bruno.png) @contentType(image/png) +} diff --git a/packages/bruno-tests/src/echo/index.js b/packages/bruno-tests/src/echo/index.js index ba9b403ae..89a9208e0 100644 --- a/packages/bruno-tests/src/echo/index.js +++ b/packages/bruno-tests/src/echo/index.js @@ -19,6 +19,17 @@ router.post('/xml-raw', (req, res) => { return res.send(req.rawBody); }); +router.post('/bin', (req, res) => { + const rawBody = req.body; + + if (!rawBody || rawBody.length === 0) { + return res.status(400).send('No data received'); + } + + res.set('Content-Type', req.headers['content-type'] || 'application/octet-stream'); + res.send(rawBody); +}); + router.get('/bom-json-test', (req, res) => { const jsonData = { message: 'Hello!', diff --git a/packages/bruno-tests/src/index.js b/packages/bruno-tests/src/index.js index a09cb434b..a482fc128 100644 --- a/packages/bruno-tests/src/index.js +++ b/packages/bruno-tests/src/index.js @@ -10,6 +10,7 @@ const multipartRouter = require('./multipart'); const app = new express(); const port = process.env.PORT || 8080; +app.use(express.raw({type: '*/*', limit: '100mb'})); app.use(cors()); app.use(xmlParser()); app.use(bodyParser.text()); From af182a9c0035be957b800a6bc459064eb758d120 Mon Sep 17 00:00:00 2001 From: Sanjai Kumar Date: Tue, 4 Feb 2025 17:41:24 +0530 Subject: [PATCH 05/18] refactor: rename binaryFile to file and update related references --- .../{Binary => FileBody}/StyledWrapper.js | 0 .../RequestPane/{Binary => FileBody}/index.js | 14 ++--- .../RequestBody/RequestBodyMode/index.js | 2 +- .../RequestPane/RequestBody/index.js | 8 +-- .../ReduxStore/slices/collections/actions.js | 2 +- .../ReduxStore/slices/collections/index.js | 32 +++++------ .../bruno-app/src/utils/codegenerator/har.js | 7 +-- .../bruno-app/src/utils/collections/export.js | 2 +- .../bruno-app/src/utils/collections/index.js | 12 ++-- .../src/utils/curl/curl-to-json.spec.js | 2 +- packages/bruno-app/src/utils/curl/index.js | 6 +- .../bruno-app/src/utils/importers/common.js | 2 +- .../bruno-electron/src/ipc/network/index.js | 6 +- .../src/ipc/network/prepare-request.js | 8 +-- .../bruno-electron/src/utils/collection.js | 4 +- .../bruno-electron/src/utils/filesystem.js | 16 ++---- packages/bruno-lang/v2/src/bruToJson.js | 14 ++--- packages/bruno-lang/v2/src/jsonToBru.js | 10 ++-- .../bruno-lang/v2/tests/fixtures/request.bru | 2 +- .../bruno-lang/v2/tests/fixtures/request.json | 2 +- .../bruno-schema/src/collections/index.js | 6 +- .../binaryFile/binary-file-types.bru | 27 --------- .../collection/binaryFile/binary-file.json | 9 --- .../echo/echo file body/echo file body.bru | 40 ++++++++++++++ .../echo file body/echo image file body.bru | 22 ++++++++ .../echo file body/echo json file body.bru | 55 +++++++++++++++++++ .../echo file body/echo text file body.bru | 30 ++++++++++ .../collection/echo/multiline/echo binary.bru | 4 +- packages/bruno-tests/collection/file.txt | 3 + 29 files changed, 226 insertions(+), 121 deletions(-) rename packages/bruno-app/src/components/RequestPane/{Binary => FileBody}/StyledWrapper.js (100%) rename packages/bruno-app/src/components/RequestPane/{Binary => FileBody}/index.js (93%) delete mode 100644 packages/bruno-tests/collection/binaryFile/binary-file-types.bru delete mode 100644 packages/bruno-tests/collection/binaryFile/binary-file.json create mode 100644 packages/bruno-tests/collection/echo/echo file body/echo file body.bru create mode 100644 packages/bruno-tests/collection/echo/echo file body/echo image file body.bru create mode 100644 packages/bruno-tests/collection/echo/echo file body/echo json file body.bru create mode 100644 packages/bruno-tests/collection/echo/echo file body/echo text file body.bru create mode 100644 packages/bruno-tests/collection/file.txt diff --git a/packages/bruno-app/src/components/RequestPane/Binary/StyledWrapper.js b/packages/bruno-app/src/components/RequestPane/FileBody/StyledWrapper.js similarity index 100% rename from packages/bruno-app/src/components/RequestPane/Binary/StyledWrapper.js rename to packages/bruno-app/src/components/RequestPane/FileBody/StyledWrapper.js diff --git a/packages/bruno-app/src/components/RequestPane/Binary/index.js b/packages/bruno-app/src/components/RequestPane/FileBody/index.js similarity index 93% rename from packages/bruno-app/src/components/RequestPane/Binary/index.js rename to packages/bruno-app/src/components/RequestPane/FileBody/index.js index f4a6eb007..d97953aa5 100644 --- a/packages/bruno-app/src/components/RequestPane/Binary/index.js +++ b/packages/bruno-app/src/components/RequestPane/FileBody/index.js @@ -3,22 +3,22 @@ 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/index'; +import { addFile as _addFile, updateFile, deleteFile } 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 Binary = ({ item, collection }) => { +const FileBody = ({ item, collection }) => { const dispatch = useDispatch(); const { storedTheme } = useTheme(); - const params = item.draft ? get(item, 'draft.request.body.binaryFile') : get(item, 'request.body.binaryFile'); + 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( - addBinaryFile({ + _addFile({ itemUid: item.uid, collectionUid: collection.uid, }) @@ -47,7 +47,7 @@ const Binary = ({ item, collection }) => { } } dispatch( - updateBinaryFile({ + updateFile({ param: param, itemUid: item.uid, collectionUid: collection.uid @@ -57,7 +57,7 @@ const Binary = ({ item, collection }) => { const handleRemoveParams = (param) => { dispatch( - deleteBinaryFile({ + deleteFile({ paramUid: param.uid, itemUid: item.uid, collectionUid: collection.uid @@ -161,4 +161,4 @@ const Binary = ({ item, collection }) => { ); }; -export default Binary; \ No newline at end of file +export default FileBody; \ No newline at end of file diff --git a/packages/bruno-app/src/components/RequestPane/RequestBody/RequestBodyMode/index.js b/packages/bruno-app/src/components/RequestPane/RequestBody/RequestBodyMode/index.js index 6e5d575c4..db73597df 100644 --- a/packages/bruno-app/src/components/RequestPane/RequestBody/RequestBodyMode/index.js +++ b/packages/bruno-app/src/components/RequestPane/RequestBody/RequestBodyMode/index.js @@ -132,7 +132,7 @@ const RequestBodyMode = ({ item, collection }) => { className="dropdown-item" onClick={() => { dropdownTippyRef.current.hide(); - onModeChange('binaryFile'); + onModeChange('file'); }} > File / Binary diff --git a/packages/bruno-app/src/components/RequestPane/RequestBody/index.js b/packages/bruno-app/src/components/RequestPane/RequestBody/index.js index 9a71a4ac3..8f7fa8465 100644 --- a/packages/bruno-app/src/components/RequestPane/RequestBody/index.js +++ b/packages/bruno-app/src/components/RequestPane/RequestBody/index.js @@ -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 + if (bodyMode === 'file') { + return } if (bodyMode === 'formUrlEncoded') { @@ -77,4 +77,4 @@ const RequestBody = ({ item, collection }) => { return No Body; }; -export default RequestBody; +export default RequestBody; \ No newline at end of file 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 880536019..d251112c7 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js @@ -759,7 +759,7 @@ export const newHttpRequest = (params) => (dispatch, getState) => { sparql: null, multipartForm: null, formUrlEncoded: null, - binaryFile: null + file: null }, auth: auth ?? { mode: 'none' 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 25bf4fa68..3ae0fa4e5 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js @@ -897,7 +897,7 @@ export const collectionsSlice = createSlice({ } } }, - addBinaryFile: (state, action) => { + addFile: (state, action) => { const collection = findCollectionByUid(state.collections, action.payload.collectionUid); if (collection) { @@ -907,9 +907,9 @@ export const collectionsSlice = createSlice({ if (!item.draft) { item.draft = cloneDeep(item); } - item.draft.request.body.binaryFile = item.draft.request.body.binaryFile || []; + item.draft.request.body.file = item.draft.request.body.file || []; - item.draft.request.body.binaryFile.push({ + item.draft.request.body.file.push({ uid: uuid(), filePath: '', contentType: '', @@ -918,7 +918,7 @@ export const collectionsSlice = createSlice({ } } }, - updateBinaryFile: (state, action) => { + updateFile: (state, action) => { const collection = findCollectionByUid(state.collections, action.payload.collectionUid); if (collection) { @@ -929,7 +929,7 @@ export const collectionsSlice = createSlice({ item.draft = cloneDeep(item); } - const param = find(item.draft.request.body.binaryFile, (p) => p.uid === action.payload.param.uid); + 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)); @@ -937,7 +937,7 @@ export const collectionsSlice = createSlice({ param.contentType = action.payload.param.contentType || contentType || ''; param.selected = action.payload.param.selected; - item.draft.request.body.binaryFile = item.draft.request.body.binaryFile.map((p) => { + item.draft.request.body.file = item.draft.request.body.file.map((p) => { p.selected = p.uid === param.uid; return p; }); @@ -945,7 +945,7 @@ export const collectionsSlice = createSlice({ } } }, - deleteBinaryFile: (state, action) => { + deleteFile: (state, action) => { const collection = findCollectionByUid(state.collections, action.payload.collectionUid); if (collection) { @@ -956,13 +956,13 @@ export const collectionsSlice = createSlice({ item.draft = cloneDeep(item); } - item.draft.request.body.binaryFile = filter( - item.draft.request.body.binaryFile, + item.draft.request.body.file = filter( + item.draft.request.body.file, (p) => p.uid !== action.payload.paramUid ); - if (item.draft.request.body.binaryFile.length > 0) { - item.draft.request.body.binaryFile[0].selected = true; + if (item.draft.request.body.file.length > 0) { + item.draft.request.body.file[0].selected = true; } } } @@ -1023,8 +1023,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': { @@ -2028,9 +2028,9 @@ export const { addMultipartFormParam, updateMultipartFormParam, deleteMultipartFormParam, - addBinaryFile, - updateBinaryFile, - deleteBinaryFile, + addFile, + updateFile, + deleteFile, moveMultipartFormParam, updateRequestAuthMode, updateRequestBodyMode, diff --git a/packages/bruno-app/src/utils/codegenerator/har.js b/packages/bruno-app/src/utils/codegenerator/har.js index 8514588ab..110f82db5 100644 --- a/packages/bruno-app/src/utils/codegenerator/har.js +++ b/packages/bruno-app/src/utils/codegenerator/har.js @@ -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 ''; @@ -93,8 +93,8 @@ const createPostData = (body, type) => { ...(param.type === 'file' && { fileName: param.value }) })) }; - case 'binaryFile': - const binary = { + case 'file': + return { mimeType: body[body.mode].filter((param) => param.enabled)[0].contentType, params: body[body.mode] .filter((param) => param.selected) @@ -102,7 +102,6 @@ const createPostData = (body, type) => { value: param.filePath, })) }; - return binary; default: return { mimeType: contentType, diff --git a/packages/bruno-app/src/utils/collections/export.js b/packages/bruno-app/src/utils/collections/export.js index b7ca9f5ba..3d15fdd07 100644 --- a/packages/bruno-app/src/utils/collections/export.js +++ b/packages/bruno-app/src/utils/collections/export.js @@ -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) { diff --git a/packages/bruno-app/src/utils/collections/index.js b/packages/bruno-app/src/utils/collections/index.js index fa4a2acd4..eb53cfb48 100644 --- a/packages/bruno-app/src/utils/collections/index.js +++ b/packages/bruno-app/src/utils/collections/index.js @@ -281,7 +281,7 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {} }); }; - const copyBinaryFileParams = (params = []) => { + const copyFileParams = (params = []) => { return map(params, (param) => { return { uid: param.uid, @@ -320,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, @@ -673,7 +673,7 @@ export const humanizeRequestBodyMode = (mode) => { label = 'SPARQL'; break; } - case 'binaryFile': { + case 'file': { label = 'File / Binary'; break; } @@ -777,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; }; @@ -788,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; }; diff --git a/packages/bruno-app/src/utils/curl/curl-to-json.spec.js b/packages/bruno-app/src/utils/curl/curl-to-json.spec.js index 6f8206139..4c3194c2e 100644 --- a/packages/bruno-app/src/utils/curl/curl-to-json.spec.js +++ b/packages/bruno-app/src/utils/curl/curl-to-json.spec.js @@ -117,7 +117,7 @@ describe('curlToJson', () => { value: ['/path/to/file'], enabled: true, contentType: 'application/json;charset=utf-8', - type: 'binaryFile' + type: 'file' } ] }); diff --git a/packages/bruno-app/src/utils/curl/index.js b/packages/bruno-app/src/utils/curl/index.js index d91588178..ad4f1edf6 100644 --- a/packages/bruno-app/src/utils/curl/index.js +++ b/packages/bruno-app/src/utils/curl/index.js @@ -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); diff --git a/packages/bruno-app/src/utils/importers/common.js b/packages/bruno-app/src/utils/importers/common.js index af187cc82..9d370a455 100644 --- a/packages/bruno-app/src/utils/importers/common.js +++ b/packages/bruno-app/src/utils/importers/common.js @@ -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); diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index baa9f098b..427b3ab65 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -28,7 +28,7 @@ const { makeAxiosInstance } = require('./axios-instance'); const { addAwsV4Interceptor, resolveAwsV4Credentials } = require('./awsv4auth-helper'); const { addDigestInterceptor } = require('./digestauth-helper'); const { shouldUseProxy, PatchedHttpsProxyAgent } = require('../../utils/proxy-util'); -const { chooseFileToSave, writeBinaryFile, writeFile } = require('../../utils/filesystem'); +const { chooseFileToSave, writeFile } = require('../../utils/filesystem'); const { getCookieStringForUrl, addCookieToJar, getDomainsWithCookies } = require('../../utils/cookies'); const { resolveOAuth2AuthorizationCodeAccessToken, @@ -614,7 +614,7 @@ const registerNetworkIpc = (mainWindow) => { url: request.url, method: request.method, headers: request.headers, - data: request.mode == 'binaryFile'? "": safeParseJSON(safeStringifyJSON(request.data)) , + data: request.mode == 'file'? "": safeParseJSON(safeStringifyJSON(request.data)) , timestamp: Date.now() }, collectionUid, @@ -1371,7 +1371,7 @@ const registerNetworkIpc = (mainWindow) => { if (encoding === 'utf-8') { await writeFile(filePath, data); } else { - await writeBinaryFile(filePath, data); + await writeFile(filePath, data, true); } } } catch (error) { diff --git a/packages/bruno-electron/src/ipc/network/prepare-request.js b/packages/bruno-electron/src/ipc/network/prepare-request.js index 70d8017d1..705d19f22 100644 --- a/packages/bruno-electron/src/ipc/network/prepare-request.js +++ b/packages/bruno-electron/src/ipc/network/prepare-request.js @@ -253,14 +253,14 @@ const prepareRequest = async (item, collection, abortController) => { axiosRequest.data = request.body.sparql; } - if (request.body.mode === 'binaryFile') { + if (request.body.mode === 'file') { if (!contentTypeDefined) { axiosRequest.headers['content-type'] = 'application/octet-stream'; // Default headers for binary file uploads } - const binaryFile = find(request.body.binaryFile, (param) => param.selected); - if (binaryFile) { - let { filePath, contentType } = binaryFile; + const bodyFile = find(request.body.file, (param) => param.selected); + if (bodyFile) { + let { filePath, contentType } = bodyFile; axiosRequest.headers['content-type'] = contentType; if (filePath) { diff --git a/packages/bruno-electron/src/utils/collection.js b/packages/bruno-electron/src/utils/collection.js index 46624677c..bb8d17e97 100644 --- a/packages/bruno-electron/src/utils/collection.js +++ b/packages/bruno-electron/src/utils/collection.js @@ -240,7 +240,7 @@ const hydrateRequestWithUuid = (request, pathname) => { const assertions = get(request, 'request.assertions', []); const bodyFormUrlEncoded = get(request, 'request.body.formUrlEncoded', []); const bodyMultipartForm = get(request, 'request.body.multipartForm', []); - const binaryFile = get(request, 'request.body.binaryFile', []); + const file = get(request, 'request.body.file', []); params.forEach((param) => (param.uid = uuid())); headers.forEach((header) => (header.uid = uuid())); @@ -249,7 +249,7 @@ const hydrateRequestWithUuid = (request, pathname) => { assertions.forEach((assertion) => (assertion.uid = uuid())); bodyFormUrlEncoded.forEach((param) => (param.uid = uuid())); bodyMultipartForm.forEach((param) => (param.uid = uuid())); - binaryFile.forEach((param) => (param.uid = uuid())); + file.forEach((param) => (param.uid = uuid())); return request; }; diff --git a/packages/bruno-electron/src/utils/filesystem.js b/packages/bruno-electron/src/utils/filesystem.js index d37742be4..16a07b6af 100644 --- a/packages/bruno-electron/src/utils/filesystem.js +++ b/packages/bruno-electron/src/utils/filesystem.js @@ -68,20 +68,13 @@ function normalizeWslPath(pathname) { return pathname.replace(/^\/wsl.localhost/, '\\\\wsl.localhost').replace(/\//g, '\\'); } -const writeFile = async (pathname, content) => { +const writeFile = async (pathname, content, isBinary = false) => { try { - fs.writeFileSync(pathname, content, { - encoding: 'utf8' + await fs.writeFile(pathname, content, { + encoding: !isBinary ? "utf-8" : null }); } catch (err) { - return Promise.reject(err); - } -}; - -const writeBinaryFile = async (pathname, content) => { - try { - fs.writeFileSync(pathname, content); - } catch (err) { + console.error(`Error writing file at ${pathname}:`, err); return Promise.reject(err); } }; @@ -265,7 +258,6 @@ module.exports = { isWSLPath, normalizeWslPath, writeFile, - writeBinaryFile, hasJsonExtension, hasBruExtension, createDirectory, diff --git a/packages/bruno-lang/v2/src/bruToJson.js b/packages/bruno-lang/v2/src/bruToJson.js index 3b228036f..26a670681 100644 --- a/packages/bruno-lang/v2/src/bruToJson.js +++ b/packages/bruno-lang/v2/src/bruToJson.js @@ -25,7 +25,7 @@ const grammar = ohm.grammar(`Bru { BruFile = (meta | http | query | params | headers | auths | bodies | varsandassert | script | tests | docs)* auths = authawsv4 | authbasic | authbearer | authdigest | authNTLM | authOAuth2 | authwsse | authapikey bodies = bodyjson | bodytext | bodyxml | bodysparql | bodygraphql | bodygraphqlvars | bodyforms | body - bodyforms = bodyformurlencoded | bodymultipart | bodybinaryfile + bodyforms = bodyformurlencoded | bodymultipart | bodyfile params = paramspath | paramsquery nl = "\\r"? "\\n" @@ -102,7 +102,7 @@ const grammar = ohm.grammar(`Bru { bodyformurlencoded = "body:form-urlencoded" dictionary bodymultipart = "body:multipart-form" dictionary - bodybinaryfile = "body:binary-file" dictionary + bodyfile = "body:file" dictionary script = scriptreq | scriptres scriptreq = "script:pre-request" st* "{" nl* textblock tagend @@ -174,7 +174,7 @@ const multipartExtractContentType = (pair) => { } }; -const binaryFileExtractContentType = (pair) => { +const fileExtractContentType = (pair) => { if (_.isString(pair.value)) { const match = pair.value.match(/^(.*?)\s*@contentType\((.*?)\)\s*$/); if (match && match.length > 2) { @@ -204,10 +204,10 @@ const mapPairListToKeyValPairsMultipart = (pairList = [], parseEnabled = true) = }); }; -const mapPairListToKeyValPairsBinaryFile = (pairList = [], parseEnabled = true) => { +const mapPairListToKeyValPairsFile = (pairList = [], parseEnabled = true) => { const pairs = mapPairListToKeyValPairs(pairList, parseEnabled); return pairs.map((pair) => { - binaryFileExtractContentType(pair); + fileExtractContentType(pair); if (pair.value.startsWith('@file(') && pair.value.endsWith(')')) { let filePath = pair.value.replace(/^@file\(/, '').replace(/\)$/, ''); @@ -609,10 +609,10 @@ const sem = grammar.createSemantics().addAttribute('ast', { } }; }, - bodybinaryfile(_1, dictionary) { + bodyfile(_1, dictionary) { return { body: { - binaryFile: mapPairListToKeyValPairsBinaryFile(dictionary.ast) + file: mapPairListToKeyValPairsFile(dictionary.ast) } }; }, diff --git a/packages/bruno-lang/v2/src/jsonToBru.js b/packages/bruno-lang/v2/src/jsonToBru.js index 164ea6a35..0d0b1d9c2 100644 --- a/packages/bruno-lang/v2/src/jsonToBru.js +++ b/packages/bruno-lang/v2/src/jsonToBru.js @@ -314,13 +314,13 @@ ${indentString(body.sparql)} } - if (body && body.binaryFile && body.binaryFile.length) { - bru += `body:binary-file {`; - const binaryFiles = enabled(body.binaryFile, "selected").concat(disabled(body.binaryFile, "selected")); + if (body && body.file && body.file.length) { + bru += `body:file {`; + const files = enabled(body.file, "selected").concat(disabled(body.file, "selected")); - if (binaryFiles.length) { + if (files.length) { bru += `\n${indentString( - binaryFiles + files .map((item) => { const selected = item.selected ? '' : '~'; const contentType = diff --git a/packages/bruno-lang/v2/tests/fixtures/request.bru b/packages/bruno-lang/v2/tests/fixtures/request.bru index 59f37ac89..ad66c64e8 100644 --- a/packages/bruno-lang/v2/tests/fixtures/request.bru +++ b/packages/bruno-lang/v2/tests/fixtures/request.bru @@ -102,7 +102,7 @@ body:multipart-form { ~message: hello } -body:binary-file { +body:file { file: @file(path/to/file.json) @contentType(application/json) file: @file(path/to/file.json) @contentType(application/json) ~file: @file(path/to/file2.json) @contentType(application/json) diff --git a/packages/bruno-lang/v2/tests/fixtures/request.json b/packages/bruno-lang/v2/tests/fixtures/request.json index 166040509..1cfe98809 100644 --- a/packages/bruno-lang/v2/tests/fixtures/request.json +++ b/packages/bruno-lang/v2/tests/fixtures/request.json @@ -138,7 +138,7 @@ "type": "text" } ], - "binaryFile" : [ + "file" : [ { "filePath": "path/to/file.json", "contentType": "application/json", diff --git a/packages/bruno-schema/src/collections/index.js b/packages/bruno-schema/src/collections/index.js index fdf88b38c..e0d05d167 100644 --- a/packages/bruno-schema/src/collections/index.js +++ b/packages/bruno-schema/src/collections/index.js @@ -75,7 +75,7 @@ const multipartFormSchema = Yup.object({ .strict(); -const binaryFileSchema = Yup.object({ +const fileSchema = Yup.object({ uid: uidSchema, filePath: Yup.string().nullable(), contentType: Yup.string().nullable(), @@ -86,7 +86,7 @@ const binaryFileSchema = Yup.object({ const requestBodySchema = Yup.object({ mode: Yup.string() - .oneOf(['none', 'json', 'text', 'xml', 'formUrlEncoded', 'multipartForm', 'graphql', 'sparql', 'binaryFile']) + .oneOf(['none', 'json', 'text', 'xml', 'formUrlEncoded', 'multipartForm', 'graphql', 'sparql', 'file']) .required('mode is required'), json: Yup.string().nullable(), text: Yup.string().nullable(), @@ -95,7 +95,7 @@ const requestBodySchema = Yup.object({ formUrlEncoded: Yup.array().of(keyValueSchema).nullable(), multipartForm: Yup.array().of(multipartFormSchema).nullable(), graphql: graphqlBodySchema.nullable(), - binaryFile: Yup.array().of(binaryFileSchema).nullable() + file: Yup.array().of(fileSchema).nullable() }) .noUnknown(true) .strict(); diff --git a/packages/bruno-tests/collection/binaryFile/binary-file-types.bru b/packages/bruno-tests/collection/binaryFile/binary-file-types.bru deleted file mode 100644 index 93275971f..000000000 --- a/packages/bruno-tests/collection/binaryFile/binary-file-types.bru +++ /dev/null @@ -1,27 +0,0 @@ -meta { - name: binary-files-types - type: http - seq: 1 -} - -post { - url: {{host}}/api/binaryFile/binary-file-types - body: binaryFile - auth: none -} - -body:binary-file { - file1: @file() @contentType() - file2: @file(binaryFile/binary-file.json) @contentType() - file3: @file(binaryFile/binary-file.json) @contentType(application/json) -} - -assert { - res.status: eq 200 - res.body.find(p=>p.name === 'file1').value[0]: isUndefined - res.body.find(p=>p.name === 'file1').contentType: isUndefined - res.body.find(p=>p.name === 'file2').value[0]: eq binaryFile/binary-file.json - res.body.find(p=>p.name === 'file2').contentType: eq isUndefined - res.body.find(p=>p.name === 'file3').value[0]: eq binaryFile/binary-file.json - res.body.find(p=>p.name === 'file3').contentType: eq application/json -} diff --git a/packages/bruno-tests/collection/binaryFile/binary-file.json b/packages/bruno-tests/collection/binaryFile/binary-file.json deleted file mode 100644 index 2ff269bff..000000000 --- a/packages/bruno-tests/collection/binaryFile/binary-file.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "version": "1", - "name": "bruno-testing", - "type": "collection", - "ignore": [ - "node_modules", - ".git" - ] -} \ No newline at end of file diff --git a/packages/bruno-tests/collection/echo/echo file body/echo file body.bru b/packages/bruno-tests/collection/echo/echo file body/echo file body.bru new file mode 100644 index 000000000..b820e4808 --- /dev/null +++ b/packages/bruno-tests/collection/echo/echo file body/echo file body.bru @@ -0,0 +1,40 @@ +meta { + name: echo file body + type: http + seq: 4 +} + +post { + url: {{echo-host}} + body: file + auth: none +} + +body:file { + file: @file(ping.bru) @contentType(bru) +} + +tests { + test("should return bru file body contents", function() { + const data = res.getBody(); + const expectedData = `meta { + name: ping + type: http + seq: 1 + } + + get { + url: {{host}}/ping + body: none + auth: none + } + `; + expect(res.getBody()).to.eql(expectedData); + }); + + + test("should return proper header", function() { + const contentType = res.getHeader('content-type'); + expect(contentType).to.eql('bru'); + }); +} \ No newline at end of file diff --git a/packages/bruno-tests/collection/echo/echo file body/echo image file body.bru b/packages/bruno-tests/collection/echo/echo file body/echo image file body.bru new file mode 100644 index 000000000..da1899a96 --- /dev/null +++ b/packages/bruno-tests/collection/echo/echo file body/echo image file body.bru @@ -0,0 +1,22 @@ +meta { + name: echo image body + type: http + seq: 1 +} + +post { + url: {{echo-host}} + body: file + auth: none +} + +body:file { + file: @file(bruno.png) @contentType(image/png) +} + +tests { + test("should return proper header", function() { + const contentType = res.getHeader('content-type'); + expect(contentType).to.eql('image/png'); + }); +} diff --git a/packages/bruno-tests/collection/echo/echo file body/echo json file body.bru b/packages/bruno-tests/collection/echo/echo file body/echo json file body.bru new file mode 100644 index 000000000..f2173a4b9 --- /dev/null +++ b/packages/bruno-tests/collection/echo/echo file body/echo json file body.bru @@ -0,0 +1,55 @@ +meta { + name: echo json file body + type: http + seq: 2 +} + +post { + url: {{echo-host}} + body: file + auth: none +} + +body:file { + file: @file(file.json) @contentType(application/json; charset=utf-8) +} + +tests { + test("should return json file body contents buffer", function() { + const data = res.getBody(); + expect(res.getBody()).to.eql({ + "type": "Buffer", + "data": [ + 123, + 10, + 32, + 32, + 34, + 104, + 101, + 108, + 108, + 111, + 34, + 58, + 32, + 34, + 98, + 114, + 117, + 110, + 111, + 34, + 10, + 125, + 10 + ] + }); + }); + + test("should return proper header", function() { + const contentType = res.getHeader('content-type'); + expect(contentType).to.eql('application/json; charset=utf-8'); + }); + +} \ No newline at end of file diff --git a/packages/bruno-tests/collection/echo/echo file body/echo text file body.bru b/packages/bruno-tests/collection/echo/echo file body/echo text file body.bru new file mode 100644 index 000000000..707a096cd --- /dev/null +++ b/packages/bruno-tests/collection/echo/echo file body/echo text file body.bru @@ -0,0 +1,30 @@ +meta { + name: echo text file body + type: http + seq: 3 +} + +post { + url: {{echo-host}} + body: file + auth: none +} + +body:file { + file: @file(file.txt) @contentType(text/plain; charset=utf-8) +} + +tests { + test("should return json file body contents", function() { + const data = res.getBody(); + const expectedData = `file.txt + + hello, bruno`; + expect(res.getBody()).to.eql(expectedData); + }); + + test("should return proper header", function() { + const contentType = res.getHeader('content-type'); + expect(contentType).to.eql('text/plain; charset=utf-8'); + }); +} \ No newline at end of file diff --git a/packages/bruno-tests/collection/echo/multiline/echo binary.bru b/packages/bruno-tests/collection/echo/multiline/echo binary.bru index d11b30413..704419886 100644 --- a/packages/bruno-tests/collection/echo/multiline/echo binary.bru +++ b/packages/bruno-tests/collection/echo/multiline/echo binary.bru @@ -6,10 +6,10 @@ meta { post { url: {{echo-host}} - body: binaryFile + body: file auth: none } -body:binary-file { +body:file { file: @file(bruno.png) @contentType(image/png) } diff --git a/packages/bruno-tests/collection/file.txt b/packages/bruno-tests/collection/file.txt new file mode 100644 index 000000000..0a1443d43 --- /dev/null +++ b/packages/bruno-tests/collection/file.txt @@ -0,0 +1,3 @@ +file.txt + +hello, bruno From 8f604efc7e6a8405049c945f53f8761738111db3 Mon Sep 17 00:00:00 2001 From: lohit Date: Tue, 4 Feb 2025 21:56:34 +0530 Subject: [PATCH 06/18] fixes tests for the file body pr (#3940) fixes tests for bruno-app and bruno-electron --- packages/bruno-app/src/utils/curl/curl-to-json.spec.js | 6 ++---- .../bruno-electron/src/ipc/network/prepare-request.js | 6 +++--- .../bruno-electron/tests/network/prepare-request.spec.js | 8 +++++--- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/bruno-app/src/utils/curl/curl-to-json.spec.js b/packages/bruno-app/src/utils/curl/curl-to-json.spec.js index 4c3194c2e..991150c57 100644 --- a/packages/bruno-app/src/utils/curl/curl-to-json.spec.js +++ b/packages/bruno-app/src/utils/curl/curl-to-json.spec.js @@ -113,11 +113,9 @@ describe('curlToJson', () => { isDataBinary: true, data: [ { - name: 'file', - value: ['/path/to/file'], - enabled: true, + filePath: '/path/to/file', contentType: 'application/json;charset=utf-8', - type: 'file' + selected: true } ] }); diff --git a/packages/bruno-electron/src/ipc/network/prepare-request.js b/packages/bruno-electron/src/ipc/network/prepare-request.js index 705d19f22..05df2fde7 100644 --- a/packages/bruno-electron/src/ipc/network/prepare-request.js +++ b/packages/bruno-electron/src/ipc/network/prepare-request.js @@ -176,10 +176,10 @@ const setAuthHeaders = (axiosRequest, request, collectionRoot) => { return axiosRequest; }; -const prepareRequest = async (item, collection, abortController) => { +const prepareRequest = async (item, collection = {}, abortController) => { const request = item.draft ? item.draft.request : item.request; const collectionRoot = get(collection, 'root', {}); - const collectionPath = collection.pathname; + const collectionPath = collection?.pathname; const headers = {}; let contentTypeDefined = false; let url = request.url; @@ -191,7 +191,7 @@ const prepareRequest = async (item, collection, abortController) => { } }); - const scriptFlow = collection.brunoConfig?.scripts?.flow ?? 'sandwich'; + const scriptFlow = collection?.brunoConfig?.scripts?.flow ?? 'sandwich'; const requestTreePath = getTreePathFromCollectionToItem(collection, item); if (requestTreePath && requestTreePath.length > 0) { mergeHeaders(collection, request, requestTreePath); diff --git a/packages/bruno-electron/tests/network/prepare-request.spec.js b/packages/bruno-electron/tests/network/prepare-request.spec.js index a624d3ea5..34bedcc90 100644 --- a/packages/bruno-electron/tests/network/prepare-request.spec.js +++ b/packages/bruno-electron/tests/network/prepare-request.spec.js @@ -7,15 +7,17 @@ describe('prepare-request: prepareRequest', () => { describe('Decomments request body', () => { it('If request body is valid JSON', async () => { const body = { mode: 'json', json: '{\n"test": "{{someVar}}" // comment\n}' }; - const expected = '{\n"test": "{{someVar}}" \n}'; - const result = prepareRequest({ request: { body } }, {}); + const expected = `{ +\"test\": \"{{someVar}}\" +}`; + const result = await prepareRequest({ request: { body }, collection: { pathname: '' } }); expect(result.data).toEqual(expected); }); it('If request body is not valid JSON', async () => { const body = { mode: 'json', json: '{\n"test": {{someVar}} // comment\n}' }; const expected = '{\n"test": {{someVar}} \n}'; - const result = prepareRequest({ request: { body } }, {}); + const result = await prepareRequest({ request: { body }, collection: { pathname: '' } }); expect(result.data).toEqual(expected); }); From 038f2d1f0b6f3c06ef277f5f23f0ddf36e0748d7 Mon Sep 17 00:00:00 2001 From: lohit Date: Tue, 4 Feb 2025 22:08:54 +0530 Subject: [PATCH 07/18] temp. revert tests for file body (#3941) * temporarily revert tests for file body --- .../echo/echo file body/echo file body.bru | 40 -------------- .../echo file body/echo image file body.bru | 22 -------- .../echo file body/echo json file body.bru | 55 ------------------- .../echo file body/echo text file body.bru | 30 ---------- 4 files changed, 147 deletions(-) delete mode 100644 packages/bruno-tests/collection/echo/echo file body/echo file body.bru delete mode 100644 packages/bruno-tests/collection/echo/echo file body/echo image file body.bru delete mode 100644 packages/bruno-tests/collection/echo/echo file body/echo json file body.bru delete mode 100644 packages/bruno-tests/collection/echo/echo file body/echo text file body.bru diff --git a/packages/bruno-tests/collection/echo/echo file body/echo file body.bru b/packages/bruno-tests/collection/echo/echo file body/echo file body.bru deleted file mode 100644 index b820e4808..000000000 --- a/packages/bruno-tests/collection/echo/echo file body/echo file body.bru +++ /dev/null @@ -1,40 +0,0 @@ -meta { - name: echo file body - type: http - seq: 4 -} - -post { - url: {{echo-host}} - body: file - auth: none -} - -body:file { - file: @file(ping.bru) @contentType(bru) -} - -tests { - test("should return bru file body contents", function() { - const data = res.getBody(); - const expectedData = `meta { - name: ping - type: http - seq: 1 - } - - get { - url: {{host}}/ping - body: none - auth: none - } - `; - expect(res.getBody()).to.eql(expectedData); - }); - - - test("should return proper header", function() { - const contentType = res.getHeader('content-type'); - expect(contentType).to.eql('bru'); - }); -} \ No newline at end of file diff --git a/packages/bruno-tests/collection/echo/echo file body/echo image file body.bru b/packages/bruno-tests/collection/echo/echo file body/echo image file body.bru deleted file mode 100644 index da1899a96..000000000 --- a/packages/bruno-tests/collection/echo/echo file body/echo image file body.bru +++ /dev/null @@ -1,22 +0,0 @@ -meta { - name: echo image body - type: http - seq: 1 -} - -post { - url: {{echo-host}} - body: file - auth: none -} - -body:file { - file: @file(bruno.png) @contentType(image/png) -} - -tests { - test("should return proper header", function() { - const contentType = res.getHeader('content-type'); - expect(contentType).to.eql('image/png'); - }); -} diff --git a/packages/bruno-tests/collection/echo/echo file body/echo json file body.bru b/packages/bruno-tests/collection/echo/echo file body/echo json file body.bru deleted file mode 100644 index f2173a4b9..000000000 --- a/packages/bruno-tests/collection/echo/echo file body/echo json file body.bru +++ /dev/null @@ -1,55 +0,0 @@ -meta { - name: echo json file body - type: http - seq: 2 -} - -post { - url: {{echo-host}} - body: file - auth: none -} - -body:file { - file: @file(file.json) @contentType(application/json; charset=utf-8) -} - -tests { - test("should return json file body contents buffer", function() { - const data = res.getBody(); - expect(res.getBody()).to.eql({ - "type": "Buffer", - "data": [ - 123, - 10, - 32, - 32, - 34, - 104, - 101, - 108, - 108, - 111, - 34, - 58, - 32, - 34, - 98, - 114, - 117, - 110, - 111, - 34, - 10, - 125, - 10 - ] - }); - }); - - test("should return proper header", function() { - const contentType = res.getHeader('content-type'); - expect(contentType).to.eql('application/json; charset=utf-8'); - }); - -} \ No newline at end of file diff --git a/packages/bruno-tests/collection/echo/echo file body/echo text file body.bru b/packages/bruno-tests/collection/echo/echo file body/echo text file body.bru deleted file mode 100644 index 707a096cd..000000000 --- a/packages/bruno-tests/collection/echo/echo file body/echo text file body.bru +++ /dev/null @@ -1,30 +0,0 @@ -meta { - name: echo text file body - type: http - seq: 3 -} - -post { - url: {{echo-host}} - body: file - auth: none -} - -body:file { - file: @file(file.txt) @contentType(text/plain; charset=utf-8) -} - -tests { - test("should return json file body contents", function() { - const data = res.getBody(); - const expectedData = `file.txt - - hello, bruno`; - expect(res.getBody()).to.eql(expectedData); - }); - - test("should return proper header", function() { - const contentType = res.getHeader('content-type'); - expect(contentType).to.eql('text/plain; charset=utf-8'); - }); -} \ No newline at end of file From 722d9788caba9fb1ada104f804ee612ed3ec030b Mon Sep 17 00:00:00 2001 From: naman-bruno Date: Thu, 6 Feb 2025 19:34:10 +0530 Subject: [PATCH 08/18] Feature: Improve tab UX (#3831) --------- Co-authored-by: ramki-bruno --- .../RequestTabs/RequestTab/SpecialTab.js | 8 +-- .../RequestTabs/RequestTab/index.js | 11 +-- .../Collection/CollectionItem/index.js | 42 +++++------ .../Sidebar/Collections/Collection/index.js | 32 ++++++--- .../src/providers/ReduxStore/index.js | 3 +- .../middlewares/draft/middleware.js | 55 +++++++++++++++ .../ReduxStore/middlewares/draft/utils.js | 21 ++++++ .../src/providers/ReduxStore/slices/tabs.js | 70 +++++++++++++------ packages/bruno-app/src/utils/tabs/index.js | 7 ++ 9 files changed, 189 insertions(+), 60 deletions(-) create mode 100644 packages/bruno-app/src/providers/ReduxStore/middlewares/draft/middleware.js create mode 100644 packages/bruno-app/src/providers/ReduxStore/middlewares/draft/utils.js diff --git a/packages/bruno-app/src/components/RequestTabs/RequestTab/SpecialTab.js b/packages/bruno-app/src/components/RequestTabs/RequestTab/SpecialTab.js index 1cbb0aa05..b895c10fe 100644 --- a/packages/bruno-app/src/components/RequestTabs/RequestTab/SpecialTab.js +++ b/packages/bruno-app/src/components/RequestTabs/RequestTab/SpecialTab.js @@ -2,15 +2,15 @@ import React from 'react'; import CloseTabIcon from './CloseTabIcon'; import { IconVariable, IconSettings, IconRun, IconFolder, IconShieldLock } from '@tabler/icons'; -const SpecialTab = ({ handleCloseClick, type, tabName }) => { +const SpecialTab = ({ handleCloseClick, type, tabName, handleDoubleClick }) => { const getTabInfo = (type, tabName) => { switch (type) { case 'collection-settings': { return ( - <> +
Collection - +
); } case 'collection-overview': { @@ -31,7 +31,7 @@ const SpecialTab = ({ handleCloseClick, type, tabName }) => { } case 'folder-settings': { return ( -
+
{tabName || 'Folder'}
diff --git a/packages/bruno-app/src/components/RequestTabs/RequestTab/index.js b/packages/bruno-app/src/components/RequestTabs/RequestTab/index.js index 2d74a4290..562fc319f 100644 --- a/packages/bruno-app/src/components/RequestTabs/RequestTab/index.js +++ b/packages/bruno-app/src/components/RequestTabs/RequestTab/index.js @@ -1,6 +1,6 @@ import React, { useState, useRef, Fragment } from 'react'; import get from 'lodash/get'; -import { closeTabs } from 'providers/ReduxStore/slices/tabs'; +import { closeTabs, makeTabPermanent } from 'providers/ReduxStore/slices/tabs'; import { saveRequest } from 'providers/ReduxStore/slices/collections/actions'; import { deleteRequestDraft } from 'providers/ReduxStore/slices/collections'; import { useTheme } from 'providers/Theme'; @@ -73,13 +73,13 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi if (['collection-settings', 'collection-overview', 'folder-settings', 'variables', 'collection-runner', 'security-settings'].includes(tab.type)) { return ( {tab.type === 'folder-settings' ? ( - + dispatch(makeTabPermanent({ uid: tab.uid }))} type={tab.type} tabName={folder?.name} /> ) : ( - + dispatch(makeTabPermanent({ uid: tab.uid }))} type={tab.type} /> )} ); @@ -144,8 +144,9 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi /> )}
dispatch(makeTabPermanent({ uid: tab.uid }))} onMouseUp={(e) => { if (!item.draft) return handleMouseUp(e); diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js index 2bfece171..3da23bcf5 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js @@ -5,7 +5,7 @@ import classnames from 'classnames'; import { useDrag, useDrop } from 'react-dnd'; import { IconChevronRight, IconDots } from '@tabler/icons'; import { useSelector, useDispatch } from 'react-redux'; -import { addTab, focusTab } from 'providers/ReduxStore/slices/tabs'; +import { addTab, focusTab, makeTabPermanent } from 'providers/ReduxStore/slices/tabs'; import { moveItem, showInFolder, sendRequest } from 'providers/ReduxStore/slices/collections/actions'; import { collectionFolderClicked } from 'providers/ReduxStore/slices/collections'; import Dropdown from 'components/Dropdown'; @@ -23,7 +23,9 @@ import { hideHomePage } from 'providers/ReduxStore/slices/app'; import toast from 'react-hot-toast'; import StyledWrapper from './StyledWrapper'; import NetworkError from 'components/ResponsePane/NetworkError/index'; -import CollectionItemIcon from './CollectionItemIcon/index'; +import { findItemInCollection } from 'utils/collections'; +import CollectionItemIcon from './CollectionItemIcon'; +import { scrollToTheActiveTab } from 'utils/tabs'; const CollectionItem = ({ item, collection, searchText }) => { const tabs = useSelector((state) => state.tabs.tabs); @@ -83,13 +85,6 @@ const CollectionItem = ({ item, collection, searchText }) => { 'item-hovered': isOver }); - const scrollToTheActiveTab = () => { - const activeTab = document.querySelector('.request-tab.active'); - if (activeTab) { - activeTab.scrollIntoView({ behavior: 'smooth', block: 'start' }); - } - }; - const handleRun = async () => { dispatch(sendRequest(item, collection.uid)).catch((err) => toast.custom((t) => toast.dismiss(t.id)} />, { @@ -99,10 +94,13 @@ const CollectionItem = ({ item, collection, searchText }) => { }; const handleClick = (event) => { + if (event.detail != 1) return; //scroll to the active tab setTimeout(scrollToTheActiveTab, 50); - - if (isItemARequest(item)) { + + const isRequest = isItemARequest(item); + + if (isRequest) { dispatch(hideHomePage()); if (itemIsOpenedInTabs(item, tabs)) { dispatch( @@ -112,20 +110,21 @@ const CollectionItem = ({ item, collection, searchText }) => { ); return; } + dispatch( addTab({ uid: item.uid, collectionUid: collection.uid, - requestPaneTab: getDefaultRequestPaneTab(item) + requestPaneTab: getDefaultRequestPaneTab(item), + type: 'request', }) ); - return; - } + } else { dispatch( addTab({ uid: item.uid, collectionUid: collection.uid, - type: 'folder-settings' + type: 'folder-settings', }) ); dispatch( @@ -134,9 +133,12 @@ const CollectionItem = ({ item, collection, searchText }) => { collectionUid: collection.uid }) ); + } }; - const handleFolderCollapse = () => { + const handleFolderCollapse = (e) => { + e.stopPropagation(); + e.preventDefault(); dispatch( collectionFolderClicked({ itemUid: item.uid, @@ -156,10 +158,6 @@ const CollectionItem = ({ item, collection, searchText }) => { } }; - const handleDoubleClick = (event) => { - setRenameItemModalOpen(true); - }; - let indents = range(item.depth); const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref); const isFolder = isItemAFolder(item); @@ -180,6 +178,10 @@ const CollectionItem = ({ item, collection, searchText }) => { } } + const handleDoubleClick = (event) => { + dispatch(makeTabPermanent({ uid: item.uid })) + }; + // we need to sort request items by seq property const sortRequestItems = (items = []) => { return items.sort((a, b) => a.seq - b.seq); diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js index 1b16f4eea..f8e4484bc 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js @@ -7,8 +7,8 @@ import { IconChevronRight, IconDots, IconLoader2 } from '@tabler/icons'; import Dropdown from 'components/Dropdown'; import { collapseCollection } from 'providers/ReduxStore/slices/collections'; import { mountCollection, moveItemToRootOfCollection } from 'providers/ReduxStore/slices/collections/actions'; -import { useDispatch } from 'react-redux'; -import { addTab } from 'providers/ReduxStore/slices/tabs'; +import { useDispatch, useSelector } from 'react-redux'; +import { addTab, makeTabPermanent } from 'providers/ReduxStore/slices/tabs'; import NewRequest from 'components/Sidebar/NewRequest'; import NewFolder from 'components/Sidebar/NewFolder'; import CollectionItem from './CollectionItem'; @@ -20,7 +20,8 @@ import { isItemAFolder, isItemARequest } from 'utils/collections'; import RenameCollection from './RenameCollection'; import StyledWrapper from './StyledWrapper'; import CloneCollection from './CloneCollection'; -import { areItemsLoading } from 'utils/collections'; +import { areItemsLoading, findItemInCollection } from 'utils/collections'; +import { scrollToTheActiveTab } from 'utils/tabs'; const Collection = ({ collection, searchText }) => { const [showNewFolderModal, setShowNewFolderModal] = useState(false); @@ -29,6 +30,7 @@ const Collection = ({ collection, searchText }) => { const [showCloneCollectionModalOpen, setShowCloneCollectionModalOpen] = useState(false); const [showExportCollectionModal, setShowExportCollectionModal] = useState(false); const [showRemoveCollectionModal, setShowRemoveCollectionModal] = useState(false); + const tabs = useSelector((state) => state.tabs.tabs); const dispatch = useDispatch(); const isLoading = areItemsLoading(collection); @@ -60,9 +62,11 @@ const Collection = ({ collection, searchText }) => { }); const handleClick = (event) => { + if (event.detail != 1) return; // Check if the click came from the chevron icon const isChevronClick = event.target.closest('svg')?.classList.contains('chevron-icon'); - + setTimeout(scrollToTheActiveTab, 50); + if (collection.mountStatus === 'unmounted') { dispatch(mountCollection({ collectionUid: collection.uid, @@ -70,20 +74,30 @@ const Collection = ({ collection, searchText }) => { brunoConfig: collection.brunoConfig })); } + dispatch(collapseCollection(collection.uid)); - - // Only open collection settings if not clicking the chevron + if(!isChevronClick) { dispatch( addTab({ - uid: uuid(), + uid: collection.uid, collectionUid: collection.uid, - type: 'collection-settings' + type: 'collection-settings', }) ); } }; + const handleDoubleClick = (event) => { + dispatch(makeTabPermanent({ uid: collection.uid })) + }; + + const handleCollectionCollapse = (e) => { + e.stopPropagation(); + e.preventDefault(); + dispatch(collapseCollection(collection.uid)); + } + const handleRightClick = (event) => { const _menuDropdown = menuDropdownTippyRef.current; if (_menuDropdown) { @@ -158,6 +172,7 @@ const Collection = ({ collection, searchText }) => {
{ strokeWidth={2} className={`chevron-icon ${iconClassName}`} style={{ width: 16, minWidth: 16, color: 'rgb(160 160 160)' }} + onClick={handleCollectionCollapse} />
File
Content-Type
Enabled
+
File
+
+
Content-Type
+
+
Selected
+
- - handleParamChange( - { - target: { - value: newValue - } - }, - param, - 'value' - ) - } - collection={collection} - /> + + handleParamChange( + { + target: { + filePath: path + } + }, + param, + 'filePath' + ) + } + collection={collection} + /> { handleParamChange( { target: { - value: newValue + contentType: newValue } }, param, @@ -141,19 +132,19 @@ const Binary = ({ item, collection }) => { handleParamChange(e, param, 'enabled')} + onChange={(e) => handleParamChange(e, param, 'selected')} /> -
- +
+