Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(command-palette): Add command palette for quick switching between requests and workspaces #6968

Merged
merged 6 commits into from
Jan 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading