mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-25 21:55:49 +00:00
Bugfix/close saved deleting collections (#7048)
This commit is contained in:
committed by
GitHub
parent
1443fb0f4e
commit
78240d9232
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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(() => [
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user