feat(database): add Notion-style timeline view - #555
Merged
Merged
Conversation
Add a Timeline database layout (DatabaseViewLayout.Timeline = 8, ViewLayout.Timeline = 10) that plots rows as bars on a horizontally scrolling, infinitely growing canvas next to a docked table. Renderer ported from frappe/gantt (MIT, attributed in file headers) and restyled with the calendar's toolbar, gutters, today pill and event cards: - Hours / Day / Week / Bi-week / Month / Quarter / Year scales - Move, resize-start / resize-end and progress drags with column snapping, auto-scroll near the edges, Escape to cancel, undo / redo groups - Dependency arrows from a self-relation field; dependents follow the dragged bar and a bar cannot start before its dependencies - Progress fill from a number field - Hover card, selection, keyboard open, off-screen pills, Today / steppers - Undated rows in the table and the No Date list; click on the canvas dates them The layout setting is stored like the calendar's under layout_settings['8'] (layout_ty, first_day_of_week_v2 with the legacy and user-metadata fallbacks, field_id, show_table, dependency_field_id, progress_field_id); read-only viewers keep a local override. Tests: 29 jest cases (geometry, dependencies, layout setting), a Playwright e2e spec and a 22-scenario BDD feature covering every interaction. Requires the matching server change in AppFlowy-Cloud-Premium (feat/timeline-layout) so the new layout ids are accepted. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NoEH4HcEiojEcioVSLE3QM
There was a problem hiding this comment.
Sorry @appflowy, your pull request is larger than the review limit of 150,000 diff characters
Dropping a bar cleared the drag preview at once, but the row selectors re-read cell data through a 150 ms trailing debounce, so the bar painted at its old dates for several frames before jumping to the new ones. Add createLocalFirstObserver: the user's own transactions re-read in a microtask (folding a dropped bar and its followers into one read, flushed with the drop itself), while remote bursts keep the debounce. Wire it into useDateFieldEventsSelector and useTimelineFieldValues, so date, progress and dependency edits all land in the same paint. A new BDD scenario samples the bar's position every animation frame across a drop and fails if any frame after the first move shows the origin again. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NoEH4HcEiojEcioVSLE3QM
Register ViewLayout.Timeline in every layout switch the calendar is in: isDatabaseLayout, the view-loader collab-type map, ViewModal, published CollabView / DatabaseView skeletons, copy-link row params, first-child navigation, mobile outline and chat icons. Add the `timeline` document block type so a timeline can be embedded from the slash menu (Timeline / Linked Timeline) and created from the sidebar's New page menu. BDD: timeline-integration.feature covers the New page menu, both slash entries (including the page modal), and a visitor opening a published timeline read-only. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NoEH4HcEiojEcioVSLE3QM
Give the docked table Notion's hover gutter: `+` inserts a row below, `⋮⋮` opens insert above / below, duplicate and delete, and dragging it reorders rows (with the List view's clear-sorts confirmation). The gutter is editor-only; header and "+ New row" line up with it. Reuse the List view's ListRowActions and move its row DnD hook to the shared drag-and-drop folder (useRowDnd, now with an optional wider drop target so a drop anywhere along a timeline row counts). Fix a latent List bug on the way: Radix opened the ⋮⋮ menu on pointer-down and cancelled the event, which stopped the browser from ever starting the native drag on the same handle. The trigger now opens on click, so the handle can be dragged. BDD: hover gutter insert / insert above / duplicate / delete, drag reorder + undo, and no gutter for published visitors. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NoEH4HcEiojEcioVSLE3QM
Notion's dependency options on the timeline settings: - Shift dependents: "Only when dates overlap" (default — a follower moves just far enough to start after the bar it depends on, cascading through the chain), "Keep the time between items" (the former behaviour), or "Never" (followers stay, and a dependent may be dragged before its dependency). Stored as `dependency_shift_ty`. - Avoid weekends: a shifted follower never lands on a Saturday or Sunday (`avoid_weekends`). - Drag to connect: every bar shows a connector handle when a dependency field is bound; dragging it onto another bar appends the source row to the target's relation cell, with duplicate and cycle guards. A dashed connector follows the pointer. useUpdateRelationCell is now a thin wrapper over a row-agnostic useUpdateRelationCellDispatch. The layout setting reader also carries `end_field_id` for the upcoming separate start/end feature. Tests: 4 new jest cases for the shift math; BDD scenarios for each shift mode, avoid weekends (date-aware) and the connector drag. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NoEH4HcEiojEcioVSLE3QM
Notion's "separate start and end dates": an optional End date field (`end_field_id`) in the timeline settings makes each bar run from the start field's date to the end field's date. A row whose end is missing or earlier than its start is a single-unit bar; a row without a start stays undated. Drags write the two cells as one undo group; moving a bar that has no end only moves its start, while the end handle creates the end date. Tests: a hook test for the merged selector (range, backwards, open, unbinding) and a BDD scenario covering bind, move, undo, resize and unbind. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NoEH4HcEiojEcioVSLE3QM
Notion's table properties: a "Table properties" submenu in the timeline settings picks which properties appear as columns of the docked table (`table_field_ids`, kept in the view's property order, separate from the bar's chips). Each column is 140px, headed by the field's icon and name, and rendered with the same CardField cells the board uses. A calculations footer under the table offers the grid's calculation menu per column. GridCalculateRowCell now accepts explicit row orders so it works outside the grid provider. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NoEH4HcEiojEcioVSLE3QM
Group the timeline the way the List groups: a header row per visible group (the List's header — collapse toggle, value tag, count, hide / remove-grouping menu, quick add) with a tinted band across the canvas, the group's rows beneath it, and a per-group "+ New row" footer that creates rows already holding the group's value. Collapsed and hidden groups drop out of the canvas; bar and arrow geometry stays index-based over the mixed item list. The grouping selector, hide-empty toggle and layout-setting accessor now understand DatabaseViewLayout.Timeline (`layout_settings['8']`), a TimelineGroupingProvider wraps the view in DatabaseViews, and the settings menu gains the shared Group submenu. Manual row reordering is off while grouped; inserts from the row gutter stay in their group. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NoEH4HcEiojEcioVSLE3QM
The default became "only when dates overlap"; the e2e spec asserts frappe's move_dependencies behaviour, which the BDD suite now covers per mode. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NoEH4HcEiojEcioVSLE3QM
…inding - Set up dependencies (settings item, or the first connector drop on a view without them): creates a two-way "Blocked by" / "Blocking" self-relation pair (or reuses an existing one), binds it, shows both as table columns and keeps them off the bars — Notion's one click. - The bound field may list either side: "Field lists: Blocked by / Blocking" (`dependency_direction`) flips the edges so a "Blocking" property draws the same arrows and writes go to the right cell. - Per-link type and lag: clicking an arrow opens an editor with finish-to-start / start-to-start / finish-to-finish / start-to-finish, a lag in days (negative = lead) and Remove dependency. Stored in `dependency_links` keyed "predecessor:successor"; missing = FS, lag 0. - Arrows route per type (FS keeps frappe's route; the others run orthogonally between the matching edges, detouring along the row boundary), and the shift math and drag clamps honour type and lag: start-type links bound a bar's start, end-type links its end. The finish-to-start clamp now means "cannot start before its dependency finishes" (was: before it starts). Tests: 7 new jest cases (direction, constraintStart per type/lag, SS + lag shifting, minEnd clamp, per-type routing); BDD scenarios for setup, connector-driven setup, Blocking-side binding, and editing type / lag / removal from the arrow; e2e clamp expectation updated. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NoEH4HcEiojEcioVSLE3QM
A dependent could not be dragged over or before the bar it depends on: the drag stopped at the constraint (frappe's rule). Notion places the dragged bar where it is dropped and re-routes the arrow; only the bar's own dependents shift. Drop the minStart / minEnd clamps and the "Cannot start before its dependencies" string. Also lock in that dragging writes nothing until the drop: a scenario counts Yjs updates on the dragged row and its follower while the pointer is held with both bars visibly moved (zero), then after release (both written). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NoEH4HcEiojEcioVSLE3QM
…able Making arrows clickable had lifted their SVG above the rows, so lines were painted over cards and, once the canvas scrolled, over the sticky table cells. Put the layer back beneath the rows (cards cover the lines, the table hides them) and hit-test link clicks from the row canvas instead, via isPointInStroke on each arrow's wide invisible twin. The click the browser fires after a drop (on the common ancestor of the press and release targets) is now ignored by the canvas, so releasing a bar over a line no longer opens the link editor or clears the selection. BDD: a scenario asserts the line loses the hit test to the row layer and, after scrolling, to the docked table cell. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NoEH4HcEiojEcioVSLE3QM
- TimelineLinkEditor derives its lag draft during render (previous-prop pattern) instead of an effect, and is memoized; the view memoizes the anchor selection and close handler so an open editor no longer re-renders on every scroll frame. - readTimelineLayoutSetting caches parsed dependency links and table field ids per stored Yjs value (WeakMap) — getSnapshot runs on every subscriber render and was re-allocating both each time. - buildDependencyGraph dedupes edges with a Set instead of an O(n) includes per insert. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NoEH4HcEiojEcioVSLE3QM
The header placed the table toggle after the property columns while each row placed its open button before them, so column edges in the header sat one button-width left of the cells. Rows now keep the open button in the same trailing slot as the toggle, and the calculations footer reserves that slot too; a BDD assertion checks header, cell and calculation x. Also a second react-best-practice pass on the last three commits: - the connector-drag hook bound its window listeners per pointermove (its effect depended on the preview object); it now depends on a boolean like the bar drag does - one memoized row index serves the bar press and the link editor's titles instead of a Map rebuild per press and two rescans per render - the layout-setting comparators short-circuit on identity now that parsed links / ids are cached Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NoEH4HcEiojEcioVSLE3QM
Three re-render findings from reviewing the branch against the Vercel React rules, all on hot paths: - TimelineRow handed the memoized TimelineBar a fresh onLinkPointerDown lambda every render, re-rendering every bar's cells and tooltip subtree on scroll-pill flips, drag start/end and selection; it is a stable useCallback now. - The header's drag highlight re-reconciled every column label per drag step; the labels are their own memo child (HeaderColumns) so a step touches one element. - Every table row mounted its own useNewRowDispatch / useDuplicateRowDispatch (each a bundle of database-context subscriptions read only in click handlers). ListRowActions is split into a presentational RowActionsMenu plus the List's hook-owning wrapper; the timeline creates the dispatches once and hands rows a stable actions object (deps behind a ref). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NoEH4HcEiojEcioVSLE3QM
The bar's hover card aligned to the bar's start and clamped only to the viewport, so a bar running under the sticky table opened its card over the table cells. The tooltip now pads its collision boundary on the left up to the table's right edge (measured once per opening), so the card shifts right of the table while still sitting above the bar. Covered by a new step in the hover scenario. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NoEH4HcEiojEcioVSLE3QM
* Fix duplicate feed layout import * Add Confluence HTML ZIP import to web * Improve import cancellation, keyboard access, and lazy loading * Test native file chooser activation for page imports * Enable Confluence workspace import from settings * Open imported space home pages from the sidebar
…550) * fix: preserve calendar draft isolation on dismissal and save failure * fix: handle row registration delay during document duplication
Selecting a row swapped the docked table cell's opaque background for the theme's selection fill, which is translucent — so a bar or arrow scrolled under the table showed through the selected row, as if drawn over the cells. The tint is now painted over the opaque background. The hover scenario checks the selected cell's computed background. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NoEH4HcEiojEcioVSLE3QM
Read desktop-authored Yjs table columns and dependency metadata, including nested updates. Align new-row and calculation footers, keep the table divider continuous, and route dependency connectors to the bar edges without reversed bends. Validated with TypeScript checks, focused timeline tests, and live desktop and web checks.
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 free
to 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.
Description
Adds a Timeline database layout (
DatabaseViewLayout.Timeline = 8,ViewLayout.Timeline = 10) that plots rows as bars on a horizontally scrolling, infinitely growing canvas next to a docked table — the Notion timeline anatomy, styled to match the calendar view.Depends on the server change in AppFlowy-IO/AppFlowy-Cloud-Premium#1162 (new layout ids,
timelinedocument block type,TimelineLayoutSetting).What's included
src/components/database/timeline/, ported from frappe/gantt (MIT, attributed in file headers) and rebuilt on our Yjs selectors / dispatch hooks,@tanstack/react-virtualrows andsrc/components/ui.+insert below,⋮⋮menu with insert above / below, duplicate, delete, and drag to reorder), table properties as extra columns (configured separately from bar chips) and a calculations footer.+, Layout-menu conversion, sidebar New page → Timeline, slash menu/timelineand/linked timeline(newtimelinedocument block), page modal, publish, mobile outline / chat icons, copy-link.layout_settings['8']withlayout_ty,first_day_of_week_v2(fallbacks),field_id,end_field_id,show_table,table_field_ids,dependency_field_id,dependency_direction,dependency_links,dependency_shift_ty,avoid_weekends,progress_field_id,hide_empty_groups;use24Hourfrom user metadata; read-only viewers keep a local override.createLocalFirstObserver) so a dropped bar never snaps back while the 150 ms remote debounce catches up.⋮⋮row handle could never start a native drag because Radix cancelled pointer-down; it now opens on click.TIMELINE_VIEW_ENABLEDinsrc/application/constants.ts.Feature Preview
Screenshots (Month scale with a dependency arrow and 40% progress fill; Week scale; table hidden; table columns + calculations; grouped) — to be dropped in from the session; the CLI cannot upload images.
Checklist
General
Testing
timeline/__tests__/geometry.test.ts,dependencies.test.ts(shift modes, avoid weekends, link types / lag, per-type routing),database-yjs/__tests__/timeline-layout.test.ts,timeline-events-selector.test.tsx(start/end fields),local-first-observer.test.tsplaywright/e2e/database/timeline.spec.ts(2)playwright/bdd/features/database/timeline.feature(36 scenarios: drag / snap / undo / redo, no snap-back on drop, resize + Escape, hover card, selection & open, undated rows + hide table, dependencies for every shift mode, avoid weekends, connector drag, one-click setup, Blocking-side binding, arrow editor (type / lag / remove), progress handle, every scale, steppers / pills / Today, No Date list, settings, Layout-menu conversion, empty state, row gutter insert / duplicate / delete / reorder, separate start & end fields, table columns + calculations, grouping) andtimeline-integration.feature(4: New page menu,/timeline, linked timeline + page modal, published read-only)pnpm type-check,pnpm lintcleanFeature-Specific
🤖 Generated with Claude Code
https://claude.ai/code/session_01NoEH4HcEiojEcioVSLE3QM