Conversation
A Dashboard database view (DatabaseViewLayout 9, ViewLayout 11) shows a
row-based grid of widgets. Each widget renders another database view, from
this or any other database in the workspace, with its own filters, sorts
and settings.
- View and Edit modes; Edit adds widget headers, drag to move, width and
row-height handles, a widget menu and an add-widget picker for existing
or new views. Limits: 12 widgets, 4 per row, 12-column widths.
- Dashboard global filters ("Filter multiple sources") map one property per
source database and are AND-ed with each widget's own view filters;
View-mode changes stay local until "Save for everybody".
- New Number chart type for KPI tiles.
- Registration across tabs, layout switcher, icons, app shell, publish and a
Linked Dashboard slash item.
- Unit tests and a 50-scenario Playwright BDD suite.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…perty order - generateNiceTicks rounded ticks by float multiplication, so small ranges rendered labels such as 0.30000000000000004 (clipped to "0001"). Ticks are now rounded to the step's precision, and whole-number data (counts) gets whole-number steps. - A chart without an X axis picked the first groupable field in Y.Map iteration order, which depends on how the doc was built (a date field could win over a select field). It now follows the view's field_orders, like desktop's select_chart_group_field. test(dashboard): make the dashboard BDD suite pass (50/50) - Take fixture view ids from the workspace database list: a container database's doc also holds a hidden inline view no folder view points at. - Re-enter Edit mode after seeding widgets into a new dashboard (it falls back to View mode when widgets arrive without the picker). - Invite members into a custom space: private spaces have no roster. - Compare server and browser layouts with sorted object keys. - Measure both ends of a cross-row widget drag in one scroll position. - The sidebar lists database views with a dot, not the layout icon. - The empty-database Number chart scenario configures both charts. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Rows, widgets and global filters stored without an id, and rows that spill over the four-widget limit, got random ids on every parse. A setting held as a Y.Array is re-parsed on every read (toJSON returns a new array, so the per-value cache misses), so each read looked like a layout change: the layout store returned a new snapshot on every getSnapshot call and useSyncExternalStore could loop. Fallback ids are now positional (r:<row>, w:<row>:<index>, gf:<index>) and spill rows use <row id>:<chunk>. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- A Number chart using Count (or without a Y field) only needs the row orders, so it no longer calls ensureRow for every row. The drill-down popup loads the rows it lists that are not open yet. - The Number chart value is memoized on the aggregation, the Y field and the rows only, so title, number format and X-axis edits do not recompute it, and its item keeps its identity while the value and row ids do. - The Number chart title input is keyed by the stored title instead of copying it into state in an effect. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
DatabaseActions imported DashboardActions statically, which pulled the global filter editor (through the global-filters barrel) into every database view's bundle. DashboardActions is now a lazy component rendered only for dashboards, and it imports GlobalFilterButton from its own file. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…road subscriptions - Dashboard extra filters move from the DatabaseContext value to a DatabaseExtraFiltersContext that Database provides and only useRowOrdersSelector reads, so a global filter change no longer re-renders every database context consumer of a widget. - DashboardSettings subscribes to the widget-title flag alone (useDashboardShowWidgetTitles) instead of the whole dashboard layout. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- DashboardContext keeps the layout and mode; global filters move to DashboardFiltersContext and source docs / names to DashboardSourcesContext, so registering a source no longer re-renders every widget's database and filter edits leave the grid alone. - Global filter sources use one cached store per source doc (useSyncExternalStore): one set of Yjs observers per doc however many components list it, only the listed docs are observed, a change re-reads only its doc and unchanged property lists keep their identity, and the unconditional re-read on mount is gone. - The filter bar mounts the sources hook only while it shows chips; GlobalFilterBar, GlobalFilterChip, the editor's target rows, condition select and value control are memoized, and the debounced name input is its own component. - Chip labels are memoized and condition labels come from a cached map; activity is computed once per label. - The filter menu returns to the list while rendering when the edited filter disappears (no empty frame); the property picker and addable sources are computed once per source list. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
# Conflicts: # src/components/database/components/tabs/AddViewButton.tsx
…line The cloud requires an active Pro plan to create a Dashboard view on hosted release builds, so the add-view menu disables the Dashboard entry with an explanation instead of failing on the server. It reuses the Timeline plan check (and its per-workspace cache) with a Dashboard message. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… widgets Second React best-practices review of the dashboard. - Widgets read only stable contexts: host services (DashboardHostContext), UI plumbing with getRows/updateRows, and the dragged widget id in its own context. Edit state and titles arrive as props, and the nested database is memoized, so a row resize, drag start or host row-map change no longer re-renders every widget's database. - The widget menu computes its move / duplicate state from the rows while it is open, so widget actions stay stable. - The layout store reuses unchanged row, widget and filter objects. - A row-height drag defers the nested database's height; resize handles set touch-action: none so touch drags are not cancelled. - The widget picker defers its query and memoizes the option groups. - A local filter override equal to the saved filters is dropped, so the "only you see these changes" badge no longer sticks. - Switching dashboard views resets Edit mode and local filters during render. - Source registration callbacks have their own stable context, and useGlobalFilterSources keeps its list while nothing it lists changed. - The filter editor passes plain values to its rows and controls, so saving the name re-renders none of them. - The Number chart title input follows the stored title during render instead of remounting, so Enter keeps focus and remote edits keep a draft. DashboardProvider stays an eager import: lazy-loading it put a Suspense boundary around the whole database view (the tab bar blanked while the chunk loaded, and Edit mode came up a render after the dashboard). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Row docs are mutated in place, so editing a value a chart reads (a Value typed into a table widget next to a Number chart on a dashboard, or a collaborator's edit) changed neither the row orders nor the row map and the chart kept its stale total. Observe the row data of the charted rows and recompute at most once per frame. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Widgets and rows can be addressed by a view's own name (viewsByName), and opening a row from a widget works for list, board, gallery and timeline widgets, not only tables. - Fixture databases can be described by a spec (property names, types and select options with name-derived ids shared across databases). - Template-data pruning checks the removed field ids, so a database may have its own "Type" property. - registerDashboardWorld for scenarios that build their own workspace; the member browser takes an optional viewport. - A global filter chip changed in View mode is local: only the chip is checked then. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Seven features (28 scenarios) modelled on Notion's dashboard templates: sales pipeline, bug triage, team status across two databases, executive KPI review, daily planner, company home across three databases and personal finance. Given steps build databases, rows, views (filters, sorts, grouping, chart / calendar / timeline settings) and dashboards through the cloud API and one Yjs transaction per view, then wait for the server to hold them; When steps drive the real UI (picker, width handles, board drag, table cells, row pages, chart drill-down, "Filter multiple sources"). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Yjs values read inside page.evaluate are untyped, so allow `any` in the helper file, and add the missing blank line before a statement. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
On a busy machine the dev server occasionally fails a dynamic import and
the app shows one of its error screens ("Couldn’t load this page",
"Something went wrong", or the element fallback). The dashboard test bridge
now reloads the tab like a user would (at most three times per tab), so a
fixture navigation does not time out waiting for the database to mount.
It only reloads after the page reported a failed dynamic import: those
screens also catch real render errors, which must still fail the scenario.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The cell-edit observer reacted to any change in a charted row (other columns, row height, the last-modified stamp), so every edit next to a large grid made each chart re-read all rows. Watch only the X / Y cells (and the row timestamps when grouping by created or edited time), read from a ref so settings changes never re-subscribe, and keep the row-order array while its ids are unchanged so a filtered view's late re-emit does not recompute again. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ript Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The add-view menu now has a "Dashboard" entry, and Playwright's substring matchers (`hasText: 'Board'`, `getByRole` without `exact`) resolved to both items, which failed six view-management specs in CI. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Third React best-practices review of the dashboard. - The app rebuilds loadViewMeta (and with it navigateToView) whenever the trash list or the database relations change. That reached every widget's nested database and refetched every widget's view meta. Widgets now get one stable wrapper per host service that calls the latest one. - WidgetContext carries only what its consumers read (the widget id, name, icon, layout, chrome flags), so a move or a resize of another widget no longer re-renders a widget's nested shell. - The chart drill-down list is rebuilt once after its rows finish loading instead of once per loaded row (quadratic for a Number chart in Count mode, whose rows are not loaded before the popup opens). - The Number chart formatters are built once. - Losing write access leaves Edit mode during render, like a view switch. - Dashboard imports GlobalFilterBar directly instead of through the barrel. - Documented that "Save for everybody" writes the override as a snapshot (last writer wins), like every other dashboard layout write. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
# Conflicts: # src/components/database/components/settings/Layout.tsx # src/components/database/components/settings/__tests__/Layout.test.tsx # src/components/database/components/tabs/AddViewButton.tsx # src/components/database/components/tabs/__tests__/AddViewButton.test.tsx # src/components/editor/components/panels/slash-panel/SlashPanel.tsx
Vite only exposes APPFLOWY* variables from .env files to the app, and the define for EXPERIMENTAL_DATABASE_VIEW_CREATION_ENABLED read the shell environment only, so setting it in .env did nothing. The define now falls back to the .env value, so a local `pnpm dev` can opt in without exporting the variable in every shell. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…o plan The server side of the Dashboard view is merged (AppFlowy-Cloud-Premium #1191), so the 50 feature scenarios and the 28 real-world scenarios join the Playwright matrix as two groups. CI's release server refuses Dashboard creation without a Pro plan, so the fixtures seed the workspace's Pro entitlement the way the Timeline fixtures do; local debug servers skip it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Fourth React best-practices review of the dashboard. - A non-host widget resolved its source permissions only after its doc had loaded and its deletion probe had settled, and its nested database mounted read-only until then, which also held back its row prefetch: four serial round trips per widget. The permission resolver now wraps the placeholder too, so the probe runs alongside the doc load; the deletion probe starts from the ids; host widgets reuse the host's permissions instead of probing. - The workspace database catalog (only used to name other databases in the global-filter editor, and refetched on every folder change) stays off while every widget shows the host database. - The picker's option list is a memoized component with content-visibility, so a search keystroke re-renders the input alone. - The host-service key list is checked for exhaustiveness at compile time. - The pure global-filter field reader lives in its own module, so database views no longer load the per-doc observer store eagerly. - Test fixture: the database-mount poll tolerates the bridge's reload. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Hovering a row in Edit mode shows a round ↓ at its left edge that inserts a new row below it (the widget picker opens for the new row) and the round + at its right edge that adds into the row, like Notion. The ↓ is disabled with the widget-limit tooltip when the dashboard is full. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…nd and the widget menu - Filters and sorts a viewer applies inside a widget in View mode stay local until "Save for everybody", like Notion. The widget's nested database gets a proxy view (view-conditions-overlay.ts) whose filters and sorts live in a local, unsynced Yjs doc and mirror the real view until the viewer changes something; DatabaseViewOverlayContext makes useDatabaseView() return it, so every selector, menu and dispatcher works unchanged. Read-only members can use widget filters and sorts (useConditionsReadOnly). Save for everybody writes the local copies to their views as one undo step; Reset drops them; the local-changes badge counts them. - /dashboard in a document creates a new database whose first tab is a dashboard (a grid page plus a dashboard tab, like the Feed page). - Clicking a widget's title opens its menu, as right-clicking does. - Tests: overlay unit tests, dashboard page creation tests, header test; BDD scenarios for local widget filters (owner, save, read-only member), the slash commands (new and linked dashboard) and Change view. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- /dashboard: remove the Dashboard page too when creation fails after it exists (it is a sibling page of the grid under the document) - resolve a widget's View-mode conditions overlay during render so the nested database mounts once with it; re-check the store after mount so a StrictMode simulated unmount cannot leave a destroyed overlay in place - move the unsaved-widget count into DashboardLocalWidgetChangesContext so widgets stop re-rendering on every private filter edit - DatabaseViews' advanced-mode effect reads the overlay view; the group-by filter cleanup writes the shared view's filters - the widget title button takes its accessible name from the widget name - drop the dead has-[[data-state=open]] selector on row edge controls Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Widget private conditions (View-mode overlay):
- advanced-filter selectors read the viewer's private filters
- useDatabaseView returns the overlay only for the view it targets, so the
calendar draft's cloned doc keeps its own view
- the overlay is clean again once the private copy matches the shared view
- the overlay store is create-only while widgets render; stale overlays are
released after commit (no provider update during render)
- "Save for everybody" skips widgets whose source database the viewer
cannot write, and only shows when something can be saved
- the full-row prefetch counts private conditions and global filters
Global filters and charts:
- a global-filter change keeps the view's last rows until the recompute
instead of a loading frame that unmounted rows and replayed charts
- charts show the spinner while a new filter hydrates rows
- column reorders no longer regroup chart data
- the drill-down lists and loads rows a page at a time
- the calendar draft counts private and global filters as hiding a row
Shell:
- Edit mode is derived in DashboardProvider ('auto' for an empty
dashboard), removing the effect-driven flash on new dashboards
- rows take dashboard-wide state as props, so a commit re-renders only
the rows that changed
- Enter in the widget picker picks from the current query
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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 Notion-style Dashboard database view: a grid of widgets, laid out in rows, where each widget shows another database view. That view can come from this database or any other database in the workspace, and keeps its own filters, sorts and settings.
DatabaseViewLayout.Dashboard = 9andViewLayout.Dashboard = 11.layout_settings['9'].Server side: AppFlowy-IO/AppFlowy-Cloud-Premium#1191 (new layout ids, queryable layout lists, linked-view dependencies, publish and duplicate support, Pro plan check) is merged and part of cloud
0.18.57.What's included
src/components/database/dashboard/)<Database>, like the document database block.view-conditions-overlay.ts) that mirrors the real view until changed (BigInt-safe for Yrs-native condition maps, and rebinding when sync replaces the view); the dashboard keeps them across widget moves and releases them when a widget is removed or re-pointed; read-only members can use them too; "Save for everybody" writes them to the view.ChartType.Number = 4) for KPI tiles: the chart aggregations (count, sum, average, min, max, median, count values) as one number, with number formatting and a custom title.+(Pro workspaces only, like Timeline), the Layout switcher (an existing dashboard keeps its option while creation is off, like Timeline), icons, the app shell and page modal, publish, and the Dashboard (/dashboard: a new database whose first tab is a dashboard, created like the Feed page via a grid) and Linked Dashboard slash items. A dashboard can't be created as a new page; the server rejects that, as it does for List, Gallery and Feed.loadViewMeta(trash / relations changes) no longer re-renders every nested database.DashboardProvider(automatic for an empty dashboard), so a new dashboard no longer flashes View mode first.Feature Preview
Screenshots of View mode, Edit mode (handles and widget menu), the widget picker, the global filter editor and the Number tiles still need to be added; the CLI cannot upload images.
Open question
When no rows match, a Number tile shows "No rows to count", while Notion shows
0. The bug-triage scenario currently expects "No rows to count". Should it match Notion instead?Checklist
General
Testing
DashboardContext, drag-and-drop and resize hooks, the widget menu and picker, and the stacked layout;useChartData(including the cell-edit recompute), and settings;dashboard-creation,dashboard-widgets,dashboard-layout,dashboard-modes,dashboard-global-filters,dashboard-number-chartanddashboard-integrationfeature files.playwright/bdd/features/database/dashboard-usecases/, modelled on Notion's dashboard templates:pnpm test:e2e:bdd:dashboardruns both BDD suites (the web build needsEXPERIMENTAL_DATABASE_VIEW_CREATION_ENABLED=true, as CI already sets). They are in the Playwright CI matrix asdashboard-bddanddashboard-usecase-bdd; the fixtures seed the workspace's Pro plan like the Timeline fixtures. They pass once CI'slatest-amd64cloud and worker images include #1191 (cloud0.18.57or later).hasText: 'Board'), which also matched "Dashboard"; they now match exactly.tsc -p tsconfig.web.jsonandeslintpass for the whole repo.timeline.featurescenarios "Separate start and end date fields…" and "Table properties add columns with a calculations footer" fail locally on bar and column pixel positions. They fail the same way on unmodifiedorigin/main(2b1fa12) against the same server.Feature-Specific
🤖 Generated with Claude Code