Skip to content

Commit c755930

Browse files
feat: timeline
1 parent a360030 commit c755930

4 files changed

Lines changed: 62 additions & 37 deletions

File tree

examples/react/timeline/src/index.tsx

Lines changed: 55 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
import { TanStackDevtools } from '@tanstack/react-devtools'
66
import { timeDevtoolsPlugin } from '@tanstack/react-time-devtools'
77
import {
8+
getTimeClient,
89
toPlainDateString,
910
toPlainDateTimeString,
1011
toPlainTimeString,
@@ -38,6 +39,7 @@ import type {
3839
Event,
3940
ResizeError,
4041
Resource,
42+
TimelineLayout,
4143
TimelineResourceRow,
4244
} from '@tanstack/time'
4345
import type { Connection, Edge, Node, NodeProps } from '@xyflow/react'
@@ -190,7 +192,6 @@ function getSampleEvents(): Array<Event<Resource>> {
190192
start: weekdayAt(3, 10, 0),
191193
end: weekdayAt(3, 15, 0),
192194
resources: [resourceQA],
193-
dependsOn: ['3'],
194195
},
195196
{
196197
id: '6',
@@ -205,31 +206,27 @@ function getSampleEvents(): Array<Event<Resource>> {
205206
start: weekdayAt(3, 10, 0),
206207
end: weekdayAt(3, 12, 30),
207208
resources: [resourceDesign],
208-
dependsOn: ['1'],
209209
},
210210
{
211211
id: '8',
212212
title: 'Auth Module',
213-
start: weekdayAt(4, 0, 0),
213+
start: weekdayAt(5, 6, 0),
214214
end: weekdayAt(5, 10, 0),
215215
resources: [resourceBackend],
216-
dependsOn: ['3'],
217216
},
218217
{
219218
id: '9',
220219
title: 'Load Testing',
221220
start: weekdayAt(4, 10, 30),
222221
end: weekdayAt(4, 14, 30),
223222
resources: [resourceQA],
224-
dependsOn: ['8'],
225223
},
226224
{
227225
id: '10',
228226
title: 'Deployment',
229227
start: weekdayAt(5, 7, 30),
230228
end: weekdayAt(5, 13, 0),
231229
resources: [resourceDevOps],
232-
dependsOn: ['6'],
233230
},
234231
]
235232
}
@@ -578,8 +575,14 @@ const DEP_CANVAS_NODE_TYPES = { depCanvas: DepCanvasNode }
578575
const CANVAS_NODE_ID = '__dep_canvas__'
579576

