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) => {
dataStoreTableRef.value?.addColumn(column);
const onAddColumn = async (column: DataStoreColumnCreatePayload) => {
if (!dataStoreTableRef.value) {
return false;
}
return await dataStoreTableRef.value.addColumn(column);
};
onMounted(async () => {

View File

@@ -43,7 +43,7 @@ vi.mock('@n8n/i18n', async (importOriginal) => ({
}));
describe('AddColumnButton', () => {
const addColumnHandler = vi.fn();
const addColumnHandler = vi.fn().mockResolvedValue(true);
const renderComponent = createComponentRenderer(AddColumnButton, {
props: {
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 () => {
const { getByTestId, getByPlaceholderText } = renderComponent();
const addButton = getByTestId('data-store-add-column-trigger-button');

View File

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

View File

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

View File

@@ -105,7 +105,7 @@ const { mapToAGCellType } = useDataStoreTypes();
const dataStoreStore = useDataStoreStore();
useClipboard({ onPaste: onClipboardPaste });
const { copy: copyToClipboard } = useClipboard({ onPaste: onClipboardPaste });
// AG Grid State
const gridApi = ref<GridApi | null>(null);
@@ -125,7 +125,7 @@ const contentLoading = ref(false);
const lastFocusedCell = ref<{ rowIndex: number; colId: string } | null>(null);
const isTextEditorOpen = ref(false);
const gridContainer = useTemplateRef('gridContainer');
const gridContainer = useTemplateRef<HTMLDivElement>('gridContainer');
// Pagination
const pageSizeOptions = [10, 20, 50];
@@ -141,6 +141,11 @@ const selectedCount = computed(() => selectedRowIds.value.size);
const onGridReady = (params: GridReadyEvent) => {
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 = () => {
@@ -241,8 +246,10 @@ const onAddColumn = async (column: DataStoreColumnCreatePayload) => {
return { ...row, [newColumn.name]: null };
});
refreshGridData();
return true;
} catch (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
if (col.type === 'string') {
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
columnDef.cellEditorParams = (params: CellEditRequestEvent<DataStoreRow>) => ({
value: params.value ?? '',
@@ -596,8 +606,20 @@ function onClipboardPaste(data: string) {
const colDef = focusedCell.column.getColDef();
if (colDef.cellDataType === 'text') {
row.setDataValue(focusedCell.column.getColId(), data);
} else if (!Number.isNaN(Number(data))) {
row.setDataValue(focusedCell.column.getColId(), Number(data));
} else if (colDef.cellDataType === 'number') {
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 key = (params.event as KeyboardEvent).key;
if (key !== 'Delete' && key !== 'Backspace') return;
if (params.api.getEditingCells().length > 0) {
return;
}
const isEditing = params.api.getEditingCells().length > 0;
if (isEditing || selectedRowIds.value.size === 0) return;
const event = params.event as KeyboardEvent;
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();
};
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 () => {
if (selectedRowIds.value.size === 0) return;
@@ -683,8 +727,9 @@ const handleDeleteSelected = async () => {
await fetchDataStoreContent();
toast.showMessage({
toast.showToast({
title: i18n.baseText('dataStore.deleteRows.success'),
message: '',
type: 'success',
});
} catch (error) {
@@ -858,7 +903,8 @@ defineExpose({
}
:global(.ag-large-text-input) {
position: fixed;
position: absolute;
min-width: 420px;
padding: 0;
textarea {