fix(editor): Data tables UI fixes (no-changelog) (#18966)

This commit is contained in:
Svetoslav Dekov
2025-08-30 00:21:29 +03:00
committed by GitHub
parent d64a94753f
commit 33be183a9e
5 changed files with 93 additions and 18 deletions

View File

@@ -82,8 +82,11 @@ const onToggleSave = (value: boolean) => {
} }
}; };
const onAddColumn = (column: DataStoreColumnCreatePayload) => { const onAddColumn = async (column: DataStoreColumnCreatePayload) => {
dataStoreTableRef.value?.addColumn(column); if (!dataStoreTableRef.value) {
return false;
}
return await dataStoreTableRef.value.addColumn(column);
}; };
onMounted(async () => { onMounted(async () => {

View File

@@ -43,7 +43,7 @@ vi.mock('@n8n/i18n', async (importOriginal) => ({
})); }));
describe('AddColumnButton', () => { describe('AddColumnButton', () => {
const addColumnHandler = vi.fn(); const addColumnHandler = vi.fn().mockResolvedValue(true);
const renderComponent = createComponentRenderer(AddColumnButton, { const renderComponent = createComponentRenderer(AddColumnButton, {
props: { props: {
params: { params: {
@@ -244,6 +244,24 @@ describe('AddColumnButton', () => {
}); });
}); });
it('should not close popover if submission fails', async () => {
const { getByPlaceholderText, getByTestId } = renderComponent();
addColumnHandler.mockResolvedValueOnce(false);
const addButton = getByTestId('data-store-add-column-trigger-button');
await fireEvent.click(addButton);
const nameInput = getByPlaceholderText('Enter column name');
await fireEvent.update(nameInput, 'testColumn');
const submitButton = getByTestId('data-store-add-column-submit-button');
await fireEvent.click(submitButton);
await waitFor(() => {
expect(getByTestId('data-store-add-column-submit-button')).toBeInTheDocument();
});
});
it('should allow submission with Enter key', async () => { it('should allow submission with Enter key', async () => {
const { getByTestId, getByPlaceholderText } = renderComponent(); const { getByTestId, getByPlaceholderText } = renderComponent();
const addButton = getByTestId('data-store-add-column-trigger-button'); const addButton = getByTestId('data-store-add-column-trigger-button');

View File

@@ -13,7 +13,7 @@ import { useDebounce } from '@/composables/useDebounce';
const props = defineProps<{ const props = defineProps<{
// the params key is needed so that we can pass this directly to ag-grid as column // the params key is needed so that we can pass this directly to ag-grid as column
params: { params: {
onAddColumn: (column: DataStoreColumnCreatePayload) => void; onAddColumn: (column: DataStoreColumnCreatePayload) => Promise<boolean>;
}; };
popoverId?: string; popoverId?: string;
useTextTrigger?: boolean; useTextTrigger?: boolean;
@@ -38,11 +38,18 @@ const isSelectOpen = ref(false);
const popoverId = computed(() => props.popoverId ?? 'add-column-popover'); const popoverId = computed(() => props.popoverId ?? 'add-column-popover');
const onAddButtonClicked = () => { const onAddButtonClicked = async () => {
if (!columnName.value || !columnType.value) { validateName();
if (!columnName.value || !columnType.value || error.value) {
return;
}
const success = await props.params.onAddColumn({
name: columnName.value,
type: columnType.value,
});
if (!success) {
return; return;
} }
props.params.onAddColumn({ name: columnName.value, type: columnType.value });
columnName.value = ''; columnName.value = '';
columnType.value = 'string'; columnType.value = 'string';
popoverOpen.value = false; popoverOpen.value = false;

View File

@@ -20,6 +20,7 @@ vi.mock('ag-grid-vue3', () => ({
api: { api: {
refreshHeader: vi.fn(), refreshHeader: vi.fn(),
applyTransaction: vi.fn(), applyTransaction: vi.fn(),
setGridOption: vi.fn(),
}, },
}); });
}, },

View File

@@ -105,7 +105,7 @@ const { mapToAGCellType } = useDataStoreTypes();
const dataStoreStore = useDataStoreStore(); const dataStoreStore = useDataStoreStore();
useClipboard({ onPaste: onClipboardPaste }); const { copy: copyToClipboard } = useClipboard({ onPaste: onClipboardPaste });
// AG Grid State // AG Grid State
const gridApi = ref<GridApi | null>(null); const gridApi = ref<GridApi | null>(null);
@@ -125,7 +125,7 @@ const contentLoading = ref(false);
const lastFocusedCell = ref<{ rowIndex: number; colId: string } | null>(null); const lastFocusedCell = ref<{ rowIndex: number; colId: string } | null>(null);
const isTextEditorOpen = ref(false); const isTextEditorOpen = ref(false);
const gridContainer = useTemplateRef('gridContainer'); const gridContainer = useTemplateRef<HTMLDivElement>('gridContainer');
// Pagination // Pagination
const pageSizeOptions = [10, 20, 50]; const pageSizeOptions = [10, 20, 50];
@@ -141,6 +141,11 @@ const selectedCount = computed(() => selectedRowIds.value.size);
const onGridReady = (params: GridReadyEvent) => { const onGridReady = (params: GridReadyEvent) => {
gridApi.value = params.api; gridApi.value = params.api;
// Ensure popups (e.g., agLargeTextCellEditor) are positioned relative to the grid container
// to avoid misalignment when the page scrolls.
if (gridContainer?.value) {
params.api.setGridOption('popupParent', gridContainer.value as unknown as HTMLElement);
}
}; };
const refreshGridData = () => { const refreshGridData = () => {
@@ -241,8 +246,10 @@ const onAddColumn = async (column: DataStoreColumnCreatePayload) => {
return { ...row, [newColumn.name]: null }; return { ...row, [newColumn.name]: null };
}); });
refreshGridData(); refreshGridData();
return true;
} catch (error) { } catch (error) {
toast.showError(error, i18n.baseText('dataStore.addColumn.error')); toast.showError(error, i18n.baseText('dataStore.addColumn.error'));
return false;
} }
}; };
@@ -307,6 +314,9 @@ const createColumnDef = (col: DataStoreColumn, extraProps: Partial<ColDef> = {})
// Enable large text editor for text columns // Enable large text editor for text columns
if (col.type === 'string') { if (col.type === 'string') {
columnDef.cellEditor = 'agLargeTextCellEditor'; columnDef.cellEditor = 'agLargeTextCellEditor';
// Use popup editor so it is not clipped by the grid viewport and positions correctly
columnDef.cellEditorPopup = true;
columnDef.cellEditorPopupPosition = 'over';
// Provide initial value for the editor, otherwise agLargeTextCellEditor breaks // Provide initial value for the editor, otherwise agLargeTextCellEditor breaks
columnDef.cellEditorParams = (params: CellEditRequestEvent<DataStoreRow>) => ({ columnDef.cellEditorParams = (params: CellEditRequestEvent<DataStoreRow>) => ({
value: params.value ?? '', value: params.value ?? '',
@@ -596,8 +606,20 @@ function onClipboardPaste(data: string) {
const colDef = focusedCell.column.getColDef(); const colDef = focusedCell.column.getColDef();
if (colDef.cellDataType === 'text') { if (colDef.cellDataType === 'text') {
row.setDataValue(focusedCell.column.getColId(), data); row.setDataValue(focusedCell.column.getColId(), data);
} else if (!Number.isNaN(Number(data))) { } else if (colDef.cellDataType === 'number') {
row.setDataValue(focusedCell.column.getColId(), Number(data)); if (!Number.isNaN(Number(data))) {
row.setDataValue(focusedCell.column.getColId(), Number(data));
}
} else if (colDef.cellDataType === 'date') {
if (!Number.isNaN(Date.parse(data))) {
row.setDataValue(focusedCell.column.getColId(), new Date(data));
}
} else if (colDef.cellDataType === 'boolean') {
if (data === 'true') {
row.setDataValue(focusedCell.column.getColId(), true);
} else if (data === 'false') {
row.setDataValue(focusedCell.column.getColId(), false);
}
} }
} }
@@ -644,16 +666,38 @@ const onSelectionChanged = () => {
}; };
const onCellKeyDown = async (params: CellKeyDownEvent<DataStoreRow>) => { const onCellKeyDown = async (params: CellKeyDownEvent<DataStoreRow>) => {
const key = (params.event as KeyboardEvent).key; if (params.api.getEditingCells().length > 0) {
if (key !== 'Delete' && key !== 'Backspace') return; return;
}
const isEditing = params.api.getEditingCells().length > 0; const event = params.event as KeyboardEvent;
if (isEditing || selectedRowIds.value.size === 0) return; if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 'c') {
event.preventDefault();
await handleCopyFocusedCell(params);
return;
}
params.event?.preventDefault(); if ((event.key !== 'Delete' && event.key !== 'Backspace') || selectedRowIds.value.size === 0) {
return;
}
event.preventDefault();
await handleDeleteSelected(); await handleDeleteSelected();
}; };
const handleCopyFocusedCell = async (params: CellKeyDownEvent<DataStoreRow>) => {
const focused = params.api.getFocusedCell();
if (!focused) {
return;
}
const row = params.api.getDisplayedRowAtIndex(focused.rowIndex);
const colDef = focused.column.getColDef();
if (row?.data && colDef.field) {
const rawValue = row.data[colDef.field];
const text = rawValue === null || rawValue === undefined ? '' : String(rawValue);
await copyToClipboard(text);
}
};
const handleDeleteSelected = async () => { const handleDeleteSelected = async () => {
if (selectedRowIds.value.size === 0) return; if (selectedRowIds.value.size === 0) return;
@@ -683,8 +727,9 @@ const handleDeleteSelected = async () => {
await fetchDataStoreContent(); await fetchDataStoreContent();
toast.showMessage({ toast.showToast({
title: i18n.baseText('dataStore.deleteRows.success'), title: i18n.baseText('dataStore.deleteRows.success'),
message: '',
type: 'success', type: 'success',
}); });
} catch (error) { } catch (error) {
@@ -858,7 +903,8 @@ defineExpose({
} }
:global(.ag-large-text-input) { :global(.ag-large-text-input) {
position: fixed; position: absolute;
min-width: 420px;
padding: 0; padding: 0;
textarea { textarea {