Skip to content

Commit

Permalink
feat(command-palette): Add command palette for quick switching betwee…
Browse files Browse the repository at this point in the history
…n 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
  • Loading branch information
gatzjames committed Jan 5, 2024
1 parent 54a989a commit 4085d34
Show file tree
Hide file tree
Showing 4 changed files with 204 additions and 0 deletions.
46 changes: 46 additions & 0 deletions packages/insomnia-smoke-test/tests/smoke/command-palette.test.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 152 additions & 0 deletions packages/insomnia/src/ui/components/command-palette.tsx
Original file line number Diff line number Diff line change
@@ -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>
);
};
2 changes: 2 additions & 0 deletions packages/insomnia/src/ui/routes/organization.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -769,6 +770,7 @@ const OrganizationRoute = () => {
</div>
<Toast />
</div>
{workspaceId && <CommandPalette />}
</InsomniaEventStreamProvider>
);
};
Expand Down
4 changes: 4 additions & 0 deletions packages/insomnia/src/ui/routes/workspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import { invariant } from '../../utils/invariant';
type Collection = Child[];

export interface WorkspaceLoaderData {
workspaces: Workspace[];
activeWorkspace: Workspace;
activeWorkspaceMeta: WorkspaceMeta;
activeProject: Project;
Expand Down Expand Up @@ -235,7 +236,10 @@ export const workspaceLoader: LoaderFunction = async ({
}
}

const workspaces = await models.workspace.findByParentId(projectId);

return {
workspaces,
activeWorkspace,
activeProject,
gitRepository,
Expand Down

0 comments on commit 4085d34

Please sign in to comment.