mirror of
https://github.com/Kong/insomnia.git
synced 2026-04-21 22:57:59 -04:00
feat(command-palette): Add command palette for quick switching between requests and workspaces (#6968)
* Add command palette for quick switching between requests and workspaces * truncate text * small style update * add smoke test for command-palette * fix shortcut for different platforms * wait for request to switch
This commit is contained in:
@@ -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');
|
||||
});
|
||||
152
packages/insomnia/src/ui/components/command-palette.tsx
Normal file
152
packages/insomnia/src/ui/components/command-palette.tsx
Normal file
@@ -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 (
|
||||
<ModalOverlay isOpen={isOpen} onOpenChange={setIsOpen} isDismissable className="w-full h-[--visual-viewport-height] fixed z-10 top-0 left-0 flex pt-20 justify-center bg-black/30">
|
||||
<Modal className="max-w-2xl h-max w-full rounded-md flex flex-col overflow-hidden border border-solid border-[--hl-sm] max-h-[80vh] bg-[--color-bg] text-[--color-font]">
|
||||
<Dialog className="outline-none h-max overflow-hidden flex flex-col">
|
||||
{({ close }) => (
|
||||
<ComboBox
|
||||
aria-label='Quick switcher'
|
||||
className='flex flex-col divide-y divide-solid divide-[--hl-sm] overflow-hidden'
|
||||
autoFocus
|
||||
allowsCustomValue={false}
|
||||
menuTrigger='focus'
|
||||
shouldFocusWrap
|
||||
defaultFilter={(text, filter) => {
|
||||
// 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();
|
||||
}}
|
||||
>
|
||||
<Label
|
||||
aria-label="Filter"
|
||||
className="group relative flex items-center gap-2 p-2 flex-1"
|
||||
>
|
||||
<Icon icon="search" className="text-[--color-font] pl-2" />
|
||||
<Input
|
||||
placeholder="Search and switch between requests, collections and documents"
|
||||
className="py-1 w-full pl-2 pr-7 bg-[--color-bg] text-[--color-font]"
|
||||
/>
|
||||
</Label>
|
||||
<ListBox
|
||||
className="flex-1 overflow-y-auto outline-none flex flex-col data-[empty]:hidden"
|
||||
items={[
|
||||
{
|
||||
id: 'requests',
|
||||
name: 'Requests',
|
||||
children: collection.map(item => item.doc).filter(item => !isRequestGroup(item)).map(item => ({
|
||||
id: item._id,
|
||||
icon: isRequest(item) ? (
|
||||
<span
|
||||
className={
|
||||
`w-10 flex-shrink-0 flex text-[0.65rem] rounded-sm border border-solid border-[--hl-sm] items-center justify-center
|
||||
${{
|
||||
'GET': 'text-[--color-font-surprise] bg-[rgba(var(--color-surprise-rgb),0.5)]',
|
||||
'POST': 'text-[--color-font-success] bg-[rgba(var(--color-success-rgb),0.5)]',
|
||||
'HEAD': 'text-[--color-font-info] bg-[rgba(var(--color-info-rgb),0.5)]',
|
||||
'OPTIONS': 'text-[--color-font-info] bg-[rgba(var(--color-info-rgb),0.5)]',
|
||||
'DELETE': 'text-[--color-font-danger] bg-[rgba(var(--color-danger-rgb),0.5)]',
|
||||
'PUT': 'text-[--color-font-warning] bg-[rgba(var(--color-warning-rgb),0.5)]',
|
||||
'PATCH': 'text-[--color-font-notice] bg-[rgba(var(--color-notice-rgb),0.5)]',
|
||||
}[item.method] || 'text-[--color-font] bg-[--hl-md]'}`
|
||||
}
|
||||
>
|
||||
{getMethodShortHand(item)}
|
||||
</span>
|
||||
) : isWebSocketRequest(item) ? (
|
||||
<span className="w-10 flex-shrink-0 flex text-[0.65rem] rounded-sm border border-solid border-[--hl-sm] items-center justify-center text-[--color-font-notice] bg-[rgba(var(--color-notice-rgb),0.5)]">
|
||||
WS
|
||||
</span>
|
||||
) : isGrpcRequest(item) && (
|
||||
<span className="w-10 flex-shrink-0 flex text-[0.65rem] rounded-sm border border-solid border-[--hl-sm] items-center justify-center text-[--color-font-info] bg-[rgba(var(--color-info-rgb),0.5)]">
|
||||
gRPC
|
||||
</span>
|
||||
),
|
||||
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: <Icon icon={workspace.scope === 'collection' ? 'bars' : 'file'} className="text-[--color-font] w-10 flex-shrink-0 flex items-center justify-center" />,
|
||||
name: workspace.name,
|
||||
description: '',
|
||||
textValue: `${workspace.scope === 'collection' ? 'Collection' : 'Document'} ${workspace.name}`,
|
||||
})),
|
||||
},
|
||||
]}
|
||||
>
|
||||
{section => (
|
||||
<Section className='flex-1 flex flex-col'>
|
||||
<Header className='p-2 text-xs uppercase text-[--hl] select-none'>{section.name}</Header>
|
||||
<Collection items={section.children}>
|
||||
{item => (
|
||||
<ListBoxItem textValue={item.textValue} className="group outline-none select-none">
|
||||
<div
|
||||
className={`flex select-none outline-none ${item.id === workspaceId || item.id === requestId ? 'text-[--color-font] font-bold' : 'text-[--hl]'} group-aria-selected:text-[--color-font] relative group-hover:bg-[--hl-xs] group-data-[focused]:bg-[--hl-sm] group-focus:bg-[--hl-sm] transition-colors gap-2 px-4 items-center h-[--line-height-xs] w-full overflow-hidden`}
|
||||
>
|
||||
{item.icon}
|
||||
<Text className="flex-1 px-1 truncate" slot="label">{item.name}</Text>
|
||||
<Text className="flex-1 px-1 truncate" slot="description">{item.description}</Text>
|
||||
</div>
|
||||
</ListBoxItem>
|
||||
)}
|
||||
</Collection>
|
||||
</Section>
|
||||
)}
|
||||
</ListBox>
|
||||
</ComboBox>
|
||||
)}
|
||||
</Dialog>
|
||||
</Modal>
|
||||
</ModalOverlay>
|
||||
);
|
||||
};
|
||||
@@ -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 = () => {
|
||||
</div>
|
||||
<Toast />
|
||||
</div>
|
||||
{workspaceId && <CommandPalette />}
|
||||
</InsomniaEventStreamProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user