BULK_ACTIONS: fix the keyboard delete action in bulk actions (#22841)

* fix the keyboard delete action in bulk actions

* reverted the file change

* added support for column header click select all column delete handle as well
This commit is contained in:
Ashish Gupta 2025-08-12 16:34:08 +05:30 committed by GitHub
parent 9811bf09a0
commit eb1b3df7c5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 450 additions and 3 deletions

View File

@ -38,6 +38,8 @@ import {
fillRowDetails,
fillStoredProcedureCode,
firstTimeGridAddRowAction,
performColumnSelectAndDeleteOperation,
performDeleteOperationOnEntity,
pressKeyXTimes,
validateImportStatus,
} from '../../utils/importUtils';
@ -878,6 +880,226 @@ test.describe('Bulk Import Export', () => {
await afterAction();
});
test('Keyboard Delete selection', async ({ page }) => {
test.slow(true);
const dbEntity = new DatabaseClass();
const { apiContext, afterAction } = await getApiContext(page);
await dbEntity.create(apiContext);
await test.step('should export data database schema details', async () => {
await dbEntity.visitEntityPage(page);
const downloadPromise = page.waitForEvent('download');
await page.click('[data-testid="manage-button"]');
await page.click('[data-testid="export-button-title"]');
await page.fill('#fileName', dbEntity.entity.name);
await page.click('#submit-button');
const download = await downloadPromise;
// Wait for the download process to complete and save the downloaded file somewhere.
await download.saveAs('downloads/' + download.suggestedFilename());
});
await test.step(
'should import and perform edit operation on entity',
async () => {
await dbEntity.visitEntityPage(page);
await page.click('[data-testid="manage-button"] > .anticon');
await page.click('[data-testid="import-button-title"]');
const fileInput = await page.$('[type="file"]');
await fileInput?.setInputFiles([
'downloads/' + dbEntity.entity.name + '.csv',
]);
// Adding manual wait for the file to load
await page.waitForTimeout(500);
// Adding some assertion to make sure that CSV loaded correctly
await expect(page.locator('.rdg-header-row')).toBeVisible();
await expect(page.getByTestId('add-row-btn')).toBeVisible();
await expect(page.getByRole('button', { name: 'Next' })).toBeVisible();
await expect(
page.getByRole('button', { name: 'Previous' })
).toBeVisible();
// Click on first cell and edit
await page.click('.rdg-cell[role="gridcell"]');
await fillRowDetails(
{
...databaseDetails1,
owners: [
EntityDataClass.user1.responseData?.['displayName'],
EntityDataClass.user2.responseData?.['displayName'],
],
domains: EntityDataClass.domain1.responseData,
},
page,
undefined,
true
);
await fillRecursiveEntityTypeFQNDetails(
`${dbEntity.entityResponseData.fullyQualifiedName}.${databaseSchemaDetails1.name}`,
databaseSchemaDetails1.entityType,
page
);
await page.getByRole('button', { name: 'Next' }).click();
await validateImportStatus(page, {
passed: '9',
processed: '9',
failed: '0',
});
const rowStatus = [
'Entity created',
'Entity updated',
'Entity updated',
'Entity updated',
'Entity updated',
'Entity updated',
'Entity updated',
'Entity updated',
];
await expect(page.locator('.rdg-cell-details')).toHaveText(rowStatus);
const updateButtonResponse = page.waitForResponse(
`/api/v1/databases/name/*/importAsync?*dryRun=false&recursive=true*`
);
await page.getByRole('button', { name: 'Update' }).click();
await page
.locator('.inovua-react-toolkit-load-mask__background-layer')
.waitFor({ state: 'detached' });
await updateButtonResponse;
await page.waitForEvent('framenavigated');
await toastNotification(page, /details updated successfully/);
}
);
await test.step(
'should export data database schema details after edit changes',
async () => {
await dbEntity.visitEntityPage(page);
const downloadPromise = page.waitForEvent('download');
await page.click('[data-testid="manage-button"]');
await page.click('[data-testid="export-button-title"]');
await page.fill('#fileName', `${dbEntity.entity.name}-delete`);
await page.click('#submit-button');
const download = await downloadPromise;
// Wait for the download process to complete and save the downloaded file somewhere.
await download.saveAs('downloads/' + download.suggestedFilename());
}
);
await test.step('Perform Column Select and Delete Operation', async () => {
await page.click('[data-testid="manage-button"] > .anticon');
await page.click('[data-testid="import-button-title"]');
const fileInput = await page.$('[type="file"]');
await fileInput?.setInputFiles([
'downloads/' + `${dbEntity.entity.name}-delete` + '.csv',
]);
// Adding manual wait for the file to load
await page.waitForTimeout(500);
// Adding some assertion to make sure that CSV loaded correctly
await expect(page.locator('.rdg-header-row')).toBeVisible();
await expect(page.getByTestId('add-row-btn')).toBeVisible();
await expect(page.getByRole('button', { name: 'Next' })).toBeVisible();
await expect(
page.getByRole('button', { name: 'Previous' })
).toBeVisible();
// Perform Delete Operation on Edit Operation on Entity
await performColumnSelectAndDeleteOperation(page);
});
await test.step('Perform Cell Delete Operation and Save', async () => {
await page.locator('.rdg-cell-name').first().click();
// Perform Delete Operation on Edit Operation on Entity
await performDeleteOperationOnEntity(page);
await page.getByRole('button', { name: 'Next' }).click();
await validateImportStatus(page, {
passed: '10',
processed: '10',
failed: '0',
});
const rowStatus = [
'Entity updated',
'Entity updated',
'Entity updated',
'Entity updated',
'Entity updated',
'Entity updated',
'Entity updated',
'Entity updated',
'Entity updated',
];
await expect(page.locator('.rdg-cell-details')).toHaveText(rowStatus);
const updateButtonResponse = page.waitForResponse(
`/api/v1/databases/name/*/importAsync?*dryRun=false&recursive=true*`
);
await page.getByRole('button', { name: 'Update' }).click();
await page
.locator('.inovua-react-toolkit-load-mask__background-layer')
.waitFor({ state: 'detached' });
await updateButtonResponse;
await page.waitForEvent('framenavigated');
await toastNotification(page, /details updated successfully/);
});
await test.step('should verify the removed value from entity', async () => {
await page.getByTestId('column-name').first().click();
await page.waitForLoadState('networkidle');
await expect(
page
.getByTestId('asset-description-container')
.getByText('No description')
).toBeVisible();
await expect(
page.getByTestId('tags-container').getByTestId('add-tag')
).toBeVisible();
await expect(
page.getByTestId('glossary-container').getByTestId('add-tag')
).toBeVisible();
await expect(page.getByTestId('Tier')).toContainText('No Tier');
await expect(page.getByTestId('certification-label')).toContainText(
'No Certification'
);
await expect(page.getByTestId('owner-label')).toContainText('No Owners');
await expect(page.getByTestId('no-domain-text')).toBeVisible();
});
await dbEntity.delete(apiContext);
await afterAction();
});
// Skip this test for now, since it is not working in AUT but working in local and CI
// <Mostly around the config since it is working in local and CI and not working in AUT>
test.skip('Range selection', async ({ page }) => {

View File

@ -580,9 +580,12 @@ export const fillRowDetails = async (
};
},
page: Page,
customPropertyRecord?: Record<string, string>
customPropertyRecord?: Record<string, string>,
isFirstCellClick?: boolean
) => {
await page.locator('.rdg-cell-name').last().click();
if (!isFirstCellClick) {
await page.locator('.rdg-cell-name').last().click();
}
const activeCell = page.locator(RDG_ACTIVE_CELL_SELECTOR);
const isActive = await activeCell.isVisible();
@ -960,3 +963,86 @@ export const firstTimeGridAddRowAction = async (page: Page) => {
await expect(lastRowFirstCell).toBeFocused();
};
export const performDeleteOperationOnEntity = async (page: Page) => {
await page.locator(RDG_ACTIVE_CELL_SELECTOR).press('ArrowRight');
// Description Remove
await page
.locator(RDG_ACTIVE_CELL_SELECTOR)
.press('ArrowRight', { delay: 100 });
await page.locator(RDG_ACTIVE_CELL_SELECTOR).press('Backspace');
// Owner Remove
await page
.locator(RDG_ACTIVE_CELL_SELECTOR)
.press('ArrowRight', { delay: 100 });
await page.locator(RDG_ACTIVE_CELL_SELECTOR).press('Backspace');
// Tag Remove
await page
.locator(RDG_ACTIVE_CELL_SELECTOR)
.press('ArrowRight', { delay: 100 });
await page.locator(RDG_ACTIVE_CELL_SELECTOR).press('Backspace');
// Glossary Remove
await page
.locator(RDG_ACTIVE_CELL_SELECTOR)
.press('ArrowRight', { delay: 100 });
await page.locator(RDG_ACTIVE_CELL_SELECTOR).press('Backspace');
// Tier Remove
await page
.locator(RDG_ACTIVE_CELL_SELECTOR)
.press('ArrowRight', { delay: 100 });
await page.locator(RDG_ACTIVE_CELL_SELECTOR).press('Delete');
// Certification Remove
await page
.locator(RDG_ACTIVE_CELL_SELECTOR)
.press('ArrowRight', { delay: 100 });
await page.locator(RDG_ACTIVE_CELL_SELECTOR).press('Delete');
// Retention Period Remove
await page
.locator(RDG_ACTIVE_CELL_SELECTOR)
.press('ArrowRight', { delay: 100 });
await page.locator(RDG_ACTIVE_CELL_SELECTOR).press('Delete');
// Source URL Remove
await page
.locator(RDG_ACTIVE_CELL_SELECTOR)
.press('ArrowRight', { delay: 100 });
await page.locator(RDG_ACTIVE_CELL_SELECTOR).press('Delete');
// Domains Remove
await page
.locator(RDG_ACTIVE_CELL_SELECTOR)
.press('ArrowRight', { delay: 100 });
await page.locator(RDG_ACTIVE_CELL_SELECTOR).press('Delete');
};
export const performColumnSelectAndDeleteOperation = async (page: Page) => {
const displayNameHeader = page.getByRole('columnheader', {
name: 'Display Name',
});
const firstRow = page.locator('.rdg-row').first();
const firstCell = firstRow.locator('.rdg-cell').nth(1);
await displayNameHeader.click();
await expect(firstCell).not.toBeFocused();
await expect(displayNameHeader).toBeFocused();
await expect(page.locator('.rdg-cell-range-selections')).toHaveCount(9);
await page.locator(RDG_ACTIVE_CELL_SELECTOR).press('Delete');
await expect(
page.getByRole('gridcell', { name: 'Playwright,Database', exact: true })
).not.toBeVisible(); // Display Name cell should be deleted
};

View File

@ -482,6 +482,136 @@ export function useGridEditController({
return;
}
// Handle Delete and Backspace keys to clear cell content
if (e.key === 'Delete' || e.key === 'Backspace') {
// Only handle if we're in a cell and not in a text input
const target = e.target as HTMLElement;
const isInInput =
target.tagName === 'INPUT' ||
target.tagName === 'TEXTAREA' ||
target.contentEditable === 'true';
// Check if we're in a custom editor (like tag selector, dropdown, etc.)
const isInCustomEditor =
target.closest('.async-select-list') ||
target.closest('.react-grid-select-dropdown') ||
target.closest('.ant-select') ||
target.closest('.ant-select-dropdown');
if (!isInInput && !isInCustomEditor) {
e.preventDefault();
e.stopPropagation();
// Check if we're in a column header (whole column selection)
const isInColumnHeader =
target.closest('.rdg-header-row') ||
target.getAttribute('role') === 'columnheader';
// Check if we're in a row header (whole row selection)
const isInRowHeader =
target.closest('.rdg-row-header') ||
target.getAttribute('role') === 'rowheader';
if (isInColumnHeader) {
// Handle whole column deletion
const colAttr = target.getAttribute('aria-colindex');
if (colAttr) {
const colIndex = parseInt(colAttr, 10) - 1;
const column = columns[colIndex];
if (column && column.editable) {
// Clear all cells in the column
const newRows = [...dataSource];
for (let row = 0; row < newRows.length; row++) {
newRows[row] = { ...newRows[row], [column.key]: '' };
}
setDataSource(newRows);
pushToUndoStack(dataSource);
}
}
} else if (isInRowHeader) {
// Handle whole row deletion
const rowAttr = target.getAttribute('aria-rowindex');
if (rowAttr) {
const rowIndex = parseInt(rowAttr, 10) - 2; // -2 for header row offset
if (rowIndex >= 0 && rowIndex < dataSource.length) {
// Clear all cells in the row
const newRows = [...dataSource];
const row = newRows[rowIndex];
// Clear all editable columns in the row
columns.forEach((column) => {
if (column.editable) {
row[column.key] = '';
}
});
setDataSource(newRows);
pushToUndoStack(dataSource);
}
}
} else if (selectedRange) {
// Handle range selection deletion
const { startRow, endRow, startCol, endCol } = selectedRange;
const newRows = [...dataSource];
for (let row = startRow; row <= endRow; row++) {
if (row < newRows.length) {
for (let col = startCol; col <= endCol; col++) {
const column = columns[col];
if (column && column.editable) {
newRows[row] = { ...newRows[row], [column.key]: '' };
}
}
}
}
setDataSource(newRows);
pushToUndoStack(dataSource);
} else {
// Handle single cell deletion
const cellIndices = getCellIndices(target);
if (cellIndices) {
const { row, col } = cellIndices;
const column = columns[col];
if (column && column.editable && row < dataSource.length) {
// Clear the cell content
const newRows = [...dataSource];
newRows[row] = { ...newRows[row], [column.key]: '' };
setDataSource(newRows);
pushToUndoStack(dataSource);
}
}
}
return;
}
// For custom editors, we need to handle the case where the event is stopped
// but we still want to clear the cell content
if (isInCustomEditor) {
// Get the cell that contains this custom editor
const cellElement = target.closest('.rdg-cell');
if (cellElement) {
const cellIndices = getCellIndices(cellElement);
if (cellIndices) {
const { row, col } = cellIndices;
const column = columns[col];
if (column && column.editable && row < dataSource.length) {
// Clear the cell content for custom editors
const newRows = [...dataSource];
newRows[row] = { ...newRows[row], [column.key]: '' };
setDataSource(newRows);
pushToUndoStack(dataSource);
}
}
}
}
}
// Shift+Arrow for range selection (Excel-like)
if (
selectedRange &&
@ -568,7 +698,16 @@ export function useGridEditController({
);
}
};
}, [undo, redo, gridContainer, dataSource, selectedRange, columns]);
}, [
undo,
redo,
gridContainer,
dataSource,
selectedRange,
columns,
setDataSource,
pushToUndoStack,
]);
useEffect(() => {
if (!gridContainer) {