Browse Source

Fix filter test (#1459)

* chore: move grid_view_editor.rs to view_editor folder

* chore: hide invisible rows

* fix: lock issue

* fix: flutter test potential failed

* chore: separate group tests

Co-authored-by: nathan <[email protected]>
Nathan.fooo 2 years ago
parent
commit
fc10ee2d6b
77 changed files with 1613 additions and 1421 deletions
  1. 14 7
      frontend/app_flowy/lib/core/grid_notification.dart
  2. 3 3
      frontend/app_flowy/lib/plugins/board/application/board_listener.dart
  3. 1 0
      frontend/app_flowy/lib/plugins/board/application/card/card_bloc.dart
  4. 2 2
      frontend/app_flowy/lib/plugins/board/application/group_listener.dart
  5. 1 0
      frontend/app_flowy/lib/plugins/board/presentation/board_page.dart
  6. 2 2
      frontend/app_flowy/lib/plugins/grid/application/block/block_cache.dart
  7. 7 5
      frontend/app_flowy/lib/plugins/grid/application/block/block_listener.dart
  8. 2 2
      frontend/app_flowy/lib/plugins/grid/application/cell/cell_listener.dart
  9. 2 2
      frontend/app_flowy/lib/plugins/grid/application/field/field_listener.dart
  10. 2 2
      frontend/app_flowy/lib/plugins/grid/application/field/grid_listener.dart
  11. 2 2
      frontend/app_flowy/lib/plugins/grid/application/filter/filter_bloc.dart
  12. 2 2
      frontend/app_flowy/lib/plugins/grid/application/filter/filter_listener.dart
  13. 58 28
      frontend/app_flowy/lib/plugins/grid/application/row/row_cache.dart
  14. 2 2
      frontend/app_flowy/lib/plugins/grid/application/row/row_listener.dart
  15. 2 2
      frontend/app_flowy/lib/plugins/grid/application/setting/setting_listener.dart
  16. 7 4
      frontend/app_flowy/lib/plugins/trash/application/trash_listener.dart
  17. 3 2
      frontend/app_flowy/test/bloc_test/board_test/create_card_test.dart
  18. 17 15
      frontend/app_flowy/test/bloc_test/board_test/create_or_edit_field_test.dart
  19. 45 0
      frontend/app_flowy/test/bloc_test/board_test/group_by_checkbox_field_test.dart
  20. 0 231
      frontend/app_flowy/test/bloc_test/board_test/group_by_field_test.dart
  21. 95 0
      frontend/app_flowy/test/bloc_test/board_test/group_by_multi_select_field_test.dart
  22. 51 0
      frontend/app_flowy/test/bloc_test/board_test/group_by_unsupport_field_test.dart
  23. 156 4
      frontend/app_flowy/test/bloc_test/board_test/util.dart
  24. 22 7
      frontend/app_flowy/test/bloc_test/grid_test/field_edit_bloc_test.dart
  25. 85 5
      frontend/app_flowy/test/bloc_test/grid_test/filter_bloc_test.dart
  26. 4 3
      frontend/app_flowy/test/bloc_test/grid_test/grid_bloc_test.dart
  27. 13 12
      frontend/app_flowy/test/bloc_test/grid_test/grid_header_bloc_test.dart
  28. 2 2
      frontend/app_flowy/test/bloc_test/grid_test/select_option_bloc_test.dart
  29. 52 116
      frontend/app_flowy/test/bloc_test/grid_test/util.dart
  30. 126 289
      frontend/app_flowy/test/bloc_test/home_test/app_bloc_test.dart
  31. 69 0
      frontend/app_flowy/test/bloc_test/home_test/create_page_test.dart
  32. 70 149
      frontend/app_flowy/test/bloc_test/home_test/trash_bloc_test.dart
  33. 1 0
      frontend/rust-lib/Cargo.lock
  34. 1 0
      frontend/rust-lib/flowy-grid/Cargo.toml
  35. 6 6
      frontend/rust-lib/flowy-grid/src/dart_notification.rs
  36. 2 2
      frontend/rust-lib/flowy-grid/src/entities/block_entities.rs
  37. 0 1
      frontend/rust-lib/flowy-grid/src/entities/filter_entities/checkbox_filter.rs
  38. 0 1
      frontend/rust-lib/flowy-grid/src/entities/filter_entities/date_filter.rs
  39. 1 1
      frontend/rust-lib/flowy-grid/src/entities/filter_entities/filter_changeset.rs
  40. 0 2
      frontend/rust-lib/flowy-grid/src/entities/filter_entities/number_filter.rs
  41. 0 1
      frontend/rust-lib/flowy-grid/src/entities/filter_entities/select_option_filter.rs
  42. 0 1
      frontend/rust-lib/flowy-grid/src/entities/filter_entities/text_filter.rs
  43. 2 0
      frontend/rust-lib/flowy-grid/src/entities/filter_entities/util.rs
  44. 3 3
      frontend/rust-lib/flowy-grid/src/event_handler.rs
  45. 2 2
      frontend/rust-lib/flowy-grid/src/event_map.rs
  46. 3 6
      frontend/rust-lib/flowy-grid/src/manager.rs
  47. 3 3
      frontend/rust-lib/flowy-grid/src/services/block_manager.rs
  48. 15 15
      frontend/rust-lib/flowy-grid/src/services/cell/any_cell_data.rs
  49. 6 6
      frontend/rust-lib/flowy-grid/src/services/cell/cell_operation.rs
  50. 2 2
      frontend/rust-lib/flowy-grid/src/services/field/type_options/checkbox_type_option/checkbox_filter.rs
  51. 2 2
      frontend/rust-lib/flowy-grid/src/services/field/type_options/date_type_option/date_filter.rs
  52. 2 2
      frontend/rust-lib/flowy-grid/src/services/field/type_options/number_type_option/number_filter.rs
  53. 3 3
      frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/select_filter.rs
  54. 3 3
      frontend/rust-lib/flowy-grid/src/services/field/type_options/text_type_option/text_filter.rs
  55. 2 2
      frontend/rust-lib/flowy-grid/src/services/field/type_options/url_type_option/url_filter.rs
  56. 2 2
      frontend/rust-lib/flowy-grid/src/services/field/type_options/util/cell_data_util.rs
  57. 13 2
      frontend/rust-lib/flowy-grid/src/services/filter/cache.rs
  58. 74 158
      frontend/rust-lib/flowy-grid/src/services/filter/controller.rs
  59. 87 0
      frontend/rust-lib/flowy-grid/src/services/filter/entities.rs
  60. 2 0
      frontend/rust-lib/flowy-grid/src/services/filter/mod.rs
  61. 11 6
      frontend/rust-lib/flowy-grid/src/services/filter/task.rs
  62. 11 6
      frontend/rust-lib/flowy-grid/src/services/grid_editor.rs
  63. 1 1
      frontend/rust-lib/flowy-grid/src/services/grid_editor_trait_impl.rs
  64. 1 2
      frontend/rust-lib/flowy-grid/src/services/mod.rs
  65. 46 0
      frontend/rust-lib/flowy-grid/src/services/view_editor/changed_notifier.rs
  66. 68 194
      frontend/rust-lib/flowy-grid/src/services/view_editor/editor.rs
  67. 65 56
      frontend/rust-lib/flowy-grid/src/services/view_editor/editor_manager.rs
  68. 8 0
      frontend/rust-lib/flowy-grid/src/services/view_editor/mod.rs
  69. 166 0
      frontend/rust-lib/flowy-grid/src/services/view_editor/trait_impl.rs
  70. 5 2
      frontend/rust-lib/flowy-grid/tests/grid/filter_test/checkbox_filter_test.rs
  71. 5 5
      frontend/rust-lib/flowy-grid/tests/grid/filter_test/date_filter_test.rs
  72. 6 6
      frontend/rust-lib/flowy-grid/tests/grid/filter_test/number_filter_test.rs
  73. 20 2
      frontend/rust-lib/flowy-grid/tests/grid/filter_test/script.rs
  74. 6 6
      frontend/rust-lib/flowy-grid/tests/grid/filter_test/select_option_filter_test.rs
  75. 36 7
      frontend/rust-lib/flowy-grid/tests/grid/filter_test/text_filter_test.rs
  76. 1 1
      frontend/rust-lib/flowy-grid/tests/grid/grid_editor.rs
  77. 1 1
      frontend/scripts/makefile/tests.toml

+ 14 - 7
frontend/app_flowy/lib/core/grid_notification.dart

@@ -9,31 +9,38 @@ import 'package:flowy_sdk/rust_stream.dart';
 import 'notification_helper.dart';
 
 // GridPB
-typedef GridNotificationCallback = void Function(GridNotification, Either<Uint8List, FlowyError>);
+typedef GridNotificationCallback = void Function(
+    GridDartNotification, Either<Uint8List, FlowyError>);
 
-class GridNotificationParser extends NotificationParser<GridNotification, FlowyError> {
-  GridNotificationParser({String? id, required GridNotificationCallback callback})
+class GridNotificationParser
+    extends NotificationParser<GridDartNotification, FlowyError> {
+  GridNotificationParser(
+      {String? id, required GridNotificationCallback callback})
       : super(
           id: id,
           callback: callback,
-          tyParser: (ty) => GridNotification.valueOf(ty),
+          tyParser: (ty) => GridDartNotification.valueOf(ty),
           errorParser: (bytes) => FlowyError.fromBuffer(bytes),
         );
 }
 
-typedef GridNotificationHandler = Function(GridNotification ty, Either<Uint8List, FlowyError> result);
+typedef GridNotificationHandler = Function(
+    GridDartNotification ty, Either<Uint8List, FlowyError> result);
 
 class GridNotificationListener {
   StreamSubscription<SubscribeObject>? _subscription;
   GridNotificationParser? _parser;
 
-  GridNotificationListener({required String objectId, required GridNotificationHandler handler})
+  GridNotificationListener(
+      {required String objectId, required GridNotificationHandler handler})
       : _parser = GridNotificationParser(id: objectId, callback: handler) {
-    _subscription = RustStreamReceiver.listen((observable) => _parser?.parse(observable));
+    _subscription =
+        RustStreamReceiver.listen((observable) => _parser?.parse(observable));
   }
 
   Future<void> stop() async {
     _parser = null;
     await _subscription?.cancel();
+    _subscription = null;
   }
 }

+ 3 - 3
frontend/app_flowy/lib/plugins/board/application/board_listener.dart

@@ -32,18 +32,18 @@ class BoardListener {
   }
 
   void _handler(
-    GridNotification ty,
+    GridDartNotification ty,
     Either<Uint8List, FlowyError> result,
   ) {
     switch (ty) {
-      case GridNotification.DidUpdateGroupView:
+      case GridDartNotification.DidUpdateGroupView:
         result.fold(
           (payload) => _groupUpdateNotifier?.value =
               left(GroupViewChangesetPB.fromBuffer(payload)),
           (error) => _groupUpdateNotifier?.value = right(error),
         );
         break;
-      case GridNotification.DidGroupByNewField:
+      case GridDartNotification.DidGroupByNewField:
         result.fold(
           (payload) => _groupByNewFieldNotifier?.value =
               left(GroupViewChangesetPB.fromBuffer(payload).newGroups),

+ 1 - 0
frontend/app_flowy/lib/plugins/board/application/card/card_bloc.dart

@@ -66,6 +66,7 @@ class BoardCardBloc extends Bloc<BoardCardEvent, BoardCardState> {
         state.cells.map((cell) => cell.identifier.fieldContext).toList(),
       ),
       rowPB: state.rowPB,
+      visible: true,
     );
   }
 

+ 2 - 2
frontend/app_flowy/lib/plugins/board/application/group_listener.dart

@@ -27,11 +27,11 @@ class GroupListener {
   }
 
   void _handler(
-    GridNotification ty,
+    GridDartNotification ty,
     Either<Uint8List, FlowyError> result,
   ) {
     switch (ty) {
-      case GridNotification.DidUpdateGroup:
+      case GridDartNotification.DidUpdateGroup:
         result.fold(
           (payload) => _groupNotifier?.value =
               left(GroupRowsNotificationPB.fromBuffer(payload)),

+ 1 - 0
frontend/app_flowy/lib/plugins/board/presentation/board_page.dart

@@ -287,6 +287,7 @@ class _BoardContentState extends State<BoardContent> {
       gridId: gridId,
       fields: UnmodifiableListView(fieldController.fieldContexts),
       rowPB: rowPB,
+      visible: true,
     );
 
     final dataController = GridRowDataController(

+ 2 - 2
frontend/app_flowy/lib/plugins/grid/application/block/block_cache.dart

@@ -13,7 +13,7 @@ class GridBlockCache {
   late GridRowCache _rowCache;
   late GridBlockListener _listener;
 
-  List<RowInfo> get rows => _rowCache.rows;
+  List<RowInfo> get rows => _rowCache.visibleRows;
   GridRowCache get rowCache => _rowCache;
 
   GridBlockCache({
@@ -30,7 +30,7 @@ class GridBlockCache {
     _listener = GridBlockListener(blockId: block.id);
     _listener.start((result) {
       result.fold(
-        (changesets) => _rowCache.applyChangesets(changesets),
+        (changeset) => _rowCache.applyChangesets(changeset),
         (err) => Log.error(err),
       );
     });

+ 7 - 5
frontend/app_flowy/lib/plugins/grid/application/block/block_listener.dart

@@ -7,11 +7,12 @@ import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid/block_entities.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid/dart_notification.pb.dart';
 
-typedef GridBlockUpdateNotifierValue = Either<List<GridBlockChangesetPB>, FlowyError>;
+typedef GridBlockUpdateNotifierValue = Either<GridBlockChangesetPB, FlowyError>;
 
 class GridBlockListener {
   final String blockId;
-  PublishNotifier<GridBlockUpdateNotifierValue>? _rowsUpdateNotifier = PublishNotifier();
+  PublishNotifier<GridBlockUpdateNotifierValue>? _rowsUpdateNotifier =
+      PublishNotifier();
   GridNotificationListener? _listener;
 
   GridBlockListener({required this.blockId});
@@ -29,11 +30,12 @@ class GridBlockListener {
     _rowsUpdateNotifier?.addPublishListener(onBlockChanged);
   }
 
-  void _handler(GridNotification ty, Either<Uint8List, FlowyError> result) {
+  void _handler(GridDartNotification ty, Either<Uint8List, FlowyError> result) {
     switch (ty) {
-      case GridNotification.DidUpdateGridBlock:
+      case GridDartNotification.DidUpdateGridBlock:
         result.fold(
-          (payload) => _rowsUpdateNotifier?.value = left([GridBlockChangesetPB.fromBuffer(payload)]),
+          (payload) => _rowsUpdateNotifier?.value =
+              left(GridBlockChangesetPB.fromBuffer(payload)),
           (error) => _rowsUpdateNotifier?.value = right(error),
         );
         break;

+ 2 - 2
frontend/app_flowy/lib/plugins/grid/application/cell/cell_listener.dart

@@ -22,9 +22,9 @@ class CellListener {
         objectId: "$rowId:$fieldId", handler: _handler);
   }
 
-  void _handler(GridNotification ty, Either<Uint8List, FlowyError> result) {
+  void _handler(GridDartNotification ty, Either<Uint8List, FlowyError> result) {
     switch (ty) {
-      case GridNotification.DidUpdateCell:
+      case GridDartNotification.DidUpdateCell:
         result.fold(
           (payload) => _updateCellNotifier?.value = left(unit),
           (error) => _updateCellNotifier?.value = right(error),

+ 2 - 2
frontend/app_flowy/lib/plugins/grid/application/field/field_listener.dart

@@ -27,11 +27,11 @@ class SingleFieldListener {
   }
 
   void _handler(
-    GridNotification ty,
+    GridDartNotification ty,
     Either<Uint8List, FlowyError> result,
   ) {
     switch (ty) {
-      case GridNotification.DidUpdateField:
+      case GridDartNotification.DidUpdateField:
         result.fold(
           (payload) =>
               _updateFieldNotifier?.value = left(FieldPB.fromBuffer(payload)),

+ 2 - 2
frontend/app_flowy/lib/plugins/grid/application/field/grid_listener.dart

@@ -25,9 +25,9 @@ class GridFieldsListener {
     );
   }
 
-  void _handler(GridNotification ty, Either<Uint8List, FlowyError> result) {
+  void _handler(GridDartNotification ty, Either<Uint8List, FlowyError> result) {
     switch (ty) {
-      case GridNotification.DidUpdateGridField:
+      case GridDartNotification.DidUpdateGridField:
         result.fold(
           (payload) => updateFieldsNotifier?.value =
               left(GridFieldChangesetPB.fromBuffer(payload)),

+ 2 - 2
frontend/app_flowy/lib/plugins/grid/application/filter/filter_bloc.dart

@@ -4,7 +4,7 @@ import 'package:flowy_sdk/protobuf/flowy-grid/checkbox_filter.pbenum.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid/date_filter.pbenum.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid/number_filter.pb.dart';
-import 'package:flowy_sdk/protobuf/flowy-grid/text_filter.pbserver.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/text_filter.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid/util.pb.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:freezed_annotation/freezed_annotation.dart';
@@ -114,7 +114,7 @@ class GridFilterBloc extends Bloc<GridFilterEvent, GridFilterState> {
             (element) => !deleteFilterIds.contains(element.id),
           );
 
-          // Inserts the new fitler if it's not exist
+          // Inserts the new filter if it's not exist
           for (final newFilter in changeset.insertFilters) {
             final index =
                 filters.indexWhere((element) => element.id == newFilter.id);

+ 2 - 2
frontend/app_flowy/lib/plugins/grid/application/filter/filter_listener.dart

@@ -29,11 +29,11 @@ class FilterListener {
   }
 
   void _handler(
-    GridNotification ty,
+    GridDartNotification ty,
     Either<Uint8List, FlowyError> result,
   ) {
     switch (ty) {
-      case GridNotification.DidUpdateFilter:
+      case GridDartNotification.DidUpdateFilter:
         result.fold(
           (payload) => _filterNotifier?.value =
               left(FilterChangesetNotificationPB.fromBuffer(payload)),

+ 58 - 28
frontend/app_flowy/lib/plugins/grid/application/row/row_cache.dart

@@ -33,13 +33,18 @@ class GridRowCache {
   List<RowInfo> _rowInfos = [];
 
   /// Use Map for faster access the raw row data.
-  final HashMap<String, RowPB> _rowByRowId;
+  final HashMap<String, RowInfo> _rowInfoByRowId;
 
   final GridCellCache _cellCache;
   final IGridRowFieldNotifier _fieldNotifier;
   final _RowChangesetNotifier _rowChangeReasonNotifier;
 
-  UnmodifiableListView<RowInfo> get rows => UnmodifiableListView(_rowInfos);
+  UnmodifiableListView<RowInfo> get visibleRows {
+    var visibleRows = [..._rowInfos];
+    visibleRows.retainWhere((element) => element.visible);
+    return UnmodifiableListView(visibleRows);
+  }
+
   GridCellCache get cellCache => _cellCache;
 
   GridRowCache({
@@ -47,7 +52,7 @@ class GridRowCache {
     required this.block,
     required IGridRowFieldNotifier notifier,
   })  : _cellCache = GridCellCache(gridId: gridId),
-        _rowByRowId = HashMap(),
+        _rowInfoByRowId = HashMap(),
         _rowChangeReasonNotifier = _RowChangesetNotifier(),
         _fieldNotifier = notifier {
     //
@@ -55,7 +60,12 @@ class GridRowCache {
         .receive(const RowsChangedReason.fieldDidChange()));
     notifier.onRowFieldChanged(
         (field) => _cellCache.removeCellWithFieldId(field.id));
-    _rowInfos = block.rows.map((rowPB) => buildGridRow(rowPB)).toList();
+
+    for (final row in block.rows) {
+      final rowInfo = buildGridRow(row);
+      _rowInfos.add(rowInfo);
+      _rowInfoByRowId[rowInfo.rowPB.id] = rowInfo;
+    }
   }
 
   Future<void> dispose() async {
@@ -64,14 +74,12 @@ class GridRowCache {
     await _cellCache.dispose();
   }
 
-  void applyChangesets(List<GridBlockChangesetPB> changesets) {
-    for (final changeset in changesets) {
-      _deleteRows(changeset.deletedRows);
-      _insertRows(changeset.insertedRows);
-      _updateRows(changeset.updatedRows);
-      _hideRows(changeset.hideRows);
-      _showRows(changeset.visibleRows);
-    }
+  void applyChangesets(GridBlockChangesetPB changeset) {
+    _deleteRows(changeset.deletedRows);
+    _insertRows(changeset.insertedRows);
+    _updateRows(changeset.updatedRows);
+    _hideRows(changeset.invisibleRows);
+    _showRows(changeset.visibleRows);
   }
 
   void _deleteRows(List<String> deletedRows) {
@@ -89,7 +97,7 @@ class GridRowCache {
       if (deletedRowByRowId[rowInfo.rowPB.id] == null) {
         newRows.add(rowInfo);
       } else {
-        _rowByRowId.remove(rowInfo.rowPB.id);
+        _rowInfoByRowId.remove(rowInfo.rowPB.id);
         deletedIndex.add(DeletedIndex(index: index, row: rowInfo));
       }
     });
@@ -109,10 +117,9 @@ class GridRowCache {
         rowId: insertRow.row.id,
       );
       insertIndexs.add(insertIndex);
-      _rowInfos.insert(
-        insertRow.index,
-        (buildGridRow(insertRow.row)),
-      );
+      final rowInfo = buildGridRow(insertRow.row);
+      _rowInfos.insert(insertRow.index, rowInfo);
+      _rowInfoByRowId[rowInfo.rowPB.id] = rowInfo;
     }
 
     _rowChangeReasonNotifier.receive(RowsChangedReason.insert(insertIndexs));
@@ -130,10 +137,11 @@ class GridRowCache {
         (rowInfo) => rowInfo.rowPB.id == rowId,
       );
       if (index != -1) {
-        _rowByRowId[rowId] = updatedRow;
+        final rowInfo = buildGridRow(updatedRow);
+        _rowInfoByRowId[rowId] = rowInfo;
 
         _rowInfos.removeAt(index);
-        _rowInfos.insert(index, buildGridRow(updatedRow));
+        _rowInfos.insert(index, rowInfo);
         updatedIndexs[rowId] = UpdatedIndex(index: index, rowId: rowId);
       }
     }
@@ -141,9 +149,26 @@ class GridRowCache {
     _rowChangeReasonNotifier.receive(RowsChangedReason.update(updatedIndexs));
   }
 
-  void _hideRows(List<String> hideRows) {}
+  void _hideRows(List<String> invisibleRows) {
+    for (final rowId in invisibleRows) {
+      _rowInfoByRowId[rowId]?.visible = false;
+    }
 
-  void _showRows(List<String> visibleRows) {}
+    if (invisibleRows.isNotEmpty) {
+      _rowChangeReasonNotifier
+          .receive(const RowsChangedReason.filterDidChange());
+    }
+  }
+
+  void _showRows(List<String> visibleRows) {
+    for (final rowId in visibleRows) {
+      _rowInfoByRowId[rowId]?.visible = true;
+    }
+    if (visibleRows.isNotEmpty) {
+      _rowChangeReasonNotifier
+          .receive(const RowsChangedReason.filterDidChange());
+    }
+  }
 
   void onRowsChanged(void Function(RowsChangedReason) onRowChanged) {
     _rowChangeReasonNotifier.addListener(() {
@@ -163,9 +188,10 @@ class GridRowCache {
 
       notifyUpdate() {
         if (onCellUpdated != null) {
-          final row = _rowByRowId[rowId];
-          if (row != null) {
-            final GridCellMap cellDataMap = _makeGridCells(rowId, row);
+          final rowInfo = _rowInfoByRowId[rowId];
+          if (rowInfo != null) {
+            final GridCellMap cellDataMap =
+                _makeGridCells(rowId, rowInfo.rowPB);
             onCellUpdated(cellDataMap, _rowChangeReasonNotifier.reason);
           }
         }
@@ -188,7 +214,7 @@ class GridRowCache {
   }
 
   GridCellMap loadGridCells(String rowId) {
-    final RowPB? data = _rowByRowId[rowId];
+    final RowPB? data = _rowInfoByRowId[rowId]?.rowPB;
     if (data == null) {
       _loadRow(rowId);
     }
@@ -230,7 +256,6 @@ class GridRowCache {
     final updatedRow = optionRow.row;
     updatedRow.freeze();
 
-    _rowByRowId[updatedRow.id] = updatedRow;
     final index =
         _rowInfos.indexWhere((rowInfo) => rowInfo.rowPB.id == updatedRow.id);
     if (index != -1) {
@@ -238,6 +263,7 @@ class GridRowCache {
       if (_rowInfos[index].rowPB != updatedRow) {
         final rowInfo = _rowInfos.removeAt(index).copyWith(rowPB: updatedRow);
         _rowInfos.insert(index, rowInfo);
+        _rowInfoByRowId[rowInfo.rowPB.id] = rowInfo;
 
         // Calculate the update index
         final UpdatedIndexs updatedIndexs = UpdatedIndexs();
@@ -258,6 +284,7 @@ class GridRowCache {
       gridId: gridId,
       fields: _fieldNotifier.fields,
       rowPB: rowPB,
+      visible: true,
     );
   }
 }
@@ -275,16 +302,18 @@ class _RowChangesetNotifier extends ChangeNotifier {
       update: (_) => notifyListeners(),
       fieldDidChange: (_) => notifyListeners(),
       initial: (_) {},
+      filterDidChange: (_FilterDidChange value) => notifyListeners(),
     );
   }
 }
 
-@freezed
+@unfreezed
 class RowInfo with _$RowInfo {
-  const factory RowInfo({
+  factory RowInfo({
     required String gridId,
     required UnmodifiableListView<GridFieldContext> fields,
     required RowPB rowPB,
+    required bool visible,
   }) = _RowInfo;
 }
 
@@ -298,6 +327,7 @@ class RowsChangedReason with _$RowsChangedReason {
   const factory RowsChangedReason.delete(DeletedIndexs items) = _Delete;
   const factory RowsChangedReason.update(UpdatedIndexs indexs) = _Update;
   const factory RowsChangedReason.fieldDidChange() = _FieldDidChange;
+  const factory RowsChangedReason.filterDidChange() = _FilterDidChange;
   const factory RowsChangedReason.initial() = InitialListState;
 }
 

+ 2 - 2
frontend/app_flowy/lib/plugins/grid/application/row/row_listener.dart

@@ -23,9 +23,9 @@ class RowListener {
     _listener = GridNotificationListener(objectId: rowId, handler: _handler);
   }
 
-  void _handler(GridNotification ty, Either<Uint8List, FlowyError> result) {
+  void _handler(GridDartNotification ty, Either<Uint8List, FlowyError> result) {
     switch (ty) {
-      case GridNotification.DidUpdateRow:
+      case GridDartNotification.DidUpdateRow:
         result.fold(
           (payload) =>
               updateRowNotifier?.value = left(RowPB.fromBuffer(payload)),

+ 2 - 2
frontend/app_flowy/lib/plugins/grid/application/setting/setting_listener.dart

@@ -24,9 +24,9 @@ class SettingListener {
     _listener = GridNotificationListener(objectId: gridId, handler: _handler);
   }
 
-  void _handler(GridNotification ty, Either<Uint8List, FlowyError> result) {
+  void _handler(GridDartNotification ty, Either<Uint8List, FlowyError> result) {
     switch (ty) {
-      case GridNotification.DidUpdateGridSetting:
+      case GridDartNotification.DidUpdateGridSetting:
         result.fold(
           (payload) => _updateSettingNotifier?.value = left(
             GridSettingPB.fromBuffer(payload),

+ 7 - 4
frontend/app_flowy/lib/plugins/trash/application/trash_listener.dart

@@ -8,7 +8,8 @@ import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-folder/trash.pb.dart';
 import 'package:flowy_sdk/rust_stream.dart';
 
-typedef TrashUpdatedCallback = void Function(Either<List<TrashPB>, FlowyError> trashOrFailed);
+typedef TrashUpdatedCallback = void Function(
+    Either<List<TrashPB>, FlowyError> trashOrFailed);
 
 class TrashListener {
   StreamSubscription<SubscribeObject>? _subscription;
@@ -17,11 +18,13 @@ class TrashListener {
 
   void start({TrashUpdatedCallback? trashUpdated}) {
     _trashUpdated = trashUpdated;
-    _parser = FolderNotificationParser(callback: _bservableCallback);
-    _subscription = RustStreamReceiver.listen((observable) => _parser?.parse(observable));
+    _parser = FolderNotificationParser(callback: _observableCallback);
+    _subscription =
+        RustStreamReceiver.listen((observable) => _parser?.parse(observable));
   }
 
-  void _bservableCallback(FolderNotification ty, Either<Uint8List, FlowyError> result) {
+  void _observableCallback(
+      FolderNotification ty, Either<Uint8List, FlowyError> result) {
     switch (ty) {
       case FolderNotification.TrashUpdated:
         if (_trashUpdated != null) {

+ 3 - 2
frontend/app_flowy/test/bloc_test/board_test/create_card_test.dart

@@ -14,10 +14,11 @@ void main() {
   group('$BoardBloc', () {
     late BoardBloc boardBloc;
     late String groupId;
+    late BoardTestContext context;
 
     setUp(() async {
-      await boardTest.context.createTestBoard();
-      boardBloc = BoardBloc(view: boardTest.context.gridView)
+      context = await boardTest.createTestBoard();
+      boardBloc = BoardBloc(view: context.gridView)
         ..add(const BoardEvent.initial());
       await boardResponseFuture();
       groupId = boardBloc.state.groupIds.first;

+ 17 - 15
frontend/app_flowy/test/bloc_test/board_test/create_or_edit_field_test.dart

@@ -16,22 +16,23 @@ void main() {
   group('The grouped field is not changed after editing a field:', () {
     late BoardBloc boardBloc;
     late FieldEditorBloc editorBloc;
+    late BoardTestContext context;
     setUpAll(() async {
-      await boardTest.context.createTestBoard();
+      context = await boardTest.createTestBoard();
     });
 
     setUp(() async {
-      boardBloc = BoardBloc(view: boardTest.context.gridView)
+      boardBloc = BoardBloc(view: context.gridView)
         ..add(const BoardEvent.initial());
 
-      final fieldContext = boardTest.context.singleSelectFieldContext();
+      final fieldContext = context.singleSelectFieldContext();
       final loader = FieldTypeOptionLoader(
-        gridId: boardTest.context.gridView.id,
+        gridId: context.gridView.id,
         field: fieldContext.field,
       );
 
       editorBloc = FieldEditorBloc(
-        gridId: boardTest.context.gridView.id,
+        gridId: context.gridView.id,
         fieldName: fieldContext.name,
         isGroupField: fieldContext.isGroupField,
         loader: loader,
@@ -46,7 +47,7 @@ void main() {
       wait: boardResponseDuration(),
       verify: (bloc) {
         assert(bloc.groupControllers.values.length == 4);
-        assert(boardTest.context.fieldContexts.length == 2);
+        assert(context.fieldContexts.length == 2);
       },
     );
 
@@ -75,19 +76,20 @@ void main() {
         assert(bloc.groupControllers.values.length == 4,
             "Expected 4, but receive ${bloc.groupControllers.values.length}");
 
-        assert(boardTest.context.fieldContexts.length == 2,
-            "Expected 2, but receive ${boardTest.context.fieldContexts.length}");
+        assert(context.fieldContexts.length == 2,
+            "Expected 2, but receive ${context.fieldContexts.length}");
       },
     );
   });
   group('The grouped field is not changed after creating a new field:', () {
     late BoardBloc boardBloc;
+    late BoardTestContext context;
     setUpAll(() async {
-      await boardTest.context.createTestBoard();
+      context = await boardTest.createTestBoard();
     });
 
     setUp(() async {
-      boardBloc = BoardBloc(view: boardTest.context.gridView)
+      boardBloc = BoardBloc(view: context.gridView)
         ..add(const BoardEvent.initial());
       await boardResponseFuture();
     });
@@ -98,14 +100,14 @@ void main() {
       wait: boardResponseDuration(),
       verify: (bloc) {
         assert(bloc.groupControllers.values.length == 4);
-        assert(boardTest.context.fieldContexts.length == 2);
+        assert(context.fieldContexts.length == 2);
       },
     );
 
     test('create a field', () async {
-      await boardTest.context.createField(FieldType.Checkbox);
+      await context.createField(FieldType.Checkbox);
       await boardResponseFuture();
-      final checkboxField = boardTest.context.fieldContexts.last.field;
+      final checkboxField = context.fieldContexts.last.field;
       assert(checkboxField.fieldType == FieldType.Checkbox);
     });
 
@@ -117,8 +119,8 @@ void main() {
         assert(bloc.groupControllers.values.length == 4,
             "Expected 4, but receive ${bloc.groupControllers.values.length}");
 
-        assert(boardTest.context.fieldContexts.length == 3,
-            "Expected 3, but receive ${boardTest.context.fieldContexts.length}");
+        assert(context.fieldContexts.length == 3,
+            "Expected 3, but receive ${context.fieldContexts.length}");
       },
     );
   });

+ 45 - 0
frontend/app_flowy/test/bloc_test/board_test/group_by_checkbox_field_test.dart

@@ -0,0 +1,45 @@
+import 'package:app_flowy/plugins/board/application/board_bloc.dart';
+import 'package:app_flowy/plugins/grid/application/setting/group_bloc.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+import 'util.dart';
+
+void main() {
+  late AppFlowyBoardTest boardTest;
+
+  setUpAll(() async {
+    boardTest = await AppFlowyBoardTest.ensureInitialized();
+  });
+
+  // Group by checkbox field
+  test('group by checkbox field test', () async {
+    final context = await boardTest.createTestBoard();
+    final boardBloc = BoardBloc(view: context.gridView)
+      ..add(const BoardEvent.initial());
+    await boardResponseFuture();
+
+    // assert the initial values
+    assert(boardBloc.groupControllers.values.length == 4);
+    assert(context.fieldContexts.length == 2);
+
+    // create checkbox field
+    await context.createField(FieldType.Checkbox);
+    await boardResponseFuture();
+    assert(context.fieldContexts.length == 3);
+
+    // set group by checkbox
+    final checkboxField = context.fieldContexts.last.field;
+    final gridGroupBloc = GridGroupBloc(
+      viewId: context.gridView.id,
+      fieldController: context.fieldController,
+    );
+    gridGroupBloc.add(GridGroupEvent.setGroupByField(
+      checkboxField.id,
+      checkboxField.fieldType,
+    ));
+    await boardResponseFuture();
+
+    assert(boardBloc.groupControllers.values.length == 2);
+  });
+}

+ 0 - 231
frontend/app_flowy/test/bloc_test/board_test/group_by_field_test.dart

@@ -1,231 +0,0 @@
-import 'package:app_flowy/plugins/board/application/board_bloc.dart';
-import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
-import 'package:app_flowy/plugins/grid/application/cell/select_option_editor_bloc.dart';
-import 'package:app_flowy/plugins/grid/application/field/field_editor_bloc.dart';
-import 'package:app_flowy/plugins/grid/application/setting/group_bloc.dart';
-import 'package:bloc_test/bloc_test.dart';
-import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
-import 'package:flutter_test/flutter_test.dart';
-
-import 'util.dart';
-
-void main() {
-  late AppFlowyBoardTest boardTest;
-
-  setUpAll(() async {
-    boardTest = await AppFlowyBoardTest.ensureInitialized();
-  });
-
-  // Group by multi-select with no options
-  group('Group by multi-select with no options', () {
-    //
-    late FieldPB multiSelectField;
-    late String expectedGroupName;
-
-    setUpAll(() async {
-      await boardTest.context.createTestBoard();
-    });
-
-    test('create multi-select field', () async {
-      await boardTest.context.createField(FieldType.MultiSelect);
-      await boardResponseFuture();
-
-      assert(boardTest.context.fieldContexts.length == 3);
-      multiSelectField = boardTest.context.fieldContexts.last.field;
-      expectedGroupName = "No ${multiSelectField.name}";
-      assert(multiSelectField.fieldType == FieldType.MultiSelect);
-    });
-
-    blocTest<GridGroupBloc, GridGroupState>(
-      "set grouped by the new multi-select field",
-      build: () => GridGroupBloc(
-        viewId: boardTest.context.gridView.id,
-        fieldController: boardTest.context.fieldController,
-      ),
-      act: (bloc) async {
-        bloc.add(GridGroupEvent.setGroupByField(
-          multiSelectField.id,
-          multiSelectField.fieldType,
-        ));
-      },
-      wait: boardResponseDuration(),
-    );
-
-    blocTest<BoardBloc, BoardState>(
-      "assert only have the 'No status' group",
-      build: () => BoardBloc(view: boardTest.context.gridView)
-        ..add(const BoardEvent.initial()),
-      wait: boardResponseDuration(),
-      verify: (bloc) {
-        assert(bloc.groupControllers.values.length == 1,
-            "Expected 1, but receive ${bloc.groupControllers.values.length}");
-
-        assert(
-            bloc.groupControllers.values.first.group.desc == expectedGroupName,
-            "Expected $expectedGroupName, but receive ${bloc.groupControllers.values.first.group.desc}");
-      },
-    );
-  });
-
-  group('Group by multi-select with two options', () {
-    late FieldPB multiSelectField;
-
-    setUpAll(() async {
-      await boardTest.context.createTestBoard();
-    });
-
-    test('create multi-select field', () async {
-      await boardTest.context.createField(FieldType.MultiSelect);
-      await boardResponseFuture();
-
-      assert(boardTest.context.fieldContexts.length == 3);
-      multiSelectField = boardTest.context.fieldContexts.last.field;
-      assert(multiSelectField.fieldType == FieldType.MultiSelect);
-
-      final cellController =
-          await boardTest.context.makeCellController(multiSelectField.id)
-              as GridSelectOptionCellController;
-
-      final multiSelectOptionBloc =
-          SelectOptionCellEditorBloc(cellController: cellController);
-      multiSelectOptionBloc.add(const SelectOptionEditorEvent.initial());
-      await boardResponseFuture();
-
-      multiSelectOptionBloc.add(const SelectOptionEditorEvent.newOption("A"));
-      await boardResponseFuture();
-
-      multiSelectOptionBloc.add(const SelectOptionEditorEvent.newOption("B"));
-      await boardResponseFuture();
-    });
-
-    blocTest<GridGroupBloc, GridGroupState>(
-      "set grouped by multi-select field",
-      build: () => GridGroupBloc(
-        viewId: boardTest.context.gridView.id,
-        fieldController: boardTest.context.fieldController,
-      ),
-      act: (bloc) async {
-        await boardResponseFuture();
-        bloc.add(GridGroupEvent.setGroupByField(
-          multiSelectField.id,
-          multiSelectField.fieldType,
-        ));
-      },
-      wait: boardResponseDuration(),
-    );
-
-    blocTest<BoardBloc, BoardState>(
-      "check the groups' order",
-      build: () => BoardBloc(view: boardTest.context.gridView)
-        ..add(const BoardEvent.initial()),
-      wait: boardResponseDuration(),
-      verify: (bloc) {
-        assert(bloc.groupControllers.values.length == 3,
-            "Expected 3, but receive ${bloc.groupControllers.values.length}");
-
-        final groups =
-            bloc.groupControllers.values.map((e) => e.group).toList();
-        assert(groups[0].desc == "No ${multiSelectField.name}");
-        assert(groups[1].desc == "B");
-        assert(groups[2].desc == "A");
-      },
-    );
-  });
-
-  // Group by checkbox field
-  group('Group by checkbox field:', () {
-    late BoardBloc boardBloc;
-    late FieldPB checkboxField;
-    setUpAll(() async {
-      await boardTest.context.createTestBoard();
-    });
-
-    setUp(() async {
-      boardBloc = BoardBloc(view: boardTest.context.gridView)
-        ..add(const BoardEvent.initial());
-      await boardResponseFuture();
-    });
-
-    blocTest<BoardBloc, BoardState>(
-      "initial",
-      build: () => boardBloc,
-      wait: boardResponseDuration(),
-      verify: (bloc) {
-        assert(bloc.groupControllers.values.length == 4);
-        assert(boardTest.context.fieldContexts.length == 2);
-      },
-    );
-
-    test('create checkbox field', () async {
-      await boardTest.context.createField(FieldType.Checkbox);
-      await boardResponseFuture();
-
-      assert(boardTest.context.fieldContexts.length == 3);
-      checkboxField = boardTest.context.fieldContexts.last.field;
-      assert(checkboxField.fieldType == FieldType.Checkbox);
-    });
-
-    blocTest<GridGroupBloc, GridGroupState>(
-      "set grouped by checkbox field",
-      build: () => GridGroupBloc(
-        viewId: boardTest.context.gridView.id,
-        fieldController: boardTest.context.fieldController,
-      ),
-      act: (bloc) async {
-        bloc.add(GridGroupEvent.setGroupByField(
-          checkboxField.id,
-          checkboxField.fieldType,
-        ));
-      },
-      wait: boardResponseDuration(),
-    );
-
-    blocTest<BoardBloc, BoardState>(
-      "check the number of groups is 2",
-      build: () => boardBloc,
-      wait: boardResponseDuration(),
-      verify: (bloc) {
-        assert(bloc.groupControllers.values.length == 2);
-      },
-    );
-  });
-
-  // Group with not support grouping field
-  group('Group with not support grouping field:', () {
-    late FieldEditorBloc editorBloc;
-    setUpAll(() async {
-      await boardTest.context.createTestBoard();
-      final fieldContext = boardTest.context.singleSelectFieldContext();
-      editorBloc = boardTest.context.createFieldEditor(
-        fieldContext: fieldContext,
-      )..add(const FieldEditorEvent.initial());
-
-      await boardResponseFuture();
-    });
-
-    blocTest<FieldEditorBloc, FieldEditorState>(
-      "switch to text field",
-      build: () => editorBloc,
-      wait: boardResponseDuration(),
-      act: (bloc) async {
-        bloc.add(const FieldEditorEvent.switchToField(FieldType.RichText));
-      },
-      verify: (bloc) {
-        bloc.state.field.fold(
-          () => throw Exception(),
-          (field) => field.fieldType == FieldType.RichText,
-        );
-      },
-    );
-    blocTest<BoardBloc, BoardState>(
-      'assert the number of groups is 1',
-      build: () => BoardBloc(view: boardTest.context.gridView)
-        ..add(const BoardEvent.initial()),
-      wait: boardResponseDuration(),
-      verify: (bloc) {
-        assert(bloc.groupControllers.values.length == 1,
-            "Expected 1, but receive ${bloc.groupControllers.values.length}");
-      },
-    );
-  });
-}

+ 95 - 0
frontend/app_flowy/test/bloc_test/board_test/group_by_multi_select_field_test.dart

@@ -0,0 +1,95 @@
+import 'package:app_flowy/plugins/board/application/board_bloc.dart';
+import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
+import 'package:app_flowy/plugins/grid/application/cell/select_option_editor_bloc.dart';
+import 'package:app_flowy/plugins/grid/application/setting/group_bloc.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+import 'util.dart';
+
+void main() {
+  late AppFlowyBoardTest boardTest;
+
+  setUpAll(() async {
+    boardTest = await AppFlowyBoardTest.ensureInitialized();
+  });
+
+  test('group by multi select with no options test', () async {
+    final context = await boardTest.createTestBoard();
+
+    // create multi-select field
+    await context.createField(FieldType.MultiSelect);
+    await boardResponseFuture();
+    assert(context.fieldContexts.length == 3);
+    final multiSelectField = context.fieldContexts.last.field;
+
+    // set grouped by the new multi-select field"
+    final gridGroupBloc = GridGroupBloc(
+      viewId: context.gridView.id,
+      fieldController: context.fieldController,
+    );
+    gridGroupBloc.add(GridGroupEvent.setGroupByField(
+      multiSelectField.id,
+      multiSelectField.fieldType,
+    ));
+    await boardResponseFuture();
+
+    //assert only have the 'No status' group
+    final boardBloc = BoardBloc(view: context.gridView)
+      ..add(const BoardEvent.initial());
+    await boardResponseFuture();
+    assert(boardBloc.groupControllers.values.length == 1,
+        "Expected 1, but receive ${boardBloc.groupControllers.values.length}");
+    final expectedGroupName = "No ${multiSelectField.name}";
+    assert(
+        boardBloc.groupControllers.values.first.group.desc == expectedGroupName,
+        "Expected $expectedGroupName, but receive ${boardBloc.groupControllers.values.first.group.desc}");
+  });
+
+  test('group by multi select with no options test', () async {
+    final context = await boardTest.createTestBoard();
+
+    // create multi-select field
+    await context.createField(FieldType.MultiSelect);
+    await boardResponseFuture();
+    assert(context.fieldContexts.length == 3);
+    final multiSelectField = context.fieldContexts.last.field;
+
+    // Create options
+    final cellController = await context.makeCellController(multiSelectField.id)
+        as GridSelectOptionCellController;
+
+    final multiSelectOptionBloc =
+        SelectOptionCellEditorBloc(cellController: cellController);
+    multiSelectOptionBloc.add(const SelectOptionEditorEvent.initial());
+    await boardResponseFuture();
+    multiSelectOptionBloc.add(const SelectOptionEditorEvent.newOption("A"));
+    await boardResponseFuture();
+    multiSelectOptionBloc.add(const SelectOptionEditorEvent.newOption("B"));
+    await boardResponseFuture();
+
+    // set grouped by the new multi-select field"
+    final gridGroupBloc = GridGroupBloc(
+      viewId: context.gridView.id,
+      fieldController: context.fieldController,
+    );
+    gridGroupBloc.add(GridGroupEvent.setGroupByField(
+      multiSelectField.id,
+      multiSelectField.fieldType,
+    ));
+    await boardResponseFuture();
+
+    // assert there are only three group
+    final boardBloc = BoardBloc(view: context.gridView)
+      ..add(const BoardEvent.initial());
+    await boardResponseFuture();
+    assert(boardBloc.groupControllers.values.length == 3,
+        "Expected 3, but receive ${boardBloc.groupControllers.values.length}");
+
+    final groups =
+        boardBloc.groupControllers.values.map((e) => e.group).toList();
+    assert(groups[0].desc == "No ${multiSelectField.name}");
+    assert(groups[1].desc == "B");
+    assert(groups[2].desc == "A");
+  });
+}

+ 51 - 0
frontend/app_flowy/test/bloc_test/board_test/group_by_unsupport_field_test.dart

@@ -0,0 +1,51 @@
+import 'package:app_flowy/plugins/board/application/board_bloc.dart';
+import 'package:app_flowy/plugins/grid/application/field/field_editor_bloc.dart';
+import 'package:bloc_test/bloc_test.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+import 'util.dart';
+
+void main() {
+  late AppFlowyBoardTest boardTest;
+  late FieldEditorBloc editorBloc;
+  late BoardTestContext context;
+
+  setUpAll(() async {
+    boardTest = await AppFlowyBoardTest.ensureInitialized();
+    context = await boardTest.createTestBoard();
+    final fieldContext = context.singleSelectFieldContext();
+    editorBloc = context.createFieldEditor(
+      fieldContext: fieldContext,
+    )..add(const FieldEditorEvent.initial());
+
+    await boardResponseFuture();
+  });
+
+  group('Group with not support grouping field:', () {
+    blocTest<FieldEditorBloc, FieldEditorState>(
+      "switch to text field",
+      build: () => editorBloc,
+      wait: boardResponseDuration(),
+      act: (bloc) async {
+        bloc.add(const FieldEditorEvent.switchToField(FieldType.RichText));
+      },
+      verify: (bloc) {
+        bloc.state.field.fold(
+          () => throw Exception(),
+          (field) => field.fieldType == FieldType.RichText,
+        );
+      },
+    );
+    blocTest<BoardBloc, BoardState>(
+      'assert the number of groups is 1',
+      build: () =>
+          BoardBloc(view: context.gridView)..add(const BoardEvent.initial()),
+      wait: boardResponseDuration(),
+      verify: (bloc) {
+        assert(bloc.groupControllers.values.length == 1,
+            "Expected 1, but receive ${bloc.groupControllers.values.length}");
+      },
+    );
+  });
+}

+ 156 - 4
frontend/app_flowy/test/bloc_test/board_test/util.dart

@@ -1,12 +1,58 @@
+import 'dart:collection';
+
+import 'package:app_flowy/plugins/board/application/board_data_controller.dart';
+import 'package:app_flowy/plugins/board/board.dart';
+import 'package:app_flowy/plugins/grid/application/block/block_cache.dart';
+import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
+import 'package:app_flowy/plugins/grid/application/field/field_controller.dart';
+import 'package:app_flowy/plugins/grid/application/field/field_editor_bloc.dart';
+import 'package:app_flowy/plugins/grid/application/field/field_service.dart';
+import 'package:app_flowy/plugins/grid/application/field/type_option/type_option_context.dart';
+import 'package:app_flowy/plugins/grid/application/row/row_bloc.dart';
+import 'package:app_flowy/plugins/grid/application/row/row_cache.dart';
+import 'package:app_flowy/plugins/grid/application/row/row_data_controller.dart';
+import 'package:app_flowy/workspace/application/app/app_service.dart';
+import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
+
+import '../../util.dart';
 import '../grid_test/util.dart';
 
 class AppFlowyBoardTest {
-  final AppFlowyGridTest context;
-  AppFlowyBoardTest(this.context);
+  final AppFlowyUnitTest unitTest;
+
+  AppFlowyBoardTest({required this.unitTest});
 
   static Future<AppFlowyBoardTest> ensureInitialized() async {
-    final inner = await AppFlowyGridTest.ensureInitialized();
-    return AppFlowyBoardTest(inner);
+    final inner = await AppFlowyUnitTest.ensureInitialized();
+    return AppFlowyBoardTest(unitTest: inner);
+  }
+
+  Future<BoardTestContext> createTestBoard() async {
+    final app = await unitTest.createTestApp();
+    final builder = BoardPluginBuilder();
+    return AppService()
+        .createView(
+      appId: app.id,
+      name: "Test Board",
+      dataFormatType: builder.dataFormatType,
+      pluginType: builder.pluginType,
+      layoutType: builder.layoutType!,
+    )
+        .then((result) {
+      return result.fold(
+        (view) async {
+          final context =
+              BoardTestContext(view, BoardDataController(view: view));
+          final result = await context._boardDataController.openGrid();
+          result.fold((l) => null, (r) => throw Exception(r));
+          return context;
+        },
+        (error) {
+          throw Exception();
+        },
+      );
+    });
   }
 }
 
@@ -17,3 +63,109 @@ Future<void> boardResponseFuture() {
 Duration boardResponseDuration({int milliseconds = 200}) {
   return Duration(milliseconds: milliseconds);
 }
+
+class BoardTestContext {
+  final ViewPB gridView;
+  final BoardDataController _boardDataController;
+
+  BoardTestContext(this.gridView, this._boardDataController);
+
+  List<RowInfo> get rowInfos {
+    return _boardDataController.rowInfos;
+  }
+
+  UnmodifiableMapView<String, GridBlockCache> get blocks {
+    return _boardDataController.blocks;
+  }
+
+  List<GridFieldContext> get fieldContexts => fieldController.fieldContexts;
+
+  GridFieldController get fieldController {
+    return _boardDataController.fieldController;
+  }
+
+  FieldEditorBloc createFieldEditor({
+    GridFieldContext? fieldContext,
+  }) {
+    IFieldTypeOptionLoader loader;
+    if (fieldContext == null) {
+      loader = NewFieldTypeOptionLoader(gridId: gridView.id);
+    } else {
+      loader =
+          FieldTypeOptionLoader(gridId: gridView.id, field: fieldContext.field);
+    }
+
+    final editorBloc = FieldEditorBloc(
+      fieldName: fieldContext?.name ?? '',
+      isGroupField: fieldContext?.isGroupField ?? false,
+      loader: loader,
+      gridId: gridView.id,
+    );
+    return editorBloc;
+  }
+
+  Future<IGridCellController> makeCellController(String fieldId) async {
+    final builder = await makeCellControllerBuilder(fieldId);
+    return builder.build();
+  }
+
+  Future<GridCellControllerBuilder> makeCellControllerBuilder(
+    String fieldId,
+  ) async {
+    final RowInfo rowInfo = rowInfos.last;
+    final blockCache = blocks[rowInfo.rowPB.blockId];
+    final rowCache = blockCache?.rowCache;
+
+    final fieldController = _boardDataController.fieldController;
+
+    final rowDataController = GridRowDataController(
+      rowInfo: rowInfo,
+      fieldController: fieldController,
+      rowCache: rowCache!,
+    );
+
+    final rowBloc = RowBloc(
+      rowInfo: rowInfo,
+      dataController: rowDataController,
+    )..add(const RowEvent.initial());
+    await gridResponseFuture();
+
+    return GridCellControllerBuilder(
+      cellId: rowBloc.state.gridCellMap[fieldId]!,
+      cellCache: rowCache.cellCache,
+      delegate: rowDataController,
+    );
+  }
+
+  Future<FieldEditorBloc> createField(FieldType fieldType) async {
+    final editorBloc = createFieldEditor()
+      ..add(const FieldEditorEvent.initial());
+    await gridResponseFuture();
+    editorBloc.add(FieldEditorEvent.switchToField(fieldType));
+    await gridResponseFuture();
+    return Future(() => editorBloc);
+  }
+
+  GridFieldContext singleSelectFieldContext() {
+    final fieldContext = fieldContexts
+        .firstWhere((element) => element.fieldType == FieldType.SingleSelect);
+    return fieldContext;
+  }
+
+  GridFieldCellContext singleSelectFieldCellContext() {
+    final field = singleSelectFieldContext().field;
+    return GridFieldCellContext(gridId: gridView.id, field: field);
+  }
+
+  GridFieldContext textFieldContext() {
+    final fieldContext = fieldContexts
+        .firstWhere((element) => element.fieldType == FieldType.RichText);
+    return fieldContext;
+  }
+
+  GridFieldContext checkboxFieldContext() {
+    final fieldContext = fieldContexts
+        .firstWhere((element) => element.fieldType == FieldType.Checkbox);
+    return fieldContext;
+  }
+}

+ 22 - 7
frontend/app_flowy/test/bloc_test/grid_test/field_edit_bloc_test.dart

@@ -3,9 +3,24 @@ import 'package:app_flowy/plugins/grid/application/prelude.dart';
 import 'package:bloc_test/bloc_test.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
 import 'package:flutter_test/flutter_test.dart';
-
 import 'util.dart';
 
+Future<FieldEditorBloc> createEditorBloc(AppFlowyGridTest gridTest) async {
+  final context = await gridTest.createTestGrid();
+  final fieldContext = context.singleSelectFieldContext();
+  final loader = FieldTypeOptionLoader(
+    gridId: context.gridView.id,
+    field: fieldContext.field,
+  );
+
+  return FieldEditorBloc(
+    gridId: context.gridView.id,
+    fieldName: fieldContext.name,
+    isGroupField: fieldContext.isGroupField,
+    loader: loader,
+  )..add(const FieldEditorEvent.initial());
+}
+
 void main() {
   late AppFlowyGridTest gridTest;
 
@@ -17,15 +32,15 @@ void main() {
     late FieldEditorBloc editorBloc;
 
     setUp(() async {
-      await gridTest.createTestGrid();
-      final fieldContext = gridTest.singleSelectFieldContext();
+      final context = await gridTest.createTestGrid();
+      final fieldContext = context.singleSelectFieldContext();
       final loader = FieldTypeOptionLoader(
-        gridId: gridTest.gridView.id,
+        gridId: context.gridView.id,
         field: fieldContext.field,
       );
 
       editorBloc = FieldEditorBloc(
-        gridId: gridTest.gridView.id,
+        gridId: context.gridView.id,
         fieldName: fieldContext.name,
         isGroupField: fieldContext.isGroupField,
         loader: loader,
@@ -65,7 +80,7 @@ void main() {
           (field) {
             // The default length of the fields is 3. The length of the fields
             // should not change after switching to other field type
-            assert(gridTest.fieldContexts.length == 3);
+            // assert(gridTest.fieldContexts.length == 3);
             assert(field.fieldType == FieldType.RichText);
           },
         );
@@ -80,7 +95,7 @@ void main() {
       },
       wait: gridResponseDuration(),
       verify: (bloc) {
-        assert(gridTest.fieldContexts.length == 2);
+        // assert(gridTest.fieldContexts.length == 2);
       },
     );
   });

+ 85 - 5
frontend/app_flowy/test/bloc_test/grid_test/filter_bloc_test.dart

@@ -1,4 +1,6 @@
 import 'package:app_flowy/plugins/grid/application/filter/filter_bloc.dart';
+import 'package:app_flowy/plugins/grid/application/grid_bloc.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/checkbox_filter.pbenum.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid/text_filter.pb.dart';
 import 'package:flutter_test/flutter_test.dart';
 import 'package:bloc_test/bloc_test.dart';
@@ -11,15 +13,17 @@ void main() {
   });
 
   group('$GridFilterBloc', () {
+    late GridTestContext context;
     setUp(() async {
-      await gridTest.createTestGrid();
+      context = await gridTest.createTestGrid();
     });
+
     blocTest<GridFilterBloc, GridFilterState>(
       "create a text filter",
-      build: () => GridFilterBloc(viewId: gridTest.gridView.id)
+      build: () => GridFilterBloc(viewId: context.gridView.id)
         ..add(const GridFilterEvent.initial()),
       act: (bloc) async {
-        final textField = gridTest.textFieldContext();
+        final textField = context.textFieldContext();
         bloc.add(
           GridFilterEvent.createTextFilter(
               fieldId: textField.id,
@@ -35,10 +39,10 @@ void main() {
 
     blocTest<GridFilterBloc, GridFilterState>(
       "delete a text filter",
-      build: () => GridFilterBloc(viewId: gridTest.gridView.id)
+      build: () => GridFilterBloc(viewId: context.gridView.id)
         ..add(const GridFilterEvent.initial()),
       act: (bloc) async {
-        final textField = gridTest.textFieldContext();
+        final textField = context.textFieldContext();
         bloc.add(
           GridFilterEvent.createTextFilter(
               fieldId: textField.id,
@@ -61,4 +65,80 @@ void main() {
       },
     );
   });
+
+  test('filter rows with condition: text is empty', () async {
+    final context = await gridTest.createTestGrid();
+    final filterBloc = GridFilterBloc(viewId: context.gridView.id)
+      ..add(const GridFilterEvent.initial());
+
+    final gridBloc = GridBloc(view: context.gridView)
+      ..add(const GridEvent.initial());
+
+    final textField = context.textFieldContext();
+    await gridResponseFuture();
+    filterBloc.add(
+      GridFilterEvent.createTextFilter(
+          fieldId: textField.id,
+          condition: TextFilterCondition.TextIsEmpty,
+          content: ""),
+    );
+
+    await gridResponseFuture();
+    assert(gridBloc.state.rowInfos.length == 3);
+  });
+
+  test('filter rows with condition: text is not empty', () async {
+    final context = await gridTest.createTestGrid();
+    final filterBloc = GridFilterBloc(viewId: context.gridView.id)
+      ..add(const GridFilterEvent.initial());
+
+    final textField = context.textFieldContext();
+    await gridResponseFuture();
+    filterBloc.add(
+      GridFilterEvent.createTextFilter(
+          fieldId: textField.id,
+          condition: TextFilterCondition.TextIsNotEmpty,
+          content: ""),
+    );
+    await gridResponseFuture();
+    assert(context.rowInfos.isEmpty);
+  });
+
+  test('filter rows with condition: checkbox uncheck', () async {
+    final context = await gridTest.createTestGrid();
+    final checkboxField = context.checkboxFieldContext();
+    final filterBloc = GridFilterBloc(viewId: context.gridView.id)
+      ..add(const GridFilterEvent.initial());
+    final gridBloc = GridBloc(view: context.gridView)
+      ..add(const GridEvent.initial());
+
+    await gridResponseFuture();
+    filterBloc.add(
+      GridFilterEvent.createCheckboxFilter(
+        fieldId: checkboxField.id,
+        condition: CheckboxFilterCondition.IsUnChecked,
+      ),
+    );
+    await gridResponseFuture();
+    assert(gridBloc.state.rowInfos.length == 3);
+  });
+
+  test('filter rows with condition: checkbox check', () async {
+    final context = await gridTest.createTestGrid();
+    final checkboxField = context.checkboxFieldContext();
+    final filterBloc = GridFilterBloc(viewId: context.gridView.id)
+      ..add(const GridFilterEvent.initial());
+    final gridBloc = GridBloc(view: context.gridView)
+      ..add(const GridEvent.initial());
+
+    await gridResponseFuture();
+    filterBloc.add(
+      GridFilterEvent.createCheckboxFilter(
+        fieldId: checkboxField.id,
+        condition: CheckboxFilterCondition.IsChecked,
+      ),
+    );
+    await gridResponseFuture();
+    assert(gridBloc.state.rowInfos.isEmpty);
+  });
 }

+ 4 - 3
frontend/app_flowy/test/bloc_test/grid_test/grid_bloc_test.dart

@@ -10,14 +10,15 @@ void main() {
   });
 
   group('Edit Grid:', () {
+    late GridTestContext context;
     setUp(() async {
-      await gridTest.createTestGrid();
+      context = await gridTest.createTestGrid();
     });
     // The initial number of rows is 3 for each grid.
     blocTest<GridBloc, GridState>(
       "create a row",
       build: () =>
-          GridBloc(view: gridTest.gridView)..add(const GridEvent.initial()),
+          GridBloc(view: context.gridView)..add(const GridEvent.initial()),
       act: (bloc) => bloc.add(const GridEvent.createRow()),
       wait: const Duration(milliseconds: 300),
       verify: (bloc) {
@@ -28,7 +29,7 @@ void main() {
     blocTest<GridBloc, GridState>(
       "delete the last row",
       build: () =>
-          GridBloc(view: gridTest.gridView)..add(const GridEvent.initial()),
+          GridBloc(view: context.gridView)..add(const GridEvent.initial()),
       act: (bloc) async {
         await gridResponseFuture();
         bloc.add(GridEvent.deleteRow(bloc.state.rowInfos.last));

+ 13 - 12
frontend/app_flowy/test/bloc_test/grid_test/grid_header_bloc_test.dart

@@ -14,10 +14,11 @@ void main() {
 
   group('$GridHeaderBloc', () {
     late FieldActionSheetBloc actionSheetBloc;
+    late GridTestContext context;
     setUp(() async {
-      await gridTest.createTestGrid();
+      context = await gridTest.createTestGrid();
       actionSheetBloc = FieldActionSheetBloc(
-        fieldCellContext: gridTest.singleSelectFieldCellContext(),
+        fieldCellContext: context.singleSelectFieldCellContext(),
       );
     });
 
@@ -25,8 +26,8 @@ void main() {
       "hides property",
       build: () {
         final bloc = GridHeaderBloc(
-          gridId: gridTest.gridView.id,
-          fieldController: gridTest.fieldController,
+          gridId: context.gridView.id,
+          fieldController: context.fieldController,
         )..add(const GridHeaderEvent.initial());
         return bloc;
       },
@@ -44,8 +45,8 @@ void main() {
       "shows property",
       build: () {
         final bloc = GridHeaderBloc(
-          gridId: gridTest.gridView.id,
-          fieldController: gridTest.fieldController,
+          gridId: context.gridView.id,
+          fieldController: context.fieldController,
         )..add(const GridHeaderEvent.initial());
         return bloc;
       },
@@ -65,8 +66,8 @@ void main() {
       "duplicate property",
       build: () {
         final bloc = GridHeaderBloc(
-          gridId: gridTest.gridView.id,
-          fieldController: gridTest.fieldController,
+          gridId: context.gridView.id,
+          fieldController: context.fieldController,
         )..add(const GridHeaderEvent.initial());
         return bloc;
       },
@@ -84,8 +85,8 @@ void main() {
       "delete property",
       build: () {
         final bloc = GridHeaderBloc(
-          gridId: gridTest.gridView.id,
-          fieldController: gridTest.fieldController,
+          gridId: context.gridView.id,
+          fieldController: context.fieldController,
         )..add(const GridHeaderEvent.initial());
         return bloc;
       },
@@ -103,8 +104,8 @@ void main() {
       "update name",
       build: () {
         final bloc = GridHeaderBloc(
-          gridId: gridTest.gridView.id,
-          fieldController: gridTest.fieldController,
+          gridId: context.gridView.id,
+          fieldController: context.fieldController,
         )..add(const GridHeaderEvent.initial());
         return bloc;
       },

+ 2 - 2
frontend/app_flowy/test/bloc_test/grid_test/select_option_bloc_test.dart

@@ -9,9 +9,9 @@ import 'package:bloc_test/bloc_test.dart';
 import 'util.dart';
 
 void main() {
-  late AppFlowyGridSelectOptionCellTest cellTest;
+  late AppFlowyGridCellTest cellTest;
   setUpAll(() async {
-    cellTest = await AppFlowyGridSelectOptionCellTest.ensureInitialized();
+    cellTest = await AppFlowyGridCellTest.ensureInitialized();
   });
 
   group('SingleSelectOptionBloc', () {

+ 52 - 116
frontend/app_flowy/test/bloc_test/grid_test/util.dart

@@ -1,6 +1,4 @@
 import 'dart:collection';
-import 'package:app_flowy/plugins/board/application/board_data_controller.dart';
-import 'package:app_flowy/plugins/board/board.dart';
 import 'package:app_flowy/plugins/grid/application/block/block_cache.dart';
 import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
 import 'package:app_flowy/plugins/grid/application/field/field_controller.dart';
@@ -18,64 +16,28 @@ import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
 
 import '../../util.dart';
 
-/// Create a empty Grid for test
-class AppFlowyGridTest {
-  final AppFlowyUnitTest unitTest;
-  late ViewPB gridView;
-  GridDataController? _gridDataController;
-  BoardDataController? _boardDataController;
-
-  AppFlowyGridTest({required this.unitTest});
+class GridTestContext {
+  final ViewPB gridView;
+  final GridDataController _gridDataController;
 
-  static Future<AppFlowyGridTest> ensureInitialized() async {
-    final inner = await AppFlowyUnitTest.ensureInitialized();
-    return AppFlowyGridTest(unitTest: inner);
-  }
+  GridTestContext(this.gridView, this._gridDataController);
 
   List<RowInfo> get rowInfos {
-    if (_gridDataController != null) {
-      return _gridDataController!.rowInfos;
-    }
-
-    if (_boardDataController != null) {
-      return _boardDataController!.rowInfos;
-    }
-
-    throw Exception();
+    return _gridDataController.rowInfos;
   }
 
   UnmodifiableMapView<String, GridBlockCache> get blocks {
-    if (_gridDataController != null) {
-      return _gridDataController!.blocks;
-    }
-
-    if (_boardDataController != null) {
-      return _boardDataController!.blocks;
-    }
-
-    throw Exception();
+    return _gridDataController.blocks;
   }
 
   List<GridFieldContext> get fieldContexts => fieldController.fieldContexts;
 
   GridFieldController get fieldController {
-    if (_gridDataController != null) {
-      return _gridDataController!.fieldController;
-    }
-
-    if (_boardDataController != null) {
-      return _boardDataController!.fieldController;
-    }
-
-    throw Exception();
+    return _gridDataController.fieldController;
   }
 
   Future<void> createRow() async {
-    if (_gridDataController != null) {
-      return _gridDataController!.createRow();
-    }
-
-    throw Exception();
+    return _gridDataController.createRow();
   }
 
   FieldEditorBloc createFieldEditor({
@@ -109,14 +71,7 @@ class AppFlowyGridTest {
     final RowInfo rowInfo = rowInfos.last;
     final blockCache = blocks[rowInfo.rowPB.blockId];
     final rowCache = blockCache?.rowCache;
-    late GridFieldController fieldController;
-    if (_gridDataController != null) {
-      fieldController = _gridDataController!.fieldController;
-    }
-
-    if (_boardDataController != null) {
-      fieldController = _boardDataController!.fieldController;
-    }
+    final fieldController = _gridDataController.fieldController;
 
     final rowDataController = GridRowDataController(
       rowInfo: rowInfo,
@@ -163,55 +118,56 @@ class AppFlowyGridTest {
     return fieldContext;
   }
 
-  Future<void> createTestGrid() async {
-    final app = await unitTest.createTestApp();
-    final builder = GridPluginBuilder();
-    final result = await AppService().createView(
-      appId: app.id,
-      name: "Test Grid",
-      dataFormatType: builder.dataFormatType,
-      pluginType: builder.pluginType,
-      layoutType: builder.layoutType!,
-    );
-    await result.fold(
-      (view) async {
-        gridView = view;
-        _gridDataController = GridDataController(view: view);
-        await openGrid();
-      },
-      (error) {},
-    );
+  GridFieldContext checkboxFieldContext() {
+    final fieldContext = fieldContexts
+        .firstWhere((element) => element.fieldType == FieldType.Checkbox);
+    return fieldContext;
   }
+}
+
+/// Create a empty Grid for test
+class AppFlowyGridTest {
+  final AppFlowyUnitTest unitTest;
+
+  AppFlowyGridTest({required this.unitTest});
 
-  Future<void> openGrid() async {
-    final result = await _gridDataController!.openGrid();
-    result.fold((l) => null, (r) => throw Exception(r));
+  static Future<AppFlowyGridTest> ensureInitialized() async {
+    final inner = await AppFlowyUnitTest.ensureInitialized();
+    return AppFlowyGridTest(unitTest: inner);
   }
 
-  Future<void> createTestBoard() async {
+  Future<GridTestContext> createTestGrid() async {
     final app = await unitTest.createTestApp();
-    final builder = BoardPluginBuilder();
-    final result = await AppService().createView(
+    final builder = GridPluginBuilder();
+    final context = await AppService()
+        .createView(
       appId: app.id,
-      name: "Test Board",
+      name: "Test Grid",
       dataFormatType: builder.dataFormatType,
       pluginType: builder.pluginType,
       layoutType: builder.layoutType!,
-    );
-    await result.fold(
-      (view) async {
-        _boardDataController = BoardDataController(view: view);
-        final result = await _boardDataController!.openGrid();
-        result.fold((l) => null, (r) => throw Exception(r));
-        gridView = view;
-      },
-      (error) {},
-    );
+    )
+        .then((result) {
+      return result.fold(
+        (view) async {
+          final context = GridTestContext(view, GridDataController(view: view));
+          final result = await context._gridDataController.openGrid();
+          result.fold((l) => null, (r) => throw Exception(r));
+          return context;
+        },
+        (error) {
+          throw Exception();
+        },
+      );
+    });
+
+    return context;
   }
 }
 
 /// Create a new Grid for cell test
 class AppFlowyGridCellTest {
+  late GridTestContext context;
   final AppFlowyGridTest gridTest;
   AppFlowyGridCellTest({required this.gridTest});
 
@@ -220,32 +176,12 @@ class AppFlowyGridCellTest {
     return AppFlowyGridCellTest(gridTest: gridTest);
   }
 
-  Future<void> createTestRow() async {
-    await gridTest.createRow();
-  }
-
-  Future<void> createTestGrid() async {
-    await gridTest.createTestGrid();
-  }
-}
-
-class AppFlowyGridSelectOptionCellTest {
-  final AppFlowyGridCellTest _gridCellTest;
-
-  AppFlowyGridSelectOptionCellTest(AppFlowyGridCellTest cellTest)
-      : _gridCellTest = cellTest;
-
-  static Future<AppFlowyGridSelectOptionCellTest> ensureInitialized() async {
-    final gridTest = await AppFlowyGridCellTest.ensureInitialized();
-    return AppFlowyGridSelectOptionCellTest(gridTest);
-  }
-
   Future<void> createTestGrid() async {
-    await _gridCellTest.createTestGrid();
+    context = await gridTest.createTestGrid();
   }
 
   Future<void> createTestRow() async {
-    await _gridCellTest.createTestRow();
+    await context.createRow();
   }
 
   Future<GridSelectOptionCellController> makeCellController(
@@ -253,17 +189,17 @@ class AppFlowyGridSelectOptionCellTest {
     assert(fieldType == FieldType.SingleSelect ||
         fieldType == FieldType.MultiSelect);
 
-    final fieldContexts = _gridCellTest.gridTest.fieldContexts;
+    final fieldContexts = context.fieldContexts;
     final field =
         fieldContexts.firstWhere((element) => element.fieldType == fieldType);
-    final cellController = await _gridCellTest.gridTest
-        .makeCellController(field.id) as GridSelectOptionCellController;
+    final cellController = await context.makeCellController(field.id)
+        as GridSelectOptionCellController;
     return cellController;
   }
 }
 
-Future<void> gridResponseFuture() {
-  return Future.delayed(gridResponseDuration(milliseconds: 200));
+Future<void> gridResponseFuture({int milliseconds = 500}) {
+  return Future.delayed(gridResponseDuration(milliseconds: milliseconds));
 }
 
 Duration gridResponseDuration({int milliseconds = 200}) {

+ 126 - 289
frontend/app_flowy/test/bloc_test/home_test/app_bloc_test.dart

@@ -1,16 +1,10 @@
-import 'package:app_flowy/plugins/board/application/board_bloc.dart';
-import 'package:app_flowy/plugins/board/board.dart';
 import 'package:app_flowy/plugins/document/application/doc_bloc.dart';
 import 'package:app_flowy/plugins/document/document.dart';
-import 'package:app_flowy/plugins/grid/application/grid_bloc.dart';
 import 'package:app_flowy/plugins/grid/grid.dart';
 import 'package:app_flowy/workspace/application/app/app_bloc.dart';
 import 'package:app_flowy/workspace/application/menu/menu_view_section_bloc.dart';
 import 'package:flowy_sdk/dispatch/dispatch.dart';
-import 'package:flowy_sdk/protobuf/flowy-folder/app.pb.dart';
-import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart';
 import 'package:flutter_test/flutter_test.dart';
-import 'package:bloc_test/bloc_test.dart';
 import '../../util.dart';
 
 void main() {
@@ -19,310 +13,153 @@ void main() {
     testContext = await AppFlowyUnitTest.ensureInitialized();
   });
 
-  group(
-    '$AppBloc',
-    () {
-      late AppPB app;
-      setUp(() async {
-        app = await testContext.createTestApp();
-      });
+  test('rename app test', () async {
+    final app = await testContext.createTestApp();
+    final bloc = AppBloc(app: app)..add(const AppEvent.initial());
+    await blocResponseFuture();
 
-      blocTest<AppBloc, AppState>(
-        "Create a document",
-        build: () => AppBloc(app: app)..add(const AppEvent.initial()),
-        act: (bloc) {
-          bloc.add(
-              AppEvent.createView("Test document", DocumentPluginBuilder()));
-        },
-        wait: blocResponseDuration(),
-        verify: (bloc) {
-          assert(bloc.state.views.length == 1);
-          assert(bloc.state.views.last.name == "Test document");
-          assert(bloc.state.views.last.layout == ViewLayoutTypePB.Document);
-        },
-      );
+    bloc.add(const AppEvent.rename('Hello world'));
+    await blocResponseFuture();
 
-      blocTest<AppBloc, AppState>(
-        "Create a grid",
-        build: () => AppBloc(app: app)..add(const AppEvent.initial()),
-        act: (bloc) {
-          bloc.add(AppEvent.createView("Test grid", GridPluginBuilder()));
-        },
-        wait: blocResponseDuration(),
-        verify: (bloc) {
-          assert(bloc.state.views.length == 1);
-          assert(bloc.state.views.last.name == "Test grid");
-          assert(bloc.state.views.last.layout == ViewLayoutTypePB.Grid);
-        },
-      );
-
-      blocTest<AppBloc, AppState>(
-        "Create a Kanban board",
-        build: () => AppBloc(app: app)..add(const AppEvent.initial()),
-        act: (bloc) {
-          bloc.add(AppEvent.createView("Test board", BoardPluginBuilder()));
-        },
-        wait: const Duration(milliseconds: 100),
-        verify: (bloc) {
-          assert(bloc.state.views.length == 1);
-          assert(bloc.state.views.last.name == "Test board");
-          assert(bloc.state.views.last.layout == ViewLayoutTypePB.Board);
-        },
-      );
-    },
-  );
-
-  group('$AppBloc', () {
-    late AppPB app;
-    setUpAll(() async {
-      app = await testContext.createTestApp();
-    });
-
-    blocTest<AppBloc, AppState>(
-      "rename the app",
-      build: () => AppBloc(app: app)..add(const AppEvent.initial()),
-      wait: blocResponseDuration(),
-      act: (bloc) => bloc.add(const AppEvent.rename('Hello world')),
-      verify: (bloc) {
-        assert(bloc.state.app.name == 'Hello world');
-      },
-    );
-
-    blocTest<AppBloc, AppState>(
-      "delete the app",
-      build: () => AppBloc(app: app)..add(const AppEvent.initial()),
-      wait: blocResponseDuration(),
-      act: (bloc) => bloc.add(const AppEvent.delete()),
-      verify: (bloc) async {
-        final apps = await testContext.loadApps();
-        assert(apps.where((element) => element.id == app.id).isEmpty);
-      },
-    );
+    assert(bloc.state.app.name == 'Hello world');
   });
 
-  group('$AppBloc', () {
-    late ViewPB view;
-    late AppPB app;
-    setUpAll(() async {
-      app = await testContext.createTestApp();
-    });
+  test('delete ap test', () async {
+    final app = await testContext.createTestApp();
+    final bloc = AppBloc(app: app)..add(const AppEvent.initial());
+    await blocResponseFuture();
 
-    blocTest<AppBloc, AppState>(
-      "create a document",
-      build: () => AppBloc(app: app)..add(const AppEvent.initial()),
-      act: (bloc) {
-        bloc.add(AppEvent.createView("Test document", DocumentPluginBuilder()));
-      },
-      wait: blocResponseDuration(),
-      verify: (bloc) {
-        assert(bloc.state.views.length == 1);
-        view = bloc.state.views.last;
-      },
-    );
+    bloc.add(const AppEvent.delete());
+    await blocResponseFuture();
 
-    blocTest<AppBloc, AppState>(
-      "delete the document",
-      build: () => AppBloc(app: app)..add(const AppEvent.initial()),
-      act: (bloc) => bloc.add(AppEvent.deleteView(view.id)),
-      wait: blocResponseDuration(),
-      verify: (bloc) {
-        assert(bloc.state.views.isEmpty);
-      },
-    );
+    final apps = await testContext.loadApps();
+    assert(apps.where((element) => element.id == app.id).isEmpty);
   });
 
-  group('$AppBloc', () {
-    late AppPB app;
-    setUpAll(() async {
-      app = await testContext.createTestApp();
-    });
-    blocTest<AppBloc, AppState>(
-      "create documents' order test",
-      build: () => AppBloc(app: app)..add(const AppEvent.initial()),
-      act: (bloc) async {
-        bloc.add(AppEvent.createView("1", DocumentPluginBuilder()));
-        await blocResponseFuture();
-        bloc.add(AppEvent.createView("2", DocumentPluginBuilder()));
-        await blocResponseFuture();
-        bloc.add(AppEvent.createView("3", DocumentPluginBuilder()));
-        await blocResponseFuture();
-      },
-      wait: blocResponseDuration(),
-      verify: (bloc) {
-        assert(bloc.state.views[0].name == '1');
-        assert(bloc.state.views[1].name == '2');
-        assert(bloc.state.views[2].name == '3');
-      },
-    );
+  test('create documents in order', () async {
+    final app = await testContext.createTestApp();
+    final bloc = AppBloc(app: app)..add(const AppEvent.initial());
+    await blocResponseFuture();
+
+    bloc.add(AppEvent.createView("1", DocumentPluginBuilder()));
+    await blocResponseFuture();
+    bloc.add(AppEvent.createView("2", DocumentPluginBuilder()));
+    await blocResponseFuture();
+    bloc.add(AppEvent.createView("3", DocumentPluginBuilder()));
+    await blocResponseFuture();
+
+    assert(bloc.state.views[0].name == '1');
+    assert(bloc.state.views[1].name == '2');
+    assert(bloc.state.views[2].name == '3');
   });
 
-  group('$AppBloc', () {
-    late AppPB app;
-    setUpAll(() async {
-      app = await testContext.createTestApp();
-    });
-    blocTest<AppBloc, AppState>(
-      "reorder documents",
-      build: () => AppBloc(app: app)..add(const AppEvent.initial()),
-      act: (bloc) async {
-        bloc.add(AppEvent.createView("1", DocumentPluginBuilder()));
-        await blocResponseFuture();
-        bloc.add(AppEvent.createView("2", DocumentPluginBuilder()));
-        await blocResponseFuture();
-        bloc.add(AppEvent.createView("3", DocumentPluginBuilder()));
-        await blocResponseFuture();
-
-        final appViewData = AppViewDataContext(appId: app.id);
-        appViewData.views = bloc.state.views;
-        final viewSectionBloc = ViewSectionBloc(
-          appViewData: appViewData,
-        )..add(const ViewSectionEvent.initial());
-        await blocResponseFuture();
-
-        viewSectionBloc.add(const ViewSectionEvent.moveView(0, 2));
-        await blocResponseFuture();
-      },
-      wait: blocResponseDuration(),
-      verify: (bloc) {
-        assert(bloc.state.views[0].name == '2');
-        assert(bloc.state.views[1].name == '3');
-        assert(bloc.state.views[2].name == '1');
-      },
-    );
+  test('reorder documents test', () async {
+    final app = await testContext.createTestApp();
+    final bloc = AppBloc(app: app)..add(const AppEvent.initial());
+    await blocResponseFuture();
+
+    bloc.add(AppEvent.createView("1", DocumentPluginBuilder()));
+    await blocResponseFuture();
+    bloc.add(AppEvent.createView("2", DocumentPluginBuilder()));
+    await blocResponseFuture();
+    bloc.add(AppEvent.createView("3", DocumentPluginBuilder()));
+    await blocResponseFuture();
+
+    final appViewData = AppViewDataContext(appId: app.id);
+    appViewData.views = bloc.state.views;
+    final viewSectionBloc = ViewSectionBloc(
+      appViewData: appViewData,
+    )..add(const ViewSectionEvent.initial());
+    await blocResponseFuture();
+
+    viewSectionBloc.add(const ViewSectionEvent.moveView(0, 2));
+    await blocResponseFuture();
+
+    assert(bloc.state.views[0].name == '2');
+    assert(bloc.state.views[1].name == '3');
+    assert(bloc.state.views[2].name == '1');
   });
 
-  group('$AppBloc', () {
-    late AppPB app;
-    setUpAll(() async {
-      app = await testContext.createTestApp();
-    });
-    blocTest<AppBloc, AppState>(
+  test('open latest view test', () async {
+    final app = await testContext.createTestApp();
+    final bloc = AppBloc(app: app)..add(const AppEvent.initial());
+    await blocResponseFuture();
+    assert(
+      bloc.state.latestCreatedView == null,
       "assert initial latest create view is null after initialize",
-      build: () => AppBloc(app: app)..add(const AppEvent.initial()),
-      wait: blocResponseDuration(),
-      verify: (bloc) {
-        assert(bloc.state.latestCreatedView == null);
-      },
     );
-    blocTest<AppBloc, AppState>(
+
+    bloc.add(AppEvent.createView("1", DocumentPluginBuilder()));
+    await blocResponseFuture();
+    assert(
+      bloc.state.latestCreatedView!.id == bloc.state.views.last.id,
       "create a view and assert the latest create view is this view",
-      build: () => AppBloc(app: app)..add(const AppEvent.initial()),
-      act: (bloc) async {
-        bloc.add(AppEvent.createView("1", DocumentPluginBuilder()));
-      },
-      wait: blocResponseDuration(),
-      verify: (bloc) {
-        assert(bloc.state.latestCreatedView!.id == bloc.state.views.last.id);
-      },
     );
 
-    blocTest<AppBloc, AppState>(
+    bloc.add(AppEvent.createView("2", DocumentPluginBuilder()));
+    await blocResponseFuture();
+    assert(
+      bloc.state.latestCreatedView!.id == bloc.state.views.last.id,
       "create a view and assert the latest create view is this view",
-      build: () => AppBloc(app: app)..add(const AppEvent.initial()),
-      act: (bloc) async {
-        bloc.add(AppEvent.createView("2", DocumentPluginBuilder()));
-      },
-      wait: blocResponseDuration(),
-      verify: (bloc) {
-        assert(bloc.state.views[0].name == "1");
-        assert(bloc.state.latestCreatedView!.id == bloc.state.views.last.id);
-      },
-    );
-    blocTest<AppBloc, AppState>(
-      "check latest create view is null after reinitialize",
-      build: () => AppBloc(app: app)..add(const AppEvent.initial()),
-      wait: blocResponseDuration(),
-      verify: (bloc) {
-        assert(bloc.state.latestCreatedView == null);
-      },
     );
   });
 
-  group('$AppBloc', () {
-    late AppPB app;
-    late ViewPB latestCreatedView;
-    setUpAll(() async {
-      app = await testContext.createTestApp();
-    });
-
-// Document
-    blocTest<AppBloc, AppState>(
-      "create a document view",
-      build: () => AppBloc(app: app)..add(const AppEvent.initial()),
-      act: (bloc) async {
-        bloc.add(AppEvent.createView("New document", DocumentPluginBuilder()));
-      },
-      wait: blocResponseDuration(),
-      verify: (bloc) {
-        latestCreatedView = bloc.state.views.last;
-      },
-    );
-
-    blocTest<DocumentBloc, DocumentState>(
-      "open the document",
-      build: () => DocumentBloc(view: latestCreatedView)
-        ..add(const DocumentEvent.initial()),
-      wait: blocResponseDuration(),
-    );
-
-    test('check latest opened view is this document', () async {
-      final workspaceSetting = await FolderEventReadCurrentWorkspace()
-          .send()
-          .then((result) => result.fold((l) => l, (r) => throw Exception()));
-      workspaceSetting.latestView.id == latestCreatedView.id;
-    });
-
-// Grid
-    blocTest<AppBloc, AppState>(
-      "create a grid view",
-      build: () => AppBloc(app: app)..add(const AppEvent.initial()),
-      act: (bloc) async {
-        bloc.add(AppEvent.createView("New grid", GridPluginBuilder()));
-      },
-      wait: blocResponseDuration(),
-      verify: (bloc) {
-        latestCreatedView = bloc.state.views.last;
-      },
-    );
-    blocTest<GridBloc, GridState>(
-      "open the grid",
-      build: () =>
-          GridBloc(view: latestCreatedView)..add(const GridEvent.initial()),
-      wait: blocResponseDuration(),
-    );
-
-    test('check latest opened view is this grid', () async {
-      final workspaceSetting = await FolderEventReadCurrentWorkspace()
-          .send()
-          .then((result) => result.fold((l) => l, (r) => throw Exception()));
-      workspaceSetting.latestView.id == latestCreatedView.id;
-    });
-
-// Board
-    blocTest<AppBloc, AppState>(
-      "create a board view",
-      build: () => AppBloc(app: app)..add(const AppEvent.initial()),
-      act: (bloc) async {
-        bloc.add(AppEvent.createView("New board", BoardPluginBuilder()));
-      },
-      wait: blocResponseDuration(),
-      verify: (bloc) {
-        latestCreatedView = bloc.state.views.last;
-      },
-    );
-
-    blocTest<BoardBloc, BoardState>(
-      "open the board",
-      build: () =>
-          BoardBloc(view: latestCreatedView)..add(const BoardEvent.initial()),
-      wait: blocResponseDuration(),
-    );
+  test('open latest documents test', () async {
+    final app = await testContext.createTestApp();
+    final bloc = AppBloc(app: app)..add(const AppEvent.initial());
+    await blocResponseFuture();
+
+    bloc.add(AppEvent.createView("document 1", DocumentPluginBuilder()));
+    await blocResponseFuture();
+    final document1 = bloc.state.latestCreatedView;
+    assert(document1!.name == "document 1");
+
+    bloc.add(AppEvent.createView("document 2", DocumentPluginBuilder()));
+    await blocResponseFuture();
+    final document2 = bloc.state.latestCreatedView;
+    assert(document2!.name == "document 2");
+
+    // Open document 1
+    // ignore: unused_local_variable
+    final documentBloc = DocumentBloc(view: document1!)
+      ..add(const DocumentEvent.initial());
+    await blocResponseFuture();
+
+    final workspaceSetting = await FolderEventReadCurrentWorkspace()
+        .send()
+        .then((result) => result.fold((l) => l, (r) => throw Exception()));
+    workspaceSetting.latestView.id == document1.id;
+  });
 
-    test('check latest opened view is this board', () async {
-      final workspaceSetting = await FolderEventReadCurrentWorkspace()
-          .send()
-          .then((result) => result.fold((l) => l, (r) => throw Exception()));
-      workspaceSetting.latestView.id == latestCreatedView.id;
-    });
+  test('open latest grid test', () async {
+    final app = await testContext.createTestApp();
+    final bloc = AppBloc(app: app)..add(const AppEvent.initial());
+    await blocResponseFuture();
+
+    bloc.add(AppEvent.createView("grid 1", GridPluginBuilder()));
+    await blocResponseFuture();
+    final grid1 = bloc.state.latestCreatedView;
+    assert(grid1!.name == "grid 1");
+
+    bloc.add(AppEvent.createView("grid 2", GridPluginBuilder()));
+    await blocResponseFuture();
+    final grid2 = bloc.state.latestCreatedView;
+    assert(grid2!.name == "grid 2");
+
+    var workspaceSetting = await FolderEventReadCurrentWorkspace()
+        .send()
+        .then((result) => result.fold((l) => l, (r) => throw Exception()));
+    workspaceSetting.latestView.id == grid1!.id;
+
+    // Open grid 1
+    // ignore: unused_local_variable
+    final documentBloc = DocumentBloc(view: grid1)
+      ..add(const DocumentEvent.initial());
+    await blocResponseFuture();
+
+    workspaceSetting = await FolderEventReadCurrentWorkspace()
+        .send()
+        .then((result) => result.fold((l) => l, (r) => throw Exception()));
+    workspaceSetting.latestView.id == grid1.id;
   });
 }

+ 69 - 0
frontend/app_flowy/test/bloc_test/home_test/create_page_test.dart

@@ -0,0 +1,69 @@
+import 'package:app_flowy/plugins/board/board.dart';
+import 'package:app_flowy/plugins/document/document.dart';
+import 'package:app_flowy/plugins/grid/grid.dart';
+import 'package:app_flowy/workspace/application/app/app_bloc.dart';
+import 'package:flowy_sdk/protobuf/flowy-folder/app.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:bloc_test/bloc_test.dart';
+import '../../util.dart';
+
+void main() {
+  late AppFlowyUnitTest testContext;
+  setUpAll(() async {
+    testContext = await AppFlowyUnitTest.ensureInitialized();
+  });
+
+  group(
+    '$AppBloc',
+    () {
+      late AppPB app;
+      setUp(() async {
+        app = await testContext.createTestApp();
+      });
+
+      blocTest<AppBloc, AppState>(
+        "Create a document",
+        build: () => AppBloc(app: app)..add(const AppEvent.initial()),
+        act: (bloc) {
+          bloc.add(
+              AppEvent.createView("Test document", DocumentPluginBuilder()));
+        },
+        wait: blocResponseDuration(),
+        verify: (bloc) {
+          assert(bloc.state.views.length == 1);
+          assert(bloc.state.views.last.name == "Test document");
+          assert(bloc.state.views.last.layout == ViewLayoutTypePB.Document);
+        },
+      );
+
+      blocTest<AppBloc, AppState>(
+        "Create a grid",
+        build: () => AppBloc(app: app)..add(const AppEvent.initial()),
+        act: (bloc) {
+          bloc.add(AppEvent.createView("Test grid", GridPluginBuilder()));
+        },
+        wait: blocResponseDuration(),
+        verify: (bloc) {
+          assert(bloc.state.views.length == 1);
+          assert(bloc.state.views.last.name == "Test grid");
+          assert(bloc.state.views.last.layout == ViewLayoutTypePB.Grid);
+        },
+      );
+
+      blocTest<AppBloc, AppState>(
+        "Create a Kanban board",
+        build: () => AppBloc(app: app)..add(const AppEvent.initial()),
+        act: (bloc) {
+          bloc.add(AppEvent.createView("Test board", BoardPluginBuilder()));
+        },
+        wait: const Duration(milliseconds: 100),
+        verify: (bloc) {
+          assert(bloc.state.views.length == 1);
+          assert(bloc.state.views.last.name == "Test board");
+          assert(bloc.state.views.last.layout == ViewLayoutTypePB.Board);
+        },
+      );
+    },
+  );
+}

+ 70 - 149
frontend/app_flowy/test/bloc_test/home_test/trash_bloc_test.dart

@@ -1,20 +1,53 @@
 import 'package:app_flowy/plugins/document/document.dart';
 import 'package:app_flowy/plugins/trash/application/trash_bloc.dart';
 import 'package:app_flowy/workspace/application/app/app_bloc.dart';
-import 'package:bloc_test/bloc_test.dart';
 import 'package:flowy_sdk/protobuf/flowy-folder/app.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart';
 import 'package:flutter_test/flutter_test.dart';
 
 import '../../util.dart';
 
-void main() {
-  late AppFlowyUnitTest test;
+class TrashTestContext {
   late AppPB app;
   late AppBloc appBloc;
-  late TrashBloc trashBloc;
+  late List<ViewPB> allViews;
+  final AppFlowyUnitTest unitTest;
+
+  TrashTestContext(this.unitTest);
+
+  Future<void> initialize() async {
+    app = await unitTest.createTestApp();
+    appBloc = AppBloc(app: app)..add(const AppEvent.initial());
+
+    appBloc.add(AppEvent.createView(
+      "Document 1",
+      DocumentPluginBuilder(),
+    ));
+    await blocResponseFuture();
+
+    appBloc.add(AppEvent.createView(
+      "Document 2",
+      DocumentPluginBuilder(),
+    ));
+    await blocResponseFuture();
+
+    appBloc.add(
+      AppEvent.createView(
+        "Document 3",
+        DocumentPluginBuilder(),
+      ),
+    );
+    await blocResponseFuture();
+
+    allViews = [...appBloc.state.app.belongings.items];
+    assert(allViews.length == 3);
+  }
+}
+
+void main() {
+  late AppFlowyUnitTest unitTest;
   setUpAll(() async {
-    test = await AppFlowyUnitTest.ensureInitialized();
+    unitTest = await AppFlowyUnitTest.ensureInitialized();
   });
 
   // 1. Create three views
@@ -22,158 +55,46 @@ void main() {
   // 3. Delete all views and check the state
   // 4. Put back a view
   // 5. Put back all views
-  group('$TrashBloc', () {
-    late ViewPB deletedView;
-    late List<ViewPB> allViews;
-    setUpAll(() async {
-      /// Create a new app with three documents
-      app = await test.createTestApp();
-      appBloc = AppBloc(app: app)
-        ..add(const AppEvent.initial())
-        ..add(AppEvent.createView(
-          "Document 1",
-          DocumentPluginBuilder(),
-        ))
-        ..add(AppEvent.createView(
-          "Document 2",
-          DocumentPluginBuilder(),
-        ))
-        ..add(
-          AppEvent.createView(
-            "Document 3",
-            DocumentPluginBuilder(),
-          ),
-        );
+
+  group('trash test: ', () {
+    test('delete a view', () async {
+      final context = TrashTestContext(unitTest);
+      await context.initialize();
+      final trashBloc = TrashBloc()..add(const TrashEvent.initial());
       await blocResponseFuture(millisecond: 200);
-      allViews = [...appBloc.state.app.belongings.items];
-      assert(allViews.length == 3);
-    });
 
-    setUp(() async {
-      trashBloc = TrashBloc()..add(const TrashEvent.initial());
+      // delete a view
+      final deletedView = context.appBloc.state.app.belongings.items[0];
+      context.appBloc.add(AppEvent.deleteView(deletedView.id));
       await blocResponseFuture();
-    });
+      assert(context.appBloc.state.app.belongings.items.length == 2);
+      assert(trashBloc.state.objects.length == 1);
+      assert(trashBloc.state.objects.first.id == deletedView.id);
 
-    blocTest<TrashBloc, TrashState>(
-      "delete a view",
-      build: () => trashBloc,
-      act: (bloc) async {
-        deletedView = appBloc.state.app.belongings.items[0];
-        appBloc.add(AppEvent.deleteView(deletedView.id));
-      },
-      wait: blocResponseDuration(),
-      verify: (bloc) {
-        assert(appBloc.state.app.belongings.items.length == 2);
-        assert(bloc.state.objects.length == 1);
-        assert(bloc.state.objects.first.id == deletedView.id);
-      },
-    );
+      // put back
+      trashBloc.add(TrashEvent.putback(deletedView.id));
+      await blocResponseFuture();
+      assert(context.appBloc.state.app.belongings.items.length == 3);
+      assert(trashBloc.state.objects.isEmpty);
 
-    blocTest<TrashBloc, TrashState>(
-      "delete all views",
-      build: () => trashBloc,
-      act: (bloc) async {
-        for (final view in appBloc.state.app.belongings.items) {
-          appBloc.add(AppEvent.deleteView(view.id));
-          await blocResponseFuture();
-        }
-      },
-      wait: blocResponseDuration(),
-      verify: (bloc) {
-        assert(bloc.state.objects[0].id == allViews[0].id);
-        assert(bloc.state.objects[1].id == allViews[1].id);
-        assert(bloc.state.objects[2].id == allViews[2].id);
-      },
-    );
-    blocTest<TrashBloc, TrashState>(
-      "put back a trash",
-      build: () => trashBloc,
-      act: (bloc) async {
-        bloc.add(TrashEvent.putback(allViews[0].id));
-      },
-      wait: blocResponseDuration(),
-      verify: (bloc) {
-        assert(appBloc.state.app.belongings.items.length == 1);
-        assert(bloc.state.objects.length == 2);
-      },
-    );
-    blocTest<TrashBloc, TrashState>(
-      "put back all trash",
-      build: () => trashBloc,
-      act: (bloc) async {
-        bloc.add(const TrashEvent.restoreAll());
-      },
-      wait: blocResponseDuration(),
-      verify: (bloc) {
-        assert(appBloc.state.app.belongings.items.length == 3);
-        assert(bloc.state.objects.isEmpty);
-      },
-    );
-    //
-  });
+      // delete all views
+      for (final view in context.allViews) {
+        context.appBloc.add(AppEvent.deleteView(view.id));
+        await blocResponseFuture();
+      }
+      assert(trashBloc.state.objects[0].id == context.allViews[0].id);
+      assert(trashBloc.state.objects[1].id == context.allViews[1].id);
+      assert(trashBloc.state.objects[2].id == context.allViews[2].id);
 
-  // 1. Create three views
-  // 2. Delete a trash permanently and check the state
-  // 3. Delete all views permanently
-  group('$TrashBloc', () {
-    setUpAll(() async {
-      /// Create a new app with three documents
-      app = await test.createTestApp();
-      appBloc = AppBloc(app: app)
-        ..add(const AppEvent.initial())
-        ..add(AppEvent.createView(
-          "Document 1",
-          DocumentPluginBuilder(),
-        ))
-        ..add(AppEvent.createView(
-          "Document 2",
-          DocumentPluginBuilder(),
-        ))
-        ..add(
-          AppEvent.createView(
-            "Document 3",
-            DocumentPluginBuilder(),
-          ),
-        );
-      await blocResponseFuture(millisecond: 200);
-    });
+      // delete a view permanently
+      trashBloc.add(TrashEvent.delete(trashBloc.state.objects[0]));
+      await blocResponseFuture();
+      assert(trashBloc.state.objects.length == 2);
 
-    setUp(() async {
-      trashBloc = TrashBloc()..add(const TrashEvent.initial());
+      // delete all view permanently
+      trashBloc.add(const TrashEvent.deleteAll());
       await blocResponseFuture();
+      assert(trashBloc.state.objects.isEmpty);
     });
-
-    blocTest<TrashBloc, TrashState>(
-      "delete a view permanently",
-      build: () => trashBloc,
-      act: (bloc) async {
-        final view = appBloc.state.app.belongings.items[0];
-        appBloc.add(AppEvent.deleteView(view.id));
-        await blocResponseFuture();
-
-        trashBloc.add(TrashEvent.delete(trashBloc.state.objects[0]));
-      },
-      wait: blocResponseDuration(),
-      verify: (bloc) {
-        assert(appBloc.state.app.belongings.items.length == 2);
-        assert(bloc.state.objects.isEmpty);
-      },
-    );
-    blocTest<TrashBloc, TrashState>(
-      "delete all view permanently",
-      build: () => trashBloc,
-      act: (bloc) async {
-        for (final view in appBloc.state.app.belongings.items) {
-          appBloc.add(AppEvent.deleteView(view.id));
-          await blocResponseFuture();
-        }
-        trashBloc.add(const TrashEvent.deleteAll());
-      },
-      wait: blocResponseDuration(),
-      verify: (bloc) {
-        assert(appBloc.state.app.belongings.items.isEmpty);
-        assert(bloc.state.objects.isEmpty);
-      },
-    );
   });
 }

+ 1 - 0
frontend/rust-lib/Cargo.lock

@@ -956,6 +956,7 @@ name = "flowy-grid"
 version = "0.1.0"
 dependencies = [
  "anyhow",
+ "async-stream",
  "atomic_refcell",
  "bytes",
  "chrono",

+ 1 - 0
frontend/rust-lib/flowy-grid/Cargo.toml

@@ -44,6 +44,7 @@ url = { version = "2"}
 futures = "0.3.15"
 atomic_refcell = "0.1.8"
 crossbeam-utils = "0.8.7"
+async-stream = "0.3.2"
 
 [dev-dependencies]
 flowy-test = { path = "../flowy-test" }

+ 6 - 6
frontend/rust-lib/flowy-grid/src/dart_notification.rs

@@ -3,7 +3,7 @@ use flowy_derive::ProtoBuf_Enum;
 const OBSERVABLE_CATEGORY: &str = "Grid";
 
 #[derive(ProtoBuf_Enum, Debug)]
-pub enum GridNotification {
+pub enum GridDartNotification {
     Unknown = 0,
     DidCreateBlock = 11,
     DidUpdateGridBlock = 20,
@@ -18,19 +18,19 @@ pub enum GridNotification {
     DidUpdateGridSetting = 70,
 }
 
-impl std::default::Default for GridNotification {
+impl std::default::Default for GridDartNotification {
     fn default() -> Self {
-        GridNotification::Unknown
+        GridDartNotification::Unknown
     }
 }
 
-impl std::convert::From<GridNotification> for i32 {
-    fn from(notification: GridNotification) -> Self {
+impl std::convert::From<GridDartNotification> for i32 {
+    fn from(notification: GridDartNotification) -> Self {
         notification as i32
     }
 }
 
 #[tracing::instrument(level = "trace")]
-pub fn send_dart_notification(id: &str, ty: GridNotification) -> DartNotifyBuilder {
+pub fn send_dart_notification(id: &str, ty: GridDartNotification) -> DartNotifyBuilder {
     DartNotifyBuilder::new(id, ty, OBSERVABLE_CATEGORY)
 }

+ 2 - 2
frontend/rust-lib/flowy-grid/src/entities/block_entities.rs

@@ -152,7 +152,7 @@ impl std::convert::From<&RowRevision> for InsertedRowPB {
     }
 }
 
-#[derive(Debug, Default, ProtoBuf)]
+#[derive(Debug, Default, Clone, ProtoBuf)]
 pub struct GridBlockChangesetPB {
     #[pb(index = 1)]
     pub block_id: String,
@@ -170,7 +170,7 @@ pub struct GridBlockChangesetPB {
     pub visible_rows: Vec<String>,
 
     #[pb(index = 6)]
-    pub hide_rows: Vec<String>,
+    pub invisible_rows: Vec<String>,
 }
 impl GridBlockChangesetPB {
     pub fn insert(block_id: String, inserted_rows: Vec<InsertedRowPB>) -> Self {

+ 0 - 1
frontend/rust-lib/flowy-grid/src/entities/filter_entities/checkbox_filter.rs

@@ -1,7 +1,6 @@
 use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
 use flowy_error::ErrorCode;
 use grid_rev_model::FilterRevision;
-use std::sync::Arc;
 
 #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)]
 pub struct CheckboxFilterPB {

+ 0 - 1
frontend/rust-lib/flowy-grid/src/entities/filter_entities/date_filter.rs

@@ -3,7 +3,6 @@ use flowy_error::ErrorCode;
 use grid_rev_model::FilterRevision;
 use serde::{Deserialize, Serialize};
 use std::str::FromStr;
-use std::sync::Arc;
 
 #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)]
 pub struct DateFilterPB {

+ 1 - 1
frontend/rust-lib/flowy-grid/src/entities/filter_entities/filter_changeset.rs

@@ -1,4 +1,4 @@
-use crate::entities::{FilterPB, InsertedRowPB, RepeatedFilterPB, RowPB};
+use crate::entities::FilterPB;
 use flowy_derive::ProtoBuf;
 
 #[derive(Debug, Default, ProtoBuf)]

+ 0 - 2
frontend/rust-lib/flowy-grid/src/entities/filter_entities/number_filter.rs

@@ -2,8 +2,6 @@ use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
 use flowy_error::ErrorCode;
 use grid_rev_model::FilterRevision;
 
-use std::sync::Arc;
-
 #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)]
 pub struct NumberFilterPB {
     #[pb(index = 1)]

+ 0 - 1
frontend/rust-lib/flowy-grid/src/entities/filter_entities/select_option_filter.rs

@@ -2,7 +2,6 @@ use crate::services::field::SelectOptionIds;
 use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
 use flowy_error::ErrorCode;
 use grid_rev_model::FilterRevision;
-use std::sync::Arc;
 
 #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)]
 pub struct SelectOptionFilterPB {

+ 0 - 1
frontend/rust-lib/flowy-grid/src/entities/filter_entities/text_filter.rs

@@ -1,7 +1,6 @@
 use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
 use flowy_error::ErrorCode;
 use grid_rev_model::FilterRevision;
-use std::sync::Arc;
 
 #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)]
 pub struct TextFilterPB {

+ 2 - 0
frontend/rust-lib/flowy-grid/src/entities/filter_entities/util.rs

@@ -96,6 +96,7 @@ impl TryInto<DeleteFilterParams> for DeleteFilterPayloadPB {
     }
 }
 
+#[derive(Debug)]
 pub struct DeleteFilterParams {
     pub filter_type: FilterType,
     pub filter_id: String,
@@ -177,6 +178,7 @@ impl TryInto<CreateFilterParams> for CreateFilterPayloadPB {
     }
 }
 
+#[derive(Debug)]
 pub struct CreateFilterParams {
     pub field_id: String,
     pub field_type_rev: FieldTypeRevision,

+ 3 - 3
frontend/rust-lib/flowy-grid/src/event_handler.rs

@@ -1,6 +1,6 @@
 use crate::entities::*;
 use crate::manager::GridManager;
-use crate::services::cell::AnyCellData;
+use crate::services::cell::TypeCellData;
 use crate::services::field::{
     default_type_option_builder_from_type, select_type_option_from_field_rev, type_option_builder_from_json_str,
     DateCellChangeset, DateChangesetPB, SelectOptionCellChangeset, SelectOptionCellChangesetPB,
@@ -414,8 +414,8 @@ pub(crate) async fn get_select_option_handler(
             //
             let cell_rev = editor.get_cell_rev(&params.row_id, &params.field_id).await?;
             let type_option = select_type_option_from_field_rev(&field_rev)?;
-            let any_cell_data: AnyCellData = match cell_rev {
-                None => AnyCellData {
+            let any_cell_data: TypeCellData = match cell_rev {
+                None => TypeCellData {
                     data: "".to_string(),
                     field_type: field_rev.ty.into(),
                 },

+ 2 - 2
frontend/rust-lib/flowy-grid/src/event_map.rs

@@ -161,8 +161,8 @@ pub enum GridEvent {
     /// [UpdateSelectOption] event is used to update a FieldTypeOptionData whose field_type is
     /// FieldType::SingleSelect or FieldType::MultiSelect.
     ///
-    /// This event may trigger the GridNotification::DidUpdateCell event.
-    /// For example, GridNotification::DidUpdateCell will be triggered if the [SelectOptionChangesetPB]
+    /// This event may trigger the GridDartNotification::DidUpdateCell event.
+    /// For example, GridDartNotification::DidUpdateCell will be triggered if the [SelectOptionChangesetPB]
     /// carries a change that updates the name of the option.
     #[event(input = "SelectOptionChangesetPB")]
     UpdateSelectOption = 32,

+ 3 - 6
frontend/rust-lib/flowy-grid/src/manager.rs

@@ -1,12 +1,12 @@
 use crate::entities::GridLayout;
 
 use crate::services::grid_editor::{GridRevisionCompress, GridRevisionEditor};
-use crate::services::grid_view_manager::make_grid_view_rev_manager;
 use crate::services::persistence::block_index::BlockIndexCache;
 use crate::services::persistence::kv::GridKVPersistence;
 use crate::services::persistence::migration::GridMigration;
 use crate::services::persistence::rev_sqlite::SQLiteGridRevisionPersistence;
 use crate::services::persistence::GridDatabase;
+use crate::services::view_editor::make_grid_view_rev_manager;
 use bytes::Bytes;
 
 use flowy_database::ConnectionPool;
@@ -126,13 +126,10 @@ impl GridManager {
             return Ok(editor);
         }
 
+        let mut grid_editors = self.grid_editors.write().await;
         let db_pool = self.grid_user.db_pool()?;
         let editor = self.make_grid_rev_editor(grid_id, db_pool).await?;
-        self.grid_editors
-            .write()
-            .await
-            .insert(grid_id.to_string(), editor.clone());
-        // self.task_scheduler.write().await.register_handler(editor.clone());
+        grid_editors.insert(grid_id.to_string(), editor.clone());
         Ok(editor)
     }
 

+ 3 - 3
frontend/rust-lib/flowy-grid/src/services/block_manager.rs

@@ -1,4 +1,4 @@
-use crate::dart_notification::{send_dart_notification, GridNotification};
+use crate::dart_notification::{send_dart_notification, GridDartNotification};
 use crate::entities::{CellChangesetPB, GridBlockChangesetPB, InsertedRowPB, RowPB};
 use crate::manager::GridUser;
 use crate::services::block_editor::{GridBlockRevisionCompress, GridBlockRevisionEditor};
@@ -237,7 +237,7 @@ impl GridBlockManager {
     }
 
     async fn notify_did_update_block(&self, block_id: &str, changeset: GridBlockChangesetPB) -> FlowyResult<()> {
-        send_dart_notification(block_id, GridNotification::DidUpdateGridBlock)
+        send_dart_notification(block_id, GridDartNotification::DidUpdateGridBlock)
             .payload(changeset)
             .send();
         Ok(())
@@ -245,7 +245,7 @@ impl GridBlockManager {
 
     async fn notify_did_update_cell(&self, changeset: CellChangesetPB) -> FlowyResult<()> {
         let id = format!("{}:{}", changeset.row_id, changeset.field_id);
-        send_dart_notification(&id, GridNotification::DidUpdateCell).send();
+        send_dart_notification(&id, GridDartNotification::DidUpdateCell).send();
         Ok(())
     }
 }

+ 15 - 15
frontend/rust-lib/flowy-grid/src/services/cell/any_cell_data.rs

@@ -6,17 +6,17 @@ use grid_rev_model::CellRevision;
 use serde::{Deserialize, Serialize};
 use std::str::FromStr;
 
-/// AnyCellData is a generic CellData, you can parse the cell_data according to the field_type.
+/// TypeCellData is a generic CellData, you can parse the cell_data according to the field_type.
 /// When the type of field is changed, it's different from the field_type of AnyCellData.
 /// So it will return an empty data. You could check the CellDataOperation trait for more information.
 #[derive(Debug, Serialize, Deserialize)]
-pub struct AnyCellData {
+pub struct TypeCellData {
     pub data: String,
     pub field_type: FieldType,
 }
 
-impl AnyCellData {
-    pub fn from_field_type(field_type: &FieldType) -> AnyCellData {
+impl TypeCellData {
+    pub fn from_field_type(field_type: &FieldType) -> TypeCellData {
         Self {
             data: "".to_string(),
             field_type: field_type.clone(),
@@ -24,11 +24,11 @@ impl AnyCellData {
     }
 }
 
-impl std::str::FromStr for AnyCellData {
+impl std::str::FromStr for TypeCellData {
     type Err = FlowyError;
 
     fn from_str(s: &str) -> Result<Self, Self::Err> {
-        let type_option_cell_data: AnyCellData = serde_json::from_str(s).map_err(|err| {
+        let type_option_cell_data: TypeCellData = serde_json::from_str(s).map_err(|err| {
             let msg = format!("Deserialize {} to any cell data failed. Serde error: {}", s, err);
             FlowyError::internal().context(msg)
         })?;
@@ -36,15 +36,15 @@ impl std::str::FromStr for AnyCellData {
     }
 }
 
-impl std::convert::TryInto<AnyCellData> for String {
+impl std::convert::TryInto<TypeCellData> for String {
     type Error = FlowyError;
 
-    fn try_into(self) -> Result<AnyCellData, Self::Error> {
-        AnyCellData::from_str(&self)
+    fn try_into(self) -> Result<TypeCellData, Self::Error> {
+        TypeCellData::from_str(&self)
     }
 }
 
-impl std::convert::TryFrom<&CellRevision> for AnyCellData {
+impl std::convert::TryFrom<&CellRevision> for TypeCellData {
     type Error = FlowyError;
 
     fn try_from(value: &CellRevision) -> Result<Self, Self::Error> {
@@ -52,7 +52,7 @@ impl std::convert::TryFrom<&CellRevision> for AnyCellData {
     }
 }
 
-impl std::convert::TryFrom<CellRevision> for AnyCellData {
+impl std::convert::TryFrom<CellRevision> for TypeCellData {
     type Error = FlowyError;
 
     fn try_from(value: CellRevision) -> Result<Self, Self::Error> {
@@ -60,18 +60,18 @@ impl std::convert::TryFrom<CellRevision> for AnyCellData {
     }
 }
 
-impl<T> std::convert::From<AnyCellData> for CellData<T>
+impl<T> std::convert::From<TypeCellData> for CellData<T>
 where
     T: FromCellString,
 {
-    fn from(any_call_data: AnyCellData) -> Self {
+    fn from(any_call_data: TypeCellData) -> Self {
         CellData::from(any_call_data.data)
     }
 }
 
-impl AnyCellData {
+impl TypeCellData {
     pub fn new(content: String, field_type: FieldType) -> Self {
-        AnyCellData {
+        TypeCellData {
             data: content,
             field_type,
         }

+ 6 - 6
frontend/rust-lib/flowy-grid/src/services/cell/cell_operation.rs

@@ -1,5 +1,5 @@
 use crate::entities::FieldType;
-use crate::services::cell::{AnyCellData, CellBytes};
+use crate::services::cell::{CellBytes, TypeCellData};
 use crate::services::field::*;
 use std::fmt::Debug;
 
@@ -9,11 +9,11 @@ use grid_rev_model::{CellRevision, FieldRevision, FieldTypeRevision};
 /// This trait is used when doing filter/search on the grid.
 pub trait CellFilterOperation<T> {
     /// Return true if any_cell_data match the filter condition.
-    fn apply_filter(&self, any_cell_data: AnyCellData, filter: &T) -> FlowyResult<bool>;
+    fn apply_filter(&self, any_cell_data: TypeCellData, filter: &T) -> FlowyResult<bool>;
 }
 
 pub trait CellGroupOperation {
-    fn apply_group(&self, any_cell_data: AnyCellData, group_content: &str) -> FlowyResult<bool>;
+    fn apply_group(&self, any_cell_data: TypeCellData, group_content: &str) -> FlowyResult<bool>;
 }
 
 /// Return object that describes the cell.
@@ -126,17 +126,17 @@ pub fn apply_cell_data_changeset<C: ToString, T: AsRef<FieldRevision>>(
         FieldType::URL => URLTypeOptionPB::from(field_rev).apply_changeset(changeset.into(), cell_rev),
     }?;
 
-    Ok(AnyCellData::new(s, field_type).to_json())
+    Ok(TypeCellData::new(s, field_type).to_json())
 }
 
-pub fn decode_any_cell_data<T: TryInto<AnyCellData, Error = FlowyError> + Debug>(
+pub fn decode_any_cell_data<T: TryInto<TypeCellData, Error = FlowyError> + Debug>(
     data: T,
     field_rev: &FieldRevision,
 ) -> (FieldType, CellBytes) {
     let to_field_type = field_rev.ty.into();
     match data.try_into() {
         Ok(any_cell_data) => {
-            let AnyCellData { data, field_type } = any_cell_data;
+            let TypeCellData { data, field_type } = any_cell_data;
             match try_decode_cell_data(data.into(), &field_type, &to_field_type, field_rev) {
                 Ok(cell_bytes) => (field_type, cell_bytes),
                 Err(e) => {

+ 2 - 2
frontend/rust-lib/flowy-grid/src/services/field/type_options/checkbox_type_option/checkbox_filter.rs

@@ -1,5 +1,5 @@
 use crate::entities::{CheckboxFilterCondition, CheckboxFilterPB};
-use crate::services::cell::{AnyCellData, CellData, CellFilterOperation};
+use crate::services::cell::{CellData, CellFilterOperation, TypeCellData};
 use crate::services::field::{CheckboxCellData, CheckboxTypeOptionPB};
 use flowy_error::FlowyResult;
 
@@ -14,7 +14,7 @@ impl CheckboxFilterPB {
 }
 
 impl CellFilterOperation<CheckboxFilterPB> for CheckboxTypeOptionPB {
-    fn apply_filter(&self, any_cell_data: AnyCellData, filter: &CheckboxFilterPB) -> FlowyResult<bool> {
+    fn apply_filter(&self, any_cell_data: TypeCellData, filter: &CheckboxFilterPB) -> FlowyResult<bool> {
         if !any_cell_data.is_checkbox() {
             return Ok(true);
         }

+ 2 - 2
frontend/rust-lib/flowy-grid/src/services/field/type_options/date_type_option/date_filter.rs

@@ -1,5 +1,5 @@
 use crate::entities::{DateFilterCondition, DateFilterPB};
-use crate::services::cell::{AnyCellData, CellData, CellFilterOperation};
+use crate::services::cell::{CellData, CellFilterOperation, TypeCellData};
 use crate::services::field::{DateTimestamp, DateTypeOptionPB};
 use chrono::NaiveDateTime;
 use flowy_error::FlowyResult;
@@ -60,7 +60,7 @@ impl DateFilterPB {
 }
 
 impl CellFilterOperation<DateFilterPB> for DateTypeOptionPB {
-    fn apply_filter(&self, any_cell_data: AnyCellData, filter: &DateFilterPB) -> FlowyResult<bool> {
+    fn apply_filter(&self, any_cell_data: TypeCellData, filter: &DateFilterPB) -> FlowyResult<bool> {
         if !any_cell_data.is_date() {
             return Ok(true);
         }

+ 2 - 2
frontend/rust-lib/flowy-grid/src/services/field/type_options/number_type_option/number_filter.rs

@@ -1,5 +1,5 @@
 use crate::entities::{NumberFilterCondition, NumberFilterPB};
-use crate::services::cell::{AnyCellData, CellFilterOperation};
+use crate::services::cell::{CellFilterOperation, TypeCellData};
 use crate::services::field::{NumberCellData, NumberTypeOptionPB};
 use flowy_error::FlowyResult;
 use rust_decimal::prelude::Zero;
@@ -38,7 +38,7 @@ impl NumberFilterPB {
 }
 
 impl CellFilterOperation<NumberFilterPB> for NumberTypeOptionPB {
-    fn apply_filter(&self, any_cell_data: AnyCellData, filter: &NumberFilterPB) -> FlowyResult<bool> {
+    fn apply_filter(&self, any_cell_data: TypeCellData, filter: &NumberFilterPB) -> FlowyResult<bool> {
         if !any_cell_data.is_number() {
             return Ok(true);
         }

+ 3 - 3
frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/select_filter.rs

@@ -1,7 +1,7 @@
 #![allow(clippy::needless_collect)]
 
 use crate::entities::{SelectOptionCondition, SelectOptionFilterPB};
-use crate::services::cell::{AnyCellData, CellFilterOperation};
+use crate::services::cell::{CellFilterOperation, TypeCellData};
 use crate::services::field::{MultiSelectTypeOptionPB, SingleSelectTypeOptionPB};
 use crate::services::field::{SelectTypeOptionSharedAction, SelectedSelectOptions};
 use flowy_error::FlowyResult;
@@ -41,7 +41,7 @@ impl SelectOptionFilterPB {
 }
 
 impl CellFilterOperation<SelectOptionFilterPB> for MultiSelectTypeOptionPB {
-    fn apply_filter(&self, any_cell_data: AnyCellData, filter: &SelectOptionFilterPB) -> FlowyResult<bool> {
+    fn apply_filter(&self, any_cell_data: TypeCellData, filter: &SelectOptionFilterPB) -> FlowyResult<bool> {
         if !any_cell_data.is_multi_select() {
             return Ok(true);
         }
@@ -52,7 +52,7 @@ impl CellFilterOperation<SelectOptionFilterPB> for MultiSelectTypeOptionPB {
 }
 
 impl CellFilterOperation<SelectOptionFilterPB> for SingleSelectTypeOptionPB {
-    fn apply_filter(&self, any_cell_data: AnyCellData, filter: &SelectOptionFilterPB) -> FlowyResult<bool> {
+    fn apply_filter(&self, any_cell_data: TypeCellData, filter: &SelectOptionFilterPB) -> FlowyResult<bool> {
         if !any_cell_data.is_single_select() {
             return Ok(true);
         }

+ 3 - 3
frontend/rust-lib/flowy-grid/src/services/field/type_options/text_type_option/text_filter.rs

@@ -1,5 +1,5 @@
 use crate::entities::{TextFilterCondition, TextFilterPB};
-use crate::services::cell::{AnyCellData, CellData, CellFilterOperation};
+use crate::services::cell::{CellData, CellFilterOperation, TypeCellData};
 use crate::services::field::{RichTextTypeOptionPB, TextCellData};
 use flowy_error::FlowyResult;
 
@@ -21,9 +21,9 @@ impl TextFilterPB {
 }
 
 impl CellFilterOperation<TextFilterPB> for RichTextTypeOptionPB {
-    fn apply_filter(&self, any_cell_data: AnyCellData, filter: &TextFilterPB) -> FlowyResult<bool> {
+    fn apply_filter(&self, any_cell_data: TypeCellData, filter: &TextFilterPB) -> FlowyResult<bool> {
         if !any_cell_data.is_text() {
-            return Ok(true);
+            return Ok(false);
         }
 
         let cell_data: CellData<TextCellData> = any_cell_data.into();

+ 2 - 2
frontend/rust-lib/flowy-grid/src/services/field/type_options/url_type_option/url_filter.rs

@@ -1,10 +1,10 @@
 use crate::entities::TextFilterPB;
-use crate::services::cell::{AnyCellData, CellData, CellFilterOperation};
+use crate::services::cell::{CellData, CellFilterOperation, TypeCellData};
 use crate::services::field::{TextCellData, URLTypeOptionPB};
 use flowy_error::FlowyResult;
 
 impl CellFilterOperation<TextFilterPB> for URLTypeOptionPB {
-    fn apply_filter(&self, any_cell_data: AnyCellData, filter: &TextFilterPB) -> FlowyResult<bool> {
+    fn apply_filter(&self, any_cell_data: TypeCellData, filter: &TextFilterPB) -> FlowyResult<bool> {
         if !any_cell_data.is_url() {
             return Ok(true);
         }

+ 2 - 2
frontend/rust-lib/flowy-grid/src/services/field/type_options/util/cell_data_util.rs

@@ -1,9 +1,9 @@
-use crate::services::cell::AnyCellData;
+use crate::services::cell::TypeCellData;
 use grid_rev_model::CellRevision;
 use std::str::FromStr;
 
 pub fn get_cell_data(cell_rev: &CellRevision) -> String {
-    match AnyCellData::from_str(&cell_rev.data) {
+    match TypeCellData::from_str(&cell_rev.data) {
         Ok(type_option) => type_option.data,
         Err(_) => String::new(),
     }

+ 13 - 2
frontend/rust-lib/flowy-grid/src/services/filter/cache.rs

@@ -1,9 +1,8 @@
 use crate::entities::{CheckboxFilterPB, DateFilterPB, FieldType, NumberFilterPB, SelectOptionFilterPB, TextFilterPB};
 use crate::services::filter::FilterType;
-
 use std::collections::HashMap;
 
-#[derive(Default)]
+#[derive(Default, Debug)]
 pub(crate) struct FilterMap {
     pub(crate) text_filter: HashMap<FilterType, TextFilterPB>,
     pub(crate) url_filter: HashMap<FilterType, TextFilterPB>,
@@ -18,6 +17,18 @@ impl FilterMap {
         Self::default()
     }
 
+    pub(crate) fn has_filter(&self, filter_type: &FilterType) -> bool {
+        match filter_type.field_type {
+            FieldType::RichText => self.text_filter.get(filter_type).is_some(),
+            FieldType::Number => self.number_filter.get(filter_type).is_some(),
+            FieldType::DateTime => self.date_filter.get(filter_type).is_some(),
+            FieldType::SingleSelect => self.select_option_filter.get(filter_type).is_some(),
+            FieldType::MultiSelect => self.select_option_filter.get(filter_type).is_some(),
+            FieldType::Checkbox => self.checkbox_filter.get(filter_type).is_some(),
+            FieldType::URL => self.url_filter.get(filter_type).is_some(),
+        }
+    }
+
     pub(crate) fn is_empty(&self) -> bool {
         if !self.text_filter.is_empty() {
             return false;

+ 74 - 158
frontend/rust-lib/flowy-grid/src/services/filter/controller.rs

@@ -1,21 +1,21 @@
-use crate::dart_notification::{send_dart_notification, GridNotification};
 use crate::entities::filter_entities::*;
-use crate::entities::setting_entities::*;
-use crate::entities::{FieldType, GridBlockChangesetPB};
-use crate::services::cell::{AnyCellData, CellFilterOperation};
+
+use crate::entities::FieldType;
+use crate::services::cell::{CellFilterOperation, TypeCellData};
 use crate::services::field::*;
-use crate::services::filter::{FilterMap, FilterResult, FILTER_HANDLER_ID};
+use crate::services::filter::{FilterChangeset, FilterMap, FilterResult, FilterResultNotification, FilterType};
 use crate::services::row::GridBlock;
+use crate::services::view_editor::{GridViewChanged, GridViewChangedNotifier};
 use flowy_error::FlowyResult;
 use flowy_task::{QualityOfService, Task, TaskContent, TaskDispatcher};
-use grid_rev_model::{CellRevision, FieldId, FieldRevision, FieldTypeRevision, FilterRevision, RowRevision};
+use grid_rev_model::{CellRevision, FieldId, FieldRevision, FilterRevision, RowRevision};
 use lib_infra::future::Fut;
 use std::collections::HashMap;
 use std::sync::Arc;
 use tokio::sync::RwLock;
 
 type RowId = String;
-pub trait GridViewFilterDelegate: Send + Sync + 'static {
+pub trait FilterDelegate: Send + Sync + 'static {
     fn get_filter_rev(&self, filter_id: FilterType) -> Fut<Vec<Arc<FilterRevision>>>;
     fn get_field_rev(&self, field_id: &str) -> Fut<Option<Arc<FieldRevision>>>;
     fn get_field_revs(&self, field_ids: Option<Vec<String>>) -> Fut<Vec<Arc<FieldRevision>>>;
@@ -24,41 +24,48 @@ pub trait GridViewFilterDelegate: Send + Sync + 'static {
 
 pub struct FilterController {
     view_id: String,
-    delegate: Box<dyn GridViewFilterDelegate>,
+    handler_id: String,
+    delegate: Box<dyn FilterDelegate>,
     filter_map: FilterMap,
     result_by_row_id: HashMap<RowId, FilterResult>,
     task_scheduler: Arc<RwLock<TaskDispatcher>>,
+    notifier: GridViewChangedNotifier,
 }
+
 impl FilterController {
     pub async fn new<T>(
         view_id: &str,
+        handler_id: &str,
         delegate: T,
         task_scheduler: Arc<RwLock<TaskDispatcher>>,
         filter_revs: Vec<Arc<FilterRevision>>,
+        notifier: GridViewChangedNotifier,
     ) -> Self
     where
-        T: GridViewFilterDelegate,
+        T: FilterDelegate,
     {
         let mut this = Self {
             view_id: view_id.to_string(),
+            handler_id: handler_id.to_string(),
             delegate: Box::new(delegate),
             filter_map: FilterMap::new(),
             result_by_row_id: HashMap::default(),
             task_scheduler,
+            notifier,
         };
         this.load_filters(filter_revs).await;
         this
     }
 
     pub async fn close(&self) {
-        self.task_scheduler.write().await.unregister_handler(FILTER_HANDLER_ID);
+        self.task_scheduler.write().await.unregister_handler(&self.handler_id);
     }
 
     #[tracing::instrument(name = "schedule_filter_task", level = "trace", skip(self))]
     async fn gen_task(&mut self, predicate: &str) {
         let task_id = self.task_scheduler.read().await.next_task_id();
         let task = Task::new(
-            FILTER_HANDLER_ID,
+            &self.handler_id,
             task_id,
             TaskContent::Text(predicate.to_owned()),
             QualityOfService::UserInteractive,
@@ -71,17 +78,14 @@ impl FilterController {
             return;
         }
         let field_rev_by_field_id = self.get_filter_revs_map().await;
-        let _ = row_revs
-            .iter()
-            .flat_map(|row_rev| {
-                filter_row(
-                    row_rev,
-                    &self.filter_map,
-                    &mut self.result_by_row_id,
-                    &field_rev_by_field_id,
-                )
-            })
-            .collect::<Vec<String>>();
+        row_revs.iter().for_each(|row_rev| {
+            let _ = filter_row(
+                row_rev,
+                &self.filter_map,
+                &mut self.result_by_row_id,
+                &field_rev_by_field_id,
+            );
+        });
 
         row_revs.retain(|row_rev| {
             self.result_by_row_id
@@ -100,53 +104,40 @@ impl FilterController {
             .collect::<HashMap<String, Arc<FieldRevision>>>()
     }
 
+    #[tracing::instrument(name = "receive_task_result", level = "trace", skip_all, fields(filter_result), err)]
     pub async fn process(&mut self, _predicate: &str) -> FlowyResult<()> {
         let field_rev_by_field_id = self.get_filter_revs_map().await;
-        let mut changesets = vec![];
         for block in self.delegate.get_blocks().await.into_iter() {
             // The row_ids contains the row that its visibility was changed.
-            let row_ids = block
-                .row_revs
-                .iter()
-                .flat_map(|row_rev| {
-                    filter_row(
-                        row_rev,
-                        &self.filter_map,
-                        &mut self.result_by_row_id,
-                        &field_rev_by_field_id,
-                    )
-                })
-                .collect::<Vec<String>>();
-
             let mut visible_rows = vec![];
-            let mut hide_rows = vec![];
+            let mut invisible_rows = vec![];
 
-            // Query the filter result from the cache
-            for row_id in row_ids {
-                if self
-                    .result_by_row_id
-                    .get(&row_id)
-                    .map(|result| result.is_visible())
-                    .unwrap_or(false)
-                {
-                    visible_rows.push(row_id);
+            for row_rev in &block.row_revs {
+                let (row_id, is_visible) = filter_row(
+                    row_rev,
+                    &self.filter_map,
+                    &mut self.result_by_row_id,
+                    &field_rev_by_field_id,
+                );
+                if is_visible {
+                    visible_rows.push(row_id)
                 } else {
-                    hide_rows.push(row_id);
+                    invisible_rows.push(row_id);
                 }
             }
 
-            let changeset = GridBlockChangesetPB {
+            let notification = FilterResultNotification {
+                view_id: self.view_id.clone(),
                 block_id: block.block_id,
-                hide_rows,
+                invisible_rows,
                 visible_rows,
-                ..Default::default()
             };
-
-            // Save the changeset for each block
-            changesets.push(changeset);
+            tracing::Span::current().record("filter_result", &format!("{:?}", &notification).as_str());
+            let _ = self
+                .notifier
+                .send(GridViewChanged::DidReceiveFilterResult(notification));
         }
 
-        self.notify(changesets).await;
         Ok(())
     }
 
@@ -163,20 +154,13 @@ impl FilterController {
         self.gen_task("").await;
     }
 
-    async fn notify(&self, changesets: Vec<GridBlockChangesetPB>) {
-        for changeset in changesets {
-            send_dart_notification(&self.view_id, GridNotification::DidUpdateGridBlock)
-                .payload(changeset)
-                .send();
-        }
-    }
-
+    #[tracing::instrument(level = "trace", skip_all)]
     async fn load_filters(&mut self, filter_revs: Vec<Arc<FilterRevision>>) {
         for filter_rev in filter_revs {
             if let Some(field_rev) = self.delegate.get_field_rev(&filter_rev.field_id).await {
                 let filter_type = FilterType::from(&field_rev);
-                let field_type: FieldType = field_rev.ty.into();
-                match &field_type {
+                tracing::trace!("Create filter with type: {:?}", filter_type);
+                match &filter_type.field_type {
                     FieldType::RichText => {
                         let _ = self
                             .filter_map
@@ -220,12 +204,13 @@ impl FilterController {
 }
 
 /// Returns None if there is no change in this row after applying the filter
+#[tracing::instrument(level = "trace", skip_all)]
 fn filter_row(
     row_rev: &Arc<RowRevision>,
     filter_map: &FilterMap,
     result_by_row_id: &mut HashMap<RowId, FilterResult>,
     field_rev_by_field_id: &HashMap<FieldId, Arc<FieldRevision>>,
-) -> Option<String> {
+) -> (String, bool) {
     // Create a filter result cache if it's not exist
     let filter_result = result_by_row_id
         .entry(row_rev.id.clone())
@@ -234,31 +219,31 @@ fn filter_row(
     // Iterate each cell of the row to check its visibility
     for (field_id, field_rev) in field_rev_by_field_id {
         let filter_type = FilterType::from(field_rev);
+        if !filter_map.has_filter(&filter_type) {
+            // tracing::trace!(
+            //     "Can't find filter for filter type: {:?}. Current filters: {:?}",
+            //     filter_type,
+            //     filter_map
+            // );
+            continue;
+        }
+
         let cell_rev = row_rev.cells.get(field_id);
         // if the visibility of the cell_rew is changed, which means the visibility of the
         // row is changed too.
         if let Some(is_visible) = filter_cell(&filter_type, field_rev, filter_map, cell_rev) {
-            let prev_is_visible = filter_result.visible_by_filter_id.get(&filter_type).cloned();
             filter_result.visible_by_filter_id.insert(filter_type, is_visible);
-            match prev_is_visible {
-                None => {
-                    if !is_visible {
-                        return Some(row_rev.id.clone());
-                    }
-                }
-                Some(prev_is_visible) => {
-                    if prev_is_visible != is_visible {
-                        return Some(row_rev.id.clone());
-                    }
-                }
-            }
+            return (row_rev.id.clone(), is_visible);
         }
     }
 
-    None
+    (row_rev.id.clone(), true)
 }
 
-// Return None if there is no change in this cell after applying the filter
+// Returns None if there is no change in this cell after applying the filter
+// Returns Some if the visibility of the cell is changed
+
+#[tracing::instrument(level = "trace", skip_all)]
 fn filter_cell(
     filter_id: &FilterType,
     field_rev: &Arc<FieldRevision>,
@@ -266,9 +251,16 @@ fn filter_cell(
     cell_rev: Option<&CellRevision>,
 ) -> Option<bool> {
     let any_cell_data = match cell_rev {
-        None => AnyCellData::from_field_type(&filter_id.field_type),
-        Some(cell_rev) => AnyCellData::try_from(cell_rev).ok()?,
+        None => TypeCellData::from_field_type(&filter_id.field_type),
+        Some(cell_rev) => match TypeCellData::try_from(cell_rev) {
+            Ok(cell_data) => cell_data,
+            Err(err) => {
+                tracing::error!("Deserialize TypeCellData failed: {}", err);
+                TypeCellData::from_field_type(&filter_id.field_type)
+            }
+        },
     };
+    tracing::trace!("filter cell: {:?}", any_cell_data);
 
     let is_visible = match &filter_id.field_type {
         FieldType::RichText => filter_map.text_filter.get(filter_id).and_then(|filter| {
@@ -331,79 +323,3 @@ fn filter_cell(
 
     is_visible
 }
-
-pub struct FilterChangeset {
-    insert_filter: Option<FilterType>,
-    delete_filter: Option<FilterType>,
-}
-
-impl FilterChangeset {
-    pub fn from_insert(filter_id: FilterType) -> Self {
-        Self {
-            insert_filter: Some(filter_id),
-            delete_filter: None,
-        }
-    }
-
-    pub fn from_delete(filter_id: FilterType) -> Self {
-        Self {
-            insert_filter: None,
-            delete_filter: Some(filter_id),
-        }
-    }
-}
-
-impl std::convert::From<&GridSettingChangesetParams> for FilterChangeset {
-    fn from(params: &GridSettingChangesetParams) -> Self {
-        let insert_filter = params.insert_filter.as_ref().map(|insert_filter_params| FilterType {
-            field_id: insert_filter_params.field_id.clone(),
-            field_type: insert_filter_params.field_type_rev.into(),
-        });
-
-        let delete_filter = params
-            .delete_filter
-            .as_ref()
-            .map(|delete_filter_params| delete_filter_params.filter_type.clone());
-        FilterChangeset {
-            insert_filter,
-            delete_filter,
-        }
-    }
-}
-
-#[derive(Hash, Eq, PartialEq, Clone)]
-pub struct FilterType {
-    pub field_id: String,
-    pub field_type: FieldType,
-}
-
-impl FilterType {
-    pub fn field_type_rev(&self) -> FieldTypeRevision {
-        self.field_type.clone().into()
-    }
-}
-
-impl std::convert::From<&Arc<FieldRevision>> for FilterType {
-    fn from(rev: &Arc<FieldRevision>) -> Self {
-        Self {
-            field_id: rev.id.clone(),
-            field_type: rev.ty.into(),
-        }
-    }
-}
-
-impl std::convert::From<&CreateFilterParams> for FilterType {
-    fn from(params: &CreateFilterParams) -> Self {
-        let field_type: FieldType = params.field_type_rev.into();
-        Self {
-            field_id: params.field_id.clone(),
-            field_type,
-        }
-    }
-}
-
-impl std::convert::From<&DeleteFilterParams> for FilterType {
-    fn from(params: &DeleteFilterParams) -> Self {
-        params.filter_type.clone()
-    }
-}

+ 87 - 0
frontend/rust-lib/flowy-grid/src/services/filter/entities.rs

@@ -0,0 +1,87 @@
+use crate::entities::{CreateFilterParams, DeleteFilterParams, FieldType, GridSettingChangesetParams};
+use grid_rev_model::{FieldRevision, FieldTypeRevision};
+use std::sync::Arc;
+
+pub struct FilterChangeset {
+    pub(crate) insert_filter: Option<FilterType>,
+    pub(crate) delete_filter: Option<FilterType>,
+}
+
+impl FilterChangeset {
+    pub fn from_insert(filter_id: FilterType) -> Self {
+        Self {
+            insert_filter: Some(filter_id),
+            delete_filter: None,
+        }
+    }
+
+    pub fn from_delete(filter_id: FilterType) -> Self {
+        Self {
+            insert_filter: None,
+            delete_filter: Some(filter_id),
+        }
+    }
+}
+
+impl std::convert::From<&GridSettingChangesetParams> for FilterChangeset {
+    fn from(params: &GridSettingChangesetParams) -> Self {
+        let insert_filter = params.insert_filter.as_ref().map(|insert_filter_params| FilterType {
+            field_id: insert_filter_params.field_id.clone(),
+            field_type: insert_filter_params.field_type_rev.into(),
+        });
+
+        let delete_filter = params
+            .delete_filter
+            .as_ref()
+            .map(|delete_filter_params| delete_filter_params.filter_type.clone());
+        FilterChangeset {
+            insert_filter,
+            delete_filter,
+        }
+    }
+}
+
+#[derive(Hash, Eq, PartialEq, Debug, Clone)]
+pub struct FilterType {
+    pub field_id: String,
+    pub field_type: FieldType,
+}
+
+impl FilterType {
+    pub fn field_type_rev(&self) -> FieldTypeRevision {
+        self.field_type.clone().into()
+    }
+}
+
+impl std::convert::From<&Arc<FieldRevision>> for FilterType {
+    fn from(rev: &Arc<FieldRevision>) -> Self {
+        Self {
+            field_id: rev.id.clone(),
+            field_type: rev.ty.into(),
+        }
+    }
+}
+
+impl std::convert::From<&CreateFilterParams> for FilterType {
+    fn from(params: &CreateFilterParams) -> Self {
+        let field_type: FieldType = params.field_type_rev.into();
+        Self {
+            field_id: params.field_id.clone(),
+            field_type,
+        }
+    }
+}
+
+impl std::convert::From<&DeleteFilterParams> for FilterType {
+    fn from(params: &DeleteFilterParams) -> Self {
+        params.filter_type.clone()
+    }
+}
+
+#[derive(Clone, Debug)]
+pub struct FilterResultNotification {
+    pub view_id: String,
+    pub block_id: String,
+    pub visible_rows: Vec<String>,
+    pub invisible_rows: Vec<String>,
+}

+ 2 - 0
frontend/rust-lib/flowy-grid/src/services/filter/mod.rs

@@ -1,7 +1,9 @@
 mod cache;
 mod controller;
+mod entities;
 mod task;
 
 pub(crate) use cache::*;
 pub use controller::*;
+pub use entities::*;
 pub(crate) use task::*;

+ 11 - 6
frontend/rust-lib/flowy-grid/src/services/filter/task.rs

@@ -4,22 +4,27 @@ use lib_infra::future::BoxResultFuture;
 use std::sync::Arc;
 use tokio::sync::RwLock;
 
-pub const FILTER_HANDLER_ID: &str = "grid_filter";
+pub struct FilterTaskHandler {
+    handler_id: String,
+    filter_controller: Arc<RwLock<FilterController>>,
+}
 
-pub struct FilterTaskHandler(Arc<RwLock<FilterController>>);
 impl FilterTaskHandler {
-    pub fn new(filter_controller: Arc<RwLock<FilterController>>) -> Self {
-        Self(filter_controller)
+    pub fn new(handler_id: String, filter_controller: Arc<RwLock<FilterController>>) -> Self {
+        Self {
+            handler_id,
+            filter_controller,
+        }
     }
 }
 
 impl TaskHandler for FilterTaskHandler {
     fn handler_id(&self) -> &str {
-        FILTER_HANDLER_ID
+        &self.handler_id
     }
 
     fn run(&self, content: TaskContent) -> BoxResultFuture<(), anyhow::Error> {
-        let filter_controller = self.0.clone();
+        let filter_controller = self.filter_controller.clone();
         Box::pin(async move {
             if let TaskContent::Text(predicate) = content {
                 let _ = filter_controller

+ 11 - 6
frontend/rust-lib/flowy-grid/src/services/grid_editor.rs

@@ -1,4 +1,4 @@
-use crate::dart_notification::{send_dart_notification, GridNotification};
+use crate::dart_notification::{send_dart_notification, GridDartNotification};
 use crate::entities::CellPathParams;
 use crate::entities::*;
 use crate::manager::GridUser;
@@ -11,9 +11,9 @@ use crate::services::field::{
 
 use crate::services::filter::FilterType;
 use crate::services::grid_editor_trait_impl::GridViewEditorDelegateImpl;
-use crate::services::grid_view_manager::GridViewManager;
 use crate::services::persistence::block_index::BlockIndexCache;
 use crate::services::row::{GridBlock, RowRevisionBuilder};
+use crate::services::view_editor::{GridViewChanged, GridViewManager};
 use bytes::Bytes;
 use flowy_database::ConnectionPool;
 use flowy_error::{ErrorCode, FlowyError, FlowyResult};
@@ -30,7 +30,7 @@ use lib_infra::future::{to_future, FutureResult};
 use lib_ot::core::EmptyAttributes;
 use std::collections::HashMap;
 use std::sync::Arc;
-use tokio::sync::RwLock;
+use tokio::sync::{broadcast, RwLock};
 
 pub struct GridRevisionEditor {
     pub grid_id: String,
@@ -73,6 +73,7 @@ impl GridRevisionEditor {
 
         // View manager
         let view_manager = Arc::new(GridViewManager::new(grid_id.to_owned(), user.clone(), delegate).await?);
+
         let editor = Arc::new(Self {
             grid_id: grid_id.to_owned(),
             user,
@@ -96,7 +97,7 @@ impl GridRevisionEditor {
         });
     }
 
-    /// Save the type-option data to disk and send a `GridNotification::DidUpdateField` notification
+    /// Save the type-option data to disk and send a `GridDartNotification::DidUpdateField` notification
     /// to dart side.
     ///
     /// It will do nothing if the passed-in type_option_data is empty
@@ -439,6 +440,10 @@ impl GridRevisionEditor {
         Ok(())
     }
 
+    pub async fn subscribe_view_changed(&self) -> broadcast::Receiver<GridViewChanged> {
+        self.view_manager.subscribe_view_changed().await
+    }
+
     pub async fn duplicate_row(&self, _row_id: &str) -> FlowyResult<()> {
         Ok(())
     }
@@ -811,7 +816,7 @@ impl GridRevisionEditor {
             let notified_changeset = GridFieldChangesetPB::update(&self.grid_id, vec![updated_field.clone()]);
             let _ = self.notify_did_update_grid(notified_changeset).await?;
 
-            send_dart_notification(field_id, GridNotification::DidUpdateField)
+            send_dart_notification(field_id, GridDartNotification::DidUpdateField)
                 .payload(updated_field)
                 .send();
         }
@@ -820,7 +825,7 @@ impl GridRevisionEditor {
     }
 
     async fn notify_did_update_grid(&self, changeset: GridFieldChangesetPB) -> FlowyResult<()> {
-        send_dart_notification(&self.grid_id, GridNotification::DidUpdateGridField)
+        send_dart_notification(&self.grid_id, GridDartNotification::DidUpdateGridField)
             .payload(changeset)
             .send();
         Ok(())

+ 1 - 1
frontend/rust-lib/flowy-grid/src/services/grid_editor_trait_impl.rs

@@ -1,6 +1,6 @@
 use crate::services::block_manager::GridBlockManager;
-use crate::services::grid_view_editor::GridViewEditorDelegate;
 use crate::services::row::GridBlock;
+use crate::services::view_editor::GridViewEditorDelegate;
 use flowy_sync::client_grid::GridRevisionPad;
 use flowy_task::TaskDispatcher;
 use grid_rev_model::{FieldRevision, RowRevision};

+ 1 - 2
frontend/rust-lib/flowy-grid/src/services/mod.rs

@@ -7,9 +7,8 @@ pub mod field;
 pub mod filter;
 pub mod grid_editor;
 mod grid_editor_trait_impl;
-pub mod grid_view_editor;
-pub mod grid_view_manager;
 pub mod group;
 pub mod persistence;
 pub mod row;
 pub mod setting;
+pub mod view_editor;

+ 46 - 0
frontend/rust-lib/flowy-grid/src/services/view_editor/changed_notifier.rs

@@ -0,0 +1,46 @@
+use crate::dart_notification::{send_dart_notification, GridDartNotification};
+use crate::entities::GridBlockChangesetPB;
+use crate::services::filter::FilterResultNotification;
+use async_stream::stream;
+use futures::stream::StreamExt;
+use tokio::sync::broadcast;
+
+#[derive(Clone)]
+pub enum GridViewChanged {
+    DidReceiveFilterResult(FilterResultNotification),
+}
+
+pub type GridViewChangedNotifier = broadcast::Sender<GridViewChanged>;
+
+pub(crate) struct GridViewChangedReceiverRunner(pub(crate) Option<broadcast::Receiver<GridViewChanged>>);
+impl GridViewChangedReceiverRunner {
+    pub(crate) async fn run(mut self) {
+        let mut receiver = self.0.take().expect("Only take once");
+        let stream = stream! {
+            loop {
+                match receiver.recv().await {
+                    Ok(changed) => yield changed,
+                    Err(_e) => break,
+                }
+            }
+        };
+        stream
+            .for_each(|changed| async {
+                match changed {
+                    GridViewChanged::DidReceiveFilterResult(notification) => {
+                        let changeset = GridBlockChangesetPB {
+                            block_id: notification.block_id,
+                            visible_rows: notification.visible_rows,
+                            invisible_rows: notification.invisible_rows,
+                            ..Default::default()
+                        };
+
+                        send_dart_notification(&changeset.block_id, GridDartNotification::DidUpdateGridBlock)
+                            .payload(changeset)
+                            .send()
+                    }
+                }
+            })
+            .await;
+    }
+}

+ 68 - 194
frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs → frontend/rust-lib/flowy-grid/src/services/view_editor/editor.rs

@@ -1,29 +1,23 @@
-use crate::dart_notification::{send_dart_notification, GridNotification};
+use crate::dart_notification::{send_dart_notification, GridDartNotification};
 use crate::entities::*;
-use crate::services::filter::{
-    FilterChangeset, FilterController, FilterTaskHandler, FilterType, GridViewFilterDelegate,
-};
+use crate::services::filter::{FilterChangeset, FilterController, FilterTaskHandler, FilterType};
+
 use crate::services::group::{
     default_group_configuration, find_group_field, make_group_controller, Group, GroupConfigurationReader,
-    GroupConfigurationWriter, GroupController, MoveGroupRowContext,
+    GroupController, MoveGroupRowContext,
 };
 use crate::services::row::GridBlock;
-use bytes::Bytes;
+use crate::services::view_editor::changed_notifier::GridViewChangedNotifier;
+use crate::services::view_editor::trait_impl::*;
 use flowy_database::ConnectionPool;
-use flowy_error::{FlowyError, FlowyResult};
-use flowy_http_model::revision::Revision;
-use flowy_revision::{
-    RevisionCloudService, RevisionManager, RevisionMergeable, RevisionObjectDeserializer, RevisionObjectSerializer,
-};
+use flowy_error::FlowyResult;
+use flowy_revision::RevisionManager;
 use flowy_sync::client_grid::{GridViewRevisionChangeset, GridViewRevisionPad};
-use flowy_sync::util::make_operations_from_revisions;
 use flowy_task::TaskDispatcher;
-use grid_rev_model::{
-    gen_grid_filter_id, FieldRevision, FieldTypeRevision, FilterRevision, GroupConfigurationRevision, RowChangeset,
-    RowRevision,
-};
-use lib_infra::future::{to_future, Fut, FutureResult};
-use lib_ot::core::EmptyAttributes;
+use grid_rev_model::{gen_grid_filter_id, FieldRevision, FieldTypeRevision, FilterRevision, RowChangeset, RowRevision};
+use lib_infra::future::Fut;
+use lib_infra::ref_map::RefCountValue;
+use nanoid::nanoid;
 use std::future::Future;
 use std::sync::Arc;
 use tokio::sync::RwLock;
@@ -41,7 +35,6 @@ pub trait GridViewEditorDelegate: Send + Sync + 'static {
     fn get_task_scheduler(&self) -> Arc<RwLock<TaskDispatcher>>;
 }
 
-#[allow(dead_code)]
 pub struct GridViewRevisionEditor {
     user_id: String,
     view_id: String,
@@ -51,13 +44,15 @@ pub struct GridViewRevisionEditor {
     group_controller: Arc<RwLock<Box<dyn GroupController>>>,
     filter_controller: Arc<RwLock<FilterController>>,
 }
+
 impl GridViewRevisionEditor {
     #[tracing::instrument(level = "trace", skip_all, err)]
-    pub(crate) async fn new(
+    pub async fn new(
         user_id: &str,
         token: &str,
         view_id: String,
         delegate: Arc<dyn GridViewEditorDelegate>,
+        notifier: GridViewChangedNotifier,
         mut rev_manager: RevisionManager<Arc<ConnectionPool>>,
     ) -> FlowyResult<Self> {
         let cloud = Arc::new(GridViewRevisionCloudService {
@@ -77,7 +72,7 @@ impl GridViewRevisionEditor {
 
         let user_id = user_id.to_owned();
         let group_controller = Arc::new(RwLock::new(group_controller));
-        let filter_controller = make_filter_controller(&view_id, delegate.clone(), pad.clone()).await;
+        let filter_controller = make_filter_controller(&view_id, delegate.clone(), notifier.clone(), pad.clone()).await;
         Ok(Self {
             pad,
             user_id,
@@ -89,21 +84,25 @@ impl GridViewRevisionEditor {
         })
     }
 
-    pub(crate) async fn close(&self) {
-        self.filter_controller.read().await.close().await;
+    #[tracing::instrument(name = "close grid view editor", level = "trace", skip_all)]
+    pub fn close(&self) {
+        let filter_controller = self.filter_controller.clone();
+        tokio::spawn(async move {
+            filter_controller.read().await.close().await;
+        });
     }
 
-    pub(crate) async fn filter_rows(&self, _block_id: &str, mut rows: Vec<Arc<RowRevision>>) -> Vec<Arc<RowRevision>> {
+    pub async fn filter_rows(&self, _block_id: &str, mut rows: Vec<Arc<RowRevision>>) -> Vec<Arc<RowRevision>> {
         self.filter_controller.write().await.filter_row_revs(&mut rows).await;
         rows
     }
 
-    pub(crate) async fn duplicate_view_data(&self) -> FlowyResult<String> {
+    pub async fn duplicate_view_data(&self) -> FlowyResult<String> {
         let json_str = self.pad.read().await.json_str()?;
         Ok(json_str)
     }
 
-    pub(crate) async fn will_create_view_row(&self, row_rev: &mut RowRevision, params: &CreateRowParams) {
+    pub async fn will_create_view_row(&self, row_rev: &mut RowRevision, params: &CreateRowParams) {
         if params.group_id.is_none() {
             return;
         }
@@ -116,7 +115,7 @@ impl GridViewRevisionEditor {
             .await;
     }
 
-    pub(crate) async fn did_create_view_row(&self, row_pb: &RowPB, params: &CreateRowParams) {
+    pub async fn did_create_view_row(&self, row_pb: &RowPB, params: &CreateRowParams) {
         // Send the group notification if the current view has groups
         match params.group_id.as_ref() {
             None => {}
@@ -139,7 +138,7 @@ impl GridViewRevisionEditor {
     }
 
     #[tracing::instrument(level = "trace", skip_all)]
-    pub(crate) async fn did_delete_view_row(&self, row_rev: &RowRevision) {
+    pub async fn did_delete_view_row(&self, row_rev: &RowRevision) {
         // Send the group notification if the current view has groups;
         let changesets = self
             .mut_group_controller(|group_controller, field_rev| {
@@ -155,7 +154,7 @@ impl GridViewRevisionEditor {
         }
     }
 
-    pub(crate) async fn did_update_view_cell(&self, row_rev: &RowRevision) {
+    pub async fn did_update_view_cell(&self, row_rev: &RowRevision) {
         let changesets = self
             .mut_group_controller(|group_controller, field_rev| {
                 group_controller.did_update_group_row(row_rev, &field_rev)
@@ -169,7 +168,7 @@ impl GridViewRevisionEditor {
         }
     }
 
-    pub(crate) async fn move_view_group_row(
+    pub async fn move_view_group_row(
         &self,
         row_rev: &RowRevision,
         row_changeset: &mut RowChangeset,
@@ -195,7 +194,7 @@ impl GridViewRevisionEditor {
     }
     /// Only call once after grid view editor initialized
     #[tracing::instrument(level = "trace", skip(self))]
-    pub(crate) async fn load_view_groups(&self) -> FlowyResult<Vec<GroupPB>> {
+    pub async fn load_view_groups(&self) -> FlowyResult<Vec<GroupPB>> {
         let groups = self
             .group_controller
             .read()
@@ -209,7 +208,7 @@ impl GridViewRevisionEditor {
     }
 
     #[tracing::instrument(level = "trace", skip(self), err)]
-    pub(crate) async fn move_view_group(&self, params: MoveGroupParams) -> FlowyResult<()> {
+    pub async fn move_view_group(&self, params: MoveGroupParams) -> FlowyResult<()> {
         let _ = self
             .group_controller
             .write()
@@ -237,22 +236,22 @@ impl GridViewRevisionEditor {
         Ok(())
     }
 
-    pub(crate) async fn group_id(&self) -> String {
+    pub async fn group_id(&self) -> String {
         self.group_controller.read().await.field_id().to_string()
     }
 
-    pub(crate) async fn get_view_setting(&self) -> GridSettingPB {
+    pub async fn get_view_setting(&self) -> GridSettingPB {
         let field_revs = self.delegate.get_field_revs(None).await;
         let grid_setting = make_grid_setting(&*self.pad.read().await, &field_revs);
         grid_setting
     }
 
-    pub(crate) async fn get_all_view_filters(&self) -> Vec<Arc<FilterRevision>> {
+    pub async fn get_all_view_filters(&self) -> Vec<Arc<FilterRevision>> {
         let field_revs = self.delegate.get_field_revs(None).await;
         self.pad.read().await.get_all_filters(&field_revs)
     }
 
-    pub(crate) async fn get_view_filters(&self, filter_type: &FilterType) -> Vec<Arc<FilterRevision>> {
+    pub async fn get_view_filters(&self, filter_type: &FilterType) -> Vec<Arc<FilterRevision>> {
         let field_type_rev: FieldTypeRevision = filter_type.field_type.clone().into();
         self.pad
             .read()
@@ -262,7 +261,7 @@ impl GridViewRevisionEditor {
 
     /// Initialize new group when grouping by a new field
     ///
-    pub(crate) async fn initialize_new_group(&self, params: InsertGroupParams) -> FlowyResult<()> {
+    pub async fn initialize_new_group(&self, params: InsertGroupParams) -> FlowyResult<()> {
         if let Some(field_rev) = self.delegate.get_field_rev(&params.field_id).await {
             let _ = self
                 .modify(|pad| {
@@ -283,7 +282,7 @@ impl GridViewRevisionEditor {
         Ok(())
     }
 
-    pub(crate) async fn delete_view_group(&self, params: DeleteGroupParams) -> FlowyResult<()> {
+    pub async fn delete_view_group(&self, params: DeleteGroupParams) -> FlowyResult<()> {
         self.modify(|pad| {
             let changeset = pad.delete_group(&params.group_id, &params.field_id, &params.field_type_rev)?;
             Ok(changeset)
@@ -291,7 +290,8 @@ impl GridViewRevisionEditor {
         .await
     }
 
-    pub(crate) async fn insert_view_filter(&self, params: CreateFilterParams) -> FlowyResult<()> {
+    #[tracing::instrument(level = "trace", skip(self), err)]
+    pub async fn insert_view_filter(&self, params: CreateFilterParams) -> FlowyResult<()> {
         let filter_type = FilterType::from(&params);
         let filter_rev = FilterRevision {
             id: gen_grid_filter_id(),
@@ -319,7 +319,8 @@ impl GridViewRevisionEditor {
         Ok(())
     }
 
-    pub(crate) async fn delete_view_filter(&self, params: DeleteFilterParams) -> FlowyResult<()> {
+    #[tracing::instrument(level = "trace", skip(self), err)]
+    pub async fn delete_view_filter(&self, params: DeleteFilterParams) -> FlowyResult<()> {
         let filter_type = params.filter_type;
         let field_type_rev = filter_type.field_type_rev();
         let filters = self
@@ -347,7 +348,7 @@ impl GridViewRevisionEditor {
     }
 
     #[tracing::instrument(level = "trace", skip_all, err)]
-    pub(crate) async fn did_update_view_field_type_option(&self, field_id: &str) -> FlowyResult<()> {
+    pub async fn did_update_view_field_type_option(&self, field_id: &str) -> FlowyResult<()> {
         if let Some(field_rev) = self.delegate.get_field_rev(field_id).await {
             let filter_type = FilterType::from(&field_rev);
             let filter_changeset = FilterChangeset::from_insert(filter_type);
@@ -367,7 +368,7 @@ impl GridViewRevisionEditor {
     /// * `field_id`:
     ///
     #[tracing::instrument(level = "debug", skip_all, err)]
-    pub(crate) async fn group_by_view_field(&self, field_id: &str) -> FlowyResult<()> {
+    pub async fn group_by_view_field(&self, field_id: &str) -> FlowyResult<()> {
         if let Some(field_rev) = self.delegate.get_field_rev(field_id).await {
             let row_revs = self.delegate.get_row_revs().await;
             let new_group_controller = new_group_controller_with_field_rev(
@@ -395,7 +396,7 @@ impl GridViewRevisionEditor {
 
             debug_assert!(!changeset.is_empty());
             if !changeset.is_empty() {
-                send_dart_notification(&changeset.view_id, GridNotification::DidGroupByNewField)
+                send_dart_notification(&changeset.view_id, GridDartNotification::DidGroupByNewField)
                     .payload(changeset)
                     .send();
             }
@@ -405,25 +406,25 @@ impl GridViewRevisionEditor {
 
     async fn notify_did_update_setting(&self) {
         let setting = self.get_view_setting().await;
-        send_dart_notification(&self.view_id, GridNotification::DidUpdateGridSetting)
+        send_dart_notification(&self.view_id, GridDartNotification::DidUpdateGridSetting)
             .payload(setting)
             .send();
     }
 
     pub async fn notify_did_update_group_rows(&self, payload: GroupRowsNotificationPB) {
-        send_dart_notification(&payload.group_id, GridNotification::DidUpdateGroup)
+        send_dart_notification(&payload.group_id, GridDartNotification::DidUpdateGroup)
             .payload(payload)
             .send();
     }
 
     pub async fn notify_did_update_filter(&self, changeset: FilterChangesetNotificationPB) {
-        send_dart_notification(&changeset.view_id, GridNotification::DidUpdateFilter)
+        send_dart_notification(&changeset.view_id, GridDartNotification::DidUpdateFilter)
             .payload(changeset)
             .send();
     }
 
     async fn notify_did_update_view(&self, changeset: GroupViewChangesetPB) {
-        send_dart_notification(&self.view_id, GridNotification::DidUpdateGroupView)
+        send_dart_notification(&self.view_id, GridDartNotification::DidUpdateGroupView)
             .payload(changeset)
             .send();
     }
@@ -473,6 +474,12 @@ impl GridViewRevisionEditor {
     }
 }
 
+impl RefCountValue for GridViewRevisionEditor {
+    fn did_remove(&self) {
+        self.close();
+    }
+}
+
 async fn new_group_controller(
     user_id: String,
     view_id: String,
@@ -521,6 +528,7 @@ async fn new_group_controller_with_field_rev(
 async fn make_filter_controller(
     view_id: &str,
     delegate: Arc<dyn GridViewEditorDelegate>,
+    notifier: GridViewChangedNotifier,
     pad: Arc<RwLock<GridViewRevisionPad>>,
 ) -> Arc<RwLock<FilterController>> {
     let field_revs = delegate.get_field_revs(None).await;
@@ -530,160 +538,26 @@ async fn make_filter_controller(
         editor_delegate: delegate.clone(),
         view_revision_pad: pad,
     };
-    let filter_controller = FilterController::new(view_id, filter_delegate, task_scheduler.clone(), filter_revs).await;
+    let handler_id = gen_handler_id();
+    let filter_controller = FilterController::new(
+        view_id,
+        &handler_id,
+        filter_delegate,
+        task_scheduler.clone(),
+        filter_revs,
+        notifier,
+    )
+    .await;
     let filter_controller = Arc::new(RwLock::new(filter_controller));
     task_scheduler
         .write()
         .await
-        .register_handler(FilterTaskHandler::new(filter_controller.clone()));
+        .register_handler(FilterTaskHandler::new(handler_id, filter_controller.clone()));
     filter_controller
 }
 
-async fn apply_change(
-    _user_id: &str,
-    rev_manager: Arc<RevisionManager<Arc<ConnectionPool>>>,
-    change: GridViewRevisionChangeset,
-) -> FlowyResult<()> {
-    let GridViewRevisionChangeset { operations: delta, md5 } = change;
-    let (base_rev_id, rev_id) = rev_manager.next_rev_id_pair();
-    let delta_data = delta.json_bytes();
-    let revision = Revision::new(&rev_manager.object_id, base_rev_id, rev_id, delta_data, md5);
-    let _ = rev_manager.add_local_revision(&revision).await?;
-    Ok(())
-}
-
-struct GridViewRevisionCloudService {
-    #[allow(dead_code)]
-    token: String,
-}
-
-impl RevisionCloudService for GridViewRevisionCloudService {
-    fn fetch_object(&self, _user_id: &str, _object_id: &str) -> FutureResult<Vec<Revision>, FlowyError> {
-        FutureResult::new(async move { Ok(vec![]) })
-    }
-}
-
-pub struct GridViewRevisionSerde();
-impl RevisionObjectDeserializer for GridViewRevisionSerde {
-    type Output = GridViewRevisionPad;
-
-    fn deserialize_revisions(object_id: &str, revisions: Vec<Revision>) -> FlowyResult<Self::Output> {
-        let pad = GridViewRevisionPad::from_revisions(object_id, revisions)?;
-        Ok(pad)
-    }
-}
-
-impl RevisionObjectSerializer for GridViewRevisionSerde {
-    fn combine_revisions(revisions: Vec<Revision>) -> FlowyResult<Bytes> {
-        let operations = make_operations_from_revisions::<EmptyAttributes>(revisions)?;
-        Ok(operations.json_bytes())
-    }
-}
-
-pub struct GridViewRevisionCompress();
-impl RevisionMergeable for GridViewRevisionCompress {
-    fn combine_revisions(&self, revisions: Vec<Revision>) -> FlowyResult<Bytes> {
-        GridViewRevisionSerde::combine_revisions(revisions)
-    }
-}
-
-struct GroupConfigurationReaderImpl(Arc<RwLock<GridViewRevisionPad>>);
-
-impl GroupConfigurationReader for GroupConfigurationReaderImpl {
-    fn get_configuration(&self) -> Fut<Option<Arc<GroupConfigurationRevision>>> {
-        let view_pad = self.0.clone();
-        to_future(async move {
-            let mut groups = view_pad.read().await.get_all_groups();
-            if groups.is_empty() {
-                None
-            } else {
-                debug_assert_eq!(groups.len(), 1);
-                Some(groups.pop().unwrap())
-            }
-        })
-    }
-}
-
-struct GroupConfigurationWriterImpl {
-    user_id: String,
-    rev_manager: Arc<RevisionManager<Arc<ConnectionPool>>>,
-    view_pad: Arc<RwLock<GridViewRevisionPad>>,
-}
-
-impl GroupConfigurationWriter for GroupConfigurationWriterImpl {
-    fn save_configuration(
-        &self,
-        field_id: &str,
-        field_type: FieldTypeRevision,
-        group_configuration: GroupConfigurationRevision,
-    ) -> Fut<FlowyResult<()>> {
-        let user_id = self.user_id.clone();
-        let rev_manager = self.rev_manager.clone();
-        let view_pad = self.view_pad.clone();
-        let field_id = field_id.to_owned();
-
-        to_future(async move {
-            let changeset = view_pad.write().await.insert_or_update_group_configuration(
-                &field_id,
-                &field_type,
-                group_configuration,
-            )?;
-
-            if let Some(changeset) = changeset {
-                let _ = apply_change(&user_id, rev_manager, changeset).await?;
-            }
-            Ok(())
-        })
-    }
-}
-
-pub fn make_grid_setting(view_pad: &GridViewRevisionPad, field_revs: &[Arc<FieldRevision>]) -> GridSettingPB {
-    let layout_type: GridLayout = view_pad.layout.clone().into();
-    let filter_configurations = view_pad
-        .get_all_filters(field_revs)
-        .into_iter()
-        .map(|filter| FilterPB::from(filter.as_ref()))
-        .collect::<Vec<FilterPB>>();
-
-    let group_configurations = view_pad
-        .get_groups_by_field_revs(field_revs)
-        .into_iter()
-        .map(|group| GridGroupConfigurationPB::from(group.as_ref()))
-        .collect::<Vec<GridGroupConfigurationPB>>();
-
-    GridSettingPB {
-        layouts: GridLayoutPB::all(),
-        layout_type,
-        filter_configurations: filter_configurations.into(),
-        group_configurations: group_configurations.into(),
-    }
-}
-
-struct GridViewFilterDelegateImpl {
-    editor_delegate: Arc<dyn GridViewEditorDelegate>,
-    view_revision_pad: Arc<RwLock<GridViewRevisionPad>>,
-}
-
-impl GridViewFilterDelegate for GridViewFilterDelegateImpl {
-    fn get_filter_rev(&self, filter_id: FilterType) -> Fut<Vec<Arc<FilterRevision>>> {
-        let pad = self.view_revision_pad.clone();
-        to_future(async move {
-            let field_type_rev: FieldTypeRevision = filter_id.field_type.into();
-            pad.read().await.get_filters(&filter_id.field_id, &field_type_rev)
-        })
-    }
-
-    fn get_field_rev(&self, field_id: &str) -> Fut<Option<Arc<FieldRevision>>> {
-        self.editor_delegate.get_field_rev(field_id)
-    }
-
-    fn get_field_revs(&self, field_ids: Option<Vec<String>>) -> Fut<Vec<Arc<FieldRevision>>> {
-        self.editor_delegate.get_field_revs(field_ids)
-    }
-
-    fn get_blocks(&self) -> Fut<Vec<GridBlock>> {
-        self.editor_delegate.get_blocks()
-    }
+fn gen_handler_id() -> String {
+    nanoid!(10)
 }
 
 #[cfg(test)]

+ 65 - 56
frontend/rust-lib/flowy-grid/src/services/grid_view_manager.rs → frontend/rust-lib/flowy-grid/src/services/view_editor/editor_manager.rs

@@ -3,49 +3,54 @@ use crate::entities::{
     MoveGroupParams, RepeatedGridGroupPB, RowPB,
 };
 use crate::manager::GridUser;
-
-use crate::services::grid_view_editor::{GridViewEditorDelegate, GridViewRevisionCompress, GridViewRevisionEditor};
+use crate::services::filter::FilterType;
 use crate::services::persistence::rev_sqlite::SQLiteGridViewRevisionPersistence;
-
-use dashmap::DashMap;
+use crate::services::view_editor::changed_notifier::*;
+use crate::services::view_editor::trait_impl::GridViewRevisionCompress;
+use crate::services::view_editor::{GridViewEditorDelegate, GridViewRevisionEditor};
 use flowy_database::ConnectionPool;
 use flowy_error::FlowyResult;
 use flowy_revision::{
     RevisionManager, RevisionPersistence, RevisionPersistenceConfiguration, SQLiteRevisionSnapshotPersistence,
 };
-
-use crate::services::filter::FilterType;
 use grid_rev_model::{FilterRevision, RowChangeset, RowRevision};
 use lib_infra::future::Fut;
+use lib_infra::ref_map::RefCountHashMap;
 use std::sync::Arc;
+use tokio::sync::{broadcast, RwLock};
 
-type ViewId = String;
-
-pub(crate) struct GridViewManager {
+pub struct GridViewManager {
     grid_id: String,
     user: Arc<dyn GridUser>,
     delegate: Arc<dyn GridViewEditorDelegate>,
-    view_editors: DashMap<ViewId, Arc<GridViewRevisionEditor>>,
+    view_editors: RwLock<RefCountHashMap<Arc<GridViewRevisionEditor>>>,
+    pub notifier: broadcast::Sender<GridViewChanged>,
 }
 
 impl GridViewManager {
-    pub(crate) async fn new(
+    pub async fn new(
         grid_id: String,
         user: Arc<dyn GridUser>,
         delegate: Arc<dyn GridViewEditorDelegate>,
     ) -> FlowyResult<Self> {
+        let (notifier, _) = broadcast::channel(100);
+        tokio::spawn(GridViewChangedReceiverRunner(Some(notifier.subscribe())).run());
+        let view_editors = RwLock::new(RefCountHashMap::default());
         Ok(Self {
             grid_id,
             user,
             delegate,
-            view_editors: DashMap::default(),
+            view_editors,
+            notifier,
         })
     }
 
-    pub(crate) async fn close(&self, _view_id: &str) {
-        if let Ok(editor) = self.get_default_view_editor().await {
-            let _ = editor.close().await;
-        }
+    pub async fn close(&self, view_id: &str) {
+        self.view_editors.write().await.remove(view_id);
+    }
+
+    pub async fn subscribe_view_changed(&self) -> broadcast::Receiver<GridViewChanged> {
+        self.notifier.subscribe()
     }
 
     pub async fn filter_rows(&self, block_id: &str, rows: Vec<Arc<RowRevision>>) -> FlowyResult<Vec<Arc<RowRevision>>> {
@@ -54,94 +59,94 @@ impl GridViewManager {
         Ok(rows)
     }
 
-    pub(crate) async fn duplicate_grid_view(&self) -> FlowyResult<String> {
+    pub async fn duplicate_grid_view(&self) -> FlowyResult<String> {
         let editor = self.get_default_view_editor().await?;
         let view_data = editor.duplicate_view_data().await?;
         Ok(view_data)
     }
 
     /// When the row was created, we may need to modify the [RowRevision] according to the [CreateRowParams].
-    pub(crate) async fn will_create_row(&self, row_rev: &mut RowRevision, params: &CreateRowParams) {
-        for view_editor in self.view_editors.iter() {
+    pub async fn will_create_row(&self, row_rev: &mut RowRevision, params: &CreateRowParams) {
+        for view_editor in self.view_editors.read().await.values() {
             view_editor.will_create_view_row(row_rev, params).await;
         }
     }
 
     /// Notify the view that the row was created. For the moment, the view is just sending notifications.
-    pub(crate) async fn did_create_row(&self, row_pb: &RowPB, params: &CreateRowParams) {
-        for view_editor in self.view_editors.iter() {
+    pub async fn did_create_row(&self, row_pb: &RowPB, params: &CreateRowParams) {
+        for view_editor in self.view_editors.read().await.values() {
             view_editor.did_create_view_row(row_pb, params).await;
         }
     }
 
     /// Insert/Delete the group's row if the corresponding cell data was changed.  
-    pub(crate) async fn did_update_cell(&self, row_id: &str) {
+    pub async fn did_update_cell(&self, row_id: &str) {
         match self.delegate.get_row_rev(row_id).await {
             None => {
                 tracing::warn!("Can not find the row in grid view");
             }
             Some(row_rev) => {
-                for view_editor in self.view_editors.iter() {
+                for view_editor in self.view_editors.read().await.values() {
                     view_editor.did_update_view_cell(&row_rev).await;
                 }
             }
         }
     }
 
-    pub(crate) async fn group_by_field(&self, field_id: &str) -> FlowyResult<()> {
+    pub async fn group_by_field(&self, field_id: &str) -> FlowyResult<()> {
         let view_editor = self.get_default_view_editor().await?;
         let _ = view_editor.group_by_view_field(field_id).await?;
         Ok(())
     }
 
-    pub(crate) async fn did_delete_row(&self, row_rev: Arc<RowRevision>) {
-        for view_editor in self.view_editors.iter() {
+    pub async fn did_delete_row(&self, row_rev: Arc<RowRevision>) {
+        for view_editor in self.view_editors.read().await.values() {
             view_editor.did_delete_view_row(&row_rev).await;
         }
     }
 
-    pub(crate) async fn get_setting(&self) -> FlowyResult<GridSettingPB> {
+    pub async fn get_setting(&self) -> FlowyResult<GridSettingPB> {
         let view_editor = self.get_default_view_editor().await?;
         Ok(view_editor.get_view_setting().await)
     }
 
-    pub(crate) async fn get_all_filters(&self) -> FlowyResult<Vec<Arc<FilterRevision>>> {
+    pub async fn get_all_filters(&self) -> FlowyResult<Vec<Arc<FilterRevision>>> {
         let view_editor = self.get_default_view_editor().await?;
         Ok(view_editor.get_all_view_filters().await)
     }
 
-    pub(crate) async fn get_filters(&self, filter_id: &FilterType) -> FlowyResult<Vec<Arc<FilterRevision>>> {
+    pub async fn get_filters(&self, filter_id: &FilterType) -> FlowyResult<Vec<Arc<FilterRevision>>> {
         let view_editor = self.get_default_view_editor().await?;
         Ok(view_editor.get_view_filters(filter_id).await)
     }
 
-    pub(crate) async fn insert_or_update_filter(&self, params: CreateFilterParams) -> FlowyResult<()> {
+    pub async fn insert_or_update_filter(&self, params: CreateFilterParams) -> FlowyResult<()> {
         let view_editor = self.get_default_view_editor().await?;
         view_editor.insert_view_filter(params).await
     }
 
-    pub(crate) async fn delete_filter(&self, params: DeleteFilterParams) -> FlowyResult<()> {
+    pub async fn delete_filter(&self, params: DeleteFilterParams) -> FlowyResult<()> {
         let view_editor = self.get_default_view_editor().await?;
         view_editor.delete_view_filter(params).await
     }
 
-    pub(crate) async fn load_groups(&self) -> FlowyResult<RepeatedGridGroupPB> {
+    pub async fn load_groups(&self) -> FlowyResult<RepeatedGridGroupPB> {
         let view_editor = self.get_default_view_editor().await?;
         let groups = view_editor.load_view_groups().await?;
         Ok(RepeatedGridGroupPB { items: groups })
     }
 
-    pub(crate) async fn insert_or_update_group(&self, params: InsertGroupParams) -> FlowyResult<()> {
+    pub async fn insert_or_update_group(&self, params: InsertGroupParams) -> FlowyResult<()> {
         let view_editor = self.get_default_view_editor().await?;
         view_editor.initialize_new_group(params).await
     }
 
-    pub(crate) async fn delete_group(&self, params: DeleteGroupParams) -> FlowyResult<()> {
+    pub async fn delete_group(&self, params: DeleteGroupParams) -> FlowyResult<()> {
         let view_editor = self.get_default_view_editor().await?;
         view_editor.delete_view_group(params).await
     }
 
-    pub(crate) async fn move_group(&self, params: MoveGroupParams) -> FlowyResult<()> {
+    pub async fn move_group(&self, params: MoveGroupParams) -> FlowyResult<()> {
         let view_editor = self.get_default_view_editor().await?;
         let _ = view_editor.move_view_group(params).await?;
         Ok(())
@@ -150,7 +155,7 @@ impl GridViewManager {
     /// It may generate a RowChangeset when the Row was moved from one group to another.
     /// The return value, [RowChangeset], contains the changes made by the groups.
     ///
-    pub(crate) async fn move_group_row(
+    pub async fn move_group_row(
         &self,
         row_rev: Arc<RowRevision>,
         to_group_id: String,
@@ -182,7 +187,7 @@ impl GridViewManager {
     /// * `field_id`: the id of the field in current view
     ///
     #[tracing::instrument(level = "trace", skip(self), err)]
-    pub(crate) async fn did_update_view_field_type_option(&self, field_id: &str) -> FlowyResult<()> {
+    pub async fn did_update_view_field_type_option(&self, field_id: &str) -> FlowyResult<()> {
         let view_editor = self.get_default_view_editor().await?;
         if view_editor.group_id().await == field_id {
             let _ = view_editor.group_by_view_field(field_id).await?;
@@ -192,34 +197,38 @@ impl GridViewManager {
         Ok(())
     }
 
-    pub(crate) async fn get_view_editor(&self, view_id: &str) -> FlowyResult<Arc<GridViewRevisionEditor>> {
+    pub async fn get_view_editor(&self, view_id: &str) -> FlowyResult<Arc<GridViewRevisionEditor>> {
         debug_assert!(!view_id.is_empty());
-        match self.view_editors.get(view_id) {
-            None => {
-                let editor = Arc::new(make_view_editor(&self.user, view_id, self.delegate.clone()).await?);
-                self.view_editors.insert(view_id.to_owned(), editor.clone());
-                Ok(editor)
-            }
-            Some(view_editor) => Ok(view_editor.clone()),
+        if let Some(editor) = self.view_editors.read().await.get(view_id) {
+            return Ok(editor);
         }
+        tracing::trace!("{:p} create view_editor", self);
+        let mut view_editors = self.view_editors.write().await;
+        let editor = Arc::new(self.make_view_editor(view_id).await?);
+        view_editors.insert(view_id.to_owned(), editor.clone());
+        Ok(editor)
     }
 
     async fn get_default_view_editor(&self) -> FlowyResult<Arc<GridViewRevisionEditor>> {
         self.get_view_editor(&self.grid_id).await
     }
-}
 
-async fn make_view_editor(
-    user: &Arc<dyn GridUser>,
-    view_id: &str,
-    delegate: Arc<dyn GridViewEditorDelegate>,
-) -> FlowyResult<GridViewRevisionEditor> {
-    let rev_manager = make_grid_view_rev_manager(user, view_id).await?;
-    let user_id = user.user_id()?;
-    let token = user.token()?;
-    let view_id = view_id.to_owned();
+    async fn make_view_editor(&self, view_id: &str) -> FlowyResult<GridViewRevisionEditor> {
+        let rev_manager = make_grid_view_rev_manager(&self.user, view_id).await?;
+        let user_id = self.user.user_id()?;
+        let token = self.user.token()?;
+        let view_id = view_id.to_owned();
 
-    GridViewRevisionEditor::new(&user_id, &token, view_id, delegate, rev_manager).await
+        GridViewRevisionEditor::new(
+            &user_id,
+            &token,
+            view_id,
+            self.delegate.clone(),
+            self.notifier.clone(),
+            rev_manager,
+        )
+        .await
+    }
 }
 
 pub async fn make_grid_view_rev_manager(

+ 8 - 0
frontend/rust-lib/flowy-grid/src/services/view_editor/mod.rs

@@ -0,0 +1,8 @@
+mod changed_notifier;
+mod editor;
+mod editor_manager;
+mod trait_impl;
+
+pub use changed_notifier::*;
+pub use editor::*;
+pub use editor_manager::*;

+ 166 - 0
frontend/rust-lib/flowy-grid/src/services/view_editor/trait_impl.rs

@@ -0,0 +1,166 @@
+use crate::entities::{FilterPB, GridGroupConfigurationPB, GridLayout, GridLayoutPB, GridSettingPB};
+use crate::services::filter::{FilterDelegate, FilterType};
+use crate::services::group::{GroupConfigurationReader, GroupConfigurationWriter};
+use crate::services::row::GridBlock;
+use crate::services::view_editor::GridViewEditorDelegate;
+use bytes::Bytes;
+use flowy_database::ConnectionPool;
+use flowy_error::{FlowyError, FlowyResult};
+use flowy_http_model::revision::Revision;
+use flowy_revision::{
+    RevisionCloudService, RevisionManager, RevisionMergeable, RevisionObjectDeserializer, RevisionObjectSerializer,
+};
+use flowy_sync::client_grid::{GridViewRevisionChangeset, GridViewRevisionPad};
+use flowy_sync::util::make_operations_from_revisions;
+use grid_rev_model::{FieldRevision, FieldTypeRevision, FilterRevision, GroupConfigurationRevision};
+use lib_infra::future::{to_future, Fut, FutureResult};
+use lib_ot::core::EmptyAttributes;
+use std::sync::Arc;
+use tokio::sync::RwLock;
+
+pub(crate) struct GridViewRevisionCloudService {
+    #[allow(dead_code)]
+    pub(crate) token: String,
+}
+
+impl RevisionCloudService for GridViewRevisionCloudService {
+    fn fetch_object(&self, _user_id: &str, _object_id: &str) -> FutureResult<Vec<Revision>, FlowyError> {
+        FutureResult::new(async move { Ok(vec![]) })
+    }
+}
+
+pub(crate) struct GridViewRevisionSerde();
+impl RevisionObjectDeserializer for GridViewRevisionSerde {
+    type Output = GridViewRevisionPad;
+
+    fn deserialize_revisions(object_id: &str, revisions: Vec<Revision>) -> FlowyResult<Self::Output> {
+        let pad = GridViewRevisionPad::from_revisions(object_id, revisions)?;
+        Ok(pad)
+    }
+}
+
+impl RevisionObjectSerializer for GridViewRevisionSerde {
+    fn combine_revisions(revisions: Vec<Revision>) -> FlowyResult<Bytes> {
+        let operations = make_operations_from_revisions::<EmptyAttributes>(revisions)?;
+        Ok(operations.json_bytes())
+    }
+}
+
+pub(crate) struct GridViewRevisionCompress();
+impl RevisionMergeable for GridViewRevisionCompress {
+    fn combine_revisions(&self, revisions: Vec<Revision>) -> FlowyResult<Bytes> {
+        GridViewRevisionSerde::combine_revisions(revisions)
+    }
+}
+
+pub(crate) struct GroupConfigurationReaderImpl(pub(crate) Arc<RwLock<GridViewRevisionPad>>);
+
+impl GroupConfigurationReader for GroupConfigurationReaderImpl {
+    fn get_configuration(&self) -> Fut<Option<Arc<GroupConfigurationRevision>>> {
+        let view_pad = self.0.clone();
+        to_future(async move {
+            let mut groups = view_pad.read().await.get_all_groups();
+            if groups.is_empty() {
+                None
+            } else {
+                debug_assert_eq!(groups.len(), 1);
+                Some(groups.pop().unwrap())
+            }
+        })
+    }
+}
+
+pub(crate) struct GroupConfigurationWriterImpl {
+    pub(crate) user_id: String,
+    pub(crate) rev_manager: Arc<RevisionManager<Arc<ConnectionPool>>>,
+    pub(crate) view_pad: Arc<RwLock<GridViewRevisionPad>>,
+}
+
+impl GroupConfigurationWriter for GroupConfigurationWriterImpl {
+    fn save_configuration(
+        &self,
+        field_id: &str,
+        field_type: FieldTypeRevision,
+        group_configuration: GroupConfigurationRevision,
+    ) -> Fut<FlowyResult<()>> {
+        let user_id = self.user_id.clone();
+        let rev_manager = self.rev_manager.clone();
+        let view_pad = self.view_pad.clone();
+        let field_id = field_id.to_owned();
+
+        to_future(async move {
+            let changeset = view_pad.write().await.insert_or_update_group_configuration(
+                &field_id,
+                &field_type,
+                group_configuration,
+            )?;
+
+            if let Some(changeset) = changeset {
+                let _ = apply_change(&user_id, rev_manager, changeset).await?;
+            }
+            Ok(())
+        })
+    }
+}
+
+pub(crate) async fn apply_change(
+    _user_id: &str,
+    rev_manager: Arc<RevisionManager<Arc<ConnectionPool>>>,
+    change: GridViewRevisionChangeset,
+) -> FlowyResult<()> {
+    let GridViewRevisionChangeset { operations: delta, md5 } = change;
+    let (base_rev_id, rev_id) = rev_manager.next_rev_id_pair();
+    let delta_data = delta.json_bytes();
+    let revision = Revision::new(&rev_manager.object_id, base_rev_id, rev_id, delta_data, md5);
+    let _ = rev_manager.add_local_revision(&revision).await?;
+    Ok(())
+}
+
+pub fn make_grid_setting(view_pad: &GridViewRevisionPad, field_revs: &[Arc<FieldRevision>]) -> GridSettingPB {
+    let layout_type: GridLayout = view_pad.layout.clone().into();
+    let filter_configurations = view_pad
+        .get_all_filters(field_revs)
+        .into_iter()
+        .map(|filter| FilterPB::from(filter.as_ref()))
+        .collect::<Vec<FilterPB>>();
+
+    let group_configurations = view_pad
+        .get_groups_by_field_revs(field_revs)
+        .into_iter()
+        .map(|group| GridGroupConfigurationPB::from(group.as_ref()))
+        .collect::<Vec<GridGroupConfigurationPB>>();
+
+    GridSettingPB {
+        layouts: GridLayoutPB::all(),
+        layout_type,
+        filter_configurations: filter_configurations.into(),
+        group_configurations: group_configurations.into(),
+    }
+}
+
+pub(crate) struct GridViewFilterDelegateImpl {
+    pub(crate) editor_delegate: Arc<dyn GridViewEditorDelegate>,
+    pub(crate) view_revision_pad: Arc<RwLock<GridViewRevisionPad>>,
+}
+
+impl FilterDelegate for GridViewFilterDelegateImpl {
+    fn get_filter_rev(&self, filter_id: FilterType) -> Fut<Vec<Arc<FilterRevision>>> {
+        let pad = self.view_revision_pad.clone();
+        to_future(async move {
+            let field_type_rev: FieldTypeRevision = filter_id.field_type.into();
+            pad.read().await.get_filters(&filter_id.field_id, &field_type_rev)
+        })
+    }
+
+    fn get_field_rev(&self, field_id: &str) -> Fut<Option<Arc<FieldRevision>>> {
+        self.editor_delegate.get_field_rev(field_id)
+    }
+
+    fn get_field_revs(&self, field_ids: Option<Vec<String>>) -> Fut<Vec<Arc<FieldRevision>>> {
+        self.editor_delegate.get_field_revs(field_ids)
+    }
+
+    fn get_blocks(&self) -> Fut<Vec<GridBlock>> {
+        self.editor_delegate.get_blocks()
+    }
+}

+ 5 - 2
frontend/rust-lib/flowy-grid/tests/grid/filter_test/checkbox_filter_test.rs

@@ -9,7 +9,10 @@ async fn grid_filter_checkbox_is_check_test() {
         CreateCheckboxFilter {
             condition: CheckboxFilterCondition::IsChecked,
         },
-        AssertNumberOfRows { expected: 2 },
+        AssertFilterChanged {
+            visible_row_len: 2,
+            hide_row_len: 3,
+        },
     ];
     test.run_scripts(scripts).await;
 }
@@ -21,7 +24,7 @@ async fn grid_filter_checkbox_is_uncheck_test() {
         CreateCheckboxFilter {
             condition: CheckboxFilterCondition::IsUnChecked,
         },
-        AssertNumberOfRows { expected: 3 },
+        AssertNumberOfVisibleRows { expected: 3 },
     ];
     test.run_scripts(scripts).await;
 }

+ 5 - 5
frontend/rust-lib/flowy-grid/tests/grid/filter_test/date_filter_test.rs

@@ -12,7 +12,7 @@ async fn grid_filter_date_is_test() {
             end: None,
             timestamp: Some(1647251762),
         },
-        AssertNumberOfRows { expected: 3 },
+        AssertNumberOfVisibleRows { expected: 3 },
     ];
     test.run_scripts(scripts).await;
 }
@@ -27,7 +27,7 @@ async fn grid_filter_date_after_test() {
             end: None,
             timestamp: Some(1647251762),
         },
-        AssertNumberOfRows { expected: 2 },
+        AssertNumberOfVisibleRows { expected: 2 },
     ];
     test.run_scripts(scripts).await;
 }
@@ -42,7 +42,7 @@ async fn grid_filter_date_on_or_after_test() {
             end: None,
             timestamp: Some(1668359085),
         },
-        AssertNumberOfRows { expected: 2 },
+        AssertNumberOfVisibleRows { expected: 2 },
     ];
     test.run_scripts(scripts).await;
 }
@@ -57,7 +57,7 @@ async fn grid_filter_date_on_or_before_test() {
             end: None,
             timestamp: Some(1668359085),
         },
-        AssertNumberOfRows { expected: 4 },
+        AssertNumberOfVisibleRows { expected: 4 },
     ];
     test.run_scripts(scripts).await;
 }
@@ -72,7 +72,7 @@ async fn grid_filter_date_within_test() {
             end: Some(1668704685),
             timestamp: None,
         },
-        AssertNumberOfRows { expected: 5 },
+        AssertNumberOfVisibleRows { expected: 5 },
     ];
     test.run_scripts(scripts).await;
 }

+ 6 - 6
frontend/rust-lib/flowy-grid/tests/grid/filter_test/number_filter_test.rs

@@ -10,7 +10,7 @@ async fn grid_filter_number_is_equal_test() {
             condition: NumberFilterCondition::Equal,
             content: "1".to_string(),
         },
-        AssertNumberOfRows { expected: 1 },
+        AssertNumberOfVisibleRows { expected: 1 },
     ];
     test.run_scripts(scripts).await;
 }
@@ -23,7 +23,7 @@ async fn grid_filter_number_is_less_than_test() {
             condition: NumberFilterCondition::LessThan,
             content: "3".to_string(),
         },
-        AssertNumberOfRows { expected: 2 },
+        AssertNumberOfVisibleRows { expected: 2 },
     ];
     test.run_scripts(scripts).await;
 }
@@ -37,7 +37,7 @@ async fn grid_filter_number_is_less_than_test2() {
             condition: NumberFilterCondition::LessThan,
             content: "$3".to_string(),
         },
-        AssertNumberOfRows { expected: 2 },
+        AssertNumberOfVisibleRows { expected: 2 },
     ];
     test.run_scripts(scripts).await;
 }
@@ -50,7 +50,7 @@ async fn grid_filter_number_is_less_than_or_equal_test() {
             condition: NumberFilterCondition::LessThanOrEqualTo,
             content: "3".to_string(),
         },
-        AssertNumberOfRows { expected: 3 },
+        AssertNumberOfVisibleRows { expected: 3 },
     ];
     test.run_scripts(scripts).await;
 }
@@ -63,7 +63,7 @@ async fn grid_filter_number_is_empty_test() {
             condition: NumberFilterCondition::NumberIsEmpty,
             content: "".to_string(),
         },
-        AssertNumberOfRows { expected: 1 },
+        AssertNumberOfVisibleRows { expected: 1 },
     ];
     test.run_scripts(scripts).await;
 }
@@ -76,7 +76,7 @@ async fn grid_filter_number_is_not_empty_test() {
             condition: NumberFilterCondition::NumberIsNotEmpty,
             content: "".to_string(),
         },
-        AssertNumberOfRows { expected: 4 },
+        AssertNumberOfVisibleRows { expected: 4 },
     ];
     test.run_scripts(scripts).await;
 }

+ 20 - 2
frontend/rust-lib/flowy-grid/tests/grid/filter_test/script.rs

@@ -3,6 +3,7 @@
 #![allow(dead_code)]
 #![allow(unused_imports)]
 
+use std::time::Duration;
 use bytes::Bytes;
 use futures::TryFutureExt;
 use flowy_grid::entities::{CreateFilterParams, CreateFilterPayloadPB, DeleteFilterParams, GridLayout, GridSettingChangesetParams, GridSettingPB, RowPB, TextFilterCondition, FieldType, NumberFilterCondition, CheckboxFilterCondition, DateFilterCondition, DateFilterContent, SelectOptionCondition, TextFilterPB, NumberFilterPB, CheckboxFilterPB, DateFilterPB, SelectOptionFilterPB};
@@ -10,6 +11,7 @@ use flowy_grid::services::field::SelectOptionIds;
 use flowy_grid::services::setting::GridSettingChangesetBuilder;
 use grid_rev_model::{FieldRevision, FieldTypeRevision};
 use flowy_grid::services::filter::FilterType;
+use flowy_grid::services::view_editor::GridViewChanged;
 use crate::grid::grid_editor::GridEditorTest;
 
 pub enum FilterScript {
@@ -53,13 +55,18 @@ pub enum FilterScript {
         condition: u32,
         content: String
     },
-    AssertNumberOfRows{
+    AssertNumberOfVisibleRows {
         expected: usize,
     },
+    AssertFilterChanged{
+        visible_row_len:usize,
+        hide_row_len: usize,
+    },
     #[allow(dead_code)]
     AssertGridSetting {
         expected_setting: GridSettingPB,
     },
+    Wait { millisecond: u64 }
 }
 
 pub struct GridFilterTest {
@@ -160,12 +167,23 @@ impl GridFilterTest {
                 let setting = self.editor.get_setting().await.unwrap();
                 assert_eq!(expected_setting, setting);
             }
-            FilterScript::AssertNumberOfRows { expected } => {
+            FilterScript::AssertFilterChanged { visible_row_len, hide_row_len} => {
+                let mut receiver = self.editor.subscribe_view_changed().await;
+                let changed = receiver.recv().await.unwrap();
+                match changed { GridViewChanged::DidReceiveFilterResult(changed) => {
+                    assert_eq!(changed.visible_rows.len(), visible_row_len);
+                    assert_eq!(changed.invisible_rows.len(), hide_row_len);
+                } }
+            }
+            FilterScript::AssertNumberOfVisibleRows { expected } => {
                 //
                 let grid = self.editor.get_grid().await.unwrap();
                 let rows = grid.blocks.into_iter().map(|block| block.rows).flatten().collect::<Vec<RowPB>>();
                 assert_eq!(rows.len(), expected);
             }
+            FilterScript::Wait { millisecond } => {
+                tokio::time::sleep(Duration::from_millis(millisecond)).await;
+            }
         }
     }
 

+ 6 - 6
frontend/rust-lib/flowy-grid/tests/grid/filter_test/select_option_filter_test.rs

@@ -10,7 +10,7 @@ async fn grid_filter_multi_select_is_empty_test() {
             condition: SelectOptionCondition::OptionIsEmpty,
             option_ids: vec![],
         },
-        AssertNumberOfRows { expected: 2 },
+        AssertNumberOfVisibleRows { expected: 2 },
     ];
     test.run_scripts(scripts).await;
 }
@@ -23,7 +23,7 @@ async fn grid_filter_multi_select_is_not_empty_test() {
             condition: SelectOptionCondition::OptionIsNotEmpty,
             option_ids: vec![],
         },
-        AssertNumberOfRows { expected: 3 },
+        AssertNumberOfVisibleRows { expected: 3 },
     ];
     test.run_scripts(scripts).await;
 }
@@ -37,7 +37,7 @@ async fn grid_filter_multi_select_is_test() {
             condition: SelectOptionCondition::OptionIs,
             option_ids: vec![options.remove(0).id, options.remove(0).id],
         },
-        AssertNumberOfRows { expected: 2 },
+        AssertNumberOfVisibleRows { expected: 2 },
     ];
     test.run_scripts(scripts).await;
 }
@@ -51,7 +51,7 @@ async fn grid_filter_multi_select_is_test2() {
             condition: SelectOptionCondition::OptionIs,
             option_ids: vec![options.remove(1).id],
         },
-        AssertNumberOfRows { expected: 1 },
+        AssertNumberOfVisibleRows { expected: 1 },
     ];
     test.run_scripts(scripts).await;
 }
@@ -64,7 +64,7 @@ async fn grid_filter_single_select_is_empty_test() {
             condition: SelectOptionCondition::OptionIsEmpty,
             option_ids: vec![],
         },
-        AssertNumberOfRows { expected: 2 },
+        AssertNumberOfVisibleRows { expected: 2 },
     ];
     test.run_scripts(scripts).await;
 }
@@ -78,7 +78,7 @@ async fn grid_filter_single_select_is_test() {
             condition: SelectOptionCondition::OptionIs,
             option_ids: vec![options.remove(0).id],
         },
-        AssertNumberOfRows { expected: 2 },
+        AssertNumberOfVisibleRows { expected: 2 },
     ];
     test.run_scripts(scripts).await;
 }

+ 36 - 7
frontend/rust-lib/flowy-grid/tests/grid/filter_test/text_filter_test.rs

@@ -12,7 +12,27 @@ async fn grid_filter_text_is_empty_test() {
             content: "".to_string(),
         },
         AssertFilterCount { count: 1 },
-        AssertNumberOfRows { expected: 0 },
+        AssertFilterChanged {
+            visible_row_len: 1,
+            hide_row_len: 4,
+        },
+    ];
+    test.run_scripts(scripts).await;
+}
+
+#[tokio::test]
+async fn grid_filter_text_is_not_empty_test() {
+    let mut test = GridFilterTest::new().await;
+    let scripts = vec![
+        CreateTextFilter {
+            condition: TextFilterCondition::TextIsNotEmpty,
+            content: "".to_string(),
+        },
+        AssertFilterCount { count: 1 },
+        AssertFilterChanged {
+            visible_row_len: 4,
+            hide_row_len: 1,
+        },
     ];
     test.run_scripts(scripts).await;
 }
@@ -25,7 +45,10 @@ async fn grid_filter_is_text_test() {
             condition: TextFilterCondition::Is,
             content: "A".to_string(),
         },
-        AssertNumberOfRows { expected: 1 },
+        AssertFilterChanged {
+            visible_row_len: 1,
+            hide_row_len: 4,
+        },
     ];
     test.run_scripts(scripts).await;
 }
@@ -38,7 +61,10 @@ async fn grid_filter_contain_text_test() {
             condition: TextFilterCondition::Contains,
             content: "A".to_string(),
         },
-        AssertNumberOfRows { expected: 3 },
+        AssertFilterChanged {
+            visible_row_len: 3,
+            hide_row_len: 2,
+        },
     ];
     test.run_scripts(scripts).await;
 }
@@ -51,7 +77,10 @@ async fn grid_filter_start_with_text_test() {
             condition: TextFilterCondition::StartsWith,
             content: "A".to_string(),
         },
-        AssertNumberOfRows { expected: 2 },
+        AssertFilterChanged {
+            visible_row_len: 2,
+            hide_row_len: 3,
+        },
     ];
     test.run_scripts(scripts).await;
 }
@@ -64,7 +93,7 @@ async fn grid_filter_ends_with_text_test() {
             condition: TextFilterCondition::EndsWith,
             content: "A".to_string(),
         },
-        AssertNumberOfRows { expected: 2 },
+        AssertNumberOfVisibleRows { expected: 2 },
     ];
     test.run_scripts(scripts).await;
 }
@@ -81,7 +110,7 @@ async fn grid_filter_delete_test() {
     let scripts = vec![
         InsertFilter { payload },
         AssertFilterCount { count: 1 },
-        AssertNumberOfRows { expected: 0 },
+        AssertNumberOfVisibleRows { expected: 1 },
     ];
     test.run_scripts(scripts).await;
 
@@ -92,7 +121,7 @@ async fn grid_filter_delete_test() {
             filter_type: FilterType::from(&field_rev),
         },
         AssertFilterCount { count: 0 },
-        AssertNumberOfRows { expected: 5 },
+        AssertNumberOfVisibleRows { expected: 5 },
     ])
     .await;
 }

+ 1 - 1
frontend/rust-lib/flowy-grid/tests/grid/grid_editor.rs

@@ -220,7 +220,7 @@ fn make_test_grid() -> BuildGridContext {
             1 => {
                 for field_type in FieldType::iter() {
                     match field_type {
-                        FieldType::RichText => row_builder.insert_text_cell("B"),
+                        FieldType::RichText => row_builder.insert_text_cell(""),
                         FieldType::Number => row_builder.insert_number_cell("2"),
                         FieldType::DateTime => row_builder.insert_date_cell("1647251762"),
                         FieldType::MultiSelect => row_builder

+ 1 - 1
frontend/scripts/makefile/tests.toml

@@ -4,7 +4,7 @@ dependencies = ["build-test-lib"]
 description = "Run flutter unit tests"
 script = '''
 cd app_flowy
-flutter test --dart-define=RUST_LOG=${TEST_RUST_LOG}
+flutter test --dart-define=RUST_LOG=${TEST_RUST_LOG} --concurrency=1
 '''
 
 [tasks.rust_unit_test]