diff --git a/packages/bruno-app/src/providers/App/useIpcEvents.js b/packages/bruno-app/src/providers/App/useIpcEvents.js index 3b79b7b36..da9c55bf6 100644 --- a/packages/bruno-app/src/providers/App/useIpcEvents.js +++ b/packages/bruno-app/src/providers/App/useIpcEvents.js @@ -177,10 +177,6 @@ const useIpcEvents = () => { } }); - const removeCollectionAlreadyOpenedListener = ipcRenderer.on('main:collection-already-opened', (pathname) => { - toast.success('Collection is already opened'); - }); - const removeDisplayErrorListener = ipcRenderer.on('main:display-error', (error) => { if (typeof error === 'string') { return toast.error(error || 'Something went wrong!'); @@ -290,7 +286,6 @@ const useIpcEvents = () => { removeWorkspaceEnvironmentAddedListener(); removeWorkspaceEnvironmentChangedListener(); removeWorkspaceEnvironmentDeletedListener(); - removeCollectionAlreadyOpenedListener(); removeDisplayErrorListener(); removeScriptEnvUpdateListener(); removeGlobalEnvironmentVariablesUpdateListener(); 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 050594686..2fac806e3 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js @@ -2242,43 +2242,94 @@ export const updateBrunoConfig = (brunoConfig, collectionUid) => (dispatch, getS }; export const openCollectionEvent = (uid, pathname, brunoConfig) => (dispatch, getState) => { - const collection = { - version: '1', - uid: uid, - name: brunoConfig.name, - pathname: pathname, - items: [], - runtimeVariables: {}, - brunoConfig: brunoConfig - }; - const { ipcRenderer } = window; return new Promise((resolve, reject) => { + const state = getState(); + const activeWorkspace = state.workspaces.workspaces.find((w) => w.uid === state.workspaces.activeWorkspaceUid); + + // Check if collection already exists in Redux state + const existingCollection = state.collections.collections.find( + (c) => normalizePath(c.pathname) === normalizePath(pathname) + ); + + // Check if collection is already in the current workspace + const isAlreadyInWorkspace = activeWorkspace?.collections?.some( + (c) => normalizePath(c.path) === normalizePath(pathname) + ); + + // If collection already exists in Redux AND in current workspace, show toast and return + if (existingCollection && isAlreadyInWorkspace) { + toast.success('Collection is already opened'); + resolve(); + return; + } + + // If collection exists in Redux but not in workspace, add to workspace + if (existingCollection) { + if (state.app.sidebarCollapsed) { + dispatch(toggleSidebarCollapse()); + } + + if (activeWorkspace) { + const workspaceCollection = { + name: brunoConfig.name, + path: pathname + }; + + ipcRenderer + .invoke('renderer:add-collection-to-workspace', activeWorkspace.pathname, workspaceCollection) + .then(() => { + toast.success('Collection added to workspace'); + }) + .catch((err) => { + console.error('Failed to add collection to workspace', err); + toast.error('Failed to add collection to workspace'); + }); + } + + resolve(); + return; + } + + // Collection doesn't exist - create it + const collection = { + version: '1', + uid: uid, + name: brunoConfig.name, + pathname: pathname, + items: [], + runtimeVariables: {}, + brunoConfig: brunoConfig + }; + ipcRenderer.invoke('renderer:get-collection-security-config', pathname).then((securityConfig) => { collectionSchema .validate(collection) .then(() => dispatch(_createCollection({ ...collection, securityConfig }))) .then(() => { - const state = getState(); - if (state.app.sidebarCollapsed) { + const currentState = getState(); + if (currentState.app.sidebarCollapsed) { dispatch(toggleSidebarCollapse()); } - const activeWorkspace = state.workspaces.workspaces.find((w) => w.uid === state.workspaces.activeWorkspaceUid); - if (activeWorkspace) { - const isAlreadyInWorkspace = activeWorkspace.collections?.some( + const currentWorkspace = currentState.workspaces.workspaces.find( + (w) => w.uid === currentState.workspaces.activeWorkspaceUid + ); + + if (currentWorkspace) { + const alreadyInWorkspace = currentWorkspace.collections?.some( (c) => normalizePath(c.path) === normalizePath(pathname) ); - if (!isAlreadyInWorkspace) { + if (!alreadyInWorkspace) { const workspaceCollection = { name: brunoConfig.name, path: pathname }; ipcRenderer - .invoke('renderer:add-collection-to-workspace', activeWorkspace.pathname, workspaceCollection) + .invoke('renderer:add-collection-to-workspace', currentWorkspace.pathname, workspaceCollection) .catch((err) => { console.error('Failed to add collection to workspace', err); toast.error('Failed to add collection to workspace'); diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/workspaces/actions.js b/packages/bruno-app/src/providers/ReduxStore/slices/workspaces/actions.js index ca3928357..e56051dd0 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/workspaces/actions.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/workspaces/actions.js @@ -184,8 +184,12 @@ const loadWorkspaceCollectionsForSwitch = async (dispatch, workspace) => { .map((wc) => wc.path) .filter((p) => p && !alreadyOpenCollections.includes(normalizePath(p))); - if (collectionPaths.length > 0) { - await openCollectionsFunction(collectionPaths, updatedWorkspace.pathname); + const uniqueCollectionPaths = [...new Map( + collectionPaths.map((p) => [normalizePath(p), p]) + ).values()]; + + if (uniqueCollectionPaths.length > 0) { + await openCollectionsFunction(uniqueCollectionPaths, updatedWorkspace.pathname); } } @@ -405,9 +409,14 @@ export const workspaceConfigUpdatedEvent = (workspacePath, workspaceUid, workspa .map((workspaceCollection) => workspaceCollection.path) .filter((collectionPath) => collectionPath && !openCollections.includes(normalizePath(collectionPath))); - if (newCollectionPaths.length > 0) { + // Deduplicate paths to prevent "collection already opened" toast + const uniqueNewCollectionPaths = [...new Map( + newCollectionPaths.map((p) => [normalizePath(p), p]) + ).values()]; + + if (uniqueNewCollectionPaths.length > 0) { try { - await dispatch(openMultipleCollections(newCollectionPaths, { workspaceId: workspace.pathname })); + await dispatch(openMultipleCollections(uniqueNewCollectionPaths, { workspaceId: workspace.pathname })); } catch (error) { } } diff --git a/packages/bruno-electron/src/app/collections.js b/packages/bruno-electron/src/app/collections.js index db7c7e3d0..e82bef6a8 100644 --- a/packages/bruno-electron/src/app/collections.js +++ b/packages/bruno-electron/src/app/collections.js @@ -98,26 +98,17 @@ const openCollectionDialog = async (win, watcher) => { }; const openCollection = async (win, watcher, collectionPath, options = {}) => { - if (!watcher.hasWatcher(collectionPath)) { + // If watcher already exists, collection is already loaded in the app + // Just send the collection info so frontend can add to workspace if needed + if (watcher.hasWatcher(collectionPath)) { try { let brunoConfig = await getCollectionConfigFile(collectionPath); const uid = generateUidBasedOnHash(collectionPath); - - // Always ensure node_modules and .git are ignored, regardless of user config - // This prevents infinite loops with symlinked directories (e.g., npm workspaces) - const defaultIgnores = ['node_modules', '.git']; - const userIgnores = brunoConfig.ignore || []; - brunoConfig.ignore = [...new Set([...defaultIgnores, ...userIgnores])]; - - // Transform the config to add existence checks for protobuf files and import paths brunoConfig = await transformBrunoConfigAfterRead(brunoConfig, collectionPath); - const { size, filesCount } = await getCollectionStats(collectionPath); brunoConfig.size = size; brunoConfig.filesCount = filesCount; - win.webContents.send('main:collection-opened', collectionPath, uid, brunoConfig); - ipcMain.emit('main:collection-opened', win, collectionPath, uid, brunoConfig); } catch (err) { if (!options.dontSendDisplayErrors) { win.webContents.send('main:display-error', { @@ -125,16 +116,49 @@ const openCollection = async (win, watcher, collectionPath, options = {}) => { }); } } - } else { - win.webContents.send('main:collection-already-opened', collectionPath); + return; + } + + try { + let brunoConfig = await getCollectionConfigFile(collectionPath); + const uid = generateUidBasedOnHash(collectionPath); + + // Always ensure node_modules and .git are ignored, regardless of user config + const defaultIgnores = ['node_modules', '.git']; + const userIgnores = brunoConfig.ignore || []; + brunoConfig.ignore = [...new Set([...defaultIgnores, ...userIgnores])]; + + brunoConfig = await transformBrunoConfigAfterRead(brunoConfig, collectionPath); + + const { size, filesCount } = await getCollectionStats(collectionPath); + brunoConfig.size = size; + brunoConfig.filesCount = filesCount; + + win.webContents.send('main:collection-opened', collectionPath, uid, brunoConfig); + ipcMain.emit('main:collection-opened', win, collectionPath, uid, brunoConfig); + } catch (err) { + if (!options.dontSendDisplayErrors) { + win.webContents.send('main:display-error', { + message: err.message || 'An error occurred while opening the local collection' + }); + } } }; const openCollectionsByPathname = async (win, watcher, collectionPaths, options = {}) => { + const seenPaths = new Set(); + for (const collectionPath of collectionPaths) { const resolvedPath = path.isAbsolute(collectionPath) ? collectionPath : normalizeAndResolvePath(collectionPath); + + const normalizedPath = path.normalize(resolvedPath); + if (seenPaths.has(normalizedPath)) { + continue; + } + seenPaths.add(normalizedPath); + if (isDirectory(resolvedPath)) { await openCollection(win, watcher, resolvedPath, options); } else { diff --git a/packages/bruno-electron/src/ipc/workspace.js b/packages/bruno-electron/src/ipc/workspace.js index 01704c1cb..34a825191 100644 --- a/packages/bruno-electron/src/ipc/workspace.js +++ b/packages/bruno-electron/src/ipc/workspace.js @@ -4,6 +4,7 @@ const fsExtra = require('fs-extra'); const archiver = require('archiver'); const extractZip = require('extract-zip'); const { ipcMain, dialog } = require('electron'); +const isDev = require('electron-is-dev'); const { createDirectory, sanitizeName } = require('../utils/filesystem'); const yaml = require('js-yaml'); const LastOpenedWorkspaces = require('../store/last-opened-workspaces'); @@ -609,11 +610,22 @@ const registerWorkspaceIpc = (mainWindow, workspaceWatcher) => { } }); + // Guard to prevent main:renderer-ready from running multiple times (only needed in dev mode due to strict mode) + let rendererReadyProcessed = false; + ipcMain.on('main:renderer-ready', async (win) => { + if (isDev && rendererReadyProcessed) { + return; + } + rendererReadyProcessed = true; + try { + let defaultWorkspacePath = null; + const defaultResult = await defaultWorkspaceManager.ensureDefaultWorkspaceExists(); if (defaultResult) { const { workspacePath, workspaceUid } = defaultResult; + defaultWorkspacePath = workspacePath; const workspaceConfig = readWorkspaceConfig(workspacePath); const configForClient = prepareWorkspaceConfigForClient(workspaceConfig, true); @@ -628,6 +640,10 @@ const registerWorkspaceIpc = (mainWindow, workspaceWatcher) => { const invalidPaths = []; for (const workspacePath of workspacePaths) { + if (defaultWorkspacePath && workspacePath === defaultWorkspacePath) { + continue; + } + const workspaceYmlPath = path.join(workspacePath, 'workspace.yml'); if (fs.existsSync(workspaceYmlPath)) { diff --git a/packages/bruno-electron/src/store/default-workspace.js b/packages/bruno-electron/src/store/default-workspace.js index 24534a670..b2bb6a0ad 100644 --- a/packages/bruno-electron/src/store/default-workspace.js +++ b/packages/bruno-electron/src/store/default-workspace.js @@ -164,12 +164,20 @@ class DefaultWorkspaceManager { const lastOpenedCollections = preferencesStore.get('lastOpenedCollections', []); if (lastOpenedCollections && lastOpenedCollections.length > 0) { + const seenPaths = new Set(); const collections = lastOpenedCollections .map((collectionPath) => { if (!collectionPath || typeof collectionPath !== 'string') { return null; } const absolutePath = path.resolve(collectionPath); + const normalizedPath = path.normalize(absolutePath); + + if (seenPaths.has(normalizedPath)) { + return null; + } + seenPaths.add(normalizedPath); + const collectionName = path.basename(absolutePath); return { diff --git a/packages/bruno-electron/src/utils/workspace-config.js b/packages/bruno-electron/src/utils/workspace-config.js index 3ea73b4b5..d6f8f00fb 100644 --- a/packages/bruno-electron/src/utils/workspace-config.js +++ b/packages/bruno-electron/src/utils/workspace-config.js @@ -400,15 +400,28 @@ const getWorkspaceCollections = (workspacePath) => { const config = readWorkspaceConfig(workspacePath); const collections = config.collections || []; - return collections.map((collection) => { - if (collection.path && !path.isAbsolute(collection.path)) { - return { - ...collection, - path: path.resolve(workspacePath, collection.path) - }; - } - return collection; - }); + const seenPaths = new Set(); + return collections + .map((collection) => { + if (collection.path && !path.isAbsolute(collection.path)) { + return { + ...collection, + path: path.resolve(workspacePath, collection.path) + }; + } + return collection; + }) + .filter((collection) => { + if (!collection.path) { + return false; + } + const normalizedPath = path.normalize(collection.path); + if (seenPaths.has(normalizedPath)) { + return false; + } + seenPaths.add(normalizedPath); + return true; + }); }; const getWorkspaceApiSpecs = (workspacePath) => {