Browse Source

chore: config notification of grid

appflowy 3 years ago
parent
commit
1237962ab2
28 changed files with 763 additions and 81 deletions
  1. 18 2
      frontend/app_flowy/lib/core/notification_helper.dart
  2. 3 4
      frontend/app_flowy/lib/startup/home_deps_resolver.dart
  3. 21 2
      frontend/app_flowy/lib/workspace/application/grid/grid_bloc.dart
  4. 60 0
      frontend/app_flowy/lib/workspace/application/grid/grid_listener.dart
  5. 6 6
      frontend/app_flowy/lib/workspace/application/view/view_listener.dart
  6. 2 1
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/grid_page.dart
  7. 41 0
      frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid-data-model/grid.pb.dart
  8. 10 0
      frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid-data-model/grid.pbjson.dart
  9. 11 0
      frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid/dart_notification.pb.dart
  10. 34 0
      frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid/dart_notification.pbenum.dart
  11. 25 0
      frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid/dart_notification.pbjson.dart
  12. 9 0
      frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid/dart_notification.pbserver.dart
  13. 1 0
      frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid/protobuf.dart
  14. 1 1
      frontend/rust-lib/flowy-grid/Flowy.toml
  15. 36 0
      frontend/rust-lib/flowy-grid/src/dart_notification.rs
  16. 3 3
      frontend/rust-lib/flowy-grid/src/event_handler.rs
  17. 1 0
      frontend/rust-lib/flowy-grid/src/lib.rs
  18. 106 0
      frontend/rust-lib/flowy-grid/src/protobuf/model/dart_notification.rs
  19. 3 0
      frontend/rust-lib/flowy-grid/src/protobuf/model/mod.rs
  20. 10 0
      frontend/rust-lib/flowy-grid/src/protobuf/proto/dart_notification.proto
  21. 104 23
      frontend/rust-lib/flowy-grid/src/services/block_meta_editor.rs
  22. 34 14
      frontend/rust-lib/flowy-grid/src/services/grid_editor.rs
  23. 8 4
      frontend/rust-lib/flowy-grid/src/services/row/row_loader.rs
  24. 1 1
      frontend/rust-lib/flowy-grid/tests/grid/script.rs
  25. 10 10
      shared-lib/flowy-collaboration/src/client_grid/grid_meta_pad.rs
  26. 25 0
      shared-lib/flowy-grid-data-model/src/entities/grid.rs
  27. 177 10
      shared-lib/flowy-grid-data-model/src/protobuf/model/grid.rs
  28. 3 0
      shared-lib/flowy-grid-data-model/src/protobuf/proto/grid.proto

+ 18 - 2
frontend/app_flowy/lib/core/notification_helper.dart

@@ -4,7 +4,9 @@ import 'package:flowy_sdk/protobuf/flowy-user/protobuf.dart';
 import 'package:dartz/dartz.dart';
 import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-folder/dart_notification.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/dart_notification.pb.dart';
 
+// User
 typedef UserNotificationCallback = void Function(UserNotification, Either<Uint8List, FlowyError>);
 
 class UserNotificationParser extends NotificationParser<UserNotification, FlowyError> {
@@ -17,10 +19,11 @@ class UserNotificationParser extends NotificationParser<UserNotification, FlowyE
         );
 }
 
-typedef NotificationCallback = void Function(FolderNotification, Either<Uint8List, FlowyError>);
+// Folder
+typedef FolderNotificationCallback = void Function(FolderNotification, Either<Uint8List, FlowyError>);
 
 class FolderNotificationParser extends NotificationParser<FolderNotification, FlowyError> {
-  FolderNotificationParser({String? id, required NotificationCallback callback})
+  FolderNotificationParser({String? id, required FolderNotificationCallback callback})
       : super(
           id: id,
           callback: callback,
@@ -29,6 +32,19 @@ class FolderNotificationParser extends NotificationParser<FolderNotification, Fl
         );
 }
 
