Skip to content

feat: support database undo redo history - #402

Open
appflowy wants to merge 9 commits into
mainfrom
database-undo-redo-history
Open

feat: support database undo redo history#402
appflowy wants to merge 9 commits into
mainfrom
database-undo-redo-history

Conversation

@appflowy

@appflowy appflowy commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Add one database-scoped undo/redo timeline across the database Yjs document and affected row Yjs documents.
  • Align Web behavior with Desktop for captured operations, deliberately excluded operations, redo preservation, the 100-entry limit, and compound actions.
  • Own history shortcuts at the common database shell so Grid, Board, Calendar, Chart, conditions, and view tabs behave consistently while native editors keep their own undo first.
  • Add production-path action -> undo -> redo coverage, including no-history and redo-preservation checks for excluded actions.

Undo/redo behavior contract

History records committed local database actions. A logical action that changes the database document and one or more row documents is one history entry.

Captured (can undo/redo)

  • Cell values: title/RichText, Number, URL, Checkbox, Single Select, Multi Select, Checklist, Date/Time, Person, Media, manual Summary/Translate edits, and Calendar start/end edits.
  • Rows: create, duplicate, delete, reorder, Board/group card move, and row icon/cover changes.
  • Fields: create/delete/clear/options/type-options/settings for non-Relation fields; rename/icon/reorder/duplicate for every field type; and type conversion when neither the old nor new type is Relation.
  • Relation field metadata that stays local: rename, icon, reorder, and duplicate. Duplicating a two-way Relation creates a one-way copy without reciprocal metadata.
  • View configuration: basic and advanced filters, sorts, groups (group-by/clear/update/move/collapse/create/delete), calculation configuration, layout configuration, and field display settings.

Not captured (cannot undo/redo from database history)

  • Relation cell values, reciprocal Relation updates, Relation field create/delete, Relation target/limit/one-way/two-way configuration, and every type conversion to or from Relation.
  • Database view/tab/page create, rename, or delete.
  • Row document body edits, which use the document editor's own history.
  • Comments, reactions, template definitions/content, and uncommitted editor drafts.
  • AI-generated Summary/Translate writes. Manual edits to those cells remain captured.
  • Derived calculation values, derived row metadata, collaboration/remote updates, hydration, cache/search/export work, and other background/system writes.
  • External side effects outside database/row documents, such as physical files, reminders, and general folder operations.

Excluded actions, remote changes, and no-op actions do not consume history and do not clear an existing redo stack.

Row create/delete boundary

Row create and row delete are supported on both Web and Desktop. Undo/redo restores local row membership/order and retained row data. Relation prefill, backlinks, and cleanup in other rows or databases are outside the database history transaction, so undoing a row lifecycle action does not reconstruct or remove those external Relation side effects.

History and shortcuts

  • One per-database timeline preserves ordering across the database document and row documents.
  • Compound multi-document actions undo and redo atomically.
  • The timeline retains the latest 100 logical entries; a captured new action clears redo.
  • Cmd/Ctrl+Z undoes, Cmd/Ctrl+Shift+Z redoes, and non-macOS also supports Ctrl+Y.
  • Shortcuts are active only while the database surface owns focus/pointer context; native editable controls receive their native history first.

Validation

  • pnpm lint
  • pnpm exec jest --runInBand --no-coverage — 155 suites / 1,839 tests passed
  • Production-hook and rendered-component tests cover every documented captured action with action -> undo -> redo assertions, plus no-history/redo-preservation assertions for excluded policy paths.
  • git diff --check

Design references

Summary by Sourcery

Implement database-scoped undo and redo history with consistent shortcuts, policy-based exclusions, atomic cross-document actions, and comprehensive validation.

New Features:

  • Add database-scoped undo and redo history spanning database configuration and row documents.
  • Provide consistent database history shortcuts across database views while preserving native editor undo behavior.

Bug Fixes:

  • Exclude relation operations, derived/system updates, AI-generated writes, view tab lifecycle changes, and other unsupported actions from local database history without disrupting redo.

Enhancements:

  • Support atomic compound actions, global ordering across row documents, row lifecycle history, redo preservation, and a 100-entry history limit.
  • Align Web database history behavior with Desktop, including field metadata, view configuration, calendar and board actions, and cell editing semantics.

CI:

  • Extend Playwright CI matrix and BDD generation to cover database row and undo/redo workflows.

Tests:

  • Add unit, rendered-component, and production-path coverage for captured actions, excluded operations, compound actions, shortcut ownership, redo preservation, and history limits.

@sourcery-ai

sourcery-ai Bot commented Jun 15, 2026

Copy link
Copy Markdown

Reviewer's Guide

Implements a unified Yjs-based database/row undo-redo system with hotkey handling, central history management, and comprehensive Playwright BDD tests, while ensuring relation-cell edits are excluded from history and that all UI changes (cells, rows, fields, filters, sorts, groups, calculations) participate in a global history timeline.

