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 ( + + + + + + + + + + + + {params && params.length + ? params.map((param, index) => { + return ( + + + + + + + ); + }) + : null} + +
+
File
+
+
Content-Type
+
+
Selected
+
+ + handleParamChange( + { + target: { + filePath: path + } + }, + param, + 'filePath' + ) + } + collection={collection} + /> + + + handleParamChange( + { + target: { + contentType: newValue + } + }, + param, + 'contentType' + ) + } + onRun={handleRun} + collection={collection} + /> + +
+ handleParamChange(e, param, 'selected')} + /> +
+
+
+ +
+
+
+ +
+
+ ); +}; +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} />