Bladeren bron

Merge branch 'main' into feat/flowy-overlay

Vincent Chan 2 jaren geleden
bovenliggende
commit
bd40768c6a
100 gewijzigde bestanden met toevoegingen van 1560 en 789 verwijderingen
  1. 9 2
      .github/workflows/dart_lint.yml
  2. 4 1
      .github/workflows/dart_test.yml
  3. 11 3
      .github/workflows/flowy_editor_test.yml
  4. 11 3
      .github/workflows/rust_lint.yml
  5. 11 4
      .github/workflows/rust_test.yml
  6. 7 0
      frontend/app_flowy/assets/images/grid/setting/group.svg
  7. 2 1
      frontend/app_flowy/assets/translations/en.json
  8. 36 19
      frontend/app_flowy/lib/plugins/board/application/board_bloc.dart
  9. 44 49
      frontend/app_flowy/lib/plugins/board/application/board_data_controller.dart
  10. 24 8
      frontend/app_flowy/lib/plugins/board/application/board_listener.dart
  11. 3 7
      frontend/app_flowy/lib/plugins/board/application/card/board_date_cell_bloc.dart
  12. 5 5
      frontend/app_flowy/lib/plugins/board/application/card/card_bloc.dart
  13. 5 5
      frontend/app_flowy/lib/plugins/board/application/card/card_data_controller.dart
  14. 1 0
      frontend/app_flowy/lib/plugins/board/application/toolbar/board_setting_bloc.dart
  15. 11 11
      frontend/app_flowy/lib/plugins/board/presentation/board_page.dart
  16. 12 5
      frontend/app_flowy/lib/plugins/board/presentation/toolbar/board_setting.dart
  17. 3 3
      frontend/app_flowy/lib/plugins/board/presentation/toolbar/board_toolbar.dart
  18. 3 3
      frontend/app_flowy/lib/plugins/grid/application/block/block_cache.dart
  19. 13 12
      frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/cell_controller.dart
  20. 6 6
      frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/cell_service.dart
  21. 1 1
      frontend/app_flowy/lib/plugins/grid/application/cell/date_cal_bloc.dart
  22. 3 7
      frontend/app_flowy/lib/plugins/grid/application/cell/date_cell_bloc.dart
  23. 1 1
      frontend/app_flowy/lib/plugins/grid/application/cell/select_option_service.dart
  24. 0 192
      frontend/app_flowy/lib/plugins/grid/application/field/field_cache.dart
  25. 281 0
      frontend/app_flowy/lib/plugins/grid/application/field/field_controller.dart
  26. 1 0
      frontend/app_flowy/lib/plugins/grid/application/field/field_service.dart
  27. 4 3
      frontend/app_flowy/lib/plugins/grid/application/field/type_option/type_option_data_controller.dart
  28. 6 4
      frontend/app_flowy/lib/plugins/grid/application/grid_bloc.dart
  29. 8 23
      frontend/app_flowy/lib/plugins/grid/application/grid_data_controller.dart
  30. 10 11
      frontend/app_flowy/lib/plugins/grid/application/grid_header_bloc.dart
  31. 11 9
      frontend/app_flowy/lib/plugins/grid/application/row/row_bloc.dart
  32. 4 3
      frontend/app_flowy/lib/plugins/grid/application/row/row_cache.dart
  33. 5 5
      frontend/app_flowy/lib/plugins/grid/application/row/row_data_controller.dart
  34. 84 0
      frontend/app_flowy/lib/plugins/grid/application/setting/group_bloc.dart
  35. 19 15
      frontend/app_flowy/lib/plugins/grid/application/setting/property_bloc.dart
  36. 59 0
      frontend/app_flowy/lib/plugins/grid/application/setting/setting_controller.dart
  37. 47 0
      frontend/app_flowy/lib/plugins/grid/application/setting/setting_listener.dart
  38. 32 0
      frontend/app_flowy/lib/plugins/grid/application/setting/setting_service.dart
  39. 13 10
      frontend/app_flowy/lib/plugins/grid/presentation/grid_page.dart
  40. 2 2
      frontend/app_flowy/lib/plugins/grid/presentation/layout/layout.dart
  41. 5 5
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/grid_header.dart
  42. 6 4
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/builder.dart
  43. 2 2
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/grid_row.dart
  44. 3 3
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/row_detail.dart
  45. 110 0
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/grid_group.dart
  46. 16 16
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/grid_property.dart
  47. 3 3
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/grid_setting.dart
  48. 5 5
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/grid_toolbar.dart
  49. 6 6
      frontend/app_flowy/lib/startup/deps_resolver.dart
  50. 6 0
      frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_data.dart
  51. 5 0
      frontend/app_flowy/packages/appflowy_editor/CHANGELOG.md
  52. 44 25
      frontend/app_flowy/packages/appflowy_editor/README.md
  53. 35 32
      frontend/app_flowy/packages/appflowy_editor/documentation/customizing.md
  54. BIN
      frontend/app_flowy/packages/appflowy_editor/documentation/images/appflowy-editor-example.gif
  55. 43 26
      frontend/app_flowy/packages/appflowy_editor/documentation/testing.md
  56. 3 2
      frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/image_node_widget.dart
  57. 1 1
      frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/network_image_node_widget.dart
  58. 1 1
      frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/youtube_link_node_widget.dart
  59. 1 0
      frontend/app_flowy/packages/appflowy_editor/lib/appflowy_editor.dart
  60. 0 1
      frontend/app_flowy/packages/appflowy_editor/lib/src/document/node_iterator.dart
  61. 1 3
      frontend/app_flowy/packages/appflowy_editor/lib/src/document/position.dart
  62. 1 3
      frontend/app_flowy/packages/appflowy_editor/lib/src/document/text_delta.dart
  63. 2 1
      frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/node_extensions.dart
  64. 2 1
      frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_widget.dart
  65. 3 3
      frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/bulleted_list_text.dart
  66. 3 3
      frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/checkbox_text.dart
  67. 1 1
      frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/default_selectable.dart
  68. 1 1
      frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/flowy_rich_text.dart
  69. 3 3
      frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/heading_text.dart
  70. 3 3
      frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/number_list_text.dart
  71. 3 3
      frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/quoted_text.dart
  72. 3 3
      frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/rich_text.dart
  73. 3 3
      frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection/selectable.dart
  74. 1 1
      frontend/app_flowy/packages/appflowy_editor/lib/src/service/selection_service.dart
  75. 6 1
      frontend/app_flowy/packages/appflowy_editor/pubspec.yaml
  76. 2 3
      frontend/app_flowy/packages/appflowy_editor/test/legacy/flowy_editor_test.dart
  77. 1 1
      frontend/app_flowy/pubspec.lock
  78. 2 0
      frontend/rust-lib/flowy-grid/src/dart_notification.rs
  79. 14 14
      frontend/rust-lib/flowy-grid/src/entities/filter_entities/util.rs
  80. 9 5
      frontend/rust-lib/flowy-grid/src/entities/group_entities/group.rs
  81. 8 2
      frontend/rust-lib/flowy-grid/src/entities/group_entities/group_changeset.rs
  82. 10 11
      frontend/rust-lib/flowy-grid/src/entities/setting_entities.rs
  83. 33 5
      frontend/rust-lib/flowy-grid/src/event_handler.rs
  84. 6 3
      frontend/rust-lib/flowy-grid/src/event_map.rs
  85. 3 3
      frontend/rust-lib/flowy-grid/src/macros.rs
  86. 7 7
      frontend/rust-lib/flowy-grid/src/services/cell/cell_operation.rs
  87. 3 3
      frontend/rust-lib/flowy-grid/src/services/field/field_builder.rs
  88. 45 0
      frontend/rust-lib/flowy-grid/src/services/field/field_operation.rs
  89. 2 0
      frontend/rust-lib/flowy-grid/src/services/field/mod.rs
  90. 2 2
      frontend/rust-lib/flowy-grid/src/services/field/type_options/checkbox_type_option/checkbox_type_option.rs
  91. 2 2
      frontend/rust-lib/flowy-grid/src/services/field/type_options/date_type_option/date_type_option.rs
  92. 2 2
      frontend/rust-lib/flowy-grid/src/services/field/type_options/number_type_option/number_type_option.rs
  93. 2 2
      frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/multi_select_type_option.rs
  94. 2 2
      frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/select_option.rs
  95. 2 2
      frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/single_select_type_option.rs
  96. 2 2
      frontend/rust-lib/flowy-grid/src/services/field/type_options/text_type_option/text_type_option.rs
  97. 2 2
      frontend/rust-lib/flowy-grid/src/services/field/type_options/url_type_option/url_type_option.rs
  98. 7 7
      frontend/rust-lib/flowy-grid/src/services/filter/filter_service.rs
  99. 24 5
      frontend/rust-lib/flowy-grid/src/services/grid_editor.rs
  100. 225 106
      frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs

