feat: drag-to-check rows in PricingView (Excel-style)

Dragging the mouse over rows now directly toggles Check Ying on all
covered rows on mouseup. The check value (on/off) is determined by
the state of the first row touched, so dragging over unchecked rows
marks them and vice versa. Rows light up blue during the drag to
show what will be affected.

Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
Christian Vidal Wolf
2026-04-28 09:26:48 +02:00
co-authored by claude-flow
parent d1074f78de
commit 8e1a533ea2
+28 -29
View File
@@ -115,13 +115,15 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
const [pinnedColumns, setPinnedColumns] = usePersistentState<Set<string>>('pricing-pinnedColumns', new Set(['articleNo', 'articleName']));
const [showPinPanel, setShowPinPanel] = useState(false);
// Drag-to-select state
const [selectedRows, setSelectedRows] = useState<Set<number>>(new Set());
// Drag-to-check state (Excel-style drag to toggle Check Ying)
const [dragSelectedRows, setDragSelectedRows] = useState<Set<number>>(new Set());
const isDraggingRef = useRef(false);
const dragStartIndexRef = useRef<number | null>(null);
const dragModeRef = useRef<'add' | 'remove'>('add');
const dragCheckValueRef = useRef<boolean>(true); // what value to set on mouseup
const lastDragIndexRef = useRef<number | null>(null);
const visibleDataIndexesRef = useRef<number[]>([]);
const dataRef = useRef(data);
dataRef.current = data;
const handleRowMouseDown = useCallback((e: React.MouseEvent, dataIndex: number) => {
if ((e.target as HTMLElement).closest('input, button, a')) return;
@@ -129,20 +131,10 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
isDraggingRef.current = true;
dragStartIndexRef.current = dataIndex;
lastDragIndexRef.current = dataIndex;
if (e.ctrlKey || e.metaKey) {
dragModeRef.current = selectedRows.has(dataIndex) ? 'remove' : 'add';
setSelectedRows(prev => {
const next = new Set(prev);
if (next.has(dataIndex)) next.delete(dataIndex); else next.add(dataIndex);
return next;
});
return;
}
dragModeRef.current = selectedRows.has(dataIndex) ? 'remove' : 'add';
setSelectedRows(new Set([dataIndex]));
}, [selectedRows]);
// Toggle based on current state of the first row touched
dragCheckValueRef.current = !dataRef.current[dataIndex]?.[COLUMNS.VALIDATED_CHECK];
setDragSelectedRows(new Set([dataIndex]));
}, [COLUMNS.VALIDATED_CHECK]);
const handleRowMouseEnter = useCallback((dataIndex: number) => {
if (!isDraggingRef.current || dragStartIndexRef.current === null) return;
@@ -153,23 +145,30 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
const end = dataIndex;
const [lo, hi] = start <= end ? [start, end] : [end, start];
setSelectedRows(prev => {
const next = new Set(prev);
setDragSelectedRows(() => {
const next = new Set<number>();
visibleDataIndexesRef.current.forEach((idx: number) => {
if (idx >= lo && idx <= hi) {
if (dragModeRef.current === 'add') next.add(idx);
else next.delete(idx);
}
if (idx >= lo && idx <= hi) next.add(idx);
});
return next;
});
}, []);
useEffect(() => {
const handleMouseUp = () => { isDraggingRef.current = false; };
const handleMouseUp = async () => {
if (!isDraggingRef.current) return;
isDraggingRef.current = false;
const rows = new Set(dragSelectedRows);
if (rows.size === 0) return;
setDragSelectedRows(new Set());
const targetValue = dragCheckValueRef.current;
for (const idx of Array.from(rows)) {
await handleToggleCheck(idx as number, targetValue);
}
};
document.addEventListener('mouseup', handleMouseUp);
return () => document.removeEventListener('mouseup', handleMouseUp);
}, []);
}, [dragSelectedRows]);
// Close search dropdown on click outside
useEffect(() => {
@@ -1900,7 +1899,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
const isValidated = !!row[COLUMNS.VALIDATED_CHECK];
const note = String(row[COLUMNS.VALIDATED_NOTE] || '');
const isRowSelected = selectedRows.has(dataIndex);
const isRowSelected = dragSelectedRows.has(dataIndex);
return (
<tr
@@ -2233,11 +2232,11 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
<> · Click any <span className="text-blue-400">price cell</span> to edit inline</>
)}
</p>
{selectedRows.size > 0 && (
{dragSelectedRows.size > 0 && (
<span className="flex items-center gap-1.5 text-xs text-blue-400 bg-blue-500/10 border border-blue-500/20 px-2 py-0.5 rounded mb-1">
{selectedRows.size} selected
{dragSelectedRows.size} selected
<button
onClick={() => setSelectedRows(new Set())}
onClick={() => setDragSelectedRows(new Set())}
className="ml-1 text-blue-300 hover:text-white transition-colors"
title="Clear selection"
>