Filed unassigned, found while implementing #6655 (the object-bound gantt refusal). Not the same defect and not fixed by that card — see "Why #6655 does not cover this" below.
What happens
calculateDateRange has no empty-list guard. With variant: 'gantt' and a literal but EMPTY items array, it reduces nothing:
constallDates=items.flatMap((row)=>(row.items||[]).flatMap((item)=>[item.startDate,item.endDate]));constminTimestamp=Math.min(...allDates.map((d)=>newDate(d).getTime()));
allDates is [], Math.min() over an empty list is Infinity, and new Date(Infinity).toISOString() throws.
Measured on 371dd804a (the #6655 branch head, which does not change this path), with a throwaway probe since no pin covers it:
AssertionError: PROBE renderer: expected RangeError: Invalid time value to be null
AssertionError: PROBE ObjectTimeline: expected RangeError: Invalid time value to be null
Tests 2 failed (2)
Both entry points crash identically:
TimelineRenderer given { type: 'timeline', variant: 'gantt', items: [] } directly;ObjectTimeline given the same schema — an authored empty array is truthy, so effectiveItems returns it as authored items and hands it straight through.
Why this is reachable in practice
An empty gantt is a perfectly ordinary state, not a malformed document. Any author or generator that builds items from a collection produces items: [] the moment the collection is empty — a filtered project list with no matches, a fresh workspace, a plan whose rows are yet to be added. The current behaviour is a hard render crash for the empty case of an otherwise valid schema, where an empty grid or an empty-state panel is what every other surface in this repo does.
Why #6655 does not cover this
#6655 is the OBJECT-BOUND path composing the wrong item shape: it maps records to flat feed items, which have no nested items, so the gantt branch reads undefined on every row. Its ruling put an authoring refusal in ObjectTimeline, deliberately keyed on whether the items were AUTHORED so that legitimate literal gantt documents keep rendering.
This one is the opposite case — the items ARE authored, the shape IS the gantt row shape, and the document is legitimate. It is the crash site's own missing guard, on the path #6655 was explicitly told to leave untouched. Neither the refusal nor its pins fire here, and the empirical result above was taken with #6655's fix in the tree.
Where a fix would go
packages/plugin-timeline/src/renderer.tsx — calculateDateRange (the function starts at :165 and throws at :174 on 371dd804a), or its caller on the gantt branch at :424.
Two directions, both plausible, which is why this is filed rather than fixed:
- Guard in
calculateDateRange — return a sentinel range for an empty list and let the gantt render an empty grid with a valid axis. Cheapest, and keeps schema.minDate / schema.maxDate meaningful (an author who pinned an explicit range arguably should get exactly that range with no rows in it, which this direction gives for free). - Empty state on the gantt branch — render the repo's standard empty state instead of a zero-row chart, matching what the grid and kanban surfaces do. Better product behaviour, but it is a UI decision about what an empty gantt should look like, not a mechanical fix.
The same empty-list question also applies to the axis header row (generateTimeScaleHeaders) and to calculateBarDimensions, whose totalDuration is 0 for a degenerate range — worth deciding once for the whole branch rather than patching the one throw.
Filed unassigned, found while implementing #6655 (the object-bound gantt refusal). Not the same defect and not fixed by that card — see "Why #6655 does not cover this" below.
What happens
calculateDateRangehas no empty-list guard. Withvariant: 'gantt'and a literal but EMPTYitemsarray, it reduces nothing:allDatesis[],Math.min()over an empty list isInfinity, andnew Date(Infinity).toISOString()throws.Measured on
371dd804a(the #6655 branch head, which does not change this path), with a throwaway probe since no pin covers it:Both entry points crash identically:
TimelineRenderergiven{ type: 'timeline', variant: 'gantt', items: [] }directly;ObjectTimelinegiven the same schema — an authored empty array is truthy, soeffectiveItemsreturns it as authored items and hands it straight through.Why this is reachable in practice
An empty gantt is a perfectly ordinary state, not a malformed document. Any author or generator that builds
itemsfrom a collection producesitems: []the moment the collection is empty — a filtered project list with no matches, a fresh workspace, a plan whose rows are yet to be added. The current behaviour is a hard render crash for the empty case of an otherwise valid schema, where an empty grid or an empty-state panel is what every other surface in this repo does.Why #6655 does not cover this
#6655 is the OBJECT-BOUND path composing the wrong item shape: it maps records to flat feed items, which have no nested
items, so the gantt branch readsundefinedon every row. Its ruling put an authoring refusal inObjectTimeline, deliberately keyed on whether the items were AUTHORED so that legitimate literal gantt documents keep rendering.This one is the opposite case — the items ARE authored, the shape IS the gantt row shape, and the document is legitimate. It is the crash site's own missing guard, on the path #6655 was explicitly told to leave untouched. Neither the refusal nor its pins fire here, and the empirical result above was taken with #6655's fix in the tree.
Where a fix would go
packages/plugin-timeline/src/renderer.tsx—calculateDateRange(the function starts at:165and throws at:174on371dd804a), or its caller on the gantt branch at:424.Two directions, both plausible, which is why this is filed rather than fixed:
calculateDateRange— return a sentinel range for an empty list and let the gantt render an empty grid with a valid axis. Cheapest, and keepsschema.minDate/schema.maxDatemeaningful (an author who pinned an explicit range arguably should get exactly that range with no rows in it, which this direction gives for free).The same empty-list question also applies to the axis header row (
generateTimeScaleHeaders) and tocalculateBarDimensions, whosetotalDurationis0for a degenerate range — worth deciding once for the whole branch rather than patching the one throw.