mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-10-01 11:52:12 +00:00
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:
parent
9811bf09a0
commit
eb1b3df7c5
@ -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 }) => {
|
||||
|
@ -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
|
||||
};
|
||||
|
@ -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) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user