Skip to content

feat(database): add mouse click-and-drag multi-row selection in grid …#8773

Open
PaiavullaNikhil wants to merge 2 commits into
AppFlowy-IO:mainfrom
PaiavullaNikhil:feat/grid-multi-row-selection
Open

feat(database): add mouse click-and-drag multi-row selection in grid …#8773
PaiavullaNikhil wants to merge 2 commits into
AppFlowy-IO:mainfrom
PaiavullaNikhil:feat/grid-multi-row-selection

Conversation

@PaiavullaNikhil
Copy link
Copy Markdown

@PaiavullaNikhil PaiavullaNikhil commented May 28, 2026

…view (#8771)

Feature Preview


PR Checklist

  • My code adheres to AppFlowy's Conventions
  • I've listed at least one issue that this PR fixes in the description above.
  • I've added a test(s) to validate changes in this PR, or this PR only contains semantic changes.
  • All existing tests are passing.

Summary by Sourcery

Add row multi-selection support to the database grid and integrate it with keyboard shortcuts and row actions.

New Features:

  • Enable mouse click-and-drag selection of multiple rows in the grid view via the leading gutter area.
  • Introduce a shared grid row selection controller to manage selected rows across the grid, row widgets, and action menus.
  • Highlight selected rows in the grid UI to visually indicate the current selection.
  • Add keyboard shortcuts for grid selection management, including delete/backspace to delete selected rows with confirmation, Escape to clear selection, and Ctrl/Cmd+A to select all rows.
  • Allow the row context menu delete action to operate on the entire current selection when invoked on a selected row.

@sourcery-ai
Copy link
Copy Markdown
Contributor

sourcery-ai Bot commented May 28, 2026

Reviewer's Guide

Implements a reusable grid row selection controller to support mouse click-and-drag multi-row selection, keyboard-based bulk operations (delete, clear, select all), and integrates visual and contextual behavior changes across grid rows and row actions.

Sequence diagram for keyboard bulk delete using grid selection controller

sequenceDiagram
  actor User
  participant GridShortcuts
  participant GridSelectionController
  participant GridBloc
  participant RowBackendService
  participant Dialogs as showConfirmDeletionDialog

  User->>GridShortcuts: press Delete/Backspace
  GridShortcuts->>GridSelectionController: selectedRowIds
  GridShortcuts->>GridSelectionController: hasSelection
  alt [hasSelection]
    GridShortcuts->>GridBloc: viewId
    GridShortcuts->>Dialogs: showConfirmDeletionDialog(onConfirm)
    User->>Dialogs: confirm
    Dialogs->>RowBackendService: deleteRows(viewId, selectedIds)
    Dialogs->>GridSelectionController: clearSelection()
  end
Loading

File-Level Changes

Change Details Files
Introduce GridSelectionController to manage multi-row selection state and range selection logic for grid rows.
  • Added a ChangeNotifier-based GridSelectionController that tracks selected row IDs and last selected row, supports single, multi-, and range selection via selectRow, and range selection by indices via selectRange.
  • Exposed helpers to clear selection, select all, and query selection state (isSelected, hasSelection, selectedRowIds).
frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/selection_controller.dart
Wire GridSelectionController into GridPage lifecycle and provide it via Provider to descendants.
  • Instantiate GridSelectionController in _GridPageState using GridBloc.rowInfos as the backing data source and dispose it with the page.
  • Wrap the existing BlocListener/BlocConsumer tree in a ChangeNotifierProvider so children can access the selection state.
frontend/appflowy_flutter/lib/plugins/database/grid/presentation/grid_page.dart
Add mouse click-and-drag behavior on the row leading gutter area to select contiguous row ranges.
  • Wrap the grid rows list child in a Listener to handle pointer down/move/up/cancel events.
  • Detect clicks within the leading gutter using layout padding, map pointer Y position plus scroll offset into a row index using compact/non-compact row heights, and perform single/multi/range selection based on keyboard modifiers (shift/meta/ctrl).
  • Track drag start index and update selection via GridSelectionController.selectRange while dragging.
frontend/appflowy_flutter/lib/plugins/database/grid/presentation/grid_page.dart
Update keyboard shortcuts to operate on the current row selection for bulk delete, clearing selection, and select-all.
  • Replace the previous Shortcuts/Actions plumbing with CallbackShortcuts bindings for Delete, Backspace, Escape, and Ctrl/Cmd+A key combinations.
  • Implement _deleteSelectedRows with focus-guard to avoid firing when a text input is active, showing a confirmation dialog and deleting all selected rows via RowBackendService.deleteRows, then clearing selection.
  • Add helpers to clear selection and select all rows through GridSelectionController.
frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/shortcuts.dart
Visually reflect row selection state and integrate selection with the row action menu delete behavior.
  • Wrap GridRow content in a Consumer of GridSelectionController and apply a primary-colored background when the row is selected.
  • Provide both GridBloc and GridSelectionController to RowActionMenu via MultiProvider from the row leading widget.
  • Change RowAction.delete to delete either all currently selected rows (if the clicked row is selected) or just the single row, and clear the selection after a bulk delete.
frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/row/row.dart
frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/row/action.dart

Possibly linked issues


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

@CLAassistant
Copy link
Copy Markdown

CLAassistant commented May 28, 2026

CLA assistant check
All committers have signed the CLA.

Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 3 issues, and left some high level feedback:

  • The drag selection logic in _GridRowsState hardcodes rowHeight (32/36) and does its own y→row index math; consider reusing the same sizing source used by the row layout (or a shared constant/helper) so selection stays accurate if row height or layout changes.
  • GridSelectionController.selectRow and selectRange both implement range-selection behavior in slightly different ways and only selectRow updates _lastSelectedRowId; consider centralizing the range-selection logic and keeping _lastSelectedRowId consistent so shift-click, drag, and subsequent range selections behave predictably.
  • In _RowLeadingState the RowActionMenu wraps the existing GridSelectionController again with ChangeNotifierProvider.value; since the controller is already available higher up, you can remove this extra provider and just rely on the inherited one to simplify the tree and avoid redundant wiring.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The drag selection logic in `_GridRowsState` hardcodes `rowHeight` (32/36) and does its own y→row index math; consider reusing the same sizing source used by the row layout (or a shared constant/helper) so selection stays accurate if row height or layout changes.
- `GridSelectionController.selectRow` and `selectRange` both implement range-selection behavior in slightly different ways and only `selectRow` updates `_lastSelectedRowId`; consider centralizing the range-selection logic and keeping `_lastSelectedRowId` consistent so shift-click, drag, and subsequent range selections behave predictably.
- In `_RowLeadingState` the `RowActionMenu` wraps the existing `GridSelectionController` again with `ChangeNotifierProvider.value`; since the controller is already available higher up, you can remove this extra provider and just rely on the inherited one to simplify the tree and avoid redundant wiring.

## Individual Comments

### Comment 1
<location path="frontend/appflowy_flutter/lib/plugins/database/grid/presentation/grid_page.dart" line_range="506-507" />
<code_context>
+          
+          final selection = context.read<GridSelectionController>();
+          final rowInfos = selection.getRowInfos();
+          final clampedIndex = currentRowIndex.clamp(0, rowInfos.length - 1);
+          selection.selectRange(_dragStartRowIndex!, clampedIndex);
+        }
+      },
</code_context>
<issue_to_address>
**issue (bug_risk):** Fix type mismatch from `clamp` returning `num` when passing indices to `selectRange`.

