mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-11 09:51:30 +00:00
fix: cross-collection drag and drop tab and format issues (#7584)
Close the open tab when a request is moved to a different collection via drag and drop, preventing the "Request no longer exists" error. Add format conversion when dragging requests between collections with different formats (.bru vs .yml). A new IPC handler parses the source file and re-serializes it in the target collection's format. Folder cross-format moves are blocked with a toast error. Co-authored-by: Chirag Chandrashekhar <cchirag85@gmail.com>
This commit is contained in:
committed by
GitHub
parent
03dcb6b7b9
commit
ff975c44f2
@@ -1112,6 +1112,10 @@ export const handleCollectionItemDrop
|
||||
const draggedItemDirectory = findParentItemInCollection(sourceCollection, draggedItemUid) || sourceCollection;
|
||||
const draggedItemDirectoryItems = cloneDeep(draggedItemDirectory.items);
|
||||
|
||||
const sourceFormat = sourceCollection?.format || 'bru';
|
||||
const targetFormat = collection?.format || 'bru';
|
||||
const isCrossFormatMove = isCrossCollectionMove && sourceFormat !== targetFormat;
|
||||
|
||||
const handleMoveToNewLocation = async ({
|
||||
draggedItem,
|
||||
draggedItemDirectoryItems,
|
||||
@@ -1124,10 +1128,22 @@ export const handleCollectionItemDrop
|
||||
const { pathname: draggedItemPathname, uid: draggedItemUid } = draggedItem;
|
||||
|
||||
const newDirname = path.dirname(newPathname);
|
||||
await dispatch(moveItem({
|
||||
targetDirname: newDirname,
|
||||
sourcePathname: draggedItemPathname
|
||||
}));
|
||||
|
||||
if (isCrossFormatMove && isItemARequest(draggedItem)) {
|
||||
const { ipcRenderer } = window;
|
||||
const result = await ipcRenderer.invoke('renderer:move-item-cross-format', {
|
||||
targetDirname: newDirname,
|
||||
sourcePathname: draggedItemPathname,
|
||||
sourceFormat,
|
||||
targetFormat
|
||||
});
|
||||
newPathname = result.newPathname;
|
||||
} else {
|
||||
await dispatch(moveItem({
|
||||
targetDirname: newDirname,
|
||||
sourcePathname: draggedItemPathname
|
||||
}));
|
||||
}
|
||||
|
||||
// Update sequences in the source directory
|
||||
if (draggedItemDirectoryItems?.length) {
|
||||
@@ -1191,6 +1207,11 @@ export const handleCollectionItemDrop
|
||||
if (!newPathname) return;
|
||||
if (targetItemPathname?.startsWith(draggedItemPathname)) return;
|
||||
|
||||
if (isCrossFormatMove && isItemAFolder(draggedItem)) {
|
||||
toast.error('Moving folders between collections with different formats is not supported');
|
||||
return;
|
||||
}
|
||||
|
||||
// Discard operation if dragging a root item to the collection name (same location)
|
||||
const isTargetTheCollection = targetItemPathname === collection.pathname;
|
||||
const isDraggedItemAtRoot = draggedItemDirectory === sourceCollection;
|
||||
@@ -1210,6 +1231,11 @@ export const handleCollectionItemDrop
|
||||
} else {
|
||||
await handleReorderInSameLocation({ draggedItem, targetItemDirectoryItems, targetItem });
|
||||
}
|
||||
|
||||
if (isCrossCollectionMove) {
|
||||
dispatch(closeTabs({ tabUids: [draggedItemUid] }));
|
||||
}
|
||||
|
||||
resolve();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
||||
@@ -1448,6 +1448,40 @@ const registerRendererEventHandlers = (mainWindow, watcher) => {
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('renderer:move-item-cross-format', async (event, { targetDirname, sourcePathname, sourceFormat, targetFormat }) => {
|
||||
try {
|
||||
if (!fs.existsSync(sourcePathname)) {
|
||||
throw new Error(`Source path: ${sourcePathname} does not exist`);
|
||||
}
|
||||
if (!fs.existsSync(targetDirname)) {
|
||||
throw new Error(`Target directory: ${targetDirname} does not exist`);
|
||||
}
|
||||
|
||||
const sourceBasename = path.basename(sourcePathname);
|
||||
const filenameWithoutExt = sourceBasename.replace(/\.(bru|yml|yaml)$/, '');
|
||||
const targetExt = targetFormat === 'yml' ? 'yml' : 'bru';
|
||||
const targetFilename = `${filenameWithoutExt}.${targetExt}`;
|
||||
const targetPathname = path.join(targetDirname, targetFilename);
|
||||
|
||||
if (fs.existsSync(targetPathname)) {
|
||||
throw new Error(`A file with the name "${targetFilename}" already exists in the target location`);
|
||||
}
|
||||
|
||||
const sourceContent = await fs.promises.readFile(sourcePathname, 'utf8');
|
||||
const parsedRequest = parseRequest(sourceContent, { format: sourceFormat });
|
||||
const finalContent = stringifyRequest(parsedRequest, { format: targetFormat });
|
||||
|
||||
await writeFile(targetPathname, finalContent);
|
||||
await removePath(sourcePathname);
|
||||
|
||||
moveRequestUid(sourcePathname, targetPathname);
|
||||
|
||||
return { newPathname: targetPathname };
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('renderer:move-folder-item', async (event, folderPath, destinationPath) => {
|
||||
try {
|
||||
const folderName = path.basename(folderPath);
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import { test, expect } from '../../../playwright';
|
||||
import { closeAllCollections } from '../../utils/page';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
|
||||
test.describe('Cross-Format Collection Drag and Drop', () => {
|
||||
test.afterEach(async ({ pageWithUserData: page }) => {
|
||||
await closeAllCollections(page);
|
||||
});
|
||||
|
||||
test('Cross-format drag and drop should convert request between bru and yml', async ({
|
||||
pageWithUserData: page,
|
||||
collectionFixturePath
|
||||
}) => {
|
||||
const requestName = 'cross-format-request';
|
||||
|
||||
// Both collections should already be loaded via init-user-data
|
||||
await expect(page.locator('#sidebar-collection-name').filter({ hasText: 'bru-collection' })).toBeVisible();
|
||||
await expect(page.locator('#sidebar-collection-name').filter({ hasText: 'yml-collection' })).toBeVisible();
|
||||
|
||||
// Expand the bru collection and locate the request
|
||||
await page.locator('#sidebar-collection-name').filter({ hasText: 'bru-collection' }).click();
|
||||
const bruCollectionContainer = page
|
||||
.locator('.collection-name')
|
||||
.filter({ hasText: 'bru-collection' })
|
||||
.locator('..');
|
||||
const bruRequest = bruCollectionContainer.locator('.collection-item-name').filter({ hasText: requestName }).first();
|
||||
await expect(bruRequest).toBeVisible();
|
||||
|
||||
// Drag the .bru request into the yml collection
|
||||
const ymlCollection = page.locator('.collection-name').filter({ hasText: 'yml-collection' });
|
||||
await bruRequest.dragTo(ymlCollection);
|
||||
|
||||
// Verify the request appears in the yml collection (increase timeout for file watcher processing)
|
||||
const ymlCollectionContainer = page
|
||||
.locator('.collection-name')
|
||||
.filter({ hasText: 'yml-collection' })
|
||||
.locator('..');
|
||||
// The yml collection may need to be expanded after the drop
|
||||
const ymlCollectionItems = ymlCollectionContainer.locator('.collection-item-name').filter({ hasText: requestName });
|
||||
// Wait for file watcher to process the new file, then expand collection if needed
|
||||
await expect(async () => {
|
||||
if (await ymlCollectionItems.count() === 0) {
|
||||
await page.locator('#sidebar-collection-name').filter({ hasText: 'yml-collection' }).click();
|
||||
}
|
||||
await expect(ymlCollectionItems).toBeVisible();
|
||||
}).toPass({ timeout: 15000 });
|
||||
|
||||
// Verify the request is no longer in the bru collection
|
||||
await page.locator('#sidebar-collection-name').filter({ hasText: 'bru-collection' }).click();
|
||||
await expect(bruCollectionContainer.locator('.collection-item-name').filter({ hasText: requestName })).toHaveCount(0);
|
||||
|
||||
// Verify the file was converted to .yml format on disk
|
||||
const ymlFile = path.join(collectionFixturePath!, 'yml-collection', `${requestName}.yml`);
|
||||
const bruFile = path.join(collectionFixturePath!, 'yml-collection', `${requestName}.bru`);
|
||||
expect(fs.existsSync(ymlFile)).toBe(true);
|
||||
expect(fs.existsSync(bruFile)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -98,4 +98,40 @@ test.describe('Cross-Collection Drag and Drop', () => {
|
||||
await page.locator('#sidebar-collection-name').filter({ hasText: 'target-collection' }).click();
|
||||
await expect(targetCollectionContainer.locator('.collection-item-name').filter({ hasText: requestName }).first()).toBeVisible();
|
||||
});
|
||||
|
||||
test('Tab should be closed after cross-collection drag and drop', async ({ page, createTmpDir }) => {
|
||||
const requestName = 'tab-close-request';
|
||||
|
||||
// Create source and target collections
|
||||
await createCollection(page, 'source-collection', await createTmpDir('source-collection'));
|
||||
await createRequest(page, requestName, 'source-collection', { url: 'https://echo.usebruno.com' });
|
||||
|
||||
await createCollection(page, 'target-collection', await createTmpDir('target-collection'));
|
||||
|
||||
// Open the request to create a tab
|
||||
const sourceCollectionContainer = page
|
||||
.locator('.collection-name')
|
||||
.filter({ hasText: 'source-collection' })
|
||||
.locator('..');
|
||||
const sourceRequest = sourceCollectionContainer.locator('.collection-item-name').filter({ hasText: requestName }).first();
|
||||
await sourceRequest.click();
|
||||
|
||||
// Verify the tab is open
|
||||
const requestTab = page.locator('.request-tab .tab-label').filter({ hasText: requestName });
|
||||
await expect(requestTab).toBeVisible();
|
||||
|
||||
// Drag the request to target collection
|
||||
const targetCollection = page.locator('.collection-name').filter({ hasText: 'target-collection' });
|
||||
await sourceRequest.dragTo(targetCollection);
|
||||
|
||||
// Verify the tab is closed after cross-collection move
|
||||
await expect(requestTab).not.toBeVisible();
|
||||
|
||||
// Verify the request appears in the target collection
|
||||
const targetCollectionContainer = page
|
||||
.locator('.collection-name')
|
||||
.filter({ hasText: 'target-collection' })
|
||||
.locator('..');
|
||||
await expect(targetCollectionContainer.locator('.collection-item-name').filter({ hasText: requestName })).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"version": "1",
|
||||
"name": "bru-collection",
|
||||
"type": "collection"
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
meta {
|
||||
name: cross-format-request
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: https://echo.usebruno.com
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
opencollection: "1"
|
||||
info:
|
||||
name: yml-collection
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"collections": [
|
||||
{
|
||||
"path": "{{collectionPath}}/bru-collection",
|
||||
"securityConfig": {
|
||||
"jsSandboxMode": "safe"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "{{collectionPath}}/yml-collection",
|
||||
"securityConfig": {
|
||||
"jsSandboxMode": "safe"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"lastOpenedCollections": [
|
||||
"{{collectionPath}}/bru-collection",
|
||||
"{{collectionPath}}/yml-collection"
|
||||
],
|
||||
"preferences": {
|
||||
"onboarding": {
|
||||
"hasLaunchedBefore": true,
|
||||
"hasSeenWelcomeModal": true
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user