Переглянути джерело

Merge pull request #442 from AppFlowy-IO/feat_row_delete

Feature: support row delete
Nathan.fooo 3 роки тому
батько
коміт
5d9763ff7f
59 змінених файлів з 1951 додано та 798 видалено
  1. 0 4
      .github/workflows/dart_lint.yml
  2. 0 6
      .github/workflows/rust_lint.yml
  3. 0 6
      .github/workflows/rust_test.yml
  4. 4 0
      frontend/app_flowy/assets/translations/en.json
  5. 1 3
      frontend/app_flowy/lib/startup/deps_resolver.dart
  6. 9 5
      frontend/app_flowy/lib/workspace/application/grid/cell_bloc/selection_editor_bloc.dart
  7. 14 14
      frontend/app_flowy/lib/workspace/application/grid/field/field_action_sheet_bloc.dart
  8. 67 83
      frontend/app_flowy/lib/workspace/application/grid/grid_bloc.dart
  9. 0 92
      frontend/app_flowy/lib/workspace/application/grid/grid_block_service.dart
  10. 43 0
      frontend/app_flowy/lib/workspace/application/grid/grid_listener.dart
  11. 1 9
      frontend/app_flowy/lib/workspace/application/grid/grid_service.dart
  12. 1 1
      frontend/app_flowy/lib/workspace/application/grid/prelude.dart
  13. 58 0
      frontend/app_flowy/lib/workspace/application/grid/row/row_action_sheet_bloc.dart
  14. 22 32
      frontend/app_flowy/lib/workspace/application/grid/row/row_bloc.dart
  15. 36 3
      frontend/app_flowy/lib/workspace/application/grid/row/row_service.dart
  16. 34 14
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/grid_page.dart
  17. 1 1
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/layout/sizes.dart
  18. 0 96
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/selection_cell/extension.dart
  19. 2 3
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/selection_cell/selection_editor.dart
  20. 101 0
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/selection_cell/text_field.dart
  21. 5 15
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/field_cell_action_sheet.dart
  22. 29 6
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/row/grid_row.dart
  23. 148 0
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/row/row_action_sheet.dart
  24. 34 0
      frontend/app_flowy/packages/flowy_sdk/lib/dispatch/dart_event/flowy-grid/dart_event.dart
  25. 155 0
      frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid-data-model/grid.pb.dart
  26. 29 1
      frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid-data-model/grid.pbjson.dart
  27. 19 19
      frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid-data-model/meta.pb.dart
  28. 6 6
      frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid-data-model/meta.pbjson.dart
  29. 2 2
      frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid/dart_notification.pbenum.dart
  30. 2 2
      frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid/dart_notification.pbjson.dart
  31. 4 0
      frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid/event_map.pbenum.dart
  32. 3 1
      frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid/event_map.pbjson.dart
  33. 1 1
      frontend/rust-lib/flowy-grid/src/dart_notification.rs
  34. 22 0
      frontend/rust-lib/flowy-grid/src/event_handler.rs
  35. 8 0
      frontend/rust-lib/flowy-grid/src/event_map.rs
  36. 4 3
      frontend/rust-lib/flowy-grid/src/manager.rs
  37. 8 8
      frontend/rust-lib/flowy-grid/src/protobuf/model/dart_notification.rs
  38. 10 3
      frontend/rust-lib/flowy-grid/src/protobuf/model/event_map.rs
  39. 1 1
      frontend/rust-lib/flowy-grid/src/protobuf/proto/dart_notification.proto
  40. 2 0
      frontend/rust-lib/flowy-grid/src/protobuf/proto/event_map.proto
  41. 32 8
      frontend/rust-lib/flowy-grid/src/services/block_meta_editor.rs
  42. 73 51
      frontend/rust-lib/flowy-grid/src/services/block_meta_manager.rs
  43. 1 1
      frontend/rust-lib/flowy-grid/src/services/field/type_options/date_type_option.rs
  44. 33 36
      frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option.rs
  45. 26 12
      frontend/rust-lib/flowy-grid/src/services/grid_editor.rs
  46. 10 23
      frontend/rust-lib/flowy-grid/src/services/row/row_loader.rs
  47. 1 1
      frontend/rust-lib/flowy-grid/tests/grid/grid_test.rs
  48. 1 0
      frontend/rust-lib/flowy-revision/src/cache/disk/folder_rev_impl.rs
  49. 1 0
      frontend/rust-lib/flowy-sdk/src/deps_resolve/util.rs
  50. 75 3
      shared-lib/flowy-grid-data-model/src/entities/grid.rs
  51. 10 10
      shared-lib/flowy-grid-data-model/src/entities/meta.rs
  52. 619 25
      shared-lib/flowy-grid-data-model/src/protobuf/model/grid.rs
  53. 90 91
      shared-lib/flowy-grid-data-model/src/protobuf/model/meta.rs
  54. 11 0
      shared-lib/flowy-grid-data-model/src/protobuf/proto/grid.proto
  55. 3 3
      shared-lib/flowy-grid-data-model/src/protobuf/proto/meta.proto
  56. 2 20
      shared-lib/flowy-grid-data-model/tests/serde_test.rs
  57. 62 55
      shared-lib/flowy-sync/src/client_grid/grid_block_meta_pad.rs
  58. 4 4
      shared-lib/flowy-sync/src/client_grid/grid_builder.rs
  59. 11 15
      shared-lib/flowy-sync/src/client_grid/grid_meta_pad.rs

+ 0 - 4
.github/workflows/dart_lint.yml

@@ -8,12 +8,8 @@ name: Flutter lint
 on:
   push:
     branches: [ main ]
-    paths:
-      - 'frontend/app_flowy'
   pull_request:
     branches: [ main ]
-    paths:
-      - 'frontend/app_flowy'
 
 env:
   CARGO_TERM_COLOR: always

+ 0 - 6
.github/workflows/rust_lint.yml

@@ -3,14 +3,8 @@ name: Rust lint
 on:
   push:
     branches: [ main ]
-    paths:
-      - 'frontend/rust-lib'
-      - 'shared-lib'
   pull_request:
     branches: [ main ]
-    paths:
-      - 'frontend/rust-lib'
-      - 'shared-lib'
 
 
 env:

+ 0 - 6
.github/workflows/rust_test.yml

@@ -4,15 +4,9 @@ on:
   push:
     branches: 
       - 'main'
-    paths:
-      - 'frontend/rust-lib'
-      - 'shared-lib'
   pull_request:
     branches: 
       - 'main'
-    paths:
-      - 'frontend/rust-lib'
-      - 'shared-lib'
 
 env:
   CARGO_TERM_COLOR: always

+ 4 - 0
frontend/app_flowy/assets/translations/en.json

@@ -174,6 +174,10 @@
       "addOption": "Add option",
       "editProperty": "Edit property"
     },