+ 9 - 2
.github/workflows/dart_lint.yml

@@ -7,9 +7,16 @@ name: Flutter lint
 
 on:
   push:
-    branches: [main]
+    branches:
+      - "main"
+    paths:
+      - "frontend/app_flowy/**"
+
   pull_request:
-    branches: [main]
+    branches:
+      - "main"
+    paths:
+      - "frontend/app_flowy/**"
 
 env:
   CARGO_TERM_COLOR: always

+ 4 - 1
.github/workflows/dart_test.yml

@@ -4,11 +4,14 @@ on:
   push:
     branches:
       - "main"
+    paths:
+      - "frontend/app_flowy/**"
 
   pull_request:
     branches:
       - "main"
-      - "feat/flowy_editor"
+    paths:
+      - "frontend/app_flowy/**"
 
 env:
   CARGO_TERM_COLOR: always

+ 11 - 3
.github/workflows/flowy_editor_test.yml

@@ -5,13 +5,13 @@ on:
     branches:
       - "main"
     paths:
-      - "frontend/app_flowy/packages/appflowy_editor"
+      - "frontend/app_flowy/packages/appflowy_editor/**"
 
   pull_request:
     branches:
       - "main"
     paths:
-      - "frontend/app_flowy/packages/appflowy_editor"
+      - "frontend/app_flowy/packages/appflowy_editor/**"
 
 env:
   CARGO_TERM_COLOR: always
@@ -37,4 +37,12 @@ jobs:
         working-directory: frontend/app_flowy/packages/appflowy_editor
         run: |
           flutter pub get
-          flutter test
+          flutter test --coverage
+
+      - uses: codecov/codecov-action@v3
+        with: 
+          name: appflowy_editor
+          env_vars: ${{ matrix.os }}
+          fail_ci_if_error: true
+          verbose: true
+

+ 11 - 3
.github/workflows/rust_lint.yml

@@ -2,10 +2,18 @@ name: Rust lint
 
 on:
   push:
-    branches: [ main ]
-  pull_request:
-    branches: [ main ]
+    branches:
+      - "main"
+    paths:
+      - "frontend/rust-lib/**"
+      - "shared-lib/**"
 
+  pull_request:
+    branches:
+      - "main"
+    paths:
+      - "frontend/rust-lib/**"
+      - "shared-lib/**"
 
 env:
   CARGO_TERM_COLOR: always

+ 11 - 4
.github/workflows/rust_test.yml

@@ -2,11 +2,18 @@ name: Unit test(Rust)
 
 on:
   push:
-    branches: 
-      - 'main'
+    branches:
+      - "main"
+    paths:
+      - "frontend/rust-lib/**"
+      - "shared-lib/**"
+
   pull_request:
-    branches: 
-      - 'main'
+    branches:
+      - "main"
+    paths:
+      - "frontend/rust-lib/**"
+      - "shared-lib/**"
 
 env:
   CARGO_TERM_COLOR: always

+ 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';
@@ -83,8 +83,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(
@@ -96,6 +95,7 @@ class _BoardContentState extends State<BoardContent> {
                   const _ToolbarBlocAdaptor(),
                   Expanded(
                     child: AFBoard(
+                      key: UniqueKey(),
                       scrollManager: scrollManager,
                       scrollController: scrollController,
                       dataController: context.read<BoardBloc>().boardController,
@@ -223,10 +223,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,
     );
@@ -253,7 +253,7 @@ class _BoardContentState extends State<BoardContent> {
         dataController: cardController,
         openCard: (context) => _openCard(
           gridId,
-          fieldCache,
+          fieldController,
           rowPB,
           rowCache,
           context,
@@ -272,17 +272,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,
     );
 
@@ -308,7 +308,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);

+ 12 - 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,
       );
 }
 
@@ -125,6 +126,8 @@ extension _GridSettingExtension on BoardSettingAction {
     switch (this) {
       case BoardSettingAction.properties:
         return 'grid/setting/properties';
+      case BoardSettingAction.groups:
+        return 'grid/setting/group';
     }
   }
 
@@ -132,6 +135,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();
     }
   }
 }