580577
function buildTimelineEdges(events: Array<Event<Resource>>): Array<Edge> {
581-
return events.flatMap((event) =>
582-
(event.dependsOn ?? []).map((sourceId) => ({
578+
const visibleEventIds = new Set(events.map((e) => e.id))
579+
580+
return events.flatMap((event) => {
581+
const deps = (event.dependsOn ?? []).filter((sourceId) =>
582+
visibleEventIds.has(sourceId),
583+
)
584+
585+
return deps.map((sourceId) => ({
583586
id: `dep-${sourceId}-${event.id}`,
584587
source: CANVAS_NODE_ID,
585588
sourceHandle: `${sourceId}-source`,
@@ -589,12 +592,12 @@ function buildTimelineEdges(events: Array<Event<Resource>>): Array<Edge> {
589592
animated: true,
590593
markerEnd: { type: MarkerType.ArrowClosed, color: '#f59e0b' },
591594
style: { stroke: '#f59e0b', strokeWidth: 2 },
592-
})),
593-
)
595+
}))
596+
})
594597
}
595598

596599
interface TimelineDependencyOverlayProps {
597-
calendarEvents: Array<Event<Resource>>
600+
timelineLayout: TimelineLayout<Resource, Event<Resource>>
598601
eventBarRefs: React.MutableRefObject<Map<string, HTMLDivElement>>
599602
rowsContainerRef: React.RefObject<HTMLDivElement | null>
600603
resizeState: {
@@ -608,12 +611,12 @@ interface TimelineDependencyOverlayProps {
608611
}
609612

610613
function TimelineDependencyOverlay({
611-
calendarEvents,
612-
eventBarRefs,
614+
timelineLayout,
613615
rowsContainerRef,
614616
resizeState,
615617
activeDragEvent,
616618
onDependencyCreate,
619+
eventBarRefs,
617620
}: TimelineDependencyOverlayProps) {
618621
const [rfNodes, setRfNodes, onNodesChangeBase] = useNodesState<Node>([])
619622
const [rfEdges, setRfEdges, onEdgesChange] = useEdgesState<Edge>([])
@@ -644,16 +647,24 @@ function TimelineDependencyOverlay({
644647
const containerRect = container.getBoundingClientRect()
645648
const positions: Array<HandlePosition> = []
646649

647-
for (const [eventId, el] of eventBarRefs.current) {
648-
const rect = el.getBoundingClientRect()
649-
positions.push({
650-
eventId,
651-
x: rect.left - containerRect.left,
652-
y: rect.top - containerRect.top,
653-
width: rect.width,
654-
height: rect.height,
650+
// Use actual event-bar DOM rects so handles follow resize previews
651+
// (resize updates styles on the event bar element, while timelineLayout
652+
// may remain based on the committed start/end).
653+
timelineLayout.rows.forEach((row: TimelineResourceRow) => {
654+
row.events.forEach((e: any) => {
655+
const el = eventBarRefs.current.get(e.event.id)
656+
if (!el) return
657+
658+
const rect = el.getBoundingClientRect()
659+
positions.push({
660+
eventId: e.event.id,
661+
x: rect.left - containerRect.left,
662+
y: rect.top - containerRect.top,
663+
width: rect.width,
664+
height: rect.height,
665+
})
655666
})
656-
}
667+
})
657668

658669
setRfNodes([
659670
{
@@ -668,8 +679,15 @@ function TimelineDependencyOverlay({
668679
draggable: false,
669680
},
670681
])
671-
setRfEdges(buildTimelineEdges(calendarEvents))
672-
}, [calendarEvents, eventBarRefs, rowsContainerRef, setRfNodes, setRfEdges])
682+
const allEvents = Array.from(
683+
new Map(
684+
timelineLayout.rows
685+
.flatMap((r: any) => r.events.map((e: any) => e.event))
686+
.map((e: any) => [e.id, e]),
687+
).values(),
688+
)
689+
setRfEdges(buildTimelineEdges(allEvents))
690+
}, [timelineLayout, rowsContainerRef, eventBarRefs, setRfNodes, setRfEdges])
673691

674692
useLayoutEffect(() => {
675693
updatePositions()
@@ -1013,11 +1031,6 @@ function TimelineDemo() {
10131031
return map
10141032
}, [])
10151033

1016-
const calendarEvents = useMemo(
1017-
() => calendar.getEvents(),
1018-
[calendar.days, calendar],
1019-
)
1020-
10211034
const timelineLayout = useMemo(
10221035
() => calendar.getTimelineLayout(),
10231036
[calendar.days],
@@ -1189,6 +1202,18 @@ function TimelineDemo() {
11891202
)
11901203

11911204
if (validation.blocked) {
1205+
getTimeClient().emit('event:update:error', {
1206+
eventId: draggedEvent.id,
1207+
eventTitle: draggedEvent.title,
1208+
reason: 'unavailable-time',
1209+
message:
1210+
validation.message ?? 'This move is blocked by availability.',
1211+
originalStart: draggedEvent.start,
1212+
originalEnd: draggedEvent.end,
1213+
attemptedStart: nextStart,
1214+
attemptedEnd: nextEnd,
1215+
})
1216+
11921217
setResizeError({
11931218
eventId: draggedEvent.id,
11941219
eventTitle: draggedEvent.title,
@@ -1432,7 +1457,7 @@ function TimelineDemo() {
14321457
style={{ overflow: 'hidden' }}
14331458
>
14341459
<TimelineDependencyOverlay
1435-
calendarEvents={calendarEvents}
1460+
timelineLayout={timelineLayout}
14361461
eventBarRefs={eventBarRefsMap}
14371462
rowsContainerRef={rowsContainerRef}
14381463
resizeState={resizeState}

packages/react-time/src/useCalendar/useCalendar.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,7 @@ export const useCalendar = <
258258
: undefined,
259259
}
260260

261-
getTimeClient().emit('event:resize:error', {
261+
getTimeClient().emit('event:update:error', {
262262
eventId: id,
263263
eventTitle: event?.title ?? 'Unknown Event',
264264
reason: validation.error.reason,
@@ -530,8 +530,8 @@ export const useCalendar = <
530530
)
531531

532532
const validateMove = useCallback<typeof calendarCore.validateMove>(
533-
(eventId, newStart, newEnd) =>
534-
calendarCore.validateMove(eventId, newStart, newEnd),
533+
(eventId, newStart, newEnd, newResources) =>
534+
calendarCore.validateMove(eventId, newStart, newEnd, newResources),
535535
[calendarCore],
536536
)
537537

packages/time-devtools/src/components/Shell.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,8 @@ const getEventTypeLabel = (
4545
return { text: 'Removed', color: 'red' }
4646
case 'time:event:resized':
4747
return { text: 'Resized', color: 'yellow' }
48-
case 'time:event:resize:error':
49-
return { text: 'Resize Error', color: 'red' }
48+
case 'time:event:update:error':
49+
return { text: 'Update Error', color: 'red' }
5050
case 'time:calendar:navigate':
5151
return { text: 'Navigate', color: 'purple' }
5252
case 'time:calendar:viewMode:changed':
@@ -68,7 +68,7 @@ const getEventDescription = (entry: ActivityLogEntry): string => {
6868
return `${details.eventTitle || 'Event'}`
6969
case 'time:event:resized':
7070
return `Resized to ${String(details.start)} - ${String(details.end)}`
71-
case 'time:event:resize:error':
71+
case 'time:event:update:error':
7272
return `${details.eventTitle || 'Event'} - ${String(details.message)}`
7373
case 'time:calendar:navigate':
7474
return `${String(details.direction)}${String(details.targetDate)}`

packages/time/src/client/TimeClient.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export interface TimeEventMap {
3333
start: string
3434
end: string
3535
}
36-
'time:event:resize:error': {
36+
'time:event:update:error': {
3737
eventId: string
3838
eventTitle: string
3939
reason: 'unavailable-time' | 'invalid-time' | 'min-duration' | 'blocked'

0 commit comments

Comments
 (0)