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 797771bbb..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 }) => {
- 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 }) => {
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 }) => {
return filePath;
});
- onChange(filePaths);
+ onChange(isSingleFilePicker ? filePaths[0] : filePaths);
})
.catch((error) => {
console.error(error);
@@ -42,14 +41,14 @@ const FilePickerEditor = ({ value, onChange, collection }) => {
};
const clear = () => {
- onChange([]);
+ onChange(isSingleFilePicker ? '' : []);
};
const renderButtonText = (filenames) => {
if (filenames.length == 1) {
return filenames[0];
}
- return filenames.length + ' files selected';
+ return filenames.length + ' file(s) selected';
};
return filenames.length > 0 ? (
@@ -66,9 +65,9 @@ const FilePickerEditor = ({ value, onChange, collection }) => {
) : (
);
};
-export default FilePickerEditor;
+export default FilePickerEditor;
\ No newline at end of file
diff --git a/packages/bruno-app/src/components/RequestPane/FileBody/StyledWrapper.js b/packages/bruno-app/src/components/RequestPane/FileBody/StyledWrapper.js
new file mode 100644
index 000000000..35adfcc1f
--- /dev/null
+++ b/packages/bruno-app/src/components/RequestPane/FileBody/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/FileBody/index.js b/packages/bruno-app/src/components/RequestPane/FileBody/index.js
new file mode 100644
index 000000000..d97953aa5
--- /dev/null
+++ b/packages/bruno-app/src/components/RequestPane/FileBody/index.js
@@ -0,0 +1,164 @@
+import React, { useState, useEffect } from 'react';
+import { get, cloneDeep, isArray } from 'lodash';
+import { IconTrash } from '@tabler/icons';
+import { useDispatch } from 'react-redux';
+import { useTheme } from 'providers/Theme';
+import { 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 FileBody = ({ item, collection }) => {
+ const dispatch = useDispatch();
+ const { storedTheme } = useTheme();
+ const params = item.draft ? get(item, 'draft.request.body.file') : get(item, 'request.body.file');
+
+ const [enabledFileUid, setEnableFileUid] = useState(params && params.length ? params[0].uid : '');
+
+ const addFile = () => {
+ dispatch(
+ _addFile({
+ itemUid: item.uid,
+ collectionUid: collection.uid,
+ })
+ );
+ };
+
+ const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
+ const handleRun = () => dispatch(sendRequest(item, collection.uid));
+
+ const handleParamChange = (e, _param, type) => {
+ const param = cloneDeep(_param);
+ switch (type) {
+ case 'filePath': {
+ param.filePath = e.target.filePath;
+ param.contentType = "";
+ break;
+ }
+ case 'contentType': {
+ param.contentType = e.target.contentType;
+ break;
+ }
+ case 'selected': {
+ param.selected = e.target.selected;
+ setEnableFileUid(param.uid)
+ break;
+ }
+ }
+ dispatch(
+ updateFile({
+ param: param,
+ itemUid: item.uid,
+ collectionUid: collection.uid
+ })
+ );
+ };
+
+ const handleRemoveParams = (param) => {
+ dispatch(
+ deleteFile({
+ paramUid: param.uid,
+ itemUid: item.uid,
+ collectionUid: collection.uid
+ })
+ );
+ };
+
+ return (
+
+
+
+
+ |
+ File
+ |
+
+ Content-Type
+ |
+
+ Selected
+ |
+ |
+
+
+
+ {params && params.length
+ ? params.map((param, index) => {
+ return (
+
+ |
+
+ handleParamChange(
+ {
+ target: {
+ filePath: path
+ }
+ },
+ param,
+ 'filePath'
+ )
+ }
+ collection={collection}
+ />
+ |
+
+
+ handleParamChange(
+ {
+ target: {
+ contentType: newValue
+ }
+ },
+ param,
+ 'contentType'
+ )
+ }
+ onRun={handleRun}
+ collection={collection}
+ />
+ |
+
+
+ handleParamChange(e, param, 'selected')}
+ />
+
+ |
+
+
+
+
+ |
+
+ );
+ })
+ : null}
+
+
+
+
+
+
+ );
+};
+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 29b66d58d..db73597df 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('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 ca60c8662..8f7fa8465 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 FileBody from '../FileBody/index';
const RequestBody = ({ item, collection }) => {
const dispatch = useDispatch();
@@ -62,6 +63,10 @@ const RequestBody = ({ item, collection }) => {
);
}
+ if (bodyMode === 'file') {
+ return
+ }
+
if (bodyMode === 'formUrlEncoded') {
return ;
}
@@ -72,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/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..3e426eb7f 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);
@@ -280,6 +282,9 @@ const CollectionItem = ({ item, collection, searchText }) => {
style={{
paddingLeft: 8
}}
+ onClick={handleClick}
+ onContextMenu={handleRightClick}
+ onDoubleClick={handleDoubleClick}
>
{isFolder ? (
@@ -295,9 +300,6 @@ const CollectionItem = ({ item, collection, searchText }) => {
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..3fe00c686 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);
@@ -52,6 +54,16 @@ const Collection = ({ collection, searchText }) => {
);
};
+ const ensureCollectionIsMounted = () => {
+ if (collection.mountStatus === 'unmounted') {
+ dispatch(mountCollection({
+ collectionUid: collection.uid,
+ collectionPathname: collection.pathname,
+ brunoConfig: collection.brunoConfig
+ }));
+ }
+ }
+
const hasSearchText = searchText && searchText?.trim()?.length;
const collectionIsCollapsed = hasSearchText ? false : collection.collapsed;
@@ -60,30 +72,37 @@ 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');
-
- if (collection.mountStatus === 'unmounted') {
- dispatch(mountCollection({
- collectionUid: collection.uid,
- collectionPathname: collection.pathname,
- brunoConfig: collection.brunoConfig
- }));
- }
- dispatch(collapseCollection(collection.uid));
+ setTimeout(scrollToTheActiveTab, 50);
- // Only open collection settings if not clicking the chevron
+ ensureCollectionIsMounted();
+
+ dispatch(collapseCollection(collection.uid));
+
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();
+ ensureCollectionIsMounted();
+ dispatch(collapseCollection(collection.uid));
+ }
+
const handleRightClick = (event) => {
const _menuDropdown = menuDropdownTippyRef.current;
if (_menuDropdown) {
@@ -158,6 +177,7 @@ const Collection = ({ collection, searchText }) => {
{
strokeWidth={2}
className={`chevron-icon ${iconClassName}`}
style={{ width: 16, minWidth: 16, color: 'rgb(160 160 160)' }}
+ onClick={handleCollectionCollapse}
/>