+    "row": {
+      "duplicate": "Duplicate",
+      "delete": "Delete"
+    },
     "selectOption": {
       "purpleColor": "Purple",
       "pinkColor": "Pink",

+ 1 - 3
frontend/app_flowy/lib/startup/deps_resolver.dart

@@ -5,7 +5,6 @@ import 'package:app_flowy/workspace/application/app/prelude.dart';
 import 'package:app_flowy/workspace/application/doc/prelude.dart';
 import 'package:app_flowy/workspace/application/grid/field/type_option/multi_select_bloc.dart';
 import 'package:app_flowy/workspace/application/grid/prelude.dart';
-import 'package:app_flowy/workspace/application/grid/row/row_listener.dart';
 import 'package:app_flowy/workspace/application/trash/prelude.dart';
 import 'package:app_flowy/workspace/application/workspace/prelude.dart';
 import 'package:app_flowy/workspace/application/edit_pannel/edit_pannel_bloc.dart';
@@ -148,13 +147,12 @@ void _resolveDocDeps(GetIt getIt) {
 void _resolveGridDeps(GetIt getIt) {
   // Grid
   getIt.registerFactoryParam<GridBloc, View, void>(
-    (view, _) => GridBloc(view: view, service: GridService()),
+    (view, _) => GridBloc(view: view),
   );
 
   getIt.registerFactoryParam<RowBloc, RowData, void>(
     (data, _) => RowBloc(
       rowData: data,
-      rowlistener: RowListener(rowId: data.rowId),
     ),
   );
 

+ 9 - 5
frontend/app_flowy/lib/workspace/application/grid/cell_bloc/selection_editor_bloc.dart

@@ -110,7 +110,7 @@ class SelectOptionEditorBloc extends Bloc<SelectOptionEditorEvent, SelectOptionE
   void _loadOptions() async {
     _delayOperation?.cancel();
     _delayOperation = Timer(
-      const Duration(milliseconds: 300),
+      const Duration(milliseconds: 1),
       () async {
         final result = await _selectOptionService.getOpitonContext(
           gridId: state.gridId,
@@ -119,10 +119,14 @@ class SelectOptionEditorBloc extends Bloc<SelectOptionEditorEvent, SelectOptionE
         );
 
         result.fold(
-          (selectOptionContext) => add(SelectOptionEditorEvent.didReceiveOptions(
-            selectOptionContext.options,
-            selectOptionContext.selectOptions,
-          )),
+          (selectOptionContext) {
+            if (!isClosed) {
+              add(SelectOptionEditorEvent.didReceiveOptions(
+                selectOptionContext.options,
+                selectOptionContext.selectOptions,
+              ));
+            }
+          },
           (err) => Log.error(err),
         );
       },

+ 14 - 14
frontend/app_flowy/lib/workspace/application/grid/field/action_sheet_bloc.dart → frontend/app_flowy/lib/workspace/application/grid/field/field_action_sheet_bloc.dart

@@ -5,14 +5,14 @@ import 'package:freezed_annotation/freezed_annotation.dart';
 import 'dart:async';
 import 'field_service.dart';
 
-part 'action_sheet_bloc.freezed.dart';
+part 'field_action_sheet_bloc.freezed.dart';
 
-class FieldActionSheetBloc extends Bloc<ActionSheetEvent, ActionSheetState> {
+class FieldActionSheetBloc extends Bloc<FieldActionSheetEvent, FieldActionSheetState> {
   final FieldService service;
 
   FieldActionSheetBloc({required Field field, required this.service})
-      : super(ActionSheetState.initial(EditFieldContext.create()..gridField = field)) {
-    on<ActionSheetEvent>(
+      : super(FieldActionSheetState.initial(EditFieldContext.create()..gridField = field)) {
+    on<FieldActionSheetEvent>(
       (event, emit) async {
         await event.map(
           updateFieldName: (_UpdateFieldName value) async {
@@ -56,23 +56,23 @@ class FieldActionSheetBloc extends Bloc<ActionSheetEvent, ActionSheetState> {
 }
 
 @freezed
-class ActionSheetEvent with _$ActionSheetEvent {
-  const factory ActionSheetEvent.updateFieldName(String name) = _UpdateFieldName;
-  const factory ActionSheetEvent.hideField() = _HideField;
-  const factory ActionSheetEvent.duplicateField() = _DuplicateField;
-  const factory ActionSheetEvent.deleteField() = _DeleteField;
-  const factory ActionSheetEvent.saveField() = _SaveField;
+class FieldActionSheetEvent with _$FieldActionSheetEvent {
+  const factory FieldActionSheetEvent.updateFieldName(String name) = _UpdateFieldName;
+  const factory FieldActionSheetEvent.hideField() = _HideField;
+  const factory FieldActionSheetEvent.duplicateField() = _DuplicateField;
+  const factory FieldActionSheetEvent.deleteField() = _DeleteField;
+  const factory FieldActionSheetEvent.saveField() = _SaveField;
 }
 
 @freezed
-class ActionSheetState with _$ActionSheetState {
-  const factory ActionSheetState({
+class FieldActionSheetState with _$FieldActionSheetState {
+  const factory FieldActionSheetState({
     required EditFieldContext editContext,
     required String errorText,
     required String fieldName,
-  }) = _ActionSheetState;
+  }) = _FieldActionSheetState;
 
-  factory ActionSheetState.initial(EditFieldContext editContext) => ActionSheetState(
+  factory FieldActionSheetState.initial(EditFieldContext editContext) => FieldActionSheetState(
         editContext: editContext,
         errorText: '',
         fieldName: editContext.gridField.name,

+ 67 - 83
frontend/app_flowy/lib/workspace/application/grid/grid_bloc.dart

@@ -6,33 +6,33 @@ import 'package:flowy_sdk/protobuf/flowy-folder-data-model/view.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid-data-model/protobuf.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:freezed_annotation/freezed_annotation.dart';
-import 'package:equatable/equatable.dart';
-import 'grid_block_service.dart';
 import 'field/grid_listenr.dart';
+import 'grid_listener.dart';
 import 'grid_service.dart';
 
 part 'grid_bloc.freezed.dart';
 
 class GridBloc extends Bloc<GridEvent, GridState> {
   final View view;
-  final GridService service;
+  final GridService _gridService;
+  final GridListener _gridListener;
   final GridFieldsListener _fieldListener;
-  GridBlockService? _blockService;
 
-  GridBloc({required this.view, required this.service})
+  GridBloc({required this.view})
       : _fieldListener = GridFieldsListener(gridId: view.id),
+        _gridService = GridService(),
+        _gridListener = GridListener(gridId: view.id),
         super(GridState.initial()) {
     on<GridEvent>(
       (event, emit) async {
         await event.map(
           initial: (InitialGrid value) async {
             await _initGrid(emit);
+            _startListening();
           },
           createRow: (_CreateRow value) {
-            service.createRow(gridId: view.id);
+            _gridService.createRow(gridId: view.id);
           },
-          delete: (_Delete value) {},
-          rename: (_Rename value) {},
           updateDesc: (_Desc value) {},
           didReceiveRowUpdate: (_DidReceiveRowUpdate value) {
             emit(state.copyWith(rows: value.rows));
@@ -48,7 +48,7 @@ class GridBloc extends Bloc<GridEvent, GridState> {
   @override
   Future<void> close() async {
     await _fieldListener.stop();
-    await _blockService?.stop();
+    await _gridListener.stop();
     return super.close();
   }
 
@@ -64,22 +64,29 @@ class GridBloc extends Bloc<GridEvent, GridState> {
     await _loadGrid(emit);
   }
 
-  Future<void> _initGridBlock(Grid grid) async {
-    _blockService = GridBlockService(
-      gridId: grid.id,
-      blockOrders: grid.blockOrders,
-    );
-
-    _blockService?.blocksUpdateNotifier?.addPublishListener((result) {
-      result.fold(
-        (blockMap) => add(GridEvent.didReceiveRowUpdate(_buildRows(blockMap))),
-        (err) => Log.error('$err'),
-      );
+  void _startListening() {
+    _gridListener.rowsUpdateNotifier.addPublishListener((result) {
+      result.fold((gridBlockChangeset) {
+        for (final changeset in gridBlockChangeset) {
+          if (changeset.insertedRows.isNotEmpty) {
+            _insertRows(changeset.insertedRows);
+          }
+
+          if (changeset.deletedRows.isNotEmpty) {
+            _deleteRows(changeset.deletedRows);
+          }
+
+          if (changeset.updatedRows.isNotEmpty) {
+            _updateRows(changeset.updatedRows);
+          }
+        }
+      }, (err) => Log.error(err));
     });
+    _gridListener.start();
   }
 
   Future<void> _loadGrid(Emitter<GridState> emit) async {
-    final result = await service.openGrid(gridId: view.id);
+    final result = await _gridService.loadGrid(gridId: view.id);
     return Future(
       () => result.fold(
         (grid) async => await _loadFields(grid, emit),
@@ -89,14 +96,14 @@ class GridBloc extends Bloc<GridEvent, GridState> {
   }
 
   Future<void> _loadFields(Grid grid, Emitter<GridState> emit) async {
-    final result = await service.getFields(gridId: grid.id, fieldOrders: grid.fieldOrders);
+    final result = await _gridService.getFields(gridId: grid.id, fieldOrders: grid.fieldOrders);
     return Future(
       () => result.fold(
         (fields) {
-          _initGridBlock(grid);
           emit(state.copyWith(
             grid: Some(grid),
             fields: fields.items,
+            rows: _buildRows(grid.blockOrders),
             loadingState: GridLoadingState.finish(left(unit)),
           ));
         },
@@ -105,30 +112,50 @@ class GridBloc extends Bloc<GridEvent, GridState> {
     );
   }
 
-  List<GridBlockRow> _buildRows(GridBlockMap blockMap) {
-    List<GridBlockRow> rows = [];
-    blockMap.forEach((_, GridBlock gridBlock) {
-      rows.addAll(gridBlock.rowOrders.map(
-        (rowOrder) => GridBlockRow(
-          gridId: view.id,
-          blockId: gridBlock.id,
-          rowId: rowOrder.rowId,
-          height: rowOrder.height.toDouble(),
-        ),
-      ));
-    });
-    return rows;
+  void _deleteRows(List<RowOrder> deletedRows) {
+    final List<RowOrder> rows = List.from(state.rows);
+    rows.retainWhere(
+      (row) => deletedRows.where((deletedRow) => deletedRow.rowId == row.rowId).isEmpty,
+    );
+
+    add(GridEvent.didReceiveRowUpdate(rows));
+  }
+
+  void _insertRows(List<IndexRowOrder> createdRows) {
+    final List<RowOrder> rows = List.from(state.rows);
+    for (final newRow in createdRows) {
+      if (newRow.hasIndex()) {
+        rows.insert(newRow.index, newRow.rowOrder);
+      } else {
+        rows.add(newRow.rowOrder);
+      }
+    }
+    add(GridEvent.didReceiveRowUpdate(rows));
+  }
+
+  void _updateRows(List<RowOrder> updatedRows) {
+    final List<RowOrder> rows = List.from(state.rows);
+    for (final updatedRow in updatedRows) {
+      final index = rows.indexWhere((row) => row.rowId == updatedRow.rowId);
+      if (index != -1) {
+        rows.removeAt(index);
+        rows.insert(index, updatedRow);
+      }
+    }
+    add(GridEvent.didReceiveRowUpdate(rows));
+  }
+
+  List<RowOrder> _buildRows(List<GridBlockOrder> blockOrders) {
+    return blockOrders.expand((blockOrder) => blockOrder.rowOrders).toList();
   }
 }
 
 @freezed
 class GridEvent with _$GridEvent {
   const factory GridEvent.initial() = InitialGrid;
-  const factory GridEvent.rename(String gridId, String name) = _Rename;
   const factory GridEvent.updateDesc(String gridId, String desc) = _Desc;
-  const factory GridEvent.delete(String gridId) = _Delete;
   const factory GridEvent.createRow() = _CreateRow;
-  const factory GridEvent.didReceiveRowUpdate(List<GridBlockRow> rows) = _DidReceiveRowUpdate;
+  const factory GridEvent.didReceiveRowUpdate(List<RowOrder> rows) = _DidReceiveRowUpdate;
   const factory GridEvent.didReceiveFieldUpdate(List<Field> fields) = _DidReceiveFieldUpdate;
 }
 
@@ -137,7 +164,7 @@ class GridState with _$GridState {
   const factory GridState({
     required GridLoadingState loadingState,
     required List<Field> fields,
-    required List<GridBlockRow> rows,
+    required List<RowOrder> rows,
     required Option<Grid> grid,
   }) = _GridState;
 
@@ -154,46 +181,3 @@ class GridLoadingState with _$GridLoadingState {
   const factory GridLoadingState.loading() = _Loading;
   const factory GridLoadingState.finish(Either<Unit, FlowyError> successOrFail) = _Finish;
 }
-
-class GridBlockRow {
-  final String gridId;
-  final String rowId;
-  final String blockId;
-  final double height;
-
-  const GridBlockRow({
-    required this.gridId,
-    required this.rowId,
-    required this.blockId,
-    required this.height,
-  });
-}
-
-class RowData extends Equatable {
-  final String gridId;
-  final String rowId;
-  final String blockId;
-  final List<Field> fields;
-  final double height;
-
-  const RowData({
-    required this.gridId,
-    required this.rowId,
-    required this.blockId,
-    required this.fields,
-    required this.height,
-  });
-
-  factory RowData.fromBlockRow(GridBlockRow row, List<Field> fields) {
-    return RowData(
-      gridId: row.gridId,
-      rowId: row.rowId,
-      blockId: row.blockId,
-      fields: fields,
-      height: row.height,
-    );
-  }
-
-  @override
-  List<Object> get props => [rowId, fields];
-}

+ 0 - 92
frontend/app_flowy/lib/workspace/application/grid/grid_block_service.dart

@@ -1,92 +0,0 @@
-import 'dart:collection';
-import 'package:dartz/dartz.dart';
-import 'package:flowy_sdk/dispatch/dispatch.dart';
-import 'package:flowy_sdk/log.dart';
-import 'package:flowy_sdk/protobuf/flowy-grid-data-model/grid.pb.dart';
-import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
-import 'package:flowy_sdk/protobuf/flowy-grid/dart_notification.pb.dart';
-import 'package:flowy_infra/notifier.dart';
-import 'dart:async';
-import 'dart:typed_data';
-import 'package:app_flowy/core/notification_helper.dart';
-
-typedef GridBlockMap = LinkedHashMap<String, GridBlock>;
-typedef BlocksUpdateNotifierValue = Either<GridBlockMap, FlowyError>;
-
-class GridBlockService {
-  String gridId;
-  GridBlockMap blockMap = GridBlockMap();
-  late GridBlockListener _blockListener;
-  PublishNotifier<BlocksUpdateNotifierValue>? blocksUpdateNotifier = PublishNotifier();
-
-  GridBlockService({required this.gridId, required List<GridBlockOrder> blockOrders}) {
-    _loadGridBlocks(blockOrders);
-
-    _blockListener = GridBlockListener(gridId: gridId);
-    _blockListener.blockUpdateNotifier.addPublishListener((result) {
-      result.fold(
-        (blockOrder) => _loadGridBlocks(blockOrder),
-        (err) => Log.error(err),
-      );
-    });
-    _blockListener.start();
-  }
-
-  Future<void> stop() async {
-    await _blockListener.stop();
-    blocksUpdateNotifier?.dispose();
-    blocksUpdateNotifier = null;
-  }
-
-  void _loadGridBlocks(List<GridBlockOrder> blockOrders) {
-    final payload = QueryGridBlocksPayload.create()
-      ..gridId = gridId
-      ..blockOrders.addAll(blockOrders);
-
-    GridEventGetGridBlocks(payload).send().then((result) {
-      result.fold(
-        (repeatedBlocks) {
-          for (final gridBlock in repeatedBlocks.items) {
-            blockMap[gridBlock.id] = gridBlock;
-          }
-          blocksUpdateNotifier?.value = left(blockMap);
-        },
-        (err) => blocksUpdateNotifier?.value = right(err),
-      );
-    });
-  }
-}
-
-class GridBlockListener {
-  final String gridId;
-  PublishNotifier<Either<List<GridBlockOrder>, FlowyError>> blockUpdateNotifier = PublishNotifier(comparable: null);
-  GridNotificationListener? _listener;
-
-  GridBlockListener({required this.gridId});
-
-  void start() {
-    _listener = GridNotificationListener(
-      objectId: gridId,
-      handler: _handler,
-    );
-  }
-
-  void _handler(GridNotification ty, Either<Uint8List, FlowyError> result) {
-    switch (ty) {
-      case GridNotification.DidUpdateBlock:
-        result.fold(
-          (payload) => blockUpdateNotifier.value = left([GridBlockOrder.fromBuffer(payload)]),
-          (error) => blockUpdateNotifier.value = right(error),
-        );
-        break;
-
-      default:
-        break;
-    }
-  }
-
-  Future<void> stop() async {
-    await _listener?.stop();
-    blockUpdateNotifier.dispose();
-  }
-}

+ 43 - 0
frontend/app_flowy/lib/workspace/application/grid/grid_listener.dart

@@ -0,0 +1,43 @@
+import 'package:dartz/dartz.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid-data-model/grid.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/dart_notification.pb.dart';
+import 'package:flowy_infra/notifier.dart';
+import 'dart:async';
+import 'dart:typed_data';
+import 'package:app_flowy/core/notification_helper.dart';
+
+class GridListener {
+  final String gridId;
+  PublishNotifier<Either<List<GridBlockOrderChangeset>, FlowyError>> rowsUpdateNotifier =
+      PublishNotifier(comparable: null);
+  GridNotificationListener? _listener;
+
+  GridListener({required this.gridId});
+
+  void start() {
+    _listener = GridNotificationListener(
+      objectId: gridId,
+      handler: _handler,
+    );
+  }
+
+  void _handler(GridNotification ty, Either<Uint8List, FlowyError> result) {
+    switch (ty) {
+      case GridNotification.DidUpdateGridBlock:
+        result.fold(
+          (payload) => rowsUpdateNotifier.value = left([GridBlockOrderChangeset.fromBuffer(payload)]),
+          (error) => rowsUpdateNotifier.value = right(error),
+        );
+        break;
+
+      default:
+        break;
+    }
+  }
+
+  Future<void> stop() async {
+    await _listener?.stop();
+    rowsUpdateNotifier.dispose();
+  }
+}

+ 1 - 9
frontend/app_flowy/lib/workspace/application/grid/grid_service.dart

@@ -5,7 +5,7 @@ import 'package:flowy_sdk/protobuf/flowy-grid-data-model/grid.pb.dart';
 import 'package:dartz/dartz.dart';
 
 class GridService {
-  Future<Either<Grid, FlowyError>> openGrid({required String gridId}) async {
+  Future<Either<Grid, FlowyError>> loadGrid({required String gridId}) async {
     await FolderEventSetLatestView(ViewId(value: gridId)).send();
 
     final payload = GridId(value: gridId);
@@ -18,14 +18,6 @@ class GridService {
     return GridEventCreateRow(payload).send();
   }
 
-  Future<Either<RepeatedGridBlock, FlowyError>> getGridBlocks(
-      {required String gridId, required List<GridBlockOrder> blockOrders}) {
-    final payload = QueryGridBlocksPayload.create()
-      ..gridId = gridId
-      ..blockOrders.addAll(blockOrders);
-    return GridEventGetGridBlocks(payload).send();
-  }
-
   Future<Either<RepeatedField, FlowyError>> getFields({required String gridId, required List<FieldOrder> fieldOrders}) {
     final payload = QueryFieldPayload.create()
       ..gridId = gridId

+ 1 - 1
frontend/app_flowy/lib/workspace/application/grid/prelude.dart

@@ -7,7 +7,7 @@ export 'data.dart';
 // Field
 export 'field/field_service.dart';
 export 'field/grid_header_bloc.dart';
-export 'field/action_sheet_bloc.dart';
+export 'field/field_action_sheet_bloc.dart';
 export 'field/field_editor_bloc.dart';
 export 'field/field_switch_bloc.dart';
 

+ 58 - 0
frontend/app_flowy/lib/workspace/application/grid/row/row_action_sheet_bloc.dart

@@ -0,0 +1,58 @@
+import 'package:app_flowy/workspace/application/grid/row/row_service.dart';
+import 'package:flowy_sdk/log.dart';
+import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:freezed_annotation/freezed_annotation.dart';
+import 'dart:async';
+import 'package:dartz/dartz.dart';
+
+part 'row_action_sheet_bloc.freezed.dart';
+
+class RowActionSheetBloc extends Bloc<RowActionSheetEvent, RowActionSheetState> {
+  final RowService _rowService;
+
+  RowActionSheetBloc({required RowData rowData})
+      : _rowService = RowService(gridId: rowData.gridId, rowId: rowData.rowId),
+        super(RowActionSheetState.initial(rowData)) {
+    on<RowActionSheetEvent>(
+      (event, emit) async {
+        await event.map(
+          deleteRow: (_DeleteRow value) async {
+            final result = await _rowService.deleteRow();
+            logResult(result);
+          },
+          duplicateRow: (_DuplicateRow value) async {
+            final result = await _rowService.duplicateRow();
+            logResult(result);
+          },
+        );
+      },
+    );
+  }
+
+  @override
+  Future<void> close() async {
+    return super.close();
+  }
+
+  void logResult(Either<Unit, FlowyError> result) {
+    result.fold((l) => null, (err) => Log.error(err));
+  }
+}
+
+@freezed
+class RowActionSheetEvent with _$RowActionSheetEvent {
+  const factory RowActionSheetEvent.duplicateRow() = _DuplicateRow;
+  const factory RowActionSheetEvent.deleteRow() = _DeleteRow;
+}
+
+@freezed
+class RowActionSheetState with _$RowActionSheetState {
+  const factory RowActionSheetState({
+    required RowData rowData,
+  }) = _RowActionSheetState;
+
+  factory RowActionSheetState.initial(RowData rowData) => RowActionSheetState(
+        rowData: rowData,
+      );
+}

+ 22 - 32
frontend/app_flowy/lib/workspace/application/grid/row/row_bloc.dart

@@ -1,7 +1,6 @@
 import 'dart:collection';
 
 import 'package:app_flowy/workspace/application/grid/field/grid_listenr.dart';
-import 'package:app_flowy/workspace/application/grid/grid_bloc.dart';
 import 'package:flowy_sdk/log.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid-data-model/grid.pb.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
@@ -16,19 +15,14 @@ part 'row_bloc.freezed.dart';
 typedef CellDataMap = LinkedHashMap<String, CellData>;
 
 class RowBloc extends Bloc<RowEvent, RowState> {
-  final RowService rowService;
-  final RowListener rowlistener;
-  final GridFieldsListener fieldListener;
-
-  RowBloc({required RowData rowData, required this.rowlistener})
-      : rowService = RowService(
-          gridId: rowData.gridId,
-          blockId: rowData.blockId,
-          rowId: rowData.rowId,
-        ),
-        fieldListener = GridFieldsListener(
-          gridId: rowData.gridId,
-        ),
+  final RowService _rowService;
+  final RowListener _rowlistener;
+  final GridFieldsListener _fieldListener;
+
+  RowBloc({required RowData rowData})
+      : _rowService = RowService(gridId: rowData.gridId, rowId: rowData.rowId),
+        _fieldListener = GridFieldsListener(gridId: rowData.gridId),
+        _rowlistener = RowListener(rowId: rowData.rowId),
         super(RowState.initial(rowData)) {
     on<RowEvent>(
       (event, emit) async {
@@ -38,7 +32,7 @@ class RowBloc extends Bloc<RowEvent, RowState> {
             await _loadRow(emit);
           },
           createRow: (_CreateRow value) {
-            rowService.createRow();
+            _rowService.createRow();
           },
           didReceiveFieldUpdate: (_DidReceiveFieldUpdate value) async {
             await _handleFieldUpdate(emit, value);
@@ -52,7 +46,7 @@ class RowBloc extends Bloc<RowEvent, RowState> {
   }
 
   void _handleRowUpdate(_DidUpdateRow value, Emitter<RowState> emit) {
-    final CellDataMap cellDataMap = _makeCellDatas(value.row, state.fields);
+    final CellDataMap cellDataMap = _makeCellDatas(value.row, state.rowData.fields);
     emit(state.copyWith(
       row: Future(() => Some(value.row)),
       cellDataMap: Some(cellDataMap),
@@ -67,39 +61,39 @@ class RowBloc extends Bloc<RowEvent, RowState> {
     );
 
     emit(state.copyWith(
-      fields: value.fields,
+      rowData: state.rowData.copyWith(fields: value.fields),
       cellDataMap: Some(cellDataMap),
     ));
   }
 
   @override
   Future<void> close() async {
-    await rowlistener.stop();
-    await fieldListener.stop();
+    await _rowlistener.stop();
+    await _fieldListener.stop();
     return super.close();
   }
 
   Future<void> _startListening() async {
-    rowlistener.updateRowNotifier.addPublishListener((result) {
+    _rowlistener.updateRowNotifier.addPublishListener((result) {
       result.fold(
         (row) => add(RowEvent.didUpdateRow(row)),
         (err) => Log.error(err),
       );
     });
 
-    fieldListener.updateFieldsNotifier.addPublishListener((result) {
+    _fieldListener.updateFieldsNotifier.addPublishListener((result) {
       result.fold(
         (fields) => add(RowEvent.didReceiveFieldUpdate(fields)),
         (err) => Log.error(err),
       );
     });
 
-    rowlistener.start();
-    fieldListener.start();
+    _rowlistener.start();
+    _fieldListener.start();
   }
 
   Future<void> _loadRow(Emitter<RowState> emit) async {
-    rowService.getRow().then((result) {
+    _rowService.getRow().then((result) {
       return result.fold(
         (row) => add(RowEvent.didUpdateRow(row)),
         (err) => Log.error(err),
@@ -113,7 +107,7 @@ class RowBloc extends Bloc<RowEvent, RowState> {
       if (field.visibility) {
         map[field.id] = CellData(
           rowId: row.id,
-          gridId: rowService.gridId,
+          gridId: _rowService.gridId,
           cell: row.cellByFieldId[field.id],
           field: field,
         );
@@ -134,17 +128,13 @@ class RowEvent with _$RowEvent {
 @freezed
 class RowState with _$RowState {
   const factory RowState({
-    required String rowId,
-    required double rowHeight,
-    required List<Field> fields,
+    required RowData rowData,
     required Future<Option<Row>> row,
     required Option<CellDataMap> cellDataMap,
   }) = _RowState;
 
-  factory RowState.initial(RowData data) => RowState(
-        rowId: data.rowId,
-        rowHeight: data.height,
-        fields: data.fields,
+  factory RowState.initial(RowData rowData) => RowState(
+        rowData: rowData,
         row: Future(() => none()),
         cellDataMap: none(),
       );

+ 36 - 3
frontend/app_flowy/lib/workspace/application/grid/row/row_service.dart

@@ -1,5 +1,4 @@
 import 'package:dartz/dartz.dart';
-import 'package:equatable/equatable.dart';
 import 'package:flowy_sdk/dispatch/dispatch.dart';
 import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid-data-model/grid.pb.dart';
@@ -11,9 +10,8 @@ part 'row_service.freezed.dart';
 class RowService {
   final String gridId;
   final String rowId;
-  final String blockId;
 
-  RowService({required this.gridId, required this.rowId, required this.blockId});
+  RowService({required this.gridId, required this.rowId});
 
   Future<Either<Row, FlowyError>> createRow() {
     CreateRowPayload payload = CreateRowPayload.create()
@@ -30,6 +28,22 @@ class RowService {
 
     return GridEventGetRow(payload).send();
   }
+
+  Future<Either<Unit, FlowyError>> deleteRow() {
+    final payload = RowIdentifierPayload.create()
+      ..gridId = gridId
+      ..rowId = rowId;
+
+    return GridEventDeleteRow(payload).send();
+  }
+
+  Future<Either<Unit, FlowyError>> duplicateRow() {
+    final payload = RowIdentifierPayload.create()
+      ..gridId = gridId
+      ..rowId = rowId;
+
+    return GridEventDuplicateRow(payload).send();
+  }
 }
 
 @freezed
@@ -41,3 +55,22 @@ class CellData with _$CellData {
     Cell? cell,
   }) = _CellData;
 }
+
+@freezed
+class RowData with _$RowData {
+  const factory RowData({
+    required String gridId,
+    required String rowId,
+    required List<Field> fields,
+    required double height,
+  }) = _RowData;
+
+  factory RowData.fromBlockRow(String gridId, RowOrder row, List<Field> fields) {
+    return RowData(
+      gridId: gridId,
+      rowId: row.rowId,
+      fields: fields,
+      height: row.height.toDouble(),
+    );
+  }
+}

+ 34 - 14
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/grid_page.dart

@@ -1,5 +1,6 @@
 import 'package:app_flowy/startup/startup.dart';
 import 'package:app_flowy/workspace/application/grid/grid_bloc.dart';
+import 'package:app_flowy/workspace/application/grid/row/row_service.dart';
 import 'package:flowy_infra_ui/style_widget/scrolling/styled_list.dart';
 import 'package:flowy_infra_ui/style_widget/scrolling/styled_scroll_bar.dart';
 import 'package:flowy_infra_ui/style_widget/scrolling/styled_scrollview.dart';
@@ -72,6 +73,7 @@ class FlowyGrid extends StatefulWidget {
 
 class _FlowyGridState extends State<FlowyGrid> {
   final _scrollController = GridScrollController();
+  // final _key = GlobalKey<SliverAnimatedListState>();
 
   @override
   void dispose() {
@@ -90,6 +92,7 @@ class _FlowyGridState extends State<FlowyGrid> {
           return const Center(child: CircularProgressIndicator.adaptive());
         }
 
+// _key.currentState.insertItem(index)
         final child = BlocBuilder<GridBloc, GridState>(
           builder: (context, state) {
             return SizedBox(
@@ -102,7 +105,7 @@ class _FlowyGridState extends State<FlowyGrid> {
                   slivers: [
                     _renderToolbar(gridId),
                     GridHeader(gridId: gridId, fields: List.from(state.fields)),
-                    _renderRows(context),
+                    _renderRows(gridId: gridId, context: context),
                     const GridFooter(),
                   ],
                 ),
@@ -144,7 +147,7 @@ class _FlowyGridState extends State<FlowyGrid> {
     );
   }
 
-  Widget _renderRows(BuildContext context) {
+  Widget _renderRows({required String gridId, required BuildContext context}) {
     return BlocBuilder<GridBloc, GridState>(
       buildWhen: (previous, current) {
         final rowChanged = previous.rows.length != current.rows.length;
@@ -153,19 +156,36 @@ class _FlowyGridState extends State<FlowyGrid> {
       },
       builder: (context, state) {
         return SliverList(
-          delegate: SliverChildBuilderDelegate(
-            (context, index) {
-              final blockRow = context.read<GridBloc>().state.rows[index];
-              final fields = context.read<GridBloc>().state.fields;
-              final rowData = RowData.fromBlockRow(blockRow, fields);
-              return GridRowWidget(data: rowData, key: ValueKey(rowData.rowId));
-            },
-            childCount: context.read<GridBloc>().state.rows.length,
-            addRepaintBoundaries: true,
-            addAutomaticKeepAlives: true,
-          ),
-        );
+            delegate: SliverChildBuilderDelegate(
+          (context, index) {
+            final blockRow = context.read<GridBloc>().state.rows[index];
+            final fields = context.read<GridBloc>().state.fields;
+            final rowData = RowData.fromBlockRow(gridId, blockRow, fields);
+            return GridRowWidget(data: rowData, key: ValueKey(rowData.rowId));
+          },
+          childCount: context.read<GridBloc>().state.rows.length,
+          addRepaintBoundaries: true,
+          addAutomaticKeepAlives: true,
+        ));
+
+        // return SliverAnimatedList(
+        //   key: _key,
+        //   initialItemCount: context.read<GridBloc>().state.rows.length,
+        //   itemBuilder: (BuildContext context, int index, Animation<double> animation) {
+        //     final blockRow = context.read<GridBloc>().state.rows[index];
+        //     final fields = context.read<GridBloc>().state.fields;
+        //     final rowData = RowData.fromBlockRow(blockRow, fields);
+        //     return _renderRow(rowData, animation);
+        //   },
+        // );
       },
     );
   }
+
+  // Widget _renderRow(RowData rowData, Animation<double> animation) {
+  //   return SizeTransition(
+  //     sizeFactor: animation,
+  //     child: GridRowWidget(data: rowData, key: ValueKey(rowData.rowId)),
+  //   );
+  // }
 }

+ 1 - 1
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/layout/sizes.dart

@@ -6,7 +6,7 @@ class GridSize {
   static double get scrollBarSize => 12 * scale;
   static double get headerHeight => 40 * scale;
   static double get footerHeight => 40 * scale;
-  static double get leadingHeaderPadding => 30 * scale;
+  static double get leadingHeaderPadding => 50 * scale;
   static double get trailHeaderPadding => 140 * scale;
   static double get headerContainerPadding => 0 * scale;
   static double get cellHPadding => 10 * scale;

+ 0 - 96
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/selection_cell/extension.dart

@@ -1,6 +1,3 @@
-import 'dart:collection';
-
-import 'package:flowy_infra/size.dart';
 import 'package:flowy_infra/theme.dart';
 import 'package:flowy_infra_ui/style_widget/text.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid/selection_type_option.pb.dart';
@@ -8,7 +5,6 @@ import 'package:flutter/material.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:app_flowy/generated/locale_keys.g.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
-import 'package:textfield_tags/textfield_tags.dart';
 
 extension SelectOptionColorExtension on SelectOptionColor {
   Color make(BuildContext context) {
@@ -63,98 +59,6 @@ extension SelectOptionColorExtension on SelectOptionColor {
   }
 }
 
-class SelectOptionTextField extends StatelessWidget {
-  final FocusNode _focusNode;
-  final TextEditingController _controller;
-  final TextfieldTagsController tagController;
-  final List<SelectOption> options;
-  final LinkedHashMap<String, SelectOption> selectedOptionMap;
-
-  final double distanceToText;
-
-  final Function(String) onNewTag;
-
-  SelectOptionTextField({
-    required this.options,
-    required this.selectedOptionMap,
-    required this.distanceToText,
-    required this.tagController,
-    required this.onNewTag,
-    TextEditingController? controller,
-    FocusNode? focusNode,
-    Key? key,
-  })  : _controller = controller ?? TextEditingController(),
-        _focusNode = focusNode ?? FocusNode(),
-        super(key: key);
-
-  @override
-  Widget build(BuildContext context) {
-    final theme = context.watch<AppTheme>();
-
-    return TextFieldTags(
-      textEditingController: _controller,
-      textfieldTagsController: tagController,
-      initialTags: selectedOptionMap.keys.toList(),
-      focusNode: _focusNode,
-      textSeparators: const [' ', ','],
-      inputfieldBuilder: (BuildContext context, editController, focusNode, error, onChanged, onSubmitted) {
-        return ((context, sc, tags, onTagDelegate) {
-          tags.retainWhere((name) {
-            return options.where((option) => option.name == name).isEmpty;
-          });
-          if (tags.isNotEmpty) {
-            assert(tags.length == 1);
-            onNewTag(tags.first);
-          }
-
-          return TextField(
-            autofocus: true,
-            controller: editController,
-            focusNode: focusNode,
-            onChanged: onChanged,
-            onSubmitted: onSubmitted,
-            maxLines: 1,
-            style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
-            decoration: InputDecoration(
-              enabledBorder: OutlineInputBorder(
-                borderSide: BorderSide(color: theme.main1, width: 1.0),
-                borderRadius: Corners.s10Border,
-              ),
-              isDense: true,
-              prefixIcon: _renderTags(sc),
-              hintText: LocaleKeys.grid_selectOption_searchOption.tr(),
-              prefixIconConstraints: BoxConstraints(maxWidth: distanceToText),
-              focusedBorder: OutlineInputBorder(
-                borderSide: BorderSide(
-                  color: theme.main1,
-                  width: 1.0,
-                ),
-                borderRadius: Corners.s10Border,
-              ),
-            ),
-          );
-        });
-      },
-    );
-  }
-
-  Widget? _renderTags(ScrollController sc) {
-    if (selectedOptionMap.isEmpty) {
-      return null;
-    }
-
-    final children = selectedOptionMap.values.map((option) => SelectOptionTag(option: option)).toList();
-    return Padding(
-      padding: const EdgeInsets.all(8.0),
-      child: SingleChildScrollView(
-        controller: sc,
-        scrollDirection: Axis.horizontal,
-        child: Row(children: children),
-      ),
-    );
-  }
-}
-
 class SelectOptionTag extends StatelessWidget {
   final SelectOption option;
   final bool isSelected;

+ 2 - 3
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/selection_cell/selection_editor.dart

@@ -1,5 +1,4 @@
 import 'dart:collection';
-
 import 'package:app_flowy/workspace/application/grid/cell_bloc/selection_editor_bloc.dart';
 import 'package:app_flowy/workspace/application/grid/row/row_service.dart';
 import 'package:app_flowy/workspace/presentation/plugins/grid/src/layout/sizes.dart';
@@ -21,6 +20,7 @@ import 'package:app_flowy/generated/locale_keys.g.dart';
 import 'package:textfield_tags/textfield_tags.dart';
 
 import 'extension.dart';
+import 'text_field.dart';
 
 const double _editorPannelWidth = 300;
 
@@ -135,8 +135,7 @@ class _TextField extends StatelessWidget {
 
   @override
   Widget build(BuildContext context) {
-    return BlocConsumer<SelectOptionEditorBloc, SelectOptionEditorState>(
-      listener: (context, state) {},
+    return BlocBuilder<SelectOptionEditorBloc, SelectOptionEditorState>(
       builder: (context, state) {
         final optionMap = LinkedHashMap<String, SelectOption>.fromIterable(state.selectedOptions,
             key: (option) => option.name, value: (option) => option);

+ 101 - 0
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/selection_cell/text_field.dart

@@ -0,0 +1,101 @@
+import 'dart:collection';
+
+import 'package:flowy_infra/size.dart';
+import 'package:flowy_infra/theme.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/selection_type_option.pb.dart';
+import 'package:flutter/material.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:app_flowy/generated/locale_keys.g.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:textfield_tags/textfield_tags.dart';
+
+import 'extension.dart';
+
+class SelectOptionTextField extends StatelessWidget {
+  final FocusNode _focusNode;
+  final TextEditingController _controller;
+  final TextfieldTagsController tagController;
+  final List<SelectOption> options;
+  final LinkedHashMap<String, SelectOption> selectedOptionMap;
+
+  final double distanceToText;
+
+  final Function(String) onNewTag;
+
+  SelectOptionTextField({
+    required this.options,
+    required this.selectedOptionMap,
+    required this.distanceToText,
+    required this.tagController,
+    required this.onNewTag,
+    TextEditingController? controller,
+    FocusNode? focusNode,
+    Key? key,
+  })  : _controller = controller ?? TextEditingController(),
+        _focusNode = focusNode ?? FocusNode(),
+        super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    final theme = context.watch<AppTheme>();
+
+    return TextFieldTags(
+      textEditingController: _controller,
+      textfieldTagsController: tagController,
+      initialTags: selectedOptionMap.keys.toList(),
+      focusNode: _focusNode,
+      textSeparators: const [' ', ','],
+      inputfieldBuilder: (BuildContext context, editController, focusNode, error, onChanged, onSubmitted) {
+        return ((context, sc, tags, onTagDelegate) {
+          return TextField(
+            autofocus: true,
+            controller: editController,
+            focusNode: focusNode,
+            onChanged: onChanged,
+            onSubmitted: (text) {
+              if (onSubmitted != null) {
+                onSubmitted(text);
+              }
+
+              if (text.isNotEmpty) {
+                onNewTag(text);
+              }
+            },
+            maxLines: 1,
+            style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
+            decoration: InputDecoration(
+              enabledBorder: OutlineInputBorder(
+                borderSide: BorderSide(color: theme.main1, width: 1.0),
+                borderRadius: Corners.s10Border,
+              ),
+              isDense: true,
+              prefixIcon: _renderTags(sc),
+              hintText: LocaleKeys.grid_selectOption_searchOption.tr(),
+              prefixIconConstraints: BoxConstraints(maxWidth: distanceToText),
+              focusedBorder: OutlineInputBorder(
+                borderSide: BorderSide(color: theme.main1, width: 1.0),
+                borderRadius: Corners.s10Border,
+              ),
+            ),
+          );
+        });
+      },
+    );
+  }
+
+  Widget? _renderTags(ScrollController sc) {
+    if (selectedOptionMap.isEmpty) {
+      return null;
+    }
+
+    final children = selectedOptionMap.values.map((option) => SelectOptionTag(option: option)).toList();
+    return Padding(
+      padding: const EdgeInsets.all(8.0),
+      child: SingleChildScrollView(
+        controller: sc,
+        scrollDirection: Axis.horizontal,
+        child: Row(children: children),
+      ),
+    );
+  }
+}

+ 5 - 15
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/field_cell_action_sheet.dart

@@ -23,7 +23,7 @@ class GridFieldCellActionSheet extends StatelessWidget with FlowyOverlayDelegate
         child: this,
         constraints: BoxConstraints.loose(const Size(240, 200)),
       ),
-      identifier: identifier(),
+      identifier: GridFieldCellActionSheet.identifier(),
       anchorContext: overlayContext,
       anchorDirection: AnchorDirection.bottomWithLeftAligned,
       delegate: this,
@@ -68,7 +68,7 @@ class _EditFieldButton extends StatelessWidget {
   @override
   Widget build(BuildContext context) {
     final theme = context.watch<AppTheme>();
-    return BlocBuilder<FieldActionSheetBloc, ActionSheetState>(
+    return BlocBuilder<FieldActionSheetBloc, FieldActionSheetState>(
       builder: (context, state) {
         return SizedBox(
           height: GridSize.typeOptionItemHeight,
@@ -100,16 +100,6 @@ class _FieldOperationList extends StatelessWidget {
         )
         .toList();
 
-    return FieldOperationList(actions: actions);
-  }
-}
-
-class FieldOperationList extends StatelessWidget {
-  final List<FieldActionCell> actions;
-  const FieldOperationList({required this.actions, Key? key}) : super(key: key);
-
-  @override
-  Widget build(BuildContext context) {
     return GridView(
       // https://api.flutter.dev/flutter/widgets/AnimatedList/shrinkWrap.html
       shrinkWrap: true,
@@ -182,13 +172,13 @@ extension _FieldActionExtension on FieldAction {
   void run(BuildContext context) {
     switch (this) {
       case FieldAction.hide:
-        context.read<FieldActionSheetBloc>().add(const ActionSheetEvent.hideField());
+        context.read<FieldActionSheetBloc>().add(const FieldActionSheetEvent.hideField());
         break;
       case FieldAction.duplicate:
-        context.read<FieldActionSheetBloc>().add(const ActionSheetEvent.duplicateField());
+        context.read<FieldActionSheetBloc>().add(const FieldActionSheetEvent.duplicateField());
         break;
       case FieldAction.delete:
-        context.read<FieldActionSheetBloc>().add(const ActionSheetEvent.deleteField());
+        context.read<FieldActionSheetBloc>().add(const FieldActionSheetEvent.deleteField());
         break;
     }
   }

+ 29 - 6
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/row/grid_row.dart

@@ -9,6 +9,8 @@ import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:provider/provider.dart';
 
+import 'row_action_sheet.dart';
+
 class GridRowWidget extends StatefulWidget {
   final RowData data;
   const GridRowWidget({required this.data, Key? key}) : super(key: key);
@@ -39,10 +41,10 @@ class _GridRowWidgetState extends State<GridRowWidget> {
           onEnter: (p) => _rowStateNotifier.onEnter = true,
           onExit: (p) => _rowStateNotifier.onEnter = false,
           child: BlocBuilder<RowBloc, RowState>(
-            buildWhen: (p, c) => p.rowHeight != c.rowHeight,
+            buildWhen: (p, c) => p.rowData.height != c.rowData.height,
             builder: (context, state) {
               return SizedBox(
-                height: _rowBloc.state.rowHeight,
+                height: _rowBloc.state.rowData.height,
                 child: Row(
                   crossAxisAlignment: CrossAxisAlignment.stretch,
                   children: const [
@@ -83,7 +85,8 @@ class _RowLeading extends StatelessWidget {
     return Row(
       mainAxisAlignment: MainAxisAlignment.center,
       children: const [
-        AppendRowButton(),
+        _InsertRowButton(),
+        _DeleteRowButton(),
       ],
     );
   }
@@ -98,15 +101,16 @@ class _RowTrailing extends StatelessWidget {
   }
 }
 
-class AppendRowButton extends StatelessWidget {
-  const AppendRowButton({Key? key}) : super(key: key);
+class _InsertRowButton extends StatelessWidget {
+  const _InsertRowButton({Key? key}) : super(key: key);
 
   @override
   Widget build(BuildContext context) {
     final theme = context.watch<AppTheme>();
     return FlowyIconButton(
       hoverColor: theme.hover,
-      width: 22,
+      width: 20,
+      height: 30,
       onPressed: () => context.read<RowBloc>().add(const RowEvent.createRow()),
       iconPadding: const EdgeInsets.all(3),
       icon: svgWidget("home/add"),
@@ -114,6 +118,25 @@ class AppendRowButton extends StatelessWidget {
   }
 }
 
+class _DeleteRowButton extends StatelessWidget {
+  const _DeleteRowButton({Key? key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    final theme = context.watch<AppTheme>();
+    return FlowyIconButton(
+      hoverColor: theme.hover,
+      width: 20,
+      height: 30,
+      onPressed: () => GridRowActionSheet(
+        rowData: context.read<RowBloc>().state.rowData,
+      ).show(context),
+      iconPadding: const EdgeInsets.all(3),
+      icon: svgWidget("editor/details"),
+    );
+  }
+}
+
 class _RowCells extends StatelessWidget {
   const _RowCells({Key? key}) : super(key: key);
 

+ 148 - 0
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/row/row_action_sheet.dart

@@ -0,0 +1,148 @@
+import 'package:app_flowy/workspace/application/grid/row/row_action_sheet_bloc.dart';
+import 'package:app_flowy/workspace/application/grid/row/row_service.dart';
+import 'package:app_flowy/workspace/presentation/plugins/grid/src/layout/sizes.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:app_flowy/generated/locale_keys.g.dart';
+import 'package:flowy_infra/image.dart';
+import 'package:flowy_infra/theme.dart';
+import 'package:flowy_infra_ui/flowy_infra_ui.dart';
+import 'package:flowy_infra_ui/style_widget/button.dart';
+import 'package:flowy_infra_ui/style_widget/scrolling/styled_list.dart';
+import 'package:flowy_infra_ui/style_widget/text.dart';
+import 'package:flowy_infra_ui/widget/spacing.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+class GridRowActionSheet extends StatelessWidget {
+  final RowData rowData;
+  const GridRowActionSheet({required this.rowData, Key? key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return BlocProvider(
+      create: (context) => RowActionSheetBloc(rowData: rowData),
+      child: BlocBuilder<RowActionSheetBloc, RowActionSheetState>(
+        builder: (context, state) {
+          final cells = _RowAction.values
+              .map(
+                (action) => _RowActionCell(
+                  action: action,
+                  onDismissed: () => remove(context),
+                ),
+              )
+              .toList();
+
+          //
+          final list = ListView.separated(
+            shrinkWrap: true,
+            controller: ScrollController(),
+            itemCount: cells.length,
+            separatorBuilder: (context, index) {
+              return VSpace(GridSize.typeOptionSeparatorHeight);
+            },
+            physics: StyledScrollPhysics(),
+            itemBuilder: (BuildContext context, int index) {
+              return cells[index];
+            },
+          );
+          return list;
+        },
+      ),
+    );
+  }
+
+  void show(BuildContext overlayContext) {
+    FlowyOverlay.of(overlayContext).insertWithAnchor(
+      widget: OverlayContainer(
+        child: this,
+        constraints: BoxConstraints.loose(const Size(140, 200)),
+      ),
+      identifier: GridRowActionSheet.identifier(),
+      anchorContext: overlayContext,
+      anchorDirection: AnchorDirection.leftWithCenterAligned,
+    );
+  }
+
+  void remove(BuildContext overlayContext) {
+    FlowyOverlay.of(overlayContext).remove(GridRowActionSheet.identifier());
+  }
+
+  static String identifier() {
+    return (GridRowActionSheet).toString();
+  }
+}
+
+class _RowActionCell extends StatelessWidget {
+  final _RowAction action;
+  final VoidCallback onDismissed;
+  const _RowActionCell({required this.action, required this.onDismissed, Key? key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    final theme = context.watch<AppTheme>();
+
+    return SizedBox(
+      height: GridSize.typeOptionItemHeight,
+      child: FlowyButton(
+        text: FlowyText.medium(
+          action.title(),
+          fontSize: 12,
+          color: action.enable() ? theme.textColor : theme.shader3,
+        ),
+        hoverColor: theme.hover,
+        onTap: () {
+          if (action.enable()) {
+            action.performAction(context);
+          }
+          onDismissed();
+        },
+        leftIcon: svgWidget(action.iconName(), color: theme.iconColor),
+      ),
+    );
+  }
+}
+
+enum _RowAction {
+  delete,
+  duplicate,
+}
+
+extension _RowActionExtension on _RowAction {
+  String iconName() {
+    switch (this) {
+      case _RowAction.duplicate:
+        return 'grid/duplicate';
+      case _RowAction.delete:
+        return 'grid/delete';
+    }
+  }
+
+  String title() {
+    switch (this) {
+      case _RowAction.duplicate:
+        return LocaleKeys.grid_row_duplicate.tr();
+      case _RowAction.delete:
+        return LocaleKeys.grid_row_delete.tr();
+    }
+  }
+
+  bool enable() {
+    switch (this) {
+      case _RowAction.duplicate:
+        return false;
+      case _RowAction.delete:
+        return true;
+    }
+  }
+
+  void performAction(BuildContext context) {
+    switch (this) {
+      case _RowAction.duplicate:
+        context.read<RowActionSheetBloc>().add(const RowActionSheetEvent.duplicateRow());
+        break;
+      case _RowAction.delete:
+        context.read<RowActionSheetBloc>().add(const RowActionSheetEvent.deleteRow());
+        break;
+    }
+  }
+}

+ 34 - 0
frontend/app_flowy/packages/flowy_sdk/lib/dispatch/dart_event/flowy-grid/dart_event.dart

@@ -239,6 +239,40 @@ class GridEventGetRow {
     }
 }
 
+class GridEventDeleteRow {
+     RowIdentifierPayload request;
+     GridEventDeleteRow(this.request);
+
+    Future<Either<Unit, FlowyError>> send() {
+    final request = FFIRequest.create()
+          ..event = GridEvent.DeleteRow.toString()
+          ..payload = requestToBytes(this.request);
+
+    return Dispatch.asyncRequest(request)
+        .then((bytesResult) => bytesResult.fold(
+           (bytes) => left(unit),
+           (errBytes) => right(FlowyError.fromBuffer(errBytes)),
+        ));
+    }
+}
+
+class GridEventDuplicateRow {
+     RowIdentifierPayload request;
+     GridEventDuplicateRow(this.request);
+
+    Future<Either<Unit, FlowyError>> send() {
+    final request = FFIRequest.create()
+          ..event = GridEvent.DuplicateRow.toString()
+          ..payload = requestToBytes(this.request);
+
+    return Dispatch.asyncRequest(request)
+        .then((bytesResult) => bytesResult.fold(
+           (bytes) => left(unit),
+           (errBytes) => right(FlowyError.fromBuffer(errBytes)),
+        ));
+    }
+}
+
 class GridEventGetCell {
      CellIdentifierPayload request;
      GridEventGetCell(this.request);

+ 155 - 0
frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid-data-model/grid.pb.dart

@@ -803,17 +803,22 @@ class RepeatedGridBlock extends $pb.GeneratedMessage {
 class GridBlockOrder extends $pb.GeneratedMessage {
   static final $pb.BuilderInfo _i = $pb.BuilderInfo(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'GridBlockOrder', createEmptyInstance: create)
     ..aOS(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'blockId')
+    ..pc<RowOrder>(2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'rowOrders', $pb.PbFieldType.PM, subBuilder: RowOrder.create)
     ..hasRequiredFields = false
   ;
 
   GridBlockOrder._() : super();
   factory GridBlockOrder({
     $core.String? blockId,
+    $core.Iterable<RowOrder>? rowOrders,
   }) {
     final _result = create();
     if (blockId != null) {
       _result.blockId = blockId;
     }
+    if (rowOrders != null) {
+      _result.rowOrders.addAll(rowOrders);
+    }
     return _result;
   }
   factory GridBlockOrder.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
@@ -845,6 +850,156 @@ class GridBlockOrder extends $pb.GeneratedMessage {
   $core.bool hasBlockId() => $_has(0);
   @$pb.TagNumber(1)
   void clearBlockId() => clearField(1);
+
+  @$pb.TagNumber(2)
+  $core.List<RowOrder> get rowOrders => $_getList(1);
+}
+
+class GridBlockOrderChangeset extends $pb.GeneratedMessage {
+  static final $pb.BuilderInfo _i = $pb.BuilderInfo(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'GridBlockOrderChangeset', createEmptyInstance: create)
+    ..aOS(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'blockId')
+    ..pc<IndexRowOrder>(2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'insertedRows', $pb.PbFieldType.PM, subBuilder: IndexRowOrder.create)
+    ..pc<RowOrder>(3, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'deletedRows', $pb.PbFieldType.PM, subBuilder: RowOrder.create)
+    ..pc<RowOrder>(4, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'updatedRows', $pb.PbFieldType.PM, subBuilder: RowOrder.create)
+    ..hasRequiredFields = false
+  ;
+
+  GridBlockOrderChangeset._() : super();
+  factory GridBlockOrderChangeset({
+    $core.String? blockId,
+    $core.Iterable<IndexRowOrder>? insertedRows,
+    $core.Iterable<RowOrder>? deletedRows,
+    $core.Iterable<RowOrder>? updatedRows,
+  }) {
+    final _result = create();
+    if (blockId != null) {
+      _result.blockId = blockId;
+    }
+    if (insertedRows != null) {
+      _result.insertedRows.addAll(insertedRows);
+    }
+    if (deletedRows != null) {
+      _result.deletedRows.addAll(deletedRows);
+    }
+    if (updatedRows != null) {
+      _result.updatedRows.addAll(updatedRows);
+    }
+    return _result;
+  }
+  factory GridBlockOrderChangeset.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
+  factory GridBlockOrderChangeset.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
+  @$core.Deprecated(
+  'Using this can add significant overhead to your binary. '
+  'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
+  'Will be removed in next major version')
+  GridBlockOrderChangeset clone() => GridBlockOrderChangeset()..mergeFromMessage(this);
+  @$core.Deprecated(
+  'Using this can add significant overhead to your binary. '
+  'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
+  'Will be removed in next major version')
+  GridBlockOrderChangeset copyWith(void Function(GridBlockOrderChangeset) updates) => super.copyWith((message) => updates(message as GridBlockOrderChangeset)) as GridBlockOrderChangeset; // ignore: deprecated_member_use
+  $pb.BuilderInfo get info_ => _i;
+  @$core.pragma('dart2js:noInline')
+  static GridBlockOrderChangeset create() => GridBlockOrderChangeset._();
+  GridBlockOrderChangeset createEmptyInstance() => create();
+  static $pb.PbList<GridBlockOrderChangeset> createRepeated() => $pb.PbList<GridBlockOrderChangeset>();
+  @$core.pragma('dart2js:noInline')
+  static GridBlockOrderChangeset getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<GridBlockOrderChangeset>(create);
+  static GridBlockOrderChangeset? _defaultInstance;
+
+  @$pb.TagNumber(1)
+  $core.String get blockId => $_getSZ(0);
+  @$pb.TagNumber(1)
+  set blockId($core.String v) { $_setString(0, v); }
+  @$pb.TagNumber(1)
+  $core.bool hasBlockId() => $_has(0);
+  @$pb.TagNumber(1)
+  void clearBlockId() => clearField(1);
+
+  @$pb.TagNumber(2)
+  $core.List<IndexRowOrder> get insertedRows => $_getList(1);
+
+  @$pb.TagNumber(3)
+  $core.List<RowOrder> get deletedRows => $_getList(2);
+
+  @$pb.TagNumber(4)
+  $core.List<RowOrder> get updatedRows => $_getList(3);
+}
+
+enum IndexRowOrder_OneOfIndex {
+  index_, 
+  notSet
+}
+
+class IndexRowOrder extends $pb.GeneratedMessage {
+  static const $core.Map<$core.int, IndexRowOrder_OneOfIndex> _IndexRowOrder_OneOfIndexByTag = {
+    2 : IndexRowOrder_OneOfIndex.index_,
+    0 : IndexRowOrder_OneOfIndex.notSet
+  };
+  static final $pb.BuilderInfo _i = $pb.BuilderInfo(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'IndexRowOrder', createEmptyInstance: create)
+    ..oo(0, [2])
+    ..aOM<RowOrder>(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'rowOrder', subBuilder: RowOrder.create)
+    ..a<$core.int>(2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'index', $pb.PbFieldType.O3)
+    ..hasRequiredFields = false
+  ;
+
+  IndexRowOrder._() : super();
+  factory IndexRowOrder({
+    RowOrder? rowOrder,
+    $core.int? index,
+  }) {
+    final _result = create();
+    if (rowOrder != null) {
+      _result.rowOrder = rowOrder;
+    }
+    if (index != null) {
+      _result.index = index;
+    }
+    return _result;
+  }
+  factory IndexRowOrder.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
+  factory IndexRowOrder.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
+  @$core.Deprecated(
+  'Using this can add significant overhead to your binary. '
+  'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
+  'Will be removed in next major version')
+  IndexRowOrder clone() => IndexRowOrder()..mergeFromMessage(this);
+  @$core.Deprecated(
+  'Using this can add significant overhead to your binary. '
+  'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
+  'Will be removed in next major version')
+  IndexRowOrder copyWith(void Function(IndexRowOrder) updates) => super.copyWith((message) => updates(message as IndexRowOrder)) as IndexRowOrder; // ignore: deprecated_member_use
+  $pb.BuilderInfo get info_ => _i;
+  @$core.pragma('dart2js:noInline')
+  static IndexRowOrder create() => IndexRowOrder._();
+  IndexRowOrder createEmptyInstance() => create();
+  static $pb.PbList<IndexRowOrder> createRepeated() => $pb.PbList<IndexRowOrder>();
+  @$core.pragma('dart2js:noInline')
+  static IndexRowOrder getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<IndexRowOrder>(create);
+  static IndexRowOrder? _defaultInstance;
+
+  IndexRowOrder_OneOfIndex whichOneOfIndex() => _IndexRowOrder_OneOfIndexByTag[$_whichOneof(0)]!;
+  void clearOneOfIndex() => clearField($_whichOneof(0));
+
+  @$pb.TagNumber(1)
+  RowOrder get rowOrder => $_getN(0);
+  @$pb.TagNumber(1)
+  set rowOrder(RowOrder v) { setField(1, v); }
+  @$pb.TagNumber(1)
+  $core.bool hasRowOrder() => $_has(0);
+  @$pb.TagNumber(1)
+  void clearRowOrder() => clearField(1);
+  @$pb.TagNumber(1)
+  RowOrder ensureRowOrder() => $_ensure(0);
+
+  @$pb.TagNumber(2)
+  $core.int get index => $_getIZ(1);
+  @$pb.TagNumber(2)
+  set index($core.int v) { $_setSignedInt32(1, v); }
+  @$pb.TagNumber(2)
+  $core.bool hasIndex() => $_has(1);
+  @$pb.TagNumber(2)
+  void clearIndex() => clearField(2);
 }
 
 class GridBlock extends $pb.GeneratedMessage {

+ 29 - 1
frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid-data-model/grid.pbjson.dart

@@ -165,11 +165,39 @@ const GridBlockOrder$json = const {
   '1': 'GridBlockOrder',
   '2': const [
     const {'1': 'block_id', '3': 1, '4': 1, '5': 9, '10': 'blockId'},
+    const {'1': 'row_orders', '3': 2, '4': 3, '5': 11, '6': '.RowOrder', '10': 'rowOrders'},
   ],
 };
 
 /// Descriptor for `GridBlockOrder`. Decode as a `google.protobuf.DescriptorProto`.
-final $typed_data.Uint8List gridBlockOrderDescriptor = $convert.base64Decode('Cg5HcmlkQmxvY2tPcmRlchIZCghibG9ja19pZBgBIAEoCVIHYmxvY2tJZA==');
+final $typed_data.Uint8List gridBlockOrderDescriptor = $convert.base64Decode('Cg5HcmlkQmxvY2tPcmRlchIZCghibG9ja19pZBgBIAEoCVIHYmxvY2tJZBIoCgpyb3dfb3JkZXJzGAIgAygLMgkuUm93T3JkZXJSCXJvd09yZGVycw==');
+@$core.Deprecated('Use gridBlockOrderChangesetDescriptor instead')
+const GridBlockOrderChangeset$json = const {
+  '1': 'GridBlockOrderChangeset',
+  '2': const [
+    const {'1': 'block_id', '3': 1, '4': 1, '5': 9, '10': 'blockId'},
+    const {'1': 'inserted_rows', '3': 2, '4': 3, '5': 11, '6': '.IndexRowOrder', '10': 'insertedRows'},
+    const {'1': 'deleted_rows', '3': 3, '4': 3, '5': 11, '6': '.RowOrder', '10': 'deletedRows'},
+    const {'1': 'updated_rows', '3': 4, '4': 3, '5': 11, '6': '.RowOrder', '10': 'updatedRows'},
+  ],
+};
+
+/// Descriptor for `GridBlockOrderChangeset`. Decode as a `google.protobuf.DescriptorProto`.
+final $typed_data.Uint8List gridBlockOrderChangesetDescriptor = $convert.base64Decode('ChdHcmlkQmxvY2tPcmRlckNoYW5nZXNldBIZCghibG9ja19pZBgBIAEoCVIHYmxvY2tJZBIzCg1pbnNlcnRlZF9yb3dzGAIgAygLMg4uSW5kZXhSb3dPcmRlclIMaW5zZXJ0ZWRSb3dzEiwKDGRlbGV0ZWRfcm93cxgDIAMoCzIJLlJvd09yZGVyUgtkZWxldGVkUm93cxIsCgx1cGRhdGVkX3Jvd3MYBCADKAsyCS5Sb3dPcmRlclILdXBkYXRlZFJvd3M=');
+@$core.Deprecated('Use indexRowOrderDescriptor instead')
+const IndexRowOrder$json = const {
+  '1': 'IndexRowOrder',
+  '2': const [
+    const {'1': 'row_order', '3': 1, '4': 1, '5': 11, '6': '.RowOrder', '10': 'rowOrder'},
+    const {'1': 'index', '3': 2, '4': 1, '5': 5, '9': 0, '10': 'index'},
+  ],
+  '8': const [
+    const {'1': 'one_of_index'},
+  ],
+};
+
+/// Descriptor for `IndexRowOrder`. Decode as a `google.protobuf.DescriptorProto`.
+final $typed_data.Uint8List indexRowOrderDescriptor = $convert.base64Decode('Cg1JbmRleFJvd09yZGVyEiYKCXJvd19vcmRlchgBIAEoCzIJLlJvd09yZGVyUghyb3dPcmRlchIWCgVpbmRleBgCIAEoBUgAUgVpbmRleEIOCgxvbmVfb2ZfaW5kZXg=');
 @$core.Deprecated('Use gridBlockDescriptor instead')
 const GridBlock$json = const {
   '1': 'GridBlock',

+ 19 - 19
frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid-data-model/meta.pb.dart

@@ -17,7 +17,7 @@ class GridMeta extends $pb.GeneratedMessage {
   static final $pb.BuilderInfo _i = $pb.BuilderInfo(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'GridMeta', createEmptyInstance: create)
     ..aOS(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'gridId')
     ..pc<FieldMeta>(2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'fields', $pb.PbFieldType.PM, subBuilder: FieldMeta.create)
-    ..pc<GridBlockMeta>(3, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'blockMetas', $pb.PbFieldType.PM, subBuilder: GridBlockMeta.create)
+    ..pc<GridBlockMeta>(3, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'blocks', $pb.PbFieldType.PM, subBuilder: GridBlockMeta.create)
     ..hasRequiredFields = false
   ;
 
@@ -25,7 +25,7 @@ class GridMeta extends $pb.GeneratedMessage {
   factory GridMeta({
     $core.String? gridId,
     $core.Iterable<FieldMeta>? fields,
-    $core.Iterable<GridBlockMeta>? blockMetas,
+    $core.Iterable<GridBlockMeta>? blocks,
   }) {
     final _result = create();
     if (gridId != null) {
@@ -34,8 +34,8 @@ class GridMeta extends $pb.GeneratedMessage {
     if (fields != null) {
       _result.fields.addAll(fields);
     }
-    if (blockMetas != null) {
-      _result.blockMetas.addAll(blockMetas);
+    if (blocks != null) {
+      _result.blocks.addAll(blocks);
     }
     return _result;
   }
@@ -73,7 +73,7 @@ class GridMeta extends $pb.GeneratedMessage {
   $core.List<FieldMeta> get fields => $_getList(1);
 
   @$pb.TagNumber(3)
-  $core.List<GridBlockMeta> get blockMetas => $_getList(2);
+  $core.List<GridBlockMeta> get blocks => $_getList(2);
 }
 
 class GridBlockMeta extends $pb.GeneratedMessage {
@@ -154,21 +154,21 @@ class GridBlockMeta extends $pb.GeneratedMessage {
 class GridBlockMetaData extends $pb.GeneratedMessage {
   static final $pb.BuilderInfo _i = $pb.BuilderInfo(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'GridBlockMetaData', createEmptyInstance: create)
     ..aOS(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'blockId')
-    ..pc<RowMeta>(2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'rowMetas', $pb.PbFieldType.PM, subBuilder: RowMeta.create)
+    ..pc<RowMeta>(2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'rows', $pb.PbFieldType.PM, subBuilder: RowMeta.create)
     ..hasRequiredFields = false
   ;
 
   GridBlockMetaData._() : super();
   factory GridBlockMetaData({
     $core.String? blockId,
-    $core.Iterable<RowMeta>? rowMetas,
+    $core.Iterable<RowMeta>? rows,
   }) {
     final _result = create();
     if (blockId != null) {
       _result.blockId = blockId;
     }
-    if (rowMetas != null) {
-      _result.rowMetas.addAll(rowMetas);
+    if (rows != null) {
+      _result.rows.addAll(rows);
     }
     return _result;
   }
@@ -203,7 +203,7 @@ class GridBlockMetaData extends $pb.GeneratedMessage {
   void clearBlockId() => clearField(1);
 
   @$pb.TagNumber(2)
-  $core.List<RowMeta> get rowMetas => $_getList(1);
+  $core.List<RowMeta> get rows => $_getList(1);
 }
 
 class FieldMeta extends $pb.GeneratedMessage {
@@ -1014,7 +1014,7 @@ class CellMetaChangeset extends $pb.GeneratedMessage {
 class BuildGridContext extends $pb.GeneratedMessage {
   static final $pb.BuilderInfo _i = $pb.BuilderInfo(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'BuildGridContext', createEmptyInstance: create)
     ..pc<FieldMeta>(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'fieldMetas', $pb.PbFieldType.PM, subBuilder: FieldMeta.create)
-    ..aOM<GridBlockMeta>(2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'blockMetas', subBuilder: GridBlockMeta.create)
+    ..aOM<GridBlockMeta>(2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'blockMeta', subBuilder: GridBlockMeta.create)
     ..aOM<GridBlockMetaData>(3, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'blockMetaData', subBuilder: GridBlockMetaData.create)
     ..hasRequiredFields = false
   ;
@@ -1022,15 +1022,15 @@ class BuildGridContext extends $pb.GeneratedMessage {
   BuildGridContext._() : super();
   factory BuildGridContext({
     $core.Iterable<FieldMeta>? fieldMetas,
-    GridBlockMeta? blockMetas,
+    GridBlockMeta? blockMeta,
     GridBlockMetaData? blockMetaData,
   }) {
     final _result = create();
     if (fieldMetas != null) {
       _result.fieldMetas.addAll(fieldMetas);
     }
-    if (blockMetas != null) {
-      _result.blockMetas = blockMetas;
+    if (blockMeta != null) {
+      _result.blockMeta = blockMeta;
     }
     if (blockMetaData != null) {
       _result.blockMetaData = blockMetaData;
@@ -1062,15 +1062,15 @@ class BuildGridContext extends $pb.GeneratedMessage {
   $core.List<FieldMeta> get fieldMetas => $_getList(0);
 
   @$pb.TagNumber(2)
-  GridBlockMeta get blockMetas => $_getN(1);
+  GridBlockMeta get blockMeta => $_getN(1);
   @$pb.TagNumber(2)
-  set blockMetas(GridBlockMeta v) { setField(2, v); }
+  set blockMeta(GridBlockMeta v) { setField(2, v); }
   @$pb.TagNumber(2)
-  $core.bool hasBlockMetas() => $_has(1);
+  $core.bool hasBlockMeta() => $_has(1);
   @$pb.TagNumber(2)
-  void clearBlockMetas() => clearField(2);
+  void clearBlockMeta() => clearField(2);
   @$pb.TagNumber(2)
-  GridBlockMeta ensureBlockMetas() => $_ensure(1);
+  GridBlockMeta ensureBlockMeta() => $_ensure(1);
 
   @$pb.TagNumber(3)
   GridBlockMetaData get blockMetaData => $_getN(2);

+ 6 - 6
frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid-data-model/meta.pbjson.dart

@@ -29,12 +29,12 @@ const GridMeta$json = const {
   '2': const [
     const {'1': 'grid_id', '3': 1, '4': 1, '5': 9, '10': 'gridId'},
     const {'1': 'fields', '3': 2, '4': 3, '5': 11, '6': '.FieldMeta', '10': 'fields'},
-    const {'1': 'block_metas', '3': 3, '4': 3, '5': 11, '6': '.GridBlockMeta', '10': 'blockMetas'},
+    const {'1': 'blocks', '3': 3, '4': 3, '5': 11, '6': '.GridBlockMeta', '10': 'blocks'},
   ],
 };
 
 /// Descriptor for `GridMeta`. Decode as a `google.protobuf.DescriptorProto`.
-final $typed_data.Uint8List gridMetaDescriptor = $convert.base64Decode('CghHcmlkTWV0YRIXCgdncmlkX2lkGAEgASgJUgZncmlkSWQSIgoGZmllbGRzGAIgAygLMgouRmllbGRNZXRhUgZmaWVsZHMSLwoLYmxvY2tfbWV0YXMYAyADKAsyDi5HcmlkQmxvY2tNZXRhUgpibG9ja01ldGFz');
+final $typed_data.Uint8List gridMetaDescriptor = $convert.base64Decode('CghHcmlkTWV0YRIXCgdncmlkX2lkGAEgASgJUgZncmlkSWQSIgoGZmllbGRzGAIgAygLMgouRmllbGRNZXRhUgZmaWVsZHMSJgoGYmxvY2tzGAMgAygLMg4uR3JpZEJsb2NrTWV0YVIGYmxvY2tz');
 @$core.Deprecated('Use gridBlockMetaDescriptor instead')
 const GridBlockMeta$json = const {
   '1': 'GridBlockMeta',
@@ -52,12 +52,12 @@ const GridBlockMetaData$json = const {
   '1': 'GridBlockMetaData',
   '2': const [
     const {'1': 'block_id', '3': 1, '4': 1, '5': 9, '10': 'blockId'},
-    const {'1': 'row_metas', '3': 2, '4': 3, '5': 11, '6': '.RowMeta', '10': 'rowMetas'},
+    const {'1': 'rows', '3': 2, '4': 3, '5': 11, '6': '.RowMeta', '10': 'rows'},
   ],
 };
 
 /// Descriptor for `GridBlockMetaData`. Decode as a `google.protobuf.DescriptorProto`.
-final $typed_data.Uint8List gridBlockMetaDataDescriptor = $convert.base64Decode('ChFHcmlkQmxvY2tNZXRhRGF0YRIZCghibG9ja19pZBgBIAEoCVIHYmxvY2tJZBIlCglyb3dfbWV0YXMYAiADKAsyCC5Sb3dNZXRhUghyb3dNZXRhcw==');
+final $typed_data.Uint8List gridBlockMetaDataDescriptor = $convert.base64Decode('ChFHcmlkQmxvY2tNZXRhRGF0YRIZCghibG9ja19pZBgBIAEoCVIHYmxvY2tJZBIcCgRyb3dzGAIgAygLMgguUm93TWV0YVIEcm93cw==');
 @$core.Deprecated('Use fieldMetaDescriptor instead')
 const FieldMeta$json = const {
   '1': 'FieldMeta',
@@ -208,10 +208,10 @@ const BuildGridContext$json = const {
   '1': 'BuildGridContext',
   '2': const [
     const {'1': 'field_metas', '3': 1, '4': 3, '5': 11, '6': '.FieldMeta', '10': 'fieldMetas'},
-    const {'1': 'block_metas', '3': 2, '4': 1, '5': 11, '6': '.GridBlockMeta', '10': 'blockMetas'},
+    const {'1': 'block_meta', '3': 2, '4': 1, '5': 11, '6': '.GridBlockMeta', '10': 'blockMeta'},
     const {'1': 'block_meta_data', '3': 3, '4': 1, '5': 11, '6': '.GridBlockMetaData', '10': 'blockMetaData'},
   ],
 };
 
 /// Descriptor for `BuildGridContext`. Decode as a `google.protobuf.DescriptorProto`.
-final $typed_data.Uint8List buildGridContextDescriptor = $convert.base64Decode('ChBCdWlsZEdyaWRDb250ZXh0EisKC2ZpZWxkX21ldGFzGAEgAygLMgouRmllbGRNZXRhUgpmaWVsZE1ldGFzEi8KC2Jsb2NrX21ldGFzGAIgASgLMg4uR3JpZEJsb2NrTWV0YVIKYmxvY2tNZXRhcxI6Cg9ibG9ja19tZXRhX2RhdGEYAyABKAsyEi5HcmlkQmxvY2tNZXRhRGF0YVINYmxvY2tNZXRhRGF0YQ==');
+final $typed_data.Uint8List buildGridContextDescriptor = $convert.base64Decode('ChBCdWlsZEdyaWRDb250ZXh0EisKC2ZpZWxkX21ldGFzGAEgAygLMgouRmllbGRNZXRhUgpmaWVsZE1ldGFzEi0KCmJsb2NrX21ldGEYAiABKAsyDi5HcmlkQmxvY2tNZXRhUglibG9ja01ldGESOgoPYmxvY2tfbWV0YV9kYXRhGAMgASgLMhIuR3JpZEJsb2NrTWV0YURhdGFSDWJsb2NrTWV0YURhdGE=');

+ 2 - 2
frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid/dart_notification.pbenum.dart

@@ -12,7 +12,7 @@ import 'package:protobuf/protobuf.dart' as $pb;
 class GridNotification extends $pb.ProtobufEnum {
   static const GridNotification Unknown = GridNotification._(0, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Unknown');
   static const GridNotification DidCreateBlock = GridNotification._(11, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'DidCreateBlock');
-  static const GridNotification DidUpdateBlock = GridNotification._(20, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'DidUpdateBlock');
+  static const GridNotification DidUpdateGridBlock = GridNotification._(20, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'DidUpdateGridBlock');
   static const GridNotification DidUpdateRow = GridNotification._(30, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'DidUpdateRow');
   static const GridNotification DidUpdateCell = GridNotification._(31, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'DidUpdateCell');
   static const GridNotification DidUpdateGrid = GridNotification._(40, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'DidUpdateGrid');
@@ -21,7 +21,7 @@ class GridNotification extends $pb.ProtobufEnum {
   static const $core.List<GridNotification> values = <GridNotification> [
     Unknown,
     DidCreateBlock,
-    DidUpdateBlock,
+    DidUpdateGridBlock,
     DidUpdateRow,
     DidUpdateCell,
     DidUpdateGrid,

+ 2 - 2
frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid/dart_notification.pbjson.dart

@@ -14,7 +14,7 @@ const GridNotification$json = const {
   '2': const [
     const {'1': 'Unknown', '2': 0},
     const {'1': 'DidCreateBlock', '2': 11},
-    const {'1': 'DidUpdateBlock', '2': 20},
+    const {'1': 'DidUpdateGridBlock', '2': 20},
     const {'1': 'DidUpdateRow', '2': 30},
     const {'1': 'DidUpdateCell', '2': 31},
     const {'1': 'DidUpdateGrid', '2': 40},
@@ -23,4 +23,4 @@ const GridNotification$json = const {
 };
 
 /// Descriptor for `GridNotification`. Decode as a `google.protobuf.EnumDescriptorProto`.
-final $typed_data.Uint8List gridNotificationDescriptor = $convert.base64Decode('ChBHcmlkTm90aWZpY2F0aW9uEgsKB1Vua25vd24QABISCg5EaWRDcmVhdGVCbG9jaxALEhIKDkRpZFVwZGF0ZUJsb2NrEBQSEAoMRGlkVXBkYXRlUm93EB4SEQoNRGlkVXBkYXRlQ2VsbBAfEhEKDURpZFVwZGF0ZUdyaWQQKBISCg5EaWRVcGRhdGVGaWVsZBAp');
+final $typed_data.Uint8List gridNotificationDescriptor = $convert.base64Decode('ChBHcmlkTm90aWZpY2F0aW9uEgsKB1Vua25vd24QABISCg5EaWRDcmVhdGVCbG9jaxALEhYKEkRpZFVwZGF0ZUdyaWRCbG9jaxAUEhAKDERpZFVwZGF0ZVJvdxAeEhEKDURpZFVwZGF0ZUNlbGwQHxIRCg1EaWRVcGRhdGVHcmlkECgSEgoORGlkVXBkYXRlRmllbGQQKQ==');

+ 4 - 0
frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid/event_map.pbenum.dart

@@ -24,6 +24,8 @@ class GridEvent extends $pb.ProtobufEnum {
   static const GridEvent ApplySelectOptionChangeset = GridEvent._(32, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'ApplySelectOptionChangeset');
   static const GridEvent CreateRow = GridEvent._(50, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'CreateRow');
   static const GridEvent GetRow = GridEvent._(51, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'GetRow');
+  static const GridEvent DeleteRow = GridEvent._(52, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'DeleteRow');
+  static const GridEvent DuplicateRow = GridEvent._(53, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'DuplicateRow');
   static const GridEvent GetCell = GridEvent._(70, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'GetCell');
   static const GridEvent UpdateCell = GridEvent._(71, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'UpdateCell');
   static const GridEvent ApplySelectOptionCellChangeset = GridEvent._(72, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'ApplySelectOptionCellChangeset');
@@ -43,6 +45,8 @@ class GridEvent extends $pb.ProtobufEnum {
     ApplySelectOptionChangeset,
     CreateRow,
     GetRow,
+    DeleteRow,
+    DuplicateRow,
     GetCell,
     UpdateCell,
     ApplySelectOptionCellChangeset,

+ 3 - 1
frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid/event_map.pbjson.dart

@@ -26,6 +26,8 @@ const GridEvent$json = const {
     const {'1': 'ApplySelectOptionChangeset', '2': 32},
     const {'1': 'CreateRow', '2': 50},
     const {'1': 'GetRow', '2': 51},
+    const {'1': 'DeleteRow', '2': 52},
+    const {'1': 'DuplicateRow', '2': 53},
     const {'1': 'GetCell', '2': 70},
     const {'1': 'UpdateCell', '2': 71},
     const {'1': 'ApplySelectOptionCellChangeset', '2': 72},
@@ -33,4 +35,4 @@ const GridEvent$json = const {
 };
 
 /// Descriptor for `GridEvent`. Decode as a `google.protobuf.EnumDescriptorProto`.
-final $typed_data.Uint8List gridEventDescriptor = $convert.base64Decode('CglHcmlkRXZlbnQSDwoLR2V0R3JpZERhdGEQABIRCg1HZXRHcmlkQmxvY2tzEAESDQoJR2V0RmllbGRzEAoSDwoLVXBkYXRlRmllbGQQCxIPCgtDcmVhdGVGaWVsZBAMEg8KC0RlbGV0ZUZpZWxkEA0SEQoNU3dpdGNoVG9GaWVsZBAOEhIKDkR1cGxpY2F0ZUZpZWxkEA8SFwoTR2V0RWRpdEZpZWxkQ29udGV4dBAQEhMKD05ld1NlbGVjdE9wdGlvbhAeEhoKFkdldFNlbGVjdE9wdGlvbkNvbnRleHQQHxIeChpBcHBseVNlbGVjdE9wdGlvbkNoYW5nZXNldBAgEg0KCUNyZWF0ZVJvdxAyEgoKBkdldFJvdxAzEgsKB0dldENlbGwQRhIOCgpVcGRhdGVDZWxsEEcSIgoeQXBwbHlTZWxlY3RPcHRpb25DZWxsQ2hhbmdlc2V0EEg=');
+final $typed_data.Uint8List gridEventDescriptor = $convert.base64Decode('CglHcmlkRXZlbnQSDwoLR2V0R3JpZERhdGEQABIRCg1HZXRHcmlkQmxvY2tzEAESDQoJR2V0RmllbGRzEAoSDwoLVXBkYXRlRmllbGQQCxIPCgtDcmVhdGVGaWVsZBAMEg8KC0RlbGV0ZUZpZWxkEA0SEQoNU3dpdGNoVG9GaWVsZBAOEhIKDkR1cGxpY2F0ZUZpZWxkEA8SFwoTR2V0RWRpdEZpZWxkQ29udGV4dBAQEhMKD05ld1NlbGVjdE9wdGlvbhAeEhoKFkdldFNlbGVjdE9wdGlvbkNvbnRleHQQHxIeChpBcHBseVNlbGVjdE9wdGlvbkNoYW5nZXNldBAgEg0KCUNyZWF0ZVJvdxAyEgoKBkdldFJvdxAzEg0KCURlbGV0ZVJvdxA0EhAKDER1cGxpY2F0ZVJvdxA1EgsKB0dldENlbGwQRhIOCgpVcGRhdGVDZWxsEEcSIgoeQXBwbHlTZWxlY3RPcHRpb25DZWxsQ2hhbmdlc2V0EEg=');

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

@@ -6,7 +6,7 @@ const OBSERVABLE_CATEGORY: &str = "Grid";
 pub enum GridNotification {
     Unknown = 0,
     DidCreateBlock = 11,
-    DidUpdateBlock = 20,
+    DidUpdateGridBlock = 20,
     DidUpdateRow = 30,
     DidUpdateCell = 31,
     DidUpdateGrid = 40,

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

@@ -179,6 +179,28 @@ pub(crate) async fn get_row_handler(
     }
 }
 
+#[tracing::instrument(level = "debug", skip(data, manager), err)]
+pub(crate) async fn delete_row_handler(
+    data: Data<RowIdentifierPayload>,
+    manager: AppData<Arc<GridManager>>,
+) -> Result<(), FlowyError> {
+    let params: RowIdentifier = data.into_inner().try_into()?;
+    let editor = manager.get_grid_editor(&params.grid_id)?;
+    let _ = editor.delete_row(&params.row_id).await?;
+    Ok(())
+}
+
+#[tracing::instrument(level = "debug", skip(data, manager), err)]
+pub(crate) async fn duplicate_row_handler(
+    data: Data<RowIdentifierPayload>,
+    manager: AppData<Arc<GridManager>>,
+) -> Result<(), FlowyError> {
+    let params: RowIdentifier = data.into_inner().try_into()?;
+    let editor = manager.get_grid_editor(&params.grid_id)?;
+    let _ = editor.duplicate_row(&params.row_id).await?;
+    Ok(())
+}
+
 #[tracing::instrument(level = "debug", skip(data, manager), err)]
 pub(crate) async fn create_row_handler(
     data: Data<CreateRowPayload>,

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

@@ -20,6 +20,8 @@ pub fn create(grid_manager: Arc<GridManager>) -> Module {
         // Row
         .event(GridEvent::CreateRow, create_row_handler)
         .event(GridEvent::GetRow, get_row_handler)
+        .event(GridEvent::DeleteRow, delete_row_handler)
+        .event(GridEvent::DuplicateRow, duplicate_row_handler)
         // Cell
         .event(GridEvent::GetCell, get_cell_handler)
         .event(GridEvent::UpdateCell, update_cell_handler)
@@ -81,6 +83,12 @@ pub enum GridEvent {
     #[event(input = "RowIdentifierPayload", output = "Row")]
     GetRow = 51,
 
+    #[event(input = "RowIdentifierPayload")]
+    DeleteRow = 52,
+
+    #[event(input = "RowIdentifierPayload")]
+    DuplicateRow = 53,
+
     #[event(input = "CellIdentifierPayload", output = "Cell")]
     GetCell = 70,
 

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

@@ -110,6 +110,7 @@ impl GridManager {
         }
     }
 
+    #[tracing::instrument(level = "trace", skip(self, pool), err)]
     async fn make_grid_editor(
         &self,
         grid_id: &str,
@@ -175,11 +176,11 @@ pub async fn make_grid_view_data(
     grid_manager: Arc<GridManager>,
     build_context: BuildGridContext,
 ) -> FlowyResult<Bytes> {
-    let block_id = build_context.block_metas.block_id.clone();
+    let block_id = build_context.block_meta.block_id.clone();
     let grid_meta = GridMeta {
         grid_id: view_id.to_string(),
         fields: build_context.field_metas,
-        block_metas: vec![build_context.block_metas],
+        blocks: vec![build_context.block_meta],
     };
 
     // Create grid
@@ -190,7 +191,7 @@ pub async fn make_grid_view_data(
     let _ = grid_manager.create_grid(view_id, repeated_revision).await?;
 
     // Indexing the block's rows
-    build_context.block_meta_data.row_metas.iter().for_each(|row| {
+    build_context.block_meta_data.rows.iter().for_each(|row| {
         let _ = grid_manager
             .block_index_persistence
             .insert_or_update(&row.block_id, &row.id);

+ 8 - 8
frontend/rust-lib/flowy-grid/src/protobuf/model/dart_notification.rs

@@ -27,7 +27,7 @@
 pub enum GridNotification {
     Unknown = 0,
     DidCreateBlock = 11,
-    DidUpdateBlock = 20,
+    DidUpdateGridBlock = 20,
     DidUpdateRow = 30,
     DidUpdateCell = 31,
     DidUpdateGrid = 40,
@@ -43,7 +43,7 @@ impl ::protobuf::ProtobufEnum for GridNotification {
         match value {
             0 => ::std::option::Option::Some(GridNotification::Unknown),
             11 => ::std::option::Option::Some(GridNotification::DidCreateBlock),
-            20 => ::std::option::Option::Some(GridNotification::DidUpdateBlock),
+            20 => ::std::option::Option::Some(GridNotification::DidUpdateGridBlock),
             30 => ::std::option::Option::Some(GridNotification::DidUpdateRow),
             31 => ::std::option::Option::Some(GridNotification::DidUpdateCell),
             40 => ::std::option::Option::Some(GridNotification::DidUpdateGrid),
@@ -56,7 +56,7 @@ impl ::protobuf::ProtobufEnum for GridNotification {
         static values: &'static [GridNotification] = &[
             GridNotification::Unknown,
             GridNotification::DidCreateBlock,
-            GridNotification::DidUpdateBlock,
+            GridNotification::DidUpdateGridBlock,
             GridNotification::DidUpdateRow,
             GridNotification::DidUpdateCell,
             GridNotification::DidUpdateGrid,
@@ -89,11 +89,11 @@ impl ::protobuf::reflect::ProtobufValue for GridNotification {
 }
 
 static file_descriptor_proto_data: &'static [u8] = b"\
-    \n\x17dart_notification.proto*\x93\x01\n\x10GridNotification\x12\x0b\n\
-    \x07Unknown\x10\0\x12\x12\n\x0eDidCreateBlock\x10\x0b\x12\x12\n\x0eDidUp\
-    dateBlock\x10\x14\x12\x10\n\x0cDidUpdateRow\x10\x1e\x12\x11\n\rDidUpdate\
-    Cell\x10\x1f\x12\x11\n\rDidUpdateGrid\x10(\x12\x12\n\x0eDidUpdateField\
-    \x10)b\x06proto3\
+    \n\x17dart_notification.proto*\x97\x01\n\x10GridNotification\x12\x0b\n\
+    \x07Unknown\x10\0\x12\x12\n\x0eDidCreateBlock\x10\x0b\x12\x16\n\x12DidUp\
+    dateGridBlock\x10\x14\x12\x10\n\x0cDidUpdateRow\x10\x1e\x12\x11\n\rDidUp\
+    dateCell\x10\x1f\x12\x11\n\rDidUpdateGrid\x10(\x12\x12\n\x0eDidUpdateFie\
+    ld\x10)b\x06proto3\
 ";
 
 static file_descriptor_proto_lazy: ::protobuf::rt::LazyV2<::protobuf::descriptor::FileDescriptorProto> = ::protobuf::rt::LazyV2::INIT;

+ 10 - 3
frontend/rust-lib/flowy-grid/src/protobuf/model/event_map.rs

@@ -39,6 +39,8 @@ pub enum GridEvent {
     ApplySelectOptionChangeset = 32,
     CreateRow = 50,
     GetRow = 51,
+    DeleteRow = 52,
+    DuplicateRow = 53,
     GetCell = 70,
     UpdateCell = 71,
     ApplySelectOptionCellChangeset = 72,
@@ -65,6 +67,8 @@ impl ::protobuf::ProtobufEnum for GridEvent {
             32 => ::std::option::Option::Some(GridEvent::ApplySelectOptionChangeset),
             50 => ::std::option::Option::Some(GridEvent::CreateRow),
             51 => ::std::option::Option::Some(GridEvent::GetRow),
+            52 => ::std::option::Option::Some(GridEvent::DeleteRow),
+            53 => ::std::option::Option::Some(GridEvent::DuplicateRow),
             70 => ::std::option::Option::Some(GridEvent::GetCell),
             71 => ::std::option::Option::Some(GridEvent::UpdateCell),
             72 => ::std::option::Option::Some(GridEvent::ApplySelectOptionCellChangeset),
@@ -88,6 +92,8 @@ impl ::protobuf::ProtobufEnum for GridEvent {
             GridEvent::ApplySelectOptionChangeset,
             GridEvent::CreateRow,
             GridEvent::GetRow,
+            GridEvent::DeleteRow,
+            GridEvent::DuplicateRow,
             GridEvent::GetCell,
             GridEvent::UpdateCell,
             GridEvent::ApplySelectOptionCellChangeset,
@@ -119,15 +125,16 @@ impl ::protobuf::reflect::ProtobufValue for GridEvent {
 }
 
 static file_descriptor_proto_data: &'static [u8] = b"\
-    \n\x0fevent_map.proto*\xde\x02\n\tGridEvent\x12\x0f\n\x0bGetGridData\x10\
+    \n\x0fevent_map.proto*\xff\x02\n\tGridEvent\x12\x0f\n\x0bGetGridData\x10\
     \0\x12\x11\n\rGetGridBlocks\x10\x01\x12\r\n\tGetFields\x10\n\x12\x0f\n\
     \x0bUpdateField\x10\x0b\x12\x0f\n\x0bCreateField\x10\x0c\x12\x0f\n\x0bDe\
     leteField\x10\r\x12\x11\n\rSwitchToField\x10\x0e\x12\x12\n\x0eDuplicateF\
     ield\x10\x0f\x12\x17\n\x13GetEditFieldContext\x10\x10\x12\x13\n\x0fNewSe\
     lectOption\x10\x1e\x12\x1a\n\x16GetSelectOptionContext\x10\x1f\x12\x1e\n\
     \x1aApplySelectOptionChangeset\x10\x20\x12\r\n\tCreateRow\x102\x12\n\n\
-    \x06GetRow\x103\x12\x0b\n\x07GetCell\x10F\x12\x0e\n\nUpdateCell\x10G\x12\
-    \"\n\x1eApplySelectOptionCellChangeset\x10Hb\x06proto3\
+    \x06GetRow\x103\x12\r\n\tDeleteRow\x104\x12\x10\n\x0cDuplicateRow\x105\
+    \x12\x0b\n\x07GetCell\x10F\x12\x0e\n\nUpdateCell\x10G\x12\"\n\x1eApplySe\
+    lectOptionCellChangeset\x10Hb\x06proto3\
 ";
 
 static file_descriptor_proto_lazy: ::protobuf::rt::LazyV2<::protobuf::descriptor::FileDescriptorProto> = ::protobuf::rt::LazyV2::INIT;

+ 1 - 1
frontend/rust-lib/flowy-grid/src/protobuf/proto/dart_notification.proto

@@ -3,7 +3,7 @@ syntax = "proto3";
 enum GridNotification {
     Unknown = 0;
     DidCreateBlock = 11;
-    DidUpdateBlock = 20;
+    DidUpdateGridBlock = 20;
     DidUpdateRow = 30;
     DidUpdateCell = 31;
     DidUpdateGrid = 40;

+ 2 - 0
frontend/rust-lib/flowy-grid/src/protobuf/proto/event_map.proto

@@ -15,6 +15,8 @@ enum GridEvent {
     ApplySelectOptionChangeset = 32;
     CreateRow = 50;
     GetRow = 51;
+    DeleteRow = 52;
+    DuplicateRow = 53;
     GetCell = 70;
     UpdateCell = 71;
     ApplySelectOptionCellChangeset = 72;

+ 32 - 8
frontend/rust-lib/flowy-grid/src/services/block_meta_editor.rs

@@ -7,6 +7,7 @@ use flowy_sync::entities::revision::Revision;
 use flowy_sync::util::make_delta_from_revisions;
 use lib_infra::future::FutureResult;
 use lib_ot::core::PlainTextAttributes;
+use std::borrow::Cow;
 
 use std::sync::Arc;
 use tokio::sync::RwLock;
@@ -41,24 +42,37 @@ impl ClientGridBlockMetaEditor {
         })
     }
 
-    pub(crate) async fn create_row(&self, row: RowMeta, start_row_id: Option<String>) -> FlowyResult<i32> {
+    /// return current number of rows and the inserted index. The inserted index will be None if the start_row_id is None
+    pub(crate) async fn create_row(
+        &self,
+        row: RowMeta,
+        start_row_id: Option<String>,
+    ) -> FlowyResult<(i32, Option<i32>)> {
         let mut row_count = 0;
+        let mut row_index = None;
         let _ = self
             .modify(|pad| {
+                if let Some(start_row_id) = start_row_id.as_ref() {
+                    match pad.index_of_row(start_row_id) {
+                        None => {}
+                        Some(index) => row_index = Some(index + 1),
+                    }
+                }
+
                 let change = pad.add_row_meta(row, start_row_id)?;
                 row_count = pad.number_of_rows();
                 Ok(change)
             })
             .await?;
 
-        Ok(row_count)
+        Ok((row_count, row_index))
     }
 
-    pub async fn delete_rows(&self, ids: Vec<String>) -> FlowyResult<i32> {
+    pub async fn delete_rows(&self, ids: Vec<Cow<'_, String>>) -> FlowyResult<i32> {
         let mut row_count = 0;
         let _ = self
             .modify(|pad| {
-                let changeset = pad.delete_rows(&ids)?;
+                let changeset = pad.delete_rows(ids)?;
                 row_count = pad.number_of_rows();
                 Ok(changeset)
             })
@@ -71,17 +85,27 @@ impl ClientGridBlockMetaEditor {
         Ok(())
     }
 
-    pub async fn get_row_metas(&self, row_ids: Option<Vec<String>>) -> FlowyResult<Vec<Arc<RowMeta>>> {
-        let row_metas = self.pad.read().await.get_row_metas(&row_ids)?;
+    pub async fn get_row_metas<T>(&self, row_ids: Option<Vec<Cow<'_, T>>>) -> FlowyResult<Vec<Arc<RowMeta>>>
+    where
+        T: AsRef<str> + ToOwned + ?Sized,
+    {
+        let row_metas = self.pad.read().await.get_row_metas(row_ids)?;
         Ok(row_metas)
     }
 
-    pub async fn get_cell_metas(&self, field_id: &str, row_ids: &Option<Vec<String>>) -> FlowyResult<Vec<CellMeta>> {
+    pub async fn get_cell_metas(
+        &self,
+        field_id: &str,
+        row_ids: Option<Vec<Cow<'_, String>>>,
+    ) -> FlowyResult<Vec<CellMeta>> {
         let cell_metas = self.pad.read().await.get_cell_metas(field_id, row_ids)?;
         Ok(cell_metas)
     }
 
-    pub async fn get_row_orders(&self, row_ids: &Option<Vec<String>>) -> FlowyResult<Vec<RowOrder>> {
+    pub async fn get_row_orders<T>(&self, row_ids: Option<Vec<Cow<'_, T>>>) -> FlowyResult<Vec<RowOrder>>
+    where
+        T: AsRef<str> + ToOwned + ?Sized,
+    {
         let row_orders = self
             .pad
             .read()

+ 73 - 51
frontend/rust-lib/flowy-grid/src/services/block_meta_manager.rs

@@ -2,13 +2,14 @@ use crate::dart_notification::{send_dart_notification, GridNotification};
 use crate::manager::GridUser;
 use crate::services::block_meta_editor::ClientGridBlockMetaEditor;
 use crate::services::persistence::block_index::BlockIndexPersistence;
-use crate::services::row::{make_block_row_ids, make_rows_from_row_metas, GridBlockSnapshot};
+use crate::services::row::{group_row_orders, make_rows_from_row_metas, GridBlockSnapshot};
+use std::borrow::Cow;
 
 use dashmap::DashMap;
 use flowy_error::FlowyResult;
 use flowy_grid_data_model::entities::{
     CellMeta, CellMetaChangeset, CellNotificationData, FieldMeta, GridBlockMeta, GridBlockMetaChangeset,
-    GridBlockOrder, RowMeta, RowMetaChangeset, RowOrder,
+    GridBlockOrderChangeset, IndexRowOrder, RowMeta, RowMetaChangeset, RowOrder,
 };
 use flowy_revision::disk::SQLiteGridBlockMetaRevisionPersistence;
 use flowy_revision::{RevisionManager, RevisionPersistence};
@@ -18,6 +19,7 @@ use std::sync::Arc;
 pub(crate) struct GridBlockMetaEditorManager {
     grid_id: String,
     user: Arc<dyn GridUser>,
+    // Key: block_id
     editor_map: DashMap<String, Arc<ClientGridBlockMetaEditor>>,
     persistence: Arc<BlockIndexPersistence>,
 }
@@ -68,8 +70,14 @@ impl GridBlockMetaEditorManager {
     ) -> FlowyResult<i32> {
         let _ = self.persistence.insert_or_update(&row_meta.block_id, &row_meta.id)?;
         let editor = self.get_editor(&row_meta.block_id).await?;
-        let row_count = editor.create_row(row_meta, start_row_id).await?;
-        self.notify_block_did_update_row(block_id).await?;
+
+        let mut index_row_order = IndexRowOrder::from(&row_meta);
+        let (row_count, row_index) = editor.create_row(row_meta, start_row_id).await?;
+        index_row_order.index = row_index;
+
+        let _ = self
+            .notify_did_update_grid_rows(GridBlockOrderChangeset::from_insert(block_id, vec![index_row_order]))
+            .await?;
         Ok(row_count)
     }
 
@@ -79,39 +87,73 @@ impl GridBlockMetaEditorManager {
     ) -> FlowyResult<Vec<GridBlockMetaChangeset>> {
         let mut changesets = vec![];
         for (block_id, row_metas) in rows_by_block_id {
+            let mut inserted_row_orders = vec![];
             let editor = self.get_editor(&block_id).await?;
             let mut row_count = 0;
-            for row in &row_metas {
+            for row in row_metas {
                 let _ = self.persistence.insert_or_update(&row.block_id, &row.id)?;
-                row_count = editor.create_row(row.clone(), None).await?;
+                inserted_row_orders.push(IndexRowOrder::from(&row));
+                row_count = editor.create_row(row, None).await?.0;
             }
             changesets.push(GridBlockMetaChangeset::from_row_count(&block_id, row_count));
-            let _ = self.notify_block_did_update_row(&block_id).await?;
+
+            let _ = self
+                .notify_did_update_grid_rows(GridBlockOrderChangeset::from_insert(&block_id, inserted_row_orders))
+                .await?;
         }
 
         Ok(changesets)
     }
 
-    pub(crate) async fn delete_rows(&self, row_orders: Vec<RowOrder>) -> FlowyResult<Vec<GridBlockMetaChangeset>> {
-        let mut changesets = vec![];
-        for block_row_ids in make_block_row_ids(&row_orders) {
-            let editor = self.get_editor(&block_row_ids.block_id).await?;
-            let row_count = editor.delete_rows(block_row_ids.row_ids).await?;
+    pub async fn update_row(&self, changeset: RowMetaChangeset) -> FlowyResult<()> {
+        let editor = self.get_editor_from_row_id(&changeset.row_id).await?;
+        let _ = editor.update_row(changeset.clone()).await?;
 
-            let changeset = GridBlockMetaChangeset::from_row_count(&block_row_ids.block_id, row_count);
-            changesets.push(changeset);
+        match editor
+            .get_row_orders(Some(vec![Cow::Borrowed(&changeset.row_id)]))
+            .await?
+            .pop()
+        {
+            None => {}
+            Some(row_order) => {
+                let block_order_changeset = GridBlockOrderChangeset::from_update(&editor.block_id, vec![row_order]);
+                let _ = self.notify_did_update_grid_rows(block_order_changeset).await?;
+            }
         }
 
-        Ok(changesets)
+        Ok(())
     }
 
-    pub async fn update_row(&self, changeset: RowMetaChangeset) -> FlowyResult<()> {
-        let editor = self.get_editor_from_row_id(&changeset.row_id).await?;
-        let _ = editor.update_row(changeset.clone()).await?;
-        let _ = self.notify_block_did_update_row(&editor.block_id).await?;
+    pub async fn delete_row(&self, row_id: &str) -> FlowyResult<()> {
+        let row_id = row_id.to_owned();
+        let block_id = self.persistence.get_block_id(&row_id)?;
+        let editor = self.get_editor(&block_id).await?;
+        let row_orders = editor.get_row_orders(Some(vec![Cow::Borrowed(&row_id)])).await?;
+        let _ = editor.delete_rows(vec![Cow::Borrowed(&row_id)]).await?;
+        let _ = self
+            .notify_did_update_grid_rows(GridBlockOrderChangeset::from_delete(&block_id, row_orders))
+            .await?;
+
         Ok(())
     }
 
+    pub(crate) async fn delete_rows(&self, row_orders: Vec<RowOrder>) -> FlowyResult<Vec<GridBlockMetaChangeset>> {
+        let mut changesets = vec![];
+        for block_order in group_row_orders(row_orders) {
+            let editor = self.get_editor(&block_order.block_id).await?;
+            let row_ids = block_order
+                .row_orders
+                .into_iter()
+                .map(|row_order| Cow::Owned(row_order.row_id))
+                .collect::<Vec<Cow<String>>>();
+            let row_count = editor.delete_rows(row_ids).await?;
+            let changeset = GridBlockMetaChangeset::from_row_count(&block_order.block_id, row_count);
+            changesets.push(changeset);
+        }
+
+        Ok(changesets)
+    }
+
     pub async fn update_cell(&self, changeset: CellMetaChangeset) -> FlowyResult<()> {
         let row_id = changeset.row_id.clone();
         let editor = self.get_editor_from_row_id(&row_id).await?;
@@ -130,7 +172,7 @@ impl GridBlockMetaEditorManager {
 
     pub async fn get_row_meta(&self, row_id: &str) -> FlowyResult<Option<Arc<RowMeta>>> {
         let editor = self.get_editor_from_row_id(row_id).await?;
-        let row_ids = vec![row_id.to_owned()];
+        let row_ids = vec![Cow::Borrowed(row_id)];
         let mut row_metas = editor.get_row_metas(Some(row_ids)).await?;
         if row_metas.is_empty() {
             Ok(None)
@@ -139,11 +181,16 @@ impl GridBlockMetaEditorManager {
         }
     }
 
+    pub async fn get_row_orders(&self, block_id: &str) -> FlowyResult<Vec<RowOrder>> {
+        let editor = self.get_editor(block_id).await?;
+        editor.get_row_orders::<&str>(None).await
+    }
+
     pub(crate) async fn make_block_snapshots(&self, block_ids: Vec<String>) -> FlowyResult<Vec<GridBlockSnapshot>> {
         let mut snapshots = vec![];
         for block_id in block_ids {
             let editor = self.get_editor(&block_id).await?;
-            let row_metas = editor.get_row_metas(None).await?;
+            let row_metas = editor.get_row_metas::<&str>(None).await?;
             snapshots.push(GridBlockSnapshot { block_id, row_metas });
         }
         Ok(snapshots)
@@ -155,21 +202,20 @@ impl GridBlockMetaEditorManager {
         &self,
         block_ids: Vec<String>,
         field_id: &str,
-        row_ids: Option<Vec<String>>,
+        row_ids: Option<Vec<Cow<'_, String>>>,
     ) -> FlowyResult<Vec<CellMeta>> {
         let mut block_cell_metas = vec![];
         for block_id in block_ids {
             let editor = self.get_editor(&block_id).await?;
-            let cell_metas = editor.get_cell_metas(field_id, &row_ids).await?;
+            let cell_metas = editor.get_cell_metas(field_id, row_ids.clone()).await?;
             block_cell_metas.extend(cell_metas);
         }
         Ok(block_cell_metas)
     }
 
-    async fn notify_block_did_update_row(&self, block_id: &str) -> FlowyResult<()> {
-        let block_order: GridBlockOrder = block_id.into();
-        send_dart_notification(&self.grid_id, GridNotification::DidUpdateBlock)
-            .payload(block_order)
+    async fn notify_did_update_grid_rows(&self, changeset: GridBlockOrderChangeset) -> FlowyResult<()> {
+        send_dart_notification(&self.grid_id, GridNotification::DidUpdateGridBlock)
+            .payload(changeset)
             .send();
         Ok(())
     }
@@ -196,30 +242,6 @@ impl GridBlockMetaEditorManager {
             }
         }
         Ok(())
-
-        //
-        // let field_meta_map = field_metas
-        //     .iter()
-        //     .map(|field_meta| (&field_meta.id, field_meta))
-        //     .collect::<HashMap<&String, &FieldMeta>>();
-        //
-        // let mut cells = vec![];
-        // changeset
-        //     .cell_by_field_id
-        //     .into_iter()
-        //     .for_each(
-        //         |(field_id, cell_meta)| match make_cell_by_field_id(&field_meta_map, field_id, cell_meta) {
-        //             None => {}
-        //             Some((_, cell)) => cells.push(cell),
-        //         },
-        //     );
-        //
-        // if !cells.is_empty() {
-        //     send_dart_notification(&changeset.row_id, GridNotification::DidUpdateRow)
-        //         .payload(RepeatedCell::from(cells))
-        //         .send();
-        // }
-        // Ok(())
     }
 }
 

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

@@ -70,7 +70,7 @@ impl CellDataOperation for DateTypeOption {
     ) -> Result<String, FlowyError> {
         let changeset = changeset.into();
         if changeset.parse::<f64>().is_err() || changeset.parse::<i64>().is_err() {
-            return Err(FlowyError::internal().context(format!("Parse {} failed", changeset.to_string())));
+            return Err(FlowyError::internal().context(format!("Parse {} failed", changeset)));
         };
 
         Ok(TypeOptionCellData::new(changeset, self.field_type()).json())

+ 33 - 36
frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option.rs

@@ -16,9 +16,28 @@ use std::str::FromStr;
 pub const SELECTION_IDS_SEPARATOR: &str = ",";
 
 pub trait SelectOptionOperation: TypeOptionDataEntry + Send + Sync {
-    fn insert_option(&mut self, new_option: SelectOption);
-    fn delete_option(&mut self, delete_option: SelectOption);
+    fn insert_option(&mut self, new_option: SelectOption) {
+        let options = self.mut_options();
+        if let Some(index) = options
+            .iter()
+            .position(|option| option.id == new_option.id || option.name == new_option.name)
+        {
+            options.remove(index);
+            options.insert(index, new_option);
+        } else {
+            options.insert(0, new_option);
+        }
+    }
+
+    fn delete_option(&mut self, delete_option: SelectOption) {
+        let options = self.mut_options();
+        if let Some(index) = options.iter().position(|option| option.id == delete_option.id) {
+            options.remove(index);
+        }
+    }
+
     fn option_context(&self, cell_meta: &Option<CellMeta>) -> SelectOptionContext;
+    fn mut_options(&mut self) -> &mut Vec<SelectOption>;
 }
 
 // Single select
@@ -33,21 +52,6 @@ pub struct SingleSelectTypeOption {
 impl_type_option!(SingleSelectTypeOption, FieldType::SingleSelect);
 
 impl SelectOptionOperation for SingleSelectTypeOption {
-    fn insert_option(&mut self, new_option: SelectOption) {
-        if let Some(index) = self.options.iter().position(|option| option.id == new_option.id) {
-            self.options.remove(index);
-            self.options.insert(index, new_option);
-        } else {
-            self.options.insert(0, new_option);
-        }
-    }
-
-    fn delete_option(&mut self, delete_option: SelectOption) {
-        if let Some(index) = self.options.iter().position(|option| option.id == delete_option.id) {
-            self.options.remove(index);
-        }
-    }
-
     fn option_context(&self, cell_meta: &Option<CellMeta>) -> SelectOptionContext {
         let select_options = make_select_context_from(cell_meta, &self.options);
         SelectOptionContext {
@@ -55,6 +59,10 @@ impl SelectOptionOperation for SingleSelectTypeOption {
             select_options,
         }
     }
+
+    fn mut_options(&mut self) -> &mut Vec<SelectOption> {
+        &mut self.options
+    }
 }
 
 impl CellDataOperation for SingleSelectTypeOption {
@@ -139,21 +147,6 @@ impl MultiSelectTypeOption {
 }
 
 impl SelectOptionOperation for MultiSelectTypeOption {
-    fn insert_option(&mut self, new_option: SelectOption) {
-        if let Some(index) = self.options.iter().position(|option| option.id == new_option.id) {
-            self.options.remove(index);
-            self.options.insert(index, new_option);
-        } else {
-            self.options.insert(0, new_option);
-        }
-    }
-
-    fn delete_option(&mut self, delete_option: SelectOption) {
-        if let Some(index) = self.options.iter().position(|option| option.id == delete_option.id) {
-            self.options.remove(index);
-        }
-    }
-
     fn option_context(&self, cell_meta: &Option<CellMeta>) -> SelectOptionContext {
         let select_options = make_select_context_from(cell_meta, &self.options);
         SelectOptionContext {
@@ -161,6 +154,10 @@ impl SelectOptionOperation for MultiSelectTypeOption {
             select_options,
         }
     }
+
+    fn mut_options(&mut self) -> &mut Vec<SelectOption> {
+        &mut self.options
+    }
 }
 
 impl CellDataOperation for MultiSelectTypeOption {
@@ -472,7 +469,7 @@ mod tests {
         let single_select = SingleSelectTypeOptionBuilder::default()
             .option(google_option.clone())
             .option(facebook_option.clone())
-            .option(twitter_option.clone());
+            .option(twitter_option);
 
         let field_meta = FieldBuilder::new(single_select)
             .name("Platform")
@@ -481,7 +478,7 @@ mod tests {
 
         let type_option = SingleSelectTypeOption::from(&field_meta);
 
-        let option_ids = vec![google_option.id.clone(), facebook_option.id.clone()].join(SELECTION_IDS_SEPARATOR);
+        let option_ids = vec![google_option.id.clone(), facebook_option.id].join(SELECTION_IDS_SEPARATOR);
         let data = SelectOptionCellChangeset::from_insert(&option_ids).cell_data();
         let cell_data = type_option.apply_changeset(data, None).unwrap();
         assert_eq!(type_option.decode_cell_data(cell_data, &field_meta), google_option.name,);
@@ -514,7 +511,7 @@ mod tests {
         let multi_select = MultiSelectTypeOptionBuilder::default()
             .option(google_option.clone())
             .option(facebook_option.clone())
-            .option(twitter_option.clone());
+            .option(twitter_option);
 
         let field_meta = FieldBuilder::new(multi_select)
             .name("Platform")
@@ -528,7 +525,7 @@ mod tests {
         let cell_data = type_option.apply_changeset(data, None).unwrap();
         assert_eq!(
             type_option.decode_cell_data(cell_data, &field_meta),
-            vec![google_option.name.clone(), facebook_option.name.clone()].join(SELECTION_IDS_SEPARATOR),
+            vec![google_option.name.clone(), facebook_option.name].join(SELECTION_IDS_SEPARATOR),
         );
 
         let data = SelectOptionCellChangeset::from_insert(&google_option.id).cell_data();

+ 26 - 12
frontend/rust-lib/flowy-grid/src/services/grid_editor.rs

@@ -267,6 +267,14 @@ impl ClientGridEditor {
             }
         }
     }
+    pub async fn delete_row(&self, row_id: &str) -> FlowyResult<()> {
+        let _ = self.block_meta_manager.delete_row(row_id).await?;
+        Ok(())
+    }
+
+    pub async fn duplicate_row(&self, _row_id: &str) -> FlowyResult<()> {
+        Ok(())
+    }
 
     pub async fn get_cell(&self, params: &CellIdentifier) -> Option<Cell> {
         let field_meta = self.get_field_meta(&params.field_id).await?;
@@ -316,6 +324,11 @@ impl ClientGridEditor {
         Ok(grid_blocks)
     }
 
+    // pub async fn get_field_metas<T>(&self, field_ids: Option<Vec<T>>) -> FlowyResult<Vec<FieldMeta>>
+    //     where
+    //         T: Into<FieldOrder>,
+    // {
+
     pub async fn delete_rows(&self, row_orders: Vec<RowOrder>) -> FlowyResult<()> {
         let changesets = self.block_meta_manager.delete_rows(row_orders).await?;
         for changeset in changesets {
@@ -325,17 +338,18 @@ impl ClientGridEditor {
     }
 
     pub async fn grid_data(&self) -> FlowyResult<Grid> {
-        let field_orders = self.pad.read().await.get_field_orders();
-        let block_orders = self
-            .pad
-            .read()
-            .await
-            .get_block_metas()
-            .into_iter()
-            .map(|grid_block_meta| GridBlockOrder {
-                block_id: grid_block_meta.block_id,
-            })
-            .collect::<Vec<_>>();
+        let pad_read_guard = self.pad.read().await;
+        let field_orders = pad_read_guard.get_field_orders();
+        let mut block_orders = vec![];
+        for block_order in pad_read_guard.get_block_metas() {
+            let row_orders = self.block_meta_manager.get_row_orders(&block_order.block_id).await?;
+            let block_order = GridBlockOrder {
+                block_id: block_order.block_id,
+                row_orders,
+            };
+            block_orders.push(block_order);
+        }
+
         Ok(Grid {
             id: self.grid_id.clone(),
             field_orders,
@@ -416,7 +430,7 @@ impl ClientGridEditor {
         debug_assert!(field_metas.len() == 1);
 
         if let Some(field_meta) = field_metas.pop() {
-            send_dart_notification(&field_id, GridNotification::DidUpdateField)
+            send_dart_notification(field_id, GridNotification::DidUpdateField)
                 .payload(field_meta)
                 .send();
         }

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

@@ -1,41 +1,28 @@
 use crate::services::row::decode_cell_data;
 use flowy_error::FlowyResult;
 use flowy_grid_data_model::entities::{
-    Cell, CellMeta, FieldMeta, GridBlock, RepeatedGridBlock, Row, RowMeta, RowOrder,
+    Cell, CellMeta, FieldMeta, GridBlock, GridBlockOrder, RepeatedGridBlock, Row, RowMeta, RowOrder,
 };
 use rayon::iter::{IntoParallelIterator, ParallelIterator};
+
 use std::collections::HashMap;
 
 use std::sync::Arc;
 
-pub(crate) struct BlockRowIds {
-    pub(crate) block_id: String,
-    pub(crate) row_ids: Vec<String>,
-}
-
-impl BlockRowIds {
-    pub fn new(block_id: &str) -> Self {
-        BlockRowIds {
-            block_id: block_id.to_owned(),
-            row_ids: vec![],
-        }
-    }
-}
-
 pub struct GridBlockSnapshot {
     pub(crate) block_id: String,
     pub row_metas: Vec<Arc<RowMeta>>,
 }
 
-pub(crate) fn make_block_row_ids(row_orders: &[RowOrder]) -> Vec<BlockRowIds> {
-    let mut map: HashMap<&String, BlockRowIds> = HashMap::new();
-    row_orders.iter().for_each(|row_order| {
-        let block_id = &row_order.block_id;
-        let row_id = row_order.row_id.clone();
+pub(crate) fn group_row_orders(row_orders: Vec<RowOrder>) -> Vec<GridBlockOrder> {
+    let mut map: HashMap<String, GridBlockOrder> = HashMap::new();
+    row_orders.into_iter().for_each(|row_order| {
+        // Memory Optimization: escape clone block_id
+        let block_id = row_order.block_id.clone();
         map.entry(block_id)
-            .or_insert_with(|| BlockRowIds::new(block_id))
-            .row_ids
-            .push(row_id);
+            .or_insert_with(|| GridBlockOrder::new(&row_order.block_id))
+            .row_orders
+            .push(row_order);
     });
     map.into_values().collect::<Vec<_>>()
 }

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

@@ -4,7 +4,7 @@ use chrono::NaiveDateTime;
 use flowy_grid::services::field::{
     MultiSelectTypeOption, SelectOption, SelectOptionCellChangeset, SingleSelectTypeOption, SELECTION_IDS_SEPARATOR,
 };
-use flowy_grid::services::row::{apply_cell_data_changeset, decode_cell_data, CellDataOperation, CreateRowMetaBuilder};
+use flowy_grid::services::row::{decode_cell_data, CreateRowMetaBuilder};
 use flowy_grid_data_model::entities::{
     CellMetaChangeset, FieldChangesetParams, FieldType, GridBlockMeta, GridBlockMetaChangeset, RowMetaChangeset,
     TypeOptionDataEntry,

+ 1 - 0
frontend/rust-lib/flowy-revision/src/cache/disk/folder_rev_impl.rs

@@ -0,0 +1 @@
+

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

@@ -0,0 +1 @@
+

+ 75 - 3
shared-lib/flowy-grid-data-model/src/entities/grid.rs

@@ -260,11 +260,83 @@ impl std::convert::From<Vec<GridBlock>> for RepeatedGridBlock {
 pub struct GridBlockOrder {
     #[pb(index = 1)]
     pub block_id: String,
+
+    #[pb(index = 2)]
+    pub row_orders: Vec<RowOrder>,
 }
 
-impl std::convert::From<&str> for GridBlockOrder {
-    fn from(s: &str) -> Self {
-        GridBlockOrder { block_id: s.to_owned() }
+impl GridBlockOrder {
+    pub fn new(block_id: &str) -> Self {
+        GridBlockOrder {
+            block_id: block_id.to_owned(),
+            row_orders: vec![],
+        }
+    }
+}
+
+#[derive(Debug, Clone, Default, ProtoBuf)]
+pub struct GridBlockOrderChangeset {
+    #[pb(index = 1)]
+    pub block_id: String,
+
+    #[pb(index = 2)]
+    pub inserted_rows: Vec<IndexRowOrder>,
+
+    #[pb(index = 3)]
+    pub deleted_rows: Vec<RowOrder>,
+
+    #[pb(index = 4)]
+    pub updated_rows: Vec<RowOrder>,
+}
+
+#[derive(Debug, Clone, Default, ProtoBuf)]
+pub struct IndexRowOrder {
+    #[pb(index = 1)]
+    pub row_order: RowOrder,
+
+    #[pb(index = 2, one_of)]
+    pub index: Option<i32>,
+}
+
+impl std::convert::From<RowOrder> for IndexRowOrder {
+    fn from(row_order: RowOrder) -> Self {
+        Self { row_order, index: None }
+    }
+}
+
+impl std::convert::From<&RowMeta> for IndexRowOrder {
+    fn from(row: &RowMeta) -> Self {
+        let row_order = RowOrder::from(row);
+        Self::from(row_order)
+    }
+}
+
+impl GridBlockOrderChangeset {
+    pub fn from_insert(block_id: &str, inserted_rows: Vec<IndexRowOrder>) -> Self {
+        Self {
+            block_id: block_id.to_owned(),
+            inserted_rows,
+            deleted_rows: vec![],
+            updated_rows: vec![],
+        }
+    }
+
+    pub fn from_delete(block_id: &str, deleted_rows: Vec<RowOrder>) -> Self {
+        Self {
+            block_id: block_id.to_owned(),
+            inserted_rows: vec![],
+            deleted_rows,
+            updated_rows: vec![],
+        }
+    }
+
+    pub fn from_update(block_id: &str, updated_rows: Vec<RowOrder>) -> Self {
+        Self {
+            block_id: block_id.to_owned(),
+            inserted_rows: vec![],
+            deleted_rows: vec![],
+            updated_rows,
+        }
     }
 }
 

+ 10 - 10
shared-lib/flowy-grid-data-model/src/entities/meta.rs

@@ -18,7 +18,7 @@ pub struct GridMeta {
     pub fields: Vec<FieldMeta>,
 
     #[pb(index = 3)]
-    pub block_metas: Vec<GridBlockMeta>,
+    pub blocks: Vec<GridBlockMeta>,
 }
 
 #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, ProtoBuf)]
@@ -74,7 +74,7 @@ pub struct GridBlockMetaData {
     pub block_id: String,
 
     #[pb(index = 2)]
-    pub row_metas: Vec<RowMeta>,
+    pub rows: Vec<RowMeta>,
 }
 
 #[derive(Debug, Clone, Default, Serialize, Deserialize, ProtoBuf, Eq, PartialEq)]
@@ -398,7 +398,7 @@ impl std::convert::From<CellMetaChangeset> for RowMetaChangeset {
             row_id: changeset.row_id,
             height: None,
             visibility: None,
-            cell_by_field_id: cell_by_field_id,
+            cell_by_field_id,
         }
     }
 }
@@ -409,7 +409,7 @@ pub struct BuildGridContext {
     pub field_metas: Vec<FieldMeta>,
 
     #[pb(index = 2)]
-    pub block_metas: GridBlockMeta,
+    pub block_meta: GridBlockMeta,
 
     #[pb(index = 3)]
     pub block_meta_data: GridBlockMetaData,
@@ -417,16 +417,16 @@ pub struct BuildGridContext {
 
 impl std::default::Default for BuildGridContext {
     fn default() -> Self {
-        let grid_block = GridBlockMeta::new();
-        let grid_block_meta_data = GridBlockMetaData {
-            block_id: grid_block.block_id.clone(),
-            row_metas: vec![],
+        let block_meta = GridBlockMeta::new();
+        let block_meta_data = GridBlockMetaData {
+            block_id: block_meta.block_id.clone(),
+            rows: vec![],
         };
 
         Self {
             field_metas: vec![],
-            block_metas: grid_block,
-            block_meta_data: grid_block_meta_data,
+            block_meta,
+            block_meta_data,
         }
     }
 }

+ 619 - 25
shared-lib/flowy-grid-data-model/src/protobuf/model/grid.rs

@@ -2715,6 +2715,7 @@ impl ::protobuf::reflect::ProtobufValue for RepeatedGridBlock {
 pub struct GridBlockOrder {
     // message fields
     pub block_id: ::std::string::String,
+    pub row_orders: ::protobuf::RepeatedField<RowOrder>,
     // special fields
     pub unknown_fields: ::protobuf::UnknownFields,
     pub cached_size: ::protobuf::CachedSize,
@@ -2756,10 +2757,40 @@ impl GridBlockOrder {
     pub fn take_block_id(&mut self) -> ::std::string::String {
         ::std::mem::replace(&mut self.block_id, ::std::string::String::new())
     }
+
+    // repeated .RowOrder row_orders = 2;
+
+
+    pub fn get_row_orders(&self) -> &[RowOrder] {
+        &self.row_orders
+    }
+    pub fn clear_row_orders(&mut self) {
+        self.row_orders.clear();
+    }
+
+    // Param is passed by value, moved
+    pub fn set_row_orders(&mut self, v: ::protobuf::RepeatedField<RowOrder>) {
+        self.row_orders = v;
+    }
+
+    // Mutable pointer to the field.
+    pub fn mut_row_orders(&mut self) -> &mut ::protobuf::RepeatedField<RowOrder> {
+        &mut self.row_orders
+    }
+
+    // Take field
+    pub fn take_row_orders(&mut self) -> ::protobuf::RepeatedField<RowOrder> {
+        ::std::mem::replace(&mut self.row_orders, ::protobuf::RepeatedField::new())
+    }
 }
 
 impl ::protobuf::Message for GridBlockOrder {
     fn is_initialized(&self) -> bool {
+        for v in &self.row_orders {
+            if !v.is_initialized() {
+                return false;
+            }
+        };
         true
     }
 
@@ -2770,6 +2801,9 @@ impl ::protobuf::Message for GridBlockOrder {
                 1 => {
                     ::protobuf::rt::read_singular_proto3_string_into(wire_type, is, &mut self.block_id)?;
                 },
+                2 => {
+                    ::protobuf::rt::read_repeated_message_into(wire_type, is, &mut self.row_orders)?;
+                },
                 _ => {
                     ::protobuf::rt::read_unknown_or_skip_group(field_number, wire_type, is, self.mut_unknown_fields())?;
                 },
@@ -2785,6 +2819,10 @@ impl ::protobuf::Message for GridBlockOrder {
         if !self.block_id.is_empty() {
             my_size += ::protobuf::rt::string_size(1, &self.block_id);
         }
+        for value in &self.row_orders {
+            let len = value.compute_size();
+            my_size += 1 + ::protobuf::rt::compute_raw_varint32_size(len) + len;
+        };
         my_size += ::protobuf::rt::unknown_fields_size(self.get_unknown_fields());
         self.cached_size.set(my_size);
         my_size
@@ -2794,6 +2832,11 @@ impl ::protobuf::Message for GridBlockOrder {
         if !self.block_id.is_empty() {
             os.write_string(1, &self.block_id)?;
         }
+        for v in &self.row_orders {
+            os.write_tag(2, ::protobuf::wire_format::WireTypeLengthDelimited)?;
+            os.write_raw_varint32(v.get_cached_size())?;
+            v.write_to_with_cached_sizes(os)?;
+        };
         os.write_unknown_fields(self.get_unknown_fields())?;
         ::std::result::Result::Ok(())
     }
@@ -2837,6 +2880,11 @@ impl ::protobuf::Message for GridBlockOrder {
                 |m: &GridBlockOrder| { &m.block_id },
                 |m: &mut GridBlockOrder| { &mut m.block_id },
             ));
+            fields.push(::protobuf::reflect::accessor::make_repeated_field_accessor::<_, ::protobuf::types::ProtobufTypeMessage<RowOrder>>(
+                "row_orders",
+                |m: &GridBlockOrder| { &m.row_orders },
+                |m: &mut GridBlockOrder| { &mut m.row_orders },
+            ));
             ::protobuf::reflect::MessageDescriptor::new_pb_name::<GridBlockOrder>(
                 "GridBlockOrder",
                 fields,
@@ -2854,6 +2902,7 @@ impl ::protobuf::Message for GridBlockOrder {
 impl ::protobuf::Clear for GridBlockOrder {
     fn clear(&mut self) {
         self.block_id.clear();
+        self.row_orders.clear();
         self.unknown_fields.clear();
     }
 }
@@ -2870,6 +2919,544 @@ impl ::protobuf::reflect::ProtobufValue for GridBlockOrder {
     }
 }
 
+#[derive(PartialEq,Clone,Default)]
+pub struct GridBlockOrderChangeset {
+    // message fields
+    pub block_id: ::std::string::String,
+    pub inserted_rows: ::protobuf::RepeatedField<IndexRowOrder>,
+    pub deleted_rows: ::protobuf::RepeatedField<RowOrder>,
+    pub updated_rows: ::protobuf::RepeatedField<RowOrder>,
+    // special fields
+    pub unknown_fields: ::protobuf::UnknownFields,
+    pub cached_size: ::protobuf::CachedSize,
+}
+
+impl<'a> ::std::default::Default for &'a GridBlockOrderChangeset {
+    fn default() -> &'a GridBlockOrderChangeset {
+        <GridBlockOrderChangeset as ::protobuf::Message>::default_instance()
+    }
+}
+
+impl GridBlockOrderChangeset {
+    pub fn new() -> GridBlockOrderChangeset {
+        ::std::default::Default::default()
+    }
+
+    // string block_id = 1;
+
+
+    pub fn get_block_id(&self) -> &str {
+        &self.block_id
+    }
+    pub fn clear_block_id(&mut self) {
+        self.block_id.clear();
+    }
+
+    // Param is passed by value, moved
+    pub fn set_block_id(&mut self, v: ::std::string::String) {
+        self.block_id = v;
+    }
+
+    // Mutable pointer to the field.
+    // If field is not initialized, it is initialized with default value first.
+    pub fn mut_block_id(&mut self) -> &mut ::std::string::String {
+        &mut self.block_id
+    }
+
+    // Take field
+    pub fn take_block_id(&mut self) -> ::std::string::String {
+        ::std::mem::replace(&mut self.block_id, ::std::string::String::new())
+    }
+
+    // repeated .IndexRowOrder inserted_rows = 2;
+
+
+    pub fn get_inserted_rows(&self) -> &[IndexRowOrder] {
+        &self.inserted_rows
+    }
+    pub fn clear_inserted_rows(&mut self) {
+        self.inserted_rows.clear();
+    }
+
+    // Param is passed by value, moved
+    pub fn set_inserted_rows(&mut self, v: ::protobuf::RepeatedField<IndexRowOrder>) {
+        self.inserted_rows = v;
+    }
+
+    // Mutable pointer to the field.
+    pub fn mut_inserted_rows(&mut self) -> &mut ::protobuf::RepeatedField<IndexRowOrder> {
+        &mut self.inserted_rows
+    }
+
+    // Take field
+    pub fn take_inserted_rows(&mut self) -> ::protobuf::RepeatedField<IndexRowOrder> {
+        ::std::mem::replace(&mut self.inserted_rows, ::protobuf::RepeatedField::new())
+    }
+
+    // repeated .RowOrder deleted_rows = 3;
+
+
+    pub fn get_deleted_rows(&self) -> &[RowOrder] {
+        &self.deleted_rows
+    }
+    pub fn clear_deleted_rows(&mut self) {
+        self.deleted_rows.clear();
+    }
+
+    // Param is passed by value, moved
+    pub fn set_deleted_rows(&mut self, v: ::protobuf::RepeatedField<RowOrder>) {
+        self.deleted_rows = v;
+    }
+
+    // Mutable pointer to the field.
+    pub fn mut_deleted_rows(&mut self) -> &mut ::protobuf::RepeatedField<RowOrder> {
+        &mut self.deleted_rows
+    }
+
+    // Take field
+    pub fn take_deleted_rows(&mut self) -> ::protobuf::RepeatedField<RowOrder> {
+        ::std::mem::replace(&mut self.deleted_rows, ::protobuf::RepeatedField::new())
+    }
+
+    // repeated .RowOrder updated_rows = 4;
+
+
+    pub fn get_updated_rows(&self) -> &[RowOrder] {
+        &self.updated_rows
+    }
+    pub fn clear_updated_rows(&mut self) {
+        self.updated_rows.clear();
+    }
+
+    // Param is passed by value, moved
+    pub fn set_updated_rows(&mut self, v: ::protobuf::RepeatedField<RowOrder>) {
+        self.updated_rows = v;
+    }
+
+    // Mutable pointer to the field.
+    pub fn mut_updated_rows(&mut self) -> &mut ::protobuf::RepeatedField<RowOrder> {
+        &mut self.updated_rows
+    }
+
+    // Take field
+    pub fn take_updated_rows(&mut self) -> ::protobuf::RepeatedField<RowOrder> {
+        ::std::mem::replace(&mut self.updated_rows, ::protobuf::RepeatedField::new())
+    }
+}
+
+impl ::protobuf::Message for GridBlockOrderChangeset {
+    fn is_initialized(&self) -> bool {
+        for v in &self.inserted_rows {
+            if !v.is_initialized() {
+                return false;
+            }
+        };
+        for v in &self.deleted_rows {
+            if !v.is_initialized() {
+                return false;
+            }
+        };
+        for v in &self.updated_rows {
+            if !v.is_initialized() {
+                return false;
+            }
+        };
+        true
+    }
+
+    fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::ProtobufResult<()> {
+        while !is.eof()? {
+            let (field_number, wire_type) = is.read_tag_unpack()?;
+            match field_number {
+                1 => {
+                    ::protobuf::rt::read_singular_proto3_string_into(wire_type, is, &mut self.block_id)?;
+                },
+                2 => {
+                    ::protobuf::rt::read_repeated_message_into(wire_type, is, &mut self.inserted_rows)?;
+                },
+                3 => {
+                    ::protobuf::rt::read_repeated_message_into(wire_type, is, &mut self.deleted_rows)?;
+                },
+                4 => {
+                    ::protobuf::rt::read_repeated_message_into(wire_type, is, &mut self.updated_rows)?;
+                },
+                _ => {
+                    ::protobuf::rt::read_unknown_or_skip_group(field_number, wire_type, is, self.mut_unknown_fields())?;
+                },
+            };
+        }
+        ::std::result::Result::Ok(())
+    }
+
+    // Compute sizes of nested messages
+    #[allow(unused_variables)]
+    fn compute_size(&self) -> u32 {
+        let mut my_size = 0;
+        if !self.block_id.is_empty() {
+            my_size += ::protobuf::rt::string_size(1, &self.block_id);
+        }
+        for value in &self.inserted_rows {
+            let len = value.compute_size();
+            my_size += 1 + ::protobuf::rt::compute_raw_varint32_size(len) + len;
+        };
+        for value in &self.deleted_rows {
+            let len = value.compute_size();
+            my_size += 1 + ::protobuf::rt::compute_raw_varint32_size(len) + len;
+        };
+        for value in &self.updated_rows {
+            let len = value.compute_size();
+            my_size += 1 + ::protobuf::rt::compute_raw_varint32_size(len) + len;
+        };
+        my_size += ::protobuf::rt::unknown_fields_size(self.get_unknown_fields());
+        self.cached_size.set(my_size);
+        my_size
+    }
+
+    fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::ProtobufResult<()> {
+        if !self.block_id.is_empty() {
+            os.write_string(1, &self.block_id)?;
+        }
+        for v in &self.inserted_rows {
+            os.write_tag(2, ::protobuf::wire_format::WireTypeLengthDelimited)?;
+            os.write_raw_varint32(v.get_cached_size())?;
+            v.write_to_with_cached_sizes(os)?;
+        };
+        for v in &self.deleted_rows {
+            os.write_tag(3, ::protobuf::wire_format::WireTypeLengthDelimited)?;
+            os.write_raw_varint32(v.get_cached_size())?;
+            v.write_to_with_cached_sizes(os)?;
+        };
+        for v in &self.updated_rows {
+            os.write_tag(4, ::protobuf::wire_format::WireTypeLengthDelimited)?;
+            os.write_raw_varint32(v.get_cached_size())?;
+            v.write_to_with_cached_sizes(os)?;
+        };
+        os.write_unknown_fields(self.get_unknown_fields())?;
+        ::std::result::Result::Ok(())
+    }
+
+    fn get_cached_size(&self) -> u32 {
+        self.cached_size.get()
+    }
+
+    fn get_unknown_fields(&self) -> &::protobuf::UnknownFields {
+        &self.unknown_fields
+    }
+
+    fn mut_unknown_fields(&mut self) -> &mut ::protobuf::UnknownFields {
+        &mut self.unknown_fields
+    }
+
+    fn as_any(&self) -> &dyn (::std::any::Any) {
+        self as &dyn (::std::any::Any)
+    }
+    fn as_any_mut(&mut self) -> &mut dyn (::std::any::Any) {
+        self as &mut dyn (::std::any::Any)
+    }
+    fn into_any(self: ::std::boxed::Box<Self>) -> ::std::boxed::Box<dyn (::std::any::Any)> {
+        self
+    }
+
+    fn descriptor(&self) -> &'static ::protobuf::reflect::MessageDescriptor {
+        Self::descriptor_static()
+    }
+
+    fn new() -> GridBlockOrderChangeset {
+        GridBlockOrderChangeset::new()
+    }
+
+    fn descriptor_static() -> &'static ::protobuf::reflect::MessageDescriptor {
+        static descriptor: ::protobuf::rt::LazyV2<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::LazyV2::INIT;
+        descriptor.get(|| {
+            let mut fields = ::std::vec::Vec::new();
+            fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeString>(
+                "block_id",
+                |m: &GridBlockOrderChangeset| { &m.block_id },
+                |m: &mut GridBlockOrderChangeset| { &mut m.block_id },
+            ));
+            fields.push(::protobuf::reflect::accessor::make_repeated_field_accessor::<_, ::protobuf::types::ProtobufTypeMessage<IndexRowOrder>>(
+                "inserted_rows",
+                |m: &GridBlockOrderChangeset| { &m.inserted_rows },
+                |m: &mut GridBlockOrderChangeset| { &mut m.inserted_rows },
+            ));
+            fields.push(::protobuf::reflect::accessor::make_repeated_field_accessor::<_, ::protobuf::types::ProtobufTypeMessage<RowOrder>>(
+                "deleted_rows",
+                |m: &GridBlockOrderChangeset| { &m.deleted_rows },
+                |m: &mut GridBlockOrderChangeset| { &mut m.deleted_rows },
+            ));
+            fields.push(::protobuf::reflect::accessor::make_repeated_field_accessor::<_, ::protobuf::types::ProtobufTypeMessage<RowOrder>>(
+                "updated_rows",
+                |m: &GridBlockOrderChangeset| { &m.updated_rows },
+                |m: &mut GridBlockOrderChangeset| { &mut m.updated_rows },
+            ));
+            ::protobuf::reflect::MessageDescriptor::new_pb_name::<GridBlockOrderChangeset>(
+                "GridBlockOrderChangeset",
+                fields,
+                file_descriptor_proto()
+            )
+        })
+    }
+
+    fn default_instance() -> &'static GridBlockOrderChangeset {
+        static instance: ::protobuf::rt::LazyV2<GridBlockOrderChangeset> = ::protobuf::rt::LazyV2::INIT;
+        instance.get(GridBlockOrderChangeset::new)
+    }
+}
+
+impl ::protobuf::Clear for GridBlockOrderChangeset {
+    fn clear(&mut self) {
+        self.block_id.clear();
+        self.inserted_rows.clear();
+        self.deleted_rows.clear();
+        self.updated_rows.clear();
+        self.unknown_fields.clear();
+    }
+}
+
+impl ::std::fmt::Debug for GridBlockOrderChangeset {
+    fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
+        ::protobuf::text_format::fmt(self, f)
+    }
+}
+
+impl ::protobuf::reflect::ProtobufValue for GridBlockOrderChangeset {
+    fn as_ref(&self) -> ::protobuf::reflect::ReflectValueRef {
+        ::protobuf::reflect::ReflectValueRef::Message(self)
+    }
+}
+
+#[derive(PartialEq,Clone,Default)]
+pub struct IndexRowOrder {
+    // message fields
+    pub row_order: ::protobuf::SingularPtrField<RowOrder>,
+    // message oneof groups
+    pub one_of_index: ::std::option::Option<IndexRowOrder_oneof_one_of_index>,
+    // special fields
+    pub unknown_fields: ::protobuf::UnknownFields,
+    pub cached_size: ::protobuf::CachedSize,
+}
+
+impl<'a> ::std::default::Default for &'a IndexRowOrder {
+    fn default() -> &'a IndexRowOrder {
+        <IndexRowOrder as ::protobuf::Message>::default_instance()
+    }
+}
+
+#[derive(Clone,PartialEq,Debug)]
+pub enum IndexRowOrder_oneof_one_of_index {
+    index(i32),
+}
+
+impl IndexRowOrder {
+    pub fn new() -> IndexRowOrder {
+        ::std::default::Default::default()
+    }
+
+    // .RowOrder row_order = 1;
+
+
+    pub fn get_row_order(&self) -> &RowOrder {
+        self.row_order.as_ref().unwrap_or_else(|| <RowOrder as ::protobuf::Message>::default_instance())
+    }
+    pub fn clear_row_order(&mut self) {
+        self.row_order.clear();
+    }
+
+    pub fn has_row_order(&self) -> bool {
+        self.row_order.is_some()
+    }
+
+    // Param is passed by value, moved
+    pub fn set_row_order(&mut self, v: RowOrder) {
+        self.row_order = ::protobuf::SingularPtrField::some(v);
+    }
+
+    // Mutable pointer to the field.
+    // If field is not initialized, it is initialized with default value first.
+    pub fn mut_row_order(&mut self) -> &mut RowOrder {
+        if self.row_order.is_none() {
+            self.row_order.set_default();
+        }
+        self.row_order.as_mut().unwrap()
+    }
+
+    // Take field
+    pub fn take_row_order(&mut self) -> RowOrder {
+        self.row_order.take().unwrap_or_else(|| RowOrder::new())
+    }
+
+    // int32 index = 2;
+
+
+    pub fn get_index(&self) -> i32 {
+        match self.one_of_index {
+            ::std::option::Option::Some(IndexRowOrder_oneof_one_of_index::index(v)) => v,
+            _ => 0,
+        }
+    }
+    pub fn clear_index(&mut self) {
+        self.one_of_index = ::std::option::Option::None;
+    }
+
+    pub fn has_index(&self) -> bool {
+        match self.one_of_index {
+            ::std::option::Option::Some(IndexRowOrder_oneof_one_of_index::index(..)) => true,
+            _ => false,
+        }
+    }
+
+    // Param is passed by value, moved
+    pub fn set_index(&mut self, v: i32) {
+        self.one_of_index = ::std::option::Option::Some(IndexRowOrder_oneof_one_of_index::index(v))
+    }
+}
+
+impl ::protobuf::Message for IndexRowOrder {
+    fn is_initialized(&self) -> bool {
+        for v in &self.row_order {
+            if !v.is_initialized() {
+                return false;
+            }
+        };
+        true
+    }
+
+    fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::ProtobufResult<()> {
+        while !is.eof()? {
+            let (field_number, wire_type) = is.read_tag_unpack()?;
+            match field_number {
+                1 => {
+                    ::protobuf::rt::read_singular_message_into(wire_type, is, &mut self.row_order)?;
+                },
+                2 => {
+                    if wire_type != ::protobuf::wire_format::WireTypeVarint {
+                        return ::std::result::Result::Err(::protobuf::rt::unexpected_wire_type(wire_type));
+                    }
+                    self.one_of_index = ::std::option::Option::Some(IndexRowOrder_oneof_one_of_index::index(is.read_int32()?));
+                },
+                _ => {
+                    ::protobuf::rt::read_unknown_or_skip_group(field_number, wire_type, is, self.mut_unknown_fields())?;
+                },
+            };
+        }
+        ::std::result::Result::Ok(())
+    }
+
+    // Compute sizes of nested messages
+    #[allow(unused_variables)]
+    fn compute_size(&self) -> u32 {
+        let mut my_size = 0;
+        if let Some(ref v) = self.row_order.as_ref() {
+            let len = v.compute_size();
+            my_size += 1 + ::protobuf::rt::compute_raw_varint32_size(len) + len;
+        }
+        if let ::std::option::Option::Some(ref v) = self.one_of_index {
+            match v {
+                &IndexRowOrder_oneof_one_of_index::index(v) => {
+                    my_size += ::protobuf::rt::value_size(2, v, ::protobuf::wire_format::WireTypeVarint);
+                },
+            };
+        }
+        my_size += ::protobuf::rt::unknown_fields_size(self.get_unknown_fields());
+        self.cached_size.set(my_size);
+        my_size
+    }
+
+    fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::ProtobufResult<()> {
+        if let Some(ref v) = self.row_order.as_ref() {
+            os.write_tag(1, ::protobuf::wire_format::WireTypeLengthDelimited)?;
+            os.write_raw_varint32(v.get_cached_size())?;
+            v.write_to_with_cached_sizes(os)?;
+        }
+        if let ::std::option::Option::Some(ref v) = self.one_of_index {
+            match v {
+                &IndexRowOrder_oneof_one_of_index::index(v) => {
+                    os.write_int32(2, v)?;
+                },
+            };
+        }
+        os.write_unknown_fields(self.get_unknown_fields())?;
+        ::std::result::Result::Ok(())
+    }
+
+    fn get_cached_size(&self) -> u32 {
+        self.cached_size.get()
+    }
+
+    fn get_unknown_fields(&self) -> &::protobuf::UnknownFields {
+        &self.unknown_fields
+    }
+
+    fn mut_unknown_fields(&mut self) -> &mut ::protobuf::UnknownFields {
+        &mut self.unknown_fields
+    }
+
+    fn as_any(&self) -> &dyn (::std::any::Any) {
+        self as &dyn (::std::any::Any)
+    }
+    fn as_any_mut(&mut self) -> &mut dyn (::std::any::Any) {
+        self as &mut dyn (::std::any::Any)
+    }
+    fn into_any(self: ::std::boxed::Box<Self>) -> ::std::boxed::Box<dyn (::std::any::Any)> {
+        self
+    }
+
+    fn descriptor(&self) -> &'static ::protobuf::reflect::MessageDescriptor {
+        Self::descriptor_static()
+    }
+
+    fn new() -> IndexRowOrder {
+        IndexRowOrder::new()
+    }
+
+    fn descriptor_static() -> &'static ::protobuf::reflect::MessageDescriptor {
+        static descriptor: ::protobuf::rt::LazyV2<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::LazyV2::INIT;
+        descriptor.get(|| {
+            let mut fields = ::std::vec::Vec::new();
+            fields.push(::protobuf::reflect::accessor::make_singular_ptr_field_accessor::<_, ::protobuf::types::ProtobufTypeMessage<RowOrder>>(
+                "row_order",
+                |m: &IndexRowOrder| { &m.row_order },
+                |m: &mut IndexRowOrder| { &mut m.row_order },
+            ));
+            fields.push(::protobuf::reflect::accessor::make_singular_i32_accessor::<_>(
+                "index",
+                IndexRowOrder::has_index,
+                IndexRowOrder::get_index,
+            ));
+            ::protobuf::reflect::MessageDescriptor::new_pb_name::<IndexRowOrder>(
+                "IndexRowOrder",
+                fields,
+                file_descriptor_proto()
+            )
+        })
+    }
+
+    fn default_instance() -> &'static IndexRowOrder {
+        static instance: ::protobuf::rt::LazyV2<IndexRowOrder> = ::protobuf::rt::LazyV2::INIT;
+        instance.get(IndexRowOrder::new)
+    }
+}
+
+impl ::protobuf::Clear for IndexRowOrder {
+    fn clear(&mut self) {
+        self.row_order.clear();
+        self.one_of_index = ::std::option::Option::None;
+        self.unknown_fields.clear();
+    }
+}
+
+impl ::std::fmt::Debug for IndexRowOrder {
+    fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
+        ::protobuf::text_format::fmt(self, f)
+    }
+}
+
+impl ::protobuf::reflect::ProtobufValue for IndexRowOrder {
+    fn as_ref(&self) -> ::protobuf::reflect::ReflectValueRef {
+        ::protobuf::reflect::ReflectValueRef::Message(self)
+    }
+}
+
 #[derive(PartialEq,Clone,Default)]
 pub struct GridBlock {
     // message fields
@@ -5283,31 +5870,38 @@ static file_descriptor_proto_data: &'static [u8] = b"\
     \x18\x01\x20\x01(\tR\x03key\x12\x1b\n\x05value\x18\x02\x20\x01(\x0b2\x05\
     .CellR\x05value:\x028\x01\")\n\x0bRepeatedRow\x12\x1a\n\x05items\x18\x01\
     \x20\x03(\x0b2\x04.RowR\x05items\"5\n\x11RepeatedGridBlock\x12\x20\n\x05\
-    items\x18\x01\x20\x03(\x0b2\n.GridBlockR\x05items\"+\n\x0eGridBlockOrder\
-    \x12\x19\n\x08block_id\x18\x01\x20\x01(\tR\x07blockId\"E\n\tGridBlock\
-    \x12\x0e\n\x02id\x18\x01\x20\x01(\tR\x02id\x12(\n\nrow_orders\x18\x02\
-    \x20\x03(\x0b2\t.RowOrderR\trowOrders\";\n\x04Cell\x12\x19\n\x08field_id\
-    \x18\x01\x20\x01(\tR\x07fieldId\x12\x18\n\x07content\x18\x02\x20\x01(\tR\
-    \x07content\"\x8f\x01\n\x14CellNotificationData\x12\x17\n\x07grid_id\x18\
-    \x01\x20\x01(\tR\x06gridId\x12\x19\n\x08field_id\x18\x02\x20\x01(\tR\x07\
-    fieldId\x12\x15\n\x06row_id\x18\x03\x20\x01(\tR\x05rowId\x12\x1a\n\x07co\
-    ntent\x18\x04\x20\x01(\tH\0R\x07contentB\x10\n\x0eone_of_content\"+\n\
-    \x0cRepeatedCell\x12\x1b\n\x05items\x18\x01\x20\x03(\x0b2\x05.CellR\x05i\
-    tems\"'\n\x11CreateGridPayload\x12\x12\n\x04name\x18\x01\x20\x01(\tR\x04\
-    name\"\x1e\n\x06GridId\x12\x14\n\x05value\x18\x01\x20\x01(\tR\x05value\"\
-    #\n\x0bGridBlockId\x12\x14\n\x05value\x18\x01\x20\x01(\tR\x05value\"f\n\
-    \x10CreateRowPayload\x12\x17\n\x07grid_id\x18\x01\x20\x01(\tR\x06gridId\
-    \x12\"\n\x0cstart_row_id\x18\x02\x20\x01(\tH\0R\nstartRowIdB\x15\n\x13on\
-    e_of_start_row_id\"\xb6\x01\n\x12CreateFieldPayload\x12\x17\n\x07grid_id\
-    \x18\x01\x20\x01(\tR\x06gridId\x12\x1c\n\x05field\x18\x02\x20\x01(\x0b2\
-    \x06.FieldR\x05field\x12(\n\x10type_option_data\x18\x03\x20\x01(\x0cR\
-    \x0etypeOptionData\x12&\n\x0estart_field_id\x18\x04\x20\x01(\tH\0R\x0cst\
-    artFieldIdB\x17\n\x15one_of_start_field_id\"d\n\x11QueryFieldPayload\x12\
-    \x17\n\x07grid_id\x18\x01\x20\x01(\tR\x06gridId\x126\n\x0cfield_orders\
-    \x18\x02\x20\x01(\x0b2\x13.RepeatedFieldOrderR\x0bfieldOrders\"e\n\x16Qu\
-    eryGridBlocksPayload\x12\x17\n\x07grid_id\x18\x01\x20\x01(\tR\x06gridId\
-    \x122\n\x0cblock_orders\x18\x02\x20\x03(\x0b2\x0f.GridBlockOrderR\x0bblo\
-    ckOrdersb\x06proto3\
+    items\x18\x01\x20\x03(\x0b2\n.GridBlockR\x05items\"U\n\x0eGridBlockOrder\
+    \x12\x19\n\x08block_id\x18\x01\x20\x01(\tR\x07blockId\x12(\n\nrow_orders\
+    \x18\x02\x20\x03(\x0b2\t.RowOrderR\trowOrders\"\xc5\x01\n\x17GridBlockOr\
+    derChangeset\x12\x19\n\x08block_id\x18\x01\x20\x01(\tR\x07blockId\x123\n\
+    \rinserted_rows\x18\x02\x20\x03(\x0b2\x0e.IndexRowOrderR\x0cinsertedRows\
+    \x12,\n\x0cdeleted_rows\x18\x03\x20\x03(\x0b2\t.RowOrderR\x0bdeletedRows\
+    \x12,\n\x0cupdated_rows\x18\x04\x20\x03(\x0b2\t.RowOrderR\x0bupdatedRows\
+    \"_\n\rIndexRowOrder\x12&\n\trow_order\x18\x01\x20\x01(\x0b2\t.RowOrderR\
+    \x08rowOrder\x12\x16\n\x05index\x18\x02\x20\x01(\x05H\0R\x05indexB\x0e\n\
+    \x0cone_of_index\"E\n\tGridBlock\x12\x0e\n\x02id\x18\x01\x20\x01(\tR\x02\
+    id\x12(\n\nrow_orders\x18\x02\x20\x03(\x0b2\t.RowOrderR\trowOrders\";\n\
+    \x04Cell\x12\x19\n\x08field_id\x18\x01\x20\x01(\tR\x07fieldId\x12\x18\n\
+    \x07content\x18\x02\x20\x01(\tR\x07content\"\x8f\x01\n\x14CellNotificati\
+    onData\x12\x17\n\x07grid_id\x18\x01\x20\x01(\tR\x06gridId\x12\x19\n\x08f\
+    ield_id\x18\x02\x20\x01(\tR\x07fieldId\x12\x15\n\x06row_id\x18\x03\x20\
+    \x01(\tR\x05rowId\x12\x1a\n\x07content\x18\x04\x20\x01(\tH\0R\x07content\
+    B\x10\n\x0eone_of_content\"+\n\x0cRepeatedCell\x12\x1b\n\x05items\x18\
+    \x01\x20\x03(\x0b2\x05.CellR\x05items\"'\n\x11CreateGridPayload\x12\x12\
+    \n\x04name\x18\x01\x20\x01(\tR\x04name\"\x1e\n\x06GridId\x12\x14\n\x05va\
+    lue\x18\x01\x20\x01(\tR\x05value\"#\n\x0bGridBlockId\x12\x14\n\x05value\
+    \x18\x01\x20\x01(\tR\x05value\"f\n\x10CreateRowPayload\x12\x17\n\x07grid\
+    _id\x18\x01\x20\x01(\tR\x06gridId\x12\"\n\x0cstart_row_id\x18\x02\x20\
+    \x01(\tH\0R\nstartRowIdB\x15\n\x13one_of_start_row_id\"\xb6\x01\n\x12Cre\
+    ateFieldPayload\x12\x17\n\x07grid_id\x18\x01\x20\x01(\tR\x06gridId\x12\
+    \x1c\n\x05field\x18\x02\x20\x01(\x0b2\x06.FieldR\x05field\x12(\n\x10type\
+    _option_data\x18\x03\x20\x01(\x0cR\x0etypeOptionData\x12&\n\x0estart_fie\
+    ld_id\x18\x04\x20\x01(\tH\0R\x0cstartFieldIdB\x17\n\x15one_of_start_fiel\
+    d_id\"d\n\x11QueryFieldPayload\x12\x17\n\x07grid_id\x18\x01\x20\x01(\tR\
+    \x06gridId\x126\n\x0cfield_orders\x18\x02\x20\x01(\x0b2\x13.RepeatedFiel\
+    dOrderR\x0bfieldOrders\"e\n\x16QueryGridBlocksPayload\x12\x17\n\x07grid_\
+    id\x18\x01\x20\x01(\tR\x06gridId\x122\n\x0cblock_orders\x18\x02\x20\x03(\
+    \x0b2\x0f.GridBlockOrderR\x0bblockOrdersb\x06proto3\
 ";
 
 static file_descriptor_proto_lazy: ::protobuf::rt::LazyV2<::protobuf::descriptor::FileDescriptorProto> = ::protobuf::rt::LazyV2::INIT;

+ 90 - 91
shared-lib/flowy-grid-data-model/src/protobuf/model/meta.rs

@@ -28,7 +28,7 @@ pub struct GridMeta {
     // message fields
     pub grid_id: ::std::string::String,
     pub fields: ::protobuf::RepeatedField<FieldMeta>,
-    pub block_metas: ::protobuf::RepeatedField<GridBlockMeta>,
+    pub blocks: ::protobuf::RepeatedField<GridBlockMeta>,
     // special fields
     pub unknown_fields: ::protobuf::UnknownFields,
     pub cached_size: ::protobuf::CachedSize,
@@ -96,29 +96,29 @@ impl GridMeta {
         ::std::mem::replace(&mut self.fields, ::protobuf::RepeatedField::new())
     }
 
-    // repeated .GridBlockMeta block_metas = 3;
+    // repeated .GridBlockMeta blocks = 3;
 
 
-    pub fn get_block_metas(&self) -> &[GridBlockMeta] {
-        &self.block_metas
+    pub fn get_blocks(&self) -> &[GridBlockMeta] {
+        &self.blocks
     }
-    pub fn clear_block_metas(&mut self) {
-        self.block_metas.clear();
+    pub fn clear_blocks(&mut self) {
+        self.blocks.clear();
     }
 
     // Param is passed by value, moved
-    pub fn set_block_metas(&mut self, v: ::protobuf::RepeatedField<GridBlockMeta>) {
-        self.block_metas = v;
+    pub fn set_blocks(&mut self, v: ::protobuf::RepeatedField<GridBlockMeta>) {
+        self.blocks = v;
     }
 
     // Mutable pointer to the field.
-    pub fn mut_block_metas(&mut self) -> &mut ::protobuf::RepeatedField<GridBlockMeta> {
-        &mut self.block_metas
+    pub fn mut_blocks(&mut self) -> &mut ::protobuf::RepeatedField<GridBlockMeta> {
+        &mut self.blocks
     }
 
     // Take field
-    pub fn take_block_metas(&mut self) -> ::protobuf::RepeatedField<GridBlockMeta> {
-        ::std::mem::replace(&mut self.block_metas, ::protobuf::RepeatedField::new())
+    pub fn take_blocks(&mut self) -> ::protobuf::RepeatedField<GridBlockMeta> {
+        ::std::mem::replace(&mut self.blocks, ::protobuf::RepeatedField::new())
     }
 }
 
@@ -129,7 +129,7 @@ impl ::protobuf::Message for GridMeta {
                 return false;
             }
         };
-        for v in &self.block_metas {
+        for v in &self.blocks {
             if !v.is_initialized() {
                 return false;
             }
@@ -148,7 +148,7 @@ impl ::protobuf::Message for GridMeta {
                     ::protobuf::rt::read_repeated_message_into(wire_type, is, &mut self.fields)?;
                 },
                 3 => {
-                    ::protobuf::rt::read_repeated_message_into(wire_type, is, &mut self.block_metas)?;
+                    ::protobuf::rt::read_repeated_message_into(wire_type, is, &mut self.blocks)?;
                 },
                 _ => {
                     ::protobuf::rt::read_unknown_or_skip_group(field_number, wire_type, is, self.mut_unknown_fields())?;
@@ -169,7 +169,7 @@ impl ::protobuf::Message for GridMeta {
             let len = value.compute_size();
             my_size += 1 + ::protobuf::rt::compute_raw_varint32_size(len) + len;
         };
-        for value in &self.block_metas {
+        for value in &self.blocks {
             let len = value.compute_size();
             my_size += 1 + ::protobuf::rt::compute_raw_varint32_size(len) + len;
         };
@@ -187,7 +187,7 @@ impl ::protobuf::Message for GridMeta {
             os.write_raw_varint32(v.get_cached_size())?;
             v.write_to_with_cached_sizes(os)?;
         };
-        for v in &self.block_metas {
+        for v in &self.blocks {
             os.write_tag(3, ::protobuf::wire_format::WireTypeLengthDelimited)?;
             os.write_raw_varint32(v.get_cached_size())?;
             v.write_to_with_cached_sizes(os)?;
@@ -241,9 +241,9 @@ impl ::protobuf::Message for GridMeta {
                 |m: &mut GridMeta| { &mut m.fields },
             ));
             fields.push(::protobuf::reflect::accessor::make_repeated_field_accessor::<_, ::protobuf::types::ProtobufTypeMessage<GridBlockMeta>>(
-                "block_metas",
-                |m: &GridMeta| { &m.block_metas },
-                |m: &mut GridMeta| { &mut m.block_metas },
+                "blocks",
+                |m: &GridMeta| { &m.blocks },
+                |m: &mut GridMeta| { &mut m.blocks },
             ));
             ::protobuf::reflect::MessageDescriptor::new_pb_name::<GridMeta>(
                 "GridMeta",
@@ -263,7 +263,7 @@ impl ::protobuf::Clear for GridMeta {
     fn clear(&mut self) {
         self.grid_id.clear();
         self.fields.clear();
-        self.block_metas.clear();
+        self.blocks.clear();
         self.unknown_fields.clear();
     }
 }
@@ -513,7 +513,7 @@ impl ::protobuf::reflect::ProtobufValue for GridBlockMeta {
 pub struct GridBlockMetaData {
     // message fields
     pub block_id: ::std::string::String,
-    pub row_metas: ::protobuf::RepeatedField<RowMeta>,
+    pub rows: ::protobuf::RepeatedField<RowMeta>,
     // special fields
     pub unknown_fields: ::protobuf::UnknownFields,
     pub cached_size: ::protobuf::CachedSize,
@@ -556,35 +556,35 @@ impl GridBlockMetaData {
         ::std::mem::replace(&mut self.block_id, ::std::string::String::new())
     }
 
-    // repeated .RowMeta row_metas = 2;
+    // repeated .RowMeta rows = 2;
 
 
-    pub fn get_row_metas(&self) -> &[RowMeta] {
-        &self.row_metas
+    pub fn get_rows(&self) -> &[RowMeta] {
+        &self.rows
     }
-    pub fn clear_row_metas(&mut self) {
-        self.row_metas.clear();
+    pub fn clear_rows(&mut self) {
+        self.rows.clear();
     }
 
     // Param is passed by value, moved
-    pub fn set_row_metas(&mut self, v: ::protobuf::RepeatedField<RowMeta>) {
-        self.row_metas = v;
+    pub fn set_rows(&mut self, v: ::protobuf::RepeatedField<RowMeta>) {
+        self.rows = v;
     }
 
     // Mutable pointer to the field.
-    pub fn mut_row_metas(&mut self) -> &mut ::protobuf::RepeatedField<RowMeta> {
-        &mut self.row_metas
+    pub fn mut_rows(&mut self) -> &mut ::protobuf::RepeatedField<RowMeta> {
+        &mut self.rows
     }
 
     // Take field
-    pub fn take_row_metas(&mut self) -> ::protobuf::RepeatedField<RowMeta> {
-        ::std::mem::replace(&mut self.row_metas, ::protobuf::RepeatedField::new())
+    pub fn take_rows(&mut self) -> ::protobuf::RepeatedField<RowMeta> {
+        ::std::mem::replace(&mut self.rows, ::protobuf::RepeatedField::new())
     }
 }
 
 impl ::protobuf::Message for GridBlockMetaData {
     fn is_initialized(&self) -> bool {
-        for v in &self.row_metas {
+        for v in &self.rows {
             if !v.is_initialized() {
                 return false;
             }
@@ -600,7 +600,7 @@ impl ::protobuf::Message for GridBlockMetaData {
                     ::protobuf::rt::read_singular_proto3_string_into(wire_type, is, &mut self.block_id)?;
                 },
                 2 => {
-                    ::protobuf::rt::read_repeated_message_into(wire_type, is, &mut self.row_metas)?;
+                    ::protobuf::rt::read_repeated_message_into(wire_type, is, &mut self.rows)?;
                 },
                 _ => {
                     ::protobuf::rt::read_unknown_or_skip_group(field_number, wire_type, is, self.mut_unknown_fields())?;
@@ -617,7 +617,7 @@ impl ::protobuf::Message for GridBlockMetaData {
         if !self.block_id.is_empty() {
             my_size += ::protobuf::rt::string_size(1, &self.block_id);
         }
-        for value in &self.row_metas {
+        for value in &self.rows {
             let len = value.compute_size();
             my_size += 1 + ::protobuf::rt::compute_raw_varint32_size(len) + len;
         };
@@ -630,7 +630,7 @@ impl ::protobuf::Message for GridBlockMetaData {
         if !self.block_id.is_empty() {
             os.write_string(1, &self.block_id)?;
         }
-        for v in &self.row_metas {
+        for v in &self.rows {
             os.write_tag(2, ::protobuf::wire_format::WireTypeLengthDelimited)?;
             os.write_raw_varint32(v.get_cached_size())?;
             v.write_to_with_cached_sizes(os)?;
@@ -679,9 +679,9 @@ impl ::protobuf::Message for GridBlockMetaData {
                 |m: &mut GridBlockMetaData| { &mut m.block_id },
             ));
             fields.push(::protobuf::reflect::accessor::make_repeated_field_accessor::<_, ::protobuf::types::ProtobufTypeMessage<RowMeta>>(
-                "row_metas",
-                |m: &GridBlockMetaData| { &m.row_metas },
-                |m: &mut GridBlockMetaData| { &mut m.row_metas },
+                "rows",
+                |m: &GridBlockMetaData| { &m.rows },
+                |m: &mut GridBlockMetaData| { &mut m.rows },
             ));
             ::protobuf::reflect::MessageDescriptor::new_pb_name::<GridBlockMetaData>(
                 "GridBlockMetaData",
@@ -700,7 +700,7 @@ impl ::protobuf::Message for GridBlockMetaData {
 impl ::protobuf::Clear for GridBlockMetaData {
     fn clear(&mut self) {
         self.block_id.clear();
-        self.row_metas.clear();
+        self.rows.clear();
         self.unknown_fields.clear();
     }
 }
@@ -3114,7 +3114,7 @@ impl ::protobuf::reflect::ProtobufValue for CellMetaChangeset {
 pub struct BuildGridContext {
     // message fields
     pub field_metas: ::protobuf::RepeatedField<FieldMeta>,
-    pub block_metas: ::protobuf::SingularPtrField<GridBlockMeta>,
+    pub block_meta: ::protobuf::SingularPtrField<GridBlockMeta>,
     pub block_meta_data: ::protobuf::SingularPtrField<GridBlockMetaData>,
     // special fields
     pub unknown_fields: ::protobuf::UnknownFields,
@@ -3157,37 +3157,37 @@ impl BuildGridContext {
         ::std::mem::replace(&mut self.field_metas, ::protobuf::RepeatedField::new())
     }
 
-    // .GridBlockMeta block_metas = 2;
+    // .GridBlockMeta block_meta = 2;
 
 
-    pub fn get_block_metas(&self) -> &GridBlockMeta {
-        self.block_metas.as_ref().unwrap_or_else(|| <GridBlockMeta as ::protobuf::Message>::default_instance())
+    pub fn get_block_meta(&self) -> &GridBlockMeta {
+        self.block_meta.as_ref().unwrap_or_else(|| <GridBlockMeta as ::protobuf::Message>::default_instance())
     }
-    pub fn clear_block_metas(&mut self) {
-        self.block_metas.clear();
+    pub fn clear_block_meta(&mut self) {
+        self.block_meta.clear();
     }
 
-    pub fn has_block_metas(&self) -> bool {
-        self.block_metas.is_some()
+    pub fn has_block_meta(&self) -> bool {
+        self.block_meta.is_some()
     }
 
     // Param is passed by value, moved
-    pub fn set_block_metas(&mut self, v: GridBlockMeta) {
-        self.block_metas = ::protobuf::SingularPtrField::some(v);
+    pub fn set_block_meta(&mut self, v: GridBlockMeta) {
+        self.block_meta = ::protobuf::SingularPtrField::some(v);
     }
 
     // Mutable pointer to the field.
     // If field is not initialized, it is initialized with default value first.
-    pub fn mut_block_metas(&mut self) -> &mut GridBlockMeta {
-        if self.block_metas.is_none() {
-            self.block_metas.set_default();
+    pub fn mut_block_meta(&mut self) -> &mut GridBlockMeta {
+        if self.block_meta.is_none() {
+            self.block_meta.set_default();
         }
-        self.block_metas.as_mut().unwrap()
+        self.block_meta.as_mut().unwrap()
     }
 
     // Take field
-    pub fn take_block_metas(&mut self) -> GridBlockMeta {
-        self.block_metas.take().unwrap_or_else(|| GridBlockMeta::new())
+    pub fn take_block_meta(&mut self) -> GridBlockMeta {
+        self.block_meta.take().unwrap_or_else(|| GridBlockMeta::new())
     }
 
     // .GridBlockMetaData block_meta_data = 3;
@@ -3231,7 +3231,7 @@ impl ::protobuf::Message for BuildGridContext {
                 return false;
             }
         };
-        for v in &self.block_metas {
+        for v in &self.block_meta {
             if !v.is_initialized() {
                 return false;
             }
@@ -3252,7 +3252,7 @@ impl ::protobuf::Message for BuildGridContext {
                     ::protobuf::rt::read_repeated_message_into(wire_type, is, &mut self.field_metas)?;
                 },
                 2 => {
-                    ::protobuf::rt::read_singular_message_into(wire_type, is, &mut self.block_metas)?;
+                    ::protobuf::rt::read_singular_message_into(wire_type, is, &mut self.block_meta)?;
                 },
                 3 => {
                     ::protobuf::rt::read_singular_message_into(wire_type, is, &mut self.block_meta_data)?;
@@ -3273,7 +3273,7 @@ impl ::protobuf::Message for BuildGridContext {
             let len = value.compute_size();
             my_size += 1 + ::protobuf::rt::compute_raw_varint32_size(len) + len;
         };
-        if let Some(ref v) = self.block_metas.as_ref() {
+        if let Some(ref v) = self.block_meta.as_ref() {
             let len = v.compute_size();
             my_size += 1 + ::protobuf::rt::compute_raw_varint32_size(len) + len;
         }
@@ -3292,7 +3292,7 @@ impl ::protobuf::Message for BuildGridContext {
             os.write_raw_varint32(v.get_cached_size())?;
             v.write_to_with_cached_sizes(os)?;
         };
-        if let Some(ref v) = self.block_metas.as_ref() {
+        if let Some(ref v) = self.block_meta.as_ref() {
             os.write_tag(2, ::protobuf::wire_format::WireTypeLengthDelimited)?;
             os.write_raw_varint32(v.get_cached_size())?;
             v.write_to_with_cached_sizes(os)?;
@@ -3346,9 +3346,9 @@ impl ::protobuf::Message for BuildGridContext {
                 |m: &mut BuildGridContext| { &mut m.field_metas },
             ));
             fields.push(::protobuf::reflect::accessor::make_singular_ptr_field_accessor::<_, ::protobuf::types::ProtobufTypeMessage<GridBlockMeta>>(
-                "block_metas",
-                |m: &BuildGridContext| { &m.block_metas },
-                |m: &mut BuildGridContext| { &mut m.block_metas },
+                "block_meta",
+                |m: &BuildGridContext| { &m.block_meta },
+                |m: &mut BuildGridContext| { &mut m.block_meta },
             ));
             fields.push(::protobuf::reflect::accessor::make_singular_ptr_field_accessor::<_, ::protobuf::types::ProtobufTypeMessage<GridBlockMetaData>>(
                 "block_meta_data",
@@ -3372,7 +3372,7 @@ impl ::protobuf::Message for BuildGridContext {
 impl ::protobuf::Clear for BuildGridContext {
     fn clear(&mut self) {
         self.field_metas.clear();
-        self.block_metas.clear();
+        self.block_meta.clear();
         self.block_meta_data.clear();
         self.unknown_fields.clear();
     }
@@ -3453,26 +3453,26 @@ impl ::protobuf::reflect::ProtobufValue for FieldType {
 }
 
 static file_descriptor_proto_data: &'static [u8] = b"\
-    \n\nmeta.proto\"x\n\x08GridMeta\x12\x17\n\x07grid_id\x18\x01\x20\x01(\tR\
+    \n\nmeta.proto\"o\n\x08GridMeta\x12\x17\n\x07grid_id\x18\x01\x20\x01(\tR\
     \x06gridId\x12\"\n\x06fields\x18\x02\x20\x03(\x0b2\n.FieldMetaR\x06field\
-    s\x12/\n\x0bblock_metas\x18\x03\x20\x03(\x0b2\x0e.GridBlockMetaR\nblockM\
-    etas\"o\n\rGridBlockMeta\x12\x19\n\x08block_id\x18\x01\x20\x01(\tR\x07bl\
-    ockId\x12&\n\x0fstart_row_index\x18\x02\x20\x01(\x05R\rstartRowIndex\x12\
-    \x1b\n\trow_count\x18\x03\x20\x01(\x05R\x08rowCount\"U\n\x11GridBlockMet\
-    aData\x12\x19\n\x08block_id\x18\x01\x20\x01(\tR\x07blockId\x12%\n\trow_m\
-    etas\x18\x02\x20\x03(\x0b2\x08.RowMetaR\x08rowMetas\"\xbc\x02\n\tFieldMe\
-    ta\x12\x0e\n\x02id\x18\x01\x20\x01(\tR\x02id\x12\x12\n\x04name\x18\x02\
-    \x20\x01(\tR\x04name\x12\x12\n\x04desc\x18\x03\x20\x01(\tR\x04desc\x12)\
-    \n\nfield_type\x18\x04\x20\x01(\x0e2\n.FieldTypeR\tfieldType\x12\x16\n\
-    \x06frozen\x18\x05\x20\x01(\x08R\x06frozen\x12\x1e\n\nvisibility\x18\x06\
-    \x20\x01(\x08R\nvisibility\x12\x14\n\x05width\x18\x07\x20\x01(\x05R\x05w\
-    idth\x12>\n\x0ctype_options\x18\x08\x20\x03(\x0b2\x1b.FieldMeta.TypeOpti\
-    onsEntryR\x0btypeOptions\x1a>\n\x10TypeOptionsEntry\x12\x10\n\x03key\x18\
-    \x01\x20\x01(\tR\x03key\x12\x14\n\x05value\x18\x02\x20\x01(\tR\x05value:\
-    \x028\x01\"\xa8\x03\n\x15FieldChangesetPayload\x12\x19\n\x08field_id\x18\
-    \x01\x20\x01(\tR\x07fieldId\x12\x17\n\x07grid_id\x18\x02\x20\x01(\tR\x06\
-    gridId\x12\x14\n\x04name\x18\x03\x20\x01(\tH\0R\x04name\x12\x14\n\x04des\
-    c\x18\x04\x20\x01(\tH\x01R\x04desc\x12+\n\nfield_type\x18\x05\x20\x01(\
+    s\x12&\n\x06blocks\x18\x03\x20\x03(\x0b2\x0e.GridBlockMetaR\x06blocks\"o\
+    \n\rGridBlockMeta\x12\x19\n\x08block_id\x18\x01\x20\x01(\tR\x07blockId\
+    \x12&\n\x0fstart_row_index\x18\x02\x20\x01(\x05R\rstartRowIndex\x12\x1b\
+    \n\trow_count\x18\x03\x20\x01(\x05R\x08rowCount\"L\n\x11GridBlockMetaDat\
+    a\x12\x19\n\x08block_id\x18\x01\x20\x01(\tR\x07blockId\x12\x1c\n\x04rows\
+    \x18\x02\x20\x03(\x0b2\x08.RowMetaR\x04rows\"\xbc\x02\n\tFieldMeta\x12\
+    \x0e\n\x02id\x18\x01\x20\x01(\tR\x02id\x12\x12\n\x04name\x18\x02\x20\x01\
+    (\tR\x04name\x12\x12\n\x04desc\x18\x03\x20\x01(\tR\x04desc\x12)\n\nfield\
+    _type\x18\x04\x20\x01(\x0e2\n.FieldTypeR\tfieldType\x12\x16\n\x06frozen\
+    \x18\x05\x20\x01(\x08R\x06frozen\x12\x1e\n\nvisibility\x18\x06\x20\x01(\
+    \x08R\nvisibility\x12\x14\n\x05width\x18\x07\x20\x01(\x05R\x05width\x12>\
+    \n\x0ctype_options\x18\x08\x20\x03(\x0b2\x1b.FieldMeta.TypeOptionsEntryR\
+    \x0btypeOptions\x1a>\n\x10TypeOptionsEntry\x12\x10\n\x03key\x18\x01\x20\
+    \x01(\tR\x03key\x12\x14\n\x05value\x18\x02\x20\x01(\tR\x05value:\x028\
+    \x01\"\xa8\x03\n\x15FieldChangesetPayload\x12\x19\n\x08field_id\x18\x01\
+    \x20\x01(\tR\x07fieldId\x12\x17\n\x07grid_id\x18\x02\x20\x01(\tR\x06grid\
+    Id\x12\x14\n\x04name\x18\x03\x20\x01(\tH\0R\x04name\x12\x14\n\x04desc\
+    \x18\x04\x20\x01(\tH\x01R\x04desc\x12+\n\nfield_type\x18\x05\x20\x01(\
     \x0e2\n.FieldTypeH\x02R\tfieldType\x12\x18\n\x06frozen\x18\x06\x20\x01(\
     \x08H\x03R\x06frozen\x12\x20\n\nvisibility\x18\x07\x20\x01(\x08H\x04R\nv\
     isibility\x12\x16\n\x05width\x18\x08\x20\x01(\x05H\x05R\x05width\x12*\n\
@@ -3498,14 +3498,13 @@ static file_descriptor_proto_data: &'static [u8] = b"\
     \x01\n\x11CellMetaChangeset\x12\x17\n\x07grid_id\x18\x01\x20\x01(\tR\x06\
     gridId\x12\x15\n\x06row_id\x18\x02\x20\x01(\tR\x05rowId\x12\x19\n\x08fie\
     ld_id\x18\x03\x20\x01(\tR\x07fieldId\x12\x14\n\x04data\x18\x04\x20\x01(\
-    \tH\0R\x04dataB\r\n\x0bone_of_data\"\xac\x01\n\x10BuildGridContext\x12+\
-    \n\x0bfield_metas\x18\x01\x20\x03(\x0b2\n.FieldMetaR\nfieldMetas\x12/\n\
-    \x0bblock_metas\x18\x02\x20\x01(\x0b2\x0e.GridBlockMetaR\nblockMetas\x12\
-    :\n\x0fblock_meta_data\x18\x03\x20\x01(\x0b2\x12.GridBlockMetaDataR\rblo\
-    ckMetaData*d\n\tFieldType\x12\x0c\n\x08RichText\x10\0\x12\n\n\x06Number\
-    \x10\x01\x12\x0c\n\x08DateTime\x10\x02\x12\x10\n\x0cSingleSelect\x10\x03\
-    \x12\x0f\n\x0bMultiSelect\x10\x04\x12\x0c\n\x08Checkbox\x10\x05b\x06prot\
-    o3\
+    \tH\0R\x04dataB\r\n\x0bone_of_data\"\xaa\x01\n\x10BuildGridContext\x12+\
+    \n\x0bfield_metas\x18\x01\x20\x03(\x0b2\n.FieldMetaR\nfieldMetas\x12-\n\
+    \nblock_meta\x18\x02\x20\x01(\x0b2\x0e.GridBlockMetaR\tblockMeta\x12:\n\
+    \x0fblock_meta_data\x18\x03\x20\x01(\x0b2\x12.GridBlockMetaDataR\rblockM\
+    etaData*d\n\tFieldType\x12\x0c\n\x08RichText\x10\0\x12\n\n\x06Number\x10\
+    \x01\x12\x0c\n\x08DateTime\x10\x02\x12\x10\n\x0cSingleSelect\x10\x03\x12\
+    \x0f\n\x0bMultiSelect\x10\x04\x12\x0c\n\x08Checkbox\x10\x05b\x06proto3\
 ";
 
 static file_descriptor_proto_lazy: ::protobuf::rt::LazyV2<::protobuf::descriptor::FileDescriptorProto> = ::protobuf::rt::LazyV2::INIT;

+ 11 - 0
shared-lib/flowy-grid-data-model/src/protobuf/proto/grid.proto

@@ -57,6 +57,17 @@ message RepeatedGridBlock {
 }
 message GridBlockOrder {
     string block_id = 1;
+    repeated RowOrder row_orders = 2;
+}
+message GridBlockOrderChangeset {
+    string block_id = 1;
+    repeated IndexRowOrder inserted_rows = 2;
+    repeated RowOrder deleted_rows = 3;
+    repeated RowOrder updated_rows = 4;
+}
+message IndexRowOrder {
+    RowOrder row_order = 1;
+    oneof one_of_index { int32 index = 2; };
 }
 message GridBlock {
     string id = 1;

+ 3 - 3
shared-lib/flowy-grid-data-model/src/protobuf/proto/meta.proto

@@ -3,7 +3,7 @@ syntax = "proto3";
 message GridMeta {
     string grid_id = 1;
     repeated FieldMeta fields = 2;
-    repeated GridBlockMeta block_metas = 3;
+    repeated GridBlockMeta blocks = 3;
 }
 message GridBlockMeta {
     string block_id = 1;
@@ -12,7 +12,7 @@ message GridBlockMeta {
 }
 message GridBlockMetaData {
     string block_id = 1;
-    repeated RowMeta row_metas = 2;
+    repeated RowMeta rows = 2;
 }
 message FieldMeta {
     string id = 1;
@@ -63,7 +63,7 @@ message CellMetaChangeset {
 }
 message BuildGridContext {
     repeated FieldMeta field_metas = 1;
-    GridBlockMeta block_metas = 2;
+    GridBlockMeta block_meta = 2;
     GridBlockMetaData block_meta_data = 3;
 }
 enum FieldType {

+ 2 - 20
shared-lib/flowy-grid-data-model/tests/serde_test.rs

@@ -1,34 +1,16 @@
 use flowy_grid_data_model::entities::*;
 
-#[test]
-fn grid_serde_test() {
-    let grid_id = "1".to_owned();
-    let fields = vec![create_field("1")];
-    let grid = GridMeta {
-        grid_id,
-        fields,
-        block_metas: vec![],
-    };
-
-    let grid_1_json = serde_json::to_string(&grid).unwrap();
-    let _: GridMeta = serde_json::from_str(&grid_1_json).unwrap();
-    assert_eq!(
-        grid_1_json,
-        r#"{"id":"1","fields":[{"id":"1","name":"Text Field","desc":"","field_type":"RichText","frozen":false,"visibility":true,"width":150,"type_options":{"type_id":"","value":[]}}],"blocks":[]}"#
-    )
-}
-
 #[test]
 fn grid_default_serde_test() {
     let grid_id = "1".to_owned();
     let grid = GridMeta {
         grid_id,
         fields: vec![],
-        block_metas: vec![],
+        blocks: vec![],
     };
 
     let json = serde_json::to_string(&grid).unwrap();
-    assert_eq!(json, r#"{"id":"1","fields":[],"blocks":[]}"#)
+    assert_eq!(json, r#"{"grid_id":"1","fields":[],"blocks":[]}"#)
 }
 
 fn create_field(field_id: &str) -> FieldMeta {

+ 62 - 55
shared-lib/flowy-sync/src/client_grid/grid_block_meta_pad.rs

@@ -5,6 +5,7 @@ use flowy_grid_data_model::entities::{CellMeta, GridBlockMetaData, RowMeta, RowM
 use lib_infra::uuid;
 use lib_ot::core::{OperationTransformable, PlainTextAttributes, PlainTextDelta, PlainTextDeltaBuilder};
 use serde::{Deserialize, Serialize};
+use std::borrow::Cow;
 
 use std::collections::HashMap;
 use std::sync::Arc;
@@ -15,7 +16,7 @@ pub type GridBlockMetaDeltaBuilder = PlainTextDeltaBuilder;
 #[derive(Debug, Deserialize, Serialize, Clone)]
 pub struct GridBlockMetaPad {
     block_id: String,
-    row_metas: Vec<Arc<RowMeta>>,
+    rows: Vec<Arc<RowMeta>>,
 
     #[serde(skip)]
     pub(crate) delta: GridBlockMetaDelta,
@@ -30,16 +31,8 @@ impl GridBlockMetaPad {
             CollaborateError::internal().context(msg)
         })?;
         let block_id = meta_data.block_id;
-        let rows = meta_data
-            .row_metas
-            .into_iter()
-            .map(Arc::new)
-            .collect::<Vec<Arc<RowMeta>>>();
-        Ok(Self {
-            block_id,
-            row_metas: rows,
-            delta,
-        })
+        let rows = meta_data.rows.into_iter().map(Arc::new).collect::<Vec<Arc<RowMeta>>>();
+        Ok(Self { block_id, rows, delta })
     }
 
     pub fn from_revisions(_grid_id: &str, revisions: Vec<Revision>) -> CollaborateResult<Self> {
@@ -55,14 +48,11 @@ impl GridBlockMetaPad {
     ) -> CollaborateResult<Option<GridBlockMetaChange>> {
         self.modify(|rows| {
             if let Some(start_row_id) = start_row_id {
-                if start_row_id.is_empty() {
-                    rows.insert(0, Arc::new(row));
-                    return Ok(Some(()));
-                }
-
-                if let Some(index) = rows.iter().position(|row| row.id == start_row_id) {
-                    rows.insert(index + 1, Arc::new(row));
-                    return Ok(Some(()));
+                if !start_row_id.is_empty() {
+                    if let Some(index) = rows.iter().position(|row| row.id == start_row_id) {
+                        rows.insert(index + 1, Arc::new(row));
+                        return Ok(Some(()));
+                    }
                 }
             }
 
@@ -71,38 +61,48 @@ impl GridBlockMetaPad {
         })
     }
 
-    pub fn delete_rows(&mut self, row_ids: &[String]) -> CollaborateResult<Option<GridBlockMetaChange>> {
+    pub fn delete_rows(&mut self, row_ids: Vec<Cow<'_, String>>) -> CollaborateResult<Option<GridBlockMetaChange>> {
         self.modify(|rows| {
-            rows.retain(|row| !row_ids.contains(&row.id));
+            rows.retain(|row| !row_ids.contains(&Cow::Borrowed(&row.id)));
             Ok(Some(()))
         })
     }
 
-    pub fn get_row_metas(&self, row_ids: &Option<Vec<String>>) -> CollaborateResult<Vec<Arc<RowMeta>>> {
+    pub fn get_row_metas<T>(&self, row_ids: Option<Vec<Cow<'_, T>>>) -> CollaborateResult<Vec<Arc<RowMeta>>>
+    where
+        T: AsRef<str> + ToOwned + ?Sized,
+    {
         match row_ids {
-            None => Ok(self.row_metas.to_vec()),
+            None => Ok(self.rows.to_vec()),
             Some(row_ids) => {
                 let row_map = self
-                    .row_metas
+                    .rows
                     .iter()
-                    .map(|row| (&row.id, row.clone()))
-                    .collect::<HashMap<&String, Arc<RowMeta>>>();
+                    .map(|row| (row.id.as_str(), row.clone()))
+                    .collect::<HashMap<&str, Arc<RowMeta>>>();
 
                 Ok(row_ids
                     .iter()
-                    .flat_map(|row_id| match row_map.get(row_id) {
-                        None => {
-                            tracing::error!("Can't find the row with id: {}", row_id);
-                            None
+                    .flat_map(|row_id| {
+                        let row_id = row_id.as_ref().as_ref();
+                        match row_map.get(row_id) {
+                            None => {
+                                tracing::error!("Can't find the row with id: {}", row_id);
+                                None
+                            }
+                            Some(row) => Some(row.clone()),
                         }
-                        Some(row) => Some(row.clone()),
                     })
                     .collect::<Vec<_>>())
             }
         }
     }
 
-    pub fn get_cell_metas(&self, field_id: &str, row_ids: &Option<Vec<String>>) -> CollaborateResult<Vec<CellMeta>> {
+    pub fn get_cell_metas(
+        &self,
+        field_id: &str,
+        row_ids: Option<Vec<Cow<'_, String>>>,
+    ) -> CollaborateResult<Vec<CellMeta>> {
         let rows = self.get_row_metas(row_ids)?;
         let cell_metas = rows
             .iter()
@@ -115,7 +115,14 @@ impl GridBlockMetaPad {
     }
 
     pub fn number_of_rows(&self) -> i32 {
-        self.row_metas.len() as i32
+        self.rows.len() as i32
+    }
+
+    pub fn index_of_row(&self, row_id: &str) -> Option<i32> {
+        self.rows
+            .iter()
+            .position(|row| row.id == row_id)
+            .map(|index| index as i32)
     }
 
     pub fn update_row(&mut self, changeset: RowMetaChangeset) -> CollaborateResult<Option<GridBlockMetaChange>> {
@@ -148,7 +155,7 @@ impl GridBlockMetaPad {
         F: for<'a> FnOnce(&'a mut Vec<Arc<RowMeta>>) -> CollaborateResult<Option<()>>,
     {
         let cloned_self = self.clone();
-        match f(&mut self.row_metas)? {
+        match f(&mut self.rows)? {
             None => Ok(None),
             Some(_) => {
                 let old = cloned_self.to_json()?;
@@ -215,13 +222,13 @@ impl std::default::Default for GridBlockMetaPad {
     fn default() -> Self {
         let block_meta_data = GridBlockMetaData {
             block_id: uuid(),
-            row_metas: vec![],
+            rows: vec![],
         };
 
         let delta = make_block_meta_delta(&block_meta_data);
         GridBlockMetaPad {
             block_id: block_meta_data.block_id,
-            row_metas: block_meta_data.row_metas.into_iter().map(Arc::new).collect::<Vec<_>>(),
+            rows: block_meta_data.rows.into_iter().map(Arc::new).collect::<Vec<_>>(),
             delta,
         }
     }
@@ -231,6 +238,7 @@ impl std::default::Default for GridBlockMetaPad {
 mod tests {
     use crate::client_grid::{GridBlockMetaDelta, GridBlockMetaPad};
     use flowy_grid_data_model::entities::{RowMeta, RowMetaChangeset};
+    use std::borrow::Cow;
 
     #[test]
     fn block_meta_add_row() {
@@ -246,7 +254,7 @@ mod tests {
         let change = pad.add_row_meta(row, None).unwrap().unwrap();
         assert_eq!(
             change.delta.to_delta_str(),
-            r#"[{"retain":29},{"insert":"{\"id\":\"1\",\"block_id\":\"1\",\"cell_by_field_id\":{},\"height\":0,\"visibility\":false}"},{"retain":2}]"#
+            r#"[{"retain":24},{"insert":"{\"id\":\"1\",\"block_id\":\"1\",\"cells\":{},\"height\":0,\"visibility\":false}"},{"retain":2}]"#
         );
     }
 
@@ -260,24 +268,24 @@ mod tests {
         let change = pad.add_row_meta(row_1.clone(), None).unwrap().unwrap();
         assert_eq!(
             change.delta.to_delta_str(),
-            r#"[{"retain":29},{"insert":"{\"id\":\"1\",\"block_id\":\"1\",\"cell_by_field_id\":{},\"height\":0,\"visibility\":false}"},{"retain":2}]"#
+            r#"[{"retain":24},{"insert":"{\"id\":\"1\",\"block_id\":\"1\",\"cells\":{},\"height\":0,\"visibility\":false}"},{"retain":2}]"#
         );
 
         let change = pad.add_row_meta(row_2.clone(), None).unwrap().unwrap();
         assert_eq!(
             change.delta.to_delta_str(),
-            r#"[{"retain":106},{"insert":",{\"id\":\"2\",\"block_id\":\"1\",\"cell_by_field_id\":{},\"height\":0,\"visibility\":false}"},{"retain":2}]"#
+            r#"[{"retain":90},{"insert":",{\"id\":\"2\",\"block_id\":\"1\",\"cells\":{},\"height\":0,\"visibility\":false}"},{"retain":2}]"#
         );
 
         let change = pad.add_row_meta(row_3.clone(), Some("2".to_string())).unwrap().unwrap();
         assert_eq!(
             change.delta.to_delta_str(),
-            r#"[{"retain":114},{"insert":"3\",\"block_id\":\"1\",\"cell_by_field_id\":{},\"height\":0,\"visibility\":false},{\"id\":\""},{"retain":72}]"#
+            r#"[{"retain":157},{"insert":",{\"id\":\"3\",\"block_id\":\"1\",\"cells\":{},\"height\":0,\"visibility\":false}"},{"retain":2}]"#
         );
 
-        assert_eq!(*pad.row_metas[0], row_1);
-        assert_eq!(*pad.row_metas[1], row_3);
-        assert_eq!(*pad.row_metas[2], row_2);
+        assert_eq!(*pad.rows[0], row_1);
+        assert_eq!(*pad.rows[1], row_2);
+        assert_eq!(*pad.rows[2], row_3);
     }
 
     fn test_row_meta(id: &str, pad: &GridBlockMetaPad) -> RowMeta {
@@ -301,9 +309,9 @@ mod tests {
         let _ = pad.add_row_meta(row_2.clone(), None).unwrap().unwrap();
         let _ = pad.add_row_meta(row_3.clone(), Some("1".to_string())).unwrap().unwrap();
 
-        assert_eq!(*pad.row_metas[0], row_3);
-        assert_eq!(*pad.row_metas[1], row_1);
-        assert_eq!(*pad.row_metas[2], row_2);
+        assert_eq!(*pad.rows[0], row_1);
+        assert_eq!(*pad.rows[1], row_3);
+        assert_eq!(*pad.rows[2], row_2);
     }
 
     #[test]
@@ -317,9 +325,9 @@ mod tests {
         let _ = pad.add_row_meta(row_2.clone(), None).unwrap().unwrap();
         let _ = pad.add_row_meta(row_3.clone(), Some("".to_string())).unwrap().unwrap();
 
-        assert_eq!(*pad.row_metas[0], row_3);
-        assert_eq!(*pad.row_metas[1], row_1);
-        assert_eq!(*pad.row_metas[2], row_2);
+        assert_eq!(*pad.rows[0], row_1);
+        assert_eq!(*pad.rows[1], row_2);
+        assert_eq!(*pad.rows[2], row_3);
     }
 
     #[test]
@@ -335,10 +343,10 @@ mod tests {
         };
 
         let _ = pad.add_row_meta(row.clone(), None).unwrap().unwrap();
-        let change = pad.delete_rows(&[row.id]).unwrap().unwrap();
+        let change = pad.delete_rows(vec![Cow::Borrowed(&row.id)]).unwrap().unwrap();
         assert_eq!(
             change.delta.to_delta_str(),
-            r#"[{"retain":29},{"delete":77},{"retain":2}]"#
+            r#"[{"retain":24},{"delete":66},{"retain":2}]"#
         );
 
         assert_eq!(pad.delta_str(), pre_delta_str);
@@ -367,18 +375,17 @@ mod tests {
 
         assert_eq!(
             change.delta.to_delta_str(),
-            r#"[{"retain":85},{"insert":"10"},{"retain":15},{"insert":"tru"},{"delete":4},{"retain":4}]"#
+            r#"[{"retain":69},{"insert":"10"},{"retain":15},{"insert":"tru"},{"delete":4},{"retain":4}]"#
         );
 
         assert_eq!(
             pad.to_json().unwrap(),
-            r#"{"block_id":"1","row_metas":[{"id":"1","block_id":"1","cell_by_field_id":{},"height":100,"visibility":true}]}"#
+            r#"{"block_id":"1","rows":[{"id":"1","block_id":"1","cells":{},"height":100,"visibility":true}]}"#
         );
     }
 
     fn test_pad() -> GridBlockMetaPad {
-        let delta =
-            GridBlockMetaDelta::from_delta_str(r#"[{"insert":"{\"block_id\":\"1\",\"row_metas\":[]}"}]"#).unwrap();
+        let delta = GridBlockMetaDelta::from_delta_str(r#"[{"insert":"{\"block_id\":\"1\",\"rows\":[]}"}]"#).unwrap();
         GridBlockMetaPad::from_delta(delta).unwrap()
     }
 }

+ 4 - 4
shared-lib/flowy-sync/src/client_grid/grid_builder.rs

@@ -13,9 +13,9 @@ impl GridBuilder {
     }
 
     pub fn add_empty_row(mut self) -> Self {
-        let row = RowMeta::new(&self.build_context.block_metas.block_id);
-        self.build_context.block_meta_data.row_metas.push(row);
-        self.build_context.block_metas.row_count += 1;
+        let row = RowMeta::new(&self.build_context.block_meta.block_id);
+        self.build_context.block_meta_data.rows.push(row);
+        self.build_context.block_meta.row_count += 1;
         self
     }
 
@@ -57,7 +57,7 @@ mod tests {
         let grid_meta = GridMeta {
             grid_id,
             fields: build_context.field_metas,
-            block_metas: vec![build_context.block_metas],
+            blocks: vec![build_context.block_meta],
         };
 
         let grid_meta_delta = make_grid_delta(&grid_meta);

+ 11 - 15
shared-lib/flowy-sync/src/client_grid/grid_meta_pad.rs

@@ -235,12 +235,12 @@ impl GridMetaPad {
 
     pub fn create_block_meta(&mut self, block: GridBlockMeta) -> CollaborateResult<Option<GridChangeset>> {
         self.modify_grid(|grid_meta| {
-            if grid_meta.block_metas.iter().any(|b| b.block_id == block.block_id) {
+            if grid_meta.blocks.iter().any(|b| b.block_id == block.block_id) {
                 tracing::warn!("Duplicate grid block");
                 Ok(None)
             } else {
-                match grid_meta.block_metas.last() {
-                    None => grid_meta.block_metas.push(block),
+                match grid_meta.blocks.last() {
+                    None => grid_meta.blocks.push(block),
                     Some(last_block) => {
                         if last_block.start_row_index > block.start_row_index
                             && last_block.len() > block.start_row_index
@@ -248,7 +248,7 @@ impl GridMetaPad {
                             let msg = "GridBlock's start_row_index should be greater than the last_block's start_row_index and its len".to_string();
                             return Err(CollaborateError::internal().context(msg))
                         }
-                        grid_meta.block_metas.push(block);
+                        grid_meta.blocks.push(block);
                     }
                 }
                 Ok(Some(()))
@@ -257,7 +257,7 @@ impl GridMetaPad {
     }
 
     pub fn get_block_metas(&self) -> Vec<GridBlockMeta> {
-        self.grid_meta.block_metas.clone()
+        self.grid_meta.blocks.clone()
     }
 
     pub fn update_block_meta(&mut self, changeset: GridBlockMetaChangeset) -> CollaborateResult<Option<GridChangeset>> {
@@ -320,19 +320,15 @@ impl GridMetaPad {
     where
         F: FnOnce(&mut GridBlockMeta) -> CollaborateResult<Option<()>>,
     {
-        self.modify_grid(|grid_meta| {
-            match grid_meta
-                .block_metas
-                .iter()
-                .position(|block| block.block_id == block_id)
-            {
+        self.modify_grid(
+            |grid_meta| match grid_meta.blocks.iter().position(|block| block.block_id == block_id) {
                 None => {
                     tracing::warn!("[GridMetaPad]: Can't find any block with id: {}", block_id);
                     Ok(None)
                 }
-                Some(index) => f(&mut grid_meta.block_metas[index]),
-            }
-        })
+                Some(index) => f(&mut grid_meta.blocks[index]),
+            },
+        )
     }
 
     pub fn modify_field<F>(&mut self, field_id: &str, f: F) -> CollaborateResult<Option<GridChangeset>>
@@ -380,7 +376,7 @@ impl std::default::Default for GridMetaPad {
         let grid = GridMeta {
             grid_id: uuid(),
             fields: vec![],
-            block_metas: vec![],
+            blocks: vec![],
         };
         let delta = make_grid_delta(&grid);
         GridMetaPad {