Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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 (
<Modal
size="md"
title="Ignore Folder"
confirmText="Ignore"
handleConfirm={onConfirm}
handleCancel={onClose}
>
Ignoring <span className="font-medium">{item.name}</span> will hide it from this
{' '}
{isYamlCollection ? 'opencollection (YAML)' : 'Bruno (JSON)'}
{' '}
collection by adding it to the
{' '}
<span className="font-medium">ignore</span>
{' '}
list in
{' '}
<span className="font-medium">{configFileName}</span>
. The folder and its files are not deleted. To restore it later, remove the entry from the
{' '}
<span className="font-medium">ignore</span>
{' '}
list in
{' '}
<span className="font-medium">{configFileName}</span>
.
</Modal>
);
};

export default IgnoreCollectionItem;
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand Down Expand Up @@ -93,6 +95,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);
Expand Down Expand Up @@ -457,6 +460,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({
Expand Down Expand Up @@ -654,6 +666,9 @@ const CollectionItem = ({ item, collectionUid, collectionPathname, searchText })
{deleteItemModalOpen && (
<DeleteCollectionItem item={item} collectionUid={collectionUid} onClose={() => setDeleteItemModalOpen(false)} />
)}
{ignoreItemModalOpen && (
<IgnoreCollectionItem item={item} collectionUid={collectionUid} onClose={() => setIgnoreItemModalOpen(false)} />
)}
{newRequestModalOpen && (
<NewRequest item={item} collectionUid={collectionUid} onClose={() => setNewRequestModalOpen(false)} />
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ import {
sortCollections as _sortCollections,
updateCollectionMountStatus,
moveCollection,
deleteItem as _deleteItemFromState,
brunoConfigUpdateEvent as _brunoConfigUpdateEvent,
workspaceEnvUpdateEvent,
requestCancelled,
resetRunResults,
Expand Down Expand Up @@ -2744,6 +2746,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,
Expand Down
14 changes: 10 additions & 4 deletions packages/bruno-electron/src/app/collection-watcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -811,8 +811,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, {
Expand All @@ -834,8 +832,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,
Expand Down
76 changes: 50 additions & 26 deletions packages/bruno-electron/src/ipc/collection.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -1708,36 +1709,59 @@ const registerRendererEventHandlers = (mainWindow, watcher) => {
}
});

ipcMain.handle('renderer:update-bruno-config', async (event, brunoConfig, collectionPath, collectionRoot) => {
try {
const transformedBrunoConfig = transformBrunoConfigBeforeSave(brunoConfig);
const format = getCollectionFormat(collectionPath);
const writeBrunoConfig = async (brunoConfig, collectionPath, collectionRoot) => {
const transformedBrunoConfig = transformBrunoConfigBeforeSave(brunoConfig);
const format = getCollectionFormat(collectionPath);
Comment thread
coderabbitai[bot] marked this conversation as resolved.

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;
}
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;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}
const content = await stringifyCollection(rootToWrite, transformedBrunoConfig, { format });
await writeFile(path.join(collectionPath, 'opencollection.yml'), content);
} else {
throw new Error(`Invalid collection format: ${format}`);
}
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 {
await writeBrunoConfig(brunoConfig, collectionPath, collectionRoot);
} catch (error) {
return Promise.reject(error);
}
});

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);
}
Expand Down
Loading