Sequence diagram for grid undo hotkey using unified history

sequenceDiagram
  actor User
  participant GridProvider
  participant useDatabaseRowHistoryHotkeys
  participant useDatabaseHistory
  participant DatabaseHistoryManager
  participant DatabaseHistorySourceController as RowHistorySource
  participant YUndoManager as Y_UndoManager

  User->>GridProvider: keydown (Cmd/Ctrl+Z)
  GridProvider->>useDatabaseRowHistoryHotkeys: useDatabaseRowHistoryHotkeys(undefined, { enabled, useLatest: true })
  useDatabaseRowHistoryHotkeys->>useDatabaseHistory: useDatabaseHistory(undefined)
  useDatabaseHistory->>DatabaseHistoryManager: getOrCreateDatabaseHistoryManager(databaseDoc)
  useDatabaseHistory-->>useDatabaseRowHistoryHotkeys: { canUndo, undo, redo }

  useDatabaseRowHistoryHotkeys->>useDatabaseRowHistoryHotkeys: handleKeyDown(event)
  useDatabaseRowHistoryHotkeys->>DatabaseHistoryManager: undo()
  DatabaseHistoryManager->>RowHistorySource: undo()
  RowHistorySource->>Y_UndoManager: undo()
  Y_UndoManager-->>RowHistorySource: StackItem
  RowHistorySource-->>DatabaseHistoryManager: StackItem
  DatabaseHistoryManager-->>useDatabaseRowHistoryHotkeys: updated canUndo/canRedo
  useDatabaseRowHistoryHotkeys-->>GridProvider: re-render via subscription
  GridProvider-->>User: grid UI updated (cell/row/field/etc.)
Loading

File-Level Changes

Change Details Files
Introduce centralized Yjs-backed database and row history manager with policy-aware capture and React hooks for undo/redo.
  • Add history manager that aggregates per-database and per-row Y.UndoManager instances into a single ordered undo/redo stack.
  • Define history origin types and policy helpers to decide whether actions are captured (including auto-skipping relation operations).
  • Expose helpers to run database and row actions (runDatabaseAction/runDatabaseRowAction) and a history-aware executeDatabaseOperations wrapper for batched operations.
  • Provide React hooks (useDatabaseHistory, useDatabaseRowHistory, useLatestDatabaseRowHistory) that wire history state into components and register row docs with the manager.
src/application/database-yjs/history.ts
src/application/database-yjs/__tests__/history.test.ts
Wire history manager into database dispatch and relation logic so structural and cell operations participate in undo/redo with appropriate policies.
  • Replace direct rowDoc.transact/databaseDoc.transact calls for cell, row meta, relation, row creation, row clear/duplicate, and calculation updates with runDatabaseRowAction or runDatabaseAction including action metadata and skip policies where needed.
  • Update calculation dispatch to use executeDatabaseOperations from the new history module, tagging operations with a non-capturing action where appropriate.
  • Ensure relation-specific operations (e.g., reciprocal field maintenance and relation cell clear/update) are tagged with policy: 'skip' or relation.* types so they are excluded from history.
src/application/database-yjs/dispatch.ts
src/application/database-yjs/dispatch/cell.ts
src/application/database-yjs/dispatch/relation.ts
src/application/database-yjs/dispatch/calculation.ts
src/application/database-yjs/dispatch/row.ts
src/application/database-yjs/dispatch/group.ts
src/application/database-yjs/dispatch/sort-filter.ts
src/application/database-yjs/dispatch/utils.ts
Expose history utilities to tests and UI, and add global database/row undo-redo hotkey handling for grid and row-detail views.
  • Export history utilities from the database-yjs index and attach them to the window test context (TEST_DATABASE_HISTORY) alongside the existing Yjs test hooks.
  • Add a useDatabaseRowHistoryHotkeys hook that listens for undo/redo hotkeys (mod+z / mod+shift+z / ctrl+y) and triggers database-scoped or row-scoped history based on options.
  • Integrate the hotkey hook into DatabaseRow (row detail page) and GridProvider, with focus tracking and readOnly checks to ensure hotkeys only act when the grid/row is active.
  • Add new UNDO/REDO entries to the generic hotkey map and adjust TextCellEditing so that undo/redo keystrokes bubble to the history system while still suppressing other keys when editing.
src/application/database-yjs/index.ts
src/components/database/DatabaseContext.tsx
src/components/database/DatabaseRow.tsx
src/components/database/grid/GridProvider.tsx
src/components/database/hooks/useDatabaseRowHistoryHotkeys.ts
src/components/database/hooks/index.ts
src/utils/hotkeys.ts
src/components/database/components/cell/text/TextCellEditing.tsx
Add end-to-end BDD scenarios and step definitions validating database/row undo-redo behavior and UI state across many operation types.
  • Define a comprehensive Playwright BDD feature covering row title edits, grid cell edits, row insertion, reordering, field lifecycle, filters, sorts, groups, calculations, and relation exclusion semantics in the context of undo/redo.
  • Implement step definitions that seed Yjs docs directly, invoke history-aware helpers (runDatabaseAction/runDatabaseRowAction), and assert both Yjs state and rendered grid/row UI after each undo/redo step.
  • Provide helper utilities in the step file to synthesize operations such as creating fields/filters/sorts/groups/calculations, editing cells, moving rows, and manipulating relation cells with or without history capture.
