Browse Source

Merge pull request #867 from AppFlowy-IO/feat/move_card

feat: move card from one column to another
Nathan.fooo 2 years ago
parent
commit
a0753cea2d
70 changed files with 1141 additions and 504 deletions
  1. 1 1
      frontend/.vscode/launch.json
  2. 1 1
      frontend/app_flowy/lib/plugins/blank/blank.dart
  3. 37 6
      frontend/app_flowy/lib/plugins/board/application/board_bloc.dart
  4. 16 2
      frontend/app_flowy/lib/plugins/board/application/group_controller.dart
  5. 2 2
      frontend/app_flowy/lib/plugins/board/board.dart
  6. 2 2
      frontend/app_flowy/lib/plugins/doc/document.dart
  7. 1 1
      frontend/app_flowy/lib/plugins/grid/application/cell/select_option_editor_bloc.dart
  8. 1 1
      frontend/app_flowy/lib/plugins/grid/application/cell/select_option_service.dart
  9. 20 22
      frontend/app_flowy/lib/plugins/grid/application/row/row_service.dart
  10. 2 2
      frontend/app_flowy/lib/plugins/grid/grid.dart
  11. 3 6
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/number_cell.dart
  12. 2 2
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/text_cell.dart
  13. 1 1
      frontend/app_flowy/lib/plugins/trash/menu.dart
  14. 1 1
      frontend/app_flowy/lib/plugins/trash/trash.dart
  15. 21 21
      frontend/app_flowy/lib/startup/plugin/plugin.dart
  16. 13 7
      frontend/app_flowy/lib/startup/plugin/src/sandbox.dart
  17. 2 2
      frontend/app_flowy/lib/workspace/application/app/app_bloc.dart
  18. 2 6
      frontend/app_flowy/lib/workspace/application/app/app_service.dart
  19. 1 1
      frontend/app_flowy/lib/workspace/application/menu/menu_bloc.dart
  20. 13 0
      frontend/app_flowy/lib/workspace/application/view/view_ext.dart
  21. 1 0
      frontend/app_flowy/lib/workspace/presentation/home/home_screen.dart
  22. 1 1
      frontend/app_flowy/lib/workspace/presentation/home/home_stack.dart
  23. 1 1
      frontend/app_flowy/lib/workspace/presentation/home/menu/app/header/header.dart
  24. 1 1
      frontend/app_flowy/packages/appflowy_board/lib/src/utils/log.dart
  25. 22 9
      frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column_data.dart
  26. 1 1
      frontend/app_flowy/pubspec.lock
  27. 48 26
      frontend/rust-lib/flowy-folder/src/entities/view.rs
  28. 3 3
      frontend/rust-lib/flowy-folder/src/manager.rs
  29. 7 7
      frontend/rust-lib/flowy-folder/src/services/persistence/version_1/view_sql.rs
  30. 14 15
      frontend/rust-lib/flowy-folder/src/services/view/controller.rs
  31. 4 5
      frontend/rust-lib/flowy-folder/tests/workspace/folder_test.rs
  32. 11 11
      frontend/rust-lib/flowy-folder/tests/workspace/script.rs
  33. 10 0
      frontend/rust-lib/flowy-grid/src/entities/block_entities.rs
  34. 3 3
      frontend/rust-lib/flowy-grid/src/entities/cell_entities.rs
  35. 10 27
      frontend/rust-lib/flowy-grid/src/entities/grid_entities.rs
  36. 14 1
      frontend/rust-lib/flowy-grid/src/entities/group_entities/group.rs
  37. 18 0
      frontend/rust-lib/flowy-grid/src/entities/group_entities/group_changeset.rs
  38. 1 1
      frontend/rust-lib/flowy-grid/src/event_handler.rs
  39. 3 3
      frontend/rust-lib/flowy-grid/src/services/block_editor.rs
  40. 16 20
      frontend/rust-lib/flowy-grid/src/services/block_manager.rs
  41. 2 9
      frontend/rust-lib/flowy-grid/src/services/block_manager_trait_impl.rs
  42. 0 2
      frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/single_select_type_option.rs
  43. 46 17
      frontend/rust-lib/flowy-grid/src/services/grid_editor.rs
  44. 62 54
      frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs
  45. 28 54
      frontend/rust-lib/flowy-grid/src/services/grid_view_manager.rs
  46. 39 5
      frontend/rust-lib/flowy-grid/src/services/group/group_generator/checkbox_group.rs
  47. 87 51
      frontend/rust-lib/flowy-grid/src/services/group/group_generator/group_controller.rs
  48. 2 2
      frontend/rust-lib/flowy-grid/src/services/group/group_generator/mod.rs
  49. 159 21
      frontend/rust-lib/flowy-grid/src/services/group/group_generator/select_option_group.rs
  50. 68 10
      frontend/rust-lib/flowy-grid/src/services/group/group_service.rs
  51. 2 2
      frontend/rust-lib/flowy-grid/tests/grid/block_test/row_test.rs
  52. 3 3
      frontend/rust-lib/flowy-grid/tests/grid/block_test/script.rs
  53. 1 1
      frontend/rust-lib/flowy-grid/tests/grid/cell_test/script.rs
  54. 1 1
      frontend/rust-lib/flowy-grid/tests/grid/field_test/script.rs
  55. 1 1
      frontend/rust-lib/flowy-grid/tests/grid/filter_test/script.rs
  56. 15 2
      frontend/rust-lib/flowy-grid/tests/grid/grid_editor.rs
  57. 2 0
      frontend/rust-lib/flowy-grid/tests/grid/group_test/mod.rs
  58. 97 0
      frontend/rust-lib/flowy-grid/tests/grid/group_test/script.rs
  59. 101 0
      frontend/rust-lib/flowy-grid/tests/grid/group_test/test.rs
  60. 1 0
      frontend/rust-lib/flowy-grid/tests/grid/mod.rs
  61. 2 2
      frontend/rust-lib/flowy-net/src/local_server/server.rs
  62. 1 3
      frontend/rust-lib/flowy-revision/src/rev_persistence.rs
  63. 13 8
      frontend/rust-lib/flowy-sdk/src/deps_resolve/folder_deps.rs
  64. 1 1
      frontend/rust-lib/flowy-sdk/src/lib.rs
  65. 17 8
      frontend/rust-lib/flowy-test/src/helper.rs
  66. 23 5
      shared-lib/flowy-folder-data-model/src/revision/view_rev.rs
  67. 5 4
      shared-lib/flowy-folder-data-model/src/user_default.rs
  68. 16 1
      shared-lib/flowy-grid-data-model/src/revision/grid_block.rs
  69. 5 5
      shared-lib/flowy-sync/src/client_folder/folder_pad.rs
  70. 10 10
      shared-lib/flowy-sync/src/client_grid/block_revision_pad.rs

+ 1 - 1
frontend/.vscode/launch.json

@@ -29,7 +29,7 @@
             "program": "./lib/main.dart",
             "type": "dart",
             "env": {
-                "RUST_LOG": "debug"
+                "RUST_LOG": "trace"
             },
             "cwd": "${workspaceRoot}/app_flowy"
         },

+ 1 - 1
frontend/app_flowy/lib/plugins/blank/blank.dart

@@ -16,7 +16,7 @@ class BlankPluginBuilder extends PluginBuilder {
   String get menuName => "Blank";
 
   @override
-  PluginType get pluginType => DefaultPlugin.blank.type();
+  PluginType get pluginType => PluginType.blank;
 }
 
 class BlankPluginConfig implements PluginConfig {

+ 37 - 6
frontend/app_flowy/lib/plugins/board/application/board_bloc.dart

@@ -2,6 +2,7 @@ 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/row/row_cache.dart';
+import 'package:app_flowy/plugins/grid/application/row/row_service.dart';
 import 'package:appflowy_board/appflowy_board.dart';
 import 'package:dartz/dartz.dart';
 import 'package:equatable/equatable.dart';
@@ -21,13 +22,15 @@ part 'board_bloc.freezed.dart';
 class BoardBloc extends Bloc<BoardEvent, BoardState> {
   final BoardDataController _dataController;
   late final AFBoardDataController afBoardDataController;
-  List<GroupController> groupControllers = [];
+  final MoveRowFFIService _rowService;
+  Map<String, GroupController> groupControllers = {};
 
   GridFieldCache get fieldCache => _dataController.fieldCache;
   String get gridId => _dataController.gridId;
 
   BoardBloc({required ViewPB view})
-      : _dataController = BoardDataController(view: view),
+      : _rowService = MoveRowFFIService(gridId: view.id),
+        _dataController = BoardDataController(view: view),
         super(BoardState.initial(view.id)) {
     afBoardDataController = AFBoardDataController(
       onMoveColumn: (
@@ -38,13 +41,21 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
         columnId,
         fromIndex,
         toIndex,
-      ) {},
+      ) {
+        final fromRow = groupControllers[columnId]?.rowAtIndex(fromIndex);
+        final toRow = groupControllers[columnId]?.rowAtIndex(toIndex);
+        _moveRow(fromRow, toRow);
+      },
       onMoveColumnItemToColumn: (
         fromColumnId,
         fromIndex,
         toColumnId,
         toIndex,
-      ) {},
+      ) {
+        final fromRow = groupControllers[fromColumnId]?.rowAtIndex(fromIndex);
+        final toRow = groupControllers[toColumnId]?.rowAtIndex(toIndex);
+        _moveRow(fromRow, toRow);
+      },
     );
 
     on<BoardEvent>(
@@ -76,15 +87,31 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
           didReceiveRows: (List<RowInfo> rowInfos) {
             emit(state.copyWith(rowInfos: rowInfos));
           },
+          didReceiveError: (FlowyError error) {
+            emit(state.copyWith(noneOrError: some(error)));
+          },
         );
       },
     );
   }
 
