瀏覽代碼

chore: update board column name

appflowy 2 年之前
父節點
當前提交
82b44c2c98
共有 30 個文件被更改,包括 562 次插入215 次删除
  1. 31 15
      frontend/app_flowy/lib/plugins/board/application/board_bloc.dart
  2. 34 4
      frontend/app_flowy/lib/plugins/board/application/board_data_controller.dart
  3. 50 0
      frontend/app_flowy/lib/plugins/board/application/board_listener.dart
  4. 6 6
      frontend/app_flowy/lib/plugins/board/application/group_controller.dart
  5. 2 2
      frontend/app_flowy/lib/plugins/board/application/group_listener.dart
  6. 4 4
      frontend/app_flowy/lib/plugins/board/presentation/board_page.dart
  7. 24 8
      frontend/app_flowy/packages/appflowy_board/example/lib/multi_board_list_example.dart
  8. 10 6
      frontend/app_flowy/packages/appflowy_board/example/lib/single_board_list_example.dart
  9. 16 4
      frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart
  10. 4 4
      frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column.dart
  11. 35 3
      frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column_data.dart
  12. 17 15
      frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_data.dart
  13. 1 1
      frontend/app_flowy/pubspec.lock
  14. 1 1
      frontend/rust-lib/flowy-grid/src/entities/block_entities.rs
  15. 12 0
      frontend/rust-lib/flowy-grid/src/entities/group_entities/group.rs
  16. 19 3
      frontend/rust-lib/flowy-grid/src/entities/group_entities/group_changeset.rs
  17. 57 45
      frontend/rust-lib/flowy-grid/src/services/grid_editor.rs
  18. 12 7
      frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs
  19. 3 3
      frontend/rust-lib/flowy-grid/src/services/grid_view_manager.rs
  20. 93 24
      frontend/rust-lib/flowy-grid/src/services/group/configuration.rs
  21. 14 6
      frontend/rust-lib/flowy-grid/src/services/group/controller.rs
  22. 12 6
      frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/multi_select_controller.rs
  23. 12 6
      frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/single_select_controller.rs
  24. 30 15
      frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/util.rs
  25. 6 17
      frontend/rust-lib/flowy-grid/src/services/group/entities.rs
  26. 15 6
      frontend/rust-lib/flowy-grid/src/services/group/group_service.rs
  27. 12 1
      frontend/rust-lib/flowy-grid/tests/grid/group_test/script.rs
  28. 23 0
      frontend/rust-lib/flowy-grid/tests/grid/group_test/test.rs
  29. 2 2
      shared-lib/flowy-grid-data-model/src/revision/grid_block.rs
  30. 5 1
      shared-lib/flowy-grid-data-model/src/revision/group_rev.rs

+ 31 - 15
frontend/app_flowy/lib/plugins/board/application/board_bloc.dart

