fix: restore cursor focus on save and show placeholder for empty cells (#6795)

This commit is contained in:
Pooja
2026-01-31 09:08:42 +05:30
committed by GitHub
parent 3ddf8e2a8b
commit 0c3d20b198
20 changed files with 152 additions and 58 deletions

View File

@@ -56,7 +56,7 @@ const Headers = ({ collection }) => {
isKeyField: true,
placeholder: 'Name',
width: '30%',
render: ({ row, value, onChange, isLastEmptyRow }) => (
render: ({ value, onChange }) => (
<SingleLineEditor
value={value || ''}
theme={storedTheme}
@@ -64,7 +64,7 @@ const Headers = ({ collection }) => {
onChange={(newValue) => onChange(newValue.replace(/[\r\n]/g, ''))}
autocomplete={headerAutoCompleteList}
collection={collection}
placeholder={isLastEmptyRow ? 'Name' : ''}
placeholder={!value ? 'Name' : ''}
/>
)
},
@@ -72,7 +72,7 @@ const Headers = ({ collection }) => {
key: 'value',
name: 'Value',
placeholder: 'Value',
render: ({ row, value, onChange, isLastEmptyRow }) => (
render: ({ value, onChange }) => (
<SingleLineEditor
value={value || ''}
theme={storedTheme}
@@ -80,7 +80,7 @@ const Headers = ({ collection }) => {
onChange={onChange}
collection={collection}
autocomplete={MimeTypes}
placeholder={isLastEmptyRow ? 'Value' : ''}
placeholder={!value ? 'Value' : ''}
/>
)
}

View File

@@ -46,14 +46,14 @@ const VarsTable = ({ collection, vars, varType }) => {
</div>
),
placeholder: varType === 'request' ? 'Value' : 'Expr',
render: ({ row, value, onChange, isLastEmptyRow }) => (
render: ({ value, onChange }) => (
<MultiLineEditor
value={value || ''}
theme={storedTheme}
onSave={onSave}
onChange={onChange}
collection={collection}
placeholder={isLastEmptyRow ? (varType === 'request' ? 'Value' : 'Expr') : ''}
placeholder={!value ? (varType === 'request' ? 'Value' : 'Expr') : ''}
/>
)
}

View File

@@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import React, { useCallback, useMemo, useRef, useState, useEffect } from 'react';
import { IconTrash, IconAlertCircle, IconGripVertical, IconMinusVertical } from '@tabler/icons';
import { Tooltip } from 'react-tooltip';
import { uuid } from 'utils/common';
@@ -312,7 +312,7 @@ const EditableTable = ({
className="mousetrap"
value={value || ''}
readOnly={column.readOnly}
placeholder={isEmpty ? column.placeholder || column.name : ''}
placeholder={!value ? column.placeholder || column.name : ''}
onChange={(e) => handleValueChange(row.uid, column.key, e.target.value)}
/>
{errorIcon}

View File

@@ -472,7 +472,7 @@ const EnvironmentVariablesTable = ({
id={`${actualIndex}.name`}
name={`${actualIndex}.name`}
value={variable.name}
placeholder={isLastEmptyRow ? 'Name' : ''}
placeholder={!variable.value || (typeof variable.value === 'string' && variable.value.trim() === '') ? 'Value' : ''}
onChange={(e) => handleNameChange(actualIndex, e)}
onBlur={() => handleNameBlur(actualIndex)}
onKeyDown={(e) => handleNameKeyDown(actualIndex, e)}

View File

@@ -60,7 +60,7 @@ const Headers = ({ collection, folder }) => {
isKeyField: true,
placeholder: 'Name',
width: '30%',
render: ({ row, value, onChange, isLastEmptyRow }) => (
render: ({ value, onChange }) => (
<SingleLineEditor
value={value || ''}
theme={storedTheme}
@@ -68,7 +68,7 @@ const Headers = ({ collection, folder }) => {
onChange={(newValue) => onChange(newValue.replace(/[\r\n]/g, ''))}
autocomplete={headerAutoCompleteList}
collection={collection}
placeholder={isLastEmptyRow ? 'Name' : ''}
placeholder={!value ? 'Name' : ''}
/>
)
},
@@ -76,7 +76,7 @@ const Headers = ({ collection, folder }) => {
key: 'value',
name: 'Value',
placeholder: 'Value',
render: ({ row, value, onChange, isLastEmptyRow }) => (
render: ({ value, onChange }) => (
<SingleLineEditor
value={value || ''}
theme={storedTheme}
@@ -85,7 +85,7 @@ const Headers = ({ collection, folder }) => {
collection={collection}
item={folder}
autocomplete={MimeTypes}
placeholder={isLastEmptyRow ? 'Value' : ''}
placeholder={!value ? 'Value' : ''}
/>
)
}

View File

@@ -51,7 +51,7 @@ const VarsTable = ({ folder, collection, vars, varType }) => {
</div>
),
placeholder: varType === 'request' ? 'Value' : 'Expr',
render: ({ row, value, onChange, isLastEmptyRow }) => (
render: ({ value, onChange }) => (
<MultiLineEditor
value={value || ''}
theme={storedTheme}
@@ -59,7 +59,7 @@ const VarsTable = ({ folder, collection, vars, varType }) => {
onChange={onChange}
collection={collection}
item={folder}
placeholder={isLastEmptyRow ? (varType === 'request' ? 'Value' : 'Expr') : ''}
placeholder={!value ? (varType === 'request' ? 'Value' : 'Expr') : ''}
/>
)
}

View File

@@ -125,7 +125,7 @@ const Assertions = ({ item, collection }) => {
key: 'value',
name: 'Value',
width: '30%',
render: ({ row, value, onChange, isLastEmptyRow }) => {
render: ({ row, value, onChange }) => {
const { operator, value: assertionValue } = parseAssertionOperator(value);
if (isUnaryOperator(operator)) {
@@ -141,7 +141,7 @@ const Assertions = ({ item, collection }) => {
onRun={handleRun}
collection={collection}
item={item}
placeholder={isLastEmptyRow ? 'Value' : ''}
placeholder={!value ? 'Value' : ''}
/>
);
}

View File

@@ -47,7 +47,7 @@ const FormUrlEncodedParams = ({ item, collection }) => {
key: 'value',
name: 'Value',
placeholder: 'Value',
render: ({ row, value, onChange, isLastEmptyRow }) => (
render: ({ value, onChange }) => (
<MultiLineEditor
value={value || ''}
theme={storedTheme}
@@ -57,7 +57,7 @@ const FormUrlEncodedParams = ({ item, collection }) => {
onRun={handleRun}
collection={collection}
item={item}
placeholder={isLastEmptyRow ? 'Value' : ''}
placeholder={!value ? 'Value' : ''}
/>
)
}

View File

@@ -157,7 +157,7 @@ const MultipartFormParams = ({ item, collection }) => {
allowNewlines={true}
collection={collection}
item={item}
placeholder={isLastEmptyRow ? 'Value' : ''}
placeholder={!value ? 'Value' : ''}
/>
</div>
{!hasTextValue && !isLastEmptyRow && (
@@ -177,12 +177,12 @@ const MultipartFormParams = ({ item, collection }) => {
key: 'contentType',
name: 'Content-Type',
placeholder: 'Auto',
width: '30%',
render: ({ row, value, onChange, isLastEmptyRow }) => (
width: '20%',
render: ({ value, onChange }) => (
<SingleLineEditor
onSave={onSave}
theme={storedTheme}
placeholder={isLastEmptyRow ? 'Auto' : ''}
placeholder={!value ? 'Auto' : ''}
value={value || ''}
onChange={onChange}
onRun={handleRun}

View File

@@ -70,7 +70,7 @@ const QueryParams = ({ item, collection }) => {
key: 'value',
name: 'Value',
placeholder: 'Value',
render: ({ row, value, onChange, isLastEmptyRow }) => (
render: ({ value, onChange }) => (
<MultiLineEditor
value={value || ''}
theme={storedTheme}
@@ -80,7 +80,7 @@ const QueryParams = ({ item, collection }) => {
collection={collection}
item={item}
variablesAutocomplete={true}
placeholder={isLastEmptyRow ? 'Value' : ''}
placeholder={!value ? 'Value' : ''}
/>
)
}

View File

@@ -66,7 +66,7 @@ const RequestHeaders = ({ item, collection, addHeaderText }) => {
isKeyField: true,
placeholder: 'Name',
width: '30%',
render: ({ value, onChange, isLastEmptyRow }) => (
render: ({ value, onChange }) => (
<SingleLineEditor
value={value || ''}
theme={storedTheme}
@@ -76,7 +76,7 @@ const RequestHeaders = ({ item, collection, addHeaderText }) => {
onRun={handleRun}
collection={collection}
item={item}
placeholder={isLastEmptyRow ? 'Name' : ''}
placeholder={!value ? 'Name' : ''}
/>
)
},
@@ -84,7 +84,7 @@ const RequestHeaders = ({ item, collection, addHeaderText }) => {
key: 'value',
name: 'Value',
placeholder: 'Value',
render: ({ value, onChange, isLastEmptyRow }) => (
render: ({ value, onChange }) => (
<SingleLineEditor
value={value || ''}
theme={storedTheme}
@@ -94,7 +94,7 @@ const RequestHeaders = ({ item, collection, addHeaderText }) => {
autocomplete={MimeTypes}
collection={collection}
item={item}
placeholder={isLastEmptyRow ? 'Value' : ''}
placeholder={!value ? 'Value' : ''}
/>
)
}

View File

@@ -61,7 +61,7 @@ const VarsTable = ({ item, collection, vars, varType }) => {
</div>
),
placeholder: varType === 'request' ? 'Value' : 'Expr',
render: ({ row, value, onChange, isLastEmptyRow }) => (
render: ({ value, onChange }) => (
<MultiLineEditor
value={value || ''}
theme={storedTheme}
@@ -70,7 +70,7 @@ const VarsTable = ({ item, collection, vars, varType }) => {
onRun={handleRun}
collection={collection}
item={item}
placeholder={isLastEmptyRow ? (varType === 'request' ? 'Value' : 'Expr') : ''}
placeholder={!value ? (varType === 'request' ? 'Value' : 'Expr') : ''}
/>
)
}

View File

@@ -130,12 +130,12 @@ const ResponseExampleFileBody = ({ item, collection, exampleUid, editMode = fals
placeholder: 'Auto',
width: '30%',
readOnly: !editMode,
render: ({ row, value, onChange, isLastEmptyRow }) => (
render: ({ value, onChange }) => (
<SingleLineEditor
className="flex items-center justify-center"
onSave={() => {}}
theme={storedTheme}
placeholder={isLastEmptyRow ? 'Auto' : ''}
placeholder={!value ? 'Auto' : ''}
value={value || ''}
onChange={onChange}
onRun={() => {}}

View File

@@ -58,7 +58,7 @@ const ResponseExampleFormUrlEncodedParams = ({ item, collection, exampleUid, edi
placeholder: 'Value',
width: '60%',
readOnly: !editMode,
render: ({ row, value, onChange, isLastEmptyRow }) => (
render: ({ value, onChange }) => (
<MultiLineEditor
value={value || ''}
theme={storedTheme}
@@ -68,7 +68,7 @@ const ResponseExampleFormUrlEncodedParams = ({ item, collection, exampleUid, edi
onRun={() => {}}
collection={collection}
item={item}
placeholder={isLastEmptyRow ? 'Value' : ''}
placeholder={!value ? 'Value' : ''}
/>
)
}

View File

@@ -68,7 +68,7 @@ const ResponseExampleHeaders = ({ editMode, item, collection, exampleUid }) => {
placeholder: 'Key',
width: '40%',
readOnly: !editMode,
render: ({ row, value, onChange, isLastEmptyRow }) => (
render: ({ value, onChange }) => (
<SingleLineEditor
value={value || ''}
readOnly={!editMode}
@@ -78,7 +78,7 @@ const ResponseExampleHeaders = ({ editMode, item, collection, exampleUid }) => {
autocomplete={headerAutoCompleteList}
onRun={() => {}}
collection={collection}
placeholder={isLastEmptyRow ? 'Key' : ''}
placeholder={!value ? 'Key' : ''}
/>
)
},
@@ -88,7 +88,7 @@ const ResponseExampleHeaders = ({ editMode, item, collection, exampleUid }) => {
placeholder: 'Value',
width: '60%',
readOnly: !editMode,
render: ({ row, value, onChange, isLastEmptyRow }) => (
render: ({ value, onChange }) => (
<SingleLineEditor
value={value || ''}
readOnly={!editMode}
@@ -100,7 +100,7 @@ const ResponseExampleHeaders = ({ editMode, item, collection, exampleUid }) => {
allowNewlines={true}
collection={collection}
item={item}
placeholder={isLastEmptyRow ? 'Value' : ''}
placeholder={!value ? 'Value' : ''}
/>
)
}

View File

@@ -206,7 +206,7 @@ const ResponseExampleMultipartFormParams = ({ item, collection, exampleUid, edit
collection={collection}
item={item}
readOnly={!editMode}
placeholder={isLastEmptyRow ? 'Value' : ''}
placeholder={!value ? 'Value' : ''}
/>
</div>
{!hasTextValue && !isLastEmptyRow && (
@@ -228,11 +228,11 @@ const ResponseExampleMultipartFormParams = ({ item, collection, exampleUid, edit
placeholder: 'Auto',
width: '30%',
readOnly: !editMode,
render: ({ row, value, onChange, isLastEmptyRow }) => (
render: ({ value, onChange }) => (
<SingleLineEditor
onSave={() => {}}
theme={storedTheme}
placeholder={isLastEmptyRow ? 'Auto' : ''}
placeholder={!value ? 'Auto' : ''}
value={value || ''}
onChange={onChange}
onRun={() => {}}

View File

@@ -105,7 +105,7 @@ const ResponseExampleParams = ({ editMode, item, collection, exampleUid }) => {
placeholder: 'Name',
width: '40%',
readOnly: !editMode,
render: ({ row, value, onChange, isLastEmptyRow }) => (
render: ({ value, onChange }) => (
<SingleLineEditor
value={value || ''}
theme={storedTheme}
@@ -115,7 +115,7 @@ const ResponseExampleParams = ({ editMode, item, collection, exampleUid }) => {
collection={collection}
variablesAutocomplete={true}
readOnly={!editMode}
placeholder={isLastEmptyRow ? 'Name' : ''}
placeholder={!value ? 'Name' : ''}
/>
)
},
@@ -125,7 +125,7 @@ const ResponseExampleParams = ({ editMode, item, collection, exampleUid }) => {
placeholder: 'Value',
width: '60%',
readOnly: !editMode,
render: ({ row, value, onChange, isLastEmptyRow }) => (
render: ({ value, onChange }) => (
<SingleLineEditor
value={value || ''}
theme={storedTheme}
@@ -135,7 +135,7 @@ const ResponseExampleParams = ({ editMode, item, collection, exampleUid }) => {
collection={collection}
variablesAutocomplete={true}
readOnly={!editMode}
placeholder={isLastEmptyRow ? 'Value' : ''}
placeholder={!value ? 'Value' : ''}
/>
)
}
@@ -154,7 +154,7 @@ const ResponseExampleParams = ({ editMode, item, collection, exampleUid }) => {
placeholder: 'Value',
width: '60%',
readOnly: !editMode,
render: ({ row, value, onChange, isLastEmptyRow }) => (
render: ({ value, onChange }) => (
<SingleLineEditor
value={value || ''}
theme={storedTheme}
@@ -164,7 +164,7 @@ const ResponseExampleParams = ({ editMode, item, collection, exampleUid }) => {
collection={collection}
variablesAutocomplete={true}
readOnly={!editMode}
placeholder={isLastEmptyRow ? 'Value' : ''}
placeholder={!value ? 'Value' : ''}
/>
)
}

View File

@@ -124,7 +124,7 @@ const ResponseExampleResponseHeaders = ({ editMode, item, collection, exampleUid
placeholder: 'Key',
width: '40%',
readOnly: !editMode,
render: ({ row, value, onChange, isLastEmptyRow }) => (
render: ({ value, onChange }) => (
<SingleLineEditor
value={value || ''}
theme={storedTheme}
@@ -134,7 +134,7 @@ const ResponseExampleResponseHeaders = ({ editMode, item, collection, exampleUid
onRun={() => {}}
collection={collection}
readOnly={!editMode}
placeholder={isLastEmptyRow ? 'Key' : ''}
placeholder={!value ? 'Key' : ''}
/>
)
},
@@ -144,7 +144,7 @@ const ResponseExampleResponseHeaders = ({ editMode, item, collection, exampleUid
placeholder: 'Value',
width: '60%',
readOnly: !editMode,
render: ({ row, value, onChange, isLastEmptyRow }) => (
render: ({ value, onChange }) => (
<SingleLineEditor
value={value || ''}
theme={storedTheme}
@@ -156,7 +156,7 @@ const ResponseExampleResponseHeaders = ({ editMode, item, collection, exampleUid
collection={collection}
item={item}
readOnly={!editMode}
placeholder={isLastEmptyRow ? 'Value' : ''}
placeholder={!value ? 'Value' : ''}
/>
)
}

View File

@@ -66,6 +66,51 @@ const wsStatusCodes = {
1015: 'TLS_HANDSHAKE'
};
/**
* Preserves UIDs from existing array items when merging with new data.
* UIDs are matched by position to keep React keys stable after file reloads.
*/
const preserveUidsAtPaths = (existing, updated, paths) => {
if (!existing || !updated) return updated;
const merged = cloneDeep(updated);
paths.forEach((path) => {
const newArray = get(merged, path);
const existingArray = get(existing, path, []);
if (Array.isArray(newArray) && newArray.length) {
set(
merged,
path,
newArray.map((item, i) => (existingArray[i]?.uid ? { ...item, uid: existingArray[i].uid } : item))
);
}
});
return merged;
};
// Paths containing arrays with UIDs that need preservation
const REQUEST_UID_PATHS = [
'params',
'headers',
'vars.req',
'vars.res',
'assertions',
'body.formUrlEncoded',
'body.multipartForm',
'body.file'
];
const ROOT_UID_PATHS = ['request.headers', 'request.vars.req', 'request.vars.res'];
const mergeRequestWithPreservedUids = (existingRequest, newRequest) =>
preserveUidsAtPaths(existingRequest, newRequest, REQUEST_UID_PATHS);
const mergeRootWithPreservedUids = (existingRoot, newRoot) =>
preserveUidsAtPaths(existingRoot, newRoot, ROOT_UID_PATHS);
const initialState = {
collections: [],
collectionSortOrder: 'default',
@@ -2561,7 +2606,7 @@ export const collectionsSlice = createSlice({
const collection = findCollectionByUid(state.collections, file.meta.collectionUid);
if (isCollectionRoot) {
if (collection) {
collection.root = file.data;
collection.root = mergeRootWithPreservedUids(collection.root, file.data);
}
return;
}
@@ -2573,7 +2618,7 @@ export const collectionsSlice = createSlice({
if (file?.data?.meta?.name) {
folderItem.name = file?.data?.meta?.name;
}
folderItem.root = file.data;
folderItem.root = mergeRootWithPreservedUids(folderItem.root, file.data);
if (file?.data?.meta?.seq) {
folderItem.seq = file.data?.meta?.seq;
}
@@ -2621,7 +2666,7 @@ export const collectionsSlice = createSlice({
currentItem.type = file.data.type;
currentItem.seq = file.data.seq;
currentItem.tags = file.data.tags;
currentItem.request = file.data.request;
currentItem.request = mergeRequestWithPreservedUids(currentItem.request, file.data.request);
currentItem.filename = file.meta.name;
currentItem.pathname = file.meta.pathname;
currentItem.settings = file.data.settings;
@@ -2700,7 +2745,7 @@ export const collectionsSlice = createSlice({
const collection = findCollectionByUid(state.collections, file.meta.collectionUid);
if (isCollectionRoot) {
if (collection) {
collection.root = file.data;
collection.root = mergeRootWithPreservedUids(collection.root, file.data);
}
return;
}
@@ -2715,7 +2760,7 @@ export const collectionsSlice = createSlice({
if (file?.data?.meta?.seq) {
folderItem.seq = file?.data?.meta?.seq;
}
folderItem.root = file.data;
folderItem.root = mergeRootWithPreservedUids(folderItem.root, file.data);
}
return;
}
@@ -2740,7 +2785,7 @@ export const collectionsSlice = createSlice({
item.type = file.data.type;
item.seq = file.data.seq;
item.tags = file.data.tags;
item.request = file.data.request;
item.request = mergeRequestWithPreservedUids(item.request, file.data.request);
item.settings = file.data.settings;
item.examples = file.data.examples;
item.filename = file.meta.name;

View File

@@ -0,0 +1,49 @@
import { test, expect } from '../../playwright';
import { createCollection, closeAllCollections, createRequest, selectRequestPaneTab } from '../utils/page';
test.describe('EditableTable - Focus and Placeholder', () => {
test.afterEach(async ({ page }) => {
await closeAllCollections(page);
});
test('Cursor focus restored after save and placeholder shown for empty value', async ({ page, createTmpDir }) => {
const collectionName = 'test-editable-table';
// Create a new collection
await createCollection(page, collectionName, await createTmpDir());
// Create a request
await createRequest(page, 'Test Request', collectionName, {
url: 'https://httpbin.org/get'
});
// Navigate to Params tab
await selectRequestPaneTab(page, 'Params');
// Find the Query params table
const queryTable = page.locator('table').first();
const firstRow = queryTable.locator('tbody tr').first();
// Get the Name input (regular input)
const nameInput = firstRow.locator('input[type="text"]').first();
await nameInput.click();
await page.keyboard.type('testParam');
// Verify input has focus before save
await expect(nameInput).toBeFocused();
// Save the request
await page.keyboard.press('Meta+s');
// Wait for save toast
await expect(page.getByText('Request saved successfully').last()).toBeVisible();
// Verify cursor focus is restored after save
await expect(nameInput).toBeFocused();
// Verify placeholder shows for empty Value field
const valueCell = firstRow.locator('[data-testid="column-value"]');
const placeholder = valueCell.locator('pre.CodeMirror-placeholder');
await expect(placeholder).toHaveText('Value');
});
});