+
-
+ {isCollectionLoading ? (
+
+ ) : (
+
+ )}
Bruno Collection
-
Export in Bruno format
+
+ {isCollectionLoading ? 'Loading collection...' : 'Export in Bruno format'}
+
-
+
-
+ {isCollectionLoading ? (
+
+ ) : (
+
+ )}
Postman Collection
-
Export in Postman format
+
+ {isCollectionLoading ? 'Loading collection...' : 'Export in Postman format'}
+
diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js
index 4b787fc3d..0f44b467a 100644
--- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js
+++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js
@@ -61,13 +61,14 @@ const Collection = ({ collection, searchText }) => {
};
const ensureCollectionIsMounted = () => {
- if (collection.mountStatus === 'unmounted') {
- dispatch(mountCollection({
- collectionUid: collection.uid,
- collectionPathname: collection.pathname,
- brunoConfig: collection.brunoConfig
- }));
+ if(collection.mountStatus === 'mounted'){
+ return;
}
+ dispatch(mountCollection({
+ collectionUid: collection.uid,
+ collectionPathname: collection.pathname,
+ brunoConfig: collection.brunoConfig
+ }));
}
const hasSearchText = searchText && searchText?.trim()?.length;
@@ -269,6 +270,7 @@ const Collection = ({ collection, searchText }) => {
className="dropdown-item"
onClick={(e) => {
menuDropdownTippyRef.current.hide();
+ ensureCollectionIsMounted();
handleRun();
}}
>
@@ -287,6 +289,7 @@ const Collection = ({ collection, searchText }) => {
className="dropdown-item"
onClick={(e) => {
menuDropdownTippyRef.current.hide();
+ ensureCollectionIsMounted();
setShowShareCollectionModal(true);
}}
>
diff --git a/packages/bruno-app/src/providers/App/useIpcEvents.js b/packages/bruno-app/src/providers/App/useIpcEvents.js
index 583e31725..34b3e3d5d 100644
--- a/packages/bruno-app/src/providers/App/useIpcEvents.js
+++ b/packages/bruno-app/src/providers/App/useIpcEvents.js
@@ -24,7 +24,7 @@ import toast from 'react-hot-toast';
import { useDispatch } from 'react-redux';
import { isElectron } from 'utils/common/platform';
import { globalEnvironmentsUpdateEvent, updateGlobalEnvironments } from 'providers/ReduxStore/slices/global-environments';
-import { collectionAddOauth2CredentialsByUrl } from 'providers/ReduxStore/slices/collections/index';
+import { collectionAddOauth2CredentialsByUrl, updateCollectionLoadingState } from 'providers/ReduxStore/slices/collections/index';
import { addLog } from 'providers/ReduxStore/slices/logs';
const useIpcEvents = () => {
@@ -179,6 +179,10 @@ const useIpcEvents = () => {
dispatch(collectionAddOauth2CredentialsByUrl(payload));
});
+ const removeCollectionLoadingStateListener = ipcRenderer.on('main:collection-loading-state-updated', (val) => {
+ dispatch(updateCollectionLoadingState(val));
+ });
+
return () => {
removeCollectionTreeUpdateListener();
removeOpenCollectionListener();
@@ -199,6 +203,7 @@ const useIpcEvents = () => {
removeGlobalEnvironmentsUpdatesListener();
removeSnapshotHydrationListener();
removeCollectionOauth2CredentialsUpdatesListener();
+ removeCollectionLoadingStateListener();
};
}, [isElectron]);
};
diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js
index 44db21df4..6805fa32b 100644
--- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js
+++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js
@@ -67,6 +67,12 @@ export const collectionsSlice = createSlice({
}
}
},
+ updateCollectionLoadingState: (state, action) => {
+ const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
+ if (collection) {
+ collection.isLoading = action.payload.isLoading;
+ }
+ },
setCollectionSecurityConfig: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
if (collection) {
@@ -2413,6 +2419,7 @@ export const collectionsSlice = createSlice({
export const {
createCollection,
updateCollectionMountStatus,
+ updateCollectionLoadingState,
setCollectionSecurityConfig,
brunoConfigUpdateEvent,
renameCollection,
diff --git a/packages/bruno-app/src/utils/collections/index.js b/packages/bruno-app/src/utils/collections/index.js
index c5ef12b04..3ea85aa61 100644
--- a/packages/bruno-app/src/utils/collections/index.js
+++ b/packages/bruno-app/src/utils/collections/index.js
@@ -137,6 +137,10 @@ export const findEnvironmentInCollectionByName = (collection, name) => {
};
export const areItemsLoading = (folder) => {
+ if (!folder || folder.isLoading) {
+ return true;
+ }
+
let flattenedItems = flattenItems(folder.items);
return flattenedItems?.reduce((isLoading, i) => {
if (i?.loading) {
diff --git a/packages/bruno-electron/src/app/collection-watcher.js b/packages/bruno-electron/src/app/collection-watcher.js
index 908e04ccf..e5f4503ac 100644
--- a/packages/bruno-electron/src/app/collection-watcher.js
+++ b/packages/bruno-electron/src/app/collection-watcher.js
@@ -166,7 +166,7 @@ const unlinkEnvironmentFile = async (win, pathname, collectionUid) => {
}
};
-const add = async (win, pathname, collectionUid, collectionPath, useWorkerThread) => {
+const add = async (win, pathname, collectionUid, collectionPath, useWorkerThread, watcher) => {
console.log(`watcher add: ${pathname}`);
if (isBrunoConfigFile(pathname, collectionPath)) {
@@ -251,6 +251,8 @@ const add = async (win, pathname, collectionUid, collectionPath, useWorkerThread
}
if (hasBruExtension(pathname)) {
+ watcher.addFileToProcessing(collectionUid, pathname);
+
const file = {
meta: {
collectionUid,
@@ -270,8 +272,11 @@ const add = async (win, pathname, collectionUid, collectionPath, useWorkerThread
file.size = sizeInMB(fileStats?.size);
hydrateRequestWithUuid(file.data, pathname);
win.webContents.send('main:collection-tree-updated', 'addFile', file);
+
} catch (error) {
console.error(error);
+ } finally {
+ watcher.markFileAsProcessed(win, collectionUid, pathname);
}
return;
}
@@ -320,6 +325,8 @@ const add = async (win, pathname, collectionUid, collectionPath, useWorkerThread
file.size = sizeInMB(fileStats?.size);
hydrateRequestWithUuid(file.data, pathname);
win.webContents.send('main:collection-tree-updated', 'addFile', file);
+ } finally {
+ watcher.markFileAsProcessed(win, collectionUid, pathname);
}
}
};
@@ -510,7 +517,10 @@ const unlinkDir = async (win, pathname, collectionUid, collectionPath) => {
win.webContents.send('main:collection-tree-updated', 'unlinkDir', directory);
};
-const onWatcherSetupComplete = (win, watchPath) => {
+const onWatcherSetupComplete = (win, watchPath, collectionUid, watcher) => {
+ // Mark discovery as complete
+ watcher.completeCollectionDiscovery(win, collectionUid);
+
const UiStateSnapshotStore = new UiStateSnapshot();
const collectionsSnapshotState = UiStateSnapshotStore.getCollections();
const collectionSnapshotState = collectionsSnapshotState?.find(c => c?.pathname == watchPath);
@@ -520,6 +530,75 @@ const onWatcherSetupComplete = (win, watchPath) => {
class CollectionWatcher {
constructor() {
this.watchers = {};
+ this.loadingStates = {};
+ }
+
+ // Initialize loading state tracking for a collection
+ initializeLoadingState(collectionUid) {
+ if (!this.loadingStates[collectionUid]) {
+ this.loadingStates[collectionUid] = {
+ isDiscovering: false, // Initial discovery phase
+ isProcessing: false, // Processing discovered files
+ pendingFiles: new Set(), // Files that need processing
+ };
+ }
+ }
+
+ startCollectionDiscovery(win, collectionUid) {
+ this.initializeLoadingState(collectionUid);
+ const state = this.loadingStates[collectionUid];
+
+ state.isDiscovering = true;
+ state.pendingFiles.clear();
+
+ win.webContents.send('main:collection-loading-state-updated', {
+ collectionUid,
+ isLoading: true
+ });
+ }
+
+ addFileToProcessing(collectionUid, filepath) {
+ this.initializeLoadingState(collectionUid);
+ const state = this.loadingStates[collectionUid];
+ state.pendingFiles.add(filepath);
+ }
+
+ markFileAsProcessed(win, collectionUid, filepath) {
+ if (!this.loadingStates[collectionUid]) return;
+
+ const state = this.loadingStates[collectionUid];
+ state.pendingFiles.delete(filepath);
+
+ // If discovery is complete and no pending files, mark as not loading
+ if (!state.isDiscovering && state.pendingFiles.size === 0 && state.isProcessing) {
+ state.isProcessing = false;
+ win.webContents.send('main:collection-loading-state-updated', {
+ collectionUid,
+ isLoading: false
+ });
+ }
+ }
+
+ completeCollectionDiscovery(win, collectionUid) {
+ if (!this.loadingStates[collectionUid]) return;
+
+ const state = this.loadingStates[collectionUid];
+ state.isDiscovering = false;
+
+ // If there are pending files, start processing phase
+ if (state.pendingFiles.size > 0) {
+ state.isProcessing = true;
+ } else {
+ // No pending files, collection is fully loaded
+ win.webContents.send('main:collection-loading-state-updated', {
+ collectionUid,
+ isLoading: false
+ });
+ }
+ }
+
+ cleanupLoadingState(collectionUid) {
+ delete this.loadingStates[collectionUid];
}
addWatcher(win, watchPath, collectionUid, brunoConfig, forcePolling = false, useWorkerThread) {
@@ -527,6 +606,10 @@ class CollectionWatcher {
this.watchers[watchPath].close();
}
+ this.initializeLoadingState(collectionUid);
+
+ this.startCollectionDiscovery(win, collectionUid);
+
const ignores = brunoConfig?.ignore || [];
setTimeout(() => {
const watcher = chokidar.watch(watchPath, {
@@ -552,8 +635,8 @@ class CollectionWatcher {
let startedNewWatcher = false;
watcher
- .on('ready', () => onWatcherSetupComplete(win, watchPath))
- .on('add', (pathname) => add(win, pathname, collectionUid, watchPath, useWorkerThread))
+ .on('ready', () => onWatcherSetupComplete(win, watchPath, collectionUid, this))
+ .on('add', (pathname) => add(win, pathname, collectionUid, watchPath, useWorkerThread, this))
.on('addDir', (pathname) => addDirectory(win, pathname, collectionUid, watchPath))
.on('change', (pathname) => change(win, pathname, collectionUid, watchPath))
.on('unlink', (pathname) => unlink(win, pathname, collectionUid, watchPath))
@@ -588,11 +671,15 @@ class CollectionWatcher {
return this.watchers[watchPath];
}
- removeWatcher(watchPath, win) {
+ removeWatcher(watchPath, win, collectionUid) {
if (this.watchers[watchPath]) {
this.watchers[watchPath].close();
this.watchers[watchPath] = null;
}
+
+ if (collectionUid) {
+ this.cleanupLoadingState(collectionUid);
+ }
}
getWatcherByItemPath(itemPath) {
diff --git a/packages/bruno-electron/src/ipc/collection.js b/packages/bruno-electron/src/ipc/collection.js
index 089507334..8ca5ac2f9 100644
--- a/packages/bruno-electron/src/ipc/collection.js
+++ b/packages/bruno-electron/src/ipc/collection.js
@@ -593,10 +593,10 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
}
});
- ipcMain.handle('renderer:remove-collection', async (event, collectionPath) => {
+ ipcMain.handle('renderer:remove-collection', async (event, collectionPath, collectionUid) => {
if (watcher && mainWindow) {
console.log(`watcher stopWatching: ${collectionPath}`);
- watcher.removeWatcher(collectionPath, mainWindow);
+ watcher.removeWatcher(collectionPath, mainWindow, collectionUid);
lastOpenedCollections.remove(collectionPath);
}
});