+// Grid
+typedef GridNotificationCallback = void Function(GridNotification, Either<Uint8List, FlowyError>);
+
+class GridNotificationParser extends NotificationParser<GridNotification, FlowyError> {
+  GridNotificationParser({String? id, required GridNotificationCallback callback})
+      : super(
+          id: id,
+          callback: callback,
+          tyParser: (ty) => GridNotification.valueOf(ty),
+          errorParser: (bytes) => FlowyError.fromBuffer(bytes),
+        );
+}
+
 class NotificationParser<T, E> {
   String? id;
   void Function(T, Either<Uint8List, E>) callback;

+ 3 - 4
frontend/app_flowy/lib/startup/home_deps_resolver.dart

@@ -16,6 +16,8 @@ import 'package:flowy_sdk/protobuf/flowy-grid-data-model/grid.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-user-data-model/user_profile.pb.dart';
 import 'package:get_it/get_it.dart';
 
+import '../workspace/application/grid/grid_listener.dart';
+
 class HomeDepsResolver {
   static Future<void> resolve(GetIt getIt) async {
     getIt.registerFactoryParam<UserListener, UserProfile, void>(
@@ -90,10 +92,7 @@ class HomeDepsResolver {
 
     // Grid
     getIt.registerFactoryParam<GridBloc, View, void>(
-      (view, _) => GridBloc(
-        view: view,
-        service: GridService(),
-      ),
+      (view, _) => GridBloc(view: view, service: GridService(), listener: GridListener(gridId: view.id)),
     );
 
     getIt.registerFactoryParam<RowBloc, GridRowData, void>(

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

@@ -1,13 +1,14 @@
 import 'dart:async';
 
 import 'package:dartz/dartz.dart';
+import 'package:flowy_sdk/log.dart';
 import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-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 'data.dart';
+import 'grid_listener.dart';
 import 'grid_service.dart';
 
 part 'grid_bloc.freezed.dart';
@@ -15,14 +16,16 @@ part 'grid_bloc.freezed.dart';
 class GridBloc extends Bloc<GridEvent, GridState> {
   final GridService service;
   final View view;
+  final GridListener listener;
   Grid? _grid;
   List<Field>? _fields;
 
-  GridBloc({required this.view, required this.service}) : super(GridState.initial()) {
+  GridBloc({required this.view, required this.service, required this.listener}) : super(GridState.initial()) {
     on<GridEvent>(
       (event, emit) async {
         await event.map(
           initial: (InitialGrid value) async {
+            await _startGridListening();
             await _loadGrid(emit);
             await _loadFields(emit);
             await _loadGridInfo(emit);
@@ -40,9 +43,25 @@ class GridBloc extends Bloc<GridEvent, GridState> {
 
   @override
   Future<void> close() async {
+    await listener.close();
     return super.close();
   }
 
+  Future<void> _startGridListening() async {
+    listener.createRowNotifier.addPublishListener((result) {
+      result.fold((row) {
+        //
+        Log.info("$row");
+      }, (err) => null);
+    });
+
+    listener.deleteRowNotifier.addPublishListener((result) {
+      result.fold((l) => null, (r) => null);
+    });
+
+    listener.start();
+  }
+
   Future<void> _loadGrid(Emitter<GridState> emit) async {
     final result = await service.openGrid(gridId: view.id);
     result.fold(

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

@@ -0,0 +1,60 @@
+import 'package:flowy_sdk/protobuf/dart-notify/subject.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid-data-model/grid.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/dart_notification.pb.dart';
+import 'package:flowy_sdk/rust_stream.dart';
+import 'package:flowy_infra/notifier.dart';
+import 'dart:async';
+import 'dart:typed_data';
+import 'package:app_flowy/core/notification_helper.dart';
+import 'package:dartz/dartz.dart';
+
+typedef CreateRowNotifiedValue = Either<RepeatedRow, FlowyError>;
+typedef DeleteRowNotifierValue = Either<RepeatedRow, FlowyError>;
+
+class GridListener {
+  final String gridId;
+  PublishNotifier<CreateRowNotifiedValue> createRowNotifier = PublishNotifier<CreateRowNotifiedValue>();
+  PublishNotifier<DeleteRowNotifierValue> deleteRowNotifier = PublishNotifier<DeleteRowNotifierValue>();
+  StreamSubscription<SubscribeObject>? _subscription;
+  late GridNotificationParser _parser;
+
+  GridListener({required this.gridId});
+
+  void start() {
+    _parser = GridNotificationParser(
+      id: gridId,
+      callback: (ty, result) {
+        _handleObservableType(ty, result);
+      },
+    );
+
+    _subscription = RustStreamReceiver.listen((observable) => _parser.parse(observable));
+  }
+
+  void _handleObservableType(GridNotification ty, Either<Uint8List, FlowyError> result) {
+    switch (ty) {
+      case GridNotification.GridDidCreateRows:
+        result.fold(
+          (payload) => createRowNotifier.value = left(RepeatedRow.fromBuffer(payload)),
+          (error) => createRowNotifier.value = right(error),
+        );
+        break;
+      case GridNotification.GridDidDeleteRow:
+        result.fold(
+          (payload) => deleteRowNotifier.value = left(RepeatedRow.fromBuffer(payload)),
+          (error) => deleteRowNotifier.value = right(error),
+        );
+        break;
+
+      default:
+        break;
+    }
+  }
+
+  Future<void> close() async {
+    await _subscription?.cancel();
+    createRowNotifier.dispose();
+    deleteRowNotifier.dispose();
+  }
+}

+ 6 - 6
frontend/app_flowy/lib/workspace/application/view/view_listener.dart

@@ -9,15 +9,15 @@ import 'package:flowy_sdk/protobuf/flowy-folder/dart_notification.pb.dart';
 import 'package:flowy_sdk/rust_stream.dart';
 import 'package:flowy_infra/notifier.dart';
 
-typedef DeleteNotifierValue = Either<View, FlowyError>;
-typedef UpdateNotifierValue = Either<View, FlowyError>;
-typedef RestoreNotifierValue = Either<View, FlowyError>;
+typedef DeleteViewNotifyValue = Either<View, FlowyError>;
+typedef UpdateViewNotifiedValue = Either<View, FlowyError>;
+typedef RestoreViewNotifiedValue = Either<View, FlowyError>;
 
 class ViewListener {
   StreamSubscription<SubscribeObject>? _subscription;
-  PublishNotifier<UpdateNotifierValue> updatedNotifier = PublishNotifier<UpdateNotifierValue>();
-  PublishNotifier<DeleteNotifierValue> deletedNotifier = PublishNotifier<DeleteNotifierValue>();
-  PublishNotifier<RestoreNotifierValue> restoredNotifier = PublishNotifier<RestoreNotifierValue>();
+  PublishNotifier<UpdateViewNotifiedValue> updatedNotifier = PublishNotifier<UpdateViewNotifiedValue>();
+  PublishNotifier<DeleteViewNotifyValue> deletedNotifier = PublishNotifier<DeleteViewNotifyValue>();
+  PublishNotifier<RestoreViewNotifiedValue> restoredNotifier = PublishNotifier<RestoreViewNotifiedValue>();
   late FolderNotificationParser _parser;
   View view;
 

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

@@ -139,9 +139,10 @@ class _GridBodyState extends State<GridBody> {
       delegate: SliverChildBuilderDelegate(
         (context, index) {
           final data = gridInfo.rowAtIndex(index);
-          return RepaintBoundary(child: GridRowWidget(data: data));
+          return GridRowWidget(data: data);
         },
         childCount: gridInfo.numberOfRows(),
+        addRepaintBoundaries: true,
       ),
     );
   }

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

@@ -607,6 +607,47 @@ class Cell extends $pb.GeneratedMessage {
   void clearContent() => clearField(2);
 }
 
+class RepeatedCell extends $pb.GeneratedMessage {
+  static final $pb.BuilderInfo _i = $pb.BuilderInfo(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'RepeatedCell', createEmptyInstance: create)
+    ..pc<Cell>(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'items', $pb.PbFieldType.PM, subBuilder: Cell.create)
+    ..hasRequiredFields = false
+  ;
+
+  RepeatedCell._() : super();
+  factory RepeatedCell({
+    $core.Iterable<Cell>? items,
+  }) {
+    final _result = create();
+    if (items != null) {
+      _result.items.addAll(items);
+    }
+    return _result;
+  }
+  factory RepeatedCell.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
+  factory RepeatedCell.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')
+  RepeatedCell clone() => RepeatedCell()..mergeFromMessage(this);
+  @$core.Deprecated(
+  'Using this can add significant overhead to your binary. '
+  'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
+  'Will be removed in next major version')
+  RepeatedCell copyWith(void Function(RepeatedCell) updates) => super.copyWith((message) => updates(message as RepeatedCell)) as RepeatedCell; // ignore: deprecated_member_use
+  $pb.BuilderInfo get info_ => _i;
+  @$core.pragma('dart2js:noInline')
+  static RepeatedCell create() => RepeatedCell._();
+  RepeatedCell createEmptyInstance() => create();
+  static $pb.PbList<RepeatedCell> createRepeated() => $pb.PbList<RepeatedCell>();
+  @$core.pragma('dart2js:noInline')
+  static RepeatedCell getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<RepeatedCell>(create);
+  static RepeatedCell? _defaultInstance;
+
+  @$pb.TagNumber(1)
+  $core.List<Cell> get items => $_getList(0);
+}
+
 class CreateGridPayload extends $pb.GeneratedMessage {
   static final $pb.BuilderInfo _i = $pb.BuilderInfo(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'CreateGridPayload', createEmptyInstance: create)
     ..aOS(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'name')

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

@@ -131,6 +131,16 @@ const Cell$json = const {
 
 /// Descriptor for `Cell`. Decode as a `google.protobuf.DescriptorProto`.
 final $typed_data.Uint8List cellDescriptor = $convert.base64Decode('CgRDZWxsEhkKCGZpZWxkX2lkGAEgASgJUgdmaWVsZElkEhgKB2NvbnRlbnQYAiABKAlSB2NvbnRlbnQ=');
+@$core.Deprecated('Use repeatedCellDescriptor instead')
+const RepeatedCell$json = const {
+  '1': 'RepeatedCell',
+  '2': const [
+    const {'1': 'items', '3': 1, '4': 3, '5': 11, '6': '.Cell', '10': 'items'},
+  ],
+};
+
+/// Descriptor for `RepeatedCell`. Decode as a `google.protobuf.DescriptorProto`.
+final $typed_data.Uint8List repeatedCellDescriptor = $convert.base64Decode('CgxSZXBlYXRlZENlbGwSGwoFaXRlbXMYASADKAsyBS5DZWxsUgVpdGVtcw==');
 @$core.Deprecated('Use createGridPayloadDescriptor instead')
 const CreateGridPayload$json = const {
   '1': 'CreateGridPayload',

+ 11 - 0
frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid/dart_notification.pb.dart

@@ -0,0 +1,11 @@
+///
+//  Generated code. Do not modify.
+//  source: dart_notification.proto
+//
+// @dart = 2.12
+// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields
+
+import 'dart:core' as $core;
+
+export 'dart_notification.pbenum.dart';
+

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

@@ -0,0 +1,34 @@
+///
+//  Generated code. Do not modify.
+//  source: dart_notification.proto
+//
+// @dart = 2.12
+// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields
+
+// ignore_for_file: UNDEFINED_SHOWN_NAME
+import 'dart:core' as $core;
+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 GridDidCreateRows = GridNotification._(10, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'GridDidCreateRows');
+  static const GridNotification GridDidDeleteRow = GridNotification._(11, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'GridDidDeleteRow');
+  static const GridNotification GridDidUpdateRows = GridNotification._(12, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'GridDidUpdateRows');
+  static const GridNotification GridDidUpdateCells = GridNotification._(20, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'GridDidUpdateCells');
+  static const GridNotification GridDidUpdateFields = GridNotification._(30, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'GridDidUpdateFields');
+
+  static const $core.List<GridNotification> values = <GridNotification> [
+    Unknown,
+    GridDidCreateRows,
+    GridDidDeleteRow,
+    GridDidUpdateRows,
+    GridDidUpdateCells,
+    GridDidUpdateFields,
+  ];
+
+  static final $core.Map<$core.int, GridNotification> _byValue = $pb.ProtobufEnum.initByValue(values);
+  static GridNotification? valueOf($core.int value) => _byValue[value];
+
+  const GridNotification._($core.int v, $core.String n) : super(v, n);
+}
+

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

@@ -0,0 +1,25 @@
+///
+//  Generated code. Do not modify.
+//  source: dart_notification.proto
+//
+// @dart = 2.12
+// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields,deprecated_member_use_from_same_package
+
+import 'dart:core' as $core;
+import 'dart:convert' as $convert;
+import 'dart:typed_data' as $typed_data;
+@$core.Deprecated('Use gridNotificationDescriptor instead')
+const GridNotification$json = const {
+  '1': 'GridNotification',
+  '2': const [
+    const {'1': 'Unknown', '2': 0},
+    const {'1': 'GridDidCreateRows', '2': 10},
+    const {'1': 'GridDidDeleteRow', '2': 11},
+    const {'1': 'GridDidUpdateRows', '2': 12},
+    const {'1': 'GridDidUpdateCells', '2': 20},
+    const {'1': 'GridDidUpdateFields', '2': 30},
+  ],
+};
+
+/// Descriptor for `GridNotification`. Decode as a `google.protobuf.EnumDescriptorProto`.
+final $typed_data.Uint8List gridNotificationDescriptor = $convert.base64Decode('ChBHcmlkTm90aWZpY2F0aW9uEgsKB1Vua25vd24QABIVChFHcmlkRGlkQ3JlYXRlUm93cxAKEhQKEEdyaWREaWREZWxldGVSb3cQCxIVChFHcmlkRGlkVXBkYXRlUm93cxAMEhYKEkdyaWREaWRVcGRhdGVDZWxscxAUEhcKE0dyaWREaWRVcGRhdGVGaWVsZHMQHg==');

+ 9 - 0
frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid/dart_notification.pbserver.dart

@@ -0,0 +1,9 @@
+///
+//  Generated code. Do not modify.
+//  source: dart_notification.proto
+//
+// @dart = 2.12
+// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields,deprecated_member_use_from_same_package
+
+export 'dart_notification.pb.dart';
+

+ 1 - 0
frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid/protobuf.dart

@@ -1,6 +1,7 @@
 // Auto-generated, do not edit 
 export './date_description.pb.dart';
 export './text_description.pb.dart';
+export './dart_notification.pb.dart';
 export './checkbox_description.pb.dart';
 export './selection_description.pb.dart';
 export './event_map.pb.dart';

+ 1 - 1
frontend/rust-lib/flowy-grid/Flowy.toml

@@ -1,3 +1,3 @@
 
-proto_crates = ["src/event_map.rs", "src/services/cell/description"]
+proto_crates = ["src/event_map.rs", "src/services/cell/description", "src/dart_notification.rs"]
 event_files = ["src/event_map.rs"]

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

@@ -0,0 +1,36 @@
+use dart_notify::DartNotifyBuilder;
+use flowy_derive::ProtoBuf_Enum;
+const OBSERVABLE_CATEGORY: &str = "Grid";
+
+#[derive(ProtoBuf_Enum, Debug)]
+pub enum GridNotification {
+    Unknown = 0,
+    GridDidCreateRows = 10,
+    GridDidDeleteRow = 11,
+    GridDidUpdateRows = 12,
+
+    GridDidUpdateCells = 20,
+    GridDidUpdateFields = 30,
+}
+
+impl std::default::Default for GridNotification {
+    fn default() -> Self {
+        GridNotification::Unknown
+    }
+}
+
+impl std::convert::From<GridNotification> for i32 {
+    fn from(notification: GridNotification) -> Self {
+        notification as i32
+    }
+}
+
+#[tracing::instrument(level = "trace")]
+pub fn send_dart_notification(id: &str, ty: GridNotification) -> DartNotifyBuilder {
+    DartNotifyBuilder::new(id, ty, OBSERVABLE_CATEGORY)
+}
+
+#[tracing::instrument(level = "trace")]
+pub fn send_anonymous_dart_notification(ty: GridNotification) -> DartNotifyBuilder {
+    DartNotifyBuilder::new("", ty, OBSERVABLE_CATEGORY)
+}

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

@@ -45,11 +45,11 @@ pub(crate) async fn get_fields_handler(
 pub(crate) async fn create_row_handler(
     data: Data<CreateRowPayload>,
     manager: AppData<Arc<GridManager>>,
-) -> DataResult<Row, FlowyError> {
+) -> Result<(), FlowyError> {
     let payload: CreateRowPayload = data.into_inner();
     let editor = manager.get_grid_editor(payload.grid_id.as_ref())?;
-    let row = editor.create_row(payload.upper_row_id).await?;
-    data_result(row)
+    let _ = editor.create_row(payload.upper_row_id).await?;
+    Ok(())
 }
 
 #[tracing::instrument(level = "debug", skip_all, err)]

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

@@ -5,6 +5,7 @@ mod event_handler;
 pub mod event_map;
 pub mod manager;
 
+mod dart_notification;
 mod protobuf;
 pub mod services;
 pub mod util;

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

@@ -0,0 +1,106 @@
+// This file is generated by rust-protobuf 2.25.2. Do not edit
+// @generated
+
+// https://github.com/rust-lang/rust-clippy/issues/702
+#![allow(unknown_lints)]
+#![allow(clippy::all)]
+
+#![allow(unused_attributes)]
+#![cfg_attr(rustfmt, rustfmt::skip)]
+
+#![allow(box_pointers)]
+#![allow(dead_code)]
+#![allow(missing_docs)]
+#![allow(non_camel_case_types)]
+#![allow(non_snake_case)]
+#![allow(non_upper_case_globals)]
+#![allow(trivial_casts)]
+#![allow(unused_imports)]
+#![allow(unused_results)]
+//! Generated file from `dart_notification.proto`
+
+/// Generated files are compatible only with the same version
+/// of protobuf runtime.
+// const _PROTOBUF_VERSION_CHECK: () = ::protobuf::VERSION_2_25_2;
+
+#[derive(Clone,PartialEq,Eq,Debug,Hash)]
+pub enum GridNotification {
+    Unknown = 0,
+    GridDidCreateRows = 10,
+    GridDidDeleteRow = 11,
+    GridDidUpdateRows = 12,
+    GridDidUpdateCells = 20,
+    GridDidUpdateFields = 30,
+}
+
+impl ::protobuf::ProtobufEnum for GridNotification {
+    fn value(&self) -> i32 {
+        *self as i32
+    }
+
+    fn from_i32(value: i32) -> ::std::option::Option<GridNotification> {
+        match value {
+            0 => ::std::option::Option::Some(GridNotification::Unknown),
+            10 => ::std::option::Option::Some(GridNotification::GridDidCreateRows),
+            11 => ::std::option::Option::Some(GridNotification::GridDidDeleteRow),
+            12 => ::std::option::Option::Some(GridNotification::GridDidUpdateRows),
+            20 => ::std::option::Option::Some(GridNotification::GridDidUpdateCells),
+            30 => ::std::option::Option::Some(GridNotification::GridDidUpdateFields),
+            _ => ::std::option::Option::None
+        }
+    }
+
+    fn values() -> &'static [Self] {
+        static values: &'static [GridNotification] = &[
+            GridNotification::Unknown,
+            GridNotification::GridDidCreateRows,
+            GridNotification::GridDidDeleteRow,
+            GridNotification::GridDidUpdateRows,
+            GridNotification::GridDidUpdateCells,
+            GridNotification::GridDidUpdateFields,
+        ];
+        values
+    }
+
+    fn enum_descriptor_static() -> &'static ::protobuf::reflect::EnumDescriptor {
+        static descriptor: ::protobuf::rt::LazyV2<::protobuf::reflect::EnumDescriptor> = ::protobuf::rt::LazyV2::INIT;
+        descriptor.get(|| {
+            ::protobuf::reflect::EnumDescriptor::new_pb_name::<GridNotification>("GridNotification", file_descriptor_proto())
+        })
+    }
+}
+
+impl ::std::marker::Copy for GridNotification {
+}
+
+impl ::std::default::Default for GridNotification {
+    fn default() -> Self {
+        GridNotification::Unknown
+    }
+}
+
+impl ::protobuf::reflect::ProtobufValue for GridNotification {
+    fn as_ref(&self) -> ::protobuf::reflect::ReflectValueRef {
+        ::protobuf::reflect::ReflectValueRef::Enum(::protobuf::ProtobufEnum::descriptor(self))
+    }
+}
+
+static file_descriptor_proto_data: &'static [u8] = b"\
+    \n\x17dart_notification.proto*\x94\x01\n\x10GridNotification\x12\x0b\n\
+    \x07Unknown\x10\0\x12\x15\n\x11GridDidCreateRows\x10\n\x12\x14\n\x10Grid\
+    DidDeleteRow\x10\x0b\x12\x15\n\x11GridDidUpdateRows\x10\x0c\x12\x16\n\
+    \x12GridDidUpdateCells\x10\x14\x12\x17\n\x13GridDidUpdateFields\x10\x1eb\
+    \x06proto3\
+";
+
+static file_descriptor_proto_lazy: ::protobuf::rt::LazyV2<::protobuf::descriptor::FileDescriptorProto> = ::protobuf::rt::LazyV2::INIT;
+
+fn parse_descriptor_proto() -> ::protobuf::descriptor::FileDescriptorProto {
+    ::protobuf::Message::parse_from_bytes(file_descriptor_proto_data).unwrap()
+}
+
+pub fn file_descriptor_proto() -> &'static ::protobuf::descriptor::FileDescriptorProto {
+    file_descriptor_proto_lazy.get(|| {
+        parse_descriptor_proto()
+    })
+}

+ 3 - 0
frontend/rust-lib/flowy-grid/src/protobuf/model/mod.rs

@@ -7,6 +7,9 @@ pub use date_description::*;
 mod text_description;
 pub use text_description::*;
 
+mod dart_notification;
+pub use dart_notification::*;
+
 mod checkbox_description;
 pub use checkbox_description::*;
 

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

@@ -0,0 +1,10 @@
+syntax = "proto3";
+
+enum GridNotification {
+    Unknown = 0;
+    GridDidCreateRows = 10;
+    GridDidDeleteRow = 11;
+    GridDidUpdateRows = 12;
+    GridDidUpdateCells = 20;
+    GridDidUpdateFields = 30;
+}

+ 104 - 23
frontend/rust-lib/flowy-grid/src/services/block_meta_editor.rs

@@ -1,5 +1,5 @@
 use crate::manager::GridUser;
-use crate::services::row::make_row_ids_per_block;
+use crate::services::row::{make_cell, make_row_ids_per_block, make_rows};
 use bytes::Bytes;
 
 use dashmap::DashMap;
@@ -8,7 +8,8 @@ use flowy_collaboration::entities::revision::Revision;
 use flowy_collaboration::util::make_delta_from_revisions;
 use flowy_error::{FlowyError, FlowyResult};
 use flowy_grid_data_model::entities::{
-    GridBlock, GridBlockChangeset, RepeatedRowOrder, RowMeta, RowMetaChangeset, RowOrder,
+    Cell, FieldMeta, GridBlock, GridBlockChangeset, RepeatedCell, RepeatedRow, RepeatedRowOrder, RowMeta,
+    RowMetaChangeset, RowOrder,
 };
 use flowy_sync::disk::SQLiteGridBlockMetaRevisionPersistence;
 use flowy_sync::{
@@ -19,6 +20,7 @@ use lib_ot::core::PlainTextAttributes;
 
 use std::collections::HashMap;
 
+use crate::dart_notification::{send_dart_notification, GridNotification};
 use std::sync::Arc;
 use tokio::sync::RwLock;
 
@@ -26,17 +28,20 @@ type RowId = String;
 type BlockId = String;
 
 pub(crate) struct GridBlockMetaEditorManager {
+    grid_id: String,
     user: Arc<dyn GridUser>,
     editor_map: DashMap<String, Arc<ClientGridBlockMetaEditor>>,
     block_id_by_row_id: DashMap<BlockId, RowId>,
 }
 
 impl GridBlockMetaEditorManager {
-    pub(crate) async fn new(user: &Arc<dyn GridUser>, blocks: Vec<GridBlock>) -> FlowyResult<Self> {
+    pub(crate) async fn new(grid_id: &str, user: &Arc<dyn GridUser>, blocks: Vec<GridBlock>) -> FlowyResult<Self> {
         let editor_map = make_block_meta_editor_map(user, blocks).await?;
         let user = user.clone();
         let block_id_by_row_id = DashMap::new();
+        let grid_id = grid_id.to_owned();
         let manager = Self {
+            grid_id,
             user,
             editor_map,
             block_id_by_row_id,
@@ -56,25 +61,41 @@ impl GridBlockMetaEditorManager {
         }
     }
 
-    pub(crate) async fn create_row(&self, row: RowMeta, upper_row_id: Option<String>) -> FlowyResult<i32> {
-        self.block_id_by_row_id.insert(row.id.clone(), row.block_id.clone());
-        let editor = self.get_editor(&row.block_id).await?;
-        editor.create_row(row, upper_row_id).await
+    pub(crate) async fn create_row(
+        &self,
+        field_metas: &[FieldMeta],
+        row_meta: RowMeta,
+        upper_row_id: Option<String>,
+    ) -> FlowyResult<i32> {
+        self.block_id_by_row_id
+            .insert(row_meta.id.clone(), row_meta.block_id.clone());
+        let editor = self.get_editor(&row_meta.block_id).await?;
+
+        let rows = make_rows(field_metas, vec![row_meta.clone().into()]);
+        send_dart_notification(&self.grid_id, GridNotification::GridDidCreateRows)
+            .payload(RepeatedRow::from(rows))
+            .send();
+
+        self.notify_did_create_rows(field_metas, vec![row_meta.clone()]);
+
+        editor.create_row(row_meta, upper_row_id).await
     }
 
     pub(crate) async fn insert_row(
         &self,
+        field_metas: &[FieldMeta],
         rows_by_block_id: HashMap<String, Vec<RowMeta>>,
     ) -> FlowyResult<Vec<GridBlockChangeset>> {
         let mut changesets = vec![];
-        for (block_id, rows) in rows_by_block_id {
+        for (block_id, row_metas) in rows_by_block_id {
             let editor = self.get_editor(&block_id).await?;
             let mut row_count = 0;
-            for row in rows {
+            for row in &row_metas {
                 self.block_id_by_row_id.insert(row.id.clone(), row.block_id.clone());
-                row_count = editor.create_row(row, None).await?;
+                row_count = editor.create_row(row.clone(), None).await?;
             }
             changesets.push(GridBlockChangeset::from_row_count(&block_id, row_count));
+            self.notify_did_create_rows(field_metas, row_metas);
         }
 
         Ok(changesets)
@@ -104,19 +125,17 @@ impl GridBlockMetaEditorManager {
     }
 
     pub async fn update_row(&self, changeset: RowMetaChangeset) -> FlowyResult<()> {
-        match self.block_id_by_row_id.get(&changeset.row_id) {
-            None => {
-                let msg = format!(
-                    "Update Row failed. Can't find the corresponding block with row_id: {}",
-                    changeset.row_id
-                );
-                Err(FlowyError::internal().context(msg))
-            }
-            Some(block_id) => {
-                let editor = self.get_editor(&block_id).await?;
-                editor.update_row(changeset).await
-            }
-        }
+        let editor = self.get_editor_from_row_id(&changeset.row_id).await?;
+        let _ = editor.update_row(changeset.clone()).await?;
+        let _ = self.notify_did_update_row()?;
+        Ok(())
+    }
+
+    pub async fn update_cells(&self, field_metas: &[FieldMeta], changeset: RowMetaChangeset) -> FlowyResult<()> {
+        let editor = self.get_editor_from_row_id(&changeset.row_id).await?;
+        let _ = editor.update_row(changeset.clone()).await?;
+        self.notify_did_update_cells(changeset, field_metas)?;
+        Ok(())
     }
 
     pub(crate) async fn get_all_rows(&self, grid_blocks: Vec<GridBlock>) -> FlowyResult<Vec<Arc<RowMeta>>> {
@@ -159,6 +178,68 @@ impl GridBlockMetaEditorManager {
         }
         Ok(row_metas)
     }
+
+    async fn get_editor_from_row_id(&self, row_id: &str) -> FlowyResult<Arc<ClientGridBlockMetaEditor>> {
+        match self.block_id_by_row_id.get(row_id) {
+            None => {
+                let msg = format!(
+                    "Update Row failed. Can't find the corresponding block with row_id: {}",
+                    row_id
+                );
+                Err(FlowyError::internal().context(msg))
+            }
+            Some(block_id) => {
+                let editor = self.get_editor(&block_id).await?;
+                Ok(editor)
+            }
+        }
+    }
+
+    fn notify_did_create_rows(&self, field_metas: &[FieldMeta], row_metas: Vec<RowMeta>) {
+        let rows = make_rows(
+            field_metas,
+            row_metas
+                .into_iter()
+                .map(|row_meta| Arc::new(row_meta))
+                .collect::<Vec<_>>(),
+        );
+        send_dart_notification(&self.grid_id, GridNotification::GridDidCreateRows)
+            .payload(RepeatedRow::from(rows))
+            .send();
+    }
+
+    fn notify_did_update_row(&self) -> FlowyResult<()> {
+        // send_dart_notification(&changeset.row_id, GridNotification::GridDidUpdateRows)
+        //     .payload(RepeatedRow::from(cells))
+        //     .send();
+
+        todo!()
+    }
+
+    fn notify_did_update_cells(&self, changeset: RowMetaChangeset, field_metas: &[FieldMeta]) -> FlowyResult<()> {
+        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(&field_meta_map, field_id, cell_meta) {
+                    None => {}
+                    Some((_, cell)) => cells.push(cell),
+                },
+            );
+
+        if !cells.is_empty() {
+            send_dart_notification(&changeset.row_id, GridNotification::GridDidUpdateCells)
+                .payload(RepeatedCell::from(cells))
+                .send();
+        }
+        Ok(())
+    }
 }
 
 async fn make_block_meta_editor_map(

+ 34 - 14
frontend/rust-lib/flowy-grid/src/services/grid_editor.rs

@@ -1,16 +1,17 @@
 use crate::manager::GridUser;
 use crate::services::block_meta_editor::GridBlockMetaEditorManager;
 use bytes::Bytes;
-use flowy_collaboration::client_grid::{GridChange, GridMetaPad};
+use flowy_collaboration::client_grid::{GridChangeset, GridMetaPad};
 use flowy_collaboration::entities::revision::Revision;
 use flowy_collaboration::util::make_delta_from_revisions;
 use flowy_error::{FlowyError, FlowyResult};
 use flowy_grid_data_model::entities::{
-    CellMetaChangeset, FieldChangeset, FieldMeta, Grid, GridBlock, GridBlockChangeset, RepeatedFieldOrder,
-    RepeatedRowOrder, Row, RowMeta, RowMetaChangeset,
+    Cell, CellMetaChangeset, Field, FieldChangeset, FieldMeta, Grid, GridBlock, GridBlockChangeset, RepeatedField,
+    RepeatedFieldOrder, RepeatedRow, RepeatedRowOrder, Row, RowMeta, RowMetaChangeset,
 };
 use std::collections::HashMap;
 
+use crate::dart_notification::{send_dart_notification, GridNotification};
 use crate::services::row::{
     make_row_by_row_id, make_rows, row_meta_from_context, serialize_cell_data, RowMetaContext, RowMetaContextBuilder,
 };
@@ -40,8 +41,9 @@ impl ClientGridEditor {
         let rev_manager = Arc::new(rev_manager);
         let grid_meta_pad = Arc::new(RwLock::new(grid_pad));
 
-        let block_meta_manager =
-            Arc::new(GridBlockMetaEditorManager::new(&user, grid_meta_pad.read().await.get_blocks().clone()).await?);
+        let block_meta_manager = Arc::new(
+            GridBlockMetaEditorManager::new(grid_id, &user, grid_meta_pad.read().await.get_blocks().clone()).await?,
+        );
 
         Ok(Arc::new(Self {
             grid_id: grid_id.to_owned(),
@@ -54,6 +56,7 @@ impl ClientGridEditor {
 
     pub async fn create_field(&self, field_meta: FieldMeta) -> FlowyResult<()> {
         let _ = self.modify(|grid| Ok(grid.create_field(field_meta)?)).await?;
+        let _ = self.notify_did_update_fields().await?;
         Ok(())
     }
 
@@ -81,7 +84,7 @@ impl ClientGridEditor {
         Ok(())
     }
 
-    pub async fn create_row(&self, upper_row_id: Option<String>) -> FlowyResult<Row> {
+    pub async fn create_row(&self, upper_row_id: Option<String>) -> FlowyResult<()> {
         let field_metas = self.grid_meta_pad.read().await.get_field_metas(None)?;
         let block_id = self.last_block_id().await?;
 
@@ -92,17 +95,17 @@ impl ClientGridEditor {
         // insert the row
         let row_count = self
             .block_meta_manager
-            .create_row(row_meta.clone(), upper_row_id)
+            .create_row(&field_metas, row_meta, upper_row_id)
             .await?;
-        let row = make_rows(&field_metas, vec![row_meta.into()]).pop().unwrap();
 
         // update block row count
         let changeset = GridBlockChangeset::from_row_count(&block_id, row_count);
         let _ = self.update_block(changeset).await?;
-        Ok(row)
+        Ok(())
     }
 
     pub async fn insert_rows(&self, contexts: Vec<RowMetaContext>) -> FlowyResult<()> {
+        let field_metas = self.grid_meta_pad.read().await.get_field_metas(None)?;
         let block_id = self.last_block_id().await?;
         let mut rows_by_block_id: HashMap<String, Vec<RowMeta>> = HashMap::new();
         for ctx in contexts {
@@ -112,7 +115,10 @@ impl ClientGridEditor {
                 .or_insert_with(Vec::new)
                 .push(row_meta);
         }
-        let changesets = self.block_meta_manager.insert_row(rows_by_block_id).await?;
+        let changesets = self
+            .block_meta_manager
+            .insert_row(&field_metas, rows_by_block_id)
+            .await?;
         for changeset in changesets {
             let _ = self.update_block(changeset).await?;
         }
@@ -136,8 +142,13 @@ impl ClientGridEditor {
             }
         }
 
+        let field_metas = self.get_field_metas(None).await?;
         let row_changeset: RowMetaChangeset = changeset.into();
-        self.update_row(row_changeset).await
+        let _ = self
+            .block_meta_manager
+            .update_cells(&field_metas, row_changeset)
+            .await?;
+        Ok(())
     }
 
     pub async fn get_rows(&self, row_orders: Option<RepeatedRowOrder>) -> FlowyResult<Vec<Row>> {
@@ -205,7 +216,7 @@ impl ClientGridEditor {
 
     async fn modify<F>(&self, f: F) -> FlowyResult<()>
     where
-        F: for<'a> FnOnce(&'a mut GridMetaPad) -> FlowyResult<Option<GridChange>>,
+        F: for<'a> FnOnce(&'a mut GridMetaPad) -> FlowyResult<Option<GridChangeset>>,
     {
         let mut write_guard = self.grid_meta_pad.write().await;
         match f(&mut *write_guard)? {
@@ -217,8 +228,8 @@ impl ClientGridEditor {
         Ok(())
     }
 
-    async fn apply_change(&self, change: GridChange) -> FlowyResult<()> {
-        let GridChange { delta, md5 } = change;
+    async fn apply_change(&self, change: GridChangeset) -> FlowyResult<()> {
+        let GridChangeset { delta, md5 } = change;
         let user_id = self.user.user_id()?;
         let (base_rev_id, rev_id) = self.rev_manager.next_rev_id_pair();
         let delta_data = delta.to_delta_bytes();
@@ -243,6 +254,15 @@ impl ClientGridEditor {
             Some(grid_block) => Ok(grid_block.id.clone()),
         }
     }
+
+    async fn notify_did_update_fields(&self) -> FlowyResult<()> {
+        let field_metas = self.get_field_metas(None).await?;
+        let repeated_field: RepeatedField = field_metas.into_iter().map(Field::from).collect::<Vec<_>>().into();
+        send_dart_notification(&self.grid_id, GridNotification::GridDidUpdateFields)
+            .payload(repeated_field)
+            .send();
+        Ok(())
+    }
 }
 
 #[cfg(feature = "flowy_unit_test")]

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

@@ -48,7 +48,11 @@ pub(crate) fn make_rows(fields: &[FieldMeta], row_metas: Vec<Arc<RowMeta>>) -> V
 }
 
 #[inline(always)]
-fn make_cell(field_map: &HashMap<&String, &FieldMeta>, field_id: String, raw_cell: CellMeta) -> Option<(String, Cell)> {
+pub fn make_cell(
+    field_map: &HashMap<&String, &FieldMeta>,
+    field_id: String,
+    raw_cell: CellMeta,
+) -> Option<(String, Cell)> {
     let field_meta = field_map.get(&field_id)?;
     match deserialize_cell_data(raw_cell.data, field_meta) {
         Ok(content) => {
@@ -63,9 +67,9 @@ fn make_cell(field_map: &HashMap<&String, &FieldMeta>, field_id: String, raw_cel
 }
 
 pub(crate) fn make_row_by_row_id(fields: &[FieldMeta], row_metas: Vec<Arc<RowMeta>>) -> HashMap<String, Row> {
-    let field_map = fields
+    let field_meta_map = fields
         .iter()
-        .map(|field| (&field.id, field))
+        .map(|field_meta| (&field_meta.id, field_meta))
         .collect::<HashMap<&String, &FieldMeta>>();
 
     let make_row = |row_meta: Arc<RowMeta>| {
@@ -73,7 +77,7 @@ pub(crate) fn make_row_by_row_id(fields: &[FieldMeta], row_metas: Vec<Arc<RowMet
             .cell_by_field_id
             .clone()
             .into_par_iter()
-            .flat_map(|(field_id, raw_cell)| make_cell(&field_map, field_id, raw_cell))
+            .flat_map(|(field_id, raw_cell)| make_cell(&field_meta_map, field_id, raw_cell))
             .collect::<HashMap<String, Cell>>();
 
         let row = Row {

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

@@ -172,7 +172,7 @@ impl GridEditorTest {
                 assert_eq!(compared_block, block);
             }
             EditorScript::CreateEmptyRow => {
-                self.editor.create_row().await.unwrap();
+                self.editor.create_row(None).await.unwrap();
                 self.row_metas = self.editor.get_row_metas(None).await.unwrap();
                 self.grid_blocks = self.editor.get_blocks().await.unwrap();
             }

+ 10 - 10
shared-lib/flowy-collaboration/src/client_grid/grid_meta_pad.rs

@@ -35,7 +35,7 @@ impl GridMetaPad {
         Self::from_delta(grid_delta)
     }
 
-    pub fn create_field(&mut self, field_meta: FieldMeta) -> CollaborateResult<Option<GridChange>> {
+    pub fn create_field(&mut self, field_meta: FieldMeta) -> CollaborateResult<Option<GridChangeset>> {
         self.modify_grid(|grid| {
             if grid.fields.contains(&field_meta) {
                 tracing::warn!("Duplicate grid field");
@@ -47,7 +47,7 @@ impl GridMetaPad {
         })
     }
 
-    pub fn delete_field(&mut self, field_id: &str) -> CollaborateResult<Option<GridChange>> {
+    pub fn delete_field(&mut self, field_id: &str) -> CollaborateResult<Option<GridChangeset>> {
         self.modify_grid(|grid| match grid.fields.iter().position(|field| field.id == field_id) {
             None => Ok(None),
             Some(index) => {
@@ -95,7 +95,7 @@ impl GridMetaPad {
         }
     }
 
-    pub fn update_field(&mut self, changeset: FieldChangeset) -> CollaborateResult<Option<GridChange>> {
+    pub fn update_field(&mut self, changeset: FieldChangeset) -> CollaborateResult<Option<GridChangeset>> {
         let field_id = changeset.field_id.clone();
         self.modify_field(&field_id, |field| {
             let mut is_changed = None;
@@ -138,7 +138,7 @@ impl GridMetaPad {
         })
     }
 
-    pub fn create_block(&mut self, block: GridBlock) -> CollaborateResult<Option<GridChange>> {
+    pub fn create_block(&mut self, block: GridBlock) -> CollaborateResult<Option<GridChangeset>> {
         self.modify_grid(|grid| {
             if grid.blocks.iter().any(|b| b.id == block.id) {
                 tracing::warn!("Duplicate grid block");
@@ -165,7 +165,7 @@ impl GridMetaPad {
         self.grid_meta.blocks.clone()
     }
 
-    pub fn update_block(&mut self, changeset: GridBlockChangeset) -> CollaborateResult<Option<GridChange>> {
+    pub fn update_block(&mut self, changeset: GridBlockChangeset) -> CollaborateResult<Option<GridChangeset>> {
         let block_id = changeset.block_id.clone();
         self.modify_block(&block_id, |block| {
             let mut is_changed = None;
@@ -200,7 +200,7 @@ impl GridMetaPad {
         &self.grid_meta.fields
     }
 
-    fn modify_grid<F>(&mut self, f: F) -> CollaborateResult<Option<GridChange>>
+    fn modify_grid<F>(&mut self, f: F) -> CollaborateResult<Option<GridChangeset>>
     where
         F: FnOnce(&mut GridMeta) -> CollaborateResult<Option<()>>,
     {
@@ -214,14 +214,14 @@ impl GridMetaPad {
                     None => Ok(None),
                     Some(delta) => {
                         self.delta = self.delta.compose(&delta)?;
-                        Ok(Some(GridChange { delta, md5: self.md5() }))
+                        Ok(Some(GridChangeset { delta, md5: self.md5() }))
                     }
                 }
             }
         }
     }
 
-    pub fn modify_block<F>(&mut self, block_id: &str, f: F) -> CollaborateResult<Option<GridChange>>
+    pub fn modify_block<F>(&mut self, block_id: &str, f: F) -> CollaborateResult<Option<GridChangeset>>
     where
         F: FnOnce(&mut GridBlock) -> CollaborateResult<Option<()>>,
     {
@@ -234,7 +234,7 @@ impl GridMetaPad {
         })
     }
 
-    pub fn modify_field<F>(&mut self, field_id: &str, f: F) -> CollaborateResult<Option<GridChange>>
+    pub fn modify_field<F>(&mut self, field_id: &str, f: F) -> CollaborateResult<Option<GridChangeset>>
     where
         F: FnOnce(&mut FieldMeta) -> CollaborateResult<Option<()>>,
     {
@@ -254,7 +254,7 @@ fn json_from_grid(grid: &Arc<GridMeta>) -> CollaborateResult<String> {
     Ok(json)
 }
 
-pub struct GridChange {
+pub struct GridChangeset {
     pub delta: GridMetaDelta,
     /// md5: the md5 of the grid after applying the change.
     pub md5: String,

+ 25 - 0
shared-lib/flowy-grid-data-model/src/entities/grid.rs

@@ -204,6 +204,31 @@ impl Cell {
     }
 }
 
+#[derive(Debug, Default, ProtoBuf)]
+pub struct RepeatedCell {
+    #[pb(index = 1)]
+    pub items: Vec<Cell>,
+}
+
+impl std::ops::Deref for RepeatedCell {
+    type Target = Vec<Cell>;
+    fn deref(&self) -> &Self::Target {
+        &self.items
+    }
+}
+
+impl std::ops::DerefMut for RepeatedCell {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        &mut self.items
+    }
+}
+
+impl std::convert::From<Vec<Cell>> for RepeatedCell {
+    fn from(items: Vec<Cell>) -> Self {
+        Self { items }
+    }
+}
+
 #[derive(ProtoBuf, Default)]
 pub struct CreateGridPayload {
     #[pb(index = 1)]

+ 177 - 10
shared-lib/flowy-grid-data-model/src/protobuf/model/grid.rs

@@ -2115,6 +2115,172 @@ impl ::protobuf::reflect::ProtobufValue for Cell {
     }
 }
 
+#[derive(PartialEq,Clone,Default)]
+pub struct RepeatedCell {
+    // message fields
+    pub items: ::protobuf::RepeatedField<Cell>,
+    // special fields
+    pub unknown_fields: ::protobuf::UnknownFields,
+    pub cached_size: ::protobuf::CachedSize,
+}
+
+impl<'a> ::std::default::Default for &'a RepeatedCell {
+    fn default() -> &'a RepeatedCell {
+        <RepeatedCell as ::protobuf::Message>::default_instance()
+    }
+}
+
+impl RepeatedCell {
+    pub fn new() -> RepeatedCell {
+        ::std::default::Default::default()
+    }
+
+    // repeated .Cell items = 1;
+
+
+    pub fn get_items(&self) -> &[Cell] {
+        &self.items
+    }
+    pub fn clear_items(&mut self) {
+        self.items.clear();
+    }
+
+    // Param is passed by value, moved
+    pub fn set_items(&mut self, v: ::protobuf::RepeatedField<Cell>) {
+        self.items = v;
+    }
+
+    // Mutable pointer to the field.
+    pub fn mut_items(&mut self) -> &mut ::protobuf::RepeatedField<Cell> {
+        &mut self.items
+    }
+
+    // Take field
+    pub fn take_items(&mut self) -> ::protobuf::RepeatedField<Cell> {
+        ::std::mem::replace(&mut self.items, ::protobuf::RepeatedField::new())
+    }
+}
+
+impl ::protobuf::Message for RepeatedCell {
+    fn is_initialized(&self) -> bool {
+        for v in &self.items {
+            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_repeated_message_into(wire_type, is, &mut self.items)?;
+                },
+                _ => {
+                    ::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;
+        for value in &self.items {
+            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<()> {
+        for v in &self.items {
+            os.write_tag(1, ::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() -> RepeatedCell {
+        RepeatedCell::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_repeated_field_accessor::<_, ::protobuf::types::ProtobufTypeMessage<Cell>>(
+                "items",
+                |m: &RepeatedCell| { &m.items },
+                |m: &mut RepeatedCell| { &mut m.items },
+            ));
+            ::protobuf::reflect::MessageDescriptor::new_pb_name::<RepeatedCell>(
+                "RepeatedCell",
+                fields,
+                file_descriptor_proto()
+            )
+        })
+    }
+
+    fn default_instance() -> &'static RepeatedCell {
+        static instance: ::protobuf::rt::LazyV2<RepeatedCell> = ::protobuf::rt::LazyV2::INIT;
+        instance.get(RepeatedCell::new)
+    }
+}
+
+impl ::protobuf::Clear for RepeatedCell {
+    fn clear(&mut self) {
+        self.items.clear();
+        self.unknown_fields.clear();
+    }
+}
+
+impl ::std::fmt::Debug for RepeatedCell {
+    fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
+        ::protobuf::text_format::fmt(self, f)
+    }
+}
+
+impl ::protobuf::reflect::ProtobufValue for RepeatedCell {
+    fn as_ref(&self) -> ::protobuf::reflect::ReflectValueRef {
+        ::protobuf::reflect::ReflectValueRef::Message(self)
+    }
+}
+
 #[derive(PartialEq,Clone,Default)]
 pub struct CreateGridPayload {
     // message fields
@@ -3129,16 +3295,17 @@ static file_descriptor_proto_data: &'static [u8] = b"\
     \x02\x20\x01(\x0b2\x05.CellR\x05value:\x028\x01\")\n\x0bRepeatedRow\x12\
     \x1a\n\x05items\x18\x01\x20\x03(\x0b2\x04.RowR\x05items\";\n\x04Cell\x12\
     \x19\n\x08field_id\x18\x01\x20\x01(\tR\x07fieldId\x12\x18\n\x07content\
-    \x18\x02\x20\x01(\tR\x07content\"'\n\x11CreateGridPayload\x12\x12\n\x04n\
-    ame\x18\x01\x20\x01(\tR\x04name\"\x1e\n\x06GridId\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\x0cupper_row_id\x18\x02\x20\x01(\
-    \tH\0R\nupperRowIdB\x15\n\x13one_of_upper_row_id\"d\n\x11QueryFieldPaylo\
-    ad\x12\x17\n\x07grid_id\x18\x01\x20\x01(\tR\x06gridId\x126\n\x0cfield_or\
-    ders\x18\x02\x20\x01(\x0b2\x13.RepeatedFieldOrderR\x0bfieldOrders\"\\\n\
-    \x0fQueryRowPayload\x12\x17\n\x07grid_id\x18\x01\x20\x01(\tR\x06gridId\
-    \x120\n\nrow_orders\x18\x02\x20\x01(\x0b2\x11.RepeatedRowOrderR\trowOrde\
-    rsb\x06proto3\
+    \x18\x02\x20\x01(\tR\x07content\"+\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\
+    \x05value\x18\x01\x20\x01(\tR\x05value\"f\n\x10CreateRowPayload\x12\x17\
+    \n\x07grid_id\x18\x01\x20\x01(\tR\x06gridId\x12\"\n\x0cupper_row_id\x18\
+    \x02\x20\x01(\tH\0R\nupperRowIdB\x15\n\x13one_of_upper_row_id\"d\n\x11Qu\
+    eryFieldPayload\x12\x17\n\x07grid_id\x18\x01\x20\x01(\tR\x06gridId\x126\
+    \n\x0cfield_orders\x18\x02\x20\x01(\x0b2\x13.RepeatedFieldOrderR\x0bfiel\
+    dOrders\"\\\n\x0fQueryRowPayload\x12\x17\n\x07grid_id\x18\x01\x20\x01(\t\
+    R\x06gridId\x120\n\nrow_orders\x18\x02\x20\x01(\x0b2\x11.RepeatedRowOrde\
+    rR\trowOrdersb\x06proto3\
 ";
 
 static file_descriptor_proto_lazy: ::protobuf::rt::LazyV2<::protobuf::descriptor::FileDescriptorProto> = ::protobuf::rt::LazyV2::INIT;

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

@@ -43,6 +43,9 @@ message Cell {
     string field_id = 1;
     string content = 2;
 }
+message RepeatedCell {
+    repeated Cell items = 1;
+}
 message CreateGridPayload {
     string name = 1;
 }