Browse Source

Merge pull request #977 from AppFlowy-IO/feat/support_switch_field

Feat/support grouping by different field
Nathan.fooo 2 years ago
parent
commit
60c8ec13eb
99 changed files with 1974 additions and 1241 deletions
  1. 7 0
      frontend/app_flowy/assets/images/grid/setting/group.svg
  2. 2 1
      frontend/app_flowy/assets/translations/en.json
  3. 36 19
      frontend/app_flowy/lib/plugins/board/application/board_bloc.dart
  4. 44 49
      frontend/app_flowy/lib/plugins/board/application/board_data_controller.dart
  5. 24 8
      frontend/app_flowy/lib/plugins/board/application/board_listener.dart
  6. 3 7
      frontend/app_flowy/lib/plugins/board/application/card/board_date_cell_bloc.dart
  7. 5 5
      frontend/app_flowy/lib/plugins/board/application/card/card_bloc.dart
  8. 5 5
      frontend/app_flowy/lib/plugins/board/application/card/card_data_controller.dart
  9. 1 0
      frontend/app_flowy/lib/plugins/board/application/toolbar/board_setting_bloc.dart
  10. 11 11
      frontend/app_flowy/lib/plugins/board/presentation/board_page.dart
  11. 16 5
      frontend/app_flowy/lib/plugins/board/presentation/toolbar/board_setting.dart
  12. 3 3
      frontend/app_flowy/lib/plugins/board/presentation/toolbar/board_toolbar.dart
  13. 3 3
      frontend/app_flowy/lib/plugins/grid/application/block/block_cache.dart
  14. 13 12
      frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/cell_controller.dart
  15. 6 6
      frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/cell_service.dart
  16. 1 1
      frontend/app_flowy/lib/plugins/grid/application/cell/date_cal_bloc.dart
  17. 3 7
      frontend/app_flowy/lib/plugins/grid/application/cell/date_cell_bloc.dart
  18. 1 1
      frontend/app_flowy/lib/plugins/grid/application/cell/select_option_service.dart
  19. 0 192
      frontend/app_flowy/lib/plugins/grid/application/field/field_cache.dart
  20. 281 0
      frontend/app_flowy/lib/plugins/grid/application/field/field_controller.dart
  21. 1 0
      frontend/app_flowy/lib/plugins/grid/application/field/field_service.dart
  22. 4 3
      frontend/app_flowy/lib/plugins/grid/application/field/type_option/type_option_data_controller.dart
  23. 6 4
      frontend/app_flowy/lib/plugins/grid/application/grid_bloc.dart
  24. 8 23
      frontend/app_flowy/lib/plugins/grid/application/grid_data_controller.dart
  25. 10 11
      frontend/app_flowy/lib/plugins/grid/application/grid_header_bloc.dart
  26. 11 9
      frontend/app_flowy/lib/plugins/grid/application/row/row_bloc.dart
  27. 4 3
      frontend/app_flowy/lib/plugins/grid/application/row/row_cache.dart
  28. 5 5
      frontend/app_flowy/lib/plugins/grid/application/row/row_data_controller.dart
  29. 84 0
      frontend/app_flowy/lib/plugins/grid/application/setting/group_bloc.dart
  30. 19 15
      frontend/app_flowy/lib/plugins/grid/application/setting/property_bloc.dart
  31. 59 0
      frontend/app_flowy/lib/plugins/grid/application/setting/setting_controller.dart
  32. 47 0
      frontend/app_flowy/lib/plugins/grid/application/setting/setting_listener.dart
  33. 32 0
      frontend/app_flowy/lib/plugins/grid/application/setting/setting_service.dart
  34. 13 10
      frontend/app_flowy/lib/plugins/grid/presentation/grid_page.dart
  35. 2 2
      frontend/app_flowy/lib/plugins/grid/presentation/layout/layout.dart
  36. 5 5
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/grid_header.dart
  37. 6 4
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/builder.dart
  38. 2 2
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/grid_row.dart
  39. 3 3
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/row_detail.dart
  40. 110 0
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/grid_group.dart
  41. 19 15
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/grid_property.dart
  42. 4 4
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/grid_setting.dart
  43. 4 4
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/grid_toolbar.dart
  44. 6 6
      frontend/app_flowy/lib/startup/deps_resolver.dart
  45. 6 0
      frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_data.dart
  46. 2 0
      frontend/rust-lib/flowy-grid/src/dart_notification.rs
  47. 14 14
      frontend/rust-lib/flowy-grid/src/entities/filter_entities/util.rs
  48. 9 5
      frontend/rust-lib/flowy-grid/src/entities/group_entities/group.rs
  49. 8 2
      frontend/rust-lib/flowy-grid/src/entities/group_entities/group_changeset.rs
  50. 10 11
      frontend/rust-lib/flowy-grid/src/entities/setting_entities.rs
  51. 33 5
      frontend/rust-lib/flowy-grid/src/event_handler.rs
  52. 6 3
      frontend/rust-lib/flowy-grid/src/event_map.rs
  53. 3 3
      frontend/rust-lib/flowy-grid/src/macros.rs
  54. 7 7
      frontend/rust-lib/flowy-grid/src/services/cell/cell_operation.rs
  55. 3 3
      frontend/rust-lib/flowy-grid/src/services/field/field_builder.rs
  56. 45 0
      frontend/rust-lib/flowy-grid/src/services/field/field_operation.rs
  57. 2 0
      frontend/rust-lib/flowy-grid/src/services/field/mod.rs
  58. 2 2
      frontend/rust-lib/flowy-grid/src/services/field/type_options/checkbox_type_option/checkbox_type_option.rs
  59. 2 2
      frontend/rust-lib/flowy-grid/src/services/field/type_options/date_type_option/date_type_option.rs
  60. 2 2
      frontend/rust-lib/flowy-grid/src/services/field/type_options/number_type_option/number_type_option.rs
  61. 2 2
      frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/multi_select_type_option.rs
  62. 2 2
      frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/select_option.rs
  63. 2 2
      frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/single_select_type_option.rs
  64. 2 2
      frontend/rust-lib/flowy-grid/src/services/field/type_options/text_type_option/text_type_option.rs
  65. 2 2
      frontend/rust-lib/flowy-grid/src/services/field/type_options/url_type_option/url_type_option.rs
  66. 7 7
      frontend/rust-lib/flowy-grid/src/services/filter/filter_service.rs
  67. 24 5
      frontend/rust-lib/flowy-grid/src/services/grid_editor.rs
  68. 225 106
      frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs
  69. 23 7
      frontend/rust-lib/flowy-grid/src/services/grid_view_manager.rs
  70. 129 93
      frontend/rust-lib/flowy-grid/src/services/group/configuration.rs
  71. 33 32
      frontend/rust-lib/flowy-grid/src/services/group/controller.rs
  72. 17 20
      frontend/rust-lib/flowy-grid/src/services/group/controller_impls/checkbox_controller.rs
  73. 80 0
      frontend/rust-lib/flowy-grid/src/services/group/controller_impls/default_controller.rs
  74. 2 0
      frontend/rust-lib/flowy-grid/src/services/group/controller_impls/mod.rs
  75. 12 22
      frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/multi_select_controller.rs
  76. 11 21
      frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/single_select_controller.rs
  77. 23 7
      frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/util.rs
  78. 5 3
      frontend/rust-lib/flowy-grid/src/services/group/entities.rs
  79. 0 293
      frontend/rust-lib/flowy-grid/src/services/group/group_service.rs
  80. 114 0
      frontend/rust-lib/flowy-grid/src/services/group/group_util.rs
  81. 3 2
      frontend/rust-lib/flowy-grid/src/services/group/mod.rs
  82. 2 2
      frontend/rust-lib/flowy-grid/src/services/setting/setting_builder.rs
  83. 1 1
      frontend/rust-lib/flowy-grid/tests/grid/block_test/script.rs
  84. 2 2
      frontend/rust-lib/flowy-grid/tests/grid/field_test/test.rs
  85. 2 2
      frontend/rust-lib/flowy-grid/tests/grid/field_test/util.rs
  86. 4 4
      frontend/rust-lib/flowy-grid/tests/grid/filter_test/script.rs
  87. 5 5
      frontend/rust-lib/flowy-grid/tests/grid/filter_test/text_filter_test.rs
  88. 8 1
      frontend/rust-lib/flowy-grid/tests/grid/grid_editor.rs
  89. 1 1
      frontend/rust-lib/flowy-grid/tests/grid/grid_test.rs
  90. 55 10
      frontend/rust-lib/flowy-grid/tests/grid/group_test/script.rs
  91. 32 13
      frontend/rust-lib/flowy-grid/tests/grid/group_test/test.rs
  92. 1 1
      frontend/rust-lib/flowy-grid/tests/grid/script.rs
  93. 5 5
      shared-lib/flowy-grid-data-model/src/revision/grid_rev.rs
  94. 9 1
      shared-lib/flowy-grid-data-model/src/revision/grid_setting_rev.rs
  95. 20 22
      shared-lib/flowy-grid-data-model/src/revision/group_rev.rs
  96. 9 5
      shared-lib/flowy-sync/src/client_grid/view_revision_pad.rs
  97. 1 1
      shared-lib/lib-ot/src/core/document/document_operation.rs
  98. 1 0
      shared-lib/lib-ot/src/core/document/mod.rs
  99. 15 15
      shared-lib/lib-ot/tests/main.rs

+ 7 - 0
frontend/app_flowy/assets/images/grid/setting/group.svg

@@ -0,0 +1,7 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M10 2H13C13.5523 2 14 2.44772 14 3V6" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M6 2H3C2.44772 2 2 2.44772 2 3V6" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M6 14H3C2.44772 14 2 13.5523 2 13V10" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M10 14H13C13.5523 14 14 13.5523 14 13V10" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
+<rect x="6" y="6" width="4" height="4" rx="1" stroke="#333333"/>
+</svg>

+ 2 - 1
frontend/app_flowy/assets/translations/en.json

@@ -160,7 +160,8 @@
     "settings": {
       "filter": "Filter",
       "sortBy": "Sort by",
-      "Properties": "Properties"
+      "Properties": "Properties",
+      "group": "Group"
     },
     "field": {
       "hide": "Hide",

+ 36 - 19
frontend/app_flowy/lib/plugins/board/application/board_bloc.dart

@@ -1,6 +1,6 @@
 import 'dart:async';
 import 'package:app_flowy/plugins/grid/application/block/block_cache.dart';
-import 'package:app_flowy/plugins/grid/application/field/field_cache.dart';
+import 'package:app_flowy/plugins/grid/application/field/field_controller.dart';
 import 'package:app_flowy/plugins/grid/application/row/row_cache.dart';
 import 'package:app_flowy/plugins/grid/application/row/row_service.dart';
 import 'package:appflowy_board/appflowy_board.dart';
@@ -25,7 +25,8 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
   final MoveRowFFIService _rowService;
   LinkedHashMap<String, GroupController> groupControllers = LinkedHashMap();
 
-  GridFieldCache get fieldCache => _gridDataController.fieldCache;
+  GridFieldController get fieldController =>
+      _gridDataController.fieldController;
   String get gridId => _gridDataController.gridId;
 
   BoardBloc({required ViewPB view})
@@ -110,9 +111,11 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
             emit(state.copyWith(noneOrError: some(error)));
           },
           didReceiveGroups: (List<GroupPB> groups) {
-            emit(state.copyWith(
-              groupIds: groups.map((group) => group.groupId).toList(),
-            ));
+            emit(
+              state.copyWith(
+                groupIds: groups.map((group) => group.groupId).toList(),
+              ),
+            );
           },
         );
       },
@@ -154,6 +157,23 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
   }
 
   void initializeGroups(List<GroupPB> groups) {
+    for (var controller in groupControllers.values) {
+      controller.dispose();
+    }
+    groupControllers.clear();
+    boardController.clear();
+
+    //
+    List<AFBoardColumnData> columns = groups.map((group) {
+      return AFBoardColumnData(
+        id: group.groupId,
+        name: group.desc,
+        items: _buildRows(group),
+        customData: group,
+      );
+    }).toList();
+    boardController.addColumns(columns);
+
     for (final group in groups) {
       final delegate = GroupControllerDelegateImpl(
         controller: boardController,
@@ -184,38 +204,35 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
         }
       },
       didLoadGroups: (groups) {
-        List<AFBoardColumnData> columns = groups.map((group) {
-          return AFBoardColumnData(
-            id: group.groupId,
-            name: group.desc,
-            items: _buildRows(group),
-            customData: group,
-          );
-        }).toList();
-
-        boardController.addColumns(columns);
+        if (isClosed) return;
         initializeGroups(groups);
         add(BoardEvent.didReceiveGroups(groups));
       },
       onDeletedGroup: (groupIds) {
+        if (isClosed) return;
         //
       },
       onInsertedGroup: (insertedGroups) {
+        if (isClosed) return;
         //
       },
       onUpdatedGroup: (updatedGroups) {
-        //
+        if (isClosed) return;
         for (final group in updatedGroups) {
           final columnController =
               boardController.getColumnController(group.groupId);
-          if (columnController != null) {
-            columnController.updateColumnName(group.desc);
-          }
+          columnController?.updateColumnName(group.desc);
         }
       },
       onError: (err) {
         Log.error(err);
       },
+      onResetGroups: (groups) {
+        if (isClosed) return;
+
+        initializeGroups(groups);
+        add(BoardEvent.didReceiveGroups(groups));
+      },
     );
   }
 

+ 44 - 49
frontend/app_flowy/lib/plugins/board/application/board_data_controller.dart

@@ -1,7 +1,7 @@
 import 'dart:collection';
 
 import 'package:app_flowy/plugins/grid/application/block/block_cache.dart';
-import 'package:app_flowy/plugins/grid/application/field/field_cache.dart';
+import 'package:app_flowy/plugins/grid/application/field/field_controller.dart';
 import 'package:app_flowy/plugins/grid/application/grid_service.dart';
 import 'package:app_flowy/plugins/grid/application/row/row_cache.dart';
 import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
@@ -12,12 +12,13 @@ import 'package:flowy_sdk/protobuf/flowy-grid/protobuf.dart';
 
 import 'board_listener.dart';
 
-typedef OnFieldsChanged = void Function(UnmodifiableListView<FieldPB>);
+typedef OnFieldsChanged = void Function(UnmodifiableListView<GridFieldContext>);
 typedef OnGridChanged = void Function(GridPB);
 typedef DidLoadGroups = void Function(List<GroupPB>);
 typedef OnUpdatedGroup = void Function(List<GroupPB>);
 typedef OnDeletedGroup = void Function(List<String>);
 typedef OnInsertedGroup = void Function(List<InsertedGroupPB>);
+typedef OnResetGroups = void Function(List<GroupPB>);
 
 typedef OnRowsChanged = void Function(
   List<RowInfo>,
@@ -28,7 +29,7 @@ typedef OnError = void Function(FlowyError);
 class BoardDataController {
   final String gridId;
   final GridFFIService _gridFFIService;
-  final GridFieldCache fieldCache;
+  final GridFieldController fieldController;
   final BoardListener _listener;
 
   // key: the block id
@@ -55,7 +56,7 @@ class BoardDataController {
         // ignore: prefer_collection_literals
         _blocks = LinkedHashMap(),
         _gridFFIService = GridFFIService(gridId: view.id),
-        fieldCache = GridFieldCache(gridId: view.id);
+        fieldController = GridFieldController(gridId: view.id);
 
   void addListener({
     required OnGridChanged onGridChanged,
@@ -65,6 +66,7 @@ class BoardDataController {
     required OnUpdatedGroup onUpdatedGroup,
     required OnDeletedGroup onDeletedGroup,
     required OnInsertedGroup onInsertedGroup,
+    required OnResetGroups onResetGroups,
     required OnError? onError,
   }) {
     _onGridChanged = onGridChanged;
@@ -73,28 +75,36 @@ class BoardDataController {
     _onRowsChanged = onRowsChanged;
     _onError = onError;
 
-    fieldCache.addListener(onFields: (fields) {
+    fieldController.addListener(onFields: (fields) {
       _onFieldsChanged?.call(UnmodifiableListView(fields));
     });
 
-    _listener.start(onBoardChanged: (result) {
-      result.fold(
-        (changeset) {
-          if (changeset.updateGroups.isNotEmpty) {
-            onUpdatedGroup.call(changeset.updateGroups);
-          }
-
-          if (changeset.insertedGroups.isNotEmpty) {
-            onInsertedGroup.call(changeset.insertedGroups);
-          }
-
-          if (changeset.deletedGroups.isNotEmpty) {
-            onDeletedGroup.call(changeset.deletedGroups);
-          }
-        },
-        (e) => _onError?.call(e),
-      );
-    });
+    _listener.start(
+      onBoardChanged: (result) {
+        result.fold(
+          (changeset) {
+            if (changeset.updateGroups.isNotEmpty) {
+              onUpdatedGroup.call(changeset.updateGroups);
+            }
+
+            if (changeset.insertedGroups.isNotEmpty) {
+              onInsertedGroup.call(changeset.insertedGroups);
+            }
+
+            if (changeset.deletedGroups.isNotEmpty) {
+              onDeletedGroup.call(changeset.deletedGroups);
+            }
+          },
+          (e) => _onError?.call(e),
+        );
+      },
+      onGroupByNewField: (result) {
+        result.fold(
+          (groups) => onResetGroups(groups),
+          (e) => _onError?.call(e),
+        );
+      },
+    );
   }
 
   Future<Either<Unit, FlowyError>> loadData() async {
@@ -103,16 +113,15 @@ class BoardDataController {
       () => result.fold(
         (grid) async {
           _onGridChanged?.call(grid);
-
-          return await _loadFields(grid).then((result) {
-            return result.fold(
-              (l) {
-                _loadGroups(grid.blocks);
-                return left(l);
-              },
-              (err) => right(err),
-            );
-          });
+          return await fieldController.loadFields(fieldIds: grid.fields).then(
+                (result) => result.fold(
+                  (l) {
+                    _loadGroups(grid.blocks);
+                    return left(l);
+                  },
+                  (err) => right(err),
+                ),
+              );
         },
         (err) => right(err),
       ),
@@ -126,33 +135,19 @@ class BoardDataController {
 
   Future<void> dispose() async {
     await _gridFFIService.closeGrid();
-    await fieldCache.dispose();
+    await fieldController.dispose();
 
     for (final blockCache in _blocks.values) {
       blockCache.dispose();
     }
   }
 
-  Future<Either<Unit, FlowyError>> _loadFields(GridPB grid) async {
-    final result = await _gridFFIService.getFields(fieldIds: grid.fields);
-    return Future(
-      () => result.fold(
-        (fields) {
-          fieldCache.fields = fields.items;
-          _onFieldsChanged?.call(UnmodifiableListView(fieldCache.fields));
-          return left(unit);
-        },
-        (err) => right(err),
-      ),
-    );
-  }
-
   Future<void> _loadGroups(List<BlockPB> blocks) async {
     for (final block in blocks) {
       final cache = GridBlockCache(
         gridId: gridId,
         block: block,
-        fieldCache: fieldCache,
+        fieldController: fieldController,
       );
 
       cache.addListener(onRowsChanged: (reason) {

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

@@ -5,20 +5,26 @@ import 'package:flowy_infra/notifier.dart';
 import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid/dart_notification.pb.dart';
 import 'package:dartz/dartz.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/group.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid/group_changeset.pb.dart';
 
-typedef UpdateBoardNotifiedValue = Either<GroupViewChangesetPB, FlowyError>;
+typedef GroupUpdateValue = Either<GroupViewChangesetPB, FlowyError>;
+typedef GroupByNewFieldValue = Either<List<GroupPB>, FlowyError>;
 
 class BoardListener {
   final String viewId;
-  PublishNotifier<UpdateBoardNotifiedValue>? _groupNotifier = PublishNotifier();
+  PublishNotifier<GroupUpdateValue>? _groupUpdateNotifier = PublishNotifier();
+  PublishNotifier<GroupByNewFieldValue>? _groupByNewFieldNotifier =
+      PublishNotifier();
   GridNotificationListener? _listener;
   BoardListener(this.viewId);
 
   void start({
-    required void Function(UpdateBoardNotifiedValue) onBoardChanged,
+    required void Function(GroupUpdateValue) onBoardChanged,
+    required void Function(GroupByNewFieldValue) onGroupByNewField,
   }) {
-    _groupNotifier?.addPublishListener(onBoardChanged);
+    _groupUpdateNotifier?.addPublishListener(onBoardChanged);
+    _groupByNewFieldNotifier?.addPublishListener(onGroupByNewField);
     _listener = GridNotificationListener(
       objectId: viewId,
       handler: _handler,
@@ -32,9 +38,16 @@ class BoardListener {
     switch (ty) {
       case GridNotification.DidUpdateGroupView:
         result.fold(
-          (payload) => _groupNotifier?.value =
+          (payload) => _groupUpdateNotifier?.value =
               left(GroupViewChangesetPB.fromBuffer(payload)),
-          (error) => _groupNotifier?.value = right(error),
+          (error) => _groupUpdateNotifier?.value = right(error),
+        );
+        break;
+      case GridNotification.DidGroupByNewField:
+        result.fold(
+          (payload) => _groupByNewFieldNotifier?.value =
+              left(GroupViewChangesetPB.fromBuffer(payload).newGroups),
+          (error) => _groupByNewFieldNotifier?.value = right(error),
         );
         break;
       default:
@@ -44,7 +57,10 @@ class BoardListener {
 
   Future<void> stop() async {
     await _listener?.stop();
-    _groupNotifier?.dispose();
-    _groupNotifier = null;
+    _groupUpdateNotifier?.dispose();
+    _groupUpdateNotifier = null;
+
+    _groupByNewFieldNotifier?.dispose();
+    _groupByNewFieldNotifier = null;
   }
 }

+ 3 - 7
frontend/app_flowy/lib/plugins/board/application/card/board_date_cell_bloc.dart

@@ -1,6 +1,6 @@
 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:flowy_sdk/protobuf/flowy-grid/date_type_option_entities.pb.dart';
-import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:freezed_annotation/freezed_annotation.dart';
 import 'dart:async';
@@ -20,8 +20,6 @@ class BoardDateCellBloc extends Bloc<BoardDateCellEvent, BoardDateCellState> {
             emit(state.copyWith(
                 data: cellData, dateStr: _dateStrFromCellData(cellData)));
           },
-          didReceiveFieldUpdate: (FieldPB value) =>
-              emit(state.copyWith(field: value)),
         );
       },
     );
@@ -53,8 +51,6 @@ class BoardDateCellEvent with _$BoardDateCellEvent {
   const factory BoardDateCellEvent.initial() = _InitialCell;
   const factory BoardDateCellEvent.didReceiveCellUpdate(DateCellDataPB? data) =
       _DidReceiveCellUpdate;
-  const factory BoardDateCellEvent.didReceiveFieldUpdate(FieldPB field) =
-      _DidReceiveFieldUpdate;
 }
 
 @freezed
@@ -62,14 +58,14 @@ class BoardDateCellState with _$BoardDateCellState {
   const factory BoardDateCellState({
     required DateCellDataPB? data,
     required String dateStr,
-    required FieldPB field,
+    required GridFieldContext fieldContext,
   }) = _BoardDateCellState;
 
   factory BoardDateCellState.initial(GridDateCellController context) {
     final cellData = context.getCellData();
 
     return BoardDateCellState(
-      field: context.field,
+      fieldContext: context.fieldContext,
       data: cellData,
       dateStr: _dateStrFromCellData(cellData),
     );

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

@@ -59,7 +59,7 @@ class BoardCardBloc extends Bloc<BoardCardEvent, BoardCardState> {
     return RowInfo(
       gridId: _rowService.gridId,
       fields: UnmodifiableListView(
-        state.cells.map((cell) => cell.identifier.field).toList(),
+        state.cells.map((cell) => cell.identifier.fieldContext).toList(),
       ),
       rowPB: state.rowPB,
     );
@@ -120,9 +120,9 @@ class BoardCellEquatable extends Equatable {
 
   @override
   List<Object?> get props => [
-        identifier.field.id,
-        identifier.field.fieldType,
-        identifier.field.visibility,
-        identifier.field.width,
+        identifier.fieldContext.id,
+        identifier.fieldContext.fieldType,
+        identifier.fieldContext.visibility,
+        identifier.fieldContext.width,
       ];
 }

+ 5 - 5
frontend/app_flowy/lib/plugins/board/application/card/card_data_controller.dart

@@ -1,7 +1,7 @@
 import 'package:app_flowy/plugins/board/presentation/card/card_cell_builder.dart';
 import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
 import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_field_notifier.dart';
-import 'package:app_flowy/plugins/grid/application/field/field_cache.dart';
+import 'package:app_flowy/plugins/grid/application/field/field_controller.dart';
 import 'package:app_flowy/plugins/grid/application/row/row_cache.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid/block_entities.pb.dart';
 import 'package:flutter/foundation.dart';
@@ -10,15 +10,15 @@ typedef OnCardChanged = void Function(GridCellMap, RowsChangedReason);
 
 class CardDataController extends BoardCellBuilderDelegate {
   final RowPB rowPB;
-  final GridFieldCache _fieldCache;
+  final GridFieldController _fieldController;
   final GridRowCache _rowCache;
   final List<VoidCallback> _onCardChangedListeners = [];
 
   CardDataController({
     required this.rowPB,
-    required GridFieldCache fieldCache,
+    required GridFieldController fieldController,
     required GridRowCache rowCache,
-  })  : _fieldCache = fieldCache,
+  })  : _fieldController = fieldController,
         _rowCache = rowCache;
 
   GridCellMap loadData() {
@@ -41,7 +41,7 @@ class CardDataController extends BoardCellBuilderDelegate {
   @override
   GridCellFieldNotifier buildFieldNotifier() {
     return GridCellFieldNotifier(
-        notifier: GridCellFieldNotifierImpl(_fieldCache));
+        notifier: GridCellFieldNotifierImpl(_fieldController));
   }
 
   @override

+ 1 - 0
frontend/app_flowy/lib/plugins/board/application/toolbar/board_setting_bloc.dart

@@ -43,4 +43,5 @@ class BoardSettingState with _$BoardSettingState {
 
 enum BoardSettingAction {
   properties,
+  groups,
 }

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

@@ -5,7 +5,7 @@ import 'dart:collection';
 import 'package:app_flowy/generated/locale_keys.g.dart';
 import 'package:app_flowy/plugins/board/application/card/card_data_controller.dart';
 import 'package:app_flowy/plugins/grid/application/row/row_cache.dart';
-import 'package:app_flowy/plugins/grid/application/field/field_cache.dart';
+import 'package:app_flowy/plugins/grid/application/field/field_controller.dart';
 import 'package:app_flowy/plugins/grid/application/row/row_data_controller.dart';
 import 'package:app_flowy/plugins/grid/presentation/widgets/cell/cell_builder.dart';
 import 'package:app_flowy/plugins/grid/presentation/widgets/row/row_detail.dart';
@@ -82,8 +82,7 @@ class _BoardContentState extends State<BoardContent> {
     return BlocListener<BoardBloc, BoardState>(
       listener: (context, state) => _handleEditState(state, context),
       child: BlocBuilder<BoardBloc, BoardState>(
-        buildWhen: (previous, current) =>
-            previous.groupIds.length != current.groupIds.length,
+        buildWhen: (previous, current) => previous.groupIds != current.groupIds,
         builder: (context, state) {
           final theme = context.read<AppTheme>();
           return Container(
@@ -95,6 +94,7 @@ class _BoardContentState extends State<BoardContent> {
                   const _ToolbarBlocAdaptor(),
                   Expanded(
                     child: AFBoard(
+                      key: UniqueKey(),
                       scrollManager: scrollManager,
                       scrollController: scrollController,
                       dataController: context.read<BoardBloc>().boardController,
@@ -222,10 +222,10 @@ class _BoardContentState extends State<BoardContent> {
     /// Return placeholder widget if the rowCache is null.
     if (rowCache == null) return SizedBox(key: ObjectKey(columnItem));
 
-    final fieldCache = context.read<BoardBloc>().fieldCache;
+    final fieldController = context.read<BoardBloc>().fieldController;
     final gridId = context.read<BoardBloc>().gridId;
     final cardController = CardDataController(
-      fieldCache: fieldCache,
+      fieldController: fieldController,
       rowCache: rowCache,
       rowPB: rowPB,
     );
@@ -252,7 +252,7 @@ class _BoardContentState extends State<BoardContent> {
         dataController: cardController,
         openCard: (context) => _openCard(
           gridId,
-          fieldCache,
+          fieldController,
           rowPB,
           rowCache,
           context,
@@ -271,17 +271,17 @@ class _BoardContentState extends State<BoardContent> {
     );
   }
 
-  void _openCard(String gridId, GridFieldCache fieldCache, RowPB rowPB,
-      GridRowCache rowCache, BuildContext context) {
+  void _openCard(String gridId, GridFieldController fieldController,
+      RowPB rowPB, GridRowCache rowCache, BuildContext context) {
     final rowInfo = RowInfo(
       gridId: gridId,
-      fields: UnmodifiableListView(fieldCache.fields),
+      fields: UnmodifiableListView(fieldController.fieldContexts),
       rowPB: rowPB,
     );
 
     final dataController = GridRowDataController(
       rowInfo: rowInfo,
-      fieldCache: fieldCache,
+      fieldController: fieldController,
       rowCache: rowCache,
     );
 
@@ -302,7 +302,7 @@ class _ToolbarBlocAdaptor extends StatelessWidget {
         final bloc = context.read<BoardBloc>();
         final toolbarContext = BoardToolbarContext(
           viewId: bloc.gridId,
-          fieldCache: bloc.fieldCache,
+          fieldController: bloc.fieldController,
         );
 
         return BoardToolbar(toolbarContext: toolbarContext);

+ 16 - 5
frontend/app_flowy/lib/plugins/board/presentation/toolbar/board_setting.dart

@@ -1,7 +1,8 @@
 import 'package:app_flowy/generated/locale_keys.g.dart';
 import 'package:app_flowy/plugins/board/application/toolbar/board_setting_bloc.dart';
-import 'package:app_flowy/plugins/grid/application/field/field_cache.dart';
+import 'package:app_flowy/plugins/grid/application/field/field_controller.dart';
 import 'package:app_flowy/plugins/grid/presentation/layout/sizes.dart';
+import 'package:app_flowy/plugins/grid/presentation/widgets/toolbar/grid_group.dart';
 import 'package:app_flowy/plugins/grid/presentation/widgets/toolbar/grid_property.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flowy_infra/image.dart';
@@ -18,16 +19,16 @@ import 'board_toolbar.dart';
 
 class BoardSettingContext {
   final String viewId;
-  final GridFieldCache fieldCache;
+  final GridFieldController fieldController;
   BoardSettingContext({
     required this.viewId,
-    required this.fieldCache,
+    required this.fieldController,
   });
 
   factory BoardSettingContext.from(BoardToolbarContext toolbarContext) =>
       BoardSettingContext(
         viewId: toolbarContext.viewId,
-        fieldCache: toolbarContext.fieldCache,
+        fieldController: toolbarContext.fieldController,
       );
 }
 
@@ -92,7 +93,13 @@ class BoardSettingList extends StatelessWidget {
           case BoardSettingAction.properties:
             GridPropertyList(
                     gridId: settingContext.viewId,
-                    fieldCache: settingContext.fieldCache)
+                    fieldController: settingContext.fieldController)
+                .show(context);
+            break;
+          case BoardSettingAction.groups:
+            GridGroupList(
+                    viewId: settingContext.viewId,
+                    fieldController: settingContext.fieldController)
                 .show(context);
             break;
         }
@@ -156,6 +163,8 @@ extension _GridSettingExtension on BoardSettingAction {
     switch (this) {
       case BoardSettingAction.properties:
         return 'grid/setting/properties';
+      case BoardSettingAction.groups:
+        return 'grid/setting/group';
     }
   }
 
@@ -163,6 +172,8 @@ extension _GridSettingExtension on BoardSettingAction {
     switch (this) {
       case BoardSettingAction.properties:
         return LocaleKeys.grid_settings_Properties.tr();
+      case BoardSettingAction.groups:
+        return LocaleKeys.grid_settings_group.tr();
     }
   }
 }

+ 3 - 3
frontend/app_flowy/lib/plugins/board/presentation/toolbar/board_toolbar.dart

@@ -1,4 +1,4 @@
-import 'package:app_flowy/plugins/grid/application/field/field_cache.dart';
+import 'package:app_flowy/plugins/grid/application/field/field_controller.dart';
 import 'package:flowy_infra/image.dart';
 import 'package:flowy_infra/theme.dart';
 import 'package:flowy_infra_ui/style_widget/icon_button.dart';
@@ -9,11 +9,11 @@ import 'board_setting.dart';
 
 class BoardToolbarContext {
   final String viewId;
-  final GridFieldCache fieldCache;
+  final GridFieldController fieldController;
 
   BoardToolbarContext({
     required this.viewId,
-    required this.fieldCache,
+    required this.fieldController,
   });
 }
 

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

@@ -2,7 +2,7 @@ import 'dart:async';
 import 'package:flowy_sdk/log.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid/block_entities.pb.dart';
 
-import '../field/field_cache.dart';
+import '../field/field_controller.dart';
 import '../row/row_cache.dart';
 import 'block_listener.dart';
 
@@ -19,12 +19,12 @@ class GridBlockCache {
   GridBlockCache({
     required this.gridId,
     required this.block,
-    required GridFieldCache fieldCache,
+    required GridFieldController fieldController,
   }) {
     _rowCache = GridRowCache(
       gridId: gridId,
       block: block,
-      notifier: GridRowFieldNotifierImpl(fieldCache),
+      notifier: GridRowFieldNotifierImpl(fieldController),
     );
 
     _listener = GridBlockListener(blockId: block.id);

+ 13 - 12
frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/context_builder.dart → frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/cell_controller.dart

@@ -148,10 +148,10 @@ class IGridCellController<T, D> extends Equatable {
         _cellDataLoader = cellDataLoader,
         _cellDataPersistence = cellDataPersistence,
         _fieldNotifier = fieldNotifier,
-        _fieldService =
-            FieldService(gridId: cellId.gridId, fieldId: cellId.field.id),
-        _cacheKey =
-            GridCellCacheKey(rowId: cellId.rowId, fieldId: cellId.field.id);
+        _fieldService = FieldService(
+            gridId: cellId.gridId, fieldId: cellId.fieldContext.id),
+        _cacheKey = GridCellCacheKey(
+            rowId: cellId.rowId, fieldId: cellId.fieldContext.id);
 
   IGridCellController<T, D> clone() {
     return IGridCellController(
@@ -166,11 +166,11 @@ class IGridCellController<T, D> extends Equatable {
 
   String get rowId => cellId.rowId;
 
-  String get fieldId => cellId.field.id;
+  String get fieldId => cellId.fieldContext.id;
 
-  FieldPB get field => cellId.field;
+  GridFieldContext get fieldContext => cellId.fieldContext;
 
-  FieldType get fieldType => cellId.field.fieldType;
+  FieldType get fieldType => cellId.fieldContext.fieldType;
 
   VoidCallback? startListening(
       {required void Function(T?) onCellChanged,
@@ -182,7 +182,8 @@ class IGridCellController<T, D> extends Equatable {
     isListening = true;
 
     _cellDataNotifier = ValueNotifier(_cellsCache.get(_cacheKey));
-    _cellListener = CellListener(rowId: cellId.rowId, fieldId: cellId.field.id);
+    _cellListener =
+        CellListener(rowId: cellId.rowId, fieldId: cellId.fieldContext.id);
 
     /// 1.Listen on user edit event and load the new cell data if needed.
     /// For example:
@@ -308,14 +309,14 @@ class IGridCellController<T, D> extends Equatable {
 
   @override
   List<Object> get props =>
-      [_cellsCache.get(_cacheKey) ?? "", cellId.rowId + cellId.field.id];
+      [_cellsCache.get(_cacheKey) ?? "", cellId.rowId + cellId.fieldContext.id];
 }
 
 class GridCellFieldNotifierImpl extends IGridCellFieldNotifier {
-  final GridFieldCache _cache;
-  FieldChangesetCallback? _onChangesetFn;
+  final GridFieldController _cache;
+  OnChangeset? _onChangesetFn;
 
-  GridCellFieldNotifierImpl(GridFieldCache cache) : _cache = cache;
+  GridCellFieldNotifierImpl(GridFieldController cache) : _cache = cache;
 
   @override
   void onCellDispose() {

+ 6 - 6
frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/cell_service.dart

@@ -16,12 +16,12 @@ import 'package:app_flowy/plugins/grid/application/cell/cell_listener.dart';
 import 'package:app_flowy/plugins/grid/application/field/field_service.dart';
 import 'dart:convert' show utf8;
 
-import '../../field/field_cache.dart';
+import '../../field/field_controller.dart';
 import '../../field/type_option/type_option_context.dart';
 import 'cell_field_notifier.dart';
 part 'cell_service.freezed.dart';
 part 'cell_data_loader.dart';
-part 'context_builder.dart';
+part 'cell_controller.dart';
 part 'cell_cache.dart';
 part 'cell_data_persistence.dart';
 
@@ -60,17 +60,17 @@ class GridCellIdentifier with _$GridCellIdentifier {
   const factory GridCellIdentifier({
     required String gridId,
     required String rowId,
-    required FieldPB field,
+    required GridFieldContext fieldContext,
   }) = _GridCellIdentifier;
 
   // ignore: unused_element
   const GridCellIdentifier._();
 
-  String get fieldId => field.id;
+  String get fieldId => fieldContext.id;
 
-  FieldType get fieldType => field.fieldType;
+  FieldType get fieldType => fieldContext.fieldType;
 
   ValueKey key() {
-    return ValueKey("$rowId$fieldId${field.fieldType}");
+    return ValueKey("$rowId$fieldId${fieldContext.fieldType}");
   }
 }

+ 1 - 1
frontend/app_flowy/lib/plugins/grid/application/cell/date_cal_bloc.dart

@@ -176,7 +176,7 @@ class DateCalBloc extends Bloc<DateCalEvent, DateCalState> {
 
     final result = await FieldService.updateFieldTypeOption(
       gridId: cellController.gridId,
-      fieldId: cellController.field.id,
+      fieldId: cellController.fieldContext.id,
       typeOptionData: newDateTypeOption.writeToBuffer(),
     );
 

+ 3 - 7
frontend/app_flowy/lib/plugins/grid/application/cell/date_cell_bloc.dart

@@ -1,5 +1,5 @@
+import 'package:app_flowy/plugins/grid/application/field/field_controller.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid/date_type_option_entities.pb.dart';
-import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:freezed_annotation/freezed_annotation.dart';
 import 'dart:async';
@@ -20,8 +20,6 @@ class DateCellBloc extends Bloc<DateCellEvent, DateCellState> {
             emit(state.copyWith(
                 data: cellData, dateStr: _dateStrFromCellData(cellData)));
           },
-          didReceiveFieldUpdate: (FieldPB value) =>
-              emit(state.copyWith(field: value)),
         );
       },
     );
@@ -53,8 +51,6 @@ class DateCellEvent with _$DateCellEvent {
   const factory DateCellEvent.initial() = _InitialCell;
   const factory DateCellEvent.didReceiveCellUpdate(DateCellDataPB? data) =
       _DidReceiveCellUpdate;
-  const factory DateCellEvent.didReceiveFieldUpdate(FieldPB field) =
-      _DidReceiveFieldUpdate;
 }
 
 @freezed
@@ -62,14 +58,14 @@ class DateCellState with _$DateCellState {
   const factory DateCellState({
     required DateCellDataPB? data,
     required String dateStr,
-    required FieldPB field,
+    required GridFieldContext fieldContext,
   }) = _DateCellState;
 
   factory DateCellState.initial(GridDateCellController context) {
     final cellData = context.getCellData();
 
     return DateCellState(
-      field: context.field,
+      fieldContext: context.fieldContext,
       data: cellData,
       dateStr: _dateStrFromCellData(cellData),
     );

+ 1 - 1
frontend/app_flowy/lib/plugins/grid/application/cell/select_option_service.dart

@@ -11,7 +11,7 @@ class SelectOptionService {
   SelectOptionService({required this.cellId});
 
   String get gridId => cellId.gridId;
-  String get fieldId => cellId.field.id;
+  String get fieldId => cellId.fieldContext.id;
   String get rowId => cellId.rowId;
 
   Future<Either<Unit, FlowyError>> create({required String name}) {

+ 0 - 192
frontend/app_flowy/lib/plugins/grid/application/field/field_cache.dart

@@ -1,192 +0,0 @@
-import 'dart:collection';
-
-import 'package:app_flowy/plugins/grid/application/field/grid_listener.dart';
-import 'package:flowy_sdk/log.dart';
-import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
-import 'package:flutter/foundation.dart';
-
-import '../row/row_cache.dart';
-
-class FieldsNotifier extends ChangeNotifier {
-  List<FieldPB> _fields = [];
-
-  set fields(List<FieldPB> fields) {
-    _fields = fields;
-    notifyListeners();
-  }
-
-  List<FieldPB> get fields => _fields;
-}
-
-typedef FieldChangesetCallback = void Function(FieldChangesetPB);
-typedef FieldsCallback = void Function(List<FieldPB>);
-
-class GridFieldCache {
-  final String gridId;
-  final GridFieldsListener _fieldListener;
-  FieldsNotifier? _fieldNotifier = FieldsNotifier();
-  final Map<FieldsCallback, VoidCallback> _fieldsCallbackMap = {};
-  final Map<FieldChangesetCallback, FieldChangesetCallback>
-      _changesetCallbackMap = {};
-
-  GridFieldCache({required this.gridId})
-      : _fieldListener = GridFieldsListener(gridId: gridId) {
-    _fieldListener.start(onFieldsChanged: (result) {
-      result.fold(
-        (changeset) {
-          _deleteFields(changeset.deletedFields);
-          _insertFields(changeset.insertedFields);
-          _updateFields(changeset.updatedFields);
-          for (final listener in _changesetCallbackMap.values) {
-            listener(changeset);
-          }
-        },
-        (err) => Log.error(err),
-      );
-    });
-  }
-
-  Future<void> dispose() async {
-    await _fieldListener.stop();
-    _fieldNotifier?.dispose();
-    _fieldNotifier = null;
-  }
-
-  UnmodifiableListView<FieldPB> get unmodifiableFields =>
-      UnmodifiableListView(_fieldNotifier?.fields ?? []);
-
-  List<FieldPB> get fields => [..._fieldNotifier?.fields ?? []];
-
-  set fields(List<FieldPB> fields) {
-    _fieldNotifier?.fields = [...fields];
-  }
-
-  void addListener({
-    FieldsCallback? onFields,
-    FieldChangesetCallback? onChangeset,
-    bool Function()? listenWhen,
-  }) {
-    if (onChangeset != null) {
-      fn(c) {
-        if (listenWhen != null && listenWhen() == false) {
-          return;
-        }
-        onChangeset(c);
-      }
-
-      _changesetCallbackMap[onChangeset] = fn;
-    }
-
-    if (onFields != null) {
-      fn() {
-        if (listenWhen != null && listenWhen() == false) {
-          return;
-        }
-        onFields(fields);
-      }
-
-      _fieldsCallbackMap[onFields] = fn;
-      _fieldNotifier?.addListener(fn);
-    }
-  }
-
-  void removeListener({
-    FieldsCallback? onFieldsListener,
-    FieldChangesetCallback? onChangesetListener,
-  }) {
-    if (onFieldsListener != null) {
-      final fn = _fieldsCallbackMap.remove(onFieldsListener);
-      if (fn != null) {
-        _fieldNotifier?.removeListener(fn);
-      }
-    }
-
-    if (onChangesetListener != null) {
-      _changesetCallbackMap.remove(onChangesetListener);
-    }
-  }
-
-  void _deleteFields(List<FieldIdPB> deletedFields) {
-    if (deletedFields.isEmpty) {
-      return;
-    }
-    final List<FieldPB> newFields = fields;
-    final Map<String, FieldIdPB> deletedFieldMap = {
-      for (var fieldOrder in deletedFields) fieldOrder.fieldId: fieldOrder
-    };
-
-    newFields.retainWhere((field) => (deletedFieldMap[field.id] == null));
-    _fieldNotifier?.fields = newFields;
-  }
-
-  void _insertFields(List<IndexFieldPB> insertedFields) {
-    if (insertedFields.isEmpty) {
-      return;
-    }
-    final List<FieldPB> newFields = fields;
-    for (final indexField in insertedFields) {
-      if (newFields.length > indexField.index) {
-        newFields.insert(indexField.index, indexField.field_1);
-      } else {
-        newFields.add(indexField.field_1);
-      }
-    }
-    _fieldNotifier?.fields = newFields;
-  }
-
-  void _updateFields(List<FieldPB> updatedFields) {
-    if (updatedFields.isEmpty) {
-      return;
-    }
-    final List<FieldPB> newFields = fields;
-    for (final updatedField in updatedFields) {
-      final index =
-          newFields.indexWhere((field) => field.id == updatedField.id);
-      if (index != -1) {
-        newFields.removeAt(index);
-        newFields.insert(index, updatedField);
-      }
-    }
-    _fieldNotifier?.fields = newFields;
-  }
-}
-
-class GridRowFieldNotifierImpl extends IGridRowFieldNotifier {
-  final GridFieldCache _cache;
-  FieldChangesetCallback? _onChangesetFn;
-  FieldsCallback? _onFieldFn;
-  GridRowFieldNotifierImpl(GridFieldCache cache) : _cache = cache;
-
-  @override
-  UnmodifiableListView<FieldPB> get fields => _cache.unmodifiableFields;
-
-  @override
-  void onRowFieldsChanged(VoidCallback callback) {
-    _onFieldFn = (_) => callback();
-    _cache.addListener(onFields: _onFieldFn);
-  }
-
-  @override
-  void onRowFieldChanged(void Function(FieldPB) callback) {
-    _onChangesetFn = (FieldChangesetPB changeset) {
-      for (final updatedField in changeset.updatedFields) {
-        callback(updatedField);
-      }
-    };
-
-    _cache.addListener(onChangeset: _onChangesetFn);
-  }
-
-  @override
-  void onRowDispose() {
-    if (_onFieldFn != null) {
-      _cache.removeListener(onFieldsListener: _onFieldFn!);
-      _onFieldFn = null;
-    }
-
-    if (_onChangesetFn != null) {
-      _cache.removeListener(onChangesetListener: _onChangesetFn!);
-      _onChangesetFn = null;
-    }
-  }
-}

+ 281 - 0
frontend/app_flowy/lib/plugins/grid/application/field/field_controller.dart

@@ -0,0 +1,281 @@
+import 'dart:collection';
+import 'package:app_flowy/plugins/grid/application/field/grid_listener.dart';
+import 'package:app_flowy/plugins/grid/application/grid_service.dart';
+import 'package:app_flowy/plugins/grid/application/setting/setting_listener.dart';
+import 'package:app_flowy/plugins/grid/application/setting/setting_service.dart';
+import 'package:dartz/dartz.dart';
+import 'package:flowy_sdk/log.dart';
+import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/setting_entities.pb.dart';
+import 'package:flutter/foundation.dart';
+import '../row/row_cache.dart';
+
+class _GridFieldNotifier extends ChangeNotifier {
+  List<GridFieldContext> _fieldContexts = [];
+
+  set fieldContexts(List<GridFieldContext> fieldContexts) {
+    _fieldContexts = fieldContexts;
+    notifyListeners();
+  }
+
+  void notify() {
+    notifyListeners();
+  }
+
+  List<GridFieldContext> get fieldContexts => _fieldContexts;
+}
+
+typedef OnChangeset = void Function(FieldChangesetPB);
+typedef OnReceiveFields = void Function(List<GridFieldContext>);
+
+class GridFieldController {
+  final String gridId;
+  final GridFieldsListener _fieldListener;
+  final SettingListener _settingListener;
+  final Map<OnReceiveFields, VoidCallback> _fieldCallbackMap = {};
+  final Map<OnChangeset, OnChangeset> _changesetCallbackMap = {};
+
+  _GridFieldNotifier? _fieldNotifier = _GridFieldNotifier();
+  List<String> _groupFieldIds = [];
+  final GridFFIService _gridFFIService;
+  final SettingFFIService _settingFFIService;
+
+  List<GridFieldContext> get fieldContexts =>
+      [..._fieldNotifier?.fieldContexts ?? []];
+
+  GridFieldController({required this.gridId})
+      : _fieldListener = GridFieldsListener(gridId: gridId),
+        _gridFFIService = GridFFIService(gridId: gridId),
+        _settingFFIService = SettingFFIService(viewId: gridId),
+        _settingListener = SettingListener(gridId: gridId) {
+    //Listen on field's changes
+    _fieldListener.start(onFieldsChanged: (result) {
+      result.fold(
+        (changeset) {
+          _deleteFields(changeset.deletedFields);
+          _insertFields(changeset.insertedFields);
+          _updateFields(changeset.updatedFields);
+          for (final listener in _changesetCallbackMap.values) {
+            listener(changeset);
+          }
+        },
+        (err) => Log.error(err),
+      );
+    });
+
+    //Listen on setting changes
+    _settingListener.start(onSettingUpdated: (result) {
+      result.fold(
+        (setting) => _updateFieldsWhenSettingChanged(setting),
+        (r) => Log.error(r),
+      );
+    });
+
+    _settingFFIService.getSetting().then((result) {
+      result.fold(
+        (setting) => _updateFieldsWhenSettingChanged(setting),
+        (err) => Log.error(err),
+      );
+    });
+  }
+
+  void _updateFieldsWhenSettingChanged(GridSettingPB setting) {
+    _groupFieldIds = setting.groupConfigurations.items
+        .map((item) => item.groupFieldId)
+        .toList();
+
+    _updateFieldContexts();
+  }
+
+  void _updateFieldContexts() {
+    if (_fieldNotifier != null) {
+      for (var field in _fieldNotifier!.fieldContexts) {
+        if (_groupFieldIds.contains(field.id)) {
+          field._isGroupField = true;
+        } else {
+          field._isGroupField = false;
+        }
+      }
+      _fieldNotifier?.notify();
+    }
+  }
+
+  Future<void> dispose() async {
+    await _fieldListener.stop();
+    _fieldNotifier?.dispose();
+    _fieldNotifier = null;
+  }
+
+  Future<Either<Unit, FlowyError>> loadFields(
+      {required List<FieldIdPB> fieldIds}) async {
+    final result = await _gridFFIService.getFields(fieldIds: fieldIds);
+    return Future(
+      () => result.fold(
+        (newFields) {
+          _fieldNotifier?.fieldContexts = newFields.items
+              .map((field) => GridFieldContext(field: field))
+              .toList();
+          _updateFieldContexts();
+          return left(unit);
+        },
+        (err) => right(err),
+      ),
+    );
+  }
+
+  void addListener({
+    OnReceiveFields? onFields,
+    OnChangeset? onChangeset,
+    bool Function()? listenWhen,
+  }) {
+    if (onChangeset != null) {
+      callback(c) {
+        if (listenWhen != null && listenWhen() == false) {
+          return;
+        }
+        onChangeset(c);
+      }
+
+      _changesetCallbackMap[onChangeset] = callback;
+    }
+
+    if (onFields != null) {
+      callback() {
+        if (listenWhen != null && listenWhen() == false) {
+          return;
+        }
+        onFields(fieldContexts);
+      }
+
+      _fieldCallbackMap[onFields] = callback;
+      _fieldNotifier?.addListener(callback);
+    }
+  }
+
+  void removeListener({
+    OnReceiveFields? onFieldsListener,
+    OnChangeset? onChangesetListener,
+  }) {
+    if (onFieldsListener != null) {
+      final callback = _fieldCallbackMap.remove(onFieldsListener);
+      if (callback != null) {
+        _fieldNotifier?.removeListener(callback);
+      }
+    }
+
+    if (onChangesetListener != null) {
+      _changesetCallbackMap.remove(onChangesetListener);
+    }
+  }
+
+  void _deleteFields(List<FieldIdPB> deletedFields) {
+    if (deletedFields.isEmpty) {
+      return;
+    }
+    final List<GridFieldContext> newFields = fieldContexts;
+    final Map<String, FieldIdPB> deletedFieldMap = {
+      for (var fieldOrder in deletedFields) fieldOrder.fieldId: fieldOrder
+    };
+
+    newFields.retainWhere((field) => (deletedFieldMap[field.id] == null));
+    _fieldNotifier?.fieldContexts = newFields;
+  }
+
+  void _insertFields(List<IndexFieldPB> insertedFields) {
+    if (insertedFields.isEmpty) {
+      return;
+    }
+    final List<GridFieldContext> newFields = fieldContexts;
+    for (final indexField in insertedFields) {
+      final gridField = GridFieldContext(field: indexField.field_1);
+      if (newFields.length > indexField.index) {
+        newFields.insert(indexField.index, gridField);
+      } else {
+        newFields.add(gridField);
+      }
+    }
+    _fieldNotifier?.fieldContexts = newFields;
+  }
+
+  void _updateFields(List<FieldPB> updatedFields) {
+    if (updatedFields.isEmpty) {
+      return;
+    }
+    final List<GridFieldContext> newFields = fieldContexts;
+    for (final updatedField in updatedFields) {
+      final index =
+          newFields.indexWhere((field) => field.id == updatedField.id);
+      if (index != -1) {
+        newFields.removeAt(index);
+        final gridField = GridFieldContext(field: updatedField);
+        newFields.insert(index, gridField);
+      }
+    }
+    _fieldNotifier?.fieldContexts = newFields;
+  }
+}
+
+class GridRowFieldNotifierImpl extends IGridRowFieldNotifier {
+  final GridFieldController _cache;
+  OnChangeset? _onChangesetFn;
+  OnReceiveFields? _onFieldFn;
+  GridRowFieldNotifierImpl(GridFieldController cache) : _cache = cache;
+
+  @override
+  UnmodifiableListView<GridFieldContext> get fields =>
+      UnmodifiableListView(_cache.fieldContexts);
+
+  @override
+  void onRowFieldsChanged(VoidCallback callback) {
+    _onFieldFn = (_) => callback();
+    _cache.addListener(onFields: _onFieldFn);
+  }
+
+  @override
+  void onRowFieldChanged(void Function(FieldPB) callback) {
+    _onChangesetFn = (FieldChangesetPB changeset) {
+      for (final updatedField in changeset.updatedFields) {
+        callback(updatedField);
+      }
+    };
+
+    _cache.addListener(onChangeset: _onChangesetFn);
+  }
+
+  @override
+  void onRowDispose() {
+    if (_onFieldFn != null) {
+      _cache.removeListener(onFieldsListener: _onFieldFn!);
+      _onFieldFn = null;
+    }
+
+    if (_onChangesetFn != null) {
+      _cache.removeListener(onChangesetListener: _onChangesetFn!);
+      _onChangesetFn = null;
+    }
+  }
+}
+
+class GridFieldContext {
+  final FieldPB _field;
+  bool _isGroupField = false;
+
+  String get id => _field.id;
+
+  FieldType get fieldType => _field.fieldType;
+
+  bool get visibility => _field.visibility;
+
+  double get width => _field.width.toDouble();
+
+  bool get isPrimary => _field.isPrimary;
+
+  String get name => _field.name;
+
+  FieldPB get field => _field;
+
+  bool get isGroupField => _isGroupField;
+
+  GridFieldContext({required FieldPB field}) : _field = field;
+}

+ 1 - 0
frontend/app_flowy/lib/plugins/grid/application/field/field_service.dart

@@ -5,6 +5,7 @@ import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid/grid_entities.pb.dart';
 import 'package:flutter/foundation.dart';
 import 'package:freezed_annotation/freezed_annotation.dart';
+
 part 'field_service.freezed.dart';
 
 /// FieldService consists of lots of event functions. We define the events in the backend(Rust),

+ 4 - 3
frontend/app_flowy/lib/plugins/grid/application/field/type_option/type_option_data_controller.dart

@@ -1,3 +1,4 @@
+import 'package:app_flowy/plugins/grid/application/field/field_controller.dart';
 import 'package:flowy_infra/notifier.dart';
 import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
@@ -17,12 +18,12 @@ class TypeOptionDataController {
   TypeOptionDataController({
     required this.gridId,
     required this.loader,
-    FieldPB? field,
+    GridFieldContext? fieldContext,
   }) {
-    if (field != null) {
+    if (fieldContext != null) {
       _data = FieldTypeOptionDataPB.create()
         ..gridId = gridId
-        ..field_2 = field;
+        ..field_2 = fieldContext.field;
     }
   }
 

+ 6 - 4
frontend/app_flowy/lib/plugins/grid/application/grid_bloc.dart

@@ -7,6 +7,7 @@ import 'package:flowy_sdk/protobuf/flowy-grid/protobuf.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:freezed_annotation/freezed_annotation.dart';
 import 'block/block_cache.dart';
+import 'field/field_controller.dart';
 import 'grid_data_controller.dart';
 import 'row/row_cache.dart';
 import 'dart:collection';
@@ -101,7 +102,7 @@ class GridEvent with _$GridEvent {
     RowsChangedReason listState,
   ) = _DidReceiveRowUpdate;
   const factory GridEvent.didReceiveFieldUpdate(
-    UnmodifiableListView<FieldPB> fields,
+    UnmodifiableListView<GridFieldContext> fields,
   ) = _DidReceiveFieldUpdate;
 
   const factory GridEvent.didReceiveGridUpdate(
@@ -138,9 +139,9 @@ class GridLoadingState with _$GridLoadingState {
 }
 
 class GridFieldEquatable extends Equatable {
-  final UnmodifiableListView<FieldPB> _fields;
+  final UnmodifiableListView<GridFieldContext> _fields;
   const GridFieldEquatable(
-    UnmodifiableListView<FieldPB> fields,
+    UnmodifiableListView<GridFieldContext> fields,
   ) : _fields = fields;
 
   @override
@@ -157,5 +158,6 @@ class GridFieldEquatable extends Equatable {
     ];
   }
 
-  UnmodifiableListView<FieldPB> get value => UnmodifiableListView(_fields);
+  UnmodifiableListView<GridFieldContext> get value =>
+      UnmodifiableListView(_fields);
 }

+ 8 - 23
frontend/app_flowy/lib/plugins/grid/application/grid_data_controller.dart

@@ -4,16 +4,15 @@ import 'package:flowy_sdk/log.dart';
 import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid/block_entities.pb.dart';
-import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid/grid_entities.pb.dart';
 import 'dart:async';
 import 'package:dartz/dartz.dart';
 import 'block/block_cache.dart';
-import 'field/field_cache.dart';
+import 'field/field_controller.dart';
 import 'prelude.dart';
 import 'row/row_cache.dart';
 
-typedef OnFieldsChanged = void Function(UnmodifiableListView<FieldPB>);
+typedef OnFieldsChanged = void Function(UnmodifiableListView<GridFieldContext>);
 typedef OnGridChanged = void Function(GridPB);
 
 typedef OnRowsChanged = void Function(
@@ -25,7 +24,7 @@ typedef ListenOnRowChangedCondition = bool Function();
 class GridDataController {
   final String gridId;
   final GridFFIService _gridFFIService;
-  final GridFieldCache fieldCache;
+  final GridFieldController fieldController;
 
   // key: the block id
   final LinkedHashMap<String, GridBlockCache> _blocks;
@@ -49,7 +48,7 @@ class GridDataController {
         // ignore: prefer_collection_literals
         _blocks = LinkedHashMap(),
         _gridFFIService = GridFFIService(gridId: view.id),
-        fieldCache = GridFieldCache(gridId: view.id);
+        fieldController = GridFieldController(gridId: view.id);
 
   void addListener({
     required OnGridChanged onGridChanged,
@@ -60,7 +59,7 @@ class GridDataController {
     _onRowChanged = onRowsChanged;
     _onFieldsChanged = onFieldsChanged;
 
-    fieldCache.addListener(onFields: (fields) {
+    fieldController.addListener(onFields: (fields) {
       _onFieldsChanged?.call(UnmodifiableListView(fields));
     });
   }
@@ -72,7 +71,7 @@ class GridDataController {
         (grid) async {
           _initialBlocks(grid.blocks);
           _onGridChanged?.call(grid);
-          return await _loadFields(grid);
+          return await fieldController.loadFields(fieldIds: grid.fields);
         },
         (err) => right(err),
       ),
@@ -85,7 +84,7 @@ class GridDataController {
 
   Future<void> dispose() async {
     await _gridFFIService.closeGrid();
-    await fieldCache.dispose();
+    await fieldController.dispose();
 
     for (final blockCache in _blocks.values) {
       blockCache.dispose();
@@ -102,7 +101,7 @@ class GridDataController {
       final cache = GridBlockCache(
         gridId: gridId,
         block: block,
-        fieldCache: fieldCache,
+        fieldController: fieldController,
       );
 
       cache.addListener(
@@ -114,18 +113,4 @@ class GridDataController {
       _blocks[block.id] = cache;
     }
   }
-
-  Future<Either<Unit, FlowyError>> _loadFields(GridPB grid) async {
-    final result = await _gridFFIService.getFields(fieldIds: grid.fields);
-    return Future(
-      () => result.fold(
-        (fields) {
-          fieldCache.fields = fields.items;
-          _onFieldsChanged?.call(UnmodifiableListView(fieldCache.fields));
-          return left(unit);
-        },
-        (err) => right(err),
-      ),
-    );
-  }
 }

+ 10 - 11
frontend/app_flowy/lib/plugins/grid/application/grid_header_bloc.dart

@@ -4,19 +4,18 @@ import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:freezed_annotation/freezed_annotation.dart';
 import 'dart:async';
-
-import 'field/field_cache.dart';
+import 'field/field_controller.dart';
 
 part 'grid_header_bloc.freezed.dart';
 
 class GridHeaderBloc extends Bloc<GridHeaderEvent, GridHeaderState> {
-  final GridFieldCache fieldCache;
+  final GridFieldController fieldController;
   final String gridId;
 
   GridHeaderBloc({
     required this.gridId,
-    required this.fieldCache,
-  }) : super(GridHeaderState.initial(fieldCache.fields)) {
+    required this.fieldController,
+  }) : super(GridHeaderState.initial(fieldController.fieldContexts)) {
     on<GridHeaderEvent>(
       (event, emit) async {
         await event.map(
@@ -36,7 +35,7 @@ class GridHeaderBloc extends Bloc<GridHeaderEvent, GridHeaderState> {
 
   Future<void> _moveField(
       _MoveField value, Emitter<GridHeaderState> emit) async {
-    final fields = List<FieldPB>.from(state.fields);
+    final fields = List<GridFieldContext>.from(state.fields);
     fields.insert(value.toIndex, fields.removeAt(value.fromIndex));
     emit(state.copyWith(fields: fields));
 
@@ -49,7 +48,7 @@ class GridHeaderBloc extends Bloc<GridHeaderEvent, GridHeaderState> {
   }
 
   Future<void> _startListening() async {
-    fieldCache.addListener(
+    fieldController.addListener(
       onFields: (fields) => add(GridHeaderEvent.didReceiveFieldUpdate(fields)),
       listenWhen: () => !isClosed,
     );
@@ -64,18 +63,18 @@ class GridHeaderBloc extends Bloc<GridHeaderEvent, GridHeaderState> {
 @freezed
 class GridHeaderEvent with _$GridHeaderEvent {
   const factory GridHeaderEvent.initial() = _InitialHeader;
-  const factory GridHeaderEvent.didReceiveFieldUpdate(List<FieldPB> fields) =
-      _DidReceiveFieldUpdate;
+  const factory GridHeaderEvent.didReceiveFieldUpdate(
+      List<GridFieldContext> fields) = _DidReceiveFieldUpdate;
   const factory GridHeaderEvent.moveField(
       FieldPB field, int fromIndex, int toIndex) = _MoveField;
 }
 
 @freezed
 class GridHeaderState with _$GridHeaderState {
-  const factory GridHeaderState({required List<FieldPB> fields}) =
+  const factory GridHeaderState({required List<GridFieldContext> fields}) =
       _GridHeaderState;
 
-  factory GridHeaderState.initial(List<FieldPB> fields) {
+  factory GridHeaderState.initial(List<GridFieldContext> fields) {
     // final List<FieldPB> newFields = List.from(fields);
     // newFields.retainWhere((field) => field.visibility);
     return GridHeaderState(fields: fields);

+ 11 - 9
frontend/app_flowy/lib/plugins/grid/application/row/row_bloc.dart

@@ -1,7 +1,7 @@
 import 'dart:collection';
 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:equatable/equatable.dart';
-import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:freezed_annotation/freezed_annotation.dart';
 import 'dart:async';
@@ -35,7 +35,7 @@ class RowBloc extends Bloc<RowEvent, RowState> {
           },
           didReceiveCells: (_DidReceiveCells value) async {
             final cells = value.gridCellMap.values
-                .map((e) => GridCellEquatable(e.field))
+                .map((e) => GridCellEquatable(e.fieldContext))
                 .toList();
             emit(state.copyWith(
               gridCellMap: value.gridCellMap,
@@ -87,21 +87,23 @@ class RowState with _$RowState {
         rowInfo: rowInfo,
         gridCellMap: cellDataMap,
         cells: UnmodifiableListView(
-          cellDataMap.values.map((e) => GridCellEquatable(e.field)).toList(),
+          cellDataMap.values
+              .map((e) => GridCellEquatable(e.fieldContext))
+              .toList(),
         ),
       );
 }
 
 class GridCellEquatable extends Equatable {
-  final FieldPB _field;
+  final GridFieldContext _fieldContext;
 
-  const GridCellEquatable(FieldPB field) : _field = field;
+  const GridCellEquatable(GridFieldContext field) : _fieldContext = field;
 
   @override
   List<Object?> get props => [
-        _field.id,
-        _field.fieldType,
-        _field.visibility,
-        _field.width,
+        _fieldContext.id,
+        _fieldContext.fieldType,
+        _fieldContext.visibility,
+        _fieldContext.width,
       ];
 }

+ 4 - 3
frontend/app_flowy/lib/plugins/grid/application/row/row_cache.dart

@@ -1,5 +1,6 @@
 import 'dart:collection';
 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:flowy_sdk/dispatch/dispatch.dart';
 import 'package:flowy_sdk/log.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid/block_entities.pb.dart';
@@ -12,7 +13,7 @@ part 'row_cache.freezed.dart';
 typedef RowUpdateCallback = void Function();
 
 abstract class IGridRowFieldNotifier {
-  UnmodifiableListView<FieldPB> get fields;
+  UnmodifiableListView<GridFieldContext> get fields;
   void onRowFieldsChanged(VoidCallback callback);
   void onRowFieldChanged(void Function(FieldPB) callback);
   void onRowDispose();
@@ -217,7 +218,7 @@ class GridRowCache {
         cellDataMap[field.id] = GridCellIdentifier(
           rowId: rowId,
           gridId: gridId,
-          field: field,
+          fieldContext: field,
         );
       }
     }
@@ -284,7 +285,7 @@ class _RowChangesetNotifier extends ChangeNotifier {
 class RowInfo with _$RowInfo {
   const factory RowInfo({
     required String gridId,
-    required UnmodifiableListView<FieldPB> fields,
+    required UnmodifiableListView<GridFieldContext> fields,
     required RowPB rowPB,
   }) = _RowInfo;
 }

+ 5 - 5
frontend/app_flowy/lib/plugins/grid/application/row/row_data_controller.dart

@@ -2,7 +2,7 @@ import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_field_
 import 'package:flutter/material.dart';
 import '../../presentation/widgets/cell/cell_builder.dart';
 import '../cell/cell_service/cell_service.dart';
-import '../field/field_cache.dart';
+import '../field/field_controller.dart';
 import 'row_cache.dart';
 
 typedef OnRowChanged = void Function(GridCellMap, RowsChangedReason);
@@ -10,14 +10,14 @@ typedef OnRowChanged = void Function(GridCellMap, RowsChangedReason);
 class GridRowDataController extends GridCellBuilderDelegate {
   final RowInfo rowInfo;
   final List<VoidCallback> _onRowChangedListeners = [];
-  final GridFieldCache _fieldCache;
+  final GridFieldController _fieldController;
   final GridRowCache _rowCache;
 
   GridRowDataController({
     required this.rowInfo,
-    required GridFieldCache fieldCache,
+    required GridFieldController fieldController,
     required GridRowCache rowCache,
-  })  : _fieldCache = fieldCache,
+  })  : _fieldController = fieldController,
         _rowCache = rowCache;
 
   GridCellMap loadData() {
@@ -41,7 +41,7 @@ class GridRowDataController extends GridCellBuilderDelegate {
   @override
   GridCellFieldNotifier buildFieldNotifier() {
     return GridCellFieldNotifier(
-        notifier: GridCellFieldNotifierImpl(_fieldCache));
+        notifier: GridCellFieldNotifierImpl(_fieldController));
   }
 
   @override

+ 84 - 0
frontend/app_flowy/lib/plugins/grid/application/setting/group_bloc.dart

@@ -0,0 +1,84 @@
+import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:freezed_annotation/freezed_annotation.dart';
+import 'dart:async';
+
+import '../field/field_controller.dart';
+import 'setting_service.dart';
+
+part 'group_bloc.freezed.dart';
+
+class GridGroupBloc extends Bloc<GridGroupEvent, GridGroupState> {
+  final GridFieldController _fieldController;
+  final SettingFFIService _settingFFIService;
+  Function(List<GridFieldContext>)? _onFieldsFn;
+
+  GridGroupBloc(
+      {required String viewId, required GridFieldController fieldController})
+      : _fieldController = fieldController,
+        _settingFFIService = SettingFFIService(viewId: viewId),
+        super(GridGroupState.initial(viewId, fieldController.fieldContexts)) {
+    on<GridGroupEvent>(
+      (event, emit) async {
+        event.when(
+          initial: () {
+            _startListening();
+          },
+          didReceiveFieldUpdate: (fieldContexts) {
+            emit(state.copyWith(fieldContexts: fieldContexts));
+          },
+          setGroupByField: (String fieldId, FieldType fieldType) {
+            _settingFFIService.groupByField(
+              fieldId: fieldId,
+              fieldType: fieldType,
+            );
+          },
+        );
+      },
+    );
+  }
+
+  @override
+  Future<void> close() async {
+    if (_onFieldsFn != null) {
+      _fieldController.removeListener(onFieldsListener: _onFieldsFn!);
+      _onFieldsFn = null;
+    }
+    return super.close();
+  }
+
+  void _startListening() {
+    _onFieldsFn = (fieldContexts) =>
+        add(GridGroupEvent.didReceiveFieldUpdate(fieldContexts));
+    _fieldController.addListener(
+      onFields: _onFieldsFn,
+      listenWhen: () => !isClosed,
+    );
+  }
+}
+
+@freezed
+class GridGroupEvent with _$GridGroupEvent {
+  const factory GridGroupEvent.initial() = _Initial;
+  const factory GridGroupEvent.setGroupByField(
+    String fieldId,
+    FieldType fieldType,
+  ) = _GroupByField;
+  const factory GridGroupEvent.didReceiveFieldUpdate(
+      List<GridFieldContext> fields) = _DidReceiveFieldUpdate;
+}
+
+@freezed
+class GridGroupState with _$GridGroupState {
+  const factory GridGroupState({
+    required String gridId,
+    required List<GridFieldContext> fieldContexts,
+  }) = _GridGroupState;
+
+  factory GridGroupState.initial(
+          String gridId, List<GridFieldContext> fieldContexts) =>
+      GridGroupState(
+        gridId: gridId,
+        fieldContexts: fieldContexts,
+      );
+}

+ 19 - 15
frontend/app_flowy/lib/plugins/grid/application/setting/property_bloc.dart

@@ -1,21 +1,22 @@
 import 'package:app_flowy/plugins/grid/application/field/field_service.dart';
 import 'package:flowy_sdk/log.dart';
-import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:freezed_annotation/freezed_annotation.dart';
 import 'dart:async';
 
-import '../field/field_cache.dart';
+import '../field/field_controller.dart';
 
 part 'property_bloc.freezed.dart';
 
 class GridPropertyBloc extends Bloc<GridPropertyEvent, GridPropertyState> {
-  final GridFieldCache _fieldCache;
-  Function(List<FieldPB>)? _onFieldsFn;
+  final GridFieldController _fieldController;
+  Function(List<GridFieldContext>)? _onFieldsFn;
 
-  GridPropertyBloc({required String gridId, required GridFieldCache fieldCache})
-      : _fieldCache = fieldCache,
-        super(GridPropertyState.initial(gridId, fieldCache.fields)) {
+  GridPropertyBloc(
+      {required String gridId, required GridFieldController fieldController})
+      : _fieldController = fieldController,
+        super(
+            GridPropertyState.initial(gridId, fieldController.fieldContexts)) {
     on<GridPropertyEvent>(
       (event, emit) async {
         await event.map(
@@ -33,7 +34,7 @@ class GridPropertyBloc extends Bloc<GridPropertyEvent, GridPropertyState> {
             );
           },
           didReceiveFieldUpdate: (_DidReceiveFieldUpdate value) {
-            emit(state.copyWith(fields: value.fields));
+            emit(state.copyWith(fieldContexts: value.fields));
           },
           moveField: (_MoveField value) {
             //
@@ -46,7 +47,7 @@ class GridPropertyBloc extends Bloc<GridPropertyEvent, GridPropertyState> {
   @override
   Future<void> close() async {
     if (_onFieldsFn != null) {
-      _fieldCache.removeListener(onFieldsListener: _onFieldsFn!);
+      _fieldController.removeListener(onFieldsListener: _onFieldsFn!);
       _onFieldsFn = null;
     }
     return super.close();
@@ -55,7 +56,7 @@ class GridPropertyBloc extends Bloc<GridPropertyEvent, GridPropertyState> {
   void _startListening() {
     _onFieldsFn =
         (fields) => add(GridPropertyEvent.didReceiveFieldUpdate(fields));
-    _fieldCache.addListener(
+    _fieldController.addListener(
       onFields: _onFieldsFn,
       listenWhen: () => !isClosed,
     );
@@ -67,8 +68,8 @@ class GridPropertyEvent with _$GridPropertyEvent {
   const factory GridPropertyEvent.initial() = _Initial;
   const factory GridPropertyEvent.setFieldVisibility(
       String fieldId, bool visibility) = _SetFieldVisibility;
-  const factory GridPropertyEvent.didReceiveFieldUpdate(List<FieldPB> fields) =
-      _DidReceiveFieldUpdate;
+  const factory GridPropertyEvent.didReceiveFieldUpdate(
+      List<GridFieldContext> fields) = _DidReceiveFieldUpdate;
   const factory GridPropertyEvent.moveField(int fromIndex, int toIndex) =
       _MoveField;
 }
@@ -77,12 +78,15 @@ class GridPropertyEvent with _$GridPropertyEvent {
 class GridPropertyState with _$GridPropertyState {
   const factory GridPropertyState({
     required String gridId,
-    required List<FieldPB> fields,
+    required List<GridFieldContext> fieldContexts,
   }) = _GridPropertyState;
 
-  factory GridPropertyState.initial(String gridId, List<FieldPB> fields) =>
+  factory GridPropertyState.initial(
+    String gridId,
+    List<GridFieldContext> fieldContexts,
+  ) =>
       GridPropertyState(
         gridId: gridId,
-        fields: fields,
+        fieldContexts: fieldContexts,
       );
 }

+ 59 - 0
frontend/app_flowy/lib/plugins/grid/application/setting/setting_controller.dart

@@ -0,0 +1,59 @@
+import 'package:app_flowy/plugins/grid/application/setting/setting_service.dart';
+import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/setting_entities.pb.dart';
+import 'setting_listener.dart';
+
+typedef OnError = void Function(FlowyError);
+typedef OnSettingUpdated = void Function(GridSettingPB);
+
+class SettingController {
+  final String viewId;
+  final SettingFFIService _ffiService;
+  final SettingListener _listener;
+  OnSettingUpdated? _onSettingUpdated;
+  OnError? _onError;
+  GridSettingPB? _setting;
+  GridSettingPB? get setting => _setting;
+
+  SettingController({
+    required this.viewId,
+  })  : _ffiService = SettingFFIService(viewId: viewId),
+        _listener = SettingListener(gridId: viewId) {
+    // Load setting
+    _ffiService.getSetting().then((result) {
+      result.fold(
+        (newSetting) => updateSetting(newSetting),
+        (err) => _onError?.call(err),
+      );
+    });
+
+    // Listen on the seting changes
+    _listener.start(onSettingUpdated: (result) {
+      result.fold(
+        (newSetting) => updateSetting(newSetting),
+        (err) => _onError?.call(err),
+      );
+    });
+  }
+
+  void startListeing({
+    required OnSettingUpdated onSettingUpdated,
+    required OnError onError,
+  }) {
+    assert(_onSettingUpdated == null, 'Should call once');
+    assert(_onError == null, 'Should call once');
+    _onSettingUpdated = onSettingUpdated;
+    _onError = onError;
+  }
+
+  void updateSetting(GridSettingPB newSetting) {
+    _setting = newSetting;
+    _onSettingUpdated?.call(newSetting);
+  }
+
+  void dispose() {
+    _onSettingUpdated = null;
+    _onError = null;
+    _listener.stop();
+  }
+}

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

@@ -0,0 +1,47 @@
+import 'dart:typed_data';
+
+import 'package:app_flowy/core/grid_notification.dart';
+import 'package:dartz/dartz.dart';
+import 'package:flowy_infra/notifier.dart';
+import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/dart_notification.pbserver.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/setting_entities.pb.dart';
+
+typedef UpdateSettingNotifiedValue = Either<GridSettingPB, FlowyError>;
+
+class SettingListener {
+  final String gridId;
+  GridNotificationListener? _listener;
+  PublishNotifier<UpdateSettingNotifiedValue>? _updateSettingNotifier =
+      PublishNotifier();
+
+  SettingListener({required this.gridId});
+
+  void start({
+    required void Function(UpdateSettingNotifiedValue) onSettingUpdated,
+  }) {
+    _updateSettingNotifier?.addPublishListener(onSettingUpdated);
+    _listener = GridNotificationListener(objectId: gridId, handler: _handler);
+  }
+
+  void _handler(GridNotification ty, Either<Uint8List, FlowyError> result) {
+    switch (ty) {
+      case GridNotification.DidUpdateGridSetting:
+        result.fold(
+          (payload) => _updateSettingNotifier?.value = left(
+            GridSettingPB.fromBuffer(payload),
+          ),
+          (error) => _updateSettingNotifier?.value = right(error),
+        );
+        break;
+      default:
+        break;
+    }
+  }
+
+  Future<void> stop() async {
+    await _listener?.stop();
+    _updateSettingNotifier?.dispose();
+    _updateSettingNotifier = null;
+  }
+}

+ 32 - 0
frontend/app_flowy/lib/plugins/grid/application/setting/setting_service.dart

@@ -0,0 +1,32 @@
+import 'package:dartz/dartz.dart';
+import 'package:flowy_sdk/dispatch/dispatch.dart';
+import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/grid_entities.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/group.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/setting_entities.pb.dart';
+
+class SettingFFIService {
+  final String viewId;
+
+  const SettingFFIService({required this.viewId});
+
+  Future<Either<GridSettingPB, FlowyError>> getSetting() {
+    final payload = GridIdPB.create()..value = viewId;
+    return GridEventGetGridSetting(payload).send();
+  }
+
+  Future<Either<Unit, FlowyError>> groupByField({
+    required String fieldId,
+    required FieldType fieldType,
+  }) {
+    final insertGroupPayload = InsertGroupPayloadPB.create()
+      ..fieldId = fieldId
+      ..fieldType = fieldType;
+    final payload = GridSettingChangesetPayloadPB.create()
+      ..gridId = viewId
+      ..insertGroup = insertGroupPayload;
+
+    return GridEventUpdateGridSetting(payload).send();
+  }
+}

+ 13 - 10
frontend/app_flowy/lib/plugins/grid/presentation/grid_page.dart

@@ -1,4 +1,4 @@
-import 'package:app_flowy/plugins/grid/application/field/field_cache.dart';
+import 'package:app_flowy/plugins/grid/application/field/field_controller.dart';
 import 'package:app_flowy/plugins/grid/application/row/row_data_controller.dart';
 import 'package:app_flowy/startup/startup.dart';
 import 'package:app_flowy/plugins/grid/application/grid_bloc.dart';
@@ -157,10 +157,11 @@ class _FlowyGridState extends State<FlowyGrid> {
   }
 
   Widget _gridHeader(BuildContext context, String gridId) {
-    final fieldCache = context.read<GridBloc>().dataController.fieldCache;
+    final fieldController =
+        context.read<GridBloc>().dataController.fieldController;
     return GridHeaderSliverAdaptor(
       gridId: gridId,
-      fieldCache: fieldCache,
+      fieldController: fieldController,
       anchorScrollController: headerScrollController,
     );
   }
@@ -173,10 +174,11 @@ class _GridToolbarAdaptor extends StatelessWidget {
   Widget build(BuildContext context) {
     return BlocSelector<GridBloc, GridState, GridToolbarContext>(
       selector: (state) {
-        final fieldCache = context.read<GridBloc>().dataController.fieldCache;
+        final fieldController =
+            context.read<GridBloc>().dataController.fieldController;
         return GridToolbarContext(
           gridId: state.gridId,
-          fieldCache: fieldCache,
+          fieldController: fieldController,
         );
       },
       builder: (context, toolbarContext) {
@@ -247,10 +249,11 @@ class _GridRowsState extends State<_GridRows> {
     /// Return placeholder widget if the rowCache is null.
     if (rowCache == null) return const SizedBox();
 
-    final fieldCache = context.read<GridBloc>().dataController.fieldCache;
+    final fieldController =
+        context.read<GridBloc>().dataController.fieldController;
     final dataController = GridRowDataController(
       rowInfo: rowInfo,
-      fieldCache: fieldCache,
+      fieldController: fieldController,
       rowCache: rowCache,
     );
 
@@ -264,7 +267,7 @@ class _GridRowsState extends State<_GridRows> {
           _openRowDetailPage(
             context,
             rowInfo,
-            fieldCache,
+            fieldController,
             rowCache,
             cellBuilder,
           );
@@ -277,13 +280,13 @@ class _GridRowsState extends State<_GridRows> {
   void _openRowDetailPage(
     BuildContext context,
     RowInfo rowInfo,
-    GridFieldCache fieldCache,
+    GridFieldController fieldController,
     GridRowCache rowCache,
     GridCellBuilder cellBuilder,
   ) {
     final dataController = GridRowDataController(
       rowInfo: rowInfo,
-      fieldCache: fieldCache,
+      fieldController: fieldController,
       rowCache: rowCache,
     );
 

+ 2 - 2
frontend/app_flowy/lib/plugins/grid/presentation/layout/layout.dart

@@ -1,8 +1,8 @@
-import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
+import 'package:app_flowy/plugins/grid/application/field/field_controller.dart';
 import 'sizes.dart';
 
 class GridLayout {
-  static double headerWidth(List<FieldPB> fields) {
+  static double headerWidth(List<GridFieldContext> fields) {
     if (fields.isEmpty) return 0;
 
     final fieldsWidth = fields

+ 5 - 5
frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/grid_header.dart

@@ -1,5 +1,5 @@
 import 'package:app_flowy/generated/locale_keys.g.dart';
-import 'package:app_flowy/plugins/grid/application/field/field_cache.dart';
+import 'package:app_flowy/plugins/grid/application/field/field_controller.dart';
 import 'package:app_flowy/plugins/grid/application/field/type_option/type_option_context.dart';
 import 'package:app_flowy/startup/startup.dart';
 import 'package:app_flowy/plugins/grid/application/prelude.dart';
@@ -18,11 +18,11 @@ import 'field_cell.dart';
 
 class GridHeaderSliverAdaptor extends StatefulWidget {
   final String gridId;
-  final GridFieldCache fieldCache;
+  final GridFieldController fieldController;
   final ScrollController anchorScrollController;
   const GridHeaderSliverAdaptor({
     required this.gridId,
-    required this.fieldCache,
+    required this.fieldController,
     required this.anchorScrollController,
     Key? key,
   }) : super(key: key);
@@ -38,7 +38,7 @@ class _GridHeaderSliverAdaptorState extends State<GridHeaderSliverAdaptor> {
     return BlocProvider(
       create: (context) {
         final bloc = getIt<GridHeaderBloc>(
-            param1: widget.gridId, param2: widget.fieldCache);
+            param1: widget.gridId, param2: widget.fieldController);
         bloc.add(const GridHeaderEvent.initial());
         return bloc;
       },
@@ -84,7 +84,7 @@ class _GridHeaderState extends State<_GridHeader> {
         final cells = state.fields
             .where((field) => field.visibility)
             .map((field) =>
-                GridFieldCellContext(gridId: widget.gridId, field: field))
+                GridFieldCellContext(gridId: widget.gridId, field: field.field))
             .map((ctx) => GridFieldCell(ctx, key: ValueKey(ctx.field.id)))
             .toList();
 

+ 6 - 4
frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/builder.dart

@@ -1,5 +1,6 @@
 import 'dart:typed_data';
 
+import 'package:app_flowy/plugins/grid/application/field/field_controller.dart';
 import 'package:app_flowy/plugins/grid/application/field/type_option/type_option_context.dart';
 import 'package:app_flowy/plugins/grid/application/field/type_option/type_option_data_controller.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid/checkbox_type_option.pb.dart';
@@ -129,17 +130,18 @@ TypeOptionWidgetBuilder makeTypeOptionWidgetBuilder({
 
 TypeOptionContext<T> makeTypeOptionContext<T extends GeneratedMessage>({
   required String gridId,
-  required FieldPB field,
+  required GridFieldContext fieldContext,
 }) {
-  final loader = FieldTypeOptionLoader(gridId: gridId, field: field);
+  final loader =
+      FieldTypeOptionLoader(gridId: gridId, field: fieldContext.field);
   final dataController = TypeOptionDataController(
     gridId: gridId,
     loader: loader,
-    field: field,
+    fieldContext: fieldContext,
   );
   return makeTypeOptionContextWithDataController(
     gridId: gridId,
-    fieldType: field.fieldType,
+    fieldType: fieldContext.fieldType,
     dataController: dataController,
   );
 }

+ 2 - 2
frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/grid_row.dart

@@ -189,13 +189,13 @@ class RowContent extends StatelessWidget {
         final GridCellWidget child = builder.build(cellId);
 
         return CellContainer(
-          width: cellId.field.width.toDouble(),
+          width: cellId.fieldContext.width.toDouble(),
           rowStateNotifier:
               Provider.of<RegionStateNotifier>(context, listen: false),
           accessoryBuilder: (buildContext) {
             final builder = child.accessoryBuilder;
             List<GridCellAccessory> accessories = [];
-            if (cellId.field.isPrimary) {
+            if (cellId.fieldContext.isPrimary) {
               accessories.add(PrimaryCellAccessory(
                 onTapCallback: onExpand,
                 isCellEditing: buildContext.isCellEditing,

+ 3 - 3
frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/row_detail.dart

@@ -215,7 +215,7 @@ class _RowDetailCell extends StatelessWidget {
             SizedBox(
               width: 150,
               child: FieldCellButton(
-                field: cellId.field,
+                field: cellId.fieldContext.field,
                 onTap: () => _showFieldEditor(context),
               ),
             ),
@@ -230,10 +230,10 @@ class _RowDetailCell extends StatelessWidget {
   void _showFieldEditor(BuildContext context) {
     FieldEditor(
       gridId: cellId.gridId,
-      fieldName: cellId.field.name,
+      fieldName: cellId.fieldContext.name,
       typeOptionLoader: FieldTypeOptionLoader(
         gridId: cellId.gridId,
-        field: cellId.field,
+        field: cellId.fieldContext.field,
       ),
     ).show(context);
   }

+ 110 - 0
frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/grid_group.dart

@@ -0,0 +1,110 @@
+import 'package:app_flowy/plugins/grid/application/field/field_controller.dart';
+import 'package:app_flowy/plugins/grid/presentation/layout/sizes.dart';
+import 'package:app_flowy/plugins/grid/presentation/widgets/header/field_type_extension.dart';
+import 'package:flowy_infra/image.dart';
+import 'package:flowy_infra/theme.dart';
+import 'package:flowy_infra_ui/flowy_infra_ui.dart';
+import 'package:flowy_infra_ui/style_widget/button.dart';
+import 'package:flowy_infra_ui/style_widget/text.dart';
+import 'package:flowy_infra_ui/widget/spacing.dart';
+import 'package:flutter/material.dart';
+import 'package:app_flowy/plugins/grid/application/setting/group_bloc.dart';
+
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+class GridGroupList extends StatelessWidget {
+  final String viewId;
+  final GridFieldController fieldController;
+  const GridGroupList({
+    required this.viewId,
+    required this.fieldController,
+    Key? key,
+  }) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return BlocProvider(
+      create: (context) => GridGroupBloc(
+        viewId: viewId,
+        fieldController: fieldController,
+      )..add(const GridGroupEvent.initial()),
+      child: BlocBuilder<GridGroupBloc, GridGroupState>(
+        builder: (context, state) {
+          final cells = state.fieldContexts.map((fieldContext) {
+            return _GridGroupCell(
+              fieldContext: fieldContext,
+              key: ValueKey(fieldContext.id),
+            );
+          }).toList();
+
+          return ListView.separated(
+            shrinkWrap: true,
+            itemCount: cells.length,
+            itemBuilder: (BuildContext context, int index) {
+              return cells[index];
+            },
+            separatorBuilder: (BuildContext context, int index) {
+              return VSpace(GridSize.typeOptionSeparatorHeight);
+            },
+          );
+        },
+      ),
+    );
+  }
+
+  void show(BuildContext context) {
+    FlowyOverlay.of(context).insertWithAnchor(
+      widget: OverlayContainer(
+        constraints: BoxConstraints.loose(const Size(260, 400)),
+        child: this,
+      ),
+      identifier: identifier(),
+      anchorContext: context,
+      anchorDirection: AnchorDirection.bottomRight,
+      style: FlowyOverlayStyle(blur: false),
+    );
+  }
+
+  static String identifier() {
+    return (GridGroupList).toString();
+  }
+}
+
+class _GridGroupCell extends StatelessWidget {
+  final GridFieldContext fieldContext;
+  const _GridGroupCell({required this.fieldContext, Key? key})
+      : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    final theme = context.read<AppTheme>();
+
+    Widget? rightIcon;
+    if (fieldContext.isGroupField) {
+      rightIcon = Padding(
+        padding: const EdgeInsets.all(2.0),
+        child: svgWidget("grid/checkmark"),
+      );
+    }
+
+    return SizedBox(
+      height: GridSize.typeOptionItemHeight,
+      child: FlowyButton(
+        text: FlowyText.medium(fieldContext.name, fontSize: 12),
+        hoverColor: theme.hover,
+        leftIcon: svgWidget(fieldContext.fieldType.iconName(),
+            color: theme.iconColor),
+        rightIcon: rightIcon,
+        onTap: () {
+          context.read<GridGroupBloc>().add(
+                GridGroupEvent.setGroupByField(
+                  fieldContext.id,
+                  fieldContext.fieldType,
+                ),
+              );
+          FlowyOverlay.of(context).remove(GridGroupList.identifier());
+        },
+      ),
+    );
+  }
+}

+ 19 - 15
frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/grid_property.dart

@@ -9,21 +9,20 @@ import 'package:flowy_infra_ui/style_widget/button.dart';
 import 'package:flowy_infra_ui/style_widget/icon_button.dart';
 import 'package:flowy_infra_ui/style_widget/text.dart';
 import 'package:flowy_infra_ui/widget/spacing.dart';
-import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:styled_widget/styled_widget.dart';
 
-import '../../../application/field/field_cache.dart';
+import '../../../application/field/field_controller.dart';
 import '../../layout/sizes.dart';
 import '../header/field_editor.dart';
 
 class GridPropertyList extends StatelessWidget with FlowyOverlayDelegate {
   final String gridId;
-  final GridFieldCache fieldCache;
+  final GridFieldController fieldController;
   const GridPropertyList({
     required this.gridId,
-    required this.fieldCache,
+    required this.fieldController,
     Key? key,
   }) : super(key: key);
 
@@ -45,13 +44,13 @@ class GridPropertyList extends StatelessWidget with FlowyOverlayDelegate {
   Widget build(BuildContext context) {
     return BlocProvider(
       create: (context) =>
-          getIt<GridPropertyBloc>(param1: gridId, param2: fieldCache)
+          getIt<GridPropertyBloc>(param1: gridId, param2: fieldController)
             ..add(const GridPropertyEvent.initial()),
       child: BlocBuilder<GridPropertyBloc, GridPropertyState>(
         builder: (context, state) {
-          final cells = state.fields.map((field) {
+          final cells = state.fieldContexts.map((field) {
             return _GridPropertyCell(
-                gridId: gridId, field: field, key: ValueKey(field.id));
+                gridId: gridId, fieldContext: field, key: ValueKey(field.id));
           }).toList();
 
           return ListView.separated(
@@ -78,16 +77,17 @@ class GridPropertyList extends StatelessWidget with FlowyOverlayDelegate {
 }
 
 class _GridPropertyCell extends StatelessWidget {
-  final FieldPB field;
+  final GridFieldContext fieldContext;
   final String gridId;
-  const _GridPropertyCell({required this.gridId, required this.field, Key? key})
+  const _GridPropertyCell(
+      {required this.gridId, required this.fieldContext, Key? key})
       : super(key: key);
 
   @override
   Widget build(BuildContext context) {
     final theme = context.watch<AppTheme>();
 
-    final checkmark = field.visibility
+    final checkmark = fieldContext.visibility
         ? svgWidget('home/show', color: theme.iconColor)
         : svgWidget('home/hide', color: theme.iconColor);
 
@@ -105,7 +105,7 @@ class _GridPropertyCell extends StatelessWidget {
           onPressed: () {
             context.read<GridPropertyBloc>().add(
                 GridPropertyEvent.setFieldVisibility(
-                    field.id, !field.visibility));
+                    fieldContext.id, !fieldContext.visibility));
           },
           icon: checkmark.padding(all: 6),
         )
@@ -115,14 +115,18 @@ class _GridPropertyCell extends StatelessWidget {
 
   FlowyButton _editFieldButton(AppTheme theme, BuildContext context) {
     return FlowyButton(
-      text: FlowyText.medium(field.name, fontSize: 12),
+      text: FlowyText.medium(fieldContext.name, fontSize: 12),
       hoverColor: theme.hover,
-      leftIcon: svgWidget(field.fieldType.iconName(), color: theme.iconColor),
+      leftIcon:
+          svgWidget(fieldContext.fieldType.iconName(), color: theme.iconColor),
       onTap: () {
         FieldEditor(
           gridId: gridId,
-          fieldName: field.name,
-          typeOptionLoader: FieldTypeOptionLoader(gridId: gridId, field: field),
+          fieldName: fieldContext.name,
+          typeOptionLoader: FieldTypeOptionLoader(
+            gridId: gridId,
+            field: fieldContext.field,
+          ),
         ).show(context, anchorDirection: AnchorDirection.bottomRight);
       },
     );

+ 4 - 4
frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/grid_setting.dart

@@ -11,17 +11,17 @@ import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 
 import 'package:app_flowy/generated/locale_keys.g.dart';
-import '../../../application/field/field_cache.dart';
+import '../../../application/field/field_controller.dart';
 import '../../layout/sizes.dart';
 import 'grid_property.dart';
 
 class GridSettingContext {
   final String gridId;
-  final GridFieldCache fieldCache;
+  final GridFieldController fieldController;
 
   GridSettingContext({
     required this.gridId,
-    required this.fieldCache,
+    required this.fieldController,
   });
 }
 
@@ -44,7 +44,7 @@ class GridSettingList extends StatelessWidget {
           case GridSettingAction.properties:
             GridPropertyList(
                     gridId: settingContext.gridId,
-                    fieldCache: settingContext.fieldCache)
+                    fieldController: settingContext.fieldController)
                 .show(context);
             break;
         }

+ 4 - 4
frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/grid_toolbar.dart

@@ -5,16 +5,16 @@ import 'package:flowy_infra_ui/style_widget/icon_button.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 
-import '../../../application/field/field_cache.dart';
+import '../../../application/field/field_controller.dart';
 import '../../layout/sizes.dart';
 import 'grid_setting.dart';
 
 class GridToolbarContext {
   final String gridId;
-  final GridFieldCache fieldCache;
+  final GridFieldController fieldController;
   GridToolbarContext({
     required this.gridId,
-    required this.fieldCache,
+    required this.fieldController,
   });
 }
 
@@ -26,7 +26,7 @@ class GridToolbar extends StatelessWidget {
   Widget build(BuildContext context) {
     final settingContext = GridSettingContext(
       gridId: toolbarContext.gridId,
-      fieldCache: toolbarContext.fieldCache,
+      fieldController: toolbarContext.fieldController,
     );
     return SizedBox(
       height: 40,

+ 6 - 6
frontend/app_flowy/lib/startup/deps_resolver.dart

@@ -21,7 +21,7 @@ import 'package:flowy_sdk/protobuf/flowy-user/user_profile.pb.dart';
 import 'package:fluttertoast/fluttertoast.dart';
 import 'package:get_it/get_it.dart';
 
-import '../plugins/grid/application/field/field_cache.dart';
+import '../plugins/grid/application/field/field_controller.dart';
 
 class DependencyResolver {
   static Future<void> resolve(GetIt getIt) async {
@@ -154,10 +154,10 @@ void _resolveGridDeps(GetIt getIt) {
     (view, _) => GridBloc(view: view),
   );
 
-  getIt.registerFactoryParam<GridHeaderBloc, String, GridFieldCache>(
-    (gridId, fieldCache) => GridHeaderBloc(
+  getIt.registerFactoryParam<GridHeaderBloc, String, GridFieldController>(
+    (gridId, fieldController) => GridHeaderBloc(
       gridId: gridId,
-      fieldCache: fieldCache,
+      fieldController: fieldController,
     ),
   );
 
@@ -200,7 +200,7 @@ void _resolveGridDeps(GetIt getIt) {
     ),
   );
 
-  getIt.registerFactoryParam<GridPropertyBloc, String, GridFieldCache>(
-    (gridId, cache) => GridPropertyBloc(gridId: gridId, fieldCache: cache),
+  getIt.registerFactoryParam<GridPropertyBloc, String, GridFieldController>(
+    (gridId, cache) => GridPropertyBloc(gridId: gridId, fieldController: cache),
   );
 }

+ 6 - 0
frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_data.dart

@@ -89,6 +89,12 @@ class AFBoardDataController extends ChangeNotifier
     if (columnIds.isNotEmpty && notify) notifyListeners();
   }
 
+  void clear() {
+    _columnDatas.clear();
+    _columnControllers.clear();
+    notifyListeners();
+  }
+
   AFBoardColumnDataController? getColumnController(String columnId) {
     final columnController = _columnControllers[columnId];
     if (columnController == null) {

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

@@ -13,6 +13,8 @@ pub enum GridNotification {
     DidUpdateField = 50,
     DidUpdateGroupView = 60,
     DidUpdateGroup = 61,
+    DidGroupByNewField = 62,
+    DidUpdateGridSetting = 70,
 }
 
 impl std::default::Default for GridNotification {

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

@@ -10,33 +10,33 @@ use std::convert::TryInto;
 use std::sync::Arc;
 
 #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)]
-pub struct GridFilterConfiguration {
+pub struct GridFilterConfigurationPB {
     #[pb(index = 1)]
     pub id: String,
 }
 
 #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)]
-pub struct RepeatedGridConfigurationFilterPB {
+pub struct RepeatedGridFilterConfigurationPB {
     #[pb(index = 1)]
-    pub items: Vec<GridFilterConfiguration>,
+    pub items: Vec<GridFilterConfigurationPB>,
 }
 
-impl std::convert::From<&FilterConfigurationRevision> for GridFilterConfiguration {
+impl std::convert::From<&FilterConfigurationRevision> for GridFilterConfigurationPB {
     fn from(rev: &FilterConfigurationRevision) -> Self {
         Self { id: rev.id.clone() }
     }
 }
 
-impl std::convert::From<Vec<Arc<FilterConfigurationRevision>>> for RepeatedGridConfigurationFilterPB {
+impl std::convert::From<Vec<Arc<FilterConfigurationRevision>>> for RepeatedGridFilterConfigurationPB {
     fn from(revs: Vec<Arc<FilterConfigurationRevision>>) -> Self {
-        RepeatedGridConfigurationFilterPB {
+        RepeatedGridFilterConfigurationPB {
             items: revs.into_iter().map(|rev| rev.as_ref().into()).collect(),
         }
     }
 }
 
-impl std::convert::From<Vec<GridFilterConfiguration>> for RepeatedGridConfigurationFilterPB {
-    fn from(items: Vec<GridFilterConfiguration>) -> Self {
+impl std::convert::From<Vec<GridFilterConfigurationPB>> for RepeatedGridFilterConfigurationPB {
+    fn from(items: Vec<GridFilterConfigurationPB>) -> Self {
         Self { items }
     }
 }
@@ -78,7 +78,7 @@ pub struct DeleteFilterParams {
 }
 
 #[derive(ProtoBuf, Debug, Default, Clone)]
-pub struct CreateGridFilterPayloadPB {
+pub struct InsertFilterPayloadPB {
     #[pb(index = 1)]
     pub field_id: String,
 
@@ -92,7 +92,7 @@ pub struct CreateGridFilterPayloadPB {
     pub content: Option<String>,
 }
 
-impl CreateGridFilterPayloadPB {
+impl InsertFilterPayloadPB {
     #[allow(dead_code)]
     pub fn new<T: Into<i32>>(field_rev: &FieldRevision, condition: T, content: Option<String>) -> Self {
         Self {
@@ -104,10 +104,10 @@ impl CreateGridFilterPayloadPB {
     }
 }
 
-impl TryInto<CreateFilterParams> for CreateGridFilterPayloadPB {
+impl TryInto<InsertFilterParams> for InsertFilterPayloadPB {
     type Error = ErrorCode;
 
-    fn try_into(self) -> Result<CreateFilterParams, Self::Error> {
+    fn try_into(self) -> Result<InsertFilterParams, Self::Error> {
         let field_id = NotEmptyStr::parse(self.field_id)
             .map_err(|_| ErrorCode::FieldIdIsEmpty)?
             .0;
@@ -130,7 +130,7 @@ impl TryInto<CreateFilterParams> for CreateGridFilterPayloadPB {
             }
         }
 
-        Ok(CreateFilterParams {
+        Ok(InsertFilterParams {
             field_id,
             field_type_rev: self.field_type.into(),
             condition,
@@ -139,7 +139,7 @@ impl TryInto<CreateFilterParams> for CreateGridFilterPayloadPB {
     }
 }
 
-pub struct CreateFilterParams {
+pub struct InsertFilterParams {
     pub field_id: String,
     pub field_type_rev: FieldTypeRevision,
     pub condition: u8,

+ 9 - 5
frontend/rust-lib/flowy-grid/src/entities/group_entities/group.rs

@@ -91,6 +91,9 @@ pub struct GroupPB {
 
     #[pb(index = 5)]
     pub is_default: bool,
+
+    #[pb(index = 6)]
+    pub is_visible: bool,
 }
 
 impl std::convert::From<Group> for GroupPB {
@@ -101,6 +104,7 @@ impl std::convert::From<Group> for GroupPB {
             desc: group.name,
             rows: group.rows,
             is_default: group.is_default,
+            is_visible: group.is_visible,
         }
     }
 }
@@ -126,7 +130,7 @@ impl std::convert::From<Vec<Arc<GroupConfigurationRevision>>> for RepeatedGridGr
 }
 
 #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)]
-pub struct CreateGridGroupPayloadPB {
+pub struct InsertGroupPayloadPB {
     #[pb(index = 1)]
     pub field_id: String,
 
@@ -134,22 +138,22 @@ pub struct CreateGridGroupPayloadPB {
     pub field_type: FieldType,
 }
 
-impl TryInto<CreatGroupParams> for CreateGridGroupPayloadPB {
+impl TryInto<InsertGroupParams> for InsertGroupPayloadPB {
     type Error = ErrorCode;
 
-    fn try_into(self) -> Result<CreatGroupParams, Self::Error> {
+    fn try_into(self) -> Result<InsertGroupParams, Self::Error> {
         let field_id = NotEmptyStr::parse(self.field_id)
             .map_err(|_| ErrorCode::FieldIdIsEmpty)?
             .0;
 
-        Ok(CreatGroupParams {
+        Ok(InsertGroupParams {
             field_id,
             field_type_rev: self.field_type.into(),
         })
     }
 }
 
-pub struct CreatGroupParams {
+pub struct InsertGroupParams {
     pub field_id: String,
     pub field_type_rev: FieldTypeRevision,
 }

+ 8 - 2
frontend/rust-lib/flowy-grid/src/entities/group_entities/group_changeset.rs

@@ -134,15 +134,21 @@ pub struct GroupViewChangesetPB {
     pub inserted_groups: Vec<InsertedGroupPB>,
 
     #[pb(index = 3)]
-    pub deleted_groups: Vec<String>,
+    pub new_groups: Vec<GroupPB>,
 
     #[pb(index = 4)]
+    pub deleted_groups: Vec<String>,
+
+    #[pb(index = 5)]
     pub update_groups: Vec<GroupPB>,
 }
 
 impl GroupViewChangesetPB {
     pub fn is_empty(&self) -> bool {
-        self.inserted_groups.is_empty() && self.deleted_groups.is_empty() && self.update_groups.is_empty()
+        self.new_groups.is_empty()
+            && self.inserted_groups.is_empty()
+            && self.deleted_groups.is_empty()
+            && self.update_groups.is_empty()
     }
 }
 

+ 10 - 11
frontend/rust-lib/flowy-grid/src/entities/setting_entities.rs

@@ -1,13 +1,12 @@
 use crate::entities::{
-    CreatGroupParams, CreateFilterParams, CreateGridFilterPayloadPB, CreateGridGroupPayloadPB, DeleteFilterParams,
-    DeleteFilterPayloadPB, DeleteGroupParams, DeleteGroupPayloadPB, RepeatedGridConfigurationFilterPB,
+    DeleteFilterParams, DeleteFilterPayloadPB, DeleteGroupParams, DeleteGroupPayloadPB, InsertFilterParams,
+    InsertFilterPayloadPB, InsertGroupParams, InsertGroupPayloadPB, RepeatedGridFilterConfigurationPB,
     RepeatedGridGroupConfigurationPB,
 };
 use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
 use flowy_error::ErrorCode;
 use flowy_grid_data_model::parser::NotEmptyStr;
 use flowy_grid_data_model::revision::LayoutRevision;
-use std::collections::HashMap;
 use std::convert::TryInto;
 use strum::IntoEnumIterator;
 use strum_macros::EnumIter;
@@ -19,13 +18,13 @@ pub struct GridSettingPB {
     pub layouts: Vec<GridLayoutPB>,
 
     #[pb(index = 2)]
-    pub current_layout_type: GridLayout,
+    pub layout_type: GridLayout,
 
     #[pb(index = 3)]
-    pub filter_configuration_by_field_id: HashMap<String, RepeatedGridConfigurationFilterPB>,
+    pub filter_configurations: RepeatedGridFilterConfigurationPB,
 
     #[pb(index = 4)]
-    pub group_configuration_by_field_id: HashMap<String, RepeatedGridGroupConfigurationPB>,
+    pub group_configurations: RepeatedGridGroupConfigurationPB,
 }
 
 #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)]
@@ -85,13 +84,13 @@ pub struct GridSettingChangesetPayloadPB {
     pub layout_type: GridLayout,
 
     #[pb(index = 3, one_of)]
-    pub insert_filter: Option<CreateGridFilterPayloadPB>,
+    pub insert_filter: Option<InsertFilterPayloadPB>,
 
     #[pb(index = 4, one_of)]
     pub delete_filter: Option<DeleteFilterPayloadPB>,
 
     #[pb(index = 5, one_of)]
-    pub insert_group: Option<CreateGridGroupPayloadPB>,
+    pub insert_group: Option<InsertGroupPayloadPB>,
 
     #[pb(index = 6, one_of)]
     pub delete_group: Option<DeleteGroupPayloadPB>,
@@ -102,7 +101,7 @@ impl TryInto<GridSettingChangesetParams> for GridSettingChangesetPayloadPB {
 
     fn try_into(self) -> Result<GridSettingChangesetParams, Self::Error> {
         let view_id = NotEmptyStr::parse(self.grid_id)
-            .map_err(|_| ErrorCode::FieldIdIsEmpty)?
+            .map_err(|_| ErrorCode::ViewIdInvalid)?
             .0;
 
         let insert_filter = match self.insert_filter {
@@ -139,9 +138,9 @@ impl TryInto<GridSettingChangesetParams> for GridSettingChangesetPayloadPB {
 pub struct GridSettingChangesetParams {
     pub grid_id: String,
     pub layout_type: LayoutRevision,
-    pub insert_filter: Option<CreateFilterParams>,
+    pub insert_filter: Option<InsertFilterParams>,
     pub delete_filter: Option<DeleteFilterParams>,
-    pub insert_group: Option<CreatGroupParams>,
+    pub insert_group: Option<InsertGroupParams>,
     pub delete_group: Option<DeleteGroupParams>,
 }
 

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

@@ -35,6 +35,32 @@ pub(crate) async fn get_grid_setting_handler(
     data_result(grid_setting)
 }
 
+#[tracing::instrument(level = "trace", skip(data, manager), err)]
+pub(crate) async fn update_grid_setting_handler(
+    data: Data<GridSettingChangesetPayloadPB>,
+    manager: AppData<Arc<GridManager>>,
+) -> Result<(), FlowyError> {
+    let params: GridSettingChangesetParams = data.into_inner().try_into()?;
+
+    let editor = manager.get_grid_editor(&params.grid_id)?;
+    if let Some(insert_params) = params.insert_group {
+        let _ = editor.create_group(insert_params).await?;
+    }
+
+    if let Some(delete_params) = params.delete_group {
+        let _ = editor.delete_group(delete_params).await?;
+    }
+
+    if let Some(create_filter) = params.insert_filter {
+        let _ = editor.create_filter(create_filter).await?;
+    }
+
+    if let Some(delete_filter) = params.delete_filter {
+        let _ = editor.delete_filter(delete_filter).await?;
+    }
+    Ok(())
+}
+
 #[tracing::instrument(level = "debug", skip(data, manager), err)]
 pub(crate) async fn get_grid_blocks_handler(
     data: Data<QueryBlocksPayloadPB>,
@@ -203,12 +229,14 @@ pub(crate) async fn move_field_handler(
 
 /// The FieldMeta contains multiple data, each of them belongs to a specific FieldType.
 async fn get_type_option_data(field_rev: &FieldRevision, field_type: &FieldType) -> FlowyResult<Vec<u8>> {
-    let s = field_rev
-        .get_type_option_str(field_type)
-        .unwrap_or_else(|| default_type_option_builder_from_type(field_type).entry().json_str());
+    let s = field_rev.get_type_option_str(field_type).unwrap_or_else(|| {
+        default_type_option_builder_from_type(field_type)
+            .data_format()
+            .json_str()
+    });
     let field_type: FieldType = field_rev.ty.into();
     let builder = type_option_builder_from_json_str(&s, &field_type);
-    let type_option_data = builder.entry().protobuf_bytes().to_vec();
+    let type_option_data = builder.data_format().protobuf_bytes().to_vec();
 
     Ok(type_option_data)
 }
@@ -337,7 +365,7 @@ pub(crate) async fn update_select_option_handler(
             type_option.delete_option(option);
         }
 
-        mut_field_rev.insert_type_option_entry(&*type_option);
+        mut_field_rev.insert_type_option(&*type_option);
         let _ = editor.replace_field(field_rev).await?;
 
         if let Some(cell_content_changeset) = cell_content_changeset {

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

@@ -11,7 +11,7 @@ pub fn create(grid_manager: Arc<GridManager>) -> Module {
         .event(GridEvent::GetGrid, get_grid_handler)
         .event(GridEvent::GetGridBlocks, get_grid_blocks_handler)
         .event(GridEvent::GetGridSetting, get_grid_setting_handler)
-        // .event(GridEvent::UpdateGridSetting, update_grid_setting_handler)
+        .event(GridEvent::UpdateGridSetting, update_grid_setting_handler)
         // Field
         .event(GridEvent::GetFields, get_fields_handler)
         .event(GridEvent::UpdateField, update_field_handler)
@@ -75,8 +75,8 @@ pub enum GridEvent {
 
     /// [UpdateGridSetting] event is used to update the grid's settings.
     ///
-    /// The event handler accepts [GridIdPB] and return errors if failed to modify the grid's settings.
-    #[event(input = "GridIdPB", input = "GridSettingChangesetPayloadPB")]
+    /// The event handler accepts [GridSettingChangesetPayloadPB] and return errors if failed to modify the grid's settings.
+    #[event(input = "GridSettingChangesetPayloadPB")]
     UpdateGridSetting = 3,
 
     /// [GetFields] event is used to get the grid's settings.
@@ -225,4 +225,7 @@ pub enum GridEvent {
 
     #[event(input = "MoveGroupRowPayloadPB")]
     MoveGroupRow = 112,
+
+    #[event(input = "MoveGroupRowPayloadPB")]
+    GroupByField = 113,
 }

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

@@ -30,7 +30,7 @@ macro_rules! impl_type_option {
     ($target: ident, $field_type:expr) => {
         impl std::convert::From<&FieldRevision> for $target {
             fn from(field_rev: &FieldRevision) -> $target {
-                match field_rev.get_type_option_entry::<$target>($field_type.into()) {
+                match field_rev.get_type_option::<$target>($field_type.into()) {
                     None => $target::default(),
                     Some(target) => target,
                 }
@@ -39,7 +39,7 @@ macro_rules! impl_type_option {
 
         impl std::convert::From<&std::sync::Arc<FieldRevision>> for $target {
             fn from(field_rev: &std::sync::Arc<FieldRevision>) -> $target {
-                match field_rev.get_type_option_entry::<$target>($field_type.into()) {
+                match field_rev.get_type_option::<$target>($field_type.into()) {
                     None => $target::default(),
                     Some(target) => target,
                 }
@@ -52,7 +52,7 @@ macro_rules! impl_type_option {
             }
         }
 
-        impl TypeOptionDataEntry for $target {
+        impl TypeOptionDataFormat for $target {
             fn json_str(&self) -> String {
                 match serde_json::to_string(&self) {
                     Ok(s) => s,

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

@@ -101,25 +101,25 @@ pub fn try_decode_cell_data(
         let field_type: FieldTypeRevision = t_field_type.into();
         let data = match t_field_type {
             FieldType::RichText => field_rev
-                .get_type_option_entry::<RichTextTypeOptionPB>(field_type)?
+                .get_type_option::<RichTextTypeOptionPB>(field_type)?
                 .decode_cell_data(cell_data.into(), s_field_type, field_rev),
             FieldType::Number => field_rev
-                .get_type_option_entry::<NumberTypeOptionPB>(field_type)?
+                .get_type_option::<NumberTypeOptionPB>(field_type)?
                 .decode_cell_data(cell_data.into(), s_field_type, field_rev),
             FieldType::DateTime => field_rev
-                .get_type_option_entry::<DateTypeOptionPB>(field_type)?
+                .get_type_option::<DateTypeOptionPB>(field_type)?
                 .decode_cell_data(cell_data.into(), s_field_type, field_rev),
             FieldType::SingleSelect => field_rev
-                .get_type_option_entry::<SingleSelectTypeOptionPB>(field_type)?
+                .get_type_option::<SingleSelectTypeOptionPB>(field_type)?
                 .decode_cell_data(cell_data.into(), s_field_type, field_rev),
             FieldType::MultiSelect => field_rev
-                .get_type_option_entry::<MultiSelectTypeOptionPB>(field_type)?
+                .get_type_option::<MultiSelectTypeOptionPB>(field_type)?
                 .decode_cell_data(cell_data.into(), s_field_type, field_rev),
             FieldType::Checkbox => field_rev
-                .get_type_option_entry::<CheckboxTypeOptionPB>(field_type)?
+                .get_type_option::<CheckboxTypeOptionPB>(field_type)?
                 .decode_cell_data(cell_data.into(), s_field_type, field_rev),
             FieldType::URL => field_rev
-                .get_type_option_entry::<URLTypeOptionPB>(field_type)?
+                .get_type_option::<URLTypeOptionPB>(field_type)?
                 .decode_cell_data(cell_data.into(), s_field_type, field_rev),
         };
         Some(data)

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

@@ -1,7 +1,7 @@
 use crate::entities::{FieldPB, FieldType};
 use crate::services::field::type_options::*;
 use bytes::Bytes;
-use flowy_grid_data_model::revision::{FieldRevision, TypeOptionDataEntry};
+use flowy_grid_data_model::revision::{FieldRevision, TypeOptionDataFormat};
 use indexmap::IndexMap;
 
 pub struct FieldBuilder {
@@ -78,14 +78,14 @@ impl FieldBuilder {
 
     pub fn build(self) -> FieldRevision {
         let mut field_rev = self.field_rev;
-        field_rev.insert_type_option_entry(self.type_option_builder.entry());
+        field_rev.insert_type_option(self.type_option_builder.data_format());
         field_rev
     }
 }
 
 pub trait TypeOptionBuilder {
     fn field_type(&self) -> FieldType;
-    fn entry(&self) -> &dyn TypeOptionDataEntry;
+    fn data_format(&self) -> &dyn TypeOptionDataFormat;
 }
 
 pub fn default_type_option_builder_from_type(field_type: &FieldType) -> Box<dyn TypeOptionBuilder> {

+ 45 - 0
frontend/rust-lib/flowy-grid/src/services/field/field_operation.rs

@@ -0,0 +1,45 @@
+use crate::services::field::{MultiSelectTypeOptionPB, SingleSelectTypeOptionPB};
+use crate::services::grid_editor::GridRevisionEditor;
+use flowy_error::FlowyResult;
+use flowy_grid_data_model::revision::{TypeOptionDataDeserializer, TypeOptionDataFormat};
+use std::sync::Arc;
+
+pub async fn edit_field_type_option<T>(
+    field_id: &str,
+    editor: Arc<GridRevisionEditor>,
+    action: impl FnOnce(&mut T),
+) -> FlowyResult<()>
+where
+    T: TypeOptionDataDeserializer + TypeOptionDataFormat,
+{
+    let get_type_option = async {
+        let field_rev = editor.get_field_rev(field_id).await?;
+        field_rev.get_type_option::<T>(field_rev.ty)
+    };
+
+    if let Some(mut type_option) = get_type_option.await {
+        action(&mut type_option);
+        let bytes = type_option.protobuf_bytes().to_vec();
+        let _ = editor
+            .update_field_type_option(&editor.grid_id, field_id, bytes)
+            .await?;
+    }
+
+    Ok(())
+}
+
+pub async fn edit_single_select_type_option(
+    field_id: &str,
+    editor: Arc<GridRevisionEditor>,
+    action: impl FnOnce(&mut SingleSelectTypeOptionPB),
+) -> FlowyResult<()> {
+    edit_field_type_option(field_id, editor, action).await
+}
+
+pub async fn edit_multi_select_type_option(
+    field_id: &str,
+    editor: Arc<GridRevisionEditor>,
+    action: impl FnOnce(&mut MultiSelectTypeOptionPB),
+) -> FlowyResult<()> {
+    edit_field_type_option(field_id, editor, action).await
+}

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

@@ -1,5 +1,7 @@
 mod field_builder;
+mod field_operation;
 pub(crate) mod type_options;
 
 pub use field_builder::*;
+pub use field_operation::*;
 pub use type_options::*;

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

@@ -5,7 +5,7 @@ use crate::services::field::{BoxTypeOptionBuilder, CheckboxCellData, TypeOptionB
 use bytes::Bytes;
 use flowy_derive::ProtoBuf;
 use flowy_error::{FlowyError, FlowyResult};
-use flowy_grid_data_model::revision::{CellRevision, FieldRevision, TypeOptionDataDeserializer, TypeOptionDataEntry};
+use flowy_grid_data_model::revision::{CellRevision, FieldRevision, TypeOptionDataDeserializer, TypeOptionDataFormat};
 use serde::{Deserialize, Serialize};
 use std::str::FromStr;
 
@@ -26,7 +26,7 @@ impl TypeOptionBuilder for CheckboxTypeOptionBuilder {
         FieldType::Checkbox
     }
 
-    fn entry(&self) -> &dyn TypeOptionDataEntry {
+    fn data_format(&self) -> &dyn TypeOptionDataFormat {
         &self.0
     }
 }

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

@@ -9,7 +9,7 @@ use chrono::format::strftime::StrftimeItems;
 use chrono::{NaiveDateTime, Timelike};
 use flowy_derive::ProtoBuf;
 use flowy_error::{ErrorCode, FlowyError, FlowyResult};
-use flowy_grid_data_model::revision::{CellRevision, FieldRevision, TypeOptionDataDeserializer, TypeOptionDataEntry};
+use flowy_grid_data_model::revision::{CellRevision, FieldRevision, TypeOptionDataDeserializer, TypeOptionDataFormat};
 use serde::{Deserialize, Serialize};
 
 // Date
@@ -189,7 +189,7 @@ impl TypeOptionBuilder for DateTypeOptionBuilder {
         FieldType::DateTime
     }
 
-    fn entry(&self) -> &dyn TypeOptionDataEntry {
+    fn data_format(&self) -> &dyn TypeOptionDataFormat {
         &self.0
     }
 }

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

@@ -6,7 +6,7 @@ use crate::services::field::{BoxTypeOptionBuilder, NumberCellData, TypeOptionBui
 use bytes::Bytes;
 use flowy_derive::ProtoBuf;
 use flowy_error::{FlowyError, FlowyResult};
-use flowy_grid_data_model::revision::{CellRevision, FieldRevision, TypeOptionDataDeserializer, TypeOptionDataEntry};
+use flowy_grid_data_model::revision::{CellRevision, FieldRevision, TypeOptionDataDeserializer, TypeOptionDataFormat};
 
 use rust_decimal::Decimal;
 
@@ -45,7 +45,7 @@ impl TypeOptionBuilder for NumberTypeOptionBuilder {
         FieldType::Number
     }
 
-    fn entry(&self) -> &dyn TypeOptionDataEntry {
+    fn data_format(&self) -> &dyn TypeOptionDataFormat {
         &self.0
     }
 }

+ 2 - 2
frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/multi_select_type_option.rs

@@ -9,7 +9,7 @@ use crate::services::field::{
 use bytes::Bytes;
 use flowy_derive::ProtoBuf;
 use flowy_error::{FlowyError, FlowyResult};
-use flowy_grid_data_model::revision::{CellRevision, FieldRevision, TypeOptionDataDeserializer, TypeOptionDataEntry};
+use flowy_grid_data_model::revision::{CellRevision, FieldRevision, TypeOptionDataDeserializer, TypeOptionDataFormat};
 use serde::{Deserialize, Serialize};
 
 // Multiple select
@@ -108,7 +108,7 @@ impl TypeOptionBuilder for MultiSelectTypeOptionBuilder {
         FieldType::MultiSelect
     }
 
-    fn entry(&self) -> &dyn TypeOptionDataEntry {
+    fn data_format(&self) -> &dyn TypeOptionDataFormat {
         &self.0
     }
 }

+ 2 - 2
frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/select_option.rs

@@ -5,7 +5,7 @@ use bytes::Bytes;
 use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
 use flowy_error::{internal_error, ErrorCode, FlowyResult};
 use flowy_grid_data_model::parser::NotEmptyStr;
-use flowy_grid_data_model::revision::{FieldRevision, TypeOptionDataEntry};
+use flowy_grid_data_model::revision::{FieldRevision, TypeOptionDataFormat};
 use nanoid::nanoid;
 use serde::{Deserialize, Serialize};
 
@@ -75,7 +75,7 @@ pub fn make_selected_select_options(
     }
 }
 
-pub trait SelectOptionOperation: TypeOptionDataEntry + Send + Sync {
+pub trait SelectOptionOperation: TypeOptionDataFormat + Send + Sync {
     fn insert_option(&mut self, new_option: SelectOptionPB) {
         let options = self.mut_options();
         if let Some(index) = options

+ 2 - 2
frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/single_select_type_option.rs

@@ -9,7 +9,7 @@ use crate::services::field::{BoxTypeOptionBuilder, TypeOptionBuilder};
 use bytes::Bytes;
 use flowy_derive::ProtoBuf;
 use flowy_error::{FlowyError, FlowyResult};
-use flowy_grid_data_model::revision::{CellRevision, FieldRevision, TypeOptionDataDeserializer, TypeOptionDataEntry};
+use flowy_grid_data_model::revision::{CellRevision, FieldRevision, TypeOptionDataDeserializer, TypeOptionDataFormat};
 use serde::{Deserialize, Serialize};
 
 // Single select
@@ -91,7 +91,7 @@ impl TypeOptionBuilder for SingleSelectTypeOptionBuilder {
         FieldType::SingleSelect
     }
 
-    fn entry(&self) -> &dyn TypeOptionDataEntry {
+    fn data_format(&self) -> &dyn TypeOptionDataFormat {
         &self.0
     }
 }

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

@@ -8,7 +8,7 @@ use crate::services::field::{BoxTypeOptionBuilder, TypeOptionBuilder};
 use bytes::Bytes;
 use flowy_derive::ProtoBuf;
 use flowy_error::{FlowyError, FlowyResult};
-use flowy_grid_data_model::revision::{CellRevision, FieldRevision, TypeOptionDataDeserializer, TypeOptionDataEntry};
+use flowy_grid_data_model::revision::{CellRevision, FieldRevision, TypeOptionDataDeserializer, TypeOptionDataFormat};
 use serde::{Deserialize, Serialize};
 
 #[derive(Default)]
@@ -21,7 +21,7 @@ impl TypeOptionBuilder for RichTextTypeOptionBuilder {
         FieldType::RichText
     }
 
-    fn entry(&self) -> &dyn TypeOptionDataEntry {
+    fn data_format(&self) -> &dyn TypeOptionDataFormat {
         &self.0
     }
 }

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

@@ -6,7 +6,7 @@ use bytes::Bytes;
 use fancy_regex::Regex;
 use flowy_derive::ProtoBuf;
 use flowy_error::{FlowyError, FlowyResult};
-use flowy_grid_data_model::revision::{CellRevision, FieldRevision, TypeOptionDataDeserializer, TypeOptionDataEntry};
+use flowy_grid_data_model::revision::{CellRevision, FieldRevision, TypeOptionDataDeserializer, TypeOptionDataFormat};
 use lazy_static::lazy_static;
 use serde::{Deserialize, Serialize};
 
@@ -20,7 +20,7 @@ impl TypeOptionBuilder for URLTypeOptionBuilder {
         FieldType::URL
     }
 
-    fn entry(&self) -> &dyn TypeOptionDataEntry {
+    fn data_format(&self) -> &dyn TypeOptionDataFormat {
         &self.0
     }
 }

+ 7 - 7
frontend/rust-lib/flowy-grid/src/services/filter/filter_service.rs

@@ -188,7 +188,7 @@ fn filter_cell(
         FieldType::RichText => filter_cache.text_filter.get(&filter_id).and_then(|filter| {
             Some(
                 field_rev
-                    .get_type_option_entry::<RichTextTypeOptionPB>(field_type_rev)?
+                    .get_type_option::<RichTextTypeOptionPB>(field_type_rev)?
                     .apply_filter(any_cell_data, filter.value())
                     .ok(),
             )
@@ -196,7 +196,7 @@ fn filter_cell(
         FieldType::Number => filter_cache.number_filter.get(&filter_id).and_then(|filter| {
             Some(
                 field_rev
-                    .get_type_option_entry::<NumberTypeOptionPB>(field_type_rev)?
+                    .get_type_option::<NumberTypeOptionPB>(field_type_rev)?
                     .apply_filter(any_cell_data, filter.value())
                     .ok(),
             )
@@ -204,7 +204,7 @@ fn filter_cell(
         FieldType::DateTime => filter_cache.date_filter.get(&filter_id).and_then(|filter| {
             Some(
                 field_rev
-                    .get_type_option_entry::<DateTypeOptionPB>(field_type_rev)?
+                    .get_type_option::<DateTypeOptionPB>(field_type_rev)?
                     .apply_filter(any_cell_data, filter.value())
                     .ok(),
             )
@@ -212,7 +212,7 @@ fn filter_cell(
         FieldType::SingleSelect => filter_cache.select_option_filter.get(&filter_id).and_then(|filter| {
             Some(
                 field_rev
-                    .get_type_option_entry::<SingleSelectTypeOptionPB>(field_type_rev)?
+                    .get_type_option::<SingleSelectTypeOptionPB>(field_type_rev)?
                     .apply_filter(any_cell_data, filter.value())
                     .ok(),
             )
@@ -220,7 +220,7 @@ fn filter_cell(
         FieldType::MultiSelect => filter_cache.select_option_filter.get(&filter_id).and_then(|filter| {
             Some(
                 field_rev
-                    .get_type_option_entry::<MultiSelectTypeOptionPB>(field_type_rev)?
+                    .get_type_option::<MultiSelectTypeOptionPB>(field_type_rev)?
                     .apply_filter(any_cell_data, filter.value())
                     .ok(),
             )
@@ -228,7 +228,7 @@ fn filter_cell(
         FieldType::Checkbox => filter_cache.checkbox_filter.get(&filter_id).and_then(|filter| {
             Some(
                 field_rev
-                    .get_type_option_entry::<CheckboxTypeOptionPB>(field_type_rev)?
+                    .get_type_option::<CheckboxTypeOptionPB>(field_type_rev)?
                     .apply_filter(any_cell_data, filter.value())
                     .ok(),
             )
@@ -236,7 +236,7 @@ fn filter_cell(
         FieldType::URL => filter_cache.url_filter.get(&filter_id).and_then(|filter| {
             Some(
                 field_rev
-                    .get_type_option_entry::<URLTypeOptionPB>(field_type_rev)?
+                    .get_type_option::<URLTypeOptionPB>(field_type_rev)?
                     .apply_filter(any_cell_data, filter.value())
                     .ok(),
             )

+ 24 - 5
frontend/rust-lib/flowy-grid/src/services/grid_editor.rs

@@ -179,6 +179,10 @@ impl GridRevisionEditor {
             None => Err(ErrorCode::FieldDoesNotExist.into()),
             Some(field_type) => {
                 let _ = self.update_field_rev(params, field_type).await?;
+                match self.view_manager.did_update_field(&field_id).await {
+                    Ok(_) => {}
+                    Err(e) => tracing::error!("View manager update field failed: {:?}", e),
+                }
                 let _ = self.notify_did_update_grid_field(&field_id).await?;
                 Ok(())
             }
@@ -207,6 +211,11 @@ impl GridRevisionEditor {
         Ok(())
     }
 
+    pub async fn group_by_field(&self, field_id: &str) -> FlowyResult<()> {
+        let _ = self.view_manager.group_by_field(field_id).await?;
+        Ok(())
+    }
+
     pub async fn switch_to_field_type(&self, field_id: &str, field_type: &FieldType) -> FlowyResult<()> {
         // let block_ids = self
         //     .get_block_metas()
@@ -221,7 +230,9 @@ impl GridRevisionEditor {
 
         let type_option_json_builder = |field_type: &FieldTypeRevision| -> String {
             let field_type: FieldType = field_type.into();
-            return default_type_option_builder_from_type(&field_type).entry().json_str();
+            return default_type_option_builder_from_type(&field_type)
+                .data_format()
+                .json_str();
         };
 
         let _ = self
@@ -521,12 +532,20 @@ impl GridRevisionEditor {
         self.view_manager.get_setting().await
     }
 
-    pub async fn get_grid_filter(&self) -> FlowyResult<Vec<GridFilterConfiguration>> {
+    pub async fn get_grid_filter(&self) -> FlowyResult<Vec<GridFilterConfigurationPB>> {
         self.view_manager.get_filters().await
     }
 
-    pub async fn update_filter(&self, params: CreateFilterParams) -> FlowyResult<()> {
-        let _ = self.view_manager.update_filter(params).await?;
+    pub async fn create_group(&self, params: InsertGroupParams) -> FlowyResult<()> {
+        self.view_manager.insert_or_update_group(params).await
+    }
+
+    pub async fn delete_group(&self, params: DeleteGroupParams) -> FlowyResult<()> {
+        self.view_manager.delete_group(params).await
+    }
+
+    pub async fn create_filter(&self, params: InsertFilterParams) -> FlowyResult<()> {
+        let _ = self.view_manager.insert_or_update_filter(params).await?;
         Ok(())
     }
 
@@ -824,7 +843,7 @@ impl JsonDeserializer for TypeOptionJsonDeserializer {
     fn deserialize(&self, type_option_data: Vec<u8>) -> CollaborateResult<String> {
         // The type_option_data sent from Dart is serialized by protobuf.
         let builder = type_option_builder_from_bytes(type_option_data, &self.0);
-        let json = builder.entry().json_str();
+        let json = builder.data_format().json_str();
         tracing::trace!("Deserialize type option data to: {}", json);
         Ok(json)
     }

+ 225 - 106
frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs

@@ -1,12 +1,16 @@
 use crate::dart_notification::{send_dart_notification, GridNotification};
 use crate::entities::{
-    CreateFilterParams, CreateRowParams, DeleteFilterParams, GridFilterConfiguration, GridLayout, GridLayoutPB,
-    GridSettingPB, GroupChangesetPB, GroupPB, GroupViewChangesetPB, InsertedGroupPB, InsertedRowPB, MoveGroupParams,
-    RepeatedGridConfigurationFilterPB, RepeatedGridGroupConfigurationPB, RowPB,
+    CreateRowParams, DeleteFilterParams, DeleteGroupParams, GridFilterConfigurationPB, GridGroupConfigurationPB,
+    GridLayout, GridLayoutPB, GridSettingPB, GroupChangesetPB, GroupPB, GroupViewChangesetPB, InsertFilterParams,
+    InsertGroupParams, InsertedGroupPB, InsertedRowPB, MoveGroupParams, RepeatedGridFilterConfigurationPB,
+    RepeatedGridGroupConfigurationPB, RowPB,
 };
 use crate::services::grid_editor_task::GridServiceTaskScheduler;
 use crate::services::grid_view_manager::{GridViewFieldDelegate, GridViewRowDelegate};
-use crate::services::group::{GroupConfigurationReader, GroupConfigurationWriter, GroupService};
+use crate::services::group::{
+    default_group_configuration, find_group_field, make_group_controller, GroupConfigurationReader,
+    GroupConfigurationWriter, GroupController, MoveGroupRowContext,
+};
 use flowy_error::{FlowyError, FlowyResult};
 use flowy_grid_data_model::revision::{
     gen_grid_filter_id, FieldRevision, FieldTypeRevision, FilterConfigurationRevision, GroupConfigurationRevision,
@@ -16,9 +20,7 @@ use flowy_revision::{RevisionCloudService, RevisionManager, RevisionObjectBuilde
 use flowy_sync::client_grid::{GridViewRevisionChangeset, GridViewRevisionPad};
 use flowy_sync::entities::revision::Revision;
 use lib_infra::future::{wrap_future, AFFuture, FutureResult};
-use std::collections::HashMap;
-
-use std::sync::atomic::{AtomicBool, Ordering};
+use std::future::Future;
 use std::sync::Arc;
 use tokio::sync::RwLock;
 
@@ -30,11 +32,9 @@ pub struct GridViewRevisionEditor {
     rev_manager: Arc<RevisionManager>,
     field_delegate: Arc<dyn GridViewFieldDelegate>,
     row_delegate: Arc<dyn GridViewRowDelegate>,
-    group_service: Arc<RwLock<GroupService>>,
+    group_controller: Arc<RwLock<Box<dyn GroupController>>>,
     scheduler: Arc<dyn GridServiceTaskScheduler>,
-    did_load_group: AtomicBool,
 }
-
 impl GridViewRevisionEditor {
     #[tracing::instrument(level = "trace", skip_all, err)]
     pub(crate) async fn new(
@@ -52,16 +52,16 @@ impl GridViewRevisionEditor {
         let view_revision_pad = rev_manager.load::<GridViewRevisionPadBuilder>(Some(cloud)).await?;
         let pad = Arc::new(RwLock::new(view_revision_pad));
         let rev_manager = Arc::new(rev_manager);
-
-        let configuration_reader = GroupConfigurationReaderImpl(pad.clone());
-        let configuration_writer = GroupConfigurationWriterImpl {
-            user_id: user_id.to_owned(),
-            rev_manager: rev_manager.clone(),
-            view_pad: pad.clone(),
-        };
-        let group_service = GroupService::new(view_id.clone(), configuration_reader, configuration_writer).await;
+        let group_controller = new_group_controller(
+            user_id.to_owned(),
+            view_id.clone(),
+            pad.clone(),
+            rev_manager.clone(),
+            field_delegate.clone(),
+            row_delegate.clone(),
+        )
+        .await?;
         let user_id = user_id.to_owned();
-        let did_load_group = AtomicBool::new(false);
         Ok(Self {
             pad,
             user_id,
@@ -70,24 +70,21 @@ impl GridViewRevisionEditor {
             scheduler,
             field_delegate,
             row_delegate,
-            group_service: Arc::new(RwLock::new(group_service)),
-            did_load_group,
+            group_controller: Arc::new(RwLock::new(group_controller)),
         })
     }
 
     pub(crate) async fn will_create_row(&self, row_rev: &mut RowRevision, params: &CreateRowParams) {
-        match params.group_id.as_ref() {
-            None => {}
-            Some(group_id) => {
-                self.group_service
-                    .write()
-                    .await
-                    .will_create_row(row_rev, group_id, |field_id| {
-                        self.field_delegate.get_field_rev(&field_id)
-                    })
-                    .await;
-            }
+        if params.group_id.is_none() {
+            return;
         }
+        let group_id = params.group_id.as_ref().unwrap();
+        let _ = self
+            .mut_group_controller(|group_controller, field_rev| {
+                group_controller.will_create_row(row_rev, &field_rev, group_id);
+                Ok(())
+            })
+            .await;
     }
 
     pub(crate) async fn did_create_row(&self, row_pb: &RowPB, params: &CreateRowParams) {
@@ -112,13 +109,11 @@ impl GridViewRevisionEditor {
 
     pub(crate) async fn did_delete_row(&self, row_rev: &RowRevision) {
         // Send the group notification if the current view has groups;
-        if let Some(changesets) = self
-            .group_service
-            .write()
-            .await
-            .did_delete_row(row_rev, |field_id| self.field_delegate.get_field_rev(&field_id))
-            .await
-        {
+        let changesets = self
+            .mut_group_controller(|group_controller, field_rev| group_controller.did_delete_row(row_rev, &field_rev))
+            .await;
+
+        if let Some(changesets) = changesets {
             for changeset in changesets {
                 self.notify_did_update_group(changeset).await;
             }
@@ -126,13 +121,11 @@ impl GridViewRevisionEditor {
     }
 
     pub(crate) async fn did_update_row(&self, row_rev: &RowRevision) {
-        if let Some(changesets) = self
-            .group_service
-            .write()
-            .await
-            .did_update_row(row_rev, |field_id| self.field_delegate.get_field_rev(&field_id))
-            .await
-        {
+        let changesets = self
+            .mut_group_controller(|group_controller, field_rev| group_controller.did_update_row(row_rev, &field_rev))
+            .await;
+
+        if let Some(changesets) = changesets {
             for changeset in changesets {
                 self.notify_did_update_group(changeset).await;
             }
@@ -146,54 +139,38 @@ impl GridViewRevisionEditor {
         to_group_id: &str,
         to_row_id: Option<String>,
     ) -> Vec<GroupChangesetPB> {
-        match self
-            .group_service
-            .write()
-            .await
-            .move_group_row(row_rev, row_changeset, to_group_id, to_row_id, |field_id| {
-                self.field_delegate.get_field_rev(&field_id)
+        let changesets = self
+            .mut_group_controller(|group_controller, field_rev| {
+                let move_row_context = MoveGroupRowContext {
+                    row_rev,
+                    row_changeset,
+                    field_rev: field_rev.as_ref(),
+                    to_group_id,
+                    to_row_id,
+                };
+
+                let changesets = group_controller.move_group_row(move_row_context)?;
+                Ok(changesets)
             })
-            .await
-        {
-            None => vec![],
-            Some(changesets) => changesets,
-        }
+            .await;
+
+        changesets.unwrap_or_default()
     }
     /// Only call once after grid view editor initialized
     #[tracing::instrument(level = "trace", skip(self))]
     pub(crate) async fn load_groups(&self) -> FlowyResult<Vec<GroupPB>> {
-        let groups = if !self.did_load_group.load(Ordering::SeqCst) {
-            self.did_load_group.store(true, Ordering::SeqCst);
-            let field_revs = self.field_delegate.get_field_revs().await;
-            let row_revs = self.row_delegate.gv_row_revs().await;
-
-            match self
-                .group_service
-                .write()
-                .await
-                .load_groups(&field_revs, row_revs)
-                .await
-            {
-                None => vec![],
-                Some(groups) => groups,
-            }
-        } else {
-            self.group_service.read().await.groups().await
-        };
-
+        let groups = self.group_controller.read().await.groups();
         tracing::trace!("Number of groups: {}", groups.len());
         Ok(groups.into_iter().map(GroupPB::from).collect())
     }
 
     pub(crate) async fn move_group(&self, params: MoveGroupParams) -> FlowyResult<()> {
         let _ = self
-            .group_service
+            .group_controller
             .write()
             .await
-            .move_group(&params.from_group_id, &params.to_group_id)
-            .await?;
-
-        match self.group_service.read().await.get_group(&params.from_group_id).await {
+            .move_group(&params.from_group_id, &params.to_group_id)?;
+        match self.group_controller.read().await.get_group(&params.from_group_id) {
             None => {}
             Some((index, group)) => {
                 let inserted_group = InsertedGroupPB {
@@ -206,6 +183,7 @@ impl GridViewRevisionEditor {
                     inserted_groups: vec![inserted_group],
                     deleted_groups: vec![params.from_group_id.clone()],
                     update_groups: vec![],
+                    new_groups: vec![],
                 };
 
                 self.notify_did_update_view(changeset).await;
@@ -220,27 +198,52 @@ impl GridViewRevisionEditor {
         grid_setting
     }
 
-    pub(crate) async fn get_filters(&self) -> Vec<GridFilterConfiguration> {
+    pub(crate) async fn get_filters(&self) -> Vec<GridFilterConfigurationPB> {
         let field_revs = self.field_delegate.get_field_revs().await;
         match self.pad.read().await.get_all_filters(&field_revs) {
             None => vec![],
             Some(filters) => filters
                 .into_values()
                 .flatten()
-                .map(|filter| GridFilterConfiguration::from(filter.as_ref()))
+                .map(|filter| GridFilterConfigurationPB::from(filter.as_ref()))
                 .collect(),
         }
     }
 
-    pub(crate) async fn insert_filter(&self, insert_filter: CreateFilterParams) -> FlowyResult<()> {
+    pub(crate) async fn insert_group(&self, params: InsertGroupParams) -> FlowyResult<()> {
+        if let Some(field_rev) = self.field_delegate.get_field_rev(&params.field_id).await {
+            let _ = self
+                .modify(|pad| {
+                    let configuration = default_group_configuration(&field_rev);
+                    let changeset = pad.insert_group(&params.field_id, &params.field_type_rev, configuration)?;
+                    Ok(changeset)
+                })
+                .await?;
+        }
+        if self.group_controller.read().await.field_id() != params.field_id {
+            let _ = self.group_by_field(&params.field_id).await?;
+            self.notify_did_update_setting().await;
+        }
+        Ok(())
+    }
+
+    pub(crate) async fn delete_group(&self, params: DeleteGroupParams) -> FlowyResult<()> {
+        self.modify(|pad| {
+            let changeset = pad.delete_filter(&params.field_id, &params.field_type_rev, &params.group_id)?;
+            Ok(changeset)
+        })
+        .await
+    }
+
+    pub(crate) async fn insert_filter(&self, params: InsertFilterParams) -> FlowyResult<()> {
         self.modify(|pad| {
             let filter_rev = FilterConfigurationRevision {
                 id: gen_grid_filter_id(),
-                field_id: insert_filter.field_id.clone(),
-                condition: insert_filter.condition,
-                content: insert_filter.content,
+                field_id: params.field_id.clone(),
+                condition: params.condition,
+                content: params.content,
             };
-            let changeset = pad.insert_filter(&insert_filter.field_id, &insert_filter.field_type_rev, filter_rev)?;
+            let changeset = pad.insert_filter(&params.field_id, &params.field_type_rev, filter_rev)?;
             Ok(changeset)
         })
         .await
@@ -260,7 +263,7 @@ impl GridViewRevisionEditor {
     #[tracing::instrument(level = "trace", skip_all, err)]
     pub(crate) async fn did_update_field(&self, field_id: &str) -> FlowyResult<()> {
         if let Some(field_rev) = self.field_delegate.get_field_rev(field_id).await {
-            match self.group_service.write().await.did_update_field(&field_rev).await? {
+            match self.group_controller.write().await.did_update_field(&field_rev)? {
                 None => {}
                 Some(changeset) => {
                     self.notify_did_update_view(changeset).await;
@@ -270,6 +273,44 @@ impl GridViewRevisionEditor {
         Ok(())
     }
 
+    pub(crate) async fn group_by_field(&self, field_id: &str) -> FlowyResult<()> {
+        if let Some(field_rev) = self.field_delegate.get_field_rev(field_id).await {
+            let new_group_controller = new_group_controller_with_field_rev(
+                self.user_id.clone(),
+                self.view_id.clone(),
+                self.pad.clone(),
+                self.rev_manager.clone(),
+                field_rev,
+                self.row_delegate.clone(),
+            )
+            .await?;
+
+            let new_groups = new_group_controller.groups().into_iter().map(GroupPB::from).collect();
+
+            *self.group_controller.write().await = new_group_controller;
+            let changeset = GroupViewChangesetPB {
+                view_id: self.view_id.clone(),
+                new_groups,
+                ..Default::default()
+            };
+
+            debug_assert!(!changeset.is_empty());
+            if !changeset.is_empty() {
+                send_dart_notification(&changeset.view_id, GridNotification::DidGroupByNewField)
+                    .payload(changeset)
+                    .send();
+            }
+        }
+        Ok(())
+    }
+
+    async fn notify_did_update_setting(&self) {
+        let setting = self.get_setting().await;
+        send_dart_notification(&self.view_id, GridNotification::DidUpdateGridSetting)
+            .payload(setting)
+            .send();
+    }
+
     pub async fn notify_did_update_group(&self, changeset: GroupChangesetPB) {
         send_dart_notification(&changeset.group_id, GridNotification::DidUpdateGroup)
             .payload(changeset)
@@ -295,6 +336,78 @@ impl GridViewRevisionEditor {
         }
         Ok(())
     }
+
+    async fn mut_group_controller<F, T>(&self, f: F) -> Option<T>
+    where
+        F: FnOnce(&mut Box<dyn GroupController>, Arc<FieldRevision>) -> FlowyResult<T>,
+    {
+        let group_field_id = self.group_controller.read().await.field_id().to_owned();
+        match self.field_delegate.get_field_rev(&group_field_id).await {
+            None => None,
+            Some(field_rev) => {
+                let mut write_guard = self.group_controller.write().await;
+                f(&mut write_guard, field_rev).ok()
+            }
+        }
+    }
+
+    #[allow(dead_code)]
+    async fn async_mut_group_controller<F, O, T>(&self, f: F) -> Option<T>
+    where
+        F: FnOnce(Arc<RwLock<Box<dyn GroupController>>>, Arc<FieldRevision>) -> O,
+        O: Future<Output = FlowyResult<T>> + Sync + 'static,
+    {
+        let group_field_id = self.group_controller.read().await.field_id().to_owned();
+        match self.field_delegate.get_field_rev(&group_field_id).await {
+            None => None,
+            Some(field_rev) => {
+                let _write_guard = self.group_controller.write().await;
+                f(self.group_controller.clone(), field_rev).await.ok()
+            }
+        }
+    }
+}
+async fn new_group_controller(
+    user_id: String,
+    view_id: String,
+    pad: Arc<RwLock<GridViewRevisionPad>>,
+    rev_manager: Arc<RevisionManager>,
+    field_delegate: Arc<dyn GridViewFieldDelegate>,
+    row_delegate: Arc<dyn GridViewRowDelegate>,
+) -> FlowyResult<Box<dyn GroupController>> {
+    let configuration_reader = GroupConfigurationReaderImpl(pad.clone());
+    let field_revs = field_delegate.get_field_revs().await;
+    // Read the group field or find a new group field
+    let field_rev = configuration_reader
+        .get_configuration()
+        .await
+        .and_then(|configuration| {
+            field_revs
+                .iter()
+                .find(|field_rev| field_rev.id == configuration.field_id)
+                .cloned()
+        })
+        .unwrap_or_else(|| find_group_field(&field_revs).unwrap());
+
+    new_group_controller_with_field_rev(user_id, view_id, pad, rev_manager, field_rev, row_delegate).await
+}
+
+async fn new_group_controller_with_field_rev(
+    user_id: String,
+    view_id: String,
+    pad: Arc<RwLock<GridViewRevisionPad>>,
+    rev_manager: Arc<RevisionManager>,
+    field_rev: Arc<FieldRevision>,
+    row_delegate: Arc<dyn GridViewRowDelegate>,
+) -> FlowyResult<Box<dyn GroupController>> {
+    let configuration_reader = GroupConfigurationReaderImpl(pad.clone());
+    let configuration_writer = GroupConfigurationWriterImpl {
+        user_id,
+        rev_manager,
+        view_pad: pad,
+    };
+    let row_revs = row_delegate.gv_row_revs().await;
+    make_group_controller(view_id, field_rev, row_revs, configuration_reader, configuration_writer).await
 }
 
 async fn apply_change(
@@ -335,13 +448,10 @@ impl RevisionObjectBuilder for GridViewRevisionPadBuilder {
 struct GroupConfigurationReaderImpl(Arc<RwLock<GridViewRevisionPad>>);
 
 impl GroupConfigurationReader for GroupConfigurationReaderImpl {
-    fn get_group_configuration(
-        &self,
-        field_rev: Arc<FieldRevision>,
-    ) -> AFFuture<Option<Arc<GroupConfigurationRevision>>> {
+    fn get_configuration(&self) -> AFFuture<Option<Arc<GroupConfigurationRevision>>> {
         let view_pad = self.0.clone();
         wrap_future(async move {
-            let mut groups = view_pad.read().await.groups.get_objects(&field_rev.id, &field_rev.ty)?;
+            let mut groups = view_pad.read().await.get_all_groups();
             if groups.is_empty() {
                 None
             } else {
@@ -359,7 +469,7 @@ struct GroupConfigurationWriterImpl {
 }
 
 impl GroupConfigurationWriter for GroupConfigurationWriterImpl {
-    fn save_group_configuration(
+    fn save_configuration(
         &self,
         field_id: &str,
         field_type: FieldTypeRevision,
@@ -385,31 +495,40 @@ impl GroupConfigurationWriter for GroupConfigurationWriterImpl {
 }
 
 pub fn make_grid_setting(view_pad: &GridViewRevisionPad, field_revs: &[Arc<FieldRevision>]) -> GridSettingPB {
-    let current_layout_type: GridLayout = view_pad.layout.clone().into();
-    let filters_by_field_id = view_pad
+    let layout_type: GridLayout = view_pad.layout.clone().into();
+    let filter_configurations = view_pad
         .get_all_filters(field_revs)
         .map(|filters_by_field_id| {
             filters_by_field_id
                 .into_iter()
-                .map(|(k, v)| (k, v.into()))
-                .collect::<HashMap<String, RepeatedGridConfigurationFilterPB>>()
+                .map(|(_, v)| {
+                    let repeated_filter: RepeatedGridFilterConfigurationPB = v.into();
+                    repeated_filter.items
+                })
+                .flatten()
+                .collect::<Vec<GridFilterConfigurationPB>>()
         })
         .unwrap_or_default();
-    let groups_by_field_id = view_pad
-        .get_all_groups(field_revs)
+
+    let group_configurations = view_pad
+        .get_groups_by_field_revs(field_revs)
         .map(|groups_by_field_id| {
             groups_by_field_id
                 .into_iter()
-                .map(|(k, v)| (k, v.into()))
-                .collect::<HashMap<String, RepeatedGridGroupConfigurationPB>>()
+                .map(|(_, v)| {
+                    let repeated_group: RepeatedGridGroupConfigurationPB = v.into();
+                    repeated_group.items
+                })
+                .flatten()
+                .collect::<Vec<GridGroupConfigurationPB>>()
         })
         .unwrap_or_default();
 
     GridSettingPB {
         layouts: GridLayoutPB::all(),
-        current_layout_type,
-        filter_configuration_by_field_id: filters_by_field_id,
-        group_configuration_by_field_id: groups_by_field_id,
+        layout_type,
+        filter_configurations: filter_configurations.into(),
+        group_configurations: group_configurations.into(),
     }
 }
 

+ 23 - 7
frontend/rust-lib/flowy-grid/src/services/grid_view_manager.rs

@@ -1,6 +1,6 @@
 use crate::entities::{
-    CreateFilterParams, CreateRowParams, DeleteFilterParams, GridFilterConfiguration, GridSettingPB, MoveGroupParams,
-    RepeatedGridGroupPB, RowPB,
+    CreateRowParams, DeleteFilterParams, DeleteGroupParams, GridFilterConfigurationPB, GridSettingPB,
+    InsertFilterParams, InsertGroupParams, MoveGroupParams, RepeatedGridGroupPB, RowPB,
 };
 use crate::manager::GridUser;
 use crate::services::grid_editor_task::GridServiceTaskScheduler;
@@ -84,6 +84,12 @@ impl GridViewManager {
         }
     }
 
+    pub(crate) async fn group_by_field(&self, field_id: &str) -> FlowyResult<()> {
+        let view_editor = self.get_default_view_editor().await?;
+        let _ = view_editor.group_by_field(field_id).await?;
+        Ok(())
+    }
+
     pub(crate) async fn did_update_cell(&self, row_id: &str, _field_id: &str) {
         self.did_update_row(row_id).await
     }
@@ -99,19 +105,19 @@ impl GridViewManager {
         Ok(view_editor.get_setting().await)
     }
 
-    pub(crate) async fn get_filters(&self) -> FlowyResult<Vec<GridFilterConfiguration>> {
+    pub(crate) async fn get_filters(&self) -> FlowyResult<Vec<GridFilterConfigurationPB>> {
         let view_editor = self.get_default_view_editor().await?;
         Ok(view_editor.get_filters().await)
     }
 
-    pub(crate) async fn update_filter(&self, insert_filter: CreateFilterParams) -> FlowyResult<()> {
+    pub(crate) async fn insert_or_update_filter(&self, params: InsertFilterParams) -> FlowyResult<()> {
         let view_editor = self.get_default_view_editor().await?;
-        view_editor.insert_filter(insert_filter).await
+        view_editor.insert_filter(params).await
     }
 
-    pub(crate) async fn delete_filter(&self, delete_filter: DeleteFilterParams) -> FlowyResult<()> {
+    pub(crate) async fn delete_filter(&self, params: DeleteFilterParams) -> FlowyResult<()> {
         let view_editor = self.get_default_view_editor().await?;
-        view_editor.delete_filter(delete_filter).await
+        view_editor.delete_filter(params).await
     }
 
     pub(crate) async fn load_groups(&self) -> FlowyResult<RepeatedGridGroupPB> {
@@ -120,6 +126,16 @@ impl GridViewManager {
         Ok(RepeatedGridGroupPB { items: groups })
     }
 
+    pub(crate) async fn insert_or_update_group(&self, params: InsertGroupParams) -> FlowyResult<()> {
+        let view_editor = self.get_default_view_editor().await?;
+        view_editor.insert_group(params).await
+    }
+
+    pub(crate) async fn delete_group(&self, params: DeleteGroupParams) -> FlowyResult<()> {
+        let view_editor = self.get_default_view_editor().await?;
+        view_editor.delete_group(params).await
+    }
+
     pub(crate) async fn move_group(&self, params: MoveGroupParams) -> FlowyResult<()> {
         let view_editor = self.get_default_view_editor().await?;
         let _ = view_editor.move_group(params).await?;

+ 129 - 93
frontend/rust-lib/flowy-grid/src/services/group/configuration.rs

@@ -1,24 +1,22 @@
-use crate::entities::{GroupPB, GroupViewChangesetPB, InsertedGroupPB};
-use crate::services::group::{default_group_configuration, Group};
+use crate::entities::{GroupPB, GroupViewChangesetPB};
+use crate::services::group::{default_group_configuration, GeneratedGroup, Group};
 use flowy_error::{FlowyError, FlowyResult};
 use flowy_grid_data_model::revision::{
     FieldRevision, FieldTypeRevision, GroupConfigurationContentSerde, GroupConfigurationRevision, GroupRevision,
 };
 use indexmap::IndexMap;
 use lib_infra::future::AFFuture;
+use std::collections::HashMap;
 use std::fmt::Formatter;
 use std::marker::PhantomData;
 use std::sync::Arc;
 
 pub trait GroupConfigurationReader: Send + Sync + 'static {
-    fn get_group_configuration(
-        &self,
-        field_rev: Arc<FieldRevision>,
-    ) -> AFFuture<Option<Arc<GroupConfigurationRevision>>>;
+    fn get_configuration(&self) -> AFFuture<Option<Arc<GroupConfigurationRevision>>>;
 }
 
 pub trait GroupConfigurationWriter: Send + Sync + 'static {
-    fn save_group_configuration(
+    fn save_configuration(
         &self,
         field_id: &str,
         field_type: FieldTypeRevision,
@@ -26,7 +24,7 @@ pub trait GroupConfigurationWriter: Send + Sync + 'static {
     ) -> AFFuture<FlowyResult<()>>;
 }
 
-impl<T> std::fmt::Display for GenericGroupConfiguration<T> {
+impl<T> std::fmt::Display for GroupContext<T> {
     fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
         self.groups_map.iter().for_each(|(_, group)| {
             let _ = f.write_fmt(format_args!("Group:{} has {} rows \n", group.id, group.rows.len()));
@@ -39,9 +37,9 @@ impl<T> std::fmt::Display for GenericGroupConfiguration<T> {
     }
 }
 
-pub struct GenericGroupConfiguration<C> {
-    view_id: String,
-    pub configuration: Arc<GroupConfigurationRevision>,
+pub struct GroupContext<C> {
+    pub view_id: String,
+    configuration: Arc<GroupConfigurationRevision>,
     configuration_content: PhantomData<C>,
     field_rev: Arc<FieldRevision>,
     groups_map: IndexMap<String, Group>,
@@ -50,7 +48,7 @@ pub struct GenericGroupConfiguration<C> {
     writer: Arc<dyn GroupConfigurationWriter>,
 }
 
-impl<C> GenericGroupConfiguration<C>
+impl<C> GroupContext<C>
 where
     C: GroupConfigurationContentSerde,
 {
@@ -67,16 +65,17 @@ where
             field_id: field_rev.id.clone(),
             name: format!("No {}", field_rev.name),
             is_default: true,
+            is_visible: true,
             rows: vec![],
-            content: "".to_string(),
+            filter_content: "".to_string(),
         };
-        let configuration = match reader.get_group_configuration(field_rev.clone()).await {
+        let configuration = match reader.get_configuration().await {
             None => {
-                let default_group_configuration = default_group_configuration(&field_rev);
+                let default_configuration = default_group_configuration(&field_rev);
                 writer
-                    .save_group_configuration(&field_rev.id, field_rev.ty, default_group_configuration.clone())
+                    .save_configuration(&field_rev.id, field_rev.ty, default_configuration.clone())
                     .await?;
-                Arc::new(default_group_configuration)
+                Arc::new(default_configuration)
             }
             Some(configuration) => configuration,
         };
@@ -134,47 +133,105 @@ where
         }
     }
 
-    pub(crate) fn merge_groups(&mut self, groups: Vec<Group>) -> FlowyResult<Option<GroupViewChangesetPB>> {
+    #[tracing::instrument(level = "debug", skip(self, generated_groups), err)]
+    pub(crate) fn init_groups(
+        &mut self,
+        generated_groups: Vec<GeneratedGroup>,
+        reset: bool,
+    ) -> FlowyResult<Option<GroupViewChangesetPB>> {
+        let mut new_groups = vec![];
+        let mut filter_content_map = HashMap::new();
+        generated_groups.into_iter().for_each(|generate_group| {
+            filter_content_map.insert(generate_group.group_rev.id.clone(), generate_group.filter_content);
+            new_groups.push(generate_group.group_rev);
+        });
+
         let MergeGroupResult {
-            groups,
-            inserted_groups,
-            updated_groups,
-        } = merge_groups(&self.configuration.groups, groups);
+            mut all_group_revs,
+            new_group_revs,
+            updated_group_revs: _,
+            deleted_group_revs,
+        } = if reset {
+            merge_groups(&[], new_groups)
+        } else {
+            merge_groups(&self.configuration.groups, new_groups)
+        };
 
-        let group_revs = groups
-            .iter()
-            .map(|group| GroupRevision::new(group.id.clone(), group.name.clone()))
-            .collect::<Vec<GroupRevision>>();
+        let deleted_group_ids = deleted_group_revs
+            .into_iter()
+            .map(|group_rev| group_rev.id)
+            .collect::<Vec<String>>();
 
-        self.mut_configuration(move |configuration| {
+        self.mut_configuration(|configuration| {
             let mut is_changed = false;
-            for new_group_rev in group_revs {
+            if !deleted_group_ids.is_empty() {
+                configuration
+                    .groups
+                    .retain(|group| !deleted_group_ids.contains(&group.id));
+                is_changed = true;
+            }
+
+            for group_rev in &mut all_group_revs {
                 match configuration
                     .groups
                     .iter()
-                    .position(|group_rev| group_rev.id == new_group_rev.id)
+                    .position(|old_group_rev| old_group_rev.id == group_rev.id)
                 {
                     None => {
-                        configuration.groups.push(new_group_rev);
+                        configuration.groups.push(group_rev.clone());
                         is_changed = true;
                     }
                     Some(pos) => {
-                        let removed_group = configuration.groups.remove(pos);
-                        if removed_group != new_group_rev {
+                        let mut old_group = configuration.groups.remove(pos);
+                        group_rev.update_with_other(&old_group);
+
+                        // Take the GroupRevision if the name has changed
+                        if is_group_changed(group_rev, &old_group) {
+                            old_group.name = group_rev.name.clone();
                             is_changed = true;
+                            configuration.groups.insert(pos, old_group);
                         }
-                        configuration.groups.insert(pos, new_group_rev);
                     }
                 }
             }
             is_changed
         })?;
 
-        groups.into_iter().for_each(|group| {
-            self.groups_map.insert(group.id.clone(), group);
+        // The len of the filter_content_map should equal to the len of the all_group_revs
+        debug_assert_eq!(filter_content_map.len(), all_group_revs.len());
+        all_group_revs.into_iter().for_each(|group_rev| {
+            if let Some(filter_content) = filter_content_map.get(&group_rev.id) {
+                let group = Group::new(
+                    group_rev.id,
+                    self.field_rev.id.clone(),
+                    group_rev.name,
+                    filter_content.clone(),
+                );
+                self.groups_map.insert(group.id.clone(), group);
+            }
         });
 
-        let changeset = make_group_view_changeset(self.view_id.clone(), inserted_groups, updated_groups);
+        let new_groups = new_group_revs
+            .into_iter()
+            .flat_map(|group_rev| {
+                let filter_content = filter_content_map.get(&group_rev.id)?;
+                let group = Group::new(
+                    group_rev.id,
+                    self.field_rev.id.clone(),
+                    group_rev.name,
+                    filter_content.clone(),
+                );
+                Some(GroupPB::from(group))
+            })
+            .collect();
+
+        let changeset = GroupViewChangesetPB {
+            view_id: self.view_id.clone(),
+            new_groups,
+            deleted_groups: deleted_group_ids,
+            update_groups: vec![],
+            inserted_groups: vec![],
+        };
         tracing::trace!("Group changeset: {:?}", changeset);
         if changeset.is_empty() {
             Ok(None)
@@ -221,10 +278,7 @@ where
         let field_id = self.field_rev.id.clone();
         let field_type = self.field_rev.ty;
         tokio::spawn(async move {
-            match writer
-                .save_group_configuration(&field_id, field_type, configuration)
-                .await
-            {
+            match writer.save_configuration(&field_id, field_type, configuration).await {
                 Ok(_) => {}
                 Err(e) => {
                     tracing::error!("Save group configuration failed: {}", e);
@@ -260,82 +314,64 @@ where
     }
 }
 
-fn merge_groups(old_groups: &[GroupRevision], groups: Vec<Group>) -> MergeGroupResult {
+fn merge_groups(old_groups: &[GroupRevision], new_groups: Vec<GroupRevision>) -> MergeGroupResult {
     let mut merge_result = MergeGroupResult::new();
     if old_groups.is_empty() {
-        merge_result.groups = groups;
+        merge_result.all_group_revs = new_groups.clone();
+        merge_result.new_group_revs = new_groups;
         return merge_result;
     }
 
     // group_map is a helper map is used to filter out the new groups.
-    let mut group_map: IndexMap<String, Group> = IndexMap::new();
-    groups.into_iter().for_each(|group| {
-        group_map.insert(group.id.clone(), group);
+    let mut new_group_map: IndexMap<String, GroupRevision> = IndexMap::new();
+    new_groups.into_iter().for_each(|group_rev| {
+        new_group_map.insert(group_rev.id.clone(), group_rev);
     });
 
     // The group is ordered in old groups. Add them before adding the new groups
-    for group_rev in old_groups {
-        if let Some(group) = group_map.remove(&group_rev.id) {
-            if group.name == group_rev.name {
-                merge_result.add_group(group);
-            } else {
-                merge_result.add_updated_group(group);
+    for old in old_groups {
+        if let Some(new) = new_group_map.remove(&old.id) {
+            merge_result.all_group_revs.push(new.clone());
+            if is_group_changed(&new, old) {
+                merge_result.updated_group_revs.push(new);
             }
+        } else {
+            merge_result.deleted_group_revs.push(old.clone());
         }
     }
 
     // Find out the new groups
-    group_map
-        .into_values()
-        .enumerate()
-        .for_each(|(index, group)| merge_result.add_insert_group(index, group));
-
+    let new_groups = new_group_map.into_values();
+    for (_, group) in new_groups.into_iter().enumerate() {
+        merge_result.all_group_revs.push(group.clone());
+        merge_result.new_group_revs.push(group);
+    }
     merge_result
 }
 
+fn is_group_changed(new: &GroupRevision, old: &GroupRevision) -> bool {
+    if new.name != old.name {
+        return true;
+    }
+
+    false
+}
+
 struct MergeGroupResult {
-    groups: Vec<Group>,
-    inserted_groups: Vec<InsertedGroupPB>,
-    updated_groups: Vec<Group>,
+    // Contains the new groups and the updated groups
+    all_group_revs: Vec<GroupRevision>,
+    new_group_revs: Vec<GroupRevision>,
+    updated_group_revs: Vec<GroupRevision>,
+    deleted_group_revs: Vec<GroupRevision>,
 }
 
 impl MergeGroupResult {
     fn new() -> Self {
         Self {
-            groups: vec![],
-            inserted_groups: vec![],
-            updated_groups: vec![],
+            all_group_revs: vec![],
+            new_group_revs: vec![],
+            updated_group_revs: vec![],
+            deleted_group_revs: vec![],
         }
     }
-
-    fn add_updated_group(&mut self, group: Group) {
-        self.groups.push(group.clone());
-        self.updated_groups.push(group);
-    }
-
-    fn add_group(&mut self, group: Group) {
-        self.groups.push(group);
-    }
-
-    fn add_insert_group(&mut self, index: usize, group: Group) {
-        self.groups.push(group.clone());
-        let inserted_group = InsertedGroupPB {
-            group: GroupPB::from(group),
-            index: index as i32,
-        };
-        self.inserted_groups.push(inserted_group);
-    }
-}
-
-fn make_group_view_changeset(
-    view_id: String,
-    inserted_groups: Vec<InsertedGroupPB>,
-    updated_group: Vec<Group>,
-) -> GroupViewChangesetPB {
-    GroupViewChangesetPB {
-        view_id,
-        inserted_groups,
-        deleted_groups: vec![],
-        update_groups: updated_group.into_iter().map(GroupPB::from).collect(),
-    }
 }

+ 33 - 32
frontend/rust-lib/flowy-grid/src/services/group/controller.rs

@@ -1,11 +1,11 @@
 use crate::entities::{GroupChangesetPB, GroupViewChangesetPB, InsertedRowPB, RowPB};
 use crate::services::cell::{decode_any_cell_data, CellBytesParser};
 use crate::services::group::action::GroupAction;
-use crate::services::group::configuration::GenericGroupConfiguration;
+use crate::services::group::configuration::GroupContext;
 use crate::services::group::entities::Group;
 use flowy_error::FlowyResult;
 use flowy_grid_data_model::revision::{
-    FieldRevision, GroupConfigurationContentSerde, RowChangeset, RowRevision, TypeOptionDataDeserializer,
+    FieldRevision, GroupConfigurationContentSerde, GroupRevision, RowChangeset, RowRevision, TypeOptionDataDeserializer,
 };
 
 use std::marker::PhantomData;
@@ -19,14 +19,19 @@ pub trait GroupController: GroupControllerSharedOperation + Send + Sync {
 }
 
 pub trait GroupGenerator {
-    type ConfigurationType;
+    type Context;
     type TypeOptionType;
 
     fn generate_groups(
         field_id: &str,
-        configuration: &Self::ConfigurationType,
+        group_ctx: &Self::Context,
         type_option: &Option<Self::TypeOptionType>,
-    ) -> Vec<Group>;
+    ) -> Vec<GeneratedGroup>;
+}
+
+pub struct GeneratedGroup {
+    pub group_rev: GroupRevision,
+    pub filter_content: String,
 }
 
 pub struct MoveGroupRowContext<'a> {
@@ -43,7 +48,7 @@ pub trait GroupControllerSharedOperation: Send + Sync {
     fn field_id(&self) -> &str;
     fn groups(&self) -> Vec<Group>;
     fn get_group(&self, group_id: &str) -> Option<(usize, Group)>;
-    fn fill_groups(&mut self, row_revs: &[Arc<RowRevision>], field_rev: &FieldRevision) -> FlowyResult<Vec<Group>>;
+    fn fill_groups(&mut self, row_revs: &[Arc<RowRevision>], field_rev: &FieldRevision) -> FlowyResult<()>;
     fn move_group(&mut self, from_group_id: &str, to_group_id: &str) -> FlowyResult<()>;
     fn did_update_row(
         &mut self,
@@ -69,7 +74,7 @@ pub trait GroupControllerSharedOperation: Send + Sync {
 pub struct GenericGroupController<C, T, G, P> {
     pub field_id: String,
     pub type_option: Option<T>,
-    pub configuration: GenericGroupConfiguration<C>,
+    pub group_ctx: GroupContext<C>,
     group_action_phantom: PhantomData<G>,
     cell_parser_phantom: PhantomData<P>,
 }
@@ -78,21 +83,18 @@ impl<C, T, G, P> GenericGroupController<C, T, G, P>
 where
     C: GroupConfigurationContentSerde,
     T: TypeOptionDataDeserializer,
-    G: GroupGenerator<ConfigurationType = GenericGroupConfiguration<C>, TypeOptionType = T>,
+    G: GroupGenerator<Context = GroupContext<C>, TypeOptionType = T>,
 {
-    pub async fn new(
-        field_rev: &Arc<FieldRevision>,
-        mut configuration: GenericGroupConfiguration<C>,
-    ) -> FlowyResult<Self> {
+    pub async fn new(field_rev: &Arc<FieldRevision>, mut configuration: GroupContext<C>) -> FlowyResult<Self> {
         let field_type_rev = field_rev.ty;
-        let type_option = field_rev.get_type_option_entry::<T>(field_type_rev);
+        let type_option = field_rev.get_type_option::<T>(field_type_rev);
         let groups = G::generate_groups(&field_rev.id, &configuration, &type_option);
-        let _ = configuration.merge_groups(groups)?;
+        let _ = configuration.init_groups(groups, true)?;
 
         Ok(Self {
             field_id: field_rev.id.clone(),
             type_option,
-            configuration,
+            group_ctx: configuration,
             group_action_phantom: PhantomData,
             cell_parser_phantom: PhantomData,
         })
@@ -105,7 +107,7 @@ where
         row_rev: &RowRevision,
         other_group_changesets: &[GroupChangesetPB],
     ) -> GroupChangesetPB {
-        let default_group = self.configuration.get_mut_default_group();
+        let default_group = self.group_ctx.get_mut_default_group();
 
         // [other_group_inserted_row] contains all the inserted rows except the default group.
         let other_group_inserted_row = other_group_changesets
@@ -171,7 +173,7 @@ where
     P: CellBytesParser,
     C: GroupConfigurationContentSerde,
     T: TypeOptionDataDeserializer,
-    G: GroupGenerator<ConfigurationType = GenericGroupConfiguration<C>, TypeOptionType = T>,
+    G: GroupGenerator<Context = GroupContext<C>, TypeOptionType = T>,
 
     Self: GroupAction<CellDataType = P::Object>,
 {
@@ -180,23 +182,23 @@ where
     }
 
     fn groups(&self) -> Vec<Group> {
-        self.configuration.clone_groups()
+        self.group_ctx.clone_groups()
     }
 
     fn get_group(&self, group_id: &str) -> Option<(usize, Group)> {
-        let group = self.configuration.get_group(group_id)?;
+        let group = self.group_ctx.get_group(group_id)?;
         Some((group.0, group.1.clone()))
     }
 
     #[tracing::instrument(level = "trace", skip_all, fields(row_count=%row_revs.len(), group_result))]
-    fn fill_groups(&mut self, row_revs: &[Arc<RowRevision>], field_rev: &FieldRevision) -> FlowyResult<Vec<Group>> {
+    fn fill_groups(&mut self, row_revs: &[Arc<RowRevision>], field_rev: &FieldRevision) -> FlowyResult<()> {
         for row_rev in row_revs {
             if let Some(cell_rev) = row_rev.cells.get(&self.field_id) {
                 let mut grouped_rows: Vec<GroupedRow> = vec![];
                 let cell_bytes = decode_any_cell_data(cell_rev.data.clone(), field_rev);
                 let cell_data = cell_bytes.parser::<P>()?;
-                for group in self.configuration.concrete_groups() {
-                    if self.can_group(&group.content, &cell_data) {
+                for group in self.group_ctx.concrete_groups() {
+                    if self.can_group(&group.filter_content, &cell_data) {
                         grouped_rows.push(GroupedRow {
                             row: row_rev.into(),
                             group_id: group.id.clone(),
@@ -205,25 +207,25 @@ where
                 }
 
                 if grouped_rows.is_empty() {
-                    self.configuration.get_mut_default_group().add_row(row_rev.into());
+                    self.group_ctx.get_mut_default_group().add_row(row_rev.into());
                 } else {
                     for group_row in grouped_rows {
-                        if let Some(group) = self.configuration.get_mut_group(&group_row.group_id) {
+                        if let Some(group) = self.group_ctx.get_mut_group(&group_row.group_id) {
                             group.add_row(group_row.row);
                         }
                     }
                 }
             } else {
-                self.configuration.get_mut_default_group().add_row(row_rev.into());
+                self.group_ctx.get_mut_default_group().add_row(row_rev.into());
             }
         }
 
-        tracing::Span::current().record("group_result", &format!("{},", self.configuration,).as_str());
-        Ok(self.groups())
+        tracing::Span::current().record("group_result", &format!("{},", self.group_ctx,).as_str());
+        Ok(())
     }
 
     fn move_group(&mut self, from_group_id: &str, to_group_id: &str) -> FlowyResult<()> {
-        self.configuration.move_group(from_group_id, to_group_id)
+        self.group_ctx.move_group(from_group_id, to_group_id)
     }
 
     fn did_update_row(
@@ -271,10 +273,9 @@ where
     }
 
     fn did_update_field(&mut self, field_rev: &FieldRevision) -> FlowyResult<Option<GroupViewChangesetPB>> {
-        let field_type_rev = field_rev.ty;
-        let type_option = field_rev.get_type_option_entry::<T>(field_type_rev);
-        let groups = G::generate_groups(&field_rev.id, &self.configuration, &type_option);
-        let changeset = self.configuration.merge_groups(groups)?;
+        let type_option = field_rev.get_type_option::<T>(field_rev.ty);
+        let groups = G::generate_groups(&field_rev.id, &self.group_ctx, &type_option);
+        let changeset = self.group_ctx.init_groups(groups, false)?;
         Ok(changeset)
     }
 }

+ 17 - 20
frontend/rust-lib/flowy-grid/src/services/group/controller_impls/checkbox_controller.rs

@@ -1,13 +1,13 @@
 use crate::entities::GroupChangesetPB;
 use crate::services::field::{CheckboxCellData, CheckboxCellDataParser, CheckboxTypeOptionPB, CHECK, UNCHECK};
 use crate::services::group::action::GroupAction;
-use crate::services::group::configuration::GenericGroupConfiguration;
+use crate::services::group::configuration::GroupContext;
 use crate::services::group::controller::{
     GenericGroupController, GroupController, GroupGenerator, MoveGroupRowContext,
 };
-use crate::services::group::entities::Group;
 
-use flowy_grid_data_model::revision::{CheckboxGroupConfigurationRevision, FieldRevision, RowRevision};
+use crate::services::group::GeneratedGroup;
+use flowy_grid_data_model::revision::{CheckboxGroupConfigurationRevision, FieldRevision, GroupRevision, RowRevision};
 
 pub type CheckboxGroupController = GenericGroupController<
     CheckboxGroupConfigurationRevision,
@@ -16,7 +16,7 @@ pub type CheckboxGroupController = GenericGroupController<
     CheckboxCellDataParser,
 >;
 
-pub type CheckboxGroupConfiguration = GenericGroupConfiguration<CheckboxGroupConfigurationRevision>;
+pub type CheckboxGroupContext = GroupContext<CheckboxGroupConfigurationRevision>;
 
 impl GroupAction for CheckboxGroupController {
     type CellDataType = CheckboxCellData;
@@ -49,26 +49,23 @@ impl GroupController for CheckboxGroupController {
 
 pub struct CheckboxGroupGenerator();
 impl GroupGenerator for CheckboxGroupGenerator {
-    type ConfigurationType = CheckboxGroupConfiguration;
+    type Context = CheckboxGroupContext;
     type TypeOptionType = CheckboxTypeOptionPB;
 
     fn generate_groups(
-        field_id: &str,
-        _configuration: &Self::ConfigurationType,
+        _field_id: &str,
+        _group_ctx: &Self::Context,
         _type_option: &Option<Self::TypeOptionType>,
-    ) -> Vec<Group> {
-        let check_group = Group::new(
-            "true".to_string(),
-            field_id.to_owned(),
-            "".to_string(),
-            CHECK.to_string(),
-        );
-        let uncheck_group = Group::new(
-            "false".to_string(),
-            field_id.to_owned(),
-            "".to_string(),
-            UNCHECK.to_string(),
-        );
+    ) -> Vec<GeneratedGroup> {
+        let check_group = GeneratedGroup {
+            group_rev: GroupRevision::new("true".to_string(), CHECK.to_string()),
+            filter_content: "".to_string(),
+        };
+
+        let uncheck_group = GeneratedGroup {
+            group_rev: GroupRevision::new("false".to_string(), UNCHECK.to_string()),
+            filter_content: "".to_string(),
+        };
         vec![check_group, uncheck_group]
     }
 }

+ 80 - 0
frontend/rust-lib/flowy-grid/src/services/group/controller_impls/default_controller.rs

@@ -0,0 +1,80 @@
+use crate::entities::{GroupChangesetPB, GroupViewChangesetPB, RowPB};
+use crate::services::group::{Group, GroupController, GroupControllerSharedOperation, MoveGroupRowContext};
+use flowy_error::FlowyResult;
+use flowy_grid_data_model::revision::{FieldRevision, RowRevision};
+use std::sync::Arc;
+
+pub struct DefaultGroupController {
+    pub field_id: String,
+    pub group: Group,
+}
+
+const DEFAULT_GROUP_CONTROLLER: &str = "DefaultGroupController";
+
+impl DefaultGroupController {
+    pub fn new(field_rev: &Arc<FieldRevision>) -> Self {
+        let group = Group::new(
+            DEFAULT_GROUP_CONTROLLER.to_owned(),
+            field_rev.id.clone(),
+            "".to_owned(),
+            "".to_owned(),
+        );
+        Self {
+            field_id: field_rev.id.clone(),
+            group,
+        }
+    }
+}
+
+impl GroupControllerSharedOperation for DefaultGroupController {
+    fn field_id(&self) -> &str {
+        &self.field_id
+    }
+
+    fn groups(&self) -> Vec<Group> {
+        vec![self.group.clone()]
+    }
+
+    fn get_group(&self, _group_id: &str) -> Option<(usize, Group)> {
+        Some((0, self.group.clone()))
+    }
+
+    fn fill_groups(&mut self, row_revs: &[Arc<RowRevision>], _field_rev: &FieldRevision) -> FlowyResult<()> {
+        row_revs.iter().for_each(|row_rev| {
+            self.group.add_row(RowPB::from(row_rev));
+        });
+        Ok(())
+    }
+
+    fn move_group(&mut self, _from_group_id: &str, _to_group_id: &str) -> FlowyResult<()> {
+        Ok(())
+    }
+
+    fn did_update_row(
+        &mut self,
+        _row_rev: &RowRevision,
+        _field_rev: &FieldRevision,
+    ) -> FlowyResult<Vec<GroupChangesetPB>> {
+        todo!()
+    }
+
+    fn did_delete_row(
+        &mut self,
+        _row_rev: &RowRevision,
+        _field_rev: &FieldRevision,
+    ) -> FlowyResult<Vec<GroupChangesetPB>> {
+        todo!()
+    }
+
+    fn move_group_row(&mut self, _context: MoveGroupRowContext) -> FlowyResult<Vec<GroupChangesetPB>> {
+        todo!()
+    }
+
+    fn did_update_field(&mut self, _field_rev: &FieldRevision) -> FlowyResult<Option<GroupViewChangesetPB>> {
+        Ok(None)
+    }
+}
+
+impl GroupController for DefaultGroupController {
+    fn will_create_row(&mut self, _row_rev: &mut RowRevision, _field_rev: &FieldRevision, _group_id: &str) {}
+}

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

@@ -1,5 +1,7 @@
 mod checkbox_controller;
+mod default_controller;
 mod select_option_controller;
 
 pub use checkbox_controller::*;
+pub use default_controller::*;
 pub use select_option_controller::*;

+ 12 - 22
frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/multi_select_controller.rs

@@ -7,7 +7,8 @@ use crate::services::group::controller::{
     GenericGroupController, GroupController, GroupGenerator, MoveGroupRowContext,
 };
 use crate::services::group::controller_impls::select_option_controller::util::*;
-use crate::services::group::entities::Group;
+
+use crate::services::group::GeneratedGroup;
 use flowy_grid_data_model::revision::{FieldRevision, RowRevision, SelectOptionGroupConfigurationRevision};
 
 // MultiSelect
@@ -27,8 +28,8 @@ impl GroupAction for MultiSelectGroupController {
 
     fn add_row_if_match(&mut self, row_rev: &RowRevision, cell_data: &Self::CellDataType) -> Vec<GroupChangesetPB> {
         let mut changesets = vec![];
-        self.configuration.iter_mut_groups(|group| {
-            if let Some(changeset) = add_row(group, cell_data, row_rev) {
+        self.group_ctx.iter_mut_groups(|group| {
+            if let Some(changeset) = add_select_option_row(group, cell_data, row_rev) {
                 changesets.push(changeset);
             }
         });
@@ -37,8 +38,8 @@ impl GroupAction for MultiSelectGroupController {
 
     fn remove_row_if_match(&mut self, row_rev: &RowRevision, cell_data: &Self::CellDataType) -> Vec<GroupChangesetPB> {
         let mut changesets = vec![];
-        self.configuration.iter_mut_groups(|group| {
-            if let Some(changeset) = remove_row(group, cell_data, row_rev) {
+        self.group_ctx.iter_mut_groups(|group| {
+            if let Some(changeset) = remove_select_option_row(group, cell_data, row_rev) {
                 changesets.push(changeset);
             }
         });
@@ -47,7 +48,7 @@ impl GroupAction for MultiSelectGroupController {
 
     fn move_row(&mut self, cell_data: &Self::CellDataType, mut context: MoveGroupRowContext) -> Vec<GroupChangesetPB> {
         let mut group_changeset = vec![];
-        self.configuration.iter_mut_groups(|group| {
+        self.group_ctx.iter_mut_groups(|group| {
             if let Some(changeset) = move_select_option_row(group, cell_data, &mut context) {
                 group_changeset.push(changeset);
             }
@@ -58,7 +59,7 @@ impl GroupAction for MultiSelectGroupController {
 
 impl GroupController for MultiSelectGroupController {
     fn will_create_row(&mut self, row_rev: &mut RowRevision, field_rev: &FieldRevision, group_id: &str) {
-        match self.configuration.get_group(group_id) {
+        match self.group_ctx.get_group(group_id) {
             None => tracing::warn!("Can not find the group: {}", group_id),
             Some((_, group)) => {
                 let cell_rev = insert_select_option_cell(group.id.clone(), field_rev);
@@ -70,27 +71,16 @@ impl GroupController for MultiSelectGroupController {
 
 pub struct MultiSelectGroupGenerator();
 impl GroupGenerator for MultiSelectGroupGenerator {
-    type ConfigurationType = SelectOptionGroupConfiguration;
+    type Context = SelectOptionGroupContext;
     type TypeOptionType = MultiSelectTypeOptionPB;
     fn generate_groups(
         field_id: &str,
-        _configuration: &Self::ConfigurationType,
+        group_ctx: &Self::Context,
         type_option: &Option<Self::TypeOptionType>,
-    ) -> Vec<Group> {
+    ) -> Vec<GeneratedGroup> {
         match type_option {
             None => vec![],
-            Some(type_option) => type_option
-                .options
-                .iter()
-                .map(|option| {
-                    Group::new(
-                        option.id.clone(),
-                        field_id.to_owned(),
-                        option.name.clone(),
-                        option.id.clone(),
-                    )
-                })
-                .collect(),
+            Some(type_option) => generate_select_option_groups(field_id, group_ctx, &type_option.options),
         }
     }
 }

+ 11 - 21
frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/single_select_controller.rs

@@ -9,6 +9,7 @@ use crate::services::group::controller::{
 use crate::services::group::controller_impls::select_option_controller::util::*;
 use crate::services::group::entities::Group;
 
+use crate::services::group::GeneratedGroup;
 use flowy_grid_data_model::revision::{FieldRevision, RowRevision, SelectOptionGroupConfigurationRevision};
 
 // SingleSelect
@@ -27,8 +28,8 @@ impl GroupAction for SingleSelectGroupController {
 
     fn add_row_if_match(&mut self, row_rev: &RowRevision, cell_data: &Self::CellDataType) -> Vec<GroupChangesetPB> {
         let mut changesets = vec![];
-        self.configuration.iter_mut_groups(|group| {
-            if let Some(changeset) = add_row(group, cell_data, row_rev) {
+        self.group_ctx.iter_mut_groups(|group| {
+            if let Some(changeset) = add_select_option_row(group, cell_data, row_rev) {
                 changesets.push(changeset);
             }
         });
@@ -37,8 +38,8 @@ impl GroupAction for SingleSelectGroupController {
 
     fn remove_row_if_match(&mut self, row_rev: &RowRevision, cell_data: &Self::CellDataType) -> Vec<GroupChangesetPB> {
         let mut changesets = vec![];
-        self.configuration.iter_mut_groups(|group| {
-            if let Some(changeset) = remove_row(group, cell_data, row_rev) {
+        self.group_ctx.iter_mut_groups(|group| {
+            if let Some(changeset) = remove_select_option_row(group, cell_data, row_rev) {
                 changesets.push(changeset);
             }
         });
@@ -47,7 +48,7 @@ impl GroupAction for SingleSelectGroupController {
 
     fn move_row(&mut self, cell_data: &Self::CellDataType, mut context: MoveGroupRowContext) -> Vec<GroupChangesetPB> {
         let mut group_changeset = vec![];
-        self.configuration.iter_mut_groups(|group| {
+        self.group_ctx.iter_mut_groups(|group| {
             if let Some(changeset) = move_select_option_row(group, cell_data, &mut context) {
                 group_changeset.push(changeset);
             }
@@ -58,7 +59,7 @@ impl GroupAction for SingleSelectGroupController {
 
 impl GroupController for SingleSelectGroupController {
     fn will_create_row(&mut self, row_rev: &mut RowRevision, field_rev: &FieldRevision, group_id: &str) {
-        let group: Option<&mut Group> = self.configuration.get_mut_group(group_id);
+        let group: Option<&mut Group> = self.group_ctx.get_mut_group(group_id);
         match group {
             None => {}
             Some(group) => {
@@ -72,27 +73,16 @@ impl GroupController for SingleSelectGroupController {
 
 pub struct SingleSelectGroupGenerator();
 impl GroupGenerator for SingleSelectGroupGenerator {
-    type ConfigurationType = SelectOptionGroupConfiguration;
+    type Context = SelectOptionGroupContext;
     type TypeOptionType = SingleSelectTypeOptionPB;
     fn generate_groups(
         field_id: &str,
-        _configuration: &Self::ConfigurationType,
+        group_ctx: &Self::Context,
         type_option: &Option<Self::TypeOptionType>,
-    ) -> Vec<Group> {
+    ) -> Vec<GeneratedGroup> {
         match type_option {
             None => vec![],
-            Some(type_option) => type_option
-                .options
-                .iter()
-                .map(|option| {
-                    Group::new(
-                        option.id.clone(),
-                        field_id.to_owned(),
-                        option.name.clone(),
-                        option.id.clone(),
-                    )
-                })
-                .collect(),
+            Some(type_option) => generate_select_option_groups(field_id, group_ctx, &type_option.options),
         }
     }
 }

+ 23 - 7
frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/util.rs

@@ -1,15 +1,15 @@
 use crate::entities::{GroupChangesetPB, InsertedRowPB, RowPB};
 use crate::services::cell::insert_select_option_cell;
-use crate::services::field::SelectOptionCellDataPB;
-use crate::services::group::configuration::GenericGroupConfiguration;
-use crate::services::group::Group;
+use crate::services::field::{SelectOptionCellDataPB, SelectOptionPB};
+use crate::services::group::configuration::GroupContext;
+use crate::services::group::{GeneratedGroup, Group};
 
 use crate::services::group::controller::MoveGroupRowContext;
-use flowy_grid_data_model::revision::{RowRevision, SelectOptionGroupConfigurationRevision};
+use flowy_grid_data_model::revision::{GroupRevision, RowRevision, SelectOptionGroupConfigurationRevision};
 
-pub type SelectOptionGroupConfiguration = GenericGroupConfiguration<SelectOptionGroupConfigurationRevision>;
+pub type SelectOptionGroupContext = GroupContext<SelectOptionGroupConfigurationRevision>;
 
-pub fn add_row(
+pub fn add_select_option_row(
     group: &mut Group,
     cell_data: &SelectOptionCellDataPB,
     row_rev: &RowRevision,
@@ -42,7 +42,7 @@ pub fn add_row(
     }
 }
 
-pub fn remove_row(
+pub fn remove_select_option_row(
     group: &mut Group,
     cell_data: &SelectOptionCellDataPB,
     row_rev: &RowRevision,
@@ -125,3 +125,19 @@ pub fn move_select_option_row(
         Some(changeset)
     }
 }
+
+pub fn generate_select_option_groups(
+    _field_id: &str,
+    _group_ctx: &SelectOptionGroupContext,
+    options: &[SelectOptionPB],
+) -> Vec<GeneratedGroup> {
+    let groups = options
+        .iter()
+        .map(|option| GeneratedGroup {
+            group_rev: GroupRevision::new(option.id.clone(), option.name.clone()),
+            filter_content: option.id.clone(),
+        })
+        .collect();
+
+    groups
+}

+ 5 - 3
frontend/rust-lib/flowy-grid/src/services/group/entities.rs

@@ -6,21 +6,23 @@ pub struct Group {
     pub field_id: String,
     pub name: String,
     pub is_default: bool,
+    pub is_visible: bool,
     pub(crate) rows: Vec<RowPB>,
 
     /// [content] is used to determine which group the cell belongs to.
-    pub content: String,
+    pub filter_content: String,
 }
 
 impl Group {
-    pub fn new(id: String, field_id: String, name: String, content: String) -> Self {
+    pub fn new(id: String, field_id: String, name: String, filter_content: String) -> Self {
         Self {
             id,
             field_id,
             is_default: false,
+            is_visible: true,
             name,
             rows: vec![],
-            content,
+            filter_content,
         }
     }
 

+ 0 - 293
frontend/rust-lib/flowy-grid/src/services/group/group_service.rs

@@ -1,293 +0,0 @@
-use crate::entities::{FieldType, GroupChangesetPB, GroupViewChangesetPB};
-use crate::services::group::configuration::GroupConfigurationReader;
-use crate::services::group::controller::{GroupController, MoveGroupRowContext};
-use crate::services::group::{
-    CheckboxGroupConfiguration, CheckboxGroupController, Group, GroupConfigurationWriter, MultiSelectGroupController,
-    SelectOptionGroupConfiguration, SingleSelectGroupController,
-};
-use flowy_error::FlowyResult;
-use flowy_grid_data_model::revision::{
-    CheckboxGroupConfigurationRevision, DateGroupConfigurationRevision, FieldRevision, GroupConfigurationRevision,
-    NumberGroupConfigurationRevision, RowChangeset, RowRevision, SelectOptionGroupConfigurationRevision,
-    TextGroupConfigurationRevision, UrlGroupConfigurationRevision,
-};
-use std::future::Future;
-use std::sync::Arc;
-
-pub(crate) struct GroupService {
-    view_id: String,
-    configuration_reader: Arc<dyn GroupConfigurationReader>,
-    configuration_writer: Arc<dyn GroupConfigurationWriter>,
-    group_controller: Option<Box<dyn GroupController>>,
-}
-
-impl GroupService {
-    pub(crate) async fn new<R, W>(view_id: String, configuration_reader: R, configuration_writer: W) -> Self
-    where
-        R: GroupConfigurationReader,
-        W: GroupConfigurationWriter,
-    {
-        Self {
-            view_id,
-            configuration_reader: Arc::new(configuration_reader),
-            configuration_writer: Arc::new(configuration_writer),
-            group_controller: None,
-        }
-    }
-
-    pub(crate) async fn groups(&self) -> Vec<Group> {
-        self.group_controller
-            .as_ref()
-            .map(|group_controller| group_controller.groups())
-            .unwrap_or_default()
-    }
-
-    pub(crate) async fn get_group(&self, group_id: &str) -> Option<(usize, Group)> {
-        self.group_controller
-            .as_ref()
-            .and_then(|group_controller| group_controller.get_group(group_id))
-    }
-
-    pub(crate) async fn load_groups(
-        &mut self,
-        field_revs: &[Arc<FieldRevision>],
-        row_revs: Vec<Arc<RowRevision>>,
-    ) -> Option<Vec<Group>> {
-        let field_rev = find_group_field(field_revs)?;
-        let field_type: FieldType = field_rev.ty.into();
-
-        let mut group_controller = self.make_group_controller(&field_type, &field_rev).await.ok()??;
-        let groups = match group_controller.fill_groups(&row_revs, &field_rev) {
-            Ok(groups) => groups,
-            Err(e) => {
-                tracing::error!("Fill groups failed:{:?}", e);
-                vec![]
-            }
-        };
-        self.group_controller = Some(group_controller);
-        Some(groups)
-    }
-
-    pub(crate) async fn will_create_row<F, O>(&mut self, row_rev: &mut RowRevision, group_id: &str, get_field_fn: F)
-    where
-        F: FnOnce(String) -> O,
-        O: Future<Output = Option<Arc<FieldRevision>>> + Send + Sync + 'static,
-    {
-        if let Some(group_controller) = self.group_controller.as_mut() {
-            let field_id = group_controller.field_id().to_owned();
-            match get_field_fn(field_id).await {
-                None => {}
-                Some(field_rev) => {
-                    group_controller.will_create_row(row_rev, &field_rev, group_id);
-                }
-            }
-        }
-    }
-
-    pub(crate) async fn did_delete_row<F, O>(
-        &mut self,
-        row_rev: &RowRevision,
-        get_field_fn: F,
-    ) -> Option<Vec<GroupChangesetPB>>
-    where
-        F: FnOnce(String) -> O,
-        O: Future<Output = Option<Arc<FieldRevision>>> + Send + Sync + 'static,
-    {
-        let group_controller = self.group_controller.as_mut()?;
-        let field_id = group_controller.field_id().to_owned();
-        let field_rev = get_field_fn(field_id).await?;
-
-        match group_controller.did_delete_row(row_rev, &field_rev) {
-            Ok(changesets) => Some(changesets),
-            Err(e) => {
-                tracing::error!("Delete group data failed, {:?}", e);
-                None
-            }
-        }
-    }
-
-    pub(crate) async fn move_group_row<F, O>(
-        &mut self,
-        row_rev: &RowRevision,
-        row_changeset: &mut RowChangeset,
-        to_group_id: &str,
-        to_row_id: Option<String>,
-        get_field_fn: F,
-    ) -> Option<Vec<GroupChangesetPB>>
-    where
-        F: FnOnce(String) -> O,
-        O: Future<Output = Option<Arc<FieldRevision>>> + Send + Sync + 'static,
-    {
-        let group_controller = self.group_controller.as_mut()?;
-        let field_id = group_controller.field_id().to_owned();
-        let field_rev = get_field_fn(field_id).await?;
-        let move_row_context = MoveGroupRowContext {
-            row_rev,
-            row_changeset,
-            field_rev: field_rev.as_ref(),
-            to_group_id,
-            to_row_id,
-        };
-
-        match group_controller.move_group_row(move_row_context) {
-            Ok(changesets) => Some(changesets),
-            Err(e) => {
-                tracing::error!("Move group data failed, {:?}", e);
-                None
-            }
-        }
-    }
-
-    #[tracing::instrument(level = "trace", skip_all)]
-    pub(crate) async fn did_update_row<F, O>(
-        &mut self,
-        row_rev: &RowRevision,
-        get_field_fn: F,
-    ) -> Option<Vec<GroupChangesetPB>>
-    where
-        F: FnOnce(String) -> O,
-        O: Future<Output = Option<Arc<FieldRevision>>> + Send + Sync + 'static,
-    {
-        let group_controller = self.group_controller.as_mut()?;
-        let field_id = group_controller.field_id().to_owned();
-        let field_rev = get_field_fn(field_id).await?;
-
-        match group_controller.did_update_row(row_rev, &field_rev) {
-            Ok(changeset) => Some(changeset),
-            Err(e) => {
-                tracing::error!("Update group data failed, {:?}", e);
-                None
-            }
-        }
-    }
-
-    #[tracing::instrument(level = "trace", skip_all)]
-    pub(crate) async fn move_group(&mut self, from_group_id: &str, to_group_id: &str) -> FlowyResult<()> {
-        match self.group_controller.as_mut() {
-            None => Ok(()),
-            Some(group_controller) => {
-                let _ = group_controller.move_group(from_group_id, to_group_id)?;
-                Ok(())
-            }
-        }
-    }
-
-    #[tracing::instrument(level = "trace", name = "group_did_update_field", skip(self, field_rev), err)]
-    pub(crate) async fn did_update_field(
-        &mut self,
-        field_rev: &FieldRevision,
-    ) -> FlowyResult<Option<GroupViewChangesetPB>> {
-        match self.group_controller.as_mut() {
-            None => Ok(None),
-            Some(group_controller) => group_controller.did_update_field(field_rev),
-        }
-    }
-
-    #[tracing::instrument(level = "trace", skip(self, field_rev), err)]
-    async fn make_group_controller(
-        &self,
-        field_type: &FieldType,
-        field_rev: &Arc<FieldRevision>,
-    ) -> FlowyResult<Option<Box<dyn GroupController>>> {
-        let mut group_controller: Option<Box<dyn GroupController>> = None;
-        match field_type {
-            FieldType::RichText => {
-                // let generator = GroupGenerator::<TextGroupConfigurationPB>::from_configuration(configuration);
-            }
-            FieldType::Number => {
-                // let generator = GroupGenerator::<NumberGroupConfigurationPB>::from_configuration(configuration);
-            }
-            FieldType::DateTime => {
-                // let generator = GroupGenerator::<DateGroupConfigurationPB>::from_configuration(configuration);
-            }
-            FieldType::SingleSelect => {
-                let configuration = SelectOptionGroupConfiguration::new(
-                    self.view_id.clone(),
-                    field_rev.clone(),
-                    self.configuration_reader.clone(),
-                    self.configuration_writer.clone(),
-                )
-                .await?;
-                let controller = SingleSelectGroupController::new(field_rev, configuration).await?;
-                group_controller = Some(Box::new(controller));
-            }
-            FieldType::MultiSelect => {
-                let configuration = SelectOptionGroupConfiguration::new(
-                    self.view_id.clone(),
-                    field_rev.clone(),
-                    self.configuration_reader.clone(),
-                    self.configuration_writer.clone(),
-                )
-                .await?;
-                let controller = MultiSelectGroupController::new(field_rev, configuration).await?;
-                group_controller = Some(Box::new(controller));
-            }
-            FieldType::Checkbox => {
-                let configuration = CheckboxGroupConfiguration::new(
-                    self.view_id.clone(),
-                    field_rev.clone(),
-                    self.configuration_reader.clone(),
-                    self.configuration_writer.clone(),
-                )
-                .await?;
-                let controller = CheckboxGroupController::new(field_rev, configuration).await?;
-                group_controller = Some(Box::new(controller));
-            }
-            FieldType::URL => {
-                // let generator = GroupGenerator::<UrlGroupConfigurationPB>::from_configuration(configuration);
-            }
-        }
-        Ok(group_controller)
-    }
-}
-
-fn find_group_field(field_revs: &[Arc<FieldRevision>]) -> Option<Arc<FieldRevision>> {
-    let field_rev = field_revs
-        .iter()
-        .find(|field_rev| {
-            let field_type: FieldType = field_rev.ty.into();
-            field_type.can_be_group()
-        })
-        .cloned();
-    field_rev
-}
-
-pub fn default_group_configuration(field_rev: &FieldRevision) -> GroupConfigurationRevision {
-    let field_id = field_rev.id.clone();
-    let field_type_rev = field_rev.ty;
-    let field_type: FieldType = field_rev.ty.into();
-    match field_type {
-        FieldType::RichText => {
-            GroupConfigurationRevision::new(field_id, field_type_rev, TextGroupConfigurationRevision::default())
-                .unwrap()
-        }
-        FieldType::Number => {
-            GroupConfigurationRevision::new(field_id, field_type_rev, NumberGroupConfigurationRevision::default())
-                .unwrap()
-        }
-        FieldType::DateTime => {
-            GroupConfigurationRevision::new(field_id, field_type_rev, DateGroupConfigurationRevision::default())
-                .unwrap()
-        }
-
-        FieldType::SingleSelect => GroupConfigurationRevision::new(
-            field_id,
-            field_type_rev,
-            SelectOptionGroupConfigurationRevision::default(),
-        )
-        .unwrap(),
-        FieldType::MultiSelect => GroupConfigurationRevision::new(
-            field_id,
-            field_type_rev,
-            SelectOptionGroupConfigurationRevision::default(),
-        )
-        .unwrap(),
-        FieldType::Checkbox => {
-            GroupConfigurationRevision::new(field_id, field_type_rev, CheckboxGroupConfigurationRevision::default())
-                .unwrap()
-        }
-        FieldType::URL => {
-            GroupConfigurationRevision::new(field_id, field_type_rev, UrlGroupConfigurationRevision::default()).unwrap()
-        }
-    }
-}

+ 114 - 0
frontend/rust-lib/flowy-grid/src/services/group/group_util.rs

@@ -0,0 +1,114 @@
+use crate::entities::FieldType;
+use crate::services::group::configuration::GroupConfigurationReader;
+use crate::services::group::controller::GroupController;
+use crate::services::group::{
+    CheckboxGroupContext, CheckboxGroupController, DefaultGroupController, GroupConfigurationWriter,
+    MultiSelectGroupController, SelectOptionGroupContext, SingleSelectGroupController,
+};
+use flowy_error::FlowyResult;
+use flowy_grid_data_model::revision::{
+    CheckboxGroupConfigurationRevision, DateGroupConfigurationRevision, FieldRevision, GroupConfigurationRevision,
+    NumberGroupConfigurationRevision, RowRevision, SelectOptionGroupConfigurationRevision,
+    TextGroupConfigurationRevision, UrlGroupConfigurationRevision,
+};
+use std::sync::Arc;
+
+#[tracing::instrument(level = "trace", skip_all, err)]
+pub async fn make_group_controller<R, W>(
+    view_id: String,
+    field_rev: Arc<FieldRevision>,
+    row_revs: Vec<Arc<RowRevision>>,
+    configuration_reader: R,
+    configuration_writer: W,
+) -> FlowyResult<Box<dyn GroupController>>
+where
+    R: GroupConfigurationReader,
+    W: GroupConfigurationWriter,
+{
+    let field_type: FieldType = field_rev.ty.into();
+
+    let mut group_controller: Box<dyn GroupController>;
+    let configuration_reader = Arc::new(configuration_reader);
+    let configuration_writer = Arc::new(configuration_writer);
+
+    match field_type {
+        FieldType::SingleSelect => {
+            let configuration =
+                SelectOptionGroupContext::new(view_id, field_rev.clone(), configuration_reader, configuration_writer)
+                    .await?;
+            let controller = SingleSelectGroupController::new(&field_rev, configuration).await?;
+            group_controller = Box::new(controller);
+        }
+        FieldType::MultiSelect => {
+            let configuration =
+                SelectOptionGroupContext::new(view_id, field_rev.clone(), configuration_reader, configuration_writer)
+                    .await?;
+            let controller = MultiSelectGroupController::new(&field_rev, configuration).await?;
+            group_controller = Box::new(controller);
+        }
+        FieldType::Checkbox => {
+            let configuration =
+                CheckboxGroupContext::new(view_id, field_rev.clone(), configuration_reader, configuration_writer)
+                    .await?;
+            let controller = CheckboxGroupController::new(&field_rev, configuration).await?;
+            group_controller = Box::new(controller);
+        }
+        _ => {
+            group_controller = Box::new(DefaultGroupController::new(&field_rev));
+        }
+    }
+
+    let _ = group_controller.fill_groups(&row_revs, &field_rev)?;
+    Ok(group_controller)
+}
+
+pub fn find_group_field(field_revs: &[Arc<FieldRevision>]) -> Option<Arc<FieldRevision>> {
+    let field_rev = field_revs
+        .iter()
+        .find(|field_rev| {
+            let field_type: FieldType = field_rev.ty.into();
+            field_type.can_be_group()
+        })
+        .cloned();
+    field_rev
+}
+
+pub fn default_group_configuration(field_rev: &FieldRevision) -> GroupConfigurationRevision {
+    let field_id = field_rev.id.clone();
+    let field_type_rev = field_rev.ty;
+    let field_type: FieldType = field_rev.ty.into();
+    match field_type {
+        FieldType::RichText => {
+            GroupConfigurationRevision::new(field_id, field_type_rev, TextGroupConfigurationRevision::default())
+                .unwrap()
+        }
+        FieldType::Number => {
+            GroupConfigurationRevision::new(field_id, field_type_rev, NumberGroupConfigurationRevision::default())
+                .unwrap()
+        }
+        FieldType::DateTime => {
+            GroupConfigurationRevision::new(field_id, field_type_rev, DateGroupConfigurationRevision::default())
+                .unwrap()
+        }
+
+        FieldType::SingleSelect => GroupConfigurationRevision::new(
+            field_id,
+            field_type_rev,
+            SelectOptionGroupConfigurationRevision::default(),
+        )
+        .unwrap(),
+        FieldType::MultiSelect => GroupConfigurationRevision::new(
+            field_id,
+            field_type_rev,
+            SelectOptionGroupConfigurationRevision::default(),
+        )
+        .unwrap(),
+        FieldType::Checkbox => {
+            GroupConfigurationRevision::new(field_id, field_type_rev, CheckboxGroupConfigurationRevision::default())
+                .unwrap()
+        }
+        FieldType::URL => {
+            GroupConfigurationRevision::new(field_id, field_type_rev, UrlGroupConfigurationRevision::default()).unwrap()
+        }
+    }
+}

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

@@ -3,9 +3,10 @@ mod configuration;
 mod controller;
 mod controller_impls;
 mod entities;
-mod group_service;
+mod group_util;
 
 pub(crate) use configuration::*;
+pub(crate) use controller::*;
 pub(crate) use controller_impls::*;
 pub(crate) use entities::*;
-pub(crate) use group_service::*;
+pub(crate) use group_util::*;

+ 2 - 2
frontend/rust-lib/flowy-grid/src/services/setting/setting_builder.rs

@@ -1,4 +1,4 @@
-use crate::entities::{CreateFilterParams, DeleteFilterParams, GridLayout, GridSettingChangesetParams};
+use crate::entities::{DeleteFilterParams, GridLayout, GridSettingChangesetParams, InsertFilterParams};
 
 pub struct GridSettingChangesetBuilder {
     params: GridSettingChangesetParams,
@@ -17,7 +17,7 @@ impl GridSettingChangesetBuilder {
         Self { params }
     }
 
-    pub fn insert_filter(mut self, params: CreateFilterParams) -> Self {
+    pub fn insert_filter(mut self, params: InsertFilterParams) -> Self {
         self.params.insert_filter = Some(params);
         self
     }

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

@@ -176,7 +176,7 @@ impl GridRowTest {
             FieldType::Number => {
                 let field_rev = self.editor.get_field_rev(&cell_id.field_id).await.unwrap();
                 let number_type_option = field_rev
-                    .get_type_option_entry::<NumberTypeOptionPB>(FieldType::Number.into())
+                    .get_type_option::<NumberTypeOptionPB>(FieldType::Number.into())
                     .unwrap();
                 let cell_data = self
                     .editor

+ 2 - 2
frontend/rust-lib/flowy-grid/tests/grid/field_test/test.rs

@@ -4,7 +4,7 @@ use crate::grid::field_test::util::*;
 use flowy_grid::entities::FieldChangesetParams;
 use flowy_grid::services::field::selection_type_option::SelectOptionPB;
 use flowy_grid::services::field::SingleSelectTypeOptionPB;
-use flowy_grid_data_model::revision::TypeOptionDataEntry;
+use flowy_grid_data_model::revision::TypeOptionDataFormat;
 
 #[tokio::test]
 async fn grid_create_field() {
@@ -86,7 +86,7 @@ async fn grid_update_field() {
     let mut expected_field_rev = single_select_field.clone();
     expected_field_rev.frozen = true;
     expected_field_rev.width = 1000;
-    expected_field_rev.insert_type_option_entry(&single_select_type_option);
+    expected_field_rev.insert_type_option(&single_select_type_option);
 
     let scripts = vec![
         CreateField { params },

+ 2 - 2
frontend/rust-lib/flowy-grid/tests/grid/field_test/util.rs

@@ -12,7 +12,7 @@ pub fn create_text_field(grid_id: &str) -> (InsertFieldParams, FieldRevision) {
     let cloned_field_rev = field_rev.clone();
 
     let type_option_data = field_rev
-        .get_type_option_entry::<RichTextTypeOptionPB>(field_rev.ty)
+        .get_type_option::<RichTextTypeOptionPB>(field_rev.ty)
         .unwrap()
         .protobuf_bytes()
         .to_vec();
@@ -45,7 +45,7 @@ pub fn create_single_select_field(grid_id: &str) -> (InsertFieldParams, FieldRev
     let field_rev = FieldBuilder::new(single_select).name("Name").visibility(true).build();
     let cloned_field_rev = field_rev.clone();
     let type_option_data = field_rev
-        .get_type_option_entry::<SingleSelectTypeOptionPB>(field_rev.ty)
+        .get_type_option::<SingleSelectTypeOptionPB>(field_rev.ty)
         .unwrap()
         .protobuf_bytes()
         .to_vec();

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

@@ -3,14 +3,14 @@
 #![allow(dead_code)]
 #![allow(unused_imports)]
 
-use flowy_grid::entities::{CreateFilterParams, CreateGridFilterPayloadPB, DeleteFilterParams, GridLayout, GridSettingChangesetParams, GridSettingPB};
+use flowy_grid::entities::{InsertFilterParams, InsertFilterPayloadPB, DeleteFilterParams, GridLayout, GridSettingChangesetParams, GridSettingPB};
 use flowy_grid::services::setting::GridSettingChangesetBuilder;
 use flowy_grid_data_model::revision::{FieldRevision, FieldTypeRevision};
 use crate::grid::grid_editor::GridEditorTest;
 
 pub enum FilterScript {
     InsertGridTableFilter {
-        payload: CreateGridFilterPayloadPB,
+        payload: InsertFilterPayloadPB,
     },
     AssertTableFilterCount {
         count: i32,
@@ -47,8 +47,8 @@ impl GridFilterTest {
         match script {
            
             FilterScript::InsertGridTableFilter { payload } => {
-                let params: CreateFilterParams = payload.try_into().unwrap();
-                let _ = self.editor.update_filter(params).await.unwrap();
+                let params: InsertFilterParams = payload.try_into().unwrap();
+                let _ = self.editor.create_filter(params).await.unwrap();
             }
             FilterScript::AssertTableFilterCount { count } => {
                 let filters = self.editor.get_grid_filter().await.unwrap();

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

@@ -1,13 +1,13 @@
 use crate::grid::filter_test::script::FilterScript::*;
 use crate::grid::filter_test::script::*;
-use flowy_grid::entities::{CreateGridFilterPayloadPB, FieldType, TextFilterCondition};
+use flowy_grid::entities::{FieldType, InsertFilterPayloadPB, TextFilterCondition};
 use flowy_grid_data_model::revision::FieldRevision;
 
 #[tokio::test]
 async fn grid_filter_create_test() {
     let mut test = GridFilterTest::new().await;
     let field_rev = test.get_field_rev(FieldType::RichText);
-    let payload = CreateGridFilterPayloadPB::new(field_rev, TextFilterCondition::TextIsEmpty, Some("abc".to_owned()));
+    let payload = InsertFilterPayloadPB::new(field_rev, TextFilterCondition::TextIsEmpty, Some("abc".to_owned()));
     let scripts = vec![InsertGridTableFilter { payload }, AssertTableFilterCount { count: 1 }];
     test.run_scripts(scripts).await;
 }
@@ -19,7 +19,7 @@ async fn grid_filter_invalid_condition_panic_test() {
     let field_rev = test.get_field_rev(FieldType::RichText).clone();
 
     // 100 is not a valid condition, so this test should be panic.
-    let payload = CreateGridFilterPayloadPB::new(&field_rev, 100, Some("".to_owned()));
+    let payload = InsertFilterPayloadPB::new(&field_rev, 100, Some("".to_owned()));
     let scripts = vec![InsertGridTableFilter { payload }];
     test.run_scripts(scripts).await;
 }
@@ -46,6 +46,6 @@ async fn grid_filter_delete_test() {
 #[tokio::test]
 async fn grid_filter_get_rows_test() {}
 
-fn create_filter(field_rev: &FieldRevision, condition: TextFilterCondition, s: &str) -> CreateGridFilterPayloadPB {
-    CreateGridFilterPayloadPB::new(field_rev, condition, Some(s.to_owned()))
+fn create_filter(field_rev: &FieldRevision, condition: TextFilterCondition, s: &str) -> InsertFilterPayloadPB {
+    InsertFilterPayloadPB::new(field_rev, condition, Some(s.to_owned()))
 }

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

@@ -85,7 +85,7 @@ impl GridEditorTest {
             .row_revs
     }
 
-    pub async fn grid_filters(&self) -> Vec<GridFilterConfiguration> {
+    pub async fn grid_filters(&self) -> Vec<GridFilterConfigurationPB> {
         self.editor.get_grid_filter().await.unwrap()
     }
 
@@ -195,6 +195,8 @@ fn make_test_grid() -> BuildGridContext {
                         FieldType::SingleSelect => {
                             row_builder.insert_single_select_cell(|mut options| options.remove(0))
                         }
+                        FieldType::MultiSelect => row_builder
+                            .insert_multi_select_cell(|mut options| vec![options.remove(0), options.remove(0)]),
                         FieldType::Checkbox => row_builder.insert_checkbox_cell("true"),
                         _ => "".to_owned(),
                     };
@@ -209,6 +211,8 @@ fn make_test_grid() -> BuildGridContext {
                         FieldType::SingleSelect => {
                             row_builder.insert_single_select_cell(|mut options| options.remove(0))
                         }
+                        FieldType::MultiSelect => row_builder
+                            .insert_multi_select_cell(|mut options| vec![options.remove(0), options.remove(0)]),
                         FieldType::Checkbox => row_builder.insert_checkbox_cell("true"),
                         _ => "".to_owned(),
                     };
@@ -223,6 +227,9 @@ fn make_test_grid() -> BuildGridContext {
                         FieldType::SingleSelect => {
                             row_builder.insert_single_select_cell(|mut options| options.remove(1))
                         }
+                        FieldType::MultiSelect => {
+                            row_builder.insert_multi_select_cell(|mut options| vec![options.remove(0)])
+                        }
                         FieldType::Checkbox => row_builder.insert_checkbox_cell("false"),
                         _ => "".to_owned(),
                     };

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

@@ -8,7 +8,7 @@ use flowy_grid::services::field::{
 use flowy_grid::services::row::{decode_cell_data_from_type_option_cell_data, CreateRowMetaBuilder};
 use flowy_grid_data_model::entities::{
     CellChangeset, FieldChangesetParams, FieldType, GridBlockInfoChangeset, GridBlockMetaSnapshot, RowMetaChangeset,
-    TypeOptionDataEntry,
+    TypeOptionDataFormat,
 };
 
 #[tokio::test]

+ 55 - 10
frontend/rust-lib/flowy-grid/tests/grid/group_test/script.rs

@@ -1,11 +1,13 @@
 use crate::grid::grid_editor::GridEditorTest;
 use flowy_grid::entities::{
-    CreateRowParams, FieldChangesetParams, FieldType, GridLayout, GroupPB, MoveGroupParams, MoveGroupRowParams, RowPB,
+    CreateRowParams, FieldType, GridLayout, GroupPB, MoveGroupParams, MoveGroupRowParams, RowPB,
 };
 use flowy_grid::services::cell::{delete_select_option_cell, insert_select_option_cell};
-use flowy_grid_data_model::revision::RowChangeset;
-use std::time::Duration;
-use tokio::time::interval;
+use flowy_grid::services::field::{
+    edit_single_select_type_option, SelectOptionOperation, SelectOptionPB, SingleSelectTypeOptionPB,
+};
+use flowy_grid_data_model::revision::{FieldRevision, RowChangeset};
+use std::sync::Arc;
 
 pub enum GroupScript {
     AssertGroupRowCount {
@@ -44,8 +46,11 @@ pub enum GroupScript {
         from_group_index: usize,
         to_group_index: usize,
     },
-    UpdateField {
-        changeset: FieldChangesetParams,
+    UpdateSingleSelectOption {
+        inserted_options: Vec<SelectOptionPB>,
+    },
+    GroupByField {
+        field_id: String,
     },
 }
 
@@ -174,10 +179,16 @@ impl GridGroupTest {
                 assert_eq!(group.group_id, group_pb.group_id);
                 assert_eq!(group.desc, group_pb.desc);
             }
-            GroupScript::UpdateField { changeset } => {
-                self.editor.update_field(changeset).await.unwrap();
-                let mut interval = interval(Duration::from_millis(130));
-                interval.tick().await;
+            GroupScript::UpdateSingleSelectOption { inserted_options } => {
+                self.edit_single_select_type_option(|type_option| {
+                    for inserted_option in inserted_options {
+                        type_option.insert_option(inserted_option);
+                    }
+                })
+                .await;
+            }
+            GroupScript::GroupByField { field_id } => {
+                self.editor.group_by_field(&field_id).await.unwrap();
             }
         }
     }
@@ -191,6 +202,40 @@ impl GridGroupTest {
         let groups = self.group_at_index(group_index).await;
         groups.rows.get(row_index).unwrap().clone()
     }
+
+    #[allow(dead_code)]
+    pub async fn get_multi_select_field(&self) -> Arc<FieldRevision> {
+        let field = self
+            .inner
+            .field_revs
+            .iter()
+            .find(|field_rev| {
+                let field_type: FieldType = field_rev.ty.into();
+                field_type.is_multi_select()
+            })
+            .unwrap()
+            .clone();
+        field
+    }
+
+    pub async fn get_single_select_field(&self) -> Arc<FieldRevision> {
+        self.inner
+            .field_revs
+            .iter()
+            .find(|field_rev| {
+                let field_type: FieldType = field_rev.ty.into();
+                field_type.is_single_select()
+            })
+            .unwrap()
+            .clone()
+    }
+
+    pub async fn edit_single_select_type_option(&self, action: impl FnOnce(&mut SingleSelectTypeOptionPB)) {
+        let single_select = self.get_single_select_field().await;
+        edit_single_select_type_option(&single_select.id, self.editor.clone(), action)
+            .await
+            .unwrap();
+    }
 }
 
 impl std::ops::Deref for GridGroupTest {

+ 32 - 13
frontend/rust-lib/flowy-grid/tests/grid/group_test/test.rs

@@ -1,6 +1,7 @@
 use crate::grid::group_test::script::GridGroupTest;
 use crate::grid::group_test::script::GroupScript::*;
-use flowy_grid::entities::FieldChangesetParams;
+
+use flowy_grid::services::field::SelectOptionPB;
 
 #[tokio::test]
 async fn group_init_test() {
@@ -370,23 +371,41 @@ async fn group_move_group_test() {
 }
 
 #[tokio::test]
-async fn group_update_field_test() {
+async fn group_insert_single_select_option_test() {
     let mut test = GridGroupTest::new().await;
-    let group = test.group_at_index(0).await;
-    let changeset = FieldChangesetParams {
-        field_id: group.field_id.clone(),
-        grid_id: test.grid_id.clone(),
-        name: Some("ABC".to_string()),
-        ..Default::default()
-    };
+    let new_option_name = "New option";
+    let scripts = vec![
+        AssertGroupCount(4),
+        UpdateSingleSelectOption {
+            inserted_options: vec![SelectOptionPB::new(new_option_name)],
+        },
+        AssertGroupCount(5),
+    ];
+    test.run_scripts(scripts).await;
 
-    // group.desc = "ABC".to_string();
+    // the group at index 4 is the default_group, so the new insert group will be the
+    // index 3.
+    let group_3 = test.group_at_index(3).await;
+    assert_eq!(group_3.desc, new_option_name);
+}
+
+#[tokio::test]
+async fn group_group_by_other_field() {
+    let mut test = GridGroupTest::new().await;
+    let multi_select_field = test.get_multi_select_field().await;
     let scripts = vec![
-        UpdateField { changeset },
-        AssertGroup {
+        GroupByField {
+            field_id: multi_select_field.id.clone(),
+        },
+        AssertGroupRowCount {
             group_index: 0,
-            expected_group: group,
+            row_count: 3,
         },
+        AssertGroupRowCount {
+            group_index: 1,
+            row_count: 2,
+        },
+        AssertGroupCount(4),
     ];
     test.run_scripts(scripts).await;
 }

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

@@ -5,7 +5,7 @@ use flowy_grid::services::row::CreateRowMetaPayload;
 use flowy_grid_data_model::entities::{
     BuildGridContext, CellChangeset, Field, FieldChangesetParams, FieldMeta, FieldOrder, FieldType,
     GridBlockInfoChangeset, GridBlockMetaSnapshot, InsertFieldParams, RowMeta, RowMetaChangeset, RowOrder,
-    TypeOptionDataEntry,
+    TypeOptionDataFormat,
 };
 use flowy_revision::REVISION_WRITE_INTERVAL_IN_MILLIS;
 use flowy_sync::client_grid::GridBuilder;

+ 5 - 5
shared-lib/flowy-grid-data-model/src/revision/grid_rev.rs

@@ -143,15 +143,15 @@ impl FieldRevision {
         }
     }
 
-    pub fn insert_type_option_entry<T>(&mut self, entry: &T)
+    pub fn insert_type_option<T>(&mut self, type_option: &T)
     where
-        T: TypeOptionDataEntry + ?Sized,
+        T: TypeOptionDataFormat + ?Sized,
     {
         let id = self.ty.to_string();
-        self.type_options.insert(id, entry.json_str());
+        self.type_options.insert(id, type_option.json_str());
     }
 
-    pub fn get_type_option_entry<T: TypeOptionDataDeserializer>(&self, field_type_rev: FieldTypeRevision) -> Option<T> {
+    pub fn get_type_option<T: TypeOptionDataDeserializer>(&self, field_type_rev: FieldTypeRevision) -> Option<T> {
         let id = field_type_rev.to_string();
         // TODO: cache the deserialized type option
         self.type_options.get(&id).map(|s| T::from_json_str(s))
@@ -171,7 +171,7 @@ impl FieldRevision {
 
 /// The macro [impl_type_option] will implement the [TypeOptionDataEntry] for the type that
 /// supports the serde trait and the TryInto<Bytes> trait.
-pub trait TypeOptionDataEntry {
+pub trait TypeOptionDataFormat {
     fn json_str(&self) -> String;
     fn protobuf_bytes(&self) -> Bytes;
 }

+ 9 - 1
shared-lib/flowy-grid-data-model/src/revision/grid_setting_rev.rs

@@ -60,7 +60,7 @@ where
             .cloned()
     }
 
-    pub fn get_all_objects(&self, field_revs: &[Arc<FieldRevision>]) -> Option<HashMap<String, Vec<Arc<T>>>> {
+    pub fn get_objects_by_field_revs(&self, field_revs: &[Arc<FieldRevision>]) -> Option<HashMap<String, Vec<Arc<T>>>> {
         // Get the objects according to the FieldType, so we need iterate the field_revs.
         let objects_by_field_id = field_revs
             .iter()
@@ -76,6 +76,10 @@ where
         Some(objects_by_field_id)
     }
 
+    pub fn get_all_objects(&self) -> Vec<Arc<T>> {
+        self.inner.values().map(|map| map.all_objects()).flatten().collect()
+    }
+
     /// add object to the end of the list
     pub fn add_object(&mut self, field_id: &str, field_type: &FieldTypeRevision, object: T) {
         let object_rev_map = self
@@ -111,6 +115,10 @@ where
     pub fn new() -> Self {
         ObjectIndexMap::default()
     }
+
+    pub fn all_objects(&self) -> Vec<Arc<T>> {
+        self.object_by_field_type.values().cloned().flatten().collect()
+    }
 }
 
 impl<T> std::ops::Deref for ObjectIndexMap<T>

+ 20 - 22
shared-lib/flowy-grid-data-model/src/revision/group_rev.rs

@@ -4,9 +4,8 @@ use serde_json::Error;
 use serde_repr::*;
 
 pub trait GroupConfigurationContentSerde: Sized + Send + Sync {
-    fn from_configuration_content(s: &str) -> Result<Self, serde_json::Error>;
-
-    fn to_configuration_content(&self) -> Result<String, serde_json::Error>;
+    fn from_json(s: &str) -> Result<Self, serde_json::Error>;
+    fn to_json(&self) -> Result<String, serde_json::Error>;
 }
 
 #[derive(Debug, Clone, Serialize, Deserialize, Default)]
@@ -15,6 +14,7 @@ pub struct GroupConfigurationRevision {
     pub field_id: String,
     pub field_type_rev: FieldTypeRevision,
     pub groups: Vec<GroupRevision>,
+    // This content is serde in Json format
     pub content: String,
 }
 
@@ -23,7 +23,7 @@ impl GroupConfigurationRevision {
     where
         T: GroupConfigurationContentSerde,
     {
-        let content = content.to_configuration_content()?;
+        let content = content.to_json()?;
         Ok(Self {
             id: gen_grid_group_id(),
             field_id,
@@ -40,10 +40,10 @@ pub struct TextGroupConfigurationRevision {
 }
 
 impl GroupConfigurationContentSerde for TextGroupConfigurationRevision {
-    fn from_configuration_content(s: &str) -> Result<Self, Error> {
+    fn from_json(s: &str) -> Result<Self, Error> {
         serde_json::from_str(s)
     }
-    fn to_configuration_content(&self) -> Result<String, Error> {
+    fn to_json(&self) -> Result<String, Error> {
         serde_json::to_string(self)
     }
 }
@@ -54,10 +54,10 @@ pub struct NumberGroupConfigurationRevision {
 }
 
 impl GroupConfigurationContentSerde for NumberGroupConfigurationRevision {
-    fn from_configuration_content(s: &str) -> Result<Self, Error> {
+    fn from_json(s: &str) -> Result<Self, Error> {
         serde_json::from_str(s)
     }
-    fn to_configuration_content(&self) -> Result<String, Error> {
+    fn to_json(&self) -> Result<String, Error> {
         serde_json::to_string(self)
     }
 }
@@ -68,10 +68,10 @@ pub struct UrlGroupConfigurationRevision {
 }
 
 impl GroupConfigurationContentSerde for UrlGroupConfigurationRevision {
-    fn from_configuration_content(s: &str) -> Result<Self, Error> {
+    fn from_json(s: &str) -> Result<Self, Error> {
         serde_json::from_str(s)
     }
-    fn to_configuration_content(&self) -> Result<String, Error> {
+    fn to_json(&self) -> Result<String, Error> {
         serde_json::to_string(self)
     }
 }
@@ -82,11 +82,11 @@ pub struct CheckboxGroupConfigurationRevision {
 }
 
 impl GroupConfigurationContentSerde for CheckboxGroupConfigurationRevision {
-    fn from_configuration_content(s: &str) -> Result<Self, Error> {
+    fn from_json(s: &str) -> Result<Self, Error> {
         serde_json::from_str(s)
     }
 
-    fn to_configuration_content(&self) -> Result<String, Error> {
+    fn to_json(&self) -> Result<String, Error> {
         serde_json::to_string(self)
     }
 }
@@ -97,11 +97,11 @@ pub struct SelectOptionGroupConfigurationRevision {
 }
 
 impl GroupConfigurationContentSerde for SelectOptionGroupConfigurationRevision {
-    fn from_configuration_content(s: &str) -> Result<Self, Error> {
+    fn from_json(s: &str) -> Result<Self, Error> {
         serde_json::from_str(s)
     }
 
-    fn to_configuration_content(&self) -> Result<String, Error> {
+    fn to_json(&self) -> Result<String, Error> {
         serde_json::to_string(self)
     }
 }
@@ -113,22 +113,17 @@ pub struct GroupRevision {
     #[serde(default)]
     pub name: String,
 
-    #[serde(skip, default = "IS_DEFAULT_GROUP")]
-    pub is_default: bool,
-
     #[serde(default = "GROUP_REV_VISIBILITY")]
     pub visible: bool,
 }
 
 const GROUP_REV_VISIBILITY: fn() -> bool = || true;
-const IS_DEFAULT_GROUP: fn() -> bool = || false;
 
 impl GroupRevision {
     pub fn new(id: String, group_name: String) -> Self {
         Self {
             id,
             name: group_name,
-            is_default: false,
             visible: true,
         }
     }
@@ -137,10 +132,13 @@ impl GroupRevision {
         Self {
             id,
             name: group_name,
-            is_default: true,
             visible: true,
         }
     }
+
+    pub fn update_with_other(&mut self, other: &GroupRevision) {
+        self.visible = other.visible
+    }
 }
 
 #[derive(Default, Serialize, Deserialize)]
@@ -150,10 +148,10 @@ pub struct DateGroupConfigurationRevision {
 }
 
 impl GroupConfigurationContentSerde for DateGroupConfigurationRevision {
-    fn from_configuration_content(s: &str) -> Result<Self, Error> {
+    fn from_json(s: &str) -> Result<Self, Error> {
         serde_json::from_str(s)
     }
-    fn to_configuration_content(&self) -> Result<String, Error> {
+    fn to_json(&self) -> Result<String, Error> {
         serde_json::to_string(self)
     }
 }

+ 9 - 5
shared-lib/flowy-sync/src/client_grid/view_revision_pad.rs

@@ -48,8 +48,12 @@ impl GridViewRevisionPad {
         Self::from_delta(delta)
     }
 
-    pub fn get_all_groups(&self, field_revs: &[Arc<FieldRevision>]) -> Option<GroupConfigurationsByFieldId> {
-        self.groups.get_all_objects(field_revs)
+    pub fn get_groups_by_field_revs(&self, field_revs: &[Arc<FieldRevision>]) -> Option<GroupConfigurationsByFieldId> {
+        self.groups.get_objects_by_field_revs(field_revs)
+    }
+
+    pub fn get_all_groups(&self) -> Vec<Arc<GroupConfigurationRevision>> {
+        self.groups.get_all_objects()
     }
 
     #[tracing::instrument(level = "trace", skip_all, err)]
@@ -57,12 +61,12 @@ impl GridViewRevisionPad {
         &mut self,
         field_id: &str,
         field_type: &FieldTypeRevision,
-        group_rev: GroupConfigurationRevision,
+        group_configuration_rev: GroupConfigurationRevision,
     ) -> CollaborateResult<Option<GridViewRevisionChangeset>> {
         self.modify(|view| {
             // Only save one group
             view.groups.clear();
-            view.groups.add_object(field_id, field_type, group_rev);
+            view.groups.add_object(field_id, field_type, group_configuration_rev);
             Ok(Some(()))
         })
     }
@@ -111,7 +115,7 @@ impl GridViewRevisionPad {
     }
 
     pub fn get_all_filters(&self, field_revs: &[Arc<FieldRevision>]) -> Option<FilterConfigurationsByFieldId> {
-        self.filters.get_all_objects(field_revs)
+        self.filters.get_objects_by_field_revs(field_revs)
     }
 
     pub fn get_filters(

+ 1 - 1
shared-lib/lib-ot/src/core/document/document_operation.rs

@@ -178,7 +178,7 @@ mod tests {
                 node_type: "text".into(),
                 attributes: NodeAttributes::new(),
                 delta: None,
-                children: vec![Box::new(NodeSubTree::new("text".into()))],
+                children: vec![Box::new(NodeSubTree::new("text"))],
             })],
         };
         let result = serde_json::to_string(&insert).unwrap();

+ 1 - 0
shared-lib/lib-ot/src/core/document/mod.rs

@@ -1,3 +1,4 @@
+#![allow(clippy::module_inception)]
 mod attributes;
 mod document;
 mod document_operation;

+ 15 - 15
shared-lib/lib-ot/tests/main.rs

@@ -13,7 +13,7 @@ fn test_documents() {
     let mut document = DocumentTree::new();
     let transaction = {
         let mut tb = TransactionBuilder::new(&document);
-        tb.insert_nodes_at_path(&vec![0].into(), &vec![Box::new(NodeSubTree::new("text"))]);
+        tb.insert_nodes_at_path(&vec![0].into(), &[Box::new(NodeSubTree::new("text"))]);
         tb.finalize()
     };
     document.apply(transaction).unwrap();
@@ -47,16 +47,16 @@ fn test_inserts_nodes() {
     let mut document = DocumentTree::new();
     let transaction = {
         let mut tb = TransactionBuilder::new(&document);
-        tb.insert_nodes_at_path(&vec![0].into(), &vec![Box::new(NodeSubTree::new("text"))]);
-        tb.insert_nodes_at_path(&vec![1].into(), &vec![Box::new(NodeSubTree::new("text"))]);
-        tb.insert_nodes_at_path(&vec![2].into(), &vec![Box::new(NodeSubTree::new("text"))]);
+        tb.insert_nodes_at_path(&vec![0].into(), &[Box::new(NodeSubTree::new("text"))]);
+        tb.insert_nodes_at_path(&vec![1].into(), &[Box::new(NodeSubTree::new("text"))]);
+        tb.insert_nodes_at_path(&vec![2].into(), &[Box::new(NodeSubTree::new("text"))]);
         tb.finalize()
     };
     document.apply(transaction).unwrap();
 
     let transaction = {
         let mut tb = TransactionBuilder::new(&document);
-        tb.insert_nodes_at_path(&vec![1].into(), &vec![Box::new(NodeSubTree::new("text"))]);
+        tb.insert_nodes_at_path(&vec![1].into(), &[Box::new(NodeSubTree::new("text"))]);
         tb.finalize()
     };
     document.apply(transaction).unwrap();
@@ -69,11 +69,11 @@ fn test_inserts_subtrees() {
         let mut tb = TransactionBuilder::new(&document);
         tb.insert_nodes_at_path(
             &vec![0].into(),
-            &vec![Box::new(NodeSubTree {
+            &[Box::new(NodeSubTree {
                 node_type: "text".into(),
                 attributes: NodeAttributes::new(),
                 delta: None,
-                children: vec![Box::new(NodeSubTree::new("image".into()))],
+                children: vec![Box::new(NodeSubTree::new("image"))],
             })],
         );
         tb.finalize()
@@ -90,9 +90,9 @@ fn test_update_nodes() {
     let mut document = DocumentTree::new();
     let transaction = {
         let mut tb = TransactionBuilder::new(&document);
-        tb.insert_nodes_at_path(&vec![0].into(), &vec![Box::new(NodeSubTree::new("text"))]);
-        tb.insert_nodes_at_path(&vec![1].into(), &vec![Box::new(NodeSubTree::new("text"))]);
-        tb.insert_nodes_at_path(&vec![2].into(), &vec![Box::new(NodeSubTree::new("text"))]);
+        tb.insert_nodes_at_path(&vec![0].into(), &[Box::new(NodeSubTree::new("text"))]);
+        tb.insert_nodes_at_path(&vec![1].into(), &[Box::new(NodeSubTree::new("text"))]);
+        tb.insert_nodes_at_path(&vec![2].into(), &[Box::new(NodeSubTree::new("text"))]);
         tb.finalize()
     };
     document.apply(transaction).unwrap();
@@ -115,9 +115,9 @@ fn test_delete_nodes() {
     let mut document = DocumentTree::new();
     let transaction = {
         let mut tb = TransactionBuilder::new(&document);
-        tb.insert_nodes_at_path(&vec![0].into(), &vec![Box::new(NodeSubTree::new("text"))]);
-        tb.insert_nodes_at_path(&vec![1].into(), &vec![Box::new(NodeSubTree::new("text"))]);
-        tb.insert_nodes_at_path(&vec![2].into(), &vec![Box::new(NodeSubTree::new("text"))]);
+        tb.insert_nodes_at_path(&vec![0].into(), &[Box::new(NodeSubTree::new("text"))]);
+        tb.insert_nodes_at_path(&vec![1].into(), &[Box::new(NodeSubTree::new("text"))]);
+        tb.insert_nodes_at_path(&vec![2].into(), &[Box::new(NodeSubTree::new("text"))]);
         tb.finalize()
     };
     document.apply(transaction).unwrap();
@@ -138,8 +138,8 @@ fn test_errors() {
     let mut document = DocumentTree::new();
     let transaction = {
         let mut tb = TransactionBuilder::new(&document);
-        tb.insert_nodes_at_path(&vec![0].into(), &vec![Box::new(NodeSubTree::new("text"))]);
-        tb.insert_nodes_at_path(&vec![100].into(), &vec![Box::new(NodeSubTree::new("text"))]);
+        tb.insert_nodes_at_path(&vec![0].into(), &[Box::new(NodeSubTree::new("text"))]);
+        tb.insert_nodes_at_path(&vec![100].into(), &[Box::new(NodeSubTree::new("text"))]);
         tb.finalize()
     };
     let result = document.apply(transaction);