feat: folder sequencing (#4595)

Co-authored-by: Pooja Belaramani <pooja@usebruno.com>
Co-authored-by: naman-bruno <naman@usebruno.com>
Co-authored-by: lohit <lohit@usebruno.com>
This commit is contained in:
lohit
2025-05-05 16:52:00 +05:30
committed by GitHub
parent 526fcabffe
commit 38c307d6f1
39 changed files with 1280 additions and 612 deletions

View File

@@ -1,5 +1,5 @@
import React, { useRef, useEffect } from 'react';
import { useDispatch } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import { useFormik } from 'formik';
import * as Yup from 'yup';
import { browseDirectory } from 'providers/ReduxStore/slices/collections/actions';
@@ -11,11 +11,13 @@ import Help from 'components/Help';
import PathDisplay from 'components/PathDisplay';
import { useState } from 'react';
import { IconArrowBackUp, IconEdit } from "@tabler/icons";
import { findCollectionByUid } from 'utils/collections/index';
const CloneCollection = ({ onClose, collection }) => {
const CloneCollection = ({ onClose, collectionUid }) => {
const inputRef = useRef();
const dispatch = useDispatch();
const [isEditing, toggleEditing] = useState(false);
const collection = useSelector(state => findCollectionByUid(state.collections.collections, collectionUid));
const { name } = collection;
const formik = useFormik({
@@ -46,7 +48,7 @@ const CloneCollection = ({ onClose, collection }) => {
values.collectionName,
values.collectionFolderName,
values.collectionLocation,
collection.pathname
collection?.pathname
)
)
.then(() => {

View File

@@ -15,7 +15,7 @@ import Portal from 'components/Portal';
import Dropdown from 'components/Dropdown';
import StyledWrapper from './StyledWrapper';
const CloneCollectionItem = ({ collection, item, onClose }) => {
const CloneCollectionItem = ({ collectionUid, collectionPathname, item, onClose }) => {
const dispatch = useDispatch();
const isFolder = isItemAFolder(item);
const inputRef = useRef();
@@ -49,7 +49,7 @@ const CloneCollectionItem = ({ collection, item, onClose }) => {
.test('not-reserved', `The file names "collection" and "folder" are reserved in bruno`, value => !['collection', 'folder'].includes(value))
}),
onSubmit: (values) => {
dispatch(cloneItem(values.name, values.filename, item.uid, collection.uid))
dispatch(cloneItem(values.name, values.filename, item.uid, collectionUid))
.then(() => {
toast.success('Request cloned!');
onClose();
@@ -172,8 +172,7 @@ const CloneCollectionItem = ({ collection, item, onClose }) => {
) : (
<div className='relative flex flex-row gap-1 items-center justify-between'>
<PathDisplay
collection={collection}
dirName={path.relative(collection?.pathname, path.dirname(item?.pathname))}
dirName={path.relative(collectionPathname, path.dirname(item?.pathname))}
baseName={formik.values.filename}
/>
</div>

View File

@@ -7,11 +7,11 @@ import { deleteItem } from 'providers/ReduxStore/slices/collections/actions';
import { recursivelyGetAllItemUids } from 'utils/collections';
import StyledWrapper from './StyledWrapper';
const DeleteCollectionItem = ({ onClose, item, collection }) => {
const DeleteCollectionItem = ({ onClose, item, collectionUid }) => {
const dispatch = useDispatch();
const isFolder = isItemAFolder(item);
const onConfirm = () => {
dispatch(deleteItem(item.uid, collection.uid)).then(() => {
dispatch(deleteItem(item.uid, collectionUid)).then(() => {
if (isFolder) {
// close all tabs that belong to the folder

View File

@@ -10,9 +10,11 @@ import { getLanguages } from 'utils/codegenerator/targets';
import { useSelector } from 'react-redux';
import { getGlobalEnvironmentVariables } from 'utils/collections/index';
const GenerateCodeItem = ({ collection, item, onClose }) => {
const GenerateCodeItem = ({ collectionUid, item, onClose }) => {
const languages = getLanguages();
const collection = useSelector(state => state.collections.collections?.find(c => c.uid === collectionUid));
const { globalEnvironments, activeGlobalEnvironmentUid } = useSelector((state) => state.globalEnvironments);
const globalEnvironmentVariables = getGlobalEnvironmentVariables({ globalEnvironments, activeGlobalEnvironmentUid });

View File

@@ -16,7 +16,7 @@ import Portal from 'components/Portal';
import Dropdown from 'components/Dropdown';
import StyledWrapper from './StyledWrapper';
const RenameCollectionItem = ({ collection, item, onClose }) => {
const RenameCollectionItem = ({ collectionUid, collectionPathname, item, onClose }) => {
const dispatch = useDispatch();
const isFolder = isItemAFolder(item);
const inputRef = useRef();
@@ -57,13 +57,13 @@ const RenameCollectionItem = ({ collection, item, onClose }) => {
return;
}
if (!isFolder && item.draft) {
await dispatch(saveRequest(item.uid, collection.uid, true));
await dispatch(saveRequest(item.uid, collectionUid, true));
}
const { name: newName, filename: newFilename } = values;
try {
let renameConfig = {
itemUid: item.uid,
collectionUid: collection.uid,
collectionUid,
};
renameConfig['newName'] = newName;
if (itemFilename !== newFilename) {
@@ -191,8 +191,7 @@ const RenameCollectionItem = ({ collection, item, onClose }) => {
) : (
<div className='relative flex flex-row gap-1 items-center justify-between'>
<PathDisplay
collection={collection}
dirName={path.relative(collection?.pathname, path.dirname(item?.pathname))}
dirName={path.relative(collectionPathname, path.dirname(item?.pathname))}
baseName={formik.values.filename}
/>
</div>

View File

@@ -2,16 +2,18 @@ import React from 'react';
import get from 'lodash/get';
import { uuid } from 'utils/common';
import Modal from 'components/Modal';
import { useDispatch } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import { addTab } from 'providers/ReduxStore/slices/tabs';
import { runCollectionFolder } from 'providers/ReduxStore/slices/collections/actions';
import { flattenItems } from 'utils/collections';
import StyledWrapper from './StyledWrapper';
import { areItemsLoading } from 'utils/collections';
const RunCollectionItem = ({ collection, item, onClose }) => {
const RunCollectionItem = ({ collectionUid, item, onClose }) => {
const dispatch = useDispatch();
const collection = useSelector(state => state.collections.collections?.find(c => c.uid === collectionUid));
const onSubmit = (recursive) => {
dispatch(
addTab({
@@ -34,8 +36,6 @@ const RunCollectionItem = ({ collection, item, onClose }) => {
const recursiveRunLength = getRequestsCount(flattenedItems);
const isFolderLoading = areItemsLoading(item);
console.log(item);
console.log(isFolderLoading);
return (
<StyledWrapper>

View File

@@ -22,6 +22,65 @@ const Wrapper = styled.div`
height: 1.875rem;
cursor: pointer;
user-select: none;
position: relative;
/* Common styles for drop indicators */
&::before,
&::after {
content: '';
position: absolute;
left: 0;
right: 0;
height: 2px;
background: ${(props) => props.theme.dragAndDrop.border};
opacity: 0;
pointer-events: none;
}
&::before {
top: 0;
}
&::after {
bottom: 0;
}
/* Drop target styles */
&.drop-target {
background-color: ${(props) => props.theme.dragAndDrop.hoverBg};
&::before,
&::after {
opacity: 0;
}
}
&.drop-target-above {
&::before {
opacity: 1;
height: 2px;
}
}
&.drop-target-below {
&::after {
opacity: 1;
height: 2px;
}
}
/* Inside drop target style */
&.drop-target {
&::before {
top: 0;
bottom: 0;
height: 100%;
opacity: 1;
background: ${(props) => props.theme.dragAndDrop.hoverBg};
border: ${(props) => props.theme.dragAndDrop.borderStyle} ${(props) => props.theme.dragAndDrop.border};
// border-radius: 4px;
}
}
.rotate-90 {
transform: rotateZ(90deg);
@@ -45,6 +104,20 @@ const Wrapper = styled.div`
}
}
&.item-target {
background: #ccc3;
}
&.item-seperator {
.seperator {
bottom: 0px;
position: absolute;
height: 3px;
width: 100%;
background: #ccc3;
}
}
&.item-focused-in-tab {
background: ${(props) => props.theme.sidebar.collection.item.bg};

View File

@@ -1,4 +1,4 @@
import React, { useState, useRef, forwardRef, useEffect } from 'react';
import React, { useState, useRef, forwardRef } from 'react';
import range from 'lodash/range';
import filter from 'lodash/filter';
import classnames from 'classnames';
@@ -6,7 +6,7 @@ import { useDrag, useDrop } from 'react-dnd';
import { IconChevronRight, IconDots } from '@tabler/icons';
import { useSelector, useDispatch } from 'react-redux';
import { addTab, focusTab, makeTabPermanent } from 'providers/ReduxStore/slices/tabs';
import { moveItem, showInFolder, sendRequest } from 'providers/ReduxStore/slices/collections/actions';
import { handleCollectionItemDrop, moveItem, sendRequest, showInFolder, updateItemsSequences } from 'providers/ReduxStore/slices/collections/actions';
import { collectionFolderClicked } from 'providers/ReduxStore/slices/collections';
import Dropdown from 'components/Dropdown';
import NewRequest from 'components/Sidebar/NewRequest';
@@ -26,13 +26,21 @@ import NetworkError from 'components/ResponsePane/NetworkError/index';
import CollectionItemInfo from './CollectionItemInfo/index';
import CollectionItemIcon from './CollectionItemIcon';
import { scrollToTheActiveTab } from 'utils/tabs';
import { isTabForItemActive as isTabForItemActiveSelector, isTabForItemPresent as isTabForItemPresentSelector } from 'src/selectors/tab';
import { isEqual } from 'lodash';
const CollectionItem = ({ item, collection, searchText }) => {
const tabs = useSelector((state) => state.tabs.tabs);
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
const CollectionItem = ({ item, collectionUid, collectionPathname, searchText }) => {
const _isTabForItemActiveSelector = isTabForItemActiveSelector({ itemUid: item.uid });
const isTabForItemActive = useSelector(_isTabForItemActiveSelector, isEqual);
const _isTabForItemPresentSelector = isTabForItemPresentSelector({ itemUid: item.uid });
const isTabForItemPresent = useSelector(_isTabForItemPresentSelector, isEqual);
const isSidebarDragging = useSelector((state) => state.app.isDragging);
const dispatch = useDispatch();
const collectionItemRef = useRef(null);
// We use a single ref for drag and drop.
const ref = useRef(null);
const [renameItemModalOpen, setRenameItemModalOpen] = useState(false);
const [cloneItemModalOpen, setCloneItemModalOpen] = useState(false);
@@ -44,9 +52,12 @@ const CollectionItem = ({ item, collection, searchText }) => {
const [itemInfoModalOpen, setItemInfoModalOpen] = useState(false);
const hasSearchText = searchText && searchText?.trim()?.length;
const itemIsCollapsed = hasSearchText ? false : item.collapsed;
const isFolder = isItemAFolder(item);
const [dropType, setDropType] = useState(null); // 'adjacent' or 'inside'
const [{ isDragging }, drag] = useDrag({
type: `collection-item-${collection.uid}`,
type: `collection-item-${collectionUid}`,
item: item,
collect: (monitor) => ({
isDragging: monitor.isDragging()
@@ -56,21 +67,51 @@ const CollectionItem = ({ item, collection, searchText }) => {
}
});
const [{ isOver }, drop] = useDrop({
accept: `collection-item-${collection.uid}`,
drop: (draggedItem) => {
dispatch(moveItem(collection.uid, draggedItem.uid, item.uid));
const determineDropType = (monitor) => {
const hoverBoundingRect = ref.current?.getBoundingClientRect();
const clientOffset = monitor.getClientOffset();
if (!hoverBoundingRect || !clientOffset) return null;
const clientY = clientOffset.y - hoverBoundingRect.top;
const folderUpperThreshold = hoverBoundingRect.height * 0.35;
const fileUpperThreshold = hoverBoundingRect.height * 0.5;
if (isItemAFolder(item)) {
return clientY < folderUpperThreshold ? 'adjacent' : 'inside';
} else {
return clientY < fileUpperThreshold ? 'adjacent' : null;
}
};
const [{ isOver, canDrop }, drop] = useDrop({
accept: `collection-item-${collectionUid}`,
hover: (draggedItem, monitor) => {
const { uid: targetItemUid } = item;
const { uid: draggedItemUid } = draggedItem;
if (draggedItemUid === targetItemUid) return;
const dropType = determineDropType(monitor);
setDropType(dropType);
},
canDrop: (draggedItem) => {
return draggedItem.uid !== item.uid;
drop: async (draggedItem, monitor) => {
const { uid: targetItemUid } = item;
const { uid: draggedItemUid } = draggedItem;
if (draggedItemUid === targetItemUid) return;
const dropType = determineDropType(monitor);
if (!dropType) return;
await dispatch(handleCollectionItemDrop({ targetItem: item, draggedItem, dropType, collectionUid }))
setDropType(null);
},
canDrop: (draggedItem) => draggedItem.uid !== item.uid,
collect: (monitor) => ({
isOver: monitor.isOver(),
isOver: monitor.isOver()
}),
});
drag(drop(collectionItemRef));
const dropdownTippyRef = useRef();
const MenuIcon = forwardRef((props, ref) => {
return (
@@ -84,13 +125,15 @@ const CollectionItem = ({ item, collection, searchText }) => {
'rotate-90': !itemIsCollapsed
});
const itemRowClassName = classnames('flex collection-item-name items-center', {
'item-focused-in-tab': item.uid == activeTabUid,
'item-hovered': isOver
const itemRowClassName = classnames('flex collection-item-name relative items-center', {
'item-focused-in-tab': isTabForItemActive,
'item-hovered': isOver && canDrop,
'drop-target': isOver && dropType === 'inside',
'drop-target-above': isOver && dropType === 'adjacent'
});
const handleRun = async () => {
dispatch(sendRequest(item, collection.uid)).catch((err) =>
dispatch(sendRequest(item, collectionUid)).catch((err) =>
toast.custom((t) => <NetworkError onClose={() => toast.dismiss(t.id)} />, {
duration: 5000
})
@@ -101,12 +144,10 @@ const CollectionItem = ({ item, collection, searchText }) => {
if (event && event.detail != 1) return;
//scroll to the active tab
setTimeout(scrollToTheActiveTab, 50);
const isRequest = isItemARequest(item);
if (isRequest) {
dispatch(hideHomePage());
if (itemIsOpenedInTabs(item, tabs)) {
if (isTabForItemPresent) {
dispatch(
focusTab({
uid: item.uid
@@ -114,11 +155,10 @@ const CollectionItem = ({ item, collection, searchText }) => {
);
return;
}
dispatch(
addTab({
uid: item.uid,
collectionUid: collection.uid,
collectionUid: collectionUid,
requestPaneTab: getDefaultRequestPaneTab(item),
type: 'request',
})
@@ -127,14 +167,14 @@ const CollectionItem = ({ item, collection, searchText }) => {
dispatch(
addTab({
uid: item.uid,
collectionUid: collection.uid,
collectionUid: collectionUid,
type: 'folder-settings',
})
);
dispatch(
collectionFolderClicked({
itemUid: item.uid,
collectionUid: collection.uid
collectionUid: collectionUid
})
);
}
@@ -146,10 +186,10 @@ const CollectionItem = ({ item, collection, searchText }) => {
dispatch(
collectionFolderClicked({
itemUid: item.uid,
collectionUid: collection.uid
collectionUid: collectionUid
})
);
}
};
const handleRightClick = (event) => {
const _menuDropdown = dropdownTippyRef.current;
@@ -164,7 +204,6 @@ const CollectionItem = ({ item, collection, searchText }) => {
let indents = range(item.depth);
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
const isFolder = isItemAFolder(item);
const className = classnames('flex flex-col w-full', {
'is-sidebar-dragging': isSidebarDragging
@@ -183,49 +222,14 @@ const CollectionItem = ({ item, collection, searchText }) => {
}
const handleDoubleClick = (event) => {
dispatch(makeTabPermanent({ uid: item.uid }))
dispatch(makeTabPermanent({ uid: item.uid }));
};
// we need to sort request items by seq property
const sortRequestItems = (items = []) => {
// Sort items by their "seq" property.
const sortItemsBySequence = (items = []) => {
return items.sort((a, b) => a.seq - b.seq);
};
// we need to sort folder items by name alphabetically
const sortFolderItems = (items = []) => {
return items.sort((a, b) => a.name.localeCompare(b.name));
};
const handleGenerateCode = (e) => {
e.stopPropagation();
dropdownTippyRef.current.hide();
if (item?.request?.url !== '' || (item?.draft?.request?.url !== undefined && item?.draft?.request?.url !== '')) {
setGenerateCodeItemModalOpen(true);
} else {
toast.error('URL is required');
}
};
const viewFolderSettings = () => {
if (isItemAFolder(item)) {
if (itemIsOpenedInTabs(item, tabs)) {
dispatch(
focusTab({
uid: item.uid
})
);
return;
}
dispatch(
addTab({
uid: item.uid,
collectionUid: collection.uid,
type: 'folder-settings'
})
);
return;
}
};
const handleShowInFolder = () => {
dispatch(showInFolder(item.pathname)).catch((error) => {
console.error('Error opening the folder', error);
@@ -233,62 +237,89 @@ const CollectionItem = ({ item, collection, searchText }) => {
});
};
const requestItems = sortRequestItems(filter(item.items, (i) => isItemARequest(i)));
const folderItems = sortFolderItems(filter(item.items, (i) => isItemAFolder(i)));
const folderItems = sortItemsBySequence(filter(item.items, (i) => isItemAFolder(i)));
const requestItems = sortItemsBySequence(filter(item.items, (i) => isItemARequest(i)));
const handleGenerateCode = (e) => {
e.stopPropagation();
dropdownTippyRef.current.hide();
if (
(item?.request?.url !== '') ||
(item?.draft?.request?.url !== undefined && item?.draft?.request?.url !== '')
) {
setGenerateCodeItemModalOpen(true);
} else {
toast.error('URL is required');
}
};
const viewFolderSettings = () => {
if (isItemAFolder(item)) {
if (itemIsOpenedInTabs(item, tabs)) {
dispatch(focusTab({ uid: item.uid }));
return;
}
dispatch(
addTab({
uid: item.uid,
collectionUid,
type: 'folder-settings'
})
);
}
};
return (
<StyledWrapper className={className}>
{renameItemModalOpen && (
<RenameCollectionItem item={item} collection={collection} onClose={() => setRenameItemModalOpen(false)} />
<RenameCollectionItem item={item} collectionUid={collectionUid} collectionPathname={collectionPathname} onClose={() => setRenameItemModalOpen(false)} />
)}
{cloneItemModalOpen && (
<CloneCollectionItem item={item} collection={collection} onClose={() => setCloneItemModalOpen(false)} />
<CloneCollectionItem item={item} collectionUid={collectionUid} collectionPathname={collectionPathname} onClose={() => setCloneItemModalOpen(false)} />
)}
{deleteItemModalOpen && (
<DeleteCollectionItem item={item} collection={collection} onClose={() => setDeleteItemModalOpen(false)} />
<DeleteCollectionItem item={item} collectionUid={collectionUid} collectionPathname={collectionPathname} onClose={() => setDeleteItemModalOpen(false)} />
)}
{newRequestModalOpen && (
<NewRequest item={item} collection={collection} onClose={() => setNewRequestModalOpen(false)} />
<NewRequest item={item} collectionUid={collectionUid} collectionPathname={collectionPathname} onClose={() => setNewRequestModalOpen(false)} />
)}
{newFolderModalOpen && (
<NewFolder item={item} collection={collection} onClose={() => setNewFolderModalOpen(false)} />
<NewFolder item={item} collectionUid={collectionUid} collectionPathname={collectionPathname} onClose={() => setNewFolderModalOpen(false)} />
)}
{runCollectionModalOpen && (
<RunCollectionItem collection={collection} item={item} onClose={() => setRunCollectionModalOpen(false)} />
<RunCollectionItem collectionUid={collectionUid} item={item} onClose={() => setRunCollectionModalOpen(false)} />
)}
{generateCodeItemModalOpen && (
<GenerateCodeItem collection={collection} item={item} onClose={() => setGenerateCodeItemModalOpen(false)} />
<GenerateCodeItem collectionUid={collectionUid} item={item} onClose={() => setGenerateCodeItemModalOpen(false)} />
)}
{itemInfoModalOpen && (
<CollectionItemInfo item={item} collection={collection} onClose={() => setItemInfoModalOpen(false)} />
<CollectionItemInfo item={item} onClose={() => setItemInfoModalOpen(false)} />
)}
<div className={itemRowClassName} ref={collectionItemRef}>
<div
className={itemRowClassName}
ref={(node) => {
ref.current = node;
drag(drop(node));
}}
>
<div className="flex items-center h-full w-full">
{indents && indents.length
? indents.map((i) => {
return (
<div
onClick={handleClick}
onContextMenu={handleRightClick}
onDoubleClick={handleDoubleClick}
className="indent-block"
key={i}
style={{
width: 16,
minWidth: 16,
height: '100%'
}}
>
&nbsp;{/* Indent */}
</div>
);
})
? indents.map((i) => (
<div
onClick={handleClick}
onContextMenu={handleRightClick}
onDoubleClick={handleDoubleClick}
className="indent-block"
key={i}
style={{ width: 16, minWidth: 16, height: '100%' }}
>
&nbsp;{/* Indent */}
</div>
))
: null}
<div
className="flex flex-grow items-center h-full overflow-hidden"
style={{
paddingLeft: 8
}}
style={{ paddingLeft: 8 }}
onClick={handleClick}
onContextMenu={handleRightClick}
onDoubleClick={handleDoubleClick}
@@ -304,10 +335,7 @@ const CollectionItem = ({ item, collection, searchText }) => {
/>
) : null}
</div>
<div
className="ml-1 flex w-full h-full items-center overflow-hidden"
>
<div className="ml-1 flex w-full h-full items-center overflow-hidden">
<CollectionItemIcon item={item} />
<span className="item-name" title={item.name}>
{item.name}
@@ -429,17 +457,16 @@ const CollectionItem = ({ item, collection, searchText }) => {
</div>
</div>
</div>
{!itemIsCollapsed ? (
<div>
{folderItems && folderItems.length
? folderItems.map((i) => {
return <CollectionItem key={i.uid} item={i} collection={collection} searchText={searchText} />;
return <CollectionItem key={i.uid} item={i} collectionUid={collectionUid} collectionPathname={collectionPathname} searchText={searchText} />;
})
: null}
{requestItems && requestItems.length
? requestItems.map((i) => {
return <CollectionItem key={i.uid} item={i} collection={collection} searchText={searchText} />;
return <CollectionItem key={i.uid} item={i} collectionUid={collectionUid} collectionPathname={collectionPathname} searchText={searchText} />;
})
: null}
</div>
@@ -448,4 +475,4 @@ const CollectionItem = ({ item, collection, searchText }) => {
);
};
export default CollectionItem;
export default React.memo(CollectionItem);

View File

@@ -1,12 +1,14 @@
import React from 'react';
import toast from 'react-hot-toast';
import Modal from 'components/Modal';
import { useDispatch } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import { IconFiles } from '@tabler/icons';
import { removeCollection } from 'providers/ReduxStore/slices/collections/actions';
import { findCollectionByUid } from 'utils/collections/index';
const RemoveCollection = ({ onClose, collection }) => {
const RemoveCollection = ({ onClose, collectionUid }) => {
const dispatch = useDispatch();
const collection = useSelector(state => findCollectionByUid(state.collections.collections, collectionUid));
const onConfirm = () => {
dispatch(removeCollection(collection.uid))

View File

@@ -2,13 +2,15 @@ import React, { useRef, useEffect } from 'react';
import { useFormik } from 'formik';
import * as Yup from 'yup';
import Modal from 'components/Modal';
import { useDispatch } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import toast from 'react-hot-toast';
import { renameCollection } from 'providers/ReduxStore/slices/collections/actions';
import { findCollectionByUid } from 'utils/collections/index';
const RenameCollection = ({ collection, onClose }) => {
const RenameCollection = ({ collectionUid, onClose }) => {
const dispatch = useDispatch();
const inputRef = useRef();
const collection = useSelector(state => findCollectionByUid(state.collections.collections, collectionUid));
const formik = useFormik({
enableReinitialize: true,
initialValues: {

View File

@@ -62,6 +62,36 @@ const Wrapper = styled.div`
color: white;
}
}
&.drop-target {
background-color: ${(props) => props.theme.dragAndDrop.hoverBg};
transition: ${(props) => props.theme.dragAndDrop.transition};
}
&.drop-target-above {
border: none;
border-top: ${(props) => props.theme.dragAndDrop.borderStyle} ${(props) => props.theme.dragAndDrop.border};
margin-top: -2px;
background: transparent;
transition: ${(props) => props.theme.dragAndDrop.transition};
}
&.drop-target-below {
border: none;
border-bottom: ${(props) => props.theme.dragAndDrop.borderStyle} ${(props) => props.theme.dragAndDrop.border};
margin-bottom: -2px;
background: transparent;
transition: ${(props) => props.theme.dragAndDrop.transition};
}
}
.collection-name.drop-target {
border: ${(props) => props.theme.dragAndDrop.borderStyle} ${(props) => props.theme.dragAndDrop.border};
border-radius: 4px;
background-color: ${(props) => props.theme.dragAndDrop.hoverBg};
margin: -2px;
transition: ${(props) => props.theme.dragAndDrop.transition};
box-shadow: 0 0 0 2px ${(props) => props.theme.dragAndDrop.hoverBg};
}
#sidebar-collection-name {

View File

@@ -6,7 +6,7 @@ import { useDrop, useDrag } from 'react-dnd';
import { IconChevronRight, IconDots, IconLoader2 } from '@tabler/icons';
import Dropdown from 'components/Dropdown';
import { collapseCollection } from 'providers/ReduxStore/slices/collections';
import { mountCollection, moveItemToRootOfCollection, moveCollectionAndPersist } from 'providers/ReduxStore/slices/collections/actions';
import { mountCollection, moveCollectionAndPersist, handleCollectionItemDrop } from 'providers/ReduxStore/slices/collections/actions';
import { useDispatch, useSelector } from 'react-redux';
import { addTab, makeTabPermanent } from 'providers/ReduxStore/slices/tabs';
import NewRequest from 'components/Sidebar/NewRequest';
@@ -33,7 +33,7 @@ const Collection = ({ collection, searchText }) => {
const dispatch = useDispatch();
const isLoading = areItemsLoading(collection);
const collectionRef = useRef(null);
const menuDropdownTippyRef = useRef();
const onMenuDropdownCreate = (ref) => (menuDropdownTippyRef.current = ref);
const MenuIcon = forwardRef((props, ref) => {
@@ -144,7 +144,7 @@ const Collection = ({ collection, searchText }) => {
drop: (draggedItem, monitor) => {
const itemType = monitor.getItemType();
if (isCollectionItem(itemType)) {
dispatch(moveItemToRootOfCollection(collection.uid, draggedItem.uid))
dispatch(handleCollectionItemDrop({ targetItem: collection, draggedItem, dropType: 'inside', collectionUid: collection.uid }))
} else {
dispatch(moveCollectionAndPersist({draggedItem, targetItem: collection}));
}
@@ -170,33 +170,28 @@ const Collection = ({ collection, searchText }) => {
});
// we need to sort request items by seq property
const sortRequestItems = (items = []) => {
const sortItemsBySequence = (items = []) => {
return items.sort((a, b) => a.seq - b.seq);
};
// we need to sort folder items by name alphabetically
const sortFolderItems = (items = []) => {
return items.sort((a, b) => a.name.localeCompare(b.name));
};
const requestItems = sortRequestItems(filter(collection.items, (i) => isItemARequest(i)));
const folderItems = sortFolderItems(filter(collection.items, (i) => isItemAFolder(i)));
const requestItems = sortItemsBySequence(filter(collection.items, (i) => isItemARequest(i)));
const folderItems = sortItemsBySequence(filter(collection.items, (i) => isItemAFolder(i)));
return (
<StyledWrapper className="flex flex-col">
{showNewRequestModal && <NewRequest collection={collection} onClose={() => setShowNewRequestModal(false)} />}
{showNewFolderModal && <NewFolder collection={collection} onClose={() => setShowNewFolderModal(false)} />}
{showNewRequestModal && <NewRequest collectionUid={collection.uid} collectionPathname={collection.pathname} onClose={() => setShowNewRequestModal(false)} />}
{showNewFolderModal && <NewFolder collectionUid={collection.uid} collectionPathname={collection.pathname} onClose={() => setShowNewFolderModal(false)} />}
{showRenameCollectionModal && (
<RenameCollection collection={collection} onClose={() => setShowRenameCollectionModal(false)} />
<RenameCollection collectionUid={collection.uid} collectionPathname={collection.pathname} onClose={() => setShowRenameCollectionModal(false)} />
)}
{showRemoveCollectionModal && (
<RemoveCollection collection={collection} onClose={() => setShowRemoveCollectionModal(false)} />
<RemoveCollection collectionUid={collection.uid} collectionPathname={collection.pathname} onClose={() => setShowRemoveCollectionModal(false)} />
)}
{showShareCollectionModal && (
<ShareCollection collection={collection} onClose={() => setShowShareCollectionModal(false)} />
<ShareCollection collectionUid={collection.uid} collectionPathname={collection.pathname} onClose={() => setShowShareCollectionModal(false)} />
)}
{showCloneCollectionModalOpen && (
<CloneCollection collection={collection} onClose={() => setShowCloneCollectionModalOpen(false)} />
<CloneCollection collectionUid={collection.uid} collectionPathname={collection.pathname} onClose={() => setShowCloneCollectionModalOpen(false)} />
)}
<div className={collectionRowClassName}
ref={collectionRef}
@@ -300,16 +295,12 @@ const Collection = ({ collection, searchText }) => {
<div>
{!collectionIsCollapsed ? (
<div>
{folderItems && folderItems.length
? folderItems.map((i) => {
return <CollectionItem key={i.uid} item={i} collection={collection} searchText={searchText} />;
})
: null}
{requestItems && requestItems.length
? requestItems.map((i) => {
return <CollectionItem key={i.uid} item={i} collection={collection} searchText={searchText} />;
})
: null}
{folderItems?.map?.((i) => {
return <CollectionItem key={i.uid} item={i} collectionUid={collection.uid} collectionPathname={collection.pathname} searchText={searchText} />;
})}
{requestItems?.map?.((i) => {
return <CollectionItem key={i.uid} item={i} collectionUid={collection.uid} collectionPathname={collection.pathname} searchText={searchText} />;
})}
</div>
) : null}
</div>

View File

@@ -14,7 +14,7 @@ import Dropdown from "components/Dropdown";
import { IconCaretDown } from "@tabler/icons";
import StyledWrapper from './StyledWrapper';
const NewFolder = ({ collection, item, onClose }) => {
const NewFolder = ({ collectionUid, item, onClose }) => {
const dispatch = useDispatch();
const inputRef = useRef();
const [isEditing, toggleEditing] = useState(false);
@@ -52,7 +52,7 @@ const NewFolder = ({ collection, item, onClose }) => {
})
}),
onSubmit: (values) => {
dispatch(newFolder(values.folderName, values.directoryName, collection.uid, item ? item.uid : null))
dispatch(newFolder(values.folderName, values.directoryName, collectionUid, item ? item.uid : null))
.then(() => {
toast.success('New folder created!');
onClose();

View File

@@ -5,7 +5,7 @@ import toast from 'react-hot-toast';
import path from 'utils/common/path';
import { uuid } from 'utils/common';
import Modal from 'components/Modal';
import { useDispatch } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import { newEphemeralHttpRequest } from 'providers/ReduxStore/slices/collections';
import { newHttpRequest } from 'providers/ReduxStore/slices/collections/actions';
import { addTab } from 'providers/ReduxStore/slices/tabs';
@@ -20,9 +20,11 @@ import Portal from 'components/Portal';
import Help from 'components/Help';
import StyledWrapper from './StyledWrapper';
const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
const NewRequest = ({ collectionUid, item, isEphemeral, onClose }) => {
const dispatch = useDispatch();
const inputRef = useRef();
const collection = useSelector(state => state.collections.collections?.find(c => c.uid === collectionUid));
const {
brunoConfig: { presets: collectionPresets = {} }
} = collection;
@@ -135,14 +137,14 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
requestType: values.requestType,
requestUrl: values.requestUrl,
requestMethod: values.requestMethod,
collectionUid: collection.uid
collectionUid: collectionUid
})
)
.then(() => {
dispatch(
addTab({
uid: uid,
collectionUid: collection.uid,
collectionUid: collectionUid,
requestPaneTab: getDefaultRequestPaneTab({ type: values.requestType })
})
);
@@ -158,7 +160,7 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
requestType: curlRequestTypeDetected,
requestUrl: request.url,
requestMethod: request.method,
collectionUid: collection.uid,
collectionUid: collectionUid,
itemUid: item ? item.uid : null,
headers: request.headers,
body: request.body,
@@ -178,7 +180,7 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
requestType: values.requestType,
requestUrl: values.requestUrl,
requestMethod: values.requestMethod,
collectionUid: collection.uid,
collectionUid: collectionUid,
itemUid: item ? item.uid : null
})
)
@@ -389,8 +391,6 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
) : (
<div className='relative flex flex-row gap-1 items-center justify-between'>
<PathDisplay
collection={collection}
dirName={path.relative(collection?.pathname, item?.pathname || collection?.pathname)}
baseName={formik.values.filename? `${formik.values.filename}.bru` : ''}
/>
</div>