From 7ddd2d3f17aac90bc655783dc9775e121f374238 Mon Sep 17 00:00:00 2001 From: Pooja Date: Thu, 2 Apr 2026 11:38:36 +0530 Subject: [PATCH] fix: allow file selection in multipart form without entering a key first (#7640) --- .../MultipartFormParams/StyledWrapper.js | 2 +- .../RequestPane/MultipartFormParams/index.js | 35 +++++---- .../StyledWrapper.js | 22 ++++++ .../multipart-form-file-select.spec.ts | 78 +++++++++++++++++++ 4 files changed, 123 insertions(+), 14 deletions(-) create mode 100644 tests/request/multipart-form/multipart-form-file-select.spec.ts diff --git a/packages/bruno-app/src/components/RequestPane/MultipartFormParams/StyledWrapper.js b/packages/bruno-app/src/components/RequestPane/MultipartFormParams/StyledWrapper.js index 6d7448307..b191ace70 100644 --- a/packages/bruno-app/src/components/RequestPane/MultipartFormParams/StyledWrapper.js +++ b/packages/bruno-app/src/components/RequestPane/MultipartFormParams/StyledWrapper.js @@ -15,7 +15,7 @@ const Wrapper = styled.div` transition: color 0.15s ease; &:hover { - color: ${(props) => props.theme.colors.text.link}; + color: ${(props) => props.theme.text}; } } diff --git a/packages/bruno-app/src/components/RequestPane/MultipartFormParams/index.js b/packages/bruno-app/src/components/RequestPane/MultipartFormParams/index.js index 28d9b1a4b..0cd73b960 100644 --- a/packages/bruno-app/src/components/RequestPane/MultipartFormParams/index.js +++ b/packages/bruno-app/src/components/RequestPane/MultipartFormParams/index.js @@ -65,12 +65,21 @@ const MultipartFormParams = ({ item, collection }) => { const currentParams = item.draft ? get(item, 'draft.request.body.multipartForm') : get(item, 'request.body.multipartForm'); - const updatedParams = (currentParams || []).map((p) => { - if (p.uid === row.uid) { - return { ...p, type: 'file', value: processedPaths }; - } - return p; - }); + const existsInParams = (currentParams || []).some((p) => p.uid === row.uid); + let updatedParams; + if (existsInParams) { + updatedParams = currentParams.map((p) => { + if (p.uid === row.uid) { + return { ...p, type: 'file', value: processedPaths }; + } + return p; + }); + } else { + updatedParams = [ + ...(currentParams || []), + { uid: row.uid, name: row.name || '', enabled: true, type: 'file', value: processedPaths, contentType: '' } + ]; + } handleParamsChange(updatedParams); }) .catch((error) => { @@ -139,13 +148,6 @@ const MultipartFormParams = ({ item, collection }) => { if (fileName) { return (
-
{ item={item} />
+
); } diff --git a/packages/bruno-app/src/components/ResponseExample/ResponseExampleRequestPane/ResponseExampleMultipartFormParams/StyledWrapper.js b/packages/bruno-app/src/components/ResponseExample/ResponseExampleRequestPane/ResponseExampleMultipartFormParams/StyledWrapper.js index b025e456d..58ade60c7 100644 --- a/packages/bruno-app/src/components/ResponseExample/ResponseExampleRequestPane/ResponseExampleMultipartFormParams/StyledWrapper.js +++ b/packages/bruno-app/src/components/ResponseExample/ResponseExampleRequestPane/ResponseExampleMultipartFormParams/StyledWrapper.js @@ -71,7 +71,29 @@ const Wrapper = styled.div` .upload-btn, .clear-file-btn { + display: flex; + align-items: center; + justify-content: center; + padding: 4px; + color: ${(props) => props.theme.colors.text.muted}; + background: transparent; + border: none; + cursor: pointer; + border-radius: 4px; + transition: color 0.15s ease; flex-shrink: 0; + + &:hover { + color: ${(props) => props.theme.text}; + } + } + + .clear-file-btn:hover { + color: ${(props) => props.theme.colors.text.danger}; + } + + .file-value-cell { + width: 100%; } .value-cell { diff --git a/tests/request/multipart-form/multipart-form-file-select.spec.ts b/tests/request/multipart-form/multipart-form-file-select.spec.ts new file mode 100644 index 000000000..799df32b2 --- /dev/null +++ b/tests/request/multipart-form/multipart-form-file-select.spec.ts @@ -0,0 +1,78 @@ +import { test, expect } from '../../../playwright'; +import { closeAllCollections, createCollection, createRequest, openCollection, openRequest, saveRequest, selectRequestPaneTab } from '../../utils/page'; +import { buildCommonLocators } from '../../utils/page/locators'; +import * as fs from 'fs'; +import * as path from 'path'; + +test.describe.serial('Multipart Form - File Select Without Key', () => { + let tmpDir: string; + let testFilePath: string; + + test.afterAll(async ({ page, electronApp }) => { + await electronApp.evaluate(({ dialog }) => { + if ((dialog as any).__originalShowOpenDialog) { + dialog.showOpenDialog = (dialog as any).__originalShowOpenDialog; + delete (dialog as any).__originalShowOpenDialog; + } + }); + await closeAllCollections(page); + }); + + test.beforeAll(async ({ page, electronApp, createTmpDir }) => { + tmpDir = await createTmpDir('multipart-file-select'); + + // Create a temp file that the mocked dialog will "select" + testFilePath = path.join(tmpDir, 'test-file.txt'); + await fs.promises.writeFile(testFilePath, 'hello world'); + expect(fs.existsSync(testFilePath)).toBe(true); + + await electronApp.evaluate(({ dialog }, filePath: string) => { + (dialog as any).__originalShowOpenDialog = dialog.showOpenDialog; + dialog.showOpenDialog = async () => ({ + canceled: false, + filePaths: [filePath] + }); + }, testFilePath); + + await test.step('Create collection and request', async () => { + await createCollection(page, 'multipart-file-select', tmpDir); + await createRequest(page, 'test-file-select', '', { + url: 'https://testbench-sanity.usebruno.com/api/echo/json', + method: 'POST', + inFolder: false + }); + }); + + await test.step('Open the request', async () => { + await openCollection(page, 'multipart-file-select'); + await openRequest(page, 'multipart-file-select', 'test-file-select', { persist: true }); + }); + + await test.step('Switch body mode to Multipart Form', async () => { + await selectRequestPaneTab(page, 'Body'); + const locators = buildCommonLocators(page); + await locators.request.bodyModeSelector().click(); + await page.locator('.dropdown-item').filter({ hasText: 'Multipart Form' }).click(); + }); + }); + + test('file select should work on empty row without a key', async ({ page }) => { + const table = buildCommonLocators(page).table('editable-table'); + + await test.step('Click upload on empty last row (no key entered)', async () => { + const lastRow = table.allRows().last(); + const uploadBtn = lastRow.locator('.upload-btn'); + await expect(uploadBtn).toBeVisible(); + await uploadBtn.click(); + }); + + await test.step('Verify the file name appears in the row', async () => { + const fileCell = table.allRows().locator('.file-value-cell').first(); + await expect(fileCell).toBeVisible(); + await expect(fileCell).toContainText('test-file.txt'); + }); + + // Save the request to clear draft state + await saveRequest(page); + }); +});