playwright/bdd/features/database/row-undo-redo.feature
playwright/bdd/steps/database-row-undo-redo.steps.ts

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@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.

Hey - I've found 1 issue, and left some high level feedback:

  • In useDatabaseRowHistoryHotkeys, the useEffect depends on canUndo/canRedo, causing the global keydown listener to be torn down and re-attached on every history state change; consider using refs or reading from the history manager inside the handler to avoid this churn while still using the latest state.
  • The executeDatabaseOperations helper always wraps operations in console.time/console.timeEnd, which will log in production; consider guarding these with an environment check or removing them to avoid noisy logs in non-dev environments.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `useDatabaseRowHistoryHotkeys`, the `useEffect` depends on `canUndo`/`canRedo`, causing the global `keydown` listener to be torn down and re-attached on every history state change; consider using refs or reading from the history manager inside the handler to avoid this churn while still using the latest state.
- The `executeDatabaseOperations` helper always wraps operations in `console.time/console.timeEnd`, which will log in production; consider guarding these with an environment check or removing them to avoid noisy logs in non-dev environments.

## Individual Comments

### Comment 1
<location path="src/application/database-yjs/history.ts" line_range="61-62" />
<code_context>
+    scope: Y.AbstractType<Y.YMapEvent<unknown>>,
+    readonly rowId?: RowId
+  ) {
+    this.undoManager = new Y.UndoManager(scope, {
+      trackedOrigins: new Set([DatabaseHistoryOrigin, DatabaseRowHistoryOrigin]),
+      captureTimeout: 0,
+    });
</code_context>
<issue_to_address>
**issue (bug_risk):** The undo manager `trackedOrigins` set won’t match the origin instances you pass into transactions, so history entries will not be recorded as intended.

In `DatabaseHistorySourceController`, `trackedOrigins` is a `Set` of classes (`DatabaseHistoryOrigin`, `DatabaseRowHistoryOrigin`), but `runDatabaseAction` / `runDatabaseRowAction` pass *instances* returned from `createDatabaseHistoryOrigin` / `createDatabaseRowHistoryOrigin` into `doc.transact`. Yjs compares `origin` by identity, so those instances will never match the class values in `trackedOrigins`, and nothing will be captured by this undo manager.

You can either:
- Drop `trackedOrigins` and just use origins for grouping, or
- Use shared origin objects/symbols that you both put in `trackedOrigins` and pass to `transact` (one shared instance per origin type if you need to distinguish row vs database).

As written, the undo manager’s stacks will stay empty, so undo/redo will not work as intended.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread src/application/database-yjs/history.ts Outdated
Comment on lines +61 to +62
this.undoManager = new Y.UndoManager(scope, {
trackedOrigins: new Set([DatabaseHistoryOrigin, DatabaseRowHistoryOrigin]),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (bug_risk): The undo manager trackedOrigins set won’t match the origin instances you pass into transactions, so history entries will not be recorded as intended.

In DatabaseHistorySourceController, trackedOrigins is a Set of classes (DatabaseHistoryOrigin, DatabaseRowHistoryOrigin), but runDatabaseAction / runDatabaseRowAction pass instances returned from createDatabaseHistoryOrigin / createDatabaseRowHistoryOrigin into doc.transact. Yjs compares origin by identity, so those instances will never match the class values in trackedOrigins, and nothing will be captured by this undo manager.

You can either:

  • Drop trackedOrigins and just use origins for grouping, or
  • Use shared origin objects/symbols that you both put in trackedOrigins and pass to transact (one shared instance per origin type if you need to distinguish row vs database).

As written, the undo manager’s stacks will stay empty, so undo/redo will not work as intended.

appflowy added 7 commits June 15, 2026 21:55
…story

# Conflicts:
#	.github/workflows/playwright-test.yml
#	src/application/database-yjs/__tests__/field-type-conversion.test.tsx
#	src/application/database-yjs/__tests__/useAddDatabaseView.test.tsx
#	src/application/database-yjs/dispatch.ts
#	src/application/database-yjs/dispatch/cell.ts
#	src/application/database-yjs/dispatch/group.ts
#	src/application/database-yjs/dispatch/relation.ts
#	src/application/database-yjs/dispatch/row.ts
#	src/application/database-yjs/dispatch/sort-filter.ts
#	src/components/database/DatabaseContext.tsx
#	src/components/database/DatabaseViews.tsx
#	src/components/database/grid/GridProvider.tsx
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