Uh oh!
There was an error while loading. Please reload this page.
fix(plugin-timeline,i18n): refuse object-bound variant: gantt instead of throwing - #6749
Merged
os-sales merged 1 commit intoAug 29, 2026
Merged
Conversation
…ad of throwing `ObjectTimeline` maps each record to a flat FEED item — one per record, no nested `items` — while the renderer's gantt branch reads a gantt ROW (`row.items[].startDate`). Every `row.items` was therefore `undefined`, `calculateDateRange` reduced an empty list, `Math.min()` over it was `Infinity`, and `new Date(Infinity).toISOString()` threw `RangeError: Invalid time value` mid-render. No guard, no diagnostic — the component simply threw. Per the maintainer ruling of 2026-08-29 this refuses loudly rather than composing gantt rows from records (that capability was presented and NOT adopted; it stays open and unruled). The object-bound path now renders an author-facing diagnostic naming the limitation, matching the nearest sibling convention in this repo — `PivotTable`'s out-of-vocabulary aggregation refusal: a `role="alert"` box that names the refused value and what the surface does render. The condition keys on whether `items` were AUTHORED, not on the variant alone. This component also answers the bare `timeline` key, and the in-repo catalog fixture `plugin-timeline/gantt-style-timeline.json` is a literal gantt flowing through it; refusing on `variant` alone would have taken it down. The feed variants and the presentational `TimelineRenderer` are untouched. The diagnostic goes through the package's own translate channel rather than an English literal, so it is added to all ten locale packs. The German value quotes with `„…“` per that pack's convention, which moves the `de` quote-pairing census 54 -> 55 with a line naming this card. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CRJge11jso9TpXRWFt1Z49
Contributor
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
os-sales
marked this pull request as ready for review
August 29, 2026 06:13
Uh oh!
There was an error while loading. Please reload this page.
os-sales
deleted the
claude/issue-6655-object-timeline-gantt-refuse-loudly
branch
August 29, 2026 06:25
This was referenced Aug 29, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes#6655
The defect
The two timeline item shapes are not interchangeable, and this path crossed them.
ObjectTimelinemaps each record to a flat feed item — one per record, no nesteditems. The renderer's gantt branch reads a gantt row instead (row.items[].startDate). Against composed feed items everyrow.itemsisundefined, soallDatesis empty,Math.min()over it isInfinity, andnew Date(Infinity).toISOString()throwsRangeError: Invalid time valueduring render. No guard, no diagnostic — the component simply threw.Re-derived on this PR's base
faac0d935(the card's line numbers were from merge-basef241a4df4and are stale):calculateDateRangeis atrenderer.tsx:165and throws at:174, reached fromTimelineRendererat:424.What this implements
The maintainer ruling of 2026-08-29 on the card adopted option 2, refuse loudly, verbatim 「同意」. Option 1 — composing real gantt rows from records — was presented and explicitly not taken; that capability stays open and unruled, and nothing here implements any part of it.
The object-bound path now renders an author-facing diagnostic naming the limitation instead of throwing.
Convention matched, and why. The repo has more than one convention for "this authoring shape is not supported" on a renderer. I matched
plugin-dashboard/src/PivotTable.tsx(objectui#2941), which is the nearest neighbour on every axis that matters: it refuses an out-of-vocabulary authored enum value on a renderer, after the hooks so hook order stays stable, through arole="alert"box that names both the refused value and what the surface does render — and its own comment calls this "refuse loudly", the same phrase the ruling uses. The two alternatives are a poorer fit:DatasetReportRenderer's out-of-spec chart type is a muted dashed note precisely because a grouped table beneath still carries the numbers, andDashboardGridLayoutlowers an unsupported widget to a text node; neither applies where nothing renders at all. The markup also matches this same file's existingtimeline-errorstate, soObjectTimelinehas one diagnostic shape rather than two.The refusal keys on authored items, not on the variant
ObjectTimelineis also the renderer behind the baretimelinekey (view:timelinein./index; the presentational registration in./renderercarriesskipFallbackso the bare key lands here). The in-repo catalog fixtureplugin-timeline/gantt-style-timeline.jsonis exactly that —type: 'timeline',variant: 'gantt', literal gantt rows — and it flows through this component untouched. A refusal keyed onvariantalone would have taken it, and every other authored gantt, down with it. The condition therefore reuses the sameschema.itemstesteffectiveItemsitself makes, and both are commented as coupled.The ruling's second clause
Resolved by the second of the two routes the card allowed — the refusal is what the author now sees instead of a silently-ignored option — not by dropping the composition on the feed variants. That route was closed by evidence, not by preference: objectui#6355's landed pin
ObjectTimeline.scaleComposition.test.tsxrendersvariant: 'vertical'and assertsschema.scale === 'year'andresolveTimelineScale(schema) === 'year'on it. Gating the composition on gantt would turn that pin red, which is the ruling's own pin 2 ("object-bound plus vertical or horizontal, unchanged") failing. So the axis is still composed for the feed variants exactly as before, and on the gantt variant the author who set it is now told why it has no effect rather than getting a crash.Measured, not asserted: pin 1b renders an object-bound gantt carrying
timeline.scale: 'year'and shows the diagnostic present and theyearaxis those records would have produced (2099,2100) absent from the DOM.The diagnostic goes through the i18n channel
An English literal would have tripped
check:i18n-keys, whose message is explicit thatpackages/i18n/src/locales/en.tsis the source of truth and that an inline default is not a fix. The key is added to all ten locale packs, mirrored byte-identically into this package's provider-lessTIMELINE_DEFAULT_TRANSLATIONStable (the seamtimeline-scale-vocabulary-defaults.test.tspins).The German value quotes with the
„…“pair that pack's convention requires (de-quote-pairing-3876.test.tsbans U+0022 in anydevalue). That adds one correctly-paired span, so the two census assertions in that file move 54 to 55 with a line naming this card — the maintenance those counters are explicitly designed for, andrdqstays 0, which is the load-bearing half.Evidence
All readings on this branch. Test and gate readings after the final commit
371dd804a; the red-first reading is the one that must predate it.Red first, on the unmodified base
faac0d935. The pin was written and run before any fix:The 4 red are pins 1 and 1b. The 6 green are pins 2 and 3 — already passing on the base, which is what makes them a real "unchanged" baseline rather than filler.
Ablation, against the committed implementation at
371dd804a. The refusal block was deleted, the deletion proven on disk before any run (thetimeline-unsupported-variantmarker went 1 to 0 and an injected marker went 0 to 1;git diff --statshowed 1 insertion, 9 deletions), and the restore was trapped onEXIT INT TERMwith absolute paths. No rebuild is involved on either leg: the pin imports the mutated module by relative specifier (import { ObjectTimeline } from '../ObjectTimeline'), so vitest compiles that source directly and nodistcan stale it.Tests 4 failed | 6 passed (10),RangeError: Invalid time value— the same 4, and pins 2 and 3 stayed green under ablation, so the pin discriminates rather than going uniformly red.git diff HEADempty and on-disk blob1b67bd7e1dd2c104263e298df48ee69f3bdefc18equal to that path's HEAD blob hash.Test Files 71 passed (71),Tests 1016 passed (1016).Suites (
@object-ui/i18nand@object-ui/plugin-timeline, at371dd804a):Test Files 71 passed (71),Tests 1016 passed (1016). Both packages'type-check:Donefortsc --noEmit && tsc -p tsconfig.test.json, and the new pin is verifiably inside that program —tsc -p tsconfig.test.json --listFileslists it.Gates (each quoted from its own verdict line, at
371dd804a):check:control-bytes—OK (scanned 5606 tracked text file(s); skipped 85 binary)check:i18n-keys—2587/2587 literal keys resolve(it was2586/2587with the key missing, which is how the en-pack requirement was found)check:i18n-drift—No en value changed in this range/1 key(s) added, 0 removedcheck:vi-mock-specifiers—OK (3938 tracked source file(s) … 751 relative specifier(s) resolved)check:self-import—No package names itself inside its own src/.check:phantom-deps—Every in-scope import is declared by the package that publishes it.check:esm-specifiers— exit 0, no findingcheck:i18n-dead-keys— a report, not a gate, by its own footerLint, narrowed and declared. CI runs
pnpm lint(turbo run lint, each package's owneslint .) across all 47 workspace projects; this run covered the 2 the diff touches —Scope: 2 of 47 workspace projects— with0 errorsin both (plugin-timeline20 files linted, 71 warnings;i18n84 files, 34 warnings; counts from eslint's own--format json). The narrowing excludes nothing:eslint.config.jsdeclares noprojectService, noparserOptionsand noprojectkey, so type-aware linting is off and this diff cannot move the verdict on a file it does not contain.plugin-timelinewent 72 to 71 warnings mid-review — the one new warning wasreact-refresh/only-export-componentson a constant I had exported, so it is module-local now and adds no public surface.NOT MEASURED — neither green nor red, reported with the reason:
check:sdui-registration-pins— refused to run:No console build to weigh at apps/console/dist/assets, and the gate itself says "This is exit 2, not a pass … a run with nothing to read has measured nothing". It needspnpm --filter @object-ui/console build. This diff changes no registration, nosideEffectsarray and no bundle entry.check:readme-exports— reported its own population collapsed:2098 export symbol(s) read from 10 of 40 package(s) (27 unbuilt), every finding of the form "its type entry is not on disk — runpnpm buildfirst" and none in a package this diff touches. It needs a full repo build.Both are cheap for CI, which builds the repo anyway.
Out of scope, filed separately
A literal-rows gantt with an empty
itemsarray (items: []) still throws the sameRangeError—calculateDateRangehas no empty-list guard, and an empty array is truthy so it takes the authored path. That is the crash site's own missing guard on the legitimate literal-rows path, which this card is explicitly not the fix site for. Recorded in the report to PM rather than widened into this PR.Generated by Claude Code