Bugfix/close saved deleting collections (#7048)

This commit is contained in:
Chirag Chandrashekhar
2026-02-06 12:31:58 +05:30
committed by GitHub
parent 1443fb0f4e
commit 78240d9232
16 changed files with 136 additions and 70 deletions

View File

@@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react';
import { closeTabs } from 'providers/ReduxStore/slices/tabs';
import { closeTabs } from 'providers/ReduxStore/slices/collections/actions';
import { useDispatch } from 'react-redux';
import ErrorBanner from 'ui/ErrorBanner';
import Button from 'ui/Button';

View File

@@ -1,5 +1,5 @@
import React, { useEffect, useState, useCallback } from 'react';
import { closeTabs } from 'providers/ReduxStore/slices/tabs';
import { closeTabs } from 'providers/ReduxStore/slices/collections/actions';
import { useDispatch } from 'react-redux';
import ErrorBanner from 'ui/ErrorBanner';
import Button from 'ui/Button';

View File

@@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react';
import { closeTabs } from 'providers/ReduxStore/slices/tabs';
import { closeTabs } from 'providers/ReduxStore/slices/collections/actions';
import { useDispatch } from 'react-redux';
import ErrorBanner from 'ui/ErrorBanner';
import Button from 'ui/Button';

View File

@@ -1,8 +1,8 @@
import React, { useState, useRef, useMemo } from 'react';
import { useDispatch } from 'react-redux';
import { closeTabs, makeTabPermanent } from 'providers/ReduxStore/slices/tabs';
import { makeTabPermanent } from 'providers/ReduxStore/slices/tabs';
import { deleteRequestDraft } from 'providers/ReduxStore/slices/collections';
import { saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import { saveRequest, closeTabs } from 'providers/ReduxStore/slices/collections/actions';
import { hasExampleChanges, findItemInCollection } from 'utils/collections';
import ExampleIcon from 'components/Icons/ExampleIcon';
import ConfirmRequestClose from '../RequestTab/ConfirmRequestClose';

View File

@@ -1,7 +1,7 @@
import React, { useCallback, useState, useRef, Fragment, useMemo, useEffect } from 'react';
import get from 'lodash/get';
import { closeTabs, makeTabPermanent } from 'providers/ReduxStore/slices/tabs';
import { saveRequest, saveCollectionRoot, saveFolderRoot, saveEnvironment } from 'providers/ReduxStore/slices/collections/actions';
import { makeTabPermanent } from 'providers/ReduxStore/slices/tabs';
import { saveRequest, saveCollectionRoot, saveFolderRoot, saveEnvironment, closeTabs } from 'providers/ReduxStore/slices/collections/actions';
import { deleteRequestDraft, deleteCollectionDraft, deleteFolderDraft, clearEnvironmentsDraft } from 'providers/ReduxStore/slices/collections';
import { clearGlobalEnvironmentDraft } from 'providers/ReduxStore/slices/global-environments';
import { saveGlobalEnvironment } from 'providers/ReduxStore/slices/global-environments';
@@ -474,19 +474,42 @@ function RequestTabMenu({ menuDropdownRef, tabLabelRef, collectionRequestTabs, t
} catch (err) { }
}
async function handleCloseMultipleTabs(tabs) {
const tabUidsToClose = [];
for (const tab of tabs) {
const item = findItemInCollection(collection, tab.uid);
if (item && hasRequestChanges(item)) {
try {
await dispatch(saveRequest(item.uid, collection.uid, true));
} catch (err) {
continue;
}
}
if (tab?.uid) {
tabUidsToClose.push(tab.uid);
}
}
if (tabUidsToClose.length > 0) {
dispatch(closeTabs({ tabUids: tabUidsToClose }));
}
}
async function handleCloseOtherTabs() {
const otherTabs = collectionRequestTabs.filter((_, index) => index !== tabIndex);
await Promise.all(otherTabs.map((tab) => handleCloseTab(tab.uid)));
await handleCloseMultipleTabs(otherTabs);
}
async function handleCloseTabsToTheLeft() {
const leftTabs = collectionRequestTabs.filter((_, index) => index < tabIndex);
await Promise.all(leftTabs.map((tab) => handleCloseTab(tab.uid)));
await handleCloseMultipleTabs(leftTabs);
}
async function handleCloseTabsToTheRight() {
const rightTabs = collectionRequestTabs.filter((_, index) => index > tabIndex);
await Promise.all(rightTabs.map((tab) => handleCloseTab(tab.uid)));
await handleCloseMultipleTabs(rightTabs);
}
function handleCloseSavedTabs() {
@@ -497,7 +520,7 @@ function RequestTabMenu({ menuDropdownRef, tabLabelRef, collectionRequestTabs, t
}
async function handleCloseAllTabs() {
await Promise.all(collectionRequestTabs.map((tab) => handleCloseTab(tab.uid)));
await handleCloseMultipleTabs(collectionRequestTabs);
}
const menuItems = useMemo(() => [

View File

@@ -3,7 +3,7 @@ import { useSelector, useDispatch } from 'react-redux';
import { pluralizeWord } from 'utils/common';
import { IconAlertTriangle, IconDeviceFloppy } from '@tabler/icons';
import { clearAllSaveTransientRequestModals } from 'providers/ReduxStore/slices/collections';
import { closeTabs } from 'providers/ReduxStore/slices/tabs';
import { closeTabs } from 'providers/ReduxStore/slices/collections/actions';
import toast from 'react-hot-toast';
import Modal from 'components/Modal';
import Button from 'ui/Button';

View File

@@ -9,8 +9,7 @@ import toast from 'react-hot-toast';
import StyledWrapper from './StyledWrapper';
import useCollectionFolderTree from 'hooks/useCollectionFolderTree';
import { removeSaveTransientRequestModal, deleteRequestDraft } from 'providers/ReduxStore/slices/collections';
import { newFolder } from 'providers/ReduxStore/slices/collections/actions';
import { closeTabs } from 'providers/ReduxStore/slices/tabs';
import { newFolder, closeTabs } from 'providers/ReduxStore/slices/collections/actions';
import { sanitizeName, validateName, validateNameError } from 'utils/common/regex';
import { resolveRequestFilename } from 'utils/common/platform';
import { transformRequestToSaveToFilesystem, findCollectionByUid, findItemInCollection } from 'utils/collections';
@@ -127,11 +126,7 @@ const SaveTransientRequest = ({ item: itemProp, collection: collectionProp, isOp
format
});
dispatch(
closeTabs({
tabUids: [item.uid]
})
);
dispatch(closeTabs({ tabUids: [item.uid] }));
dispatch({
type: 'collections/deleteItem',

View File

@@ -2,8 +2,7 @@ import React from 'react';
import Modal from 'components/Modal';
import { isItemAFolder } from 'utils/tabs';
import { useDispatch } from 'react-redux';
import { closeTabs } from 'providers/ReduxStore/slices/tabs';
import { deleteItem } from 'providers/ReduxStore/slices/collections/actions';
import { deleteItem, closeTabs } from 'providers/ReduxStore/slices/collections/actions';
import { recursivelyGetAllItemUids } from 'utils/collections';
import StyledWrapper from './StyledWrapper';
import toast from 'react-hot-toast';

View File

@@ -3,8 +3,7 @@ import Modal from 'components/Modal';
import Portal from 'components/Portal';
import { useDispatch } from 'react-redux';
import { deleteResponseExample } from 'providers/ReduxStore/slices/collections';
import { saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import { closeTabs } from 'providers/ReduxStore/slices/tabs';
import { saveRequest, closeTabs } from 'providers/ReduxStore/slices/collections/actions';
const DeleteResponseExampleModal = ({ onClose, example, item, collection }) => {
const dispatch = useDispatch();

View File

@@ -4,12 +4,11 @@ import * as Yup from 'yup';
import Modal from 'components/Modal';
import { useDispatch } from 'react-redux';
import { isItemAFolder } from 'utils/tabs';
import { renameItem, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import { renameItem, saveRequest, closeTabs } from 'providers/ReduxStore/slices/collections/actions';
import path from 'utils/common/path';
import { IconArrowBackUp, IconEdit, IconCaretDown } from '@tabler/icons';
import { sanitizeName, validateName, validateNameError } from 'utils/common/regex';
import toast from 'react-hot-toast';
import { closeTabs } from 'providers/ReduxStore/slices/tabs';
import Help from 'components/Help';
import PathDisplay from 'components/PathDisplay';
import Portal from 'components/Portal';

View File

@@ -11,10 +11,11 @@ import {
saveRequest,
saveCollectionRoot,
saveFolderRoot,
saveCollectionSettings
saveCollectionSettings,
closeTabs
} from 'providers/ReduxStore/slices/collections/actions';
import { findCollectionByUid, findItemInCollection } from 'utils/collections';
import { addTab, closeTabs, reorderTabs, switchTab } from 'providers/ReduxStore/slices/tabs';
import { addTab, reorderTabs, switchTab } from 'providers/ReduxStore/slices/tabs';
import { closeWorkspaceTab } from 'providers/ReduxStore/slices/workspaceTabs';
import { toggleSidebarCollapse } from 'providers/ReduxStore/slices/app';
import { getKeyBindingsForActionAllOS } from './keyMappings';

View File

@@ -3,9 +3,9 @@ import each from 'lodash/each';
import filter from 'lodash/filter';
import { createListenerMiddleware } from '@reduxjs/toolkit';
import { removeTaskFromQueue } from 'providers/ReduxStore/slices/app';
import { addTab, closeTabs, closeAllCollectionTabs } from 'providers/ReduxStore/slices/tabs';
import { addTab } from 'providers/ReduxStore/slices/tabs';
import { collectionAddFileEvent, collectionChangeFileEvent } from 'providers/ReduxStore/slices/collections';
import { findCollectionByUid, findItemInCollectionByPathname, getDefaultRequestPaneTab, findItemInCollectionByItemUid, findItemInCollection, flattenItems } from 'utils/collections/index';
import { findCollectionByUid, findItemInCollectionByPathname, getDefaultRequestPaneTab, findItemInCollectionByItemUid } from 'utils/collections/index';
import { taskTypes } from './utils';
const taskMiddleware = createListenerMiddleware();
@@ -93,39 +93,4 @@ taskMiddleware.startListening({
}
});
/*
* When tabs are closed, check if any of them are transient requests.
* If so, delete the temporary files from the filesystem.
* Note: If a transient request was saved (moved to permanent location),
* the file will already be deleted, which is expected behavior.
*/
taskMiddleware.startListening({
actionCreator: closeTabs,
effect: (action, listenerApi) => {
const state = listenerApi.getState();
const tabUids = action.payload.tabUids || [];
const { ipcRenderer } = window;
each(tabUids, (tabUid) => {
const collections = state.collections.collections;
for (const collection of collections) {
const item = findItemInCollection(collection, tabUid);
const isTransient = item?.isTransient ?? false;
if (item && isTransient) {
ipcRenderer
.invoke('renderer:delete-item', item.pathname, item.type, collection.pathname)
.then(() => {})
.catch((err) => {
if (err.message && !err.message.includes('does not exist')) {
console.error(`Failed to delete transient request file: ${item.pathname}`, err);
}
});
break;
}
}
});
}
});
export default taskMiddleware;

View File

@@ -1,7 +1,7 @@
import { createSlice } from '@reduxjs/toolkit';
import filter from 'lodash/filter';
import brunoClipboard from 'utils/bruno-clipboard';
import { addTab, focusTab, closeTabs } from './tabs';
import { addTab, focusTab } from './tabs';
const initialState = {
isDragging: false,

View File

@@ -64,7 +64,7 @@ import {
} from './index';
import { each } from 'lodash';
import { closeAllCollectionTabs, updateResponsePaneScrollPosition } from 'providers/ReduxStore/slices/tabs';
import { closeAllCollectionTabs, closeTabs as _closeTabs, updateResponsePaneScrollPosition } from 'providers/ReduxStore/slices/tabs';
import { removeCollectionFromWorkspace } from 'providers/ReduxStore/slices/workspaces';
import { resolveRequestFilename } from 'utils/common/platform';
import { interpolateUrl, parsePathParams, splitOnFirst } from 'utils/url/index';
@@ -2964,3 +2964,47 @@ export const deleteDotEnvFile = (collectionUid, filename = '.env') => (dispatch,
.catch(reject);
});
};
/**
* Close tabs and delete any transient request files from the filesystem.
* This thunk wraps the closeTabs reducer to handle transient file cleanup automatically.
*/
export const closeTabs = ({ tabUids }) => async (dispatch, getState) => {
const { ipcRenderer } = window;
const state = getState();
const collections = state.collections.collections;
const tempDirectories = state.collections.tempDirectories || {};
// Find transient items and group by temp directory before closing tabs
const transientByTempDir = {};
each(tabUids, (tabUid) => {
for (const collection of collections) {
const item = findItemInCollection(collection, tabUid);
if (item?.isTransient && item.pathname) {
const tempDir = tempDirectories[collection.uid];
if (tempDir) {
if (!transientByTempDir[tempDir]) {
transientByTempDir[tempDir] = [];
}
transientByTempDir[tempDir].push(item.pathname);
}
break;
}
}
});
// Close the tabs first
await dispatch(_closeTabs({ tabUids }));
// Delete transient files after tabs are closed
for (const [tempDir, filePaths] of Object.entries(transientByTempDir)) {
try {
const results = await ipcRenderer.invoke('renderer:delete-transient-requests', filePaths, tempDir);
if (results.errors?.length > 0) {
console.error('Errors deleting transient files:', results.errors);
}
} catch (err) {
console.error('Failed to delete transient request files:', err);
}
}
};

View File

@@ -94,7 +94,11 @@ export const tabsSlice = createSlice({
state.activeTabUid = uid;
},
focusTab: (state, action) => {
state.activeTabUid = action.payload.uid;
const { uid } = action.payload;
const tabExists = state.tabs.some((t) => t.uid === uid);
if (tabExists) {
state.activeTabUid = uid;
}
},
switchTab: (state, action) => {
if (!state.tabs || !state.tabs.length) {

View File

@@ -421,9 +421,6 @@ const registerRendererEventHandlers = (mainWindow, watcher) => {
// Step 3: Create new file at target location with the content
await writeFile(targetPathname, fileContent);
// Step 4: Delete the old temp file
await removePath(sourcePathname);
// Return the new pathname (file watcher will handle adding to Redux)
return { newPathname: targetPathname };
} catch (error) {
@@ -1001,6 +998,46 @@ const registerRendererEventHandlers = (mainWindow, watcher) => {
}
});
// Delete transient request files by their absolute paths
// This is a simpler handler specifically for cleaning up transient requests
// tempDirectory: the collection's temp directory path to validate files belong to this collection
ipcMain.handle('renderer:delete-transient-requests', async (event, filePaths, tempDirectory) => {
const brunoTempPrefix = path.join(os.tmpdir(), 'bruno-');
const results = { deleted: [], skipped: [], errors: [] };
// Validate tempDirectory is within Bruno temp prefix
const normalizedTempDir = tempDirectory ? path.normalize(tempDirectory) : null;
if (!normalizedTempDir || !normalizedTempDir.startsWith(brunoTempPrefix)) {
return { deleted: [], skipped: filePaths.map((p) => ({ path: p, reason: 'Invalid temp directory' })), errors: [] };
}
for (const filePath of filePaths) {
try {
// Safety check: only delete files within the collection's temp directory
const normalizedPath = path.normalize(filePath);
if (!normalizedPath.startsWith(normalizedTempDir + path.sep) && normalizedPath !== normalizedTempDir) {
results.skipped.push({ path: filePath, reason: 'Not in collection temp directory' });
continue;
}
// Check if file exists before trying to delete
if (!fs.existsSync(filePath)) {
results.skipped.push({ path: filePath, reason: 'File does not exist' });
continue;
}
// Delete the file and its UID mapping
deleteRequestUid(filePath);
fs.unlinkSync(filePath);
results.deleted.push(filePath);
} catch (error) {
results.errors.push({ path: filePath, error: error.message });
}
}
return results;
});
ipcMain.handle('renderer:open-collection', async () => {
if (watcher && mainWindow) {
await openCollectionDialog(mainWindow, watcher);