+  void _moveRow(RowPB? fromRow, RowPB? toRow) {
+    if (fromRow != null && toRow != null) {
+      _rowService
+          .moveRow(
+        fromRowId: fromRow.id,
+        toRowId: toRow.id,
+      )
+          .then((result) {
+        result.fold((l) => null, (r) => add(BoardEvent.didReceiveError(r)));
+      });
+    }
+  }
+
   @override
   Future<void> close() async {
     await _dataController.dispose();
-    for (final controller in groupControllers) {
+    for (final controller in groupControllers.values) {
       controller.dispose();
     }
     return super.close();
@@ -94,11 +121,12 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
     for (final group in groups) {
       final delegate = GroupControllerDelegateImpl(afBoardDataController);
       final controller = GroupController(
+        gridId: state.gridId,
         group: group,
         delegate: delegate,
       );
       controller.startListening();
-      groupControllers.add(controller);
+      groupControllers[controller.group.groupId] = (controller);
     }
   }
 
@@ -162,6 +190,7 @@ class BoardEvent with _$BoardEvent {
   const factory BoardEvent.initial() = InitialGrid;
   const factory BoardEvent.createRow(String groupId) = _CreateRow;
   const factory BoardEvent.endEditRow(String rowId) = _EndEditRow;
+  const factory BoardEvent.didReceiveError(FlowyError error) = _DidReceiveError;
   const factory BoardEvent.didReceiveRows(List<RowInfo> rowInfos) =
       _DidReceiveRows;
   const factory BoardEvent.didReceiveGridUpdate(
@@ -177,6 +206,7 @@ class BoardState with _$BoardState {
     required Option<RowPB> editingRow,
     required List<RowInfo> rowInfos,
     required GridLoadingState loadingState,
+    required Option<FlowyError> noneOrError,
   }) = _BoardState;
 
   factory BoardState.initial(String gridId) => BoardState(
@@ -184,6 +214,7 @@ class BoardState with _$BoardState {
         grid: none(),
         gridId: gridId,
         editingRow: none(),
+        noneOrError: none(),
         loadingState: const _Loading(),
       );
 }

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

@@ -1,8 +1,11 @@
 import 'package:flowy_sdk/log.dart';
+import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid/protobuf.dart';
 
 import 'group_listener.dart';
 
+typedef OnGroupError = void Function(FlowyError);
+
 abstract class GroupControllerDelegate {
   void removeRow(String groupId, String rowId);
   void insertRow(String groupId, RowPB row, int? index);
@@ -14,8 +17,19 @@ class GroupController {
   final GroupListener _listener;
   final GroupControllerDelegate delegate;
 
-  GroupController({required this.group, required this.delegate})
-      : _listener = GroupListener(group);
+  GroupController({
+    required String gridId,
+    required this.group,
+    required this.delegate,
+  }) : _listener = GroupListener(group);
+
+  RowPB? rowAtIndex(int index) {
+    if (index < group.rows.length) {
+      return group.rows[index];
+    } else {
+      return null;
+    }
+  }
 
   void startListening() {
     _listener.start(onGroupChanged: (result) {

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

@@ -20,13 +20,13 @@ class BoardPluginBuilder implements PluginBuilder {
   String get menuName => "Board";
 
   @override
-  PluginType get pluginType => DefaultPlugin.board.type();
+  PluginType get pluginType => PluginType.board;
 
   @override
   ViewDataTypePB get dataType => ViewDataTypePB.Database;
 
   @override
-  SubViewDataTypePB get subDataType => SubViewDataTypePB.Board;
+  ViewLayoutTypePB? get subDataType => ViewLayoutTypePB.Board;
 }
 
 class BoardPluginConfig implements PluginConfig {

+ 2 - 2
frontend/app_flowy/lib/plugins/doc/document.dart

@@ -42,10 +42,10 @@ class DocumentPluginBuilder extends PluginBuilder {
   String get menuName => LocaleKeys.document_menuName.tr();
 
   @override
-  PluginType get pluginType => DefaultPlugin.editor.type();
+  PluginType get pluginType => PluginType.editor;
 
   @override
-  ViewDataTypePB get dataType => ViewDataTypePB.TextBlock;
+  ViewDataTypePB get dataType => ViewDataTypePB.Text;
 }
 
 class DocumentPlugin implements Plugin {

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

@@ -111,7 +111,7 @@ class SelectOptionCellEditorBloc
   void _loadOptions() {
     _delayOperation?.cancel();
     _delayOperation = Timer(const Duration(milliseconds: 10), () {
-      _selectOptionService.getOpitonContext().then((result) {
+      _selectOptionService.getOptionContext().then((result) {
         if (isClosed) {
           return;
         }

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

@@ -55,7 +55,7 @@ class SelectOptionService {
     return GridEventUpdateSelectOption(payload).send();
   }
 
-  Future<Either<SelectOptionCellDataPB, FlowyError>> getOpitonContext() {
+  Future<Either<SelectOptionCellDataPB, FlowyError>> getOptionContext() {
     final payload = GridCellIdPB.create()
       ..gridId = gridId
       ..fieldId = fieldId

+ 20 - 22
frontend/app_flowy/lib/plugins/grid/application/row/row_service.dart

@@ -4,7 +4,6 @@ import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid/block_entities.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid/grid_entities.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid/row_entities.pb.dart';
-import 'package:flowy_sdk/protobuf/flowy-grid/setting_entities.pb.dart';
 
 class RowFFIService {
   final String gridId;
@@ -23,27 +22,6 @@ class RowFFIService {
     return GridEventCreateTableRow(payload).send();
   }
 
-  Future<Either<Unit, FlowyError>> moveRow({
-    required String rowId,
-    required int fromIndex,
-    required int toIndex,
-    required GridLayout layout,
-    String? upperRowId,
-  }) {
-    var payload = MoveRowPayloadPB.create()
-      ..viewId = gridId
-      ..rowId = rowId
-      ..layout = layout
-      ..fromIndex = fromIndex
-      ..toIndex = toIndex;
-
-    if (upperRowId != null) {
-      payload.upperRowId = upperRowId;
-    }
-
-    return GridEventMoveRow(payload).send();
-  }
-
   Future<Either<OptionalRowPB, FlowyError>> getRow(String rowId) {
     final payload = RowIdPB.create()
       ..gridId = gridId
@@ -71,3 +49,23 @@ class RowFFIService {
     return GridEventDuplicateRow(payload).send();
   }
 }
+
+class MoveRowFFIService {
+  final String gridId;
+
+  MoveRowFFIService({
+    required this.gridId,
+  });
+
+  Future<Either<Unit, FlowyError>> moveRow({
+    required String fromRowId,
+    required String toRowId,
+  }) {
+    var payload = MoveRowPayloadPB.create()
+      ..viewId = gridId
+      ..fromRowId = fromRowId
+      ..toRowId = toRowId;
+
+    return GridEventMoveRow(payload).send();
+  }
+}

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

@@ -22,13 +22,13 @@ class GridPluginBuilder implements PluginBuilder {
   String get menuName => LocaleKeys.grid_menuName.tr();
 
   @override
-  PluginType get pluginType => DefaultPlugin.grid.type();
+  PluginType get pluginType => PluginType.grid;
 
   @override
   ViewDataTypePB get dataType => ViewDataTypePB.Database;
 
   @override
-  SubViewDataTypePB? get subDataType => SubViewDataTypePB.Grid;
+  ViewLayoutTypePB? get subDataType => ViewLayoutTypePB.Grid;
 }
 
 class GridPluginConfig implements PluginConfig {

+ 3 - 6
frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/number_cell.dart

@@ -65,6 +65,8 @@ class _NumberCellState extends GridFocusNodeCellState<GridNumberCell> {
 
   @override
   Future<void> dispose() async {
+    _delayOperation = null;
+    _cellBloc.close();
     super.dispose();
   }
 
@@ -72,15 +74,10 @@ class _NumberCellState extends GridFocusNodeCellState<GridNumberCell> {
   Future<void> focusChanged() async {
     if (mounted) {
       _delayOperation?.cancel();
-      _delayOperation = Timer(const Duration(milliseconds: 300), () {
+      _delayOperation = Timer(const Duration(milliseconds: 30), () {
         if (_cellBloc.isClosed == false &&
             _controller.text != contentFromState(_cellBloc.state)) {
           _cellBloc.add(NumberCellEvent.updateCell(_controller.text));
-
-          if (!mounted) {
-            _delayOperation = null;
-            _cellBloc.close();
-          }
         }
       });
     }

+ 2 - 2
frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/text_cell.dart

@@ -76,7 +76,7 @@ class _GridTextCellState extends GridFocusNodeCellState<GridTextCell> {
 
   @override
   Future<void> dispose() async {
-    _delayOperation?.cancel();
+    _delayOperation = null;
     _cellBloc.close();
     super.dispose();
   }
@@ -85,7 +85,7 @@ class _GridTextCellState extends GridFocusNodeCellState<GridTextCell> {
   Future<void> focusChanged() async {
     if (mounted) {
       _delayOperation?.cancel();
-      _delayOperation = Timer(const Duration(milliseconds: 300), () {
+      _delayOperation = Timer(const Duration(milliseconds: 30), () {
         if (_cellBloc.isClosed == false &&
             _controller.text != _cellBloc.state.content) {
           _cellBloc.add(TextCellEvent.updateText(_controller.text));

+ 1 - 1
frontend/app_flowy/lib/plugins/trash/menu.dart

@@ -23,7 +23,7 @@ class MenuTrash extends StatelessWidget {
         onTap: () {
           getIt<MenuSharedState>().latestOpenView = null;
           getIt<HomeStackManager>()
-              .setPlugin(makePlugin(pluginType: DefaultPlugin.trash.type()));
+              .setPlugin(makePlugin(pluginType: PluginType.trash));
         },
         child: _render(context),
       ),

+ 1 - 1
frontend/app_flowy/lib/plugins/trash/trash.dart

@@ -34,7 +34,7 @@ class TrashPluginBuilder extends PluginBuilder {
   String get menuName => "TrashPB";
 
   @override
-  PluginType get pluginType => DefaultPlugin.trash.type();
+  PluginType get pluginType => PluginType.trash;
 }
 
 class TrashPluginConfig implements PluginConfig {

+ 21 - 21
frontend/app_flowy/lib/startup/plugin/plugin.dart

@@ -9,7 +9,7 @@ import 'package:flutter/widgets.dart';
 
 export "./src/sandbox.dart";
 
-enum DefaultPlugin {
+enum PluginType {
   editor,
   blank,
   trash,
@@ -17,24 +17,24 @@ enum DefaultPlugin {
   board,
 }
 
-extension FlowyDefaultPluginExt on DefaultPlugin {
-  int type() {
-    switch (this) {
-      case DefaultPlugin.editor:
-        return 0;
-      case DefaultPlugin.blank:
-        return 1;
-      case DefaultPlugin.trash:
-        return 2;
-      case DefaultPlugin.grid:
-        return 3;
-      case DefaultPlugin.board:
-        return 4;
-    }
-  }
-}
-
-typedef PluginType = int;
+// extension FlowyDefaultPluginExt on DefaultPlugin {
+//   int type() {
+//     switch (this) {
+//       case DefaultPlugin.editor:
+//         return 0;
+//       case DefaultPlugin.blank:
+//         return 1;
+//       case DefaultPlugin.trash:
+//         return 2;
+//       case DefaultPlugin.grid:
+//         return 3;
+//       case DefaultPlugin.board:
+//         return 4;
+//     }
+//   }
+// }
+
+// typedef PluginType = int;
 typedef PluginId = String;
 
 abstract class Plugin {
@@ -54,9 +54,9 @@ abstract class PluginBuilder {
 
   PluginType get pluginType;
 
-  ViewDataTypePB get dataType => ViewDataTypePB.TextBlock;
+  ViewDataTypePB get dataType => ViewDataTypePB.Text;
 
-  SubViewDataTypePB? get subDataType => null;
+  ViewLayoutTypePB? get subDataType => null;
 }
 
 abstract class PluginConfig {

+ 13 - 7
frontend/app_flowy/lib/startup/plugin/src/sandbox.dart

@@ -6,8 +6,10 @@ import '../plugin.dart';
 import 'runner.dart';
 
 class PluginSandbox {
-  final LinkedHashMap<PluginType, PluginBuilder> _pluginBuilders = LinkedHashMap();
-  final Map<PluginType, PluginConfig> _pluginConfigs = <PluginType, PluginConfig>{};
+  final LinkedHashMap<PluginType, PluginBuilder> _pluginBuilders =
+      LinkedHashMap();
+  final Map<PluginType, PluginConfig> _pluginConfigs =
+      <PluginType, PluginConfig>{};
   late PluginRunner pluginRunner;
 
   PluginSandbox() {
@@ -15,9 +17,11 @@ class PluginSandbox {
   }
 
   int indexOf(PluginType pluginType) {
-    final index = _pluginBuilders.keys.toList().indexWhere((ty) => ty == pluginType);
+    final index =
+        _pluginBuilders.keys.toList().indexWhere((ty) => ty == pluginType);
     if (index == -1) {
-      throw PlatformException(code: '-1', message: "Can't find the flowy plugin type: $pluginType");
+      throw PlatformException(
+          code: '-1', message: "Can't find the flowy plugin type: $pluginType");
     }
     return index;
   }
@@ -27,9 +31,11 @@ class PluginSandbox {
     return plugin;
   }
 
-  void registerPlugin(PluginType pluginType, PluginBuilder builder, {PluginConfig? config}) {
+  void registerPlugin(PluginType pluginType, PluginBuilder builder,
+      {PluginConfig? config}) {
     if (_pluginBuilders.containsKey(pluginType)) {
-      throw PlatformException(code: '-1', message: "$pluginType was registered before");
+      throw PlatformException(
+          code: '-1', message: "$pluginType was registered before");
     }
     _pluginBuilders[pluginType] = builder;
 
@@ -38,7 +44,7 @@ class PluginSandbox {
     }
   }
 
-  List<int> get supportPluginTypes => _pluginBuilders.keys.toList();
+  List<PluginType> get supportPluginTypes => _pluginBuilders.keys.toList();
 
   List<PluginBuilder> get builders => _pluginBuilders.values.toList();
 

+ 2 - 2
frontend/app_flowy/lib/workspace/application/app/app_bloc.dart

@@ -86,7 +86,7 @@ class AppBloc extends Bloc<AppEvent, AppState> {
       desc: value.desc,
       dataType: value.dataType,
       pluginType: value.pluginType,
-      subDataType: value.subDataType,
+      layout: value.layout,
     );
     viewOrFailed.fold(
       (view) => emit(state.copyWith(
@@ -140,7 +140,7 @@ class AppEvent with _$AppEvent {
     String name,
     String desc,
     ViewDataTypePB dataType,
-    SubViewDataTypePB? subDataType,
+    ViewLayoutTypePB layout,
     PluginType pluginType,
   ) = CreateView;
   const factory AppEvent.delete() = Delete;

+ 2 - 6
frontend/app_flowy/lib/workspace/application/app/app_service.dart

@@ -26,18 +26,14 @@ class AppService {
     required String desc,
     required ViewDataTypePB dataType,
     required PluginType pluginType,
-    SubViewDataTypePB? subDataType,
+    required ViewLayoutTypePB layout,
   }) {
     var payload = CreateViewPayloadPB.create()
       ..belongToId = appId
       ..name = name
       ..desc = desc
       ..dataType = dataType
-      ..pluginType = pluginType;
-
-    if (subDataType != null) {
-      payload.subDataType = subDataType;
-    }
+      ..layout = layout;
 
     return FolderEventCreateView(payload).send();
   }

+ 1 - 1
frontend/app_flowy/lib/workspace/application/menu/menu_bloc.dart

@@ -113,6 +113,6 @@ class MenuState with _$MenuState {
   factory MenuState.initial() => MenuState(
         apps: [],
         successOrFailure: left(unit),
-        plugin: makePlugin(pluginType: DefaultPlugin.blank.type()),
+        plugin: makePlugin(pluginType: PluginType.blank),
       );
 }

+ 13 - 0
frontend/app_flowy/lib/workspace/application/view/view_ext.dart

@@ -40,6 +40,19 @@ extension ViewExtension on ViewPB {
     return widget;
   }
 
+  PluginType get pluginType {
+    switch (layout) {
+      case ViewLayoutTypePB.Board:
+        return PluginType.board;
+      case ViewLayoutTypePB.Document:
+        return PluginType.editor;
+      case ViewLayoutTypePB.Grid:
+        return PluginType.grid;
+    }
+
+    throw UnimplementedError;
+  }
+
   Plugin plugin() {
     final plugin = makePlugin(pluginType: pluginType, data: this);
     return plugin;

+ 1 - 0
frontend/app_flowy/lib/workspace/presentation/home/home_screen.dart

@@ -1,5 +1,6 @@
 import 'package:app_flowy/startup/plugin/plugin.dart';
 import 'package:app_flowy/workspace/application/home/home_bloc.dart';
+import 'package:app_flowy/workspace/application/view/view_ext.dart';
 import 'package:app_flowy/workspace/presentation/widgets/edit_panel/panel_animation.dart';
 import 'package:app_flowy/workspace/presentation/widgets/float_bubble/question_bubble.dart';
 import 'package:app_flowy/startup/startup.dart';

+ 1 - 1
frontend/app_flowy/lib/workspace/presentation/home/home_stack.dart

@@ -107,7 +107,7 @@ class HomeStackNotifier extends ChangeNotifier {
   Widget get titleWidget => _plugin.display.leftBarItem;
 
   HomeStackNotifier({Plugin? plugin})
-      : _plugin = plugin ?? makePlugin(pluginType: DefaultPlugin.blank.type());
+      : _plugin = plugin ?? makePlugin(pluginType: PluginType.blank);
 
   set plugin(Plugin newPlugin) {
     if (newPlugin.id == _plugin.id) {

+ 1 - 1
frontend/app_flowy/lib/workspace/presentation/home/menu/app/header/header.dart

@@ -114,7 +114,7 @@ class MenuAppHeader extends StatelessWidget {
                 LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
                 "",
                 pluginBuilder.dataType,
-                pluginBuilder.subDataType,
+                pluginBuilder.subDataType!,
                 pluginBuilder.pluginType,
               ));
         },

+ 1 - 1
frontend/app_flowy/packages/appflowy_board/lib/src/utils/log.dart

@@ -6,7 +6,7 @@ const DART_LOG = "Dart_LOG";
 class Log {
   // static const enableLog = bool.hasEnvironment(DART_LOG);
   // static final shared = Log();
-  static const enableLog = true;
+  static const enableLog = false;
 
   static void info(String? message) {
     if (enableLog) {

+ 22 - 9
frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column_data.dart

@@ -84,20 +84,28 @@ class AFBoardColumnDataController extends ChangeNotifier with EquatableMixin {
     Log.debug(
         '[$AFBoardColumnDataController] $columnData insert $item at $index');
 
-    if (columnData._items.length > index) {
-      columnData._items.insert(index, item);
+    if (_containsItem(item)) {
+      return false;
     } else {
-      columnData._items.add(item);
+      if (columnData._items.length > index) {
+        columnData._items.insert(index, item);
+      } else {
+        columnData._items.add(item);
+      }
+
+      if (notify) notifyListeners();
+      return true;
     }
-
-    if (notify) notifyListeners();
-    return true;
   }
 
   bool add(AFColumnItem item, {bool notify = true}) {
-    columnData._items.add(item);
-    if (notify) notifyListeners();
-    return true;
+    if (_containsItem(item)) {
+      return false;
+    } else {
+      columnData._items.add(item);
+      if (notify) notifyListeners();
+      return true;
+    }
   }
 
   /// Replace the item at index with the [newItem].
@@ -114,6 +122,11 @@ class AFBoardColumnDataController extends ChangeNotifier with EquatableMixin {
 
     notifyListeners();
   }
+
+  bool _containsItem(AFColumnItem item) {
+    return columnData._items.indexWhere((element) => element.id == item.id) !=
+        -1;
+  }
 }
 
 /// [AFBoardColumnData] represents the data of each Column of the Board.

+ 1 - 1
frontend/app_flowy/pubspec.lock

@@ -35,7 +35,7 @@ packages:
       path: "packages/appflowy_editor"
       relative: true
     source: path
-    version: "0.0.1"
+    version: "0.0.2"
   args:
     dependency: transitive
     description:

+ 48 - 26
frontend/rust-lib/flowy-folder/src/entities/view.rs

@@ -7,7 +7,7 @@ use crate::{
     impl_def_and_def_mut,
 };
 use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
-use flowy_folder_data_model::revision::{gen_view_id, ViewDataTypeRevision, ViewRevision};
+use flowy_folder_data_model::revision::{gen_view_id, ViewDataTypeRevision, ViewLayoutTypeRevision, ViewRevision};
 use std::convert::TryInto;
 
 #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)]
@@ -16,7 +16,7 @@ pub struct ViewPB {
     pub id: String,
 
     #[pb(index = 2)]
-    pub belong_to_id: String,
+    pub app_id: String,
 
     #[pb(index = 3)]
     pub name: String,
@@ -31,35 +31,29 @@ pub struct ViewPB {
     pub create_time: i64,
 
     #[pb(index = 7)]
-    pub plugin_type: i32,
+    pub layout: ViewLayoutTypePB,
 }
 
 impl std::convert::From<ViewRevision> for ViewPB {
     fn from(rev: ViewRevision) -> Self {
         ViewPB {
             id: rev.id,
-            belong_to_id: rev.belong_to_id,
+            app_id: rev.app_id,
             name: rev.name,
             data_type: rev.data_type.into(),
             modified_time: rev.modified_time,
             create_time: rev.create_time,
-            plugin_type: rev.plugin_type,
+            layout: rev.layout.into(),
         }
     }
 }
 
 #[derive(Eq, PartialEq, Hash, Debug, ProtoBuf_Enum, Clone)]
 pub enum ViewDataTypePB {
-    TextBlock = 0,
+    Text = 0,
     Database = 1,
 }
 
-#[derive(Eq, PartialEq, Hash, Debug, ProtoBuf_Enum, Clone)]
-pub enum SubViewDataTypePB {
-    Grid = 0,
-    Board = 1,
-}
-
 impl std::default::Default for ViewDataTypePB {
     fn default() -> Self {
         ViewDataTypeRevision::default().into()
@@ -69,7 +63,7 @@ impl std::default::Default for ViewDataTypePB {
 impl std::convert::From<ViewDataTypeRevision> for ViewDataTypePB {
     fn from(rev: ViewDataTypeRevision) -> Self {
         match rev {
-            ViewDataTypeRevision::TextBlock => ViewDataTypePB::TextBlock,
+            ViewDataTypeRevision::Text => ViewDataTypePB::Text,
             ViewDataTypeRevision::Database => ViewDataTypePB::Database,
         }
     }
@@ -78,12 +72,45 @@ impl std::convert::From<ViewDataTypeRevision> for ViewDataTypePB {
 impl std::convert::From<ViewDataTypePB> for ViewDataTypeRevision {
     fn from(ty: ViewDataTypePB) -> Self {
         match ty {
-            ViewDataTypePB::TextBlock => ViewDataTypeRevision::TextBlock,
+            ViewDataTypePB::Text => ViewDataTypeRevision::Text,
             ViewDataTypePB::Database => ViewDataTypeRevision::Database,
         }
     }
 }
 
+#[derive(Eq, PartialEq, Hash, Debug, ProtoBuf_Enum, Clone)]
+pub enum ViewLayoutTypePB {
+    Document = 0,
+    Grid = 3,
+    Board = 4,
+}
+
+impl std::default::Default for ViewLayoutTypePB {
+    fn default() -> Self {
+        ViewLayoutTypePB::Grid
+    }
+}
+
+impl std::convert::From<ViewLayoutTypeRevision> for ViewLayoutTypePB {
+    fn from(rev: ViewLayoutTypeRevision) -> Self {
+        match rev {
+            ViewLayoutTypeRevision::Grid => ViewLayoutTypePB::Grid,
+            ViewLayoutTypeRevision::Board => ViewLayoutTypePB::Board,
+            ViewLayoutTypeRevision::Document => ViewLayoutTypePB::Document,
+        }
+    }
+}
+
+impl std::convert::From<ViewLayoutTypePB> for ViewLayoutTypeRevision {
+    fn from(rev: ViewLayoutTypePB) -> Self {
+        match rev {
+            ViewLayoutTypePB::Grid => ViewLayoutTypeRevision::Grid,
+            ViewLayoutTypePB::Board => ViewLayoutTypeRevision::Board,
+            ViewLayoutTypePB::Document => ViewLayoutTypeRevision::Document,
+        }
+    }
+}
+
 #[derive(Eq, PartialEq, Debug, Default, ProtoBuf, Clone)]
 pub struct RepeatedViewPB {
     #[pb(index = 1)]
@@ -121,14 +148,11 @@ pub struct CreateViewPayloadPB {
     #[pb(index = 5)]
     pub data_type: ViewDataTypePB,
 
-    #[pb(index = 6, one_of)]
-    pub sub_data_type: Option<SubViewDataTypePB>,
+    #[pb(index = 6)]
+    pub layout: ViewLayoutTypePB,
 
     #[pb(index = 7)]
-    pub plugin_type: i32,
-
-    #[pb(index = 8)]
-    pub data: Vec<u8>,
+    pub view_content_data: Vec<u8>,
 }
 
 #[derive(Debug, Clone)]
@@ -138,10 +162,9 @@ pub struct CreateViewParams {
     pub desc: String,
     pub thumbnail: String,
     pub data_type: ViewDataTypePB,
-    pub sub_data_type: Option<SubViewDataTypePB>,
+    pub layout: ViewLayoutTypePB,
     pub view_id: String,
-    pub data: Vec<u8>,
-    pub plugin_type: i32,
+    pub view_content_data: Vec<u8>,
 }
 
 impl TryInto<CreateViewParams> for CreateViewPayloadPB {
@@ -161,11 +184,10 @@ impl TryInto<CreateViewParams> for CreateViewPayloadPB {
             name,
             desc: self.desc,
             data_type: self.data_type,
-            sub_data_type: self.sub_data_type,
+            layout: self.layout,
             thumbnail,
             view_id,
-            data: self.data,
-            plugin_type: self.plugin_type,
+            view_content_data: self.view_content_data,
         })
     }
 }

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

@@ -1,5 +1,5 @@
 use crate::entities::view::ViewDataTypePB;
-use crate::entities::SubViewDataTypePB;
+use crate::entities::ViewLayoutTypePB;
 use crate::services::folder_editor::FolderRevisionCompactor;
 use crate::{
     dart_notification::{send_dart_notification, FolderNotification},
@@ -222,7 +222,7 @@ impl DefaultFolderBuilder {
                 };
                 let _ = view_controller.set_latest_view(&view.id);
                 let _ = view_controller
-                    .create_view(&view.id, ViewDataTypePB::TextBlock, Bytes::from(view_data))
+                    .create_view(&view.id, ViewDataTypePB::Text, Bytes::from(view_data))
                     .await?;
             }
         }
@@ -261,7 +261,7 @@ pub trait ViewDataProcessor {
         &self,
         user_id: &str,
         view_id: &str,
-        sub_data_type: Option<SubViewDataTypePB>,
+        layout: ViewLayoutTypePB,
     ) -> FutureResult<Bytes, FlowyError>;
 
     fn create_view_from_delta_data(

+ 7 - 7
frontend/rust-lib/flowy-folder/src/services/persistence/version_1/view_sql.rs

@@ -13,7 +13,7 @@ use flowy_database::{
     SqliteConnection,
 };
 
-use flowy_folder_data_model::revision::{ViewDataTypeRevision, ViewRevision};
+use flowy_folder_data_model::revision::{ViewDataTypeRevision, ViewLayoutTypeRevision, ViewRevision};
 use lib_infra::util::timestamp;
 
 pub struct ViewTableSql();
@@ -87,13 +87,13 @@ pub(crate) struct ViewTable {
 impl ViewTable {
     pub fn new(view_rev: ViewRevision) -> Self {
         let data_type = match view_rev.data_type {
-            ViewDataTypeRevision::TextBlock => SqlViewDataType::Block,
+            ViewDataTypeRevision::Text => SqlViewDataType::Block,
             ViewDataTypeRevision::Database => SqlViewDataType::Grid,
         };
 
         ViewTable {
             id: view_rev.id,
-            belong_to_id: view_rev.belong_to_id,
+            belong_to_id: view_rev.app_id,
             name: view_rev.name,
             desc: view_rev.desc,
             modified_time: view_rev.modified_time,
@@ -110,13 +110,13 @@ impl ViewTable {
 impl std::convert::From<ViewTable> for ViewRevision {
     fn from(table: ViewTable) -> Self {
         let data_type = match table.view_type {
-            SqlViewDataType::Block => ViewDataTypeRevision::TextBlock,
+            SqlViewDataType::Block => ViewDataTypeRevision::Text,
             SqlViewDataType::Grid => ViewDataTypeRevision::Database,
         };
 
         ViewRevision {
             id: table.id,
-            belong_to_id: table.belong_to_id,
+            app_id: table.belong_to_id,
             name: table.name,
             desc: table.desc,
             data_type,
@@ -127,8 +127,8 @@ impl std::convert::From<ViewTable> for ViewRevision {
             ext_data: "".to_string(),
             thumbnail: table.thumbnail,
             // Store the view in ViewTable was deprecated since v0.0.2.
-            // No need to worry about plugin_type.
-            plugin_type: 0,
+            // No need to worry about layout.
+            layout: ViewLayoutTypeRevision::Document,
         }
     }
 }

+ 14 - 15
frontend/rust-lib/flowy-folder/src/services/view/controller.rs

@@ -60,14 +60,14 @@ impl ViewController {
     ) -> Result<ViewRevision, FlowyError> {
         let processor = self.get_data_processor(params.data_type.clone())?;
         let user_id = self.user.user_id()?;
-        if params.data.is_empty() {
+        if params.view_content_data.is_empty() {
             let view_data = processor
-                .create_default_view(&user_id, &params.view_id, params.sub_data_type.clone())
+                .create_default_view(&user_id, &params.view_id, params.layout.clone())
                 .await?;
-            params.data = view_data.to_vec();
+            params.view_content_data = view_data.to_vec();
         } else {
             let delta_data = processor
-                .create_view_from_delta_data(&user_id, &params.view_id, params.data.clone())
+                .create_view_from_delta_data(&user_id, &params.view_id, params.view_content_data.clone())
                 .await?;
             let _ = self
                 .create_view(&params.view_id, params.data_type.clone(), delta_data)
@@ -99,7 +99,7 @@ impl ViewController {
         let trash_controller = self.trash_controller.clone();
         self.persistence
             .begin_transaction(|transaction| {
-                let belong_to_id = view_rev.belong_to_id.clone();
+                let belong_to_id = view_rev.app_id.clone();
                 let _ = transaction.create_view(view_rev)?;
                 let _ = notify_views_changed(&belong_to_id, trash_controller, &transaction)?;
                 Ok(())
@@ -139,7 +139,7 @@ impl ViewController {
 
                 let view_info = ViewInfoPB {
                     id: view_rev.id,
-                    belong_to_id: view_rev.belong_to_id,
+                    belong_to_id: view_rev.app_id,
                     name: view_rev.name,
                     desc: view_rev.desc,
                     data_type: view_rev.data_type.into(),
@@ -197,7 +197,7 @@ impl ViewController {
             .begin_transaction(|transaction| {
                 let _ = transaction.move_view(view_id, from, to)?;
                 let view = transaction.read_view(view_id)?;
-                let _ = notify_views_changed(&view.belong_to_id, self.trash_controller.clone(), &transaction)?;
+                let _ = notify_views_changed(&view.app_id, self.trash_controller.clone(), &transaction)?;
                 Ok(())
             })
             .await?;
@@ -214,15 +214,14 @@ impl ViewController {
         let processor = self.get_data_processor(view_rev.data_type.clone())?;
         let delta_bytes = processor.get_delta_data(view_id).await?;
         let duplicate_params = CreateViewParams {
-            belong_to_id: view_rev.belong_to_id.clone(),
+            belong_to_id: view_rev.app_id.clone(),
             name: format!("{} (copy)", &view_rev.name),
             desc: view_rev.desc,
             thumbnail: view_rev.thumbnail,
             data_type: view_rev.data_type.into(),
-            sub_data_type: None,
-            data: delta_bytes.to_vec(),
+            layout: view_rev.layout.into(),
+            view_content_data: delta_bytes.to_vec(),
             view_id: gen_view_id(),
-            plugin_type: view_rev.plugin_type,
         };
 
         let _ = self.create_view_from_params(duplicate_params).await?;
@@ -252,7 +251,7 @@ impl ViewController {
                 send_dart_notification(&view_id, FolderNotification::ViewUpdated)
                     .payload(view)
                     .send();
-                let _ = notify_views_changed(&view_rev.belong_to_id, self.trash_controller.clone(), &transaction)?;
+                let _ = notify_views_changed(&view_rev.app_id, self.trash_controller.clone(), &transaction)?;
                 Ok(view_rev)
             })
             .await?;
@@ -395,7 +394,7 @@ async fn handle_trash_event(
                 .begin_transaction(|transaction| {
                     let view_revs = read_local_views_with_transaction(identifiers, &transaction)?;
                     for view_rev in view_revs {
-                        let _ = notify_views_changed(&view_rev.belong_to_id, trash_can.clone(), &transaction)?;
+                        let _ = notify_views_changed(&view_rev.app_id, trash_can.clone(), &transaction)?;
                         notify_dart(view_rev.into(), FolderNotification::ViewDeleted);
                     }
                     Ok(())
@@ -408,7 +407,7 @@ async fn handle_trash_event(
                 .begin_transaction(|transaction| {
                     let view_revs = read_local_views_with_transaction(identifiers, &transaction)?;
                     for view_rev in view_revs {
-                        let _ = notify_views_changed(&view_rev.belong_to_id, trash_can.clone(), &transaction)?;
+                        let _ = notify_views_changed(&view_rev.app_id, trash_can.clone(), &transaction)?;
                         notify_dart(view_rev.into(), FolderNotification::ViewRestored);
                     }
                     Ok(())
@@ -425,7 +424,7 @@ async fn handle_trash_event(
                         for identifier in identifiers.items {
                             let view = transaction.read_view(&identifier.id)?;
                             let _ = transaction.delete_view(&view.id)?;
-                            notify_ids.insert(view.belong_to_id.clone());
+                            notify_ids.insert(view.app_id.clone());
                             views.push(view);
                         }
                         for notify_id in notify_ids {

+ 4 - 5
frontend/rust-lib/flowy-folder/tests/workspace/folder_test.rs

@@ -1,7 +1,6 @@
 use crate::script::{invalid_workspace_name_test_case, FolderScript::*, FolderTest};
 use flowy_folder::entities::view::ViewDataTypePB;
 use flowy_folder::entities::workspace::CreateWorkspacePayloadPB;
-
 use flowy_revision::disk::RevisionState;
 use flowy_test::{event_builder::*, FlowySDKTest};
 
@@ -134,7 +133,7 @@ async fn app_create_with_view() {
         CreateView {
             name: "View A".to_owned(),
             desc: "View A description".to_owned(),
-            data_type: ViewDataTypePB::TextBlock,
+            data_type: ViewDataTypePB::Text,
         },
         CreateView {
             name: "Grid".to_owned(),
@@ -198,7 +197,7 @@ async fn view_delete_all() {
         CreateView {
             name: "View A".to_owned(),
             desc: "View A description".to_owned(),
-            data_type: ViewDataTypePB::TextBlock,
+            data_type: ViewDataTypePB::Text,
         },
         CreateView {
             name: "Grid".to_owned(),
@@ -231,7 +230,7 @@ async fn view_delete_all_permanent() {
         CreateView {
             name: "View A".to_owned(),
             desc: "View A description".to_owned(),
-            data_type: ViewDataTypePB::TextBlock,
+            data_type: ViewDataTypePB::Text,
         },
         ReadApp(app.id.clone()),
     ])
@@ -330,7 +329,7 @@ async fn folder_sync_revision_with_new_view() {
         CreateView {
             name: view_name.clone(),
             desc: view_desc.clone(),
-            data_type: ViewDataTypePB::TextBlock,
+            data_type: ViewDataTypePB::Text,
         },
         AssertCurrentRevId(3),
         AssertNextSyncRevId(Some(3)),

+ 11 - 11
frontend/rust-lib/flowy-folder/tests/workspace/script.rs

@@ -5,7 +5,7 @@ use flowy_folder::entities::{
     trash::{RepeatedTrashPB, TrashIdPB, TrashType},
     view::{CreateViewPayloadPB, UpdateViewPayloadPB},
     workspace::{CreateWorkspacePayloadPB, RepeatedWorkspacePB},
-    SubViewDataTypePB,
+    ViewLayoutTypePB,
 };
 use flowy_folder::entities::{
     app::{AppPB, RepeatedAppPB},
@@ -99,7 +99,8 @@ impl FolderTest {
             &app.id,
             "Folder View",
             "Folder test view",
-            ViewDataTypePB::TextBlock,
+            ViewDataTypePB::Text,
+            ViewLayoutTypePB::Document,
         )
         .await;
         app.belongings = RepeatedViewPB {
@@ -180,7 +181,11 @@ impl FolderTest {
             }
 
             FolderScript::CreateView { name, desc, data_type } => {
-                let view = create_view(sdk, &self.app.id, &name, &desc, data_type).await;
+                let layout = match data_type {
+                    ViewDataTypePB::Text => ViewLayoutTypePB::Document,
+                    ViewDataTypePB::Database => ViewLayoutTypePB::Grid,
+                };
+                let view = create_view(sdk, &self.app.id, &name, &desc, data_type, layout).await;
                 self.view = view;
             }
             FolderScript::AssertView(view) => {
@@ -353,21 +358,16 @@ pub async fn create_view(
     name: &str,
     desc: &str,
     data_type: ViewDataTypePB,
+    layout: ViewLayoutTypePB,
 ) -> ViewPB {
-    let sub_data_type = match data_type {
-        ViewDataTypePB::TextBlock => None,
-        ViewDataTypePB::Database => Some(SubViewDataTypePB::Grid),
-    };
-
     let request = CreateViewPayloadPB {
         belong_to_id: app_id.to_string(),
         name: name.to_string(),
         desc: desc.to_string(),
         thumbnail: None,
         data_type,
-        sub_data_type,
-        plugin_type: 0,
-        data: vec![],
+        layout,
+        view_content_data: vec![],
     };
     let view = FolderEventBuilder::new(sdk.clone())
         .event(CreateView)

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

@@ -62,6 +62,16 @@ impl std::convert::From<&RowRevision> for RowPB {
     }
 }
 
+impl std::convert::From<&mut RowRevision> for RowPB {
+    fn from(rev: &mut RowRevision) -> Self {
+        Self {
+            block_id: rev.block_id.clone(),
+            id: rev.id.clone(),
+            height: rev.height,
+        }
+    }
+}
+
 impl std::convert::From<&Arc<RowRevision>> for RowPB {
     fn from(rev: &Arc<RowRevision>) -> Self {
         Self {

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

@@ -1,7 +1,7 @@
 use flowy_derive::ProtoBuf;
 use flowy_error::ErrorCode;
 use flowy_grid_data_model::parser::NotEmptyStr;
-use flowy_grid_data_model::revision::{CellRevision, RowMetaChangeset};
+use flowy_grid_data_model::revision::{CellRevision, RowChangeset};
 use std::collections::HashMap;
 
 #[derive(ProtoBuf, Default)]
@@ -135,7 +135,7 @@ pub struct CellChangesetPB {
     pub content: Option<String>,
 }
 
-impl std::convert::From<CellChangesetPB> for RowMetaChangeset {
+impl std::convert::From<CellChangesetPB> for RowChangeset {
     fn from(changeset: CellChangesetPB) -> Self {
         let mut cell_by_field_id = HashMap::with_capacity(1);
         let field_id = changeset.field_id;
@@ -144,7 +144,7 @@ impl std::convert::From<CellChangesetPB> for RowMetaChangeset {
         };
         cell_by_field_id.insert(field_id, cell_rev);
 
-        RowMetaChangeset {
+        RowChangeset {
             row_id: changeset.row_id,
             height: None,
             visibility: None,

+ 10 - 27
frontend/rust-lib/flowy-grid/src/entities/grid_entities.rs

@@ -1,4 +1,4 @@
-use crate::entities::{BlockPB, FieldIdPB, GridLayout};
+use crate::entities::{BlockPB, FieldIdPB};
 use flowy_derive::ProtoBuf;
 use flowy_error::ErrorCode;
 use flowy_grid_data_model::parser::NotEmptyStr;
@@ -96,28 +96,16 @@ pub struct MoveRowPayloadPB {
     pub view_id: String,
 
     #[pb(index = 2)]
-    pub row_id: String,
-
-    #[pb(index = 3)]
-    pub from_index: i32,
+    pub from_row_id: String,
 
     #[pb(index = 4)]
-    pub to_index: i32,
-
-    #[pb(index = 5)]
-    pub layout: GridLayout,
-
-    #[pb(index = 6, one_of)]
-    pub upper_row_id: Option<String>,
+    pub to_row_id: String,
 }
 
 pub struct MoveRowParams {
     pub view_id: String,
-    pub row_id: String,
-    pub from_index: i32,
-    pub to_index: i32,
-    pub layout: GridLayout,
-    pub upper_row_id: Option<String>,
+    pub from_row_id: String,
+    pub to_row_id: String,
 }
 
 impl TryInto<MoveRowParams> for MoveRowPayloadPB {
@@ -125,18 +113,13 @@ impl TryInto<MoveRowParams> for MoveRowPayloadPB {
 
     fn try_into(self) -> Result<MoveRowParams, Self::Error> {
         let view_id = NotEmptyStr::parse(self.view_id).map_err(|_| ErrorCode::GridViewIdIsEmpty)?;
-        let row_id = NotEmptyStr::parse(self.row_id).map_err(|_| ErrorCode::RowIdIsEmpty)?;
-        let upper_row_id = match self.upper_row_id {
-            None => None,
-            Some(upper_row_id) => Some(NotEmptyStr::parse(upper_row_id).map_err(|_| ErrorCode::RowIdIsEmpty)?.0),
-        };
+        let from_row_id = NotEmptyStr::parse(self.from_row_id).map_err(|_| ErrorCode::RowIdIsEmpty)?;
+        let to_row_id = NotEmptyStr::parse(self.to_row_id).map_err(|_| ErrorCode::RowIdIsEmpty)?;
+
         Ok(MoveRowParams {
             view_id: view_id.0,
-            row_id: row_id.0,
-            from_index: self.from_index,
-            to_index: self.to_index,
-            layout: self.layout,
-            upper_row_id,
+            from_row_id: from_row_id.0,
+            to_row_id: to_row_id.0,
         })
     }
 }

+ 14 - 1
frontend/rust-lib/flowy-grid/src/entities/group_entities/group.rs

@@ -28,7 +28,20 @@ impl std::convert::From<&GroupConfigurationRevision> for GridGroupConfigurationP
 #[derive(ProtoBuf, Debug, Default, Clone)]
 pub struct RepeatedGridGroupPB {
     #[pb(index = 1)]
-    pub(crate) items: Vec<GroupPB>,
+    pub items: Vec<GroupPB>,
+}
+
+impl std::ops::Deref for RepeatedGridGroupPB {
+    type Target = Vec<GroupPB>;
+    fn deref(&self) -> &Self::Target {
+        &self.items
+    }
+}
+
+impl std::ops::DerefMut for RepeatedGridGroupPB {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        &mut self.items
+    }
 }
 
 #[derive(ProtoBuf, Debug, Default, Clone)]

+ 18 - 0
frontend/rust-lib/flowy-grid/src/entities/group_entities/group_changeset.rs

@@ -1,5 +1,6 @@
 use crate::entities::{InsertedRowPB, RowPB};
 use flowy_derive::ProtoBuf;
+use std::fmt::Formatter;
 
 #[derive(Debug, Default, ProtoBuf)]
 pub struct GroupRowsChangesetPB {
@@ -16,6 +17,23 @@ pub struct GroupRowsChangesetPB {
     pub updated_rows: Vec<RowPB>,
 }
 
+impl std::fmt::Display for GroupRowsChangesetPB {
+    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+        for inserted_row in &self.inserted_rows {
+            let _ = f.write_fmt(format_args!(
+                "Insert: {} row at {:?}",
+                inserted_row.row.id, inserted_row.index
+            ))?;
+        }
+
+        for deleted_row in &self.deleted_rows {
+            let _ = f.write_fmt(format_args!("Delete: {} row", deleted_row))?;
+        }
+
+        Ok(())
+    }
+}
+
 impl GroupRowsChangesetPB {
     pub fn is_empty(&self) -> bool {
         self.inserted_rows.is_empty() && self.deleted_rows.is_empty() && self.updated_rows.is_empty()

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

@@ -225,7 +225,7 @@ async fn get_type_option_data(field_rev: &FieldRevision, field_type: &FieldType)
     Ok(type_option_data)
 }
 
-#[tracing::instrument(level = "debug", skip(data, manager), err)]
+// #[tracing::instrument(level = "debug", skip(data, manager), err)]
 pub(crate) async fn get_row_handler(
     data: Data<RowIdPB>,
     manager: AppData<Arc<GridManager>>,

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

@@ -1,7 +1,7 @@
 use crate::entities::RowPB;
 use bytes::Bytes;
 use flowy_error::{FlowyError, FlowyResult};
-use flowy_grid_data_model::revision::{CellRevision, GridBlockRevision, RowMetaChangeset, RowRevision};
+use flowy_grid_data_model::revision::{CellRevision, GridBlockRevision, RowChangeset, RowRevision};
 use flowy_revision::{RevisionCloudService, RevisionCompactor, RevisionManager, RevisionObjectBuilder};
 use flowy_sync::client_grid::{GridBlockRevisionChangeset, GridBlockRevisionPad};
 use flowy_sync::entities::revision::Revision;
@@ -88,7 +88,7 @@ impl GridBlockRevisionEditor {
         Ok(row_count)
     }
 
-    pub async fn update_row(&self, changeset: RowMetaChangeset) -> FlowyResult<()> {
+    pub async fn update_row(&self, changeset: RowChangeset) -> FlowyResult<()> {
         let _ = self.modify(|block_pad| Ok(block_pad.update_row(changeset)?)).await?;
         Ok(())
     }
@@ -127,7 +127,7 @@ impl GridBlockRevisionEditor {
         Ok(cell_revs)
     }
 
-    pub async fn get_row_info(&self, row_id: &str) -> FlowyResult<Option<RowPB>> {
+    pub async fn get_row_pb(&self, row_id: &str) -> FlowyResult<Option<RowPB>> {
         let row_ids = Some(vec![Cow::Borrowed(row_id)]);
         Ok(self.get_row_infos(row_ids).await?.pop())
     }

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

@@ -7,7 +7,7 @@ use crate::services::row::{block_from_row_orders, make_row_from_row_rev, GridBlo
 use dashmap::DashMap;
 use flowy_error::FlowyResult;
 use flowy_grid_data_model::revision::{
-    GridBlockMetaRevision, GridBlockMetaRevisionChangeset, RowMetaChangeset, RowRevision,
+    GridBlockMetaRevision, GridBlockMetaRevisionChangeset, RowChangeset, RowRevision,
 };
 use flowy_revision::disk::SQLiteGridBlockRevisionPersistence;
 use flowy_revision::{RevisionManager, RevisionPersistence, SQLiteRevisionSnapshotPersistence};
@@ -103,17 +103,14 @@ impl GridBlockManager {
         Ok(changesets)
     }
 
-    pub async fn update_row<F>(&self, changeset: RowMetaChangeset, row_builder: F) -> FlowyResult<()>
-    where
-        F: FnOnce(Arc<RowRevision>) -> RowPB,
-    {
+    pub async fn update_row(&self, changeset: RowChangeset) -> FlowyResult<()> {
         let editor = self.get_editor_from_row_id(&changeset.row_id).await?;
         let _ = editor.update_row(changeset.clone()).await?;
         match editor.get_row_rev(&changeset.row_id).await? {
             None => tracing::error!("Internal error: can't find the row with id: {}", changeset.row_id),
             Some(row_rev) => {
-                let block_order_changeset =
-                    GridBlockChangesetPB::update(&editor.block_id, vec![row_builder(row_rev.clone())]);
+                let row_pb = make_row_from_row_rev(row_rev.clone());
+                let block_order_changeset = GridBlockChangesetPB::update(&editor.block_id, vec![row_pb]);
                 let _ = self
                     .notify_did_update_block(&editor.block_id, block_order_changeset)
                     .await?;
@@ -123,21 +120,23 @@ impl GridBlockManager {
     }
 
     #[tracing::instrument(level = "trace", skip_all, err)]
-    pub async fn delete_row(&self, row_id: &str) -> FlowyResult<()> {
+    pub async fn delete_row(&self, row_id: &str) -> FlowyResult<Option<Arc<RowRevision>>> {
         let row_id = row_id.to_owned();
         let block_id = self.persistence.get_block_id(&row_id)?;
         let editor = self.get_block_editor(&block_id).await?;
-        match editor.get_row_info(&row_id).await? {
-            None => {}
-            Some(row_info) => {
+        match editor.get_row_rev(&row_id).await? {
+            None => Ok(None),
+            Some(row_rev) => {
                 let _ = editor.delete_rows(vec![Cow::Borrowed(&row_id)]).await?;
                 let _ = self
-                    .notify_did_update_block(&block_id, GridBlockChangesetPB::delete(&block_id, vec![row_info.id]))
+                    .notify_did_update_block(
+                        &block_id,
+                        GridBlockChangesetPB::delete(&block_id, vec![row_rev.id.clone()]),
+                    )
                     .await?;
+                Ok(Some(row_rev))
             }
         }
-
-        Ok(())
     }
 
     pub(crate) async fn delete_rows(&self, row_orders: Vec<RowPB>) -> FlowyResult<Vec<GridBlockMetaRevisionChangeset>> {
@@ -189,12 +188,9 @@ impl GridBlockManager {
         }
     }
 
-    pub async fn update_cell<F>(&self, changeset: CellChangesetPB, row_builder: F) -> FlowyResult<()>
-    where
-        F: FnOnce(Arc<RowRevision>) -> RowPB,
-    {
-        let row_changeset: RowMetaChangeset = changeset.clone().into();
-        let _ = self.update_row(row_changeset, row_builder).await?;
+    pub async fn update_cell(&self, changeset: CellChangesetPB) -> FlowyResult<()> {
+        let row_changeset: RowChangeset = changeset.clone().into();
+        let _ = self.update_row(row_changeset).await?;
         self.notify_did_update_cell(changeset).await?;
         Ok(())
     }

+ 2 - 9
frontend/rust-lib/flowy-grid/src/services/block_manager_trait_impl.rs

@@ -1,6 +1,6 @@
 use crate::services::block_manager::GridBlockManager;
-use crate::services::grid_view_manager::{GridViewRowDelegate, GridViewRowOperation};
-use flowy_error::FlowyResult;
+use crate::services::grid_view_manager::GridViewRowDelegate;
+
 use flowy_grid_data_model::revision::RowRevision;
 use lib_infra::future::{wrap_future, AFFuture};
 use std::sync::Arc;
@@ -36,10 +36,3 @@ impl GridViewRowDelegate for Arc<GridBlockManager> {
         })
     }
 }
-
-impl GridViewRowOperation for Arc<GridBlockManager> {
-    fn gv_move_row(&self, row_rev: Arc<RowRevision>, from: usize, to: usize) -> AFFuture<FlowyResult<()>> {
-        let block_manager = self.clone();
-        wrap_future(async move { block_manager.move_row(row_rev, from, to).await })
-    }
-}

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

@@ -65,10 +65,8 @@ impl CellDataOperation<SelectOptionIds, SelectOptionCellChangeset> for SingleSel
         let select_option_changeset = changeset.try_into_inner()?;
         let new_cell_data: String;
         if let Some(insert_option_id) = select_option_changeset.insert_option_id {
-            tracing::trace!("Insert single select option: {}", &insert_option_id);
             new_cell_data = insert_option_id;
         } else {
-            tracing::trace!("Delete single select option");
             new_cell_data = "".to_string()
         }
 

+ 46 - 17
frontend/rust-lib/flowy-grid/src/services/grid_editor.rs

@@ -9,9 +9,7 @@ use crate::services::field::{default_type_option_builder_from_type, type_option_
 use crate::services::filter::GridFilterService;
 use crate::services::grid_view_manager::GridViewManager;
 use crate::services::persistence::block_index::BlockIndexCache;
-use crate::services::row::{
-    make_grid_blocks, make_row_from_row_rev, make_rows_from_row_revs, GridBlockSnapshot, RowRevisionBuilder,
-};
+use crate::services::row::{make_grid_blocks, make_rows_from_row_revs, GridBlockSnapshot, RowRevisionBuilder};
 use bytes::Bytes;
 use flowy_error::{ErrorCode, FlowyError, FlowyResult};
 use flowy_grid_data_model::revision::*;
@@ -72,7 +70,6 @@ impl GridRevisionEditor {
                 user.clone(),
                 Arc::new(grid_pad.clone()),
                 Arc::new(block_manager.clone()),
-                Arc::new(block_manager.clone()),
                 Arc::new(task_scheduler.clone()),
             )
             .await?,
@@ -289,7 +286,7 @@ impl GridRevisionEditor {
     pub async fn create_row(&self, params: CreateRowParams) -> FlowyResult<RowPB> {
         let mut row_rev = self.create_row_rev().await?;
 
-        self.view_manager.fill_row(&mut row_rev, &params).await;
+        self.view_manager.will_create_row(&mut row_rev, &params).await;
 
         let row_pb = self.create_row_pb(row_rev, params.start_row_id.clone()).await?;
 
@@ -315,9 +312,9 @@ impl GridRevisionEditor {
         Ok(row_orders)
     }
 
-    pub async fn update_row(&self, changeset: RowMetaChangeset) -> FlowyResult<()> {
+    pub async fn update_row(&self, changeset: RowChangeset) -> FlowyResult<()> {
         let row_id = changeset.row_id.clone();
-        let _ = self.block_manager.update_row(changeset, make_row_from_row_rev).await?;
+        let _ = self.block_manager.update_row(changeset).await?;
         self.view_manager.did_update_row(&row_id).await;
         Ok(())
     }
@@ -346,8 +343,10 @@ impl GridRevisionEditor {
     }
 
     pub async fn delete_row(&self, row_id: &str) -> FlowyResult<()> {
-        let _ = self.block_manager.delete_row(row_id).await?;
-        self.view_manager.did_delete_row(row_id).await;
+        let row_rev = self.block_manager.delete_row(row_id).await?;
+        if let Some(row_rev) = row_rev {
+            self.view_manager.did_delete_row(row_rev).await;
+        }
         Ok(())
     }
 
@@ -394,12 +393,11 @@ impl GridRevisionEditor {
 
         match self.grid_pad.read().await.get_field_rev(&field_id) {
             None => {
-                let msg = format!("Field not found with id: {}", &field_id);
+                let msg = format!("Field:{} not found", &field_id);
                 Err(FlowyError::internal().context(msg))
             }
             Some((_, field_rev)) => {
                 tracing::trace!("field changeset: id:{} / value:{:?}", &field_id, content);
-
                 let cell_rev = self.get_cell_rev(&row_id, &field_id).await?;
                 // Update the changeset.data property with the return value.
                 content = Some(apply_cell_data_changeset(content.unwrap(), cell_rev, field_rev)?);
@@ -409,11 +407,7 @@ impl GridRevisionEditor {
                     field_id,
                     content,
                 };
-                let _ = self
-                    .block_manager
-                    .update_cell(cell_changeset, make_row_from_row_rev)
-                    .await?;
-
+                let _ = self.block_manager.update_cell(cell_changeset).await?;
                 self.view_manager.did_update_row(&row_id).await;
                 Ok(())
             }
@@ -492,7 +486,42 @@ impl GridRevisionEditor {
     }
 
     pub async fn move_row(&self, params: MoveRowParams) -> FlowyResult<()> {
-        self.view_manager.move_row(params).await
+        let MoveRowParams {
+            view_id: _,
+            from_row_id,
+            to_row_id,
+        } = params;
+
+        match self.block_manager.get_row_rev(&from_row_id).await? {
+            None => tracing::warn!("Move row failed, can not find the row:{}", from_row_id),
+            Some(row_rev) => {
+                match (
+                    self.block_manager.index_of_row(&from_row_id).await,
+                    self.block_manager.index_of_row(&to_row_id).await,
+                ) {
+                    (Some(from_index), Some(to_index)) => {
+                        tracing::trace!("Move row from {} to {}", from_index, to_index);
+                        let _ = self
+                            .block_manager
+                            .move_row(row_rev.clone(), from_index, to_index)
+                            .await?;
+
+                        if let Some(row_changeset) = self.view_manager.move_row(row_rev, to_row_id.clone()).await {
+                            tracing::trace!("Receive row changeset after moving the row");
+                            match self.block_manager.update_row(row_changeset).await {
+                                Ok(_) => {}
+                                Err(e) => {
+                                    tracing::error!("Apply row changeset error:{:?}", e);
+                                }
+                            }
+                        }
+                    }
+                    (_, None) => tracing::warn!("Can not find the from row id: {}", from_row_id),
+                    (None, _) => tracing::warn!("Can not find the to row id: {}", to_row_id),
+                }
+            }
+        }
+        Ok(())
     }
 
     pub async fn move_field(&self, params: MoveFieldParams) -> FlowyResult<()> {

+ 62 - 54
frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs

@@ -1,19 +1,19 @@
 use crate::dart_notification::{send_dart_notification, GridNotification};
 use crate::entities::{
-    CreateRowParams, GridFilterConfiguration, GridLayout, GridSettingPB, GroupPB, GroupRowsChangesetPB, InsertedRowPB,
-    RowPB,
+    CreateRowParams, GridFilterConfiguration, GridSettingPB, GroupPB, GroupRowsChangesetPB, InsertedRowPB, RowPB,
 };
 use crate::services::grid_editor_task::GridServiceTaskScheduler;
 use crate::services::grid_view_manager::{GridViewFieldDelegate, GridViewRowDelegate};
 use crate::services::group::{default_group_configuration, GroupConfigurationDelegate, GroupService};
 use crate::services::setting::make_grid_setting;
 use flowy_error::{FlowyError, FlowyResult};
-use flowy_grid_data_model::revision::{FieldRevision, GroupConfigurationRevision, RowRevision};
+use flowy_grid_data_model::revision::{FieldRevision, GroupConfigurationRevision, RowChangeset, RowRevision};
 use flowy_revision::{RevisionCloudService, RevisionManager, RevisionObjectBuilder};
 use flowy_sync::client_grid::{GridViewRevisionChangeset, GridViewRevisionPad};
 use flowy_sync::entities::grid::GridSettingChangesetParams;
 use flowy_sync::entities::revision::Revision;
 use lib_infra::future::{wrap_future, AFFuture, FutureResult};
+use std::sync::atomic::{AtomicBool, Ordering};
 use std::sync::Arc;
 use tokio::sync::RwLock;
 
@@ -27,6 +27,7 @@ pub struct GridViewRevisionEditor {
     row_delegate: Arc<dyn GridViewRowDelegate>,
     group_service: Arc<RwLock<GroupService>>,
     scheduler: Arc<dyn GridServiceTaskScheduler>,
+    did_load_group: AtomicBool,
 }
 
 impl GridViewRevisionEditor {
@@ -47,6 +48,7 @@ impl GridViewRevisionEditor {
         let rev_manager = Arc::new(rev_manager);
         let group_service = GroupService::new(Box::new(pad.clone())).await;
         let user_id = user_id.to_owned();
+        let did_load_group = AtomicBool::new(false);
         Ok(Self {
             pad,
             user_id,
@@ -56,26 +58,22 @@ impl GridViewRevisionEditor {
             field_delegate,
             row_delegate,
             group_service: Arc::new(RwLock::new(group_service)),
+            did_load_group,
         })
     }
 
-    pub(crate) async fn fill_row(&self, row_rev: &mut RowRevision, params: &CreateRowParams) {
-        match params.layout {
-            GridLayout::Table => {
-                // Table can be grouped too
+    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
+                    .read()
+                    .await
+                    .will_create_row(row_rev, group_id, |field_id| {
+                        self.field_delegate.get_field_rev(&field_id)
+                    })
+                    .await;
             }
-            GridLayout::Board => match params.group_id.as_ref() {
-                None => {}
-                Some(group_id) => {
-                    self.group_service
-                        .read()
-                        .await
-                        .fill_row(row_rev, group_id, |field_id| {
-                            self.field_delegate.get_field_rev(&field_id)
-                        })
-                        .await;
-                }
-            },
         }
     }
 
@@ -94,12 +92,16 @@ impl GridViewRevisionEditor {
         }
     }
 
-    pub(crate) async fn did_delete_row(&self, row_id: &str) {
+    pub(crate) async fn did_delete_row(&self, row_rev: &RowRevision) {
         // Send the group notification if the current view has groups;
-        match self.group_id_of_row(row_id).await {
-            None => {}
-            Some(group_id) => {
-                let changeset = GroupRowsChangesetPB::delete(group_id, vec![row_id.to_owned()]);
+        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
+        {
+            for changeset in changesets {
                 self.notify_did_update_group(changeset).await;
             }
         }
@@ -119,44 +121,50 @@ impl GridViewRevisionEditor {
         }
     }
 
-    async fn group_id_of_row(&self, row_id: &str) -> Option<String> {
-        let read_guard = &self.group_service.read().await.groups;
-        for group in read_guard.iter() {
-            if group.contains_row(row_id) {
-                return Some(group.id.clone());
-            }
-        }
-        None
-    }
-
-    // async fn get_mut_group<F>(&self, group_id: &str, f: F) -> FlowyResult<()>
-    // where
-    //     F: Fn(&mut Group) -> FlowyResult<()>,
-    // {
-    //     for group in self.groups.write().await.iter_mut() {
-    //         if group.id == group_id {
-    //             let _ = f(group)?;
-    //         }
-    //     }
-    //     Ok(())
-    // }
-
-    pub(crate) async fn load_groups(&self) -> FlowyResult<Vec<GroupPB>> {
-        let field_revs = self.field_delegate.get_field_revs().await;
-        let row_revs = self.row_delegate.gv_row_revs().await;
-
-        match self
+    pub(crate) async fn did_move_row(
+        &self,
+        row_rev: &RowRevision,
+        row_changeset: &mut RowChangeset,
+        upper_row_id: &str,
+    ) {
+        if let Some(changesets) = self
             .group_service
             .write()
             .await
-            .load_groups(&field_revs, row_revs)
+            .did_move_row(row_rev, row_changeset, upper_row_id, |field_id| {
+                self.field_delegate.get_field_rev(&field_id)
+            })
             .await
         {
-            None => Ok(vec![]),
-            Some(groups) => Ok(groups.into_iter().map(GroupPB::from).collect()),
+            for changeset in changesets {
+                tracing::trace!("Group: {} changeset: {}", changeset.group_id, changeset);
+                self.notify_did_update_group(changeset).await;
+            }
         }
     }
 
+    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
+        };
+
+        Ok(groups.into_iter().map(GroupPB::from).collect())
+    }
+
     pub(crate) async fn get_setting(&self) -> GridSettingPB {
         let field_revs = self.field_delegate.get_field_revs().await;
         let grid_setting = make_grid_setting(self.pad.read().await.get_setting_rev(), &field_revs);

+ 28 - 54
frontend/rust-lib/flowy-grid/src/services/grid_view_manager.rs

@@ -1,17 +1,13 @@
-use crate::entities::{
-    CreateRowParams, GridFilterConfiguration, GridLayout, GridSettingPB, MoveRowParams, RepeatedGridGroupPB, RowPB,
-};
+use crate::entities::{CreateRowParams, GridFilterConfiguration, GridSettingPB, RepeatedGridGroupPB, RowPB};
 use crate::manager::GridUser;
-
 use crate::services::grid_editor_task::GridServiceTaskScheduler;
 use crate::services::grid_view_editor::GridViewRevisionEditor;
 use bytes::Bytes;
 use dashmap::DashMap;
 use flowy_error::FlowyResult;
-use flowy_grid_data_model::revision::{FieldRevision, RowRevision};
+use flowy_grid_data_model::revision::{FieldRevision, RowChangeset, RowRevision};
 use flowy_revision::disk::SQLiteGridViewRevisionPersistence;
 use flowy_revision::{RevisionCompactor, RevisionManager, RevisionPersistence, SQLiteRevisionSnapshotPersistence};
-
 use flowy_sync::entities::grid::GridSettingChangesetParams;
 use flowy_sync::entities::revision::Revision;
 use flowy_sync::util::make_text_delta_from_revisions;
@@ -31,17 +27,11 @@ pub trait GridViewRowDelegate: Send + Sync + 'static {
     fn gv_row_revs(&self) -> AFFuture<Vec<Arc<RowRevision>>>;
 }
 
-pub trait GridViewRowOperation: Send + Sync + 'static {
-    // Will be removed in the future.
-    fn gv_move_row(&self, row_rev: Arc<RowRevision>, from: usize, to: usize) -> AFFuture<FlowyResult<()>>;
-}
-
 pub(crate) struct GridViewManager {
     grid_id: String,
     user: Arc<dyn GridUser>,
     field_delegate: Arc<dyn GridViewFieldDelegate>,
     row_delegate: Arc<dyn GridViewRowDelegate>,
-    row_operation: Arc<dyn GridViewRowOperation>,
     view_editors: DashMap<ViewId, Arc<GridViewRevisionEditor>>,
     scheduler: Arc<dyn GridServiceTaskScheduler>,
 }
@@ -52,7 +42,6 @@ impl GridViewManager {
         user: Arc<dyn GridUser>,
         field_delegate: Arc<dyn GridViewFieldDelegate>,
         row_delegate: Arc<dyn GridViewRowDelegate>,
-        row_operation: Arc<dyn GridViewRowOperation>,
         scheduler: Arc<dyn GridServiceTaskScheduler>,
     ) -> FlowyResult<Self> {
         Ok(Self {
@@ -61,17 +50,25 @@ impl GridViewManager {
             scheduler,
             field_delegate,
             row_delegate,
-            row_operation,
             view_editors: DashMap::default(),
         })
     }
 
-    pub(crate) async fn fill_row(&self, row_rev: &mut RowRevision, params: &CreateRowParams) {
+    /// When the row was created, we may need to modify the [RowRevision] according to the [CreateRowParams].
+    pub(crate) async fn will_create_row(&self, row_rev: &mut RowRevision, params: &CreateRowParams) {
         for view_editor in self.view_editors.iter() {
-            view_editor.fill_row(row_rev, params).await;
+            view_editor.will_create_row(row_rev, params).await;
         }
     }
 
+    /// Notify the view that the row was created. For the moment, the view is just sending notifications.
+    pub(crate) async fn did_create_row(&self, row_pb: &RowPB, params: &CreateRowParams) {
+        for view_editor in self.view_editors.iter() {
+            view_editor.did_create_row(row_pb, params).await;
+        }
+    }
+
+    /// Insert/Delete the group's row if the corresponding data was changed.  
     pub(crate) async fn did_update_row(&self, row_id: &str) {
         match self.row_delegate.gv_get_row_rev(row_id).await {
             None => {
@@ -85,15 +82,9 @@ impl GridViewManager {
         }
     }
 
-    pub(crate) async fn did_create_row(&self, row_pb: &RowPB, params: &CreateRowParams) {
-        for view_editor in self.view_editors.iter() {
-            view_editor.did_create_row(row_pb, params).await;
-        }
-    }
-
-    pub(crate) async fn did_delete_row(&self, row_id: &str) {
+    pub(crate) async fn did_delete_row(&self, row_rev: Arc<RowRevision>) {
         for view_editor in self.view_editors.iter() {
-            view_editor.did_delete_row(row_id).await;
+            view_editor.did_delete_row(&row_rev).await;
         }
     }
 
@@ -119,37 +110,20 @@ impl GridViewManager {
         Ok(RepeatedGridGroupPB { items: groups })
     }
 
-    pub(crate) async fn move_row(&self, params: MoveRowParams) -> FlowyResult<()> {
-        let MoveRowParams {
-            view_id: _,
-            row_id,
-            from_index,
-            to_index,
-            layout,
-            upper_row_id,
-        } = params;
-
-        let from_index = from_index as usize;
-
-        match self.row_delegate.gv_get_row_rev(&row_id).await {
-            None => tracing::warn!("Move row failed, can not find the row:{}", row_id),
-            Some(row_rev) => match layout {
-                GridLayout::Table => {
-                    tracing::trace!("Move row from {} to {}", from_index, to_index);
-                    let to_index = to_index as usize;
-                    let _ = self.row_operation.gv_move_row(row_rev, from_index, to_index).await?;
-                }
-                GridLayout::Board => {
-                    if let Some(upper_row_id) = upper_row_id {
-                        if let Some(to_index) = self.row_delegate.gv_index_of_row(&upper_row_id).await {
-                            tracing::trace!("Move row from {} to {}", from_index, to_index);
-                            let _ = self.row_operation.gv_move_row(row_rev, from_index, to_index).await?;
-                        }
-                    }
-                }
-            },
+    /// It may generate a RowChangeset when the Row was moved from one group to another.
+    /// The return value, [RowChangeset], contains the changes made by the groups.
+    ///
+    pub(crate) async fn move_row(&self, row_rev: Arc<RowRevision>, to_row_id: String) -> Option<RowChangeset> {
+        let mut row_changeset = RowChangeset::new(row_rev.id.clone());
+        for view_editor in self.view_editors.iter() {
+            view_editor.did_move_row(&row_rev, &mut row_changeset, &to_row_id).await;
+        }
+
+        if row_changeset.has_changed() {
+            Some(row_changeset)
+        } else {
+            None
         }
-        Ok(())
     }
 
     pub(crate) async fn get_view_editor(&self, view_id: &str) -> FlowyResult<Arc<GridViewRevisionEditor>> {

+ 39 - 5
frontend/rust-lib/flowy-grid/src/services/group/group_generator/checkbox_group.rs

@@ -1,6 +1,6 @@
 use crate::entities::{CheckboxGroupConfigurationPB, GroupRowsChangesetPB};
 
-use flowy_grid_data_model::revision::{FieldRevision, RowRevision};
+use flowy_grid_data_model::revision::{FieldRevision, RowChangeset, RowRevision};
 
 use crate::services::field::{CheckboxCellData, CheckboxCellDataParser, CheckboxTypeOptionPB, CHECK, UNCHECK};
 use crate::services::group::{GenericGroupController, Group, GroupController, GroupGenerator, Groupable};
@@ -19,13 +19,36 @@ impl Groupable for CheckboxGroupController {
         false
     }
 
-    fn group_row(&mut self, _row_rev: &RowRevision, _cell_data: &Self::CellDataType) -> Vec<GroupRowsChangesetPB> {
+    fn add_row_if_match(
+        &mut self,
+        _row_rev: &RowRevision,
+        _cell_data: &Self::CellDataType,
+    ) -> Vec<GroupRowsChangesetPB> {
+        todo!()
+    }
+
+    fn remove_row_if_match(
+        &mut self,
+        _row_rev: &RowRevision,
+        _cell_data: &Self::CellDataType,
+    ) -> Vec<GroupRowsChangesetPB> {
+        todo!()
+    }
+
+    fn move_row_if_match(
+        &mut self,
+        _field_rev: &FieldRevision,
+        _row_rev: &RowRevision,
+        _row_changeset: &mut RowChangeset,
+        _cell_data: &Self::CellDataType,
+        _to_row_id: &str,
+    ) -> Vec<GroupRowsChangesetPB> {
         todo!()
     }
 }
 
 impl GroupController for CheckboxGroupController {
-    fn fill_row(&self, _row_rev: &mut RowRevision, _field_rev: &FieldRevision, _group_id: &str) {
+    fn will_create_row(&mut self, _row_rev: &mut RowRevision, _field_rev: &FieldRevision, _group_id: &str) {
         todo!()
     }
 }
@@ -36,11 +59,22 @@ impl GroupGenerator for CheckboxGroupGenerator {
     type TypeOptionType = CheckboxTypeOptionPB;
 
     fn generate_groups(
+        field_id: &str,
         _configuration: &Option<Self::ConfigurationType>,
         _type_option: &Option<Self::TypeOptionType>,
     ) -> Vec<Group> {
-        let check_group = Group::new("true".to_string(), "".to_string(), CHECK.to_string());
-        let uncheck_group = Group::new("false".to_string(), "".to_string(), UNCHECK.to_string());
+        let check_group = Group::new(
+            "true".to_string(),
+            field_id.to_owned(),
+            "".to_string(),
+            CHECK.to_string(),
+        );
+        let uncheck_group = Group::new(
+            "false".to_string(),
+            field_id.to_owned(),
+            "".to_string(),
+            UNCHECK.to_string(),
+        );
         vec![check_group, uncheck_group]
     }
 }

+ 87 - 51
frontend/rust-lib/flowy-grid/src/services/group/group_generator/generator.rs → frontend/rust-lib/flowy-grid/src/services/group/group_generator/group_controller.rs

@@ -3,7 +3,7 @@ use crate::services::cell::{decode_any_cell_data, CellBytesParser};
 use bytes::Bytes;
 use flowy_error::FlowyResult;
 use flowy_grid_data_model::revision::{
-    FieldRevision, GroupConfigurationRevision, RowRevision, TypeOptionDataDeserializer,
+    FieldRevision, GroupConfigurationRevision, RowChangeset, RowRevision, TypeOptionDataDeserializer,
 };
 use indexmap::IndexMap;
 use std::marker::PhantomData;
@@ -14,6 +14,7 @@ pub trait GroupGenerator {
     type TypeOptionType;
 
     fn generate_groups(
+        field_id: &str,
         configuration: &Option<Self::ConfigurationType>,
         type_option: &Option<Self::TypeOptionType>,
     ) -> Vec<Group>;
@@ -22,23 +23,51 @@ pub trait GroupGenerator {
 pub trait Groupable: Send + Sync {
     type CellDataType;
     fn can_group(&self, content: &str, cell_data: &Self::CellDataType) -> bool;
-    fn group_row(&mut self, row_rev: &RowRevision, cell_data: &Self::CellDataType) -> Vec<GroupRowsChangesetPB>;
+    fn add_row_if_match(&mut self, row_rev: &RowRevision, cell_data: &Self::CellDataType) -> Vec<GroupRowsChangesetPB>;
+    fn remove_row_if_match(
+        &mut self,
+        row_rev: &RowRevision,
+        cell_data: &Self::CellDataType,
+    ) -> Vec<GroupRowsChangesetPB>;
+
+    fn move_row_if_match(
+        &mut self,
+        field_rev: &FieldRevision,
+        row_rev: &RowRevision,
+        row_changeset: &mut RowChangeset,
+        cell_data: &Self::CellDataType,
+        to_row_id: &str,
+    ) -> Vec<GroupRowsChangesetPB>;
 }
 
 pub trait GroupController: GroupControllerSharedAction + Send + Sync {
-    fn fill_row(&self, row_rev: &mut RowRevision, field_rev: &FieldRevision, group_id: &str);
+    fn will_create_row(&mut self, row_rev: &mut RowRevision, field_rev: &FieldRevision, group_id: &str);
 }
 
 pub trait GroupControllerSharedAction: Send + Sync {
     // The field that is used for grouping the rows
     fn field_id(&self) -> &str;
-    fn build_groups(&self) -> Vec<Group>;
+    fn groups(&self) -> Vec<Group>;
     fn group_rows(&mut self, row_revs: &[Arc<RowRevision>], field_rev: &FieldRevision) -> FlowyResult<()>;
     fn did_update_row(
         &mut self,
         row_rev: &RowRevision,
         field_rev: &FieldRevision,
     ) -> FlowyResult<Vec<GroupRowsChangesetPB>>;
+
+    fn did_delete_row(
+        &mut self,
+        row_rev: &RowRevision,
+        field_rev: &FieldRevision,
+    ) -> FlowyResult<Vec<GroupRowsChangesetPB>>;
+
+    fn did_move_row(
+        &mut self,
+        row_rev: &RowRevision,
+        row_changeset: &mut RowChangeset,
+        field_rev: &FieldRevision,
+        to_row_id: &str,
+    ) -> FlowyResult<Vec<GroupRowsChangesetPB>>;
 }
 
 const DEFAULT_GROUP_ID: &str = "default_group";
@@ -60,6 +89,7 @@ pub struct GenericGroupController<C, T, G, P> {
 #[derive(Clone)]
 pub struct Group {
     pub id: String,
+    pub field_id: String,
     pub desc: String,
     rows: Vec<RowPB>,
     pub content: String,
@@ -76,9 +106,10 @@ impl std::convert::From<Group> for GroupPB {
 }
 
 impl Group {
-    pub fn new(id: String, desc: String, content: String) -> Self {
+    pub fn new(id: String, field_id: String, desc: String, content: String) -> Self {
         Self {
             id,
+            field_id,
             desc,
             rows: vec![],
             content,
@@ -106,6 +137,22 @@ impl Group {
             Some(_) => {}
         }
     }
+
+    pub fn insert_row(&mut self, index: usize, row_pb: RowPB) {
+        if index < self.rows.len() {
+            self.rows.insert(index, row_pb);
+        } else {
+            tracing::error!("Insert row index:{} beyond the bounds:{},", index, self.rows.len());
+        }
+    }
+
+    pub fn index_of_row(&self, row_id: &str) -> Option<usize> {
+        self.rows.iter().position(|row| row.id == row_id)
+    }
+
+    pub fn number_of_row(&self) -> usize {
+        self.rows.len()
+    }
 }
 
 impl<C, T, G, P> GenericGroupController<C, T, G, P>
@@ -121,10 +168,11 @@ where
         };
         let field_type_rev = field_rev.field_type_rev;
         let type_option = field_rev.get_type_option_entry::<T>(field_type_rev);
-        let groups = G::generate_groups(&configuration, &type_option);
+        let groups = G::generate_groups(&field_rev.id, &configuration, &type_option);
 
         let default_group = Group::new(
             DEFAULT_GROUP_ID.to_owned(),
+            field_rev.id.clone(),
             format!("No {}", field_rev.name),
             "".to_string(),
         );
@@ -150,7 +198,7 @@ where
         &self.field_id
     }
 
-    fn build_groups(&self) -> Vec<Group> {
+    fn groups(&self) -> Vec<Group> {
         let default_group = self.default_group.clone();
         let mut groups: Vec<Group> = self.groups_map.values().cloned().collect();
         if !default_group.rows.is_empty() {
@@ -203,55 +251,43 @@ where
         if let Some(cell_rev) = row_rev.cells.get(&self.field_id) {
             let cell_bytes = decode_any_cell_data(cell_rev.data.clone(), field_rev);
             let cell_data = cell_bytes.parser::<P>()?;
-            Ok(self.group_row(row_rev, &cell_data))
+            Ok(self.add_row_if_match(row_rev, &cell_data))
+        } else {
+            Ok(vec![])
+        }
+    }
+
+    fn did_delete_row(
+        &mut self,
+        row_rev: &RowRevision,
+        field_rev: &FieldRevision,
+    ) -> FlowyResult<Vec<GroupRowsChangesetPB>> {
+        if let Some(cell_rev) = row_rev.cells.get(&self.field_id) {
+            let cell_bytes = decode_any_cell_data(cell_rev.data.clone(), field_rev);
+            let cell_data = cell_bytes.parser::<P>()?;
+            Ok(self.remove_row_if_match(row_rev, &cell_data))
         } else {
             Ok(vec![])
         }
     }
-}
 
-// impl<C, T, G, P> GroupController<C, T, G, P>
-// where
-//     P: CellBytesParser,
-//     Self: Groupable<CellDataType = P::Object>,
-// {
-//     pub fn handle_rows(&mut self, rows: &[Arc<RowRevision>], field_rev: &FieldRevision) -> FlowyResult<()> {
-//         // The field_rev might be None if corresponding field_rev is deleted.
-//         if self.configuration.is_none() {
-//             return Ok(());
-//         }
-//
-//         for row in rows {
-//             if let Some(cell_rev) = row.cells.get(&self.field_id) {
-//                 let mut records: Vec<GroupRecord> = vec![];
-//                 let cell_bytes = decode_any_cell_data(cell_rev.data.clone(), field_rev);
-//                 let cell_data = cell_bytes.parser::<P>()?;
-//                 for group in self.groups_map.values() {
-//                     if self.can_group(&group.content, &cell_data) {
-//                         records.push(GroupRecord {
-//                             row: row.into(),
-//                             group_id: group.id.clone(),
-//                         });
-//                     }
-//                 }
-//
-//                 if records.is_empty() {
-//                     self.default_group.rows.push(row.into());
-//                 } else {
-//                     for record in records {
-//                         if let Some(group) = self.groups_map.get_mut(&record.group_id) {
-//                             group.rows.push(record.row);
-//                         }
-//                     }
-//                 }
-//             } else {
-//                 self.default_group.rows.push(row.into());
-//             }
-//         }
-//
-//         Ok(())
-//     }
-// }
+    fn did_move_row(
+        &mut self,
+        row_rev: &RowRevision,
+        row_changeset: &mut RowChangeset,
+        field_rev: &FieldRevision,
+        to_row_id: &str,
+    ) -> FlowyResult<Vec<GroupRowsChangesetPB>> {
+        if let Some(cell_rev) = row_rev.cells.get(&self.field_id) {
+            let cell_bytes = decode_any_cell_data(cell_rev.data.clone(), field_rev);
+            let cell_data = cell_bytes.parser::<P>()?;
+            tracing::trace!("Move row:{} to row:{}", row_rev.id, to_row_id);
+            Ok(self.move_row_if_match(field_rev, row_rev, row_changeset, &cell_data, to_row_id))
+        } else {
+            Ok(vec![])
+        }
+    }
+}
 
 struct GroupRecord {
     row: RowPB,

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

@@ -1,7 +1,7 @@
 mod checkbox_group;
-mod generator;
+mod group_controller;
 mod select_option_group;
 
 pub use checkbox_group::*;
-pub use generator::*;
+pub use group_controller::*;
 pub use select_option_group::*;

+ 159 - 21
frontend/rust-lib/flowy-grid/src/services/group/group_generator/select_option_group.rs

@@ -5,7 +5,7 @@ use crate::services::field::{
 };
 use crate::services::group::{GenericGroupController, Group, GroupController, GroupGenerator, Groupable};
 
-use flowy_grid_data_model::revision::{FieldRevision, RowRevision};
+use flowy_grid_data_model::revision::{FieldRevision, RowChangeset, RowRevision};
 
 // SingleSelect
 pub type SingleSelectGroupController = GenericGroupController<
@@ -21,23 +21,59 @@ impl Groupable for SingleSelectGroupController {
         cell_data.select_options.iter().any(|option| option.id == content)
     }
 
-    fn group_row(&mut self, row_rev: &RowRevision, cell_data: &Self::CellDataType) -> Vec<GroupRowsChangesetPB> {
+    fn add_row_if_match(&mut self, row_rev: &RowRevision, cell_data: &Self::CellDataType) -> Vec<GroupRowsChangesetPB> {
         let mut changesets = vec![];
         self.groups_map.iter_mut().for_each(|(_, group): (_, &mut Group)| {
-            group_select_option_row(group, &mut changesets, cell_data, row_rev);
+            add_row(group, &mut changesets, cell_data, row_rev);
         });
         changesets
     }
+
+    fn remove_row_if_match(
+        &mut self,
+        row_rev: &RowRevision,
+        cell_data: &Self::CellDataType,
+    ) -> Vec<GroupRowsChangesetPB> {
+        let mut changesets = vec![];
+        self.groups_map.iter_mut().for_each(|(_, group): (_, &mut Group)| {
+            remove_row(group, &mut changesets, cell_data, row_rev);
+        });
+        changesets
+    }
+
+    fn move_row_if_match(
+        &mut self,
+        field_rev: &FieldRevision,
+        row_rev: &RowRevision,
+        row_changeset: &mut RowChangeset,
+        cell_data: &Self::CellDataType,
+        to_row_id: &str,
+    ) -> Vec<GroupRowsChangesetPB> {
+        let mut group_changeset = vec![];
+        self.groups_map.iter_mut().for_each(|(_, group): (_, &mut Group)| {
+            move_row(
+                group,
+                &mut group_changeset,
+                field_rev,
+                row_rev,
+                row_changeset,
+                cell_data,
+                to_row_id,
+            );
+        });
+        group_changeset
+    }
 }
 
 impl GroupController for SingleSelectGroupController {
-    fn fill_row(&self, row_rev: &mut RowRevision, field_rev: &FieldRevision, group_id: &str) {
-        let group: Option<&Group> = self.groups_map.get(group_id);
+    fn will_create_row(&mut self, row_rev: &mut RowRevision, field_rev: &FieldRevision, group_id: &str) {
+        let group: Option<&mut Group> = self.groups_map.get_mut(group_id);
         match group {
             None => {}
             Some(group) => {
                 let cell_rev = insert_select_option_cell(group.id.clone(), field_rev);
                 row_rev.cells.insert(field_rev.id.clone(), cell_rev);
+                group.add_row(RowPB::from(row_rev));
             }
         }
     }
@@ -48,6 +84,7 @@ impl GroupGenerator for SingleSelectGroupGenerator {
     type ConfigurationType = SelectOptionGroupConfigurationPB;
     type TypeOptionType = SingleSelectTypeOptionPB;
     fn generate_groups(
+        field_id: &str,
         _configuration: &Option<Self::ConfigurationType>,
         type_option: &Option<Self::TypeOptionType>,
     ) -> Vec<Group> {
@@ -56,7 +93,14 @@ impl GroupGenerator for SingleSelectGroupGenerator {
             Some(type_option) => type_option
                 .options
                 .iter()
-                .map(|option| Group::new(option.id.clone(), option.name.clone(), option.id.clone()))
+                .map(|option| {
+                    Group::new(
+                        option.id.clone(),
+                        field_id.to_owned(),
+                        option.name.clone(),
+                        option.id.clone(),
+                    )
+                })
                 .collect(),
         }
     }
@@ -77,18 +121,52 @@ impl Groupable for MultiSelectGroupController {
         cell_data.select_options.iter().any(|option| option.id == content)
     }
 
-    fn group_row(&mut self, row_rev: &RowRevision, cell_data: &Self::CellDataType) -> Vec<GroupRowsChangesetPB> {
+    fn add_row_if_match(&mut self, row_rev: &RowRevision, cell_data: &Self::CellDataType) -> Vec<GroupRowsChangesetPB> {
         let mut changesets = vec![];
+        self.groups_map.iter_mut().for_each(|(_, group): (_, &mut Group)| {
+            add_row(group, &mut changesets, cell_data, row_rev);
+        });
+        changesets
+    }
 
+    fn remove_row_if_match(
+        &mut self,
+        row_rev: &RowRevision,
+        cell_data: &Self::CellDataType,
+    ) -> Vec<GroupRowsChangesetPB> {
+        let mut changesets = vec![];
         self.groups_map.iter_mut().for_each(|(_, group): (_, &mut Group)| {
-            group_select_option_row(group, &mut changesets, cell_data, row_rev);
+            remove_row(group, &mut changesets, cell_data, row_rev);
         });
         changesets
     }
+
+    fn move_row_if_match(
+        &mut self,
+        field_rev: &FieldRevision,
+        row_rev: &RowRevision,
+        row_changeset: &mut RowChangeset,
+        cell_data: &Self::CellDataType,
+        to_row_id: &str,
+    ) -> Vec<GroupRowsChangesetPB> {
+        let mut group_changeset = vec![];
+        self.groups_map.iter_mut().for_each(|(_, group): (_, &mut Group)| {
+            move_row(
+                group,
+                &mut group_changeset,
+                field_rev,
+                row_rev,
+                row_changeset,
+                cell_data,
+                to_row_id,
+            );
+        });
+        group_changeset
+    }
 }
 
 impl GroupController for MultiSelectGroupController {
-    fn fill_row(&self, row_rev: &mut RowRevision, field_rev: &FieldRevision, group_id: &str) {
+    fn will_create_row(&mut self, row_rev: &mut RowRevision, field_rev: &FieldRevision, group_id: &str) {
         let group: Option<&Group> = self.groups_map.get(group_id);
         match group {
             None => tracing::warn!("Can not find the group: {}", group_id),
@@ -106,6 +184,7 @@ impl GroupGenerator for MultiSelectGroupGenerator {
     type TypeOptionType = MultiSelectTypeOptionPB;
 
     fn generate_groups(
+        field_id: &str,
         _configuration: &Option<Self::ConfigurationType>,
         type_option: &Option<Self::TypeOptionType>,
     ) -> Vec<Group> {
@@ -114,31 +193,90 @@ impl GroupGenerator for MultiSelectGroupGenerator {
             Some(type_option) => type_option
                 .options
                 .iter()
-                .map(|option| Group::new(option.id.clone(), option.name.clone(), option.id.clone()))
+                .map(|option| {
+                    Group::new(
+                        option.id.clone(),
+                        field_id.to_owned(),
+                        option.name.clone(),
+                        option.id.clone(),
+                    )
+                })
                 .collect(),
         }
     }
 }
 
-fn group_select_option_row(
+fn add_row(
     group: &mut Group,
     changesets: &mut Vec<GroupRowsChangesetPB>,
     cell_data: &SelectOptionCellDataPB,
     row_rev: &RowRevision,
 ) {
     cell_data.select_options.iter().for_each(|option| {
-        if option.id == group.id {
-            if !group.contains_row(&row_rev.id) {
-                let row_pb = RowPB::from(row_rev);
-                changesets.push(GroupRowsChangesetPB::insert(
-                    group.id.clone(),
-                    vec![InsertedRowPB::new(row_pb.clone())],
-                ));
+        if option.id == group.id && !group.contains_row(&row_rev.id) {
+            let row_pb = RowPB::from(row_rev);
+            changesets.push(GroupRowsChangesetPB::insert(
+                group.id.clone(),
+                vec![InsertedRowPB::new(row_pb.clone())],
+            ));
+            group.add_row(row_pb);
+        }
+    });
+}
+
+fn remove_row(
+    group: &mut Group,
+    changesets: &mut Vec<GroupRowsChangesetPB>,
+    cell_data: &SelectOptionCellDataPB,
+    row_rev: &RowRevision,
+) {
+    cell_data.select_options.iter().for_each(|option| {
+        if option.id == group.id && group.contains_row(&row_rev.id) {
+            changesets.push(GroupRowsChangesetPB::delete(group.id.clone(), vec![row_rev.id.clone()]));
+            group.remove_row(&row_rev.id);
+        }
+    });
+}
+
+fn move_row(
+    group: &mut Group,
+    group_changeset: &mut Vec<GroupRowsChangesetPB>,
+    field_rev: &FieldRevision,
+    row_rev: &RowRevision,
+    row_changeset: &mut RowChangeset,
+    cell_data: &SelectOptionCellDataPB,
+    to_row_id: &str,
+) {
+    cell_data.select_options.iter().for_each(|option| {
+        // Remove the row in which group contains the row
+        let is_group_contains = group.contains_row(&row_rev.id);
+        let to_index = group.index_of_row(to_row_id);
+
+        if option.id == group.id && is_group_contains {
+            group_changeset.push(GroupRowsChangesetPB::delete(group.id.clone(), vec![row_rev.id.clone()]));
+            group.remove_row(&row_rev.id);
+        }
+
+        // Find the inserted group
+        if let Some(to_index) = to_index {
+            let row_pb = RowPB::from(row_rev);
+            let inserted_row = InsertedRowPB {
+                row: row_pb.clone(),
+                index: Some(to_index as i32),
+            };
+            group_changeset.push(GroupRowsChangesetPB::insert(group.id.clone(), vec![inserted_row]));
+            if group.number_of_row() == to_index {
                 group.add_row(row_pb);
+            } else {
+                group.insert_row(to_index, row_pb);
             }
-        } else if group.contains_row(&row_rev.id) {
-            group.remove_row(&row_rev.id);
-            changesets.push(GroupRowsChangesetPB::delete(group.id.clone(), vec![row_rev.id.clone()]));
+        }
+
+        // If the inserted row comes from other group, it needs to update the corresponding cell content.
+        if to_index.is_some() && option.id != group.id {
+            // Update the corresponding row's cell content.
+            let cell_rev = insert_select_option_cell(group.id.clone(), field_rev);
+            row_changeset.cell_by_field_id.insert(field_rev.id.clone(), cell_rev);
         }
     });
 }

+ 68 - 10
frontend/rust-lib/flowy-grid/src/services/group/group_service.rs

@@ -7,7 +7,9 @@ use crate::services::group::{
 };
 use bytes::Bytes;
 use flowy_error::FlowyResult;
-use flowy_grid_data_model::revision::{gen_grid_group_id, FieldRevision, GroupConfigurationRevision, RowRevision};
+use flowy_grid_data_model::revision::{
+    gen_grid_group_id, FieldRevision, GroupConfigurationRevision, RowChangeset, RowRevision,
+};
 use lib_infra::future::AFFuture;
 use std::future::Future;
 use std::sync::Arc;
@@ -18,7 +20,6 @@ pub trait GroupConfigurationDelegate: Send + Sync + 'static {
 }
 
 pub(crate) struct GroupService {
-    pub groups: Vec<Group>,
     delegate: Box<dyn GroupConfigurationDelegate>,
     group_controller: Option<Arc<RwLock<dyn GroupController>>>,
 }
@@ -26,12 +27,19 @@ pub(crate) struct GroupService {
 impl GroupService {
     pub(crate) async fn new(delegate: Box<dyn GroupConfigurationDelegate>) -> Self {
         Self {
-            groups: vec![],
             delegate,
             group_controller: None,
         }
     }
 
+    pub(crate) async fn groups(&self) -> Vec<Group> {
+        if let Some(group_action_handler) = self.group_controller.as_ref() {
+            group_action_handler.read().await.groups()
+        } else {
+            vec![]
+        }
+    }
+
     pub(crate) async fn load_groups(
         &mut self,
         field_revs: &[Arc<FieldRevision>],
@@ -44,15 +52,12 @@ impl GroupService {
             .build_groups(&field_type, &field_rev, row_revs, configuration)
             .await
         {
-            Ok(groups) => {
-                self.groups = groups.clone();
-                Some(groups)
-            }
+            Ok(groups) => Some(groups),
             Err(_) => None,
         }
     }
 
-    pub(crate) async fn fill_row<F, O>(&self, row_rev: &mut RowRevision, group_id: &str, get_field_fn: F)
+    pub(crate) async fn will_create_row<F, O>(&self, row_rev: &mut RowRevision, group_id: &str, get_field_fn: F)
     where
         F: FnOnce(String) -> O,
         O: Future<Output = Option<Arc<FieldRevision>>> + Send + Sync + 'static,
@@ -62,12 +67,65 @@ impl GroupService {
             match get_field_fn(field_id).await {
                 None => {}
                 Some(field_rev) => {
-                    group_controller.write().await.fill_row(row_rev, &field_rev, group_id);
+                    group_controller
+                        .write()
+                        .await
+                        .will_create_row(row_rev, &field_rev, group_id);
                 }
             }
         }
     }
 
+    pub(crate) async fn did_delete_row<F, O>(
+        &self,
+        row_rev: &RowRevision,
+        get_field_fn: F,
+    ) -> Option<Vec<GroupRowsChangesetPB>>
+    where
+        F: FnOnce(String) -> O,
+        O: Future<Output = Option<Arc<FieldRevision>>> + Send + Sync + 'static,
+    {
+        let group_controller = self.group_controller.as_ref()?;
+        let field_id = group_controller.read().await.field_id().to_owned();
+        let field_rev = get_field_fn(field_id).await?;
+
+        match group_controller.write().await.did_delete_row(row_rev, &field_rev) {
+            Ok(changesets) => Some(changesets),
+            Err(e) => {
+                tracing::error!("Delete group data failed, {:?}", e);
+                None
+            }
+        }
+    }
+
+    pub(crate) async fn did_move_row<F, O>(
+        &self,
+        row_rev: &RowRevision,
+        row_changeset: &mut RowChangeset,
+        upper_row_id: &str,
+        get_field_fn: F,
+    ) -> Option<Vec<GroupRowsChangesetPB>>
+    where
+        F: FnOnce(String) -> O,
+        O: Future<Output = Option<Arc<FieldRevision>>> + Send + Sync + 'static,
+    {
+        let group_controller = self.group_controller.as_ref()?;
+        let field_id = group_controller.read().await.field_id().to_owned();
+        let field_rev = get_field_fn(field_id).await?;
+
+        match group_controller
+            .write()
+            .await
+            .did_move_row(row_rev, row_changeset, &field_rev, upper_row_id)
+        {
+            Ok(changesets) => Some(changesets),
+            Err(e) => {
+                tracing::error!("Move group data failed, {:?}", e);
+                None
+            }
+        }
+    }
+
     #[tracing::instrument(level = "trace", skip_all)]
     pub(crate) async fn did_update_row<F, O>(
         &self,
@@ -130,7 +188,7 @@ impl GroupService {
         if let Some(group_action_handler) = self.group_controller.as_ref() {
             let mut write_guard = group_action_handler.write().await;
             let _ = write_guard.group_rows(&row_revs, field_rev)?;
-            groups = write_guard.build_groups();
+            groups = write_guard.groups();
             drop(write_guard);
         }
 

+ 2 - 2
frontend/rust-lib/flowy-grid/tests/grid/block_test/row_test.rs

@@ -3,7 +3,7 @@ use crate::grid::block_test::script::{CreateRowScriptBuilder, GridRowTest};
 use crate::grid::grid_editor::{COMPLETED, FACEBOOK, GOOGLE, PAUSED, TWITTER};
 use flowy_grid::entities::FieldType;
 use flowy_grid::services::field::{SELECTION_IDS_SEPARATOR, UNCHECK};
-use flowy_grid_data_model::revision::RowMetaChangeset;
+use flowy_grid_data_model::revision::RowChangeset;
 
 #[tokio::test]
 async fn grid_create_row_count_test() {
@@ -24,7 +24,7 @@ async fn grid_create_row_count_test() {
 async fn grid_update_row() {
     let mut test = GridRowTest::new().await;
     let row_rev = test.row_builder().build();
-    let changeset = RowMetaChangeset {
+    let changeset = RowChangeset {
         row_id: row_rev.id.clone(),
         height: None,
         visibility: None,

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

@@ -5,7 +5,7 @@ use crate::grid::grid_editor::GridEditorTest;
 use flowy_grid::entities::{CreateRowParams, FieldType, GridCellIdParams, GridLayout, RowPB};
 use flowy_grid::services::field::*;
 use flowy_grid_data_model::revision::{
-    GridBlockMetaRevision, GridBlockMetaRevisionChangeset, RowMetaChangeset, RowRevision,
+    GridBlockMetaRevision, GridBlockMetaRevisionChangeset, RowChangeset, RowRevision,
 };
 use std::collections::HashMap;
 use std::sync::Arc;
@@ -17,7 +17,7 @@ pub enum RowScript {
         row_rev: RowRevision,
     },
     UpdateRow {
-        changeset: RowMetaChangeset,
+        changeset: RowChangeset,
     },
     AssertRow {
         expected_row: RowRevision,
@@ -56,7 +56,7 @@ pub struct GridRowTest {
 
 impl GridRowTest {
     pub async fn new() -> Self {
-        let editor_test = GridEditorTest::new().await;
+        let editor_test = GridEditorTest::new_table().await;
         Self { inner: editor_test }
     }
 

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

@@ -11,7 +11,7 @@ pub struct GridCellTest {
 
 impl GridCellTest {
     pub async fn new() -> Self {
-        let inner = GridEditorTest::new().await;
+        let inner = GridEditorTest::new_table().await;
         Self { inner }
     }
 

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

@@ -26,7 +26,7 @@ pub struct GridFieldTest {
 
 impl GridFieldTest {
     pub async fn new() -> Self {
-        let editor_test = GridEditorTest::new().await;
+        let editor_test = GridEditorTest::new_table().await;
         Self { inner: editor_test }
     }
 

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

@@ -36,7 +36,7 @@ pub struct GridFilterTest {
 
 impl GridFilterTest {
     pub async fn new() -> Self {
-     let editor_test =  GridEditorTest::new().await;
+        let editor_test =  GridEditorTest::new_table().await;
         Self {
             inner: editor_test
         }

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

@@ -36,12 +36,25 @@ pub struct GridEditorTest {
 }
 
 impl GridEditorTest {
-    pub async fn new() -> Self {
+    pub async fn new_table() -> Self {
+        Self::new(GridLayout::Table).await
+    }
+
+    pub async fn new_board() -> Self {
+        Self::new(GridLayout::Board).await
+    }
+
+    pub async fn new(layout: GridLayout) -> Self {
         let sdk = FlowySDKTest::default();
         let _ = sdk.init_user().await;
         let build_context = make_test_grid();
         let view_data: Bytes = build_context.into();
-        let test = ViewTest::new_grid_view(&sdk, view_data.to_vec()).await;
+
+        let test = match layout {
+            GridLayout::Table => ViewTest::new_grid_view(&sdk, view_data.to_vec()).await,
+            GridLayout::Board => ViewTest::new_board_view(&sdk, view_data.to_vec()).await,
+        };
+
         let editor = sdk.grid_manager.open_grid(&test.view.id).await.unwrap();
         let field_revs = editor.get_field_revs(None).await.unwrap();
         let block_meta_revs = editor.get_block_meta_revs().await.unwrap();

+ 2 - 0
frontend/rust-lib/flowy-grid/tests/grid/group_test/mod.rs

@@ -0,0 +1,2 @@
+mod script;
+mod test;

+ 97 - 0
frontend/rust-lib/flowy-grid/tests/grid/group_test/script.rs

@@ -0,0 +1,97 @@
+use crate::grid::grid_editor::GridEditorTest;
+use flowy_grid::entities::{GroupPB, MoveRowParams, RowPB};
+
+pub enum GroupScript {
+    AssertGroup {
+        group_index: usize,
+        row_count: usize,
+    },
+    AssertGroupCount(usize),
+    AssertGroupRow {
+        group_index: usize,
+        row_index: usize,
+        row: RowPB,
+    },
+    MoveRow {
+        from_group_index: usize,
+        from_row_index: usize,
+        to_group_index: usize,
+        to_row_index: usize,
+    },
+}
+
+pub struct GridGroupTest {
+    inner: GridEditorTest,
+}
+
+impl GridGroupTest {
+    pub async fn new() -> Self {
+        let editor_test = GridEditorTest::new_board().await;
+        Self { inner: editor_test }
+    }
+
+    pub async fn run_scripts(&mut self, scripts: Vec<GroupScript>) {
+        for script in scripts {
+            self.run_script(script).await;
+        }
+    }
+
+    pub async fn run_script(&mut self, script: GroupScript) {
+        match script {
+            GroupScript::AssertGroup { group_index, row_count } => {
+                assert_eq!(row_count, self.group_at_index(group_index).await.rows.len());
+            }
+            GroupScript::AssertGroupCount(count) => {
+                let groups = self.editor.load_groups().await.unwrap();
+                assert_eq!(count, groups.len());
+            }
+            GroupScript::MoveRow {
+                from_group_index,
+                from_row_index,
+                to_group_index,
+                to_row_index,
+            } => {
+                let groups: Vec<GroupPB> = self.editor.load_groups().await.unwrap().items;
+                let from_row = groups.get(from_group_index).unwrap().rows.get(from_row_index).unwrap();
+                let to_row = groups.get(to_group_index).unwrap().rows.get(to_row_index).unwrap();
+                let params = MoveRowParams {
+                    view_id: self.inner.grid_id.clone(),
+                    from_row_id: from_row.id.clone(),
+                    to_row_id: to_row.id.clone(),
+                };
+
+                self.editor.move_row(params).await.unwrap();
+            }
+            GroupScript::AssertGroupRow {
+                group_index,
+                row_index,
+                row,
+            } => {
+                //
+                let group = self.group_at_index(group_index).await;
+                let compare_row = group.rows.get(row_index).unwrap().clone();
+
+                assert_eq!(row.id, compare_row.id);
+            }
+        }
+    }
+
+    pub async fn group_at_index(&self, index: usize) -> GroupPB {
+        let groups = self.editor.load_groups().await.unwrap().items;
+        groups.get(index).unwrap().clone()
+    }
+}
+
+impl std::ops::Deref for GridGroupTest {
+    type Target = GridEditorTest;
+
+    fn deref(&self) -> &Self::Target {
+        &self.inner
+    }
+}
+
+impl std::ops::DerefMut for GridGroupTest {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        &mut self.inner
+    }
+}

+ 101 - 0
frontend/rust-lib/flowy-grid/tests/grid/group_test/test.rs

@@ -0,0 +1,101 @@
+use crate::grid::group_test::script::GridGroupTest;
+use crate::grid::group_test::script::GroupScript::*;
+
+#[tokio::test]
+async fn board_init_test() {
+    let mut test = GridGroupTest::new().await;
+    let scripts = vec![
+        AssertGroupCount(3),
+        AssertGroup {
+            group_index: 0,
+            row_count: 2,
+        },
+        AssertGroup {
+            group_index: 1,
+            row_count: 2,
+        },
+        AssertGroup {
+            group_index: 2,
+            row_count: 1,
+        },
+    ];
+    test.run_scripts(scripts).await;
+}
+
+#[tokio::test]
+async fn board_move_row_test() {
+    let mut test = GridGroupTest::new().await;
+    let group = test.group_at_index(0).await;
+    let scripts = vec![
+        MoveRow {
+            from_group_index: 0,
+            from_row_index: 0,
+            to_group_index: 0,
+            to_row_index: 1,
+        },
+        AssertGroup {
+            group_index: 0,
+            row_count: 2,
+        },
+        AssertGroupRow {
+            group_index: 0,
+            row_index: 1,
+            row: group.rows.get(0).unwrap().clone(),
+        },
+    ];
+    test.run_scripts(scripts).await;
+}
+
+#[tokio::test]
+async fn board_move_row_to_other_group_test() {
+    let mut test = GridGroupTest::new().await;
+    let group = test.group_at_index(0).await;
+    let scripts = vec![
+        MoveRow {
+            from_group_index: 0,
+            from_row_index: 0,
+            to_group_index: 1,
+            to_row_index: 1,
+        },
+        AssertGroup {
+            group_index: 0,
+            row_count: 1,
+        },
+        AssertGroup {
+            group_index: 1,
+            row_count: 3,
+        },
+        AssertGroupRow {
+            group_index: 1,
+            row_index: 1,
+            row: group.rows.get(0).unwrap().clone(),
+        },
+    ];
+    test.run_scripts(scripts).await;
+}
+
+#[tokio::test]
+async fn board_move_row_to_other_group_and_reorder_test() {
+    let mut test = GridGroupTest::new().await;
+    let group = test.group_at_index(0).await;
+    let scripts = vec![
+        MoveRow {
+            from_group_index: 0,
+            from_row_index: 0,
+            to_group_index: 1,
+            to_row_index: 1,
+        },
+        MoveRow {
+            from_group_index: 1,
+            from_row_index: 1,
+            to_group_index: 1,
+            to_row_index: 2,
+        },
+        AssertGroupRow {
+            group_index: 1,
+            row_index: 2,
+            row: group.rows.get(0).unwrap().clone(),
+        },
+    ];
+    test.run_scripts(scripts).await;
+}

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

@@ -3,3 +3,4 @@ mod cell_test;
 mod field_test;
 mod filter_test;
 mod grid_editor;
+mod group_test;

+ 2 - 2
frontend/rust-lib/flowy-net/src/local_server/server.rs

@@ -305,7 +305,7 @@ impl FolderCouldServiceV1 for LocalServer {
         let time = timestamp();
         let view = ViewRevision {
             id: params.view_id,
-            belong_to_id: params.belong_to_id,
+            app_id: params.belong_to_id,
             name: params.name,
             desc: params.desc,
             data_type: params.data_type.into(),
@@ -315,7 +315,7 @@ impl FolderCouldServiceV1 for LocalServer {
             create_time: time,
             ext_data: "".to_string(),
             thumbnail: params.thumbnail,
-            plugin_type: params.plugin_type,
+            layout: params.layout.into(),
         };
         FutureResult::new(async { Ok(view) })
     }

+ 1 - 3
frontend/rust-lib/flowy-revision/src/rev_persistence.rs

@@ -320,9 +320,7 @@ impl RevisionSyncSequence {
     fn compact(&self) -> Option<(RevisionRange, VecDeque<i64>)> {
         // Make sure there are two rev_id going to sync. No need to compact if there is only
         // one rev_id in queue.
-        if self.next_rev_id().is_none() {
-            return None;
-        }
+        self.next_rev_id()?;
 
         let mut new_seq = self.0.clone();
         let mut drained = new_seq.drain(1..).collect::<VecDeque<_>>();

+ 13 - 8
frontend/rust-lib/flowy-sdk/src/deps_resolve/folder_deps.rs

@@ -1,6 +1,6 @@
 use bytes::Bytes;
 use flowy_database::ConnectionPool;
-use flowy_folder::entities::{SubViewDataTypePB, ViewDataTypePB};
+use flowy_folder::entities::{ViewDataTypePB, ViewLayoutTypePB};
 use flowy_folder::manager::{ViewDataProcessor, ViewDataProcessorMap};
 use flowy_folder::{
     errors::{internal_error, FlowyError},
@@ -184,8 +184,9 @@ impl ViewDataProcessor for TextBlockViewDataProcessor {
         &self,
         user_id: &str,
         view_id: &str,
-        _sub_data_type: Option<SubViewDataTypePB>,
+        layout: ViewLayoutTypePB,
     ) -> FutureResult<Bytes, FlowyError> {
+        debug_assert_eq!(layout, ViewLayoutTypePB::Document);
         let user_id = user_id.to_string();
         let view_id = view_id.to_string();
         let manager = self.0.clone();
@@ -209,7 +210,7 @@ impl ViewDataProcessor for TextBlockViewDataProcessor {
     }
 
     fn data_type(&self) -> ViewDataTypePB {
-        ViewDataTypePB::TextBlock
+        ViewDataTypePB::Text
     }
 }
 
@@ -261,16 +262,20 @@ impl ViewDataProcessor for GridViewDataProcessor {
         &self,
         user_id: &str,
         view_id: &str,
-        sub_data_type: Option<SubViewDataTypePB>,
+        layout: ViewLayoutTypePB,
     ) -> FutureResult<Bytes, FlowyError> {
-        let build_context = match sub_data_type.unwrap() {
-            SubViewDataTypePB::Grid => make_default_grid(),
-            SubViewDataTypePB::Board => make_default_board(),
+        let build_context = match layout {
+            ViewLayoutTypePB::Grid => make_default_grid(),
+            ViewLayoutTypePB::Board => make_default_board(),
+            ViewLayoutTypePB::Document => {
+                return FutureResult::new(async move {
+                    Err(FlowyError::internal().context(format!("Can't handle {:?} layout type", layout)))
+                });
+            }
         };
         let user_id = user_id.to_string();
         let view_id = view_id.to_string();
         let grid_manager = self.0.clone();
-
         FutureResult::new(async move { make_grid_view_data(&user_id, &view_id, grid_manager, build_context).await })
     }
 

+ 1 - 1
frontend/rust-lib/flowy-sdk/src/lib.rs

@@ -74,7 +74,7 @@ fn crate_log_filter(level: String) -> String {
     filters.push(format!("lib_ot={}", level));
     filters.push(format!("lib_ws={}", level));
     filters.push(format!("lib_infra={}", level));
-    filters.push(format!("flowy_sync={}", level));
+    // filters.push(format!("flowy_sync={}", level));
     // filters.push(format!("flowy_revision={}", level));
     // filters.push(format!("lib_dispatch={}", level));
 

+ 17 - 8
frontend/rust-lib/flowy-test/src/helper.rs

@@ -25,11 +25,11 @@ pub struct ViewTest {
 
 impl ViewTest {
     #[allow(dead_code)]
-    pub async fn new(sdk: &FlowySDKTest, data_type: ViewDataTypePB, data: Vec<u8>) -> Self {
+    pub async fn new(sdk: &FlowySDKTest, data_type: ViewDataTypePB, layout: ViewLayoutTypePB, data: Vec<u8>) -> Self {
         let workspace = create_workspace(sdk, "Workspace", "").await;
         open_workspace(sdk, &workspace.id).await;
         let app = create_app(sdk, "App", "AppFlowy GitHub Project", &workspace.id).await;
-        let view = create_view(sdk, &app.id, data_type, data).await;
+        let view = create_view(sdk, &app.id, data_type, layout, data).await;
         Self {
             sdk: sdk.clone(),
             workspace,
@@ -39,11 +39,15 @@ impl ViewTest {
     }
 
     pub async fn new_grid_view(sdk: &FlowySDKTest, data: Vec<u8>) -> Self {
-        Self::new(sdk, ViewDataTypePB::Database, data).await
+        Self::new(sdk, ViewDataTypePB::Database, ViewLayoutTypePB::Grid, data).await
+    }
+
+    pub async fn new_board_view(sdk: &FlowySDKTest, data: Vec<u8>) -> Self {
+        Self::new(sdk, ViewDataTypePB::Database, ViewLayoutTypePB::Board, data).await
     }
 
     pub async fn new_text_block_view(sdk: &FlowySDKTest) -> Self {
-        Self::new(sdk, ViewDataTypePB::TextBlock, vec![]).await
+        Self::new(sdk, ViewDataTypePB::Text, ViewLayoutTypePB::Document, vec![]).await
     }
 }
 
@@ -90,16 +94,21 @@ async fn create_app(sdk: &FlowySDKTest, name: &str, desc: &str, workspace_id: &s
     app
 }
 
-async fn create_view(sdk: &FlowySDKTest, app_id: &str, data_type: ViewDataTypePB, data: Vec<u8>) -> ViewPB {
+async fn create_view(
+    sdk: &FlowySDKTest,
+    app_id: &str,
+    data_type: ViewDataTypePB,
+    layout: ViewLayoutTypePB,
+    data: Vec<u8>,
+) -> ViewPB {
     let request = CreateViewPayloadPB {
         belong_to_id: app_id.to_string(),
         name: "View A".to_string(),
         desc: "".to_string(),
         thumbnail: Some("http://1.png".to_string()),
         data_type,
-        sub_data_type: None,
-        plugin_type: 0,
-        data,
+        layout,
+        view_content_data: data,
     };
 
     let view = FolderEventBuilder::new(sdk.clone())

+ 23 - 5
shared-lib/flowy-folder-data-model/src/revision/view_rev.rs

@@ -9,7 +9,9 @@ pub fn gen_view_id() -> String {
 pub struct ViewRevision {
     pub id: String,
 
-    pub belong_to_id: String,
+    // Maybe app_id or vi
+    #[serde(rename = "belong_to_id")]
+    pub app_id: String,
 
     pub name: String,
 
@@ -33,9 +35,10 @@ pub struct ViewRevision {
     pub thumbnail: String,
 
     #[serde(default = "DEFAULT_PLUGIN_TYPE")]
-    pub plugin_type: i32,
+    #[serde(rename = "plugin_type")]
+    pub layout: ViewLayoutTypeRevision,
 }
-const DEFAULT_PLUGIN_TYPE: fn() -> i32 = || 0;
+const DEFAULT_PLUGIN_TYPE: fn() -> ViewLayoutTypeRevision = || ViewLayoutTypeRevision::Document;
 
 impl std::convert::From<ViewRevision> for TrashRevision {
     fn from(view_rev: ViewRevision) -> Self {
@@ -52,12 +55,27 @@ impl std::convert::From<ViewRevision> for TrashRevision {
 #[derive(Eq, PartialEq, Debug, Clone, Serialize_repr, Deserialize_repr)]
 #[repr(u8)]
 pub enum ViewDataTypeRevision {
-    TextBlock = 0,
+    Text = 0,
     Database = 1,
 }
 
 impl std::default::Default for ViewDataTypeRevision {
     fn default() -> Self {
-        ViewDataTypeRevision::TextBlock
+        ViewDataTypeRevision::Text
+    }
+}
+
+#[derive(Eq, PartialEq, Debug, Clone, Serialize_repr, Deserialize_repr)]
+#[repr(u8)]
+pub enum ViewLayoutTypeRevision {
+    Document = 0,
+    // The for historical reasons, the value of Grid is not 1.
+    Grid = 3,
+    Board = 4,
+}
+
+impl std::default::Default for ViewLayoutTypeRevision {
+    fn default() -> Self {
+        ViewLayoutTypeRevision::Document
     }
 }

+ 5 - 4
shared-lib/flowy-folder-data-model/src/user_default.rs

@@ -1,5 +1,6 @@
 use crate::revision::{
-    gen_app_id, gen_view_id, gen_workspace_id, AppRevision, ViewDataTypeRevision, ViewRevision, WorkspaceRevision,
+    gen_app_id, gen_view_id, gen_workspace_id, AppRevision, ViewDataTypeRevision, ViewLayoutTypeRevision, ViewRevision,
+    WorkspaceRevision,
 };
 use chrono::Utc;
 
@@ -46,16 +47,16 @@ fn create_default_view(app_id: String, time: chrono::DateTime<Utc>) -> ViewRevis
 
     ViewRevision {
         id: view_id,
-        belong_to_id: app_id,
+        app_id,
         name,
         desc: "".to_string(),
-        data_type: ViewDataTypeRevision::TextBlock,
+        data_type: ViewDataTypeRevision::Text,
         version: 0,
         belongings: vec![],
         modified_time: time.timestamp(),
         create_time: time.timestamp(),
         ext_data: "".to_string(),
         thumbnail: "".to_string(),
-        plugin_type: 0,
+        layout: ViewLayoutTypeRevision::Document,
     }
 }

+ 16 - 1
shared-lib/flowy-grid-data-model/src/revision/grid_block.rs

@@ -42,13 +42,28 @@ impl RowRevision {
     }
 }
 #[derive(Debug, Clone, Default)]
-pub struct RowMetaChangeset {
+pub struct RowChangeset {
     pub row_id: String,
     pub height: Option<i32>,
     pub visibility: Option<bool>,
     pub cell_by_field_id: HashMap<FieldId, CellRevision>,
 }
 
+impl RowChangeset {
+    pub fn new(row_id: String) -> Self {
+        Self {
+            row_id,
+            height: None,
+            visibility: None,
+            cell_by_field_id: Default::default(),
+        }
+    }
+
+    pub fn has_changed(&self) -> bool {
+        self.height.is_some() || self.visibility.is_some() || !self.cell_by_field_id.is_empty()
+    }
+}
+
 #[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
 pub struct CellRevision {
     pub data: String,

+ 5 - 5
shared-lib/flowy-sync/src/client_folder/folder_pad.rs

@@ -202,7 +202,7 @@ impl FolderPad {
 
     #[tracing::instrument(level = "trace", skip(self), fields(view_name=%view_rev.name), err)]
     pub fn create_view(&mut self, view_rev: ViewRevision) -> CollaborateResult<Option<FolderChangeset>> {
-        let app_id = view_rev.belong_to_id.clone();
+        let app_id = view_rev.app_id.clone();
         self.with_app(&app_id, move |app| {
             if app.belongings.contains(&view_rev) {
                 tracing::warn!("[RootFolder]: Duplicate view");
@@ -243,7 +243,7 @@ impl FolderPad {
         modified_time: i64,
     ) -> CollaborateResult<Option<FolderChangeset>> {
         let view = self.read_view(view_id)?;
-        self.with_view(&view.belong_to_id, view_id, |view| {
+        self.with_view(&view.app_id, view_id, |view| {
             if let Some(name) = name {
                 view.name = name;
             }
@@ -260,7 +260,7 @@ impl FolderPad {
     #[tracing::instrument(level = "trace", skip(self), err)]
     pub fn delete_view(&mut self, view_id: &str) -> CollaborateResult<Option<FolderChangeset>> {
         let view = self.read_view(view_id)?;
-        self.with_app(&view.belong_to_id, |app| {
+        self.with_app(&view.app_id, |app| {
             app.belongings.retain(|view| view.id != view_id);
             Ok(Some(()))
         })
@@ -269,7 +269,7 @@ impl FolderPad {
     #[tracing::instrument(level = "trace", skip(self), err)]
     pub fn move_view(&mut self, view_id: &str, from: usize, to: usize) -> CollaborateResult<Option<FolderChangeset>> {
         let view = self.read_view(view_id)?;
-        self.with_app(&view.belong_to_id, |app| {
+        self.with_app(&view.app_id, |app| {
             match move_vec_element(&mut app.belongings, |view| view.id == view_id, from, to).map_err(internal_error)? {
                 true => Ok(Some(())),
                 false => Ok(None),
@@ -807,7 +807,7 @@ mod tests {
     fn test_view_folder() -> (FolderPad, FolderDelta, ViewRevision) {
         let (mut folder, mut initial_delta, app) = test_app_folder();
         let mut view_rev = ViewRevision::default();
-        view_rev.belong_to_id = app.id.clone();
+        view_rev.app_id = app.id.clone();
         view_rev.name = "🎃 my first view".to_owned();
 
         initial_delta = initial_delta

+ 10 - 10
shared-lib/flowy-sync/src/client_grid/block_revision_pad.rs

@@ -2,7 +2,7 @@ use crate::entities::revision::{md5, RepeatedRevision, Revision};
 use crate::errors::{CollaborateError, CollaborateResult};
 use crate::util::{cal_diff, make_text_delta_from_revisions};
 use flowy_grid_data_model::revision::{
-    gen_block_id, gen_row_id, CellRevision, GridBlockRevision, RowMetaChangeset, RowRevision,
+    gen_block_id, gen_row_id, CellRevision, GridBlockRevision, RowChangeset, RowRevision,
 };
 use lib_ot::core::{OperationTransform, PhantomAttributes, TextDelta, TextDeltaBuilder};
 use std::borrow::Cow;
@@ -143,7 +143,7 @@ impl GridBlockRevisionPad {
         self.block.rows.iter().position(|row| row.id == row_id)
     }
 
-    pub fn update_row(&mut self, changeset: RowMetaChangeset) -> CollaborateResult<Option<GridBlockRevisionChangeset>> {
+    pub fn update_row(&mut self, changeset: RowChangeset) -> CollaborateResult<Option<GridBlockRevisionChangeset>> {
         let row_id = changeset.row_id.clone();
         self.modify_row(&row_id, |row| {
             let mut is_changed = None;
@@ -178,8 +178,12 @@ impl GridBlockRevisionPad {
             if let Some(position) = row_revs.iter().position(|row_rev| row_rev.id == row_id) {
                 debug_assert_eq!(from, position);
                 let row_rev = row_revs.remove(position);
-                row_revs.insert(to, row_rev);
-                Ok(Some(()))
+                if to > row_revs.len() {
+                    Err(CollaborateError::out_of_bound())
+                } else {
+                    row_revs.insert(to, row_rev);
+                    Ok(Some(()))
+                }
             } else {
                 Ok(None)
             }
@@ -200,10 +204,6 @@ impl GridBlockRevisionPad {
                     None => Ok(None),
                     Some(delta) => {
                         tracing::trace!("[GridBlockRevision] Composing delta {}", delta.json_str());
-                        // tracing::debug!(
-                        //     "[GridBlockMeta] current delta: {}",
-                        //     self.delta.to_str().unwrap_or_else(|_| "".to_string())
-                        // );
                         self.delta = self.delta.compose(&delta)?;
                         Ok(Some(GridBlockRevisionChangeset {
                             delta,
@@ -275,7 +275,7 @@ impl std::default::Default for GridBlockRevisionPad {
 #[cfg(test)]
 mod tests {
     use crate::client_grid::GridBlockRevisionPad;
-    use flowy_grid_data_model::revision::{RowMetaChangeset, RowRevision};
+    use flowy_grid_data_model::revision::{RowChangeset, RowRevision};
     use lib_ot::core::TextDelta;
     use std::borrow::Cow;
 
@@ -400,7 +400,7 @@ mod tests {
             visibility: false,
         };
 
-        let changeset = RowMetaChangeset {
+        let changeset = RowChangeset {
             row_id: row.id.clone(),
             height: Some(100),
             visibility: Some(true),