Merge pull request #756 from mauriceboe/fix/planner-drag-drop-jank

fix(planner): eliminate drag-and-drop jank in trip planner
This commit is contained in:
Julien G.
2026-04-20 17:23:06 +02:00
committed by GitHub
2 changed files with 14 additions and 13 deletions
+3 -1
View File
@@ -58,4 +58,6 @@ coverage
*.tgz *.tgz
.scannerwork .scannerwork
test-data test-data
.run
@@ -336,6 +336,10 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar({
return () => document.removeEventListener('dragend', cleanup) return () => document.removeEventListener('dragend', cleanup)
}, []) }, [])
// Initialize missing transport positions outside of render to avoid setState-during-render
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(() => { days.forEach(day => initTransportPositions(day.id)) }, [days, reservations])
const toggleDay = (dayId, e) => { const toggleDay = (dayId, e) => {
e.stopPropagation() e.stopPropagation()
setExpandedDays(prev => { setExpandedDays(prev => {
@@ -490,11 +494,6 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar({
const dn = (dayNotes[String(dayId)] || []).slice().sort((a, b) => a.sort_order - b.sort_order) const dn = (dayNotes[String(dayId)] || []).slice().sort((a, b) => a.sort_order - b.sort_order)
const transport = getTransportForDay(dayId) const transport = getTransportForDay(dayId)
// Initialize positions for transports that don't have one yet
if (transport.some(r => r.day_plan_position == null)) {
initTransportPositions(dayId)
}
// All places keep their order_index — untimed can be freely moved, timed auto-sort when time is set // All places keep their order_index — untimed can be freely moved, timed auto-sort when time is set
const baseItems = [ const baseItems = [
...da.map(a => ({ type: 'place' as const, sortKey: a.order_index, data: a })), ...da.map(a => ({ type: 'place' as const, sortKey: a.order_index, data: a })),
@@ -1117,7 +1116,7 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar({
</div> </div>
{/* Tagesliste */} {/* Tagesliste */}
<div className="scroll-container trek-stagger" style={{ flex: 1, overflowY: 'auto', minHeight: 0 }}> <div className={`scroll-container${draggingId ? '' : ' trek-stagger'}`} style={{ flex: 1, overflowY: 'auto', minHeight: 0 }}>
{days.map((day, index) => { {days.map((day, index) => {
const isSelected = selectedDayId === day.id const isSelected = selectedDayId === day.id
const isExpanded = expandedDays.has(day.id) const isExpanded = expandedDays.has(day.id)
@@ -1135,7 +1134,7 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar({
{/* Tages-Header — akzeptiert Drops aus der PlacesSidebar */} {/* Tages-Header — akzeptiert Drops aus der PlacesSidebar */}
<div <div
onClick={() => { onSelectDay(day.id); if (onDayDetail) onDayDetail(day) }} onClick={() => { onSelectDay(day.id); if (onDayDetail) onDayDetail(day) }}
onDragOver={e => { e.preventDefault(); setDragOverDayId(day.id) }} onDragOver={e => { e.preventDefault(); if (dragOverDayId !== day.id) setDragOverDayId(day.id) }}
onDragLeave={e => { if (!e.currentTarget.contains(e.relatedTarget)) setDragOverDayId(null) }} onDragLeave={e => { if (!e.currentTarget.contains(e.relatedTarget)) setDragOverDayId(null) }}
onDrop={e => handleDropOnDay(e, day.id)} onDrop={e => handleDropOnDay(e, day.id)}
style={{ style={{
@@ -1349,7 +1348,7 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar({
> >
{merged.length === 0 && !dayNoteUi ? ( {merged.length === 0 && !dayNoteUi ? (
<div <div
onDragOver={e => { e.preventDefault(); setDragOverDayId(day.id) }} onDragOver={e => { e.preventDefault(); if (dragOverDayId !== day.id) setDragOverDayId(day.id) }}
onDrop={e => handleDropOnDay(e, day.id)} onDrop={e => handleDropOnDay(e, day.id)}
style={{ padding: '16px', textAlign: 'center', borderRadius: 8, style={{ padding: '16px', textAlign: 'center', borderRadius: 8,
background: dragOverDayId === day.id ? 'rgba(17,24,39,0.05)' : 'transparent', background: dragOverDayId === day.id ? 'rgba(17,24,39,0.05)' : 'transparent',
@@ -1409,7 +1408,6 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar({
return ( return (
<React.Fragment key={`place-${assignment.id}`}> <React.Fragment key={`place-${assignment.id}`}>
{showDropLine && <div style={{ height: 2, background: 'var(--text-primary)', borderRadius: 1, margin: '2px 8px' }} />}
<div <div
draggable={canEditDays} draggable={canEditDays}
onDragStart={e => { onDragStart={e => {
@@ -1499,6 +1497,7 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar({
borderLeft: lockedIds.has(assignment.id) borderLeft: lockedIds.has(assignment.id)
? '3px solid #dc2626' ? '3px solid #dc2626'
: '3px solid transparent', : '3px solid transparent',
borderTop: showDropLine ? '2px solid var(--text-primary)' : undefined,
transition: 'background 0.15s, border-color 0.15s', transition: 'background 0.15s, border-color 0.15s',
opacity: isDraggingThis ? 0.4 : 1, opacity: isDraggingThis ? 0.4 : 1,
}} }}
@@ -1722,7 +1721,6 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar({
return ( return (
<React.Fragment key={`transport-${res.id}-${day.id}`}> <React.Fragment key={`transport-${res.id}-${day.id}`}>
{showDropLine && <div style={{ height: 2, background: 'var(--text-primary)', borderRadius: 1, margin: '2px 8px' }} />}
<div <div
onClick={() => canEditDays && onEditTransport?.(res)} onClick={() => canEditDays && onEditTransport?.(res)}
onDragOver={e => { onDragOver={e => {
@@ -1771,6 +1769,8 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar({
margin: '1px 8px', margin: '1px 8px',
borderRadius: 6, borderRadius: 6,
border: `1px solid ${color}33`, border: `1px solid ${color}33`,
borderTop: showDropLine ? '2px solid var(--text-primary)' : undefined,
borderBottom: showDropLineAfter ? '2px solid var(--text-primary)' : undefined,
background: `${color}08`, background: `${color}08`,
cursor: canEditDays && onEditTransport ? 'pointer' : 'default', userSelect: 'none', cursor: canEditDays && onEditTransport ? 'pointer' : 'default', userSelect: 'none',
transition: 'background 0.1s', transition: 'background 0.1s',
@@ -1844,7 +1844,6 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar({
) )
})()} })()}
</div> </div>
{showDropLineAfter && <div style={{ height: 2, background: 'var(--text-primary)', borderRadius: 1, margin: '2px 8px' }} />}
</React.Fragment> </React.Fragment>
) )
} }
@@ -1855,7 +1854,6 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar({
const noteIdx = idx const noteIdx = idx
return ( return (
<React.Fragment key={`note-${note.id}`}> <React.Fragment key={`note-${note.id}`}>
{showDropLine && <div style={{ height: 2, background: 'var(--text-primary)', borderRadius: 1, margin: '2px 8px' }} />}
<div <div
draggable={canEditDays} draggable={canEditDays}
onDragStart={e => { if (!canEditDays) { e.preventDefault(); return } e.dataTransfer.setData('noteId', String(note.id)); e.dataTransfer.setData('fromDayId', String(day.id)); e.dataTransfer.effectAllowed = 'move'; dragDataRef.current = { noteId: String(note.id), fromDayId: String(day.id) }; setDraggingId(`note-${note.id}`) }} onDragStart={e => { if (!canEditDays) { e.preventDefault(); return } e.dataTransfer.setData('noteId', String(note.id)); e.dataTransfer.setData('fromDayId', String(day.id)); e.dataTransfer.effectAllowed = 'move'; dragDataRef.current = { noteId: String(note.id), fromDayId: String(day.id) }; setDraggingId(`note-${note.id}`) }}
@@ -1911,6 +1909,7 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar({
margin: '1px 8px', margin: '1px 8px',
borderRadius: 6, borderRadius: 6,
border: '1px solid var(--border-faint)', border: '1px solid var(--border-faint)',
borderTop: showDropLine ? '2px solid var(--text-primary)' : undefined,
background: 'var(--bg-hover)', background: 'var(--bg-hover)',
opacity: draggingId === `note-${note.id}` ? 0.4 : 1, opacity: draggingId === `note-${note.id}` ? 0.4 : 1,
transition: 'background 0.1s', cursor: 'grab', userSelect: 'none', transition: 'background 0.1s', cursor: 'grab', userSelect: 'none',