mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-11 09:51:30 +00:00
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:
@@ -24,7 +24,7 @@ const StyledWrapper = styled.div`
|
||||
}
|
||||
}
|
||||
|
||||
.default-collection-location-input {
|
||||
.default-location-input {
|
||||
max-width: 28rem;
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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({});
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -34,7 +34,7 @@ const initialState = {
|
||||
codeFont: 'default'
|
||||
},
|
||||
general: {
|
||||
defaultCollectionLocation: ''
|
||||
defaultLocation: ''
|
||||
},
|
||||
autoSave: {
|
||||
enabled: false,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
29
packages/bruno-electron/src/utils/default-location.js
Normal file
29
packages/bruno-electron/src/utils/default-location.js
Normal 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 };
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"lastOpenedCollections": ["{{projectRoot}}/tests/preferences/default-collection-location/collection"],
|
||||
"preferences": {
|
||||
"general": {
|
||||
"defaultCollectionLocation": "/tmp/bruno-collections"
|
||||
"defaultLocation": "/tmp/bruno-collections"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user