{ if (dragIndex.current != null) e.preventDefault(); },
+ onDrop: (e: React.DragEvent) => {
+ e.preventDefault();
+ if (dragIndex.current != null) { moveRow(dragIndex.current, rowIdx); dragIndex.current = null; }
+ },
+ }
+ : {})}
+ >
{showLineNumbers && (
- |
- {rowIdx + 1}
+ |
+
+ {reorderable && (
+ { dragIndex.current = rowIdx; }}
+ onDragEnd={() => { dragIndex.current = null; }}
+ className="cursor-grab text-muted-foreground/40 opacity-0 transition-opacity group-hover:opacity-100"
+ title="Drag to reorder"
+ aria-label="Drag to reorder"
+ data-testid={`line-items-drag-${rowIdx}`}
+ >
+
+
+ )}
+ {rowIdx + 1}
+
|
)}
- {columns.map((c, colIdx) => (
-
- {renderCellInput(c, colIdx, rowIdx, row)}
- |
- ))}
+ {columns.map((c, colIdx) => {
+ // Inline validation: a required, non-computed cell that's
+ // empty on a real (non-ghost) row flags red in place.
+ const invalid = !isGhost && !isList && !!c.required && !c.computed && (row[c.field] == null || row[c.field] === '');
+ return (
+
+ {renderCellInput(c, colIdx, rowIdx, row)}
+ |
+ );
+ })}
{hasRowActions && (
-
- {!isGhost && showExpand && (
-
- )}
- {!isGhost && allowDelete && (
-
- )}
+ |
+
+ {!isGhost && showExpand && (
+
+ )}
+ {!isGhost && allowDuplicate && (
+
+ )}
+ {!isGhost && allowDelete && (
+
+ )}
+
|
)}