@@ -158,7 +163,7 @@ class _BoardSettingListPopoverState extends State<BoardSettingListPopover> {
         constraints: BoxConstraints.loose(const Size(260, 400)),
         child: GridPropertyList(
           gridId: widget.settingContext.viewId,
-          fieldCache: widget.settingContext.fieldCache,
+          fieldController: widget.settingContext.fieldController,
         ),
       );
     }
@@ -169,6 +174,8 @@ class _BoardSettingListPopoverState extends State<BoardSettingListPopover> {
         settingContext: widget.settingContext,
         onAction: (action, settingContext) {
           switch (action) {
+            case BoardSettingAction.groups:
+              break;
             case BoardSettingAction.properties:
               setState(() {
                 _showGridPropertyList = true;

+ 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:appflowy_popover/popover.dart';
 import 'package:flowy_infra/image.dart';
 import 'package:flowy_infra/theme.dart';
@@ -10,11 +10,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';
@@ -158,10 +158,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,
     );
   }
@@ -174,10 +175,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) {
@@ -248,10 +250,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,
     );
 
@@ -265,7 +268,7 @@ class _GridRowsState extends State<_GridRows> {
           _openRowDetailPage(
             context,
             rowInfo,
-            fieldCache,
+            fieldController,
             rowCache,
             cellBuilder,
           );
@@ -278,13 +281,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';
@@ -20,11 +20,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);
@@ -40,7 +40,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;
       },
@@ -101,7 +101,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(
                   key: _getKeyById(ctx.field.id),
                   cellContext: ctx,

+ 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:appflowy_popover/popover.dart';
@@ -126,17 +127,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<GridCellAccessoryBuilder> accessories = [];
-            if (cellId.field.isPrimary) {
+            if (cellId.fieldContext.isPrimary) {
               accessories.add(
                 GridCellAccessoryBuilder(
                   builder: (key) => PrimaryCellAccessory(

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

@@ -225,16 +225,16 @@ class _RowDetailCellState extends State<_RowDetailCell> {
                     constraints: BoxConstraints.loose(const Size(240, 200)),
                     child: FieldEditor(
                       gridId: widget.cellId.gridId,
-                      fieldName: widget.cellId.field.name,
+                      fieldName: widget.cellId.fieldContext.field.name,
                       typeOptionLoader: FieldTypeOptionLoader(
                         gridId: widget.cellId.gridId,
-                        field: widget.cellId.field,
+                        field: widget.cellId.fieldContext.field,
                       ),
                     ),
                   );
                 },
                 child: FieldCellButton(
-                  field: widget.cellId.field,
+                  field: widget.cellId.fieldContext.field,
                   onTap: () => popover.show(),
                 ),
               ),

+ 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());
+        },
+      ),
+    );
+  }
+}

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

@@ -11,20 +11,19 @@ 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';
 
 class GridPropertyList extends StatefulWidget {
   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,15 +44,15 @@ class _GridPropertyListState extends State<GridPropertyList> {
   Widget build(BuildContext context) {
     return BlocProvider(
       create: (context) => getIt<GridPropertyBloc>(
-          param1: widget.gridId, param2: widget.fieldCache)
+          param1: widget.gridId, param2: widget.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(
               popoverMutex: _popoverMutex,
               gridId: widget.gridId,
-              field: field,
+              fieldContext: field,
               key: ValueKey(field.id),
             );
           }).toList();
@@ -76,12 +75,12 @@ class _GridPropertyListState extends State<GridPropertyList> {
 }
 
 class _GridPropertyCell extends StatelessWidget {
-  final FieldPB field;
+  final GridFieldContext fieldContext;
   final String gridId;
   final PopoverMutex popoverMutex;
   const _GridPropertyCell({
     required this.gridId,
-    required this.field,
+    required this.fieldContext,
     required this.popoverMutex,
     Key? key,
   }) : super(key: key);
@@ -90,7 +89,7 @@ class _GridPropertyCell extends StatelessWidget {
   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);
 
@@ -108,7 +107,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),
         )
@@ -122,18 +121,19 @@ class _GridPropertyCell extends StatelessWidget {
       triggerActions: PopoverTriggerActionFlags.click,
       offset: const Offset(20, 0),
       child: 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),
       ),
       popupBuilder: (BuildContext context) {
         return OverlayContainer(
           constraints: BoxConstraints.loose(const Size(240, 200)),
           child: FieldEditor(
             gridId: gridId,
-            fieldName: field.name,
-            typeOptionLoader:
-                FieldTypeOptionLoader(gridId: gridId, field: field),
+            fieldName: fieldContext.name,
+            typeOptionLoader: FieldTypeOptionLoader(
+                gridId: gridId, field: fieldContext.field),
           ),
         );
       },

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

@@ -11,16 +11,16 @@ 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';
 
 class GridSettingContext {
   final String gridId;
-  final GridFieldCache fieldCache;
+  final GridFieldController fieldController;
 
   GridSettingContext({
     required this.gridId,
-    required this.fieldCache,
+    required this.fieldController,
   });
 }
 

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

@@ -8,17 +8,17 @@ 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_property.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,
   });
 }
 
@@ -30,7 +30,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,
@@ -89,7 +89,7 @@ class _GridSettingListPopoverState extends State<_GridSettingListPopover> {
         constraints: BoxConstraints.loose(const Size(260, 400)),
         child: GridPropertyList(
           gridId: widget.settingContext.gridId,
-          fieldCache: widget.settingContext.fieldCache,
+          fieldController: widget.settingContext.fieldController,
         ),
       );
     }

