feat: change default collection location to default location (#7291)

* feat: change default collection location to default location

* refactor: migrate defaultCollectionLocation to defaultLocation in preference.json

* refactor: resolveDefaultLocation function

* fix: rename variables in default-location
This commit is contained in:
gopu-bruno
2026-02-26 16:10:56 +05:30
committed by GitHub
parent 4d61ecacb3
commit 8ce38e8480
17 changed files with 150 additions and 52 deletions

View File

@@ -24,7 +24,7 @@ const StyledWrapper = styled.div`
}
}
.default-collection-location-input {
.default-location-input {
max-width: 28rem;
}
`;

View File

@@ -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 = () => {
<div className="text-red-500">{formik.errors.autoSave.interval}</div>
)}
<div className="flex flex-col mt-6">
<label className="block select-none default-collection-location-label" htmlFor="defaultCollectionLocation">
Default Collection Location
<label className="block select-none default-location-label" htmlFor="defaultLocation">
Default Location
</label>
<p className="text-muted mt-1 text-xs">
Used as the default location for new workspaces and collections
</p>
<input
type="text"
name="defaultCollectionLocation"
id="defaultCollectionLocation"
className="block textbox mt-2 w-full cursor-pointer default-collection-location-input"
name="defaultLocation"
id="defaultLocation"
className="block textbox mt-2 w-full cursor-pointer default-location-input"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
readOnly={true}
onChange={formik.handleChange}
value={formik.values.defaultCollectionLocation || ''}
value={formik.values.defaultLocation || ''}
onClick={browseDefaultLocation}
placeholder="Click to browse for default location"
/>
<div className="mt-1">
<span
className="text-link cursor-pointer hover:underline default-collection-location-browse"
className="text-link cursor-pointer hover:underline default-location-browse"
onClick={browseDefaultLocation}
>
Browse
</span>
</div>
</div>
{formik.touched.defaultCollectionLocation && formik.errors.defaultCollectionLocation ? (
<div className="text-red-500">{formik.errors.defaultCollectionLocation}</div>
{formik.touched.defaultLocation && formik.errors.defaultLocation ? (
<div className="text-red-500">{formik.errors.defaultLocation}</div>
) : null}
</form>
</StyledWrapper>

View File

@@ -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({});

View File

@@ -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();

View File

@@ -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;

View File

@@ -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,

View File

@@ -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);

View File

@@ -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()

View File

@@ -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')

View File

@@ -34,7 +34,7 @@ const initialState = {
codeFont: 'default'
},
general: {
defaultCollectionLocation: ''
defaultLocation: ''
},
autoSave: {
enabled: false,

View File

@@ -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);
}

View File

@@ -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 {

View File

@@ -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);
}

View File

@@ -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 };

View File

@@ -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();
});
});

View File

@@ -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');

View File

@@ -3,7 +3,7 @@
"lastOpenedCollections": ["{{projectRoot}}/tests/preferences/default-collection-location/collection"],
"preferences": {
"general": {
"defaultCollectionLocation": "/tmp/bruno-collections"
"defaultLocation": "/tmp/bruno-collections"
}
}
}