🐛 fix(data-grid): 兼容表头排序与字段拖入查询区

This commit is contained in:
kunghim
2026-08-07 15:21:22 +08:00
parent f0ffef2080
commit 7cb276cc9f
4 changed files with 165 additions and 35 deletions

View File

@@ -19,10 +19,15 @@ import {
import {
SortableContext,
useSortable,
horizontalListSortingStrategy,
arrayMove
horizontalListSortingStrategy
} from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import {
DATA_GRID_COLUMN_ORDER_DRAG_MIME,
decodeDataGridColumnOrderDragPayload,
hasDataGridColumnOrderDragPayload,
moveDataGridColumnInVisibleOrder,
} from './dataGridColumnOrder';
import { ImportData, ExportDataWithOptions, ExportQueryWithOptions, ApplyChanges, PreviewChanges, DBGetColumns, DBGetIndexes, DBGetForeignKeys, DBShowCreateTable } from '../../wailsjs/go/app/App';
import ImportPreviewModal from './ImportPreviewModal';
import { useStore } from '../store';
@@ -576,42 +581,29 @@ const DataGrid: React.FC<DataGridProps> = ({
useSensor(PointerSensor, { activationConstraint: { distance: 8 } }),
);
const handleDragEnd = (event: DragEndEvent) => {
// 防御性检查:若正在调整列宽,忽略拖拽排序事件
if (isResizingRef.current) return;
const { active, over } = event;
if (active.id !== over?.id && over) {
const columnOrderDragScopeRef = useRef(generateUuid());
const reorderVisibleColumns = useCallback((sourceColumnName: string, targetColumnName: string) => {
setAllOrderedColumnNames((prevAllOrder) => {
// Calculate the new order of all columns by applying the movement
// We only move the visible columns relative to each other, but the easiest way
// is to map the visible column movement back to the full array.
const hiddenSet = new Set(localHiddenColumns);
const visibleOrder = prevAllOrder.filter(col => !hiddenSet.has(col));
const oldVisibleIndex = visibleOrder.indexOf(active.id as string);
const newVisibleIndex = visibleOrder.indexOf(over.id as string);
if (oldVisibleIndex === -1 || newVisibleIndex === -1) return prevAllOrder;
const nextVisibleOrder = arrayMove(visibleOrder, oldVisibleIndex, newVisibleIndex);
// Reconstruct allOrderedColumnNames by inserting hidden columns back to their original relative positions
// Or simpler: just keep hidden columns at the end, but that ruins user's layout.
// Better approach: build a new array
let vIndex = 0;
const nextOrder = prevAllOrder.map(col => {
if (hiddenSet.has(col)) {
return col; // Hidden columns stay at their absolute index in the master list
} else {
return nextVisibleOrder[vIndex++];
}
});
const nextOrder = moveDataGridColumnInVisibleOrder(
prevAllOrder,
new Set(localHiddenColumns),
sourceColumnName,
targetColumnName,
);
if (nextOrder === prevAllOrder) return prevAllOrder;
if (enableColumnOrderMemory && connectionId && dbName && tableName) {
setTableColumnOrder(connectionId, dbName, tableName, nextOrder);
}
return nextOrder;
});
}, [connectionId, dbName, enableColumnOrderMemory, localHiddenColumns, setTableColumnOrder, tableName]);
const handleDragEnd = (event: DragEndEvent) => {
// 防御性检查:若正在调整列宽,忽略拖拽排序事件
if (isResizingRef.current) return;
const { active, over } = event;
if (active.id !== over?.id && over) {
reorderVisibleColumns(String(active.id), String(over.id));
}
};
@@ -3110,11 +3102,28 @@ const DataGrid: React.FC<DataGridProps> = ({
},
onHeaderCell: (column: any) => ({
id: key,
columnOrderDragScope: columnOrderDragScopeRef.current,
width: column.width,
className: `gonavi-sortable-header-cell${showColumnComment || showColumnType ? '' : ' is-single-line-title'}`,
'data-i18n-language': language,
onResizeStart: handleResizeStart(key), // Only need start
onResizeAutoFit: handleResizeAutoFit(key),
onDragOver: (event: React.DragEvent<HTMLElement>) => {
if (!hasDataGridColumnOrderDragPayload(event.dataTransfer)) return;
event.preventDefault();
event.stopPropagation();
event.dataTransfer.dropEffect = 'move';
},
onDrop: (event: React.DragEvent<HTMLElement>) => {
if (!hasDataGridColumnOrderDragPayload(event.dataTransfer)) return;
const payload = decodeDataGridColumnOrderDragPayload(
event.dataTransfer.getData(DATA_GRID_COLUMN_ORDER_DRAG_MIME),
);
if (!payload || payload.scope !== columnOrderDragScopeRef.current) return;
event.preventDefault();
event.stopPropagation();
reorderVisibleColumns(payload.columnName, key);
},
onContextMenu: (event: React.MouseEvent<HTMLElement>) => {
if (!isV2Ui) return;
showColumnHeaderContextMenu(event, key);
@@ -3155,7 +3164,7 @@ const DataGrid: React.FC<DataGridProps> = ({
},
}),
}));
}, [canModifyData, cellEditMode, columnWidths, currentConnConfig, dataTableDensity, displayColumnNames, displayColumnTypeMap, effectiveEditLocator, enableVirtual, handleResizeAutoFit, handleResizeStart, isV2Ui, language, normalizedPageFindText, onSort, pinnedLeftColumnSet, renderColumnTitle, selectEditableColumnCells, showColumnComment, showColumnHeaderContextMenu, showColumnType, sortInfo]);
}, [canModifyData, cellEditMode, columnWidths, currentConnConfig, dataTableDensity, displayColumnNames, displayColumnTypeMap, effectiveEditLocator, enableVirtual, handleResizeAutoFit, handleResizeStart, isV2Ui, language, normalizedPageFindText, onSort, pinnedLeftColumnSet, renderColumnTitle, reorderVisibleColumns, selectEditableColumnCells, showColumnComment, showColumnHeaderContextMenu, showColumnType, sortInfo]);
const mergedColumns = useMemo(() => columns.map((col): ColumnType<any> => {
const dataIndex = String(col.dataIndex);

View File

@@ -77,6 +77,11 @@ import { DEFAULT_SHORTCUT_OPTIONS, getShortcutPlatform, resolveShortcutDisplay }
import { formatMongoValueForDisplay } from '../utils/mongodb';
import { SIDEBAR_SQL_EDITOR_DRAG_MIME, encodeSidebarSqlEditorDragPayload } from '../utils/sidebarSqlDrag';
import { SQL_FIELD_DRAG_MIME } from '../utils/sqlFieldDrop';
import {
DATA_GRID_COLUMN_ORDER_DRAG_MIME,
encodeDataGridColumnOrderDragPayload,
shouldBypassDndKitForNativeColumnHeaderDrag,
} from './dataGridColumnOrder';
import {
TEMPORAL_FORMATS,
formatFromDayjs,
@@ -784,6 +789,7 @@ const ResizableTitle = React.forwardRef<HTMLTableCellElement, any>((props, ref)
// --- Sortable Header Cell ---
interface SortableHeaderCellProps extends React.HTMLAttributes<HTMLTableCellElement> {
id?: string;
columnOrderDragScope?: string;
}
// --- Sortable Header Cell ---
@@ -817,7 +823,7 @@ const sortableHeaderStaticStyles = `
`;
const SortableHeaderCell: React.FC<SortableHeaderCellProps> = React.memo((props) => {
const { id, children, style: propStyle, className: propClassName, ...restProps } = props;
const { id, children, style: propStyle, className: propClassName, columnOrderDragScope, ...restProps } = props;
const [isPressed, setIsPressed] = useState(false);
const {
attributes,
@@ -875,6 +881,12 @@ const SortableHeaderCell: React.FC<SortableHeaderCellProps> = React.memo((props)
{...listeners}
onPointerDown={(e: any) => {
setIsPressed(true);
if (
(e.target as HTMLElement | null)?.closest?.('.sortable-header-cell-drag-handle')
&& shouldBypassDndKitForNativeColumnHeaderDrag(e.pointerType)
) {
return;
}
if (listeners?.onPointerDown) listeners.onPointerDown(e);
}}
>
@@ -887,13 +899,22 @@ const SortableHeaderCell: React.FC<SortableHeaderCellProps> = React.memo((props)
const columnName = String(id || '').trim();
if (!columnName || !event.dataTransfer) return;
event.stopPropagation();
event.dataTransfer.effectAllowed = 'copy';
event.dataTransfer.effectAllowed = 'copyMove';
const payload = encodeSidebarSqlEditorDragPayload({
text: columnName,
nodeType: 'column',
});
event.dataTransfer.setData(SIDEBAR_SQL_EDITOR_DRAG_MIME, payload);
event.dataTransfer.setData(SQL_FIELD_DRAG_MIME, columnName);
if (columnOrderDragScope) {
event.dataTransfer.setData(
DATA_GRID_COLUMN_ORDER_DRAG_MIME,
encodeDataGridColumnOrderDragPayload({
scope: columnOrderDragScope,
columnName,
}),
);
}
event.dataTransfer.setData('text/plain', columnName);
}}
>

View File

@@ -0,0 +1,43 @@
import { describe, expect, it } from 'vitest';
import {
DATA_GRID_COLUMN_ORDER_DRAG_MIME,
decodeDataGridColumnOrderDragPayload,
encodeDataGridColumnOrderDragPayload,
hasDataGridColumnOrderDragPayload,
moveDataGridColumnInVisibleOrder,
shouldBypassDndKitForNativeColumnHeaderDrag,
} from './dataGridColumnOrder';
describe('dataGridColumnOrder helpers', () => {
it('reorders a dragged visible column at the header drop target while hidden columns keep their slot', () => {
expect(moveDataGridColumnInVisibleOrder(
['id', 'hidden_note', 'name', 'code'],
new Set(['hidden_note']),
'code',
'id',
)).toEqual(['code', 'hidden_note', 'id', 'name']);
});
it('keeps the column reorder payload scoped to its source result set', () => {
const payload = encodeDataGridColumnOrderDragPayload({
scope: 'result-set-1',
columnName: 'title',
});
expect(decodeDataGridColumnOrderDragPayload(payload)).toEqual({
scope: 'result-set-1',
columnName: 'title',
});
expect(hasDataGridColumnOrderDragPayload({
types: [DATA_GRID_COLUMN_ORDER_DRAG_MIME],
})).toBe(true);
expect(decodeDataGridColumnOrderDragPayload('{')).toBeNull();
});
it('keeps touch and pen pointer input on dnd-kit instead of native HTML drag', () => {
expect(shouldBypassDndKitForNativeColumnHeaderDrag('mouse')).toBe(true);
expect(shouldBypassDndKitForNativeColumnHeaderDrag('touch')).toBe(false);
expect(shouldBypassDndKitForNativeColumnHeaderDrag('pen')).toBe(false);
});
});

View File

@@ -0,0 +1,57 @@
import { arrayMove } from '@dnd-kit/sortable';
export const DATA_GRID_COLUMN_ORDER_DRAG_MIME = 'application/x-gonavi-data-grid-column-order';
export type DataGridColumnOrderDragPayload = {
scope: string;
columnName: string;
};
export const encodeDataGridColumnOrderDragPayload = (
payload: DataGridColumnOrderDragPayload,
): string => JSON.stringify(payload);
export const decodeDataGridColumnOrderDragPayload = (
rawPayload: string,
): DataGridColumnOrderDragPayload | null => {
try {
const parsed = JSON.parse(String(rawPayload || ''));
const scope = String(parsed?.scope || '').trim();
const columnName = String(parsed?.columnName || '').trim();
return scope && columnName ? { scope, columnName } : null;
} catch {
return null;
}
};
export const hasDataGridColumnOrderDragPayload = (
dataTransfer: Pick<DataTransfer, 'types'> | null | undefined,
): boolean => Array.from(dataTransfer?.types || [])
.some((type) => String(type || '').toLowerCase() === DATA_GRID_COLUMN_ORDER_DRAG_MIME);
// Native HTML drag handles mouse reliably; touch and pen need dnd-kit's PointerSensor.
export const shouldBypassDndKitForNativeColumnHeaderDrag = (pointerType: string): boolean => (
pointerType === 'mouse'
);
export const moveDataGridColumnInVisibleOrder = (
allColumnNames: string[],
hiddenColumnNames: ReadonlySet<string>,
sourceColumnName: string,
targetColumnName: string,
): string[] => {
const source = String(sourceColumnName || '').trim();
const target = String(targetColumnName || '').trim();
if (!source || !target || source === target) return allColumnNames;
const visibleColumnNames = allColumnNames.filter((columnName) => !hiddenColumnNames.has(columnName));
const sourceIndex = visibleColumnNames.indexOf(source);
const targetIndex = visibleColumnNames.indexOf(target);
if (sourceIndex < 0 || targetIndex < 0) return allColumnNames;
const nextVisibleColumnNames = arrayMove(visibleColumnNames, sourceIndex, targetIndex);
let visibleIndex = 0;
return allColumnNames.map((columnName) => (
hiddenColumnNames.has(columnName) ? columnName : nextVisibleColumnNames[visibleIndex++]
));
};