mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-27 14:44:07 +00:00
699 lines
25 KiB
JavaScript
699 lines
25 KiB
JavaScript
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,
|
|
IconFileCode,
|
|
IconFileOff,
|
|
IconTransform
|
|
} from '@tabler/icons';
|
|
import OpenAPISyncIcon from 'components/Icons/OpenAPISync';
|
|
import { switchWorkspace, renameWorkspaceAction, exportWorkspaceAction, confirmWorkspaceCreation, cancelWorkspaceCreation } from 'providers/ReduxStore/slices/workspaces/actions';
|
|
import { updateWorkspace } from 'providers/ReduxStore/slices/workspaces';
|
|
import { showInFolder } from 'providers/ReduxStore/slices/collections/actions';
|
|
import { toggleCollectionFileMode, updateSettingsSelectedTab } from 'providers/ReduxStore/slices/collections';
|
|
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 CreateWorkspace from 'components/WorkspaceSidebar/CreateWorkspace';
|
|
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 { normalizePath } from 'utils/common/path';
|
|
import classNames from 'classnames';
|
|
import StyledWrapper from './StyledWrapper';
|
|
import { useTheme } from 'providers/Theme';
|
|
|
|
const MIGRATE_PILL_DISMISSED_KEY = 'bruno.migrateToYmlPill.dismissed';
|
|
|
|
const readDismissedCollections = () => {
|
|
try {
|
|
const raw = localStorage.getItem(MIGRATE_PILL_DISMISSED_KEY);
|
|
if (!raw) return [];
|
|
const parsed = JSON.parse(raw);
|
|
return Array.isArray(parsed) ? parsed : [];
|
|
} catch {
|
|
return [];
|
|
}
|
|
};
|
|
|
|
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);
|
|
const gitRootPath = collection?.git?.gitRootPath;
|
|
|
|
// Workspace rename state
|
|
const [isRenamingWorkspace, setIsRenamingWorkspace] = useState(false);
|
|
const [workspaceNameInput, setWorkspaceNameInput] = useState('');
|
|
const [workspaceNameError, setWorkspaceNameError] = useState('');
|
|
const [closeWorkspaceModalOpen, setCloseWorkspaceModalOpen] = useState(false);
|
|
const [createWorkspaceModalOpen, setCreateWorkspaceModalOpen] = useState(false);
|
|
|
|
// Migrate-to-YML pill dismissal state (persisted by collection pathname)
|
|
const [migratePillDismissed, setMigratePillDismissed] = useState(true);
|
|
useEffect(() => {
|
|
if (!collection?.pathname) return;
|
|
const dismissed = readDismissedCollections();
|
|
setMigratePillDismissed(dismissed.includes(collection.pathname));
|
|
}, [collection?.pathname]);
|
|
|
|
const dismissMigratePill = (e) => {
|
|
e?.stopPropagation();
|
|
if (!collection?.pathname) return;
|
|
const dismissed = readDismissedCollections();
|
|
if (!dismissed.includes(collection.pathname)) {
|
|
dismissed.push(collection.pathname);
|
|
try {
|
|
localStorage.setItem(MIGRATE_PILL_DISMISSED_KEY, JSON.stringify(dismissed));
|
|
} catch { }
|
|
}
|
|
setMigratePillDismissed(true);
|
|
};
|
|
|
|
const switcherRef = useRef();
|
|
const workspaceActionsRef = useRef();
|
|
const workspaceNameInputRef = useRef(null);
|
|
const workspaceRenameContainerRef = useRef(null);
|
|
const openingAdvancedRef = useRef(false);
|
|
const clickedOutsideRef = useRef(false);
|
|
const handleSaveRef = useRef(null);
|
|
const tempWorkspaceUidRef = useRef(null);
|
|
const isSavingRef = useRef(false);
|
|
|
|
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(() => {
|
|
if (openingAdvancedRef.current) return;
|
|
if (currentWorkspace?.isCreating) {
|
|
dispatch(cancelWorkspaceCreation(currentWorkspace.uid));
|
|
return;
|
|
}
|
|
setIsRenamingWorkspace(false);
|
|
setWorkspaceNameInput('');
|
|
setWorkspaceNameError('');
|
|
}, [currentWorkspace?.isCreating, currentWorkspace?.uid, dispatch]);
|
|
|
|
useEffect(() => {
|
|
if (!isRenamingWorkspace) return;
|
|
|
|
const handleClickOutside = (event) => {
|
|
if (workspaceRenameContainerRef.current && !workspaceRenameContainerRef.current.contains(event.target)) {
|
|
if (currentWorkspace?.isCreating) {
|
|
clickedOutsideRef.current = true;
|
|
handleSaveRef.current?.();
|
|
} else {
|
|
handleCancelWorkspaceRename();
|
|
}
|
|
}
|
|
};
|
|
|
|
document.addEventListener('mousedown', handleClickOutside);
|
|
const timer = setTimeout(() => {
|
|
workspaceNameInputRef.current?.focus();
|
|
workspaceNameInputRef.current?.select();
|
|
}, 50);
|
|
return () => {
|
|
document.removeEventListener('mousedown', handleClickOutside);
|
|
clearTimeout(timer);
|
|
};
|
|
}, [isRenamingWorkspace, handleCancelWorkspaceRename, currentWorkspace?.isCreating]);
|
|
|
|
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) => normalizePath(c.pathname) === normalizePath(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 viewMigrationSettings = () => {
|
|
dispatch(
|
|
addTab({
|
|
uid: collection.uid,
|
|
collectionUid: collection.uid,
|
|
type: 'collection-settings'
|
|
})
|
|
);
|
|
dispatch(updateSettingsSelectedTab({ collectionUid: collection.uid, tab: 'overview' }));
|
|
};
|
|
|
|
const viewOpenApiSync = () => {
|
|
dispatch(addTab({
|
|
uid: uuid(),
|
|
collectionUid: collection.uid,
|
|
type: 'openapi-sync'
|
|
}));
|
|
};
|
|
|
|
const handleFileModeClick = () => {
|
|
dispatch(
|
|
toggleCollectionFileMode({
|
|
collectionUid: collection.uid
|
|
})
|
|
);
|
|
};
|
|
|
|
// Build overflow menu items for the "..." dropdown
|
|
const overflowMenuItems = [
|
|
{ id: 'variables', label: 'Variables', leftSection: IconEye, onClick: viewVariables },
|
|
{ id: 'file-mode', label: collection.fileMode ? 'Switch to Code Mode' : 'Switch to File Mode', leftSection: collection.fileMode ? IconFileOff : IconFileCode, onClick: handleFileModeClick },
|
|
...(!hasOpenApiSyncConfigured
|
|
? [{ id: 'openapi-sync', label: 'OpenAPI', leftSection: OpenAPISyncIcon, onClick: viewOpenApiSync }]
|
|
: []),
|
|
{ id: 'collection-settings', label: 'Collection Settings', leftSection: IconSettings, onClick: viewCollectionSettings }
|
|
];
|
|
|
|
// 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 fromOutside = clickedOutsideRef.current;
|
|
clickedOutsideRef.current = false;
|
|
|
|
if (openingAdvancedRef.current) return;
|
|
if (isSavingRef.current) return;
|
|
|
|
const trimmedName = workspaceNameInput?.trim();
|
|
if (!trimmedName) {
|
|
if (fromOutside && currentWorkspace?.isCreating) {
|
|
dispatch(cancelWorkspaceCreation(currentWorkspace.uid));
|
|
return;
|
|
}
|
|
setWorkspaceNameError('Name is required');
|
|
return;
|
|
}
|
|
|
|
const error = validateWorkspaceName(workspaceNameInput);
|
|
if (error) {
|
|
setWorkspaceNameError(error);
|
|
if (fromOutside && currentWorkspace?.isCreating) {
|
|
dispatch(cancelWorkspaceCreation(currentWorkspace.uid));
|
|
}
|
|
return;
|
|
}
|
|
|
|
const uid = currentWorkspace?.uid;
|
|
if (!uid) return;
|
|
|
|
isSavingRef.current = true;
|
|
|
|
if (currentWorkspace?.isCreating) {
|
|
dispatch(confirmWorkspaceCreation(uid, trimmedName))
|
|
.then(() => {
|
|
setIsRenamingWorkspace(false);
|
|
setWorkspaceNameInput('');
|
|
setWorkspaceNameError('');
|
|
toast.success('Workspace created!');
|
|
})
|
|
.catch((err) => {
|
|
toast.error(err?.message || 'An error occurred while creating the workspace');
|
|
})
|
|
.finally(() => {
|
|
isSavingRef.current = false;
|
|
});
|
|
} else {
|
|
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');
|
|
})
|
|
.finally(() => {
|
|
isSavingRef.current = false;
|
|
});
|
|
}
|
|
};
|
|
|
|
// Keep ref in sync so click-outside handler always has the latest save logic
|
|
handleSaveRef.current = handleSaveWorkspaceRename;
|
|
|
|
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();
|
|
}
|
|
};
|
|
|
|
const handleOpenAdvancedCreate = () => {
|
|
openingAdvancedRef.current = true;
|
|
tempWorkspaceUidRef.current = currentWorkspace?.isCreating ? currentWorkspace.uid : null;
|
|
setCreateWorkspaceModalOpen(true);
|
|
};
|
|
|
|
const handleAdvancedCreateClose = () => {
|
|
openingAdvancedRef.current = false;
|
|
setCreateWorkspaceModalOpen(false);
|
|
setIsRenamingWorkspace(false);
|
|
setWorkspaceNameInput('');
|
|
setWorkspaceNameError('');
|
|
const tempUid = tempWorkspaceUidRef.current;
|
|
tempWorkspaceUidRef.current = null;
|
|
// Clean up the temp workspace (cancelWorkspaceCreation only switches to default
|
|
// if the temp workspace was still active, so this is safe after modal success too)
|
|
if (tempUid) {
|
|
dispatch(cancelWorkspaceCreation(tempUid));
|
|
}
|
|
};
|
|
|
|
// Check if workspace actions should be shown
|
|
const showWorkspaceActions = isScratchCollection
|
|
&& currentWorkspace
|
|
&& currentWorkspace.type !== 'default'
|
|
&& !isRenamingWorkspace;
|
|
|
|
const handleDisplayIconClick = (e) => {
|
|
const uid = isScratchCollection ? `${collection.uid}-overview` : collection.uid;
|
|
const type = isScratchCollection ? 'workspaceOverview' : 'collection-settings';
|
|
dispatch(addTab({
|
|
uid: uid,
|
|
collectionUid: collection.uid,
|
|
type: type
|
|
}));
|
|
};
|
|
|
|
return (
|
|
<StyledWrapper>
|
|
{closeWorkspaceModalOpen && currentWorkspace?.uid && (
|
|
<CloseWorkspace
|
|
workspaceUid={currentWorkspace.uid}
|
|
onClose={() => setCloseWorkspaceModalOpen(false)}
|
|
/>
|
|
)}
|
|
|
|
{createWorkspaceModalOpen && (
|
|
<CreateWorkspace onClose={handleAdvancedCreateClose} />
|
|
)}
|
|
|
|
<div className="flex items-center justify-between gap-2 py-2 px-4">
|
|
{/* Left side: Switcher dropdown or rename input */}
|
|
<div className="collection-switcher">
|
|
{isRenamingWorkspace ? (
|
|
<div className="workspace-rename-container" ref={workspaceRenameContainerRef}>
|
|
<DisplayIcon size={18} strokeWidth={1.5} className="cursor-pointer display-icon" />
|
|
<div className="workspace-input-wrapper">
|
|
<input
|
|
ref={workspaceNameInputRef}
|
|
type="text"
|
|
className="workspace-name-input"
|
|
value={workspaceNameInput}
|
|
onChange={handleWorkspaceNameChange}
|
|
onKeyDown={handleWorkspaceNameKeyDown}
|
|
autoComplete="off"
|
|
autoCorrect="off"
|
|
autoCapitalize="off"
|
|
spellCheck="false"
|
|
/>
|
|
{currentWorkspace?.isCreating && (
|
|
<button
|
|
className="cog-btn"
|
|
onMouseDown={(e) => e.preventDefault()}
|
|
onClick={handleOpenAdvancedCreate}
|
|
title="Advanced options"
|
|
>
|
|
<IconSettings size={13} strokeWidth={1.5} />
|
|
</button>
|
|
)}
|
|
</div>
|
|
<div className="inline-actions">
|
|
<button
|
|
className="inline-action-btn save"
|
|
onClick={handleSaveWorkspaceRename}
|
|
onMouseDown={(e) => e.preventDefault()}
|
|
title={currentWorkspace?.isCreating ? 'Create' : 'Save'}
|
|
>
|
|
<IconCheck size={14} strokeWidth={2} />
|
|
</button>
|
|
<button
|
|
className="inline-action-btn cancel"
|
|
onClick={handleCancelWorkspaceRename}
|
|
onMouseDown={(e) => e.preventDefault()}
|
|
title="Cancel"
|
|
>
|
|
<IconX size={14} strokeWidth={2} />
|
|
</button>
|
|
</div>
|
|
{workspaceNameError && (
|
|
<span className="workspace-error">{workspaceNameError}</span>
|
|
)}
|
|
</div>
|
|
) : (
|
|
<div className="flex flex-row justify-center items-center gap-x-1">
|
|
<DisplayIcon size={18} strokeWidth={1.5} className="cursor-pointer display-icon" onClick={handleDisplayIconClick} />
|
|
<Dropdown
|
|
placement="bottom-start"
|
|
onCreate={onSwitcherCreate}
|
|
appendTo={() => document.body}
|
|
icon={(
|
|
<button className="switcher-trigger">
|
|
<span className={classNames('switcher-name', { 'scratch-collection': isScratchCollection })}>{displayName}</span>
|
|
<IconChevronDown size={14} strokeWidth={1.5} className="chevron" />
|
|
</button>
|
|
)}
|
|
>
|
|
<div className="max-w-124 overflow-hidden">
|
|
{currentWorkspace && (
|
|
<>
|
|
<div className="label-item">Workspace</div>
|
|
<div
|
|
className={classNames('dropdown-item', {
|
|
'dropdown-item-active': isScratchCollection
|
|
})}
|
|
onClick={() => handleSwitchToWorkspace(currentWorkspace.uid)}
|
|
>
|
|
<div className="dropdown-icon">
|
|
<IconCategory size={16} strokeWidth={1.5} />
|
|
</div>
|
|
<span className="dropdown-label collection-header-dropdown-label">
|
|
{currentWorkspace.name || 'Untitled Workspace'}
|
|
</span>
|
|
{workspaceTabCount > 0 && (
|
|
<span className="dropdown-tab-count">{workspaceTabCount}</span>
|
|
)}
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{mountedCollections.length > 0 && (
|
|
<>
|
|
<div className="dropdown-separator" />
|
|
<div className="label-item">Collections</div>
|
|
{mountedCollections.map((col) => {
|
|
const colTabCount = getTabCount(col.uid);
|
|
return (
|
|
<div
|
|
key={col.uid}
|
|
className={classNames('dropdown-item', {
|
|
'dropdown-item-active': !isScratchCollection && collection.uid === col.uid
|
|
})}
|
|
onClick={() => handleSwitchToCollection(col)}
|
|
>
|
|
<div className="dropdown-icon">
|
|
<IconBox size={16} strokeWidth={1.5} />
|
|
</div>
|
|
<span className="dropdown-label collection-header-dropdown-label">{col.name || 'Untitled Collection'}</span>
|
|
{colTabCount > 0 && (
|
|
<span className="dropdown-tab-count">{colTabCount}</span>
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
</>
|
|
)}
|
|
</div>
|
|
</Dropdown>
|
|
</div>
|
|
)}
|
|
|
|
{/* Workspace actions dropdown */}
|
|
{showWorkspaceActions && (
|
|
<Dropdown
|
|
placement="bottom-start"
|
|
onCreate={onWorkspaceActionsCreate}
|
|
appendTo={() => document.body}
|
|
icon={<IconDots size={18} strokeWidth={1.5} className="workspace-actions-trigger" />}
|
|
>
|
|
<div className="dropdown-item" onClick={handleRenameWorkspaceClick}>
|
|
<div className="dropdown-icon">
|
|
<IconEdit size={16} strokeWidth={1.5} />
|
|
</div>
|
|
<span>Rename</span>
|
|
</div>
|
|
<div className="dropdown-item" onClick={handleShowInFolder}>
|
|
<div className="dropdown-icon">
|
|
<IconFolder size={16} strokeWidth={1.5} />
|
|
</div>
|
|
<span>{getRevealInFolderLabel()}</span>
|
|
</div>
|
|
<div className="dropdown-item" onClick={handleExportWorkspace}>
|
|
<div className="dropdown-icon">
|
|
<IconUpload size={16} strokeWidth={1.5} />
|
|
</div>
|
|
<span>Export</span>
|
|
</div>
|
|
<div className="dropdown-item" onClick={handleCloseWorkspaceClick}>
|
|
<div className="dropdown-icon">
|
|
<IconX size={16} strokeWidth={1.5} />
|
|
</div>
|
|
<span>Close</span>
|
|
</div>
|
|
</Dropdown>
|
|
)}
|
|
</div>
|
|
|
|
{/* Right side: Actions (only for regular collections) */}
|
|
{!isScratchCollection && (
|
|
<div className="flex flex-grow gap-1.5 items-center justify-end">
|
|
{collection.format === 'bru' && !migratePillDismissed && (
|
|
<div
|
|
className="migrate-yml-pill"
|
|
data-testid="migrate-yml-pill"
|
|
title="Migrate this collection to YML"
|
|
>
|
|
<button
|
|
type="button"
|
|
className="pill-main"
|
|
onClick={viewMigrationSettings}
|
|
>
|
|
<IconTransform size={13} strokeWidth={1.5} />
|
|
<span className="pill-label">Migrate to YML</span>
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className="pill-dismiss"
|
|
onClick={dismissMigratePill}
|
|
aria-label="Dismiss"
|
|
data-testid="migrate-yml-pill-dismiss"
|
|
>
|
|
<IconX size={12} strokeWidth={2} />
|
|
</button>
|
|
</div>
|
|
)}
|
|
{/* OpenAPI Sync - standalone only when configured and beta enabled */}
|
|
{hasOpenApiSyncConfigured && (
|
|
<ToolHint
|
|
text={hasOpenApiError ? 'OpenAPI Error' : hasOpenApiUpdates ? 'OpenAPI Updates Available' : 'OpenAPI'}
|
|
toolhintId="OpenApiSyncToolhintId"
|
|
place="bottom"
|
|
>
|
|
<ActionIcon onClick={viewOpenApiSync} aria-label="OpenAPI" size="sm" className="relative">
|
|
<OpenAPISyncIcon size={15} />
|
|
{(hasOpenApiUpdates || hasOpenApiError) && (
|
|
<span className="absolute top-0 right-0 w-1.5 h-1.5 rounded-full" style={{ backgroundColor: hasOpenApiError ? theme.status.danger.text : theme.status.warning.text }} />
|
|
)}
|
|
</ActionIcon>
|
|
</ToolHint>
|
|
)}
|
|
{/* Runner - always visible */}
|
|
<ToolHint text="Runner" toolhintId="RunnerToolhintId" place="bottom">
|
|
<ActionIcon onClick={handleRun} aria-label="Runner" size="sm" data-testid="runner">
|
|
<IconRun size={16} strokeWidth={1.5} />
|
|
</ActionIcon>
|
|
</ToolHint>
|
|
{/* JS Sandbox Mode - always visible */}
|
|
<JsSandboxMode collection={collection} />
|
|
{/* Overflow menu */}
|
|
<MenuDropdown items={overflowMenuItems} placement="bottom-end" data-testid="more-actions">
|
|
<ActionIcon label="More actions" size="sm" style={{ border: `1px solid ${theme.border.border1}`, borderRadius: theme.border.radius.base, width: 24, marginRight: 4, marginLeft: 4 }}>
|
|
<IconDots size={16} strokeWidth={1.5} />
|
|
</ActionIcon>
|
|
</MenuDropdown>
|
|
{/* Environment Selector - always visible */}
|
|
<span>
|
|
<EnvironmentSelector collection={collection} />
|
|
</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</StyledWrapper>
|
|
);
|
|
};
|
|
|
|
export default CollectionHeader;
|