diff --git a/e2e-tests/003-collection-management-tests/001-cross-collection-drag-drop-request.spec.ts b/e2e-tests/003-collection-management-tests/001-cross-collection-drag-drop-request.spec.ts new file mode 100644 index 000000000..8c1efaf59 --- /dev/null +++ b/e2e-tests/003-collection-management-tests/001-cross-collection-drag-drop-request.spec.ts @@ -0,0 +1,154 @@ +import { test, expect } from '../../playwright'; + +test.describe('Cross-Collection Drag and Drop', () => { + test('Verify request drag and drop', async ({ pageWithUserData: page, createTmpDir }) => { + // Create first collection - click dropdown menu first + await page.locator('.dropdown-icon').click(); + await page.locator('.dropdown-item').filter({ hasText: 'Create Collection' }).click(); + await page.getByLabel('Name').fill('source-collection'); + await page.getByLabel('Location').fill(await createTmpDir('source-collection')); + await page.getByRole('button', { name: 'Create', exact: true }).click(); + + await expect(page.locator('#sidebar-collection-name').filter({ hasText: 'source-collection' })).toBeVisible(); + await page.locator('#sidebar-collection-name').filter({ hasText: 'source-collection' }).click(); + await page.getByLabel('Safe Mode').check(); + await page.getByRole('button', { name: 'Save' }).click(); + + // Create a request in the first collection + await page.locator('#create-new-tab').getByRole('img').click(); + await page.getByPlaceholder('Request Name').fill('test-request'); + await page.locator('#new-request-url .CodeMirror').click(); + await page.locator('textarea').fill('https://httpbin.org/get'); + await page.getByRole('button', { name: 'Create' }).click(); + + await expect(page.locator('.collection-item-name').filter({ hasText: 'test-request' })).toBeVisible(); + + // Create second collection - click dropdown menu first + await page.locator('.dropdown-icon').click(); + await page.locator('.dropdown-item').filter({ hasText: 'Create Collection' }).click(); + await page.getByLabel('Name').fill('target-collection'); + await page.getByLabel('Location').fill(await createTmpDir('target-collection')); + await page.getByRole('button', { name: 'Create', exact: true }).click(); + + await expect(page.locator('#sidebar-collection-name').filter({ hasText: 'target-collection' })).toBeVisible(); + await page.locator('#sidebar-collection-name').filter({ hasText: 'target-collection' }).click(); + await page.getByLabel('Safe Mode').check(); + await page.getByRole('button', { name: 'Save' }).click(); + + await expect(page.locator('#sidebar-collection-name').filter({ hasText: 'source-collection' })).toBeVisible(); + await expect(page.locator('#sidebar-collection-name').filter({ hasText: 'target-collection' })).toBeVisible(); + + // Locate the request in source collection + const sourceRequest = page.locator('.collection-item-name').filter({ hasText: 'test-request' }); + await expect(sourceRequest).toBeVisible(); + + // Locate the target collection area (the collection name element) + const targetCollection = page.locator('.collection-name').filter({ hasText: 'target-collection' }); + await expect(targetCollection).toBeVisible(); + + // Perform drag and drop operation + await sourceRequest.dragTo(targetCollection); + + // Verify the request has been moved to the target collection + // Click on target collection to expand it if needed + await page.locator('#sidebar-collection-name').filter({ hasText: 'target-collection' }).click(); + + // Check that the request now appears under target collection + const targetCollectionContainer = page + .locator('.collection-name') + .filter({ hasText: 'target-collection' }) + .locator('..'); + await expect( + targetCollectionContainer.locator('.collection-item-name').filter({ hasText: 'test-request' }) + ).toBeVisible(); + + // Verify the request is no longer in the source collection + const sourceCollectionContainer = page + .locator('.collection-name') + .filter({ hasText: 'source-collection' }) + .locator('..'); + await expect( + sourceCollectionContainer.locator('.collection-item-name').filter({ hasText: 'test-request' }) + ).not.toBeVisible(); + }); + + test('Expected to show error toast message, when duplicate request found in drop location', async ({ + pageWithUserData: page, + createTmpDir + }) => { + // Create first collection (source-collection) + await page.locator('.dropdown-icon').click(); + await page.locator('.dropdown-item').filter({ hasText: 'Create Collection' }).click(); + await page.getByLabel('Name').fill('source-collection'); + await page.getByLabel('Location').fill(await createTmpDir('source-collection')); + await page.getByRole('button', { name: 'Create', exact: true }).click(); + + // Open collection + await expect(page.locator('#sidebar-collection-name').filter({ hasText: 'source-collection' })).toBeVisible(); + await page.locator('#sidebar-collection-name').filter({ hasText: 'source-collection' }).click(); + await page.getByLabel('Safe Mode').check(); + await page.getByRole('button', { name: 'Save' }).click(); + + // Create a request in the first collection (request-1) + await page.locator('#create-new-tab').getByRole('img').click(); + await page.getByPlaceholder('Request Name').fill('request-1'); + await page.locator('#new-request-url .CodeMirror').click(); + await page.locator('textarea').fill('https://httpbin.org/get'); + await page.getByRole('button', { name: 'Create' }).click(); + + // check if request-1 is created and visible in sidebar + await expect(page.locator('.collection-item-name').filter({ hasText: 'request-1' })).toBeVisible(); + + // Create second collection (target-collection) + await page.locator('.dropdown-icon').click(); + await page.locator('.dropdown-item').filter({ hasText: 'Create Collection' }).click(); + await page.getByLabel('Name').fill('target-collection'); + await page.getByLabel('Location').fill(await createTmpDir('target-collection')); + await page.getByRole('button', { name: 'Create', exact: true }).click(); + + // Open collection + await expect(page.locator('#sidebar-collection-name').filter({ hasText: 'target-collection' })).toBeVisible(); + await page.locator('#sidebar-collection-name').filter({ hasText: 'target-collection' }).click(); + await page.getByLabel('Safe Mode').check(); + await page.getByRole('button', { name: 'Save' }).click(); + + // Create a request in the target collection with the same name (request-1) + await page.locator('#create-new-tab').getByRole('img').click(); + await page.getByPlaceholder('Request Name').fill('request-1'); + await page.locator('#new-request-url .CodeMirror').click(); + await page.locator('textarea').fill('https://httpbin.org/post'); + await page.getByRole('button', { name: 'Create' }).click(); + + // Go back to source collection to drag the request + await page.locator('#sidebar-collection-name').filter({ hasText: 'source-collection' }).click(); + const sourceRequest = page.locator('.collection-item-name').filter({ hasText: 'request-1' }).first(); + await expect(sourceRequest).toBeVisible(); + + // Locate the target collection area + const targetCollection = page.locator('.collection-name').filter({ hasText: 'target-collection' }); + await expect(targetCollection).toBeVisible(); + + // Perform drag and drop operation to target-collection + await sourceRequest.dragTo(targetCollection); + + // check for error toast notification + await expect(page.getByText(/Error: Cannot copy.*already exists/i)).toBeVisible(); + + // source and target collection request should remain unchanged + const targetCollectionContainer = page + .locator('.collection-name') + .filter({ hasText: 'target-collection' }) + .locator('..'); + await expect( + targetCollectionContainer.locator('.collection-item-name').filter({ hasText: 'request-1' }) + ).toBeVisible(); + + const sourceCollectionContainer = page + .locator('.collection-name') + .filter({ hasText: 'source-collection' }) + .locator('..'); + await expect( + sourceCollectionContainer.locator('.collection-item-name').filter({ hasText: 'request-1' }) + ).toBeVisible(); + }); +}); diff --git a/e2e-tests/003-collection-management-tests/002-cross-collection-drag-drop-folder.spec.ts b/e2e-tests/003-collection-management-tests/002-cross-collection-drag-drop-folder.spec.ts new file mode 100644 index 000000000..d02bf91dc --- /dev/null +++ b/e2e-tests/003-collection-management-tests/002-cross-collection-drag-drop-folder.spec.ts @@ -0,0 +1,235 @@ +import { test, expect } from '../../playwright'; + +test.describe('Cross-Collection Drag and Drop for folder', () => { + test('Verify cross-collection folder drag and drop', async ({ pageWithUserData: page, createTmpDir }) => { + // Create first collection - click dropdown menu first + await page.locator('.dropdown-icon').click(); + await page.locator('.dropdown-item').filter({ hasText: 'Create Collection' }).click(); + await page.getByLabel('Name').fill('source-collection'); + await page.getByLabel('Location').fill(await createTmpDir('source-collection')); + await page.getByRole('button', { name: 'Create', exact: true }).click(); + + // Wait for collection to appear and click on it + await expect(page.locator('#sidebar-collection-name').filter({ hasText: 'source-collection' })).toBeVisible(); + await page.locator('#sidebar-collection-name').filter({ hasText: 'source-collection' }).click(); + await page.getByLabel('Safe Mode').check(); + await page.getByRole('button', { name: 'Save' }).click(); + + // Create a folder in the first collection + // Look for the collection menu button (usually three dots or similar) + await page.locator('.collection-actions').hover(); + await page.locator('.collection-actions .icon').click(); + await page.locator('.dropdown-item').filter({ hasText: 'New Folder' }).click(); + + // Fill folder name in the modal + await expect(page.locator('#collection-name')).toBeVisible(); + await page.locator('#collection-name').fill('test-folder'); + await page.getByRole('button', { name: 'Create' }).click(); + + // Wait for the folder to be created and appear in the sidebar + await page.waitForTimeout(2000); + await expect(page.locator('.collection-item-name').filter({ hasText: 'test-folder' })).toBeVisible(); + + // Add a request to the folder to make it more realistic + await page.locator('.collection-item-name').filter({ hasText: 'test-folder' }).click({ button: 'right' }); + await page.locator('.dropdown-item').filter({ hasText: 'New Request' }).click(); + await page.getByPlaceholder('Request Name').fill('test-request-in-folder'); + await page.locator('#new-request-url .CodeMirror').click(); + await page.locator('textarea').fill('https://httpbin.org/get'); + await page.getByRole('button', { name: 'Create' }).click(); + + // Wait for the request to be created + await page.waitForTimeout(1000); + + // Expand the folder to see the request inside + await page.locator('.collection-item-name').filter({ hasText: 'test-folder' }).click(); + await page.waitForTimeout(500); + await expect(page.locator('.collection-item-name').filter({ hasText: 'test-request-in-folder' })).toBeVisible(); + + // Create second collection - click dropdown menu first + await page.locator('.dropdown-icon').click(); + await page.locator('.dropdown-item').filter({ hasText: 'Create Collection' }).click(); + await page.getByLabel('Name').fill('target-collection'); + await page.getByLabel('Location').fill(await createTmpDir('target-collection')); + await page.getByRole('button', { name: 'Create', exact: true }).click(); + + // Wait for second collection to appear and click on it + await expect(page.locator('#sidebar-collection-name').filter({ hasText: 'target-collection' })).toBeVisible(); + await page.locator('#sidebar-collection-name').filter({ hasText: 'target-collection' }).click(); + await page.getByLabel('Safe Mode').check(); + await page.getByRole('button', { name: 'Save' }).click(); + + // Wait for both collections to be visible in sidebar + await expect(page.locator('#sidebar-collection-name').filter({ hasText: 'source-collection' })).toBeVisible(); + await expect(page.locator('#sidebar-collection-name').filter({ hasText: 'target-collection' })).toBeVisible(); + + // Locate the folder in source collection + const sourceFolder = page.locator('.collection-item-name').filter({ hasText: 'test-folder' }); + await expect(sourceFolder).toBeVisible(); + + // Locate the target collection area (the collection name element) + const targetCollection = page.locator('.collection-name').filter({ hasText: 'target-collection' }); + await expect(targetCollection).toBeVisible(); + + // Perform drag and drop operation + await sourceFolder.dragTo(targetCollection); + + // Wait for the operation to complete + await page.waitForTimeout(3000); + + // Verify the folder has been moved to the target collection + // Click on target collection to expand it if needed + await page.locator('#sidebar-collection-name').filter({ hasText: 'target-collection' }).click(); + await page.waitForTimeout(1000); + + // Check that the folder now appears under target collection + const targetCollectionContainer = page + .locator('.collection-name') + .filter({ hasText: 'target-collection' }) + .locator('..'); + await expect( + targetCollectionContainer.locator('.collection-item-name').filter({ hasText: 'test-folder' }) + ).toBeVisible(); + + // Expand the moved folder to verify the request inside is also moved + await targetCollectionContainer.locator('.collection-item-name').filter({ hasText: 'test-folder' }).click(); + await page.waitForTimeout(500); + await expect( + targetCollectionContainer.locator('.collection-item-name').filter({ hasText: 'test-request-in-folder' }) + ).toBeVisible(); + + // Verify the folder is no longer in the source collection + const sourceCollectionContainer = page + .locator('.collection-name') + .filter({ hasText: 'source-collection' }) + .locator('..'); + await expect( + sourceCollectionContainer.locator('.collection-item-name').filter({ hasText: 'test-folder' }) + ).not.toBeVisible(); + + // Verify the request is also no longer in the source collection + await expect( + sourceCollectionContainer.locator('.collection-item-name').filter({ hasText: 'test-request-in-folder' }) + ).not.toBeVisible(); + }); + + test('Verify cross-collection folder drag and drop, a duplicate folder exist. expected to throw error toast', async ({ + pageWithUserData: page, + createTmpDir + }) => { + // Create first collection (source) - use unique names for this test + await page.locator('.dropdown-icon').click(); + await page.locator('.dropdown-item').filter({ hasText: 'Create Collection' }).click(); + await page.getByLabel('Name').fill('source-collection'); + await page.getByLabel('Location').fill(await createTmpDir('source-collection')); + await page.getByRole('button', { name: 'Create', exact: true }).click(); + + // Wait for collection to appear and click on it + await expect(page.locator('#sidebar-collection-name').filter({ hasText: 'source-collection' })).toBeVisible(); + await page.locator('#sidebar-collection-name').filter({ hasText: 'source-collection' }).click(); + await page.getByLabel('Safe Mode').check(); + await page.getByRole('button', { name: 'Save' }).click(); + + // Create a folder in the first collection + await page + .locator('.collection-name') + .filter({ hasText: 'source-collection' }) + .locator('..') + .locator('.collection-actions') + .hover(); + await page + .locator('.collection-name') + .filter({ hasText: 'source-collection' }) + .locator('..') + .locator('.collection-actions .icon') + .click(); + await page.locator('.dropdown-item').filter({ hasText: 'New Folder' }).click(); + await expect(page.locator('#collection-name')).toBeVisible(); + await page.locator('#collection-name').fill('folder-1'); + await page.getByRole('button', { name: 'Create' }).click(); + + await expect(page.locator('.collection-item-name').filter({ hasText: 'folder-1' })).toBeVisible(); + + // Add a request to the folder to make it more realistic + await page.locator('.collection-item-name').filter({ hasText: 'folder-1' }).click({ button: 'right' }); + await page.locator('.dropdown-item').filter({ hasText: 'New Request' }).click(); + await page.getByPlaceholder('Request Name').fill('http-request'); + await page.locator('#new-request-url .CodeMirror').click(); + await page.locator('textarea').fill('https://httpbin.org/get'); + await page.getByRole('button', { name: 'Create' }).click(); + // Expand the folder to see the request inside + await page.locator('.collection-item-name').filter({ hasText: 'folder-1' }).click(); + await expect(page.locator('.collection-item-name').filter({ hasText: 'http-request' })).toBeVisible(); + + // Create second collection (target) + await page.locator('.dropdown-icon').click(); + await page.locator('.dropdown-item').filter({ hasText: 'Create Collection' }).click(); + await page.getByLabel('Name').fill('target-collection'); + await page.getByLabel('Location').fill(await createTmpDir('target-collection')); + await page.getByRole('button', { name: 'Create', exact: true }).click(); + + // Wait for second collection to appear and click on it + await expect(page.locator('#sidebar-collection-name').filter({ hasText: 'target-collection' })).toBeVisible(); + await page.locator('#sidebar-collection-name').filter({ hasText: 'target-collection' }).click(); + await page.getByLabel('Safe Mode').check(); + await page.getByRole('button', { name: 'Save' }).click(); + + // Create a folder with the same name in the target collection + await page + .locator('.collection-name') + .filter({ hasText: 'target-collection' }) + .locator('..') + .locator('.collection-actions') + .hover(); + await page + .locator('.collection-name') + .filter({ hasText: 'target-collection' }) + .locator('..') + .locator('.collection-actions .icon') + .click(); + await page.locator('.dropdown-item').filter({ hasText: 'New Folder' }).click(); + await expect(page.locator('#collection-name')).toBeVisible(); + await page.locator('#collection-name').fill('folder-1'); + await page.getByRole('button', { name: 'Create' }).click(); + + // Go back to source collection to drag the folder + await page.locator('#sidebar-collection-name').filter({ hasText: 'source-collection' }).click(); + + // Verify we have the folder to drag in the source collection + const sourceFolder = page.locator('.collection-item-name').filter({ hasText: 'folder-1' }).first(); + await expect(sourceFolder).toBeVisible(); + + // Locate the target collection area + const targetCollection = page.locator('.collection-name').filter({ hasText: 'target-collection' }); + await expect(targetCollection).toBeVisible(); + + // Perform drag and drop operation + await sourceFolder.dragTo(targetCollection); + + // check for error toast notification + await expect(page.getByText(/Error: Cannot copy.*already exists/i)).toBeVisible(); + + // source and target collection request should remain unchanged + const sourceCollectionContainer = page + .locator('.collection-name') + .filter({ hasText: 'source-collection' }) + .locator('..'); + await expect( + sourceCollectionContainer.locator('.collection-item-name').filter({ hasText: 'folder-1' }) + ).toBeVisible(); + await expect( + sourceCollectionContainer.locator('.collection-item-name').filter({ hasText: 'http-request' }) + ).toBeVisible(); + + const targetCollectionContainer = page + .locator('.collection-name') + .filter({ hasText: 'target-collection' }) + .locator('..'); + await expect( + targetCollectionContainer.locator('.collection-item-name').filter({ hasText: 'folder-1' }) + ).toBeVisible(); + await expect( + targetCollectionContainer.locator('.collection-item-name').filter({ hasText: 'http-request' }) + ).not.toBeVisible(); + }); +}); 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 e7a3a21ea..1b09cb7e3 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 @@ -60,8 +60,8 @@ const CollectionItem = ({ item, collectionUid, collectionPathname, searchText }) const [dropType, setDropType] = useState(null); // 'adjacent' or 'inside' const [{ isDragging }, drag, dragPreview] = useDrag({ - type: `collection-item-${collectionUid}`, - item, + type: 'collection-item', + item: { ...item, sourceCollectionUid: collectionUid }, collect: (monitor) => ({ isDragging: monitor.isDragging() }), @@ -92,10 +92,15 @@ const CollectionItem = ({ item, collectionUid, collectionPathname, searchText }) const canItemBeDropped = ({ draggedItem, targetItem, dropType }) => { const { uid: targetItemUid, pathname: targetItemPathname } = targetItem; - const { uid: draggedItemUid, pathname: draggedItemPathname } = draggedItem; + const { uid: draggedItemUid, pathname: draggedItemPathname, sourceCollectionUid } = draggedItem; if (draggedItemUid === targetItemUid) return false; + // For cross-collection moves, we allow the drop + if (sourceCollectionUid !== collectionUid) { + return true; + } + const newPathname = calculateDraggedItemNewPathname({ draggedItem, targetItem, dropType, collectionPathname }); if (!newPathname) return false; @@ -105,7 +110,7 @@ const CollectionItem = ({ item, collectionUid, collectionPathname, searchText }) }; const [{ isOver, canDrop }, drop] = useDrop({ - accept: `collection-item-${collectionUid}`, + accept: 'collection-item', hover: (draggedItem, monitor) => { const { uid: targetItemUid } = item; const { uid: draggedItemUid } = draggedItem; diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/StyledWrapper.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/StyledWrapper.js index 8c1111c29..dc2e05251 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/StyledWrapper.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/StyledWrapper.js @@ -7,6 +7,7 @@ const Wrapper = styled.div` user-select: none; padding-left: 8px; font-weight: 600; + border: ${(props) => props.theme.dragAndDrop.borderStyle} transparent; .rotate-90 { transform: rotateZ(90deg); @@ -66,6 +67,7 @@ const Wrapper = styled.div` } &.drop-target { + border: ${(props) => props.theme.dragAndDrop.borderStyle} ${(props) => props.theme.dragAndDrop.border}; background-color: ${(props) => props.theme.dragAndDrop.hoverBg}; transition: ${(props) => props.theme.dragAndDrop.transition}; } @@ -95,15 +97,6 @@ const Wrapper = styled.div` } } - .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 { white-space: nowrap; text-overflow: ellipsis; 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 67ffcb68b..3152bef8c 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js @@ -34,6 +34,7 @@ const Collection = ({ collection, searchText }) => { const [showCloneCollectionModalOpen, setShowCloneCollectionModalOpen] = useState(false); const [showShareCollectionModal, setShowShareCollectionModal] = useState(false); const [showRemoveCollectionModal, setShowRemoveCollectionModal] = useState(false); + const [dropType, setDropType] = useState(null); const dispatch = useDispatch(); const isLoading = areItemsLoading(collection); const collectionRef = useRef(null); @@ -42,7 +43,7 @@ const Collection = ({ collection, searchText }) => { const menuDropdownTippyRef = useRef(); const onMenuDropdownCreate = (ref) => (menuDropdownTippyRef.current = ref); - const MenuIcon = forwardRef((props, ref) => { + const MenuIcon = forwardRef((_props, ref) => { return (
@@ -101,7 +102,7 @@ const Collection = ({ collection, searchText }) => { } }; - const handleDoubleClick = (event) => { + const handleDoubleClick = (_event) => { dispatch(makeTabPermanent({ uid: collection.uid })) }; @@ -118,7 +119,7 @@ const Collection = ({ collection, searchText }) => { e.preventDefault(); }; - const handleRightClick = (event) => { + const handleRightClick = (_event) => { const _menuDropdown = menuDropdownTippyRef.current; if (_menuDropdown) { let menuDropdownBehavior = 'show'; @@ -140,7 +141,7 @@ const Collection = ({ collection, searchText }) => { }; const isCollectionItem = (itemType) => { - return itemType.startsWith('collection-item'); + return itemType === 'collection-item'; }; const [{ isDragging }, drag, dragPreview] = useDrag({ @@ -155,7 +156,17 @@ const Collection = ({ collection, searchText }) => { }); const [{ isOver }, drop] = useDrop({ - accept: ["collection", `collection-item-${collection.uid}`], + accept: ["collection", "collection-item"], + hover: (_draggedItem, monitor) => { + const itemType = monitor.getItemType(); + if (isCollectionItem(itemType)) { + // For collection items, always show full highlight (inside drop) + setDropType('inside'); + } else { + // For collections, show line indicator (adjacent drop) + setDropType('adjacent'); + } + }, drop: (draggedItem, monitor) => { const itemType = monitor.getItemType(); if (isCollectionItem(itemType)) { @@ -163,6 +174,7 @@ const Collection = ({ collection, searchText }) => { } else { dispatch(moveCollectionAndPersist({draggedItem, targetItem: collection})); } + setDropType(null); }, canDrop: (draggedItem) => { return draggedItem.uid !== collection.uid; @@ -183,7 +195,8 @@ const Collection = ({ collection, searchText }) => { } const collectionRowClassName = classnames('flex py-1 collection-name items-center', { - 'item-hovered': isOver, + '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 }); @@ -241,7 +254,7 @@ const Collection = ({ collection, searchText }) => { } placement="bottom-start">
{ + onClick={(_e) => { menuDropdownTippyRef.current.hide(); setShowNewRequestModal(true); }} @@ -250,7 +263,7 @@ const Collection = ({ collection, searchText }) => {
{ + onClick={(_e) => { menuDropdownTippyRef.current.hide(); setShowNewFolderModal(true); }} @@ -259,7 +272,7 @@ const Collection = ({ collection, searchText }) => {
{ + onClick={(_e) => { menuDropdownTippyRef.current.hide(); setShowCloneCollectionModalOpen(true); }} @@ -268,7 +281,7 @@ const Collection = ({ collection, searchText }) => {
{ + onClick={(_e) => { menuDropdownTippyRef.current.hide(); ensureCollectionIsMounted(); handleRun(); @@ -278,7 +291,7 @@ const Collection = ({ collection, searchText }) => {
{ + onClick={(_e) => { menuDropdownTippyRef.current.hide(); setShowRenameCollectionModal(true); }} @@ -287,7 +300,7 @@ const Collection = ({ collection, searchText }) => {
{ + onClick={(_e) => { menuDropdownTippyRef.current.hide(); ensureCollectionIsMounted(); setShowShareCollectionModal(true); @@ -297,7 +310,7 @@ const Collection = ({ collection, searchText }) => {
{ + onClick={(_e) => { menuDropdownTippyRef.current.hide(); setShowRemoveCollectionModal(true); }} @@ -306,7 +319,7 @@ const Collection = ({ collection, searchText }) => {
{ + onClick={(_e) => { menuDropdownTippyRef.current.hide(); viewCollectionSettings(); }} diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js index 2787016c4..c341cf4f5 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js @@ -726,11 +726,16 @@ export const handleCollectionItemDrop = (dispatch, getState) => { const state = getState(); const collection = findCollectionByUid(state.collections.collections, collectionUid); + // if its withincollection set the source to current collection, + // if its cross collection set the source to the source collection + const sourceCollectionUid = draggedItem.sourceCollectionUid + const isCrossCollectionMove = sourceCollectionUid && collectionUid !== sourceCollectionUid; + const sourceCollection = isCrossCollectionMove ? findCollectionByUid(state.collections.collections, sourceCollectionUid) : collection; const { uid: draggedItemUid, pathname: draggedItemPathname } = draggedItem; const { uid: targetItemUid, pathname: targetItemPathname } = targetItem; const targetItemDirectory = findParentItemInCollection(collection, targetItemUid) || collection; const targetItemDirectoryItems = cloneDeep(targetItemDirectory.items); - const draggedItemDirectory = findParentItemInCollection(collection, draggedItemUid) || collection; + const draggedItemDirectory = findParentItemInCollection(sourceCollection, draggedItemUid) || sourceCollection; const draggedItemDirectoryItems = cloneDeep(draggedItemDirectory.items); const handleMoveToNewLocation = async ({ diff --git a/packages/bruno-electron/src/ipc/collection.js b/packages/bruno-electron/src/ipc/collection.js index 30cf11111..f26ec6a2c 100644 --- a/packages/bruno-electron/src/ipc/collection.js +++ b/packages/bruno-electron/src/ipc/collection.js @@ -699,7 +699,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection // Recursive function to parse the folder and create files/folders const parseCollectionItems = (items = [], currentPath) => { items.forEach(async (item) => { - if (['http-request', 'graphql-request'].includes(item.type)) { + if (['http-request', 'graphql-request', 'grpc-request'].includes(item.type)) { const content = await stringifyRequestViaWorker(item); const filePath = path.join(currentPath, item.filename); safeWriteFileSync(filePath, content);