mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +00:00
fix(editor): Data tables UI fixes (no-changelog) (#18966)
This commit is contained in:
@@ -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 () => {
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user