瀏覽代碼

Merge pull request #846 from AppFlowy-IO/feat/board_create_card

Feat/board create card
Nathan.fooo 2 年之前
父節點
當前提交
2623974def
共有 38 個文件被更改,包括 748 次插入394 次删除
  1. 20 4
      frontend/app_flowy/lib/plugins/board/application/board_bloc.dart
  2. 22 33
      frontend/app_flowy/lib/plugins/board/application/board_data_controller.dart
  3. 2 2
      frontend/app_flowy/lib/plugins/board/application/card/card_bloc.dart
  4. 1 1
      frontend/app_flowy/lib/plugins/board/application/card/card_data_controller.dart
  5. 17 7
      frontend/app_flowy/lib/plugins/board/presentation/board_page.dart
  6. 6 1
      frontend/app_flowy/lib/plugins/board/presentation/card/card.dart
  7. 5 1
      frontend/app_flowy/lib/plugins/board/presentation/card/card_container.dart
  8. 2 2
      frontend/app_flowy/lib/plugins/grid/application/block/block_cache.dart
  9. 2 2
      frontend/app_flowy/lib/plugins/grid/application/grid_bloc.dart
  10. 4 4
      frontend/app_flowy/lib/plugins/grid/application/grid_data_controller.dart
  11. 10 2
      frontend/app_flowy/lib/plugins/grid/application/grid_service.dart
  12. 5 5
      frontend/app_flowy/lib/plugins/grid/application/row/row_action_sheet_bloc.dart
  13. 3 3
      frontend/app_flowy/lib/plugins/grid/application/row/row_bloc.dart
  14. 47 45
      frontend/app_flowy/lib/plugins/grid/application/row/row_cache.dart
  15. 3 3
      frontend/app_flowy/lib/plugins/grid/application/row/row_data_controller.dart
  16. 2 2
      frontend/app_flowy/lib/plugins/grid/presentation/grid_page.dart
  17. 1 1
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/grid_row.dart
  18. 1 1
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/row_action_sheet.dart
  19. 1 0
      frontend/rust-lib/flowy-grid/src/dart_notification.rs
  20. 8 41
      frontend/rust-lib/flowy-grid/src/entities/block_entities.rs
  21. 70 0
      frontend/rust-lib/flowy-grid/src/entities/group_entities/board_card.rs
  22. 2 0
      frontend/rust-lib/flowy-grid/src/entities/group_entities/mod.rs
  23. 15 7
      frontend/rust-lib/flowy-grid/src/event_handler.rs
  24. 4 0
      frontend/rust-lib/flowy-grid/src/event_map.rs
  25. 39 47
      frontend/rust-lib/flowy-grid/src/services/block_manager.rs
  26. 41 0
      frontend/rust-lib/flowy-grid/src/services/cell/cell_operation.rs
  27. 58 21
      frontend/rust-lib/flowy-grid/src/services/grid_editor.rs
  28. 33 10
      frontend/rust-lib/flowy-grid/src/services/group/group_generator/checkbox_group.rs
  29. 71 32
      frontend/rust-lib/flowy-grid/src/services/group/group_generator/generator.rs
  30. 74 17
      frontend/rust-lib/flowy-grid/src/services/group/group_generator/select_option_group.rs
  31. 110 54
      frontend/rust-lib/flowy-grid/src/services/group/group_service.rs
  32. 50 26
      frontend/rust-lib/flowy-grid/src/services/row/row_builder.rs
  33. 2 2
      frontend/rust-lib/flowy-grid/src/services/row/row_loader.rs
  34. 5 10
      frontend/rust-lib/flowy-grid/src/util.rs
  35. 6 5
      frontend/rust-lib/flowy-grid/tests/grid/block_test/util.rs
  36. 3 0
      shared-lib/flowy-error-code/src/code.rs
  37. 2 2
      shared-lib/flowy-grid-data-model/src/revision/grid_rev.rs
  38. 1 1
      shared-lib/flowy-sync/src/client_grid/grid_revision_pad.rs

+ 20 - 4
frontend/app_flowy/lib/plugins/board/application/board_bloc.dart

@@ -52,8 +52,21 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
             _startListening();
             await _loadGrid(emit);
           },
