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:
Pooja
2025-12-02 15:29:32 +05:30
committed by GitHub
parent bc4062b950
commit 06a024a1d9
12 changed files with 383 additions and 68 deletions

View File

@@ -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;

View File

@@ -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} />

View File

@@ -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 {

View File

@@ -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} />