+ 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) {

+ 5 - 0
frontend/app_flowy/packages/appflowy_editor/CHANGELOG.md

@@ -1,3 +1,8 @@
+## 0.0.4
+* Support more shortcut events.
+* Fix some bugs.
+* Update the documentation.
+
 ## 0.0.3
 * Support insert image.
 * Support insert link.

+ 44 - 25
frontend/app_flowy/packages/appflowy_editor/README.md

@@ -20,27 +20,39 @@ and the Flutter guide for
     <a href="https://twitter.com/appflowy"><b>Twitter</b></a>
 </p>
 
+<p align="center">
+    <a href="https://codecov.io/gh/AppFlowy-IO/AppFlowy" >
+        <img src="https://codecov.io/gh/AppFlowy-IO/AppFlowy/branch/main/graph/badge.svg?token=YTFKUF70B6"/>
+    </a>
+</p>
+
 <div align="center">
-    <img src="https://i.ibb.co/HNnc1jP/appflowy-editor-example.gif" width = "900"/>
+    <img src="https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/app_flowy/packages/appflowy_editor/documentation/images/appflowy-editor-example.gif?raw=true" width = "700" style = "padding: 100"/>
 </div>
 
 ## Key Features
 
-* Allow you to build rich, intuitive editors
-* Design and modify it your way by customizing components, shortcut events, and many more coming soon including menu options and themes
-* [Test-covered](https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/app_flowy/packages/appflowy_editor/documentation/testing.md) and maintained by AppFlowy's core team along with a community of more than 1,000 builders
+* Build rich, intuitive editors
+* Design and modify an ever expanding list of customizable features including
+  * components (such as form input controls, numbered lists, and rich text widgets)
+  * shortcut events
+  * menu options (**coming soon!**)
+  * themes (**coming soon!**)
+* [Test-coverage](https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/app_flowy/packages/appflowy_editor/documentation/testing.md) and on-going maintenance by AppFlowy's core team and community of more than 1,000 builders
 
+## Getting Started
 
-## Getting started
+Add the AppFlowy editor [Flutter package](https://docs.flutter.dev/development/packages-and-plugins/using-packages) to your environment.
 
 ```shell
 flutter pub add appflowy_editor
 flutter pub get
 ```
 
-## How to use
+## Creating Your First Editor
+
+Start by creating a new empty AppFlowyEditor object. 
 
-Let's create a new AppFlowyEditor object 
 ```dart
 final editorState = EditorState.empty(); // an empty state
 final editor = AppFlowyEditor(
@@ -50,7 +62,8 @@ final editor = AppFlowyEditor(
 );
 ```
 
-You can also create an editor from a JSON file
+You can also create an editor from a JSON object in order to configure your initial state.
+
 ```dart
 final json = ...;
 final editorState = EditorState(StateTree.fromJson(data));
@@ -61,37 +74,43 @@ final editor = AppFlowyEditor(
 );
 ```
 
-To get a sense for how you might use it, run this example:
+To get a sense for how the AppFlowy Editor works, run our example:
+
 ```shell
 git clone https://github.com/AppFlowy-IO/AppFlowy.git
 cd frontend/app_flowy/packages/appflowy_editor/example
 flutter run
 ```
 
+## Customizing Your Editor
+
+### Customizing Components
+
+Please refer to our documentation on customizing AppFlowy for a detailed discussion about [customizing components](https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/app_flowy/packages/appflowy_editor/documentation/customizing.md#customize-a-component).
 
-## How to customize 
-### Customize a component
-Please refer to [customizing a component](https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/app_flowy/packages/appflowy_editor/documentation/customizing.md#customize-a-component) for more details.
+Below are some examples of component customizations:
 
+ * [Checkbox Text](https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/checkbox_text.dart) demonstrates how to extend new styles based on existing rich text components
+ * [Image](https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/network_image_node_widget.dart) demonstrates how to extend a new node and render it
+ * See further examples of [rich-text plugins](https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text)
+    
+### Customizing Shortcut Events
 
-### Customize a shortcut event
-Please refer to [customizing a shortcut event](https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/app_flowy/packages/appflowy_editor/documentation/customizing.md#customize-a-shortcut-event) for more details.
+Please refer to our documentation on customizing AppFlowy for a detailed discussion about [customizing shortcut events](https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/app_flowy/packages/appflowy_editor/documentation/customizing.md#customize-a-shortcut-event).
 
-## More Examples
-* Customize a component
-    * [Checkbox Text](https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/checkbox_text.dart) shows you how to extend new styles based on existing rich text components
-    * [Image](https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/network_image_node_widget.dart) teaches you how to extend a new node and render it
-    * And more examples on [rich-text plugins](https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text)
-* Customize a shortcut event
-    * [BIUS](https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart) shows you how to make text bold/italic/underline/strikethrough through shortcut keys
-    * [Paste HTML](https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart) gives you an idea on how to handle pasted styles through shortcut keys
-    * Need more examples? Check out [Internal key event handlers](https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers)
+Below are some examples of shortcut event customizations:
+
+ * [BIUS](https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart) demonstrates how to make text bold/italic/underline/strikethrough through shortcut keys
+ * [Paste HTML](https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart) gives you an idea on how to handle pasted styles through shortcut keys
+ * Need more examples? Check out [Internal key event handlers](https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers)
 
 ## Glossary
 Please refer to the API documentation.
 
 ## Contributing
-Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are greatly appreciated. Please look at [CONTRIBUTING.md](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/contributing-to-appflowy) for details.
+Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are greatly appreciated. 
+
+Please look at [CONTRIBUTING.md](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/contributing-to-appflowy) for details.
 
 ## License
-Distributed under the AGPLv3 License. See LICENSE for more information.
+Distributed under the AGPLv3 License. See [LICENSE](https://github.com/AppFlowy-IO/AppFlowy-Docs/blob/main/LICENSE) for more information.

+ 35 - 32
frontend/app_flowy/packages/appflowy_editor/documentation/customizing.md

@@ -1,12 +1,12 @@
-# How to customize ...
+# Customizing Editor Features
 
-## Customize a shortcut event
+## Customizing a Shortcut Event
 
 We will use a simple example to illustrate how to quickly add a shortcut event.
 
-For example, typing `_xxx_` will be converted into _xxx_.
+In this example, text that starts and ends with an underscore ( \_ ) character will be rendered in italics for emphasis.  So typing `_xxx_` will automatically be converted into _xxx_.
 
-Let's start with a blank document.
+Let's start with a blank document:
 
 ```dart
 @override
@@ -27,7 +27,7 @@ At this point, nothing magic will happen after typing `_xxx_`.
 
 ![Before](./images/customizing_a_shortcut_event_before.gif)
 
-Next, we will create a function to handle an underscore input.
+To implement our shortcut event we will create a function to handle an underscore input.
 
 ```dart
 import 'package:appflowy_editor/appflowy_editor.dart';
@@ -35,23 +35,25 @@ import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
 
 FlowyKeyEventHandler underscoreToItalicHandler = (editorState, event) {
-  // Since we only need to handler the input of `underscore`.
-  // All inputs except `underscore` will be ignored directly.
+  // Since we only need to handle the input of an 'underscore' character,
+  // all inputs except `underscore` will be ignored immediately.
   if (event.logicalKey != LogicalKeyboardKey.underscore) {
     return KeyEventResult.ignored;
   }
 };
 ```
 
-Then, we need to determine if the currently selected node is `TextNode` and the selection is collapsed.
+Then, we need to determine if the currently selected node is a `TextNode` and if the selection is collapsed.
+
+If so, we will continue.
 
 ```dart
 // ...
 FlowyKeyEventHandler underscoreToItalicHandler = (editorState, event) {
   // ...
   
-  // Obtaining the selection and selected nodes of the current document through `selectionService`.
-  // And determine whether the selection is collapsed and whether the selected node is a text node.
+  // Obtain the selection and selected nodes of the current document through the 'selectionService'
+  // to determine whether the selection is collapsed and whether the selected node is a text node.
   final selectionService = editorState.service.selectionService;
   final selection = selectionService.currentSelection.value;
   final textNodes = selectionService.currentSelectedNodes.whereType<TextNode>();
@@ -60,11 +62,11 @@ FlowyKeyEventHandler underscoreToItalicHandler = (editorState, event) {
   }
 ```
 
-Now, we start dealing with underscore. 
+Now, we deal with handling the underscore. 
 
 Look for the position of the previous underscore and 
-1. return, if not found. 
-2. if found, the text wrapped in between two underscores will be displayed in italic.
+1. if one is _not_ found, return without doing anything. 
+2. if one is found, the text enclosed within the two underscores will be formatted to display in italics.
 
 ```dart
 // ...
@@ -73,14 +75,14 @@ FlowyKeyEventHandler underscoreToItalicHandler = (editorState, event) {
 
   final textNode = textNodes.first;
   final text = textNode.toRawString();
-  // Determine if `underscore` already exists in the text node
+  // Determine if an 'underscore' already exists in the text node
   final previousUnderscore = text.indexOf('_');
   if (previousUnderscore == -1) {
     return KeyEventResult.ignored;
   }
 
-  // Delete the previous `underscore`,
-  // update the style of the text surrounded by two underscores to `italic`,
+  // Delete the previous 'underscore',
+  // update the style of the text surrounded by the two underscores to 'italic',
   // and update the cursor position.
   TransactionBuilder(editorState)
     ..deleteText(textNode, previousUnderscore, 1)
@@ -99,7 +101,7 @@ FlowyKeyEventHandler underscoreToItalicHandler = (editorState, event) {
 };
 ```
 
-So far, the 'underscore handler' function is done and the only task left is to inject it into the AppFlowyEditor.
+Now our 'underscore handler' function is done and the only task left is to inject it into the AppFlowyEditor.
 
 ```dart
 @override
@@ -120,14 +122,15 @@ Widget build(BuildContext context) {
 
 ![After](./images/customizing_a_shortcut_event_after.gif)
 
-[Complete code example]()
+Check out the [complete code](https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/underscore_to_italic_key_event_handler.dart) file of this example.
+
 
-## Customize a component
-We will use a simple example to showcase how to quickly add a custom component.
+## Customizing a Component
+We will use a simple example to show how to quickly add a custom component.
 
-For example, we want to render an image from the network.
+In this example we will render an image from the network.
 
-To start with, let's create an empty document by running commands as follows:
+Let's start with a blank document:
 
 ```dart
 @override
@@ -144,9 +147,9 @@ Widget build(BuildContext context) {
 }
 ```
 
-Next, we choose a unique string for your custom node's type. We use `network_image` in this case. And we add `network_image_src` to the `attributes` to describe the link of the image.
+Next, we will choose a unique string for your custom node's type. 
 
-> For the definition of the [Node](), please refer to this [link]().
+We'll use `network_image` in this case. And we add `network_image_src` to the `attributes` to describe the link of the image.
 
 ```JSON
 {
@@ -157,9 +160,9 @@ Next, we choose a unique string for your custom node's type. We use `network_ima
 }
 ```
 
-Then, we create a class that inherits [NodeWidgetBuilder](). As shown in the autoprompt, we need to implement two functions:
+Then, we create a class that inherits [NodeWidgetBuilder](../lib/src/service/render_plugin_service.dart). As shown in the autoprompt, we need to implement two functions:
 1. one returns a widget 
-2. the other verifies the correctness of the [Node]().
+2. the other verifies the correctness of the [Node](../lib/src/document/node.dart).
 
 
 ```dart
@@ -179,9 +182,7 @@ class NetworkImageNodeWidgetBuilder extends NodeWidgetBuilder {
 
 Now, let's implement a simple image widget based on `Image`.
 
-**It is important to note that the `State` of the returned `Widget` must be with [Selectable]().**
-
-> For the definition of the [Selectable](), please refer to this [link]().
+Note that the `State` object that is returned by the `Widget` must implement [Selectable](../lib/src/render/selection/selectable.dart) using the `with` keyword.
 
 ```dart
 class _NetworkImageNodeWidget extends StatefulWidget {
@@ -236,7 +237,7 @@ class __NetworkImageNodeWidgetState extends State<_NetworkImageNodeWidget>
 }
 ```
 
-Finally, we return `_NetworkImageNodeWidget` in the `build` function of `NetworkImageNodeWidgetBuilder` and register `NetworkImageNodeWidgetBuilder` into `AppFlowyEditor`.
+Finally, we return `_NetworkImageNodeWidget` in the `build` function of `NetworkImageNodeWidgetBuilder`...
 
 ```dart
 class NetworkImageNodeWidgetBuilder extends NodeWidgetBuilder {
@@ -256,6 +257,8 @@ class NetworkImageNodeWidgetBuilder extends NodeWidgetBuilder {
 }
 ```
 
+... and register `NetworkImageNodeWidgetBuilder` in the `AppFlowyEditor`.
+ 
 ```dart
 final editorState = EditorState(
   document: StateTree.empty()
@@ -281,6 +284,6 @@ return AppFlowyEditor(
 );
 ```
 
-![](./images/customizing_a_component.gif)
+![Whew!](./images/customizing_a_component.gif)
 
-[Here you can check out the complete code file of this example]()
+Check out the [complete code](https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/network_image_node_widget.dart) file of this example.

BIN
frontend/app_flowy/packages/appflowy_editor/documentation/images/appflowy-editor-example.gif


+ 43 - 26
frontend/app_flowy/packages/appflowy_editor/documentation/testing.md

@@ -1,24 +1,33 @@
 # Testing
 
-> The directory structure of test files is consistent with the code files, making it easy for us to map a file with the corresponding test and check if the test is updated
+The directory structure of test files mirrors that of the code files, making it easy for us to map a file with the corresponding test and check if the test is updated.
 
-## Testing Functions
+For an overview of testing best practices in Flutter applications, please refer to Flutter's [introduction to widget testing](https://docs.flutter.dev/cookbook/testing/widget/introduction) as well as their [introduction to unit testing](https://docs.flutter.dev/cookbook/testing/unit/introduction).
+There you will learn how to do such things as such as simulate a click as well as leverage the `test` and `expect` functions.
+
+## Testing Basic Editor Functions
+
+The example code below shows how to construct a document that will be used in our testing.
 
-**Construct a document for testing**
 ```dart
 const text = 'Welcome to Appflowy 😁';
-// Get the instance of editor.
+// Get the instance of the editor.
 final editor = tester.editor;
-// Insert empty text node.
+
+// Insert an empty text node.
 editor.insertEmptyTextNode();
-// Insert text node with string.
+
+// Insert a text node with the text string we defined earlier.
 editor.insertTextNode(text);
-// Insert text node with heading style.
+
+// Insert the same text, but with the heading style.
 editor.insertTextNode(text, attributes: {
     StyleKey.subtype: StyleKey.heading,
     StyleKey.heading: StyleKey.h1,
 });
-// Insert text node with bulleted list style and bold style.
+
+// Insert our text with the bulleted list style and the bold style.
+// If you want to modify the style of the inserted text, you need to use the Delta parameter.
 editor.insertTextNode(
     '',
     attributes: {
@@ -30,66 +39,76 @@ editor.insertTextNode(
 );
 ```
 
-**The `startTesting` function must be called before testing**.
+The `startTesting` function of the editor must be called before you begin your test.
+
 ```dart
 await editor.startTesting();
 ```
 
-**Get the number of nodes in the document**
+Get the number of nodes in the document.
+
 ```dart
 final length = editor.documentLength;
 print(length);
 ```
 
-**Get the node of a defined path**
+Get the node of a defined path. In this case we are getting the first node of the document which is the text "Welcome to Appflowy 😁".
+
 ```dart
 final firstTextNode = editor.nodeAtPath([0]) as TextNode;
 ```
 
-**Update selection**
+Update the [Selection](https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/app_flowy/packages/appflowy_editor/lib/src/document/selection.dart) so that our text "Welcome to Appflowy 😁" is selected. We will start our selection from the beginning of the string.
+
 ```dart
 await editor.updateSelection(
     Selection.single(path: firstTextNode.path, startOffset: 0),
 );
 ```
 
-**Get the selection**
+Get the current selection.
+
 ```dart
 final selection = editor.documentSelection;
 print(selection);
 ```
 
-**Simulate shortcut event inputs**
+Next we will simulate the input of a shortcut key being pressed that will select all the text.
+
 ```dart
-// Command + A.
+// Meta + A.
 await editor.pressLogicKey(LogicalKeyboardKey.keyA, isMetaPressed: true);
-// Command + shift + S.
+// Meta + shift + S.
 await editor.pressLogicKey(
-    LogicalKeyboardKey.keyS, 
-    isMetaPressed: true, 
+    LogicalKeyboardKey.keyS,
+    isMetaPressed: true,
     isShiftPressed: true,
 );
 ```
 
-**Simulate a text input**
+We will then simulate text input.
+
 ```dart
 // Insert 'Hello World' at the beginning of the first node.
 editor.insertText(firstTextNode, 'Hello World', 0);
 ```
 
-**Get information about the text node**
+Once the text has been added, we can get information about the text node.
+
 ```dart
-// Get plain text.
+// Get the text of the first text node as plain text
 final textAfterInserted = firstTextNode.toRawString();
 print(textAfterInserted);
-// Get attributes.
+// Get the attributes of the text node
 final attributes = firstTextNode.attributes;
 print(attributes);
 ```
 
-## Example
-For example, we are going to test `select_all_handler.dart`
+## A Complete Code Example
 
+In the example code below we are going to test `select_all_handler.dart` by inserting 100 lines of text that read "Welcome to Appflowy 😁" and then simulating the "selectAll" shortcut key being pressed.
+
+Afterwards, we will `expect` that the current selection of the editor is equal to the selection of all the lines that were generated.
 
 ```dart
 import 'package:appflowy_editor/appflowy_editor.dart';
@@ -124,5 +143,3 @@ void main() async {
   });
 }
 ```
-
-For more information about testing, such as simulating a click, please refer to [An introduction to widget testing](https://docs.flutter.dev/cookbook/testing/widget/introduction) 

+ 3 - 2
frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/image_node_widget.dart

@@ -11,7 +11,7 @@ import 'package:flutter/material.dart';
 /// 2. create a class extends [NodeWidgetBuilder]
 /// 3. override the function `Widget build(NodeWidgetContext<Node> context)`
 ///     and return a widget to render. The returned widget should be
-///     a StatefulWidget and mixin with [Selectable].
+///     a StatefulWidget and mixin with [SelectableMixin].
 ///
 /// 4. override the getter `nodeValidator`
 ///     to verify the data structure in [Node].
@@ -50,7 +50,8 @@ class ImageNodeWidget extends StatefulWidget {
   State<ImageNodeWidget> createState() => _ImageNodeWidgetState();
 }
 
-class _ImageNodeWidgetState extends State<ImageNodeWidget> with Selectable {
+class _ImageNodeWidgetState extends State<ImageNodeWidget>
+    with SelectableMixin {
   bool isHovered = false;
   Node get node => widget.node;
   EditorState get editorState => widget.editorState;

+ 1 - 1
frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/network_image_node_widget.dart

@@ -31,7 +31,7 @@ class _NetworkImageNodeWidget extends StatefulWidget {
 }
 
 class __NetworkImageNodeWidgetState extends State<_NetworkImageNodeWidget>
-    with Selectable {
+    with SelectableMixin {
   RenderBox get _renderBox => context.findRenderObject() as RenderBox;
 
   @override

+ 1 - 1
frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/youtube_link_node_widget.dart

@@ -33,7 +33,7 @@ class LinkNodeWidget extends StatefulWidget {
 }
 
 class _YouTubeLinkNodeWidgetState extends State<LinkNodeWidget>
-    with Selectable {
+    with SelectableMixin {
   Node get node => widget.node;
   EditorState get editorState => widget.editorState;
   String get src => widget.node.attributes['youtube_link'] as String;

+ 1 - 0
frontend/app_flowy/packages/appflowy_editor/lib/appflowy_editor.dart

@@ -20,5 +20,6 @@ export 'src/service/render_plugin_service.dart';
 export 'src/service/service.dart';
 export 'src/service/selection_service.dart';
 export 'src/service/scroll_service.dart';
+export 'src/service/toolbar_service.dart';
 export 'src/service/keyboard_service.dart';
 export 'src/service/input_service.dart';

+ 0 - 1
frontend/app_flowy/packages/appflowy_editor/lib/src/document/node_iterator.dart

@@ -1,7 +1,6 @@
 import 'package:appflowy_editor/src/document/node.dart';
 
 import './state_tree.dart';
-import './node.dart';
 
 /// [NodeIterator] is used to traverse the nodes in visual order.
 class NodeIterator implements Iterator<Node> {

+ 1 - 3
frontend/app_flowy/packages/appflowy_editor/lib/src/document/position.dart

@@ -1,5 +1,3 @@
-import 'package:flutter/material.dart';
-
 import './path.dart';
 
 class Position {
@@ -21,7 +19,7 @@ class Position {
 
   @override
   int get hashCode {
-    final pathHash = hashList(path);
+    final pathHash = Object.hashAll(path);
     return Object.hash(pathHash, offset);
   }
 

+ 1 - 3
frontend/app_flowy/packages/appflowy_editor/lib/src/document/text_delta.dart

@@ -3,8 +3,6 @@ import 'dart:math';
 
 import 'package:appflowy_editor/src/document/attributes.dart';
 import 'package:flutter/foundation.dart';
-import 'package:flutter/material.dart';
-import './attributes.dart';
 
 // constant number: 2^53 - 1
 const int _maxInt = 9007199254740991;
@@ -463,7 +461,7 @@ class Delta extends Iterable<TextOperation> {
 
   @override
   int get hashCode {
-    return hashList(_operations);
+    return Object.hashAll(_operations);
   }
 
   /// Returned an inverted delta that has the opposite effect of against a base document delta.

+ 2 - 1
frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/node_extensions.dart

@@ -10,7 +10,8 @@ extension NodeExtensions on Node {
       key?.currentContext?.findRenderObject()?.unwrapOrNull<RenderBox>();
 
   BuildContext? get context => key?.currentContext;
-  Selectable? get selectable => key?.currentState?.unwrapOrNull<Selectable>();
+  SelectableMixin? get selectable =>
+      key?.currentState?.unwrapOrNull<SelectableMixin>();
 
   bool inSelection(Selection selection) {
     if (selection.start.path <= selection.end.path) {

+ 2 - 1
frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_widget.dart

@@ -32,7 +32,8 @@ class ImageNodeWidget extends StatefulWidget {
   State<ImageNodeWidget> createState() => _ImageNodeWidgetState();
 }
 
-class _ImageNodeWidgetState extends State<ImageNodeWidget> with Selectable {
+class _ImageNodeWidgetState extends State<ImageNodeWidget>
+    with SelectableMixin {
   final _imageKey = GlobalKey();
 
   double? _imageWidth;

+ 3 - 3
frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/bulleted_list_text.dart

@@ -42,7 +42,7 @@ class BulletedListTextNodeWidget extends StatefulWidget {
 // customize
 
 class _BulletedListTextNodeWidgetState extends State<BulletedListTextNodeWidget>
-    with Selectable, DefaultSelectable {
+    with SelectableMixin, DefaultSelectable {
   @override
   final iconKey = GlobalKey();
 
@@ -51,8 +51,8 @@ class _BulletedListTextNodeWidgetState extends State<BulletedListTextNodeWidget>
   final _iconRightPadding = 5.0;
 
   @override
-  Selectable<StatefulWidget> get forward =>
-      _richTextKey.currentState as Selectable;
+  SelectableMixin<StatefulWidget> get forward =>
+      _richTextKey.currentState as SelectableMixin;
 
   @override
   Widget build(BuildContext context) {

+ 3 - 3
frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/checkbox_text.dart

@@ -40,7 +40,7 @@ class CheckboxNodeWidget extends StatefulWidget {
 }
 
 class _CheckboxNodeWidgetState extends State<CheckboxNodeWidget>
-    with Selectable, DefaultSelectable {
+    with SelectableMixin, DefaultSelectable {
   @override
   final iconKey = GlobalKey();
 
@@ -49,8 +49,8 @@ class _CheckboxNodeWidgetState extends State<CheckboxNodeWidget>
   final _iconRightPadding = 5.0;
 
   @override
-  Selectable<StatefulWidget> get forward =>
-      _richTextKey.currentState as Selectable;
+  SelectableMixin<StatefulWidget> get forward =>
+      _richTextKey.currentState as SelectableMixin;
 
   @override
   Widget build(BuildContext context) {

+ 1 - 1
frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/default_selectable.dart

@@ -4,7 +4,7 @@ import 'package:appflowy_editor/src/render/selection/selectable.dart';
 import 'package:flutter/material.dart';
 
 mixin DefaultSelectable {
-  Selectable get forward;
+  SelectableMixin get forward;
 
   GlobalKey? get iconKey;
 

+ 1 - 1
frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/flowy_rich_text.dart

@@ -42,7 +42,7 @@ class FlowyRichText extends StatefulWidget {
   State<FlowyRichText> createState() => _FlowyRichTextState();
 }
 
-class _FlowyRichTextState extends State<FlowyRichText> with Selectable {
+class _FlowyRichTextState extends State<FlowyRichText> with SelectableMixin {
   var _textKey = GlobalKey();
   final _placeholderTextKey = GlobalKey();
 

+ 3 - 3
frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/heading_text.dart

@@ -39,7 +39,7 @@ class HeadingTextNodeWidget extends StatefulWidget {
 
 // customize
 class _HeadingTextNodeWidgetState extends State<HeadingTextNodeWidget>
-    with Selectable, DefaultSelectable {
+    with SelectableMixin, DefaultSelectable {
   @override
   GlobalKey? get iconKey => null;
 
@@ -47,8 +47,8 @@ class _HeadingTextNodeWidgetState extends State<HeadingTextNodeWidget>
   final _topPadding = 5.0;
 
   @override
-  Selectable<StatefulWidget> get forward =>
-      _richTextKey.currentState as Selectable;
+  SelectableMixin<StatefulWidget> get forward =>
+      _richTextKey.currentState as SelectableMixin;
 
   @override
   Offset get baseOffset {

+ 3 - 3
frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/number_list_text.dart

@@ -42,7 +42,7 @@ class NumberListTextNodeWidget extends StatefulWidget {
 // customize
 
 class _NumberListTextNodeWidgetState extends State<NumberListTextNodeWidget>
-    with Selectable, DefaultSelectable {
+    with SelectableMixin, DefaultSelectable {
   @override
   final iconKey = GlobalKey();
 
@@ -51,8 +51,8 @@ class _NumberListTextNodeWidgetState extends State<NumberListTextNodeWidget>
   final _iconRightPadding = 5.0;
 
   @override
-  Selectable<StatefulWidget> get forward =>
-      _richTextKey.currentState as Selectable;
+  SelectableMixin<StatefulWidget> get forward =>
+      _richTextKey.currentState as SelectableMixin;
 
   @override
   Widget build(BuildContext context) {

+ 3 - 3
frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/quoted_text.dart

@@ -41,7 +41,7 @@ class QuotedTextNodeWidget extends StatefulWidget {
 // customize
 
 class _QuotedTextNodeWidgetState extends State<QuotedTextNodeWidget>
-    with Selectable, DefaultSelectable {
+    with SelectableMixin, DefaultSelectable {
   @override
   final iconKey = GlobalKey();
 
@@ -50,8 +50,8 @@ class _QuotedTextNodeWidgetState extends State<QuotedTextNodeWidget>
   final _iconRightPadding = 5.0;
 
   @override
-  Selectable<StatefulWidget> get forward =>
-      _richTextKey.currentState as Selectable;
+  SelectableMixin<StatefulWidget> get forward =>
+      _richTextKey.currentState as SelectableMixin;
 
   @override
   Widget build(BuildContext context) {

+ 3 - 3
frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/rich_text.dart

@@ -40,15 +40,15 @@ class RichTextNodeWidget extends StatefulWidget {
 // customize
 
 class _RichTextNodeWidgetState extends State<RichTextNodeWidget>
-    with Selectable, DefaultSelectable {
+    with SelectableMixin, DefaultSelectable {
   @override
   GlobalKey? get iconKey => null;
 
   final _richTextKey = GlobalKey(debugLabel: 'rich_text');
 
   @override
-  Selectable<StatefulWidget> get forward =>
-      _richTextKey.currentState as Selectable;
+  SelectableMixin<StatefulWidget> get forward =>
+      _richTextKey.currentState as SelectableMixin;
 
   @override
   Widget build(BuildContext context) {

+ 3 - 3
frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection/selectable.dart

@@ -2,12 +2,12 @@ import 'package:appflowy_editor/src/document/position.dart';
 import 'package:appflowy_editor/src/document/selection.dart';
 import 'package:flutter/material.dart';
 
-/// [Selectable] is used for the editor to calculate the position
+/// [SelectableMixin] is used for the editor to calculate the position
 ///   and size of the selection.
 ///
-/// The widget returned by NodeWidgetBuilder must be with [Selectable],
+/// The widget returned by NodeWidgetBuilder must be with [SelectableMixin],
 ///   otherwise the [AppFlowySelectionService] will not work properly.
-mixin Selectable<T extends StatefulWidget> on State<T> {
+mixin SelectableMixin<T extends StatefulWidget> on State<T> {
   /// Returns the [Selection] surrounded by start and end
   ///   in current widget.
   ///

+ 1 - 1
frontend/app_flowy/packages/appflowy_editor/lib/src/service/selection_service.dart

@@ -510,7 +510,7 @@ class _AppFlowySelectionState extends State<AppFlowySelection>
     editorState.service.scrollService?.enable();
   }
 
-  Rect _transformRectToGlobal(Selectable selectable, Rect r) {
+  Rect _transformRectToGlobal(SelectableMixin selectable, Rect r) {
     final Offset topLeft = selectable.localToGlobal(Offset(r.left, r.top));
     return Rect.fromLTWH(topLeft.dx, topLeft.dy, r.width, r.height);
   }

+ 6 - 1
frontend/app_flowy/packages/appflowy_editor/pubspec.yaml

@@ -1,8 +1,13 @@
 name: appflowy_editor
 description: A highly customizable rich-text editor for Flutter
-version: 0.0.3
+version: 0.0.4
 homepage: https://github.com/AppFlowy-IO/AppFlowy
 
+platforms:
+  linux:
+  macos:
+  windows:
+
 environment:
   sdk: ">=2.17.0 <3.0.0"
   flutter: ">=1.17.0"

+ 2 - 3
frontend/app_flowy/packages/appflowy_editor/test/legacy/flowy_editor_test.dart

@@ -1,7 +1,6 @@
 import 'package:appflowy_editor/src/document/path.dart';
 import 'package:appflowy_editor/src/document/position.dart';
 import 'package:appflowy_editor/src/document/selection.dart';
-import 'package:flutter/material.dart';
 import 'package:flutter_test/flutter_test.dart';
 
 void main() {
@@ -75,7 +74,7 @@ void main() {
     final path2 = <int>[1];
     expect(pathEquals(path1, path2), true);
 
-    expect(hashList(path1), hashList(path2));
+    expect(Object.hashAll(path1), Object.hashAll(path2));
   });
 
   test('test path utils 2', () {
@@ -83,7 +82,7 @@ void main() {
     final path2 = <int>[2];
     expect(pathEquals(path1, path2), false);
 
-    expect(hashList(path1) != hashList(path2), true);
+    expect(Object.hashAll(path1) != Object.hashAll(path2), true);
   });
 
   test('test position comparator', () {

+ 1 - 1
frontend/app_flowy/pubspec.lock

@@ -35,7 +35,7 @@ packages:
       path: "packages/appflowy_editor"
       relative: true
     source: path
-    version: "0.0.3"
+    version: "0.0.4"
   appflowy_popover:
     dependency: "direct main"
     description:

+ 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(),
     }
 }
 

Some files were not shown because too many files changed in this diff