import { useState, useRef, useEffect, useCallback } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { IconCategory, IconBox, IconChevronDown, IconRun, IconEye, IconSettings, IconDots, IconEdit, IconX, IconCheck, IconFolder, IconUpload } from '@tabler/icons'; import OpenAPISyncIcon from 'components/Icons/OpenAPISync'; 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'; import toast from 'react-hot-toast'; import Dropdown from 'components/Dropdown'; import MenuDropdown from 'ui/MenuDropdown'; import CloseWorkspace from 'components/Sidebar/CloseWorkspace'; import EnvironmentSelector from 'components/Environments/EnvironmentSelector'; import ToolHint from 'components/ToolHint'; import JsSandboxMode from 'components/SecuritySettings/JsSandboxMode'; import ActionIcon from 'ui/ActionIcon'; import { getRevealInFolderLabel } from 'utils/common/platform'; import classNames from 'classnames'; import StyledWrapper from './StyledWrapper'; import { useTheme } from 'providers/Theme'; const CollectionHeader = ({ collection, isScratchCollection }) => { const dispatch = useDispatch(); const workspaces = useSelector((state) => state.workspaces.workspaces); const activeWorkspaceUid = useSelector((state) => state.workspaces.activeWorkspaceUid); const collections = useSelector((state) => state.collections.collections); const tabs = useSelector((state) => state.tabs.tabs); // Get the current active workspace const currentWorkspace = workspaces.find((w) => w.uid === activeWorkspaceUid); // Workspace rename state const [isRenamingWorkspace, setIsRenamingWorkspace] = useState(false); const [workspaceNameInput, setWorkspaceNameInput] = useState(''); const [workspaceNameError, setWorkspaceNameError] = useState(''); const [closeWorkspaceModalOpen, setCloseWorkspaceModalOpen] = useState(false); const switcherRef = useRef(); const workspaceActionsRef = useRef(); const workspaceNameInputRef = useRef(null); const workspaceRenameContainerRef = useRef(null); 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(''); } }, [isScratchCollection, currentWorkspace?.isNewlyCreated, currentWorkspace?.uid, currentWorkspace?.name, dispatch]); const handleCancelWorkspaceRename = useCallback(() => { setIsRenamingWorkspace(false); setWorkspaceNameInput(''); setWorkspaceNameError(''); }, []); useEffect(() => { if (!isRenamingWorkspace) return; const handleClickOutside = (event) => { if (workspaceRenameContainerRef.current && !workspaceRenameContainerRef.current.contains(event.target)) { handleCancelWorkspaceRename(); } }; document.addEventListener('mousedown', handleClickOutside); const timer = setTimeout(() => { workspaceNameInputRef.current?.focus(); workspaceNameInputRef.current?.select(); }, 50); return () => { document.removeEventListener('mousedown', handleClickOutside); clearTimeout(timer); }; }, [isRenamingWorkspace, handleCancelWorkspaceRename]); const collectionUpdates = useSelector((state) => state.openapiSync?.collectionUpdates || {}); const { theme } = useTheme(); if (!collection) { return null; } const hasOpenApiSyncConfigured = collection?.brunoConfig?.openapi?.[0]?.sourceUrl; const hasOpenApiUpdates = hasOpenApiSyncConfigured && collectionUpdates[collection.uid]?.hasUpdates; const hasOpenApiError = hasOpenApiSyncConfigured && collectionUpdates[collection.uid]?.error; // Get mounted collections for the current workspace (excluding scratch collections) const mountedCollections = collections.filter((c) => { if (c.mountStatus !== 'mounted') return false; const isScratch = workspaces.some((w) => w.scratchCollectionUid === c.uid); if (isScratch) return false; const workspaceCollectionPaths = currentWorkspace?.collections?.map((wc) => wc.path) || []; return workspaceCollectionPaths.some((wcPath) => c.pathname === wcPath); }); // Count tabs for the current collection const tabCount = tabs.filter((t) => t.collectionUid === collection.uid).length; // Get tab count for a given collection uid const getTabCount = (collectionUid) => tabs.filter((t) => t.collectionUid === collectionUid).length; // Get tab count for workspace (scratch collection) const workspaceTabCount = currentWorkspace?.scratchCollectionUid ? getTabCount(currentWorkspace.scratchCollectionUid) : 0; // Display name and icon based on context const displayName = isScratchCollection ? (currentWorkspace?.name || 'Untitled Workspace') : (collection.name || 'Untitled Collection'); const DisplayIcon = isScratchCollection ? IconCategory : IconBox; // Switcher handlers const handleSwitchToWorkspace = (workspaceUid) => { switcherRef.current?.hide(); if (workspaceUid) { dispatch(switchWorkspace(workspaceUid)); } }; const handleSwitchToCollection = (targetCollection) => { switcherRef.current?.hide(); if (!targetCollection?.uid) return; const existingTab = tabs.find((t) => t.collectionUid === targetCollection.uid); if (existingTab) { dispatch(focusTab({ uid: existingTab.uid })); } else { dispatch( addTab({ uid: targetCollection.uid, collectionUid: targetCollection.uid, type: 'collection-settings' }) ); } }; // Collection action handlers const handleRun = () => { dispatch( addTab({ uid: uuid(), collectionUid: collection.uid, type: 'collection-runner' }) ); }; const viewVariables = () => { dispatch( addTab({ uid: uuid(), collectionUid: collection.uid, type: 'variables' }) ); }; const viewCollectionSettings = () => { dispatch( addTab({ uid: collection.uid, collectionUid: collection.uid, type: 'collection-settings' }) ); }; const viewOpenApiSync = () => { dispatch(addTab({ uid: uuid(), collectionUid: collection.uid, type: 'openapi-sync' })); }; // Build overflow menu items for the "..." dropdown const overflowMenuItems = [ { id: 'variables', label: 'Variables', leftSection: IconEye, onClick: viewVariables }, { id: 'collection-settings', label: 'Collection Settings', leftSection: IconSettings, onClick: viewCollectionSettings }, ...(!hasOpenApiSyncConfigured ? [{ id: 'openapi-sync', label: 'OpenAPI Sync', leftSection: () => , onClick: viewOpenApiSync }] : []) ]; // Workspace action handlers (only used when isScratchCollection is true) const handleRenameWorkspaceClick = () => { workspaceActionsRef.current?.hide(); setIsRenamingWorkspace(true); setWorkspaceNameInput(currentWorkspace?.name || ''); setWorkspaceNameError(''); }; const handleCloseWorkspaceClick = () => { workspaceActionsRef.current?.hide(); if (currentWorkspace?.type === 'default') { toast.error('Cannot close the default workspace'); return; } setCloseWorkspaceModalOpen(true); }; const handleShowInFolder = () => { workspaceActionsRef.current?.hide(); const pathname = currentWorkspace?.pathname; if (pathname) { dispatch(showInFolder(pathname)).catch(() => { toast.error('Error opening the folder'); }); } }; const handleExportWorkspace = () => { workspaceActionsRef.current?.hide(); const uid = currentWorkspace?.uid; if (!uid) return; dispatch(exportWorkspaceAction(uid)) .then((result) => { if (!result?.canceled) { toast.success('Workspace exported successfully'); } }) .catch((error) => { toast.error(error?.message || 'Error exporting workspace'); }); }; const validateWorkspaceName = (name) => { const trimmed = name?.trim(); if (!trimmed) { return 'Name is required'; } if (trimmed.length > 255) { return 'Must be 255 characters or less'; } return null; }; const handleSaveWorkspaceRename = () => { const error = validateWorkspaceName(workspaceNameInput); if (error) { setWorkspaceNameError(error); return; } const uid = currentWorkspace?.uid; if (!uid) return; dispatch(renameWorkspaceAction(uid, workspaceNameInput)) .then(() => { toast.success('Workspace renamed!'); setIsRenamingWorkspace(false); setWorkspaceNameInput(''); setWorkspaceNameError(''); }) .catch((err) => { toast.error(err?.message || 'An error occurred while renaming the workspace'); setWorkspaceNameError(err?.message || 'Failed to rename workspace'); }); }; const handleWorkspaceNameChange = (e) => { setWorkspaceNameInput(e.target.value); if (workspaceNameError) { setWorkspaceNameError(''); } }; const handleWorkspaceNameKeyDown = (e) => { if (e.key === 'Enter') { e.preventDefault(); handleSaveWorkspaceRename(); } else if (e.key === 'Escape') { e.preventDefault(); handleCancelWorkspaceRename(); } }; // Check if workspace actions should be shown const showWorkspaceActions = isScratchCollection && currentWorkspace && currentWorkspace.type !== 'default' && !isRenamingWorkspace; return ( {closeWorkspaceModalOpen && currentWorkspace?.uid && ( setCloseWorkspaceModalOpen(false)} /> )}
{/* Left side: Switcher dropdown or rename input */}
{isRenamingWorkspace ? (
{workspaceNameError && ( {workspaceNameError} )}
) : ( document.body} icon={( )} > {/* Workspace section */} {currentWorkspace && ( <>
Workspace
handleSwitchToWorkspace(currentWorkspace.uid)} >
{currentWorkspace.name || 'Untitled Workspace'} {workspaceTabCount > 0 && ( {workspaceTabCount} )}
)} {/* Collections section */} {mountedCollections.length > 0 && ( <>
Collections
{mountedCollections.map((col) => { const colTabCount = getTabCount(col.uid); return (
handleSwitchToCollection(col)} >
{col.name || 'Untitled Collection'} {colTabCount > 0 && ( {colTabCount} )}
); })} )} )} {/* Workspace actions dropdown */} {showWorkspaceActions && ( document.body} icon={} >
Rename
{getRevealInFolderLabel()}
Export
Close
)}
{/* Right side: Actions (only for regular collections) */} {!isScratchCollection && (
{/* OpenAPI Sync - standalone only when configured */} {hasOpenApiSyncConfigured && ( {(hasOpenApiUpdates || hasOpenApiError) && ( )} )} {/* Runner - always visible */} {/* JS Sandbox Mode - always visible */} {/* Overflow menu */} {/* Environment Selector - always visible */}
)}
); }; export default CollectionHeader;