diff --git a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.tsx
index 91b10e80ef..9ff15fe1cb 100644
--- a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.tsx
+++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.tsx
@@ -182,7 +182,7 @@ const Component = ({ loaderData }: Route.ComponentProps) => {
{activeProject && models.project.isRemoteProject(activeProject) && }
-
+
diff --git a/packages/insomnia/src/ui/components/dropdowns/sidebar-workspace-dropdown.tsx b/packages/insomnia/src/ui/components/dropdowns/sidebar-workspace-dropdown.tsx
new file mode 100644
index 0000000000..034252955f
--- /dev/null
+++ b/packages/insomnia/src/ui/components/dropdowns/sidebar-workspace-dropdown.tsx
@@ -0,0 +1,462 @@
+import type { IconName, IconProp } from '@fortawesome/fontawesome-svg-core';
+import {
+ exportGlobalEnvironmentToFile,
+ exportMcpClientToFile,
+ exportMockServerToFile,
+} from 'insomnia/src/ui/components/settings/import-export';
+import React, { Fragment, useState } from 'react';
+import {
+ Button,
+ Collection,
+ Dialog,
+ Header,
+ Heading,
+ Label,
+ Menu,
+ MenuItem,
+ MenuSection,
+ MenuTrigger,
+ Modal,
+ ModalOverlay,
+ Popover,
+ Radio,
+ RadioGroup,
+} from 'react-aria-components';
+import { href } from 'react-router';
+
+import type { Project, Workspace } from '~/insomnia-data';
+import { models } from '~/insomnia-data';
+import { useRequestNewActionFetcher } from '~/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.new';
+import { useRequestGroupNewActionFetcher } from '~/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request-group.new';
+import { useWorkspaceDeleteActionFetcher } from '~/routes/organization.$organizationId.project.$projectId.workspace.delete';
+import { useWorkspaceUpdateActionFetcher } from '~/routes/organization.$organizationId.project.$projectId.workspace.update';
+import { useTabNavigate } from '~/ui/hooks/use-insomnia-tab';
+import type { CreateRequestType } from '~/ui/hooks/use-request';
+
+import { getProductName } from '../../../common/constants';
+import { getWorkspaceLabel } from '../../../common/get-workspace-label';
+import type { PlatformKeyCombinations } from '../../../common/settings';
+import { SegmentEvent } from '../../analytics';
+import { DropdownHint } from '../base/dropdown/dropdown-hint';
+import { Icon } from '../icon';
+import { showModal } from '../modals';
+import { ExportRequestsModal } from '../modals/export-requests-modal';
+import { ImportModal } from '../modals/import-modal/import-modal';
+import { PasteCurlModal } from '../modals/paste-curl-modal';
+import { PromptModal } from '../modals/prompt-modal';
+import { WorkspaceDuplicateModal } from '../modals/workspace-duplicate-modal';
+import { WorkspaceSettingsModal } from '../modals/workspace-settings-modal';
+
+interface Props {
+ workspace: Workspace;
+ project: Project;
+ organizationId: string;
+}
+
+interface ActionItem {
+ id: string;
+ name: string;
+ icon: IconName;
+ hint?: PlatformKeyCombinations;
+ action: () => void;
+ className?: string;
+}
+
+interface ActionSection {
+ name: string;
+ id: string;
+ icon: IconProp;
+ items: ActionItem[];
+}
+
+export const SidebarWorkspaceDropdown = ({ workspace, project, organizationId }: Props) => {
+ const projectId = project._id;
+ const workspaceId = workspace._id;
+
+ const [isDuplicateModalOpen, setIsDuplicateModalOpen] = useState(false);
+ const [isImportModalOpen, setIsImportModalOpen] = useState(false);
+ const [isExportModalOpen, setIsExportModalOpen] = useState(false);
+ const [isSettingsModalOpen, setIsSettingsModalOpen] = useState(false);
+ const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
+ const [isPasteCurlModalOpen, setPasteCurlModalOpen] = useState(false);
+
+ const updateWorkspaceFetcher = useWorkspaceUpdateActionFetcher();
+ const deleteWorkspaceFetcher = useWorkspaceDeleteActionFetcher();
+ const newRequestFetcher = useRequestNewActionFetcher();
+ const newRequestGroupFetcher = useRequestGroupNewActionFetcher();
+
+ const tabNavigate = useTabNavigate();
+
+ const workspaceName = workspace.name;
+ const projectName = project.name || getProductName();
+ const isCollection = workspace.scope === 'collection';
+
+ const createRequest = (requestType: CreateRequestType) => {
+ newRequestFetcher.submit({
+ organizationId,
+ projectId,
+ workspaceId,
+ requestType,
+ parentId: workspaceId,
+ });
+ };
+
+ const openInNewTab = (isRunner = false) => {
+ tabNavigate(
+ { organization: organizationId, project, workspace, item: workspace },
+ isRunner ? { shouldNavigate: true, asRunner: true } : { withTab: true, shouldNavigate: true },
+ );
+ };
+
+ const createSections: ActionSection[] = isCollection
+ ? [
+ {
+ name: 'Create',
+ id: 'create',
+ icon: 'plus',
+ items: [
+ {
+ id: 'New Folder',
+ name: 'New Folder',
+ icon: 'folder',
+ action: () =>
+ showModal(PromptModal, {
+ title: 'New Folder',
+ defaultValue: 'My Folder',
+ submitName: 'Create',
+ label: 'Name',
+ selectText: true,
+ onComplete: (name: string) =>
+ newRequestGroupFetcher.submit({
+ organizationId,
+ projectId,
+ workspaceId,
+ parentId: workspaceId,
+ name,
+ }),
+ }),
+ },
+ {
+ id: 'HTTP',
+ name: 'HTTP Request',
+ icon: 'plus-circle',
+ action: () => createRequest('HTTP'),
+ },
+ {
+ id: 'Event Stream',
+ name: 'Event Stream Request (SSE)',
+ icon: 'plus-circle',
+ action: () => createRequest('Event Stream'),
+ },
+ {
+ id: 'GraphQL Request',
+ name: 'GraphQL Request',
+ icon: 'plus-circle',
+ action: () => createRequest('GraphQL'),
+ },
+ {
+ id: 'gRPC Request',
+ name: 'gRPC Request',
+ icon: 'plus-circle',
+ action: () => createRequest('gRPC'),
+ },
+ {
+ id: 'WebSocket Request',
+ name: 'WebSocket Request',
+ icon: 'plus-circle',
+ action: () => createRequest('WebSocket'),
+ },
+ {
+ id: 'Socket.IO Request',
+ name: 'Socket.IO Request',
+ icon: 'plus-circle',
+ action: () => createRequest('SocketIO'),
+ },
+ ],
+ },
+ {
+ name: 'Import',
+ id: 'import-create',
+ icon: 'file-import',
+ items: [
+ {
+ id: 'From Curl',
+ name: 'From Curl',
+ icon: 'terminal',
+ action: () => setPasteCurlModalOpen(true),
+ },
+ {
+ id: 'from-file',
+ name: 'From File',
+ icon: 'file-import',
+ action: () => setIsImportModalOpen(true),
+ },
+ ],
+ },
+ {
+ name: 'Run',
+ id: 'run',
+ icon: 'circle-play',
+ items: [
+ {
+ id: 'RunCollection',
+ name: 'Run Collection',
+ icon: 'circle-play',
+ action: () => openInNewTab(true),
+ },
+ ],
+ },
+ ]
+ : [];
+
+ const actionSection: ActionSection = {
+ name: 'Actions',
+ id: 'actions',
+ icon: 'cog',
+ items: [
+ {
+ id: 'OpenInNewTab',
+ name: 'Open in New Tab',
+ icon: 'external-link-alt',
+ action: openInNewTab,
+ },
+ ...(!models.workspace.isMcp(workspace)
+ ? [
+ {
+ id: 'Duplicate',
+ name: 'Duplicate / Move',
+ icon: 'copy' as IconName,
+ action: () => setIsDuplicateModalOpen(true),
+ },
+ ]
+ : []),
+ {
+ id: 'Rename',
+ name: 'Rename',
+ icon: 'pen-to-square' as IconName,
+ action: () =>
+ showModal(PromptModal, {
+ title: `Rename ${getWorkspaceLabel(workspace).singular}`,
+ defaultValue: workspaceName,
+ submitName: 'Rename',
+ selectText: true,
+ label: 'Name',
+ onComplete: (name: string) =>
+ updateWorkspaceFetcher.submit({
+ organizationId,
+ projectId,
+ patch: { name, workspaceId },
+ }),
+ }),
+ },
+ {
+ id: 'Export',
+ name: 'Export',
+ icon: 'file-export',
+ action: () => {
+ window.main.trackSegmentEvent({
+ event: SegmentEvent.exportStarted,
+ properties: { source: `${workspace.scope}-list` },
+ });
+ if (workspace.scope === 'mock-server') {
+ return exportMockServerToFile(workspace);
+ }
+ if (workspace.scope === 'environment') {
+ return exportGlobalEnvironmentToFile(workspace);
+ }
+ if (workspace.scope === 'mcp') {
+ return exportMcpClientToFile(workspace);
+ }
+ return setIsExportModalOpen(true);
+ },
+ },
+ {
+ id: 'Settings',
+ name: 'Settings',
+ icon: 'gear',
+ action: () => setIsSettingsModalOpen(true),
+ },
+ {
+ id: 'Delete',
+ name: 'Delete',
+ icon: 'trash',
+ className: 'text-(--color-danger)',
+ action: () => setIsDeleteModalOpen(true),
+ },
+ ],
+ };
+
+ const allSections: ActionSection[] = [...createSections, actionSection];
+
+ return (
+
+
+
+
+
+
+
+
+ {isDuplicateModalOpen && (
+ setIsDuplicateModalOpen(false)} />
+ )}
+ {isImportModalOpen && (
+ setIsImportModalOpen(false)}
+ from={{ type: 'file' }}
+ projectName={projectName}
+ workspaceName={workspaceName}
+ organizationId={organizationId}
+ defaultProjectId={projectId}
+ defaultWorkspaceId={workspaceId}
+ />
+ )}
+ {isExportModalOpen && (
+ setIsExportModalOpen(false)} />
+ )}
+ {isSettingsModalOpen && (
+ setIsSettingsModalOpen(false)} />
+ )}
+ {isDeleteModalOpen && (
+ setIsDeleteModalOpen(false)}
+ isDismissable
+ className="fixed top-0 left-0 z-10 flex h-(--visual-viewport-height) w-full items-center justify-center bg-black/30"
+ >
+ setIsDeleteModalOpen(false)}
+ className="max-h-full w-full max-w-2xl rounded-md border border-solid border-(--hl-sm) bg-(--color-bg) p-(--padding-lg) text-(--color-font)"
+ >
+
+
+
+ )}
+ {isPasteCurlModalOpen && (
+ {
+ newRequestFetcher.submit({
+ organizationId,
+ projectId,
+ workspaceId,
+ requestType: 'From Curl',
+ parentId: workspaceId,
+ req,
+ });
+ }}
+ onHide={() => setPasteCurlModalOpen(false)}
+ />
+ )}
+
+ );
+};
diff --git a/packages/insomnia/src/ui/components/sidebar/project-navigation-sidebar/workspace-node.tsx b/packages/insomnia/src/ui/components/sidebar/project-navigation-sidebar/workspace-node.tsx
index 8ecf2ffc6a..d5db82d803 100644
--- a/packages/insomnia/src/ui/components/sidebar/project-navigation-sidebar/workspace-node.tsx
+++ b/packages/insomnia/src/ui/components/sidebar/project-navigation-sidebar/workspace-node.tsx
@@ -1,6 +1,7 @@
import { Button } from 'react-aria-components';
import { scopeToIconMap } from '~/common/get-workspace-label';
+import { SidebarWorkspaceDropdown } from '~/ui/components/dropdowns/sidebar-workspace-dropdown';
import { Icon } from '../../icon';
import {
@@ -18,7 +19,7 @@ interface WorkspaceNodeProps {
}
export const WorkspaceNode = ({ item, onToggle }: WorkspaceNodeProps) => {
- const { doc, collapsed } = item;
+ const { doc, collapsed, project, organizationId } = item;
const { name: workspaceName, _id: workspaceId, scope: workspaceScope } = doc;
return (
@@ -38,6 +39,9 @@ export const WorkspaceNode = ({ item, onToggle }: WorkspaceNodeProps) => {
{workspaceName}
+
+
+
);
};