Explorar el Código

Merge pull request #457 from AppFlowy-IO/fix_0.0.4_beta_2_bugs

Fix 0.0.4 beta 2 bugs
Nathan.fooo hace 3 años
padre
commit
3af558fd94
Se han modificado 91 ficheros con 2908 adiciones y 1207 borrados
  1. 1 1
      .github/workflows/rust_test.yml
  2. 12 19
      frontend/app_flowy/lib/startup/deps_resolver.dart
  3. 5 5
      frontend/app_flowy/lib/startup/tasks/app_widget.dart
  4. 5 4
      frontend/app_flowy/lib/workspace/application/grid/cell/cell_listener.dart
  5. 75 0
      frontend/app_flowy/lib/workspace/application/grid/cell/cell_service.dart
  6. 9 10
      frontend/app_flowy/lib/workspace/application/grid/cell/checkbox_cell_bloc.dart
  7. 25 19
      frontend/app_flowy/lib/workspace/application/grid/cell/date_cell_bloc.dart
  8. 39 28
      frontend/app_flowy/lib/workspace/application/grid/cell/number_cell_bloc.dart
  9. 0 0
      frontend/app_flowy/lib/workspace/application/grid/cell/select_option_service.dart
  10. 16 17
      frontend/app_flowy/lib/workspace/application/grid/cell/selection_cell_bloc.dart
  11. 15 16
      frontend/app_flowy/lib/workspace/application/grid/cell/selection_editor_bloc.dart
  12. 4 4
      frontend/app_flowy/lib/workspace/application/grid/cell/text_cell_bloc.dart
  13. 0 35
      frontend/app_flowy/lib/workspace/application/grid/cell_bloc/cell_service.dart
  14. 0 8
      frontend/app_flowy/lib/workspace/application/grid/data.dart
  15. 14 4
      frontend/app_flowy/lib/workspace/application/grid/field/field_cell_bloc.dart
  16. 7 6
      frontend/app_flowy/lib/workspace/application/grid/field/field_listener.dart
  17. 28 12
      frontend/app_flowy/lib/workspace/application/grid/field/field_service.dart
  18. 7 6
      frontend/app_flowy/lib/workspace/application/grid/field/grid_listenr.dart
  19. 26 113
      frontend/app_flowy/lib/workspace/application/grid/grid_bloc.dart
  20. 19 24
      frontend/app_flowy/lib/workspace/application/grid/grid_header_bloc.dart
  21. 5 6
      frontend/app_flowy/lib/workspace/application/grid/grid_listener.dart
  22. 117 0
      frontend/app_flowy/lib/workspace/application/grid/grid_service.dart
  23. 7 8
      frontend/app_flowy/lib/workspace/application/grid/prelude.dart
  24. 3 3
      frontend/app_flowy/lib/workspace/application/grid/row/row_action_sheet_bloc.dart
  25. 54 49
      frontend/app_flowy/lib/workspace/application/grid/row/row_bloc.dart
  26. 5 4
      frontend/app_flowy/lib/workspace/application/grid/row/row_listener.dart
  27. 254 8
      frontend/app_flowy/lib/workspace/application/grid/row/row_service.dart
  28. 17 15
      frontend/app_flowy/lib/workspace/application/grid/setting/property_bloc.dart
  29. 2 2
      frontend/app_flowy/lib/workspace/presentation/home/menu/app/header/add_button.dart
  30. 1 1
      frontend/app_flowy/lib/workspace/presentation/home/menu/app/section/item.dart
  31. 17 10
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/controller/grid_scroll.dart
  32. 133 57
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/grid_page.dart
  33. 1 1
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/cell_builder.dart
  34. 1 3
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/cell_container.dart
  35. 1 1
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/checkbox_cell.dart
  36. 15 5
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/date_cell.dart
  37. 8 4
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/number_cell.dart
  38. 2 2
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/selection_cell/selection_cell.dart
  39. 4 4
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/selection_cell/selection_editor.dart
  40. 1 1
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/text_cell.dart
  41. 2 25
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/footer/grid_footer.dart
  42. 72 7
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/field_cell.dart
  43. 95 54
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/grid_header.dart
  44. 1 1
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/row/cell/number_cell.dart
  45. 16 7
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/row/grid_row.dart
  46. 1 1
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/row/number_cell.dart
  47. 1 1
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/row/row_action_sheet.dart
  48. 23 19
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/toolbar/grid_property.dart
  49. 4 4
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/toolbar/grid_setting.dart
  50. 4 4
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/toolbar/grid_toolbar.dart
  51. 1 1
      frontend/app_flowy/lib/workspace/presentation/widgets/pop_up_action.dart
  52. 9 3
      frontend/app_flowy/packages/flowy_infra/lib/notifier.dart
  53. 1 1
      frontend/app_flowy/packages/flowy_infra_ui/lib/style_widget/button.dart
  54. 14 11
      frontend/app_flowy/packages/flowy_infra_ui/lib/style_widget/hover.dart
  55. 19 15
      frontend/app_flowy/packages/flowy_infra_ui/lib/style_widget/scrolling/styled_scrollview.dart
  56. 17 0
      frontend/app_flowy/packages/flowy_sdk/lib/dispatch/dart_event/flowy-grid/dart_event.dart
  57. 308 71
      frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid-data-model/grid.pb.dart
  58. 15 0
      frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid-data-model/grid.pbenum.dart
  59. 62 13
      frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid-data-model/grid.pbjson.dart
  60. 6 6
      frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid/dart_notification.pbenum.dart
  61. 5 5
      frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid/dart_notification.pbjson.dart
  62. 2 0
      frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid/event_map.pbenum.dart
  63. 2 1
      frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid/event_map.pbjson.dart
  64. 14 0
      frontend/app_flowy/pubspec.lock
  65. 2 0
      frontend/app_flowy/pubspec.yaml
  66. 1 1
      frontend/rust-lib/dart-ffi/Cargo.toml
  67. 2 1
      frontend/rust-lib/flowy-folder/Cargo.toml
  68. 15 4
      frontend/rust-lib/flowy-folder/src/services/folder_editor.rs
  69. 1 0
      frontend/rust-lib/flowy-folder/src/services/web_socket.rs
  70. 4 4
      frontend/rust-lib/flowy-grid/src/dart_notification.rs
  71. 11 0
      frontend/rust-lib/flowy-grid/src/event_handler.rs
  72. 6 3
      frontend/rust-lib/flowy-grid/src/event_map.rs
  73. 15 15
      frontend/rust-lib/flowy-grid/src/protobuf/model/dart_notification.rs
  74. 10 7
      frontend/rust-lib/flowy-grid/src/protobuf/model/event_map.rs
  75. 4 4
      frontend/rust-lib/flowy-grid/src/protobuf/proto/dart_notification.proto
  76. 1 0
      frontend/rust-lib/flowy-grid/src/protobuf/proto/event_map.proto
  77. 15 8
      frontend/rust-lib/flowy-grid/src/services/block_meta_editor.rs
  78. 51 40
      frontend/rust-lib/flowy-grid/src/services/block_meta_manager.rs
  79. 1 1
      frontend/rust-lib/flowy-grid/src/services/field/type_options/date_type_option.rs
  80. 97 41
      frontend/rust-lib/flowy-grid/src/services/grid_editor.rs
  81. 2 1
      frontend/rust-lib/flowy-sdk/Cargo.toml
  82. 1 1
      frontend/rust-lib/flowy-sdk/src/lib.rs
  83. 2 1
      frontend/rust-lib/flowy-text-block/Cargo.toml
  84. 20 6
      frontend/rust-lib/flowy-text-block/src/editor.rs
  85. 3 0
      frontend/rust-lib/flowy-text-block/src/web_socket.rs
  86. 0 1
      frontend/rust-lib/flowy-user/Cargo.toml
  87. 131 14
      shared-lib/flowy-grid-data-model/src/entities/grid.rs
  88. 788 273
      shared-lib/flowy-grid-data-model/src/protobuf/model/grid.rs
  89. 26 5
      shared-lib/flowy-grid-data-model/src/protobuf/proto/grid.proto
  90. 13 0
      shared-lib/flowy-sync/src/client_grid/grid_block_meta_pad.rs
  91. 37 11
      shared-lib/flowy-sync/src/client_grid/grid_meta_pad.rs

+ 1 - 1
.github/workflows/rust_test.yml

@@ -44,7 +44,7 @@ jobs:
         
       - name: Run rust-lib tests
         working-directory: frontend/rust-lib
-        run: RUST_LOG=info cargo test --no-default-features
+        run: RUST_LOG=info cargo test --no-default-features --features="sync"
         
       - name: Run shared-lib tests
         working-directory: shared-lib

+ 12 - 19
frontend/app_flowy/lib/startup/deps_resolver.dart

@@ -17,7 +17,6 @@ import 'package:app_flowy/user/presentation/router.dart';
 import 'package:app_flowy/workspace/presentation/home/home_stack.dart';
 import 'package:flowy_sdk/protobuf/flowy-folder-data-model/app.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-folder-data-model/view.pb.dart';
-import 'package:flowy_sdk/protobuf/flowy-grid-data-model/grid.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid/date_type_option.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid/number_type_option.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid/selection_type_option.pb.dart';
@@ -150,16 +149,10 @@ void _resolveGridDeps(GetIt getIt) {
     (view, _) => GridBloc(view: view),
   );
 
