diff --git a/packages/insomnia-smoke-test/tests/smoke/focus-and-keyboard.test.ts b/packages/insomnia-smoke-test/tests/smoke/focus-and-keyboard.test.ts new file mode 100644 index 0000000000..3a3ed33403 --- /dev/null +++ b/packages/insomnia-smoke-test/tests/smoke/focus-and-keyboard.test.ts @@ -0,0 +1,188 @@ +import { expect } from '@playwright/test'; + +import { loadFixture } from '../../playwright/paths'; +import { test } from '../../playwright/test'; + +// Regression coverage for the focus / keyboard-navigation rules: +// - creating a request focuses the URL bar +// - adding a param/header focuses the new row's Name cell +// - opening the KV environment editor focuses the trailing blank row's Name +// - create/rename/settings dialogs focus the Name field +// - the navigation sidebar expands/collapses folders with Left/Right arrows +// - Cmd/Ctrl-N adds the new request inside the selected folder +// +// CodeMirror-backed editors (OneLineEditor) expose focus via `data-focused="on"` on their +// `.editor__container`, which is what these tests assert against. + +const focusedEditorWithChild = (childIdPrefix: string) => + `.editor__container:has([id^="${childIdPrefix}"]) .CodeMirror-focused`; + +test.describe('Focus and keyboard navigation', () => { + test.slow(process.platform === 'darwin' || process.platform === 'win32', 'Slow app start on these platforms'); + + test('creating a request focuses the URL bar', async ({ page, insomnia }) => { + await page.getByRole('button', { name: 'Create request collection', exact: true }).click(); + + // Create a fresh HTTP request (this goes through the create -> redirect path that focuses the URL). + await insomnia.navigationSidebar.openWorkspaceActionsDropdown('My first collection'); + await page.getByRole('menuitemradio', { name: 'Http Request' }).click(); + + // The URL bar should be focused so the user can start typing immediately. + await expect.soft(page.locator(focusedEditorWithChild('request-url-bar'))).toHaveCount(1); + }); + + test('adding a query parameter focuses the Name cell', async ({ page }) => { + await page.getByRole('button', { name: 'Create request collection', exact: true }).click(); + + await page.getByRole('tab', { name: 'Params' }).click(); + await page.getByTestId('request-pane').getByRole('button', { name: 'Add', exact: true }).click(); + + await expect.soft(page.locator(focusedEditorWithChild('key-value-editor__name'))).toHaveCount(1); + }); + + test('adding a header focuses the Name cell', async ({ page }) => { + await page.getByRole('button', { name: 'Create request collection', exact: true }).click(); + + await page.getByRole('tab', { name: 'Headers' }).click(); + await page.getByTestId('request-pane').getByRole('button', { name: 'Add', exact: true }).click(); + + await expect.soft(page.locator(focusedEditorWithChild('key-value-editor__name'))).toHaveCount(1); + }); + + test('the KV environment editor focuses the blank row Name', async ({ page }) => { + await page.getByRole('button', { name: 'Create request collection', exact: true }).click(); + + await page.getByRole('button', { name: 'Manage Environments' }).click(); + await page.getByRole('button', { name: 'Manage collection environments' }).click(); + await page.getByTestId('CreateEnvironmentDropdown').click(); + await page.getByRole('menuitemradio', { name: 'Shared Environment' }).press('Enter'); + + // New environments default to KV mode, so selecting the new (empty) environment renders the KV + // editor whose trailing blank row's Name should be focused. + const newEnvironmentRow = page.getByRole('row', { name: 'New Environment' }); + await newEnvironmentRow.waitFor({ state: 'visible' }); + // Click the painted name cell rather than the row's center: the row's flexible middle leaves a + // gap that the modal's pane container reports as intercepting the click, and the blank-row + // autofocus churns focus/scroll right after the editor mounts. + await newEnvironmentRow.locator('[data-editable=true]').click(); + + await expect.soft(page.locator(focusedEditorWithChild('environment-kv-editor-name'))).toHaveCount(1); + }); + + test('workspace settings dialog focuses the Name field', async ({ page, insomnia }) => { + await page.getByRole('button', { name: 'Create request collection', exact: true }).click(); + + await insomnia.navigationSidebar.openWorkspaceActionsDropdown('My first collection'); + await page.getByRole('menuitemradio', { name: 'Settings' }).click(); + + await expect.soft(page.getByRole('dialog').getByRole('textbox', { name: 'Name' })).toBeFocused(); + }); + + test('tabbing onto the params grid focuses the Name cell', async ({ page }) => { + await page.getByRole('button', { name: 'Create request collection', exact: true }).click(); + + await page.getByRole('tab', { name: 'Params' }).click(); + // Move focus onto the params grid the way Tab would: React Aria lands focus on a row, which the + // editor forwards into the trailing blank row's Name editor so the user can start typing a new pair. + await page.getByRole('listbox', { name: 'Key-value pairs' }).getByRole('option').first().focus(); + + await expect.soft(page.locator(focusedEditorWithChild('key-value-editor__name'))).toHaveCount(1); + }); + + test('Tab from the URL bar moves focus to Send', async ({ page, insomnia }) => { + await page.getByRole('button', { name: 'Create request collection', exact: true }).click(); + + await insomnia.navigationSidebar.openWorkspaceActionsDropdown('My first collection'); + await page.getByRole('menuitemradio', { name: 'Http Request' }).click(); + + // Fresh request focuses the URL bar; Tab should advance to the Send button. (The URL autofocus + // re-grab loop must not yank focus back when Tab lands on the Send button.) + await expect.soft(page.locator(focusedEditorWithChild('request-url-bar'))).toHaveCount(1); + await page.keyboard.press('Tab'); + await expect.soft(page.getByRole('button', { name: 'Send', exact: true })).toBeFocused(); + }); + + test('Tab reaches the request tabs and they show a keyboard focus ring', async ({ page, insomnia }) => { + await page.getByRole('button', { name: 'Create request collection', exact: true }).click(); + + await insomnia.navigationSidebar.openWorkspaceActionsDropdown('My first collection'); + await page.getByRole('menuitemradio', { name: 'Http Request' }).click(); + await expect.soft(page.locator(focusedEditorWithChild('request-url-bar'))).toHaveCount(1); + + // Tab order out of the URL bar: Send -> send dropdown -> request tablist (Params). + await page.keyboard.press('Tab'); + await page.keyboard.press('Tab'); + await page.keyboard.press('Tab'); + + const paramsTab = page.getByRole('tab', { name: 'Params' }); + await expect.soft(paramsTab).toBeFocused(); + // React Aria marks keyboard focus with data-focus-visible, which drives the visible focus ring. + await expect.soft(paramsTab).toHaveAttribute('data-focus-visible', 'true'); + + // Arrow keys move between the request tabs. + await page.keyboard.press('ArrowRight'); + await expect.soft(page.getByRole('tab', { name: 'Body' })).toBeFocused(); + }); + + test.describe('with an imported collection', () => { + test.beforeEach(async ({ app, page }) => { + const text = await loadFixture('simple.yaml'); + await app.evaluate(async ({ clipboard }, text) => clipboard.writeText(text), text); + await page.getByLabel('Import').click(); + await page.locator('[data-test-id="import-from-clipboard"]').click(); + await page.getByRole('button', { name: 'Scan' }).click(); + await page.getByRole('dialog').getByRole('button', { name: 'Import' }).click(); + await page.getByRole('dialog').waitFor({ state: 'hidden' }); + // Import lands in the imported collection's debug view; wait for its folder to appear in the sidebar. + await page.getByTestId('request-node-test folder').waitFor({ state: 'visible' }); + }); + + test('Left/Right arrows collapse and expand a folder', async ({ page }) => { + const folderRow = page.locator('[role="row"]:has([data-testid="request-node-test folder"])'); + + // ArrowRight always expands (no-op if already expanded), ArrowLeft always collapses. + await folderRow.press('ArrowRight'); + await expect.soft(page.getByLabel('Collapse test folder')).toBeVisible(); + + await folderRow.press('ArrowLeft'); + await expect.soft(page.getByLabel('Expand test folder')).toBeVisible(); + + await folderRow.press('ArrowRight'); + await expect.soft(page.getByLabel('Collapse test folder')).toBeVisible(); + }); + + test('Cmd/Ctrl-N adds the request inside the selected folder', async ({ page }) => { + const folderRow = page.locator('[role="row"]:has([data-testid="request-node-test folder"])'); + + // Collapse the folder (ArrowLeft is a no-op if already collapsed) so its later auto-expansion + // is a clean signal that the new request was nested inside it. + await folderRow.press('ArrowLeft'); + await expect.soft(page.getByLabel('Expand test folder')).toBeVisible(); + + // Select the folder (no request active), then create a request via the keyboard shortcut. + await page.getByTestId('request-node-test folder').click(); + await page.locator('.app').press('ControlOrMeta+n'); + + // Navigating to the new request auto-expands its ancestor folder, proving it was created inside. + await expect.soft(page.getByLabel('Collapse test folder')).toBeVisible(); + }); + + test('request settings dialog focuses the Name field', async ({ page, insomnia }) => { + await insomnia.navigationSidebar.selectRequestDropdownOption({ + actionName: 'Settings', + requestName: 'example http', + }); + + await expect.soft(page.getByRole('dialog').getByRole('textbox', { name: 'Name' })).toBeFocused(); + }); + + test('folder settings dialog focuses the Name field', async ({ page, insomnia }) => { + await insomnia.navigationSidebar.selectRequestGroupDropdownOption({ + actionName: 'Settings', + requestGroupName: 'test folder', + }); + + await expect.soft(page.getByRole('dialog').getByRole('textbox', { name: 'Name' })).toBeFocused(); + }); + }); +}); diff --git a/packages/insomnia-smoke-test/tests/smoke/insomnia-vault.test.ts b/packages/insomnia-smoke-test/tests/smoke/insomnia-vault.test.ts index 5c090095d3..5eb96073e5 100644 --- a/packages/insomnia-smoke-test/tests/smoke/insomnia-vault.test.ts +++ b/packages/insomnia-smoke-test/tests/smoke/insomnia-vault.test.ts @@ -105,33 +105,42 @@ test.describe('Check vault used in environment', () => { // add first secret environment const firstRow = kvTable.getByRole('option').first(); - await firstRow.getByTestId('OneLineEditor').first().click(); + const firstKey = firstRow.getByTestId('OneLineEditor').first(); + await firstKey.click(); await page.keyboard.type('foo'); - await firstRow.getByTestId('OneLineEditor').nth(1).click({ delay: 200 }); - await page.keyboard.type('bar'); - // Delay the click to let debounce finish + await expect.soft(firstKey).toContainText('foo'); + // Convert the row to Secret *before* entering the value, then type the secret directly into the + // revealed editor. Converting a just-typed string to Secret races the async persistence + // round-trip (the editor's change goes through a fetcher submit + revalidation): the typed value + // may not be committed yet when the conversion reads it, so an empty string gets encrypted and the + // revealed secret comes back blank. Typing into the already-Secret field encrypts each keystroke, + // so there is no stale value to read. await firstRow.getByRole('button', { name: 'Type Selection' }).click({ delay: 200 }); await page.getByRole('menuitemradio', { name: 'Secret' }).click(); await expect.soft(firstRow.locator('.fa-eye-slash')).toBeVisible(); + // reveal the secret editor and type the value into it await firstRow.locator('.fa-eye-slash').click(); - // test decrypt secret in UI - await expect.soft(firstRow.getByTestId('OneLineEditor').nth(1)).toContainText('bar'); + const firstValue = firstRow.getByTestId('OneLineEditor').nth(1); + await firstValue.click({ delay: 200 }); + await page.keyboard.type('bar'); + // test the secret value is shown decrypted in the UI + await expect.soft(firstValue).toContainText('bar'); - // add second secret environment + // add second secret environment (same order as above: convert to Secret first, then type the value) await page.getByRole('button', { name: 'Add Row' }).click(); const secondRow = kvTable.getByRole('option').nth(1); - await secondRow.getByTestId('OneLineEditor').first().click(); + const secondKey = secondRow.getByTestId('OneLineEditor').first(); + await secondKey.click(); await page.keyboard.type('hello'); - await secondRow.getByTestId('OneLineEditor').nth(1).click({ delay: 200 }); - await page.keyboard.type('world'); - // Delay the click to let debounce finish + await expect.soft(secondKey).toContainText('hello'); await secondRow.getByRole('button', { name: 'Type Selection' }).click({ delay: 200 }); await page.getByRole('menuitemradio', { name: 'Secret' }).click(); - // ensure the secret value has been persisted before navigating away, otherwise - // the request below pops a "1 environment variable is missing" modal for vault.hello await expect.soft(secondRow.locator('.fa-eye-slash')).toBeVisible(); await secondRow.locator('.fa-eye-slash').click(); - await expect.soft(secondRow.getByTestId('OneLineEditor').nth(1)).toContainText('world'); + const secondValue = secondRow.getByTestId('OneLineEditor').nth(1); + await secondValue.click({ delay: 200 }); + await page.keyboard.type('world'); + await expect.soft(secondValue).toContainText('world'); // go back await page.getByTestId('workspace-breadcrumb-level-0').click(); diff --git a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.new.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.new.tsx index 7e52e0e4f3..8ee1f48e56 100644 --- a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.new.tsx +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.new.tsx @@ -12,10 +12,14 @@ import { import { invariant } from '~/common/utils/invariant'; import type { RequestCreatedMetricsProperties } from '~/ui/analytics'; import { AnalyticsEvent } from '~/ui/analytics'; +import { focusUrlBarOnNextRequest } from '~/ui/components/request-url-bar-focus'; import { trackCioEvent } from '~/ui/hooks/use-cio'; import type { CreateRequestType } from '~/ui/hooks/use-request'; import { createFetcherSubmitHook } from '~/ui/utils/router'; +// Request types that are edited in the RequestPane / RequestUrlBar and should focus the URL on create. +const URL_BAR_REQUEST_TYPES: CreateRequestType[] = ['HTTP', 'GraphQL', 'Event Stream', 'From Curl']; + import type { Route } from './+types/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.new'; export async function clientAction({ params, request }: Route.ClientActionArgs) { @@ -190,6 +194,11 @@ export const useRequestNewActionFetcher = createFetcherSubmitHook( workspaceId, }); + // Focus the URL bar once the newly created request opens so the user can start typing. + if (URL_BAR_REQUEST_TYPES.includes(requestType)) { + focusUrlBarOnNextRequest(); + } + return submit(JSON.stringify({ requestType, parentId, req, metrics }), { action: url, method: 'POST', diff --git a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.tsx index ee228226a1..c749930312 100644 --- a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.tsx +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.tsx @@ -414,7 +414,13 @@ const Debug = () => { } }, request_createHTTP: async () => { - const parentId = activeRequest ? activeRequest.parentId : activeWorkspace._id; + // When a request is active, create a sibling; when a folder is selected (and no request is + // active), create inside that folder; otherwise create at the workspace root. + const parentId = activeRequest + ? activeRequest.parentId + : requestGroupId && isRequestGroupId(requestGroupId) + ? requestGroupId + : activeWorkspace._id; createRequestFetcher.submit({ organizationId, projectId, diff --git a/packages/insomnia/src/ui/components/.client/codemirror/one-line-editor.tsx b/packages/insomnia/src/ui/components/.client/codemirror/one-line-editor.tsx index 9915a80d43..e8efd28622 100644 --- a/packages/insomnia/src/ui/components/.client/codemirror/one-line-editor.tsx +++ b/packages/insomnia/src/ui/components/.client/codemirror/one-line-editor.tsx @@ -62,6 +62,9 @@ export interface OneLineEditorProps { eventListeners?: EditorEventListener[]; // NOTE: stable key for caching/restoring undo history across remounts uniquenessKey?: string; + autoFocus?: boolean; + // Called once when the editor focuses itself due to `autoFocus`. Lets callers clear a one-shot flag. + onAutoFocus?: () => void; } export interface EditorEventListener { @@ -88,6 +91,8 @@ export const OneLineEditor = forwardRef onBlur, eventListeners, uniquenessKey, + autoFocus, + onAutoFocus, }, ref, ) => { @@ -311,6 +316,56 @@ export const OneLineEditor = forwardRef } }); + reactUse.useMount(() => { + initEditor(); + if (autoFocus && !readOnly) { + onAutoFocus?.(); + // An enclosing React Aria ListBox (params/headers/environment grids) restores DOM focus to + // the row right after we focus the editor, and a single deferred focus loses that race on + // slower/headless machines. So we re-assert focus across a short window, re-grabbing only when + // focus was bounced to a non-interactive element (the row) — never when the user deliberately + // moved to another control (e.g. Tab from the URL bar to Send) — until the editor holds focus + // or the window elapses. + const deadline = Date.now() + 500; + const ensureFocus = () => { + const cm = codeMirror.current; + if (!cm) { + return; + } + if (!cm.hasFocus()) { + const active = document.activeElement as HTMLElement | null; + // The row React Aria bounces focus to is a non-interactive container (role="row"/"option"); + // anything genuinely interactive (a field, button, link, menu item, etc.) means the user + // moved on purpose, so we must not steal focus back. + const role = active?.getAttribute('role'); + const userMovedToAnotherControl = + !!active && + (active.tagName === 'INPUT' || + active.tagName === 'TEXTAREA' || + active.tagName === 'SELECT' || + active.tagName === 'BUTTON' || + active.tagName === 'A' || + active.isContentEditable || + role === 'button' || + role === 'link' || + role === 'menuitem' || + role === 'menuitemradio' || + role === 'checkbox' || + role === 'tab'); + if (userMovedToAnotherControl) { + return; + } + cm.focus(); + cm.getDoc().setCursor(cm.getDoc().lineCount(), 0); + } + if (Date.now() < deadline) { + requestAnimationFrame(ensureFocus); + } + }; + requestAnimationFrame(ensureFocus); + } + }); + reactUse.useUnmount(() => { persistState(); cleanUpEditor(); diff --git a/packages/insomnia/src/ui/components/editors/environment-key-value-editor/key-value-editor.tsx b/packages/insomnia/src/ui/components/editors/environment-key-value-editor/key-value-editor.tsx index bfe7ac1dfb..bb50a26e22 100644 --- a/packages/insomnia/src/ui/components/editors/environment-key-value-editor/key-value-editor.tsx +++ b/packages/insomnia/src/ui/components/editors/environment-key-value-editor/key-value-editor.tsx @@ -76,7 +76,7 @@ export const EnvironmentKVEditor = ({ // eslint-disable-next-line react-hooks/exhaustive-deps [JSON.stringify(data)], ); - const blankNameEditorRef = useRef(null); + const blankNameEditorRef = useRef(null); // The id for the trailing blank row is derived from the persisted pairs (rather than // held in state) so it only changes when the data actually changes. This keeps it in // sync with the async data updates - if it flipped eagerly the row the user just typed @@ -96,6 +96,11 @@ export const EnvironmentKVEditor = ({ // diffs) until the user starts typing in it. const kvPairs: EnvironmentKvPairData[] = useMemo(() => [...persistedPairs, blankPair], [persistedPairs, blankPair]); const codeModalRef = useRef(null); + // Refs to each row's Name editor, keyed by pair id. React Aria's ListBox (with drag-and-drop) owns + // roving focus and lands it on the row element; we hand that focus into the row's CodeMirror editor + // rather than fighting React Aria with an imperative focus loop (which corrupts the modal's + // ariaHideOutside/inert management on the sibling environments list). + const nameEditorRefs = useRef>(new Map()); const [kvPairError, setKvPairError] = useState<{ id: string; error: string }[]>([]); const [decryptedValues, setDecryptedValues] = useState>({}); const symmetricKey = useMemo(() => (vaultKey === '' ? {} : base64decode(vaultKey, true)), [vaultKey]); @@ -305,7 +310,16 @@ export const EnvironmentKVEditor = ({ )}
{ + if (el) { + nameEditorRefs.current.set(id, el); + } else { + nameEditorRefs.current.delete(id); + } + if (isBlank) { + blankNameEditorRef.current = el; + } + }} id={`environment-kv-editor-name-${id}`} placeholder={'Input Name'} defaultValue={name} @@ -515,9 +529,13 @@ export const EnvironmentKVEditor = ({ dependencies={[kvPairError, data, symmetricKey, blankId]} className="h-full w-full overflow-y-auto p-(--padding-sm)" items={kvPairs} + // Let React Aria place focus on the trailing blank row so the editor is ready to type into on + // open/add — then onFocus below hands that focus into the row's Name editor. + autoFocus={!disabled && kvPairs.length > 0 && kvPairs[kvPairs.length - 1].name === '' ? 'last' : undefined} > {kvPair => { const { id, name, enabled } = kvPair; + const isTrailingBlankRow = name === '' && kvPair === kvPairs[kvPairs.length - 1]; return ( { + // Forward focus from the row element into its Name editor, but only for the trailing + // blank row and only when the row itself (not an inner field) received focus. + if (!disabled && isTrailingBlankRow && e.target === e.currentTarget) { + nameEditorRefs.current.get(id)?.focusEnd(); + } + }} > {renderPairItem(kvPair)} diff --git a/packages/insomnia/src/ui/components/key-value-editor/key-value-editor.tsx b/packages/insomnia/src/ui/components/key-value-editor/key-value-editor.tsx index aee1820840..684439577b 100644 --- a/packages/insomnia/src/ui/components/key-value-editor/key-value-editor.tsx +++ b/packages/insomnia/src/ui/components/key-value-editor/key-value-editor.tsx @@ -35,6 +35,13 @@ interface Pair { canDisable?: boolean; } +// Id of the row whose Name cell should grab focus after an "Add". Module-level so it survives the +// KeyValueEditor remount that the async pair save triggers, and so it isn't prematurely consumed under +// React StrictMode. Keying off the specific id (rather than a shared boolean) means only the newly +// added row can autofocus — never an unrelated row in another mounted KeyValueEditor. Set on "Add", +// read during render, and cleared once that exact row's Name editor focuses (via onAutoFocus). +let pendingFocusLastRowId: string | null = null; + function createEmptyPair() { return { id: generateId('pair'), @@ -275,6 +282,7 @@ export const KeyValueEditor: FC = ({ className="flex h-full items-center justify-center gap-2 px-4 py-1 text-xs text-(--color-font) ring-1 ring-transparent transition-all hover:bg-(--hl-xs) focus:ring-(--hl-md) focus:ring-inset aria-pressed:bg-(--hl-sm)" onPress={() => { const id = generateId('pair'); + pendingFocusLastRowId = id; upsertPair({ id, name: '', value: '', description: '', disabled: false }); }} > @@ -505,6 +513,23 @@ export const KeyValueEditor: FC = ({ textValue={pair.name + '-' + pair.value} style={{ opacity: pair.disabled ? '0.4' : '1' }} className={`relative grid h-(--line-height-sm) shrink-0 gap-2 bg-(--color-bg) px-2 outline-hidden ${showDescription ? 'grid-cols-[max-content_1fr_1fr_1fr_max-content]' : 'grid-cols-[max-content_1fr_1fr_max-content]'}`} + onFocus={event => { + if (isDisabled) { + return; + } + // Only react when the row element itself takes focus, never an inner editor/control. + if (event.target !== event.currentTarget) { + return; + } + const listbox = event.currentTarget.closest('[role="listbox"]'); + const enteredFromOutside = !listbox?.contains(event.relatedTarget as Node | null); + // Tabbing onto the grid (focus entering from outside) or landing on the trailing blank + // row drops the cursor into the blank row's Name editor so the user can start typing a + // new pair immediately. Arrow-key navigation between existing rows is left untouched. + if (enteredFromOutside || isBlank) { + blankNameEditorRef.current?.focusEnd(); + } + }} >
= ({ placeholder={namePlaceholder || 'Name'} defaultValue={pair.name} readOnly={pair.disabled || isDisabled} + autoFocus={pair.id === pendingFocusLastRowId} + onAutoFocus={() => { + if (pendingFocusLastRowId === pair.id) { + pendingFocusLastRowId = null; + } + }} getAutocompleteConstants={() => handleGetAutocompleteNameConstants?.(pair) || []} onChange={name => { upsertPair({ ...pair, name }); diff --git a/packages/insomnia/src/ui/components/modals/request-group-settings-modal.tsx b/packages/insomnia/src/ui/components/modals/request-group-settings-modal.tsx index 2599dbfadb..0c18980fc8 100644 --- a/packages/insomnia/src/ui/components/modals/request-group-settings-modal.tsx +++ b/packages/insomnia/src/ui/components/modals/request-group-settings-modal.tsx @@ -94,6 +94,7 @@ export const RequestGroupSettingsModal = ({