Skip to content

feat(database): add Notion-style dashboard view - #563

Open
appflowy wants to merge 28 commits into
mainfrom
feat/dashboard-view
Open

appflowy wants to merge 28 commits into
mainfrom
feat/dashboard-view

Conversation

@appflowy

@appflowy appflowy commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

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.

  • Layout ids: DatabaseViewLayout.Dashboard = 9 and ViewLayout.Dashboard = 11.
  • The dashboard's state is stored in 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.

⚠️ Rollout order: deploy the server's appflowy_worker from #1191 before Dashboard creation is enabled anywhere.

  • An older worker can't decode folder layout 11. It dead-letters the folder change and blocks folder writes in that workspace (error code -5).
  • Dashboard creation sits behind the same EXPERIMENTAL_DATABASE_VIEW_CREATION_ENABLED flag as Form and Timeline (fix: gate web Form and Timeline creation with CI opt-in #562): older desktop clients can't open workspaces that contain these views, so production builds hide the creation entry points and CI opts in. Existing dashboards still open and work.

What's included

  • Widgets (src/components/database/dashboard/)
    • Each widget mounts its own nested <Database>, like the document database block.
    • A widget shows a placeholder while loading, and a clear state when its view is missing, has been deleted, or the user has no access to it.
  • View and Edit modes
    • View mode is for reading and working with the data inside widgets. Filters and sorts a viewer applies inside a widget in View mode stay local to them (Notion parity): the widget's nested database edits a local, unsynced copy of the view's filters/sorts (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.
    • Edit mode adds a header to each widget with a drag handle and an options menu (open, change view, duplicate, move left/right/up/down, delete); clicking a widget's title or right-clicking it opens the menu, like Notion.
    • It also adds drag-and-drop within a row, into another row, or between rows (which starts a new row), width handles on a 12-column grid, row-height handles (mouse, touch and keyboard), and an add-widget picker for existing views or a new view in any database.
    • Notion's row edge controls: hovering a row shows a round ↓ at its left edge (insert a new row below it) and a round + at its right edge (add into the row).
    • Limits match Notion: 12 widgets in total and 4 per row. The dashboard explains the limit when an add is refused.
    • An empty dashboard opens in Edit mode.
    • Edit mode is local to each viewer and is never saved.
    • Widgets stack into one column on narrow screens.
  • Global filters (Notion's "Filter multiple sources")
    • One filter maps one property in each source database. It is combined (AND) with each widget's own view filters.
    • Supported property types: text, number, select, multi-select, checkbox, date, person, URL, checklist, created and last edited time, and created by and last edited by.
    • Changes made in View mode stay local ("Only you see these changes") until an editor chooses Save for everybody.
    • Removing the last widget of a database removes that database from every filter, in the same undo step.
  • Number chart (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.
  • Charts
    • Axis ticks are rounded.
    • By default the X axis follows the view's property order, as on desktop.
    • Charts recompute when a cell they read is edited in place, for example a value edited in a table widget next to a Number tile.
  • Where dashboards appear: view tabs + (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.
  • Performance: six React best-practices review passes.
    • A widget's source permissions are resolved alongside its doc load (host widgets reuse the host's), so its nested database mounts with real permissions and starts its row prefetch at once instead of after four serial round trips.
    • The workspace database catalog is fetched only for dashboards that show another database.
    • Widgets read only stable contexts (host services, UI plumbing, the dragged widget id) and receive Edit state as props. A resize, a drag, a change to the host database's rows, or the app rebuilding loadViewMeta (trash / relations changes) no longer re-renders every nested database.
    • The chart drill-down list is rebuilt once after its rows load, not once per loaded row.
    • The layout store reuses unchanged row, widget and filter objects.
    • The dashboard toolbar is lazy-loaded.
    • Number charts that only count rows skip loading the rows.
    • Global filter sources are cached per doc.
    • A widget's View-mode conditions overlay is resolved during render (StrictMode-safe), so its nested database mounts once, with it; the unsaved-widget count lives in its own context so private filter edits re-render the toolbar, not every widget.
    • A global-filter change keeps each widget's last rows until the recompute lands, instead of a loading frame that unmounted every row and replayed chart animations.
    • Widgets narrowed only by private or global filters fetch all rows in one request, like any filtered view, instead of hydrating them 24 at a time.
    • Edit mode is derived in DashboardProvider (automatic for an empty dashboard), so a new dashboard no longer flashes View mode first.
    • Rows take dashboard-wide state as props: resizing or moving a widget re-renders only the rows that changed.
    • The Number-chart drill-down lists and loads 100 rows at a time; column reorders no longer regroup chart data.
  • Review fixes (sixth pass):
    • Advanced filters in a View-mode widget show the viewer's private filters.
    • The private-filter overlay stands in only for the view it was made for (the calendar's draft copy keeps its own view).
    • A private copy that matches the shared view again is clean (no stale "unsaved" bar).
    • "Save for everybody" skips widgets whose source database the viewer can only read, and only appears when something can be saved.
    • Charts show a spinner while a new filter hydrates rows; a calendar draft hidden by private or global filters is released.
    • Enter in the widget picker picks from the current query.

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

  • I've included relevant documentation or comments for the changes introduced.
  • I've tested the changes in multiple environments (e.g., different browsers, operating systems).

Testing

  • I've added or updated tests to validate the changes introduced for AppFlowy Web.
    • jest: 47 test files added or updated, covering:
      • layout operations and normalization, structural sharing, the virtual filter list and extra filters;
      • DashboardContext, drag-and-drop and resize hooks, the widget menu and picker, and the stacked layout;
      • global filter utils, sources, bar and menu;
      • Number chart and chart utils, useChartData (including the cell-edit recompute), and settings;
      • tabs, the Pro gate and the slash layout;
      • the View-mode conditions overlay, its store and database context, and the chart drill-down paging.
    • Playwright BDD, 57 feature scenarios:
      • dashboard-creation, dashboard-widgets, dashboard-layout, dashboard-modes, dashboard-global-filters, dashboard-number-chart and dashboard-integration feature files.
    • Playwright BDD, 28 real-world scenarios in playwright/bdd/features/database/dashboard-usecases/, modelled on Notion's dashboard templates:
      • sales pipeline, bug triage, team status hub, executive KPI review, company home page, daily planner and personal finance;
      • they cover building a dashboard from existing views, working inside widgets (board drag, table edits, new rows, row pages), chart drill-down, dashboard-wide filters across databases, view-only teammates, live updates from a collaborator, and the phone layout.
    • pnpm test:e2e:bdd:dashboard runs both BDD suites (the web build needs EXPERIMENTAL_DATABASE_VIEW_CREATION_ENABLED=true, as CI already sets). They are in the Playwright CI matrix as dashboard-bdd and dashboard-usecase-bdd; the fixtures seed the workspace's Pro plan like the Timeline fixtures. They pass once CI's latest-amd64 cloud and worker images include #1191 (cloud 0.18.57 or later).
    • Six existing view-management specs matched the Board menu item by substring (hasText: 'Board'), which also matched "Dashboard"; they now match exactly.
    • Results (web dev server, and a cloud and worker built from #1191):
      • All 85 dashboard scenarios pass.
      • tsc -p tsconfig.web.json and eslint pass for the whole repo.
      • Full jest: 5,507 of 5,507 tests pass.
    • Known failures not caused by this branch:
      • The timeline.feature scenarios "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 unmodified origin/main (2b1fa12) against the same server.

Feature-Specific

  • For feature additions, I've added a preview (video, screenshot, or demo) in the "Feature Preview" section.
  • I've verified that this feature integrates seamlessly with existing functionality.

🤖 Generated with Claude Code

appflowy and others added 17 commits September 17, 2026 04:24
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>

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, we are unable to review this pull request

The GitHub API does not allow us to fetch diffs exceeding 20000 lines

appflowy and others added 11 commits September 18, 2026 00:44
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant