diff --git a/packages/insomnia-smoke-test/tests/smoke/environment-editor-interactions.test.ts b/packages/insomnia-smoke-test/tests/smoke/environment-editor-interactions.test.ts index 864f12517b7..067bf016e54 100644 --- a/packages/insomnia-smoke-test/tests/smoke/environment-editor-interactions.test.ts +++ b/packages/insomnia-smoke-test/tests/smoke/environment-editor-interactions.test.ts @@ -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'); diff --git a/packages/insomnia-smoke-test/tests/smoke/graphql.test.ts b/packages/insomnia-smoke-test/tests/smoke/graphql.test.ts index b747a66fa75..33b4940b339 100644 --- a/packages/insomnia-smoke-test/tests/smoke/graphql.test.ts +++ b/packages/insomnia-smoke-test/tests/smoke/graphql.test.ts @@ -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(); diff --git a/packages/insomnia-smoke-test/tests/smoke/oauth.test.ts b/packages/insomnia-smoke-test/tests/smoke/oauth.test.ts index 7e763643001..2193b2eea32 100644 --- a/packages/insomnia-smoke-test/tests/smoke/oauth.test.ts +++ b/packages/insomnia-smoke-test/tests/smoke/oauth.test.ts @@ -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(); diff --git a/packages/insomnia-smoke-test/tests/smoke/pre-request-script-features.test.ts b/packages/insomnia-smoke-test/tests/smoke/pre-request-script-features.test.ts index 9f8bb4ad0ef..49b0e108e22 100644 --- a/packages/insomnia-smoke-test/tests/smoke/pre-request-script-features.test.ts +++ b/packages/insomnia-smoke-test/tests/smoke/pre-request-script-features.test.ts @@ -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); diff --git a/packages/insomnia-smoke-test/tests/smoke/request-pane-tab.test.ts b/packages/insomnia-smoke-test/tests/smoke/request-pane-tab.test.ts index e9b4aae2af1..6503cf7ccdf 100644 --- a/packages/insomnia-smoke-test/tests/smoke/request-pane-tab.test.ts +++ b/packages/insomnia-smoke-test/tests/smoke/request-pane-tab.test.ts @@ -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(); diff --git a/packages/insomnia/src/index.html b/packages/insomnia/src/index.html index 72fab88293a..82a08ffcbd3 100644 --- a/packages/insomnia/src/index.html +++ b/packages/insomnia/src/index.html @@ -1,5 +1,5 @@ - + = ({ 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 = ({ 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 ( - - {getAuthTypeName(getAuthObjectOrNull(authentication)?.type)} - - - } + aria-label="Change Authentication type" + name="auth-type" + onSelectionChange={authType => { + onClick(authType as AuthTypes); + }} + selectedKey={selectedAuthType} > - - {authTypes.map(authType => - - onClick(authType)} - /> - - )} - - - - onClick('none')} - /> - - - onClick()} - /> - - - + + + + {item => ( +
+
+ {item.name} +
+ + {item => ( + + {({ isSelected }) => ( + <> + {item.name} + {isSelected && ( + + )} + + )} + + )} + +
+ )} +
+
+ ); }; diff --git a/packages/insomnia/src/ui/components/dropdowns/content-type-dropdown.tsx b/packages/insomnia/src/ui/components/dropdowns/content-type-dropdown.tsx index de67873f9b6..bd530bfcafc 100644 --- a/packages/insomnia/src/ui/components/dropdowns/content-type-dropdown.tsx +++ b/packages/insomnia/src/ui/components/dropdowns/content-type-dropdown.tsx @@ -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 ( - -
- {hasMimeType ? getContentTypeName(body.mimeType) : 'Body'} + ); }; + export function newBodyGraphQL(rawBody: string): RequestBody { try { // Only strip the newlines if rawBody is a parsable JSON diff --git a/packages/insomnia/src/ui/components/dropdowns/websocket-preview-mode.tsx b/packages/insomnia/src/ui/components/dropdowns/websocket-preview-mode.tsx index 08e0f1829b7..0acafa17d91 100644 --- a/packages/insomnia/src/ui/components/dropdowns/websocket-preview-mode.tsx +++ b/packages/insomnia/src/ui/components/dropdowns/websocket-preview-mode.tsx @@ -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 = ({ previewMode, onClick }) => { + +const contentTypes: { + id: string; + name: string; +}[] = [ + { + id: CONTENT_TYPE_JSON, + name: 'JSON', + }, + { + id: CONTENT_TYPE_PLAINTEXT, + name: 'Raw', + }, + ]; + +export const WebSocketPreviewMode: FC = ({ previewMode, onSelect }) => { return ( - - {{ - [CONTENT_TYPE_JSON]: 'JSON', - [CONTENT_TYPE_PLAINTEXT]: 'Raw', - }[previewMode]} - - - } + ); }; diff --git a/packages/insomnia/src/ui/components/editors/auth/auth-wrapper.tsx b/packages/insomnia/src/ui/components/editors/auth/auth-wrapper.tsx index c65762ec514..c0e49b6935c 100644 --- a/packages/insomnia/src/ui/components/editors/auth/auth-wrapper.tsx +++ b/packages/insomnia/src/ui/components/editors/auth/auth-wrapper.tsx @@ -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 = ; } else { authBody = ( -
-

+

+

{authBody}

; + return <> + + + +
+ {authBody} +
+ ; }; diff --git a/packages/insomnia/src/ui/components/editors/body/body-editor.tsx b/packages/insomnia/src/ui/components/editors/body/body-editor.tsx index 2ff49c63012..ad8d0bdb3b1 100644 --- a/packages/insomnia/src/ui/components/editors/body/body-editor.tsx +++ b/packages/insomnia/src/ui/components/editors/body/body-editor.tsx @@ -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 = ({ const mimeType = request.body.mimeType; const isBodyEmpty = typeof mimeType !== 'string' && !request.body.text; - const _render = () => { + function renderBodyEditor() { if (mimeType === CONTENT_TYPE_FORM_URLENCODED) { return ; } else if (mimeType === CONTENT_TYPE_FORM_DATA) { @@ -121,14 +123,31 @@ export const BodyEditor: FC = ({ const contentType = getContentTypeFromHeaders(request.headers) || mimeType; return ; } else if (isEventStreamRequest(request)) { - return } - documentationLinks={[]} - title="Enter a URL and connect to start receiving event stream data" - />; + return ( + } + documentationLinks={[]} + title="Enter a URL and connect to start receiving event stream data" + /> + ); + } else { + return ( + } + 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 } 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 {_render()}; + return ( + + + + + {renderBodyEditor()} + + ); }; diff --git a/packages/insomnia/src/ui/components/editors/body/graph-ql-editor.tsx b/packages/insomnia/src/ui/components/editors/body/graph-ql-editor.tsx index 0565869077e..c0e2f81626e 100644 --- a/packages/insomnia/src/ui/components/editors/body/graph-ql-editor.tsx +++ b/packages/insomnia/src/ui/components/editors/body/graph-ql-editor.tsx @@ -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 = ({ operationName, }, operations, - hideSchemaFetchErrors: false, variablesSyntaxError: '', activeReference: null, explorerVisible: false, @@ -343,16 +343,16 @@ export const GraphQLEditor: FC = ({ return ''; } if (schemaIsFetching) { - return 'fetching schema...'; + return 'Fetching schema...'; } if (schemaLastFetchTime > 0) { return ( - schema fetched + Schema fetched ); } - return schema not yet fetched; + return Schema not fetched yet; }; const loadAndSetLocalSchema = async () => { @@ -393,7 +393,6 @@ export const GraphQLEditor: FC = ({ }; const { - hideSchemaFetchErrors, variablesSyntaxError, activeReference, explorerVisible, @@ -468,13 +467,13 @@ export const GraphQLEditor: FC = ({ } const canShowSchema = schema && !schemaIsFetching && !schemaFetchError && schemaLastFetchTime > 0; return ( -
- + <> + + {state.body.operationName || 'Operations'} } @@ -495,7 +494,7 @@ export const GraphQLEditor: FC = ({ aria-label='Schema Dropdown' triggerButton={ @@ -523,9 +522,6 @@ export const GraphQLEditor: FC = ({ 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 = ({ } onClick={() => { - setState(state => ({ ...state, hideSchemaFetchErrors: false })); loadAndSetLocalSchema(); }} /> @@ -581,80 +576,85 @@ export const GraphQLEditor: FC = ({ - -
- -
-
- {!hideSchemaFetchErrors && schemaFetchError && ( -
-
- -
- {schemaFetchError.message} -
+ + + + + + + + Query Variables + + Variables to use in GraphQL query
+ (JSON format) +
+ {variablesSyntaxError && ( + {variablesSyntaxError} + )} +
+
+ Object.keys(variableTypes)} + lintOptions={{ + variableToType: variableTypes, + }} + noLint={!variableTypes} + onChange={changeVariables} + mode="graphql-variables" + placeholder="" + />
- )} -
-
- {renderSchemaFetchMessage()} -
-

- Query Variables - - Variables to use in GraphQL query
- (JSON format) -
- {variablesSyntaxError && ( - {variablesSyntaxError} - )} -

-
- Object.keys(variableTypes)} - lintOptions={{ - variableToType: variableTypes, - }} - noLint={!variableTypes} - onChange={changeVariables} - mode="graphql-variables" - placeholder="" - /> -
-
- -
- + + {!schemaFetchError &&
+ + {renderSchemaFetchMessage()} +
} + {schemaFetchError && ( + + + + + {schemaFetchError.message} + + + + )} + {graphQLExplorerPortal} -
+ ); }; diff --git a/packages/insomnia/src/ui/components/editors/request-script-editor.tsx b/packages/insomnia/src/ui/components/editors/request-script-editor.tsx index 798f3a02be9..260e17a357a 100644 --- a/packages/insomnia/src/ui/components/editors/request-script-editor.tsx +++ b/packages/insomnia/src/ui/components/editors/request-script-editor.tsx @@ -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 = ({ className, defaultValue, @@ -194,6 +396,7 @@ export const RequestScriptEditor: FC = ({ 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 = ({ ); return ( -
-
- requestScriptSnippets} - onPaste={translateHandlersInScript} - /> -
-
- - - - } - > - - - - addSnippet(getEnvVar)} - /> - - {/* - addSnippet(getGlbVar)} - /> - */} - - addSnippet(getVar)} - /> - - - addSnippet(getCollectionVar)} - /> - - - - - - addSnippet(setEnvVar)} - /> - - {/* - addSnippet(setGlbVar)} - /> - */} - - addSnippet(setVar)} - /> - - - addSnippet(setCollectionVar)} - /> - - - - - - addSnippet(unsetEnvVar)} - /> - - {/* - addSnippet(unsetGlbVar)} - /> - */} - - addSnippet(unsetCollectionVar)} - /> - - - - - - - - } - > - - addSnippet(addQueryParams)} - /> - - - addSnippet(setMethod)} - /> - - - addSnippet(addHeader)} - /> - - - addSnippet(removeHeader)} - /> - - - addSnippet(updateRequestBody)} - /> - - - addSnippet(updateRequestAuth)} - /> - - +
+ requestScriptSnippets} + onPaste={translateHandlersInScript} + /> + + {snippetsMenus.map(menu => ( + + + + + {item => { + if ('items' in item) { + return ( +
+
+ {item.name} +
+ + {item => ( + 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} + )} + +
+ ); + } - - - - } - > - - addSnippet(getStatusCode)} - /> - - - addSnippet(getStatusMsg)} - /> - - - addSnippet(getRespTime)} - /> - - - addSnippet(getJsonBody)} - /> - - - addSnippet(getTextBody)} - /> - - - addSnippet(findHeader)} - /> - - - addSnippet(getCookies)} - /> - - + return ( + 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} + ); + }} +
+
+
+ ))} - - - - } - > - - addSnippet(sendReq)} - /> - - - addSnippet(logValue)} - /> - - - addSnippet(requireAModule)} - /> - - -
+
); }; diff --git a/packages/insomnia/src/ui/components/panes/request-group-pane.tsx b/packages/insomnia/src/ui/components/panes/request-group-pane.tsx index 87fecc0d2c1..1f2ad1b1ff4 100644 --- a/packages/insomnia/src/ui/components/panes/request-group-pane.tsx +++ b/packages/insomnia/src/ui/components/panes/request-group-pane.tsx @@ -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 ( <> - - }> + + + + Auth + + + Headers + {headersCount > 0 && ( + + {headersCount} + + )} + + + Docs + + + -
- - - Headers{' '} - {headersCount > 0 && ( - {headersCount} - )} -
- } - > -
- -
- -
-
-
-
- - Docs - {activeRequestGroup.description && ( - - - - )} - - } - > - - {activeRequestGroup.description ? ( -
-
- -
-
- - - -
+ + + + + + + + {activeRequestGroup.description ? ( +
+
+
- ) : ( -
-

- - - -
-
- -

+
+ + +
- )} - - +
+ ) : ( +
+

+ + + +
+
+ +

+
+ )} + - {isRequestGroupSettingsModalOpen && ( - setIsRequestGroupSettingsModalOpen(false)} - /> - )} + { + isRequestGroupSettingsModalOpen && ( + setIsRequestGroupSettingsModalOpen(false)} + /> + ) + } + ); }; diff --git a/packages/insomnia/src/ui/components/panes/request-pane.tsx b/packages/insomnia/src/ui/components/panes/request-pane.tsx index 30a94684659..789bd36b947 100644 --- a/packages/insomnia/src/ui/components/panes/request-pane.tsx +++ b/packages/insomnia/src/ui/components/panes/request-pane.tsx @@ -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 = ({ /> - - - Parameters - {parametersCount > 0 && ( - {parametersCount} - )} -
- } - > -
-
-
- - - - -
+ + + + Params + {parametersCount > 0 && ( + + {parametersCount} + + )} + + + Body + + + Auth + + + Headers + {headersCount > 0 && ( + + {headersCount} + + )} + + + Scripts + {Boolean(activeRequest.preRequestScript || activeRequest.afterResponseScript) && ( + + + + )} + + + Docs + {activeRequest.description && ( + + + + )} + + + +
+
+ + + +
-
-
-
+
+
+
+
Query parameters
@@ -155,7 +199,7 @@ export const RequestPane: FC = ({ }); }} 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 }) => ( @@ -178,170 +222,151 @@ export const RequestPane: FC = ({ />
-
+
Path parameters - {pathParameters.length > 0 && ( -
-
- {pathParameters.map(pathParameter => ( - - - {pathParameter.name} - -
- { - onPathParameterChange(pathParameters.map(p => p.name === pathParameter.name ? { ...p, value: name } : p)); - }} - /> -
-
- ))} -
-
- )} - {pathParameters.length === 0 && !dismissPathParameterTip && ( -
- - Path parameters are url path segments that start with a colon ':' e.g. ':id' - + {pathParameters.length > 0 && ( +
+
+ {pathParameters.map(pathParameter => ( + + + {pathParameter.name} + +
+ { + onPathParameterChange(pathParameters.map(p => p.name === pathParameter.name ? { ...p, value: name } : p)); + }} + /> +
+
+ ))}
- )} -
+
+ )} + {pathParameters.length === 0 && !dismissPathParameterTip && ( +
+ + Path parameters are url path segments that start with a colon ':' e.g. ':id' + +
+ )}
- - }> + + - - }> + + - - - Headers{' '} - {headersCount > 0 && ( - {headersCount} - )} -
- } - > -
- -
- -
-
- -
- -
-
- - - Pre-request Script{' '} - {activeRequest.preRequestScript && ( - - - - )} -
- } - aria-label={'experimental'} - > + + - patchRequest(requestId, { preRequestScript })} - settings={settings} - /> - - - - After-response Script{' '} - {activeRequest.afterResponseScript && ( - - - - )} +
+
- } - aria-label={'experimental'} - > - - patchRequest(requestId, { afterResponseScript })} - settings={settings} - /> -
- - Docs - {activeRequest.description && ( - - - - )} - - } - > - + +
+ +
+
+ + + + +
+ + Pre-request +
+ {Boolean(activeRequest.preRequestScript) && ( + + + + )} +
+ +
+ + After-response +
+ {Boolean(activeRequest.afterResponseScript) && ( + + + + )} +
+
+ + + patchRequest(requestId, { preRequestScript })} + settings={settings} + /> + + + + + patchRequest(requestId, { afterResponseScript })} + settings={settings} + /> + + +
+
+ +
{activeRequest.description ? (
@@ -384,8 +409,8 @@ export const RequestPane: FC = ({

)} - - +
+ {isRequestSettingsModalOpen && ( (({ 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 = ({ 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 = ({ 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 = ({ 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 = ({ environment }) => { onChange={url => patchRequest(requestId, { url })} /> - - - Parameters - {parametersCount > 0 && ( - {parametersCount} - )} + + + + Params + {parametersCount > 0 && ( + + {parametersCount} + + )} + + + Body + + + Auth + + + Headers + {headersCount > 0 && ( + + {headersCount} + + )} + + + Docs + + + + {disabled && } +
+
+ + + +
- } - > -
- {disabled && } -
-
-
- - +
+
+
+ Query parameters +
+ + { + 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 }) => ( + + + { + isSelected ? 'Regular Edit' : 'Bulk Edit' + } + + )} +
-
-
-
- Query parameters + + + +
+
+ Path parameters + {pathParameters.length > 0 && ( +
+
+ {pathParameters.map(pathParameter => ( + + + {pathParameter.name} + +
+ { + onPathParameterChange(pathParameters.map(p => p.name === pathParameter.name ? { ...p, value: name } : p)); + }} + /> +
+
+ ))}
- - -
-
- Path parameters - {pathParameters.length > 0 && ( -
-
- {pathParameters.map(pathParameter => ( - - - {pathParameter.name} - -
- { - onPathParameterChange(pathParameters.map(p => p.name === pathParameter.name ? { ...p, value: name } : p)); - }} - /> -
-
- ))} -
-
- )} - {pathParameters.length === 0 && !dismissPathParameterTip && ( -
- - Path parameters are url path segments that start with a colon ':' e.g. ':id' - -
- )} + )} + {pathParameters.length === 0 && !dismissPathParameterTip && ( +
+ + Path parameters are url path segments that start with a colon ':' e.g. ':id' +
-
+ )}
- - }> -
- - - Send - - - -
-
- }> + + + + + + Send + + + + + {disabled && } - - - Headers{' '} - {headersCount > 0 && ( - {headersCount} - )} -
- } - > + + {disabled && } = ({ environment }) => { isDisabled={readyState} requestType="WebSocketRequest" /> - - - Docs - {activeRequest.description && ( - - - - )} - - } - > + + {activeRequest.description ? (
@@ -470,7 +509,7 @@ export const WebSocketRequestPane: FC = ({ environment }) => {

)} - + {isRequestSettingsModalOpen && (