-          createRow: () {
-            _dataController.createRow();
+          createRow: (groupId) async {
+            final result = await _dataController.createBoardCard(groupId);
+            result.fold(
+              (rowPB) {
+                emit(state.copyWith(editingRow: some(rowPB)));
+              },
+              (err) => Log.error(err),
+            );
+          },
+          endEditRow: (rowId) {
+            assert(state.editingRow.isSome());
+            state.editingRow.fold(() => null, (row) {
+              assert(row.id == rowId);
+              emit(state.copyWith(editingRow: none()));
+            });
           },
           didReceiveGridUpdate: (GridPB grid) {
             emit(state.copyWith(grid: Some(grid)));
@@ -99,7 +112,7 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
 
         boardDataController.addColumns(columns);
       },
-      onRowsChanged: (List<RowInfo> rowInfos, RowChangeReason reason) {
+      onRowsChanged: (List<RowInfo> rowInfos, RowsChangedReason reason) {
         add(BoardEvent.didReceiveRows(rowInfos));
       },
       onError: (err) {
@@ -140,7 +153,8 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
 @freezed
 class BoardEvent with _$BoardEvent {
   const factory BoardEvent.initial() = InitialGrid;
-  const factory BoardEvent.createRow() = _CreateRow;
+  const factory BoardEvent.createRow(String groupId) = _CreateRow;
+  const factory BoardEvent.endEditRow(String rowId) = _EndEditRow;
   const factory BoardEvent.didReceiveGroups(List<GroupPB> groups) =
       _DidReceiveGroup;
   const factory BoardEvent.didReceiveRows(List<RowInfo> rowInfos) =
@@ -156,6 +170,7 @@ class BoardState with _$BoardState {
     required String gridId,
     required Option<GridPB> grid,
     required List<GroupPB> groups,
+    required Option<RowPB> editingRow,
     required List<RowInfo> rowInfos,
     required GridLoadingState loadingState,
   }) = _BoardState;
@@ -165,6 +180,7 @@ class BoardState with _$BoardState {
         groups: [],
         grid: none(),
         gridId: gridId,
+        editingRow: none(),
         loadingState: const _Loading(),
       );
 }

+ 22 - 33
frontend/app_flowy/lib/plugins/board/application/board_data_controller.dart

@@ -4,7 +4,6 @@ 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/grid_service.dart';
 import 'package:app_flowy/plugins/grid/application/row/row_cache.dart';
-import 'package:flowy_sdk/log.dart';
 import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart';
 import 'dart:async';
@@ -15,14 +14,14 @@ typedef OnFieldsChanged = void Function(UnmodifiableListView<FieldPB>);
 typedef OnGridChanged = void Function(GridPB);
 typedef OnGroupChanged = void Function(List<GroupPB>);
 typedef OnRowsChanged = void Function(
-  List<RowInfo> rowInfos,
-  RowChangeReason,
+  List<RowInfo>,
+  RowsChangedReason,
 );
 typedef OnError = void Function(FlowyError);
 
 class BoardDataController {
   final String gridId;
-  final GridService _gridFFIService;
+  final GridFFIService _gridFFIService;
   final GridFieldCache fieldCache;
 
   // key: the block id
@@ -46,7 +45,7 @@ class BoardDataController {
   BoardDataController({required ViewPB view})
       : gridId = view.id,
         _blocks = LinkedHashMap.new(),
-        _gridFFIService = GridService(gridId: view.id),
+        _gridFFIService = GridFFIService(gridId: view.id),
         fieldCache = GridFieldCache(gridId: view.id);
 
   void addListener({
@@ -73,11 +72,11 @@ class BoardDataController {
       () => result.fold(
         (grid) async {
           _onGridChanged?.call(grid);
-          _initialBlocks(grid.blocks);
+
           return await _loadFields(grid).then((result) {
             return result.fold(
               (l) {
-                _loadGroups();
+                _loadGroups(grid.blocks);
                 return left(l);
               },
               (err) => right(err),
@@ -89,8 +88,8 @@ class BoardDataController {
     );
   }
 
-  Future<Either<RowPB, FlowyError>> createRow() {
-    return _gridFFIService.createRow();
+  Future<Either<RowPB, FlowyError>> createBoardCard(String groupId) {
+    return _gridFFIService.createBoardCard(groupId);
   }
 
   Future<void> dispose() async {
@@ -102,29 +101,6 @@ class BoardDataController {
     }
   }
 
-  void _initialBlocks(List<BlockPB> blocks) {
-    for (final block in blocks) {
-      if (_blocks[block.id] != null) {
-        Log.warn("Initial duplicate block's cache: ${block.id}");
-        return;
-      }
-
-      final cache = GridBlockCache(
-        gridId: gridId,
-        block: block,
-        fieldCache: fieldCache,
-      );
-
-      cache.addListener(
-        onChangeReason: (reason) {
-          _onRowsChanged?.call(rowInfos, reason);
-        },
-      );
-
-      _blocks[block.id] = cache;
-    }
-  }
-
   Future<Either<Unit, FlowyError>> _loadFields(GridPB grid) async {
     final result = await _gridFFIService.getFields(fieldIds: grid.fields);
     return Future(
@@ -139,7 +115,20 @@ class BoardDataController {
     );
   }
 
-  Future<void> _loadGroups() async {
+  Future<void> _loadGroups(List<BlockPB> blocks) async {
+    for (final block in blocks) {
+      final cache = GridBlockCache(
+        gridId: gridId,
+        block: block,
+        fieldCache: fieldCache,
+      );
+
+      cache.addListener(onRowsChanged: (reason) {
+        _onRowsChanged?.call(rowInfos, reason);
+      });
+      _blocks[block.id] = cache;
+    }
+
     final result = await _gridFFIService.loadGroups();
     return Future(
       () => result.fold(

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

@@ -74,7 +74,7 @@ class BoardCardEvent with _$BoardCardEvent {
   const factory BoardCardEvent.initial() = _InitialRow;
   const factory BoardCardEvent.createRow() = _CreateRow;
   const factory BoardCardEvent.didReceiveCells(
-      GridCellMap gridCellMap, RowChangeReason reason) = _DidReceiveCells;
+      GridCellMap gridCellMap, RowsChangedReason reason) = _DidReceiveCells;
 }
 
 @freezed
@@ -83,7 +83,7 @@ class BoardCardState with _$BoardCardState {
     required RowPB rowPB,
     required GridCellMap gridCellMap,
     required UnmodifiableListView<GridCellEquatable> cells,
-    RowChangeReason? changeReason,
+    RowsChangedReason? changeReason,
   }) = _BoardCardState;
 
   factory BoardCardState.initial(RowPB rowPB, GridCellMap cellDataMap) =>

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

@@ -6,7 +6,7 @@ import 'package:app_flowy/plugins/grid/application/row/row_cache.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid/block_entities.pb.dart';
 import 'package:flutter/foundation.dart';
 
-typedef OnCardChanged = void Function(GridCellMap, RowChangeReason);
+typedef OnCardChanged = void Function(GridCellMap, RowsChangedReason);
 
 class CardDataController extends BoardCellBuilderDelegate {
   final RowPB rowPB;

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

@@ -53,7 +53,7 @@ class BoardContent extends StatelessWidget {
           child: Padding(
             padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 20),
             child: AFBoard(
-              key: UniqueKey(),
+              // key: UniqueKey(),
               scrollController: ScrollController(),
               dataController: context.read<BoardBloc>().boardDataController,
               headerBuilder: _buildHeader,
@@ -83,11 +83,13 @@ class BoardContent extends StatelessWidget {
 
   Widget _buildFooter(BuildContext context, AFBoardColumnData columnData) {
     return AppFlowyColumnFooter(
-      icon: const Icon(Icons.add, size: 20),
-      title: const Text('New'),
-      height: 50,
-      margin: config.columnItemPadding,
-    );
+        icon: const Icon(Icons.add, size: 20),
+        title: const Text('New'),
+        height: 50,
+        margin: config.columnItemPadding,
+        onAddButtonClick: () {
+          context.read<BoardBloc>().add(BoardEvent.createRow(columnData.id));
+        });
   }
 
   Widget _buildCard(BuildContext context, AFColumnItem item) {
@@ -106,13 +108,21 @@ class BoardContent extends StatelessWidget {
     );
 
     final cellBuilder = BoardCellBuilder(cardController);
+    final isEditing = context.read<BoardBloc>().state.editingRow.fold(
+          () => false,
+          (editingRow) => editingRow.id == rowPB.id,
+        );
 
     return AppFlowyColumnItemCard(
       key: ObjectKey(item),
       child: BoardCard(
+        gridId: gridId,
+        isEditing: isEditing,
         cellBuilder: cellBuilder,
         dataController: cardController,
-        gridId: gridId,
+        onEditEditing: (rowId) {
+          context.read<BoardBloc>().add(BoardEvent.endEditRow(rowId));
+        },
       ),
     );
   }

+ 6 - 1
frontend/app_flowy/lib/plugins/board/presentation/card/card.dart

@@ -9,15 +9,21 @@ import 'package:flutter_bloc/flutter_bloc.dart';
 import 'card_cell_builder.dart';
 import 'card_container.dart';
 
+typedef OnEndEditing = void Function(String rowId);
+
 class BoardCard extends StatefulWidget {
   final String gridId;
+  final bool isEditing;
   final CardDataController dataController;
   final BoardCellBuilder cellBuilder;
+  final OnEndEditing onEditEditing;
 
   const BoardCard({
     required this.gridId,
+    required this.isEditing,
     required this.dataController,
     required this.cellBuilder,
+    required this.onEditEditing,
     Key? key,
   }) : super(key: key);
 
@@ -60,7 +66,6 @@ class _BoardCardState extends State<BoardCard> {
     return cellMap.values.map(
       (cellId) {
         final child = widget.cellBuilder.buildCell(cellId);
-
         return Padding(
           padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
           child: child,

+ 5 - 1
frontend/app_flowy/lib/plugins/board/presentation/card/card_container.dart

@@ -29,9 +29,13 @@ class BoardCardContainer extends StatelessWidget {
               );
             }
           }
+
           return Padding(
             padding: const EdgeInsets.all(8),
-            child: container,
+            child: ConstrainedBox(
+              constraints: const BoxConstraints(minHeight: 30),
+              child: container,
+            ),
           );
         },
       ),

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

@@ -42,7 +42,7 @@ class GridBlockCache {
   }
 
   void addListener({
-    required void Function(RowChangeReason) onChangeReason,
+    required void Function(RowsChangedReason) onRowsChanged,
     bool Function()? listenWhen,
   }) {
     _rowCache.onRowsChanged((reason) {
@@ -50,7 +50,7 @@ class GridBlockCache {
         return;
       }
 
-      onChangeReason(reason);
+      onRowsChanged(reason);
     });
   }
 }

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

@@ -98,7 +98,7 @@ class GridEvent with _$GridEvent {
   const factory GridEvent.createRow() = _CreateRow;
   const factory GridEvent.didReceiveRowUpdate(
     List<RowInfo> rows,
-    RowChangeReason listState,
+    RowsChangedReason listState,
   ) = _DidReceiveRowUpdate;
   const factory GridEvent.didReceiveFieldUpdate(
     UnmodifiableListView<FieldPB> fields,
@@ -117,7 +117,7 @@ class GridState with _$GridState {
     required GridFieldEquatable fields,
     required List<RowInfo> rowInfos,
     required GridLoadingState loadingState,
-    required RowChangeReason reason,
+    required RowsChangedReason reason,
   }) = _GridState;
 
   factory GridState.initial(String gridId) => GridState(

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

@@ -18,13 +18,13 @@ typedef OnGridChanged = void Function(GridPB);
 
 typedef OnRowsChanged = void Function(
   List<RowInfo> rowInfos,
-  RowChangeReason,
+  RowsChangedReason,
 );
 typedef ListenOnRowChangedCondition = bool Function();
 
 class GridDataController {
   final String gridId;
-  final GridService _gridFFIService;
+  final GridFFIService _gridFFIService;
   final GridFieldCache fieldCache;
 
   // key: the block id
@@ -47,7 +47,7 @@ class GridDataController {
   GridDataController({required ViewPB view})
       : gridId = view.id,
         _blocks = LinkedHashMap.new(),
-        _gridFFIService = GridService(gridId: view.id),
+        _gridFFIService = GridFFIService(gridId: view.id),
         fieldCache = GridFieldCache(gridId: view.id);
 
   void addListener({
@@ -105,7 +105,7 @@ class GridDataController {
       );
 
       cache.addListener(
-        onChangeReason: (reason) {
+        onRowsChanged: (reason) {
           _onRowChanged?.call(rowInfos, reason);
         },
       );

+ 10 - 2
frontend/app_flowy/lib/plugins/grid/application/grid_service.dart

@@ -3,14 +3,15 @@ import 'package:flowy_sdk/dispatch/dispatch.dart';
 import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid/block_entities.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/board_card.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid/grid_entities.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid/group.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid/row_entities.pb.dart';
 
-class GridService {
+class GridFFIService {
   final String gridId;
-  GridService({
+  GridFFIService({
     required this.gridId,
   });
 
@@ -27,6 +28,13 @@ class GridService {
     return GridEventCreateRow(payload).send();
   }
 
+  Future<Either<RowPB, FlowyError>> createBoardCard(String groupId) {
+    CreateBoardCardPayloadPB payload = CreateBoardCardPayloadPB.create()
+      ..gridId = gridId
+      ..groupId = groupId;
+    return GridEventCreateBoardCard(payload).send();
+  }
+
   Future<Either<RepeatedFieldPB, FlowyError>> getFields(
       {required List<FieldIdPB> fieldIds}) {
     final payload = QueryFieldPayloadPB.create()

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

@@ -14,13 +14,13 @@ class RowActionSheetBloc
     extends Bloc<RowActionSheetEvent, RowActionSheetState> {
   final RowFFIService _rowService;
 
-  RowActionSheetBloc({required RowInfo rowData})
+  RowActionSheetBloc({required RowInfo rowInfo})
       : _rowService = RowFFIService(
-          gridId: rowData.gridId,
-          blockId: rowData.blockId,
-          rowId: rowData.id,
+          gridId: rowInfo.gridId,
+          blockId: rowInfo.blockId,
+          rowId: rowInfo.rowPB.id,
         ),
-        super(RowActionSheetState.initial(rowData)) {
+        super(RowActionSheetState.initial(rowInfo)) {
     on<RowActionSheetEvent>(
       (event, emit) async {
         await event.map(

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

@@ -21,7 +21,7 @@ class RowBloc extends Bloc<RowEvent, RowState> {
   })  : _rowService = RowFFIService(
           gridId: rowInfo.gridId,
           blockId: rowInfo.blockId,
-          rowId: rowInfo.id,
+          rowId: rowInfo.rowPB.id,
         ),
         _dataController = dataController,
         super(RowState.initial(rowInfo, dataController.loadData())) {
@@ -71,7 +71,7 @@ class RowEvent with _$RowEvent {
   const factory RowEvent.initial() = _InitialRow;
   const factory RowEvent.createRow() = _CreateRow;
   const factory RowEvent.didReceiveCells(
-      GridCellMap gridCellMap, RowChangeReason reason) = _DidReceiveCells;
+      GridCellMap gridCellMap, RowsChangedReason reason) = _DidReceiveCells;
 }
 
 @freezed
@@ -80,7 +80,7 @@ class RowState with _$RowState {
     required RowInfo rowInfo,
     required GridCellMap gridCellMap,
     required UnmodifiableListView<GridCellEquatable> cells,
-    RowChangeReason? changeReason,
+    RowsChangedReason? changeReason,
   }) = _RowState;
 
   factory RowState.initial(RowInfo rowInfo, GridCellMap cellDataMap) =>

+ 47 - 45
frontend/app_flowy/lib/plugins/grid/application/row/row_cache.dart

@@ -51,11 +51,9 @@ class GridRowCache {
         _fieldNotifier = notifier {
     //
     notifier.onRowFieldsChanged(() => _rowChangeReasonNotifier
-        .receive(const RowChangeReason.fieldDidChange()));
+        .receive(const RowsChangedReason.fieldDidChange()));
     notifier.onRowFieldChanged((field) => _cellCache.remove(field.id));
-    _rowInfos = block.rows
-        .map((rowInfo) => buildGridRow(rowInfo.id, rowInfo.height.toDouble()))
-        .toList();
+    _rowInfos = block.rows.map((rowPB) => buildGridRow(rowPB)).toList();
   }
 
   Future<void> dispose() async {
@@ -85,16 +83,16 @@ class GridRowCache {
       for (var rowId in deletedRows) rowId: rowId
     };
 
-    _rowInfos.asMap().forEach((index, row) {
-      if (deletedRowByRowId[row.id] == null) {
-        newRows.add(row);
+    _rowInfos.asMap().forEach((index, RowInfo rowInfo) {
+      if (deletedRowByRowId[rowInfo.rowPB.id] == null) {
+        newRows.add(rowInfo);
       } else {
-        _rowByRowId.remove(row.id);
-        deletedIndex.add(DeletedIndex(index: index, row: row));
+        _rowByRowId.remove(rowInfo.rowPB.id);
+        deletedIndex.add(DeletedIndex(index: index, row: rowInfo));
       }
     });
     _rowInfos = newRows;
-    _rowChangeReasonNotifier.receive(RowChangeReason.delete(deletedIndex));
+    _rowChangeReasonNotifier.receive(RowsChangedReason.delete(deletedIndex));
   }
 
   void _insertRows(List<InsertedRowPB> insertRows) {
@@ -103,39 +101,42 @@ class GridRowCache {
     }
 
     InsertedIndexs insertIndexs = [];
-    for (final insertRow in insertRows) {
+    for (final InsertedRowPB insertRow in insertRows) {
       final insertIndex = InsertedIndex(
         index: insertRow.index,
-        rowId: insertRow.rowId,
+        rowId: insertRow.row.id,
       );
       insertIndexs.add(insertIndex);
-      _rowInfos.insert(insertRow.index,
-          (buildGridRow(insertRow.rowId, insertRow.height.toDouble())));
+      _rowInfos.insert(
+        insertRow.index,
+        (buildGridRow(insertRow.row)),
+      );
     }
 
-    _rowChangeReasonNotifier.receive(RowChangeReason.insert(insertIndexs));
+    _rowChangeReasonNotifier.receive(RowsChangedReason.insert(insertIndexs));
   }
 
-  void _updateRows(List<UpdatedRowPB> updatedRows) {
+  void _updateRows(List<RowPB> updatedRows) {
     if (updatedRows.isEmpty) {
       return;
     }
 
     final UpdatedIndexs updatedIndexs = UpdatedIndexs();
-    for (final updatedRow in updatedRows) {
-      final rowId = updatedRow.rowId;
-      final index = _rowInfos.indexWhere((row) => row.id == rowId);
+    for (final RowPB updatedRow in updatedRows) {
+      final rowId = updatedRow.id;
+      final index = _rowInfos.indexWhere(
+        (rowInfo) => rowInfo.rowPB.id == rowId,
+      );
       if (index != -1) {
-        _rowByRowId[rowId] = updatedRow.row;
+        _rowByRowId[rowId] = updatedRow;
 
         _rowInfos.removeAt(index);
-        _rowInfos.insert(
-            index, buildGridRow(rowId, updatedRow.row.height.toDouble()));
+        _rowInfos.insert(index, buildGridRow(updatedRow));
         updatedIndexs[rowId] = UpdatedIndex(index: index, rowId: rowId);
       }
     }
 
-    _rowChangeReasonNotifier.receive(RowChangeReason.update(updatedIndexs));
+    _rowChangeReasonNotifier.receive(RowsChangedReason.update(updatedIndexs));
   }
 
   void _hideRows(List<String> hideRows) {}
@@ -143,7 +144,7 @@ class GridRowCache {
   void _showRows(List<String> visibleRows) {}
 
   void onRowsChanged(
-    void Function(RowChangeReason) onRowChanged,
+    void Function(RowsChangedReason) onRowChanged,
   ) {
     _rowChangeReasonNotifier.addListener(() {
       onRowChanged(_rowChangeReasonNotifier.reason);
@@ -152,7 +153,7 @@ class GridRowCache {
 
   RowUpdateCallback addListener({
     required String rowId,
-    void Function(GridCellMap, RowChangeReason)? onCellUpdated,
+    void Function(GridCellMap, RowsChangedReason)? onCellUpdated,
     bool Function()? listenWhen,
   }) {
     listenerHandler() async {
@@ -230,40 +231,43 @@ class GridRowCache {
 
     _rowByRowId[updatedRow.id] = updatedRow;
     final index =
-        _rowInfos.indexWhere((gridRow) => gridRow.id == updatedRow.id);
+        _rowInfos.indexWhere((rowInfo) => rowInfo.rowPB.id == updatedRow.id);
     if (index != -1) {
       // update the corresponding row in _rows if they are not the same
-      if (_rowInfos[index].rawRow != updatedRow) {
-        final row = _rowInfos.removeAt(index).copyWith(rawRow: updatedRow);
-        _rowInfos.insert(index, row);
+      if (_rowInfos[index].rowPB != updatedRow) {
+        final rowInfo = _rowInfos.removeAt(index).copyWith(rowPB: updatedRow);
+        _rowInfos.insert(index, rowInfo);
 
         // Calculate the update index
         final UpdatedIndexs updatedIndexs = UpdatedIndexs();
-        updatedIndexs[row.id] = UpdatedIndex(index: index, rowId: row.id);
+        updatedIndexs[rowInfo.rowPB.id] = UpdatedIndex(
+          index: index,
+          rowId: rowInfo.rowPB.id,
+        );
 
         //
-        _rowChangeReasonNotifier.receive(RowChangeReason.update(updatedIndexs));
+        _rowChangeReasonNotifier
+            .receive(RowsChangedReason.update(updatedIndexs));
       }
     }
   }
 
-  RowInfo buildGridRow(String rowId, double rowHeight) {
+  RowInfo buildGridRow(RowPB rowPB) {
     return RowInfo(
       gridId: gridId,
       blockId: block.id,
       fields: _fieldNotifier.fields,
-      id: rowId,
-      height: rowHeight,
+      rowPB: rowPB,
     );
   }
 }
 
 class _RowChangesetNotifier extends ChangeNotifier {
-  RowChangeReason reason = const InitialListState();
+  RowsChangedReason reason = const InitialListState();
 
   _RowChangesetNotifier();
 
-  void receive(RowChangeReason newReason) {
+  void receive(RowsChangedReason newReason) {
     reason = newReason;
     reason.map(
       insert: (_) => notifyListeners(),
@@ -280,10 +284,8 @@ class RowInfo with _$RowInfo {
   const factory RowInfo({
     required String gridId,
     required String blockId,
-    required String id,
     required UnmodifiableListView<FieldPB> fields,
-    required double height,
-    RowPB? rawRow,
+    required RowPB rowPB,
   }) = _RowInfo;
 }
 
@@ -292,12 +294,12 @@ typedef DeletedIndexs = List<DeletedIndex>;
 typedef UpdatedIndexs = LinkedHashMap<String, UpdatedIndex>;
 
 @freezed
-class RowChangeReason with _$RowChangeReason {
-  const factory RowChangeReason.insert(InsertedIndexs items) = _Insert;
-  const factory RowChangeReason.delete(DeletedIndexs items) = _Delete;
-  const factory RowChangeReason.update(UpdatedIndexs indexs) = _Update;
-  const factory RowChangeReason.fieldDidChange() = _FieldDidChange;
-  const factory RowChangeReason.initial() = InitialListState;
+class RowsChangedReason with _$RowsChangedReason {
+  const factory RowsChangedReason.insert(InsertedIndexs items) = _Insert;
+  const factory RowsChangedReason.delete(DeletedIndexs items) = _Delete;
+  const factory RowsChangedReason.update(UpdatedIndexs indexs) = _Update;
+  const factory RowsChangedReason.fieldDidChange() = _FieldDidChange;
+  const factory RowsChangedReason.initial() = InitialListState;
 }
 
 class InsertedIndex {

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

@@ -5,7 +5,7 @@ import '../cell/cell_service/cell_service.dart';
 import '../field/field_cache.dart';
 import 'row_cache.dart';
 
-typedef OnRowChanged = void Function(GridCellMap, RowChangeReason);
+typedef OnRowChanged = void Function(GridCellMap, RowsChangedReason);
 
 class GridRowDataController extends GridCellBuilderDelegate {
   final RowInfo rowInfo;
@@ -21,12 +21,12 @@ class GridRowDataController extends GridCellBuilderDelegate {
         _rowCache = rowCache;
 
   GridCellMap loadData() {
-    return _rowCache.loadGridCells(rowInfo.id);
+    return _rowCache.loadGridCells(rowInfo.rowPB.id);
   }
 
   void addListener({OnRowChanged? onRowChanged}) {
     _onRowChangedListeners.add(_rowCache.addListener(
-      rowId: rowInfo.id,
+      rowId: rowInfo.rowPB.id,
       onCellUpdated: onRowChanged,
     ));
   }

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

@@ -240,7 +240,7 @@ class _GridRowsState extends State<_GridRows> {
     Animation<double> animation,
   ) {
     final rowCache =
-        context.read<GridBloc>().getRowCache(rowInfo.blockId, rowInfo.id);
+        context.read<GridBloc>().getRowCache(rowInfo.blockId, rowInfo.rowPB.id);
 
     /// Return placeholder widget if the rowCache is null.
     if (rowCache == null) return const SizedBox();
@@ -267,7 +267,7 @@ class _GridRowsState extends State<_GridRows> {
             cellBuilder,
           );
         },
-        key: ValueKey(rowInfo.id),
+        key: ValueKey(rowInfo.rowPB.id),
       ),
     );
   }

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

@@ -52,7 +52,7 @@ class _GridRowWidgetState extends State<GridRowWidget> {
       value: _rowBloc,
       child: _RowEnterRegion(
         child: BlocBuilder<RowBloc, RowState>(
-          buildWhen: (p, c) => p.rowInfo.height != c.rowInfo.height,
+          buildWhen: (p, c) => p.rowInfo.rowPB.height != c.rowInfo.rowPB.height,
           builder: (context, state) {
             final children = [
               const _RowLeading(),

+ 1 - 1
frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/row_action_sheet.dart

@@ -21,7 +21,7 @@ class GridRowActionSheet extends StatelessWidget {
   @override
   Widget build(BuildContext context) {
     return BlocProvider(
-      create: (context) => RowActionSheetBloc(rowData: rowData),
+      create: (context) => RowActionSheetBloc(rowInfo: rowData),
       child: BlocBuilder<RowActionSheetBloc, RowActionSheetState>(
         builder: (context, state) {
           final cells = _RowAction.values

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

@@ -11,6 +11,7 @@ pub enum GridNotification {
     DidUpdateRow = 30,
     DidUpdateCell = 40,
     DidUpdateField = 50,
+    DidUpdateBoard = 60,
 }
 
 impl std::default::Default for GridNotification {

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

@@ -106,48 +106,15 @@ impl std::convert::From<Vec<BlockPB>> for RepeatedBlockPB {
 #[derive(Debug, Clone, Default, ProtoBuf)]
 pub struct InsertedRowPB {
     #[pb(index = 1)]
-    pub block_id: String,
-
-    #[pb(index = 2)]
-    pub row_id: String,
-
-    #[pb(index = 3)]
-    pub height: i32,
-
-    #[pb(index = 4, one_of)]
-    pub index: Option<i32>,
-}
-
-#[derive(Debug, Default, ProtoBuf)]
-pub struct UpdatedRowPB {
-    #[pb(index = 1)]
-    pub block_id: String,
-
-    #[pb(index = 2)]
-    pub row_id: String,
-
-    #[pb(index = 3)]
     pub row: RowPB,
-}
 
-impl UpdatedRowPB {
-    pub fn new(row_rev: &RowRevision, row: RowPB) -> Self {
-        Self {
-            row_id: row_rev.id.clone(),
-            block_id: row_rev.block_id.clone(),
-            row,
-        }
-    }
+    #[pb(index = 2, one_of)]
+    pub index: Option<i32>,
 }
 
 impl std::convert::From<RowPB> for InsertedRowPB {
-    fn from(row_info: RowPB) -> Self {
-        Self {
-            row_id: row_info.id,
-            block_id: row_info.block_id,
-            height: row_info.height,
-            index: None,
-        }
+    fn from(row: RowPB) -> Self {
+        Self { row, index: None }
     }
 }
 
@@ -170,7 +137,7 @@ pub struct GridBlockChangesetPB {
     pub deleted_rows: Vec<String>,
 
     #[pb(index = 4)]
-    pub updated_rows: Vec<UpdatedRowPB>,
+    pub updated_rows: Vec<RowPB>,
 
     #[pb(index = 5)]
     pub visible_rows: Vec<String>,
@@ -179,9 +146,9 @@ pub struct GridBlockChangesetPB {
     pub hide_rows: Vec<String>,
 }
 impl GridBlockChangesetPB {
-    pub fn insert(block_id: &str, inserted_rows: Vec<InsertedRowPB>) -> Self {
+    pub fn insert(block_id: String, inserted_rows: Vec<InsertedRowPB>) -> Self {
         Self {
-            block_id: block_id.to_owned(),
+            block_id,
             inserted_rows,
             ..Default::default()
         }
@@ -195,7 +162,7 @@ impl GridBlockChangesetPB {
         }
     }
 
-    pub fn update(block_id: &str, updated_rows: Vec<UpdatedRowPB>) -> Self {
+    pub fn update(block_id: &str, updated_rows: Vec<RowPB>) -> Self {
         Self {
             block_id: block_id.to_owned(),
             updated_rows,

+ 70 - 0
frontend/rust-lib/flowy-grid/src/entities/group_entities/board_card.rs

@@ -0,0 +1,70 @@
+use crate::entities::RowPB;
+use flowy_derive::ProtoBuf;
+use flowy_error::ErrorCode;
+use flowy_grid_data_model::parser::NotEmptyStr;
+
+#[derive(ProtoBuf, Debug, Default, Clone)]
+pub struct CreateBoardCardPayloadPB {
+    #[pb(index = 1)]
+    pub grid_id: String,
+
+    #[pb(index = 2)]
+    pub group_id: String,
+}
+pub struct CreateBoardCardParams {
+    pub grid_id: String,
+    pub group_id: String,
+}
+
+impl TryInto<CreateBoardCardParams> for CreateBoardCardPayloadPB {
+    type Error = ErrorCode;
+
+    fn try_into(self) -> Result<CreateBoardCardParams, Self::Error> {
+        let grid_id = NotEmptyStr::parse(self.grid_id).map_err(|_| ErrorCode::GridIdIsEmpty)?;
+        let group_id = NotEmptyStr::parse(self.group_id).map_err(|_| ErrorCode::GroupIdIsEmpty)?;
+        Ok(CreateBoardCardParams {
+            grid_id: grid_id.0,
+            group_id: group_id.0,
+        })
+    }
+}
+
+#[derive(Debug, Default, ProtoBuf)]
+pub struct BoardCardChangesetPB {
+    #[pb(index = 1)]
+    pub group_id: String,
+
+    #[pb(index = 2)]
+    pub inserted_cards: Vec<RowPB>,
+
+    #[pb(index = 3)]
+    pub deleted_cards: Vec<String>,
+
+    #[pb(index = 4)]
+    pub updated_cards: Vec<RowPB>,
+}
+impl BoardCardChangesetPB {
+    pub fn insert(group_id: String, inserted_cards: Vec<RowPB>) -> Self {
+        Self {
+            group_id,
+            inserted_cards,
+            ..Default::default()
+        }
+    }
+
+    pub fn delete(group_id: String, deleted_cards: Vec<String>) -> Self {
+        Self {
+            group_id,
+            deleted_cards,
+            ..Default::default()
+        }
+    }
+
+    pub fn update(group_id: String, updated_cards: Vec<RowPB>) -> Self {
+        Self {
+            group_id,
+            updated_cards,
+            ..Default::default()
+        }
+    }
+}

+ 2 - 0
frontend/rust-lib/flowy-grid/src/entities/group_entities/mod.rs

@@ -1,5 +1,7 @@
+mod board_card;
 mod configuration;
 mod group;
 
+pub use board_card::*;
 pub use configuration::*;
 pub use group::*;

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

@@ -232,10 +232,7 @@ pub(crate) async fn get_row_handler(
 ) -> DataResult<OptionalRowPB, FlowyError> {
     let params: RowIdParams = data.into_inner().try_into()?;
     let editor = manager.get_grid_editor(&params.grid_id)?;
-    let row = editor
-        .get_row_rev(&params.row_id)
-        .await?
-        .and_then(make_row_from_row_rev);
+    let row = editor.get_row_rev(&params.row_id).await?.map(make_row_from_row_rev);
 
     data_result(OptionalRowPB { row })
 }
@@ -266,11 +263,11 @@ pub(crate) async fn duplicate_row_handler(
 pub(crate) async fn create_row_handler(
     data: Data<CreateRowPayloadPB>,
     manager: AppData<Arc<GridManager>>,
-) -> Result<(), FlowyError> {
+) -> DataResult<RowPB, FlowyError> {
     let params: CreateRowParams = data.into_inner().try_into()?;
     let editor = manager.get_grid_editor(params.grid_id.as_ref())?;
-    let _ = editor.create_row(params.start_row_id).await?;
-    Ok(())
+    let row = editor.create_row(params.start_row_id).await?;
+    data_result(row)
 }
 
 // #[tracing::instrument(level = "debug", skip_all, err)]
@@ -416,3 +413,14 @@ pub(crate) async fn get_groups_handler(
     let group = editor.load_groups().await?;
     data_result(group)
 }
+
+#[tracing::instrument(level = "debug", skip(data, manager), err)]
+pub(crate) async fn create_board_card_handler(
+    data: Data<CreateBoardCardPayloadPB>,
+    manager: AppData<Arc<GridManager>>,
+) -> DataResult<RowPB, FlowyError> {
+    let params: CreateBoardCardParams = data.into_inner().try_into()?;
+    let editor = manager.get_grid_editor(params.grid_id.as_ref())?;
+    let row = editor.create_board_card(&params.group_id).await?;
+    data_result(row)
+}

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

@@ -39,6 +39,7 @@ pub fn create(grid_manager: Arc<GridManager>) -> Module {
         // Date
         .event(GridEvent::UpdateDateCell, update_date_cell_handler)
         // Group
+        .event(GridEvent::CreateBoardCard, create_board_card_handler)
         .event(GridEvent::GetGroup, get_groups_handler);
 
     module
@@ -209,4 +210,7 @@ pub enum GridEvent {
 
     #[event(input = "GridIdPB", output = "RepeatedGridGroupPB")]
     GetGroup = 100,
+
+    #[event(input = "CreateBoardCardPayloadPB", output = "RowPB")]
+    CreateBoardCard = 110,
 }

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

@@ -1,9 +1,9 @@
 use crate::dart_notification::{send_dart_notification, GridNotification};
-use crate::entities::{CellChangesetPB, GridBlockChangesetPB, InsertedRowPB, RowPB, UpdatedRowPB};
+use crate::entities::{CellChangesetPB, GridBlockChangesetPB, InsertedRowPB, RowPB};
 use crate::manager::GridUser;
 use crate::services::block_revision_editor::{GridBlockRevisionCompactor, GridBlockRevisionEditor};
 use crate::services::persistence::block_index::BlockIndexCache;
-use crate::services::row::{block_from_row_orders, GridBlockSnapshot};
+use crate::services::row::{block_from_row_orders, make_row_from_row_rev, GridBlockSnapshot};
 use dashmap::DashMap;
 use flowy_error::FlowyResult;
 use flowy_grid_data_model::revision::{
@@ -62,22 +62,16 @@ impl GridBlockManager {
         Ok(self.get_editor(&block_id).await?)
     }
 
-    pub(crate) async fn create_row(
-        &self,
-        block_id: &str,
-        row_rev: RowRevision,
-        start_row_id: Option<String>,
-    ) -> FlowyResult<i32> {
+    pub(crate) async fn create_row(&self, row_rev: RowRevision, start_row_id: Option<String>) -> FlowyResult<i32> {
+        let block_id = row_rev.block_id.clone();
         let _ = self.persistence.insert(&row_rev.block_id, &row_rev.id)?;
         let editor = self.get_editor(&row_rev.block_id).await?;
 
         let mut index_row_order = InsertedRowPB::from(&row_rev);
         let (row_count, row_index) = editor.create_row(row_rev, start_row_id).await?;
         index_row_order.index = row_index;
-
-        let _ = self
-            .notify_did_update_block(block_id, GridBlockChangesetPB::insert(block_id, vec![index_row_order]))
-            .await?;
+        let changeset = GridBlockChangesetPB::insert(block_id.clone(), vec![index_row_order]);
+        let _ = self.notify_did_update_block(&block_id, changeset).await?;
         Ok(row_count)
     }
 
@@ -98,10 +92,16 @@ impl GridBlockManager {
                 row_order.index = index;
                 inserted_row_orders.push(row_order);
             }
-            changesets.push(GridBlockMetaRevisionChangeset::from_row_count(&block_id, row_count));
+            changesets.push(GridBlockMetaRevisionChangeset::from_row_count(
+                block_id.clone(),
+                row_count,
+            ));
 
             let _ = self
-                .notify_did_update_block(&block_id, GridBlockChangesetPB::insert(&block_id, inserted_row_orders))
+                .notify_did_update_block(
+                    &block_id,
+                    GridBlockChangesetPB::insert(block_id.clone(), inserted_row_orders),
+                )
                 .await?;
         }
 
@@ -110,20 +110,18 @@ impl GridBlockManager {
 
     pub async fn update_row<F>(&self, changeset: RowMetaChangeset, row_builder: F) -> FlowyResult<()>
     where
-        F: FnOnce(Arc<RowRevision>) -> Option<RowPB>,
+        F: FnOnce(Arc<RowRevision>) -> RowPB,
     {
         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) => {
-                if let Some(row) = row_builder(row_rev.clone()) {
-                    let row_order = UpdatedRowPB::new(&row_rev, row);
-                    let block_order_changeset = GridBlockChangesetPB::update(&editor.block_id, vec![row_order]);
-                    let _ = self
-                        .notify_did_update_block(&editor.block_id, block_order_changeset)
-                        .await?;
-                }
+                let block_order_changeset =
+                    GridBlockChangesetPB::update(&editor.block_id, vec![row_builder(row_rev.clone())]);
+                let _ = self
+                    .notify_did_update_block(&editor.block_id, block_order_changeset)
+                    .await?;
             }
         }
         Ok(())
@@ -156,46 +154,40 @@ impl GridBlockManager {
                 .map(|row_info| Cow::Owned(row_info.row_id().to_owned()))
                 .collect::<Vec<Cow<String>>>();
             let row_count = editor.delete_rows(row_ids).await?;
-            let changeset = GridBlockMetaRevisionChangeset::from_row_count(&grid_block.id, row_count);
+            let changeset = GridBlockMetaRevisionChangeset::from_row_count(grid_block.id.clone(), row_count);
             changesets.push(changeset);
         }
 
         Ok(changesets)
     }
 
-    pub(crate) async fn move_row(&self, row_id: &str, from: usize, to: usize) -> FlowyResult<()> {
-        let editor = self.get_editor_from_row_id(row_id).await?;
-        let _ = editor.move_row(row_id, from, to).await?;
+    pub(crate) async fn move_row(&self, row_rev: Arc<RowRevision>, from: usize, to: usize) -> FlowyResult<()> {
+        let editor = self.get_editor_from_row_id(&row_rev.id).await?;
+        let _ = editor.move_row(&row_rev.id, from, to).await?;
 
-        match editor.get_row_revs(Some(vec![Cow::Borrowed(row_id)])).await?.pop() {
-            None => {}
-            Some(row_rev) => {
-                let insert_row = InsertedRowPB {
-                    block_id: row_rev.block_id.clone(),
-                    row_id: row_rev.id.clone(),
-                    index: Some(to as i32),
-                    height: row_rev.height,
-                };
+        let delete_row_id = row_rev.id.clone();
+        let insert_row = InsertedRowPB {
+            index: Some(to as i32),
+            row: make_row_from_row_rev(row_rev),
+        };
 
-                let notified_changeset = GridBlockChangesetPB {
-                    block_id: editor.block_id.clone(),
-                    inserted_rows: vec![insert_row],
-                    deleted_rows: vec![row_rev.id.clone()],
-                    ..Default::default()
-                };
+        let notified_changeset = GridBlockChangesetPB {
+            block_id: editor.block_id.clone(),
+            inserted_rows: vec![insert_row],
+            deleted_rows: vec![delete_row_id],
+            ..Default::default()
+        };
 
-                let _ = self
-                    .notify_did_update_block(&editor.block_id, notified_changeset)
-                    .await?;
-            }
-        }
+        let _ = self
+            .notify_did_update_block(&editor.block_id, notified_changeset)
+            .await?;
 
         Ok(())
     }
 
     pub async fn update_cell<F>(&self, changeset: CellChangesetPB, row_builder: F) -> FlowyResult<()>
     where
-        F: FnOnce(Arc<RowRevision>) -> Option<RowPB>,
+        F: FnOnce(Arc<RowRevision>) -> RowPB,
     {
         let row_changeset: RowMetaChangeset = changeset.clone().into();
         let _ = self.update_row(row_changeset, row_builder).await?;

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

@@ -135,6 +135,47 @@ pub fn try_decode_cell_data(
     }
 }
 
+pub fn insert_text_cell(s: String, field_rev: &FieldRevision) -> CellRevision {
+    let data = apply_cell_data_changeset(s, None, field_rev).unwrap();
+    CellRevision::new(data)
+}
+
+pub fn insert_number_cell(num: i64, field_rev: &FieldRevision) -> CellRevision {
+    let data = apply_cell_data_changeset(num, None, field_rev).unwrap();
+    CellRevision::new(data)
+}
+
+pub fn insert_url_cell(url: String, field_rev: &FieldRevision) -> CellRevision {
+    let data = apply_cell_data_changeset(url, None, field_rev).unwrap();
+    CellRevision::new(data)
+}
+
+pub fn insert_checkbox_cell(is_check: bool, field_rev: &FieldRevision) -> CellRevision {
+    let s = if is_check {
+        CHECK.to_string()
+    } else {
+        UNCHECK.to_string()
+    };
+    let data = apply_cell_data_changeset(s, None, field_rev).unwrap();
+    CellRevision::new(data)
+}
+
+pub fn insert_date_cell(timestamp: i64, field_rev: &FieldRevision) -> CellRevision {
+    let cell_data = serde_json::to_string(&DateCellChangesetPB {
+        date: Some(timestamp.to_string()),
+        time: None,
+    })
+    .unwrap();
+    let data = apply_cell_data_changeset(cell_data, None, field_rev).unwrap();
+    CellRevision::new(data)
+}
+
+pub fn insert_select_option_cell(option_id: String, field_rev: &FieldRevision) -> CellRevision {
+    let cell_data = SelectOptionCellChangeset::from_insert(&option_id).to_str();
+    let data = apply_cell_data_changeset(cell_data, None, field_rev).unwrap();
+    CellRevision::new(data)
+}
+
 /// If the cell data is not String type, it should impl this trait.
 /// Deserialize the String into cell specific data type.  
 pub trait FromCellString {

+ 58 - 21
frontend/rust-lib/flowy-grid/src/services/grid_editor.rs

@@ -37,7 +37,7 @@ pub struct GridRevisionEditor {
     pub(crate) filter_service: Arc<GridFilterService>,
 
     #[allow(dead_code)]
-    pub(crate) group_service: Arc<GridGroupService>,
+    pub(crate) group_service: Arc<RwLock<GridGroupService>>,
 }
 
 impl Drop for GridRevisionEditor {
@@ -62,17 +62,17 @@ impl GridRevisionEditor {
         let block_meta_revs = grid_pad.read().await.get_block_meta_revs();
         let block_manager = Arc::new(GridBlockManager::new(grid_id, &user, block_meta_revs, persistence).await?);
         let filter_service =
-            Arc::new(GridFilterService::new(grid_pad.clone(), block_manager.clone(), task_scheduler.clone()).await);
+            GridFilterService::new(grid_pad.clone(), block_manager.clone(), task_scheduler.clone()).await;
         let group_service =
-            Arc::new(GridGroupService::new(grid_pad.clone(), block_manager.clone(), task_scheduler.clone()).await);
+            GridGroupService::new(grid_pad.clone(), block_manager.clone(), task_scheduler.clone()).await;
         let editor = Arc::new(Self {
             grid_id: grid_id.to_owned(),
             user,
             grid_pad,
             rev_manager,
             block_manager,
-            filter_service,
-            group_service,
+            filter_service: Arc::new(filter_service),
+            group_service: Arc::new(RwLock::new(group_service)),
         });
 
         Ok(editor)
@@ -275,20 +275,8 @@ impl GridRevisionEditor {
     }
 
     pub async fn create_row(&self, start_row_id: Option<String>) -> FlowyResult<RowPB> {
-        let field_revs = self.grid_pad.read().await.get_field_revs(None)?;
-        let block_id = self.block_id().await?;
-
-        // insert empty row below the row whose id is upper_row_id
-        let row_rev = RowRevisionBuilder::new(&block_id, &field_revs).build();
-        let row_order = RowPB::from(&row_rev);
-
-        // insert the row
-        let row_count = self.block_manager.create_row(&block_id, row_rev, start_row_id).await?;
-
-        // update block row count
-        let changeset = GridBlockMetaRevisionChangeset::from_row_count(&block_id, row_count);
-        let _ = self.update_block(changeset).await?;
-        Ok(row_order)
+        let row_rev = self.create_row_rev().await?;
+        self.create_row_pb(row_rev, start_row_id).await
     }
 
     pub async fn insert_rows(&self, row_revs: Vec<RowRevision>) -> FlowyResult<Vec<RowPB>> {
@@ -338,6 +326,7 @@ impl GridRevisionEditor {
 
     pub async fn delete_row(&self, row_id: &str) -> FlowyResult<()> {
         let _ = self.block_manager.delete_row(row_id).await?;
+        self.group_service.read().await.did_delete_card(row_id.to_owned()).await;
         Ok(())
     }
 
@@ -529,10 +518,22 @@ impl GridRevisionEditor {
     }
 
     pub async fn move_row(&self, row_id: &str, from: i32, to: i32) -> FlowyResult<()> {
-        let _ = self.block_manager.move_row(row_id, from as usize, to as usize).await?;
+        match self.block_manager.get_row_rev(row_id).await? {
+            None => tracing::warn!("Move row failed, can not find the row:{}", row_id),
+            Some(row_rev) => {
+                let _ = self
+                    .block_manager
+                    .move_row(row_rev.clone(), from as usize, to as usize)
+                    .await?;
+            }
+        }
         Ok(())
     }
 
+    pub async fn move_board_card(&self, group_id: &str, from: i32, to: i32) -> FlowyResult<()> {
+        self.group_service.write().await.move_card(group_id, from, to).await;
+        Ok(())
+    }
     pub async fn delta_bytes(&self) -> Bytes {
         self.grid_pad.read().await.delta_bytes()
     }
@@ -564,12 +565,48 @@ impl GridRevisionEditor {
         })
     }
 
+    pub async fn create_board_card(&self, group_id: &str) -> FlowyResult<RowPB> {
+        let mut row_rev = self.create_row_rev().await?;
+        let _ = self
+            .group_service
+            .write()
+            .await
+            .update_board_card(&mut row_rev, group_id)
+            .await;
+
+        let row_pb = self.create_row_pb(row_rev, None).await?;
+        self.group_service.read().await.did_create_card(group_id, &row_pb).await;
+        Ok(row_pb)
+    }
+
     #[tracing::instrument(level = "trace", skip_all, err)]
     pub async fn load_groups(&self) -> FlowyResult<RepeatedGridGroupPB> {
-        let groups = self.group_service.load_groups().await.unwrap_or_default();
+        let groups = self.group_service.write().await.load_groups().await.unwrap_or_default();
         Ok(RepeatedGridGroupPB { items: groups })
     }
 
+    async fn create_row_rev(&self) -> FlowyResult<RowRevision> {
+        let field_revs = self.grid_pad.read().await.get_field_revs(None)?;
+        let block_id = self.block_id().await?;
+
+        // insert empty row below the row whose id is upper_row_id
+        let row_rev = RowRevisionBuilder::new(&block_id, &field_revs).build();
+        Ok(row_rev)
+    }
+
+    async fn create_row_pb(&self, row_rev: RowRevision, start_row_id: Option<String>) -> FlowyResult<RowPB> {
+        let row_pb = RowPB::from(&row_rev);
+        let block_id = row_rev.block_id.clone();
+
+        // insert the row
+        let row_count = self.block_manager.create_row(row_rev, start_row_id).await?;
+
+        // update block row count
+        let changeset = GridBlockMetaRevisionChangeset::from_row_count(block_id, row_count);
+        let _ = self.update_block(changeset).await?;
+        Ok(row_pb)
+    }
+
     async fn modify<F>(&self, f: F) -> FlowyResult<()>
     where
         F: for<'a> FnOnce(&'a mut GridRevisionPad) -> FlowyResult<Option<GridChangeset>>,

+ 33 - 10
frontend/rust-lib/flowy-grid/src/services/group/group_generator/checkbox_group.rs

@@ -1,17 +1,48 @@
 use crate::entities::CheckboxGroupConfigurationPB;
+use flowy_error::FlowyResult;
+use flowy_grid_data_model::revision::{FieldRevision, RowRevision};
+use std::sync::Arc;
 
 use crate::services::field::{CheckboxCellData, CheckboxCellDataParser, CheckboxTypeOptionPB, CHECK, UNCHECK};
-use crate::services::group::{Group, GroupAction, GroupCellContentProvider, GroupController, GroupGenerator};
+use crate::services::group::{
+    Group, GroupActionHandler, GroupCellContentProvider, GroupController, GroupGenerator, Groupable,
+};
 
 pub type CheckboxGroupController =
     GroupController<CheckboxGroupConfigurationPB, CheckboxTypeOptionPB, CheckboxGroupGenerator, CheckboxCellDataParser>;
 
+impl Groupable for CheckboxGroupController {
+    type CellDataType = CheckboxCellData;
+
+    fn can_group(&self, _content: &str, _cell_data: &Self::CellDataType) -> bool {
+        false
+    }
+}
+
+impl GroupActionHandler for CheckboxGroupController {
+    fn field_id(&self) -> &str {
+        &self.field_id
+    }
+
+    fn get_groups(&self) -> Vec<Group> {
+        self.make_groups()
+    }
+
+    fn group_rows(&mut self, row_revs: &[Arc<RowRevision>], field_rev: &FieldRevision) -> FlowyResult<()> {
+        self.handle_rows(row_revs, field_rev)
+    }
+
+    fn update_card(&self, _row_rev: &mut RowRevision, _field_rev: &FieldRevision, _group_id: &str) {
+        todo!()
+    }
+}
+
 pub struct CheckboxGroupGenerator();
 impl GroupGenerator for CheckboxGroupGenerator {
     type ConfigurationType = CheckboxGroupConfigurationPB;
     type TypeOptionType = CheckboxTypeOptionPB;
 
-    fn gen_groups(
+    fn generate_groups(
         _configuration: &Option<Self::ConfigurationType>,
         _type_option: &Option<Self::TypeOptionType>,
         _cell_content_provider: &dyn GroupCellContentProvider,
@@ -33,11 +64,3 @@ impl GroupGenerator for CheckboxGroupGenerator {
         vec![check_group, uncheck_group]
     }
 }
-
-impl GroupAction for CheckboxGroupController {
-    type CellDataType = CheckboxCellData;
-
-    fn should_group(&self, _content: &str, _cell_data: &Self::CellDataType) -> bool {
-        false
-    }
-}

+ 71 - 32
frontend/rust-lib/flowy-grid/src/services/group/group_generator/generator.rs

@@ -5,16 +5,12 @@ use flowy_error::FlowyResult;
 use flowy_grid_data_model::revision::{
     FieldRevision, GroupConfigurationRevision, RowRevision, TypeOptionDataDeserializer,
 };
+
 use indexmap::IndexMap;
+
 use std::marker::PhantomData;
 use std::sync::Arc;
 
-pub trait GroupAction {
-    type CellDataType;
-
-    fn should_group(&self, content: &str, cell_data: &Self::CellDataType) -> bool;
-}
-
 pub trait GroupCellContentProvider {
     /// We need to group the rows base on the deduplication cell content when the field type is
     /// RichText.
@@ -27,22 +23,46 @@ pub trait GroupGenerator {
     type ConfigurationType;
     type TypeOptionType;
 
-    fn gen_groups(
+    fn generate_groups(
         configuration: &Option<Self::ConfigurationType>,
         type_option: &Option<Self::TypeOptionType>,
         cell_content_provider: &dyn GroupCellContentProvider,
     ) -> Vec<Group>;
 }
 
-pub struct GroupController<C, T, G, CP> {
-    pub field_rev: Arc<FieldRevision>,
-    pub groups: IndexMap<String, Group>,
+pub trait Groupable {
+    type CellDataType;
+    fn can_group(&self, content: &str, cell_data: &Self::CellDataType) -> bool;
+}
+
+pub trait GroupActionHandler: Send + Sync {
+    fn field_id(&self) -> &str;
+    fn get_groups(&self) -> Vec<Group>;
+    fn group_rows(&mut self, row_revs: &[Arc<RowRevision>], field_rev: &FieldRevision) -> FlowyResult<()>;
+    fn update_card(&self, row_rev: &mut RowRevision, field_rev: &FieldRevision, group_id: &str);
+}
+
+pub trait GroupActionHandler2: Send + Sync {
+    fn create_card(&self, row_rev: &mut RowRevision, field_rev: &FieldRevision, group_id: &str);
+}
+
+const DEFAULT_GROUP_ID: &str = "default_group";
+
+/// C: represents the group configuration structure
+/// T: the type option data deserializer that impl [TypeOptionDataDeserializer]
+/// G: the group container generator
+/// P: the parser that impl [CellBytesParser] for the CellBytes
+pub struct GroupController<C, T, G, P> {
+    pub field_id: String,
+    pub groups_map: IndexMap<String, Group>,
+    default_group: Group,
     pub type_option: Option<T>,
     pub configuration: Option<C>,
     group_action_phantom: PhantomData<G>,
-    cell_parser_phantom: PhantomData<CP>,
+    cell_parser_phantom: PhantomData<P>,
 }
 
+#[derive(Clone)]
 pub struct Group {
     pub id: String,
     pub desc: String,
@@ -60,14 +80,14 @@ impl std::convert::From<Group> for GroupPB {
     }
 }
 
-impl<C, T, G, CP> GroupController<C, T, G, CP>
+impl<C, T, G, P> GroupController<C, T, G, P>
 where
     C: TryFrom<Bytes, Error = protobuf::ProtobufError>,
     T: TypeOptionDataDeserializer,
     G: GroupGenerator<ConfigurationType = C, TypeOptionType = T>,
 {
     pub fn new(
-        field_rev: Arc<FieldRevision>,
+        field_rev: &Arc<FieldRevision>,
         configuration: GroupConfigurationRevision,
         cell_content_provider: &dyn GroupCellContentProvider,
     ) -> FlowyResult<Self> {
@@ -77,10 +97,19 @@ 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::gen_groups(&configuration, &type_option, cell_content_provider);
+        let groups = G::generate_groups(&configuration, &type_option, cell_content_provider);
+
+        let default_group = Group {
+            id: DEFAULT_GROUP_ID.to_owned(),
+            desc: format!("No {}", field_rev.name),
+            rows: vec![],
+            content: "".to_string(),
+        };
+
         Ok(Self {
-            field_rev,
-            groups: groups.into_iter().map(|group| (group.id.clone(), group)).collect(),
+            field_id: field_rev.id.clone(),
+            groups_map: groups.into_iter().map(|group| (group.id.clone(), group)).collect(),
+            default_group,
             type_option,
             configuration,
             group_action_phantom: PhantomData,
@@ -88,42 +117,52 @@ where
         })
     }
 
-    pub fn take_groups(self) -> Vec<Group> {
-        self.groups.into_values().collect()
+    pub fn make_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() {
+            groups.push(default_group);
+        }
+        groups
     }
 }
 
-impl<C, T, G, CP> GroupController<C, T, G, CP>
+impl<C, T, G, P> GroupController<C, T, G, P>
 where
-    CP: CellBytesParser,
-    Self: GroupAction<CellDataType = CP::Object>,
+    P: CellBytesParser,
+    Self: Groupable<CellDataType = P::Object>,
 {
-    pub fn group_rows(&mut self, rows: &[Arc<RowRevision>]) -> FlowyResult<()> {
+    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_rev.id) {
+            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(), &self.field_rev);
-                let cell_data = cell_bytes.parser::<CP>()?;
-                for group in self.groups.values() {
-                    if self.should_group(&group.content, &cell_data) {
+                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(),
                         });
-                        break;
                     }
                 }
 
-                for record in records {
-                    if let Some(group) = self.groups.get_mut(&record.group_id) {
-                        group.rows.push(record.row);
+                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());
             }
         }
 

+ 74 - 17
frontend/rust-lib/flowy-grid/src/services/group/group_generator/select_option_group.rs

@@ -1,9 +1,16 @@
 use crate::entities::SelectOptionGroupConfigurationPB;
+use crate::services::cell::insert_select_option_cell;
+use flowy_error::FlowyResult;
+use flowy_grid_data_model::revision::{FieldRevision, RowRevision};
+
+use std::sync::Arc;
 
 use crate::services::field::{
     MultiSelectTypeOptionPB, SelectOptionCellDataPB, SelectOptionCellDataParser, SingleSelectTypeOptionPB,
 };
-use crate::services::group::{Group, GroupAction, GroupCellContentProvider, GroupController, GroupGenerator};
+use crate::services::group::{
+    Group, GroupActionHandler, GroupCellContentProvider, GroupController, GroupGenerator, Groupable,
+};
 
 // SingleSelect
 pub type SingleSelectGroupController = GroupController<
@@ -13,11 +20,43 @@ pub type SingleSelectGroupController = GroupController<
     SelectOptionCellDataParser,
 >;
 
+impl Groupable for SingleSelectGroupController {
+    type CellDataType = SelectOptionCellDataPB;
+    fn can_group(&self, content: &str, cell_data: &SelectOptionCellDataPB) -> bool {
+        cell_data.select_options.iter().any(|option| option.id == content)
+    }
+}
+
+impl GroupActionHandler for SingleSelectGroupController {
+    fn field_id(&self) -> &str {
+        &self.field_id
+    }
+
+    fn get_groups(&self) -> Vec<Group> {
+        self.make_groups()
+    }
+
+    fn group_rows(&mut self, row_revs: &[Arc<RowRevision>], field_rev: &FieldRevision) -> FlowyResult<()> {
+        self.handle_rows(row_revs, field_rev)
+    }
+
+    fn update_card(&self, row_rev: &mut RowRevision, field_rev: &FieldRevision, group_id: &str) {
+        let group: Option<&Group> = self.groups_map.get(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);
+            }
+        }
+    }
+}
+
 pub struct SingleSelectGroupGenerator();
 impl GroupGenerator for SingleSelectGroupGenerator {
     type ConfigurationType = SelectOptionGroupConfigurationPB;
     type TypeOptionType = SingleSelectTypeOptionPB;
-    fn gen_groups(
+    fn generate_groups(
         _configuration: &Option<Self::ConfigurationType>,
         type_option: &Option<Self::TypeOptionType>,
         _cell_content_provider: &dyn GroupCellContentProvider,
@@ -38,13 +77,6 @@ impl GroupGenerator for SingleSelectGroupGenerator {
     }
 }
 
-impl GroupAction for SingleSelectGroupController {
-    type CellDataType = SelectOptionCellDataPB;
-    fn should_group(&self, content: &str, cell_data: &SelectOptionCellDataPB) -> bool {
-        cell_data.select_options.iter().any(|option| option.id == content)
-    }
-}
-
 // MultiSelect
 pub type MultiSelectGroupController = GroupController<
     SelectOptionGroupConfigurationPB,
@@ -53,12 +85,44 @@ pub type MultiSelectGroupController = GroupController<
     SelectOptionCellDataParser,
 >;
 
+impl Groupable for MultiSelectGroupController {
+    type CellDataType = SelectOptionCellDataPB;
+    fn can_group(&self, content: &str, cell_data: &SelectOptionCellDataPB) -> bool {
+        cell_data.select_options.iter().any(|option| option.id == content)
+    }
+}
+
+impl GroupActionHandler for MultiSelectGroupController {
+    fn field_id(&self) -> &str {
+        &self.field_id
+    }
+
+    fn get_groups(&self) -> Vec<Group> {
+        self.make_groups()
+    }
+
+    fn group_rows(&mut self, row_revs: &[Arc<RowRevision>], field_rev: &FieldRevision) -> FlowyResult<()> {
+        self.handle_rows(row_revs, field_rev)
+    }
+
+    fn update_card(&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),
+            Some(group) => {
+                let cell_rev = insert_select_option_cell(group.id.clone(), field_rev);
+                row_rev.cells.insert(field_rev.id.clone(), cell_rev);
+            }
+        }
+    }
+}
+
 pub struct MultiSelectGroupGenerator();
 impl GroupGenerator for MultiSelectGroupGenerator {
     type ConfigurationType = SelectOptionGroupConfigurationPB;
     type TypeOptionType = MultiSelectTypeOptionPB;
 
-    fn gen_groups(
+    fn generate_groups(
         _configuration: &Option<Self::ConfigurationType>,
         type_option: &Option<Self::TypeOptionType>,
         _cell_content_provider: &dyn GroupCellContentProvider,
@@ -78,10 +142,3 @@ impl GroupGenerator for MultiSelectGroupGenerator {
         }
     }
 }
-
-impl GroupAction for MultiSelectGroupController {
-    type CellDataType = SelectOptionCellDataPB;
-    fn should_group(&self, content: &str, cell_data: &SelectOptionCellDataPB) -> bool {
-        cell_data.select_options.iter().any(|option| option.id == content)
-    }
-}

+ 110 - 54
frontend/rust-lib/flowy-grid/src/services/group/group_service.rs

@@ -1,27 +1,30 @@
+use crate::dart_notification::{send_dart_notification, GridNotification};
+use crate::entities::{
+    BoardCardChangesetPB, CheckboxGroupConfigurationPB, DateGroupConfigurationPB, FieldType, GroupPB,
+    NumberGroupConfigurationPB, RowPB, SelectOptionGroupConfigurationPB, TextGroupConfigurationPB,
+    UrlGroupConfigurationPB,
+};
 use crate::services::block_manager::GridBlockManager;
 use crate::services::grid_editor_task::GridServiceTaskScheduler;
 use crate::services::group::{
-    CheckboxGroupController, Group, GroupCellContentProvider, MultiSelectGroupController, SingleSelectGroupController,
+    CheckboxGroupController, GroupActionHandler, GroupCellContentProvider, MultiSelectGroupController,
+    SingleSelectGroupController,
 };
 
-use crate::entities::{
-    CheckboxGroupConfigurationPB, DateGroupConfigurationPB, FieldType, GroupPB, NumberGroupConfigurationPB,
-    SelectOptionGroupConfigurationPB, TextGroupConfigurationPB, UrlGroupConfigurationPB,
-};
 use bytes::Bytes;
 use flowy_error::FlowyResult;
 use flowy_grid_data_model::revision::{gen_grid_group_id, FieldRevision, GroupConfigurationRevision, RowRevision};
 use flowy_sync::client_grid::GridRevisionPad;
+
 use std::sync::Arc;
 use tokio::sync::RwLock;
 
 pub(crate) struct GridGroupService {
     #[allow(dead_code)]
     scheduler: Arc<dyn GridServiceTaskScheduler>,
-    #[allow(dead_code)]
     grid_pad: Arc<RwLock<GridRevisionPad>>,
-    #[allow(dead_code)]
     block_manager: Arc<GridBlockManager>,
+    group_action_handler: Option<Arc<RwLock<dyn GroupActionHandler>>>,
 }
 
 impl GridGroupService {
@@ -35,14 +38,14 @@ impl GridGroupService {
             scheduler,
             grid_pad,
             block_manager,
+            group_action_handler: None,
         }
     }
 
-    pub(crate) async fn load_groups(&self) -> Option<Vec<GroupPB>> {
-        let grid_pad = self.grid_pad.read().await;
-        let field_rev = find_group_field(grid_pad.fields()).unwrap();
+    pub(crate) async fn load_groups(&mut self) -> Option<Vec<GroupPB>> {
+        let field_rev = find_group_field(self.grid_pad.read().await.fields()).unwrap();
         let field_type: FieldType = field_rev.field_type_rev.into();
-        let configuration = self.get_group_configuration(field_rev).await;
+        let configuration = self.get_group_configuration(&field_rev).await;
 
         let blocks = self.block_manager.get_block_snapshots(None).await.unwrap();
         let row_revs = blocks
@@ -51,19 +54,39 @@ impl GridGroupService {
             .flatten()
             .collect::<Vec<Arc<RowRevision>>>();
 
-        match self.build_groups(&field_type, field_rev, row_revs, configuration) {
+        match self
+            .build_groups(&field_type, &field_rev, row_revs, configuration)
+            .await
+        {
             Ok(groups) => Some(groups),
             Err(_) => None,
         }
     }
 
-    async fn get_group_configuration(&self, field_rev: &FieldRevision) -> GroupConfigurationRevision {
+    #[tracing::instrument(level = "debug", skip(self, row_rev))]
+    pub(crate) async fn update_board_card(&self, row_rev: &mut RowRevision, group_id: &str) {
+        if let Some(group_action_handler) = self.group_action_handler.as_ref() {
+            let field_id = group_action_handler.read().await.field_id().to_owned();
+
+            match self.grid_pad.read().await.get_field_rev(&field_id) {
+                None => tracing::warn!("Fail to create card because the field does not exist"),
+                Some((_, field_rev)) => {
+                    group_action_handler
+                        .write()
+                        .await
+                        .update_card(row_rev, field_rev, group_id);
+                }
+            }
+        }
+    }
+
+    pub(crate) async fn get_group_configuration(&self, field_rev: &FieldRevision) -> GroupConfigurationRevision {
         let grid_pad = self.grid_pad.read().await;
         let setting = grid_pad.get_setting_rev();
         let layout = &setting.layout;
         let configurations = setting.get_groups(layout, &field_rev.id, &field_rev.field_type_rev);
         match configurations {
-            None => self.default_group_configuration(field_rev),
+            None => default_group_configuration(field_rev),
             Some(mut configurations) => {
                 assert_eq!(configurations.len(), 1);
                 (&*configurations.pop().unwrap()).clone()
@@ -71,79 +94,112 @@ impl GridGroupService {
         }
     }
 
-    fn default_group_configuration(&self, field_rev: &FieldRevision) -> GroupConfigurationRevision {
-        let field_type: FieldType = field_rev.field_type_rev.clone().into();
-        let bytes: Bytes = match field_type {
-            FieldType::RichText => TextGroupConfigurationPB::default().try_into().unwrap(),
-            FieldType::Number => NumberGroupConfigurationPB::default().try_into().unwrap(),
-            FieldType::DateTime => DateGroupConfigurationPB::default().try_into().unwrap(),
-            FieldType::SingleSelect => SelectOptionGroupConfigurationPB::default().try_into().unwrap(),
-            FieldType::MultiSelect => SelectOptionGroupConfigurationPB::default().try_into().unwrap(),
-            FieldType::Checkbox => CheckboxGroupConfigurationPB::default().try_into().unwrap(),
-            FieldType::URL => UrlGroupConfigurationPB::default().try_into().unwrap(),
-        };
-        GroupConfigurationRevision {
-            id: gen_grid_group_id(),
-            field_id: field_rev.id.clone(),
-            field_type_rev: field_rev.field_type_rev.clone(),
-            content: Some(bytes.to_vec()),
+    pub async fn move_card(&self, _group_id: &str, _from: i32, _to: i32) {
+        // BoardCardChangesetPB {
+        //     group_id: "".to_string(),
+        //     inserted_cards: vec![],
+        //     deleted_cards: vec![],
+        //     updated_cards: vec![]
+        // }
+        // let row_pb = make_row_from_row_rev(row_rev);
+        todo!()
+    }
+
+    pub async fn did_delete_card(&self, _row_id: String) {
+        // let changeset = BoardCardChangesetPB::delete(group_id.to_owned(), vec![row_id]);
+        // self.notify_did_update_board(changeset).await;
+        todo!()
+    }
+
+    pub async fn did_create_card(&self, group_id: &str, row_pb: &RowPB) {
+        let changeset = BoardCardChangesetPB::insert(group_id.to_owned(), vec![row_pb.clone()]);
+        self.notify_did_update_board(changeset).await;
+    }
+
+    pub async fn notify_did_update_board(&self, changeset: BoardCardChangesetPB) {
+        if self.group_action_handler.is_none() {
+            return;
         }
+        send_dart_notification(&changeset.group_id, GridNotification::DidUpdateBoard)
+            .payload(changeset)
+            .send();
     }
 
     #[tracing::instrument(level = "trace", skip_all, err)]
-    fn build_groups(
-        &self,
+    async fn build_groups(
+        &mut self,
         field_type: &FieldType,
         field_rev: &Arc<FieldRevision>,
         row_revs: Vec<Arc<RowRevision>>,
         configuration: GroupConfigurationRevision,
     ) -> FlowyResult<Vec<GroupPB>> {
-        let groups: Vec<Group> = match field_type {
+        match field_type {
             FieldType::RichText => {
                 // let generator = GroupGenerator::<TextGroupConfigurationPB>::from_configuration(configuration);
-                vec![]
             }
             FieldType::Number => {
                 // let generator = GroupGenerator::<NumberGroupConfigurationPB>::from_configuration(configuration);
-                vec![]
             }
             FieldType::DateTime => {
                 // let generator = GroupGenerator::<DateGroupConfigurationPB>::from_configuration(configuration);
-                vec![]
             }
             FieldType::SingleSelect => {
-                let mut group_controller =
-                    SingleSelectGroupController::new(field_rev.clone(), configuration, &self.grid_pad)?;
-                let _ = group_controller.group_rows(&row_revs)?;
-                group_controller.take_groups()
+                let controller = SingleSelectGroupController::new(field_rev, configuration, &self.grid_pad)?;
+                self.group_action_handler = Some(Arc::new(RwLock::new(controller)));
             }
             FieldType::MultiSelect => {
-                let mut group_controller =
-                    MultiSelectGroupController::new(field_rev.clone(), configuration, &self.grid_pad)?;
-                let _ = group_controller.group_rows(&row_revs)?;
-                group_controller.take_groups()
+                let controller = MultiSelectGroupController::new(field_rev, configuration, &self.grid_pad)?;
+                self.group_action_handler = Some(Arc::new(RwLock::new(controller)));
             }
             FieldType::Checkbox => {
-                let mut group_controller =
-                    CheckboxGroupController::new(field_rev.clone(), configuration, &self.grid_pad)?;
-                let _ = group_controller.group_rows(&row_revs)?;
-                group_controller.take_groups()
+                let controller = CheckboxGroupController::new(field_rev, configuration, &self.grid_pad)?;
+                self.group_action_handler = Some(Arc::new(RwLock::new(controller)));
             }
             FieldType::URL => {
                 // let generator = GroupGenerator::<UrlGroupConfigurationPB>::from_configuration(configuration);
-                vec![]
             }
         };
 
+        let mut groups = vec![];
+        if let Some(group_action_handler) = self.group_action_handler.as_ref() {
+            let mut write_guard = group_action_handler.write().await;
+            let _ = write_guard.group_rows(&row_revs, field_rev)?;
+            groups = write_guard.get_groups();
+            drop(write_guard);
+        }
+
         Ok(groups.into_iter().map(GroupPB::from).collect())
     }
 }
 
-fn find_group_field(field_revs: &[Arc<FieldRevision>]) -> Option<&Arc<FieldRevision>> {
-    field_revs.iter().find(|field_rev| {
-        let field_type: FieldType = field_rev.field_type_rev.into();
-        field_type.can_be_group()
-    })
+fn find_group_field(field_revs: &[Arc<FieldRevision>]) -> Option<Arc<FieldRevision>> {
+    let field_rev = field_revs
+        .iter()
+        .find(|field_rev| {
+            let field_type: FieldType = field_rev.field_type_rev.into();
+            field_type.can_be_group()
+        })
+        .cloned();
+    field_rev
 }
 
 impl GroupCellContentProvider for Arc<RwLock<GridRevisionPad>> {}
+
+fn default_group_configuration(field_rev: &FieldRevision) -> GroupConfigurationRevision {
+    let field_type: FieldType = field_rev.field_type_rev.into();
+    let bytes: Bytes = match field_type {
+        FieldType::RichText => TextGroupConfigurationPB::default().try_into().unwrap(),
+        FieldType::Number => NumberGroupConfigurationPB::default().try_into().unwrap(),
+        FieldType::DateTime => DateGroupConfigurationPB::default().try_into().unwrap(),
+        FieldType::SingleSelect => SelectOptionGroupConfigurationPB::default().try_into().unwrap(),
+        FieldType::MultiSelect => SelectOptionGroupConfigurationPB::default().try_into().unwrap(),
+        FieldType::Checkbox => CheckboxGroupConfigurationPB::default().try_into().unwrap(),
+        FieldType::URL => UrlGroupConfigurationPB::default().try_into().unwrap(),
+    };
+    GroupConfigurationRevision {
+        id: gen_grid_group_id(),
+        field_id: field_rev.id.clone(),
+        field_type_rev: field_rev.field_type_rev,
+        content: Some(bytes.to_vec()),
+    }
+}

+ 50 - 26
frontend/rust-lib/flowy-grid/src/services/row/row_builder.rs

@@ -1,5 +1,8 @@
-use crate::services::cell::apply_cell_data_changeset;
-use crate::services::field::{DateCellChangesetPB, SelectOptionCellChangeset};
+use crate::services::cell::{
+    insert_checkbox_cell, insert_date_cell, insert_number_cell, insert_select_option_cell, insert_text_cell,
+    insert_url_cell,
+};
+
 use flowy_grid_data_model::revision::{gen_row_id, CellRevision, FieldRevision, RowRevision, DEFAULT_ROW_HEIGHT};
 use indexmap::IndexMap;
 use std::collections::HashMap;
@@ -34,47 +37,68 @@ impl<'a> RowRevisionBuilder<'a> {
         }
     }
 
-    pub fn insert_cell(&mut self, field_id: &str, data: String) {
+    pub fn insert_text_cell(&mut self, field_id: &str, data: String) {
         match self.field_rev_map.get(&field_id.to_owned()) {
-            None => {
-                tracing::warn!("Can't find the field with id: {}", field_id);
+            None => tracing::warn!("Can't find the text field with id: {}", field_id),
+            Some(field_rev) => {
+                self.payload
+                    .cell_by_field_id
+                    .insert(field_id.to_owned(), insert_text_cell(data, field_rev));
             }
+        }
+    }
+
+    pub fn insert_url_cell(&mut self, field_id: &str, data: String) {
+        match self.field_rev_map.get(&field_id.to_owned()) {
+            None => tracing::warn!("Can't find the url field with id: {}", field_id),
             Some(field_rev) => {
-                let data = apply_cell_data_changeset(data, None, field_rev).unwrap();
-                let cell = CellRevision::new(data);
-                self.payload.cell_by_field_id.insert(field_id.to_owned(), cell);
+                self.payload
+                    .cell_by_field_id
+                    .insert(field_id.to_owned(), insert_url_cell(data, field_rev));
             }
         }
     }
 
-    pub fn insert_date_cell(&mut self, field_id: &str, timestamp: i64) {
+    pub fn insert_number_cell(&mut self, field_id: &str, num: i64) {
+        match self.field_rev_map.get(&field_id.to_owned()) {
+            None => tracing::warn!("Can't find the number field with id: {}", field_id),
+            Some(field_rev) => {
+                self.payload
+                    .cell_by_field_id
+                    .insert(field_id.to_owned(), insert_number_cell(num, field_rev));
+            }
+        }
+    }
+
+    pub fn insert_checkbox_cell(&mut self, field_id: &str, is_check: bool) {
         match self.field_rev_map.get(&field_id.to_owned()) {
-            None => {
-                tracing::warn!("Invalid field_id: {}", field_id);
+            None => tracing::warn!("Can't find the checkbox field with id: {}", field_id),
+            Some(field_rev) => {
+                self.payload
+                    .cell_by_field_id
+                    .insert(field_id.to_owned(), insert_checkbox_cell(is_check, field_rev));
             }
+        }
+    }
+
+    pub fn insert_date_cell(&mut self, field_id: &str, timestamp: i64) {
+        match self.field_rev_map.get(&field_id.to_owned()) {
+            None => tracing::warn!("Can't find the date field with id: {}", field_id),
             Some(field_rev) => {
-                let cell_data = serde_json::to_string(&DateCellChangesetPB {
-                    date: Some(timestamp.to_string()),
-                    time: None,
-                })
-                .unwrap();
-                let data = apply_cell_data_changeset(cell_data, None, field_rev).unwrap();
-                let cell = CellRevision::new(data);
-                self.payload.cell_by_field_id.insert(field_id.to_owned(), cell);
+                self.payload
+                    .cell_by_field_id
+                    .insert(field_id.to_owned(), insert_date_cell(timestamp, field_rev));
             }
         }
     }
 
     pub fn insert_select_option_cell(&mut self, field_id: &str, data: String) {
         match self.field_rev_map.get(&field_id.to_owned()) {
-            None => {
-                tracing::warn!("Invalid field_id: {}", field_id);
-            }
+            None => tracing::warn!("Can't find the select option field with id: {}", field_id),
             Some(field_rev) => {
-                let cell_data = SelectOptionCellChangeset::from_insert(&data).to_str();
-                let data = apply_cell_data_changeset(cell_data, None, field_rev).unwrap();
-                let cell = CellRevision::new(data);
-                self.payload.cell_by_field_id.insert(field_id.to_owned(), cell);
+                self.payload
+                    .cell_by_field_id
+                    .insert(field_id.to_owned(), insert_select_option_cell(data, field_rev));
             }
         }
     }

+ 2 - 2
frontend/rust-lib/flowy-grid/src/services/row/row_loader.rs

@@ -39,8 +39,8 @@ pub(crate) fn make_row_orders_from_row_revs(row_revs: &[Arc<RowRevision>]) -> Ve
     row_revs.iter().map(RowPB::from).collect::<Vec<_>>()
 }
 
-pub(crate) fn make_row_from_row_rev(row_rev: Arc<RowRevision>) -> Option<RowPB> {
-    make_rows_from_row_revs(&[row_rev]).pop()
+pub(crate) fn make_row_from_row_rev(row_rev: Arc<RowRevision>) -> RowPB {
+    make_rows_from_row_revs(&[row_rev]).pop().unwrap()
 }
 
 pub(crate) fn make_rows_from_row_revs(row_revs: &[Arc<RowRevision>]) -> Vec<RowPB> {

+ 5 - 10
frontend/rust-lib/flowy-grid/src/util.rs

@@ -76,7 +76,7 @@ pub fn make_default_board() -> BuildGridContext {
     let multi_select_type_option = MultiSelectTypeOptionBuilder::default()
         .add_option(banana_option.clone())
         .add_option(apple_option.clone())
-        .add_option(pear_option.clone());
+        .add_option(pear_option);
     let multi_select_field = FieldBuilder::new(multi_select_type_option)
         .name("Fruit")
         .visibility(true)
@@ -114,20 +114,15 @@ pub fn make_default_board() -> BuildGridContext {
         row_builder.insert_select_option_cell(&multi_select_field_id, apple_option.id.clone());
         row_builder.insert_select_option_cell(&multi_select_field_id, banana_option.id.clone());
         // insert text
-        row_builder.insert_cell(&text_field_id, format!("Card {}", i));
+        row_builder.insert_text_cell(&text_field_id, format!("Card {}", i));
         // insert date
         row_builder.insert_date_cell(&date_field_id, timestamp);
         // number
-        row_builder.insert_cell(&number_field_id, format!("{}", i));
+        row_builder.insert_number_cell(&number_field_id, i);
         // checkbox
-        let is_check = if i % 2 == 0 {
-            CHECK.to_string()
-        } else {
-            UNCHECK.to_string()
-        };
-        row_builder.insert_cell(&checkbox_field_id, is_check);
+        row_builder.insert_checkbox_cell(&checkbox_field_id, i % 2 == 0);
         // url
-        row_builder.insert_cell(&url_field_id, "https://appflowy.io".to_string());
+        row_builder.insert_url_cell(&url_field_id, "https://appflowy.io".to_string());
 
         let row = row_builder.build();
         grid_builder.add_row(row);

+ 6 - 5
frontend/rust-lib/flowy-grid/tests/grid/block_test/util.rs

@@ -26,14 +26,14 @@ impl<'a> GridRowTestBuilder<'a> {
 
     pub fn insert_text_cell(&mut self, data: &str) -> String {
         let text_field = self.field_rev_with_type(&FieldType::RichText);
-        self.inner_builder.insert_cell(&text_field.id, data.to_string());
+        self.inner_builder.insert_text_cell(&text_field.id, data.to_string());
 
         text_field.id.clone()
     }
 
     pub fn insert_number_cell(&mut self, data: &str) -> String {
         let number_field = self.field_rev_with_type(&FieldType::Number);
-        self.inner_builder.insert_cell(&number_field.id, data.to_string());
+        self.inner_builder.insert_text_cell(&number_field.id, data.to_string());
         number_field.id.clone()
     }
 
@@ -44,20 +44,21 @@ impl<'a> GridRowTestBuilder<'a> {
         })
         .unwrap();
         let date_field = self.field_rev_with_type(&FieldType::DateTime);
-        self.inner_builder.insert_cell(&date_field.id, value);
+        self.inner_builder.insert_text_cell(&date_field.id, value);
         date_field.id.clone()
     }
 
     pub fn insert_checkbox_cell(&mut self, data: &str) -> String {
         let checkbox_field = self.field_rev_with_type(&FieldType::Checkbox);
-        self.inner_builder.insert_cell(&checkbox_field.id, data.to_string());
+        self.inner_builder
+            .insert_text_cell(&checkbox_field.id, data.to_string());
 
         checkbox_field.id.clone()
     }
 
     pub fn insert_url_cell(&mut self, data: &str) -> String {
         let url_field = self.field_rev_with_type(&FieldType::URL);
-        self.inner_builder.insert_cell(&url_field.id, data.to_string());
+        self.inner_builder.insert_text_cell(&url_field.id, data.to_string());
         url_field.id.clone()
     }
 

+ 3 - 0
shared-lib/flowy-error-code/src/code.rs

@@ -111,6 +111,9 @@ pub enum ErrorCode {
     #[display(fmt = "Field's type option data should not be empty")]
     TypeOptionDataIsEmpty = 450,
 
+    #[display(fmt = "Group id is empty")]
+    GroupIdIsEmpty = 460,
+
     #[display(fmt = "Invalid date time format")]
     InvalidDateTimeFormat = 500,
 

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

@@ -88,9 +88,9 @@ pub struct GridBlockMetaRevisionChangeset {
 }
 
 impl GridBlockMetaRevisionChangeset {
-    pub fn from_row_count(block_id: &str, row_count: i32) -> Self {
+    pub fn from_row_count(block_id: String, row_count: i32) -> Self {
         Self {
-            block_id: block_id.to_string(),
+            block_id,
             start_row_index: None,
             row_count: Some(row_count),
         }

+ 1 - 1
shared-lib/flowy-sync/src/client_grid/grid_revision_pad.rs

@@ -44,7 +44,7 @@ impl GridRevisionPad {
             .blocks
             .iter()
             .map(|block| {
-                let mut duplicated_block = (&*block.clone()).clone();
+                let mut duplicated_block = (&**block).clone();
                 duplicated_block.block_id = gen_block_id();
                 duplicated_block
             })