diff --git a/packages/frontend/editor-ui/src/features/dataStore/DataStoreDetailsView.vue b/packages/frontend/editor-ui/src/features/dataStore/DataStoreDetailsView.vue index 4c2140fe3e..9c14d1af6f 100644 --- a/packages/frontend/editor-ui/src/features/dataStore/DataStoreDetailsView.vue +++ b/packages/frontend/editor-ui/src/features/dataStore/DataStoreDetailsView.vue @@ -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 () => { diff --git a/packages/frontend/editor-ui/src/features/dataStore/components/dataGrid/AddColumnButton.test.ts b/packages/frontend/editor-ui/src/features/dataStore/components/dataGrid/AddColumnButton.test.ts index 59fcedeecd..6f19322b29 100644 --- a/packages/frontend/editor-ui/src/features/dataStore/components/dataGrid/AddColumnButton.test.ts +++ b/packages/frontend/editor-ui/src/features/dataStore/components/dataGrid/AddColumnButton.test.ts @@ -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'); diff --git a/packages/frontend/editor-ui/src/features/dataStore/components/dataGrid/AddColumnButton.vue b/packages/frontend/editor-ui/src/features/dataStore/components/dataGrid/AddColumnButton.vue index 53fd4531e3..a060a37705 100644 --- a/packages/frontend/editor-ui/src/features/dataStore/components/dataGrid/AddColumnButton.vue +++ b/packages/frontend/editor-ui/src/features/dataStore/components/dataGrid/AddColumnButton.vue @@ -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; }; 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; diff --git a/packages/frontend/editor-ui/src/features/dataStore/components/dataGrid/DataStoreTable.test.ts b/packages/frontend/editor-ui/src/features/dataStore/components/dataGrid/DataStoreTable.test.ts index 195c52bd9b..6bf4827c68 100644 --- a/packages/frontend/editor-ui/src/features/dataStore/components/dataGrid/DataStoreTable.test.ts +++ b/packages/frontend/editor-ui/src/features/dataStore/components/dataGrid/DataStoreTable.test.ts @@ -20,6 +20,7 @@ vi.mock('ag-grid-vue3', () => ({ api: { refreshHeader: vi.fn(), applyTransaction: vi.fn(), + setGridOption: vi.fn(), }, }); }, diff --git a/packages/frontend/editor-ui/src/features/dataStore/components/dataGrid/DataStoreTable.vue b/packages/frontend/editor-ui/src/features/dataStore/components/dataGrid/DataStoreTable.vue index 3aa6a3e06e..ad5407b432 100644 --- a/packages/frontend/editor-ui/src/features/dataStore/components/dataGrid/DataStoreTable.vue +++ b/packages/frontend/editor-ui/src/features/dataStore/components/dataGrid/DataStoreTable.vue @@ -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(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('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 = {}) // 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) => ({ 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) => { - 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) => { + 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 {