Conversation
WalkthroughAdds multi-workspace support across renderer and main processes: Redux workspaces slice, workspace-aware collection/environment flows, Electron IPC and workspace watcher, WorkspaceHome UI replacing Welcome, many new workspace UI components, theme tokens, and extensive test updates for new modal/dropdown flows. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant Renderer as App (renderer)
participant Main as Main Process
participant FS as File System
User->>Renderer: createWorkspace (UI)
Renderer->>Main: renderer:create-workspace (IPC)
Main->>FS: create workspace dir & write workspace.yml
Main->>Main: update last-opened store
Main-->>Renderer: return workspace config
Renderer->>Renderer: dispatch createWorkspaceAction -> update Redux
Renderer->>User: show success & switch to workspace
sequenceDiagram
participant User
participant Renderer as App (renderer)
participant Main as Main Process
participant FS as File System
User->>Renderer: createCollection (with workspaceId)
Renderer->>Main: renderer:create-collection (IPC, workspacePath)
Main->>FS: create collection folder & files
Main->>FS: update workspace.yml (add collection)
Main-->>Renderer: return collectionPath
Renderer->>Renderer: dispatch createCollection -> add to workspace in Redux
Renderer->>User: render collection in WorkspaceHome/Sidebar
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes
Possibly related PRs
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Comment |
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 14
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/bruno-electron/src/ipc/preferences.js (1)
24-24: Minor typo: "environements".Line 24 has a typo in the error message.
- console.error("Error occured while fetching global environements!"); + console.error("Error occurred while fetching global environments!");
🟡 Minor comments (16)
packages/bruno-electron/src/store/default-workspace.js-192-194 (1)
192-194: Silent migration failure may hide critical issues.The catch block logs the error but doesn't propagate it. If migration fails, the workspace will be created without the user's collections and environments, potentially causing confusion. Consider at minimum warning the user or tracking partial migration state.
packages/bruno-app/src/components/WorkspaceOverview/WorkspaceEnvironments/EnvironmentList/ConfirmSwitchEnv.js-6-37 (1)
6-37: Consider the user experience for unsaved changes.This modal warns about unsaved changes but only provides a "Close" button. Typically, unsaved changes modals offer the user choices like "Save and Continue", "Discard Changes", or "Cancel". The current implementation only allows dismissing the warning without addressing the unsaved changes, which may lead to accidental data loss.
Consider revising the modal to include actions that let the user:
- Save the changes before switching
- Discard the changes and proceed
- Cancel and stay on the current environment
packages/bruno-app/src/components/WorkspaceOverview/WorkspaceEnvironments/CopyEnvironment/index.js-15-23 (1)
15-23: Handle long environment names to prevent validation errors.If the base environment name is 44 characters or longer, appending " - Copy" (7 chars) will exceed the 50-character max and cause an immediate validation error when the modal opens. Consider truncating the base name or using a smarter default.
initialValues: { - name: environment.name + ' - Copy' + name: (environment.name.length > 43 + ? environment.name.substring(0, 43) + : environment.name) + ' - Copy' },packages/bruno-app/src/components/Sidebar/TitleBar/CloseWorkspace/index.js-26-28 (1)
26-28: Duplicate success toast.Per the
closeWorkspaceActionimplementation (see relevant snippet), it already callstoast.success('Workspace closed successfully')on success. Line 27 adds anothertoast.success('Workspace closed'), resulting in two toast notifications.Remove the redundant toast:
await dispatch(closeWorkspaceAction(workspace.uid)); - toast.success('Workspace closed'); onClose();packages/bruno-app/src/components/WorkspaceSidebar/CreateWorkspace/index.js-89-107 (1)
89-107: Accessibility issue:htmlFordoesn't match inputid.The label's
htmlFor="workspaceName"doesn't match the input'sid="workspace-name". This breaks the label-input association for accessibility tools.- <label htmlFor="workspaceName" className="block font-semibold mb-2"> + <label htmlFor="workspace-name" className="block font-semibold mb-2">packages/bruno-app/src/components/WorkspaceSidebar/CreateWorkspace/index.js-111-117 (1)
111-117: Same accessibility issue for location label.The
htmlFor="workspaceLocation"doesn't matchid="workspace-location".- <label htmlFor="workspaceLocation" className="block font-semibold mb-2"> + <label htmlFor="workspace-location" className="block font-semibold mb-2">packages/bruno-app/src/components/WorkspaceOverview/WorkspaceEnvironments/RenameEnvironment/index.js-76-91 (1)
76-91: MismatchedhtmlForandidattributes.The label's
htmlFor="name"doesn't match the input'sid="environment-name", breaking the label-input association for accessibility.- <label htmlFor="name" className="block font-semibold"> + <label htmlFor="environment-name" className="block font-semibold"> Environment Name </label>packages/bruno-app/src/utils/workspaces/index.js-53-62 (1)
53-62: Inconsistent null-safety forpreferences.workspaces.Line 57 spreads
preferences.workspaceswithout a guard, but line 67 (else branch) uses(preferences?.workspaces || {}). Ifpreferences.workspacesis undefined and the workspace is already pinned, this will throw.if (isPinned) { return { ...preferences, workspaces: { - ...preferences.workspaces, + ...(preferences?.workspaces || {}), pinnedWorkspaceUids: pinnedUids.filter((uid) => uid !== workspaceUid), pinnedOrder: pinnedOrder.filter((uid) => uid !== workspaceUid), unpinnedOrder: [...unpinnedOrder, workspaceUid] } };packages/bruno-app/src/components/WorkspaceOverview/WorkspaceEnvironments/EnvironmentList/index.js-261-265 (1)
261-265:handleConfirmSwitchappears incomplete.When
saveChangesistrue, nothing happens. If this is intentional (e.g., save logic handled elsewhere), add a comment. Otherwise, implement the save path.const handleConfirmSwitch = (saveChanges) => { - if (!saveChanges) { - setSwitchEnvConfirmClose(false); - } + setSwitchEnvConfirmClose(false); + if (saveChanges) { + // TODO: Implement save before switching + } };packages/bruno-app/src/components/Sidebar/TitleBar/index.js-83-85 (1)
83-85: Missing error handling for non-workspace import.The workspace branch has
.catch()error handling, but the non-workspaceimportCollectiondispatch does not. Add consistent error handling:} else { - dispatch(importCollection(collectionsToImport, collectionLocation)); + dispatch(importCollection(collectionsToImport, collectionLocation)) + .catch((err) => { + console.error(err); + toast.error('An error occurred while importing the collection'); + }); }packages/bruno-electron/src/ipc/global-environments.js-35-46 (1)
35-46: Inconsistent return values between workspace and non-workspace paths.When using
workspaceEnvironmentsManager, results are returned to the caller. However, when usingglobalEnvironmentsStore, no value is returned (lines 41, 54, 67, 80). This inconsistency may cause issues if the renderer expects a response.Consider returning a consistent result:
- globalEnvironmentsStore.saveGlobalEnvironment({ environmentUid, variables }); + globalEnvironmentsStore.saveGlobalEnvironment({ environmentUid, variables }); + return true;packages/bruno-electron/src/store/workspace-environments.js-294-317 (1)
294-317: Missing secrets cleanup on environment deletion.When deleting an environment, associated secrets in
environmentSecretsStoreare not removed. This could leave orphaned secret data. Also, prefer asyncfs.promises.unlink:async deleteGlobalEnvironment(workspacePath, { environmentUid }) { try { // ... const envFile = this.findEnvironmentFileByUid(workspacePath, environmentUid); // ... - fs.unlinkSync(envFile.filePath); + await fs.promises.unlink(envFile.filePath); + + // Clean up any associated secrets + const env = { name: envFile.name }; + environmentSecretsStore.deleteEnvSecrets?.(workspacePath, env);packages/bruno-app/src/providers/ReduxStore/slices/workspaces/actions.js-17-17 (1)
17-17: Module-levelipcRendereraccess may fail in non-browser contexts.Extracting
ipcRendererat module level (line 17) will throw ifwindowis undefined (e.g., during SSR or testing). Consider lazy access:-const { ipcRenderer } = window; +const getIpcRenderer = () => window.ipcRenderer;Or access
window.ipcRendererdirectly in each function as done in other files.Committable suggestion skipped: line range outside the PR's diff.
packages/bruno-app/src/providers/ReduxStore/slices/workspaces/actions.js-404-413 (1)
404-413:action.namewill be empty for arrow functions.When calling
handleWorkspaceActionwith an inline arrow function (line 425),action.nameis empty, resulting in a poor error message like "Failed to workspace":-const handleWorkspaceAction = async (action, workspaceUid, ...args) => { +const handleWorkspaceAction = async (action, actionName, ...args) => { try { - await action(workspaceUid, ...args); + await action(...args); return true; } catch (error) { - const actionName = action.name.replace('renderer:', '').replace('-', ' '); toast.error(error.message || `Failed to ${actionName} workspace`); throw error; } }; // Usage: -await handleWorkspaceAction((...args) => ipcRenderer.invoke('renderer:rename-workspace', ...args), - workspace.pathname, - newName); +await handleWorkspaceAction( + (...args) => ipcRenderer.invoke('renderer:rename-workspace', ...args), + 'rename', + workspace.pathname, + newName +);Committable suggestion skipped: line range outside the PR's diff.
packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js-2327-2337 (1)
2327-2337: Error message swallowed in openMultipleCollections.The catch block rejects without the error, losing diagnostic information:
.catch((err) => { - reject(); + reject(err); });packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js-2519-2560 (1)
2519-2560: Multiple async promise executors in utility functions.Lines 2522, 2532, 2541, 2551 all use async promise executors unnecessarily. These can be simplified:
export const loadRequestViaWorker = ({ collectionUid, pathname }) => - (dispatch, getState) => { - return new Promise(async (resolve, reject) => { - const { ipcRenderer } = window; - ipcRenderer.invoke('renderer:load-request-via-worker', { collectionUid, pathname }).then(resolve).catch(reject); - }); + () => { + const { ipcRenderer } = window; + return ipcRenderer.invoke('renderer:load-request-via-worker', { collectionUid, pathname }); };The
asynckeyword in the executor serves no purpose since you're already using.then()/.catch().Committable suggestion skipped: line range outside the PR's diff.
🧹 Nitpick comments (64)
packages/bruno-electron/src/app/onboarding.js (2)
27-30: Update JSDoc to reflect parameter changes.The function signature no longer includes
lastOpenedCollections, but the JSDoc comment doesn't document the parameters at all. Consider adding parameter documentation for clarity.Apply this diff to enhance the JSDoc:
/** * Import sample collection for new users + * @param {string} collectionLocation - Directory path where the collection will be imported + * @param {Object} mainWindow - Electron BrowserWindow instance for IPC communication + * @returns {Promise<{collectionPath: string, uid: string, brunoConfig: Object}>} */ async function importSampleCollection(collectionLocation, mainWindow) {
82-83: Simplify comments for better readability.The comments are overly verbose and spread across two lines. The explanation can be more concise.
Apply this diff to condense the comment:
- // lastOpenedCollections is still used here to check for existing collections during migration - const collections = lastOpenedCollections ? lastOpenedCollections.getAll() : []; + // Check for existing collections (supports users migrating from older versions) + const collections = lastOpenedCollections ? lastOpenedCollections.getAll() : [];packages/bruno-app/src/themes/dark.js (1)
151-182: NewlistItemandworkspacetokens look consistent; consider future color dedupingThe new
listItemandworkspacebranches are well‑named, mirror existing sidebar semantics, and fit the current dark palette (reusing#2A2D2F,#3D3D3D, and the existing accent system). That should make wiring up the new workspace surfaces straightforward.If you touch theming again later, it might be worth centralizing some of the repeated hexes (
#2A2D2F,#3D3D3D,#444, etc.) intocolorsto reduce magic values and keep light/dark parity easier to manage, but this is non‑blocking for this PR.packages/bruno-app/src/themes/light.js (1)
154-185: Good light/dark parity forlistItemandworkspace; optional follow‑up on palette reuseThe added
listItemandworkspacekeys mirror the dark theme structure and reuse the existing orange accent + gray scale, which keeps workspace styling coherent across modes and should make component code simple (same token paths per theme).Similar to the dark theme, if you later refactor theming it could help to pull
#e7e7e7/#f3f3f3/#fbfbfbinto the sharedcolorsmap so workspace and sidebar surfaces stay in lockstep, but this is a nice‑to‑have, not a blocker.packages/bruno-electron/src/store/default-workspace.js (2)
139-140: Consider hoistingrequirestatements to module level.Inline
requirecalls inside methods add overhead on each invocation. Sinceelectron-storeis already used elsewhere in the project via imported stores, consider either reusing those patterns or moving the require to the top of the file.
183-184: Inconsistent store access pattern.Line 183 creates a new
Store({ name: 'global-environments' })to clear it, but you've already importedglobalEnvironmentsStoreat the top. Consider using a clear method on the imported store for consistency, or verify the imported store exposes this capability.- const globalEnvStore = new Store({ name: 'global-environments' }); - globalEnvStore.clear(); + globalEnvironmentsStore.clear();packages/bruno-electron/src/app/collections.js (1)
135-146: Consider parallel processing for better performance.The sequential
awaitin the loop means collections are opened one at a time. For multiple collections, this could be noticeably slow. Consider usingPromise.allSettled()to open collections in parallel, similar to the pattern used inopenCollectionDialogat lines 77-91.Additionally, errors are only logged to console without user notification. Consider collecting errors and notifying the user via
win.webContents.send('main:display-error', ...)as done inopenCollectionDialog.Example refactor:
const openCollectionsByPathname = async (win, watcher, collectionPaths, options = {}) => { - for (const collectionPath of collectionPaths) { + const promises = collectionPaths.map((collectionPath) => { const resolvedPath = path.isAbsolute(collectionPath) ? collectionPath : normalizeAndResolvePath(collectionPath); if (isDirectory(resolvedPath)) { - await openCollection(win, watcher, resolvedPath, options); + return openCollection(win, watcher, resolvedPath, options).catch((err) => { + console.error(`Cannot open collection at "${resolvedPath}":`, err.message); + return { error: err, path: resolvedPath }; + }); } else { console.error(`Cannot open unknown folder: "${resolvedPath}"`); + return { error: new Error('Not a directory'), path: resolvedPath }; } - } + }); + const results = await Promise.allSettled(promises); + const errors = results.filter((r) => r.status === 'rejected' || r.value?.error); + if (errors.length > 0 && !options.dontSendDisplayErrors) { + const paths = errors.map((e) => e.value?.path || 'unknown').join(', '); + win.webContents.send('main:display-error', `Some collections could not be opened: ${paths}`); + } };packages/bruno-app/src/components/WorkspaceOverview/WorkspaceEnvironments/EnvironmentList/ConfirmSwitchEnv.js (1)
27-33: Remove the empty div.The empty
<div>at line 33 serves no purpose and can be removed for cleaner code.<div className="flex justify-between mt-6"> <div> <button className="btn btn-sm btn-danger" onClick={onCancel}> Close </button> </div> - <div></div> </div>packages/bruno-app/src/components/WorkspaceOverview/WorkspaceEnvironments/CopyEnvironment/index.js (1)
24-34: Update toast message to reflect copying action.The success toast says "Environment created!" but since this is a copy operation, it should say "Environment copied!" for clarity.
.then(() => { - toast.success('Environment created!'); + toast.success('Environment copied!'); onClose(); })packages/bruno-app/src/providers/ReduxStore/slices/workspaces/index.js (1)
39-45: Consider logging when workspace is not found.The
updateWorkspacereducer silently does nothing if the workspace doesn't exist. While this may be intentional for resilience, consider adding a console warning to aid debugging, especially during development.updateWorkspace: (state, action) => { const { uid, ...updates } = action.payload; const workspace = state.workspaces.find((w) => w.uid === uid); if (workspace) { Object.assign(workspace, updates); + } else { + console.warn(`Attempted to update non-existent workspace: ${uid}`); } },packages/bruno-app/src/components/Sidebar/TitleBar/StyledWrapper.js (2)
127-130: Simplify theme fallback chain.The deep fallback chain with a hardcoded color makes theme maintenance difficult. Consider using a single theme path or defining a default in the theme definition.
.check-icon { - color: ${(props) => props.theme.workspace?.accent || props.theme.colors?.text?.yellow || '#f0c674'}; + color: ${(props) => props.theme.workspace.accent}; flex-shrink: 0; }
132-155: Simplify theme fallback chains for pin button.Multiple fallback chains with hardcoded colors on lines 143-144 and 152-153 reduce maintainability.
.pin-btn { display: flex; align-items: center; justify-content: center; width: 22px; height: 22px; padding: 0; border: none; background: transparent; border-radius: 4px; cursor: pointer; - color: ${(props) => props.theme.dropdown?.mutedText || props.theme.text?.muted || '#888'}; + color: ${(props) => props.theme.text.muted}; transition: background 0.15s ease, color 0.15s ease, opacity 0.15s ease; opacity: 0; &.pinned { opacity: 1; } &:hover { - background: ${(props) => props.theme.dropdown?.hoverBg || props.theme.sidebar?.collection?.item?.hoverBg}; - color: ${(props) => props.theme.dropdown?.mutedText || props.theme.text?.muted || '#888'}; + background: ${(props) => props.theme.sidebar.collection.item.hoverBg}; + color: ${(props) => props.theme.text.muted}; } }packages/bruno-app/src/components/WorkspaceOverview/WorkspaceEnvironments/DeleteEnvironment/index.js (1)
11-18: Consider logging error details and adding loading state.While the error handling structure is correct, the error details are discarded on line 17. Logging these details would aid debugging. Additionally, there's no loading/disabled state during the deletion operation.
const onConfirm = () => { dispatch(deleteGlobalEnvironment({ environmentUid: environment.uid })) .then(() => { toast.success('Environment deleted successfully'); onClose(); }) - .catch(() => toast.error('An error occurred while deleting the environment')); + .catch((error) => { + console.error('Error deleting environment:', error); + toast.error(error?.message || 'An error occurred while deleting the environment'); + }); };packages/bruno-app/src/components/WorkspaceOverview/WorkspaceEnvironments/DeleteEnvironment/StyledWrapper.js (1)
3-13: Investigate why!importantis required.Lines 6, 7, and 10 use
!importantflags, which typically indicates specificity conflicts. Theborder: inherit !importantis especially unusual sinceinheritrarely requires forcing. Consider reviewing the Modal component's default styles to apply these rules without!important.packages/bruno-app/src/components/WorkspaceOverview/StyledWrapper.js (1)
96-112: Inconsistent theming approach for tab colors.Line 99 uses a CSS variable
var(--color-tab-inactive)while the rest of the file usesprops.theme.*. This inconsistency makes theme management harder. Consider using${(props) => props.theme.tabs.inactive.color}for consistency..tab-item { position: relative; cursor: pointer; - color: var(--color-tab-inactive); + color: ${(props) => props.theme.tabs.inactive.color}; border-bottom: 2px solid transparent; transition: all 0.15s ease; &:hover { color: ${(props) => props.theme.text.primary}; border-bottom-color: ${(props) => props.theme.colors.border}; } &.active { border-bottom-color: ${(props) => props.theme.colors.text.yellow}; color: ${(props) => props.theme.tabs.active.color}; } }packages/bruno-electron/src/ipc/preferences.js (1)
5-5: Unusedwatcherparameter.The
watcherparameter is declared but never used within the function body. If it's no longer needed after removing the lastOpenedCollections logic, consider removing it from the signature.-const registerPreferencesIpc = (mainWindow, watcher) => { +const registerPreferencesIpc = (mainWindow) => {Note: This would require updating the call site in
packages/bruno-electron/src/index.js.packages/bruno-app/src/components/WorkspaceOverview/WorkspaceDocs/index.js (1)
84-92: Unusual height styling.The
h-[1px]combined withmin-h-[500px](line 85) is a peculiar pattern. The element's natural height would be 1px, only expanding due to min-height. This could cause layout issues if content exceeds 500px.Consider using
h-fullormin-h-fullinstead for more predictable behavior:- <div className="h-[1px] min-h-[500px]"> + <div className="h-full min-h-[500px]">packages/bruno-app/src/components/WorkspaceOverview/WorkspaceCollections/StyledWrapper.js (1)
1-271: Well-structured styled-component with good use of modern CSS features.The CSS organization is clean with clear class naming and semantic structure. The
:has()selectors elegantly handle conditional grid layouts based on git column presence.Note: The hardcoded hex colors (
#3B82F6,#10B981,#EF4444, etc.) appear consistent with patterns elsewhere in the codebase. If theme color variables exist for these values, consider extracting them for maintainability, though the current approach is acceptable.The
:has()selector is supported in Chromium 105+, and modern Electron versions (24+) ship with Chromium 112+, so compatibility should not be a concern.packages/bruno-app/src/components/WorkspaceOverview/WorkspaceEnvironments/EnvironmentList/StyledWrapper.js (1)
190-267: Duplicate styling between.rename-containerand&.creatingblocks.The
.environment-name-inputand.inline-actionsstyles are defined identically in both.rename-container(lines 195-213) and&.creating(lines 217-235). Consider consolidating these into a shared selector.packages/bruno-app/src/components/WorkspaceOverview/WorkspaceEnvironments/ImportEnvironment/index.js (1)
18-19: Simplify filter condition.The parentheses around the filter callback condition are inconsistent with the coding guidelines (single-line conditions don't need wrapping).
- .filter((env) => - env.name && env.name !== 'undefined') + .filter((env) => env.name && env.name !== 'undefined')packages/bruno-app/src/components/WorkspaceSidebar/CreateWorkspace/index.js (1)
68-72: useEffect dependency array should be empty.The
inputRefis a ref object that doesn't change between renders. Including it in the dependency array is unnecessary. Use an empty array for a mount-only effect.useEffect(() => { if (inputRef && inputRef.current) { inputRef.current.focus(); } - }, [inputRef]); + }, []);packages/bruno-app/src/components/WorkspaceOverview/index.js (3)
41-54: Stale closure risk in click-outside handler.The
handleCancelWorkspaceRenamefunction is referenced inside the effect but not included in the dependency array. If the handler's closure captures stale state in the future, this could cause bugs.useEffect(() => { if (!isRenamingWorkspace) return; const handleClickOutside = (event) => { if (workspaceRenameContainerRef.current && !workspaceRenameContainerRef.current.contains(event.target)) { handleCancelWorkspaceRename(); } }; document.addEventListener('mousedown', handleClickOutside); return () => { document.removeEventListener('mousedown', handleClickOutside); }; - }, [isRenamingWorkspace]); + }, [isRenamingWorkspace, handleCancelWorkspaceRename]);Wrap
handleCancelWorkspaceRenameinuseCallbackto stabilize its reference if you add it to the dependency array.
123-137: Redundant validation check.Line 128-130 (
name.length < 1) is redundant since the check at line 124 already handles empty or whitespace-only strings.const validateWorkspaceName = (name) => { if (!name || name.trim() === '') { return 'Name is required'; } - if (name.length < 1) { - return 'Must be at least 1 character'; - } - if (name.length > 255) { return 'Must be 255 characters or less'; } return null; };
184-186: Dead code: duplicate null check.This null check is unreachable since the component already returns
nullat lines 56-58 whenactiveWorkspaceis falsy.- if (!activeWorkspace) { - return null; - } - const tabs = [packages/bruno-app/src/components/WorkspaceOverview/WorkspaceEnvironments/RenameEnvironment/index.js (3)
7-10: Consolidate duplicate imports.
useDispatchanduseSelectorcan be imported in a single statement fromreact-redux.-import { useDispatch } from 'react-redux'; import { renameGlobalEnvironment } from 'providers/ReduxStore/slices/global-environments'; import { validateName, validateNameError } from 'utils/common/regex'; -import { useSelector } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux';
39-52: Silent no-op when name is unchanged.If the user clicks "Rename" without changing the name, the form silently does nothing and the modal stays open. Consider closing the modal or providing feedback.
onSubmit: (values) => { if (values.name === environment.name) { + onClose(); return; }
55-59: Simplify useEffect dependency.The
inputRefreference is stable and won't change. The dependency array can be empty since the effect only needs to run once on mount.useEffect(() => { if (inputRef && inputRef.current) { inputRef.current.focus(); } - }, [inputRef]); + }, []);packages/bruno-electron/src/store/last-opened-workspaces.js (1)
25-31: Redundant property assignment.
name: workspaceConfig.nameon line 28 is redundant sinceworkspaceConfigis already spread. Only override if you intend to transform the value.const workspaceEntry = { ...workspaceConfig, uid: workspaceUid, - name: workspaceConfig.name, lastOpened: new Date().toISOString(), pathname: workspacePath };packages/bruno-app/src/components/WorkspaceOverview/WorkspaceCollections/index.js (2)
235-260: Inconsistent terminology: "Delete" vs "Remove".The modal title and confirm button say "Delete Collection" but the action button (line 335) says "Remove from workspace". For git-backed collections, the action only removes the reference, not the files. Consider aligning the terminology or making it context-dependent.
The explanation text at lines 249-258 is good, but the modal title/button could be more precise:
<Modal size="sm" - title="Delete Collection" + title="Remove Collection" handleCancel={() => setCollectionToRemove(null)} handleConfirm={confirmRemoveCollection} - confirmText="Delete Collection" + confirmText="Remove" cancelText="Cancel"
48-91: Unnecessary dependency in useMemo.
workspace.pathnameis included in the dependency array but isn't used in the memo computation. This could cause unnecessary recalculations.- }, [workspace.collections, collections, workspace.pathname]); + }, [workspace.collections, collections]);packages/bruno-app/src/components/WorkspaceOverview/WorkspaceEnvironments/CreateEnvironment/index.js (3)
51-55: Unnecessary dependency in useEffect.The
inputRefobject reference is stable across renders. Including it in the dependency array is harmless but misleading. An empty dependency array[]is more idiomatic for "run once on mount."useEffect(() => { if (inputRef && inputRef.current) { inputRef.current.focus(); } - }, [inputRef]); + }, []);
39-45: Callback invoked after modal closes—potential race condition.
onEnvironmentCreatedis called afteronClose(). IfonCloseunmounts this component or resets parent state, the callback may execute on stale context. Consider calling the callback before closing, or consolidating into a single parent handler.dispatch(addGlobalEnvironment({ name: values.name })) .then(() => { toast.success('Environment created!'); - onClose(); - // Call the callback if provided if (onEnvironmentCreated) { onEnvironmentCreated(); } + onClose(); })
21-22:enableReinitializeis unnecessary for static initial values.Since
initialValuesis always{ name: '' }and doesn't depend on props,enableReinitialize: trueadds no value here.packages/bruno-app/src/components/WorkspaceOverview/WorkspaceEnvironments/index.js (1)
22-37: DefaultTab uses CSS class instead of SharedButton component.
DefaultTabusesclassName="shared-button"on plain<button>elements, but there's an exportedSharedButtoncomponent above. Consider usingSharedButtonfor consistency, or clarify if the CSS class is intentionally different from the component.packages/bruno-app/src/components/WorkspaceOverview/WorkspaceEnvironments/EnvironmentList/index.js (3)
54-57: Redundant condition in selectedEnvironment sync.The check
hasSelectedEnvironmentChanged || selectedEnvironment.uid !== _selectedEnvironment?.uidis redundant. If UIDs differ,isEqualwould already returnfalse, makinghasSelectedEnvironmentChangedtrue.- const hasSelectedEnvironmentChanged = !isEqual(selectedEnvironment, _selectedEnvironment); - if (hasSelectedEnvironmentChanged || selectedEnvironment.uid !== _selectedEnvironment?.uid) { + if (!isEqual(selectedEnvironment, _selectedEnvironment)) { setSelectedEnvironment(_selectedEnvironment); }
144-161: Consider memoizingvalidateEnvironmentName.This function is defined inside the component body and recreated on every render. Since it references
globalEnvsfrom Redux, wrapping it withuseCallbackand includingglobalEnvsas a dependency would be more efficient, especially if passed to child components.
36-66: Complex useEffect with multiple fallback paths.The selectedEnvironment synchronization logic has multiple fallback scenarios (by uid, by name, by activeEnvironmentUid, then first). This is comprehensive but could benefit from inline comments explaining each fallback to aid future maintainers.
packages/bruno-app/src/utils/workspaces/index.js (1)
84-86: Silent no-op for cross-group drag.When dragging between pinned and unpinned groups, the function returns unchanged preferences without feedback. This may confuse callers expecting an error or state change. Consider returning a distinct value or logging.
packages/bruno-electron/src/ipc/workspace.js (3)
189-194: Direct store access breaks encapsulation.Line 193 directly accesses
lastOpenedWorkspaces.store.set(...)instead of using a method onLastOpenedWorkspaces. Consider adding anupdateorupdateNamemethod to maintain encapsulation.
69-71: Redundant try/catch blocks that only rethrow.Multiple handlers have
catch (error) { throw error; }which adds no value. Either handle the error meaningfully (e.g., wrap with context) or remove the try/catch.Also applies to: 96-98, 132-134
402-431: Error handling returns null—may hide issues.
renderer:get-default-workspacecatches all errors and returnsnull. This silently hides problems like permission errors or corrupted YAML. Consider logging the error or returning an error object.} catch (error) { + console.error('Failed to get default workspace:', error.message); return null; }packages/bruno-app/src/components/Sidebar/Collections/index.js (2)
19-35: Simplify duplicated workspace collection mapping logic.Both branches (default workspace and non-default) perform the identical mapping operation. This can be consolidated:
- if (!activeWorkspace || activeWorkspace.type === 'default') { - if (activeWorkspace && activeWorkspace.collections && activeWorkspace.collections.length > 0) { - allCollections = activeWorkspace.collections.map((wc) => { - const loadedCollection = collections.find((c) => c.pathname === wc.path); - return loadedCollection; - }).filter(Boolean); - } else { - allCollections = []; - } - } else { - if (activeWorkspace.collections && activeWorkspace.collections.length > 0) { - allCollections = activeWorkspace.collections.map((wc) => { - const loadedCollection = collections.find((c) => c.pathname === wc.path); - return loadedCollection; - }).filter(Boolean); - } - } + if (activeWorkspace?.collections?.length > 0) { + allCollections = activeWorkspace.collections + .map((wc) => collections.find((c) => c.pathname === wc.path)) + .filter(Boolean); + }
91-99: Redundant null/length check.
allCollectionsis guaranteed to have items here since the early return on line 37 handles the empty case. Simplify:- {allCollections && allCollections.length - ? allCollections.map((c) => { - return ( - <Collection searchText={searchText} collection={c} key={c.uid} /> - ); - }) - : null} + {allCollections.map((c) => ( + <Collection searchText={searchText} collection={c} key={c.uid} /> + ))}packages/bruno-app/src/components/Sidebar/TitleBar/index.js (3)
75-75: Inconsistent indentation.Line 75 uses 8 spaces which breaks from the surrounding code pattern. Should align with the rest of the function.
- setImportCollectionLocationModalOpen(false); + setImportCollectionLocationModalOpen(false);
95-102: MoveWorkspaceNameoutside component to avoid recreation on every render.Defining a
forwardRefcomponent inside TitleBar causes it to be recreated on every render, potentially causing unnecessary DOM reconciliation. Extract it outside or memoize it.+const WorkspaceName = forwardRef(({ name, onClick }, ref) => { + return ( + <div ref={ref} className="workspace-name-container" onClick={onClick}> + <span className="workspace-name">{name}</span> + <IconChevronDown size={14} stroke={1.5} className="chevron-icon" /> + </div> + ); +}); const TitleBar = ({ showSearch, setShowSearch }) => { // ... later use: // <WorkspaceName // ref={ref} // name={toTitleCase(activeWorkspace?.name) || 'Default Workspace'} // onClick={() => setShowWorkspaceDropdown(!showWorkspaceDropdown)} // />
40-47: ExtracttoTitleCaseas a utility function.This is a pure function that doesn't depend on component state or props. Move it outside the component to avoid recreation on each render and improve reusability.
packages/bruno-electron/src/ipc/global-environments.js (1)
7-7: UnusedworkspaceUidparameter.
workspaceUidis destructured but never used in the handler logic. Either remove it from the destructuring or clarify if it's intended for future use.packages/bruno-app/src/components/WorkspaceOverview/WorkspaceEnvironments/EnvironmentList/EnvironmentDetails/index.js (1)
153-178: Redundant validation check.Lines 158-160 check
name.length < 1, but this condition is already covered by lines 154-156 checking for empty/whitespace names. Remove the redundant check:if (!name || name.trim() === '') { return 'Name is required'; } - if (name.length < 1) { - return 'Must be at least 1 character'; - } - if (name.length > 255) {packages/bruno-electron/src/store/workspace-environments.js (2)
57-83: Mixing synchronous and asynchronous file operations.
parseEnvironmentFileis async but usesfs.readFileSync. For consistency and to avoid blocking the event loop, consider usingfs.promises.readFile:async parseEnvironmentFile(filePath, workspacePath) { - const content = fs.readFileSync(filePath, 'utf8'); + const content = await fs.promises.readFile(filePath, 'utf8'); const environment = await parseEnvironment(content, { format: 'yml' });Same applies to
fs.readdirSyncingetGlobalEnvironmentsandfindEnvironmentFileByUid.
332-354: Consider documenting the purpose of path-based wrapper methods.These thin wrappers (
getGlobalEnvironmentsByPath,addGlobalEnvironmentByPath, etc.) currently just delegate to their counterparts. A brief JSDoc comment explaining the API design intent would help maintainability.packages/bruno-electron/src/ipc/collection.js (2)
91-97: Missing semicolon at end of function.Per coding guidelines, always add semicolons at the end of statements.
const validatePathIsInsideCollection = (filePath) => { const collectionPath = findCollectionPathByItemPath(filePath); if (!collectionPath) { throw new Error(`Path: ${filePath} should be inside a collection`); } -} +};
818-825: Inline require and magic string 'default' for workspace type check.
- The
requireat line 820 should be moved to the top of the file with other imports.- The string literal
'default'for workspace type comparison could be fragile—consider using a constant.+const { removeCollectionFromWorkspace } = require('../utils/workspace-config'); +const DEFAULT_WORKSPACE = 'default'; // ... at top of file - if (workspacePath && workspacePath !== 'default') { + if (workspacePath && workspacePath !== DEFAULT_WORKSPACE) { try { - const { removeCollectionFromWorkspace } = require('../utils/workspace-config'); await removeCollectionFromWorkspace(workspacePath, collectionPath); } catch (error) {packages/bruno-electron/src/app/workspace-watcher.js (2)
16-19: DuplicateenvHasSecretsfunction.This function is duplicated from
collection.js(line 69-73). Consider extracting to a shared utility.
50-65: Hardcoded.ymlextension removal could be fragile.Consider using
path.parse()orpath.basename(pathname, '.yml')for safer extension handling.const parseGlobalEnvironmentFile = async (pathname, workspacePath, workspaceUid) => { const basename = path.basename(pathname); - const environmentName = basename.slice(0, -'.yml'.length); + const environmentName = path.basename(pathname, '.yml');packages/bruno-app/src/components/Sidebar/CreateCollection/index.js (1)
108-110: Silent error handling in browse.The catch block swallows errors silently. If this is for handling user cancellation, consider checking the error type. Otherwise, unexpected errors will be hidden.
packages/bruno-app/src/components/WorkspaceOverview/WorkspaceEnvironments/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js (3)
17-17: Unused proporiginalEnvironmentVariables.The prop
originalEnvironmentVariablesis declared but never used in the component. Either remove it from the signature or use it whereenvironment.variablesis referenced.
47-63: Yup conditional validation with$isLastRowcontext won't trigger.The
when('$isLastRow', ...)syntax requires passing context via Formik'svalidationContext. Since no context is provided, this conditional never activates. The customvalidatefunction at lines 64-85 handles this instead, making this schema logic dead code.Consider simplifying by removing the unused Yup conditional:
validationSchema: Yup.array().of(Yup.object({ enabled: Yup.boolean(), - name: Yup.string() - .when('$isLastRow', { - is: true, - then: (schema) => schema.optional(), - otherwise: (schema) => schema - .required('Name cannot be empty') - .matches(variableNameRegex, - 'Name contains invalid characters. Must only contain alphanumeric characters, "-", "_", "." and cannot start with a digit.') - .trim() - }), + name: Yup.string(), secret: Yup.boolean(), type: Yup.string(), uid: Yup.string(), value: Yup.mixed().nullable() })),Note: The static analysis warning about
thenon line 52 is a false positive—it's a Yup schema method, not a Promise property.
164-168:setTimeoutfor adding new row is fragile.Using
setTimeout(..., 0)to sequence state updates is a common pattern but can lead to timing issues with rapid input. Consider using Formik'ssetValuesto update both the current value and add the new row atomically.packages/bruno-electron/src/utils/workspace-config.js (2)
20-33: Code duplication:normalizeCollectionEntryis not used inaddCollectionToWorkspace.
addCollectionToWorkspace(lines 131-138) duplicates the normalization logic already present innormalizeCollectionEntry(lines 20-33). Consider reusing the helper:const addCollectionToWorkspace = async (workspacePath, collection) => { const config = readWorkspaceConfig(workspacePath); if (!config.collections) { config.collections = []; } - // Normalize collection entry - const normalizedCollection = { - name: collection.name, - path: collection.path - }; - - if (collection.remote) { - normalizedCollection.remote = collection.remote; - } + const normalizedCollection = normalizeCollectionEntry(workspacePath, collection);Also applies to: 123-151
153-193: Consider extracting the filter predicate for readability.The filter callback has side effects (setting
removedCollectionandshouldDeleteFiles). While functional, extracting the match logic could improve clarity:+const matchesCollectionPath = (workspacePath, collectionPathFromYml, targetPath) => { + if (!collectionPathFromYml) return false; + const absolutePath = path.isAbsolute(collectionPathFromYml) + ? collectionPathFromYml + : path.resolve(workspacePath, collectionPathFromYml); + return path.normalize(absolutePath) === path.normalize(targetPath); +};This is optional but would make the intent clearer.
packages/bruno-app/src/providers/ReduxStore/slices/global-environments.js (1)
177-211: Parameter reassignment detected.Line 188 reassigns
environmentUidwhich shadows the original parameter. Use a separate variable:- let environment = globalEnvironments?.find((env) => env?.uid == environmentUid); + let resolvedEnvironmentUid = environmentUid; + let environment = globalEnvironments?.find((env) => env?.uid == resolvedEnvironmentUid); if (!environment) { const activeUid = state.globalEnvironments?.activeGlobalEnvironmentUid; const activeEnv = globalEnvironments?.find((env) => env?.uid == activeUid); if (activeEnv) { environment = activeEnv; - environmentUid = activeEnv.uid; + resolvedEnvironmentUid = activeEnv.uid; } }packages/bruno-app/src/providers/ReduxStore/slices/workspaces/actions.js (3)
158-181: Unconventional pattern: using dispatch to access state.Lines 165 and 168 use
dispatch((_, getState) => getState()...)to read state. Consider passinggetStateto the helper:-const loadWorkspaceCollectionsForSwitch = async (dispatch, workspace) => { +const loadWorkspaceCollectionsForSwitch = async (dispatch, getState, workspace) => { // ... - const updatedWorkspace = await dispatch((_, getState) => getState().workspaces...); + const updatedWorkspace = getState().workspaces.workspaces.find((w) => w.uid === workspace.uid);
286-294: Silent catch block hides watcher startup failures.Consider at minimum logging the error:
try { await ipcRenderer.invoke('renderer:start-workspace-watcher', workspace.pathname); } catch (error) { + console.warn('Failed to start workspace watcher:', workspace.pathname, error); }
318-355: Multiple silent catch blocks in config update handler.Lines 347 and 351-352 have empty catches. At minimum, log for debugging:
try { await dispatch(openMultipleCollections(newCollectionPaths, { workspaceId: workspace.pathname })); } catch (error) { + console.error('Failed to open collections on config update:', error); } } } } catch (error) { + console.error('Failed to reload workspace collections:', error); }
packages/bruno-app/src/components/Sidebar/Collections/Collection/RemoveCollection/index.js
Show resolved
Hide resolved
packages/bruno-app/src/components/Sidebar/TitleBar/StyledWrapper.js
Outdated
Show resolved
Hide resolved
...ages/bruno-app/src/components/WorkspaceHome/WorkspaceEnvironments/ImportEnvironment/index.js
Show resolved
Hide resolved
| export const importCollection = (collection, collectionLocation, options = {}) => (dispatch, getState) => { | ||
| return new Promise(async (resolve, reject) => { | ||
| const { ipcRenderer } = window; | ||
|
|
||
| ipcRenderer.invoke('renderer:import-collection', collection, collectionLocation).then(resolve).catch(reject); | ||
| }); | ||
| }; | ||
| try { | ||
| const state = getState(); | ||
| const activeWorkspace = state.workspaces.workspaces.find((w) => w.uid === state.workspaces.activeWorkspaceUid); | ||
|
|
||
| export const moveCollectionAndPersist = | ||
| ({ draggedItem, targetItem }) => | ||
| (dispatch, getState) => { | ||
| dispatch(moveCollection({ draggedItem, targetItem })); | ||
| const collectionPath = await ipcRenderer.invoke('renderer:import-collection', collection, collectionLocation); | ||
|
|
||
| return new Promise((resolve, reject) => { | ||
| const { ipcRenderer } = window; | ||
| const state = getState(); | ||
| if (activeWorkspace && activeWorkspace.pathname && activeWorkspace.type !== 'default') { | ||
| const workspaceCollection = { | ||
| name: collection.name, | ||
| path: collectionPath | ||
| }; | ||
|
|
||
| const collectionPaths = state.collections.collections.map((collection) => collection.pathname); | ||
| await ipcRenderer.invoke('renderer:add-collection-to-workspace', activeWorkspace.pathname, workspaceCollection); | ||
|
|
||
| ipcRenderer.invoke('renderer:update-collection-paths', collectionPaths).then(resolve).catch(reject); | ||
| }); | ||
| }; | ||
| dispatch(addCollectionToWorkspace({ | ||
| workspaceUid: activeWorkspace.uid, | ||
| collection: workspaceCollection | ||
| })); | ||
| } | ||
|
|
||
| resolve(collectionPath); | ||
| } catch (error) { | ||
| reject(error); | ||
| } | ||
| }); | ||
| }; |
There was a problem hiding this comment.
Async promise executor - convert to async/await.
Same issue as handleCollectionItemDrop. Convert to async thunk:
-export const importCollection = (collection, collectionLocation, options = {}) => (dispatch, getState) => {
- return new Promise(async (resolve, reject) => {
+export const importCollection = (collection, collectionLocation, options = {}) => async (dispatch, getState) => {
const { ipcRenderer } = window;
try {
// ... existing logic ...
- resolve(collectionPath);
+ return collectionPath;
} catch (error) {
- reject(error);
+ throw error;
}
- });
};🧰 Tools
🪛 Biome (2.1.2)
[error] 2365-2392: Promise executor functions should not be async.
(lint/suspicious/noAsyncPromiseExecutor)
🤖 Prompt for AI Agents
In packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js
around lines 2364 to 2393, the action currently wraps an async executor in new
Promise; replace it with an async thunk by making the outer function async
(export const importCollection = (collection, collectionLocation, options = {})
=> async (dispatch, getState) => { ... }), remove the explicit new Promise, keep
the try/catch but return collectionPath on success and re-throw the caught error
(or throw a new error) on failure so callers receive a rejected promise;
preserve the ipcRenderer.invoke calls, workspace lookup,
dispatch(addCollectionToWorkspace(...)) behavior and ensure you reference
ipcRenderer from window before using it.
| const hasProcessedCollections = workspace.collections | ||
| && workspace.collections.length > 0 | ||
| && workspace.collections.some((c) => c.path && c.path.startsWith('/')); | ||
|
|
||
| if (!force && hasProcessedCollections) { | ||
| return workspace.collections; | ||
| } |
There was a problem hiding this comment.
Potential Windows path compatibility issue.
The check c.path.startsWith('/') only matches Unix absolute paths. Windows paths like C:\Users\... won't match:
- && workspace.collections.some((c) => c.path && c.path.startsWith('/'));
+ && workspace.collections.some((c) => c.path && path.isAbsolute(c.path));This requires importing path from the appropriate utility module.
Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In packages/bruno-app/src/providers/ReduxStore/slices/workspaces/actions.js
around lines 223–229, the code checks c.path.startsWith('/') which only detects
POSIX absolute paths and will miss Windows absolute paths; update the check to
use a platform-aware absolute-path test (e.g., import the project's path utility
or Node's path and use path.isAbsolute(c.path) or an equivalent helper), ensure
c.path is a string before calling the helper, and replace the startsWith
condition with that isAbsolute check so both Windows and POSIX absolute paths
are detected.
ff46f9b to
c8d13f1
Compare
| @@ -2,8 +2,9 @@ import React, { useRef, useEffect, forwardRef } from 'react'; | |||
| import { useDispatch, useSelector } from 'react-redux'; | |||
There was a problem hiding this comment.
When I ran the code locally, clicking the options dropdown in Create Collection does nothing.
| const isDefaultWorkspace = activeWorkspace?.type === 'default'; | ||
|
|
||
| const shouldShowAccordion = workspaceUid && hideLocationInput && !isDefaultWorkspace; | ||
| const actuallyHideLocationInput = hideLocationInput && !showExternalLocation && !isDefaultWorkspace; |
There was a problem hiding this comment.
This logic is hard to read and decipher. Lets discuss and cleanup when we meet in person
| <div className="flex justify-between items-center mt-8 bruno-modal-footer"> | ||
| <div className="flex advanced-options"> | ||
| <Dropdown onCreate={onDropdownCreate} icon={<AdvancedOptions />} placement="bottom-start"> | ||
| {shouldShowAccordion && ( |
There was a problem hiding this comment.
I don't see an Accordian based UI in the Create Collection component. We should be using some other term I feel.
There was a problem hiding this comment.
should i add "external location" option to a accordion or keep it in options?
packages/bruno-app/src/components/Sidebar/TitleBar/StyledWrapper.js
Outdated
Show resolved
Hide resolved
packages/bruno-app/src/components/Sidebar/TitleBar/StyledWrapper.js
Outdated
Show resolved
Hide resolved
packages/bruno-app/src/components/Sidebar/TitleBar/StyledWrapper.js
Outdated
Show resolved
Hide resolved
packages/bruno-app/src/components/Sidebar/TitleBar/StyledWrapper.js
Outdated
Show resolved
Hide resolved
| @@ -0,0 +1,134 @@ | |||
| import styled from 'styled-components'; | |||
There was a problem hiding this comment.
Please use theme props for border radius and font sizes throughout the PR
packages/bruno-app/src/components/WorkspaceOverview/WorkspaceCollections/StyledWrapper.js
Outdated
Show resolved
Hide resolved
packages/bruno-app/src/components/WorkspaceOverview/WorkspaceCollections/StyledWrapper.js
Outdated
Show resolved
Hide resolved
packages/bruno-app/src/components/WorkspaceOverview/WorkspaceCollections/StyledWrapper.js
Outdated
Show resolved
Hide resolved
| @@ -0,0 +1,271 @@ | |||
| import styled from 'styled-components'; | |||
There was a problem hiding this comment.
I see a bunch of unneeded styles in this PR. Please cleanup.
Description
This PR adds support for workspace in bruno
Contribution Checklist:
Note: Keeping the PR small and focused helps make it easier to review and merge. If you have multiple changes you want to make, please consider submitting them as separate pull requests.
Publishing to New Package Managers
Please see here for more information.
Summary by CodeRabbit
New Features
UI Changes
✏️ Tip: You can customize this high-level summary in your review settings.