feat: replace send button with Send/Cancel buttons on request url (#7675)

* feat: replace request send icon with Send/Cancel buttons

---------

Co-authored-by: naman-bruno <naman@usebruno.com>
This commit is contained in:
gopu-bruno
2026-04-07 13:42:09 +05:30
committed by GitHub
parent 4d6032ba0d
commit 476d30a49e
12 changed files with 125 additions and 85 deletions

View File

@@ -324,7 +324,7 @@ test('should create and execute HTTP request', async ({ page, createTmpDir }) =>
await page.getByRole('button', { name: 'Create' }).click(); await page.getByRole('button', { name: 'Create' }).click();
// Execute request // Execute request
await page.locator('#send-request').getByRole('img').nth(2).click(); await page.getByTestId('send-arrow-icon').click();
// Verify response // Verify response
await expect(page.getByRole('main')).toContainText('200 OK'); await expect(page.getByRole('main')).toContainText('200 OK');

View File

@@ -2,9 +2,13 @@ import styled from 'styled-components';
const Wrapper = styled.div` const Wrapper = styled.div`
height: 2.1rem; height: 2.1rem;
border: ${(props) => props.theme.requestTabPanel.url.border};
border-radius: ${(props) => props.theme.border.radius.base};
.url-input-group {
border: ${(props) => props.theme.requestTabPanel.url.border};
border-radius: ${(props) => props.theme.border.radius.base};
flex: 1;
min-width: 0;
}
.infotip { .infotip {
position: relative; position: relative;
@@ -49,6 +53,7 @@ const Wrapper = styled.div`
.shortcut { .shortcut {
font-size: 0.625rem; font-size: 0.625rem;
} }
`; `;
export default Wrapper; export default Wrapper;

View File

@@ -16,8 +16,9 @@ import { saveRequest, cancelRequest } from 'providers/ReduxStore/slices/collecti
import { getRequestFromCurlCommand } from 'utils/curl'; import { getRequestFromCurlCommand } from 'utils/curl';
import HttpMethodSelector from './HttpMethodSelector'; import HttpMethodSelector from './HttpMethodSelector';
import { useTheme } from 'providers/Theme'; import { useTheme } from 'providers/Theme';
import { IconDeviceFloppy, IconArrowRight, IconCode, IconSquareRoundedX } from '@tabler/icons'; import { IconDeviceFloppy, IconCode } from '@tabler/icons';
import SingleLineEditor from 'components/SingleLineEditor'; import SingleLineEditor from 'components/SingleLineEditor';
import SendButton from 'components/RequestPane/SendButton';
import { isMacOS } from 'utils/common/platform'; import { isMacOS } from 'utils/common/platform';
import { hasRequestChanges } from 'utils/collections'; import { hasRequestChanges } from 'utils/collections';
import StyledWrapper from './StyledWrapper'; import StyledWrapper from './StyledWrapper';
@@ -384,76 +385,67 @@ const QueryUrl = ({ item, collection, handleRun }) => {
}; };
return ( return (
<StyledWrapper className="flex items-center w-full"> <StyledWrapper className="flex items-center w-full">
<div className="flex items-center h-full min-w-fit"> <div className="flex items-center h-full url-input-group">
<HttpMethodSelector method={method} onMethodSelect={onMethodSelect} /> <div className="flex items-center h-full min-w-fit">
</div> <HttpMethodSelector method={method} onMethodSelect={onMethodSelect} />
<div
id="request-url"
className="h-full w-full flex flex-row input-container overflow-auto"
>
<SingleLineEditor
ref={editorRef}
value={url}
placeholder="Enter URL or paste a cURL request"
onSave={(finalValue) => onSave(finalValue)}
theme={storedTheme}
onChange={(newValue) => onUrlChange(newValue)}
onRun={handleRun}
onPaste={item.type === 'http-request' ? handleHttpPaste : item.type === 'graphql-request' ? handleGraphqlPaste : null}
collection={collection}
highlightPathParams={true}
item={item}
showNewlineArrow={true}
/>
</div>
<div className="flex items-center h-full mx-2 gap-3 cursor-pointer" id="send-request" onClick={handleRun}>
<div
title="Generate Code"
className="infotip"
onClick={(e) => {
handleGenerateCode(e);
}}
>
<IconCode color={theme.requestTabs.icon.color} strokeWidth={1.5} size={20} className="cursor-pointer" />
<span className="infotiptext text-xs">Generate Code</span>
</div> </div>
<div <div
title="Save Request" id="request-url"
className="infotip" className="h-full w-full flex flex-row items-center input-container overflow-auto"
onClick={(e) => {
e.stopPropagation();
if (!hasChanges) return;
onSave();
}}
> >
<IconDeviceFloppy <SingleLineEditor
color={hasChanges ? theme.draftColor : theme.requestTabs.icon.color} ref={editorRef}
strokeWidth={1.5} value={url}
size={20} placeholder="Enter URL or paste a cURL request"
className={`${hasChanges ? 'cursor-pointer' : 'cursor-default'}`} onSave={(finalValue) => onSave(finalValue)}
theme={storedTheme}
onChange={(newValue) => onUrlChange(newValue)}
onRun={handleRun}
onPaste={item.type === 'http-request' ? handleHttpPaste : item.type === 'graphql-request' ? handleGraphqlPaste : null}
collection={collection}
highlightPathParams={true}
item={item}
showNewlineArrow={true}
/> />
<span className="infotiptext text-xs"> <div className="flex items-center h-full mx-2 gap-3" id="request-actions">
Save <span className="shortcut">({saveShortcut})</span> <div
</span> title="Generate Code"
className="infotip"
onClick={(e) => {
handleGenerateCode(e);
}}
>
<IconCode color={theme.requestTabs.icon.color} strokeWidth={1.5} size={20} className="cursor-pointer" />
<span className="infotiptext text-xs">Generate Code</span>
</div>
<div
title="Save Request"
className="infotip"
onClick={(e) => {
e.stopPropagation();
if (!hasChanges) return;
onSave();
}}
>
<IconDeviceFloppy
color={hasChanges ? theme.draftColor : theme.requestTabs.icon.color}
strokeWidth={1.5}
size={20}
className={`${hasChanges ? 'cursor-pointer' : 'cursor-default'}`}
/>
<span className="infotiptext text-xs">
Save <span className="shortcut">({saveShortcut})</span>
</span>
</div>
</div>
</div> </div>
{isLoading || item.response?.stream?.running ? (
<IconSquareRoundedX
color={theme.requestTabPanel.url.iconDanger}
strokeWidth={1.5}
size={20}
data-testid="cancel-request-icon"
onClick={handleCancelRequest}
/>
) : (
<IconArrowRight
color={theme.requestTabPanel.url.icon}
strokeWidth={1.5}
size={20}
data-testid="send-arrow-icon"
/>
)}
</div> </div>
<SendButton
isLoading={isLoading || item.response?.stream?.running}
onSend={handleRun}
onCancel={handleCancelRequest}
testId="send-arrow-icon"
/>
{generateCodeItemModalOpen && ( {generateCodeItemModalOpen && (
<GenerateCodeItem <GenerateCodeItem
collectionUid={collection.uid} collectionUid={collection.uid}

View File

@@ -0,0 +1,20 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
display: flex;
align-self: stretch;
min-width: 4.1rem;
flex-shrink: 0;
> div {
display: flex;
flex: 1;
}
button {
width: 100%;
height: 100%;
}
`;
export default StyledWrapper;

View File

@@ -0,0 +1,22 @@
import React from 'react';
import Button from 'ui/Button';
import StyledWrapper from './StyledWrapper';
const SendButton = ({ isLoading = false, onSend, onCancel, testId = 'send-request-btn' }) => {
return (
<StyledWrapper className="ml-2">
<Button
size="sm"
variant={isLoading ? 'outline' : 'filled'}
color="primary"
data-testid={testId}
data-action={isLoading ? 'cancel' : 'send'}
onClick={isLoading ? onCancel : onSend}
>
{isLoading ? 'Cancel' : 'Send'}
</Button>
</StyledWrapper>
);
};
export default SendButton;

View File

@@ -3,12 +3,12 @@ import styled from 'styled-components';
const StyledWrapper = styled.div` const StyledWrapper = styled.div`
height: 2.1rem; height: 2.1rem;
position: relative; position: relative;
border: ${(props) => props.theme.requestTabPanel.url.border};
border-radius: ${(props) => props.theme.border.radius.base};
.input-container { .input-container {
background-color: ${(props) => props.theme.requestTabPanel.url.bg}; background-color: ${(props) => props.theme.requestTabPanel.url.bg};
border: ${(props) => props.theme.requestTabPanel.url.border};
border-radius: ${(props) => props.theme.border.radius.base}; border-radius: ${(props) => props.theme.border.radius.base};
position: relative;
input { input {
background-color: ${(props) => props.theme.requestTabPanel.url.bg}; background-color: ${(props) => props.theme.requestTabPanel.url.bg};
@@ -99,6 +99,7 @@ const StyledWrapper = styled.div`
} }
} }
} }
`; `;
export default StyledWrapper; export default StyledWrapper;

View File

@@ -1,4 +1,5 @@
import { IconArrowRight, IconDeviceFloppy, IconPlugConnected, IconPlugConnectedX } from '@tabler/icons'; import { IconDeviceFloppy, IconPlugConnected, IconPlugConnectedX } from '@tabler/icons';
import SendButton from 'components/RequestPane/SendButton';
import classnames from 'classnames'; import classnames from 'classnames';
import SingleLineEditor from 'components/SingleLineEditor/index'; import SingleLineEditor from 'components/SingleLineEditor/index';
import { requestUrlChanged } from 'providers/ReduxStore/slices/collections'; import { requestUrlChanged } from 'providers/ReduxStore/slices/collections';
@@ -123,7 +124,7 @@ const WsQueryUrl = ({ item, collection, handleRun }) => {
return ( return (
<StyledWrapper> <StyledWrapper>
<div className="flex items-center h-full"> <div className="flex items-center h-full">
<div className="flex items-center input-container flex-1 w-full h-full relative"> <div className="flex items-center input-container flex-1 min-w-0 h-full relative">
<div className="flex items-center justify-center px-[10px]"> <div className="flex items-center justify-center px-[10px]">
<span className="text-xs font-medium method-ws">WS</span> <span className="text-xs font-medium method-ws">WS</span>
</div> </div>
@@ -187,15 +188,14 @@ const WsQueryUrl = ({ item, collection, handleRun }) => {
</div> </div>
</div> </div>
)} )}
<div data-testid="run-button" className="cursor-pointer" onClick={handleRunClick}>
<IconArrowRight color={theme.requestTabPanel.url.icon} strokeWidth={1.5} size={20} />
</div>
</div> </div>
{connectionStatus === CONNECTION_STATUS.CONNECTED && <div className="connection-status-strip"></div>}
</div> </div>
<SendButton
onSend={handleRunClick}
testId="run-button"
/>
</div> </div>
{connectionStatus === CONNECTION_STATUS.CONNECTED && <div className="connection-status-strip"></div>}
</StyledWrapper> </StyledWrapper>
); );
}; };

View File

@@ -19,13 +19,13 @@ test.describe('Create collection', () => {
// Set the URL // Set the URL
await page.locator('#request-url .CodeMirror').click(); await page.locator('#request-url .CodeMirror').click();
await page.locator('#request-url').locator('textarea').fill('http://localhost:8081'); await page.locator('#request-url').locator('textarea').fill('http://localhost:8081');
await page.locator('#send-request').getByTitle('Save Request').click(); await page.locator('#request-actions').getByTitle('Save Request').click();
// Send a request // Send a request
await page.locator('#request-url .CodeMirror').click(); await page.locator('#request-url .CodeMirror').click();
await page.locator('#request-url').locator('textarea').fill('/ping'); await page.locator('#request-url').locator('textarea').fill('/ping');
await page.locator('#send-request').getByTitle('Save Request').click(); await page.locator('#request-actions').getByTitle('Save Request').click();
await page.locator('#send-request').getByRole('img').nth(2).click(); await page.getByTestId('send-arrow-icon').click();
// Verify the response // Verify the response
await expect(page.getByRole('main')).toContainText('200 OK'); await expect(page.getByRole('main')).toContainText('200 OK');

View File

@@ -22,7 +22,7 @@ test.describe('Multiline Variables - Read Environment Test', () => {
await expect(page.locator('.current-environment').filter({ hasText: /Test/ })).toBeVisible(); await expect(page.locator('.current-environment').filter({ hasText: /Test/ })).toBeVisible();
// send request // send request
const sendButton = page.locator('#send-request').getByRole('img').nth(2); const sendButton = page.getByTestId('send-arrow-icon');
await expect(sendButton).toBeVisible(); await expect(sendButton).toBeVisible();
await sendButton.click(); await sendButton.click();
await expect(page.locator('.response-status-code.text-ok')).toBeVisible(); await expect(page.locator('.response-status-code.text-ok')).toBeVisible();

View File

@@ -77,7 +77,7 @@ test.describe.serial('Create and Delete Response Examples', () => {
}); });
await test.step('Test form reset', async () => { await test.step('Test form reset', async () => {
await page.locator('#send-request').getByRole('img').nth(2).click(); await page.getByTestId('send-arrow-icon').click();
await clickResponseAction(page, 'response-bookmark-btn'); await clickResponseAction(page, 'response-bookmark-btn');
await page.getByTestId('create-example-name-input').fill('Test Name'); await page.getByTestId('create-example-name-input').fill('Test Name');

View File

@@ -141,7 +141,7 @@ const createUntitledRequest = async (
if (url) { if (url) {
await page.locator('#request-url .CodeMirror').click(); await page.locator('#request-url .CodeMirror').click();
await page.locator('#request-url textarea').fill(url); await page.locator('#request-url textarea').fill(url);
await page.locator('#send-request').getByTitle('Save Request').click(); await page.locator('#request-actions').getByTitle('Save Request').click();
await page.waitForTimeout(200); await page.waitForTimeout(200);
} }

View File

@@ -83,7 +83,7 @@ export const buildCommonLocators = (page: Page) => ({
newRequestUrl: () => page.locator('#new-request-url .CodeMirror'), newRequestUrl: () => page.locator('#new-request-url .CodeMirror'),
requestNameInput: () => page.getByPlaceholder('Request Name'), requestNameInput: () => page.getByPlaceholder('Request Name'),
requestTestId: () => page.getByTestId('request-name'), requestTestId: () => page.getByTestId('request-name'),
generateCodeButton: () => page.locator('#send-request .infotip').first(), generateCodeButton: () => page.locator('#request-actions .infotip').first(),
bodyModeSelector: () => page.getByTestId('request-body-mode-selector'), bodyModeSelector: () => page.getByTestId('request-body-mode-selector'),
bodyEditor: () => page.getByTestId('request-body-editor') bodyEditor: () => page.getByTestId('request-body-editor')
}, },