`clamp` returns `num` while `selectRange` requires `int` indices, so this won’t compile without a cast. Use something like `final clampedIndex = currentRowIndex.clamp(0, rowInfos.length - 1).toInt();` and ensure `rowInfos` is non-empty before computing the clamped index (per previous comment).
</issue_to_address>

### Comment 2
<location path="frontend/appflowy_flutter/lib/plugins/database/grid/presentation/grid_page.dart" line_range="470-471" />
<code_context>
     }

+    final horizontalPadding = context.read<DatabasePluginWidgetBuilderSize>().horizontalPadding;
+    final compactMode = context.read<GridBloc>().databaseController.compactModeNotifier.value;
+    final rowHeight = compactMode ? 32.0 : 36.0;
+
+    child = Listener(
</code_context>
<issue_to_address>
**suggestion:** Avoid hardcoding row height values and instead derive them from the row layout source of truth.

Using literal `32.0`/`36.0` here couples selection behavior to magic numbers that may drift from the actual row height if padding, font size, or themes change. Prefer reusing a shared row-height constant or size helper (as with other layout helpers in this module) so hit testing and visuals remain aligned.

Suggested implementation:

```
    final size = context.read<DatabasePluginWidgetBuilderSize>();
    final horizontalPadding = size.horizontalPadding;
    final rowHeight = size.rowHeight;

```

If `DatabasePluginWidgetBuilderSize` does not yet expose a `rowHeight` (or equivalent) property, you will need to:
1. Add a `rowHeight` (or appropriately named) getter/field to `DatabasePluginWidgetBuilderSize`, derived from the same source of truth as the actual grid row widgets (likely taking compact/regular mode into account there).
2. Ensure all row renderers use this same `rowHeight` value so selection hit testing and visual row size stay in sync.
</issue_to_address>

### Comment 3
<location path="frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/selection_controller.dart" line_range="8-11" />
<code_context>
+  GridSelectionController({required this.getRowInfos});
+
+  final List<RowInfo> Function() getRowInfos;
+  final Set<String> _selectedRowIds = {};
+  String? _lastSelectedRowId;
+
+  Set<String> get selectedRowIds => _selectedRowIds;
+  bool get hasSelection => _selectedRowIds.isNotEmpty;
+
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Expose `selectedRowIds` as an unmodifiable view to protect internal state.

Because this getter exposes the backing `Set`, callers can mutate selection state without going through the controller, bypassing `notifyListeners()` and violating invariants. Consider returning an `UnmodifiableSetView` or a defensive copy instead so the controller remains the single source of truth for selection changes.

Suggested implementation:

```
import 'dart:collection';

import 'package:flutter/widgets.dart';
import 'package:appflowy/plugins/database/application/row/row_cache.dart';

```

```
  final Set<String> _selectedRowIds = {};
  String? _lastSelectedRowId;

  UnmodifiableSetView<String> get selectedRowIds =>
      UnmodifiableSetView(_selectedRowIds);
  bool get hasSelection => _selectedRowIds.isNotEmpty;

```
</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 frontend/appflowy_flutter/lib/plugins/database/grid/presentation/grid_page.dart Outdated
Comment thread frontend/appflowy_flutter/lib/plugins/database/grid/presentation/grid_page.dart Outdated
@PaiavullaNikhil
Copy link
Copy Markdown
Author

feat(database): add mouse click-and-drag multi-row selection and keyboard shortcuts in grid view

Description

This PR introduces mouse click-and-drag multi-row selection, standard keyboard shortcuts for row deletion/clearing/selection, and a centralized GridSelectionController state manager to the Database Grid view.


Key Changes

1. 🎛️ Centralized Selection State (GridSelectionController)

  • Added selection_controller.dart which exposes:
    • selectRow: Handles standard single/multi-selection and Range selection (Shift + Click).
    • selectRange: Handled range selection by index (e.g. from drag selections).
    • selectAll: Selects all row IDs.
    • clearSelection: Clears selection.
  • Properly tracks _lastSelectedRowId to allow extending range selections predictably.

2. 🖱️ Click-and-Drag Multi-Row Selection

  • Integrated index-based drag selection calculations into _GridRowsState (grid_page.dart).
  • Replaced hardcoded row height values (32/36) with centralized layout constants GridSize.rowHeight and GridSize.compactRowHeight in sizes.dart, ensuring that calculation stays accurate if the row heights change or compact mode is toggled.

3. ⌨️ Grid Shortcuts (GridShortcuts)

  • Refactored shortcuts.dart to map essential shortcuts using CallbackShortcuts:
    • Delete / Backspace: Prompts confirmation dialog and deletes all selected rows.
    • Escape: Clears row selection.
    • Ctrl+A / Cmd+A: Selects all rows.
  • Prevents shortcuts from firing when text inputs or text fields are active/focused.

4. 🪟 Context/Overlay Support

  • Explicitly passed GridSelectionController to the RowActionMenu popover context using ChangeNotifierProvider.value. This ensures that context lookups do not fail when entering the overlay context tree.

Verification Plan

These changes need to be verified manually:

  1. Click-and-Drag: Select multiple rows by clicking and dragging vertically in the grid.
  2. Keyboard Actions:
    • Press Ctrl+A / Cmd+A to select all rows.
    • Press Delete/Backspace to open the confirmation dialog and bulk-delete rows.
    • Press Escape to clear selection.
  3. Shift+Click (Range Select): Click a row, hold Shift, and click another row to select the range.
  4. Input fields check: Verify that typing and deleting characters inside cell editors still behaves normally and does not trigger row deletions or selections.

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.

2 participants