diff --git a/packages/bruno-app/src/components/RequestTabPanel/ExampleNotFound/index.js b/packages/bruno-app/src/components/RequestTabPanel/ExampleNotFound/index.js index 13c5e7385..e5a8de317 100644 --- a/packages/bruno-app/src/components/RequestTabPanel/ExampleNotFound/index.js +++ b/packages/bruno-app/src/components/RequestTabPanel/ExampleNotFound/index.js @@ -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'; diff --git a/packages/bruno-app/src/components/RequestTabPanel/FolderNotFound/index.js b/packages/bruno-app/src/components/RequestTabPanel/FolderNotFound/index.js index 685951c4e..29eb6fac9 100644 --- a/packages/bruno-app/src/components/RequestTabPanel/FolderNotFound/index.js +++ b/packages/bruno-app/src/components/RequestTabPanel/FolderNotFound/index.js @@ -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'; diff --git a/packages/bruno-app/src/components/RequestTabPanel/RequestNotFound/index.js b/packages/bruno-app/src/components/RequestTabPanel/RequestNotFound/index.js index 14299c811..cf562f15e 100644 --- a/packages/bruno-app/src/components/RequestTabPanel/RequestNotFound/index.js +++ b/packages/bruno-app/src/components/RequestTabPanel/RequestNotFound/index.js @@ -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'; diff --git a/packages/bruno-app/src/components/RequestTabs/ExampleTab/index.js b/packages/bruno-app/src/components/RequestTabs/ExampleTab/index.js index 6cf429b2c..17210efc3 100644 --- a/packages/bruno-app/src/components/RequestTabs/ExampleTab/index.js +++ b/packages/bruno-app/src/components/RequestTabs/ExampleTab/index.js @@ -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'; diff --git a/packages/bruno-app/src/components/RequestTabs/RequestTab/index.js b/packages/bruno-app/src/components/RequestTabs/RequestTab/index.js index f001484e3..64651a253 100644 --- a/packages/bruno-app/src/components/RequestTabs/RequestTab/index.js +++ b/packages/bruno-app/src/components/RequestTabs/RequestTab/index.js @@ -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(() => [ diff --git a/packages/bruno-app/src/components/SaveTransientRequest/Container.js b/packages/bruno-app/src/components/SaveTransientRequest/Container.js index 6fce90ed8..27943acd7 100644 --- a/packages/bruno-app/src/components/SaveTransientRequest/Container.js +++ b/packages/bruno-app/src/components/SaveTransientRequest/Container.js @@ -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'; diff --git a/packages/bruno-app/src/components/SaveTransientRequest/index.js b/packages/bruno-app/src/components/SaveTransientRequest/index.js index 06210a289..c8569f735 100644 --- a/packages/bruno-app/src/components/SaveTransientRequest/index.js +++ b/packages/bruno-app/src/components/SaveTransientRequest/index.js @@ -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', diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/DeleteCollectionItem/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/DeleteCollectionItem/index.js index db339e2d3..c4d00674a 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/DeleteCollectionItem/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/DeleteCollectionItem/index.js @@ -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'; diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/ExampleItem/DeleteResponseExampleModal/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/ExampleItem/DeleteResponseExampleModal/index.js index 58078433a..6abbc09ac 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/ExampleItem/DeleteResponseExampleModal/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/ExampleItem/DeleteResponseExampleModal/index.js @@ -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(); diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RenameCollectionItem/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RenameCollectionItem/index.js index 07dbd5558..9103c2189 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RenameCollectionItem/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RenameCollectionItem/index.js @@ -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'; diff --git a/packages/bruno-app/src/providers/Hotkeys/index.js b/packages/bruno-app/src/providers/Hotkeys/index.js index c5bb6ad2e..4f46872a1 100644 --- a/packages/bruno-app/src/providers/Hotkeys/index.js +++ b/packages/bruno-app/src/providers/Hotkeys/index.js @@ -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'; diff --git a/packages/bruno-app/src/providers/ReduxStore/middlewares/tasks/middleware.js b/packages/bruno-app/src/providers/ReduxStore/middlewares/tasks/middleware.js index 540981b29..fb0e43470 100644 --- a/packages/bruno-app/src/providers/ReduxStore/middlewares/tasks/middleware.js +++ b/packages/bruno-app/src/providers/ReduxStore/middlewares/tasks/middleware.js @@ -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; diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/app.js b/packages/bruno-app/src/providers/ReduxStore/slices/app.js index 8bae1ab6d..6814b13c2 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/app.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/app.js @@ -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, 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 bef97ed67..b2d33e966 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js @@ -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); + } + } +}; diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/tabs.js b/packages/bruno-app/src/providers/ReduxStore/slices/tabs.js index 8f8b93499..0e4ea28dc 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/tabs.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/tabs.js @@ -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) { diff --git a/packages/bruno-electron/src/ipc/collection.js b/packages/bruno-electron/src/ipc/collection.js index 853425edc..2fa8656dc 100644 --- a/packages/bruno-electron/src/ipc/collection.js +++ b/packages/bruno-electron/src/ipc/collection.js @@ -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);