diff --git a/packages/insomnia/src/ui/components/dropdowns/auth-dropdown.tsx b/packages/insomnia/src/ui/components/dropdowns/auth-dropdown.tsx index e1dc3f53dc..f4809ef7d5 100644 --- a/packages/insomnia/src/ui/components/dropdowns/auth-dropdown.tsx +++ b/packages/insomnia/src/ui/components/dropdowns/auth-dropdown.tsx @@ -211,27 +211,27 @@ export const AuthDropdown: FC = ({ authentication, authTypes = defaultTyp name: string; }[]; }[] = [ - { - id: 'Auth Types', - name: 'Auth Types', - icon: 'lock', - items: authTypesItems.filter(item => authTypes.includes(item.id)), - }, { id: 'Other', name: 'Other', icon: 'ellipsis-h', items: [ - { - id: 'none', - name: 'None', - }, { id: 'inherit', name: 'Inherit from parent', }, + { + id: 'none', + name: 'None', + }, ], }, + { + id: 'Auth Types', + name: 'Auth Types', + icon: 'lock', + items: authTypesItems.filter(item => authTypes.includes(item.id)), + }, ]; return ( diff --git a/packages/insomnia/src/ui/components/dropdowns/request-actions-dropdown.tsx b/packages/insomnia/src/ui/components/dropdowns/request-actions-dropdown.tsx index f4b09b763f..9793b6044a 100644 --- a/packages/insomnia/src/ui/components/dropdowns/request-actions-dropdown.tsx +++ b/packages/insomnia/src/ui/components/dropdowns/request-actions-dropdown.tsx @@ -1,6 +1,6 @@ import { IconName } from '@fortawesome/fontawesome-svg-core'; import React, { Fragment, useCallback, useState } from 'react'; -import { Button, Menu, MenuItem, MenuTrigger, Popover } from 'react-aria-components'; +import { Button, Collection, Header, Menu, MenuItem, MenuTrigger, Popover, Section } from 'react-aria-components'; import { useFetcher, useParams } from 'react-router-dom'; import { exportHarRequest } from '../../../common/har'; @@ -20,6 +20,7 @@ import { getRequestActions } from '../../../plugins'; import * as pluginContexts from '../../../plugins/context/index'; import { useRequestMetaPatcher, useRequestPatcher } from '../../hooks/use-request'; import { useRootLoaderData } from '../../routes/root'; +import { DropdownHint } from '../base/dropdown/dropdown-hint'; import { Icon } from '../icon'; import { showError, showModal, showPrompt } from '../modals'; import { AlertModal } from '../modals/alert-modal'; @@ -48,7 +49,6 @@ export const RequestActionsDropdown = ({ const patchRequest = useRequestPatcher(); const { hotKeyRegistry } = settings; const [actionPlugins, setActionPlugins] = useState([]); - const [loadingActions, setLoadingActions] = useState>({}); const requestFetcher = useFetcher(); const { organizationId, projectId, workspaceId } = useParams() as { organizationId: string; projectId: string; workspaceId: string }; @@ -79,9 +79,7 @@ export const RequestActionsDropdown = ({ }); }; - const handlePluginClick = async ({ plugin, action, label }: RequestAction) => { - setLoadingActions({ ...loadingActions, [label]: true }); - + const handlePluginClick = async ({ plugin, action }: RequestAction) => { try { const context = { ...(pluginContexts.app.init(RENDER_PURPOSE_NO_RENDER)), @@ -98,7 +96,6 @@ export const RequestActionsDropdown = ({ error, }); } - setLoadingActions({ ...loadingActions, [label]: false }); }; const generateCode = () => { @@ -164,113 +161,160 @@ export const RequestActionsDropdown = ({ const canGenerateCode = isRequest(request); const codeGenerationActions: { - id: string; name: string; + id: string; icon: IconName; - hint?: PlatformKeyCombinations; - action: () => void; - }[] = canGenerateCode ? [{ - id: 'GenerateCode', - name: 'Generate Code', - action: generateCode, - icon: 'code', - hint: hotKeyRegistry.request_showGenerateCodeEditor, - }, { - id: 'CopyAsCurl', - name: 'Copy as cURL', - action: copyAsCurl, - icon: 'copy', - }] : []; + items: { + id: string; + name: string; + icon: IconName; + hint?: PlatformKeyCombinations; + action: () => void; + }[]; + }[] = !canGenerateCode ? [] : + [{ + name: 'Export', + id: 'export', + icon: 'file-export', + items: [ + { + id: 'GenerateCode', + name: 'Generate Code', + action: generateCode, + icon: 'code', + hint: hotKeyRegistry.request_showGenerateCodeEditor, + }, + { + id: 'CopyAsCurl', + name: 'Copy as cURL', + action: copyAsCurl, + icon: 'copy', + }, + ], + }]; const requestActionList: { - id: string; name: string; + id: string; icon: IconName; - hint?: PlatformKeyCombinations; - action: () => void; + items: { + id: string; + name: string; + icon: IconName; + hint?: PlatformKeyCombinations; + action: () => void; + }[]; }[] = [ - { - id: 'Duplicate', - name: 'Duplicate', - action: handleDuplicateRequest, - icon: 'copy', - }, - { - id: 'Rename', - name: 'Rename', - action: handleRename, - icon: 'edit', - }, - { - id: 'Delete', - name: 'Delete', - action: deleteRequest, - icon: 'trash', - }, - { - id: 'Pin', - name: isPinned ? 'Unpin' : 'Pin', - action: togglePin, - icon: 'thumbtack', - }, ...codeGenerationActions, - ...actionPlugins.map((plugin: RequestAction) => ({ - id: plugin.label, - name: plugin.label, - icon: loadingActions[plugin.label] ? 'refresh' : plugin.icon as IconName || 'plug', - action: () => handlePluginClick(plugin), - })), { - id: 'Settings', - name: 'Settings', - icon: 'gear', - hint: hotKeyRegistry.request_showSettings, - action: () => { - setIsSettingsModalOpen(true); - }, + name: 'Actions', + id: 'actions', + icon: 'cog', + items: [ + { + id: 'Pin', + name: isPinned ? 'Unpin' : 'Pin', + action: togglePin, + icon: 'thumbtack', + hint: hotKeyRegistry.request_togglePin, + }, + { + id: 'Duplicate', + name: 'Duplicate', + action: handleDuplicateRequest, + icon: 'copy', + hint: hotKeyRegistry.request_showDuplicate, + }, + { + id: 'Rename', + name: 'Rename', + action: handleRename, + icon: 'edit', + }, + { + id: 'Delete', + name: 'Delete', + action: deleteRequest, + icon: 'trash', + hint: hotKeyRegistry.request_showDelete, + }, + { + id: 'Settings', + name: 'Settings', + icon: 'gear', + hint: hotKeyRegistry.request_showSettings, + action: () => { + setIsSettingsModalOpen(true); + }, + }, + ], }, + ...(actionPlugins.length > 0 ? [ + { + name: 'Plugins', + id: 'plugins', + icon: 'plug' as IconName, + items: actionPlugins.map(plugin => ({ + id: plugin.label, + name: plugin.label, + icon: plugin.icon as IconName || 'plug', + action: () => + handlePluginClick(plugin), + })), + }, + ] : []), ]; return ( - isOpen && onOpen()}> - - - - requestActionList.find(({ id }) => key === id)?.action() - } - items={requestActionList} - className="border select-none text-sm min-w-max border-solid border-[--hl-sm] shadow-lg bg-[--color-bg] py-2 rounded-md overflow-y-auto max-h-[85vh] focus:outline-none" + isOpen && onOpen()}> + - - - {isSettingsModalOpen && ( - setIsSettingsModalOpen(false)} - /> - )} - + + + + requestActionList.find(i => i.items.find(a => a.id === key))?.items.find(a => a.id === key)?.action()} + items={requestActionList} + className="border select-none text-sm min-w-max border-solid border-[--hl-sm] shadow-lg bg-[--color-bg] py-2 rounded-md overflow-y-auto max-h-[85vh] focus:outline-none" + > + {section => ( +
+
+ {section.name} +
+ + {item => ( + + + {item.name} + {item.hint && ()} + + )} + +
+ )} +
+
+ + { + isSettingsModalOpen && ( + setIsSettingsModalOpen(false)} + /> + ) + } + ); }; diff --git a/packages/insomnia/src/ui/components/dropdowns/request-group-actions-dropdown.tsx b/packages/insomnia/src/ui/components/dropdowns/request-group-actions-dropdown.tsx index 7d8932f8a1..e68eb712e2 100644 --- a/packages/insomnia/src/ui/components/dropdowns/request-group-actions-dropdown.tsx +++ b/packages/insomnia/src/ui/components/dropdowns/request-group-actions-dropdown.tsx @@ -16,6 +16,7 @@ import { CreateRequestType, useRequestGroupPatcher } from '../../hooks/use-reque import { useRootLoaderData } from '../../routes/root'; import { WorkspaceLoaderData } from '../../routes/workspace'; import { type DropdownHandle, type DropdownProps } from '../base/dropdown'; +import { DropdownHint } from '../base/dropdown/dropdown-hint'; import { Icon } from '../icon'; import { showError, showModal, showPrompt } from '../modals'; import { AskModal } from '../modals/ask-modal'; @@ -151,96 +152,102 @@ export const RequestGroupActionsDropdown = ({ hint?: PlatformKeyCombinations; action: () => void; }[]; - })[] = [ + })[] = + [ { - name: 'Create', - id: 'create', - icon: 'plus', - items: [ - { - id: 'HTTP', - name: 'HTTP Request', - icon: 'plus-circle', - hint: hotKeyRegistry.request_createHTTP, - action: () => createRequest({ - requestType: 'HTTP', - parentId: requestGroup._id, - }), - }, - { - id: 'Event Stream', - name: 'Event Stream Request', - icon: 'plus-circle', - action: () => createRequest({ - requestType: 'Event Stream', - parentId: requestGroup._id, - }), - }, - { - id: 'GraphQL Request', - name: 'GraphQL Request', - icon: 'plus-circle', - action: () => createRequest({ - requestType: 'GraphQL', - parentId: requestGroup._id, - }), - }, - { - id: 'gRPC Request', - name: 'gRPC Request', - icon: 'plus-circle', - action: () => createRequest({ - requestType: 'gRPC', - parentId: requestGroup._id, - }), - }, - { - id: 'WebSocket Request', - name: 'WebSocket Request', - icon: 'plus-circle', - action: () => createRequest({ - requestType: 'WebSocket', - parentId: requestGroup._id, - }), - }, - { - id: 'From Curl', - name: 'From Curl', - icon: 'terminal', - action: () => setPasteCurlModalOpen(true), - - }, - { - id: 'New Folder', - name: 'New Folder', - icon: 'folder', - action: () => - showPrompt({ - title: 'New Folder', - defaultValue: 'My Folder', - submitName: 'Create', - label: 'Name', - selectText: true, - onComplete: name => requestFetcher.submit({ parentId: requestGroup._id, name }, - { - action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/debug/request-group/new`, - method: 'post', - }), + name: 'Create', + id: 'create', + icon: 'plus', + items: [ + { + id: 'HTTP', + name: 'HTTP Request', + icon: 'plus-circle', + hint: hotKeyRegistry.request_createHTTP, + action: () => createRequest({ + requestType: 'HTTP', + parentId: requestGroup._id, }), - }, - { - id: 'Duplicate', - name: 'Duplicate', - icon: 'copy', - hint: hotKeyRegistry.request_createHTTP, - action: () => handleRequestGroupDuplicate(), - }], + }, + { + id: 'Event Stream', + name: 'Event Stream Request (SSE)', + icon: 'plus-circle', + action: () => createRequest({ + requestType: 'Event Stream', + parentId: requestGroup._id, + }), + }, + { + id: 'GraphQL Request', + name: 'GraphQL Request', + icon: 'plus-circle', + action: () => createRequest({ + requestType: 'GraphQL', + parentId: requestGroup._id, + }), + }, + { + id: 'gRPC Request', + name: 'gRPC Request', + icon: 'plus-circle', + action: () => createRequest({ + requestType: 'gRPC', + parentId: requestGroup._id, + }), + }, + { + id: 'WebSocket Request', + name: 'WebSocket Request', + icon: 'plus-circle', + action: () => createRequest({ + requestType: 'WebSocket', + parentId: requestGroup._id, + }), + }, + { + id: 'New Folder', + name: 'New Folder', + icon: 'folder', + action: () => + showPrompt({ + title: 'New Folder', + defaultValue: 'My Folder', + submitName: 'Create', + label: 'Name', + selectText: true, + onComplete: name => requestFetcher.submit({ parentId: requestGroup._id, name }, + { + action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/debug/request-group/new`, + method: 'post', + }), + }), + }], }, { - name: 'Manage', - id: 'manage', + name: 'Import', + id: 'import', + icon: 'file-import', + items: [ + { + id: 'From Curl', + name: 'From Curl', + icon: 'terminal', + action: () => setPasteCurlModalOpen(true), + }, + ], + }, + { + name: 'Actions', + id: 'actions', icon: 'cog', items: [ + { + id: 'Duplicate', + name: 'Duplicate', + icon: 'copy', + action: () => handleRequestGroupDuplicate(), + }, { id: 'Rename', name: 'Rename', @@ -248,20 +255,6 @@ export const RequestGroupActionsDropdown = ({ action: () => handleRename(), }, - { - id: 'Delete', - name: 'Delete', - icon: 'trash', - action: () => - handleDeleteFolder(), - }, - ...actionPlugins.map(plugin => ({ - id: plugin.label, - name: plugin.label, - icon: plugin.icon as IconName || 'plug', - action: () => - handlePluginClick(plugin), - })), { id: 'Settings', name: 'Settings', @@ -269,9 +262,29 @@ export const RequestGroupActionsDropdown = ({ action: () => setIsSettingsModalOpen(true), }, + { + id: 'Delete', + name: 'Delete', + icon: 'trash', + action: () => + handleDeleteFolder(), + }, ], }, - + ...(actionPlugins.length > 0 ? [ + { + name: 'Plugins', + id: 'plugins', + icon: 'plug' as IconName, + items: actionPlugins.map(plugin => ({ + id: plugin.label, + name: plugin.label, + icon: plugin.icon as IconName || 'plug', + action: () => + handlePluginClick(plugin), + })), + }, + ] : []), ]; return ( @@ -288,10 +301,7 @@ export const RequestGroupActionsDropdown = ({ { - const item = requestGroupActionItems[0].items.find(a => a.id === key) || requestGroupActionItems[1].items.find(a => a.id === key); - item && item.action(); - }} + onAction={key => requestGroupActionItems.find(i => i.items.find(a => a.id === key))?.items.find(a => a.id === key)?.action()} items={requestGroupActionItems} className="border select-none text-sm min-w-max border-solid border-[--hl-sm] shadow-lg bg-[--color-bg] py-2 rounded-md overflow-y-auto max-h-[85vh] focus:outline-none" > @@ -310,21 +320,21 @@ export const RequestGroupActionsDropdown = ({ > {item.name} + {item.hint && ()} )} )} - - - - + + + { isSettingsModalOpen && ( - setIsSettingsModalOpen(false)} - /> + setIsSettingsModalOpen(false)} + /> ) } {isPasteCurlModalOpen && ( diff --git a/packages/insomnia/src/ui/components/dropdowns/workspace-dropdown.tsx b/packages/insomnia/src/ui/components/dropdowns/workspace-dropdown.tsx index 347b11f3e6..642a80b1fc 100644 --- a/packages/insomnia/src/ui/components/dropdowns/workspace-dropdown.tsx +++ b/packages/insomnia/src/ui/components/dropdowns/workspace-dropdown.tsx @@ -1,6 +1,6 @@ import { IconName } from '@fortawesome/fontawesome-svg-core'; import React, { FC, ReactNode, useCallback, useState } from 'react'; -import { Button, Dialog, Heading, Menu, MenuItem, MenuTrigger, Modal, ModalOverlay, Popover } from 'react-aria-components'; +import { Button, Collection, Dialog, Header, Heading, Menu, MenuItem, MenuTrigger, Modal, ModalOverlay, Popover, Section } from 'react-aria-components'; import { useFetcher, useParams, useRouteLoaderData } from 'react-router-dom'; import { getProductName } from '../../../common/constants'; @@ -8,6 +8,7 @@ import { database as db } from '../../../common/database'; import { exportMockServerToFile } from '../../../common/export'; import { getWorkspaceLabel } from '../../../common/get-workspace-label'; import { RENDER_PURPOSE_NO_RENDER } from '../../../common/render'; +import { PlatformKeyCombinations } from '../../../common/settings'; import { isRemoteProject } from '../../../models/project'; import { isRequest } from '../../../models/request'; import { isRequestGroup } from '../../../models/request-group'; @@ -19,6 +20,7 @@ import { invariant } from '../../../utils/invariant'; import { useAIContext } from '../../context/app/ai-context'; import { useRootLoaderData } from '../../routes/root'; import { WorkspaceLoaderData } from '../../routes/workspace'; +import { DropdownHint } from '../base/dropdown/dropdown-hint'; import { Icon } from '../icon'; import { InsomniaAI } from '../insomnia-ai-icon'; import { useDocBodyKeyboardShortcuts } from '../keydown-binder'; @@ -28,13 +30,6 @@ import { ImportModal } from '../modals/import-modal'; import { WorkspaceDuplicateModal } from '../modals/workspace-duplicate-modal'; import { WorkspaceSettingsModal } from '../modals/workspace-settings-modal'; -interface WorkspaceActionItem { - id: string; - name: string; - icon: ReactNode; - action: () => void; -} - export const WorkspaceDropdown: FC = () => { const { organizationId, projectId, workspaceId } = useParams<{ organizationId: string; projectId: string; workspaceId: string }>(); invariant(organizationId, 'Expected organizationId'); @@ -45,13 +40,11 @@ export const WorkspaceDropdown: FC = () => { activeMockServer, projects, } = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData; - const activeWorkspaceName = activeWorkspace.name; + const [isDuplicateModalOpen, setIsDuplicateModalOpen] = useState(false); const [isImportModalOpen, setIsImportModalOpen] = useState(false); const [isExportModalOpen, setIsExportModalOpen] = useState(false); const [isSettingsModalOpen, setIsSettingsModalOpen] = useState(false); - const workspaceName = activeWorkspace.name; - const projectName = activeProject.name ?? getProductName(); const fetcher = useFetcher(); const [isDeleteRemoteWorkspaceModalOpen, setIsDeleteRemoteWorkspaceModalOpen] = useState(false); const deleteWorkspaceFetcher = useFetcher(); @@ -105,78 +98,106 @@ export const WorkspaceDropdown: FC = () => { const isScratchpadWorkspace = isScratchpad(activeWorkspace); - const workspaceActionsList: WorkspaceActionItem[] = [ - ...!isScratchpadWorkspace ? [{ - id: 'duplicate', - name: 'Duplicate', - icon: , - action: () => setIsDuplicateModalOpen(true), - }, + const workspaceActionsList: { + name: string; + id: string; + icon: IconName; + items: { + id: string; + name: string; + icon: ReactNode; + hint?: PlatformKeyCombinations; + action: () => void; + }[]; + }[] = [ { - id: 'rename', - name: 'Rename', - icon: , - action: () => { - showPrompt({ - title: `Rename ${getWorkspaceLabel(activeWorkspace).singular}`, - defaultValue: activeWorkspaceName, - submitName: 'Rename', - selectText: true, - label: 'Name', - onComplete: name => - fetcher.submit( - { name, workspaceId: activeWorkspace._id }, - { - action: `/organization/${organizationId}/project/${activeWorkspace.parentId}/workspace/update`, - method: 'post', - encType: 'application/json', - } - ), - }); - }, - }, - { - id: 'delete', - name: 'Delete', - icon: , - action: () => { - setIsDeleteRemoteWorkspaceModalOpen(true); - }, - }] : [], - { - id: 'import', name: 'Import', - icon: , - action: () => setIsImportModalOpen(true), + id: 'import', + icon: 'cog', + items: [{ + id: 'from-file', + name: 'From File', + icon: , + action: () => setIsImportModalOpen(true), + }], }, { - id: 'export', - name: 'Export', - icon: , - action: () => activeWorkspace.scope !== 'mock-server' - ? setIsExportModalOpen(true) - : exportMockServerToFile(activeWorkspace), + name: 'Actions', + id: 'actions', + icon: 'cog', + items: [ + { + id: 'duplicate', + name: 'Duplicate', + icon: , + action: () => setIsDuplicateModalOpen(true), + }, + { + id: 'rename', + name: 'Rename', + icon: , + action: () => showPrompt({ + title: `Rename ${getWorkspaceLabel(activeWorkspace).singular}`, + defaultValue: activeWorkspace.name, + submitName: 'Rename', + selectText: true, + label: 'Name', + onComplete: name => + fetcher.submit( + { name, workspaceId: activeWorkspace._id }, + { + action: `/organization/${organizationId}/project/${activeWorkspace.parentId}/workspace/update`, + method: 'post', + encType: 'application/json', + } + ), + }), + }, + { + id: 'export', + name: 'Export', + icon: , + action: () => activeWorkspace.scope !== 'mock-server' + ? setIsExportModalOpen(true) + : exportMockServerToFile(activeWorkspace), + }, + { + id: 'settings', + name: 'Settings', + icon: , + action: () => setIsSettingsModalOpen(true), + }, + { + id: 'delete', + name: 'Delete', + icon: , + action: () => setIsDeleteRemoteWorkspaceModalOpen(true), + + }, + ...userSession.id && access.enabled && activeWorkspace.scope === 'design' ? [{ + id: 'insomnia-ai/generate-test-suite', + name: 'Auto-generate Tests For Collection', + action: generateTests, + icon: + + , + }] : [], + ], }, - { - id: 'settings', - name: 'Settings', - icon: , - action: () => setIsSettingsModalOpen(true), - }, - ...actionPlugins.map((p: WorkspaceAction) => ({ - id: p.label, - name: p.label, - icon: , - action: () => handlePluginClick(p, activeWorkspace), - })), - ...userSession.id && access.enabled && activeWorkspace.scope === 'design' ? [{ - id: 'insomnia-ai/generate-test-suite', - name: 'Auto-generate Tests For Collection', - action: generateTests, - icon: - - , - }] : [], + ...(actionPlugins.length > 0 ? [ + { + name: 'Plugins', + id: 'plugins', + icon: 'plug' as IconName, + items: actionPlugins.map(plugin => ({ + id: plugin.label, + name: plugin.label, + icon: , + action: () => + handlePluginClick(plugin, activeWorkspace), + })), + }, + ] : []), ]; return ( @@ -187,34 +208,37 @@ export const WorkspaceDropdown: FC = () => { data-testid="workspace-context-dropdown" className="px-3 py-1 h-7 flex flex-1 items-center justify-center gap-2 aria-pressed:bg-[--hl-sm] rounded-sm text-[--color-font] hover:bg-[--hl-xs] focus:ring-inset ring-1 ring-transparent focus:ring-[--hl-md] transition-all text-sm truncate" > - {activeWorkspaceName} + {activeWorkspace.name} { - const item = workspaceActionsList.find( - item => item.id === key - ); - if (item) { - item.action(); - } - }} - items={workspaceActionsList} + onAction={key => workspaceActionsList.find(i => i.items.find(a => a.id === key))?.items.find(a => a.id === key)?.action()} + items={isScratchpadWorkspace ? [] : workspaceActionsList} className="border select-none text-sm min-w-max border-solid border-[--hl-sm] shadow-lg bg-[--color-bg] py-2 rounded-md overflow-y-auto max-h-[85vh] focus:outline-none" > - {item => ( - - {item.icon} - {item.name} - + {section => ( +
+
+ {section.name} +
+ + {item => ( + + {item.icon} + {item.name} + {item.hint && ()} + + )} + +
)}
@@ -230,8 +254,8 @@ export const WorkspaceDropdown: FC = () => { setIsImportModalOpen(false)} from={{ type: 'file' }} - projectName={projectName} - workspaceName={workspaceName} + projectName={activeProject.name ?? getProductName()} + workspaceName={activeWorkspace.name} organizationId={organizationId} defaultProjectId={projectId} defaultWorkspaceId={workspaceId} diff --git a/packages/insomnia/src/ui/components/error-boundary.tsx b/packages/insomnia/src/ui/components/error-boundary.tsx index 447faf08e1..169cbb47b7 100644 --- a/packages/insomnia/src/ui/components/error-boundary.tsx +++ b/packages/insomnia/src/ui/components/error-boundary.tsx @@ -58,7 +58,7 @@ class SingleErrorBoundary extends PureComponent { render() { if (this.state.error) { return ( -
Render Failure: {this.state.error.message}
+
Render Failure: {this.state.error.message}
); } diff --git a/packages/insomnia/src/ui/routes/debug.tsx b/packages/insomnia/src/ui/routes/debug.tsx index 8d92663bb5..88d69a48ab 100644 --- a/packages/insomnia/src/ui/routes/debug.tsx +++ b/packages/insomnia/src/ui/routes/debug.tsx @@ -6,9 +6,11 @@ import { Breadcrumb, Breadcrumbs, Button, + Collection, DropIndicator, GridList, GridListItem, + Header, Input, ListBox, ListBoxItem, @@ -17,6 +19,7 @@ import { MenuTrigger, Popover, SearchField, + Section, Select, SelectValue, ToggleButton, @@ -36,7 +39,7 @@ import { useSearchParams, } from 'react-router-dom'; -import { DEFAULT_SIDEBAR_SIZE, SORT_ORDERS, SortOrder, sortOrderName } from '../../common/constants'; +import { DEFAULT_SIDEBAR_SIZE, getProductName, SORT_ORDERS, SortOrder, sortOrderName } from '../../common/constants'; import { ChangeBufferEvent, database as db } from '../../common/database'; import { generateId } from '../../common/misc'; import { PlatformKeyCombinations } from '../../common/settings'; @@ -59,6 +62,7 @@ import { WebSocketRequest, } from '../../models/websocket-request'; import { invariant } from '../../utils/invariant'; +import { DropdownHint } from '../components/base/dropdown/dropdown-hint'; import { RequestActionsDropdown } from '../components/dropdowns/request-actions-dropdown'; import { RequestGroupActionsDropdown } from '../components/dropdowns/request-group-actions-dropdown'; import { WorkspaceDropdown } from '../components/dropdowns/workspace-dropdown'; @@ -71,6 +75,7 @@ import { showModal, showPrompt } from '../components/modals'; import { AskModal } from '../components/modals/ask-modal'; import { CookiesModal } from '../components/modals/cookies-modal'; import { GenerateCodeModal } from '../components/modals/generate-code-modal'; +import { ImportModal } from '../components/modals/import-modal'; import { PasteCurlModal } from '../components/modals/paste-curl-modal'; import { PromptModal } from '../components/modals/prompt-modal'; import { RequestSettingsModal } from '../components/modals/request-settings-modal'; @@ -200,6 +205,7 @@ export const Debug: FC = () => { const [isRequestSettingsModalOpen, setIsRequestSettingsModalOpen] = useState(false); const [isEnvironmentModalOpen, setEnvironmentModalOpen] = useState(false); + const [isImportModalOpen, setIsImportModalOpen] = useState(false); const [isEnvironmentSelectOpen, setIsEnvironmentSelectOpen] = useState(false); const [isCertificatesModalOpen, setCertificatesModalOpen] = useState(false); @@ -563,91 +569,113 @@ export const Debug: FC = () => { }); const createInCollectionActionList: { - id: string; name: string; + id: string; icon: IconName; - hint?: PlatformKeyCombinations; - action: () => void; - }[] = [ + items: { + id: string; + name: string; + icon: IconName; + hint?: PlatformKeyCombinations; + action: () => void; + }[]; + }[] = + [ { - id: 'HTTP', - name: 'HTTP Request', - icon: 'plus-circle', - hint: hotKeyRegistry.request_createHTTP, - action: () => - createRequest({ - requestType: 'HTTP', - parentId: workspaceId, - }), + name: 'Create', + id: 'create', + icon: 'plus', + items: [ + { + id: 'New Folder', + name: 'New Folder', + icon: 'folder', + hint: hotKeyRegistry.request_showCreateFolder, + action: () => showPrompt({ + title: 'New Folder', + defaultValue: 'My Folder', + submitName: 'Create', + label: 'Name', + selectText: true, + onComplete: name => + requestFetcher.submit( + { parentId: workspaceId, name }, + { + action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/debug/request-group/new`, + method: 'post', + } + ), + }), + }, + { + id: 'HTTP', + name: 'HTTP Request', + icon: 'plus-circle', + hint: hotKeyRegistry.request_createHTTP, + action: () => + createRequest({ + requestType: 'HTTP', + parentId: workspaceId, + }), + }, + { + id: 'Event Stream', + name: 'Event Stream Request (SSE)', + icon: 'plus-circle', + action: () => + createRequest({ + requestType: 'Event Stream', + parentId: workspaceId, + }), + }, + { + id: 'GraphQL Request', + name: 'GraphQL Request', + icon: 'plus-circle', + action: () => + createRequest({ + requestType: 'GraphQL', + parentId: workspaceId, + }), + }, + { + id: 'gRPC Request', + name: 'gRPC Request', + icon: 'plus-circle', + action: () => + createRequest({ + requestType: 'gRPC', + parentId: workspaceId, + }), + }, + { + id: 'WebSocket Request', + name: 'WebSocket Request', + icon: 'plus-circle', + action: () => + createRequest({ + requestType: 'WebSocket', + parentId: workspaceId, + }), + }], }, { - id: 'Event Stream', - name: 'Event Stream Request', - icon: 'plus-circle', - action: () => - createRequest({ - requestType: 'Event Stream', - parentId: workspaceId, - }), - }, - { - id: 'GraphQL Request', - name: 'GraphQL Request', - icon: 'plus-circle', - action: () => - createRequest({ - requestType: 'GraphQL', - parentId: workspaceId, - }), - }, - { - id: 'gRPC Request', - name: 'gRPC Request', - icon: 'plus-circle', - action: () => - createRequest({ - requestType: 'gRPC', - parentId: workspaceId, - }), - }, - { - id: 'WebSocket Request', - name: 'WebSocket Request', - icon: 'plus-circle', - action: () => - createRequest({ - requestType: 'WebSocket', - parentId: workspaceId, - }), - }, - { - id: 'From Curl', - name: 'From Curl', - icon: 'terminal', - action: () => setPasteCurlModalOpen(true), - }, - { - id: 'New Folder', - name: 'New Folder', - icon: 'folder', - action: () => - showPrompt({ - title: 'New Folder', - defaultValue: 'My Folder', - submitName: 'Create', - label: 'Name', - selectText: true, - onComplete: name => - requestFetcher.submit( - { parentId: workspaceId, name }, - { - action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/debug/request-group/new`, - method: 'post', - } - ), - }), - }, - ]; + name: 'Import', + id: 'import', + 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), + }], + }]; const environmentsList = [baseEnvironment, ...subEnvironments].map(environment => ({ id: environment._id, @@ -978,25 +1006,30 @@ export const Debug: FC = () => { { - const item = createInCollectionActionList.find(item => item.id === key); - if (item) { - item.action(); - } - }} + onAction={key => createInCollectionActionList.find(i => i.items.find(a => a.id === key))?.items.find(a => a.id === key)?.action()} items={createInCollectionActionList} className="border select-none text-sm min-w-max border-solid border-[--hl-sm] shadow-lg bg-[--color-bg] py-2 rounded-md overflow-y-auto max-h-[85vh] focus:outline-none" > - {item => ( - - - {item.name} - + {section => ( +
+
+ {section.name} +
+ + {item => ( + + + {item.name} + {item.hint && ()} + + )} + +
)}
@@ -1250,6 +1283,17 @@ export const Debug: FC = () => { onClose={() => setEnvironmentModalOpen(false)} /> )} + {isImportModalOpen && ( + setIsImportModalOpen(false)} + from={{ type: 'file' }} + projectName={activeProject.name ?? getProductName()} + workspaceName={activeWorkspace.name} + organizationId={organizationId} + defaultProjectId={projectId} + defaultWorkspaceId={workspaceId} + /> + )} {isCookieModalOpen && ( setIsCookieModalOpen(false)} /> )}