diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/IgnoreCollectionItem/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/IgnoreCollectionItem/index.js
new file mode 100644
index 00000000000..c07af7053f5
--- /dev/null
+++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/IgnoreCollectionItem/index.js
@@ -0,0 +1,59 @@
+import React from 'react';
+import Modal from 'components/Modal';
+import { useSelector, useDispatch } from 'react-redux';
+import { ignoreFolder, closeTabs } from 'providers/ReduxStore/slices/collections/actions';
+import { recursivelyGetAllItemUids } from 'utils/collections';
+import toast from 'react-hot-toast';
+
+const IgnoreCollectionItem = ({ onClose, item, collectionUid }) => {
+ const dispatch = useDispatch();
+ const collection = useSelector((state) => state.collections.collections?.find((c) => c.uid === collectionUid));
+ const isYamlCollection = collection?.format === 'yml' || Boolean(collection?.brunoConfig?.opencollection);
+ const configFileName = isYamlCollection ? 'opencollection.yml' : 'bruno.json';
+
+ const onConfirm = () => {
+ dispatch(ignoreFolder(item.uid, collectionUid))
+ .then(() => {
+ const tabUids = [...recursivelyGetAllItemUids(item.items), item.uid];
+ dispatch(closeTabs({ tabUids }));
+ toast.success('Folder ignored');
+ })
+ .catch((error) => {
+ console.error('Error ignoring folder', error);
+ toast.error(error?.message || 'Error ignoring folder');
+ });
+ onClose();
+ };
+
+ return (
+
+ Ignoring {item.name} will hide it from this
+ {' '}
+ {isYamlCollection ? 'opencollection (YAML)' : 'Bruno (JSON)'}
+ {' '}
+ collection by adding it to the
+ {' '}
+ ignore
+ {' '}
+ list in
+ {' '}
+ {configFileName}
+ . The folder and its files are not deleted. To restore it later, remove the entry from the
+ {' '}
+ ignore
+ {' '}
+ list in
+ {' '}
+ {configFileName}
+ .
+
+ );
+};
+
+export default IgnoreCollectionItem;
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 5ffbd533c1a..d57afafb1ca 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
@@ -19,7 +19,8 @@ import {
IconSettings,
IconInfoCircle,
IconTerminal2,
- IconAppWindow
+ IconAppWindow,
+ IconEyeOff
} from '@tabler/icons';
import { useSelector, useDispatch } from 'react-redux';
import { addTab, focusTab, makeTabPermanent } from 'providers/ReduxStore/slices/tabs';
@@ -34,6 +35,7 @@ import NewApp from 'components/Sidebar/NewApp';
import RenameCollectionItem from './RenameCollectionItem';
import CloneCollectionItem from './CloneCollectionItem';
import DeleteCollectionItem from './DeleteCollectionItem';
+import IgnoreCollectionItem from './IgnoreCollectionItem';
import RunCollectionItem from './RunCollectionItem';
import GenerateCodeItem from './GenerateCodeItem';
import { isItemARequest, isItemAFolder } from 'utils/tabs';
@@ -98,6 +100,7 @@ const CollectionItem = ({ item, collectionUid, collectionPathname, searchText })
const [renameItemModalOpen, setRenameItemModalOpen] = useState(false);
const [cloneItemModalOpen, setCloneItemModalOpen] = useState(false);
const [deleteItemModalOpen, setDeleteItemModalOpen] = useState(false);
+ const [ignoreItemModalOpen, setIgnoreItemModalOpen] = useState(false);
const [createExampleModalOpen, setCreateExampleModalOpen] = useState(false);
const [generateCodeItemModalOpen, setGenerateCodeItemModalOpen] = useState(false);
const [newRequestModalOpen, setNewRequestModalOpen] = useState(false);
@@ -448,6 +451,15 @@ const CollectionItem = ({ item, collectionUid, collectionPathname, searchText })
}
);
+ if (isFolder) {
+ items.push({
+ id: 'ignore',
+ leftSection: IconEyeOff,
+ label: 'Ignore',
+ onClick: () => setIgnoreItemModalOpen(true)
+ });
+ }
+
items.push({ id: 'separator-1', type: 'divider' });
items.push({
@@ -645,6 +657,9 @@ const CollectionItem = ({ item, collectionUid, collectionPathname, searchText })
{deleteItemModalOpen && (
setDeleteItemModalOpen(false)} />
)}
+ {ignoreItemModalOpen && (
+ setIgnoreItemModalOpen(false)} />
+ )}
{newRequestModalOpen && (
setNewRequestModalOpen(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 8b551e8080b..ca8facfef03 100644
--- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js
+++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js
@@ -39,6 +39,8 @@ import {
sortCollections as _sortCollections,
updateCollectionMountStatus,
moveCollection,
+ deleteItem as _deleteItemFromState,
+ brunoConfigUpdateEvent as _brunoConfigUpdateEvent,
workspaceEnvUpdateEvent,
requestCancelled,
resetRunResults,
@@ -2767,6 +2769,32 @@ export const updateBrunoConfig = (brunoConfig, collectionUid) => (dispatch, getS
});
};
+export const ignoreFolder = (itemUid, collectionUid) => (dispatch, getState) => {
+ const state = getState();
+ const collection = findCollectionByUid(state.collections.collections, collectionUid);
+
+ return new Promise((resolve, reject) => {
+ if (!collection) {
+ return reject(new Error('Collection not found'));
+ }
+
+ const item = findItemInCollection(collection, itemUid);
+ if (!item) {
+ return reject(new Error('Unable to locate item'));
+ }
+
+ const { ipcRenderer } = window;
+ ipcRenderer
+ .invoke('renderer:ignore-folder', collectionUid, collection.pathname, collection.root, collection.brunoConfig || {}, item.pathname)
+ .then((updatedBrunoConfig) => {
+ dispatch(_brunoConfigUpdateEvent({ collectionUid, brunoConfig: updatedBrunoConfig }));
+ dispatch(_deleteItemFromState({ itemUid, collectionUid }));
+ resolve();
+ })
+ .catch(reject);
+ });
+};
+
/**
* Opens a scratch collection and creates it in Redux state.
* This is a simplified version of openCollectionEvent for scratch collections,
diff --git a/packages/bruno-electron/src/app/collection-watcher.js b/packages/bruno-electron/src/app/collection-watcher.js
index 4fab1c211a8..d1d75807849 100644
--- a/packages/bruno-electron/src/app/collection-watcher.js
+++ b/packages/bruno-electron/src/app/collection-watcher.js
@@ -21,7 +21,7 @@ const { uuid } = require('../utils/common');
const { parseValueByDataType } = require('@usebruno/common/utils');
const { getRequestUid } = require('../cache/requestUids');
const { decryptStringSafe } = require('../utils/encryption');
-const { setBrunoConfig } = require('../store/bruno-config');
+const { setBrunoConfig, getBrunoConfig } = require('../store/bruno-config');
const EnvironmentSecretsStore = require('../store/env-secrets');
const snapshotManager = require('../services/snapshot');
const { parseFileMeta, hydrateRequestWithUuid } = require('../utils/collection');
@@ -820,8 +820,6 @@ class CollectionWatcher {
// Always ignore node_modules and .git, regardless of user config
// This prevents infinite loops with symlinked directories (e.g., npm workspaces)
const defaultIgnores = ['node_modules', '.git'];
- const userIgnores = brunoConfig?.ignore || [];
- const ignores = [...new Set([...defaultIgnores, ...userIgnores])];
setTimeout(() => {
const watcher = chokidar.watch(watchPath, {
@@ -843,8 +841,16 @@ class CollectionWatcher {
return true;
}
+ const userIgnores = getBrunoConfig(collectionUid)?.ignore || [];
+ const ignores = [...new Set([...defaultIgnores, ...userIgnores])];
+ const normalizedRelativePath = relativePath.split(path.sep).join('/');
+
return ignores.some((ignorePattern) => {
- return relativePath === ignorePattern || relativePath.startsWith(ignorePattern);
+ const normalizedIgnorePattern = ignorePattern.replace(/\\/g, '/');
+ if (!normalizedIgnorePattern) {
+ return false;
+ }
+ return normalizedRelativePath === normalizedIgnorePattern || normalizedRelativePath.startsWith(`${normalizedIgnorePattern}/`);
});
},
persistent: true,
diff --git a/packages/bruno-electron/src/ipc/collection.js b/packages/bruno-electron/src/ipc/collection.js
index 595994fc208..183ac678b64 100644
--- a/packages/bruno-electron/src/ipc/collection.js
+++ b/packages/bruno-electron/src/ipc/collection.js
@@ -74,6 +74,7 @@ const interpolateVars = require('./network/interpolate-vars');
const { interpolateString } = require('./network/interpolate-string');
const { getEnvVars, getTreePathFromCollectionToItem, mergeVars, parseBruFileMeta, hydrateRequestWithUuid, transformRequestToSaveToFilesystem } = require('../utils/collection');
const { getProcessEnvVars } = require('../store/process-env');
+const { setBrunoConfig } = require('../store/bruno-config');
const { getOAuth2TokenUsingAuthorizationCode, getOAuth2TokenUsingClientCredentials, getOAuth2TokenUsingPasswordCredentials, getOAuth2TokenUsingImplicitGrant, refreshOauth2Token } = require('../utils/oauth2');
const { getCertsAndProxyConfig } = require('./network/cert-utils');
const collectionWatcher = require('../app/collection-watcher');
@@ -1702,36 +1703,55 @@ const registerRendererEventHandlers = (mainWindow, watcher) => {
}
});
+ const writeBrunoConfig = async (brunoConfig, collectionPath, collectionRoot) => {
+ const transformedBrunoConfig = transformBrunoConfigBeforeSave(_.cloneDeep(brunoConfig));
+ const format = getCollectionFormat(collectionPath);
+
+ if (format === 'bru') {
+ const brunoConfigPath = path.join(collectionPath, 'bruno.json');
+ const content = await stringifyJson(transformedBrunoConfig);
+ await writeFile(brunoConfigPath, content);
+ } else if (format === 'yml') {
+ // opencollection.yml holds both config AND the collection root. If the caller
+ // didn't supply a root (e.g. a config-only update before the tree finished
+ // loading), recover it from disk so request defaults/docs/scripts aren't wiped.
+ let rootToWrite = collectionRoot;
+ if (!rootToWrite) {
+ const ocYmlPath = path.join(collectionPath, 'opencollection.yml');
+ if (fs.existsSync(ocYmlPath)) {
+ const existing = fs.readFileSync(ocYmlPath, 'utf8');
+ rootToWrite = parseCollection(existing, { format }).collectionRoot;
+ }
+ }
+ const content = await stringifyCollection(rootToWrite, transformedBrunoConfig, { format });
+ await writeFile(path.join(collectionPath, 'opencollection.yml'), content);
+ } else {
+ throw new Error(`Invalid collection format: ${format}`);
+ }
+ };
+
ipcMain.handle('renderer:update-bruno-config', async (event, brunoConfig, collectionPath, collectionRoot) => {
try {
- const transformedBrunoConfig = transformBrunoConfigBeforeSave(brunoConfig);
- const format = getCollectionFormat(collectionPath);
+ await writeBrunoConfig(brunoConfig, collectionPath, collectionRoot);
+ } catch (error) {
+ return Promise.reject(error);
+ }
+ });
- if (format === 'bru') {
- const brunoConfigPath = path.join(collectionPath, 'bruno.json');
- const content = await stringifyJson(transformedBrunoConfig);
- await writeFile(brunoConfigPath, content);
- } else if (format === 'yml') {
- // opencollection.yml holds both config AND the collection root. If the caller
- // didn't supply a root (e.g. a config-only update before the tree finished
- // loading), recover it from disk so request defaults/docs/scripts aren't wiped.
- let rootToWrite = collectionRoot;
- if (!rootToWrite) {
- const ocYmlPath = path.join(collectionPath, 'opencollection.yml');
- if (fs.existsSync(ocYmlPath)) {
- try {
- const existing = fs.readFileSync(ocYmlPath, 'utf8');
- rootToWrite = parseCollection(existing, { format }).collectionRoot;
- } catch (e) {
- rootToWrite = collectionRoot;
- }
- }
- }
- const content = await stringifyCollection(rootToWrite, transformedBrunoConfig, { format });
- await writeFile(path.join(collectionPath, 'opencollection.yml'), content);
- } else {
- throw new Error(`Invalid collection format: ${format}`);
- }
+ ipcMain.handle('renderer:ignore-folder', async (event, collectionUid, collectionPath, collectionRoot, brunoConfig, folderPath) => {
+ try {
+ const relativePath = path.relative(collectionPath, folderPath).replace(/\\/g, '/');
+ const existingIgnores = brunoConfig?.ignore || [];
+ const updatedBrunoConfig = {
+ ...brunoConfig,
+ ignore: [...new Set([...existingIgnores, relativePath])]
+ };
+
+ await writeBrunoConfig(updatedBrunoConfig, collectionPath, collectionRoot);
+ setBrunoConfig(collectionUid, updatedBrunoConfig);
+ collectionWatcher.unlinkItemPathInWatcher(folderPath);
+
+ return updatedBrunoConfig;
} catch (error) {
return Promise.reject(error);
}