mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-08-11 01:03:51 +08:00
🐛 fix(data-grid): 兼容表头排序与字段拖入查询区
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}}
|
||||
>
|
||||
|
||||
43
frontend/src/components/dataGridColumnOrder.test.ts
Normal file
43
frontend/src/components/dataGridColumnOrder.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
57
frontend/src/components/dataGridColumnOrder.ts
Normal file
57
frontend/src/components/dataGridColumnOrder.ts
Normal 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++]
|
||||
));
|
||||
};
|
||||
Reference in New Issue
Block a user