diff --git a/packages/insomnia-smoke-test/tests/smoke/command-palette.test.ts b/packages/insomnia-smoke-test/tests/smoke/command-palette.test.ts new file mode 100644 index 0000000000..35f15297b0 --- /dev/null +++ b/packages/insomnia-smoke-test/tests/smoke/command-palette.test.ts @@ -0,0 +1,46 @@ +import { expect } from '@playwright/test'; + +import { loadFixture } from '../../playwright/paths'; +import { test } from '../../playwright/test'; + +test('Command palette - can switch between requests and workspaces', async ({ app, page }) => { + test.slow(process.platform === 'darwin' || process.platform === 'win32', 'Slow app start on these platforms'); + + // Import a collection + const text = await loadFixture('smoke-test-collection.yaml'); + await app.evaluate(async ({ clipboard }, text) => clipboard.writeText(text), text); + + await page.getByRole('button', { name: 'Create in project' }).click(); + await page.getByRole('menuitemradio', { name: 'Import' }).click(); + await page.locator('[data-test-id="import-from-clipboard"]').click(); + await page.getByRole('button', { name: 'Scan' }).click(); + await page.getByRole('dialog').getByRole('button', { name: 'Import' }).click(); + + // Import a document + const swaggerDoc = await loadFixture('swagger2.yaml'); + await app.evaluate(async ({ clipboard }, swaggerDoc) => clipboard.writeText(swaggerDoc), swaggerDoc); + + await page.getByRole('button', { name: 'Create in project' }).click(); + await page.getByRole('menuitemradio', { name: 'Import' }).click(); + await page.locator('[data-test-id="import-from-clipboard"]').click(); + await page.getByRole('button', { name: 'Scan' }).click(); + await page.getByRole('dialog').getByRole('button', { name: 'Import' }).click(); + + await page.getByLabel('Smoke tests').click(); + await page.getByTestId('sends request with cookie and get cookie in response').getByLabel('request name').click(); + await page.getByTestId('OneLineEditor').getByText('http://127.0.0.1:4010/cookies').click(); + const requestSwitchKeyboardShortcut = process.platform === 'darwin' ? 'Meta+p' : 'Control+p'; + await page.locator('body').press(requestSwitchKeyboardShortcut); + await page.getByPlaceholder('Search and switch between').fill('send js'); + await page.getByPlaceholder('Search and switch between').press('ArrowDown'); + await page.getByPlaceholder('Search and switch between').press('Enter'); + await page.getByTestId('OneLineEditor').getByText('http://127.0.0.1:4010/pets/').click(); + await page.getByRole('button', { name: 'Send' }).click(); + await page.getByText('200 OK').click(); + + await page.locator('body').press(requestSwitchKeyboardShortcut); + await page.getByPlaceholder('Search and switch between').press('ArrowUp'); + await page.getByPlaceholder('Search and switch between').press('ArrowUp'); + await page.getByPlaceholder('Search and switch between').press('Enter'); + await expect(page.getByTestId('workspace-context-dropdown').locator('span')).toContainText('E2E testing specification - swagger 2 1.0.0'); +}); diff --git a/packages/insomnia/src/ui/components/command-palette.tsx b/packages/insomnia/src/ui/components/command-palette.tsx new file mode 100644 index 0000000000..8f870cf3d5 --- /dev/null +++ b/packages/insomnia/src/ui/components/command-palette.tsx @@ -0,0 +1,152 @@ +import React from 'react'; +import { useState } from 'react'; +import { Collection, ComboBox, Dialog, Header, Input, Label, ListBox, ListBoxItem, Modal, ModalOverlay, Section, Text } from 'react-aria-components'; +import { useNavigate, useParams, useRouteLoaderData } from 'react-router-dom'; + +import { isGrpcRequest } from '../../models/grpc-request'; +import { isRequest } from '../../models/request'; +import { isRequestGroup } from '../../models/request-group'; +import { isWebSocketRequest } from '../../models/websocket-request'; +import { WorkspaceLoaderData } from '../routes/workspace'; +import { Icon } from './icon'; +import { useDocBodyKeyboardShortcuts } from './keydown-binder'; +import { getMethodShortHand } from './tags/method-tag'; + +export const CommandPalette = () => { + const [isOpen, setIsOpen] = useState(false); + const { + organizationId, + projectId, + workspaceId, + requestId, + } = useParams(); + const { + collection, + workspaces, + } = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData; + + const navigate = useNavigate(); + useDocBodyKeyboardShortcuts({ + request_quickSwitch: () => { + setIsOpen(true); + }, + }); + + return ( + + + + {({ close }) => ( + { + // Fuzzy search using Regex + const fuzzy = filter.split('').join('.*?'); + const regex = new RegExp(fuzzy, 'i'); + return regex.test(text); + }} + onSelectionChange={itemId => { + if (!itemId) { + return; + } + if (itemId.toString().startsWith('wrk_')) { + navigate(`/organization/${organizationId}/project/${projectId}/workspace/${itemId}/debug`); + } else { + navigate(`/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/debug/request/${itemId}`); + } + close(); + }} + > + + item.doc).filter(item => !isRequestGroup(item)).map(item => ({ + id: item._id, + icon: isRequest(item) ? ( + + {getMethodShortHand(item)} + + ) : isWebSocketRequest(item) ? ( + + WS + + ) : isGrpcRequest(item) && ( + + gRPC + + ), + name: item.name, + description: !isRequestGroup(item) ? item.url : '', + textValue: !isRequestGroup(item) ? `${isRequest(item) ? item.method : isWebSocketRequest(item) ? 'WebSocket' : 'gRPC'} ${item.name} ${item.url}` : '', + })), + }, + { + id: 'collections-and-documents', + name: 'Collections and documents', + children: workspaces.map(workspace => ({ + id: workspace._id, + icon: , + name: workspace.name, + description: '', + textValue: `${workspace.scope === 'collection' ? 'Collection' : 'Document'} ${workspace.name}`, + })), + }, + ]} + > + {section => ( +
+
{section.name}
+ + {item => ( + +
+ {item.icon} + {item.name} + {item.description} +
+
+ )} +
+
+ )} +
+
+ )} +
+
+
+ ); +}; diff --git a/packages/insomnia/src/ui/routes/organization.tsx b/packages/insomnia/src/ui/routes/organization.tsx index 36f3f469eb..6d312f333a 100644 --- a/packages/insomnia/src/ui/routes/organization.tsx +++ b/packages/insomnia/src/ui/routes/organization.tsx @@ -40,6 +40,7 @@ import { migrateProjectsIntoOrganization, shouldMigrateProjectUnderOrganization import { invariant } from '../../utils/invariant'; import { getLoginUrl } from '../auth-session-provider'; import { Avatar } from '../components/avatar'; +import { CommandPalette } from '../components/command-palette'; import { GitHubStarsButton } from '../components/github-stars-button'; import { Hotkey } from '../components/hotkey'; import { Icon } from '../components/icon'; @@ -769,6 +770,7 @@ const OrganizationRoute = () => { + {workspaceId && } ); }; diff --git a/packages/insomnia/src/ui/routes/workspace.tsx b/packages/insomnia/src/ui/routes/workspace.tsx index 2022cd670c..84939e986d 100644 --- a/packages/insomnia/src/ui/routes/workspace.tsx +++ b/packages/insomnia/src/ui/routes/workspace.tsx @@ -33,6 +33,7 @@ import { invariant } from '../../utils/invariant'; type Collection = Child[]; export interface WorkspaceLoaderData { + workspaces: Workspace[]; activeWorkspace: Workspace; activeWorkspaceMeta: WorkspaceMeta; activeProject: Project; @@ -235,7 +236,10 @@ export const workspaceLoader: LoaderFunction = async ({ } } + const workspaces = await models.workspace.findByParentId(projectId); + return { + workspaces, activeWorkspace, activeProject, gitRepository,