fix(Response Tabs): Tabs with a menu inside are not accessible - Response Panes (#7477)

* use thing scrollbars in the app

* fix menu/toast overflow content shift

* update content and auth dropdowns

* Update auth wrapper styles

* update body editors

* Request/request-group panes

* update request script editor

* update e2e tests

* remove log
This commit is contained in:
James Gatz
2024-06-05 12:05:21 +02:00
committed by GitHub
parent 57e9898bf0
commit 79cc2ef3ef
17 changed files with 1254 additions and 1042 deletions

View File

@@ -73,7 +73,7 @@ test.describe('Environment Editor', async () => {
await page.getByLabel('Request Collection').getByTestId('New Request').press('Enter');
// Add number variable to request body
await page.getByRole('tab', { name: 'Plain' }).click();
await page.getByRole('tab', { name: 'Body' }).click();
await page.locator('pre').filter({ hasText: '_.exampleObject.anotherNumber' }).press('Enter');
await page.getByTestId('CodeEditor').getByRole('textbox').press('Enter');

View File

@@ -21,9 +21,9 @@ test('can render schema and send GraphQL requests', async ({ app, page }) => {
// Open the graphql request
await page.getByLabel('Request Collection').getByTestId('GraphQL request').press('Enter');
await page.getByRole('tab', { name: 'GraphQL' }).click();
await page.getByRole('tab', { name: 'Body' }).click();
// Assert the schema is fetched after switching to GraphQL request
await expect(page.locator('.graphql-editor__meta')).toContainText('schema fetched just now');
await expect(page.getByText('Schema fetched just now')).toBeVisible();
// Assert schema documentation stuff
await page.getByRole('button', { name: 'schema' }).click();
@@ -63,9 +63,9 @@ test('can render schema and send GraphQL requests with object variables', async
// Open the graphql request
await page.getByLabel('Request Collection').getByTestId('GraphQL request with variables').press('Enter');
await page.getByRole('tab', { name: 'GraphQL' }).click();
await page.getByRole('tab', { name: 'Body' }).click();
// Assert the schema is fetched after switching to GraphQL request
await expect(page.locator('.graphql-editor__meta')).toContainText('schema fetched just now');
await expect(page.getByText('Schema fetched just now')).toBeVisible();
// Assert schema documentation stuff
await page.getByRole('button', { name: 'schema' }).click();
@@ -105,9 +105,9 @@ test('can render numeric environment', async ({ app, page }) => {
// Open the graphql request
await page.getByLabel('Request Collection').getByTestId('GraphQL request with number').press('Enter');
await page.getByRole('tab', { name: 'GraphQL' }).click();
await page.getByRole('tab', { name: 'Body' }).click();
// Assert the schema is fetched after switching to GraphQL request
await expect(page.locator('.graphql-editor__meta')).toContainText('schema fetched just now');
await expect(page.getByText('Schema fetched just now')).toBeVisible();
// Assert schema documentation stuff
await page.getByRole('button', { name: 'schema' }).click();
@@ -144,7 +144,7 @@ test('can send GraphQL requests after editing and prettifying query', async ({ a
await page.getByLabel('Request Collection').getByTestId('GraphQL request').press('Enter');
// Edit and prettify query
await page.getByRole('tab', { name: 'GraphQL' }).click();
await page.getByRole('tab', { name: 'Body' }).click();
await page.locator('pre[role="presentation"]:has-text("bearer")').click();
await page.locator('.app').press('Enter');
await page.locator('text=Prettify GraphQL').click();

View File

@@ -47,7 +47,8 @@ test('can make oauth2 requests', async ({ app, page }) => {
await expect(responseBody).toContainText('"sub": "admin"');
// Navigate to the OAuth2 Tab and refresh the token from there
await page.getByRole('tab', { name: 'OAuth 2' }).click();
await page.getByRole('tab', { name: 'Auth' }).click();
await expect(page.getByRole('button', { name: 'OAuth 2.0' })).toBeVisible();
const tokenInput = page.locator('[for="Access-Token"] > input');
const prevToken = await tokenInput.inputValue();

View File

@@ -217,13 +217,13 @@ test.describe('pre-request features tests', async () => {
// set request body
await page.getByRole('tab', { name: 'Body' }).click();
await page.getByRole('button', { name: 'Body' }).click();
await page.getByRole('menuitem', { name: 'JSON' }).click();
await page.getByRole('option', { name: 'JSON' }).click();
const bodyEditor = page.getByTestId('CodeEditor').getByRole('textbox');
await bodyEditor.fill('{ "rawBody": {{ _.rawBody }}, "urlencodedBody": {{ _.urlencodedBody }}, "gqlBody": {{ _.gqlBody }}, "fileBody": {{ _.fileBody }}, "formdataBody": {{ _.formdataBody }} }');
// enter script
await page.getByTestId('pre-request-script-tab').click();
await page.getByRole('tab', { name: 'Scripts' }).click();
const preRequestScriptEditor = page.getByTestId('CodeEditor').getByRole('textbox');
await preRequestScriptEditor.fill(`
const rawReq = {
@@ -500,10 +500,10 @@ test.describe('unhappy paths', async () => {
// set request body
await page.getByRole('tab', { name: 'Body' }).click();
await page.getByRole('button', { name: 'Body' }).click();
await page.getByRole('menuitem', { name: 'JSON' }).click();
await page.getByRole('option', { name: 'JSON' }).click();
// enter script
await page.getByTestId('pre-request-script-tab').click();
await page.getByRole('tab', { name: 'Scripts' }).click();
const preRequestScriptEditor = page.getByTestId('CodeEditor').getByRole('textbox');
await preRequestScriptEditor.fill(tc.preReqScript);

View File

@@ -8,11 +8,11 @@ test('Request tabs', async ({ page }) => {
await page.getByRole('menuitemradio', { name: 'HTTP Request' }).press('Enter');
await page.getByRole('tab', { name: 'Body' }).click();
await page.getByRole('button', { name: 'Body' }).click();
await page.getByRole('menuitem', { name: 'JSON' }).click();
await page.getByRole('option', { name: 'JSON' }).click();
await page.getByRole('tab', { name: 'Auth' }).click();
await page.getByRole('button', { name: 'Auth' }).click();
await page.getByRole('menuitem', { name: 'OAuth 1.0' }).click();
await page.getByRole('tab', { name: 'Parameters' }).click();
await page.getByLabel('OAuth 1.0', { exact: true }).click();
await page.getByRole('tab', { name: 'Params' }).click();
await page.getByRole('tab', { name: 'Headers' }).click();
await page.getByRole('tab', { name: 'Docs' }).click();
await page.locator('text=Add Description').click();
@@ -26,11 +26,11 @@ test('WS tabs', async ({ page }) => {
await page.getByLabel('Create in collection').click();
await page.getByRole('menuitemradio', { name: 'WebSocket Request' }).click();
await page.getByRole('tab', { name: 'JSON' }).click();
await page.getByLabel('Websocket request pane tabs').getByRole('button', { name: 'JSON' }).click();
await page.getByRole('menuitem', { name: 'JSON' }).click();
await page.getByRole('tab', { name: 'Body' }).click();
await page.getByRole('button', { name: 'JSON' }).click();
await page.getByRole('option', { name: 'JSON' }).click();
await page.getByRole('tab', { name: 'Auth' }).click();
await page.getByRole('tab', { name: 'Parameters' }).click();
await page.getByRole('tab', { name: 'Params' }).click();
await page.getByRole('tab', { name: 'Headers' }).click();
await page.getByRole('tab', { name: 'Docs' }).click();
await page.getByRole('button', { name: 'Add Description' }).click();

View File

@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="en-US" class="w-full h-full">
<html lang="en-US" class="w-full h-full overflow-hidden">
<head>
<meta charset="utf-8" />
<meta

View File

@@ -1,8 +1,9 @@
import { IconName } from '@fortawesome/fontawesome-svg-core';
import React, { FC, useCallback } from 'react';
import { Button, Collection, Header, ListBox, ListBoxItem, Popover, Section, Select, SelectValue } from 'react-aria-components';
import { useParams } from 'react-router-dom';
import {
getAuthTypeName,
HAWK_ALGORITHM_SHA256,
} from '../../../common/constants';
import type { AuthTypeAPIKey, AuthTypeAwsIam, AuthTypeBasic, AuthTypeNTLM, AuthTypes, RequestAuthentication } from '../../../models/request';
@@ -10,7 +11,7 @@ import { getAuthObjectOrNull } from '../../../network/authentication';
import { SIGNATURE_METHOD_HMAC_SHA1 } from '../../../network/o-auth-1/constants';
import { GRANT_TYPE_AUTHORIZATION_CODE } from '../../../network/o-auth-2/constants';
import { useRequestGroupPatcher, useRequestPatcher } from '../../hooks/use-request';
import { Dropdown, DropdownButton, DropdownItem, DropdownSection, ItemContent } from '../base/dropdown';
import { Icon } from '../icon';
function castOneAuthTypeToAnother(type: AuthTypes, oldAuth: RequestAuthentication | {}): RequestAuthentication {
switch (type) {
@@ -133,6 +134,7 @@ interface Props {
authTypes?: AuthTypes[];
disabled?: boolean;
}
export const AuthDropdown: FC<Props> = ({ authentication, authTypes = defaultTypes, disabled = false }) => {
const { requestId, requestGroupId } = useParams() as { organizationId: string; projectId: string; workspaceId: string; requestId?: string; requestGroupId?: string };
const patchRequest = useRequestPatcher();
@@ -147,57 +149,141 @@ export const AuthDropdown: FC<Props> = ({ authentication, authTypes = defaultTyp
requestGroupId && patchRequestGroup(requestGroupId, { authentication: newAuthentication });
}, [authentication, patchRequest, patchRequestGroup, requestGroupId, requestId]);
const isSelected = useCallback((type: AuthTypes) => {
return type === getAuthObjectOrNull(authentication)?.type;
}, [authentication]);
const selectedAuthType = getAuthObjectOrNull(authentication)?.type || 'none';
const authTypesItems: {
id: AuthTypes;
name: string;
}[] = [
{
id: 'apikey',
name: 'API Key',
},
{
id: 'basic',
name: 'Basic',
},
{
id: 'digest',
name: 'Digest',
},
{
id: 'ntlm',
name: 'NTLM',
},
{
id: 'oauth1',
name: 'OAuth 1.0',
},
{
id: 'oauth2',
name: 'OAuth 2.0',
},
{
id: 'iam',
name: 'AWS IAM',
},
{
id: 'bearer',
name: 'Bearer Token',
},
{
id: 'hawk',
name: 'Hawk',
},
{
id: 'asap',
name: 'Atlassian ASAP',
},
{
id: 'netrc',
name: 'Netrc',
},
];
const authTypeSections: {
id: string;
icon: IconName;
name: string;
items: {
id: AuthTypes;
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',
},
],
},
];
return (
<Dropdown
aria-label='Authentication Dropdown'
<Select
isDisabled={disabled}
triggerButton={
<DropdownButton className="tall !text-[--hl]">
{getAuthTypeName(getAuthObjectOrNull(authentication)?.type)}
<i className="fa fa-caret-down space-left" />
</DropdownButton>
}
aria-label="Change Authentication type"
name="auth-type"
onSelectionChange={authType => {
onClick(authType as AuthTypes);
}}
selectedKey={selectedAuthType}
>
<DropdownSection
aria-label='Auth types section'
title="Auth Types"
>
{authTypes.map(authType =>
<DropdownItem
key={authType}
aria-label={getAuthTypeName(authType, true)}
>
<ItemContent
icon={isSelected(authType) ? 'check' : 'empty'}
label={getAuthTypeName(authType, true)}
onClick={() => onClick(authType)}
/>
</DropdownItem>
)}
</DropdownSection>
<DropdownSection
aria-label="Other types section"
title="Other"
>
<DropdownItem aria-label='None' key="none">
<ItemContent
icon={isSelected('none') ? 'check' : 'empty'}
label={'No Authentication'}
onClick={() => onClick('none')}
/>
</DropdownItem>
<DropdownItem aria-label='Inherit from parent' key="inherit">
<ItemContent
icon={getAuthObjectOrNull(authentication) === null ? 'check' : 'empty'}
label={'Inherit from parent'}
onClick={() => onClick()}
/>
</DropdownItem>
</DropdownSection>
</Dropdown>
<Button className="px-4 min-w-[17ch] py-1 font-bold flex flex-1 items-center justify-between 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">
<SelectValue className="flex truncate items-center justify-center gap-2">
{({ selectedText }) => (
<div className='flex items-center gap-2 text-[--hl]'>
{selectedText || 'Auth Type'}
</div>
)}
</SelectValue>
<Icon icon="caret-down" />
</Button>
<Popover className="min-w-max">
<ListBox
items={authTypeSections}
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 => (
<Section>
<Header className='pl-2 py-1 flex items-center gap-2 text-[--hl] text-xs uppercase'>
<Icon icon={item.icon} /> <span>{item.name}</span>
</Header>
<Collection items={item.items}>
{item => (
<ListBoxItem
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}
textValue={item.name}
>
{({ isSelected }) => (
<>
<span>{item.name}</span>
{isSelected && (
<Icon
icon="check"
className="text-[--color-success] justify-self-end"
/>
)}
</>
)}
</ListBoxItem>
)}
</Collection>
</Section>
)}
</ListBox>
</Popover>
</Select>
);
};

View File

@@ -1,4 +1,6 @@
import { IconName } from '@fortawesome/fontawesome-svg-core';
import React, { FC } from 'react';
import { Button, Collection, Header, ListBox, ListBoxItem, Popover, Section, Select, SelectValue } from 'react-aria-components';
import { useParams, useRouteLoaderData } from 'react-router-dom';
import {
@@ -20,12 +22,87 @@ import { deconstructQueryStringToParams } from '../../../utils/url/querystring';
import { SegmentEvent } from '../../analytics';
import { useRequestPatcher } from '../../hooks/use-request';
import { RequestLoaderData } from '../../routes/request';
import { Dropdown, DropdownButton, DropdownItem, DropdownSection, ItemContent } from '../base/dropdown';
import { AlertModal } from '../modals/alert-modal';
import { showModal } from '../modals/index';
import { Icon } from '../icon';
import { showAlert } from '../modals/index';
const EMPTY_MIME_TYPE = null;
const contentTypeSections: {
id: string;
icon: IconName;
name: string;
items: {
id: string;
name: string;
}[];
}[] = [
{
id: 'structured',
name: 'Structured',
icon: 'bars',
items: [
{
id: CONTENT_TYPE_FORM_DATA,
name: 'Form Data',
},
{
id: CONTENT_TYPE_FORM_URLENCODED,
name: 'Form URL Encoded',
},
{
id: CONTENT_TYPE_GRAPHQL,
name: 'GraphQL',
},
],
},
{
id: 'text',
icon: 'code',
name: 'Text',
items: [
{
id: CONTENT_TYPE_JSON,
name: 'JSON',
},
{
id: CONTENT_TYPE_XML,
name: 'XML',
},
{
id: CONTENT_TYPE_YAML,
name: 'YAML',
},
{
id: CONTENT_TYPE_EDN,
name: 'EDN',
},
{
id: CONTENT_TYPE_PLAINTEXT,
name: 'Plain Text',
},
{
id: CONTENT_TYPE_OTHER,
name: 'Other',
},
],
},
{
id: 'other',
icon: 'ellipsis-h',
name: 'Other',
items: [
{
id: CONTENT_TYPE_FILE,
name: 'File',
},
{
id: 'no-body',
name: 'No Body',
},
],
},
];
export const ContentTypeDropdown: FC = () => {
const { activeRequest } = useRouteLoaderData('request/:requestId') as RequestLoaderData;
const patchRequest = useRequestPatcher();
@@ -54,14 +131,19 @@ export const ContentTypeDropdown: FC = () => {
const willPreserveForm = isFormUrlEncoded && willBeMultipart;
if (!isEmpty && !willPreserveText && !willPreserveForm) {
await showModal(AlertModal, {
showAlert({
title: 'Switch Body Type?',
message: 'Current body will be lost. Are you sure you want to continue?',
addCancel: true,
onConfirm: async () => {
patchRequest(requestId, { body: { mimeType } });
window.main.trackSegmentEvent({ event: SegmentEvent.requestBodyTypeSelect, properties: { type: mimeType } });
},
});
} else {
patchRequest(requestId, { body: { mimeType } });
window.main.trackSegmentEvent({ event: SegmentEvent.requestBodyTypeSelect, properties: { type: mimeType } });
}
patchRequest(requestId, { body: { mimeType } });
window.main.trackSegmentEvent({ event: SegmentEvent.requestBodyTypeSelect, properties: { type: mimeType } });
};
const { body } = activeRequest;
@@ -69,137 +151,71 @@ export const ContentTypeDropdown: FC = () => {
const hasParams = body && 'params' in body && body.params;
const numBodyParams = hasParams ? body.params?.filter(({ disabled }) => !disabled).length : 0;
const getIcon = (mimeType: string | null) => {
const contentType = activeRequest?.body && 'mimeType' in activeRequest.body ? activeRequest.body.mimeType : null;
const contentTypeFallback = typeof contentType === 'string' ? contentType : EMPTY_MIME_TYPE;
return mimeType === contentTypeFallback ? 'check' : 'empty';
};
return (
<Dropdown
aria-label='Change Body Type'
triggerButton={
<DropdownButton>
<div className='flex items-center gap-2 !text-[--hl]'>
{hasMimeType ? getContentTypeName(body.mimeType) : 'Body'}
{numBodyParams ?
<span className="p-2 aspect-square flex items-center color-inherit justify-between border-solid border border-[--hl-md] overflow-hidden rounded-lg text-xs shadow-small">{numBodyParams}</span>
: null}
<i className="fa fa-caret-down space-left" />
</div>
</DropdownButton>
}
<Select
aria-label="Change Body Type"
name="body-type"
onSelectionChange={mimeType => {
if (mimeType === 'no-body') {
handleChangeMimeType(EMPTY_MIME_TYPE);
} else {
handleChangeMimeType(mimeType.toString());
}
}}
selectedKey={body.mimeType ?? 'no-body'}
>
<DropdownSection
aria-label='Structured Type Section'
title={
<span>
<i className="fa fa-bars" /> Structured
</span>
}
>
<DropdownItem aria-label={getContentTypeName(CONTENT_TYPE_FORM_DATA, true)}>
<ItemContent
icon={getIcon(CONTENT_TYPE_FORM_DATA)}
label={getContentTypeName(CONTENT_TYPE_FORM_DATA, true)}
onClick={() => handleChangeMimeType(CONTENT_TYPE_FORM_DATA)}
/>
</DropdownItem>
<DropdownItem aria-label={getContentTypeName(CONTENT_TYPE_FORM_URLENCODED, true)}>
<ItemContent
icon={getIcon(CONTENT_TYPE_FORM_URLENCODED)}
label={getContentTypeName(CONTENT_TYPE_FORM_URLENCODED, true)}
onClick={() => handleChangeMimeType(CONTENT_TYPE_FORM_URLENCODED)}
/>
</DropdownItem>
<DropdownItem aria-label={getContentTypeName(CONTENT_TYPE_GRAPHQL, true)}>
<ItemContent
icon={getIcon(CONTENT_TYPE_GRAPHQL)}
label={getContentTypeName(CONTENT_TYPE_GRAPHQL, true)}
onClick={() => handleChangeMimeType(CONTENT_TYPE_GRAPHQL)}
/>
</DropdownItem>
</DropdownSection>
<DropdownSection
aria-label='Text Type Section'
title={
<span>
<i className="fa fa-code" /> Text
</span>
}
>
<DropdownItem aria-label={getContentTypeName(CONTENT_TYPE_JSON, true)}>
<ItemContent
icon={getIcon(CONTENT_TYPE_JSON)}
label={getContentTypeName(CONTENT_TYPE_JSON, true)}
onClick={() => handleChangeMimeType(CONTENT_TYPE_JSON)}
/>
</DropdownItem>
<DropdownItem aria-label={getContentTypeName(CONTENT_TYPE_XML, true)}>
<ItemContent
icon={getIcon(CONTENT_TYPE_XML)}
label={getContentTypeName(CONTENT_TYPE_XML, true)}
onClick={() => handleChangeMimeType(CONTENT_TYPE_XML)}
/>
</DropdownItem>
<DropdownItem aria-label={getContentTypeName(CONTENT_TYPE_YAML, true)}>
<ItemContent
icon={getIcon(CONTENT_TYPE_YAML)}
label={getContentTypeName(CONTENT_TYPE_YAML, true)}
onClick={() => handleChangeMimeType(CONTENT_TYPE_YAML)}
/>
</DropdownItem>
<DropdownItem aria-label={getContentTypeName(CONTENT_TYPE_EDN, true)}>
<ItemContent
icon={getIcon(CONTENT_TYPE_EDN)}
label={getContentTypeName(CONTENT_TYPE_EDN, true)}
onClick={() => handleChangeMimeType(CONTENT_TYPE_EDN)}
/>
</DropdownItem>
<DropdownItem aria-label={getContentTypeName(CONTENT_TYPE_PLAINTEXT, true)}>
<ItemContent
icon={getIcon(CONTENT_TYPE_PLAINTEXT)}
label={getContentTypeName(CONTENT_TYPE_PLAINTEXT, true)}
onClick={() => handleChangeMimeType(CONTENT_TYPE_PLAINTEXT)}
/>
</DropdownItem>
<DropdownItem aria-label={getContentTypeName(CONTENT_TYPE_OTHER, true)}>
<ItemContent
icon={getIcon(CONTENT_TYPE_OTHER)}
label={getContentTypeName(CONTENT_TYPE_OTHER, true)}
onClick={() => handleChangeMimeType(CONTENT_TYPE_OTHER)}
/>
</DropdownItem>
</DropdownSection>
<DropdownSection
aria-label='Other Type Section'
title={
<span>
<i className="fa fa-ellipsis-h" /> Other
</span>
}
>
<DropdownItem aria-label={getContentTypeName(CONTENT_TYPE_FILE, true)}>
<ItemContent
icon={getIcon(CONTENT_TYPE_FILE)}
label={getContentTypeName(CONTENT_TYPE_FILE, true)}
onClick={() => handleChangeMimeType(CONTENT_TYPE_FILE)}
/>
</DropdownItem>
<DropdownItem aria-label="No Body">
<ItemContent
icon={getIcon(EMPTY_MIME_TYPE)}
label="No Body"
onClick={() => handleChangeMimeType(EMPTY_MIME_TYPE)}
/>
</DropdownItem>
</DropdownSection>
</Dropdown>
<Button className="px-4 min-w-[12ch] py-1 font-bold flex flex-1 items-center justify-between 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">
<SelectValue className="flex truncate items-center justify-center gap-2">
<div className='flex items-center gap-2 text-[--hl]'>
{hasMimeType ? getContentTypeName(body.mimeType) : 'No Body'}
{numBodyParams ?
<span className='p-1 min-w-6 h-6 flex items-center justify-center text-xs rounded-lg border border-solid border-[--hl]'>
{numBodyParams}
</span>
: null}
</div>
</SelectValue>
<Icon icon="caret-down" />
</Button>
<Popover className="min-w-max">
<ListBox
items={contentTypeSections}
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 => (
<Section>
<Header className='pl-2 py-1 flex items-center gap-2 text-[--hl] text-xs uppercase'>
<Icon icon={item.icon} /> <span>{item.name}</span>
</Header>
<Collection items={item.items}>
{item => (
<ListBoxItem
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}
textValue={item.name}
>
{({ isSelected }) => (
<>
<span>{item.name}</span>
{isSelected && (
<Icon
icon="check"
className="text-[--color-success] justify-self-end"
/>
)}
</>
)}
</ListBoxItem>
)}
</Collection>
</Section>
)}
</ListBox>
</Popover>
</Select>
);
};
export function newBodyGraphQL(rawBody: string): RequestBody {
try {
// Only strip the newlines if rawBody is a parsable JSON

View File

@@ -1,38 +1,76 @@
import React, { FC } from 'react';
import { Button, ListBox, ListBoxItem, Popover, Select, SelectValue } from 'react-aria-components';
import { CONTENT_TYPE_JSON, CONTENT_TYPE_PLAINTEXT } from '../../../common/constants';
import { Dropdown, DropdownButton, DropdownItem, ItemContent } from '../base/dropdown';
import { Icon } from '../icon';
interface Props {
previewMode: string;
onClick: (previewMode: string) => void;
onSelect: (previewMode: string) => void;
}
export const WebSocketPreviewMode: FC<Props> = ({ previewMode, onClick }) => {
const contentTypes: {
id: string;
name: string;
}[] = [
{
id: CONTENT_TYPE_JSON,
name: 'JSON',
},
{
id: CONTENT_TYPE_PLAINTEXT,
name: 'Raw',
},
];
export const WebSocketPreviewMode: FC<Props> = ({ previewMode, onSelect }) => {
return (
<Dropdown
aria-label="Websocket Preview Mode Dropdown"
triggerButton={
<DropdownButton className="tall !text-[--hl]">
{{
[CONTENT_TYPE_JSON]: 'JSON',
[CONTENT_TYPE_PLAINTEXT]: 'Raw',
}[previewMode]}
<i className="fa fa-caret-down space-left" />
</DropdownButton>
}
<Select
aria-label="Change Body Type"
name="body-type"
onSelectionChange={contentType => {
onSelect(contentType.toString());
}}
selectedKey={previewMode}
>
<DropdownItem aria-label='JSON'>
<ItemContent
label="JSON"
onClick={() => onClick(CONTENT_TYPE_JSON)}
/>
</DropdownItem>
<DropdownItem aria-label='Raw'>
<ItemContent
label="Raw"
onClick={() => onClick(CONTENT_TYPE_PLAINTEXT)}
/>
</DropdownItem>
</Dropdown>
<Button className="px-4 min-w-[12ch] py-1 font-bold flex flex-1 items-center justify-between 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">
<SelectValue<{ id: string; name: string }>
className="flex truncate items-center justify-center gap-2"
>
{({ selectedText }) => (
<div className='flex items-center gap-2 text-[--hl]'>
{selectedText}
</div>
)}
</SelectValue>
<Icon icon="caret-down" />
</Button>
<Popover className="min-w-max">
<ListBox
items={contentTypes}
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 => (
<ListBoxItem
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}
textValue={item.name}
>
{({ isSelected }) => (
<>
<span>{item.name}</span>
{isSelected && (
<Icon
icon="check"
className="text-[--color-success] justify-self-end"
/>
)}
</>
)}
</ListBoxItem>
)}
</ListBox>
</Popover>
</Select>
);
};

View File

@@ -1,4 +1,5 @@
import React, { FC, ReactNode } from 'react';
import { Toolbar } from 'react-aria-components';
import {
AUTH_API_KEY,
@@ -13,8 +14,9 @@ import {
AUTH_OAUTH_1,
AUTH_OAUTH_2,
} from '../../../../common/constants';
import { RequestAuthentication } from '../../../../models/request';
import { AuthTypes, RequestAuthentication } from '../../../../models/request';
import { getAuthObjectOrNull } from '../../../../network/authentication';
import { AuthDropdown } from '../../dropdowns/auth-dropdown';
import { ApiKeyAuth } from './api-key-auth';
import { AsapAuth } from './asap-auth';
import { AWSAuth } from './aws-auth';
@@ -27,7 +29,7 @@ import { NTLMAuth } from './ntlm-auth';
import { OAuth1Auth } from './o-auth-1-auth';
import { OAuth2Auth } from './o-auth-2-auth';
export const AuthWrapper: FC<{ authentication?: RequestAuthentication | {}; disabled?: boolean }> = ({ authentication, disabled = false }) => {
export const AuthWrapper: FC<{ authentication?: RequestAuthentication | {}; disabled?: boolean; authTypes?: AuthTypes[] }> = ({ authentication, disabled = false, authTypes }) => {
const type = getAuthObjectOrNull(authentication)?.type || '';
let authBody: ReactNode = null;
@@ -55,8 +57,8 @@ export const AuthWrapper: FC<{ authentication?: RequestAuthentication | {}; disa
authBody = <AsapAuth />;
} else {
authBody = (
<div className="vertically-center text-center">
<p className="pad super-faint text-sm text-center">
<div className="flex w-full h-full select-none items-center justify-center">
<p className="text-sm text-center p-4 text-[--hl]">
<i
className="fa fa-unlock-alt"
style={{
@@ -72,5 +74,12 @@ export const AuthWrapper: FC<{ authentication?: RequestAuthentication | {}; disa
);
}
return <div>{authBody}</div>;
return <>
<Toolbar className="w-full flex-shrink-0 h-[--line-height-sm] border-b border-solid border-[--hl-md] flex items-center px-2">
<AuthDropdown authentication={authentication} authTypes={authTypes} />
</Toolbar>
<div className='flex-1 overflow-y-auto '>
{authBody}
</div>
</>;
};

View File

@@ -1,6 +1,7 @@
import clone from 'clone';
import { lookup } from 'mime-types';
import React, { FC, useCallback } from 'react';
import { Toolbar } from 'react-aria-components';
import { useParams } from 'react-router-dom';
import {
@@ -19,6 +20,7 @@ import {
} from '../../../../models/request';
import { NunjucksEnabledProvider } from '../../../context/nunjucks/nunjucks-enabled-context';
import { useRequestPatcher } from '../../../hooks/use-request';
import { ContentTypeDropdown } from '../../dropdowns/content-type-dropdown';
import { AskModal } from '../../modals/ask-modal';
import { showModal } from '../../modals/index';
import { EmptyStatePane } from '../../panes/empty-state-pane';
@@ -108,7 +110,7 @@ export const BodyEditor: FC<Props> = ({
const mimeType = request.body.mimeType;
const isBodyEmpty = typeof mimeType !== 'string' && !request.body.text;
const _render = () => {
function renderBodyEditor() {
if (mimeType === CONTENT_TYPE_FORM_URLENCODED) {
return <UrlEncodedEditor key={uniqueKey} onChange={handleFormUrlEncodedChange} parameters={request.body.params || []} />;
} else if (mimeType === CONTENT_TYPE_FORM_DATA) {
@@ -121,14 +123,31 @@ export const BodyEditor: FC<Props> = ({
const contentType = getContentTypeFromHeaders(request.headers) || mimeType;
return <RawEditor uniquenessKey={uniqueKey} contentType={contentType || 'text/plain'} content={request.body.text || ''} onChange={handleRawChange} />;
} else if (isEventStreamRequest(request)) {
return <EmptyStatePane
icon={<i className="fa fa-paper-plane" />}
documentationLinks={[]}
title="Enter a URL and connect to start receiving event stream data"
/>;
return (
<EmptyStatePane
icon={<i className="fa fa-paper-plane" />}
documentationLinks={[]}
title="Enter a URL and connect to start receiving event stream data"
/>
);
} else {
return (
<EmptyStatePane
icon={<SvgIcon icon="bug" />}
documentationLinks={[documentationLinks.introductionToInsomnia]}
secondaryAction="Select a body type from above to send data in the body of a request"
title="Enter a URL and send to get a response"
/>
);
}
return <EmptyStatePane icon={<SvgIcon icon="bug" />} documentationLinks={[documentationLinks.introductionToInsomnia]} secondaryAction="Select a body type from above to send data in the body of a request" title="Enter a URL and send to get a response" />;
};
}
return <NunjucksEnabledProvider disable={noRender}>{_render()}</NunjucksEnabledProvider>;
return (
<NunjucksEnabledProvider disable={noRender}>
<Toolbar className="w-full flex-shrink-0 h-[--line-height-sm] border-b border-solid border-[--hl-md] flex items-center px-2">
<ContentTypeDropdown />
</Toolbar>
{renderBodyEditor()}
</NunjucksEnabledProvider>
);
};

View File

@@ -8,8 +8,9 @@ import { DefinitionNode, DocumentNode, GraphQLNonNull, GraphQLSchema, Kind, NonN
import { buildClientSchema, getIntrospectionQuery } from 'graphql/utilities';
import { Maybe } from 'graphql-language-service';
import React, { FC, useEffect, useRef, useState } from 'react';
import { Button, Toolbar } from 'react-aria-components';
import { Button, Group, Heading, Toolbar, Tooltip, TooltipTrigger } from 'react-aria-components';
import ReactDOM from 'react-dom';
import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels';
import { useLocalStorage } from 'react-use';
import { CONTENT_TYPE_JSON } from '../../../../common/constants';
@@ -28,6 +29,7 @@ import { CodeEditor, CodeEditorHandle } from '../../codemirror/code-editor';
import { GraphQLExplorer } from '../../graph-ql-explorer/graph-ql-explorer';
import { ActiveReference } from '../../graph-ql-explorer/graph-ql-types';
import { HelpTooltip } from '../../help-tooltip';
import { Icon } from '../../icon';
import { useDocBodyKeyboardShortcuts } from '../../keydown-binder';
import { TimeFromNow } from '../../time-from-now';
@@ -174,7 +176,6 @@ interface Props {
interface State {
body: GraphQLBody;
operations: string[];
hideSchemaFetchErrors: boolean;
variablesSyntaxError: string;
explorerVisible: boolean;
activeReference: null | ActiveReference;
@@ -214,7 +215,6 @@ export const GraphQLEditor: FC<Props> = ({
operationName,
},
operations,
hideSchemaFetchErrors: false,
variablesSyntaxError: '',
activeReference: null,
explorerVisible: false,
@@ -343,16 +343,16 @@ export const GraphQLEditor: FC<Props> = ({
return '';
}
if (schemaIsFetching) {
return 'fetching schema...';
return 'Fetching schema...';
}
if (schemaLastFetchTime > 0) {
return (
<span>
schema fetched <TimeFromNow timestamp={schemaLastFetchTime} />
Schema fetched <TimeFromNow timestamp={schemaLastFetchTime} />
</span>
);
}
return <span>schema not yet fetched</span>;
return <span>Schema not fetched yet</span>;
};
const loadAndSetLocalSchema = async () => {
@@ -393,7 +393,6 @@ export const GraphQLEditor: FC<Props> = ({
};
const {
hideSchemaFetchErrors,
variablesSyntaxError,
activeReference,
explorerVisible,
@@ -468,13 +467,13 @@ export const GraphQLEditor: FC<Props> = ({
}
const canShowSchema = schema && !schemaIsFetching && !schemaFetchError && schemaLastFetchTime > 0;
return (
<div className="graphql-editor">
<Toolbar className="content-box sticky top-0 z-10 bg-[var(--color-bg)] flex flex-row border-b border-[var(--hl-md)] h-[var(--line-height-sm)] text-[var(--font-size-sm)]">
<>
<Toolbar aria-label='GraphQL toolbar' className="w-full flex-shrink-0 h-[--line-height-sm] border-b border-solid border-[--hl-md] flex items-center px-2">
<Dropdown
aria-label='Operations Dropdown'
isDisabled={!state.operations.length}
triggerButton={
<DropdownButton className="btn btn--compact">
<DropdownButton className="btn btn--compact text-[var(--hl)] p-[var(--padding-xs)] h-full">
{state.body.operationName || 'Operations'}
</DropdownButton>
}
@@ -495,7 +494,7 @@ export const GraphQLEditor: FC<Props> = ({
aria-label='Schema Dropdown'
triggerButton={
<DropdownButton
className="btn btn--compact"
className="btn btn--compact text-[var(--hl)] p-[var(--padding-xs)] h-full"
disableHoverBehavior={false}
removeBorderRadius
>
@@ -523,9 +522,6 @@ export const GraphQLEditor: FC<Props> = ({
icon={`refresh ${schemaIsFetching ? 'fa-spin' : ''}`}
label="Refresh Schema"
onClick={async () => {
// First, "forget" preference to hide errors so they always show
// again after a refresh
setState(state => ({ ...state, hideSchemaFetchErrors: false }));
setSchemaIsFetching(true);
const newState = await fetchGraphQLSchemaForRequest({
requestId: request._id,
@@ -573,7 +569,6 @@ export const GraphQLEditor: FC<Props> = ({
</>
}
onClick={() => {
setState(state => ({ ...state, hideSchemaFetchErrors: false }));
loadAndSetLocalSchema();
}}
/>
@@ -581,80 +576,85 @@ export const GraphQLEditor: FC<Props> = ({
</DropdownSection>
</Dropdown>
</Toolbar>
<div className="graphql-editor__query">
<CodeEditor
id="graphql-editor"
ref={editorRef}
dynamicHeight
showPrettifyButton
uniquenessKey={uniquenessKey ? uniquenessKey + '::query' : undefined}
defaultValue={requestBody.query || ''}
className={className}
onChange={changeQuery}
mode="graphql"
placeholder=""
hintOptions={graphqlOptions?.hintOptions}
infoOptions={graphqlOptions?.infoOptions}
jumpOptions={graphqlOptions?.jumpOptions}
lintOptions={graphqlOptions?.lintOptions}
/>
</div>
<div className="graphql-editor__schema-error">
{!hideSchemaFetchErrors && schemaFetchError && (
<div className="notice error margin no-margin-top margin-bottom-sm">
<div className="pull-right">
<button
className="icon"
onClick={() => setState(state => ({ ...state, hideSchemaFetchErrors: true }))}
>
<i className="fa fa-times" />
</button>
</div>
{schemaFetchError.message}
<br />
<PanelGroup direction={'vertical'}>
<Panel id="GraphQL Editor" minSize={20} defaultSize={60}>
<CodeEditor
id="graphql-editor"
ref={editorRef}
dynamicHeight
showPrettifyButton
uniquenessKey={uniquenessKey ? uniquenessKey + '::query' : undefined}
defaultValue={requestBody.query || ''}
className={className}
onChange={changeQuery}
mode="graphql"
placeholder=""
hintOptions={graphqlOptions?.hintOptions}
infoOptions={graphqlOptions?.infoOptions}
jumpOptions={graphqlOptions?.jumpOptions}
lintOptions={graphqlOptions?.lintOptions}
/>
</Panel>
<PanelResizeHandle className={'w-full h-[1px] bg-[--hl-md]'} />
<Panel id="GraphQL Variables editor" className='flex flex-col' minSize={20}>
<Heading className="w-full px-2 text-[--hl] select-none flex-shrink-0 h-[--line-height-sm] border-b border-solid border-[--hl-md] flex items-center">
Query Variables
<HelpTooltip className="space-left">
Variables to use in GraphQL query <br />
(JSON format)
</HelpTooltip>
{variablesSyntaxError && (
<span className="text-danger italic pull-right">{variablesSyntaxError}</span>
)}
</Heading>
<div className='flex-1 overflow-hidden'>
<CodeEditor
id="graphql-editor-variables"
dynamicHeight
enableNunjucks
uniquenessKey={uniquenessKey ? uniquenessKey + '::variables' : undefined}
showPrettifyButton={false}
defaultValue={jsonPrettify(requestBody.variables)}
className={className}
getAutocompleteConstants={() => Object.keys(variableTypes)}
lintOptions={{
variableToType: variableTypes,
}}
noLint={!variableTypes}
onChange={changeVariables}
mode="graphql-variables"
placeholder=""
/>
</div>
)}
</div>
<div className="graphql-editor__meta">
{renderSchemaFetchMessage()}
</div>
<h2 className="no-margin pad-left-sm pad-top-sm pad-bottom-sm">
Query Variables
<HelpTooltip className="space-left">
Variables to use in GraphQL query <br />
(JSON format)
</HelpTooltip>
{variablesSyntaxError && (
<span className="text-danger italic pull-right">{variablesSyntaxError}</span>
)}
</h2>
<div className="graphql-editor__variables">
<CodeEditor
id="graphql-editor-variables"
dynamicHeight
enableNunjucks
uniquenessKey={uniquenessKey ? uniquenessKey + '::variables' : undefined}
showPrettifyButton={false}
defaultValue={jsonPrettify(requestBody.variables)}
className={className}
getAutocompleteConstants={() => Object.keys(variableTypes)}
lintOptions={{
variableToType: variableTypes,
}}
noLint={!variableTypes}
onChange={changeVariables}
mode="graphql-variables"
placeholder=""
/>
</div>
<div className="flex flex-row items-center border-solid border-t border-[--hl-md] h-[--line-height-sm] text-[--font-size-sm]">
<Button className="px-4 py-1 h-full flex items-center justify-center gap-2 aria-pressed:bg-[--hl-sm] text-[--color-font] text-xs hover:bg-[--hl-xs] focus:ring-inset ring-1 ring-transparent focus:ring-[--hl-md] transition-all" onPress={beautifyRequestBody}>
</Panel>
</PanelGroup>
<Toolbar className="w-full overflow-y-auto select-none flex-shrink-0 h-[--line-height-sm] border-t border-solid border-[--hl-md] flex items-center">
<Button className="px-4 py-1 h-full flex items-center justify-center gap-2 aria-pressed:bg-[--hl-sm] text-[--color-font] text-sm hover:bg-[--hl-xs] focus:ring-inset ring-1 ring-transparent focus:ring-[--hl-md] transition-all" onPress={beautifyRequestBody}>
Prettify GraphQL
</Button>
</div>
<span className='flex-1' />
{!schemaFetchError && <div className="flex flex-shrink-0 items-center gap-2 text-sm px-2">
<Icon icon="info-circle" />
{renderSchemaFetchMessage()}
</div>}
{schemaFetchError && (
<Group className="flex items-center h-full">
<TooltipTrigger>
<Button className="px-4 py-1 h-full flex items-center justify-center gap-2 aria-pressed:bg-[--hl-sm] text-[--color-font] text-sm hover:bg-[--hl-xs] focus:ring-inset ring-1 ring-transparent focus:ring-[--hl-md] transition-all">
<Icon icon="exclamation-triangle" className='text-[--color-warning]' />
<span>Error fetching Schema</span>
</Button>
<Tooltip
offset={8}
className="border select-none text-sm max-w-xs border-solid border-[--hl-sm] shadow-lg bg-[--color-bg] text-[--color-font] px-4 py-2 rounded-md overflow-y-auto max-h-[85vh] focus:outline-none"
>
{schemaFetchError.message}
</Tooltip>
</TooltipTrigger>
</Group>
)}
</Toolbar>
{graphQLExplorerPortal}
</div>
</>
);
};

View File

@@ -1,11 +1,12 @@
import { Snippet } from 'codemirror';
import { CookieObject, Environment, InsomniaObject, Request as ScriptRequest, RequestInfo, Url, Variables } from 'insomnia-sdk';
import React, { FC, useRef } from 'react';
import { Button, Collection, Header, Menu, MenuItem, MenuTrigger, Popover, Section, Toolbar } from 'react-aria-components';
import { Settings } from '../../../models/settings';
import { translateHandlersInScript } from '../../../utils/importers/importers/postman';
import { Dropdown, DropdownButton, DropdownItem, DropdownSection, ItemContent } from '../base/dropdown';
import { CodeEditor, CodeEditorHandle } from '../codemirror/code-editor';
import { Icon } from '../icon';
interface Props {
onChange: (value: string) => void;
@@ -139,6 +140,207 @@ function getRequestScriptSnippets(insomniaObject: InsomniaObject, path: string):
return snippets;
}
interface SnippetMenuItem {
id: string;
name: string;
items: ({
id: string;
name: string;
snippet: string;
} | {
id: string;
name: string;
items: {
id: string;
name: string;
snippet: string;
}[];
})[];
}
const variableSnippetsMenu: SnippetMenuItem = {
'id': 'variable-snippets',
'name': 'Variable Snippets',
items: [
{
'id': 'get-values',
'name': 'Get values',
items: [
{
'id': 'get-env-var',
'name': 'Get an environment variable',
'snippet': getEnvVar,
},
// {
// "id": "get-glb-var",
// "name": "Get a global variable",
// "snippet": getGlbVar,
// },
{
'id': 'get-var',
'name': 'Get a variable',
'snippet': getVar,
},
{
'id': 'get-collection-var',
'name': 'Get a collection variable',
'snippet': getCollectionVar,
},
],
},
{
id: 'set-values',
name: 'Set values',
items: [
{
'id': 'set-env-var',
'name': 'Set an environment variable',
'snippet': setEnvVar,
},
// {
// "id": "set-glb-var",
// "name": "Set a global variable",
// "snippet": setGlbVar,
// },
{
'id': 'set-var',
'name': 'Set a variable',
'snippet': setVar,
},
{
'id': 'set-collection-var',
'name': 'Set a collection variable',
'snippet': setCollectionVar,
},
],
},
{
id: 'clear-values',
name: 'Clear values',
items: [
{
'id': 'unset-env-var',
'name': 'Clear an environment variable',
'snippet': unsetEnvVar,
},
// {
// "id": "unset-glb-var",
// "name": "Clear a global variable",
// "snippet": unsetGlbVar,
// },
{
'id': 'unset-collection-var',
'name': 'Clear a collection variable',
'snippet': unsetCollectionVar,
},
],
},
],
};
const requestManipulationMenu: SnippetMenuItem = {
id: 'request-manipulation',
name: 'Request Manipulation',
items: [
{
'id': 'add-query-param',
'name': 'Add query param',
'snippet': addQueryParams,
},
{
'id': 'set-method',
'name': 'Set method',
'snippet': setMethod,
},
{
'id': 'add-header',
'name': 'Add a header',
'snippet': addHeader,
},
{
'id': 'remove-header',
'name': 'Remove header',
'snippet': removeHeader,
},
{
'id': 'update-body-raw',
'name': 'Update body as raw',
'snippet': updateRequestBody,
},
{
'id': 'update-auth-method',
'name': 'Update auth method',
'snippet': updateRequestAuth,
},
],
};
const responseHandlingMenu: SnippetMenuItem = {
id: 'response-handling',
name: 'Response Handling',
items: [
{
'id': 'get-status-code',
'name': 'Get status code',
'snippet': getStatusCode,
},
{
'id': 'get-status-message',
'name': 'Get status message',
'snippet': getStatusMsg,
},
{
'id': 'get-response-time',
'name': 'Get response time',
'snippet': getRespTime,
},
{
'id': 'get-body-json',
'name': 'Get body as JSON',
'snippet': getJsonBody,
},
{
'id': 'get-body-text',
'name': 'Get body as text',
'snippet': getTextBody,
},
{
'id': 'find-header',
'name': 'Find a header by name',
'snippet': findHeader,
},
{
'id': 'get-cookies',
'name': 'Get cookies',
'snippet': getCookies,
},
],
};
const miscMenu: SnippetMenuItem = {
id: 'misc',
name: 'Misc',
items: [
{
'id': 'send-request',
'name': 'Send a request',
'snippet': sendReq,
},
{
'id': 'print-log',
'name': 'Print log',
'snippet': logValue,
},
{
'id': 'require-module',
'name': 'Require a module',
'snippet': requireAModule,
},
],
};
const snippetsMenus: SnippetMenuItem[] = [variableSnippetsMenu, requestManipulationMenu, responseHandlingMenu, miscMenu];
export const RequestScriptEditor: FC<Props> = ({
className,
defaultValue,
@@ -194,6 +396,7 @@ export const RequestScriptEditor: FC<Props> = ({
cookies: [],
}),
requestInfo: new RequestInfo({
// @TODO - Look into this event name when we introduce iteration data
eventName: 'prerequest',
iteration: 1,
iterationCount: 1,
@@ -208,289 +411,63 @@ export const RequestScriptEditor: FC<Props> = ({
);
return (
<div className='h-full flex flex-col'>
<div className="flex-1">
<CodeEditor
id={`script-editor-${uniquenessKey}`}
key={uniquenessKey}
disableContextMenu={true}
showPrettifyButton={true}
uniquenessKey={uniquenessKey}
defaultValue={defaultValue}
className={className}
onChange={onChange}
mode='text/javascript'
placeholder="..."
lintOptions={lintOptions}
ref={editorRef}
getAutocompleteSnippets={() => requestScriptSnippets}
onPaste={translateHandlersInScript}
/>
</div>
<div className="flex flex-row border-solid border-t border-[var(--hl-md)] h-[var(--line-height-sm)] text-[var(--font-size-sm)] box-border overflow-x-auto">
<Dropdown
aria-label='Variable Snippets'
placement='top left'
triggerButton={
<DropdownButton>
<ItemContent
icon="code"
label='Variable Snippets'
/>
</DropdownButton>
}
>
<DropdownSection
aria-label="Get values"
title="Get values"
>
<div className='h-full flex flex-col divide-y divide-solid divide-[--hl-md]'>
<CodeEditor
id={`script-editor-${uniquenessKey}`}
key={uniquenessKey}
disableContextMenu={true}
showPrettifyButton={true}
uniquenessKey={uniquenessKey}
defaultValue={defaultValue}
className={className}
onChange={onChange}
mode='text/javascript'
placeholder="..."
lintOptions={lintOptions}
ref={editorRef}
getAutocompleteSnippets={() => requestScriptSnippets}
onPaste={translateHandlersInScript}
/>
<Toolbar className="flex items-center h-[--line-height-sm] flex-shrink-0 flex-row text-[var(--font-size-sm)] box-border overflow-x-auto">
{snippetsMenus.map(menu => (
<MenuTrigger key={menu.id}>
<Button className="flex gap-2 px-2 items-center justify-center h-full aria-pressed:bg-[--hl-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="code" />
{menu.name}
</Button>
<Popover className="min-w-max">
<Menu
aria-label="Create a new request"
selectionMode="single"
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"
items={menu.items}
>
{item => {
if ('items' in item) {
return (
<Section>
<Header className='pl-2 py-1 text-[--hl] text-xs uppercase'>
{item.name}
</Header>
<Collection items={item.items}>
{item => (
<MenuItem onAction={() => addSnippet(item.snippet)} 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" key={item.name}>{item.name}</MenuItem>
)}
</Collection>
</Section>
);
}
<DropdownItem textValue='Get an environment variable' arial-label={'Get an environment variable'}>
<ItemContent
icon="sliders"
label='Get an environment variable'
onClick={() => addSnippet(getEnvVar)}
/>
</DropdownItem>
{/* <DropdownItem textValue='Get a global variable' arial-label={'Get a global variable'}>
<ItemContent
icon="sliders"
label='Get a global variable'
onClick={() => addSnippet(getGlbVar)}
/>
</DropdownItem> */}
<DropdownItem textValue='Get a variable' arial-label={'Get a variable'}>
<ItemContent
icon="sliders"
label='Get a variable'
onClick={() => addSnippet(getVar)}
/>
</DropdownItem>
<DropdownItem textValue='Get a collection variable' arial-label={'Get a collection variable'}>
<ItemContent
icon="sliders"
label='Get a collection variable'
onClick={() => addSnippet(getCollectionVar)}
/>
</DropdownItem>
</DropdownSection>
return (
<MenuItem onAction={() => addSnippet(item.snippet)} 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" key={item.name}>{item.name}</MenuItem>
);
}}
</Menu>
</Popover>
</MenuTrigger>
))}
<DropdownSection
aria-label="Set values"
title="Set values"
>
<DropdownItem textValue='Set an environment variable' arial-label={'Set an environment variable'}>
<ItemContent
icon="circle-plus"
label='Set an environment variable'
onClick={() => addSnippet(setEnvVar)}
/>
</DropdownItem>
{/* <DropdownItem textValue='Set a global variable' arial-label={'Set a global variable'}>
<ItemContent
icon="circle-plus"
label='Set a global variable'
onClick={() => addSnippet(setGlbVar)}
/>
</DropdownItem> */}
<DropdownItem textValue='Set a variable' arial-label={'Set a variable'}>
<ItemContent
icon="circle-plus"
label='Set a variable'
onClick={() => addSnippet(setVar)}
/>
</DropdownItem>
<DropdownItem textValue='Set a collection variable' arial-label={'Set a collection variable'}>
<ItemContent
icon="circle-plus"
label='Set a collection variable'
onClick={() => addSnippet(setCollectionVar)}
/>
</DropdownItem>
</DropdownSection>
<DropdownSection
aria-label="Clear values"
title="Clear values"
>
<DropdownItem textValue='Clear an environment variable' arial-label={'Clear an environment variable'}>
<ItemContent
icon="circle-minus"
label='Clear an environment variable'
onClick={() => addSnippet(unsetEnvVar)}
/>
</DropdownItem>
{/* <DropdownItem textValue='Clear a global variable' arial-label={'Clear a global variable'}>
<ItemContent
icon="circle-minus"
label='Clear a global variable'
onClick={() => addSnippet(unsetGlbVar)}
/>
</DropdownItem> */}
<DropdownItem textValue='Clear a collection variable' arial-label={'Clear a collection variable'}>
<ItemContent
icon="circle-minus"
label='Clear a collection variable'
onClick={() => addSnippet(unsetCollectionVar)}
/>
</DropdownItem>
</DropdownSection>
</Dropdown>
<Dropdown
aria-label='Request Manipulation'
placement='top left'
triggerButton={
<DropdownButton>
<ItemContent
icon="code"
label='Request Manipulation'
/>
</DropdownButton>
}
>
<DropdownItem textValue='Add query param' arial-label={'Add query param'}>
<ItemContent
icon="circle-plus"
label='Add a query param'
onClick={() => addSnippet(addQueryParams)}
/>
</DropdownItem>
<DropdownItem textValue='Set method' arial-label={'Set method'}>
<ItemContent
icon="circle-info"
label='Set method'
onClick={() => addSnippet(setMethod)}
/>
</DropdownItem>
<DropdownItem textValue='Add a header' arial-label={'Add a header'}>
<ItemContent
icon="circle-plus"
label='Add a header'
onClick={() => addSnippet(addHeader)}
/>
</DropdownItem>
<DropdownItem textValue='Remove header' arial-label={'Remove header'}>
<ItemContent
icon="circle-minus"
label='Remove a header'
onClick={() => addSnippet(removeHeader)}
/>
</DropdownItem>
<DropdownItem textValue='Update body as raw' arial-label={'Update body as raw'}>
<ItemContent
icon="circle-info"
label='Update body as raw'
onClick={() => addSnippet(updateRequestBody)}
/>
</DropdownItem>
<DropdownItem textValue='Update auth method' arial-label={'Update auth method'}>
<ItemContent
icon="circle-user"
label='Update auth method'
onClick={() => addSnippet(updateRequestAuth)}
/>
</DropdownItem>
</Dropdown>
<Dropdown
aria-label='Response Handling'
placement='top left'
triggerButton={
<DropdownButton>
<ItemContent
icon="code"
label='Response Handling'
/>
</DropdownButton>
}
>
<DropdownItem textValue='Get status code' arial-label={'Get status code'}>
<ItemContent
icon="circle-info"
label='Get status code'
onClick={() => addSnippet(getStatusCode)}
/>
</DropdownItem>
<DropdownItem textValue='Get status message' arial-label={'Get status message'}>
<ItemContent
icon="circle-info"
label='Get status message'
onClick={() => addSnippet(getStatusMsg)}
/>
</DropdownItem>
<DropdownItem textValue='Get response time' arial-label={'Get response time'}>
<ItemContent
icon="circle-info"
label='Get response time'
onClick={() => addSnippet(getRespTime)}
/>
</DropdownItem>
<DropdownItem textValue='Get body as JSON' arial-label={'Get body as JSON'}>
<ItemContent
icon="circle-info"
label='Get body as JSON'
onClick={() => addSnippet(getJsonBody)}
/>
</DropdownItem>
<DropdownItem textValue='Get body as text' arial-label={'Get body as text'}>
<ItemContent
icon="circle-info"
label='Get body as text'
onClick={() => addSnippet(getTextBody)}
/>
</DropdownItem>
<DropdownItem textValue='Find a header by name' arial-label={'Find a header by name'}>
<ItemContent
icon="circle-info"
label='Find a header by name'
onClick={() => addSnippet(findHeader)}
/>
</DropdownItem>
<DropdownItem textValue='Get cookies' arial-label={'Get cookies'}>
<ItemContent
icon="circle-info"
label='Get cookies'
onClick={() => addSnippet(getCookies)}
/>
</DropdownItem>
</Dropdown>
<Dropdown
aria-label='Misc'
placement='top left'
triggerButton={
<DropdownButton>
<ItemContent
icon="code"
label='Misc'
/>
</DropdownButton>
}
>
<DropdownItem textValue='Send a request' arial-label={'Send a request'}>
<ItemContent
icon="circle-play"
label='Send a request'
onClick={() => addSnippet(sendReq)}
/>
</DropdownItem>
<DropdownItem textValue='Print log' arial-label={'Print log'}>
<ItemContent
icon="print"
label='Print log'
onClick={() => addSnippet(logValue)}
/>
</DropdownItem>
<DropdownItem textValue='Require a module' arial-label={'Require a module'}>
<ItemContent
icon="circle-plus"
label='Require a module'
onClick={() => addSnippet(requireAModule)}
/>
</DropdownItem>
</Dropdown>
</div>
</Toolbar>
</div>
);
};

View File

@@ -1,12 +1,11 @@
import React, { FC, useState } from 'react';
import { Tab, TabList, TabPanel, Tabs } from 'react-aria-components';
import { useRouteLoaderData } from 'react-router-dom';
import { Settings } from '../../../models/settings';
import { useActiveRequestSyncVCSVersion, useGitVCSVersion } from '../../hooks/use-vcs-version';
import { RequestGroupLoaderData } from '../../routes/request-group';
import { WorkspaceLoaderData } from '../../routes/workspace';
import { PanelContainer, TabItem, Tabs } from '../base/tabs';
import { AuthDropdown } from '../dropdowns/auth-dropdown';
import { AuthWrapper } from '../editors/auth/auth-wrapper';
import { RequestHeadersEditor } from '../editors/request-headers-editor';
import { ErrorBoundary } from '../error-boundary';
@@ -25,106 +24,105 @@ export const RequestGroupPane: FC<{ settings: Settings }> = ({ }) => {
return (
<>
<Tabs aria-label="Request group pane tabs">
<TabItem key="auth" title={<AuthDropdown authentication={activeRequestGroup.authentication} />}>
<Tabs aria-label='Request group tabs' className="flex-1 w-full h-full flex flex-col">
<TabList className='w-full flex-shrink-0 overflow-x-auto border-solid scro border-b border-b-[--hl-md] bg-[--color-bg] flex items-center h-[--line-height-sm]' aria-label='Request pane tabs'>
<Tab
className='flex-shrink-0 h-full flex items-center justify-between cursor-pointer gap-2 outline-none select-none px-3 py-1 text-[--hl] aria-selected:text-[--color-font] hover:bg-[--hl-sm] hover:text-[--color-font] aria-selected:bg-[--hl-xs] aria-selected:focus:bg-[--hl-sm] aria-selected:hover:bg-[--hl-sm] focus:bg-[--hl-sm] transition-colors duration-300'
id='auth'
>
Auth
</Tab>
<Tab
className='flex-shrink-0 h-full flex items-center justify-between cursor-pointer gap-2 outline-none select-none px-3 py-1 text-[--hl] aria-selected:text-[--color-font] hover:bg-[--hl-sm] hover:text-[--color-font] aria-selected:bg-[--hl-xs] aria-selected:focus:bg-[--hl-sm] aria-selected:hover:bg-[--hl-sm] focus:bg-[--hl-sm] transition-colors duration-300'
id='headers'
>
<span>Headers</span>
{headersCount > 0 && (
<span className='p-1 min-w-6 h-6 flex items-center justify-center text-xs rounded-lg border border-solid border-[--hl]'>
{headersCount}
</span>
)}
</Tab>
<Tab
className='flex-shrink-0 h-full flex items-center justify-between cursor-pointer gap-2 outline-none select-none px-3 py-1 text-[--hl] aria-selected:text-[--color-font] hover:bg-[--hl-sm] hover:text-[--color-font] aria-selected:bg-[--hl-xs] aria-selected:focus:bg-[--hl-sm] aria-selected:hover:bg-[--hl-sm] focus:bg-[--hl-sm] transition-colors duration-300'
id='docs'
>
Docs
</Tab>
</TabList>
<TabPanel className='w-full flex-1 flex flex-col overflow-hidden' id='auth'>
<ErrorBoundary
key={uniqueKey}
errorClassName="font-error pad text-center"
>
<div />
<AuthWrapper authentication={activeRequestGroup.authentication} />
</ErrorBoundary>
</TabItem>
<TabItem
key="headers"
title={
<div className='flex items-center gap-2'>
Headers{' '}
{headersCount > 0 && (
<span className="p-2 aspect-square flex items-center color-inherit justify-between border-solid border border-[--hl-md] overflow-hidden rounded-lg text-xs shadow-small">{headersCount}</span>
)}
</TabPanel>
<TabPanel className='w-full flex-1 overflow-y-auto ' id='headers'>
<ErrorBoundary
key={uniqueKey}
errorClassName="font-error pad text-center"
>
<RequestHeadersEditor
bulk={false}
headers={folderHeaders}
requestType="RequestGroup"
/>
</ErrorBoundary>
</TabPanel>
<TabPanel className='w-full flex-1 overflow-y-auto ' id='docs'>
{activeRequestGroup.description ? (
<div>
<div className="pull-right pad bg-default">
<button
className="btn btn--clicky"
onClick={() => setIsRequestGroupSettingsModalOpen(true)}
>
Edit
</button>
</div>
<div className="pad">
<ErrorBoundary errorClassName="font-error pad text-center">
<MarkdownPreview
heading={activeRequestGroup.name}
markdown={activeRequestGroup.description}
/>
</ErrorBoundary>
</div>
</div>
}
>
<div className="flex flex-col relative h-full overflow-hidden">
<ErrorBoundary
key={uniqueKey}
errorClassName="font-error pad text-center"
>
<div className='overflow-y-auto flex-1 flex-shrink-0'>
<RequestHeadersEditor
bulk={false}
headers={folderHeaders}
requestType="RequestGroup"
/>
</div>
</ErrorBoundary>
</div>
</TabItem>
<TabItem
key="docs"
title={
<>
Docs
{activeRequestGroup.description && (
<span className="ml-2 p-2 border-solid border border-[--hl-md] rounded-lg">
<span className="flex w-2 h-2 bg-green-500 rounded-full" />
) : (
<div className="overflow-hidden editor vertically-center text-center">
<p className="pad text-sm text-center">
<span className="super-faint">
<i
className="fa fa-file-text-o"
style={{
fontSize: '8rem',
opacity: 0.3,
}}
/>
</span>
)}
</>
}
>
<PanelContainer className="tall">
{activeRequestGroup.description ? (
<div>
<div className="pull-right pad bg-default">
<button
className="btn btn--clicky"
onClick={() => setIsRequestGroupSettingsModalOpen(true)}
>
Edit
</button>
</div>
<div className="pad">
<ErrorBoundary errorClassName="font-error pad text-center">
<MarkdownPreview
heading={activeRequestGroup.name}
markdown={activeRequestGroup.description}
/>
</ErrorBoundary>
</div>
</div>
) : (
<div className="overflow-hidden editor vertically-center text-center">
<p className="pad text-sm text-center">
<span className="super-faint">
<i
className="fa fa-file-text-o"
style={{
fontSize: '8rem',
opacity: 0.3,
}}
/>
</span>
<br />
<br />
<button
className="btn btn--clicky faint"
onClick={() => setIsRequestGroupSettingsModalOpen(true)}
>
Add Description
</button>
</p>
</div>
)}
</PanelContainer>
</TabItem>
<br />
<br />
<button
className="btn btn--clicky faint"
onClick={() => setIsRequestGroupSettingsModalOpen(true)}
>
Add Description
</button>
</p>
</div>
)}
</TabPanel>
</Tabs>
{isRequestGroupSettingsModalOpen && (
<RequestGroupSettingsModal
requestGroup={activeRequestGroup}
onHide={() => setIsRequestGroupSettingsModalOpen(false)}
/>
)}</>
{
isRequestGroupSettingsModalOpen && (
<RequestGroupSettingsModal
requestGroup={activeRequestGroup}
onHide={() => setIsRequestGroupSettingsModalOpen(false)}
/>
)
}
</>
);
};

View File

@@ -1,5 +1,5 @@
import React, { FC, Fragment, useState } from 'react';
import { Button, Heading, ToggleButton } from 'react-aria-components';
import { Button, Heading, Tab, TabList, TabPanel, Tabs, ToggleButton } from 'react-aria-components';
import { useParams, useRouteLoaderData } from 'react-router-dom';
import { useLocalStorage } from 'react-use';
@@ -13,10 +13,7 @@ import { useRequestPatcher, useSettingsPatcher } from '../../hooks/use-request';
import { useActiveRequestSyncVCSVersion, useGitVCSVersion } from '../../hooks/use-vcs-version';
import { RequestLoaderData } from '../../routes/request';
import { WorkspaceLoaderData } from '../../routes/workspace';
import { PanelContainer, TabItem, Tabs } from '../base/tabs';
import { OneLineEditor } from '../codemirror/one-line-editor';
import { AuthDropdown } from '../dropdowns/auth-dropdown';
import { ContentTypeDropdown } from '../dropdowns/content-type-dropdown';
import { AuthWrapper } from '../editors/auth/auth-wrapper';
import { BodyEditor } from '../editors/body/body-editor';
import { RequestHeadersEditor } from '../editors/request-headers-editor';
@@ -112,39 +109,86 @@ export const RequestPane: FC<Props> = ({
/>
</ErrorBoundary>
</PaneHeader>
<Tabs aria-label="Request pane tabs">
<TabItem
key="query"
title={
<div className='flex items-center gap-2'>
Parameters
{parametersCount > 0 && (
<span className="p-2 aspect-square flex items-center color-inherit justify-between border-solid border border-[--hl-md] overflow-hidden rounded-lg text-xs shadow-small">{parametersCount}</span>
)}
<Tabs aria-label='Request pane tabs' className="flex-1 w-full h-full flex flex-col">
<TabList className='w-full flex-shrink-0 overflow-x-auto border-solid scro border-b border-b-[--hl-md] bg-[--color-bg] flex items-center h-[--line-height-sm]' aria-label='Request pane tabs'>
<Tab
className='flex-shrink-0 h-full flex items-center justify-between cursor-pointer gap-2 outline-none select-none px-3 py-1 text-[--hl] aria-selected:text-[--color-font] hover:bg-[--hl-sm] hover:text-[--color-font] aria-selected:bg-[--hl-xs] aria-selected:focus:bg-[--hl-sm] aria-selected:hover:bg-[--hl-sm] focus:bg-[--hl-sm] transition-colors duration-300'
id='params'
>
<span>Params</span>
{parametersCount > 0 && (
<span className='p-1 min-w-6 h-6 flex items-center justify-center text-xs rounded-lg border border-solid border-[--hl]'>
{parametersCount}
</span>
)}
</Tab>
<Tab
className='flex-shrink-0 h-full flex items-center justify-between cursor-pointer gap-2 outline-none select-none px-3 py-1 text-[--hl] aria-selected:text-[--color-font] hover:bg-[--hl-sm] hover:text-[--color-font] aria-selected:bg-[--hl-xs] aria-selected:focus:bg-[--hl-sm] aria-selected:hover:bg-[--hl-sm] focus:bg-[--hl-sm] transition-colors duration-300'
id='content-type'
>
<span>Body</span>
</Tab>
<Tab
className='flex-shrink-0 h-full flex items-center justify-between cursor-pointer gap-2 outline-none select-none px-3 py-1 text-[--hl] aria-selected:text-[--color-font] hover:bg-[--hl-sm] hover:text-[--color-font] aria-selected:bg-[--hl-xs] aria-selected:focus:bg-[--hl-sm] aria-selected:hover:bg-[--hl-sm] focus:bg-[--hl-sm] transition-colors duration-300'
id='auth'
>
<span>Auth</span>
</Tab>
<Tab
className='flex-shrink-0 h-full flex items-center justify-between cursor-pointer gap-2 outline-none select-none px-3 py-1 text-[--hl] aria-selected:text-[--color-font] hover:bg-[--hl-sm] hover:text-[--color-font] aria-selected:bg-[--hl-xs] aria-selected:focus:bg-[--hl-sm] aria-selected:hover:bg-[--hl-sm] focus:bg-[--hl-sm] transition-colors duration-300'
id='headers'
>
<span>Headers</span>
{headersCount > 0 && (
<span className='p-1 min-w-6 h-6 flex items-center justify-center text-xs rounded-lg border border-solid border-[--hl]'>
{headersCount}
</span>
)}
</Tab>
<Tab
className='flex-shrink-0 h-full flex items-center justify-between cursor-pointer gap-2 outline-none select-none px-3 py-1 text-[--hl] aria-selected:text-[--color-font] hover:bg-[--hl-sm] hover:text-[--color-font] aria-selected:bg-[--hl-xs] aria-selected:focus:bg-[--hl-sm] aria-selected:hover:bg-[--hl-sm] focus:bg-[--hl-sm] transition-colors duration-300'
id='scripts'
>
<span>Scripts</span>
{Boolean(activeRequest.preRequestScript || activeRequest.afterResponseScript) && (
<span className='p-1 min-w-6 h-6 flex items-center justify-center text-xs rounded-lg border border-solid border-[--hl]'>
<span className='w-2 h-2 bg-green-500 rounded-full' />
</span>
)}
</Tab>
<Tab
className='flex-shrink-0 h-full flex items-center justify-between cursor-pointer gap-2 outline-none select-none px-3 py-1 text-[--hl] aria-selected:text-[--color-font] hover:bg-[--hl-sm] hover:text-[--color-font] aria-selected:bg-[--hl-xs] aria-selected:focus:bg-[--hl-sm] aria-selected:hover:bg-[--hl-sm] focus:bg-[--hl-sm] transition-colors duration-300'
id='docs'
>
<span>Docs</span>
{activeRequest.description && (
<span className='p-1 min-w-6 h-6 flex items-center justify-center text-xs rounded-lg border border-solid border-[--hl]'>
<span className='w-2 h-2 bg-green-500 rounded-full' />
</span>
)}
</Tab>
</TabList>
<TabPanel className='w-full flex-1 flex flex-col h-full overflow-y-auto' id='params'>
<div className="p-4 flex-shrink-0">
<div className="text-xs max-h-32 flex flex-col overflow-y-auto min-h-[2em] bg-[--hl-xs] px-2 py-1 border border-solid border-[--hl-sm]">
<label className="label--small no-pad-top">Url Preview</label>
<ErrorBoundary
key={uniqueKey}
errorClassName="tall wide vertically-align font-error pad text-center"
>
<RenderedQueryString request={activeRequest} />
</ErrorBoundary>
</div>
}
>
<div className='h-full flex flex-col'>
<div className="p-4">
<div className="text-xs max-h-32 flex flex-col overflow-y-auto min-h-[2em] bg-[--hl-xs] px-2 py-1 border border-solid border-[--hl-sm]">
<label className="label--small no-pad-top">Url Preview</label>
<ErrorBoundary
key={uniqueKey}
errorClassName="tall wide vertically-align font-error pad text-center"
>
<RenderedQueryString request={activeRequest} />
</ErrorBoundary>
</div>
</div>
<div className="grid flex-1 [grid-template-rows:minmax(auto,min-content)] [grid-template-columns:100%] overflow-hidden">
<div className="min-h-[2rem] max-h-full flex flex-col overflow-y-auto [&_.key-value-editor]:p-0 flex-1">
<div className='flex items-center w-full p-4 h-4 justify-between'>
</div>
<div className="flex-shrink-0 grid flex-1 [grid-template-rows:minmax(auto,min-content)] [grid-template-columns:100%] overflow-hidden">
<div className="min-h-[2rem] max-h-full flex flex-col overflow-y-auto [&_.key-value-editor]:p-0 flex-1">
<div className='flex items-center w-full p-4 h-4 justify-between'>
<Heading className='text-xs font-bold uppercase text-[--hl]'>Query parameters</Heading>
<div className='flex items-center gap-2'>
<Button
isDisabled={!urlHasQueryParameters}
onPress={handleImportQueryFromUrl}
className="w-[14ch] flex flex-shrink-0 gap-2 items-center justify-start px-2 py-1 h-full aria-pressed:bg-[--hl-sm] aria-selected: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"
className="w-[14ch] flex flex-shrink-0 gap-2 items-center justify-start px-2 py-1 h-full asma-pressed:bg-[--hl-sm] aria-selected:bg-[--hl-xs] aria-selected:focus:bg-[--hl-sm] aria-selected:hover:bg-[--hl-sm] focus:bg-[--hl-sm] rounded-sm text-[--color-font] hover:bg-[--hl-xs] focus:ring-inset ring-1 ring-transparent focus:ring-[--hl-md] transition-colors text-sm"
>
Import from URL
</Button>
@@ -155,7 +199,7 @@ export const RequestPane: FC<Props> = ({
});
}}
isSelected={settings.useBulkParametersEditor}
className="w-[14ch] flex flex-shrink-0 gap-2 items-center justify-start px-2 py-1 h-full rounded-sm text-[--color-font] hover:bg-[--hl-xs] focus:ring-inset ring-1 ring-transparent focus:ring-[--hl-md] transition-all text-sm"
className="w-[14ch] flex flex-shrink-0 gap-2 items-center justify-start px-2 py-1 h-full rounded-sm text-[--color-font] hover:bg-[--hl-xs] focus:ring-inset ring-1 ring-transparent focus:ring-[--hl-md] transition-colors text-sm"
>
{({ isSelected }) => (
<Fragment>
@@ -178,170 +222,151 @@ export const RequestPane: FC<Props> = ({
/>
</ErrorBoundary>
</div>
<div className='flex-1 flex flex-col gap-4 p-4 overflow-y-auto'>
<div className='flex-1 flex flex-col gap-4 p-4 overflow-y-auto'>
<Heading className='text-xs font-bold uppercase text-[--hl]'>Path parameters</Heading>
{pathParameters.length > 0 && (
<div className="pr-[72.73px] w-full">
<div className='grid gap-x-[20.8px] grid-cols-2 flex-shrink-0 w-full rounded-sm overflow-hidden'>
{pathParameters.map(pathParameter => (
<Fragment key={pathParameter.name}>
<span className='p-2 select-none border-b border-solid border-[--hl-md] truncate flex items-center justify-end rounded-sm'>
{pathParameter.name}
</span>
<div className='px-2 flex items-center h-full border-b border-solid border-[--hl-md]'>
<OneLineEditor
key={activeRequest._id}
id={'key-value-editor__name' + pathParameter.name}
placeholder="Parameter value"
defaultValue={pathParameter.value || ''}
onChange={name => {
onPathParameterChange(pathParameters.map(p => p.name === pathParameter.name ? { ...p, value: name } : p));
}}
/>
</div>
</Fragment>
))}
</div>
{pathParameters.length > 0 && (
<div className="pr-[72.73px] w-full">
<div className='grid gap-x-[20.8px] grid-cols-2 flex-shrink-0 w-full rounded-sm overflow-hidden'>
{pathParameters.map(pathParameter => (
<Fragment key={pathParameter.name}>
<span className='p-2 select-none border-b border-solid border-[--hl-md] truncate flex items-center justify-end rounded-sm'>
{pathParameter.name}
</span>
<div className='px-2 flex items-center h-full border-b border-solid border-[--hl-md]'>
<OneLineEditor
key={activeRequest._id}
id={'key-value-editor__name' + pathParameter.name}
placeholder="Parameter value"
defaultValue={pathParameter.value || ''}
onChange={name => {
onPathParameterChange(pathParameters.map(p => p.name === pathParameter.name ? { ...p, value: name } : p));
}}
/>
</div>
</Fragment>
))}
</div>
)}
{pathParameters.length === 0 && !dismissPathParameterTip && (
<div className='text-sm text-[--hl] rounded-sm border border-solid border-[--hl-md] p-2 flex items-center gap-2'>
<Icon icon='info-circle' />
<span>Path parameters are url path segments that start with a colon ':' e.g. ':id' </span>
<Button
className="flex flex-shrink-0 items-center justify-center aspect-square h-6 aria-pressed:bg-[--hl-sm] rounded-sm text-[--color-font] hover:bg-[--hl-xs] ml-auto"
onPress={() => setDismissPathParameterTip('true')}
>
<Icon icon='close' />
</Button>
</div>
)}
</div>
</div>
)}
{pathParameters.length === 0 && !dismissPathParameterTip && (
<div className='text-sm text-[--hl] rounded-sm border border-solid border-[--hl-md] p-2 flex items-center gap-2'>
<Icon icon='info-circle' />
<span>Path parameters are url path segments that start with a colon ':' e.g. ':id' </span>
<Button
className="flex flex-shrink-0 items-center justify-center aspect-square h-6 aria-pressed:bg-[--hl-sm] rounded-sm text-[--color-font] hover:bg-[--hl-xs] ml-auto"
onPress={() => setDismissPathParameterTip('true')}
>
<Icon icon='close' />
</Button>
</div>
)}
</div>
</div>
</TabItem>
<TabItem key="content-type" title={<ContentTypeDropdown />}>
</TabPanel>
<TabPanel className='w-full flex-1 flex flex-col' id='content-type'>
<BodyEditor
key={uniqueKey}
request={activeRequest}
environmentId={environmentId}
/>
</TabItem>
<TabItem key="auth" title={<AuthDropdown authentication={activeRequest.authentication} />}>
</TabPanel>
<TabPanel className='w-full flex-1 flex flex-col overflow-hidden' id='auth'>
<ErrorBoundary
key={uniqueKey}
errorClassName="font-error pad text-center"
>
<AuthWrapper authentication={activeRequest.authentication} />
</ErrorBoundary>
</TabItem>
<TabItem
key="headers"
title={
<div className='flex items-center gap-2'>
Headers{' '}
{headersCount > 0 && (
<span className="p-2 aspect-square flex items-center color-inherit justify-between border-solid border border-[--hl-md] overflow-hidden rounded-lg text-xs shadow-small">{headersCount}</span>
)}
</TabPanel>
<TabPanel className='w-full flex-1 flex flex-col relative overflow-hidden' id='headers'>
<ErrorBoundary
key={uniqueKey}
errorClassName="font-error pad text-center"
>
<div className='overflow-y-auto flex-1 flex-shrink-0'>
<RequestHeadersEditor
bulk={settings.useBulkHeaderEditor}
headers={activeRequest.headers}
requestType="Request"
/>
</div>
}
>
<div className="flex flex-col relative h-full overflow-hidden">
<ErrorBoundary
key={uniqueKey}
errorClassName="font-error pad text-center"
>
<div className='overflow-y-auto flex-1 flex-shrink-0'>
<RequestHeadersEditor
bulk={settings.useBulkHeaderEditor}
headers={activeRequest.headers}
requestType="Request"
/>
</div>
</ErrorBoundary>
</ErrorBoundary>
<div className="flex flex-row border-solid border-t border-[var(--hl-md)] h-[var(--line-height-sm)] text-[var(--font-size-sm)] box-border">
<Button
className="px-4 py-1 h-full flex items-center justify-center gap-2 aria-pressed:bg-[--hl-sm] text-[--color-font] text-xs hover:bg-[--hl-xs] focus:ring-inset ring-1 ring-transparent focus:ring-[--hl-md] transition-all"
onPress={() =>
patchSettings({
useBulkHeaderEditor: !settings.useBulkHeaderEditor,
})
}
>
{settings.useBulkHeaderEditor ? 'Regular Edit' : 'Bulk Edit'}
</Button>
</div>
<div className="flex flex-row border-solid border-t border-[var(--hl-md)] h-[var(--line-height-sm)] text-[var(--font-size-sm)] box-border">
<Button
className="px-4 py-1 h-full flex items-center justify-center gap-2 aria-pressed:bg-[--hl-sm] text-[--color-font] text-xs hover:bg-[--hl-xs] focus:ring-inset ring-1 ring-transparent focus:ring-[--hl-md] transition-colors"
onPress={() =>
patchSettings({
useBulkHeaderEditor: !settings.useBulkHeaderEditor,
})
}
>
{settings.useBulkHeaderEditor ? 'Regular Edit' : 'Bulk Edit'}
</Button>
</div>
</TabItem>
<TabItem
key="pre-request-script"
data-testid="pre-request-script-tab"
title={
<div className='flex items-center gap-2'>
Pre-request Script{' '}
{activeRequest.preRequestScript && (
<span className="ml-2 p-2 border-solid border border-[--hl-md] rounded-lg">
<span className="flex w-2 h-2 bg-green-500 rounded-full" />
</span>
)}
</div>
}
aria-label={'experimental'}
>
<ErrorBoundary
key={uniqueKey}
errorClassName="tall wide vertically-align font-error pad text-center"
>
<RequestScriptEditor
uniquenessKey={uniqueKey}
defaultValue={activeRequest.preRequestScript || ''}
onChange={preRequestScript => patchRequest(requestId, { preRequestScript })}
settings={settings}
/>
</ErrorBoundary>
</TabItem>
<TabItem
key="after-response-script"
data-testid="after-response-script-tab"
title={
<div className='flex items-center gap-2'>
After-response Script{' '}
{activeRequest.afterResponseScript && (
<span className="ml-2 p-2 border-solid border border-[--hl-md] rounded-lg">
<span className="flex w-2 h-2 bg-green-500 rounded-full" />
</span>
)}
</div>
}
aria-label={'experimental'}
>
<ErrorBoundary
key={uniqueKey}
errorClassName="tall wide vertically-align font-error pad text-center"
>
<RequestScriptEditor
uniquenessKey={uniqueKey}
defaultValue={activeRequest.afterResponseScript || ''}
onChange={afterResponseScript => patchRequest(requestId, { afterResponseScript })}
settings={settings}
/>
</ErrorBoundary>
</TabItem>
<TabItem
key="docs"
title={
<>
Docs
{activeRequest.description && (
<span className="ml-2 p-2 border-solid border border-[--hl-md] rounded-lg">
<span className="flex w-2 h-2 bg-green-500 rounded-full" />
</span>
)}
</>
}
>
<PanelContainer className="tall">
</TabPanel>
<TabPanel className='w-full flex-1' id='scripts'>
<Tabs className="w-full h-full flex flex-col overflow-hidden">
<TabList className="w-full flex-shrink-0 overflow-x-auto border-solid border-b border-b-[--hl-md] px-2 bg-[--color-bg] flex items-center gap-2 h-[--line-height-sm]" aria-label="Request scripts tabs">
<Tab
className="rounded-md flex-shrink-0 h-[--line-height-xxs] text-sm flex items-center justify-between cursor-pointer w-[10.5rem] outline-none select-none px-2 py-1 hover:bg-[rgba(var(--color-surprise-rgb),50%)] text-[--hl] aria-selected:text-[--color-font-surprise] hover:text-[--color-font-surprise] aria-selected:bg-[rgba(var(--color-surprise-rgb),40%)] transition-colors duration-300"
id="pre-request"
>
<div className='flex flex-1 items-center gap-2'>
<Icon icon="arrow-right-to-bracket" />
<span>Pre-request</span>
</div>
{Boolean(activeRequest.preRequestScript) && (
<span className="p-2 rounded-lg">
<span className="flex w-2 h-2 bg-green-500 rounded-full" />
</span>
)}
</Tab>
<Tab
className="rounded-md flex-shrink-0 h-[--line-height-xxs] text-sm flex items-center justify-between cursor-pointer w-[10.5rem] outline-none select-none px-2 py-1 hover:bg-[rgba(var(--color-surprise-rgb),50%)] text-[--hl] aria-selected:text-[--color-font-surprise] hover:text-[--color-font-surprise] aria-selected:bg-[rgba(var(--color-surprise-rgb),40%)] transition-colors duration-300"
id="after-response"
>
<div className='flex flex-1 items-center gap-2'>
<Icon icon="arrow-right-from-bracket" />
<span>After-response</span>
</div>
{Boolean(activeRequest.afterResponseScript) && (
<span className="p-2 rounded-lg">
<span className="flex w-2 h-2 bg-green-500 rounded-full" />
</span>
)}
</Tab>
</TabList>
<TabPanel className="w-full flex-1" id='pre-request'>
<ErrorBoundary
key={uniqueKey}
errorClassName="tall wide vertically-align font-error pad text-center"
>
<RequestScriptEditor
uniquenessKey={uniqueKey}
defaultValue={activeRequest.preRequestScript || ''}
onChange={preRequestScript => patchRequest(requestId, { preRequestScript })}
settings={settings}
/>
</ErrorBoundary>
</TabPanel>
<TabPanel className="w-full flex-1" id="after-response">
<ErrorBoundary
key={uniqueKey}
errorClassName="tall wide vertically-align font-error pad text-center"
>
<RequestScriptEditor
uniquenessKey={uniqueKey}
defaultValue={activeRequest.afterResponseScript || ''}
onChange={afterResponseScript => patchRequest(requestId, { afterResponseScript })}
settings={settings}
/>
</ErrorBoundary>
</TabPanel>
</Tabs>
</TabPanel>
<TabPanel className='w-full flex-1' id='docs'>
<div className="tall">
{activeRequest.description ? (
<div>
<div className="pull-right pad bg-default">
@@ -384,8 +409,8 @@ export const RequestPane: FC<Props> = ({
</p>
</div>
)}
</PanelContainer>
</TabItem>
</div>
</TabPanel>
</Tabs>
{isRequestSettingsModalOpen && (
<RequestSettingsModal

View File

@@ -1,5 +1,5 @@
import React, { FC, Fragment, useEffect, useRef, useState } from 'react';
import { Button, Heading } from 'react-aria-components';
import { Button, Heading, Tab, TabList, TabPanel, Tabs, ToggleButton, Toolbar } from 'react-aria-components';
import { useParams, useRouteLoaderData } from 'react-router-dom';
import { useLocalStorage } from 'react-use';
import styled from 'styled-components';
@@ -10,16 +10,14 @@ import { Environment } from '../../../models/environment';
import { AuthTypes, getCombinedPathParametersFromUrl, RequestPathParameter } from '../../../models/request';
import { WebSocketRequest } from '../../../models/websocket-request';
import { tryToInterpolateRequestOrShowRenderErrorModal } from '../../../utils/try-interpolate';
import { buildQueryStringFromParams, joinUrlAndQueryString } from '../../../utils/url/querystring';
import { buildQueryStringFromParams, deconstructQueryStringToParams, extractQueryStringFromUrl, joinUrlAndQueryString } from '../../../utils/url/querystring';
import { useReadyState } from '../../hooks/use-ready-state';
import { useRequestPatcher } from '../../hooks/use-request';
import { useRequestPatcher, useSettingsPatcher } from '../../hooks/use-request';
import { useActiveRequestSyncVCSVersion, useGitVCSVersion } from '../../hooks/use-vcs-version';
import { WebSocketRequestLoaderData } from '../../routes/request';
import { useRootLoaderData } from '../../routes/root';
import { TabItem, Tabs } from '../base/tabs';
import { CodeEditor, CodeEditorHandle } from '../codemirror/code-editor';
import { OneLineEditor } from '../codemirror/one-line-editor';
import { AuthDropdown } from '../dropdowns/auth-dropdown';
import { WebSocketPreviewMode } from '../dropdowns/websocket-preview-mode';
import { AuthWrapper } from '../editors/auth/auth-wrapper';
import { RequestHeadersEditor } from '../editors/request-headers-editor';
@@ -42,6 +40,7 @@ const SendMessageForm = styled.form({
position: 'relative',
boxSizing: 'border-box',
});
const SendButton = styled.button<{ isConnected: boolean }>(({ isConnected }) => ({
padding: '0 var(--padding-md)',
marginLeft: 'var(--padding-xs)',
@@ -55,15 +54,6 @@ const SendButton = styled.button<{ isConnected: boolean }>(({ isConnected }) =>
},
}));
const PaneSendButton = styled.div({
display: 'flex',
flexDirection: 'row',
justifyContent: 'flex-end',
boxSizing: 'border-box',
height: 'var(--line-height-sm)',
borderBottom: '1px solid var(--hl-lg)',
padding: 3,
});
const PaneHeader = styled(OriginalPaneHeader)({
'&&': { alignItems: 'stretch' },
});
@@ -209,10 +199,7 @@ export const WebSocketRequestPane: FC<Props> = ({ environment }) => {
const { workspaceId, requestId } = useParams() as { organizationId: string; projectId: string; workspaceId: string; requestId: string };
const readyState = useReadyState({ requestId: activeRequest._id, protocol: 'webSocket' });
const {
settings,
} = useRootLoaderData();
const { useBulkParametersEditor } = settings;
const { settings } = useRootLoaderData();
const disabled = readyState;
@@ -248,7 +235,7 @@ export const WebSocketRequestPane: FC<Props> = ({ environment }) => {
const parametersCount = pathParameters.length + activeRequest.parameters.filter(p => !p.disabled).length;
const headersCount = activeRequest.headers.filter(h => !h.disabled).length;
const patchSettings = useSettingsPatcher();
const upsertPayloadWithMode = async (mode: string) => {
// @TODO: multiple payloads
const payload = await models.webSocketPayload.getByParentId(requestId);
@@ -264,9 +251,33 @@ export const WebSocketRequestPane: FC<Props> = ({ environment }) => {
};
const [isRequestSettingsModalOpen, setIsRequestSettingsModalOpen] = useState(false);
const handleImportQueryFromUrl = () => {
let query;
try {
query = extractQueryStringFromUrl(activeRequest.url);
} catch (error) {
console.warn('Failed to parse url to import querystring');
return;
}
// Remove the search string (?foo=bar&...) from the Url
const url = activeRequest.url.replace(`?${query}`, '');
const parameters = [
...activeRequest.parameters,
...deconstructQueryStringToParams(query),
];
// Only update if url changed
if (url !== activeRequest.url) {
patchRequest(requestId, { url, parameters });
}
};
const gitVersion = useGitVCSVersion();
const activeRequestSyncVersion = useActiveRequestSyncVCSVersion();
const patchRequest = useRequestPatcher();
const urlHasQueryParameters = activeRequest.url.indexOf('?') >= 0;
// Reset the response pane state when we switch requests, the environment gets modified, or the (Git|Sync)VCS version changes
const uniqueKey = `${environment?.modified}::${requestId}::${gitVersion}::${activeRequestSyncVersion}::${activeRequestMeta.activeResponseId}`;
@@ -282,136 +293,176 @@ export const WebSocketRequestPane: FC<Props> = ({ environment }) => {
onChange={url => patchRequest(requestId, { url })}
/>
</PaneHeader>
<Tabs aria-label="Websocket request pane tabs">
<TabItem
key="query"
title={
<div className='flex items-center gap-2'>
Parameters
{parametersCount > 0 && (
<span className="p-2 aspect-square flex items-center color-inherit justify-between border-solid border border-[--hl-md] overflow-hidden rounded-lg text-xs shadow-small">{parametersCount}</span>
<Tabs aria-label='Websocket request pane tabs' className="flex-1 w-full h-full flex flex-col">
<TabList className='w-full flex-shrink-0 overflow-x-auto border-solid scro border-b border-b-[--hl-md] bg-[--color-bg] flex items-center h-[--line-height-sm]' aria-label='Request pane tabs'>
<Tab
className='flex-shrink-0 h-full flex items-center justify-between cursor-pointer gap-2 outline-none select-none px-3 py-1 text-[--hl] aria-selected:text-[--color-font] hover:bg-[--hl-sm] hover:text-[--color-font] aria-selected:bg-[--hl-xs] aria-selected:focus:bg-[--hl-sm] aria-selected:hover:bg-[--hl-sm] focus:bg-[--hl-sm] transition-colors duration-300'
id='params'
>
<span>Params</span>
{parametersCount > 0 && (
<span className='p-1 min-w-6 h-6 flex items-center justify-center text-xs rounded-lg border border-solid border-[--hl]'>
{parametersCount}
</span>
)}
</Tab>
<Tab
className='flex-shrink-0 h-full flex items-center justify-between cursor-pointer gap-2 outline-none select-none px-3 py-1 text-[--hl] aria-selected:text-[--color-font] hover:bg-[--hl-sm] hover:text-[--color-font] aria-selected:bg-[--hl-xs] aria-selected:focus:bg-[--hl-sm] aria-selected:hover:bg-[--hl-sm] focus:bg-[--hl-sm] transition-colors duration-300'
id='content-type'
>
<span>Body</span>
</Tab>
<Tab
className='flex-shrink-0 h-full flex items-center justify-between cursor-pointer gap-2 outline-none select-none px-3 py-1 text-[--hl] aria-selected:text-[--color-font] hover:bg-[--hl-sm] hover:text-[--color-font] aria-selected:bg-[--hl-xs] aria-selected:focus:bg-[--hl-sm] aria-selected:hover:bg-[--hl-sm] focus:bg-[--hl-sm] transition-colors duration-300'
id='auth'
>
<span>Auth</span>
</Tab>
<Tab
className='flex-shrink-0 h-full flex items-center justify-between cursor-pointer gap-2 outline-none select-none px-3 py-1 text-[--hl] aria-selected:text-[--color-font] hover:bg-[--hl-sm] hover:text-[--color-font] aria-selected:bg-[--hl-xs] aria-selected:focus:bg-[--hl-sm] aria-selected:hover:bg-[--hl-sm] focus:bg-[--hl-sm] transition-colors duration-300'
id='headers'
>
<span>Headers</span>
{headersCount > 0 && (
<span className='p-1 min-w-6 h-6 flex items-center justify-center text-xs rounded-lg border border-solid border-[--hl]'>
{headersCount}
</span>
)}
</Tab>
<Tab
className='flex-shrink-0 h-full flex items-center justify-between cursor-pointer gap-2 outline-none select-none px-3 py-1 text-[--hl] aria-selected:text-[--color-font] hover:bg-[--hl-sm] hover:text-[--color-font] aria-selected:bg-[--hl-xs] aria-selected:focus:bg-[--hl-sm] aria-selected:hover:bg-[--hl-sm] focus:bg-[--hl-sm] transition-colors duration-300'
id='docs'
>
Docs
</Tab>
</TabList>
<TabPanel className='w-full flex-1 flex flex-col h-full overflow-y-auto' id='params'>
{disabled && <PaneReadOnlyBanner />}
<div className="p-4 flex-shrink-0">
<div className="text-xs max-h-32 flex flex-col overflow-y-auto min-h-[2em] bg-[--hl-xs] px-2 py-1 border border-solid border-[--hl-sm]">
<label className="label--small no-pad-top">Url Preview</label>
<ErrorBoundary
key={uniqueKey}
errorClassName="tall wide vertically-align font-error pad text-center"
>
<RenderedQueryString request={activeRequest} />
</ErrorBoundary>
</div>
</div>
<div className="flex-shrink-0 grid flex-1 [grid-template-rows:minmax(auto,min-content)] [grid-template-columns:100%] overflow-hidden">
<div className="min-h-[2rem] max-h-full flex flex-col overflow-y-auto [&_.key-value-editor]:p-0 flex-1">
<div className='flex items-center w-full p-4 h-4 justify-between'>
<Heading className='text-xs font-bold uppercase text-[--hl]'>Query parameters</Heading>
<div className='flex items-center gap-2'>
<Button
isDisabled={disabled || !urlHasQueryParameters}
onPress={handleImportQueryFromUrl}
className="w-[14ch] flex flex-shrink-0 gap-2 items-center justify-start px-2 py-1 h-full asma-pressed:bg-[--hl-sm] aria-selected:bg-[--hl-xs] aria-selected:focus:bg-[--hl-sm] aria-selected:hover:bg-[--hl-sm] focus:bg-[--hl-sm] rounded-sm text-[--color-font] hover:bg-[--hl-xs] focus:ring-inset ring-1 ring-transparent focus:ring-[--hl-md] transition-colors text-sm"
>
Import from URL
</Button>
<ToggleButton
isDisabled={disabled}
onChange={isSelected => {
patchSettings({
useBulkParametersEditor: isSelected,
});
}}
isSelected={settings.useBulkParametersEditor}
className="w-[14ch] flex flex-shrink-0 gap-2 items-center justify-start px-2 py-1 h-full rounded-sm text-[--color-font] hover:bg-[--hl-xs] focus:ring-inset ring-1 ring-transparent focus:ring-[--hl-md] transition-colors text-sm"
>
{({ isSelected }) => (
<Fragment>
<Icon icon={isSelected ? 'toggle-on' : 'toggle-off'} className={`${isSelected ? 'text-[--color-success]' : ''}`} />
<span>{
isSelected ? 'Regular Edit' : 'Bulk Edit'
}</span>
</Fragment>
)}
</ToggleButton>
</div>
</div>
<ErrorBoundary
key={uniqueKey}
errorClassName="tall wide vertically-align font-error pad text-center"
>
<RequestParametersEditor
bulk={settings.useBulkParametersEditor}
disabled={disabled}
/>
</ErrorBoundary>
</div>
<div className='flex-1 flex flex-col gap-4 p-4 overflow-y-auto'>
<Heading className='text-xs font-bold uppercase text-[--hl]'>Path parameters</Heading>
{pathParameters.length > 0 && (
<div className="pr-[72.73px] w-full">
<div className='grid gap-x-[20.8px] grid-cols-2 flex-shrink-0 w-full rounded-sm overflow-hidden'>
{pathParameters.map(pathParameter => (
<Fragment key={pathParameter.name}>
<span className='p-2 select-none border-b border-solid border-[--hl-md] truncate flex items-center justify-end rounded-sm'>
{pathParameter.name}
</span>
<div className='px-2 flex items-center h-full border-b border-solid border-[--hl-md]'>
<OneLineEditor
readOnly={disabled}
key={activeRequest._id}
id={'key-value-editor__name' + pathParameter.name}
placeholder="Parameter value"
defaultValue={pathParameter.value || ''}
onChange={name => {
onPathParameterChange(pathParameters.map(p => p.name === pathParameter.name ? { ...p, value: name } : p));
}}
/>
</div>
</Fragment>
))}
</div>
</div>
)}
{pathParameters.length === 0 && !dismissPathParameterTip && (
<div className='text-sm text-[--hl] rounded-sm border border-solid border-[--hl-md] p-2 flex items-center gap-2'>
<Icon icon='info-circle' />
<span>Path parameters are url path segments that start with a colon ':' e.g. ':id' </span>
<Button
className="flex flex-shrink-0 items-center justify-center aspect-square h-6 aria-pressed:bg-[--hl-sm] rounded-sm text-[--color-font] hover:bg-[--hl-xs] ml-auto"
onPress={() => setDismissPathParameterTip('true')}
>
<Icon icon='close' />
</Button>
</div>
)}
</div>
}
>
<div className="grid h-full auto-rows-auto [grid-template-columns:100%] divide-y divide-solid divide-[--hl-md]">
{disabled && <PaneReadOnlyBanner />}
<div className='h-full flex flex-col'>
<div className="p-4">
<div className="text-xs max-h-32 flex flex-col overflow-y-auto min-h-[2em] bg-[--hl-xs] px-2 py-1 border border-solid border-[--hl-sm]">
<label className="label--small no-pad-top">Url Preview</label>
<ErrorBoundary
key={uniqueKey}
errorClassName="tall wide vertically-align font-error pad text-center"
>
<RenderedQueryString request={activeRequest} />
</ErrorBoundary>
</div>
</div>
<div className="grid flex-1 [grid-template-rows:minmax(auto,min-content)] [grid-template-columns:100%] overflow-hidden">
<div className="min-h-[2rem] max-h-full flex flex-col overflow-y-auto [&_.key-value-editor]:p-0 flex-1">
<div className='flex items-center w-full p-4 h-4 justify-between'>
<Heading className='text-xs font-bold uppercase text-[--hl]'>Query parameters</Heading>
</div>
<ErrorBoundary
key={uniqueKey}
errorClassName="tall wide vertically-align font-error pad text-center"
>
<RequestParametersEditor
bulk={useBulkParametersEditor}
disabled={disabled}
/>
</ErrorBoundary>
</div>
<div className='flex-1 flex flex-col gap-4 p-4 overflow-y-auto'>
<Heading className='text-xs font-bold uppercase text-[--hl]'>Path parameters</Heading>
{pathParameters.length > 0 && (
<div className="pr-[72.73px] w-full">
<div className='grid gap-x-[20.8px] grid-cols-2 flex-shrink-0 w-full rounded-sm overflow-hidden'>
{pathParameters.map(pathParameter => (
<Fragment key={pathParameter.name}>
<span className='p-2 select-none border-b border-solid border-[--hl-md] truncate flex items-center justify-end rounded-sm'>
{pathParameter.name}
</span>
<div className='px-2 flex items-center h-full border-b border-solid border-[--hl-md]'>
<OneLineEditor
readOnly={disabled}
id={'key-value-editor__name' + pathParameter.name}
key={activeRequest._id}
placeholder={'Parameter value'}
defaultValue={pathParameter.value || ''}
onChange={name => {
onPathParameterChange(pathParameters.map(p => p.name === pathParameter.name ? { ...p, value: name } : p));
}}
/>
</div>
</Fragment>
))}
</div>
</div>
)}
{pathParameters.length === 0 && !dismissPathParameterTip && (
<div className='text-sm text-[--hl] rounded-sm border border-solid border-[--hl-md] p-2 flex items-center gap-2'>
<Icon icon='info-circle' />
<span>Path parameters are url path segments that start with a colon ':' e.g. ':id' </span>
<Button
className="flex flex-shrink-0 items-center justify-center aspect-square h-6 aria-pressed:bg-[--hl-sm] rounded-sm text-[--color-font] hover:bg-[--hl-xs] ml-auto"
onPress={() => setDismissPathParameterTip('true')}
>
<Icon icon='close' />
</Button>
</div>
)}
</div>
</div>
</div>
</div>
</TabItem>
<TabItem key="websocket-preview-mode" title={<WebSocketPreviewMode previewMode={previewMode} onClick={changeMode} />}>
<div
style={{
display: 'flex',
flexDirection: 'column',
height: '100%',
}}
>
<PaneSendButton>
<SendButton
type="submit"
form="websocketMessageForm"
isConnected={readyState}
>
Send
</SendButton>
</PaneSendButton>
<WebSocketRequestForm
key={uniqueKey}
request={activeRequest}
previewMode={previewMode}
environmentId={environment?._id || ''}
workspaceId={workspaceId}
/>
</div>
</TabItem>
<TabItem key="auth" title={<AuthDropdown authentication={activeRequest.authentication} authTypes={supportedAuthTypes} disabled={disabled} />}>
</TabPanel>
<TabPanel className='w-full flex-1 flex flex-col' id='content-type'>
<Toolbar className="w-full flex-shrink-0 px-2 border-b border-solid border-[--hl-md] py-2 h-[--line-height-sm] flex items-center gap-2 justify-between">
<WebSocketPreviewMode previewMode={previewMode} onSelect={changeMode} />
<SendButton
type="submit"
form="websocketMessageForm"
isConnected={readyState}
>
Send
</SendButton>
</Toolbar>
<WebSocketRequestForm
key={uniqueKey}
request={activeRequest}
previewMode={previewMode}
environmentId={environment?._id || ''}
workspaceId={workspaceId}
/>
</TabPanel>
<TabPanel className='w-full flex-1 flex flex-col overflow-hidden' id='auth'>
{disabled && <PaneReadOnlyBanner />}
<AuthWrapper
key={uniqueKey}
authentication={activeRequest.authentication}
disabled={disabled}
authTypes={supportedAuthTypes}
/>
</TabItem>
<TabItem
key="headers"
title={
<div className='flex items-center gap-2'>
Headers{' '}
{headersCount > 0 && (
<span className="p-2 aspect-square flex items-center color-inherit justify-between border-solid border border-[--hl-md] overflow-hidden rounded-lg text-xs shadow-small">{headersCount}</span>
)}
</div>
}
>
</TabPanel>
<TabPanel className='w-full flex-1 overflow-y-auto ' id='headers'>
{disabled && <PaneReadOnlyBanner />}
<RequestHeadersEditor
key={uniqueKey}
@@ -420,20 +471,8 @@ export const WebSocketRequestPane: FC<Props> = ({ environment }) => {
isDisabled={readyState}
requestType="WebSocketRequest"
/>
</TabItem>
<TabItem
key="docs"
title={
<>
Docs
{activeRequest.description && (
<span className="bubble space-left">
<i className="fa fa--skinny fa-check txt-xxs" />
</span>
)}
</>
}
>
</TabPanel>
<TabPanel className='w-full flex-1 overflow-y-auto ' id='docs'>
{activeRequest.description ? (
<div>
<div className="pull-right pad bg-default">
@@ -470,7 +509,7 @@ export const WebSocketRequestPane: FC<Props> = ({ environment }) => {
</p>
</div>
)}
</TabItem>
</TabPanel>
</Tabs>
{isRequestSettingsModalOpen && (
<RequestSettingsModal

View File

@@ -12,6 +12,10 @@
@tailwind components;
@tailwind utilities;
* {
scrollbar-width: thin;
}
html {
font-size: 11px;
}