-  getIt.registerFactoryParam<RowBloc, RowData, void>(
-    (data, _) => RowBloc(
-      rowData: data,
-    ),
-  );
-
-  getIt.registerFactoryParam<GridHeaderBloc, String, List<Field>>(
-    (gridId, fields) => GridHeaderBloc(
-      data: GridHeaderData(gridId: gridId, fields: fields),
-      service: FieldService(gridId: gridId),
+  getIt.registerFactoryParam<GridHeaderBloc, String, GridFieldCache>(
+    (gridId, fieldCache) => GridHeaderBloc(
+      gridId: gridId,
+      fieldCache: fieldCache,
     ),
   );
 
@@ -177,32 +170,32 @@ void _resolveGridDeps(GetIt getIt) {
     ),
   );
 
-  getIt.registerFactoryParam<TextCellBloc, CellData, void>(
+  getIt.registerFactoryParam<TextCellBloc, GridCellIdentifier, void>(
     (cellData, _) => TextCellBloc(
       service: CellService(),
       cellData: cellData,
     ),
   );
 
-  getIt.registerFactoryParam<SelectionCellBloc, CellData, void>(
+  getIt.registerFactoryParam<SelectionCellBloc, GridCellIdentifier, void>(
     (cellData, _) => SelectionCellBloc(
       cellData: cellData,
     ),
   );
 
-  getIt.registerFactoryParam<NumberCellBloc, CellData, void>(
+  getIt.registerFactoryParam<NumberCellBloc, GridCellIdentifier, void>(
     (cellData, _) => NumberCellBloc(
       cellData: cellData,
     ),
   );
 
-  getIt.registerFactoryParam<DateCellBloc, CellData, void>(
+  getIt.registerFactoryParam<DateCellBloc, GridCellIdentifier, void>(
     (cellData, _) => DateCellBloc(
-      cellData: cellData,
+      cellIdentifier: cellData,
     ),
   );
 
-  getIt.registerFactoryParam<CheckboxCellBloc, CellData, void>(
+  getIt.registerFactoryParam<CheckboxCellBloc, GridCellIdentifier, void>(
     (cellData, _) => CheckboxCellBloc(
       service: CellService(),
       cellData: cellData,
@@ -229,7 +222,7 @@ void _resolveGridDeps(GetIt getIt) {
     (typeOption, _) => NumberTypeOptionBloc(typeOption: typeOption),
   );
 
-  getIt.registerFactoryParam<GridPropertyBloc, String, List<Field>>(
-    (gridId, fields) => GridPropertyBloc(gridId: gridId, fields: fields),
+  getIt.registerFactoryParam<GridPropertyBloc, String, GridFieldCache>(
+    (gridId, cache) => GridPropertyBloc(gridId: gridId, fieldCache: cache),
   );
 }

+ 5 - 5
frontend/app_flowy/lib/startup/tasks/app_widget.dart

@@ -123,9 +123,9 @@ class ApplicationBlocObserver extends BlocObserver {
     super.onError(bloc, error, stackTrace);
   }
 
-  @override
-  void onEvent(Bloc bloc, Object? event) {
-    Log.debug("$event");
-    super.onEvent(bloc, event);
-  }
+  // @override
+  // void onEvent(Bloc bloc, Object? event) {
+  //   Log.debug("$event");
+  //   super.onEvent(bloc, event);
+  // }
 }

+ 5 - 4
frontend/app_flowy/lib/workspace/application/grid/cell_bloc/cell_listener.dart → frontend/app_flowy/lib/workspace/application/grid/cell/cell_listener.dart

@@ -12,7 +12,7 @@ typedef UpdateFieldNotifiedValue = Either<CellNotificationData, FlowyError>;
 class CellListener {
   final String rowId;
   final String fieldId;
-  PublishNotifier<UpdateFieldNotifiedValue> updateCellNotifier = PublishNotifier();
+  PublishNotifier<UpdateFieldNotifiedValue>? updateCellNotifier = PublishNotifier();
   GridNotificationListener? _listener;
   CellListener({required this.rowId, required this.fieldId});
 
@@ -24,8 +24,8 @@ class CellListener {
     switch (ty) {
       case GridNotification.DidUpdateCell:
         result.fold(
-          (payload) => updateCellNotifier.value = left(CellNotificationData.fromBuffer(payload)),
-          (error) => updateCellNotifier.value = right(error),
+          (payload) => updateCellNotifier?.value = left(CellNotificationData.fromBuffer(payload)),
+          (error) => updateCellNotifier?.value = right(error),
         );
         break;
       default:
@@ -35,6 +35,7 @@ class CellListener {
 
   Future<void> stop() async {
     await _listener?.stop();
-    updateCellNotifier.dispose();
+    updateCellNotifier?.dispose();
+    updateCellNotifier = null;
   }
 }

+ 75 - 0
frontend/app_flowy/lib/workspace/application/grid/cell/cell_service.dart

@@ -0,0 +1,75 @@
+import 'dart:collection';
+
+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:dartz/dartz.dart';
+import 'package:flowy_sdk/dispatch/dispatch.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid-data-model/grid.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/cell_entities.pb.dart';
+
+class CellService {
+  CellService();
+
+  Future<Either<void, FlowyError>> updateCell({
+    required String gridId,
+    required String fieldId,
+    required String rowId,
+    required String data,
+  }) {
+    final payload = CellChangeset.create()
+      ..gridId = gridId
+      ..fieldId = fieldId
+      ..rowId = rowId
+      ..data = data;
+    return GridEventUpdateCell(payload).send();
+  }
+
+  Future<Either<Cell, FlowyError>> getCell({
+    required String gridId,
+    required String fieldId,
+    required String rowId,
+  }) {
+    final payload = CellIdentifierPayload.create()
+      ..gridId = gridId
+      ..fieldId = fieldId
+      ..rowId = rowId;
+    return GridEventGetCell(payload).send();
+  }
+}
+
+class CellCache {
+  final CellService _cellService;
+  final HashMap<String, Cell> _cellDataMap = HashMap();
+
+  CellCache() : _cellService = CellService();
+
+  Future<Option<Cell>> getCellData(GridCellIdentifier identifier) async {
+    final cellId = _cellId(identifier);
+    final Cell? data = _cellDataMap[cellId];
+    if (data != null) {
+      return Future(() => Some(data));
+    }
+
+    final result = await _cellService.getCell(
+      gridId: identifier.gridId,
+      fieldId: identifier.field.id,
+      rowId: identifier.rowId,
+    );
+
+    return result.fold(
+      (cell) {
+        _cellDataMap[_cellId(identifier)] = cell;
+        return Some(cell);
+      },
+      (err) {
+        Log.error(err);
+        return none();
+      },
+    );
+  }
+
+  String _cellId(GridCellIdentifier identifier) {
+    return "${identifier.rowId}/${identifier.field.id}";
+  }
+}

+ 9 - 10
frontend/app_flowy/lib/workspace/application/grid/cell_bloc/checkbox_cell_bloc.dart → frontend/app_flowy/lib/workspace/application/grid/cell/checkbox_cell_bloc.dart

@@ -1,4 +1,4 @@
-import 'package:app_flowy/workspace/application/grid/cell_bloc/cell_listener.dart';
+import 'package:app_flowy/workspace/application/grid/cell/cell_listener.dart';
 import 'package:app_flowy/workspace/application/grid/row/row_service.dart';
 import 'package:flowy_sdk/log.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid-data-model/grid.pb.dart' show Cell;
@@ -15,7 +15,7 @@ class CheckboxCellBloc extends Bloc<CheckboxCellEvent, CheckboxCellState> {
 
   CheckboxCellBloc({
     required CellService service,
-    required CellData cellData,
+    required GridCellIdentifier cellData,
   })  : _service = service,
         _listener = CellListener(rowId: cellData.rowId, fieldId: cellData.field.id),
         super(CheckboxCellState.initial(cellData)) {
@@ -43,7 +43,7 @@ class CheckboxCellBloc extends Bloc<CheckboxCellEvent, CheckboxCellState> {
   }
 
   void _startListening() {
-    _listener.updateCellNotifier.addPublishListener((result) {
+    _listener.updateCellNotifier?.addPublishListener((result) {
       result.fold(
         (notificationData) async => await _loadCellData(),
         (err) => Log.error(err),
@@ -58,12 +58,11 @@ class CheckboxCellBloc extends Bloc<CheckboxCellEvent, CheckboxCellState> {
       fieldId: state.cellData.field.id,
       rowId: state.cellData.rowId,
     );
+    if (isClosed) {
+      return;
+    }
     result.fold(
-      (cell) {
-        if (!isClosed) {
-          add(CheckboxCellEvent.didReceiveCellUpdate(cell));
-        }
-      },
+      (cell) => add(CheckboxCellEvent.didReceiveCellUpdate(cell)),
       (err) => Log.error(err),
     );
   }
@@ -88,11 +87,11 @@ class CheckboxCellEvent with _$CheckboxCellEvent {
 @freezed
 class CheckboxCellState with _$CheckboxCellState {
   const factory CheckboxCellState({
-    required CellData cellData,
+    required GridCellIdentifier cellData,
     required bool isSelected,
   }) = _CheckboxCellState;
 
-  factory CheckboxCellState.initial(CellData cellData) {
+  factory CheckboxCellState.initial(GridCellIdentifier cellData) {
     return CheckboxCellState(cellData: cellData, isSelected: _isSelected(cellData.cell));
   }
 }

+ 25 - 19
frontend/app_flowy/lib/workspace/application/grid/cell_bloc/date_cell_bloc.dart → frontend/app_flowy/lib/workspace/application/grid/cell/date_cell_bloc.dart

@@ -1,8 +1,8 @@
-import 'package:app_flowy/workspace/application/grid/cell_bloc/cell_listener.dart';
+import 'package:app_flowy/workspace/application/grid/cell/cell_listener.dart';
 import 'package:app_flowy/workspace/application/grid/field/field_listener.dart';
 import 'package:app_flowy/workspace/application/grid/row/row_service.dart';
 import 'package:flowy_sdk/log.dart';
-import 'package:flowy_sdk/protobuf/flowy-grid-data-model/grid.pb.dart' show Cell;
+import 'package:flowy_sdk/protobuf/flowy-grid-data-model/grid.pb.dart' show Cell, Field;
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:freezed_annotation/freezed_annotation.dart';
 import 'dart:async';
@@ -13,13 +13,13 @@ part 'date_cell_bloc.freezed.dart';
 class DateCellBloc extends Bloc<DateCellEvent, DateCellState> {
   final CellService _service;
   final CellListener _cellListener;
-  final FieldListener _fieldListener;
+  final SingleFieldListener _fieldListener;
 
-  DateCellBloc({required CellData cellData})
+  DateCellBloc({required GridCellIdentifier cellIdentifier})
       : _service = CellService(),
-        _cellListener = CellListener(rowId: cellData.rowId, fieldId: cellData.field.id),
-        _fieldListener = FieldListener(fieldId: cellData.field.id),
-        super(DateCellState.initial(cellData)) {
+        _cellListener = CellListener(rowId: cellIdentifier.rowId, fieldId: cellIdentifier.field.id),
+        _fieldListener = SingleFieldListener(fieldId: cellIdentifier.field.id),
+        super(DateCellState.initial(cellIdentifier)) {
     on<DateCellEvent>(
       (event, emit) async {
         event.map(
@@ -35,6 +35,10 @@ class DateCellBloc extends Bloc<DateCellEvent, DateCellState> {
               content: value.cell.content,
             ));
           },
+          didReceiveFieldUpdate: (_DidReceiveFieldUpdate value) {
+            emit(state.copyWith(field: value.field));
+            _loadCellData();
+          },
         );
       },
     );
@@ -48,20 +52,20 @@ class DateCellBloc extends Bloc<DateCellEvent, DateCellState> {
   }
 
   void _startListening() {
-    _cellListener.updateCellNotifier.addPublishListener((result) {
+    _cellListener.updateCellNotifier?.addPublishListener((result) {
       result.fold(
         (notificationData) => _loadCellData(),
         (err) => Log.error(err),
       );
-    });
+    }, listenWhen: () => !isClosed);
     _cellListener.start();
 
-    _fieldListener.updateFieldNotifier.addPublishListener((result) {
+    _fieldListener.updateFieldNotifier?.addPublishListener((result) {
       result.fold(
-        (field) => _loadCellData(),
+        (field) => add(DateCellEvent.didReceiveFieldUpdate(field)),
         (err) => Log.error(err),
       );
-    });
+    }, listenWhen: () => !isClosed);
     _fieldListener.start();
   }
 
@@ -71,12 +75,11 @@ class DateCellBloc extends Bloc<DateCellEvent, DateCellState> {
       fieldId: state.cellData.field.id,
       rowId: state.cellData.rowId,
     );
+    if (isClosed) {
+      return;
+    }
     result.fold(
-      (cell) {
-        if (!isClosed) {
-          add(DateCellEvent.didReceiveCellUpdate(cell));
-        }
-      },
+      (cell) => add(DateCellEvent.didReceiveCellUpdate(cell)),
       (err) => Log.error(err),
     );
   }
@@ -97,18 +100,21 @@ class DateCellEvent with _$DateCellEvent {
   const factory DateCellEvent.initial() = _InitialCell;
   const factory DateCellEvent.selectDay(DateTime day) = _SelectDay;
   const factory DateCellEvent.didReceiveCellUpdate(Cell cell) = _DidReceiveCellUpdate;
+  const factory DateCellEvent.didReceiveFieldUpdate(Field field) = _DidReceiveFieldUpdate;
 }
 
 @freezed
 class DateCellState with _$DateCellState {
   const factory DateCellState({
-    required CellData cellData,
+    required GridCellIdentifier cellData,
     required String content,
+    required Field field,
     DateTime? selectedDay,
   }) = _DateCellState;
 
-  factory DateCellState.initial(CellData cellData) => DateCellState(
+  factory DateCellState.initial(GridCellIdentifier cellData) => DateCellState(
         cellData: cellData,
+        field: cellData.field,
         content: cellData.cell?.content ?? "",
       );
 }

+ 39 - 28
frontend/app_flowy/lib/workspace/application/grid/cell_bloc/number_cell_bloc.dart → frontend/app_flowy/lib/workspace/application/grid/cell/number_cell_bloc.dart

@@ -1,4 +1,5 @@
-import 'package:app_flowy/workspace/application/grid/cell_bloc/cell_listener.dart';
+import 'package:app_flowy/workspace/application/grid/cell/cell_listener.dart';
+import 'package:app_flowy/workspace/application/grid/field/field_listener.dart';
 import 'package:app_flowy/workspace/application/grid/row/row_service.dart';
 import 'package:flowy_sdk/log.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid-data-model/grid.pb.dart';
@@ -11,12 +12,14 @@ part 'number_cell_bloc.freezed.dart';
 
 class NumberCellBloc extends Bloc<NumberCellEvent, NumberCellState> {
   final CellService _service;
-  final CellListener _listener;
+  final CellListener _cellListener;
+  final SingleFieldListener _fieldListener;
 
   NumberCellBloc({
-    required CellData cellData,
+    required GridCellIdentifier cellData,
   })  : _service = CellService(),
-        _listener = CellListener(rowId: cellData.rowId, fieldId: cellData.field.id),
+        _cellListener = CellListener(rowId: cellData.rowId, fieldId: cellData.field.id),
+        _fieldListener = SingleFieldListener(fieldId: cellData.field.id),
         super(NumberCellState.initial(cellData)) {
     on<NumberCellEvent>(
       (event, emit) async {
@@ -27,36 +30,36 @@ class NumberCellBloc extends Bloc<NumberCellEvent, NumberCellState> {
           didReceiveCellUpdate: (_DidReceiveCellUpdate value) {
             emit(state.copyWith(content: value.cell.content));
           },
-          updateCell: (_UpdateCell value) {
-            _updateCellValue(value, emit);
+          updateCell: (_UpdateCell value) async {
+            await _updateCellValue(value, emit);
           },
         );
       },
     );
   }
 
-  void _updateCellValue(_UpdateCell value, Emitter<NumberCellState> emit) {
-    final number = num.tryParse(value.text);
-    if (number == null) {
-      emit(state.copyWith(content: ""));
-    } else {
-      _service.updateCell(
-        gridId: state.cellData.gridId,
-        fieldId: state.cellData.field.id,
-        rowId: state.cellData.rowId,
-        data: value.text,
-      );
-    }
+  Future<void> _updateCellValue(_UpdateCell value, Emitter<NumberCellState> emit) async {
+    final result = await _service.updateCell(
+      gridId: state.cellData.gridId,
+      fieldId: state.cellData.field.id,
+      rowId: state.cellData.rowId,
+      data: value.text,
+    );
+    result.fold(
+      (field) => _getCellData(),
+      (err) => Log.error(err),
+    );
   }
 
   @override
   Future<void> close() async {
-    await _listener.stop();
+    await _cellListener.stop();
+    await _fieldListener.stop();
     return super.close();
   }
 
   void _startListening() {
-    _listener.updateCellNotifier.addPublishListener((result) {
+    _cellListener.updateCellNotifier?.addPublishListener((result) {
       result.fold(
         (notificationData) async {
           await _getCellData();
@@ -64,7 +67,15 @@ class NumberCellBloc extends Bloc<NumberCellEvent, NumberCellState> {
         (err) => Log.error(err),
       );
     });
-    _listener.start();
+    _cellListener.start();
+
+    _fieldListener.updateFieldNotifier?.addPublishListener((result) {
+      result.fold(
+        (field) => _getCellData(),
+        (err) => Log.error(err),
+      );
+    });
+    _fieldListener.start();
   }
 
   Future<void> _getCellData() async {
@@ -73,12 +84,12 @@ class NumberCellBloc extends Bloc<NumberCellEvent, NumberCellState> {
       fieldId: state.cellData.field.id,
       rowId: state.cellData.rowId,
     );
+
+    if (isClosed) {
+      return;
+    }
     result.fold(
-      (cell) {
-        if (!isClosed) {
-          add(NumberCellEvent.didReceiveCellUpdate(cell));
-        }
-      },
+      (cell) => add(NumberCellEvent.didReceiveCellUpdate(cell)),
       (err) => Log.error(err),
     );
   }
@@ -94,11 +105,11 @@ class NumberCellEvent with _$NumberCellEvent {
 @freezed
 class NumberCellState with _$NumberCellState {
   const factory NumberCellState({
-    required CellData cellData,
+    required GridCellIdentifier cellData,
     required String content,
   }) = _NumberCellState;
 
-  factory NumberCellState.initial(CellData cellData) {
+  factory NumberCellState.initial(GridCellIdentifier cellData) {
     return NumberCellState(cellData: cellData, content: cellData.cell?.content ?? "");
   }
 }

+ 0 - 0
frontend/app_flowy/lib/workspace/application/grid/cell_bloc/select_option_service.dart → frontend/app_flowy/lib/workspace/application/grid/cell/select_option_service.dart


+ 16 - 17
frontend/app_flowy/lib/workspace/application/grid/cell_bloc/selection_cell_bloc.dart → frontend/app_flowy/lib/workspace/application/grid/cell/selection_cell_bloc.dart

@@ -1,5 +1,5 @@
-import 'package:app_flowy/workspace/application/grid/cell_bloc/cell_listener.dart';
-import 'package:app_flowy/workspace/application/grid/cell_bloc/select_option_service.dart';
+import 'package:app_flowy/workspace/application/grid/cell/cell_listener.dart';
+import 'package:app_flowy/workspace/application/grid/cell/select_option_service.dart';
 import 'package:app_flowy/workspace/application/grid/field/field_listener.dart';
 import 'package:app_flowy/workspace/application/grid/row/row_service.dart';
 import 'package:flowy_sdk/log.dart';
@@ -13,13 +13,13 @@ part 'selection_cell_bloc.freezed.dart';
 class SelectionCellBloc extends Bloc<SelectionCellEvent, SelectionCellState> {
   final SelectOptionService _service;
   final CellListener _cellListener;
-  final FieldListener _fieldListener;
+  final SingleFieldListener _fieldListener;
 
   SelectionCellBloc({
-    required CellData cellData,
+    required GridCellIdentifier cellData,
   })  : _service = SelectOptionService(),
         _cellListener = CellListener(rowId: cellData.rowId, fieldId: cellData.field.id),
-        _fieldListener = FieldListener(fieldId: cellData.field.id),
+        _fieldListener = SingleFieldListener(fieldId: cellData.field.id),
         super(SelectionCellState.initial(cellData)) {
     on<SelectionCellEvent>(
       (event, emit) async {
@@ -49,22 +49,21 @@ class SelectionCellBloc extends Bloc<SelectionCellEvent, SelectionCellState> {
       fieldId: state.cellData.field.id,
       rowId: state.cellData.rowId,
     );
+    if (isClosed) {
+      return;
+    }
 
     result.fold(
-      (selectOptionContext) {
-        if (!isClosed) {
-          add(SelectionCellEvent.didReceiveOptions(
-            selectOptionContext.options,
-            selectOptionContext.selectOptions,
-          ));
-        }
-      },
+      (selectOptionContext) => add(SelectionCellEvent.didReceiveOptions(
+        selectOptionContext.options,
+        selectOptionContext.selectOptions,
+      )),
       (err) => Log.error(err),
     );
   }
 
   void _startListening() {
-    _cellListener.updateCellNotifier.addPublishListener((result) {
+    _cellListener.updateCellNotifier?.addPublishListener((result) {
       result.fold(
         (notificationData) => _loadOptions(),
         (err) => Log.error(err),
@@ -72,7 +71,7 @@ class SelectionCellBloc extends Bloc<SelectionCellEvent, SelectionCellState> {
     });
     _cellListener.start();
 
-    _fieldListener.updateFieldNotifier.addPublishListener((result) {
+    _fieldListener.updateFieldNotifier?.addPublishListener((result) {
       result.fold(
         (field) => _loadOptions(),
         (err) => Log.error(err),
@@ -94,12 +93,12 @@ class SelectionCellEvent with _$SelectionCellEvent {
 @freezed
 class SelectionCellState with _$SelectionCellState {
   const factory SelectionCellState({
-    required CellData cellData,
+    required GridCellIdentifier cellData,
     required List<SelectOption> options,
     required List<SelectOption> selectedOptions,
   }) = _SelectionCellState;
 
-  factory SelectionCellState.initial(CellData cellData) => SelectionCellState(
+  factory SelectionCellState.initial(GridCellIdentifier cellData) => SelectionCellState(
         cellData: cellData,
         options: [],
         selectedOptions: [],

+ 15 - 16
frontend/app_flowy/lib/workspace/application/grid/cell_bloc/selection_editor_bloc.dart → frontend/app_flowy/lib/workspace/application/grid/cell/selection_editor_bloc.dart

@@ -1,4 +1,4 @@
-import 'package:app_flowy/workspace/application/grid/cell_bloc/cell_listener.dart';
+import 'package:app_flowy/workspace/application/grid/cell/cell_listener.dart';
 import 'package:app_flowy/workspace/application/grid/field/field_listener.dart';
 import 'package:app_flowy/workspace/application/grid/row/row_service.dart';
 import 'package:flowy_sdk/log.dart';
@@ -13,16 +13,16 @@ part 'selection_editor_bloc.freezed.dart';
 
 class SelectOptionEditorBloc extends Bloc<SelectOptionEditorEvent, SelectOptionEditorState> {
   final SelectOptionService _selectOptionService;
-  final FieldListener _fieldListener;
+  final SingleFieldListener _fieldListener;
   final CellListener _cellListener;
   Timer? _delayOperation;
 
   SelectOptionEditorBloc({
-    required CellData cellData,
+    required GridCellIdentifier cellData,
     required List<SelectOption> options,
     required List<SelectOption> selectedOptions,
   })  : _selectOptionService = SelectOptionService(),
-        _fieldListener = FieldListener(fieldId: cellData.field.id),
+        _fieldListener = SingleFieldListener(fieldId: cellData.field.id),
         _cellListener = CellListener(rowId: cellData.rowId, fieldId: cellData.field.id),
         super(SelectOptionEditorState.initial(cellData, options, selectedOptions)) {
     on<SelectOptionEditorEvent>(
@@ -117,16 +117,15 @@ class SelectOptionEditorBloc extends Bloc<SelectOptionEditorEvent, SelectOptionE
           fieldId: state.field.id,
           rowId: state.rowId,
         );
+        if (isClosed) {
+          return;
+        }
 
         result.fold(
-          (selectOptionContext) {
-            if (!isClosed) {
-              add(SelectOptionEditorEvent.didReceiveOptions(
-                selectOptionContext.options,
-                selectOptionContext.selectOptions,
-              ));
-            }
-          },
+          (selectOptionContext) => add(SelectOptionEditorEvent.didReceiveOptions(
+            selectOptionContext.options,
+            selectOptionContext.selectOptions,
+          )),
           (err) => Log.error(err),
         );
       },
@@ -134,7 +133,7 @@ class SelectOptionEditorBloc extends Bloc<SelectOptionEditorEvent, SelectOptionE
   }
 
   void _startListening() {
-    _cellListener.updateCellNotifier.addPublishListener((result) {
+    _cellListener.updateCellNotifier?.addPublishListener((result) {
       result.fold(
         (notificationData) => _loadOptions(),
         (err) => Log.error(err),
@@ -142,12 +141,12 @@ class SelectOptionEditorBloc extends Bloc<SelectOptionEditorEvent, SelectOptionE
     });
     _cellListener.start();
 
-    _fieldListener.updateFieldNotifier.addPublishListener((result) {
+    _fieldListener.updateFieldNotifier?.addPublishListener((result) {
       result.fold(
         (field) => add(SelectOptionEditorEvent.didReceiveFieldUpdate(field)),
         (err) => Log.error(err),
       );
-    });
+    }, listenWhen: () => !isClosed);
     _fieldListener.start();
   }
 }
@@ -175,7 +174,7 @@ class SelectOptionEditorState with _$SelectOptionEditorState {
   }) = _SelectOptionEditorState;
 
   factory SelectOptionEditorState.initial(
-    CellData cellData,
+    GridCellIdentifier cellData,
     List<SelectOption> options,
     List<SelectOption> selectedOptions,
   ) {

+ 4 - 4
frontend/app_flowy/lib/workspace/application/grid/cell_bloc/text_cell_bloc.dart → frontend/app_flowy/lib/workspace/application/grid/cell/text_cell_bloc.dart

@@ -11,7 +11,7 @@ class TextCellBloc extends Bloc<TextCellEvent, TextCellState> {
 
   TextCellBloc({
     required this.service,
-    required CellData cellData,
+    required GridCellIdentifier cellData,
   }) : super(TextCellState.initial(cellData)) {
     on<TextCellEvent>(
       (event, emit) async {
@@ -53,7 +53,7 @@ class TextCellBloc extends Bloc<TextCellEvent, TextCellState> {
 @freezed
 class TextCellEvent with _$TextCellEvent {
   const factory TextCellEvent.initial() = _InitialCell;
-  const factory TextCellEvent.didReceiveCellData(CellData cellData) = _DidReceiveCellData;
+  const factory TextCellEvent.didReceiveCellData(GridCellIdentifier cellData) = _DidReceiveCellData;
   const factory TextCellEvent.updateText(String text) = _UpdateText;
 }
 
@@ -61,10 +61,10 @@ class TextCellEvent with _$TextCellEvent {
 class TextCellState with _$TextCellState {
   const factory TextCellState({
     required String content,
-    required CellData cellData,
+    required GridCellIdentifier cellData,
   }) = _TextCellState;
 
-  factory TextCellState.initial(CellData cellData) => TextCellState(
+  factory TextCellState.initial(GridCellIdentifier cellData) => TextCellState(
         content: cellData.cell?.content ?? "",
         cellData: cellData,
       );

+ 0 - 35
frontend/app_flowy/lib/workspace/application/grid/cell_bloc/cell_service.dart

@@ -1,35 +0,0 @@
-import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
-import 'package:dartz/dartz.dart';
-import 'package:flowy_sdk/dispatch/dispatch.dart';
-import 'package:flowy_sdk/protobuf/flowy-grid-data-model/grid.pb.dart';
-import 'package:flowy_sdk/protobuf/flowy-grid/cell_entities.pb.dart';
-
-class CellService {
-  CellService();
-
-  Future<Either<void, FlowyError>> updateCell({
-    required String gridId,
-    required String fieldId,
-    required String rowId,
-    required String data,
-  }) {
-    final payload = CellChangeset.create()
-      ..gridId = gridId
-      ..fieldId = fieldId
-      ..rowId = rowId
-      ..data = data;
-    return GridEventUpdateCell(payload).send();
-  }
-
-  Future<Either<Cell, FlowyError>> getCell({
-    required String gridId,
-    required String fieldId,
-    required String rowId,
-  }) {
-    final payload = CellIdentifierPayload.create()
-      ..gridId = gridId
-      ..fieldId = fieldId
-      ..rowId = rowId;
-    return GridEventGetCell(payload).send();
-  }
-}

+ 0 - 8
frontend/app_flowy/lib/workspace/application/grid/data.dart

@@ -1,8 +0,0 @@
-import 'package:flowy_sdk/protobuf/flowy-grid-data-model/grid.pb.dart';
-
-class GridHeaderData {
-  final String gridId;
-  final List<Field> fields;
-
-  GridHeaderData({required this.gridId, required this.fields});
-}

+ 14 - 4
frontend/app_flowy/lib/workspace/application/grid/field/field_cell_bloc.dart

@@ -9,11 +9,13 @@ import 'dart:async';
 part 'field_cell_bloc.freezed.dart';
 
 class FieldCellBloc extends Bloc<FieldCellEvent, FieldCellState> {
-  final FieldListener _fieldListener;
+  final SingleFieldListener _fieldListener;
+  final FieldService _fieldService;
 
   FieldCellBloc({
     required GridFieldCellContext cellContext,
-  })  : _fieldListener = FieldListener(fieldId: cellContext.field.id),
+  })  : _fieldListener = SingleFieldListener(fieldId: cellContext.field.id),
+        _fieldService = FieldService(gridId: cellContext.gridId),
         super(FieldCellState.initial(cellContext)) {
     on<FieldCellEvent>(
       (event, emit) async {
@@ -24,6 +26,13 @@ class FieldCellBloc extends Bloc<FieldCellEvent, FieldCellState> {
           didReceiveFieldUpdate: (_DidReceiveFieldUpdate value) {
             emit(state.copyWith(field: value.field));
           },
+          updateWidth: (_UpdateWidth value) {
+            final defaultWidth = state.field.width.toDouble();
+            final width = defaultWidth + value.offset;
+            if (width > defaultWidth && width < 300) {
+              _fieldService.updateField(fieldId: state.field.id, width: width);
+            }
+          },
         );
       },
     );
@@ -36,12 +45,12 @@ class FieldCellBloc extends Bloc<FieldCellEvent, FieldCellState> {
   }
 
   void _startListening() {
-    _fieldListener.updateFieldNotifier.addPublishListener((result) {
+    _fieldListener.updateFieldNotifier?.addPublishListener((result) {
       result.fold(
         (field) => add(FieldCellEvent.didReceiveFieldUpdate(field)),
         (err) => Log.error(err),
       );
-    });
+    }, listenWhen: () => !isClosed);
     _fieldListener.start();
   }
 }
@@ -50,6 +59,7 @@ class FieldCellBloc extends Bloc<FieldCellEvent, FieldCellState> {
 class FieldCellEvent with _$FieldCellEvent {
   const factory FieldCellEvent.initial() = _InitialCell;
   const factory FieldCellEvent.didReceiveFieldUpdate(Field field) = _DidReceiveFieldUpdate;
+  const factory FieldCellEvent.updateWidth(double offset) = _UpdateWidth;
 }
 
 @freezed

+ 7 - 6
frontend/app_flowy/lib/workspace/application/grid/field/field_listener.dart

@@ -9,12 +9,12 @@ import 'package:app_flowy/core/notification_helper.dart';
 
 typedef UpdateFieldNotifiedValue = Either<Field, FlowyError>;
 
-class FieldListener {
+class SingleFieldListener {
   final String fieldId;
-  PublishNotifier<UpdateFieldNotifiedValue> updateFieldNotifier = PublishNotifier();
+  PublishNotifier<UpdateFieldNotifiedValue>? updateFieldNotifier = PublishNotifier();
   GridNotificationListener? _listener;
 
-  FieldListener({required this.fieldId});
+  SingleFieldListener({required this.fieldId});
 
   void start() {
     _listener = GridNotificationListener(
@@ -30,8 +30,8 @@ class FieldListener {
     switch (ty) {
       case GridNotification.DidUpdateField:
         result.fold(
-          (payload) => updateFieldNotifier.value = left(Field.fromBuffer(payload)),
-          (error) => updateFieldNotifier.value = right(error),
+          (payload) => updateFieldNotifier?.value = left(Field.fromBuffer(payload)),
+          (error) => updateFieldNotifier?.value = right(error),
         );
         break;
       default:
@@ -41,6 +41,7 @@ class FieldListener {
 
   Future<void> stop() async {
     await _listener?.stop();
-    updateFieldNotifier.dispose();
+    updateFieldNotifier?.dispose();
+    updateFieldNotifier = null;
   }
 }

+ 28 - 12
frontend/app_flowy/lib/workspace/application/grid/field/field_service.dart

@@ -1,9 +1,10 @@
 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';
 import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
+import 'package:freezed_annotation/freezed_annotation.dart';
+part 'field_service.freezed.dart';
 
 class FieldService {
   final String gridId;
@@ -19,6 +20,26 @@ class FieldService {
     return GridEventSwitchToField(payload).send();
   }
 
+  Future<Either<EditFieldContext, FlowyError>> getEditFieldContext(String fieldId, FieldType fieldType) {
+    final payload = GetEditFieldContextPayload.create()
+      ..gridId = gridId
+      ..fieldId = fieldId
+      ..fieldType = fieldType;
+
+    return GridEventGetEditFieldContext(payload).send();
+  }
+
+  Future<Either<Unit, FlowyError>> moveField(String fieldId, int fromIndex, int toIndex) {
+    final payload = MoveItemPayload.create()
+      ..gridId = gridId
+      ..itemId = fieldId
+      ..ty = MoveItemType.MoveField
+      ..fromIndex = fromIndex
+      ..toIndex = toIndex;
+
+    return GridEventMoveItem(payload).send();
+  }
+
   Future<Either<Unit, FlowyError>> updateField({
     required String fieldId,
     String? name,
@@ -98,17 +119,12 @@ class FieldService {
   }
 }
 
-class GridFieldCellContext extends Equatable {
-  final String gridId;
-  final Field field;
-
-  const GridFieldCellContext({
-    required this.gridId,
-    required this.field,
-  });
-
-  @override
-  List<Object> get props => [field.id];
+@freezed
+class GridFieldCellContext with _$GridFieldCellContext {
+  const factory GridFieldCellContext({
+    required String gridId,
+    required Field field,
+  }) = _GridFieldCellContext;
 }
 
 abstract class EditFieldContextLoader {

+ 7 - 6
frontend/app_flowy/lib/workspace/application/grid/field/grid_listenr.dart

@@ -7,11 +7,11 @@ import 'dart:async';
 import 'dart:typed_data';
 import 'package:app_flowy/core/notification_helper.dart';
 
-typedef UpdateFieldNotifiedValue = Either<List<Field>, FlowyError>;
+typedef UpdateFieldNotifiedValue = Either<GridFieldChangeset, FlowyError>;
 
 class GridFieldsListener {
   final String gridId;
-  PublishNotifier<UpdateFieldNotifiedValue> updateFieldsNotifier = PublishNotifier();
+  PublishNotifier<UpdateFieldNotifiedValue>? updateFieldsNotifier = PublishNotifier();
   GridNotificationListener? _listener;
   GridFieldsListener({required this.gridId});
 
@@ -24,10 +24,10 @@ class GridFieldsListener {
 
   void _handler(GridNotification ty, Either<Uint8List, FlowyError> result) {
     switch (ty) {
-      case GridNotification.DidUpdateGrid:
+      case GridNotification.DidUpdateGridField:
         result.fold(
-          (payload) => updateFieldsNotifier.value = left(RepeatedField.fromBuffer(payload).items),
-          (error) => updateFieldsNotifier.value = right(error),
+          (payload) => updateFieldsNotifier?.value = left(GridFieldChangeset.fromBuffer(payload)),
+          (error) => updateFieldsNotifier?.value = right(error),
         );
         break;
       default:
@@ -37,6 +37,7 @@ class GridFieldsListener {
 
   Future<void> stop() async {
     await _listener?.stop();
-    updateFieldsNotifier.dispose();
+    updateFieldsNotifier?.dispose();
+    updateFieldsNotifier = null;
   }
 }

+ 26 - 113
frontend/app_flowy/lib/workspace/application/grid/grid_bloc.dart

@@ -1,13 +1,10 @@
 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 'field/grid_listenr.dart';
-import 'grid_listener.dart';
 import 'grid_service.dart';
 import 'row/row_service.dart';
 
@@ -15,20 +12,20 @@ part 'grid_bloc.freezed.dart';
 
 class GridBloc extends Bloc<GridEvent, GridState> {
   final GridService _gridService;
-  final GridListener _gridListener;
-  final GridFieldsListener _fieldListener;
+  final GridFieldCache fieldCache;
+  final GridRowCache rowCache;
 
   GridBloc({required View view})
-      : _fieldListener = GridFieldsListener(gridId: view.id),
-        _gridService = GridService(gridId: view.id),
-        _gridListener = GridListener(gridId: view.id),
+      : _gridService = GridService(gridId: view.id),
+        fieldCache = GridFieldCache(gridId: view.id),
+        rowCache = GridRowCache(gridId: view.id),
         super(GridState.initial(view.id)) {
     on<GridEvent>(
       (event, emit) async {
         await event.map(
           initial: (InitialGrid value) async {
-            await _initGrid(emit);
             _startListening();
+            await _loadGrid(emit);
           },
           createRow: (_CreateRow value) {
             _gridService.createRow();
@@ -37,12 +34,7 @@ class GridBloc extends Bloc<GridEvent, GridState> {
             emit(state.copyWith(rows: value.rows, listState: value.listState));
           },
           didReceiveFieldUpdate: (_DidReceiveFieldUpdate value) {
-            final rows = state.rows.map((row) => row.copyWith(fields: value.fields)).toList();
-            emit(state.copyWith(
-              rows: rows,
-              fields: value.fields,
-              listState: const GridListState.reload(),
-            ));
+            emit(state.copyWith(rows: rowCache.clonedRows, fields: value.fields));
           },
         );
       },
@@ -52,42 +44,21 @@ class GridBloc extends Bloc<GridEvent, GridState> {
   @override
   Future<void> close() async {
     await _gridService.closeGrid();
-    await _fieldListener.stop();
-    await _gridListener.stop();
+    await fieldCache.dispose();
+    await rowCache.dispose();
     return super.close();
   }
 
-  Future<void> _initGrid(Emitter<GridState> emit) async {
-    _fieldListener.updateFieldsNotifier.addPublishListener((result) {
-      result.fold(
-        (fields) => add(GridEvent.didReceiveFieldUpdate(fields)),
-        (err) => Log.error(err),
-      );
-    });
-    _fieldListener.start();
-
-    await _loadGrid(emit);
-  }
-
   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);
-          }
+    fieldCache.addListener(
+      onChanged: (fields) => add(GridEvent.didReceiveFieldUpdate(fields)),
+      listenWhen: () => !isClosed,
+    );
 
-          if (changeset.updatedRows.isNotEmpty) {
-            _updateRows(changeset.updatedRows);
-          }
-        }
-      }, (err) => Log.error(err));
-    });
-    _gridListener.start();
+    rowCache.addListener(
+      onChanged: (rows, listState) => add(GridEvent.didReceiveRowUpdate(rowCache.clonedRows, listState)),
+      listenWhen: () => !isClosed,
+    );
   }
 
   Future<void> _loadGrid(Emitter<GridState> emit) async {
@@ -105,10 +76,13 @@ class GridBloc extends Bloc<GridEvent, GridState> {
     return Future(
       () => result.fold(
         (fields) {
+          fieldCache.fields = fields.items;
+          rowCache.updateWithBlock(grid.blockOrders, fieldCache.unmodifiableFields);
+
           emit(state.copyWith(
             grid: Some(grid),
-            fields: fields.items,
-            rows: _buildRows(grid.blockOrders, fields.items),
+            fields: fieldCache.clonedFields,
+            rows: rowCache.clonedRows,
             loadingState: GridLoadingState.finish(left(unit)),
           ));
         },
@@ -116,67 +90,13 @@ class GridBloc extends Bloc<GridEvent, GridState> {
       ),
     );
   }
-
-  void _deleteRows(List<RowOrder> deletedRows) {
-    final List<RowData> rows = [];
-    final List<Tuple2<int, RowData>> deletedIndex = [];
-    final Map<String, RowOrder> deletedRowMap = {for (var rowOrder in deletedRows) rowOrder.rowId: rowOrder};
-    state.rows.asMap().forEach((index, value) {
-      if (deletedRowMap[value.rowId] == null) {
-        rows.add(value);
-      } else {
-        deletedIndex.add(Tuple2(index, value));
-      }
-    });
-
-    add(GridEvent.didReceiveRowUpdate(rows, GridListState.delete(deletedIndex)));
-  }
-
-  void _insertRows(List<IndexRowOrder> createdRows) {
-    final List<RowData> rows = List.from(state.rows);
-    List<int> insertIndexs = [];
-    for (final newRow in createdRows) {
-      if (newRow.hasIndex()) {
-        insertIndexs.add(newRow.index);
-        rows.insert(newRow.index, _toRowData(newRow.rowOrder));
-      } else {
-        insertIndexs.add(rows.length);
-        rows.add(_toRowData(newRow.rowOrder));
-      }
-    }
-    add(GridEvent.didReceiveRowUpdate(rows, GridListState.insert(insertIndexs)));
-  }
-
-  void _updateRows(List<RowOrder> updatedRows) {
-    final List<RowData> rows = List.from(state.rows);
-    final List<int> updatedIndexs = [];
-    for (final updatedRow in updatedRows) {
-      final index = rows.indexWhere((row) => row.rowId == updatedRow.rowId);
-      if (index != -1) {
-        rows.removeAt(index);
-        rows.insert(index, _toRowData(updatedRow));
-        updatedIndexs.add(index);
-      }
-    }
-    add(GridEvent.didReceiveRowUpdate(rows, const GridListState.reload()));
-  }
-
-  List<RowData> _buildRows(List<GridBlockOrder> blockOrders, List<Field> fields) {
-    return blockOrders.expand((blockOrder) => blockOrder.rowOrders).map((rowOrder) {
-      return RowData.fromBlockRow(state.gridId, rowOrder, fields);
-    }).toList();
-  }
-
-  RowData _toRowData(RowOrder rowOrder) {
-    return RowData.fromBlockRow(state.gridId, rowOrder, state.fields);
-  }
 }
 
 @freezed
 class GridEvent with _$GridEvent {
   const factory GridEvent.initial() = InitialGrid;
   const factory GridEvent.createRow() = _CreateRow;
-  const factory GridEvent.didReceiveRowUpdate(List<RowData> rows, GridListState listState) = _DidReceiveRowUpdate;
+  const factory GridEvent.didReceiveRowUpdate(List<GridRow> rows, GridRowChangeReason listState) = _DidReceiveRowUpdate;
   const factory GridEvent.didReceiveFieldUpdate(List<Field> fields) = _DidReceiveFieldUpdate;
 }
 
@@ -186,9 +106,9 @@ class GridState with _$GridState {
     required String gridId,
     required Option<Grid> grid,
     required List<Field> fields,
-    required List<RowData> rows,
+    required List<GridRow> rows,
     required GridLoadingState loadingState,
-    required GridListState listState,
+    required GridRowChangeReason listState,
   }) = _GridState;
 
   factory GridState.initial(String gridId) => GridState(
@@ -197,7 +117,7 @@ class GridState with _$GridState {
         grid: none(),
         gridId: gridId,
         loadingState: const _Loading(),
-        listState: const _Reload(),
+        listState: const InitialListState(),
       );
 }
 
@@ -206,10 +126,3 @@ class GridLoadingState with _$GridLoadingState {
   const factory GridLoadingState.loading() = _Loading;
   const factory GridLoadingState.finish(Either<Unit, FlowyError> successOrFail) = _Finish;
 }
-
-@freezed
-class GridListState with _$GridListState {
-  const factory GridListState.insert(List<int> indexs) = _Insert;
-  const factory GridListState.delete(List<Tuple2<int, RowData>> indexs) = _Delete;
-  const factory GridListState.reload() = _Reload;
-}

+ 19 - 24
frontend/app_flowy/lib/workspace/application/grid/field/grid_header_bloc.dart → frontend/app_flowy/lib/workspace/application/grid/grid_header_bloc.dart

@@ -1,54 +1,49 @@
-import 'package:app_flowy/workspace/application/grid/data.dart';
-import 'package:app_flowy/workspace/application/grid/field/grid_listenr.dart';
+import 'package:app_flowy/workspace/application/grid/field/field_service.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';
 import 'package:freezed_annotation/freezed_annotation.dart';
 import 'dart:async';
-import 'field_service.dart';
+import 'grid_service.dart';
 
 part 'grid_header_bloc.freezed.dart';
 
 class GridHeaderBloc extends Bloc<GridHeaderEvent, GridHeaderState> {
-  final FieldService service;
-  final GridFieldsListener _fieldListener;
+  final FieldService _fieldService;
+  final GridFieldCache fieldCache;
 
   GridHeaderBloc({
-    required GridHeaderData data,
-    required this.service,
-  })  : _fieldListener = GridFieldsListener(gridId: data.gridId),
-        super(GridHeaderState.initial(data.fields)) {
+    required String gridId,
+    required this.fieldCache,
+  })  : _fieldService = FieldService(gridId: gridId),
+        super(GridHeaderState.initial(fieldCache.clonedFields)) {
     on<GridHeaderEvent>(
       (event, emit) async {
         await event.map(
           initial: (_InitialHeader value) async {
             _startListening();
           },
-          createField: (_CreateField value) {},
-          insertField: (_InsertField value) {},
           didReceiveFieldUpdate: (_DidReceiveFieldUpdate value) {
-            value.fields.retainWhere((field) => field.visibility);
             emit(state.copyWith(fields: value.fields));
           },
+          moveField: (_MoveField value) async {
+            final result = await _fieldService.moveField(value.field.id, value.fromIndex, value.toIndex);
+            result.fold((l) {}, (err) => Log.error(err));
+          },
         );
       },
     );
   }
 
   Future<void> _startListening() async {
-    _fieldListener.updateFieldsNotifier.addPublishListener((result) {
-      result.fold(
-        (fields) => add(GridHeaderEvent.didReceiveFieldUpdate(fields)),
-        (err) => Log.error(err),
-      );
-    });
-
-    _fieldListener.start();
+    fieldCache.addListener(
+      onChanged: (fields) => add(GridHeaderEvent.didReceiveFieldUpdate(fields)),
+      listenWhen: () => !isClosed,
+    );
   }
 
   @override
   Future<void> close() async {
-    await _fieldListener.stop();
     return super.close();
   }
 }
@@ -56,9 +51,8 @@ class GridHeaderBloc extends Bloc<GridHeaderEvent, GridHeaderState> {
 @freezed
 class GridHeaderEvent with _$GridHeaderEvent {
   const factory GridHeaderEvent.initial() = _InitialHeader;
-  const factory GridHeaderEvent.createField() = _CreateField;
-  const factory GridHeaderEvent.insertField({required bool onLeft}) = _InsertField;
   const factory GridHeaderEvent.didReceiveFieldUpdate(List<Field> fields) = _DidReceiveFieldUpdate;
+  const factory GridHeaderEvent.moveField(Field field, int fromIndex, int toIndex) = _MoveField;
 }
 
 @freezed
@@ -66,7 +60,8 @@ class GridHeaderState with _$GridHeaderState {
   const factory GridHeaderState({required List<Field> fields}) = _GridHeaderState;
 
   factory GridHeaderState.initial(List<Field> fields) {
-    fields.retainWhere((field) => field.visibility);
+    // final List<Field> newFields = List.from(fields);
+    // newFields.retainWhere((field) => field.visibility);
     return GridHeaderState(fields: fields);
   }
 }

+ 5 - 6
frontend/app_flowy/lib/workspace/application/grid/grid_listener.dart

@@ -7,13 +7,12 @@ import 'dart:async';
 import 'dart:typed_data';
 import 'package:app_flowy/core/notification_helper.dart';
 
-class GridListener {
+class GridRowListener {
   final String gridId;
-  PublishNotifier<Either<List<GridBlockOrderChangeset>, FlowyError>> rowsUpdateNotifier =
-      PublishNotifier(comparable: null);
+  PublishNotifier<Either<List<GridRowsChangeset>, FlowyError>> rowsUpdateNotifier = PublishNotifier(comparable: null);
   GridNotificationListener? _listener;
 
-  GridListener({required this.gridId});
+  GridRowListener({required this.gridId});
 
   void start() {
     _listener = GridNotificationListener(
@@ -24,9 +23,9 @@ class GridListener {
 
   void _handler(GridNotification ty, Either<Uint8List, FlowyError> result) {
     switch (ty) {
-      case GridNotification.DidUpdateGridBlock:
+      case GridNotification.DidUpdateGridRow:
         result.fold(
-          (payload) => rowsUpdateNotifier.value = left([GridBlockOrderChangeset.fromBuffer(payload)]),
+          (payload) => rowsUpdateNotifier.value = left([GridRowsChangeset.fromBuffer(payload)]),
           (error) => rowsUpdateNotifier.value = right(error),
         );
         break;

+ 117 - 0
frontend/app_flowy/lib/workspace/application/grid/grid_service.dart

@@ -1,8 +1,12 @@
+import 'package:app_flowy/workspace/application/grid/field/grid_listenr.dart';
 import 'package:dartz/dartz.dart';
 import 'package:flowy_sdk/dispatch/dispatch.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/grid.pb.dart';
+import 'package:flutter/foundation.dart';
+import 'package:freezed_annotation/freezed_annotation.dart';
 
 class GridService {
   final String gridId;
@@ -35,3 +39,116 @@ class GridService {
     return FolderEventCloseView(request).send();
   }
 }
+
+class FieldsNotifier extends ChangeNotifier {
+  List<Field> _fields = [];
+
+  set fields(List<Field> fields) {
+    _fields = fields;
+    notifyListeners();
+  }
+
+  List<Field> get fields => _fields;
+}
+
+class GridFieldCache {
+  final String gridId;
+  late final GridFieldsListener _fieldListener;
+  final FieldsNotifier _fieldNotifier = FieldsNotifier();
+  GridFieldCache({required this.gridId}) {
+    _fieldListener = GridFieldsListener(gridId: gridId);
+    _fieldListener.updateFieldsNotifier?.addPublishListener((result) {
+      result.fold(
+        (changeset) {
+          _deleteFields(changeset.deletedFields);
+          _insertFields(changeset.insertedFields);
+          _updateFields(changeset.updatedFields);
+        },
+        (err) => Log.error(err),
+      );
+    });
+    _fieldListener.start();
+  }
+
+  Future<void> dispose() async {
+    await _fieldListener.stop();
+    _fieldNotifier.dispose();
+  }
+
+  void applyChangeset(GridFieldChangeset changeset) {}
+
+  UnmodifiableListView<Field> get unmodifiableFields => UnmodifiableListView(_fieldNotifier.fields);
+
+  List<Field> get clonedFields => [..._fieldNotifier.fields];
+
+  set fields(List<Field> fields) {
+    _fieldNotifier.fields = [...fields];
+  }
+
+  VoidCallback addListener(
+      {VoidCallback? listener, void Function(List<Field>)? onChanged, bool Function()? listenWhen}) {
+    f() {
+      if (listenWhen != null && listenWhen() == false) {
+        return;
+      }
+
+      if (onChanged != null) {
+        onChanged(clonedFields);
+      }
+
+      if (listener != null) {
+        listener();
+      }
+    }
+
+    _fieldNotifier.addListener(f);
+    return f;
+  }
+
+  void removeListener(VoidCallback f) {
+    _fieldNotifier.removeListener(f);
+  }
+
+  void _deleteFields(List<FieldOrder> deletedFields) {
+    if (deletedFields.isEmpty) {
+      return;
+    }
+    final List<Field> fields = _fieldNotifier.fields;
+    final Map<String, FieldOrder> deletedFieldMap = {
+      for (var fieldOrder in deletedFields) fieldOrder.fieldId: fieldOrder
+    };
+
+    fields.retainWhere((field) => (deletedFieldMap[field.id] == null));
+    _fieldNotifier.fields = fields;
+  }
+
+  void _insertFields(List<IndexField> insertedFields) {
+    if (insertedFields.isEmpty) {
+      return;
+    }
+    final List<Field> fields = _fieldNotifier.fields;
+    for (final indexField in insertedFields) {
+      if (fields.length > indexField.index) {
+        fields.insert(indexField.index, indexField.field_1);
+      } else {
+        fields.add(indexField.field_1);
+      }
+    }
+    _fieldNotifier.fields = fields;
+  }
+
+  void _updateFields(List<Field> updatedFields) {
+    if (updatedFields.isEmpty) {
+      return;
+    }
+    final List<Field> fields = _fieldNotifier.fields;
+    for (final updatedField in updatedFields) {
+      final index = fields.indexWhere((field) => field.id == updatedField.id);
+      if (index != -1) {
+        fields.removeAt(index);
+        fields.insert(index, updatedField);
+      }
+    }
+    _fieldNotifier.fields = fields;
+  }
+}

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

@@ -2,11 +2,10 @@ export 'grid_bloc.dart';
 export 'row/row_bloc.dart';
 export 'row/row_service.dart';
 export 'grid_service.dart';
-export 'data.dart';
+export 'grid_header_bloc.dart';
 
 // Field
 export 'field/field_service.dart';
-export 'field/grid_header_bloc.dart';
 export 'field/field_action_sheet_bloc.dart';
 export 'field/field_editor_bloc.dart';
 export 'field/field_switch_bloc.dart';
@@ -17,12 +16,12 @@ export 'field/type_option/number_bloc.dart';
 export 'field/type_option/single_select_bloc.dart';
 
 // Cell
-export 'cell_bloc/text_cell_bloc.dart';
-export 'cell_bloc/number_cell_bloc.dart';
-export 'cell_bloc/selection_cell_bloc.dart';
-export 'cell_bloc/date_cell_bloc.dart';
-export 'cell_bloc/checkbox_cell_bloc.dart';
-export 'cell_bloc/cell_service.dart';
+export 'cell/text_cell_bloc.dart';
+export 'cell/number_cell_bloc.dart';
+export 'cell/selection_cell_bloc.dart';
+export 'cell/date_cell_bloc.dart';
+export 'cell/checkbox_cell_bloc.dart';
+export 'cell/cell_service.dart';
 
 // Setting
 export 'setting/setting_bloc.dart';

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

@@ -11,7 +11,7 @@ part 'row_action_sheet_bloc.freezed.dart';
 class RowActionSheetBloc extends Bloc<RowActionSheetEvent, RowActionSheetState> {
   final RowService _rowService;
 
-  RowActionSheetBloc({required RowData rowData})
+  RowActionSheetBloc({required GridRow rowData})
       : _rowService = RowService(gridId: rowData.gridId, rowId: rowData.rowId),
         super(RowActionSheetState.initial(rowData)) {
     on<RowActionSheetEvent>(
@@ -49,10 +49,10 @@ class RowActionSheetEvent with _$RowActionSheetEvent {
 @freezed
 class RowActionSheetState with _$RowActionSheetState {
   const factory RowActionSheetState({
-    required RowData rowData,
+    required GridRow rowData,
   }) = _RowActionSheetState;
 
-  factory RowActionSheetState.initial(RowData rowData) => RowActionSheetState(
+  factory RowActionSheetState.initial(GridRow rowData) => RowActionSheetState(
         rowData: rowData,
       );
 }

+ 54 - 49
frontend/app_flowy/lib/workspace/application/grid/row/row_bloc.dart

@@ -1,111 +1,115 @@
 import 'dart:collection';
 
-import 'package:app_flowy/workspace/application/grid/field/grid_listenr.dart';
-import 'package:flowy_sdk/log.dart';
+import 'package:app_flowy/workspace/application/grid/grid_service.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid-data-model/grid.pb.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:freezed_annotation/freezed_annotation.dart';
 import 'dart:async';
-import 'row_listener.dart';
 import 'row_service.dart';
 import 'package:dartz/dartz.dart';
 
 part 'row_bloc.freezed.dart';
 
-typedef CellDataMap = LinkedHashMap<String, CellData>;
+typedef CellDataMap = LinkedHashMap<String, GridCellIdentifier>;
 
 class RowBloc extends Bloc<RowEvent, RowState> {
   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),
+  final GridFieldCache _fieldCache;
+  final GridRowCache _rowCache;
+  void Function()? _rowListenCallback;
+  void Function()? _fieldListenCallback;
+
+  RowBloc({
+    required GridRow rowData,
+    required GridFieldCache fieldCache,
+    required GridRowCache rowCache,
+  })  : _rowService = RowService(gridId: rowData.gridId, rowId: rowData.rowId),
+        _fieldCache = fieldCache,
+        _rowCache = rowCache,
         super(RowState.initial(rowData)) {
     on<RowEvent>(
       (event, emit) async {
         await event.map(
           initial: (_InitialRow value) async {
-            _startListening();
+            await _startListening();
             await _loadRow(emit);
           },
           createRow: (_CreateRow value) {
             _rowService.createRow();
           },
-          didReceiveFieldUpdate: (_DidReceiveFieldUpdate value) async {
-            await _handleFieldUpdate(emit, value);
-          },
           didUpdateRow: (_DidUpdateRow value) async {
-            _handleRowUpdate(value, emit);
+            _handleRowUpdate(value.row, emit);
+          },
+          fieldsDidUpdate: (_FieldsDidUpdate value) async {
+            await _handleFieldUpdate(emit);
+          },
+          didLoadRow: (_DidLoadRow value) {
+            _handleRowUpdate(value.row, emit);
           },
         );
       },
     );
   }
 
-  void _handleRowUpdate(_DidUpdateRow value, Emitter<RowState> emit) {
-    final CellDataMap cellDataMap = _makeCellDatas(value.row, state.rowData.fields);
+  void _handleRowUpdate(Row row, Emitter<RowState> emit) {
+    final CellDataMap cellDataMap = _makeCellDatas(row, state.rowData.fields);
     emit(state.copyWith(
-      row: Future(() => Some(value.row)),
+      row: Future(() => Some(row)),
       cellDataMap: Some(cellDataMap),
     ));
   }
 
-  Future<void> _handleFieldUpdate(Emitter<RowState> emit, _DidReceiveFieldUpdate value) async {
+  Future<void> _handleFieldUpdate(Emitter<RowState> emit) async {
     final optionRow = await state.row;
     final CellDataMap cellDataMap = optionRow.fold(
       () => CellDataMap.identity(),
-      (row) => _makeCellDatas(row, value.fields),
+      (row) => _makeCellDatas(row, _fieldCache.unmodifiableFields),
     );
 
     emit(state.copyWith(
-      rowData: state.rowData.copyWith(fields: value.fields),
+      rowData: state.rowData.copyWith(fields: _fieldCache.unmodifiableFields),
       cellDataMap: Some(cellDataMap),
     ));
   }
 
   @override
   Future<void> close() async {
-    await _rowlistener.stop();
-    await _fieldListener.stop();
+    if (_rowListenCallback != null) {
+      _rowCache.removeRowListener(_rowListenCallback!);
+    }
+
+    if (_fieldListenCallback != null) {
+      _fieldCache.removeListener(_fieldListenCallback!);
+    }
     return super.close();
   }
 
   Future<void> _startListening() async {
-    _rowlistener.updateRowNotifier.addPublishListener((result) {
-      result.fold(
-        (row) => add(RowEvent.didUpdateRow(row)),
-        (err) => Log.error(err),
-      );
-    });
-
-    _fieldListener.updateFieldsNotifier.addPublishListener((result) {
-      result.fold(
-        (fields) => add(RowEvent.didReceiveFieldUpdate(fields)),
-        (err) => Log.error(err),
-      );
-    });
+    _fieldListenCallback = _fieldCache.addListener(
+      listener: () => add(const RowEvent.fieldsDidUpdate()),
+      listenWhen: () => !isClosed,
+    );
 
-    _rowlistener.start();
-    _fieldListener.start();
+    _rowListenCallback = _rowCache.addRowListener(
+      rowId: state.rowData.rowId,
+      onUpdated: (row) => add(RowEvent.didUpdateRow(row)),
+      listenWhen: () => !isClosed,
+    );
   }
 
   Future<void> _loadRow(Emitter<RowState> emit) async {
-    _rowService.getRow().then((result) {
-      return result.fold(
-        (row) => add(RowEvent.didUpdateRow(row)),
-        (err) => Log.error(err),
-      );
-    });
+    final data = await _rowCache.getRowData(state.rowData.rowId);
+    if (isClosed) {
+      return;
+    }
+    data.foldRight(null, (data, _) => add(RowEvent.didLoadRow(data)));
   }
 
   CellDataMap _makeCellDatas(Row row, List<Field> fields) {
     var map = CellDataMap.new();
     for (final field in fields) {
       if (field.visibility) {
-        map[field.id] = CellData(
+        map[field.id] = GridCellIdentifier(
           rowId: row.id,
           gridId: _rowService.gridId,
           cell: row.cellByFieldId[field.id],
@@ -121,19 +125,20 @@ class RowBloc extends Bloc<RowEvent, RowState> {
 class RowEvent with _$RowEvent {
   const factory RowEvent.initial() = _InitialRow;
   const factory RowEvent.createRow() = _CreateRow;
-  const factory RowEvent.didReceiveFieldUpdate(List<Field> fields) = _DidReceiveFieldUpdate;
+  const factory RowEvent.fieldsDidUpdate() = _FieldsDidUpdate;
+  const factory RowEvent.didLoadRow(Row row) = _DidLoadRow;
   const factory RowEvent.didUpdateRow(Row row) = _DidUpdateRow;
 }
 
 @freezed
 class RowState with _$RowState {
   const factory RowState({
-    required RowData rowData,
+    required GridRow rowData,
     required Future<Option<Row>> row,
     required Option<CellDataMap> cellDataMap,
   }) = _RowState;
 
-  factory RowState.initial(RowData rowData) => RowState(
+  factory RowState.initial(GridRow rowData) => RowState(
         rowData: rowData,
         row: Future(() => none()),
         cellDataMap: none(),

+ 5 - 4
frontend/app_flowy/lib/workspace/application/grid/row/row_listener.dart

@@ -12,7 +12,7 @@ typedef UpdateFieldNotifiedValue = Either<List<Field>, FlowyError>;
 
 class RowListener {
   final String rowId;
-  PublishNotifier<UpdateRowNotifiedValue> updateRowNotifier = PublishNotifier();
+  PublishNotifier<UpdateRowNotifiedValue>? updateRowNotifier = PublishNotifier();
   GridNotificationListener? _listener;
 
   RowListener({required this.rowId});
@@ -25,8 +25,8 @@ class RowListener {
     switch (ty) {
       case GridNotification.DidUpdateRow:
         result.fold(
-          (payload) => updateRowNotifier.value = left(Row.fromBuffer(payload)),
-          (error) => updateRowNotifier.value = right(error),
+          (payload) => updateRowNotifier?.value = left(Row.fromBuffer(payload)),
+          (error) => updateRowNotifier?.value = right(error),
         );
         break;
       default:
@@ -36,6 +36,7 @@ class RowListener {
 
   Future<void> stop() async {
     await _listener?.stop();
-    updateRowNotifier.dispose();
+    updateRowNotifier?.dispose();
+    updateRowNotifier = null;
   }
 }

+ 254 - 8
frontend/app_flowy/lib/workspace/application/grid/row/row_service.dart

@@ -1,12 +1,206 @@
+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-error/errors.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid-data-model/grid.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid/row_entities.pb.dart';
+import 'package:flutter/foundation.dart';
 import 'package:freezed_annotation/freezed_annotation.dart';
+import 'package:app_flowy/workspace/application/grid/grid_listener.dart';
 
 part 'row_service.freezed.dart';
 
+class RowsNotifier extends ChangeNotifier {
+  List<GridRow> _rows = [];
+  GridRowChangeReason _changeReason = const InitialListState();
+
+  void updateRows(List<GridRow> rows, GridRowChangeReason changeReason) {
+    _rows = rows;
+    _changeReason = changeReason;
+
+    changeReason.map(
+      insert: (_) => notifyListeners(),
+      delete: (_) => notifyListeners(),
+      update: (_) => notifyListeners(),
+      initial: (_) {},
+    );
+  }
+
+  List<GridRow> get rows => _rows;
+}
+
+class GridRowCache {
+  final String gridId;
+  final GridRowListener _rowsListener;
+  final RowsNotifier _rowNotifier = RowsNotifier();
+  final HashMap<String, Row> _rowDataMap = HashMap();
+  UnmodifiableListView<Field> _fields = UnmodifiableListView([]);
+
+  GridRowCache({required this.gridId}) : _rowsListener = GridRowListener(gridId: gridId) {
+    _rowsListener.rowsUpdateNotifier.addPublishListener((result) {
+      result.fold(
+        (changesets) {
+          for (final changeset in changesets) {
+            _deleteRows(changeset.deletedRows);
+            _insertRows(changeset.insertedRows);
+            _updateRows(changeset.updatedRows);
+          }
+        },
+        (err) => Log.error(err),
+      );
+    });
+    _rowsListener.start();
+  }
+
+  Future<void> dispose() async {
+    await _rowsListener.stop();
+    _rowNotifier.dispose();
+  }
+
+  List<GridRow> get clonedRows => [..._rowNotifier.rows];
+
+  void addListener({
+    void Function(List<GridRow>, GridRowChangeReason)? onChanged,
+    bool Function()? listenWhen,
+  }) {
+    _rowNotifier.addListener(() {
+      if (listenWhen != null && listenWhen() == false) {
+        return;
+      }
+
+      if (onChanged != null) {
+        onChanged(clonedRows, _rowNotifier._changeReason);
+      }
+    });
+  }
+
+  VoidCallback addRowListener({
+    required String rowId,
+    void Function(Row)? onUpdated,
+    bool Function()? listenWhen,
+  }) {
+    f() {
+      if (onUpdated == null) {
+        return;
+      }
+
+      if (listenWhen != null && listenWhen() == false) {
+        return;
+      }
+
+      _rowNotifier._changeReason.whenOrNull(update: (indexs) {
+        final row = _rowDataMap[rowId];
+        if (indexs[rowId] != null && row != null) {
+          onUpdated(row);
+        }
+      });
+    }
+
+    _rowNotifier.addListener(f);
+    return f;
+  }
+
+  void removeRowListener(VoidCallback callback) {
+    _rowNotifier.removeListener(callback);
+  }
+
+  Future<Option<Row>> getRowData(String rowId) async {
+    final Row? data = _rowDataMap[rowId];
+    if (data != null) {
+      return Future(() => Some(data));
+    }
+
+    final payload = RowIdentifierPayload.create()
+      ..gridId = gridId
+      ..rowId = rowId;
+
+    final result = await GridEventGetRow(payload).send();
+    return Future(() {
+      return result.fold(
+        (data) {
+          data.freeze();
+          _rowDataMap[data.id] = data;
+          return Some(data);
+        },
+        (err) {
+          Log.error(err);
+          return none();
+        },
+      );
+    });
+  }
+
+  void updateWithBlock(List<GridBlockOrder> blocks, UnmodifiableListView<Field> fields) {
+    _fields = fields;
+    final newRows = blocks.expand((block) => block.rowOrders).map((rowOrder) {
+      return GridRow.fromBlockRow(gridId, rowOrder, _fields);
+    }).toList();
+
+    _rowNotifier.updateRows(newRows, const GridRowChangeReason.initial());
+  }
+
+  void _deleteRows(List<RowOrder> deletedRows) {
+    if (deletedRows.isEmpty) {
+      return;
+    }
+
+    final List<GridRow> newRows = [];
+    final DeletedIndexs deletedIndex = [];
+    final Map<String, RowOrder> deletedRowMap = {for (var rowOrder in deletedRows) rowOrder.rowId: rowOrder};
+
+    _rowNotifier.rows.asMap().forEach((index, row) {
+      if (deletedRowMap[row.rowId] == null) {
+        newRows.add(row);
+      } else {
+        deletedIndex.add(DeletedIndex(index: index, row: row));
+      }
+    });
+
+    _rowNotifier.updateRows(newRows, GridRowChangeReason.delete(deletedIndex));
+  }
+
+  void _insertRows(List<IndexRowOrder> createdRows) {
+    if (createdRows.isEmpty) {
+      return;
+    }
+
+    InsertedIndexs insertIndexs = [];
+    final List<GridRow> newRows = _rowNotifier.rows;
+    for (final createdRow in createdRows) {
+      final gridRow = GridRow.fromBlockRow(gridId, createdRow.rowOrder, _fields);
+      insertIndexs.add(
+        InsertedIndex(
+          index: createdRow.index,
+          rowId: gridRow.rowId,
+        ),
+      );
+      newRows.insert(createdRow.index, gridRow);
+    }
+    _rowNotifier.updateRows(newRows, GridRowChangeReason.insert(insertIndexs));
+  }
+
+  void _updateRows(List<RowOrder> updatedRows) {
+    if (updatedRows.isEmpty) {
+      return;
+    }
+
+    final UpdatedIndexs updatedIndexs = UpdatedIndexs();
+    final List<GridRow> newRows = _rowNotifier.rows;
+    for (final rowOrder in updatedRows) {
+      final index = newRows.indexWhere((row) => row.rowId == rowOrder.rowId);
+      if (index != -1) {
+        newRows.removeAt(index);
+        newRows.insert(index, GridRow.fromBlockRow(gridId, rowOrder, _fields));
+        _rowDataMap.remove(rowOrder.rowId);
+        updatedIndexs[rowOrder.rowId] = UpdatedIndex(index: index, rowId: rowOrder.rowId);
+      }
+    }
+
+    _rowNotifier.updateRows(newRows, GridRowChangeReason.update(updatedIndexs));
+  }
+}
+
 class RowService {
   final String gridId;
   final String rowId;
@@ -21,6 +215,17 @@ class RowService {
     return GridEventCreateRow(payload).send();
   }
 
+  Future<Either<Unit, FlowyError>> moveRow(String rowId, int fromIndex, int toIndex) {
+    final payload = MoveItemPayload.create()
+      ..gridId = gridId
+      ..itemId = rowId
+      ..ty = MoveItemType.MoveRow
+      ..fromIndex = fromIndex
+      ..toIndex = toIndex;
+
+    return GridEventMoveItem(payload).send();
+  }
+
   Future<Either<Row, FlowyError>> getRow() {
     final payload = RowIdentifierPayload.create()
       ..gridId = gridId
@@ -47,8 +252,8 @@ class RowService {
 }
 
 @freezed
-class CellData with _$CellData {
-  const factory CellData({
+class GridCellIdentifier with _$GridCellIdentifier {
+  const factory GridCellIdentifier({
     required String gridId,
     required String rowId,
     required Field field,
@@ -57,20 +262,61 @@ class CellData with _$CellData {
 }
 
 @freezed
-class RowData with _$RowData {
-  const factory RowData({
+class GridRow with _$GridRow {
+  const factory GridRow({
     required String gridId,
     required String rowId,
     required List<Field> fields,
     required double height,
-  }) = _RowData;
+    required Future<Option<Row>> data,
+  }) = _GridRow;
 
-  factory RowData.fromBlockRow(String gridId, RowOrder row, List<Field> fields) {
-    return RowData(
+  factory GridRow.fromBlockRow(String gridId, RowOrder row, List<Field> fields) {
+    return GridRow(
       gridId: gridId,
-      rowId: row.rowId,
       fields: fields,
+      rowId: row.rowId,
+      data: Future(() => none()),
       height: row.height.toDouble(),
     );
   }
 }
+
+typedef InsertedIndexs = List<InsertedIndex>;
+typedef DeletedIndexs = List<DeletedIndex>;
+typedef UpdatedIndexs = LinkedHashMap<String, UpdatedIndex>;
+
+class InsertedIndex {
+  int index;
+  String rowId;
+  InsertedIndex({
+    required this.index,
+    required this.rowId,
+  });
+}
+
+class DeletedIndex {
+  int index;
+  GridRow row;
+  DeletedIndex({
+    required this.index,
+    required this.row,
+  });
+}
+
+class UpdatedIndex {
+  int index;
+  String rowId;
+  UpdatedIndex({
+    required this.index,
+    required this.rowId,
+  });
+}
+
+@freezed
+class GridRowChangeReason with _$GridRowChangeReason {
+  const factory GridRowChangeReason.insert(InsertedIndexs items) = _Insert;
+  const factory GridRowChangeReason.delete(DeletedIndexs items) = _Delete;
+  const factory GridRowChangeReason.update(UpdatedIndexs indexs) = _Update;
+  const factory GridRowChangeReason.initial() = InitialListState;
+}

+ 17 - 15
frontend/app_flowy/lib/workspace/application/grid/setting/property_bloc.dart

@@ -1,5 +1,5 @@
 import 'package:app_flowy/workspace/application/grid/field/field_service.dart';
-import 'package:app_flowy/workspace/application/grid/field/grid_listenr.dart';
+import 'package:app_flowy/workspace/application/grid/grid_service.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';
@@ -10,12 +10,13 @@ part 'property_bloc.freezed.dart';
 
 class GridPropertyBloc extends Bloc<GridPropertyEvent, GridPropertyState> {
   final FieldService _service;
-  final GridFieldsListener _fieldListener;
+  final GridFieldCache _fieldCache;
+  Function()? _listenFieldCallback;
 
-  GridPropertyBloc({required String gridId, required List<Field> fields})
+  GridPropertyBloc({required String gridId, required GridFieldCache fieldCache})
       : _service = FieldService(gridId: gridId),
-        _fieldListener = GridFieldsListener(gridId: gridId),
-        super(GridPropertyState.initial(gridId, fields)) {
+        _fieldCache = fieldCache,
+        super(GridPropertyState.initial(gridId, fieldCache.clonedFields)) {
     on<GridPropertyEvent>(
       (event, emit) async {
         await event.map(
@@ -32,6 +33,9 @@ class GridPropertyBloc extends Bloc<GridPropertyEvent, GridPropertyState> {
           didReceiveFieldUpdate: (_DidReceiveFieldUpdate value) {
             emit(state.copyWith(fields: value.fields));
           },
+          moveField: (_MoveField value) {
+            //
+          },
         );
       },
     );
@@ -39,20 +43,17 @@ class GridPropertyBloc extends Bloc<GridPropertyEvent, GridPropertyState> {
 
   @override
   Future<void> close() async {
-    await _fieldListener.stop();
+    if (_listenFieldCallback != null) {
+      _fieldCache.removeListener(_listenFieldCallback!);
+    }
     return super.close();
   }
 
   void _startListening() {
-    _fieldListener.updateFieldsNotifier.addPublishListener((result) {
-      result.fold(
-        (fields) {
-          add(GridPropertyEvent.didReceiveFieldUpdate(fields));
-        },
-        (err) => Log.error(err),
-      );
-    });
-    _fieldListener.start();
+    _listenFieldCallback = _fieldCache.addListener(
+      onChanged: (fields) => add(GridPropertyEvent.didReceiveFieldUpdate(fields)),
+      listenWhen: () => !isClosed,
+    );
   }
 }
 
@@ -61,6 +62,7 @@ class GridPropertyEvent with _$GridPropertyEvent {
   const factory GridPropertyEvent.initial() = _Initial;
   const factory GridPropertyEvent.setFieldVisibility(String fieldId, bool visibility) = _SetFieldVisibility;
   const factory GridPropertyEvent.didReceiveFieldUpdate(List<Field> fields) = _DidReceiveFieldUpdate;
+  const factory GridPropertyEvent.moveField(int fromIndex, int toIndex) = _MoveField;
 }
 
 @freezed

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

@@ -78,10 +78,10 @@ class CreateItem extends StatelessWidget {
   @override
   Widget build(BuildContext context) {
     final theme = context.watch<AppTheme>();
-    final config = HoverDisplayConfig(hoverColor: theme.hover);
+    final config = HoverStyle(hoverColor: theme.hover);
 
     return FlowyHover(
-      config: config,
+      style: config,
       builder: (context, onHover) {
         return GestureDetector(
           onTap: () => onSelected(pluginBuilder),

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

@@ -43,7 +43,7 @@ class ViewSectionItem extends StatelessWidget {
           return InkWell(
             onTap: () => onSelected(context.read<ViewBloc>().state.view),
             child: FlowyHover(
-              config: HoverDisplayConfig(hoverColor: theme.bg3),
+              style: HoverStyle(hoverColor: theme.bg3),
               builder: (_, onHover) => _render(context, onHover, state, theme.iconColor),
               setSelected: () => state.isEditing || isSelected,
             ),

+ 17 - 10
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/controller/grid_scroll.dart

@@ -1,21 +1,28 @@
 import 'package:flutter/material.dart';
+import 'package:linked_scroll_controller/linked_scroll_controller.dart';
 
 class GridScrollController {
-  final ScrollController _verticalController = ScrollController();
-  final ScrollController _horizontalController = ScrollController();
+  final LinkedScrollControllerGroup _scrollGroupContorller;
+  final ScrollController verticalController;
+  final ScrollController horizontalController;
 
-  ScrollController get verticalController => _verticalController;
-  ScrollController get horizontalController => _horizontalController;
+  final List<ScrollController> _linkHorizontalControllers = [];
 
-  GridScrollController();
+  GridScrollController({required LinkedScrollControllerGroup scrollGroupContorller})
+      : _scrollGroupContorller = scrollGroupContorller,
+        verticalController = ScrollController(),
+        horizontalController = scrollGroupContorller.addAndGet();
 
-  // final SelectionChangeCallback? onSelectionChanged;
-
-  // final ShouldApplySelection? shouldApplySelection;
-
-  // final ScrollCallback? onScroll;
+  ScrollController linkHorizontalController() {
+    final controller = _scrollGroupContorller.addAndGet();
+    _linkHorizontalControllers.add(controller);
+    return controller;
+  }
 
   void dispose() {
+    for (final controller in _linkHorizontalControllers) {
+      controller.dispose();
+    }
     verticalController.dispose();
     horizontalController.dispose();
   }

+ 133 - 57
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/grid_page.dart

@@ -1,14 +1,15 @@
 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_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';
 import 'package:flowy_infra_ui/widget/error_page.dart';
 import 'package:flowy_sdk/protobuf/flowy-folder-data-model/view.pb.dart';
-import 'package:flowy_sdk/protobuf/flowy-grid-data-model/grid.pb.dart' show Field;
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:flutter/material.dart';
+import 'package:linked_scroll_controller/linked_scroll_controller.dart';
 import 'controller/grid_scroll.dart';
 import 'layout/layout.dart';
 import 'layout/sizes.dart';
@@ -69,12 +70,18 @@ class FlowyGrid extends StatefulWidget {
   const FlowyGrid({Key? key}) : super(key: key);
 
   @override
-  _FlowyGridState createState() => _FlowyGridState();
+  State<FlowyGrid> createState() => _FlowyGridState();
 }
 
 class _FlowyGridState extends State<FlowyGrid> {
-  final _scrollController = GridScrollController();
-  final _key = GlobalKey<SliverAnimatedListState>();
+  final _scrollController = GridScrollController(scrollGroupContorller: LinkedScrollControllerGroup());
+  late ScrollController headerScrollController;
+
+  @override
+  void initState() {
+    headerScrollController = _scrollController.linkHorizontalController();
+    super.initState();
+  }
 
   @override
   void dispose() {
@@ -85,87 +92,120 @@ class _FlowyGridState extends State<FlowyGrid> {
   @override
   Widget build(BuildContext context) {
     return BlocBuilder<GridBloc, GridState>(
-      buildWhen: (previous, current) => previous.fields != current.fields,
+      buildWhen: (previous, current) => previous.fields.length != current.fields.length,
       builder: (context, state) {
-        if (state.fields.isEmpty) {
-          return const Center(child: CircularProgressIndicator.adaptive());
-        }
-
-        final child = SizedBox(
-          width: GridLayout.headerWidth(state.fields),
-          child: ScrollConfiguration(
-            behavior: const ScrollBehavior().copyWith(scrollbars: false),
-            child: CustomScrollView(
-              physics: StyledScrollPhysics(),
-              controller: _scrollController.verticalController,
-              slivers: [
-                _renderToolbar(state.gridId),
-                _renderGridHeader(state.gridId),
-                _renderRows(gridId: state.gridId, context: context),
-                const GridFooter(),
-              ],
-            ),
-          ),
+        final contentWidth = GridLayout.headerWidth(state.fields);
+        final child = _wrapScrollView(
+          contentWidth,
+          [
+            const _GridRows(),
+            const _GridFooter(),
+          ],
         );
 
-        return _wrapScrollbar(child);
+        return Column(
+          crossAxisAlignment: CrossAxisAlignment.start,
+          children: [
+            const _GridToolbarAdaptor(),
+            _gridHeader(context, state.gridId),
+            Flexible(child: child),
+          ],
+        );
       },
     );
   }
 
-  Widget _wrapScrollbar(Widget child) {
+  Widget _wrapScrollView(
+    double contentWidth,
+    List<Widget> slivers,
+  ) {
+    final verticalScrollView = ScrollConfiguration(
+      behavior: const ScrollBehavior().copyWith(scrollbars: false),
+      child: CustomScrollView(
+        physics: StyledScrollPhysics(),
+        controller: _scrollController.verticalController,
+        slivers: slivers,
+      ),
+    );
+
+    final sizedVerticalScrollView = SizedBox(
+      width: contentWidth,
+      child: verticalScrollView,
+    );
+
+    final horizontalScrollView = StyledSingleChildScrollView(
+      controller: _scrollController.horizontalController,
+      axis: Axis.horizontal,
+      child: sizedVerticalScrollView,
+    );
+
     return ScrollbarListStack(
       axis: Axis.vertical,
       controller: _scrollController.verticalController,
       barSize: GridSize.scrollBarSize,
-      child: StyledSingleChildScrollView(
-        controller: _scrollController.horizontalController,
-        axis: Axis.horizontal,
-        child: child,
-      ),
+      child: horizontalScrollView,
     );
   }
 
-  Widget _renderGridHeader(String gridId) {
-    return BlocSelector<GridBloc, GridState, List<Field>>(
-      selector: (state) => state.fields,
-      builder: (context, fields) {
-        return GridHeader(gridId: gridId, fields: List.from(fields));
-      },
+  Widget _gridHeader(BuildContext context, String gridId) {
+    final fieldCache = context.read<GridBloc>().fieldCache;
+    return GridHeaderSliverAdaptor(
+      gridId: gridId,
+      fieldCache: fieldCache,
+      anchorScrollController: headerScrollController,
     );
   }
+}
 
-  Widget _renderToolbar(String gridId) {
-    return BlocSelector<GridBloc, GridState, List<Field>>(
-      selector: (state) => state.fields,
-      builder: (context, fields) {
-        final toolbarContext = GridToolbarContext(
-          gridId: gridId,
-          fields: fields,
-        );
+class _GridToolbarAdaptor extends StatelessWidget {
+  const _GridToolbarAdaptor({Key? key}) : super(key: key);
 
-        return SliverToBoxAdapter(
-          child: GridToolbar(toolbarContext: toolbarContext),
+  @override
+  Widget build(BuildContext context) {
+    return BlocSelector<GridBloc, GridState, GridToolbarContext>(
+      selector: (state) {
+        final fieldCache = context.read<GridBloc>().fieldCache;
+        return GridToolbarContext(
+          gridId: state.gridId,
+          fieldCache: fieldCache,
         );
       },
+      builder: (context, toolbarContext) {
+        return GridToolbar(toolbarContext: toolbarContext);
+      },
     );
   }
+}
 
-  Widget _renderRows({required String gridId, required BuildContext context}) {
+class _GridRows extends StatefulWidget {
+  const _GridRows({Key? key}) : super(key: key);
+
+  @override
+  State<_GridRows> createState() => _GridRowsState();
+}
+
+class _GridRowsState extends State<_GridRows> {
+  final _key = GlobalKey<SliverAnimatedListState>();
+
+  @override
+  Widget build(BuildContext context) {
     return BlocConsumer<GridBloc, GridState>(
+      listenWhen: (previous, current) => previous.listState != current.listState,
       listener: (context, state) {
-        state.listState.map(
+        state.listState.mapOrNull(
           insert: (value) {
-            for (final index in value.indexs) {
-              _key.currentState?.insertItem(index);
+            for (final item in value.items) {
+              _key.currentState?.insertItem(item.index);
             }
           },
           delete: (value) {
-            for (final index in value.indexs) {
-              _key.currentState?.removeItem(index.value1, (context, animation) => _renderRow(index.value2, animation));
+            for (final item in value.items) {
+              _key.currentState?.removeItem(
+                item.index,
+                (context, animation) => _renderRow(context, item.row, animation),
+              );
             }
           },
-          reload: (updatedIndexs) {},
         );
       },
       buildWhen: (previous, current) => false,
@@ -175,17 +215,53 @@ class _FlowyGridState extends State<FlowyGrid> {
           initialItemCount: context.read<GridBloc>().state.rows.length,
           itemBuilder: (BuildContext context, int index, Animation<double> animation) {
             final rowData = context.read<GridBloc>().state.rows[index];
-            return _renderRow(rowData, animation);
+            return _renderRow(context, rowData, animation);
           },
         );
       },
     );
   }
 
-  Widget _renderRow(RowData rowData, Animation<double> animation) {
+  Widget _renderRow(BuildContext context, GridRow rowData, Animation<double> animation) {
+    final bloc = context.read<GridBloc>();
+    final fieldCache = bloc.fieldCache;
+    final rowCache = bloc.rowCache;
+
     return SizeTransition(
       sizeFactor: animation,
-      child: GridRowWidget(data: rowData, key: ValueKey(rowData.rowId)),
+      child: GridRowWidget(
+        blocBuilder: () => RowBloc(
+          rowData: rowData,
+          fieldCache: fieldCache,
+          rowCache: rowCache,
+        ),
+        key: ValueKey(rowData.rowId),
+      ),
+    );
+  }
+}
+
+class _GridFooter extends StatelessWidget {
+  const _GridFooter({Key? key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return SliverPadding(
+      padding: const EdgeInsets.only(bottom: 200),
+      sliver: SliverToBoxAdapter(
+        child: SizedBox(
+          height: GridSize.footerHeight,
+          child: Padding(
+            padding: GridSize.headerContentInsets,
+            child: Row(
+              children: [
+                SizedBox(width: GridSize.leadingHeaderPadding),
+                const SizedBox(width: 120, child: GridAddRowButton()),
+              ],
+            ),
+          ),
+        ),
+      ),
     );
   }
 }

+ 1 - 1
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/cell_builder.dart

@@ -7,7 +7,7 @@ import 'number_cell.dart';
 import 'selection_cell/selection_cell.dart';
 import 'text_cell.dart';
 
-Widget buildGridCell(CellData cellData) {
+Widget buildGridCell(GridCellIdentifier cellData) {
   final key = ValueKey(cellData.field.id + cellData.rowId);
   switch (cellData.field.fieldType) {
     case FieldType.Checkbox:

+ 1 - 3
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/cell_container.dart

@@ -33,9 +33,7 @@ class CellContainer extends StatelessWidget {
       child: Consumer<CellStateNotifier>(
         builder: (context, state, _) {
           return Container(
-            constraints: BoxConstraints(
-              maxWidth: width,
-            ),
+            constraints: BoxConstraints(maxWidth: width),
             decoration: _makeBoxDecoration(context, state),
             padding: GridSize.cellContentInsets,
             child: Center(child: child),

+ 1 - 1
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/checkbox_cell.dart

@@ -6,7 +6,7 @@ import 'package:flutter/widgets.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 
 class CheckboxCell extends StatefulWidget {
-  final CellData cellData;
+  final GridCellIdentifier cellData;
 
   const CheckboxCell({
     required this.cellData,

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

@@ -9,7 +9,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:table_calendar/table_calendar.dart';
 
 class DateCell extends GridCell {
-  final CellData cellData;
+  final GridCellIdentifier cellData;
 
   const DateCell({
     required this.cellData,
@@ -74,7 +74,13 @@ final kLastDay = DateTime(kToday.year, kToday.month + 3, kToday.day);
 class _CellCalendar extends StatefulWidget with FlowyOverlayDelegate {
   final void Function(DateTime) onSelected;
   final VoidCallback onDismissed;
-  const _CellCalendar({required this.onSelected, required this.onDismissed, Key? key}) : super(key: key);
+  final bool includeTime;
+  const _CellCalendar({
+    required this.onSelected,
+    required this.onDismissed,
+    required this.includeTime,
+    Key? key,
+  }) : super(key: key);
 
   @override
   State<_CellCalendar> createState() => _CellCalendarState();
@@ -86,7 +92,11 @@ class _CellCalendar extends StatefulWidget with FlowyOverlayDelegate {
   }) async {
     _CellCalendar.remove(context);
 
-    final calendar = _CellCalendar(onSelected: onSelected, onDismissed: onDismissed);
+    final calendar = _CellCalendar(
+      onSelected: onSelected,
+      onDismissed: onDismissed,
+      includeTime: false,
+    );
     // const size = Size(460, 400);
     // final window = await getWindowInfo();
     // FlowyOverlay.of(context).insertWithRect(
@@ -105,11 +115,11 @@ class _CellCalendar extends StatefulWidget with FlowyOverlayDelegate {
     FlowyOverlay.of(context).insertWithAnchor(
       widget: OverlayContainer(
         child: calendar,
-        constraints: BoxConstraints.loose(const Size(300, 320)),
+        constraints: BoxConstraints.tight(const Size(320, 320)),
       ),
       identifier: _CellCalendar.identifier(),
       anchorContext: context,
-      anchorDirection: AnchorDirection.bottomWithCenterAligned,
+      anchorDirection: AnchorDirection.leftWithCenterAligned,
       style: FlowyOverlayStyle(blur: false),
       delegate: calendar,
     );

+ 8 - 4
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/number_cell.dart

@@ -7,7 +7,7 @@ import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 
 class NumberCell extends GridCell {
-  final CellData cellData;
+  final GridCellIdentifier cellData;
 
   const NumberCell({
     required this.cellData,
@@ -26,7 +26,7 @@ class _NumberCellState extends State<NumberCell> {
 
   @override
   void initState() {
-    _cellBloc = getIt<NumberCellBloc>(param1: widget.cellData);
+    _cellBloc = getIt<NumberCellBloc>(param1: widget.cellData)..add(const NumberCellEvent.initial());
     _controller = TextEditingController(text: _cellBloc.state.content);
     _focusNode = CellFocusNode();
     super.initState();
@@ -48,7 +48,6 @@ class _NumberCellState extends State<NumberCell> {
           return TextField(
             controller: _controller,
             focusNode: _focusNode,
-            onChanged: (value) => focusChanged(),
             onEditingComplete: () => _focusNode.unfocus(),
             maxLines: 1,
             style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
@@ -76,7 +75,12 @@ class _NumberCellState extends State<NumberCell> {
       _delayOperation?.cancel();
       _delayOperation = Timer(const Duration(milliseconds: 300), () {
         if (_cellBloc.isClosed == false && _controller.text != _cellBloc.state.content) {
-          _cellBloc.add(NumberCellEvent.updateCell(_controller.text));
+          final number = num.tryParse(_controller.text);
+          if (number != null) {
+            _cellBloc.add(NumberCellEvent.updateCell(_controller.text));
+          } else {
+            _controller.text = "";
+          }
         }
       });
     }

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

@@ -8,7 +8,7 @@ import 'extension.dart';
 import 'selection_editor.dart';
 
 class SingleSelectCell extends GridCell {
-  final CellData cellData;
+  final GridCellIdentifier cellData;
 
   const SingleSelectCell({
     required this.cellData,
@@ -64,7 +64,7 @@ class _SingleSelectCellState extends State<SingleSelectCell> {
 
 //----------------------------------------------------------------
 class MultiSelectCell extends GridCell {
-  final CellData cellData;
+  final GridCellIdentifier cellData;
 
   const MultiSelectCell({
     required this.cellData,

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

@@ -1,5 +1,5 @@
 import 'dart:collection';
-import 'package:app_flowy/workspace/application/grid/cell_bloc/selection_editor_bloc.dart';
+import 'package:app_flowy/workspace/application/grid/cell/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';
 import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/header/type_option/edit_option_pannel.dart';
@@ -25,7 +25,7 @@ import 'text_field.dart';
 const double _editorPannelWidth = 300;
 
 class SelectOptionCellEditor extends StatelessWidget with FlowyOverlayDelegate {
-  final CellData cellData;
+  final GridCellIdentifier cellData;
   final List<SelectOption> options;
   final List<SelectOption> selectedOptions;
   final VoidCallback onDismissed;
@@ -66,7 +66,7 @@ class SelectOptionCellEditor extends StatelessWidget with FlowyOverlayDelegate {
 
   static void show(
     BuildContext context,
-    CellData cellData,
+    GridCellIdentifier cellData,
     List<SelectOption> options,
     List<SelectOption> selectedOptions,
     VoidCallback onDismissed,
@@ -199,7 +199,7 @@ class _SelectOptionCell extends StatelessWidget {
           context.read<SelectOptionEditorBloc>().add(SelectOptionEditorEvent.selectOption(option.id));
         },
         child: FlowyHover(
-          config: HoverDisplayConfig(hoverColor: theme.hover),
+          style: HoverStyle(hoverColor: theme.hover),
           builder: (_, onHover) {
             List<Widget> children = [
               SelectOptionTag(option: option, isSelected: isSelected),

+ 1 - 1
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/text_cell.dart

@@ -6,7 +6,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
 import 'cell_container.dart';
 
 class GridTextCell extends GridCell {
-  final CellData cellData;
+  final GridCellIdentifier cellData;
   const GridTextCell({
     required this.cellData,
     Key? key,

+ 2 - 25
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/footer/grid_footer.dart

@@ -1,5 +1,4 @@
 import 'package:app_flowy/workspace/application/grid/grid_bloc.dart';
-import 'package:app_flowy/workspace/presentation/plugins/grid/src/layout/sizes.dart';
 import 'package:flowy_infra/image.dart';
 import 'package:flowy_infra/theme.dart';
 import 'package:flowy_infra_ui/style_widget/button.dart';
@@ -7,30 +6,8 @@ import 'package:flowy_infra_ui/style_widget/text.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 
-class GridFooter extends StatelessWidget {
-  const GridFooter({Key? key}) : super(key: key);
-
-  @override
-  Widget build(BuildContext context) {
-    return SliverToBoxAdapter(
-      child: SizedBox(
-        height: GridSize.footerHeight,
-        child: Padding(
-          padding: GridSize.headerContentInsets,
-          child: Row(
-            children: [
-              SizedBox(width: GridSize.leadingHeaderPadding),
-              const SizedBox(width: 120, child: _AddRowButton()),
-            ],
-          ),
-        ),
-      ),
-    );
-  }
-}
-
-class _AddRowButton extends StatelessWidget {
-  const _AddRowButton({Key? key}) : super(key: key);
+class GridAddRowButton extends StatelessWidget {
+  const GridAddRowButton({Key? key}) : super(key: key);
 
   @override
   Widget build(BuildContext context) {

+ 72 - 7
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/field_cell.dart

@@ -4,7 +4,9 @@ import 'package:app_flowy/workspace/presentation/plugins/grid/src/layout/sizes.d
 import 'package:flowy_infra/image.dart';
 import 'package:flowy_infra/theme.dart';
 import 'package:flowy_infra_ui/style_widget/button.dart';
+import 'package:flowy_infra_ui/style_widget/hover.dart';
 import 'package:flowy_infra_ui/style_widget/text.dart';
+import 'package:flowy_sdk/log.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'field_type_extension.dart';
@@ -25,21 +27,22 @@ class GridFieldCell extends StatelessWidget {
       child: BlocBuilder<FieldCellBloc, FieldCellState>(
         builder: (context, state) {
           final button = FlowyButton(
-            hoverColor: theme.hover,
+            hoverColor: theme.shader6,
             onTap: () => _showActionSheet(context),
-            rightIcon: svgWidget("editor/details", color: theme.iconColor),
             leftIcon: svgWidget(state.field.fieldType.iconName(), color: theme.iconColor),
             text: FlowyText.medium(state.field.name, fontSize: 12),
             padding: GridSize.cellContentInsets,
           );
 
-          final borderSide = BorderSide(color: theme.shader4, width: 0.4);
-          final decoration = BoxDecoration(border: Border(top: borderSide, right: borderSide, bottom: borderSide));
+          const line = Positioned(top: 0, bottom: 0, right: 0, child: _DragToExpandLine());
 
-          return Container(
+          return _CellContainer(
             width: state.field.width.toDouble(),
-            decoration: decoration,
-            child: button,
+            child: Stack(
+              alignment: Alignment.centerRight,
+              fit: StackFit.expand,
+              children: [button, line],
+            ),
           );
         },
       ),
@@ -66,3 +69,65 @@ class GridFieldCell extends StatelessWidget {
     ).show(context);
   }
 }
+
+class _CellContainer extends StatelessWidget {
+  final Widget child;
+  final double width;
+  const _CellContainer({
+    required this.child,
+    required this.width,
+    Key? key,
+  }) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    final theme = context.watch<AppTheme>();
+    final borderSide = BorderSide(color: theme.shader4, width: 0.4);
+    final decoration = BoxDecoration(
+        border: Border(
+      top: borderSide,
+      right: borderSide,
+      bottom: borderSide,
+    ));
+
+    return Container(
+      width: width,
+      decoration: decoration,
+      child: ConstrainedBox(constraints: const BoxConstraints.expand(), child: child),
+    );
+  }
+}
+
+class _DragToExpandLine extends StatelessWidget {
+  const _DragToExpandLine({
+    Key? key,
+  }) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    final theme = context.watch<AppTheme>();
+
+    return InkWell(
+      onTap: () {},
+      child: GestureDetector(
+        behavior: HitTestBehavior.opaque,
+        onHorizontalDragCancel: () {},
+        onHorizontalDragUpdate: (value) {
+          // context.read<FieldCellBloc>().add(FieldCellEvent.updateWidth(value.delta.dx));
+          Log.info(value);
+        },
+        onHorizontalDragEnd: (end) {
+          Log.info(end);
+        },
+        child: FlowyHover(
+          style: HoverStyle(
+            hoverColor: theme.main1,
+            borderRadius: BorderRadius.zero,
+            contentMargin: const EdgeInsets.only(left: 5),
+          ),
+          builder: (_, onHover) => const SizedBox(width: 2),
+        ),
+      ),
+    );
+  }
+}

+ 95 - 54
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/grid_header.dart

@@ -8,92 +8,107 @@ import 'package:flowy_infra_ui/style_widget/text.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid-data-model/grid.pb.dart' hide Row;
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
-
+import 'package:reorderables/reorderables.dart';
 import 'field_editor.dart';
 import 'field_cell.dart';
 
-class GridHeader extends StatelessWidget {
+class GridHeaderSliverAdaptor extends StatefulWidget {
   final String gridId;
-  final List<Field> fields;
-  const GridHeader({Key? key, required this.gridId, required this.fields}) : super(key: key);
+  final GridFieldCache fieldCache;
+  final ScrollController anchorScrollController;
+  const GridHeaderSliverAdaptor({
+    required this.gridId,
+    required this.fieldCache,
+    required this.anchorScrollController,
+    Key? key,
+  }) : super(key: key);
 
+  @override
+  State<GridHeaderSliverAdaptor> createState() => _GridHeaderSliverAdaptorState();
+}
+
+class _GridHeaderSliverAdaptorState extends State<GridHeaderSliverAdaptor> {
   @override
   Widget build(BuildContext context) {
     return BlocProvider(
       create: (context) {
-        final bloc = getIt<GridHeaderBloc>(param1: gridId, param2: fields);
+        final bloc = getIt<GridHeaderBloc>(param1: widget.gridId, param2: widget.fieldCache);
         bloc.add(const GridHeaderEvent.initial());
         return bloc;
       },
       child: BlocBuilder<GridHeaderBloc, GridHeaderState>(
+        buildWhen: (previous, current) => previous.fields.length != current.fields.length,
         builder: (context, state) {
-          return SliverPersistentHeader(
-            delegate: _GridHeaderDelegate(gridId: gridId, fields: List.from(state.fields)),
-            floating: true,
-            pinned: true,
+          return SingleChildScrollView(
+            scrollDirection: Axis.horizontal,
+            controller: widget.anchorScrollController,
+            child: SizedBox(
+              height: GridSize.headerHeight,
+              child: _GridHeader(gridId: widget.gridId),
+            ),
           );
+
+          // return SliverPersistentHeader(
+          //   delegate: SliverHeaderDelegateImplementation(gridId: gridId, fields: state.fields),
+          //   floating: true,
+          //   pinned: true,
+          // );
         },
       ),
     );
   }
 }
 
-class _GridHeaderDelegate extends SliverPersistentHeaderDelegate {
+class _GridHeader extends StatefulWidget {
   final String gridId;
-  final List<Field> fields;
-
-  _GridHeaderDelegate({required this.gridId, required this.fields});
-
-  @override
-  Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
-    return _GridHeaderWidget(gridId: gridId, fields: fields, key: ObjectKey(fields));
-  }
-
-  @override
-  double get maxExtent => GridSize.headerHeight;
-
-  @override
-  double get minExtent => GridSize.headerHeight;
+  const _GridHeader({Key? key, required this.gridId}) : super(key: key);
 
   @override
-  bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) {
-    if (oldDelegate is _GridHeaderDelegate) {
-      return fields.length != oldDelegate.fields.length;
-    }
-    return true;
-  }
+  State<_GridHeader> createState() => _GridHeaderState();
 }
 
-class _GridHeaderWidget extends StatelessWidget {
-  final String gridId;
-  final List<Field> fields;
-
-  const _GridHeaderWidget({required this.gridId, required this.fields, Key? key}) : super(key: key);
-
+class _GridHeaderState extends State<_GridHeader> {
   @override
   Widget build(BuildContext context) {
     final theme = context.watch<AppTheme>();
-    final cells = fields.map(
-      (field) => GridFieldCell(
-        GridFieldCellContext(gridId: gridId, field: field),
-      ),
-    );
-
-    final row = Row(
-      crossAxisAlignment: CrossAxisAlignment.stretch,
-      children: [
-        const _HeaderLeading(),
-        ...cells,
-        _HeaderTrailing(gridId: gridId),
-      ],
+    return BlocBuilder<GridHeaderBloc, GridHeaderState>(
+      buildWhen: (previous, current) => previous.fields != current.fields,
+      builder: (context, state) {
+        final cells = state.fields
+            .where((field) => field.visibility)
+            .map((field) => GridFieldCellContext(gridId: widget.gridId, field: field))
+            .map((ctx) => GridFieldCell(ctx, key: ValueKey(ctx.field.id)))
+            .toList();
+
+        return Container(
+          color: theme.surface,
+          child: RepaintBoundary(
+            child: ReorderableRow(
+              crossAxisAlignment: CrossAxisAlignment.stretch,
+              scrollController: ScrollController(),
+              header: const _CellLeading(),
+              footer: _CellTrailing(gridId: widget.gridId),
+              onReorder: (int oldIndex, int newIndex) {
+                _onReorder(cells, oldIndex, context, newIndex);
+              },
+              children: cells,
+            ),
+          ),
+        );
+      },
     );
+  }
 
-    return Container(height: GridSize.headerHeight, color: theme.surface, child: row);
+  void _onReorder(List<GridFieldCell> cells, int oldIndex, BuildContext context, int newIndex) {
+    if (cells.length > oldIndex) {
+      final field = cells[oldIndex].cellContext.field;
+      context.read<GridHeaderBloc>().add(GridHeaderEvent.moveField(field, oldIndex, newIndex));
+    }
   }
 }
 
-class _HeaderLeading extends StatelessWidget {
-  const _HeaderLeading({Key? key}) : super(key: key);
+class _CellLeading extends StatelessWidget {
+  const _CellLeading({Key? key}) : super(key: key);
 
   @override
   Widget build(BuildContext context) {
@@ -103,9 +118,9 @@ class _HeaderLeading extends StatelessWidget {
   }
 }
 
-class _HeaderTrailing extends StatelessWidget {
+class _CellTrailing extends StatelessWidget {
   final String gridId;
-  const _HeaderTrailing({required this.gridId, Key? key}) : super(key: key);
+  const _CellTrailing({required this.gridId, Key? key}) : super(key: key);
 
   @override
   Widget build(BuildContext context) {
@@ -141,3 +156,29 @@ class CreateFieldButton extends StatelessWidget {
     );
   }
 }
+
+class SliverHeaderDelegateImplementation extends SliverPersistentHeaderDelegate {
+  final String gridId;
+  final List<Field> fields;
+
+  SliverHeaderDelegateImplementation({required this.gridId, required this.fields});
+
+  @override
+  Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
+    return _GridHeader(gridId: gridId);
+  }
+
+  @override
+  double get maxExtent => GridSize.headerHeight;
+
+  @override
+  double get minExtent => GridSize.headerHeight;
+
+  @override
+  bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) {
+    if (oldDelegate is SliverHeaderDelegateImplementation) {
+      return fields.length != oldDelegate.fields.length;
+    }
+    return true;
+  }
+}

+ 1 - 1
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/row/cell/number_cell.dart

@@ -4,7 +4,7 @@ import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 
 class NumberCell extends StatefulWidget {
-  final CellData cellData;
+  final GridCellIdentifier cellData;
 
   const NumberCell({
     required this.cellData,

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

@@ -1,4 +1,3 @@
-import 'package:app_flowy/startup/startup.dart';
 import 'package:app_flowy/workspace/application/grid/prelude.dart';
 import 'package:app_flowy/workspace/presentation/plugins/grid/src/layout/sizes.dart';
 import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/cell/prelude.dart';
@@ -12,8 +11,12 @@ 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);
+  final RowBloc Function() blocBuilder;
+
+  const GridRowWidget({
+    required this.blocBuilder,
+    Key? key,
+  }) : super(key: key);
 
   @override
   State<GridRowWidget> createState() => _GridRowWidgetState();
@@ -25,7 +28,8 @@ class _GridRowWidgetState extends State<GridRowWidget> {
 
   @override
   void initState() {
-    _rowBloc = getIt<RowBloc>(param1: widget.data)..add(const RowEvent.initial());
+    _rowBloc = widget.blocBuilder();
+    _rowBloc.add(const RowEvent.initial());
     _rowStateNotifier = _RegionStateNotifier();
     super.initState();
   }
@@ -44,9 +48,10 @@ class _GridRowWidgetState extends State<GridRowWidget> {
             buildWhen: (p, c) => p.rowData.height != c.rowData.height,
             builder: (context, state) {
               return SizedBox(
-                height: _rowBloc.state.rowData.height,
+                height: 42,
                 child: Row(
-                  crossAxisAlignment: CrossAxisAlignment.stretch,
+                  mainAxisSize: MainAxisSize.max,
+                  crossAxisAlignment: CrossAxisAlignment.center,
                   children: const [
                     _RowLeading(),
                     _RowCells(),
@@ -146,7 +151,11 @@ class _RowCells extends StatelessWidget {
       buildWhen: (previous, current) => previous.cellDataMap != current.cellDataMap,
       builder: (context, state) {
         final List<Widget> children = state.cellDataMap.fold(() => [], _toCells);
-        return Row(children: children);
+        return Row(
+          mainAxisSize: MainAxisSize.min,
+          mainAxisAlignment: MainAxisAlignment.center,
+          children: children,
+        );
       },
     );
   }

+ 1 - 1
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/row/number_cell.dart

@@ -4,7 +4,7 @@ import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 
 class NumberCell extends StatefulWidget {
-  final CellData cellData;
+  final GridCellIdentifier cellData;
 
   const NumberCell({
     required this.cellData,

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

@@ -14,7 +14,7 @@ import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 
 class GridRowActionSheet extends StatelessWidget {
-  final RowData rowData;
+  final GridRow rowData;
   const GridRowActionSheet({required this.rowData, Key? key}) : super(key: key);
 
   @override

+ 23 - 19
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/toolbar/grid_property.dart

@@ -1,5 +1,6 @@
 import 'package:app_flowy/startup/startup.dart';
 import 'package:app_flowy/workspace/application/grid/field/field_service.dart';
+import 'package:app_flowy/workspace/application/grid/grid_service.dart';
 import 'package:app_flowy/workspace/application/grid/setting/property_bloc.dart';
 import 'package:app_flowy/workspace/presentation/plugins/grid/src/layout/sizes.dart';
 import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/header/field_editor.dart';
@@ -18,10 +19,10 @@ import 'package:styled_widget/styled_widget.dart';
 
 class GridPropertyList extends StatelessWidget with FlowyOverlayDelegate {
   final String gridId;
-  final List<Field> fields;
+  final GridFieldCache fieldCache;
   const GridPropertyList({
     required this.gridId,
-    required this.fields,
+    required this.fieldCache,
     Key? key,
   }) : super(key: key);
 
@@ -43,23 +44,22 @@ class GridPropertyList extends StatelessWidget with FlowyOverlayDelegate {
   Widget build(BuildContext context) {
     return BlocProvider(
       create: (context) =>
-          getIt<GridPropertyBloc>(param1: gridId, param2: fields)..add(const GridPropertyEvent.initial()),
+          getIt<GridPropertyBloc>(param1: gridId, param2: fieldCache)..add(const GridPropertyEvent.initial()),
       child: BlocBuilder<GridPropertyBloc, GridPropertyState>(
         builder: (context, state) {
           final cells = state.fields.map((field) {
-            return _GridPropertyCell(gridId: gridId, field: field);
+            return _GridPropertyCell(gridId: gridId, field: field, key: ValueKey(field.id));
           }).toList();
 
           return ListView.separated(
             shrinkWrap: true,
-            controller: ScrollController(),
-            separatorBuilder: (context, index) {
-              return VSpace(GridSize.typeOptionSeparatorHeight);
-            },
             itemCount: cells.length,
             itemBuilder: (BuildContext context, int index) {
               return cells[index];
             },
+            separatorBuilder: (BuildContext context, int index) {
+              return VSpace(GridSize.typeOptionSeparatorHeight);
+            },
           );
         },
       ),
@@ -92,17 +92,7 @@ class _GridPropertyCell extends StatelessWidget {
         Expanded(
           child: SizedBox(
             height: GridSize.typeOptionItemHeight,
-            child: FlowyButton(
-              text: FlowyText.medium(field.name, fontSize: 12),
-              hoverColor: theme.hover,
-              leftIcon: svgWidget(field.fieldType.iconName(), color: theme.iconColor),
-              onTap: () {
-                FieldEditor(
-                  gridId: gridId,
-                  fieldContextLoader: FieldContextLoaderAdaptor(gridId: gridId, field: field),
-                ).show(context, anchorDirection: AnchorDirection.bottomRight);
-              },
-            ),
+            child: _editFieldButton(theme, context),
           ),
         ),
         FlowyIconButton(
@@ -116,4 +106,18 @@ class _GridPropertyCell extends StatelessWidget {
       ],
     );
   }
+
+  FlowyButton _editFieldButton(AppTheme theme, BuildContext context) {
+    return FlowyButton(
+      text: FlowyText.medium(field.name, fontSize: 12),
+      hoverColor: theme.hover,
+      leftIcon: svgWidget(field.fieldType.iconName(), color: theme.iconColor),
+      onTap: () {
+        FieldEditor(
+          gridId: gridId,
+          fieldContextLoader: FieldContextLoaderAdaptor(gridId: gridId, field: field),
+        ).show(context, anchorDirection: AnchorDirection.bottomRight);
+      },
+    );
+  }
 }

+ 4 - 4
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/toolbar/grid_setting.dart

@@ -1,3 +1,4 @@
+import 'package:app_flowy/workspace/application/grid/grid_service.dart';
 import 'package:app_flowy/workspace/application/grid/setting/setting_bloc.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flowy_infra/image.dart';
@@ -7,7 +8,6 @@ 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:flowy_sdk/protobuf/flowy-grid-data-model/grid.pb.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 
@@ -18,11 +18,11 @@ import 'grid_property.dart';
 
 class GridSettingContext {
   final String gridId;
-  final List<Field> fields;
+  final GridFieldCache fieldCache;
 
   GridSettingContext({
     required this.gridId,
-    required this.fields,
+    required this.fieldCache,
   });
 }
 
@@ -41,7 +41,7 @@ class GridSettingList extends StatelessWidget {
           case GridSettingAction.sortBy:
             break;
           case GridSettingAction.properties:
-            GridPropertyList(gridId: settingContext.gridId, fields: settingContext.fields).show(context);
+            GridPropertyList(gridId: settingContext.gridId, fieldCache: settingContext.fieldCache).show(context);
             break;
         }
       },

+ 4 - 4
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/toolbar/grid_toolbar.dart

@@ -1,8 +1,8 @@
+import 'package:app_flowy/workspace/application/grid/grid_service.dart';
 import 'package:flowy_infra/image.dart';
 import 'package:flowy_infra/theme.dart';
 import 'package:flowy_infra_ui/style_widget/extension.dart';
 import 'package:flowy_infra_ui/style_widget/icon_button.dart';
-import 'package:flowy_sdk/protobuf/flowy-grid-data-model/grid.pb.dart' hide Row;
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 
@@ -12,10 +12,10 @@ import 'grid_setting.dart';
 
 class GridToolbarContext {
   final String gridId;
-  final List<Field> fields;
+  final GridFieldCache fieldCache;
   GridToolbarContext({
     required this.gridId,
-    required this.fields,
+    required this.fieldCache,
   });
 }
 
@@ -27,7 +27,7 @@ class GridToolbar extends StatelessWidget {
   Widget build(BuildContext context) {
     final settingContext = GridSettingContext(
       gridId: toolbarContext.gridId,
-      fields: toolbarContext.fields,
+      fieldCache: toolbarContext.fieldCache,
     );
     return SizedBox(
       height: 40,

+ 1 - 1
frontend/app_flowy/lib/workspace/presentation/widgets/pop_up_action.dart

@@ -85,7 +85,7 @@ class ActionCell<T extends ActionItem> extends StatelessWidget {
     final theme = context.watch<AppTheme>();
 
     return FlowyHover(
-      config: HoverDisplayConfig(hoverColor: theme.hover),
+      style: HoverStyle(hoverColor: theme.hover),
       builder: (context, onHover) {
         return GestureDetector(
           behavior: HitTestBehavior.opaque,

+ 9 - 3
frontend/app_flowy/packages/flowy_infra/lib/notifier.dart

@@ -31,12 +31,18 @@ class PublishNotifier<T> extends ChangeNotifier {
 
   T? get currentValue => _value;
 
-  void addPublishListener(void Function(T) callback) {
+  void addPublishListener(void Function(T) callback, {bool Function()? listenWhen}) {
     super.addListener(
       () {
-        if (_value != null) {
-          callback(_value!);
+        if (_value == null) {
+          return;
         }
+
+        if (listenWhen != null && listenWhen() == false) {
+          return;
+        }
+
+        callback(_value!);
       },
     );
   }

+ 1 - 1
frontend/app_flowy/packages/flowy_infra_ui/lib/style_widget/button.dart

@@ -28,7 +28,7 @@ class FlowyButton extends StatelessWidget {
     return InkWell(
       onTap: onTap,
       child: FlowyHover(
-        config: HoverDisplayConfig(borderRadius: Corners.s6Border, hoverColor: hoverColor),
+        style: HoverStyle(borderRadius: Corners.s6Border, hoverColor: hoverColor),
         setSelected: () => isSelected,
         builder: (context, onHover) => _render(),
       ),

+ 14 - 11
frontend/app_flowy/packages/flowy_infra_ui/lib/style_widget/hover.dart

@@ -5,14 +5,14 @@ import 'package:flowy_infra/time/duration.dart';
 typedef HoverBuilder = Widget Function(BuildContext context, bool onHover);
 
 class FlowyHover extends StatefulWidget {
-  final HoverDisplayConfig config;
+  final HoverStyle style;
   final HoverBuilder builder;
   final bool Function()? setSelected;
 
   const FlowyHover({
     Key? key,
     required this.builder,
-    required this.config,
+    required this.style,
     this.setSelected,
   }) : super(key: key);
 
@@ -41,7 +41,7 @@ class _FlowyHoverState extends State<FlowyHover> {
 
     if (showHover) {
       return FlowyHoverContainer(
-        config: widget.config,
+        style: widget.style,
         child: widget.builder(context, _onHover),
       );
     } else {
@@ -50,41 +50,44 @@ class _FlowyHoverState extends State<FlowyHover> {
   }
 }
 
-class HoverDisplayConfig {
+class HoverStyle {
   final Color borderColor;
   final double borderWidth;
   final Color hoverColor;
   final BorderRadius borderRadius;
+  final EdgeInsets contentMargin;
 
-  const HoverDisplayConfig(
+  const HoverStyle(
       {this.borderColor = Colors.transparent,
       this.borderWidth = 0,
       this.borderRadius = const BorderRadius.all(Radius.circular(6)),
+      this.contentMargin = EdgeInsets.zero,
       required this.hoverColor});
 }
 
 class FlowyHoverContainer extends StatelessWidget {
-  final HoverDisplayConfig config;
+  final HoverStyle style;
   final Widget child;
 
   const FlowyHoverContainer({
     Key? key,
     required this.child,
-    required this.config,
+    required this.style,
   }) : super(key: key);
 
   @override
   Widget build(BuildContext context) {
     final hoverBorder = Border.all(
-      color: config.borderColor,
-      width: config.borderWidth,
+      color: style.borderColor,
+      width: style.borderWidth,
     );
 
     return Container(
+      margin: style.contentMargin,
       decoration: BoxDecoration(
         border: hoverBorder,
-        color: config.hoverColor,
-        borderRadius: config.borderRadius,
+        color: style.hoverColor,
+        borderRadius: style.borderRadius,
       ),
       child: child,
     );

+ 19 - 15
frontend/app_flowy/packages/flowy_infra_ui/lib/style_widget/scrolling/styled_scrollview.dart

@@ -74,21 +74,21 @@ class _StyledSingleChildScrollViewState extends State<StyledSingleChildScrollVie
 }
 
 class StyledCustomScrollView extends StatefulWidget {
-  final double? contentSize;
   final Axis axis;
   final Color? trackColor;
   final Color? handleColor;
-  final ScrollController? controller;
+  final ScrollController? verticalController;
   final List<Widget> slivers;
+  final double barSize;
 
   const StyledCustomScrollView({
     Key? key,
-    this.contentSize,
     this.axis = Axis.vertical,
     this.trackColor,
     this.handleColor,
-    this.controller,
+    this.verticalController,
     this.slivers = const <Widget>[],
+    this.barSize = 12,
   }) : super(key: key);
 
   @override
@@ -96,17 +96,17 @@ class StyledCustomScrollView extends StatefulWidget {
 }
 
 class _StyledCustomScrollViewState extends State<StyledCustomScrollView> {
-  late ScrollController scrollController;
+  late ScrollController controller;
 
   @override
   void initState() {
-    scrollController = widget.controller ?? ScrollController();
+    controller = widget.verticalController ?? ScrollController();
+
     super.initState();
   }
 
   @override
   void dispose() {
-    scrollController.dispose();
     super.dispose();
   }
 
@@ -120,19 +120,23 @@ class _StyledCustomScrollViewState extends State<StyledCustomScrollView> {
 
   @override
   Widget build(BuildContext context) {
-    return ScrollbarListStack(
-      contentSize: widget.contentSize,
-      axis: widget.axis,
-      controller: scrollController,
-      barSize: 12,
-      trackColor: widget.trackColor,
-      handleColor: widget.handleColor,
+    var child = ScrollConfiguration(
+      behavior: const ScrollBehavior().copyWith(scrollbars: false),
       child: CustomScrollView(
         scrollDirection: widget.axis,
         physics: StyledScrollPhysics(),
-        controller: scrollController,
+        controller: controller,
         slivers: widget.slivers,
       ),
     );
+
+    return ScrollbarListStack(
+      axis: widget.axis,
+      controller: controller,
+      barSize: widget.barSize,
+      trackColor: widget.trackColor,
+      handleColor: widget.handleColor,
+      child: child,
+    );
   }
 }

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

@@ -154,6 +154,23 @@ class GridEventGetEditFieldContext {
     }
 }
 
+class GridEventMoveItem {
+     MoveItemPayload request;
+     GridEventMoveItem(this.request);
+
+    Future<Either<Unit, FlowyError>> send() {
+    final request = FFIRequest.create()
+          ..event = GridEvent.MoveItem.toString()
+          ..payload = requestToBytes(this.request);
+
+    return Dispatch.asyncRequest(request)
+        .then((bytesResult) => bytesResult.fold(
+           (bytes) => left(unit),
+           (errBytes) => right(FlowyError.fromBuffer(errBytes)),
+        ));
+    }
+}
+
 class GridEventNewSelectOption {
      SelectOptionName request;
      GridEventNewSelectOption(this.request);

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

@@ -254,6 +254,140 @@ class FieldOrder extends $pb.GeneratedMessage {
   void clearFieldId() => clearField(1);
 }
 
+class GridFieldChangeset extends $pb.GeneratedMessage {
+  static final $pb.BuilderInfo _i = $pb.BuilderInfo(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'GridFieldChangeset', createEmptyInstance: create)
+    ..aOS(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'gridId')
+    ..pc<IndexField>(2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'insertedFields', $pb.PbFieldType.PM, subBuilder: IndexField.create)
+    ..pc<FieldOrder>(3, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'deletedFields', $pb.PbFieldType.PM, subBuilder: FieldOrder.create)
+    ..pc<Field>(4, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'updatedFields', $pb.PbFieldType.PM, subBuilder: Field.create)
+    ..hasRequiredFields = false
+  ;
+
+  GridFieldChangeset._() : super();
+  factory GridFieldChangeset({
+    $core.String? gridId,
+    $core.Iterable<IndexField>? insertedFields,
+    $core.Iterable<FieldOrder>? deletedFields,
+    $core.Iterable<Field>? updatedFields,
+  }) {
+    final _result = create();
+    if (gridId != null) {
+      _result.gridId = gridId;
+    }
+    if (insertedFields != null) {
+      _result.insertedFields.addAll(insertedFields);
+    }
+    if (deletedFields != null) {
+      _result.deletedFields.addAll(deletedFields);
+    }
+    if (updatedFields != null) {
+      _result.updatedFields.addAll(updatedFields);
+    }
+    return _result;
+  }
+  factory GridFieldChangeset.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
+  factory GridFieldChangeset.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')
+  GridFieldChangeset clone() => GridFieldChangeset()..mergeFromMessage(this);
+  @$core.Deprecated(
+  'Using this can add significant overhead to your binary. '
+  'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
+  'Will be removed in next major version')
+  GridFieldChangeset copyWith(void Function(GridFieldChangeset) updates) => super.copyWith((message) => updates(message as GridFieldChangeset)) as GridFieldChangeset; // ignore: deprecated_member_use
+  $pb.BuilderInfo get info_ => _i;
+  @$core.pragma('dart2js:noInline')
+  static GridFieldChangeset create() => GridFieldChangeset._();
+  GridFieldChangeset createEmptyInstance() => create();
+  static $pb.PbList<GridFieldChangeset> createRepeated() => $pb.PbList<GridFieldChangeset>();
+  @$core.pragma('dart2js:noInline')
+  static GridFieldChangeset getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<GridFieldChangeset>(create);
+  static GridFieldChangeset? _defaultInstance;
+
+  @$pb.TagNumber(1)
+  $core.String get gridId => $_getSZ(0);
+  @$pb.TagNumber(1)
+  set gridId($core.String v) { $_setString(0, v); }
+  @$pb.TagNumber(1)
+  $core.bool hasGridId() => $_has(0);
+  @$pb.TagNumber(1)
+  void clearGridId() => clearField(1);
+
+  @$pb.TagNumber(2)
+  $core.List<IndexField> get insertedFields => $_getList(1);
+
+  @$pb.TagNumber(3)
+  $core.List<FieldOrder> get deletedFields => $_getList(2);
+
+  @$pb.TagNumber(4)
+  $core.List<Field> get updatedFields => $_getList(3);
+}
+
+class IndexField extends $pb.GeneratedMessage {
+  static final $pb.BuilderInfo _i = $pb.BuilderInfo(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'IndexField', createEmptyInstance: create)
+    ..aOM<Field>(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'field', subBuilder: Field.create)
+    ..a<$core.int>(2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'index', $pb.PbFieldType.O3)
+    ..hasRequiredFields = false
+  ;
+
+  IndexField._() : super();
+  factory IndexField({
+    Field? field_1,
+    $core.int? index,
+  }) {
+    final _result = create();
+    if (field_1 != null) {
+      _result.field_1 = field_1;
+    }
+    if (index != null) {
+      _result.index = index;
+    }
+    return _result;
+  }
+  factory IndexField.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
+  factory IndexField.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')
+  IndexField clone() => IndexField()..mergeFromMessage(this);
+  @$core.Deprecated(
+  'Using this can add significant overhead to your binary. '
+  'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
+  'Will be removed in next major version')
+  IndexField copyWith(void Function(IndexField) updates) => super.copyWith((message) => updates(message as IndexField)) as IndexField; // ignore: deprecated_member_use
+  $pb.BuilderInfo get info_ => _i;
+  @$core.pragma('dart2js:noInline')
+  static IndexField create() => IndexField._();
+  IndexField createEmptyInstance() => create();
+  static $pb.PbList<IndexField> createRepeated() => $pb.PbList<IndexField>();
+  @$core.pragma('dart2js:noInline')
+  static IndexField getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<IndexField>(create);
+  static IndexField? _defaultInstance;
+
+  @$pb.TagNumber(1)
+  Field get field_1 => $_getN(0);
+  @$pb.TagNumber(1)
+  set field_1(Field v) { setField(1, v); }
+  @$pb.TagNumber(1)
+  $core.bool hasField_1() => $_has(0);
+  @$pb.TagNumber(1)
+  void clearField_1() => clearField(1);
+  @$pb.TagNumber(1)
+  Field ensureField_1() => $_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);
+}
+
 enum GetEditFieldContextPayload_OneOfFieldId {
   fieldId, 
   notSet
@@ -857,77 +991,6 @@ class GridBlockOrder extends $pb.GeneratedMessage {
   $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
@@ -1004,6 +1067,77 @@ class IndexRowOrder extends $pb.GeneratedMessage {
   void clearIndex() => clearField(2);
 }
 
+class GridRowsChangeset extends $pb.GeneratedMessage {
+  static final $pb.BuilderInfo _i = $pb.BuilderInfo(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'GridRowsChangeset', 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
+  ;
+
+  GridRowsChangeset._() : super();
+  factory GridRowsChangeset({
+    $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 GridRowsChangeset.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
+  factory GridRowsChangeset.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')
+  GridRowsChangeset clone() => GridRowsChangeset()..mergeFromMessage(this);
+  @$core.Deprecated(
+  'Using this can add significant overhead to your binary. '
+  'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
+  'Will be removed in next major version')
+  GridRowsChangeset copyWith(void Function(GridRowsChangeset) updates) => super.copyWith((message) => updates(message as GridRowsChangeset)) as GridRowsChangeset; // ignore: deprecated_member_use
+  $pb.BuilderInfo get info_ => _i;
+  @$core.pragma('dart2js:noInline')
+  static GridRowsChangeset create() => GridRowsChangeset._();
+  GridRowsChangeset createEmptyInstance() => create();
+  static $pb.PbList<GridRowsChangeset> createRepeated() => $pb.PbList<GridRowsChangeset>();
+  @$core.pragma('dart2js:noInline')
+  static GridRowsChangeset getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<GridRowsChangeset>(create);
+  static GridRowsChangeset? _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);
+}
+
 class GridBlock extends $pb.GeneratedMessage {
   static final $pb.BuilderInfo _i = $pb.BuilderInfo(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'GridBlock', createEmptyInstance: create)
     ..aOS(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'id')
@@ -1950,6 +2084,109 @@ class FieldChangesetPayload extends $pb.GeneratedMessage {
   void clearTypeOptionData() => clearField(9);
 }
 
+class MoveItemPayload extends $pb.GeneratedMessage {
+  static final $pb.BuilderInfo _i = $pb.BuilderInfo(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'MoveItemPayload', createEmptyInstance: create)
+    ..aOS(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'gridId')
+    ..aOS(2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'itemId')
+    ..a<$core.int>(3, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'fromIndex', $pb.PbFieldType.O3)
+    ..a<$core.int>(4, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'toIndex', $pb.PbFieldType.O3)
+    ..e<MoveItemType>(5, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'ty', $pb.PbFieldType.OE, defaultOrMaker: MoveItemType.MoveField, valueOf: MoveItemType.valueOf, enumValues: MoveItemType.values)
+    ..hasRequiredFields = false
+  ;
+
+  MoveItemPayload._() : super();
+  factory MoveItemPayload({
+    $core.String? gridId,
+    $core.String? itemId,
+    $core.int? fromIndex,
+    $core.int? toIndex,
+    MoveItemType? ty,
+  }) {
+    final _result = create();
+    if (gridId != null) {
+      _result.gridId = gridId;
+    }
+    if (itemId != null) {
+      _result.itemId = itemId;
+    }
+    if (fromIndex != null) {
+      _result.fromIndex = fromIndex;
+    }
+    if (toIndex != null) {
+      _result.toIndex = toIndex;
+    }
+    if (ty != null) {
+      _result.ty = ty;
+    }
+    return _result;
+  }
+  factory MoveItemPayload.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
+  factory MoveItemPayload.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')
+  MoveItemPayload clone() => MoveItemPayload()..mergeFromMessage(this);
+  @$core.Deprecated(
+  'Using this can add significant overhead to your binary. '
+  'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
+  'Will be removed in next major version')
+  MoveItemPayload copyWith(void Function(MoveItemPayload) updates) => super.copyWith((message) => updates(message as MoveItemPayload)) as MoveItemPayload; // ignore: deprecated_member_use
+  $pb.BuilderInfo get info_ => _i;
+  @$core.pragma('dart2js:noInline')
+  static MoveItemPayload create() => MoveItemPayload._();
+  MoveItemPayload createEmptyInstance() => create();
+  static $pb.PbList<MoveItemPayload> createRepeated() => $pb.PbList<MoveItemPayload>();
+  @$core.pragma('dart2js:noInline')
+  static MoveItemPayload getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<MoveItemPayload>(create);
+  static MoveItemPayload? _defaultInstance;
+
+  @$pb.TagNumber(1)
+  $core.String get gridId => $_getSZ(0);
+  @$pb.TagNumber(1)
+  set gridId($core.String v) { $_setString(0, v); }
+  @$pb.TagNumber(1)
+  $core.bool hasGridId() => $_has(0);
+  @$pb.TagNumber(1)
+  void clearGridId() => clearField(1);
+
+  @$pb.TagNumber(2)
+  $core.String get itemId => $_getSZ(1);
+  @$pb.TagNumber(2)
+  set itemId($core.String v) { $_setString(1, v); }
+  @$pb.TagNumber(2)
+  $core.bool hasItemId() => $_has(1);
+  @$pb.TagNumber(2)
+  void clearItemId() => clearField(2);
+
+  @$pb.TagNumber(3)
+  $core.int get fromIndex => $_getIZ(2);
+  @$pb.TagNumber(3)
+  set fromIndex($core.int v) { $_setSignedInt32(2, v); }
+  @$pb.TagNumber(3)
+  $core.bool hasFromIndex() => $_has(2);
+  @$pb.TagNumber(3)
+  void clearFromIndex() => clearField(3);
+
+  @$pb.TagNumber(4)
+  $core.int get toIndex => $_getIZ(3);
+  @$pb.TagNumber(4)
+  set toIndex($core.int v) { $_setSignedInt32(3, v); }
+  @$pb.TagNumber(4)
+  $core.bool hasToIndex() => $_has(3);
+  @$pb.TagNumber(4)
+  void clearToIndex() => clearField(4);
+
+  @$pb.TagNumber(5)
+  MoveItemType get ty => $_getN(4);
+  @$pb.TagNumber(5)
+  set ty(MoveItemType v) { setField(5, v); }
+  @$pb.TagNumber(5)
+  $core.bool hasTy() => $_has(4);
+  @$pb.TagNumber(5)
+  void clearTy() => clearField(5);
+}
+
 enum CellChangeset_OneOfData {
   data, 
   notSet

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

@@ -9,6 +9,21 @@
 import 'dart:core' as $core;
 import 'package:protobuf/protobuf.dart' as $pb;
 
+class MoveItemType extends $pb.ProtobufEnum {
+  static const MoveItemType MoveField = MoveItemType._(0, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'MoveField');
+  static const MoveItemType MoveRow = MoveItemType._(1, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'MoveRow');
+
+  static const $core.List<MoveItemType> values = <MoveItemType> [
+    MoveField,
+    MoveRow,
+  ];
+
+  static final $core.Map<$core.int, MoveItemType> _byValue = $pb.ProtobufEnum.initByValue(values);
+  static MoveItemType? valueOf($core.int value) => _byValue[value];
+
+  const MoveItemType._($core.int v, $core.String n) : super(v, n);
+}
+
 class FieldType extends $pb.ProtobufEnum {
   static const FieldType RichText = FieldType._(0, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'RichText');
   static const FieldType Number = FieldType._(1, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Number');

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

@@ -8,6 +8,17 @@
 import 'dart:core' as $core;
 import 'dart:convert' as $convert;
 import 'dart:typed_data' as $typed_data;
+@$core.Deprecated('Use moveItemTypeDescriptor instead')
+const MoveItemType$json = const {
+  '1': 'MoveItemType',
+  '2': const [
+    const {'1': 'MoveField', '2': 0},
+    const {'1': 'MoveRow', '2': 1},
+  ],
+};
+
+/// Descriptor for `MoveItemType`. Decode as a `google.protobuf.EnumDescriptorProto`.
+final $typed_data.Uint8List moveItemTypeDescriptor = $convert.base64Decode('CgxNb3ZlSXRlbVR5cGUSDQoJTW92ZUZpZWxkEAASCwoHTW92ZVJvdxAB');
 @$core.Deprecated('Use fieldTypeDescriptor instead')
 const FieldType$json = const {
   '1': 'FieldType',
@@ -61,6 +72,30 @@ const FieldOrder$json = const {
 
 /// Descriptor for `FieldOrder`. Decode as a `google.protobuf.DescriptorProto`.
 final $typed_data.Uint8List fieldOrderDescriptor = $convert.base64Decode('CgpGaWVsZE9yZGVyEhkKCGZpZWxkX2lkGAEgASgJUgdmaWVsZElk');
+@$core.Deprecated('Use gridFieldChangesetDescriptor instead')
+const GridFieldChangeset$json = const {
+  '1': 'GridFieldChangeset',
+  '2': const [
+    const {'1': 'grid_id', '3': 1, '4': 1, '5': 9, '10': 'gridId'},
+    const {'1': 'inserted_fields', '3': 2, '4': 3, '5': 11, '6': '.IndexField', '10': 'insertedFields'},
+    const {'1': 'deleted_fields', '3': 3, '4': 3, '5': 11, '6': '.FieldOrder', '10': 'deletedFields'},
+    const {'1': 'updated_fields', '3': 4, '4': 3, '5': 11, '6': '.Field', '10': 'updatedFields'},
+  ],
+};
+
+/// Descriptor for `GridFieldChangeset`. Decode as a `google.protobuf.DescriptorProto`.
+final $typed_data.Uint8List gridFieldChangesetDescriptor = $convert.base64Decode('ChJHcmlkRmllbGRDaGFuZ2VzZXQSFwoHZ3JpZF9pZBgBIAEoCVIGZ3JpZElkEjQKD2luc2VydGVkX2ZpZWxkcxgCIAMoCzILLkluZGV4RmllbGRSDmluc2VydGVkRmllbGRzEjIKDmRlbGV0ZWRfZmllbGRzGAMgAygLMgsuRmllbGRPcmRlclINZGVsZXRlZEZpZWxkcxItCg51cGRhdGVkX2ZpZWxkcxgEIAMoCzIGLkZpZWxkUg11cGRhdGVkRmllbGRz');
+@$core.Deprecated('Use indexFieldDescriptor instead')
+const IndexField$json = const {
+  '1': 'IndexField',
+  '2': const [
+    const {'1': 'field', '3': 1, '4': 1, '5': 11, '6': '.Field', '10': 'field'},
+    const {'1': 'index', '3': 2, '4': 1, '5': 5, '10': 'index'},
+  ],
+};
+
+/// Descriptor for `IndexField`. Decode as a `google.protobuf.DescriptorProto`.
+final $typed_data.Uint8List indexFieldDescriptor = $convert.base64Decode('CgpJbmRleEZpZWxkEhwKBWZpZWxkGAEgASgLMgYuRmllbGRSBWZpZWxkEhQKBWluZGV4GAIgASgFUgVpbmRleA==');
 @$core.Deprecated('Use getEditFieldContextPayloadDescriptor instead')
 const GetEditFieldContextPayload$json = const {
   '1': 'GetEditFieldContextPayload',
@@ -186,19 +221,6 @@ const GridBlockOrder$json = const {
 
 /// Descriptor for `GridBlockOrder`. Decode as a `google.protobuf.DescriptorProto`.
 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',
@@ -213,6 +235,19 @@ const IndexRowOrder$json = const {
 
 /// Descriptor for `IndexRowOrder`. Decode as a `google.protobuf.DescriptorProto`.
 final $typed_data.Uint8List indexRowOrderDescriptor = $convert.base64Decode('Cg1JbmRleFJvd09yZGVyEiYKCXJvd19vcmRlchgBIAEoCzIJLlJvd09yZGVyUghyb3dPcmRlchIWCgVpbmRleBgCIAEoBUgAUgVpbmRleEIOCgxvbmVfb2ZfaW5kZXg=');
+@$core.Deprecated('Use gridRowsChangesetDescriptor instead')
+const GridRowsChangeset$json = const {
+  '1': 'GridRowsChangeset',
+  '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 `GridRowsChangeset`. Decode as a `google.protobuf.DescriptorProto`.
+final $typed_data.Uint8List gridRowsChangesetDescriptor = $convert.base64Decode('ChFHcmlkUm93c0NoYW5nZXNldBIZCghibG9ja19pZBgBIAEoCVIHYmxvY2tJZBIzCg1pbnNlcnRlZF9yb3dzGAIgAygLMg4uSW5kZXhSb3dPcmRlclIMaW5zZXJ0ZWRSb3dzEiwKDGRlbGV0ZWRfcm93cxgDIAMoCzIJLlJvd09yZGVyUgtkZWxldGVkUm93cxIsCgx1cGRhdGVkX3Jvd3MYBCADKAsyCS5Sb3dPcmRlclILdXBkYXRlZFJvd3M=');
 @$core.Deprecated('Use gridBlockDescriptor instead')
 const GridBlock$json = const {
   '1': 'GridBlock',
@@ -370,6 +405,20 @@ const FieldChangesetPayload$json = const {
 
 /// Descriptor for `FieldChangesetPayload`. Decode as a `google.protobuf.DescriptorProto`.
 final $typed_data.Uint8List fieldChangesetPayloadDescriptor = $convert.base64Decode('ChVGaWVsZENoYW5nZXNldFBheWxvYWQSGQoIZmllbGRfaWQYASABKAlSB2ZpZWxkSWQSFwoHZ3JpZF9pZBgCIAEoCVIGZ3JpZElkEhQKBG5hbWUYAyABKAlIAFIEbmFtZRIUCgRkZXNjGAQgASgJSAFSBGRlc2MSKwoKZmllbGRfdHlwZRgFIAEoDjIKLkZpZWxkVHlwZUgCUglmaWVsZFR5cGUSGAoGZnJvemVuGAYgASgISANSBmZyb3plbhIgCgp2aXNpYmlsaXR5GAcgASgISARSCnZpc2liaWxpdHkSFgoFd2lkdGgYCCABKAVIBVIFd2lkdGgSKgoQdHlwZV9vcHRpb25fZGF0YRgJIAEoDEgGUg50eXBlT3B0aW9uRGF0YUINCgtvbmVfb2ZfbmFtZUINCgtvbmVfb2ZfZGVzY0ITChFvbmVfb2ZfZmllbGRfdHlwZUIPCg1vbmVfb2ZfZnJvemVuQhMKEW9uZV9vZl92aXNpYmlsaXR5Qg4KDG9uZV9vZl93aWR0aEIZChdvbmVfb2ZfdHlwZV9vcHRpb25fZGF0YQ==');
+@$core.Deprecated('Use moveItemPayloadDescriptor instead')
+const MoveItemPayload$json = const {
+  '1': 'MoveItemPayload',
+  '2': const [
+    const {'1': 'grid_id', '3': 1, '4': 1, '5': 9, '10': 'gridId'},
+    const {'1': 'item_id', '3': 2, '4': 1, '5': 9, '10': 'itemId'},
+    const {'1': 'from_index', '3': 3, '4': 1, '5': 5, '10': 'fromIndex'},
+    const {'1': 'to_index', '3': 4, '4': 1, '5': 5, '10': 'toIndex'},
+    const {'1': 'ty', '3': 5, '4': 1, '5': 14, '6': '.MoveItemType', '10': 'ty'},
+  ],
+};
+
+/// Descriptor for `MoveItemPayload`. Decode as a `google.protobuf.DescriptorProto`.
+final $typed_data.Uint8List moveItemPayloadDescriptor = $convert.base64Decode('Cg9Nb3ZlSXRlbVBheWxvYWQSFwoHZ3JpZF9pZBgBIAEoCVIGZ3JpZElkEhcKB2l0ZW1faWQYAiABKAlSBml0ZW1JZBIdCgpmcm9tX2luZGV4GAMgASgFUglmcm9tSW5kZXgSGQoIdG9faW5kZXgYBCABKAVSB3RvSW5kZXgSHQoCdHkYBSABKA4yDS5Nb3ZlSXRlbVR5cGVSAnR5');
 @$core.Deprecated('Use cellChangesetDescriptor instead')
 const CellChangeset$json = const {
   '1': 'CellChangeset',

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

@@ -12,19 +12,19 @@ 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 DidUpdateGridBlock = GridNotification._(20, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'DidUpdateGridBlock');
+  static const GridNotification DidUpdateGridRow = GridNotification._(20, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'DidUpdateGridRow');
+  static const GridNotification DidUpdateGridField = GridNotification._(21, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'DidUpdateGridField');
   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');
-  static const GridNotification DidUpdateField = GridNotification._(41, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'DidUpdateField');
+  static const GridNotification DidUpdateCell = GridNotification._(40, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'DidUpdateCell');
+  static const GridNotification DidUpdateField = GridNotification._(50, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'DidUpdateField');
 
   static const $core.List<GridNotification> values = <GridNotification> [
     Unknown,
     DidCreateBlock,
-    DidUpdateGridBlock,
+    DidUpdateGridRow,
+    DidUpdateGridField,
     DidUpdateRow,
     DidUpdateCell,
-    DidUpdateGrid,
     DidUpdateField,
   ];
 

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

@@ -14,13 +14,13 @@ const GridNotification$json = const {
   '2': const [
     const {'1': 'Unknown', '2': 0},
     const {'1': 'DidCreateBlock', '2': 11},
-    const {'1': 'DidUpdateGridBlock', '2': 20},
+    const {'1': 'DidUpdateGridRow', '2': 20},
+    const {'1': 'DidUpdateGridField', '2': 21},
     const {'1': 'DidUpdateRow', '2': 30},
-    const {'1': 'DidUpdateCell', '2': 31},
-    const {'1': 'DidUpdateGrid', '2': 40},
-    const {'1': 'DidUpdateField', '2': 41},
+    const {'1': 'DidUpdateCell', '2': 40},
+    const {'1': 'DidUpdateField', '2': 50},
   ],
 };
 
 /// Descriptor for `GridNotification`. Decode as a `google.protobuf.EnumDescriptorProto`.
-final $typed_data.Uint8List gridNotificationDescriptor = $convert.base64Decode('ChBHcmlkTm90aWZpY2F0aW9uEgsKB1Vua25vd24QABISCg5EaWRDcmVhdGVCbG9jaxALEhYKEkRpZFVwZGF0ZUdyaWRCbG9jaxAUEhAKDERpZFVwZGF0ZVJvdxAeEhEKDURpZFVwZGF0ZUNlbGwQHxIRCg1EaWRVcGRhdGVHcmlkECgSEgoORGlkVXBkYXRlRmllbGQQKQ==');
+final $typed_data.Uint8List gridNotificationDescriptor = $convert.base64Decode('ChBHcmlkTm90aWZpY2F0aW9uEgsKB1Vua25vd24QABISCg5EaWRDcmVhdGVCbG9jaxALEhQKEERpZFVwZGF0ZUdyaWRSb3cQFBIWChJEaWRVcGRhdGVHcmlkRmllbGQQFRIQCgxEaWRVcGRhdGVSb3cQHhIRCg1EaWRVcGRhdGVDZWxsECgSEgoORGlkVXBkYXRlRmllbGQQMg==');

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

@@ -19,6 +19,7 @@ class GridEvent extends $pb.ProtobufEnum {
   static const GridEvent SwitchToField = GridEvent._(14, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'SwitchToField');
   static const GridEvent DuplicateField = GridEvent._(15, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'DuplicateField');
   static const GridEvent GetEditFieldContext = GridEvent._(16, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'GetEditFieldContext');
+  static const GridEvent MoveItem = GridEvent._(17, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'MoveItem');
   static const GridEvent NewSelectOption = GridEvent._(30, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'NewSelectOption');
   static const GridEvent GetSelectOptionContext = GridEvent._(31, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'GetSelectOptionContext');
   static const GridEvent UpdateSelectOption = GridEvent._(32, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'UpdateSelectOption');
@@ -40,6 +41,7 @@ class GridEvent extends $pb.ProtobufEnum {
     SwitchToField,
     DuplicateField,
     GetEditFieldContext,
+    MoveItem,
     NewSelectOption,
     GetSelectOptionContext,
     UpdateSelectOption,

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

@@ -21,6 +21,7 @@ const GridEvent$json = const {
     const {'1': 'SwitchToField', '2': 14},
     const {'1': 'DuplicateField', '2': 15},
     const {'1': 'GetEditFieldContext', '2': 16},
+    const {'1': 'MoveItem', '2': 17},
     const {'1': 'NewSelectOption', '2': 30},
     const {'1': 'GetSelectOptionContext', '2': 31},
     const {'1': 'UpdateSelectOption', '2': 32},
@@ -35,4 +36,4 @@ const GridEvent$json = const {
 };
 
 /// Descriptor for `GridEvent`. Decode as a `google.protobuf.EnumDescriptorProto`.
-final $typed_data.Uint8List gridEventDescriptor = $convert.base64Decode('CglHcmlkRXZlbnQSDwoLR2V0R3JpZERhdGEQABIRCg1HZXRHcmlkQmxvY2tzEAESDQoJR2V0RmllbGRzEAoSDwoLVXBkYXRlRmllbGQQCxIPCgtJbnNlcnRGaWVsZBAMEg8KC0RlbGV0ZUZpZWxkEA0SEQoNU3dpdGNoVG9GaWVsZBAOEhIKDkR1cGxpY2F0ZUZpZWxkEA8SFwoTR2V0RWRpdEZpZWxkQ29udGV4dBAQEhMKD05ld1NlbGVjdE9wdGlvbhAeEhoKFkdldFNlbGVjdE9wdGlvbkNvbnRleHQQHxIWChJVcGRhdGVTZWxlY3RPcHRpb24QIBINCglDcmVhdGVSb3cQMhIKCgZHZXRSb3cQMxINCglEZWxldGVSb3cQNBIQCgxEdXBsaWNhdGVSb3cQNRILCgdHZXRDZWxsEEYSDgoKVXBkYXRlQ2VsbBBHEhoKFlVwZGF0ZUNlbGxTZWxlY3RPcHRpb24QSA==');
+final $typed_data.Uint8List gridEventDescriptor = $convert.base64Decode('CglHcmlkRXZlbnQSDwoLR2V0R3JpZERhdGEQABIRCg1HZXRHcmlkQmxvY2tzEAESDQoJR2V0RmllbGRzEAoSDwoLVXBkYXRlRmllbGQQCxIPCgtJbnNlcnRGaWVsZBAMEg8KC0RlbGV0ZUZpZWxkEA0SEQoNU3dpdGNoVG9GaWVsZBAOEhIKDkR1cGxpY2F0ZUZpZWxkEA8SFwoTR2V0RWRpdEZpZWxkQ29udGV4dBAQEgwKCE1vdmVJdGVtEBESEwoPTmV3U2VsZWN0T3B0aW9uEB4SGgoWR2V0U2VsZWN0T3B0aW9uQ29udGV4dBAfEhYKElVwZGF0ZVNlbGVjdE9wdGlvbhAgEg0KCUNyZWF0ZVJvdxAyEgoKBkdldFJvdxAzEg0KCURlbGV0ZVJvdxA0EhAKDER1cGxpY2F0ZVJvdxA1EgsKB0dldENlbGwQRhIOCgpVcGRhdGVDZWxsEEcSGgoWVXBkYXRlQ2VsbFNlbGVjdE9wdGlvbhBI');

+ 14 - 0
frontend/app_flowy/pubspec.lock

@@ -653,6 +653,13 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "4.4.0"
+  linked_scroll_controller:
+    dependency: "direct main"
+    description:
+      name: linked_scroll_controller
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.2.0"
   lint:
     dependency: transitive
     description:
@@ -947,6 +954,13 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "3.0.1+1"
+  reorderables:
+    dependency: "direct main"
+    description:
+      name: reorderables
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.4.3"
   shared_preferences:
     dependency: transitive
     description:

+ 2 - 0
frontend/app_flowy/pubspec.yaml

@@ -74,6 +74,8 @@ dependencies:
   device_info_plus: ^3.2.1
   fluttertoast: ^8.0.8
   table_calendar: ^3.0.5
+  reorderables:
+  linked_scroll_controller: ^0.2.0
 
 dev_dependencies:
   flutter_lints: ^1.0.0

+ 1 - 1
frontend/rust-lib/dart-ffi/Cargo.toml

@@ -31,7 +31,7 @@ flowy-derive = {path = "../../../shared-lib/flowy-derive" }
 [features]
 default = ["flowy-sdk/dart", "dart-notify/dart", "flutter"]
 flutter = []
-http_server = ["flowy-sdk/http_server", "flowy-sdk/use_bunyan"]
+http_sync = ["flowy-sdk/http_sync", "flowy-sdk/use_bunyan"]
 #use_serde = ["bincode"]
 #use_protobuf= ["protobuf"]
 

+ 2 - 1
frontend/rust-lib/flowy-folder/Cargo.toml

@@ -44,6 +44,7 @@ lib-infra = { path = "../../../shared-lib/lib-infra", features = ["protobuf_file
 
 [features]
 default = []
-http_server = []
+sync = []
+cloud_sync = ["sync"]
 flowy_unit_test = ["lib-ot/flowy_unit_test", "flowy-revision/flowy_unit_test"]
 dart = ["lib-infra/dart"]

+ 15 - 4
frontend/rust-lib/flowy-folder/src/services/folder_editor.rs

@@ -1,4 +1,3 @@
-use crate::services::web_socket::make_folder_ws_manager;
 use flowy_sync::{
     client_folder::{FolderChange, FolderPad},
     entities::{revision::Revision, ws_data::ServerRevisionWSData},
@@ -11,7 +10,6 @@ use flowy_sync::util::make_delta_from_revisions;
 
 use flowy_revision::{
     RevisionCloudService, RevisionCompactor, RevisionManager, RevisionObjectBuilder, RevisionWebSocket,
-    RevisionWebSocketManager,
 };
 use lib_infra::future::FutureResult;
 use lib_ot::core::PlainTextAttributes;
@@ -21,13 +19,16 @@ use std::sync::Arc;
 
 pub struct ClientFolderEditor {
     user_id: String,
+    #[allow(dead_code)]
     pub(crate) folder_id: FolderId,
     pub(crate) folder: Arc<RwLock<FolderPad>>,
     rev_manager: Arc<RevisionManager>,
-    ws_manager: Arc<RevisionWebSocketManager>,
+    #[cfg(feature = "sync")]
+    ws_manager: Arc<flowy_revision::RevisionWebSocketManager>,
 }
 
 impl ClientFolderEditor {
+    #[allow(unused_variables)]
     pub async fn new(
         user_id: &str,
         folder_id: &FolderId,
@@ -40,7 +41,9 @@ impl ClientFolderEditor {
         });
         let folder = Arc::new(RwLock::new(rev_manager.load::<FolderPadBuilder>(Some(cloud)).await?));
         let rev_manager = Arc::new(rev_manager);
-        let ws_manager = make_folder_ws_manager(
+
+        #[cfg(feature = "sync")]
+        let ws_manager = crate::services::web_socket::make_folder_ws_manager(
             user_id,
             folder_id.as_ref(),
             rev_manager.clone(),
@@ -56,15 +59,23 @@ impl ClientFolderEditor {
             folder_id,
             folder,
             rev_manager,
+            #[cfg(feature = "sync")]
             ws_manager,
         })
     }
 
+    #[cfg(feature = "sync")]
     pub async fn receive_ws_data(&self, data: ServerRevisionWSData) -> FlowyResult<()> {
         let _ = self.ws_manager.ws_passthrough_tx.send(data).await.map_err(|e| {
             let err_msg = format!("{} passthrough error: {}", self.folder_id, e);
             FlowyError::internal().context(err_msg)
         })?;
+
+        Ok(())
+    }
+
+    #[cfg(not(feature = "sync"))]
+    pub async fn receive_ws_data(&self, _data: ServerRevisionWSData) -> FlowyResult<()> {
         Ok(())
     }
 

+ 1 - 0
frontend/rust-lib/flowy-folder/src/services/web_socket.rs

@@ -14,6 +14,7 @@ use lib_ot::core::{OperationTransformable, PlainTextAttributes, PlainTextDelta};
 use parking_lot::RwLock;
 use std::{sync::Arc, time::Duration};
 
+#[allow(dead_code)]
 pub(crate) async fn make_folder_ws_manager(
     user_id: &str,
     folder_id: &str,

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

@@ -6,11 +6,11 @@ const OBSERVABLE_CATEGORY: &str = "Grid";
 pub enum GridNotification {
     Unknown = 0,
     DidCreateBlock = 11,
-    DidUpdateGridBlock = 20,
+    DidUpdateGridRow = 20,
+    DidUpdateGridField = 21,
     DidUpdateRow = 30,
-    DidUpdateCell = 31,
-    DidUpdateGrid = 40,
-    DidUpdateField = 41,
+    DidUpdateCell = 40,
+    DidUpdateField = 50,
 }
 
 impl std::default::Default for GridNotification {

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

@@ -130,6 +130,17 @@ pub(crate) async fn get_field_context_handler(
     data_result(edit_context)
 }
 
+#[tracing::instrument(level = "debug", skip(data, manager), err)]
+pub(crate) async fn move_item_handler(
+    data: Data<MoveItemPayload>,
+    manager: AppData<Arc<GridManager>>,
+) -> Result<(), FlowyError> {
+    let params: MoveItemParams = data.into_inner().try_into()?;
+    let editor = manager.get_grid_editor(&params.grid_id)?;
+    let _ = editor.move_item(params).await?;
+    Ok(())
+}
+
 async fn make_field_edit_context(
     grid_id: &str,
     field_id: Option<String>,

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

@@ -17,6 +17,8 @@ pub fn create(grid_manager: Arc<GridManager>) -> Module {
         .event(GridEvent::DeleteField, delete_field_handler)
         .event(GridEvent::SwitchToField, switch_to_field_handler)
         .event(GridEvent::DuplicateField, duplicate_field_handler)
+        .event(GridEvent::GetEditFieldContext, get_field_context_handler)
+        .event(GridEvent::MoveItem, move_item_handler)
         // Row
         .event(GridEvent::CreateRow, create_row_handler)
         .event(GridEvent::GetRow, get_row_handler)
@@ -29,9 +31,7 @@ pub fn create(grid_manager: Arc<GridManager>) -> Module {
         .event(GridEvent::NewSelectOption, new_select_option_handler)
         .event(GridEvent::UpdateSelectOption, update_select_option_handler)
         .event(GridEvent::GetSelectOptionContext, get_select_option_handler)
-        .event(GridEvent::UpdateCellSelectOption, update_cell_select_option_handler)
-        //
-        .event(GridEvent::GetEditFieldContext, get_field_context_handler);
+        .event(GridEvent::UpdateCellSelectOption, update_cell_select_option_handler);
 
     module
 }
@@ -66,6 +66,9 @@ pub enum GridEvent {
     #[event(input = "GetEditFieldContextPayload", output = "EditFieldContext")]
     GetEditFieldContext = 16,
 
+    #[event(input = "MoveItemPayload")]
+    MoveItem = 17,
+
     #[event(input = "SelectOptionName", output = "SelectOption")]
     NewSelectOption = 30,
 

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

@@ -27,11 +27,11 @@
 pub enum GridNotification {
     Unknown = 0,
     DidCreateBlock = 11,
-    DidUpdateGridBlock = 20,
+    DidUpdateGridRow = 20,
+    DidUpdateGridField = 21,
     DidUpdateRow = 30,
-    DidUpdateCell = 31,
-    DidUpdateGrid = 40,
-    DidUpdateField = 41,
+    DidUpdateCell = 40,
+    DidUpdateField = 50,
 }
 
 impl ::protobuf::ProtobufEnum for GridNotification {
@@ -43,11 +43,11 @@ 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::DidUpdateGridBlock),
+            20 => ::std::option::Option::Some(GridNotification::DidUpdateGridRow),
+            21 => ::std::option::Option::Some(GridNotification::DidUpdateGridField),
             30 => ::std::option::Option::Some(GridNotification::DidUpdateRow),
-            31 => ::std::option::Option::Some(GridNotification::DidUpdateCell),
-            40 => ::std::option::Option::Some(GridNotification::DidUpdateGrid),
-            41 => ::std::option::Option::Some(GridNotification::DidUpdateField),
+            40 => ::std::option::Option::Some(GridNotification::DidUpdateCell),
+            50 => ::std::option::Option::Some(GridNotification::DidUpdateField),
             _ => ::std::option::Option::None
         }
     }
@@ -56,10 +56,10 @@ impl ::protobuf::ProtobufEnum for GridNotification {
         static values: &'static [GridNotification] = &[
             GridNotification::Unknown,
             GridNotification::DidCreateBlock,
-            GridNotification::DidUpdateGridBlock,
+            GridNotification::DidUpdateGridRow,
+            GridNotification::DidUpdateGridField,
             GridNotification::DidUpdateRow,
             GridNotification::DidUpdateCell,
-            GridNotification::DidUpdateGrid,
             GridNotification::DidUpdateField,
         ];
         values
@@ -89,11 +89,11 @@ impl ::protobuf::reflect::ProtobufValue for GridNotification {
 }
 
 static file_descriptor_proto_data: &'static [u8] = b"\
-    \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\
+    \n\x17dart_notification.proto*\x9a\x01\n\x10GridNotification\x12\x0b\n\
+    \x07Unknown\x10\0\x12\x12\n\x0eDidCreateBlock\x10\x0b\x12\x14\n\x10DidUp\
+    dateGridRow\x10\x14\x12\x16\n\x12DidUpdateGridField\x10\x15\x12\x10\n\
+    \x0cDidUpdateRow\x10\x1e\x12\x11\n\rDidUpdateCell\x10(\x12\x12\n\x0eDidU\
+    pdateField\x102b\x06proto3\
 ";
 
 static file_descriptor_proto_lazy: ::protobuf::rt::LazyV2<::protobuf::descriptor::FileDescriptorProto> = ::protobuf::rt::LazyV2::INIT;

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

@@ -34,6 +34,7 @@ pub enum GridEvent {
     SwitchToField = 14,
     DuplicateField = 15,
     GetEditFieldContext = 16,
+    MoveItem = 17,
     NewSelectOption = 30,
     GetSelectOptionContext = 31,
     UpdateSelectOption = 32,
@@ -62,6 +63,7 @@ impl ::protobuf::ProtobufEnum for GridEvent {
             14 => ::std::option::Option::Some(GridEvent::SwitchToField),
             15 => ::std::option::Option::Some(GridEvent::DuplicateField),
             16 => ::std::option::Option::Some(GridEvent::GetEditFieldContext),
+            17 => ::std::option::Option::Some(GridEvent::MoveItem),
             30 => ::std::option::Option::Some(GridEvent::NewSelectOption),
             31 => ::std::option::Option::Some(GridEvent::GetSelectOptionContext),
             32 => ::std::option::Option::Some(GridEvent::UpdateSelectOption),
@@ -87,6 +89,7 @@ impl ::protobuf::ProtobufEnum for GridEvent {
             GridEvent::SwitchToField,
             GridEvent::DuplicateField,
             GridEvent::GetEditFieldContext,
+            GridEvent::MoveItem,
             GridEvent::NewSelectOption,
             GridEvent::GetSelectOptionContext,
             GridEvent::UpdateSelectOption,
@@ -125,16 +128,16 @@ impl ::protobuf::reflect::ProtobufValue for GridEvent {
 }
 
 static file_descriptor_proto_data: &'static [u8] = b"\
-    \n\x0fevent_map.proto*\xef\x02\n\tGridEvent\x12\x0f\n\x0bGetGridData\x10\
+    \n\x0fevent_map.proto*\xfd\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\x0bInsertField\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\x16\n\
-    \x12UpdateSelectOption\x10\x20\x12\r\n\tCreateRow\x102\x12\n\n\x06GetRow\
-    \x103\x12\r\n\tDeleteRow\x104\x12\x10\n\x0cDuplicateRow\x105\x12\x0b\n\
-    \x07GetCell\x10F\x12\x0e\n\nUpdateCell\x10G\x12\x1a\n\x16UpdateCellSelec\
-    tOption\x10Hb\x06proto3\
+    ield\x10\x0f\x12\x17\n\x13GetEditFieldContext\x10\x10\x12\x0c\n\x08MoveI\
+    tem\x10\x11\x12\x13\n\x0fNewSelectOption\x10\x1e\x12\x1a\n\x16GetSelectO\
+    ptionContext\x10\x1f\x12\x16\n\x12UpdateSelectOption\x10\x20\x12\r\n\tCr\
+    eateRow\x102\x12\n\n\x06GetRow\x103\x12\r\n\tDeleteRow\x104\x12\x10\n\
+    \x0cDuplicateRow\x105\x12\x0b\n\x07GetCell\x10F\x12\x0e\n\nUpdateCell\
+    \x10G\x12\x1a\n\x16UpdateCellSelectOption\x10Hb\x06proto3\
 ";
 
 static file_descriptor_proto_lazy: ::protobuf::rt::LazyV2<::protobuf::descriptor::FileDescriptorProto> = ::protobuf::rt::LazyV2::INIT;

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

@@ -3,9 +3,9 @@ syntax = "proto3";
 enum GridNotification {
     Unknown = 0;
     DidCreateBlock = 11;
-    DidUpdateGridBlock = 20;
+    DidUpdateGridRow = 20;
+    DidUpdateGridField = 21;
     DidUpdateRow = 30;
-    DidUpdateCell = 31;
-    DidUpdateGrid = 40;
-    DidUpdateField = 41;
+    DidUpdateCell = 40;
+    DidUpdateField = 50;
 }

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

@@ -10,6 +10,7 @@ enum GridEvent {
     SwitchToField = 14;
     DuplicateField = 15;
     GetEditFieldContext = 16;
+    MoveItem = 17;
     NewSelectOption = 30;
     GetSelectOptionContext = 31;
     UpdateSelectOption = 32;

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

@@ -51,16 +51,16 @@ impl ClientGridBlockMetaEditor {
         let mut row_count = 0;
         let mut row_index = None;
         let _ = self
-            .modify(|pad| {
+            .modify(|block_pad| {
                 if let Some(start_row_id) = start_row_id.as_ref() {
-                    match pad.index_of_row(start_row_id) {
+                    match block_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();
+                let change = block_pad.add_row_meta(row, start_row_id)?;
+                row_count = block_pad.number_of_rows();
                 Ok(change)
             })
             .await?;
@@ -71,9 +71,9 @@ impl ClientGridBlockMetaEditor {
     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)?;
-                row_count = pad.number_of_rows();
+            .modify(|block_pad| {
+                let changeset = block_pad.delete_rows(ids)?;
+                row_count = block_pad.number_of_rows();
                 Ok(changeset)
             })
             .await?;
@@ -81,7 +81,14 @@ impl ClientGridBlockMetaEditor {
     }
 
     pub async fn update_row(&self, changeset: RowMetaChangeset) -> FlowyResult<()> {
-        let _ = self.modify(|pad| Ok(pad.update_row(changeset)?)).await?;
+        let _ = self.modify(|block_pad| Ok(block_pad.update_row(changeset)?)).await?;
+        Ok(())
+    }
+
+    pub async fn move_row(&self, row_id: &str, from: usize, to: usize) -> FlowyResult<()> {
+        let _ = self
+            .modify(|block_pad| Ok(block_pad.move_row(row_id, from, to)?))
+            .await?;
         Ok(())
     }
 

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

@@ -2,14 +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::{group_row_orders, make_rows_from_row_metas, GridBlockSnapshot};
+use crate::services::row::{group_row_orders, GridBlockSnapshot};
 use std::borrow::Cow;
 
 use dashmap::DashMap;
 use flowy_error::FlowyResult;
 use flowy_grid_data_model::entities::{
-    CellChangeset, CellMeta, CellNotificationData, FieldMeta, GridBlockMeta, GridBlockMetaChangeset,
-    GridBlockOrderChangeset, IndexRowOrder, RowMeta, RowMetaChangeset, RowOrder,
+    CellChangeset, CellMeta, CellNotificationData, GridBlockMeta, GridBlockMetaChangeset, GridRowsChangeset,
+    IndexRowOrder, RowMeta, RowMetaChangeset, RowOrder,
 };
 use flowy_revision::disk::SQLiteGridBlockMetaRevisionPersistence;
 use flowy_revision::{RevisionManager, RevisionPersistence};
@@ -76,7 +76,7 @@ impl GridBlockMetaEditorManager {
         index_row_order.index = row_index;
 
         let _ = self
-            .notify_did_update_grid_rows(GridBlockOrderChangeset::from_insert(block_id, vec![index_row_order]))
+            .notify_did_update_block(GridRowsChangeset::insert(block_id, vec![index_row_order]))
             .await?;
         Ok(row_count)
     }
@@ -98,7 +98,7 @@ impl GridBlockMetaEditorManager {
             changesets.push(GridBlockMetaChangeset::from_row_count(&block_id, row_count));
 
             let _ = self
-                .notify_did_update_grid_rows(GridBlockOrderChangeset::from_insert(&block_id, inserted_row_orders))
+                .notify_did_update_block(GridRowsChangeset::insert(&block_id, inserted_row_orders))
                 .await?;
         }
 
@@ -108,19 +108,7 @@ impl GridBlockMetaEditorManager {
     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?;
-
-        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?;
-            }
-        }
-
+        let _ = self.notify_did_update_block_row(&changeset.row_id).await?;
         Ok(())
     }
 
@@ -131,7 +119,7 @@ impl GridBlockMetaEditorManager {
         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))
+            .notify_did_update_block(GridRowsChangeset::delete(&block_id, row_orders))
             .await?;
 
         Ok(())
@@ -154,11 +142,35 @@ impl GridBlockMetaEditorManager {
         Ok(changesets)
     }
 
+    pub(crate) async fn move_row(&self, row_id: &str, from: usize, to: usize) -> FlowyResult<()> {
+        let editor = self.get_editor_from_row_id(row_id).await?;
+        let _ = editor.move_row(row_id, from, to).await?;
+
+        match editor.get_row_metas(Some(vec![Cow::Borrowed(row_id)])).await?.pop() {
+            None => {}
+            Some(row_meta) => {
+                let row_order = RowOrder::from(&row_meta);
+                let insert_row = IndexRowOrder {
+                    row_order: row_order.clone(),
+                    index: Some(to as i32),
+                };
+                let notified_changeset = GridRowsChangeset {
+                    block_id: editor.block_id.clone(),
+                    inserted_rows: vec![insert_row],
+                    deleted_rows: vec![row_order],
+                    updated_rows: vec![],
+                };
+
+                let _ = self.notify_did_update_block(notified_changeset).await?;
+            }
+        }
+
+        Ok(())
+    }
+
     pub async fn update_cell(&self, changeset: CellChangeset) -> FlowyResult<()> {
-        let row_id = changeset.row_id.clone();
-        let editor = self.get_editor_from_row_id(&row_id).await?;
         let row_changeset: RowMetaChangeset = changeset.clone().into();
-        let _ = editor.update_row(row_changeset).await?;
+        let _ = self.update_row(row_changeset).await?;
 
         let cell_notification_data = CellNotificationData {
             grid_id: changeset.grid_id,
@@ -167,6 +179,7 @@ impl GridBlockMetaEditorManager {
             content: changeset.data,
         };
         self.notify_did_update_cell(cell_notification_data).await?;
+
         Ok(())
     }
 
@@ -213,8 +226,22 @@ impl GridBlockMetaEditorManager {
         Ok(block_cell_metas)
     }
 
-    async fn notify_did_update_grid_rows(&self, changeset: GridBlockOrderChangeset) -> FlowyResult<()> {
-        send_dart_notification(&self.grid_id, GridNotification::DidUpdateGridBlock)
+    async fn notify_did_update_block_row(&self, row_id: &str) -> FlowyResult<()> {
+        let editor = self.get_editor_from_row_id(row_id).await?;
+        let row_ids = Some(vec![Cow::Borrowed(&row_id)]);
+        match editor.get_row_orders(row_ids).await?.pop() {
+            None => {}
+            Some(row_order) => {
+                let block_order_changeset = GridRowsChangeset::update(&editor.block_id, vec![row_order]);
+                let _ = self.notify_did_update_block(block_order_changeset).await?;
+            }
+        }
+
+        Ok(())
+    }
+
+    async fn notify_did_update_block(&self, changeset: GridRowsChangeset) -> FlowyResult<()> {
+        send_dart_notification(&self.grid_id, GridNotification::DidUpdateGridRow)
             .payload(changeset)
             .send();
         Ok(())
@@ -227,22 +254,6 @@ impl GridBlockMetaEditorManager {
             .send();
         Ok(())
     }
-
-    #[allow(dead_code)]
-    async fn notify_did_update_row(&self, row_id: &str, field_metas: &[FieldMeta]) -> FlowyResult<()> {
-        match self.get_row_meta(row_id).await? {
-            None => {}
-            Some(row_meta) => {
-                let row_metas = vec![row_meta];
-                if let Some(row) = make_rows_from_row_metas(field_metas, &row_metas).pop() {
-                    send_dart_notification(row_id, GridNotification::DidUpdateRow)
-                        .payload(row)
-                        .send();
-                }
-            }
-        }
-        Ok(())
-    }
 }
 
 async fn make_block_meta_editor_map(

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

@@ -47,7 +47,7 @@ impl DateTypeOption {
         if self.include_time {
             format!("{} {}", self.date_format.format_str(), self.time_format.format_str())
         } else {
-            format!("{}", self.date_format.format_str())
+            self.date_format.format_str().to_string()
         }
     }
 }

+ 97 - 41
frontend/rust-lib/flowy-grid/src/services/grid_editor.rs

@@ -59,9 +59,9 @@ impl ClientGridEditor {
             grid_id,
         } = params;
         let field_id = field.id.clone();
-        let _ = self
-            .modify(|grid| {
-                if grid.contain_field(&field.id) {
+        if self.contain_field(&field_id).await {
+            let _ = self
+                .modify(|grid| {
                     let deserializer = TypeOptionJsonDeserializer(field.field_type.clone());
                     let changeset = FieldChangesetParams {
                         field_id: field.id,
@@ -74,17 +74,22 @@ impl ClientGridEditor {
                         width: Some(field.width),
                         type_option_data: Some(type_option_data),
                     };
-                    Ok(grid.update_field(changeset, deserializer)?)
-                } else {
-                    // let type_option_json = type_option_json_str_from_bytes(type_option_data, &field.field_type);
+                    Ok(grid.update_field_meta(changeset, deserializer)?)
+                })
+                .await?;
+            let _ = self.notify_did_update_grid_field(&field_id).await?;
+        } else {
+            let _ = self
+                .modify(|grid| {
                     let builder = type_option_builder_from_bytes(type_option_data, &field.field_type);
                     let field_meta = FieldBuilder::from_field(field, builder).build();
-                    Ok(grid.create_field(field_meta, start_field_id)?)
-                }
-            })
-            .await?;
-        let _ = self.notify_did_update_grid().await?;
-        let _ = self.notify_did_update_field(&field_id).await?;
+
+                    Ok(grid.create_field_meta(field_meta, start_field_id)?)
+                })
+                .await?;
+            let _ = self.notify_did_insert_grid_field(&field_id).await?;
+        }
+
         Ok(())
     }
 
@@ -100,29 +105,31 @@ impl ClientGridEditor {
 
     pub async fn update_field(&self, params: FieldChangesetParams) -> FlowyResult<()> {
         let field_id = params.field_id.clone();
-        let json_deserializer = match self.pad.read().await.get_field(params.field_id.as_str()) {
+        let json_deserializer = match self.pad.read().await.get_field_meta(params.field_id.as_str()) {
             None => return Err(ErrorCode::FieldDoesNotExist.into()),
-            Some(field_meta) => TypeOptionJsonDeserializer(field_meta.field_type.clone()),
+            Some((_, field_meta)) => TypeOptionJsonDeserializer(field_meta.field_type.clone()),
         };
 
         let _ = self
-            .modify(|grid| Ok(grid.update_field(params, json_deserializer)?))
+            .modify(|grid| Ok(grid.update_field_meta(params, json_deserializer)?))
             .await?;
-        let _ = self.notify_did_update_grid().await?;
-        let _ = self.notify_did_update_field(&field_id).await?;
+
+        let _ = self.notify_did_update_grid_field(&field_id).await?;
         Ok(())
     }
 
     pub async fn replace_field(&self, field_meta: FieldMeta) -> FlowyResult<()> {
         let field_id = field_meta.id.clone();
-        let _ = self.modify(|pad| Ok(pad.replace_field(field_meta)?)).await?;
-        let _ = self.notify_did_update_field(&field_id).await?;
+        let _ = self.modify(|pad| Ok(pad.replace_field_meta(field_meta)?)).await?;
+        let _ = self.notify_did_update_grid_field(&field_id).await?;
         Ok(())
     }
 
     pub async fn delete_field(&self, field_id: &str) -> FlowyResult<()> {
-        let _ = self.modify(|grid| Ok(grid.delete_field(field_id)?)).await?;
-        let _ = self.notify_did_update_grid().await?;
+        let _ = self.modify(|grid| Ok(grid.delete_field_meta(field_id)?)).await?;
+        let field_order = FieldOrder::from(field_id);
+        let notified_changeset = GridFieldChangeset::delete(&self.grid_id, vec![field_order]);
+        let _ = self.notify_did_update_grid(notified_changeset).await?;
         Ok(())
     }
 
@@ -145,19 +152,24 @@ impl ClientGridEditor {
         let _ = self
             .modify(|grid| Ok(grid.switch_to_field(field_id, field_type.clone(), type_option_json_builder)?))
             .await?;
-        let _ = self.notify_did_update_grid().await?;
-        let _ = self.notify_did_update_field(field_id).await?;
+
+        let _ = self.notify_did_update_grid_field(field_id).await?;
+
         Ok(())
     }
 
     pub async fn duplicate_field(&self, field_id: &str) -> FlowyResult<()> {
-        let _ = self.modify(|grid| Ok(grid.duplicate_field(field_id)?)).await?;
-        let _ = self.notify_did_update_grid().await?;
+        let duplicated_field_id = gen_field_id();
+        let _ = self
+            .modify(|grid| Ok(grid.duplicate_field_meta(field_id, &duplicated_field_id)?))
+            .await?;
+
+        let _ = self.notify_did_insert_grid_field(&duplicated_field_id).await?;
         Ok(())
     }
 
     pub async fn get_field_meta(&self, field_id: &str) -> Option<FieldMeta> {
-        let field_meta = self.pad.read().await.get_field(field_id)?.clone();
+        let field_meta = self.pad.read().await.get_field_meta(field_id)?.1.clone();
         Some(field_meta)
     }
 
@@ -302,12 +314,12 @@ impl ClientGridEditor {
         let cell_data_changeset = changeset.data.unwrap();
         let cell_meta = self.get_cell_meta(&changeset.row_id, &changeset.field_id).await?;
         tracing::trace!("{}: {:?}", &changeset.field_id, cell_meta);
-        match self.pad.read().await.get_field(&changeset.field_id) {
+        match self.pad.read().await.get_field_meta(&changeset.field_id) {
             None => {
                 let msg = format!("Field not found with id: {}", &changeset.field_id);
                 Err(FlowyError::internal().context(msg))
             }
-            Some(field_meta) => {
+            Some((_, field_meta)) => {
                 // Update the changeset.data property with the return value.
                 changeset.data = Some(apply_cell_data_changeset(cell_data_changeset, cell_meta, field_meta)?);
                 let _ = self.block_meta_manager.update_cell(changeset).await?;
@@ -326,11 +338,6 @@ 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 {
@@ -375,6 +382,43 @@ impl ClientGridEditor {
         Ok(snapshots)
     }
 
+    pub async fn move_item(&self, params: MoveItemParams) -> FlowyResult<()> {
+        match params.ty {
+            MoveItemType::MoveField => {
+                self.move_field(&params.item_id, params.from_index, params.to_index)
+                    .await
+            }
+            MoveItemType::MoveRow => self.move_row(&params.item_id, params.from_index, params.to_index).await,
+        }
+    }
+
+    pub async fn move_field(&self, field_id: &str, from: i32, to: i32) -> FlowyResult<()> {
+        let _ = self
+            .modify(|grid_pad| Ok(grid_pad.move_field(field_id, from as usize, to as usize)?))
+            .await?;
+        if let Some((index, field_meta)) = self.pad.read().await.get_field_meta(field_id) {
+            let delete_field_order = FieldOrder::from(field_id);
+            let insert_field = IndexField::from_field_meta(field_meta, index);
+            let notified_changeset = GridFieldChangeset {
+                grid_id: self.grid_id.clone(),
+                inserted_fields: vec![insert_field],
+                deleted_fields: vec![delete_field_order],
+                updated_fields: vec![],
+            };
+
+            let _ = self.notify_did_update_grid(notified_changeset).await?;
+        }
+        Ok(())
+    }
+
+    pub async fn move_row(&self, row_id: &str, from: i32, to: i32) -> FlowyResult<()> {
+        let _ = self
+            .block_meta_manager
+            .move_row(row_id, from as usize, to as usize)
+            .await?;
+        Ok(())
+    }
+
     pub async fn delta_bytes(&self) -> Bytes {
         self.pad.read().await.delta_bytes()
     }
@@ -417,28 +461,40 @@ impl ClientGridEditor {
         }
     }
 
-    async fn notify_did_update_grid(&self) -> FlowyResult<()> {
-        let field_metas = self.get_field_metas::<FieldOrder>(None).await?;
-        let repeated_field: RepeatedField = field_metas.into_iter().map(Field::from).collect::<Vec<_>>().into();
-        send_dart_notification(&self.grid_id, GridNotification::DidUpdateGrid)
-            .payload(repeated_field)
-            .send();
+    #[tracing::instrument(level = "trace", skip_all, err)]
+    async fn notify_did_insert_grid_field(&self, field_id: &str) -> FlowyResult<()> {
+        if let Some((index, field_meta)) = self.pad.read().await.get_field_meta(field_id) {
+            let index_field = IndexField::from_field_meta(field_meta, index);
+            let notified_changeset = GridFieldChangeset::insert(&self.grid_id, vec![index_field]);
+            let _ = self.notify_did_update_grid(notified_changeset).await?;
+        }
         Ok(())
     }
 
     #[tracing::instrument(level = "trace", skip_all, err)]
-    async fn notify_did_update_field(&self, field_id: &str) -> FlowyResult<()> {
+    async fn notify_did_update_grid_field(&self, field_id: &str) -> FlowyResult<()> {
         let mut field_metas = self.get_field_metas(Some(vec![field_id])).await?;
         debug_assert!(field_metas.len() == 1);
 
         if let Some(field_meta) = field_metas.pop() {
+            let updated_field = Field::from(field_meta);
+            let notified_changeset = GridFieldChangeset::update(&self.grid_id, vec![updated_field.clone()]);
+            let _ = self.notify_did_update_grid(notified_changeset).await?;
+
             send_dart_notification(field_id, GridNotification::DidUpdateField)
-                .payload(Field::from(field_meta))
+                .payload(updated_field)
                 .send();
         }
 
         Ok(())
     }
+
+    async fn notify_did_update_grid(&self, changeset: GridFieldChangeset) -> FlowyResult<()> {
+        send_dart_notification(&self.grid_id, GridNotification::DidUpdateGridField)
+            .payload(changeset)
+            .send();
+        Ok(())
+    }
 }
 
 #[cfg(feature = "flowy_unit_test")]

+ 2 - 1
frontend/rust-lib/flowy-sdk/Cargo.toml

@@ -38,6 +38,7 @@ tokio = { version = "1", features = ["full"]}
 futures-util = "0.3.15"
 
 [features]
-http_server = ["flowy-user/http_server", "flowy-folder/http_server", "flowy-text-block/http_server"]
+http_sync = ["flowy-folder/cloud_sync", "flowy-text-block/cloud_sync"]
+native_sync = ["flowy-folder/cloud_sync", "flowy-text-block/cloud_sync"]
 use_bunyan = ["lib-log/use_bunyan"]
 dart = ["flowy-user/dart", "flowy-net/dart", "flowy-folder/dart", "flowy-sync/dart", "flowy-grid/dart", "flowy-text-block/dart"]

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

@@ -193,7 +193,7 @@ fn mk_local_server(
     server_config: &ClientServerConfiguration,
 ) -> (Option<Arc<LocalServer>>, Arc<FlowyWebSocketConnect>) {
     let ws_addr = server_config.ws_addr();
-    if cfg!(feature = "http_server") {
+    if cfg!(feature = "http_sync") {
         let ws_conn = Arc::new(FlowyWebSocketConnect::new(ws_addr));
         (None, ws_conn)
     } else {

+ 2 - 1
frontend/rust-lib/flowy-text-block/Cargo.toml

@@ -51,6 +51,7 @@ rand = "0.7.3"
 lib-infra = { path = "../../../shared-lib/lib-infra", features = ["protobuf_file_gen", "proto_gen"] }
 
 [features]
-http_server = []
+sync = []
+cloud_sync = ["sync"]
 flowy_unit_test = ["lib-ot/flowy_unit_test", "flowy-revision/flowy_unit_test"]
 dart = ["lib-infra/dart"]

+ 20 - 6
frontend/rust-lib/flowy-text-block/src/editor.rs

@@ -1,4 +1,4 @@
-use crate::web_socket::{make_block_ws_manager, EditorCommandSender};
+use crate::web_socket::EditorCommandSender;
 use crate::{
     errors::FlowyError,
     queue::{EditBlockQueue, EditorCommand},
@@ -6,9 +6,7 @@ use crate::{
 };
 use bytes::Bytes;
 use flowy_error::{internal_error, FlowyResult};
-use flowy_revision::{
-    RevisionCloudService, RevisionManager, RevisionObjectBuilder, RevisionWebSocket, RevisionWebSocketManager,
-};
+use flowy_revision::{RevisionCloudService, RevisionManager, RevisionObjectBuilder, RevisionWebSocket};
 use flowy_sync::entities::ws_data::ServerRevisionWSData;
 use flowy_sync::{
     entities::{revision::Revision, text_block_info::TextBlockInfo},
@@ -27,11 +25,13 @@ pub struct ClientTextBlockEditor {
     pub doc_id: String,
     #[allow(dead_code)]
     rev_manager: Arc<RevisionManager>,
-    ws_manager: Arc<RevisionWebSocketManager>,
+    #[cfg(feature = "sync")]
+    ws_manager: Arc<flowy_revision::RevisionWebSocketManager>,
     edit_cmd_tx: EditorCommandSender,
 }
 
 impl ClientTextBlockEditor {
+    #[allow(unused_variables)]
     pub(crate) async fn new(
         doc_id: &str,
         user: Arc<dyn TextBlockUser>,
@@ -46,7 +46,8 @@ impl ClientTextBlockEditor {
         let user_id = user.user_id()?;
 
         let edit_cmd_tx = spawn_edit_queue(user, rev_manager.clone(), delta);
-        let ws_manager = make_block_ws_manager(
+        #[cfg(feature = "sync")]
+        let ws_manager = crate::web_socket::make_block_ws_manager(
             doc_id.clone(),
             user_id.clone(),
             edit_cmd_tx.clone(),
@@ -57,6 +58,7 @@ impl ClientTextBlockEditor {
         let editor = Arc::new(Self {
             doc_id,
             rev_manager,
+            #[cfg(feature = "sync")]
             ws_manager,
             edit_cmd_tx,
         });
@@ -158,17 +160,29 @@ impl ClientTextBlockEditor {
         Ok(())
     }
 
+    #[cfg(feature = "sync")]
     pub fn stop(&self) {
         self.ws_manager.stop();
     }
 
+    #[cfg(not(feature = "sync"))]
+    pub fn stop(&self) {}
+
+    #[cfg(feature = "sync")]
     pub(crate) async fn receive_ws_data(&self, data: ServerRevisionWSData) -> Result<(), FlowyError> {
         self.ws_manager.receive_ws_data(data).await
     }
+    #[cfg(not(feature = "sync"))]
+    pub(crate) async fn receive_ws_data(&self, _data: ServerRevisionWSData) -> Result<(), FlowyError> {
+        Ok(())
+    }
 
+    #[cfg(feature = "sync")]
     pub(crate) fn receive_ws_state(&self, state: &WSConnectState) {
         self.ws_manager.connect_state_changed(state.clone());
     }
+    #[cfg(not(feature = "sync"))]
+    pub(crate) fn receive_ws_state(&self, _state: &WSConnectState) {}
 }
 
 impl std::ops::Drop for ClientTextBlockEditor {

+ 3 - 0
frontend/rust-lib/flowy-text-block/src/web_socket.rs

@@ -23,6 +23,7 @@ use tokio::sync::{
 pub(crate) type EditorCommandSender = Sender<EditorCommand>;
 pub(crate) type EditorCommandReceiver = Receiver<EditorCommand>;
 
+#[allow(dead_code)]
 pub(crate) async fn make_block_ws_manager(
     doc_id: String,
     user_id: String,
@@ -49,6 +50,7 @@ pub(crate) async fn make_block_ws_manager(
     ws_manager
 }
 
+#[allow(dead_code)]
 fn listen_document_ws_state(_user_id: &str, _doc_id: &str, mut subscriber: broadcast::Receiver<WSConnectState>) {
     tokio::spawn(async move {
         while let Ok(state) = subscriber.recv().await {
@@ -67,6 +69,7 @@ pub(crate) struct TextBlockRevisionWSDataStream {
 }
 
 impl TextBlockRevisionWSDataStream {
+    #[allow(dead_code)]
     pub fn new(conflict_controller: RichTextConflictController) -> Self {
         Self {
             conflict_controller: Arc::new(conflict_controller),

+ 0 - 1
frontend/rust-lib/flowy-user/Cargo.toml

@@ -37,7 +37,6 @@ futures = "0.3.15"
 nanoid = "0.4.0"
 
 [features]
-http_server = []
 dart = ["lib-infra/dart"]
 
 [build-dependencies]

+ 131 - 14
shared-lib/flowy-grid-data-model/src/entities/grid.rs

@@ -84,6 +84,68 @@ impl std::convert::From<String> for FieldOrder {
     }
 }
 
+#[derive(Debug, Clone, Default, ProtoBuf)]
+pub struct GridFieldChangeset {
+    #[pb(index = 1)]
+    pub grid_id: String,
+
+    #[pb(index = 2)]
+    pub inserted_fields: Vec<IndexField>,
+
+    #[pb(index = 3)]
+    pub deleted_fields: Vec<FieldOrder>,
+
+    #[pb(index = 4)]
+    pub updated_fields: Vec<Field>,
+}
+
+impl GridFieldChangeset {
+    pub fn insert(grid_id: &str, inserted_fields: Vec<IndexField>) -> Self {
+        Self {
+            grid_id: grid_id.to_owned(),
+            inserted_fields,
+            deleted_fields: vec![],
+            updated_fields: vec![],
+        }
+    }
+
+    pub fn delete(grid_id: &str, deleted_fields: Vec<FieldOrder>) -> Self {
+        Self {
+            grid_id: grid_id.to_string(),
+            inserted_fields: vec![],
+            deleted_fields,
+            updated_fields: vec![],
+        }
+    }
+
+    pub fn update(grid_id: &str, updated_fields: Vec<Field>) -> Self {
+        Self {
+            grid_id: grid_id.to_string(),
+            inserted_fields: vec![],
+            deleted_fields: vec![],
+            updated_fields,
+        }
+    }
+}
+
+#[derive(Debug, Clone, Default, ProtoBuf)]
+pub struct IndexField {
+    #[pb(index = 1)]
+    pub field: Field,
+
+    #[pb(index = 2)]
+    pub index: i32,
+}
+
+impl IndexField {
+    pub fn from_field_meta(field_meta: &FieldMeta, index: usize) -> Self {
+        Self {
+            field: Field::from(field_meta.clone()),
+            index: index as i32,
+        }
+    }
+}
+
 #[derive(Debug, Default, ProtoBuf)]
 pub struct GetEditFieldContextPayload {
     #[pb(index = 1)]
@@ -278,7 +340,16 @@ impl GridBlockOrder {
 }
 
 #[derive(Debug, Clone, Default, ProtoBuf)]
-pub struct GridBlockOrderChangeset {
+pub struct IndexRowOrder {
+    #[pb(index = 1)]
+    pub row_order: RowOrder,
+
+    #[pb(index = 2, one_of)]
+    pub index: Option<i32>,
+}
+
+#[derive(Debug, Clone, Default, ProtoBuf)]
+pub struct GridRowsChangeset {
     #[pb(index = 1)]
     pub block_id: String,
 
@@ -292,15 +363,6 @@ pub struct GridBlockOrderChangeset {
     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 }
@@ -314,8 +376,8 @@ impl std::convert::From<&RowMeta> for IndexRowOrder {
     }
 }
 
-impl GridBlockOrderChangeset {
-    pub fn from_insert(block_id: &str, inserted_rows: Vec<IndexRowOrder>) -> Self {
+impl GridRowsChangeset {
+    pub fn insert(block_id: &str, inserted_rows: Vec<IndexRowOrder>) -> Self {
         Self {
             block_id: block_id.to_owned(),
             inserted_rows,
@@ -324,7 +386,7 @@ impl GridBlockOrderChangeset {
         }
     }
 
-    pub fn from_delete(block_id: &str, deleted_rows: Vec<RowOrder>) -> Self {
+    pub fn delete(block_id: &str, deleted_rows: Vec<RowOrder>) -> Self {
         Self {
             block_id: block_id.to_owned(),
             inserted_rows: vec![],
@@ -333,7 +395,7 @@ impl GridBlockOrderChangeset {
         }
     }
 
-    pub fn from_update(block_id: &str, updated_rows: Vec<RowOrder>) -> Self {
+    pub fn update(block_id: &str, updated_rows: Vec<RowOrder>) -> Self {
         Self {
             block_id: block_id.to_owned(),
             inserted_rows: vec![],
@@ -656,6 +718,61 @@ impl TryInto<FieldChangesetParams> for FieldChangesetPayload {
     }
 }
 
+#[derive(Debug, Clone, ProtoBuf_Enum)]
+pub enum MoveItemType {
+    MoveField = 0,
+    MoveRow = 1,
+}
+
+impl std::default::Default for MoveItemType {
+    fn default() -> Self {
+        MoveItemType::MoveField
+    }
+}
+
+#[derive(Debug, Clone, Default, ProtoBuf)]
+pub struct MoveItemPayload {
+    #[pb(index = 1)]
+    pub grid_id: String,
+
+    #[pb(index = 2)]
+    pub item_id: String,
+
+    #[pb(index = 3)]
+    pub from_index: i32,
+
+    #[pb(index = 4)]
+    pub to_index: i32,
+
+    #[pb(index = 5)]
+    pub ty: MoveItemType,
+}
+
+#[derive(Clone)]
+pub struct MoveItemParams {
+    pub grid_id: String,
+    pub item_id: String,
+    pub from_index: i32,
+    pub to_index: i32,
+    pub ty: MoveItemType,
+}
+
+impl TryInto<MoveItemParams> for MoveItemPayload {
+    type Error = ErrorCode;
+
+    fn try_into(self) -> Result<MoveItemParams, Self::Error> {
+        let grid_id = NotEmptyStr::parse(self.grid_id).map_err(|_| ErrorCode::GridIdIsEmpty)?;
+        let item_id = NotEmptyStr::parse(self.item_id).map_err(|_| ErrorCode::InvalidData)?;
+        Ok(MoveItemParams {
+            grid_id: grid_id.0,
+            item_id: item_id.0,
+            from_index: self.from_index,
+            to_index: self.to_index,
+            ty: self.ty,
+        })
+    }
+}
+
 #[derive(
     Debug,
     Clone,

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 788 - 273
shared-lib/flowy-grid-data-model/src/protobuf/model/grid.rs


+ 26 - 5
shared-lib/flowy-grid-data-model/src/protobuf/proto/grid.proto

@@ -17,6 +17,16 @@ message Field {
 message FieldOrder {
     string field_id = 1;
 }
+message GridFieldChangeset {
+    string grid_id = 1;
+    repeated IndexField inserted_fields = 2;
+    repeated FieldOrder deleted_fields = 3;
+    repeated Field updated_fields = 4;
+}
+message IndexField {
+    Field field = 1;
+    int32 index = 2;
+}
 message GetEditFieldContextPayload {
     string grid_id = 1;
     oneof one_of_field_id { string field_id = 2; };
@@ -58,16 +68,16 @@ message GridBlockOrder {
     string block_id = 1;
     repeated RowOrder row_orders = 2;
 }
-message GridBlockOrderChangeset {
+message IndexRowOrder {
+    RowOrder row_order = 1;
+    oneof one_of_index { int32 index = 2; };
+}
+message GridRowsChangeset {
     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;
     repeated RowOrder row_orders = 2;
@@ -123,12 +133,23 @@ message FieldChangesetPayload {
     oneof one_of_width { int32 width = 8; };
     oneof one_of_type_option_data { bytes type_option_data = 9; };
 }
+message MoveItemPayload {
+    string grid_id = 1;
+    string item_id = 2;
+    int32 from_index = 3;
+    int32 to_index = 4;
+    MoveItemType ty = 5;
+}
 message CellChangeset {
     string grid_id = 1;
     string row_id = 2;
     string field_id = 3;
     oneof one_of_data { string data = 4; };
 }
+enum MoveItemType {
+    MoveField = 0;
+    MoveRow = 1;
+}
 enum FieldType {
     RichText = 0;
     Number = 1;

+ 13 - 0
shared-lib/flowy-sync/src/client_grid/grid_block_meta_pad.rs

@@ -149,6 +149,19 @@ impl GridBlockMetaPad {
         })
     }
 
+    pub fn move_row(&mut self, row_id: &str, from: usize, to: usize) -> CollaborateResult<Option<GridBlockMetaChange>> {
+        self.modify(|row_metas| {
+            if let Some(position) = row_metas.iter().position(|row_meta| row_meta.id == row_id) {
+                debug_assert_eq!(from, position);
+                let row_meta = row_metas.remove(position);
+                row_metas.insert(to, row_meta);
+                Ok(Some(()))
+            } else {
+                Ok(None)
+            }
+        })
+    }
+
     pub fn modify<F>(&mut self, f: F) -> CollaborateResult<Option<GridBlockMetaChange>>
     where
         F: for<'a> FnOnce(&'a mut Vec<Arc<RowMeta>>) -> CollaborateResult<Option<()>>,

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

@@ -3,12 +3,11 @@ use crate::errors::{internal_error, CollaborateError, CollaborateResult};
 use crate::util::{cal_diff, make_delta_from_revisions};
 use bytes::Bytes;
 use flowy_grid_data_model::entities::{
-    gen_field_id, gen_grid_id, FieldChangesetParams, FieldMeta, FieldOrder, FieldType, GridBlockMeta,
-    GridBlockMetaChangeset, GridMeta,
+    gen_grid_id, FieldChangesetParams, FieldMeta, FieldOrder, FieldType, GridBlockMeta, GridBlockMetaChangeset,
+    GridMeta,
 };
 use lib_ot::core::{OperationTransformable, PlainTextAttributes, PlainTextDelta, PlainTextDeltaBuilder};
 use std::collections::HashMap;
-
 use std::sync::Arc;
 
 pub type GridMetaDelta = PlainTextDelta;
@@ -41,7 +40,7 @@ impl GridMetaPad {
     }
 
     #[tracing::instrument(level = "debug", skip_all, err)]
-    pub fn create_field(
+    pub fn create_field_meta(
         &mut self,
         new_field_meta: FieldMeta,
         start_field_id: Option<String>,
@@ -70,7 +69,7 @@ impl GridMetaPad {
         })
     }
 
-    pub fn delete_field(&mut self, field_id: &str) -> CollaborateResult<Option<GridChangeset>> {
+    pub fn delete_field_meta(&mut self, field_id: &str) -> CollaborateResult<Option<GridChangeset>> {
         self.modify_grid(
             |grid_meta| match grid_meta.fields.iter().position(|field| field.id == field_id) {
                 None => Ok(None),
@@ -82,13 +81,17 @@ impl GridMetaPad {
         )
     }
 
-    pub fn duplicate_field(&mut self, field_id: &str) -> CollaborateResult<Option<GridChangeset>> {
+    pub fn duplicate_field_meta(
+        &mut self,
+        field_id: &str,
+        duplicated_field_id: &str,
+    ) -> CollaborateResult<Option<GridChangeset>> {
         self.modify_grid(
             |grid_meta| match grid_meta.fields.iter().position(|field| field.id == field_id) {
                 None => Ok(None),
                 Some(index) => {
                     let mut duplicate_field_meta = grid_meta.fields[index].clone();
-                    duplicate_field_meta.id = gen_field_id();
+                    duplicate_field_meta.id = duplicated_field_id.to_string();
                     duplicate_field_meta.name = format!("{} (copy)", duplicate_field_meta.name);
                     grid_meta.fields.insert(index + 1, duplicate_field_meta);
                     Ok(Some(()))
@@ -126,7 +129,7 @@ impl GridMetaPad {
         })
     }
 
-    pub fn update_field<T: JsonDeserializer>(
+    pub fn update_field_meta<T: JsonDeserializer>(
         &mut self,
         changeset: FieldChangesetParams,
         deserializer: T,
@@ -181,11 +184,15 @@ impl GridMetaPad {
         })
     }
 
-    pub fn get_field(&self, field_id: &str) -> Option<&FieldMeta> {
-        self.grid_meta.fields.iter().find(|field| field.id == field_id)
+    pub fn get_field_meta(&self, field_id: &str) -> Option<(usize, &FieldMeta)> {
+        self.grid_meta
+            .fields
+            .iter()
+            .enumerate()
+            .find(|(_, field)| field.id == field_id)
     }
 
-    pub fn replace_field(&mut self, field_meta: FieldMeta) -> CollaborateResult<Option<GridChangeset>> {
+    pub fn replace_field_meta(&mut self, field_meta: FieldMeta) -> CollaborateResult<Option<GridChangeset>> {
         self.modify_grid(
             |grid_meta| match grid_meta.fields.iter().position(|field| field.id == field_meta.id) {
                 None => Ok(None),
@@ -198,6 +205,25 @@ impl GridMetaPad {
         )
     }
 
+    pub fn move_field(
+        &mut self,
+        field_id: &str,
+        _from_index: usize,
+        to_index: usize,
+    ) -> CollaborateResult<Option<GridChangeset>> {
+        self.modify_grid(
+            |grid_meta| match grid_meta.fields.iter().position(|field| field.id == field_id) {
+                None => Ok(None),
+                Some(index) => {
+                    // debug_assert_eq!(index, from_index);
+                    let field_meta = grid_meta.fields.remove(index);
+                    grid_meta.fields.insert(to_index, field_meta);
+                    Ok(Some(()))
+                }
+            },
+        )
+    }
+
     pub fn contain_field(&self, field_id: &str) -> bool {
         self.grid_meta.fields.iter().any(|field| field.id == field_id)
     }

Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio