diff --git a/packages/bruno-app/src/components/AppTitleBar/index.js b/packages/bruno-app/src/components/AppTitleBar/index.js index a09393b37..be35a810d 100644 --- a/packages/bruno-app/src/components/AppTitleBar/index.js +++ b/packages/bruno-app/src/components/AppTitleBar/index.js @@ -6,9 +6,10 @@ import { useDispatch, useSelector } from 'react-redux'; import { savePreferences, showManageWorkspacePage, toggleSidebarCollapse } from 'providers/ReduxStore/slices/app'; import { closeConsole, openConsole } from 'providers/ReduxStore/slices/logs'; -import { openWorkspaceDialog, switchWorkspace } from 'providers/ReduxStore/slices/workspaces/actions'; +import { createWorkspaceWithUniqueName, openWorkspaceDialog, switchWorkspace } from 'providers/ReduxStore/slices/workspaces/actions'; import { sortWorkspaces, toggleWorkspacePin } from 'utils/workspaces'; import { focusTab } from 'providers/ReduxStore/slices/tabs'; +import get from 'lodash/get'; import Bruno from 'components/Bruno'; import MenuDropdown from 'ui/MenuDropdown'; @@ -150,9 +151,20 @@ const AppTitleBar = () => { } }; - const handleCreateWorkspace = () => { - setCreateWorkspaceModalOpen(true); - }; + const handleCreateWorkspace = useCallback(async () => { + const defaultLocation = get(preferences, 'general.defaultLocation', ''); + if (!defaultLocation) { + setCreateWorkspaceModalOpen(true); + return; + } + + try { + await dispatch(createWorkspaceWithUniqueName(defaultLocation)); + toast.success('Workspace created!'); + } catch (error) { + toast.error(error?.message || 'Failed to create workspace'); + } + }, [preferences, dispatch]); const handleManageWorkspaces = () => { dispatch(showManageWorkspacePage()); @@ -240,7 +252,7 @@ const AppTitleBar = () => { ); return items; - }, [sortedWorkspaces, activeWorkspaceUid, preferences, handlePinWorkspace]); + }, [sortedWorkspaces, activeWorkspaceUid, preferences, handlePinWorkspace, handleCreateWorkspace]); return ( diff --git a/packages/bruno-app/src/components/ManageWorkspace/index.js b/packages/bruno-app/src/components/ManageWorkspace/index.js index 277df57d0..ff3355cbe 100644 --- a/packages/bruno-app/src/components/ManageWorkspace/index.js +++ b/packages/bruno-app/src/components/ManageWorkspace/index.js @@ -3,8 +3,9 @@ import { useSelector, useDispatch } from 'react-redux'; import { IconArrowLeft, IconPlus, IconFolder, IconLock, IconDots, IconCategory, IconLogin } from '@tabler/icons'; import toast from 'react-hot-toast'; +import get from 'lodash/get'; import { showHomePage } from 'providers/ReduxStore/slices/app'; -import { switchWorkspace } from 'providers/ReduxStore/slices/workspaces/actions'; +import { createWorkspaceWithUniqueName, switchWorkspace } from 'providers/ReduxStore/slices/workspaces/actions'; import { showInFolder } from 'providers/ReduxStore/slices/collections/actions'; import { sortWorkspaces } from 'utils/workspaces'; @@ -59,6 +60,21 @@ const ManageWorkspace = () => { setDeleteWorkspaceModal({ open: true, workspace }); }; + const handleCreateWorkspace = async () => { + const defaultLocation = get(preferences, 'general.defaultLocation', ''); + if (!defaultLocation) { + setCreateWorkspaceModalOpen(true); + return; + } + + try { + await dispatch(createWorkspaceWithUniqueName(defaultLocation)); + toast.success('Workspace created!'); + } catch (error) { + toast.error(error?.message || 'Failed to create workspace'); + } + }; + return ( {createWorkspaceModalOpen && ( @@ -86,7 +102,7 @@ const ManageWorkspace = () => { Manage Workspace - diff --git a/packages/bruno-app/src/components/RequestTabs/CollectionHeader/index.js b/packages/bruno-app/src/components/RequestTabs/CollectionHeader/index.js index 241f5a24f..ca1eb976a 100644 --- a/packages/bruno-app/src/components/RequestTabs/CollectionHeader/index.js +++ b/packages/bruno-app/src/components/RequestTabs/CollectionHeader/index.js @@ -15,6 +15,7 @@ import { IconUpload } from '@tabler/icons'; import { switchWorkspace, renameWorkspaceAction, exportWorkspaceAction } from 'providers/ReduxStore/slices/workspaces/actions'; +import { updateWorkspace } from 'providers/ReduxStore/slices/workspaces'; import { showInFolder } from 'providers/ReduxStore/slices/collections/actions'; import { addTab, focusTab } from 'providers/ReduxStore/slices/tabs'; import { uuid } from 'utils/common'; @@ -53,6 +54,21 @@ const CollectionHeader = ({ collection, isScratchCollection }) => { const onSwitcherCreate = (ref) => (switcherRef.current = ref); const onWorkspaceActionsCreate = (ref) => (workspaceActionsRef.current = ref); + // Auto-enter rename mode when workspace is newly created + useEffect(() => { + if (isScratchCollection && currentWorkspace?.isNewlyCreated) { + dispatch(updateWorkspace({ uid: currentWorkspace.uid, isNewlyCreated: false })); + setIsRenamingWorkspace(true); + setWorkspaceNameInput(currentWorkspace.name || ''); + setWorkspaceNameError(''); + const timer = setTimeout(() => { + workspaceNameInputRef.current?.focus(); + workspaceNameInputRef.current?.select(); + }, 50); + return () => clearTimeout(timer); + } + }, [isScratchCollection, currentWorkspace?.isNewlyCreated, currentWorkspace?.uid, currentWorkspace?.name, dispatch]); + const handleCancelWorkspaceRename = useCallback(() => { setIsRenamingWorkspace(false); setWorkspaceNameInput(''); diff --git a/packages/bruno-app/src/components/Sidebar/Collections/CreateOrOpenCollection/index.js b/packages/bruno-app/src/components/Sidebar/Collections/CreateOrOpenCollection/index.js index fbaefa2f8..ac8d37aaf 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/CreateOrOpenCollection/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/CreateOrOpenCollection/index.js @@ -1,24 +1,18 @@ -import { useState } from 'react'; import { useTheme } from '../../../../providers/Theme'; -import { useDispatch, useSelector } from 'react-redux'; +import { useDispatch } from 'react-redux'; import { openCollection } from 'providers/ReduxStore/slices/collections/actions'; import toast from 'react-hot-toast'; import styled from 'styled-components'; -import CreateCollection from 'components/Sidebar/CreateCollection'; import StyledWrapper from './StyledWrapper'; const LinkStyle = styled.span` color: ${(props) => props.theme['text-link']}; `; -const CreateOrOpenCollection = () => { +const CreateOrOpenCollection = ({ onCreateClick }) => { const { theme } = useTheme(); const dispatch = useDispatch(); - const [createCollectionModalOpen, setCreateCollectionModalOpen] = useState(false); - - const { workspaces, activeWorkspaceUid } = useSelector((state) => state.workspaces); - const activeWorkspace = workspaces.find((w) => w.uid === activeWorkspaceUid); const handleOpenCollection = () => { dispatch(openCollection()).catch( @@ -32,7 +26,7 @@ const CreateOrOpenCollection = () => { setCreateCollectionModalOpen(true)} + onClick={onCreateClick} > Create @@ -45,12 +39,6 @@ const CreateOrOpenCollection = () => { return ( - {createCollectionModalOpen ? ( - setCreateCollectionModalOpen(false)} - /> - ) : null} -
No collections found.
diff --git a/packages/bruno-app/src/components/Sidebar/Collections/InlineCollectionCreator/StyledWrapper.js b/packages/bruno-app/src/components/Sidebar/Collections/InlineCollectionCreator/StyledWrapper.js new file mode 100644 index 000000000..92738f6fb --- /dev/null +++ b/packages/bruno-app/src/components/Sidebar/Collections/InlineCollectionCreator/StyledWrapper.js @@ -0,0 +1,89 @@ +import styled from 'styled-components'; + +const StyledWrapper = styled.div` + .inline-collection-creator { + display: flex; + align-items: center; + gap: 4px; + height: 1.6rem; + padding-left: 8px; + padding-right: 4px; + } + + .input-wrapper { + display: flex; + align-items: center; + flex: 1; + min-width: 0; + border: 1px solid ${(props) => props.theme.input.border}; + border-radius: 3px; + background: ${(props) => props.theme.input.bg}; + + &:focus-within { + border-color: ${(props) => props.theme.input.focusBorder}; + } + } + + .inline-collection-input { + font-size: 13px; + padding: 1px 4px; + border: none; + background: transparent; + color: ${(props) => props.theme.text}; + outline: none; + flex: 1; + min-width: 0; + } + + .cog-btn { + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + width: 20px; + height: 100%; + border: none; + cursor: pointer; + background: transparent; + color: ${(props) => props.theme.text}; + opacity: 0.5; + + &:hover { + opacity: 1; + } + } + + .inline-actions { + display: flex; + align-items: center; + gap: 2px; + flex-shrink: 0; + } + + .inline-action-btn { + display: flex; + align-items: center; + justify-content: center; + width: 20px; + height: 20px; + border: none; + border-radius: 3px; + cursor: pointer; + background: transparent; + color: ${(props) => props.theme.text}; + + &:hover { + background: ${(props) => props.theme.sidebar.collection.item.hoverBg}; + } + + &.save { + color: ${(props) => props.theme.colors.text.green}; + } + + &.cancel { + color: ${(props) => props.theme.colors.text.danger}; + } + } +`; + +export default StyledWrapper; diff --git a/packages/bruno-app/src/components/Sidebar/Collections/InlineCollectionCreator/index.js b/packages/bruno-app/src/components/Sidebar/Collections/InlineCollectionCreator/index.js new file mode 100644 index 000000000..58a5da28e --- /dev/null +++ b/packages/bruno-app/src/components/Sidebar/Collections/InlineCollectionCreator/index.js @@ -0,0 +1,175 @@ +import { useRef, useEffect, useState, useCallback } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { IconCheck, IconX, IconSettings } from '@tabler/icons'; +import get from 'lodash/get'; +import toast from 'react-hot-toast'; +import { createCollection } from 'providers/ReduxStore/slices/collections/actions'; +import { sanitizeName, validateName, validateNameError } from 'utils/common/regex'; +import { DEFAULT_COLLECTION_FORMAT } from 'utils/common/constants'; +import { multiLineMsg } from 'utils/common'; +import { formatIpcError } from 'utils/common/error'; +import StyledWrapper from './StyledWrapper'; + +const InlineCollectionCreator = ({ onComplete, onCancel, onOpenAdvanced }) => { + const inputRef = useRef(null); + const containerRef = useRef(null); + const dispatch = useDispatch(); + const [isCreating, setIsCreating] = useState(false); + const openingAdvancedRef = useRef(false); + const clickedOutsideRef = useRef(false); + + const preferences = useSelector((state) => state.app.preferences); + const workspaces = useSelector((state) => state.workspaces?.workspaces || []); + const activeWorkspaceUid = useSelector((state) => state.workspaces?.activeWorkspaceUid); + const activeWorkspace = workspaces.find((w) => w.uid === activeWorkspaceUid); + const isDefaultWorkspace = activeWorkspace?.type === 'default'; + + const defaultLocation = isDefaultWorkspace + ? get(preferences, 'general.defaultLocation', '') + : (activeWorkspace?.pathname ? `${activeWorkspace.pathname}/collections` : ''); + + useEffect(() => { + const focusAndSelect = (value) => { + if (!inputRef.current) { + return; + } + if (value) { + inputRef.current.value = value; + } + inputRef.current.focus(); + inputRef.current.select(); + }; + + if (defaultLocation) { + window.ipcRenderer?.invoke('renderer:find-unique-folder-name', 'untitled collection', defaultLocation) + ?.then((name) => focusAndSelect(name)) + ?.catch(() => focusAndSelect()); + } else { + focusAndSelect(); + } + }, [defaultLocation]); + + const handleCancel = () => { + if (isCreating || openingAdvancedRef.current) return; + onCancel(); + }; + + const handleCreate = useCallback(async () => { + const fromOutside = clickedOutsideRef.current; + clickedOutsideRef.current = false; + + if (isCreating || openingAdvancedRef.current) return; + + const name = inputRef.current?.value?.trim(); + if (!name) { + if (fromOutside) { + onCancel(); + } else { + toast.error('Collection name is required'); + } + return; + } + + if (!validateName(name)) { + toast.error(validateNameError(name)); + if (fromOutside) { + onCancel(); + } + return; + } + + if (!defaultLocation) { + toast.error('Please set a default location in Preferences > General'); + onCancel(); + return; + } + + setIsCreating(true); + try { + const folderName = sanitizeName(name); + await dispatch(createCollection(name, folderName, defaultLocation, { format: DEFAULT_COLLECTION_FORMAT })); + toast.success('Collection created!'); + onComplete(); + } catch (e) { + toast.error(multiLineMsg('An error occurred while creating the collection', formatIpcError(e))); + setIsCreating(false); + } + }, [isCreating, defaultLocation, dispatch, onCancel, onComplete]); + + // Click outside to create + useEffect(() => { + const handleClickOutside = (e) => { + if (containerRef.current && !containerRef.current.contains(e.target)) { + clickedOutsideRef.current = true; + handleCreate(); + } + }; + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, [handleCreate]); + + const handleKeyDown = (e) => { + if (e.key === 'Enter') { + e.preventDefault(); + handleCreate(); + } else if (e.key === 'Escape') { + e.preventDefault(); + handleCancel(); + } + }; + + return ( + +
+
+ + +
+
+ + +
+
+
+ ); +}; + +export default InlineCollectionCreator; diff --git a/packages/bruno-app/src/components/Sidebar/Collections/index.js b/packages/bruno-app/src/components/Sidebar/Collections/index.js index 08b94f5e8..f0ec52c6c 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/index.js @@ -1,18 +1,17 @@ import React, { useState, useMemo } from 'react'; import { useSelector } from 'react-redux'; import Collection from './Collection'; -import CreateCollection from '../CreateCollection'; import StyledWrapper from './StyledWrapper'; import CreateOrOpenCollection from './CreateOrOpenCollection'; import CollectionSearch from './CollectionSearch/index'; +import InlineCollectionCreator from './InlineCollectionCreator'; import { normalizePath } from 'utils/common/path'; import { isScratchCollection } from 'utils/collections'; -const Collections = ({ showSearch }) => { +const Collections = ({ showSearch, isCreatingCollection, onCreateClick, onDismissCreate, onOpenAdvancedCreate }) => { const [searchText, setSearchText] = useState(''); const { collections } = useSelector((state) => state.collections); const { workspaces, activeWorkspaceUid } = useSelector((state) => state.workspaces); - const [createCollectionModalOpen, setCreateCollectionModalOpen] = useState(false); const activeWorkspace = workspaces.find((w) => w.uid === activeWorkspaceUid) || workspaces.find((w) => w.type === 'default'); @@ -30,24 +29,32 @@ const Collections = ({ showSearch }) => { if (!workspaceCollections || !workspaceCollections.length) { return ( - + {isCreatingCollection && ( + + )} + {!isCreatingCollection && } ); } return ( - {createCollectionModalOpen ? ( - setCreateCollectionModalOpen(false)} - /> - ) : null} - {showSearch && ( )}
+ {isCreatingCollection && ( + + )} {workspaceCollections && workspaceCollections.length ? workspaceCollections.map((c) => { return ( diff --git a/packages/bruno-app/src/components/Sidebar/CreateCollection/index.js b/packages/bruno-app/src/components/Sidebar/CreateCollection/index.js index 18495cbb4..121d36403 100644 --- a/packages/bruno-app/src/components/Sidebar/CreateCollection/index.js +++ b/packages/bruno-app/src/components/Sidebar/CreateCollection/index.js @@ -18,7 +18,7 @@ import StyledWrapper from './StyledWrapper'; import get from 'lodash/get'; import Button from 'ui/Button'; -const CreateCollection = ({ onClose, defaultLocation: propDefaultLocation }) => { +const CreateCollection = ({ onClose, defaultLocation: propDefaultLocation, initialCollectionName = '' }) => { const inputRef = useRef(); const dispatch = useDispatch(); const workspaces = useSelector((state) => state.workspaces?.workspaces || []); @@ -37,8 +37,8 @@ const CreateCollection = ({ onClose, defaultLocation: propDefaultLocation }) => const formik = useFormik({ enableReinitialize: true, initialValues: { - collectionName: '', - collectionFolderName: '', + collectionName: initialCollectionName, + collectionFolderName: initialCollectionName ? sanitizeName(initialCollectionName) : '', collectionLocation: defaultLocation || '', format: DEFAULT_COLLECTION_FORMAT }, @@ -86,9 +86,13 @@ const CreateCollection = ({ onClose, defaultLocation: propDefaultLocation }) => }; useEffect(() => { - if (inputRef && inputRef.current) { - inputRef.current.focus(); - } + const timer = setTimeout(() => { + if (inputRef && inputRef.current) { + inputRef.current.focus(); + inputRef.current.select(); + } + }, 50); + return () => clearTimeout(timer); }, [inputRef]); const AdvancedOptions = forwardRef((props, ref) => { diff --git a/packages/bruno-app/src/components/Sidebar/Sections/CollectionsSection/index.js b/packages/bruno-app/src/components/Sidebar/Sections/CollectionsSection/index.js index 2f623bb9b..930593773 100644 --- a/packages/bruno-app/src/components/Sidebar/Sections/CollectionsSection/index.js +++ b/packages/bruno-app/src/components/Sidebar/Sections/CollectionsSection/index.js @@ -1,4 +1,5 @@ import { useState, useMemo, useEffect } from 'react'; +import { setIsCreatingCollection } from 'providers/ReduxStore/slices/app'; import toast from 'react-hot-toast'; import get from 'lodash/get'; import { useDispatch, useSelector } from 'react-redux'; @@ -46,11 +47,13 @@ const CollectionsSection = () => { const { collections } = useSelector((state) => state.collections); const { collectionSortOrder } = useSelector((state) => state.collections); + const { isCreatingCollection } = useSelector((state) => state.app); const preferences = useSelector((state) => state.app.preferences); const [collectionsToClose, setCollectionsToClose] = useState([]); const [importData, setImportData] = useState(null); const [createCollectionModalOpen, setCreateCollectionModalOpen] = useState(false); + const [advancedCreateName, setAdvancedCreateName] = useState(''); const [importCollectionModalOpen, setImportCollectionModalOpen] = useState(false); const [importCollectionLocationModalOpen, setImportCollectionLocationModalOpen] = useState(false); const [showCloneGitModal, setShowCloneGitModal] = useState(false); @@ -241,13 +244,19 @@ const CollectionsSection = () => { }); }; + const handleOpenAdvancedCreate = (name) => { + dispatch(setIsCreatingCollection(false)); + setAdvancedCreateName(name || ''); + setCreateCollectionModalOpen(true); + }; + const addDropdownItems = [ { id: 'create', leftSection: IconPlus, label: 'Create collection', onClick: () => { - setCreateCollectionModalOpen(true); + dispatch(setIsCreatingCollection(true)); } }, { @@ -359,7 +368,11 @@ const CollectionsSection = () => { )} {createCollectionModalOpen && ( setCreateCollectionModalOpen(false)} + onClose={() => { + setCreateCollectionModalOpen(false); + setAdvancedCreateName(''); + }} + initialCollectionName={advancedCreateName} /> )} {importCollectionModalOpen && ( @@ -396,7 +409,13 @@ const CollectionsSection = () => { icon={IconBox} actions={sectionActions} > - + dispatch(setIsCreatingCollection(true))} + onDismissCreate={() => dispatch(setIsCreatingCollection(false))} + onOpenAdvancedCreate={handleOpenAdvancedCreate} + /> ); diff --git a/packages/bruno-app/src/components/WorkspaceHome/WorkspaceOverview/index.js b/packages/bruno-app/src/components/WorkspaceHome/WorkspaceOverview/index.js index e20913b93..caadd2c97 100644 --- a/packages/bruno-app/src/components/WorkspaceHome/WorkspaceOverview/index.js +++ b/packages/bruno-app/src/components/WorkspaceHome/WorkspaceOverview/index.js @@ -2,8 +2,8 @@ import React, { useState } from 'react'; import { useSelector, useDispatch } from 'react-redux'; import { IconPlus, IconFolder, IconDownload } from '@tabler/icons'; import { importCollection, openCollection, importCollectionFromZip } from 'providers/ReduxStore/slices/collections/actions'; +import { setIsCreatingCollection, toggleSidebarCollapse } from 'providers/ReduxStore/slices/app'; import toast from 'react-hot-toast'; -import CreateCollection from 'components/Sidebar/CreateCollection'; import ImportCollection from 'components/Sidebar/ImportCollection'; import ImportCollectionLocation from 'components/Sidebar/ImportCollectionLocation'; import BulkImportCollectionLocation from 'components/Sidebar/BulkImportCollectionLocation'; @@ -16,8 +16,8 @@ import StyledWrapper from './StyledWrapper'; const WorkspaceOverview = ({ workspace }) => { const dispatch = useDispatch(); const { globalEnvironments } = useSelector((state) => state.globalEnvironments); + const { sidebarCollapsed, isCreatingCollection } = useSelector((state) => state.app); - const [createCollectionModalOpen, setCreateCollectionModalOpen] = useState(false); const [importCollectionModalOpen, setImportCollectionModalOpen] = useState(false); const [importCollectionLocationModalOpen, setImportCollectionLocationModalOpen] = useState(false); const [importData, setImportData] = useState(null); @@ -29,6 +29,10 @@ const WorkspaceOverview = ({ workspace }) => { const workspaceEnvironmentsCount = globalEnvironments?.length || 0; const handleCreateCollection = async () => { + if (isCreatingCollection) { + return; + } + if (!workspace?.pathname) { toast.error('Workspace path not found'); return; @@ -37,7 +41,10 @@ const WorkspaceOverview = ({ workspace }) => { try { const { ipcRenderer } = window; await ipcRenderer.invoke('renderer:ensure-collections-folder', workspace.pathname); - setCreateCollectionModalOpen(true); + if (sidebarCollapsed) { + dispatch(toggleSidebarCollapse()); + } + dispatch(setIsCreatingCollection(true)); } catch (error) { console.error('Error ensuring collections folder exists:', error); toast.error('Error preparing workspace for collection creation'); @@ -87,10 +94,6 @@ const WorkspaceOverview = ({ workspace }) => { return ( - {createCollectionModalOpen && ( - setCreateCollectionModalOpen(false)} /> - )} - {importCollectionModalOpen && ( setImportCollectionModalOpen(false)} @@ -142,6 +145,7 @@ const WorkspaceOverview = ({ workspace }) => { size="sm" icon={} onClick={handleCreateCollection} + disabled={isCreatingCollection} > Create Collection diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/app.js b/packages/bruno-app/src/providers/ReduxStore/slices/app.js index 5dad0e492..9d91b166f 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/app.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/app.js @@ -61,7 +61,8 @@ const initialState = { envVarSearch: { collection: { query: '', expanded: false }, global: { query: '', expanded: false } - } + }, + isCreatingCollection: false }; export const appSlice = createSlice({ @@ -157,6 +158,9 @@ export const appSlice = createSlice({ setEnvVarSearchExpanded: (state, { payload: { context, expanded } }) => { if (!state.envVarSearch[context]) return; state.envVarSearch[context].expanded = expanded; + }, + setIsCreatingCollection: (state, action) => { + state.isCreatingCollection = action.payload; } }, extraReducers: (builder) => { @@ -200,7 +204,8 @@ export const { setGitVersion, setClipboard, setEnvVarSearchQuery, - setEnvVarSearchExpanded + setEnvVarSearchExpanded, + setIsCreatingCollection } = appSlice.actions; export const savePreferences = (preferences) => (dispatch, getState) => { diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/workspaces/actions.js b/packages/bruno-app/src/providers/ReduxStore/slices/workspaces/actions.js index 33651d320..142e4818f 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/workspaces/actions.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/workspaces/actions.js @@ -15,6 +15,7 @@ import { removeCollection, addTransientDirectory, updateCollectionMountStatus } import { updateGlobalEnvironments } from '../global-environments'; import { addTab, focusTab } from '../tabs'; import { normalizePath } from 'utils/common/path'; +import { sanitizeName } from 'utils/common/regex'; import toast from 'react-hot-toast'; const { ipcRenderer } = window; @@ -50,6 +51,21 @@ const transformCollection = async (collection, type) => { } }; +/** + * Creates a workspace with a unique name under the given location + */ +export const createWorkspaceWithUniqueName = (location) => { + return async (dispatch) => { + const name = await ipcRenderer?.invoke('renderer:find-unique-folder-name', 'untitled workspace', location) || 'untitled workspace'; + const folderName = sanitizeName(name); + const result = await dispatch(createWorkspaceAction(name, folderName, location)); + if (result?.workspaceUid) { + dispatch(updateWorkspace({ uid: result.workspaceUid, isNewlyCreated: true })); + } + return result; + }; +}; + export const createWorkspaceAction = (workspaceName, workspaceFolderName, workspaceLocation) => { return async (dispatch) => { try { diff --git a/packages/bruno-electron/src/ipc/filesystem.js b/packages/bruno-electron/src/ipc/filesystem.js index b28b56d73..025d5b945 100644 --- a/packages/bruno-electron/src/ipc/filesystem.js +++ b/packages/bruno-electron/src/ipc/filesystem.js @@ -8,6 +8,7 @@ const { isFile, isDirectory } = require('../utils/filesystem'); +const { findUniqueFolderName } = require('../utils/collection-import'); const registerFilesystemIpc = (mainWindow) => { ipcMain.handle('renderer:browse-directory', async (event, pathname, request) => { @@ -47,6 +48,14 @@ const registerFilesystemIpc = (mainWindow) => { ipcMain.handle('renderer:is-directory', async (_, pathname) => { return isDirectory(pathname); }); + + ipcMain.handle('renderer:find-unique-folder-name', async (_, baseName, location) => { + try { + return await findUniqueFolderName(baseName, location); + } catch (error) { + throw error; + } + }); }; module.exports = registerFilesystemIpc; 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 d6eec7305..352b027c1 100644 --- a/tests/preferences/default-collection-location/default-collection-location.spec.js +++ b/tests/preferences/default-collection-location/default-collection-location.spec.js @@ -44,6 +44,11 @@ test.describe('Default Location Feature', () => { await page.getByTestId('collections-header-add-menu').click(); await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Create collection' }).click(); + // Wait for inline creator to appear, then click the cog button to open advanced modal + const inlineCreator = page.locator('.inline-collection-creator'); + await inlineCreator.waitFor({ state: 'visible', timeout: 5000 }); + await inlineCreator.locator('.cog-btn').click(); + // Wait for modal to be visible await page.locator('.bruno-modal').waitFor({ state: 'visible' }); diff --git a/tests/utils/page/actions.ts b/tests/utils/page/actions.ts index e66078fdf..295fe1dc9 100644 --- a/tests/utils/page/actions.ts +++ b/tests/utils/page/actions.ts @@ -68,7 +68,13 @@ const createCollection = async (page, collectionName: string, collectionLocation await page.getByTestId('collections-header-add-menu').click(); await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Create collection' }).click(); + // Wait for inline creator to appear, then click the cog button to open advanced modal + const inlineCreator = page.locator('.inline-collection-creator'); + await inlineCreator.waitFor({ state: 'visible', timeout: 5000 }); + await inlineCreator.locator('.cog-btn').click(); + const createCollectionModal = page.locator('.bruno-modal-card').filter({ hasText: 'Create Collection' }); + await createCollectionModal.waitFor({ state: 'visible', timeout: 5000 }); await createCollectionModal.getByLabel('Name').fill(collectionName); const locationInput = createCollectionModal.getByLabel('Location');