@@ -20,19 +20,19 @@ import 'group_controller.dart';
 part 'board_bloc.freezed.dart';
 
 class BoardBloc extends Bloc<BoardEvent, BoardState> {
-  final BoardDataController _dataController;
-  late final AFBoardDataController afBoardDataController;
+  final BoardDataController _gridDataController;
+  late final AFBoardDataController boardController;
   final MoveRowFFIService _rowService;
   LinkedHashMap<String, GroupController> groupControllers = LinkedHashMap.new();
 
-  GridFieldCache get fieldCache => _dataController.fieldCache;
-  String get gridId => _dataController.gridId;
+  GridFieldCache get fieldCache => _gridDataController.fieldCache;
+  String get gridId => _gridDataController.gridId;
 
   BoardBloc({required ViewPB view})
       : _rowService = MoveRowFFIService(gridId: view.id),
-        _dataController = BoardDataController(view: view),
+        _gridDataController = BoardDataController(view: view),
         super(BoardState.initial(view.id)) {
-    afBoardDataController = AFBoardDataController(
+    boardController = AFBoardDataController(
       onMoveColumn: (
         fromColumnId,
         fromIndex,
@@ -70,7 +70,7 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
             await _loadGrid(emit);
           },
           createRow: (groupId) async {
-            final result = await _dataController.createBoardCard(groupId);
+            final result = await _gridDataController.createBoardCard(groupId);
             result.fold(
               (rowPB) {
                 emit(state.copyWith(editingRow: some(rowPB)));
@@ -126,7 +126,7 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
 
   @override
   Future<void> close() async {
-    await _dataController.dispose();
+    await _gridDataController.dispose();
     for (final controller in groupControllers.values) {
       controller.dispose();
     }
@@ -135,7 +135,7 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
 
   void initializeGroups(List<GroupPB> groups) {
     for (final group in groups) {
-      final delegate = GroupControllerDelegateImpl(afBoardDataController);
+      final delegate = GroupControllerDelegateImpl(boardController);
       final controller = GroupController(
         gridId: state.gridId,
         group: group,
@@ -147,12 +147,12 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
   }
 
   GridRowCache? getRowCache(String blockId) {
-    final GridBlockCache? blockCache = _dataController.blocks[blockId];
+    final GridBlockCache? blockCache = _gridDataController.blocks[blockId];
     return blockCache?.rowCache;
   }
 
   void _startListening() {
-    _dataController.addListener(
+    _gridDataController.addListener(
       onGridChanged: (grid) {
         if (!isClosed) {
           add(BoardEvent.didReceiveGridUpdate(grid));
@@ -162,18 +162,34 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
         List<AFBoardColumnData> columns = groups.map((group) {
           return AFBoardColumnData(
             id: group.groupId,
-            desc: group.desc,
+            name: group.desc,
             items: _buildRows(group.rows),
             customData: group,
           );
         }).toList();
 
-        afBoardDataController.addColumns(columns);
+        boardController.addColumns(columns);
         initializeGroups(groups);
       },
       onRowsChanged: (List<RowInfo> rowInfos, RowsChangedReason reason) {
         add(BoardEvent.didReceiveRows(rowInfos));
       },
+      onDeletedGroup: (groupIds) {
+        //
+      },
+      onInsertedGroup: (insertedGroups) {
+        //
+      },
+      onUpdatedGroup: (updatedGroups) {
+        //
+        for (final group in updatedGroups) {
+          final columnController =
+              boardController.getColumnController(group.groupId);
+          if (columnController != null) {
+            columnController.updateColumnName(group.desc);
+          }
+        }
+      },
       onError: (err) {
         Log.error(err);
       },
@@ -189,7 +205,7 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
   }
 
   Future<void> _loadGrid(Emitter<BoardState> emit) async {
-    final result = await _dataController.loadData();
+    final result = await _gridDataController.loadData();
     result.fold(
       (grid) => emit(
         state.copyWith(loadingState: GridLoadingState.finish(left(unit))),
@@ -301,6 +317,6 @@ class GroupControllerDelegateImpl extends GroupControllerDelegate {
 
   @override
   void updateRow(String groupId, RowPB row) {
-    //
+    controller.updateColumnItem(groupId, BoardColumnItem(row: row));
   }
 }

+ 34 - 4
frontend/app_flowy/lib/plugins/board/application/board_data_controller.dart

@@ -10,9 +10,15 @@ import 'dart:async';
 import 'package:dartz/dartz.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid/protobuf.dart';
 
+import 'board_listener.dart';
+
 typedef OnFieldsChanged = void Function(UnmodifiableListView<FieldPB>);
 typedef OnGridChanged = void Function(GridPB);
 typedef DidLoadGroups = void Function(List<GroupPB>);
+typedef OnUpdatedGroup = void Function(List<GroupPB>);
+typedef OnDeletedGroup = void Function(List<String>);
+typedef OnInsertedGroup = void Function(List<InsertedGroupPB>);
+
 typedef OnRowsChanged = void Function(
   List<RowInfo>,
   RowsChangedReason,
@@ -23,6 +29,7 @@ class BoardDataController {
   final String gridId;
   final GridFFIService _gridFFIService;
   final GridFieldCache fieldCache;
+  final BoardListener _listener;
 
   // key: the block id
   final LinkedHashMap<String, GridBlockCache> _blocks;
@@ -44,16 +51,20 @@ class BoardDataController {
 
   BoardDataController({required ViewPB view})
       : gridId = view.id,
+        _listener = BoardListener(view.id),
         _blocks = LinkedHashMap.new(),
         _gridFFIService = GridFFIService(gridId: view.id),
         fieldCache = GridFieldCache(gridId: view.id);
 
   void addListener({
-    OnGridChanged? onGridChanged,
+    required OnGridChanged onGridChanged,
     OnFieldsChanged? onFieldsChanged,
-    DidLoadGroups? didLoadGroups,
-    OnRowsChanged? onRowsChanged,
-    OnError? onError,
+    required DidLoadGroups didLoadGroups,
+    required OnRowsChanged onRowsChanged,
+    required OnUpdatedGroup onUpdatedGroup,
+    required OnDeletedGroup onDeletedGroup,
+    required OnInsertedGroup onInsertedGroup,
+    required OnError? onError,
   }) {
     _onGridChanged = onGridChanged;
     _onFieldsChanged = onFieldsChanged;
@@ -64,6 +75,25 @@ class BoardDataController {
     fieldCache.addListener(onFields: (fields) {
       _onFieldsChanged?.call(UnmodifiableListView(fields));
     });
+
+    _listener.start(onBoardChanged: (result) {
+      result.fold(
+        (changeset) {
+          if (changeset.updateGroups.isNotEmpty) {
+            onUpdatedGroup.call(changeset.updateGroups);
+          }
+
+          if (changeset.insertedGroups.isNotEmpty) {
+            onInsertedGroup.call(changeset.insertedGroups);
+          }
+
+          if (changeset.deletedGroups.isNotEmpty) {
+            onDeletedGroup.call(changeset.deletedGroups);
+          }
+        },
+        (e) => _onError?.call(e),
+      );
+    });
   }
 
   Future<Either<Unit, FlowyError>> loadData() async {

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

@@ -0,0 +1,50 @@
+import 'dart:typed_data';
+
+import 'package:app_flowy/core/grid_notification.dart';
+import 'package:flowy_infra/notifier.dart';
+import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/dart_notification.pb.dart';
+import 'package:dartz/dartz.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/group_changeset.pb.dart';
+
+typedef UpdateBoardNotifiedValue = Either<GroupViewChangesetPB, FlowyError>;
+
+class BoardListener {
+  final String viewId;
+  PublishNotifier<UpdateBoardNotifiedValue>? _groupNotifier = PublishNotifier();
+  GridNotificationListener? _listener;
+  BoardListener(this.viewId);
+
+  void start({
+    required void Function(UpdateBoardNotifiedValue) onBoardChanged,
+  }) {
+    _groupNotifier?.addPublishListener(onBoardChanged);
+    _listener = GridNotificationListener(
+      objectId: viewId,
+      handler: _handler,
+    );
+  }
+
+  void _handler(
+    GridNotification ty,
+    Either<Uint8List, FlowyError> result,
+  ) {
+    switch (ty) {
+      case GridNotification.DidUpdateGroupView:
+        result.fold(
+          (payload) => _groupNotifier?.value =
+              left(GroupViewChangesetPB.fromBuffer(payload)),
+          (error) => _groupNotifier?.value = right(error),
+        );
+        break;
+      default:
+        break;
+    }
+  }
+
+  Future<void> stop() async {
+    await _listener?.stop();
+    _groupNotifier?.dispose();
+    _groupNotifier = null;
+  }
+}

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

@@ -34,7 +34,12 @@ class GroupController {
   void startListening() {
     _listener.start(onGroupChanged: (result) {
       result.fold(
-        (GroupRowsChangesetPB changeset) {
+        (GroupChangesetPB changeset) {
+          for (final deletedRow in changeset.deletedRows) {
+            group.rows.removeWhere((rowPB) => rowPB.id == deletedRow);
+            delegate.removeRow(group.groupId, deletedRow);
+          }
+
           for (final insertedRow in changeset.insertedRows) {
             final index = insertedRow.hasIndex() ? insertedRow.index : null;
 
@@ -52,11 +57,6 @@ class GroupController {
             );
           }
 
-          for (final deletedRow in changeset.deletedRows) {
-            group.rows.removeWhere((rowPB) => rowPB.id == deletedRow);
-            delegate.removeRow(group.groupId, deletedRow);
-          }
-
           for (final updatedRow in changeset.updatedRows) {
             final index = group.rows.indexWhere(
               (rowPB) => rowPB.id == updatedRow.id,

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

@@ -8,7 +8,7 @@ import 'package:flowy_sdk/protobuf/flowy-grid/group.pb.dart';
 import 'package:dartz/dartz.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid/group_changeset.pb.dart';
 
-typedef UpdateGroupNotifiedValue = Either<GroupRowsChangesetPB, FlowyError>;
+typedef UpdateGroupNotifiedValue = Either<GroupChangesetPB, FlowyError>;
 
 class GroupListener {
   final GroupPB group;
@@ -34,7 +34,7 @@ class GroupListener {
       case GridNotification.DidUpdateGroup:
         result.fold(
           (payload) => _groupNotifier?.value =
-              left(GroupRowsChangesetPB.fromBuffer(payload)),
+              left(GroupChangesetPB.fromBuffer(payload)),
           (error) => _groupNotifier?.value = right(error),
         );
         break;

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

@@ -62,9 +62,8 @@ class BoardContent extends StatelessWidget {
           child: Padding(
             padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 20),
             child: AFBoard(
-              // key: UniqueKey(),
               scrollController: ScrollController(),
-              dataController: context.read<BoardBloc>().afBoardDataController,
+              dataController: context.read<BoardBloc>().boardController,
               headerBuilder: _buildHeader,
               footBuilder: _buildFooter,
               cardBuilder: (_, data) => _buildCard(context, data),
@@ -79,10 +78,11 @@ class BoardContent extends StatelessWidget {
     );
   }
 
-  Widget _buildHeader(BuildContext context, AFBoardColumnData columnData) {
+  Widget _buildHeader(
+      BuildContext context, AFBoardColumnHeaderData headerData) {
     return AppFlowyColumnHeader(
       icon: const Icon(Icons.lightbulb_circle),
-      title: Text(columnData.desc),
+      title: Text(headerData.columnName),
       addIcon: const Icon(Icons.add, size: 20),
       moreIcon: const Icon(Icons.more_horiz, size: 20),
       height: 50,

+ 24 - 8
frontend/app_flowy/packages/appflowy_board/example/lib/multi_board_list_example.dart

@@ -34,13 +34,18 @@ class _MultiBoardListExampleState extends State<MultiBoardListExample> {
       RichTextItem(title: "Card 8", subtitle: 'Aug 1, 2020 4:05 PM'),
       TextItem("Card 9"),
     ];
-    final column1 = AFBoardColumnData(id: "To Do", items: a);
-    final column2 = AFBoardColumnData(id: "In Progress", items: <AFColumnItem>[
-      RichTextItem(title: "Card 10", subtitle: 'Aug 1, 2020 4:05 PM'),
-      TextItem("Card 11"),
-    ]);
+    final column1 = AFBoardColumnData(id: "To Do", name: "To Do", items: a);
+    final column2 = AFBoardColumnData(
+      id: "In Progress",
+      name: "In Progress",
+      items: <AFColumnItem>[
+        RichTextItem(title: "Card 10", subtitle: 'Aug 1, 2020 4:05 PM'),
+        TextItem("Card 11"),
+      ],
+    );
 
-    final column3 = AFBoardColumnData(id: "Done", items: <AFColumnItem>[]);
+    final column3 =
+        AFBoardColumnData(id: "Done", name: "Done", items: <AFColumnItem>[]);
 
     boardDataController.addColumn(column1);
     boardDataController.addColumn(column2);
@@ -68,10 +73,21 @@ class _MultiBoardListExampleState extends State<MultiBoardListExample> {
               margin: config.columnItemPadding,
             );
           },
-          headerBuilder: (context, columnData) {
+          headerBuilder: (context, headerData) {
             return AppFlowyColumnHeader(
               icon: const Icon(Icons.lightbulb_circle),
-              title: Text(columnData.id),
+              title: SizedBox(
+                width: 60,
+                child: TextField(
+                  controller: TextEditingController()
+                    ..text = headerData.columnName,
+                  onSubmitted: (val) {
+                    boardDataController
+                        .getColumnController(headerData.columnId)!
+                        .updateColumnName(val);
+                  },
+                ),
+              ),
               addIcon: const Icon(Icons.add, size: 20),
               moreIcon: const Icon(Icons.more_horiz, size: 20),
               height: 50,

+ 10 - 6
frontend/app_flowy/packages/appflowy_board/example/lib/single_board_list_example.dart

@@ -13,12 +13,16 @@ class _SingleBoardListExampleState extends State<SingleBoardListExample> {
 
   @override
   void initState() {
-    final column = AFBoardColumnData(id: "1", items: [
-      TextItem("a"),
-      TextItem("b"),
-      TextItem("c"),
-      TextItem("d"),
-    ]);
+    final column = AFBoardColumnData(
+      id: "1",
+      name: "1",
+      items: [
+        TextItem("a"),
+        TextItem("b"),
+        TextItem("c"),
+        TextItem("d"),
+      ],
+    );
 
     boardData.addColumn(column);
     super.initState();

+ 16 - 4
frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart

@@ -205,13 +205,13 @@ class _BoardContentState extends State<BoardContent> {
 
         return ChangeNotifierProvider.value(
           key: ValueKey(columnData.id),
-          value: widget.dataController.columnController(columnData.id),
+          value: widget.dataController.getColumnController(columnData.id),
           child: Consumer<AFBoardColumnDataController>(
             builder: (context, value, child) {
               final boardColumn = AFBoardColumnWidget(
                 margin: _marginFromIndex(columnIndex),
                 itemMargin: widget.config.columnItemPadding,
-                headerBuilder: widget.headerBuilder,
+                headerBuilder: _buildHeader,
                 footBuilder: widget.footBuilder,
                 cardBuilder: widget.cardBuilder,
                 dataSource: dataSource,
@@ -224,7 +224,6 @@ class _BoardContentState extends State<BoardContent> {
 
               // columnKeys
               //     .removeWhere((element) => element.columnId == columnData.id);
-
               // columnKeys.add(
               //   ColumnKey(
               //     columnId: columnData.id,
@@ -245,6 +244,19 @@ class _BoardContentState extends State<BoardContent> {
     return children;
   }
 
+  Widget? _buildHeader(
+      BuildContext context, AFBoardColumnHeaderData headerData) {
+    if (widget.headerBuilder == null) {
+      return null;
+    }
+    return Selector<AFBoardColumnDataController, AFBoardColumnHeaderData>(
+      selector: (context, controller) => controller.columnData.headerData,
+      builder: (context, headerData, _) {
+        return widget.headerBuilder!(context, headerData)!;
+      },
+    );
+  }
+
   EdgeInsets _marginFromIndex(int index) {
     if (widget.dataController.columnDatas.isEmpty) {
       return widget.config.columnPadding;
@@ -273,7 +285,7 @@ class _BoardColumnDataSourceImpl extends AFBoardColumnDataDataSource {
 
   @override
   AFBoardColumnData get columnData =>
-      dataController.columnController(columnId).columnData;
+      dataController.getColumnController(columnId)!.columnData;
 
   @override
   List<String> get acceptedColumnIds => dataController.columnIds;

+ 4 - 4
frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column.dart

@@ -27,9 +27,9 @@ typedef AFBoardColumnCardBuilder = Widget Function(
   AFColumnItem item,
 );
 
-typedef AFBoardColumnHeaderBuilder = Widget Function(
+typedef AFBoardColumnHeaderBuilder = Widget? Function(
   BuildContext context,
-  AFBoardColumnData columnData,
+  AFBoardColumnHeaderData headerData,
 );
 
 typedef AFBoardColumnFooterBuilder = Widget Function(
@@ -125,8 +125,8 @@ class _AFBoardColumnWidgetState extends State<AFBoardColumnWidget> {
             .map((item) => _buildWidget(context, item))
             .toList();
 
-        final header =
-            widget.headerBuilder?.call(context, widget.dataSource.columnData);
+        final header = widget.headerBuilder
+            ?.call(context, widget.dataSource.columnData.headerData);
 
         final footer =
             widget.footBuilder?.call(context, widget.dataSource.columnData);

+ 35 - 3
frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column_data.dart

@@ -34,6 +34,13 @@ class AFBoardColumnDataController extends ChangeNotifier with EquatableMixin {
   UnmodifiableListView<AFColumnItem> get items =>
       UnmodifiableListView(columnData.items);
 
+  void updateColumnName(String newName) {
+    if (columnData.headerData.columnName != newName) {
+      columnData.headerData.columnName = newName;
+      notifyListeners();
+    }
+  }
+
   /// Remove the item at [index].
   /// * [index] the index of the item you want to remove
   /// * [notify] the default value of [notify] is true, it will notify the
@@ -123,6 +130,20 @@ class AFBoardColumnDataController extends ChangeNotifier with EquatableMixin {
     notifyListeners();
   }
 
+  void replaceOrInsertItem(AFColumnItem newItem) {
+    final index = columnData._items.indexWhere((item) => item.id == newItem.id);
+    if (index != -1) {
+      removeAt(index);
+
+      columnData._items.removeAt(index);
+      columnData._items.insert(index, newItem);
+      notifyListeners();
+    } else {
+      columnData._items.add(newItem);
+      notifyListeners();
+    }
+  }
+
   bool _containsItem(AFColumnItem item) {
     return columnData._items.indexWhere((element) => element.id == item.id) !=
         -1;
@@ -133,16 +154,20 @@ class AFBoardColumnDataController extends ChangeNotifier with EquatableMixin {
 class AFBoardColumnData<CustomData> extends ReoderFlexItem with EquatableMixin {
   @override
   final String id;
-  final String desc;
+  AFBoardColumnHeaderData headerData;
   final List<AFColumnItem> _items;
   final CustomData? customData;
 
   AFBoardColumnData({
     this.customData,
     required this.id,
-    this.desc = "",
+    required String name,
     List<AFColumnItem> items = const [],
-  }) : _items = items;
+  })  : _items = items,
+        headerData = AFBoardColumnHeaderData(
+          columnId: id,
+          columnName: name,
+        );
 
   /// Returns the readonly List<ColumnItem>
   UnmodifiableListView<AFColumnItem> get items =>
@@ -156,3 +181,10 @@ class AFBoardColumnData<CustomData> extends ReoderFlexItem with EquatableMixin {
     return 'Column:[$id]';
   }
 }
+
+class AFBoardColumnHeaderData {
+  String columnId;
+  String columnName;
+
+  AFBoardColumnHeaderData({required this.columnId, required this.columnName});
+}

+ 17 - 15
frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_data.dart

@@ -89,10 +89,6 @@ class AFBoardDataController extends ChangeNotifier
     if (columnIds.isNotEmpty && notify) notifyListeners();
   }
 
-  AFBoardColumnDataController columnController(String columnId) {
-    return _columnControllers[columnId]!;
-  }
-
   AFBoardColumnDataController? getColumnController(String columnId) {
     final columnController = _columnControllers[columnId];
     if (columnController == null) {
@@ -129,6 +125,10 @@ class AFBoardDataController extends ChangeNotifier
     getColumnController(columnId)?.removeWhere((item) => item.id == itemId);
   }
 
+  void updateColumnItem(String columnId, AFColumnItem item) {
+    getColumnController(columnId)?.replaceOrInsertItem(item);
+  }
+
   @override
   @protected
   void swapColumnItem(
@@ -137,15 +137,14 @@ class AFBoardDataController extends ChangeNotifier
     String toColumnId,
     int toColumnIndex,
   ) {
-    final item = columnController(fromColumnId).removeAt(fromColumnIndex);
-
-    if (columnController(toColumnId).items.length > toColumnIndex) {
-      assert(columnController(toColumnId).items[toColumnIndex]
-          is PhantomColumnItem);
+    final fromColumnController = getColumnController(fromColumnId)!;
+    final toColumnController = getColumnController(toColumnId)!;
+    final item = fromColumnController.removeAt(fromColumnIndex);
+    if (toColumnController.items.length > toColumnIndex) {
+      assert(toColumnController.items[toColumnIndex] is PhantomColumnItem);
     }
 
-    columnController(toColumnId).replace(toColumnIndex, item);
-
+    toColumnController.replace(toColumnIndex, item);
     onMoveColumnItemToColumn?.call(
       fromColumnId,
       fromColumnIndex,
@@ -174,9 +173,12 @@ class AFBoardDataController extends ChangeNotifier
   @override
   @protected
   bool removePhantom(String columnId) {
-    final columnController = this.columnController(columnId);
+    final columnController = getColumnController(columnId);
+    if (columnController == null) {
+      Log.warn('Can not find the column controller with columnId: $columnId');
+      return false;
+    }
     final index = columnController.items.indexWhere((item) => item.isPhantom);
-
     final isExist = index != -1;
     if (isExist) {
       columnController.removeAt(index);
@@ -190,7 +192,7 @@ class AFBoardDataController extends ChangeNotifier
   @override
   @protected
   void updatePhantom(String columnId, int newIndex) {
-    final columnDataController = columnController(columnId);
+    final columnDataController = getColumnController(columnId)!;
     final index =
         columnDataController.items.indexWhere((item) => item.isPhantom);
 
@@ -208,6 +210,6 @@ class AFBoardDataController extends ChangeNotifier
   @override
   @protected
   void insertPhantom(String columnId, int index, PhantomColumnItem item) {
-    columnController(columnId).insert(index, item);
+    getColumnController(columnId)!.insert(index, item);
   }
 }

+ 1 - 1
frontend/app_flowy/pubspec.lock

@@ -28,7 +28,7 @@ packages:
       path: "packages/appflowy_board"
       relative: true
     source: path
-    version: "0.0.4"
+    version: "0.0.5"
   appflowy_editor:
     dependency: "direct main"
     description:

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

@@ -30,7 +30,7 @@ impl BlockPB {
 }
 
 /// [RowPB] Describes a row. Has the id of the parent Block. Has the metadata of the row.
-#[derive(Debug, Default, Clone, ProtoBuf)]
+#[derive(Debug, Default, Clone, ProtoBuf, Eq, PartialEq)]
 pub struct RowPB {
     #[pb(index = 1)]
     pub block_id: String,

+ 12 - 0
frontend/rust-lib/flowy-grid/src/entities/group_entities/group.rs

@@ -1,4 +1,5 @@
 use crate::entities::{CreateRowParams, FieldType, GridLayout, RowPB};
+use crate::services::group::Group;
 use flowy_derive::ProtoBuf;
 use flowy_error::ErrorCode;
 use flowy_grid_data_model::parser::NotEmptyStr;
@@ -82,6 +83,17 @@ pub struct GroupPB {
     pub rows: Vec<RowPB>,
 }
 
+impl std::convert::From<Group> for GroupPB {
+    fn from(group: Group) -> Self {
+        Self {
+            field_id: group.field_id,
+            group_id: group.id,
+            desc: group.name,
+            rows: group.rows,
+        }
+    }
+}
+
 #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)]
 pub struct RepeatedGridGroupConfigurationPB {
     #[pb(index = 1)]

+ 19 - 3
frontend/rust-lib/flowy-grid/src/entities/group_entities/group_changeset.rs

@@ -1,5 +1,4 @@
 use crate::entities::{GroupPB, InsertedRowPB, RowPB};
-use diesel::insertable::ColumnInsertValue::Default;
 use flowy_derive::ProtoBuf;
 use flowy_error::ErrorCode;
 use flowy_grid_data_model::parser::NotEmptyStr;
@@ -42,7 +41,17 @@ impl std::fmt::Display for GroupChangesetPB {
 
 impl GroupChangesetPB {
     pub fn is_empty(&self) -> bool {
-        self.inserted_rows.is_empty() && self.deleted_rows.is_empty() && self.updated_rows.is_empty()
+        self.group_name.is_none()
+            && self.inserted_rows.is_empty()
+            && self.deleted_rows.is_empty()
+            && self.updated_rows.is_empty()
+    }
+
+    pub fn new(group_id: String) -> Self {
+        Self {
+            group_id,
+            ..Default::default()
+        }
     }
 
     pub fn name(group_id: String, name: &str) -> Self {
@@ -126,9 +135,16 @@ pub struct GroupViewChangesetPB {
 
     #[pb(index = 3)]
     pub deleted_groups: Vec<String>,
+
+    #[pb(index = 4)]
+    pub update_groups: Vec<GroupPB>,
 }
 
-impl GroupViewChangesetPB {}
+impl GroupViewChangesetPB {
+    pub fn is_empty(&self) -> bool {
+        self.inserted_groups.is_empty() && self.deleted_groups.is_empty() && self.update_groups.is_empty()
+    }
+}
 
 #[derive(Debug, Default, ProtoBuf)]
 pub struct InsertedGroupPB {

+ 57 - 45
frontend/rust-lib/flowy-grid/src/services/grid_editor.rs

@@ -188,8 +188,13 @@ impl GridRevisionEditor {
     pub async fn replace_field(&self, field_rev: Arc<FieldRevision>) -> FlowyResult<()> {
         let field_id = field_rev.id.clone();
         let _ = self
-            .modify(|grid_pad| Ok(grid_pad.replace_field_rev(field_rev)?))
+            .modify(|grid_pad| Ok(grid_pad.replace_field_rev(field_rev.clone())?))
             .await?;
+
+        match self.view_manager.did_update_field(&field_rev.id).await {
+            Ok(_) => {}
+            Err(e) => tracing::error!("View manager update field failed: {:?}", e),
+        }
         let _ = self.notify_did_update_grid_field(&field_id).await?;
         Ok(())
     }
@@ -263,59 +268,65 @@ impl GridRevisionEditor {
     }
 
     async fn update_field_rev(&self, params: FieldChangesetParams, field_type: FieldType) -> FlowyResult<()> {
-        self.modify(|grid| {
-            let deserializer = TypeOptionJsonDeserializer(field_type);
-
-            let changeset = grid.modify_field(&params.field_id, |field| {
-                let mut is_changed = None;
-                if let Some(name) = params.name {
-                    field.name = name;
-                    is_changed = Some(())
-                }
+        let _ = self
+            .modify(|grid| {
+                let deserializer = TypeOptionJsonDeserializer(field_type);
+                let changeset = grid.modify_field(&params.field_id, |field| {
+                    let mut is_changed = None;
+                    if let Some(name) = params.name {
+                        field.name = name;
+                        is_changed = Some(())
+                    }
 
-                if let Some(desc) = params.desc {
-                    field.desc = desc;
-                    is_changed = Some(())
-                }
+                    if let Some(desc) = params.desc {
+                        field.desc = desc;
+                        is_changed = Some(())
+                    }
 
-                if let Some(field_type) = params.field_type {
-                    field.ty = field_type;
-                    is_changed = Some(())
-                }
+                    if let Some(field_type) = params.field_type {
+                        field.ty = field_type;
+                        is_changed = Some(())
+                    }
 
-                if let Some(frozen) = params.frozen {
-                    field.frozen = frozen;
-                    is_changed = Some(())
-                }
+                    if let Some(frozen) = params.frozen {
+                        field.frozen = frozen;
+                        is_changed = Some(())
+                    }
 
-                if let Some(visibility) = params.visibility {
-                    field.visibility = visibility;
-                    is_changed = Some(())
-                }
+                    if let Some(visibility) = params.visibility {
+                        field.visibility = visibility;
+                        is_changed = Some(())
+                    }
 
-                if let Some(width) = params.width {
-                    field.width = width;
-                    is_changed = Some(())
-                }
+                    if let Some(width) = params.width {
+                        field.width = width;
+                        is_changed = Some(())
+                    }
 
-                if let Some(type_option_data) = params.type_option_data {
-                    match deserializer.deserialize(type_option_data) {
-                        Ok(json_str) => {
-                            let field_type = field.ty;
-                            field.insert_type_option_str(&field_type, json_str);
-                            is_changed = Some(())
-                        }
-                        Err(err) => {
-                            tracing::error!("Deserialize data to type option json failed: {}", err);
+                    if let Some(type_option_data) = params.type_option_data {
+                        match deserializer.deserialize(type_option_data) {
+                            Ok(json_str) => {
+                                let field_type = field.ty;
+                                field.insert_type_option_str(&field_type, json_str);
+                                is_changed = Some(())
+                            }
+                            Err(err) => {
+                                tracing::error!("Deserialize data to type option json failed: {}", err);
+                            }
                         }
                     }
-                }
 
-                Ok(is_changed)
-            })?;
-            Ok(changeset)
-        })
-        .await
+                    Ok(is_changed)
+                })?;
+                Ok(changeset)
+            })
+            .await?;
+
+        match self.view_manager.did_update_field(&params.field_id).await {
+            Ok(_) => {}
+            Err(e) => tracing::error!("View manager update field failed: {:?}", e),
+        }
+        Ok(())
     }
 
     pub async fn create_block(&self, block_meta_rev: GridBlockMetaRevision) -> FlowyResult<()> {
@@ -585,6 +596,7 @@ impl GridRevisionEditor {
                     .move_group_row(row_rev, to_group_id, to_row_id.clone())
                     .await
                 {
+                    tracing::trace!("Move group row cause row data changed: {:?}", row_changeset);
                     match self.block_manager.update_row(row_changeset).await {
                         Ok(_) => {}
                         Err(e) => {

+ 12 - 7
frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs

@@ -59,7 +59,7 @@ impl GridViewRevisionEditor {
             rev_manager: rev_manager.clone(),
             view_pad: pad.clone(),
         };
-        let group_service = GroupService::new(configuration_reader, configuration_writer).await;
+        let group_service = GroupService::new(view_id.clone(), configuration_reader, configuration_writer).await;
         let user_id = user_id.to_owned();
         let did_load_group = AtomicBool::new(false);
         Ok(Self {
@@ -155,7 +155,7 @@ impl GridViewRevisionEditor {
             }
         }
     }
-
+    /// Only call once after grid view editor initialized
     #[tracing::instrument(level = "trace", skip(self))]
     pub(crate) async fn load_groups(&self) -> FlowyResult<Vec<GroupPB>> {
         let groups = if !self.did_load_group.load(Ordering::SeqCst) {
@@ -198,9 +198,10 @@ impl GridViewRevisionEditor {
                 };
 
                 let changeset = GroupViewChangesetPB {
-                    view_id: "".to_string(),
+                    view_id: self.view_id.clone(),
                     inserted_groups: vec![inserted_group],
                     deleted_groups: vec![params.from_group_id.clone()],
+                    update_groups: vec![],
                 };
 
                 self.notify_did_update_view(changeset).await;
@@ -252,10 +253,15 @@ impl GridViewRevisionEditor {
         })
         .await
     }
-
+    #[tracing::instrument(level = "trace", skip_all, err)]
     pub(crate) async fn did_update_field(&self, field_id: &str) -> FlowyResult<()> {
-        if let Some(field_rev) = self.field_delegate.get_field_rev(&field_id).await {
-            let _ = self.group_service.write().await.did_update_field(&field_rev).await?;
+        if let Some(field_rev) = self.field_delegate.get_field_rev(field_id).await {
+            match self.group_service.write().await.did_update_field(&field_rev).await? {
+                None => {}
+                Some(changeset) => {
+                    self.notify_did_update_view(changeset).await;
+                }
+            }
         }
         Ok(())
     }
@@ -272,7 +278,6 @@ impl GridViewRevisionEditor {
             .send();
     }
 
-    #[allow(dead_code)]
     async fn modify<F>(&self, f: F) -> FlowyResult<()>
     where
         F: for<'a> FnOnce(&'a mut GridViewRevisionPad) -> FlowyResult<Option<GridViewRevisionChangeset>>,

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

@@ -142,10 +142,10 @@ impl GridViewManager {
                 .await;
         }
 
-        if row_changeset.has_changed() {
-            Some(row_changeset)
-        } else {
+        if row_changeset.is_empty() {
             None
+        } else {
+            Some(row_changeset)
         }
     }
 

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

@@ -1,12 +1,12 @@
+use crate::entities::{GroupPB, GroupViewChangesetPB, InsertedGroupPB};
 use crate::services::group::{default_group_configuration, Group};
 use flowy_error::{FlowyError, FlowyResult};
 use flowy_grid_data_model::revision::{
     FieldRevision, FieldTypeRevision, GroupConfigurationContentSerde, GroupConfigurationRevision, GroupRecordRevision,
 };
-use std::marker::PhantomData;
-
 use indexmap::IndexMap;
 use lib_infra::future::AFFuture;
+use std::marker::PhantomData;
 use std::sync::Arc;
 
 pub trait GroupConfigurationReader: Send + Sync + 'static {
@@ -26,6 +26,7 @@ pub trait GroupConfigurationWriter: Send + Sync + 'static {
 }
 
 pub struct GenericGroupConfiguration<C> {
+    view_id: String,
     pub configuration: Arc<GroupConfigurationRevision>,
     configuration_content: PhantomData<C>,
     field_rev: Arc<FieldRevision>,
@@ -39,6 +40,7 @@ where
 {
     #[tracing::instrument(level = "trace", skip_all, err)]
     pub async fn new(
+        view_id: String,
         field_rev: Arc<FieldRevision>,
         reader: Arc<dyn GroupConfigurationReader>,
         writer: Arc<dyn GroupConfigurationWriter>,
@@ -56,6 +58,7 @@ where
 
         // let configuration = C::from_configuration_content(&configuration_rev.content)?;
         Ok(Self {
+            view_id,
             field_rev,
             groups_map: IndexMap::new(),
             writer,
@@ -72,8 +75,18 @@ where
         self.groups_map.values().cloned().collect()
     }
 
-    pub(crate) async fn merge_groups(&mut self, groups: Vec<Group>) -> FlowyResult<()> {
-        let (group_revs, groups) = merge_groups(&self.configuration.groups, groups);
+    pub(crate) fn merge_groups(&mut self, groups: Vec<Group>) -> FlowyResult<Option<GroupViewChangesetPB>> {
+        let MergeGroupResult {
+            groups,
+            inserted_groups,
+            updated_groups,
+        } = merge_groups(&self.configuration.groups, groups);
+
+        let group_revs = groups
+            .iter()
+            .map(|group| GroupRecordRevision::new(group.id.clone(), group.name.clone()))
+            .collect();
+
         self.mut_configuration(move |configuration| {
             configuration.groups = group_revs;
             true
@@ -82,7 +95,14 @@ where
         groups.into_iter().for_each(|group| {
             self.groups_map.insert(group.id.clone(), group);
         });
-        Ok(())
+
+        let changeset = make_group_view_changeset(self.view_id.clone(), inserted_groups, updated_groups);
+        tracing::trace!("Group changeset: {:?}", changeset);
+        if changeset.is_empty() {
+            Ok(None)
+        } else {
+            Ok(Some(changeset))
+        }
     }
 
     #[allow(dead_code)]
@@ -101,7 +121,7 @@ where
         Ok(())
     }
 
-    pub(crate) fn with_mut_groups(&mut self, mut each: impl FnMut(&mut Group)) {
+    pub(crate) fn iter_mut_groups(&mut self, mut each: impl FnMut(&mut Group)) {
         self.groups_map.iter_mut().for_each(|(_, group)| {
             each(group);
         })
@@ -189,33 +209,82 @@ where
     }
 }
 
-fn merge_groups(old_group_revs: &[GroupRecordRevision], groups: Vec<Group>) -> (Vec<GroupRecordRevision>, Vec<Group>) {
-    if old_group_revs.is_empty() {
-        let new_groups = groups
-            .iter()
-            .map(|group| GroupRecordRevision::new(group.id.clone()))
-            .collect();
-        return (new_groups, groups);
+fn merge_groups(old_groups: &[GroupRecordRevision], groups: Vec<Group>) -> MergeGroupResult {
+    let mut merge_result = MergeGroupResult::new();
+    if old_groups.is_empty() {
+        merge_result.groups = groups;
+        return merge_result;
     }
 
+    // group_map is a helper map is used to filter out the new groups.
     let mut group_map: IndexMap<String, Group> = IndexMap::new();
     groups.into_iter().for_each(|group| {
         group_map.insert(group.id.clone(), group);
     });
 
-    // Inert
-    let mut sorted_groups: Vec<Group> = vec![];
-    for group_rev in old_group_revs {
+    // The group is ordered in old groups. Add them before adding the new groups
+    for group_rev in old_groups {
         if let Some(group) = group_map.remove(&group_rev.group_id) {
-            sorted_groups.push(group);
+            if group.name == group_rev.name {
+                merge_result.add_group(group);
+            } else {
+                merge_result.add_updated_group(group);
+            }
         }
     }
-    sorted_groups.extend(group_map.into_values().collect::<Vec<Group>>());
-    let new_group_revs = sorted_groups
-        .iter()
-        .map(|group| GroupRecordRevision::new(group.id.clone()))
-        .collect::<Vec<GroupRecordRevision>>();
 
-    tracing::trace!("group revs: {}, groups: {}", new_group_revs.len(), sorted_groups.len());
-    (new_group_revs, sorted_groups)
+    // Find out the new groups
+    let new_groups = group_map.into_values().collect::<Vec<Group>>();
+    for (index, group) in new_groups.into_iter().enumerate() {
+        merge_result.add_insert_group(index, group);
+    }
+    merge_result
+}
+
+struct MergeGroupResult {
+    groups: Vec<Group>,
+    inserted_groups: Vec<InsertedGroupPB>,
+    updated_groups: Vec<Group>,
+}
+
+impl MergeGroupResult {
+    fn new() -> Self {
+        Self {
+            groups: vec![],
+            inserted_groups: vec![],
+            updated_groups: vec![],
+        }
+    }
+
+    fn add_updated_group(&mut self, group: Group) {
+        self.groups.push(group.clone());
+        self.updated_groups.push(group);
+    }
+
+    fn add_group(&mut self, group: Group) {
+        self.groups.push(group.clone());
+    }
+
+    fn add_insert_group(&mut self, index: usize, group: Group) {
+        self.groups.push(group.clone());
+        let inserted_group = InsertedGroupPB {
+            group: GroupPB::from(group),
+            index: index as i32,
+        };
+        self.inserted_groups.push(inserted_group);
+    }
+}
+
+fn make_group_view_changeset(
+    view_id: String,
+    inserted_groups: Vec<InsertedGroupPB>,
+    updated_group: Vec<Group>,
+) -> GroupViewChangesetPB {
+    let changeset = GroupViewChangesetPB {
+        view_id,
+        inserted_groups,
+        deleted_groups: vec![],
+        update_groups: updated_group.into_iter().map(GroupPB::from).collect(),
+    };
+    changeset
 }

+ 14 - 6
frontend/rust-lib/flowy-grid/src/services/group/controller.rs

@@ -1,4 +1,4 @@
-use crate::entities::{GroupChangesetPB, RowPB};
+use crate::entities::{GroupChangesetPB, GroupViewChangesetPB, RowPB};
 use crate::services::cell::{decode_any_cell_data, CellBytesParser};
 use crate::services::group::action::GroupAction;
 use crate::services::group::configuration::GenericGroupConfiguration;
@@ -61,7 +61,7 @@ pub trait GroupControllerSharedOperation: Send + Sync {
 
     fn move_group_row(&mut self, context: MoveGroupRowContext) -> FlowyResult<Vec<GroupChangesetPB>>;
 
-    fn did_update_field(&mut self, field_rev: &FieldRevision) -> FlowyResult<()>;
+    fn did_update_field(&mut self, field_rev: &FieldRevision) -> FlowyResult<Option<GroupViewChangesetPB>>;
 }
 
 /// C: represents the group configuration that impl [GroupConfigurationSerde]
@@ -91,7 +91,7 @@ where
         let field_type_rev = field_rev.ty;
         let type_option = field_rev.get_type_option_entry::<T>(field_type_rev);
         let groups = G::generate_groups(&field_rev.id, &configuration, &type_option);
-        let _ = configuration.merge_groups(groups).await?;
+        let _ = configuration.merge_groups(groups)?;
         let default_group = Group::new(
             DEFAULT_GROUP_ID.to_owned(),
             field_rev.id.clone(),
@@ -114,6 +114,9 @@ impl<C, T, G, P> GroupControllerSharedOperation for GenericGroupController<C, T,
 where
     P: CellBytesParser,
     C: GroupConfigurationContentSerde,
+    T: TypeOptionDataDeserializer,
+    G: GroupGenerator<ConfigurationType = GenericGroupConfiguration<C>, TypeOptionType = T>,
+
     Self: GroupAction<CellDataType = P::Object>,
 {
     fn field_id(&self) -> &str {
@@ -179,7 +182,8 @@ 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.add_row_if_match(row_rev, &cell_data))
+            let changesets = self.add_row_if_match(row_rev, &cell_data);
+            Ok(changesets)
         } else {
             Ok(vec![])
         }
@@ -209,8 +213,12 @@ where
         }
     }
 
-    fn did_update_field(&mut self, field_rev: &FieldRevision) -> FlowyResult<()> {
-        todo!()
+    fn did_update_field(&mut self, field_rev: &FieldRevision) -> FlowyResult<Option<GroupViewChangesetPB>> {
+        let field_type_rev = field_rev.ty;
+        let type_option = field_rev.get_type_option_entry::<T>(field_type_rev);
+        let groups = G::generate_groups(&field_rev.id, &self.configuration, &type_option);
+        let changeset = self.configuration.merge_groups(groups)?;
+        Ok(changeset)
     }
 }
 

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

@@ -27,24 +27,30 @@ impl GroupAction for MultiSelectGroupController {
 
     fn add_row_if_match(&mut self, row_rev: &RowRevision, cell_data: &Self::CellDataType) -> Vec<GroupChangesetPB> {
         let mut changesets = vec![];
-        self.configuration.with_mut_groups(|group| {
-            add_row(group, &mut changesets, cell_data, row_rev);
+        self.configuration.iter_mut_groups(|group| {
+            if let Some(changeset) = add_row(group, cell_data, row_rev) {
+                changesets.push(changeset);
+            }
         });
         changesets
     }
 
     fn remove_row_if_match(&mut self, row_rev: &RowRevision, cell_data: &Self::CellDataType) -> Vec<GroupChangesetPB> {
         let mut changesets = vec![];
-        self.configuration.with_mut_groups(|group| {
-            remove_row(group, &mut changesets, cell_data, row_rev);
+        self.configuration.iter_mut_groups(|group| {
+            if let Some(changeset) = remove_row(group, cell_data, row_rev) {
+                changesets.push(changeset);
+            }
         });
         changesets
     }
 
     fn move_row(&mut self, cell_data: &Self::CellDataType, mut context: MoveGroupRowContext) -> Vec<GroupChangesetPB> {
         let mut group_changeset = vec![];
-        self.configuration.with_mut_groups(|group| {
-            move_select_option_row(group, &mut group_changeset, cell_data, &mut context);
+        self.configuration.iter_mut_groups(|group| {
+            if let Some(changeset) = move_select_option_row(group, cell_data, &mut context) {
+                group_changeset.push(changeset);
+            }
         });
         group_changeset
     }

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

@@ -27,24 +27,30 @@ impl GroupAction for SingleSelectGroupController {
 
     fn add_row_if_match(&mut self, row_rev: &RowRevision, cell_data: &Self::CellDataType) -> Vec<GroupChangesetPB> {
         let mut changesets = vec![];
-        self.configuration.with_mut_groups(|group| {
-            add_row(group, &mut changesets, cell_data, row_rev);
+        self.configuration.iter_mut_groups(|group| {
+            if let Some(changeset) = add_row(group, cell_data, row_rev) {
+                changesets.push(changeset);
+            }
         });
         changesets
     }
 
     fn remove_row_if_match(&mut self, row_rev: &RowRevision, cell_data: &Self::CellDataType) -> Vec<GroupChangesetPB> {
         let mut changesets = vec![];
-        self.configuration.with_mut_groups(|group| {
-            remove_row(group, &mut changesets, cell_data, row_rev);
+        self.configuration.iter_mut_groups(|group| {
+            if let Some(changeset) = remove_row(group, cell_data, row_rev) {
+                changesets.push(changeset);
+            }
         });
         changesets
     }
 
     fn move_row(&mut self, cell_data: &Self::CellDataType, mut context: MoveGroupRowContext) -> Vec<GroupChangesetPB> {
         let mut group_changeset = vec![];
-        self.configuration.with_mut_groups(|group| {
-            move_select_option_row(group, &mut group_changeset, cell_data, &mut context);
+        self.configuration.iter_mut_groups(|group| {
+            if let Some(changeset) = move_select_option_row(group, cell_data, &mut context) {
+                group_changeset.push(changeset);
+            }
         });
         group_changeset
     }

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

@@ -11,47 +11,56 @@ pub type SelectOptionGroupConfiguration = GenericGroupConfiguration<SelectOption
 
 pub fn add_row(
     group: &mut Group,
-    changesets: &mut Vec<GroupChangesetPB>,
     cell_data: &SelectOptionCellDataPB,
     row_rev: &RowRevision,
-) {
+) -> Option<GroupChangesetPB> {
+    let mut changeset = GroupChangesetPB::new(group.id.clone());
     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(GroupChangesetPB::insert(
-                    group.id.clone(),
-                    vec![InsertedRowPB::new(row_pb.clone())],
-                ));
+                changeset.inserted_rows.push(InsertedRowPB::new(row_pb.clone()));
                 group.add_row(row_pb);
             }
         } else if group.contains_row(&row_rev.id) {
-            changesets.push(GroupChangesetPB::delete(group.id.clone(), vec![row_rev.id.clone()]));
+            changeset.deleted_rows.push(row_rev.id.clone());
             group.remove_row(&row_rev.id);
         }
     });
+
+    if changeset.is_empty() {
+        None
+    } else {
+        Some(changeset)
+    }
 }
 
 pub fn remove_row(
     group: &mut Group,
-    changesets: &mut Vec<GroupChangesetPB>,
     cell_data: &SelectOptionCellDataPB,
     row_rev: &RowRevision,
-) {
+) -> Option<GroupChangesetPB> {
+    let mut changeset = GroupChangesetPB::new(group.id.clone());
     cell_data.select_options.iter().for_each(|option| {
         if option.id == group.id && group.contains_row(&row_rev.id) {
-            changesets.push(GroupChangesetPB::delete(group.id.clone(), vec![row_rev.id.clone()]));
+            changeset.deleted_rows.push(row_rev.id.clone());
             group.remove_row(&row_rev.id);
         }
     });
+
+    if changeset.is_empty() {
+        None
+    } else {
+        Some(changeset)
+    }
 }
 
 pub fn move_select_option_row(
     group: &mut Group,
-    group_changeset: &mut Vec<GroupChangesetPB>,
     _cell_data: &SelectOptionCellDataPB,
     context: &mut MoveGroupRowContext,
-) {
+) -> Option<GroupChangesetPB> {
+    let mut changeset = GroupChangesetPB::new(group.id.clone());
     let MoveGroupRowContext {
         row_rev,
         row_changeset,
@@ -68,7 +77,7 @@ pub fn move_select_option_row(
 
     // Remove the row in which group contains it
     if from_index.is_some() {
-        group_changeset.push(GroupChangesetPB::delete(group.id.clone(), vec![row_rev.id.clone()]));
+        changeset.deleted_rows.push(row_rev.id.clone());
         tracing::debug!("Group:{} remove row:{}", group.id, row_rev.id);
         group.remove_row(&row_rev.id);
     }
@@ -78,7 +87,7 @@ pub fn move_select_option_row(
         let mut inserted_row = InsertedRowPB::new(row_pb.clone());
         match to_index {
             None => {
-                group_changeset.push(GroupChangesetPB::insert(group.id.clone(), vec![inserted_row]));
+                changeset.inserted_rows.push(inserted_row);
                 tracing::debug!("Group:{} append row:{}", group.id, row_rev.id);
                 group.add_row(row_pb);
             }
@@ -91,7 +100,7 @@ pub fn move_select_option_row(
                     tracing::debug!("Group:{} append row:{}", group.id, row_rev.id);
                     group.add_row(row_pb);
                 }
-                group_changeset.push(GroupChangesetPB::insert(group.id.clone(), vec![inserted_row]));
+                changeset.inserted_rows.push(inserted_row);
             }
         }
 
@@ -100,6 +109,12 @@ pub fn move_select_option_row(
             tracing::debug!("Mark row:{} belong to group:{}", row_rev.id, group.id);
             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);
+            changeset.updated_rows.push(RowPB::from(*row_rev));
         }
     }
+    if changeset.is_empty() {
+        None
+    } else {
+        Some(changeset)
+    }
 }

+ 6 - 17
frontend/rust-lib/flowy-grid/src/services/group/entities.rs

@@ -1,33 +1,22 @@
-use crate::entities::{GroupPB, RowPB};
+use crate::entities::RowPB;
 
-#[derive(Clone)]
+#[derive(Clone, PartialEq, Eq)]
 pub struct Group {
     pub id: String,
     pub field_id: String,
-    pub desc: String,
-    rows: Vec<RowPB>,
+    pub name: String,
+    pub(crate) rows: Vec<RowPB>,
 
     /// [content] is used to determine which group the cell belongs to.
     pub content: String,
 }
 
-impl std::convert::From<Group> for GroupPB {
-    fn from(group: Group) -> Self {
-        Self {
-            field_id: group.field_id,
-            group_id: group.id,
-            desc: group.desc,
-            rows: group.rows,
-        }
-    }
-}
-
 impl Group {
-    pub fn new(id: String, field_id: String, desc: String, content: String) -> Self {
+    pub fn new(id: String, field_id: String, name: String, content: String) -> Self {
         Self {
             id,
             field_id,
-            desc,
+            name,
             rows: vec![],
             content,
         }

+ 15 - 6
frontend/rust-lib/flowy-grid/src/services/group/group_service.rs

@@ -1,4 +1,4 @@
-use crate::entities::{FieldType, GroupChangesetPB};
+use crate::entities::{FieldType, GroupChangesetPB, GroupViewChangesetPB};
 use crate::services::group::configuration::GroupConfigurationReader;
 use crate::services::group::controller::{GroupController, MoveGroupRowContext};
 use crate::services::group::{
@@ -15,18 +15,20 @@ use std::future::Future;
 use std::sync::Arc;
 
 pub(crate) struct GroupService {
+    view_id: String,
     configuration_reader: Arc<dyn GroupConfigurationReader>,
     configuration_writer: Arc<dyn GroupConfigurationWriter>,
     group_controller: Option<Box<dyn GroupController>>,
 }
 
 impl GroupService {
-    pub(crate) async fn new<R, W>(configuration_reader: R, configuration_writer: W) -> Self
+    pub(crate) async fn new<R, W>(view_id: String, configuration_reader: R, configuration_writer: W) -> Self
     where
         R: GroupConfigurationReader,
         W: GroupConfigurationWriter,
     {
         Self {
+            view_id,
             configuration_reader: Arc::new(configuration_reader),
             configuration_writer: Arc::new(configuration_writer),
             group_controller: None,
@@ -36,8 +38,8 @@ impl GroupService {
     pub(crate) async fn groups(&self) -> Vec<Group> {
         self.group_controller
             .as_ref()
-            .and_then(|group_controller| Some(group_controller.groups()))
-            .unwrap_or(vec![])
+            .map(|group_controller| group_controller.groups())
+            .unwrap_or_default()
     }
 
     pub(crate) async fn get_group(&self, group_id: &str) -> Option<(usize, Group)> {
@@ -170,9 +172,13 @@ impl GroupService {
         }
     }
 
-    pub(crate) async fn did_update_field(&mut self, field_rev: &FieldRevision) -> FlowyResult<()> {
+    #[tracing::instrument(level = "trace", name = "group_did_update_field", skip(self, field_rev), err)]
+    pub(crate) async fn did_update_field(
+        &mut self,
+        field_rev: &FieldRevision,
+    ) -> FlowyResult<Option<GroupViewChangesetPB>> {
         match self.group_controller.as_mut() {
-            None => Ok(()),
+            None => Ok(None),
             Some(group_controller) => group_controller.did_update_field(field_rev),
         }
     }
@@ -196,6 +202,7 @@ impl GroupService {
             }
             FieldType::SingleSelect => {
                 let configuration = SelectOptionGroupConfiguration::new(
+                    self.view_id.clone(),
                     field_rev.clone(),
                     self.configuration_reader.clone(),
                     self.configuration_writer.clone(),
@@ -206,6 +213,7 @@ impl GroupService {
             }
             FieldType::MultiSelect => {
                 let configuration = SelectOptionGroupConfiguration::new(
+                    self.view_id.clone(),
                     field_rev.clone(),
                     self.configuration_reader.clone(),
                     self.configuration_writer.clone(),
@@ -216,6 +224,7 @@ impl GroupService {
             }
             FieldType::Checkbox => {
                 let configuration = CheckboxGroupConfiguration::new(
+                    self.view_id.clone(),
                     field_rev.clone(),
                     self.configuration_reader.clone(),
                     self.configuration_writer.clone(),

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

@@ -1,9 +1,11 @@
 use crate::grid::grid_editor::GridEditorTest;
 use flowy_grid::entities::{
-    CreateRowParams, FieldType, GridLayout, GroupPB, MoveGroupParams, MoveGroupRowParams, RowPB,
+    CreateRowParams, FieldChangesetParams, FieldType, GridLayout, GroupPB, MoveGroupParams, MoveGroupRowParams, RowPB,
 };
 use flowy_grid::services::cell::insert_select_option_cell;
 use flowy_grid_data_model::revision::RowChangeset;
+use std::time::Duration;
+use tokio::time::interval;
 
 pub enum GroupScript {
     AssertGroupRowCount {
@@ -42,6 +44,9 @@ pub enum GroupScript {
         from_group_index: usize,
         to_group_index: usize,
     },
+    UpdateField {
+        changeset: FieldChangesetParams,
+    },
 }
 
 pub struct GridGroupTest {
@@ -156,6 +161,12 @@ impl GridGroupTest {
             } => {
                 let group = self.group_at_index(group_index).await;
                 assert_eq!(group.group_id, group_pb.group_id);
+                assert_eq!(group.desc, group_pb.desc);
+            }
+            GroupScript::UpdateField { changeset } => {
+                self.editor.update_field(changeset).await.unwrap();
+                let mut interval = interval(Duration::from_millis(130));
+                interval.tick().await;
             }
         }
     }

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

@@ -1,5 +1,6 @@
 use crate::grid::group_test::script::GridGroupTest;
 use crate::grid::group_test::script::GroupScript::*;
+use flowy_grid::entities::FieldChangesetParams;
 
 #[tokio::test]
 async fn group_init_test() {
@@ -314,3 +315,25 @@ async fn group_move_group_test() {
     ];
     test.run_scripts(scripts).await;
 }
+
+#[tokio::test]
+async fn group_update_field_test() {
+    let mut test = GridGroupTest::new().await;
+    let mut group = test.group_at_index(0).await;
+    let changeset = FieldChangesetParams {
+        field_id: group.field_id.clone(),
+        grid_id: test.grid_id.clone(),
+        name: Some("ABC".to_string()),
+        ..Default::default()
+    };
+
+    // group.desc = "ABC".to_string();
+    let scripts = vec![
+        UpdateField { changeset },
+        AssertGroup {
+            group_index: 0,
+            expected_group: group,
+        },
+    ];
+    test.run_scripts(scripts).await;
+}

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

@@ -59,8 +59,8 @@ impl RowChangeset {
         }
     }
 
-    pub fn has_changed(&self) -> bool {
-        self.height.is_some() || self.visibility.is_some() || !self.cell_by_field_id.is_empty()
+    pub fn is_empty(&self) -> bool {
+        self.height.is_none() && self.visibility.is_none() && self.cell_by_field_id.is_empty()
     }
 }
 

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

@@ -110,6 +110,9 @@ impl GroupConfigurationContentSerde for SelectOptionGroupConfigurationRevision {
 pub struct GroupRecordRevision {
     pub group_id: String,
 
+    #[serde(default)]
+    pub name: String,
+
     #[serde(default = "DEFAULT_GROUP_RECORD_VISIBILITY")]
     pub visible: bool,
 }
@@ -117,9 +120,10 @@ pub struct GroupRecordRevision {
 const DEFAULT_GROUP_RECORD_VISIBILITY: fn() -> bool = || true;
 
 impl GroupRecordRevision {
-    pub fn new(group_id: String) -> Self {
+    pub fn new(group_id: String, group_name: String) -> Self {
         Self {
             group_id,
+            name: group_name,
             visible: true,
         }
     }