mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-09 06:55:03 +00:00
feat: add copy paste feature for folder (#6097)
* feat: add copy paste feature for folder * add: playwright test * fix * fix * add: keyboardFocusBg in light mode * fix: copy paste in yaml collection * improvement
This commit is contained in:
@@ -131,6 +131,15 @@ const Wrapper = styled.div`
|
||||
}
|
||||
}
|
||||
|
||||
&.item-keyboard-focused {
|
||||
background: ${(props) => props.theme.sidebar.collection.item.keyboardFocusBg};
|
||||
outline: none;
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.sidebar.collection.item.keyboardFocusBg} !important;
|
||||
}
|
||||
}
|
||||
|
||||
div.tippy-box {
|
||||
position: relative;
|
||||
top: -0.625rem;
|
||||
|
||||
@@ -77,6 +77,7 @@ const CollectionItem = ({ item, collectionUid, collectionPathname, searchText })
|
||||
const [runCollectionModalOpen, setRunCollectionModalOpen] = useState(false);
|
||||
const [itemInfoModalOpen, setItemInfoModalOpen] = useState(false);
|
||||
const [examplesExpanded, setExamplesExpanded] = useState(false);
|
||||
const [isKeyboardFocused, setIsKeyboardFocused] = useState(false);
|
||||
const hasSearchText = searchText && searchText?.trim()?.length;
|
||||
const itemIsCollapsed = hasSearchText ? false : item.collapsed;
|
||||
const isFolder = isItemAFolder(item);
|
||||
@@ -186,10 +187,11 @@ const CollectionItem = ({ item, collectionUid, collectionPathname, searchText })
|
||||
});
|
||||
|
||||
const itemRowClassName = classnames('flex collection-item-name relative items-center', {
|
||||
'item-focused-in-tab': isTabForItemActive,
|
||||
'item-focused-in-tab': isTabForItemActive && !isKeyboardFocused,
|
||||
'item-hovered': isOver && canDrop,
|
||||
'drop-target': isOver && dropType === 'inside',
|
||||
'drop-target-above': isOver && dropType === 'adjacent'
|
||||
'drop-target-above': isOver && dropType === 'adjacent',
|
||||
'item-keyboard-focused': isKeyboardFocused
|
||||
});
|
||||
|
||||
const handleRun = async () => {
|
||||
@@ -393,23 +395,56 @@ const CollectionItem = ({ item, collectionUid, collectionPathname, searchText })
|
||||
}
|
||||
};
|
||||
|
||||
const handleCopyRequest = () => {
|
||||
const handleCopyItem = () => {
|
||||
dropdownTippyRef.current.hide();
|
||||
dispatch(copyRequest(item));
|
||||
toast.success('Request copied to clipboard');
|
||||
const itemType = isFolder ? 'Folder' : 'Request';
|
||||
toast.success(`${itemType} copied to clipboard`);
|
||||
};
|
||||
|
||||
const handlePasteRequest = () => {
|
||||
const handlePasteItem = () => {
|
||||
dropdownTippyRef.current.hide();
|
||||
|
||||
// Only allow paste into folders
|
||||
if (!isFolder) {
|
||||
toast.error('Paste is only available for folders');
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(pasteItem(collectionUid, item.uid))
|
||||
.then(() => {
|
||||
toast.success('Request pasted successfully');
|
||||
toast.success('Item pasted successfully');
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error(err ? err.message : 'An error occurred while pasting the request');
|
||||
toast.error(err ? err.message : 'An error occurred while pasting the item');
|
||||
});
|
||||
};
|
||||
|
||||
// Keyboard shortcuts handler
|
||||
const handleKeyDown = (e) => {
|
||||
// Detect Mac by checking both metaKey and platform
|
||||
const isMac = navigator.userAgent?.includes('Mac') || navigator.platform?.startsWith('Mac');
|
||||
const isModifierPressed = isMac ? e.metaKey : e.ctrlKey;
|
||||
|
||||
if (isModifierPressed && e.key.toLowerCase() === 'c') {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleCopyItem();
|
||||
} else if (isModifierPressed && e.key.toLowerCase() === 'v') {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handlePasteItem();
|
||||
}
|
||||
};
|
||||
|
||||
const handleFocus = () => {
|
||||
setIsKeyboardFocused(true);
|
||||
};
|
||||
|
||||
const handleBlur = () => {
|
||||
setIsKeyboardFocused(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className={className}>
|
||||
{renameItemModalOpen && (
|
||||
@@ -449,6 +484,10 @@ const CollectionItem = ({ item, collectionUid, collectionPathname, searchText })
|
||||
ref.current = node;
|
||||
drag(drop(node));
|
||||
}}
|
||||
tabIndex={0}
|
||||
onKeyDown={handleKeyDown}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
>
|
||||
<div className="flex items-center h-full w-full">
|
||||
{indents && indents.length
|
||||
@@ -568,21 +607,19 @@ const CollectionItem = ({ item, collectionUid, collectionPathname, searchText })
|
||||
</span>
|
||||
Clone
|
||||
</div>
|
||||
{!isFolder && (
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={handleCopyRequest}
|
||||
>
|
||||
<span className="dropdown-icon">
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={handleCopyItem}
|
||||
>
|
||||
<span className="dropdown-icon">
|
||||
<IconCopy size={16} strokeWidth={2} />
|
||||
</span>
|
||||
Copy
|
||||
</div>
|
||||
)}
|
||||
</span>
|
||||
Copy
|
||||
</div>
|
||||
{isFolder && hasCopiedItems && (
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={handlePasteRequest}
|
||||
onClick={handlePasteItem}
|
||||
>
|
||||
<span className="dropdown-icon">
|
||||
<IconClipboard size={16} strokeWidth={2} />
|
||||
|
||||
@@ -94,6 +94,15 @@ const Wrapper = styled.div`
|
||||
background: ${(props) => props.theme.sidebar.collection.item.bg} !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.collection-keyboard-focused {
|
||||
background: ${(props) => props.theme.sidebar.collection.item.keyboardFocusBg};
|
||||
outline: none;
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.sidebar.collection.item.keyboardFocusBg} !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#sidebar-collection-name {
|
||||
|
||||
@@ -51,6 +51,7 @@ const Collection = ({ collection, searchText }) => {
|
||||
const [showShareCollectionModal, setShowShareCollectionModal] = useState(false);
|
||||
const [showRemoveCollectionModal, setShowRemoveCollectionModal] = useState(false);
|
||||
const [dropType, setDropType] = useState(null);
|
||||
const [isKeyboardFocused, setIsKeyboardFocused] = useState(false);
|
||||
const dispatch = useDispatch();
|
||||
const isLoading = areItemsLoading(collection);
|
||||
const collectionRef = useRef(null);
|
||||
@@ -161,17 +162,38 @@ const Collection = ({ collection, searchText }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const handlePasteRequest = () => {
|
||||
const handlePasteItem = () => {
|
||||
menuDropdownTippyRef.current.hide();
|
||||
dispatch(pasteItem(collection.uid, null))
|
||||
.then(() => {
|
||||
toast.success('Request pasted successfully');
|
||||
toast.success('Item pasted successfully');
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error(err ? err.message : 'An error occurred while pasting the request');
|
||||
toast.error(err ? err.message : 'An error occurred while pasting the item');
|
||||
});
|
||||
};
|
||||
|
||||
// Keyboard shortcuts handler for collection
|
||||
const handleKeyDown = (e) => {
|
||||
// Detect Mac by checking both metaKey and platform
|
||||
const isMac = navigator.userAgent?.includes('Mac') || navigator.platform?.startsWith('Mac');
|
||||
const isModifierPressed = isMac ? e.metaKey : e.ctrlKey;
|
||||
|
||||
if (isModifierPressed && e.key.toLowerCase() === 'v') {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handlePasteItem();
|
||||
}
|
||||
};
|
||||
|
||||
const handleFocus = () => {
|
||||
setIsKeyboardFocused(true);
|
||||
};
|
||||
|
||||
const handleBlur = () => {
|
||||
setIsKeyboardFocused(false);
|
||||
};
|
||||
|
||||
const isCollectionItem = (itemType) => {
|
||||
return itemType === 'collection-item';
|
||||
};
|
||||
@@ -227,9 +249,10 @@ const Collection = ({ collection, searchText }) => {
|
||||
}
|
||||
|
||||
const collectionRowClassName = classnames('flex py-1 collection-name items-center', {
|
||||
'item-hovered': isOver && dropType === 'adjacent', // For collection-to-collection moves (show line)
|
||||
'drop-target': isOver && dropType === 'inside', // For collection-item drops (highlight full area)
|
||||
'collection-focused-in-tab': isCollectionFocused
|
||||
'item-hovered': isOver && dropType === 'adjacent', // For collection-to-collection moves (show line)
|
||||
'drop-target': isOver && dropType === 'inside', // For collection-item drops (highlight full area)
|
||||
'collection-focused-in-tab': isCollectionFocused && !isKeyboardFocused,
|
||||
'collection-keyboard-focused': isKeyboardFocused
|
||||
});
|
||||
|
||||
// we need to sort request items by seq property
|
||||
@@ -262,6 +285,10 @@ const Collection = ({ collection, searchText }) => {
|
||||
collectionRef.current = node;
|
||||
drag(drop(node));
|
||||
}}
|
||||
tabIndex={0}
|
||||
onKeyDown={handleKeyDown}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
>
|
||||
<div
|
||||
className="flex flex-grow items-center overflow-hidden"
|
||||
@@ -337,7 +364,7 @@ const Collection = ({ collection, searchText }) => {
|
||||
{hasCopiedItems && (
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={handlePasteRequest}
|
||||
onClick={handlePasteItem}
|
||||
>
|
||||
<span className="dropdown-icon">
|
||||
<IconClipboard size={16} strokeWidth={2} />
|
||||
|
||||
Reference in New Issue
Block a user