consistent dropdown layout (#7536)

* folder actions

* request sections

* workspace sections

* readd icon

* default font error

* add section to create new request dropdown

* rearrange auth options

* add file import to create menu

* hints
This commit is contained in:
Jack Kavanagh
2024-06-17 13:45:41 +02:00
committed by GitHub
parent 16d19760c2
commit 35f320e26b
6 changed files with 540 additions and 418 deletions

View File

@@ -211,27 +211,27 @@ export const AuthDropdown: FC<Props> = ({ 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 (

View File

@@ -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<RequestAction[]>([]);
const [loadingActions, setLoadingActions] = useState<Record<string, boolean>>({});
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 (
<Fragment>
<MenuTrigger onOpenChange={isOpen => isOpen && onOpen()}>
<Button
data-testid={`Dropdown-${toKebabCase(request.name)}`}
aria-label="Request Actions"
className="opacity-0 items-center hover:opacity-100 focus:opacity-100 data-[pressed]:opacity-100 flex group-focus:opacity-100 group-hover:opacity-100 justify-center h-6 aspect-square 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"
>
<Icon icon="caret-down" />
</Button>
<Popover className="min-w-max">
<Menu
aria-label="Request Actions Menu"
selectionMode="single"
onAction={key =>
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"
<MenuTrigger onOpenChange={isOpen => isOpen && onOpen()}>
<Button
data-testid={`Dropdown-${toKebabCase(request.name)}`}
aria-label="Request Actions"
className="opacity-0 items-center hover:opacity-100 focus:opacity-100 data-[pressed]:opacity-100 flex group-focus:opacity-100 group-hover:opacity-100 justify-center h-6 aspect-square 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"
>
{item => (
<MenuItem
key={item.id}
id={item.id}
className="flex gap-2 px-[--padding-md] aria-selected:font-bold items-center text-[--color-font] h-[--line-height-xs] w-full text-md whitespace-nowrap bg-transparent hover:bg-[--hl-sm] disabled:cursor-not-allowed focus:bg-[--hl-xs] focus:outline-none transition-colors"
aria-label={item.name}
>
<Icon icon={item.icon} />
<span>{item.name}</span>
</MenuItem>
)}
</Menu>
</Popover>
</MenuTrigger>
{isSettingsModalOpen && (
<RequestSettingsModal
request={request}
onHide={() => setIsSettingsModalOpen(false)}
/>
)}
</Fragment>
<Icon icon="caret-down" />
</Button>
<Popover className="min-w-max">
<Menu
aria-label="Request Actions Menu"
selectionMode="single"
onAction={key => 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 className='flex-1 flex flex-col'>
<Header className='pl-2 py-1 flex items-center gap-2 text-[--hl] text-xs uppercase'>
<Icon icon={section.icon} /> <span>{section.name}</span>
</Header>
<Collection items={section.items}>
{item => (
<MenuItem
key={item.id}
id={item.id}
className="flex gap-2 px-[--padding-md] aria-selected:font-bold items-center text-[--color-font] h-[--line-height-xs] w-full text-md whitespace-nowrap bg-transparent hover:bg-[--hl-sm] disabled:cursor-not-allowed focus:bg-[--hl-xs] focus:outline-none transition-colors"
aria-label={item.name}
>
<Icon icon={item.icon} />
<span>{item.name}</span>
{item.hint && (<DropdownHint keyBindings={item.hint} />)}
</MenuItem>
)}
</Collection>
</Section>
)}
</Menu>
</Popover>
</MenuTrigger>
{
isSettingsModalOpen && (
<RequestSettingsModal
request={request}
onHide={() => setIsSettingsModalOpen(false)}
/>
)
}
</Fragment >
);
};

View File

@@ -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 = ({
<Menu
aria-label="Request Group Actions Menu"
selectionMode="single"
onAction={key => {
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 = ({
>
<Icon icon={item.icon} />
<span>{item.name}</span>
{item.hint && (<DropdownHint keyBindings={item.hint} />)}
</MenuItem>
)}
</Collection>
</Section>
)}
</Menu>
</Popover>
</MenuTrigger>
</Menu>
</Popover>
</MenuTrigger>
{
isSettingsModalOpen && (
<RequestGroupSettingsModal
requestGroup={requestGroup}
onHide={() => setIsSettingsModalOpen(false)}
/>
<RequestGroupSettingsModal
requestGroup={requestGroup}
onHide={() => setIsSettingsModalOpen(false)}
/>
)
}
{isPasteCurlModalOpen && (

View File

@@ -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: <Icon icon='bars' />,
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: <Icon icon='pen-to-square' />,
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: <Icon icon='trash' />,
action: () => {
setIsDeleteRemoteWorkspaceModalOpen(true);
},
}] : [],
{
id: 'import',
name: 'Import',
icon: <Icon icon='file-import' />,
action: () => setIsImportModalOpen(true),
id: 'import',
icon: 'cog',
items: [{
id: 'from-file',
name: 'From File',
icon: <Icon icon='file-import' />,
action: () => setIsImportModalOpen(true),
}],
},
{
id: 'export',
name: 'Export',
icon: <Icon icon='file-export' />,
action: () => activeWorkspace.scope !== 'mock-server'
? setIsExportModalOpen(true)
: exportMockServerToFile(activeWorkspace),
name: 'Actions',
id: 'actions',
icon: 'cog',
items: [
{
id: 'duplicate',
name: 'Duplicate',
icon: <Icon icon='bars' />,
action: () => setIsDuplicateModalOpen(true),
},
{
id: 'rename',
name: 'Rename',
icon: <Icon icon='pen-to-square' />,
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: <Icon icon='file-export' />,
action: () => activeWorkspace.scope !== 'mock-server'
? setIsExportModalOpen(true)
: exportMockServerToFile(activeWorkspace),
},
{
id: 'settings',
name: 'Settings',
icon: <Icon icon='wrench' />,
action: () => setIsSettingsModalOpen(true),
},
{
id: 'delete',
name: 'Delete',
icon: <Icon icon='trash' />,
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: <span className='flex items-center py-0 px-[--padding-xs]'>
<InsomniaAI />
</span>,
}] : [],
],
},
{
id: 'settings',
name: 'Settings',
icon: <Icon icon='wrench' />,
action: () => setIsSettingsModalOpen(true),
},
...actionPlugins.map((p: WorkspaceAction) => ({
id: p.label,
name: p.label,
icon: <Icon icon={(loadingActions[p.label] ? 'refresh' : p.icon || 'code') as IconName} />,
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: <span className='flex items-center py-0 px-[--padding-xs]'>
<InsomniaAI />
</span>,
}] : [],
...(actionPlugins.length > 0 ? [
{
name: 'Plugins',
id: 'plugins',
icon: 'plug' as IconName,
items: actionPlugins.map(plugin => ({
id: plugin.label,
name: plugin.label,
icon: <Icon icon={plugin.icon as IconName || 'plug'} />,
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"
>
<span className="truncate" title={activeWorkspaceName}>{activeWorkspaceName}</span>
<span className="truncate" title={activeWorkspace.name}>{activeWorkspace.name}</span>
<Icon icon="caret-down" />
</Button>
<Popover className="min-w-max">
<Menu
aria-label="Create in project actions"
selectionMode="single"
onAction={key => {
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 => (
<MenuItem
key={item.id}
id={item.id}
className="flex gap-2 px-[--padding-md] aria-selected:font-bold items-center text-[--color-font] h-[--line-height-xs] w-full text-md whitespace-nowrap bg-transparent hover:bg-[--hl-sm] disabled:cursor-not-allowed focus:bg-[--hl-xs] focus:outline-none transition-colors"
aria-label={item.name}
>
{item.icon}
<span>{item.name}</span>
</MenuItem>
{section => (
<Section className='flex-1 flex flex-col'>
<Header className='pl-2 py-1 flex items-center gap-2 text-[--hl] text-xs uppercase'>
<Icon icon={section.icon} /> <span>{section.name}</span>
</Header>
<Collection items={section.items}>
{item => (
<MenuItem
key={item.id}
id={item.id}
className="flex gap-2 px-[--padding-md] aria-selected:font-bold items-center text-[--color-font] h-[--line-height-xs] w-full text-md whitespace-nowrap bg-transparent hover:bg-[--hl-sm] disabled:cursor-not-allowed focus:bg-[--hl-xs] focus:outline-none transition-colors"
aria-label={item.name}
>
{item.icon}
<span>{item.name}</span>
{item.hint && (<DropdownHint keyBindings={item.hint} />)}
</MenuItem>
)}
</Collection>
</Section>
)}
</Menu>
</Popover>
@@ -230,8 +254,8 @@ export const WorkspaceDropdown: FC = () => {
<ImportModal
onHide={() => setIsImportModalOpen(false)}
from={{ type: 'file' }}
projectName={projectName}
workspaceName={workspaceName}
projectName={activeProject.name ?? getProductName()}
workspaceName={activeWorkspace.name}
organizationId={organizationId}
defaultProjectId={projectId}
defaultWorkspaceId={workspaceId}

View File

@@ -58,7 +58,7 @@ class SingleErrorBoundary extends PureComponent<Props, State> {
render() {
if (this.state.error) {
return (
<div className={this.props.errorClassName ?? ''}>Render Failure: {this.state.error.message}</div>
<div className={this.props.errorClassName ?? 'font-error'}>Render Failure: {this.state.error.message}</div>
);
}

View File

@@ -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 = () => {
<Menu
aria-label="Create a new request"
selectionMode="single"
onAction={key => {
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 => (
<MenuItem
key={item.id}
id={item.id}
className="flex gap-2 px-[--padding-md] aria-selected:font-bold items-center text-[--color-font] h-[--line-height-xs] w-full text-md whitespace-nowrap bg-transparent hover:bg-[--hl-sm] disabled:cursor-not-allowed focus:bg-[--hl-xs] focus:outline-none transition-colors"
aria-label={item.name}
>
<Icon icon={item.icon} />
<span>{item.name}</span>
</MenuItem>
{section => (
<Section className='flex-1 flex flex-col'>
<Header className='pl-2 py-1 flex items-center gap-2 text-[--hl] text-xs uppercase'>
<Icon icon={section.icon} /> <span>{section.name}</span>
</Header>
<Collection items={section.items}>
{item => (
<MenuItem
key={item.id}
id={item.id}
className="flex gap-2 px-[--padding-md] aria-selected:font-bold items-center text-[--color-font] h-[--line-height-xs] w-full text-md whitespace-nowrap bg-transparent hover:bg-[--hl-sm] disabled:cursor-not-allowed focus:bg-[--hl-xs] focus:outline-none transition-colors"
aria-label={item.name}
>
<Icon icon={item.icon} />
<span>{item.name}</span>
{item.hint && (<DropdownHint keyBindings={item.hint} />)}
</MenuItem>
)}
</Collection>
</Section>
)}
</Menu>
</Popover>
@@ -1250,6 +1283,17 @@ export const Debug: FC = () => {
onClose={() => setEnvironmentModalOpen(false)}
/>
)}
{isImportModalOpen && (
<ImportModal
onHide={() => setIsImportModalOpen(false)}
from={{ type: 'file' }}
projectName={activeProject.name ?? getProductName()}
workspaceName={activeWorkspace.name}
organizationId={organizationId}
defaultProjectId={projectId}
defaultWorkspaceId={workspaceId}
/>
)}
{isCookieModalOpen && (
<CookiesModal onHide={() => setIsCookieModalOpen(false)} />
)}