diff --git a/packages/bruno-app/src/components/Preferences/General/StyledWrapper.js b/packages/bruno-app/src/components/Preferences/General/StyledWrapper.js index 570d276c0..a71eb779f 100644 --- a/packages/bruno-app/src/components/Preferences/General/StyledWrapper.js +++ b/packages/bruno-app/src/components/Preferences/General/StyledWrapper.js @@ -24,7 +24,7 @@ const StyledWrapper = styled.div` } } - .default-collection-location-input { + .default-location-input { max-width: 28rem; } `; diff --git a/packages/bruno-app/src/components/Preferences/General/index.js b/packages/bruno-app/src/components/Preferences/General/index.js index ce072267c..f8b8f112c 100644 --- a/packages/bruno-app/src/components/Preferences/General/index.js +++ b/packages/bruno-app/src/components/Preferences/General/index.js @@ -60,7 +60,7 @@ const General = () => { oauth2: Yup.object({ useSystemBrowser: Yup.boolean() }), - defaultCollectionLocation: Yup.string().max(1024) + defaultLocation: Yup.string().max(1024) }); const formik = useFormik({ @@ -83,7 +83,7 @@ const General = () => { oauth2: { useSystemBrowser: get(preferences, 'request.oauth2.useSystemBrowser', false) }, - defaultCollectionLocation: get(preferences, 'general.defaultCollectionLocation', '') + defaultLocation: get(preferences, 'general.defaultLocation', '') }, validationSchema: preferencesSchema, onSubmit: async (values) => { @@ -121,7 +121,7 @@ const General = () => { interval: newPreferences.autoSave.interval }, general: { - defaultCollectionLocation: newPreferences.defaultCollectionLocation + defaultLocation: newPreferences.defaultLocation } })) .catch((err) => console.log(err) && toast.error('Failed to update preferences')); @@ -163,11 +163,11 @@ const General = () => { dispatch(browseDirectory()) .then((dirPath) => { if (typeof dirPath === 'string') { - formik.setFieldValue('defaultCollectionLocation', dirPath); + formik.setFieldValue('defaultLocation', dirPath); } }) .catch((error) => { - formik.setFieldValue('defaultCollectionLocation', ''); + formik.setFieldValue('defaultLocation', ''); console.error(error); }); }; @@ -356,35 +356,38 @@ const General = () => {
{formik.errors.autoSave.interval}
)}
-
- {formik.touched.defaultCollectionLocation && formik.errors.defaultCollectionLocation ? ( -
{formik.errors.defaultCollectionLocation}
+ {formik.touched.defaultLocation && formik.errors.defaultLocation ? ( +
{formik.errors.defaultLocation}
) : null} diff --git a/packages/bruno-app/src/components/Sidebar/BulkImportCollectionLocation/index.js b/packages/bruno-app/src/components/Sidebar/BulkImportCollectionLocation/index.js index e02783d18..906b0ab3a 100644 --- a/packages/bruno-app/src/components/Sidebar/BulkImportCollectionLocation/index.js +++ b/packages/bruno-app/src/components/Sidebar/BulkImportCollectionLocation/index.js @@ -133,7 +133,7 @@ export const BulkImportCollectionLocation = ({ const activeWorkspace = workspaces.find((w) => w.uid === activeWorkspaceUid); const isDefaultWorkspace = !activeWorkspace || activeWorkspace.type === 'default'; const defaultLocation = isDefaultWorkspace - ? get(preferences, 'general.defaultCollectionLocation', '') + ? get(preferences, 'general.defaultLocation', '') : (activeWorkspace?.pathname ? `${activeWorkspace.pathname}/collections` : ''); const [status, setStatus] = useState({}); diff --git a/packages/bruno-app/src/components/Sidebar/CloneGitRespository/index.js b/packages/bruno-app/src/components/Sidebar/CloneGitRespository/index.js index 0c0ece623..531330db2 100644 --- a/packages/bruno-app/src/components/Sidebar/CloneGitRespository/index.js +++ b/packages/bruno-app/src/components/Sidebar/CloneGitRespository/index.js @@ -33,7 +33,7 @@ const CloneGitRepository = ({ onClose, onFinish, collectionRepositoryUrl = null const activeWorkspace = workspaces.find((w) => w.uid === activeWorkspaceUid); const isDefaultWorkspace = !activeWorkspace || activeWorkspace.type === 'default'; const defaultLocation = isDefaultWorkspace - ? get(preferences, 'general.defaultCollectionLocation', '') + ? get(preferences, 'general.defaultLocation', '') : (activeWorkspace?.pathname ? `${activeWorkspace.pathname}/collections` : ''); const inputRef = useRef(); const dispatch = useDispatch(); diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CloneCollection/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CloneCollection/index.js index 6dbb69064..697adb839 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CloneCollection/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CloneCollection/index.js @@ -26,7 +26,7 @@ const CloneCollection = ({ onClose, collectionUid }) => { const isDefaultWorkspace = activeWorkspace?.type === 'default'; const defaultLocation = isDefaultWorkspace - ? get(preferences, 'general.defaultCollectionLocation', '') + ? get(preferences, 'general.defaultLocation', '') : (activeWorkspace?.pathname ? `${activeWorkspace.pathname}/collections` : ''); const { name } = collection; diff --git a/packages/bruno-app/src/components/Sidebar/CreateCollection/index.js b/packages/bruno-app/src/components/Sidebar/CreateCollection/index.js index 4b0235f80..18495cbb4 100644 --- a/packages/bruno-app/src/components/Sidebar/CreateCollection/index.js +++ b/packages/bruno-app/src/components/Sidebar/CreateCollection/index.js @@ -32,7 +32,7 @@ const CreateCollection = ({ onClose, defaultLocation: propDefaultLocation }) => const activeWorkspace = workspaces.find((w) => w.uid === workspaceUid); const isDefaultWorkspace = activeWorkspace?.type === 'default'; - const defaultLocation = isDefaultWorkspace ? get(preferences, 'general.defaultCollectionLocation', '') : (activeWorkspace?.pathname ? `${activeWorkspace.pathname}/collections` : ''); + const defaultLocation = isDefaultWorkspace ? get(preferences, 'general.defaultLocation', '') : (activeWorkspace?.pathname ? `${activeWorkspace.pathname}/collections` : ''); const formik = useFormik({ enableReinitialize: true, diff --git a/packages/bruno-app/src/components/Sidebar/ImportCollectionLocation/index.js b/packages/bruno-app/src/components/Sidebar/ImportCollectionLocation/index.js index 4efd3c54c..52d7434c5 100644 --- a/packages/bruno-app/src/components/Sidebar/ImportCollectionLocation/index.js +++ b/packages/bruno-app/src/components/Sidebar/ImportCollectionLocation/index.js @@ -110,7 +110,7 @@ const ImportCollectionLocation = ({ onClose, handleSubmit, rawData, format }) => const isDefaultWorkspace = !activeWorkspace || activeWorkspace.type === 'default'; const defaultLocation = isDefaultWorkspace - ? get(preferences, 'general.defaultCollectionLocation', '') + ? get(preferences, 'general.defaultLocation', '') : (activeWorkspace?.pathname ? `${activeWorkspace.pathname}/collections` : ''); const collectionName = getCollectionName(format, rawData); diff --git a/packages/bruno-app/src/components/WorkspaceSidebar/CreateWorkspace/index.js b/packages/bruno-app/src/components/WorkspaceSidebar/CreateWorkspace/index.js index 2ed0caa11..396966f4f 100644 --- a/packages/bruno-app/src/components/WorkspaceSidebar/CreateWorkspace/index.js +++ b/packages/bruno-app/src/components/WorkspaceSidebar/CreateWorkspace/index.js @@ -12,20 +12,24 @@ import { browseDirectory } from 'providers/ReduxStore/slices/collections/actions import { multiLineMsg } from 'utils/common/index'; import { formatIpcError } from 'utils/common/error'; import { sanitizeName, validateName, validateNameError } from 'utils/common/regex'; +import get from 'lodash/get'; const CreateWorkspace = ({ onClose }) => { const inputRef = useRef(); const dispatch = useDispatch(); const workspaces = useSelector((state) => state.workspaces.workspaces); + const preferences = useSelector((state) => state.app.preferences); const [isSubmitting, setIsSubmitting] = useState(false); const [isEditing, setIsEditing] = useState(false); + const defaultLocation = get(preferences, 'general.defaultLocation', ''); + const formik = useFormik({ enableReinitialize: true, initialValues: { workspaceName: '', workspaceFolderName: '', - workspaceLocation: '' + workspaceLocation: defaultLocation }, validationSchema: Yup.object({ workspaceName: Yup.string() diff --git a/packages/bruno-app/src/components/WorkspaceSidebar/ImportWorkspace/index.js b/packages/bruno-app/src/components/WorkspaceSidebar/ImportWorkspace/index.js index 5aa759830..b7d006062 100644 --- a/packages/bruno-app/src/components/WorkspaceSidebar/ImportWorkspace/index.js +++ b/packages/bruno-app/src/components/WorkspaceSidebar/ImportWorkspace/index.js @@ -1,8 +1,9 @@ import React, { useState, useRef, useEffect } from 'react'; -import { useDispatch } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import { useFormik } from 'formik'; import * as Yup from 'yup'; import toast from 'react-hot-toast'; +import get from 'lodash/get'; import { IconFileZip } from '@tabler/icons'; import Modal from 'components/Modal'; import { browseDirectory } from 'providers/ReduxStore/slices/collections/actions'; @@ -13,16 +14,19 @@ import Help from 'components/Help'; const ImportWorkspace = ({ onClose }) => { const dispatch = useDispatch(); + const preferences = useSelector((state) => state.app.preferences); const [dragActive, setDragActive] = useState(false); const [selectedFile, setSelectedFile] = useState(null); const [isSubmitting, setIsSubmitting] = useState(false); const fileInputRef = useRef(null); const locationInputRef = useRef(null); + const defaultLocation = get(preferences, 'general.defaultLocation', ''); + const formik = useFormik({ enableReinitialize: true, initialValues: { - workspaceLocation: '' + workspaceLocation: defaultLocation }, validationSchema: Yup.object({ workspaceLocation: Yup.string().min(1, 'location is required').required('location is required') diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/app.js b/packages/bruno-app/src/providers/ReduxStore/slices/app.js index ee6e9d3e0..a0e86da71 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/app.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/app.js @@ -34,7 +34,7 @@ const initialState = { codeFont: 'default' }, general: { - defaultCollectionLocation: '' + defaultLocation: '' }, autoSave: { enabled: false, diff --git a/packages/bruno-electron/src/app/onboarding.js b/packages/bruno-electron/src/app/onboarding.js index b2de55e77..f507cfb1d 100644 --- a/packages/bruno-electron/src/app/onboarding.js +++ b/packages/bruno-electron/src/app/onboarding.js @@ -3,26 +3,7 @@ const path = require('node:path'); const { app } = require('electron'); const { preferencesUtil } = require('../store/preferences'); const { importCollection, findUniqueFolderName } = require('../utils/collection-import'); - -/** - * Get the default location for collections - * Tries documents first, then desktop, then userData as fallback - */ -function getDefaultCollectionLocation() { - const preferredPaths = ['documents', 'desktop', 'userData']; - - for (const pathType of preferredPaths) { - try { - return app.getPath(pathType); - } catch (error) { - console.warn(`Failed to get ${pathType} path:`, error.message); - // Continue to next path - } - } - - // This should never happen since userData should always be available - throw new Error('No valid collection location found'); -} +const { resolveDefaultLocation } = require('../utils/default-location'); /** * Import sample collection for new users @@ -86,7 +67,7 @@ async function onboardUser(mainWindow, lastOpenedCollections) { return; } - const collectionLocation = getDefaultCollectionLocation(); + const collectionLocation = resolveDefaultLocation(); await importSampleCollection(collectionLocation, mainWindow); } diff --git a/packages/bruno-electron/src/ipc/preferences.js b/packages/bruno-electron/src/ipc/preferences.js index 1a2deb28c..83adad57d 100644 --- a/packages/bruno-electron/src/ipc/preferences.js +++ b/packages/bruno-electron/src/ipc/preferences.js @@ -4,11 +4,20 @@ const { getGitVersion } = require('../utils/git'); const { globalEnvironmentsStore } = require('../store/global-environments'); const { parsedFileCacheStore } = require('../store/parsed-file-cache-idb'); const { getCachedSystemProxy, refreshSystemProxy } = require('../store/system-proxy'); +const { resolveDefaultLocation } = require('../utils/default-location'); const registerPreferencesIpc = (mainWindow) => { ipcMain.handle('renderer:ready', async (event) => { // load preferences const preferences = getPreferences(); + + // Set the default location if it hasn't been set by the user + if (!preferences.general?.defaultLocation) { + preferences.general ??= {}; + preferences.general.defaultLocation = resolveDefaultLocation(); + await savePreferences(preferences); + } + mainWindow.webContents.send('main:load-preferences', preferences); try { diff --git a/packages/bruno-electron/src/store/preferences.js b/packages/bruno-electron/src/store/preferences.js index 19b82dec5..fe0545d26 100644 --- a/packages/bruno-electron/src/store/preferences.js +++ b/packages/bruno-electron/src/store/preferences.js @@ -50,7 +50,7 @@ const defaultPreferences = { hasLaunchedBefore: false }, general: { - defaultCollectionLocation: '', + defaultLocation: '', defaultWorkspacePath: '' }, autoSave: { @@ -107,7 +107,7 @@ const preferencesSchema = Yup.object().shape({ hasLaunchedBefore: Yup.boolean() }), general: Yup.object({ - defaultCollectionLocation: Yup.string().max(1024).nullable(), + defaultLocation: Yup.string().max(1024).nullable(), defaultWorkspacePath: Yup.string().max(1024).nullable() }), autoSave: Yup.object({ @@ -230,6 +230,14 @@ class PreferencesStore { } } + // Migrate from defaultCollectionLocation to defaultLocation + if (preferences.general?.defaultCollectionLocation !== undefined + && preferences.general?.defaultLocation === undefined) { + preferences.general.defaultLocation = preferences.general.defaultCollectionLocation; + delete preferences.general.defaultCollectionLocation; + this.store.set('preferences', preferences); + } + return merge({}, defaultPreferences, preferences); } diff --git a/packages/bruno-electron/src/utils/default-location.js b/packages/bruno-electron/src/utils/default-location.js new file mode 100644 index 000000000..e84facf20 --- /dev/null +++ b/packages/bruno-electron/src/utils/default-location.js @@ -0,0 +1,29 @@ +const fs = require('node:fs'); +const path = require('node:path'); +const { app } = require('electron'); + +const BRUNO_DIR_NAME = 'bruno'; + +/** + * Returns the default location where new workspaces and collections are stored. + * Checks ~/Documents/bruno if available, otherwise falls back to the app's data directory + */ +function resolveDefaultLocation() { + const defaultPaths = [ + path.join(app.getPath('documents'), BRUNO_DIR_NAME), + app.getPath('userData') + ]; + + for (const dirPath of defaultPaths) { + try { + fs.mkdirSync(dirPath, { recursive: true }); + return dirPath; + } catch (error) { + console.warn(`Failed to create directory at ${dirPath}:`, error.message); + } + } + + throw new Error('Failed to create default location'); +} + +module.exports = { resolveDefaultLocation }; diff --git a/packages/bruno-electron/tests/store/default-location-migration.spec.js b/packages/bruno-electron/tests/store/default-location-migration.spec.js new file mode 100644 index 000000000..7912b9bb1 --- /dev/null +++ b/packages/bruno-electron/tests/store/default-location-migration.spec.js @@ -0,0 +1,60 @@ +let mockStoreData = {}; + +jest.mock('electron-store', () => { + return jest.fn().mockImplementation((opts = {}) => { + return { + get: (key, fallback) => (key in mockStoreData ? mockStoreData[key] : fallback), + set: (key, value) => { + mockStoreData[key] = value; + } + }; + }); +}); + +const { getPreferences } = require('../../src/store/preferences'); + +describe('Default Location Migration', () => { + beforeEach(() => { + // Reset mock store data before each test + mockStoreData = {}; + }); + + it('should migrate defaultCollectionLocation to defaultLocation', () => { + mockStoreData['preferences'] = { + general: { + defaultCollectionLocation: '/home/user/collections' + } + }; + + const preferences = getPreferences(); + + expect(preferences.general.defaultLocation).toBe('/home/user/collections'); + expect(mockStoreData['preferences'].general.defaultCollectionLocation).toBeUndefined(); + expect(mockStoreData['preferences'].general.defaultLocation).toBe('/home/user/collections'); + }); + + it('should not migrate if defaultLocation already exists', () => { + mockStoreData['preferences'] = { + general: { + defaultCollectionLocation: '/old/path', + defaultLocation: '/new/path' + } + }; + + const preferences = getPreferences(); + + expect(preferences.general.defaultLocation).toBe('/new/path'); + // Old key is left untouched + expect(mockStoreData['preferences'].general.defaultCollectionLocation).toBe('/old/path'); + }); + + it('should return default empty string when neither key exists', () => { + mockStoreData['preferences'] = {}; + + const preferences = getPreferences(); + + expect(preferences.general.defaultLocation).toBe(''); + // No migration occurred — store unchanged + expect(mockStoreData['preferences'].general).toBeUndefined(); + }); +}); diff --git a/tests/preferences/default-collection-location/default-collection-location.spec.js b/tests/preferences/default-collection-location/default-collection-location.spec.js index 40b49a6f0..d6eec7305 100644 --- a/tests/preferences/default-collection-location/default-collection-location.spec.js +++ b/tests/preferences/default-collection-location/default-collection-location.spec.js @@ -1,6 +1,6 @@ import { test, expect } from '../../../playwright'; -test.describe('Default Collection Location Feature', () => { +test.describe('Default Location Feature', () => { test('Should hydrate the default location from preferences', async ({ pageWithUserData: page }) => { // open preferences tab await page.locator('.preferences-button').click(); @@ -12,7 +12,7 @@ test.describe('Default Collection Location Feature', () => { await page.getByRole('tab', { name: 'General' }).click(); // verify the default location is pre-filled - const defaultLocationInput = page.locator('.default-collection-location-input'); + const defaultLocationInput = page.locator('.default-location-input'); await expect(defaultLocationInput).toHaveValue('/tmp/bruno-collections'); }); @@ -27,7 +27,7 @@ test.describe('Default Collection Location Feature', () => { await page.getByRole('tab', { name: 'General' }).click(); // set a default location (readonly input, remove readonly then fill) - const defaultLocationInput = page.locator('.default-collection-location-input'); + const defaultLocationInput = page.locator('.default-location-input'); await defaultLocationInput.evaluate((el) => { const input = el; input.removeAttribute('readonly'); @@ -91,7 +91,7 @@ test.describe('Default Collection Location Feature', () => { await page.getByRole('tab', { name: 'General' }).click(); // clear the default location field (readonly input, remove readonly then clear) - const defaultLocationInput = page.locator('.default-collection-location-input'); + const defaultLocationInput = page.locator('.default-location-input'); await defaultLocationInput.evaluate((el) => { const input = el; input.removeAttribute('readonly'); diff --git a/tests/preferences/default-collection-location/init-user-data/preferences.json b/tests/preferences/default-collection-location/init-user-data/preferences.json index 9d0a76c50..6ba9f0b83 100644 --- a/tests/preferences/default-collection-location/init-user-data/preferences.json +++ b/tests/preferences/default-collection-location/init-user-data/preferences.json @@ -3,7 +3,7 @@ "lastOpenedCollections": ["{{projectRoot}}/tests/preferences/default-collection-location/collection"], "preferences": { "general": { - "defaultCollectionLocation": "/tmp/bruno-collections" + "defaultLocation": "/tmp/bruno-collections" } } }