Browse Source

Merge pull request #460 from AppFlowy-IO/feat_row_detail

Feature: show row detail
Nathan.fooo 3 years ago
parent
commit
8a7c3a9cfc
71 changed files with 1515 additions and 951 deletions
  1. 6 0
      frontend/app_flowy/assets/images/grid/expander.svg
  2. 2 1
      frontend/app_flowy/assets/translations/en.json
  3. 6 17
      frontend/app_flowy/lib/startup/deps_resolver.dart
  4. 2 2
      frontend/app_flowy/lib/workspace/application/grid/cell/cell_service.dart
  5. 8 8
      frontend/app_flowy/lib/workspace/application/grid/cell/checkbox_cell_bloc.dart
  6. 6 6
      frontend/app_flowy/lib/workspace/application/grid/cell/date_cell_bloc.dart
  7. 3 3
      frontend/app_flowy/lib/workspace/application/grid/cell/number_cell_bloc.dart
  8. 2 1
      frontend/app_flowy/lib/workspace/application/grid/cell/select_option_service.dart
  9. 3 3
      frontend/app_flowy/lib/workspace/application/grid/cell/selection_cell_bloc.dart
  10. 2 2
      frontend/app_flowy/lib/workspace/application/grid/cell/selection_editor_bloc.dart
  11. 52 12
      frontend/app_flowy/lib/workspace/application/grid/cell/text_cell_bloc.dart
  12. 4 4
      frontend/app_flowy/lib/workspace/application/grid/field/type_option/multi_select_bloc.dart
  13. 4 5
      frontend/app_flowy/lib/workspace/application/grid/field/type_option/single_select_bloc.dart
  14. 29 2
      frontend/app_flowy/lib/workspace/application/grid/field/type_option/type_option_service.dart
  15. 9 5
      frontend/app_flowy/lib/workspace/application/grid/grid_bloc.dart
  16. 43 0
      frontend/app_flowy/lib/workspace/application/grid/grid_service.dart
  17. 13 76
      frontend/app_flowy/lib/workspace/application/grid/row/row_bloc.dart
  18. 74 0
      frontend/app_flowy/lib/workspace/application/grid/row/row_detail_bloc.dart
  19. 165 113
      frontend/app_flowy/lib/workspace/application/grid/row/row_service.dart
  20. 8 10
      frontend/app_flowy/lib/workspace/presentation/home/menu/app/header/add_button.dart
  21. 5 3
      frontend/app_flowy/lib/workspace/presentation/home/menu/app/menu_app.dart
  22. 9 13
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/grid_page.dart
  23. 11 2
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/cell_builder.dart
  24. 54 35
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/cell_container.dart
  25. 4 3
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/checkbox_cell.dart
  26. 11 6
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/date_cell.dart
  27. 11 8
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/number_cell.dart
  28. 13 13
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/selection_cell/selection_cell.dart
  29. 2 2
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/selection_cell/selection_editor.dart
  30. 34 12
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/text_cell.dart
  31. 32 9
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/field_cell.dart
  32. 30 14
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/field_cell_action_sheet.dart
  33. 3 1
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/field_editor.dart
  34. 23 21
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/field_switcher.dart
  35. 6 11
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/type_option/multi_select.dart
  36. 6 11
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/type_option/single_select.dart
  37. 1 1
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/row/cell/number_cell.dart
  38. 108 42
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/row/grid_row.dart
  39. 1 1
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/row/number_cell.dart
  40. 157 0
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/row/row_detail.dart
  41. 1 1
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/toolbar/grid_property.dart
  42. 20 22
      frontend/app_flowy/lib/workspace/presentation/widgets/pop_up_action.dart
  43. 42 19
      frontend/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/flowy_overlay.dart
  44. 124 8
      frontend/app_flowy/packages/flowy_infra_ui/lib/style_widget/hover.dart
  45. 1 1
      frontend/app_flowy/packages/flowy_sdk/lib/dispatch/dart_event/flowy-grid/dart_event.dart
  46. 14 0
      frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid-data-model/grid.pb.dart
  47. 2 1
      frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid-data-model/grid.pbjson.dart
  48. 11 56
      frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid/cell_entities.pb.dart
  49. 2 12
      frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid/cell_entities.pbjson.dart
  50. 1 3
      frontend/rust-lib/flowy-grid/Flowy.toml
  51. 15 23
      frontend/rust-lib/flowy-grid/src/event_handler.rs
  52. 1 1
      frontend/rust-lib/flowy-grid/src/event_map.rs
  53. 32 191
      frontend/rust-lib/flowy-grid/src/protobuf/model/cell_entities.rs
  54. 2 4
      frontend/rust-lib/flowy-grid/src/protobuf/proto/cell_entities.proto
  55. 0 3
      frontend/rust-lib/flowy-grid/src/services/cell/mod.rs
  56. 13 10
      frontend/rust-lib/flowy-grid/src/services/entities/cell_entities.rs
  57. 0 0
      frontend/rust-lib/flowy-grid/src/services/entities/field_entities.rs
  58. 7 0
      frontend/rust-lib/flowy-grid/src/services/entities/mod.rs
  59. 31 0
      frontend/rust-lib/flowy-grid/src/services/entities/row_entities.rs
  60. 7 1
      frontend/rust-lib/flowy-grid/src/services/field/field_builder.rs
  61. 0 2
      frontend/rust-lib/flowy-grid/src/services/field/mod.rs
  62. 58 2
      frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option.rs
  63. 28 25
      frontend/rust-lib/flowy-grid/src/services/grid_editor.rs
  64. 1 1
      frontend/rust-lib/flowy-grid/src/services/mod.rs
  65. 1 0
      frontend/rust-lib/flowy-grid/src/util.rs
  66. 2 0
      frontend/rust-lib/flowy-grid/tests/grid/script.rs
  67. 4 0
      shared-lib/flowy-grid-data-model/src/entities/grid.rs
  68. 11 2
      shared-lib/flowy-grid-data-model/src/entities/meta.rs
  69. 119 83
      shared-lib/flowy-grid-data-model/src/protobuf/model/grid.rs
  70. 1 0
      shared-lib/flowy-grid-data-model/src/protobuf/proto/grid.proto
  71. 2 2
      shared-lib/flowy-sync/src/client_grid/grid_builder.rs

+ 6 - 0
frontend/app_flowy/assets/images/grid/expander.svg

@@ -0,0 +1,6 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M6 13H3V10" stroke="#00BCF0" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M10 3H13V6" stroke="#00BCF0" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M3 13L7 9" stroke="#00BCF0" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M13 3L9 7" stroke="#00BCF0" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

+ 2 - 1
frontend/app_flowy/assets/translations/en.json

@@ -177,7 +177,8 @@
     },
     "row": {
       "duplicate": "Duplicate",
-      "delete": "Delete"
+      "delete": "Delete",
+      "textPlaceholder": "Empty"
     },
     "selectOption": {
       "purpleColor": "Purple",

+ 6 - 17
frontend/app_flowy/lib/startup/deps_resolver.dart

@@ -3,7 +3,6 @@ import 'package:app_flowy/user/application/user_listener.dart';
 import 'package:app_flowy/user/application/user_service.dart';
 import 'package:app_flowy/workspace/application/app/prelude.dart';
 import 'package:app_flowy/workspace/application/doc/prelude.dart';
-import 'package:app_flowy/workspace/application/grid/field/type_option/multi_select_bloc.dart';
 import 'package:app_flowy/workspace/application/grid/prelude.dart';
 import 'package:app_flowy/workspace/application/trash/prelude.dart';
 import 'package:app_flowy/workspace/application/workspace/prelude.dart';
@@ -19,7 +18,6 @@ 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/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';
 import 'package:flowy_sdk/protobuf/flowy-user-data-model/user_profile.pb.dart';
 import 'package:get_it/get_it.dart';
 
@@ -170,32 +168,31 @@ void _resolveGridDeps(GetIt getIt) {
     ),
   );
 
-  getIt.registerFactoryParam<TextCellBloc, GridCellIdentifier, void>(
+  getIt.registerFactoryParam<TextCellBloc, GridCell, void>(
     (cellData, _) => TextCellBloc(
-      service: CellService(),
       cellData: cellData,
     ),
   );
 
-  getIt.registerFactoryParam<SelectionCellBloc, GridCellIdentifier, void>(
+  getIt.registerFactoryParam<SelectionCellBloc, GridCell, void>(
     (cellData, _) => SelectionCellBloc(
       cellData: cellData,
     ),
   );
 
-  getIt.registerFactoryParam<NumberCellBloc, GridCellIdentifier, void>(
+  getIt.registerFactoryParam<NumberCellBloc, GridCell, void>(
     (cellData, _) => NumberCellBloc(
       cellData: cellData,
     ),
   );
 
-  getIt.registerFactoryParam<DateCellBloc, GridCellIdentifier, void>(
+  getIt.registerFactoryParam<DateCellBloc, GridCell, void>(
     (cellData, _) => DateCellBloc(
-      cellIdentifier: cellData,
+      cellData: cellData,
     ),
   );
 
-  getIt.registerFactoryParam<CheckboxCellBloc, GridCellIdentifier, void>(
+  getIt.registerFactoryParam<CheckboxCellBloc, GridCell, void>(
     (cellData, _) => CheckboxCellBloc(
       service: CellService(),
       cellData: cellData,
@@ -206,14 +203,6 @@ void _resolveGridDeps(GetIt getIt) {
     (context, _) => FieldSwitcherBloc(context),
   );
 
-  getIt.registerFactoryParam<SingleSelectTypeOptionBloc, SingleSelectTypeOption, String>(
-    (typeOption, fieldId) => SingleSelectTypeOptionBloc(typeOption, fieldId),
-  );
-
-  getIt.registerFactoryParam<MultiSelectTypeOptionBloc, MultiSelectTypeOption, String>(
-    (typeOption, fieldId) => MultiSelectTypeOptionBloc(typeOption, fieldId),
-  );
-
   getIt.registerFactoryParam<DateTypeOptionBloc, DateTypeOption, void>(
     (typeOption, _) => DateTypeOptionBloc(typeOption: typeOption),
   );

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

@@ -44,7 +44,7 @@ class CellCache {
 
   CellCache() : _cellService = CellService();
 
-  Future<Option<Cell>> getCellData(GridCellIdentifier identifier) async {
+  Future<Option<Cell>> getCellData(GridCell identifier) async {
     final cellId = _cellId(identifier);
     final Cell? data = _cellDataMap[cellId];
     if (data != null) {
@@ -69,7 +69,7 @@ class CellCache {
     );
   }
 
-  String _cellId(GridCellIdentifier identifier) {
+  String _cellId(GridCell identifier) {
     return "${identifier.rowId}/${identifier.field.id}";
   }
 }

+ 8 - 8
frontend/app_flowy/lib/workspace/application/grid/cell/checkbox_cell_bloc.dart

@@ -11,13 +11,13 @@ part 'checkbox_cell_bloc.freezed.dart';
 
 class CheckboxCellBloc extends Bloc<CheckboxCellEvent, CheckboxCellState> {
   final CellService _service;
-  final CellListener _listener;
+  final CellListener _cellListener;
 
   CheckboxCellBloc({
     required CellService service,
-    required GridCellIdentifier cellData,
+    required GridCell cellData,
   })  : _service = service,
-        _listener = CellListener(rowId: cellData.rowId, fieldId: cellData.field.id),
+        _cellListener = CellListener(rowId: cellData.rowId, fieldId: cellData.field.id),
         super(CheckboxCellState.initial(cellData)) {
     on<CheckboxCellEvent>(
       (event, emit) async {
@@ -38,18 +38,18 @@ class CheckboxCellBloc extends Bloc<CheckboxCellEvent, CheckboxCellState> {
 
   @override
   Future<void> close() async {
-    await _listener.stop();
+    await _cellListener.stop();
     return super.close();
   }
 
   void _startListening() {
-    _listener.updateCellNotifier?.addPublishListener((result) {
+    _cellListener.updateCellNotifier?.addPublishListener((result) {
       result.fold(
         (notificationData) async => await _loadCellData(),
         (err) => Log.error(err),
       );
     });
-    _listener.start();
+    _cellListener.start();
   }
 
   Future<void> _loadCellData() async {
@@ -87,11 +87,11 @@ class CheckboxCellEvent with _$CheckboxCellEvent {
 @freezed
 class CheckboxCellState with _$CheckboxCellState {
   const factory CheckboxCellState({
-    required GridCellIdentifier cellData,
+    required GridCell cellData,
     required bool isSelected,
   }) = _CheckboxCellState;
 
-  factory CheckboxCellState.initial(GridCellIdentifier cellData) {
+  factory CheckboxCellState.initial(GridCell cellData) {
     return CheckboxCellState(cellData: cellData, isSelected: _isSelected(cellData.cell));
   }
 }

+ 6 - 6
frontend/app_flowy/lib/workspace/application/grid/cell/date_cell_bloc.dart

@@ -15,11 +15,11 @@ class DateCellBloc extends Bloc<DateCellEvent, DateCellState> {
   final CellListener _cellListener;
   final SingleFieldListener _fieldListener;
 
-  DateCellBloc({required GridCellIdentifier cellIdentifier})
+  DateCellBloc({required GridCell cellData})
       : _service = CellService(),
-        _cellListener = CellListener(rowId: cellIdentifier.rowId, fieldId: cellIdentifier.field.id),
-        _fieldListener = SingleFieldListener(fieldId: cellIdentifier.field.id),
-        super(DateCellState.initial(cellIdentifier)) {
+        _cellListener = CellListener(rowId: cellData.rowId, fieldId: cellData.field.id),
+        _fieldListener = SingleFieldListener(fieldId: cellData.field.id),
+        super(DateCellState.initial(cellData)) {
     on<DateCellEvent>(
       (event, emit) async {
         event.map(
@@ -106,13 +106,13 @@ class DateCellEvent with _$DateCellEvent {
 @freezed
 class DateCellState with _$DateCellState {
   const factory DateCellState({
-    required GridCellIdentifier cellData,
+    required GridCell cellData,
     required String content,
     required Field field,
     DateTime? selectedDay,
   }) = _DateCellState;
 
-  factory DateCellState.initial(GridCellIdentifier cellData) => DateCellState(
+  factory DateCellState.initial(GridCell cellData) => DateCellState(
         cellData: cellData,
         field: cellData.field,
         content: cellData.cell?.content ?? "",

+ 3 - 3
frontend/app_flowy/lib/workspace/application/grid/cell/number_cell_bloc.dart

@@ -16,7 +16,7 @@ class NumberCellBloc extends Bloc<NumberCellEvent, NumberCellState> {
   final SingleFieldListener _fieldListener;
 
   NumberCellBloc({
-    required GridCellIdentifier cellData,
+    required GridCell cellData,
   })  : _service = CellService(),
         _cellListener = CellListener(rowId: cellData.rowId, fieldId: cellData.field.id),
         _fieldListener = SingleFieldListener(fieldId: cellData.field.id),
@@ -105,11 +105,11 @@ class NumberCellEvent with _$NumberCellEvent {
 @freezed
 class NumberCellState with _$NumberCellState {
   const factory NumberCellState({
-    required GridCellIdentifier cellData,
+    required GridCell cellData,
     required String content,
   }) = _NumberCellState;
 
-  factory NumberCellState.initial(GridCellIdentifier cellData) {
+  factory NumberCellState.initial(GridCell cellData) {
     return NumberCellState(cellData: cellData, content: cellData.cell?.content ?? "");
   }
 }

+ 2 - 1
frontend/app_flowy/lib/workspace/application/grid/cell/select_option_service.dart

@@ -1,3 +1,4 @@
+import 'package:app_flowy/workspace/application/grid/field/type_option/type_option_service.dart';
 import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
 import 'package:dartz/dartz.dart';
 import 'package:flowy_sdk/dispatch/dispatch.dart';
@@ -13,7 +14,7 @@ class SelectOptionService {
     required String rowId,
     required String name,
   }) {
-    return GridEventNewSelectOption(SelectOptionName.create()..name = name).send().then(
+    return TypeOptionService(gridId: gridId, fieldId: fieldId).newOption(name: name).then(
       (result) {
         return result.fold(
           (option) {

+ 3 - 3
frontend/app_flowy/lib/workspace/application/grid/cell/selection_cell_bloc.dart

@@ -16,7 +16,7 @@ class SelectionCellBloc extends Bloc<SelectionCellEvent, SelectionCellState> {
   final SingleFieldListener _fieldListener;
 
   SelectionCellBloc({
-    required GridCellIdentifier cellData,
+    required GridCell cellData,
   })  : _service = SelectOptionService(),
         _cellListener = CellListener(rowId: cellData.rowId, fieldId: cellData.field.id),
         _fieldListener = SingleFieldListener(fieldId: cellData.field.id),
@@ -93,12 +93,12 @@ class SelectionCellEvent with _$SelectionCellEvent {
 @freezed
 class SelectionCellState with _$SelectionCellState {
   const factory SelectionCellState({
-    required GridCellIdentifier cellData,
+    required GridCell cellData,
     required List<SelectOption> options,
     required List<SelectOption> selectedOptions,
   }) = _SelectionCellState;
 
-  factory SelectionCellState.initial(GridCellIdentifier cellData) => SelectionCellState(
+  factory SelectionCellState.initial(GridCell cellData) => SelectionCellState(
         cellData: cellData,
         options: [],
         selectedOptions: [],

+ 2 - 2
frontend/app_flowy/lib/workspace/application/grid/cell/selection_editor_bloc.dart

@@ -18,7 +18,7 @@ class SelectOptionEditorBloc extends Bloc<SelectOptionEditorEvent, SelectOptionE
   Timer? _delayOperation;
 
   SelectOptionEditorBloc({
-    required GridCellIdentifier cellData,
+    required GridCell cellData,
     required List<SelectOption> options,
     required List<SelectOption> selectedOptions,
   })  : _selectOptionService = SelectOptionService(),
@@ -174,7 +174,7 @@ class SelectOptionEditorState with _$SelectOptionEditorState {
   }) = _SelectOptionEditorState;
 
   factory SelectOptionEditorState.initial(
-    GridCellIdentifier cellData,
+    GridCell cellData,
     List<SelectOption> options,
     List<SelectOption> selectedOptions,
   ) {

+ 52 - 12
frontend/app_flowy/lib/workspace/application/grid/cell/text_cell_bloc.dart

@@ -1,22 +1,29 @@
 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:flutter_bloc/flutter_bloc.dart';
 import 'package:freezed_annotation/freezed_annotation.dart';
 import 'dart:async';
+import 'cell_listener.dart';
 import 'cell_service.dart';
 
 part 'text_cell_bloc.freezed.dart';
 
 class TextCellBloc extends Bloc<TextCellEvent, TextCellState> {
-  final CellService service;
+  final CellService _service;
+  final CellListener _cellListener;
 
   TextCellBloc({
-    required this.service,
-    required GridCellIdentifier cellData,
-  }) : super(TextCellState.initial(cellData)) {
+    required GridCell cellData,
+  })  : _service = CellService(),
+        _cellListener = CellListener(rowId: cellData.rowId, fieldId: cellData.field.id),
+        super(TextCellState.initial(cellData)) {
     on<TextCellEvent>(
       (event, emit) async {
         await event.map(
-          initial: (_InitialCell value) async {},
+          initial: (_InitialCell value) async {
+            _startListening();
+          },
           updateText: (_UpdateText value) {
             updateCellContent(value.text);
             emit(state.copyWith(content: value.text));
@@ -27,16 +34,28 @@ class TextCellBloc extends Bloc<TextCellEvent, TextCellState> {
               content: value.cellData.cell?.content ?? "",
             ));
           },
+          didReceiveCellUpdate: (_DidReceiveCellUpdate value) {
+            emit(state.copyWith(
+              cellData: state.cellData.copyWith(cell: value.cell),
+              content: value.cell.content,
+            ));
+          },
         );
       },
     );
   }
 
+  @override
+  Future<void> close() async {
+    await _cellListener.stop();
+    return super.close();
+  }
+
   void updateCellContent(String content) {
     final fieldId = state.cellData.field.id;
     final gridId = state.cellData.gridId;
     final rowId = state.cellData.rowId;
-    service.updateCell(
+    _service.updateCell(
       data: content,
       fieldId: fieldId,
       gridId: gridId,
@@ -44,16 +63,37 @@ class TextCellBloc extends Bloc<TextCellEvent, TextCellState> {
     );
   }
 
-  @override
-  Future<void> close() async {
-    return super.close();
+  void _startListening() {
+    _cellListener.updateCellNotifier?.addPublishListener((result) {
+      result.fold(
+        (notificationData) async => await _loadCellData(),
+        (err) => Log.error(err),
+      );
+    });
+    _cellListener.start();
+  }
+
+  Future<void> _loadCellData() async {
+    final result = await _service.getCell(
+      gridId: state.cellData.gridId,
+      fieldId: state.cellData.field.id,
+      rowId: state.cellData.rowId,
+    );
+    if (isClosed) {
+      return;
+    }
+    result.fold(
+      (cell) => add(TextCellEvent.didReceiveCellUpdate(cell)),
+      (err) => Log.error(err),
+    );
   }
 }
 
 @freezed
 class TextCellEvent with _$TextCellEvent {
   const factory TextCellEvent.initial() = _InitialCell;
-  const factory TextCellEvent.didReceiveCellData(GridCellIdentifier cellData) = _DidReceiveCellData;
+  const factory TextCellEvent.didReceiveCellData(GridCell cellData) = _DidReceiveCellData;
+  const factory TextCellEvent.didReceiveCellUpdate(Cell cell) = _DidReceiveCellUpdate;
   const factory TextCellEvent.updateText(String text) = _UpdateText;
 }
 
@@ -61,10 +101,10 @@ class TextCellEvent with _$TextCellEvent {
 class TextCellState with _$TextCellState {
   const factory TextCellState({
     required String content,
-    required GridCellIdentifier cellData,
+    required GridCell cellData,
   }) = _TextCellState;
 
-  factory TextCellState.initial(GridCellIdentifier cellData) => TextCellState(
+  factory TextCellState.initial(GridCell cellData) => TextCellState(
         content: cellData.cell?.content ?? "",
         cellData: cellData,
       );

+ 4 - 4
frontend/app_flowy/lib/workspace/application/grid/field/type_option/multi_select_bloc.dart

@@ -11,14 +11,14 @@ part 'multi_select_bloc.freezed.dart';
 class MultiSelectTypeOptionBloc extends Bloc<MultiSelectTypeOptionEvent, MultiSelectTypeOptionState> {
   final TypeOptionService service;
 
-  MultiSelectTypeOptionBloc(MultiSelectTypeOption typeOption, String fieldId)
-      : service = TypeOptionService(fieldId: fieldId),
-        super(MultiSelectTypeOptionState.initial(typeOption)) {
+  MultiSelectTypeOptionBloc(TypeOptionContext typeOptionContext)
+      : service = TypeOptionService(gridId: typeOptionContext.gridId, fieldId: typeOptionContext.field.id),
+        super(MultiSelectTypeOptionState.initial(MultiSelectTypeOption.fromBuffer(typeOptionContext.data))) {
     on<MultiSelectTypeOptionEvent>(
       (event, emit) async {
         await event.map(
           createOption: (_CreateOption value) async {
-            final result = await service.newOption(value.optionName);
+            final result = await service.newOption(name: value.optionName);
             result.fold(
               (option) {
                 emit(state.copyWith(typeOption: _insertOption(option)));

+ 4 - 5
frontend/app_flowy/lib/workspace/application/grid/field/type_option/single_select_bloc.dart

@@ -12,17 +12,16 @@ class SingleSelectTypeOptionBloc extends Bloc<SingleSelectTypeOptionEvent, Singl
   final TypeOptionService service;
 
   SingleSelectTypeOptionBloc(
-    SingleSelectTypeOption typeOption,
-    String fieldId,
-  )   : service = TypeOptionService(fieldId: fieldId),
+    TypeOptionContext typeOptionContext,
+  )   : service = TypeOptionService(gridId: typeOptionContext.gridId, fieldId: typeOptionContext.field.id),
         super(
-          SingleSelectTypeOptionState.initial(typeOption),
+          SingleSelectTypeOptionState.initial(SingleSelectTypeOption.fromBuffer(typeOptionContext.data)),
         ) {
     on<SingleSelectTypeOptionEvent>(
       (event, emit) async {
         await event.map(
           createOption: (_CreateOption value) async {
-            final result = await service.newOption(value.optionName);
+            final result = await service.newOption(name: value.optionName);
             result.fold(
               (option) {
                 emit(state.copyWith(typeOption: _insertOption(option)));

+ 29 - 2
frontend/app_flowy/lib/workspace/application/grid/field/type_option/type_option_service.dart

@@ -1,17 +1,44 @@
+import 'dart:typed_data';
+
 import 'package:dartz/dartz.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/cell_entities.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid/selection_type_option.pb.dart';
 
 class TypeOptionService {
+  final String gridId;
   final String fieldId;
+
   TypeOptionService({
+    required this.gridId,
     required this.fieldId,
   });
 
-  Future<Either<SelectOption, FlowyError>> newOption(String name, {bool selected = false}) {
-    final payload = SelectOptionName.create()..name = name;
+  Future<Either<SelectOption, FlowyError>> newOption({
+    required String name,
+  }) {
+    final fieldIdentifier = FieldIdentifierPayload.create()
+      ..gridId = gridId
+      ..fieldId = fieldId;
+
+    final payload = CreateSelectOptionPayload.create()
+      ..optionName = name
+      ..fieldIdentifier = fieldIdentifier;
+
     return GridEventNewSelectOption(payload).send();
   }
 }
+
+class TypeOptionContext {
+  final String gridId;
+  final Field field;
+  final Uint8List data;
+  const TypeOptionContext({
+    required this.gridId,
+    required this.field,
+    required this.data,
+  });
+}

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

@@ -13,13 +13,17 @@ part 'grid_bloc.freezed.dart';
 class GridBloc extends Bloc<GridEvent, GridState> {
   final GridService _gridService;
   final GridFieldCache fieldCache;
-  final GridRowCache rowCache;
+  late final GridRowCache rowCache;
 
   GridBloc({required View view})
       : _gridService = GridService(gridId: view.id),
         fieldCache = GridFieldCache(gridId: view.id),
-        rowCache = GridRowCache(gridId: view.id),
         super(GridState.initial(view.id)) {
+    rowCache = GridRowCache(
+      gridId: view.id,
+      dataDelegate: GridRowDataDelegateAdaptor(fieldCache),
+    );
+
     on<GridEvent>(
       (event, emit) async {
         await event.map(
@@ -51,13 +55,13 @@ class GridBloc extends Bloc<GridEvent, GridState> {
 
   void _startListening() {
     fieldCache.addListener(
-      onChanged: (fields) => add(GridEvent.didReceiveFieldUpdate(fields)),
       listenWhen: () => !isClosed,
+      onChanged: (fields) => add(GridEvent.didReceiveFieldUpdate(fields)),
     );
 
     rowCache.addListener(
-      onChanged: (rows, listState) => add(GridEvent.didReceiveRowUpdate(rowCache.clonedRows, listState)),
       listenWhen: () => !isClosed,
+      onChanged: (rows, listState) => add(GridEvent.didReceiveRowUpdate(rowCache.clonedRows, listState)),
     );
   }
 
@@ -77,7 +81,7 @@ class GridBloc extends Bloc<GridEvent, GridState> {
       () => result.fold(
         (fields) {
           fieldCache.fields = fields.items;
-          rowCache.updateWithBlock(grid.blockOrders, fieldCache.unmodifiableFields);
+          rowCache.updateWithBlock(grid.blockOrders);
 
           emit(state.copyWith(
             grid: Some(grid),

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

@@ -8,6 +8,8 @@ import 'package:flowy_sdk/protobuf/flowy-grid-data-model/grid.pb.dart';
 import 'package:flutter/foundation.dart';
 import 'package:freezed_annotation/freezed_annotation.dart';
 
+import 'row/row_service.dart';
+
 class GridService {
   final String gridId;
   GridService({
@@ -152,3 +154,44 @@ class GridFieldCache {
     _fieldNotifier.fields = fields;
   }
 }
+
+class GridRowDataDelegateAdaptor extends GridRowDataDelegate {
+  final GridFieldCache _cache;
+
+  GridRowDataDelegateAdaptor(GridFieldCache cache) : _cache = cache;
+  @override
+  UnmodifiableListView<Field> get fields => _cache.unmodifiableFields;
+
+  @override
+  GridRow buildGridRow(RowOrder rowOrder) {
+    return GridRow(
+      gridId: _cache.gridId,
+      fields: _cache.unmodifiableFields,
+      rowId: rowOrder.rowId,
+      height: rowOrder.height.toDouble(),
+    );
+  }
+
+  @override
+  void onFieldChanged(FieldDidUpdateCallback callback) {
+    _cache.addListener(listener: () {
+      callback();
+    });
+  }
+
+  @override
+  CellDataMap buildCellDataMap(Row rowData) {
+    var map = CellDataMap.new();
+    for (final field in fields) {
+      if (field.visibility) {
+        map[field.id] = GridCell(
+          rowId: rowData.id,
+          gridId: _cache.gridId,
+          cell: rowData.cellByFieldId[field.id],
+          field: field,
+        );
+      }
+    }
+    return map;
+  }
+}

+ 13 - 76
frontend/app_flowy/lib/workspace/application/grid/row/row_bloc.dart

@@ -1,7 +1,4 @@
 import 'dart:collection';
-
-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';
@@ -10,21 +7,15 @@ import 'package:dartz/dartz.dart';
 
 part 'row_bloc.freezed.dart';
 
-typedef CellDataMap = LinkedHashMap<String, GridCellIdentifier>;
-
 class RowBloc extends Bloc<RowEvent, RowState> {
   final RowService _rowService;
-  final GridFieldCache _fieldCache;
   final GridRowCache _rowCache;
-  void Function()? _rowListenCallback;
-  void Function()? _fieldListenCallback;
+  void Function()? _rowListenFn;
 
   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>(
@@ -37,87 +28,37 @@ class RowBloc extends Bloc<RowEvent, RowState> {
           createRow: (_CreateRow value) {
             _rowService.createRow();
           },
-          didUpdateRow: (_DidUpdateRow value) async {
-            _handleRowUpdate(value.row, emit);
-          },
-          fieldsDidUpdate: (_FieldsDidUpdate value) async {
-            await _handleFieldUpdate(emit);
-          },
-          didLoadRow: (_DidLoadRow value) {
-            _handleRowUpdate(value.row, emit);
+          didReceiveCellDatas: (_DidReceiveCellDatas value) async {
+            emit(state.copyWith(cellDataMap: Some(value.cellData)));
           },
         );
       },
     );
   }
 
-  void _handleRowUpdate(Row row, Emitter<RowState> emit) {
-    final CellDataMap cellDataMap = _makeCellDatas(row, state.rowData.fields);
-    emit(state.copyWith(
-      row: Future(() => Some(row)),
-      cellDataMap: Some(cellDataMap),
-    ));
-  }
-
-  Future<void> _handleFieldUpdate(Emitter<RowState> emit) async {
-    final optionRow = await state.row;
-    final CellDataMap cellDataMap = optionRow.fold(
-      () => CellDataMap.identity(),
-      (row) => _makeCellDatas(row, _fieldCache.unmodifiableFields),
-    );
-
-    emit(state.copyWith(
-      rowData: state.rowData.copyWith(fields: _fieldCache.unmodifiableFields),
-      cellDataMap: Some(cellDataMap),
-    ));
-  }
-
   @override
   Future<void> close() async {
-    if (_rowListenCallback != null) {
-      _rowCache.removeRowListener(_rowListenCallback!);
-    }
-
-    if (_fieldListenCallback != null) {
-      _fieldCache.removeListener(_fieldListenCallback!);
+    if (_rowListenFn != null) {
+      _rowCache.removeRowListener(_rowListenFn!);
     }
     return super.close();
   }
 
   Future<void> _startListening() async {
-    _fieldListenCallback = _fieldCache.addListener(
-      listener: () => add(const RowEvent.fieldsDidUpdate()),
-      listenWhen: () => !isClosed,
-    );
-
-    _rowListenCallback = _rowCache.addRowListener(
+    _rowListenFn = _rowCache.addRowListener(
       rowId: state.rowData.rowId,
-      onUpdated: (row) => add(RowEvent.didUpdateRow(row)),
+      onUpdated: (cellDatas) => add(RowEvent.didReceiveCellDatas(cellDatas)),
       listenWhen: () => !isClosed,
     );
   }
 
   Future<void> _loadRow(Emitter<RowState> emit) async {
-    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] = GridCellIdentifier(
-          rowId: row.id,
-          gridId: _rowService.gridId,
-          cell: row.cellByFieldId[field.id],
-          field: field,
-        );
+    final data = _rowCache.loadCellData(state.rowData.rowId);
+    data.foldRight(null, (cellDatas, _) {
+      if (!isClosed) {
+        add(RowEvent.didReceiveCellDatas(cellDatas));
       }
-    }
-    return map;
+    });
   }
 }
 
@@ -125,22 +66,18 @@ class RowBloc extends Bloc<RowEvent, RowState> {
 class RowEvent with _$RowEvent {
   const factory RowEvent.initial() = _InitialRow;
   const factory RowEvent.createRow() = _CreateRow;
-  const factory RowEvent.fieldsDidUpdate() = _FieldsDidUpdate;
-  const factory RowEvent.didLoadRow(Row row) = _DidLoadRow;
-  const factory RowEvent.didUpdateRow(Row row) = _DidUpdateRow;
+  const factory RowEvent.didReceiveCellDatas(CellDataMap cellData) = _DidReceiveCellDatas;
 }
 
 @freezed
 class RowState with _$RowState {
   const factory RowState({
     required GridRow rowData,
-    required Future<Option<Row>> row,
     required Option<CellDataMap> cellDataMap,
   }) = _RowState;
 
   factory RowState.initial(GridRow rowData) => RowState(
         rowData: rowData,
-        row: Future(() => none()),
         cellDataMap: none(),
       );
 }

+ 74 - 0
frontend/app_flowy/lib/workspace/application/grid/row/row_detail_bloc.dart

@@ -0,0 +1,74 @@
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:freezed_annotation/freezed_annotation.dart';
+import 'dart:async';
+import 'row_service.dart';
+
+part 'row_detail_bloc.freezed.dart';
+
+class RowDetailBloc extends Bloc<RowDetailEvent, RowDetailState> {
+  final GridRow rowData;
+  final GridRowCache _rowCache;
+  void Function()? _rowListenFn;
+
+  RowDetailBloc({
+    required this.rowData,
+    required GridRowCache rowCache,
+  })  : _rowCache = rowCache,
+        super(RowDetailState.initial()) {
+    on<RowDetailEvent>(
+      (event, emit) async {
+        await event.map(
+          initial: (_Initial value) async {
+            await _startListening();
+            _loadCellData();
+          },
+          didReceiveCellDatas: (_DidReceiveCellDatas value) {
+            emit(state.copyWith(cellDatas: value.cellDatas));
+          },
+        );
+      },
+    );
+  }
+
+  @override
+  Future<void> close() async {
+    if (_rowListenFn != null) {
+      _rowCache.removeRowListener(_rowListenFn!);
+    }
+    return super.close();
+  }
+
+  Future<void> _startListening() async {
+    _rowListenFn = _rowCache.addRowListener(
+      rowId: rowData.rowId,
+      onUpdated: (cellDatas) => add(RowDetailEvent.didReceiveCellDatas(cellDatas.values.toList())),
+      listenWhen: () => !isClosed,
+    );
+  }
+
+  Future<void> _loadCellData() async {
+    final data = _rowCache.loadCellData(rowData.rowId);
+    data.foldRight(null, (cellDataMap, _) {
+      if (!isClosed) {
+        add(RowDetailEvent.didReceiveCellDatas(cellDataMap.values.toList()));
+      }
+    });
+  }
+}
+
+@freezed
+class RowDetailEvent with _$RowDetailEvent {
+  const factory RowDetailEvent.initial() = _Initial;
+  const factory RowDetailEvent.didReceiveCellDatas(List<GridCell> cellDatas) = _DidReceiveCellDatas;
+}
+
+@freezed
+class RowDetailState with _$RowDetailState {
+  const factory RowDetailState({
+    required List<GridCell> cellDatas,
+  }) = _RowDetailState;
+
+  factory RowDetailState.initial() => RowDetailState(
+        cellDatas: List.empty(),
+      );
+}

+ 165 - 113
frontend/app_flowy/lib/workspace/application/grid/row/row_service.dart

@@ -1,4 +1,5 @@
 import 'dart:collection';
+
 import 'package:dartz/dartz.dart';
 import 'package:flowy_sdk/dispatch/dispatch.dart';
 import 'package:flowy_sdk/log.dart';
@@ -8,43 +9,42 @@ 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: (_) {},
-    );
-  }
+typedef RowUpdateCallback = void Function();
+typedef FieldDidUpdateCallback = void Function();
+typedef CellDataMap = LinkedHashMap<String, GridCell>;
 
-  List<GridRow> get rows => _rows;
+abstract class GridRowDataDelegate {
+  UnmodifiableListView<Field> get fields;
+  GridRow buildGridRow(RowOrder rowOrder);
+  CellDataMap buildCellDataMap(Row rowData);
+  void onFieldChanged(FieldDidUpdateCallback callback);
 }
 
 class GridRowCache {
   final String gridId;
+  final RowsNotifier _rowNotifier;
   final GridRowListener _rowsListener;
-  final RowsNotifier _rowNotifier = RowsNotifier();
-  final HashMap<String, Row> _rowDataMap = HashMap();
-  UnmodifiableListView<Field> _fields = UnmodifiableListView([]);
+  final GridRowDataDelegate _dataDelegate;
+
+  List<GridRow> get clonedRows => _rowNotifier.clonedRows;
 
-  GridRowCache({required this.gridId}) : _rowsListener = GridRowListener(gridId: gridId) {
+  GridRowCache({required this.gridId, required GridRowDataDelegate dataDelegate})
+      : _rowNotifier = RowsNotifier(rowBuilder: dataDelegate.buildGridRow),
+        _rowsListener = GridRowListener(gridId: gridId),
+        _dataDelegate = dataDelegate {
+    //
+    dataDelegate.onFieldChanged(() => _rowNotifier.fieldDidChange());
+
+    // listen on the row update
     _rowsListener.rowsUpdateNotifier.addPublishListener((result) {
       result.fold(
         (changesets) {
           for (final changeset in changesets) {
-            _deleteRows(changeset.deletedRows);
-            _insertRows(changeset.insertedRows);
-            _updateRows(changeset.updatedRows);
+            _rowNotifier.deleteRows(changeset.deletedRows);
+            _rowNotifier.insertRows(changeset.insertedRows);
+            _rowNotifier.updateRows(changeset.updatedRows);
           }
         },
         (err) => Log.error(err),
@@ -58,29 +58,29 @@ class GridRowCache {
     _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) {
+      if (onChanged == null) {
         return;
       }
 
-      if (onChanged != null) {
-        onChanged(clonedRows, _rowNotifier._changeReason);
+      if (listenWhen != null && listenWhen() == false) {
+        return;
       }
+
+      onChanged(clonedRows, _rowNotifier._changeReason);
     });
   }
 
-  VoidCallback addRowListener({
+  RowUpdateCallback addRowListener({
     required String rowId,
-    void Function(Row)? onUpdated,
+    void Function(CellDataMap)? onUpdated,
     bool Function()? listenWhen,
   }) {
-    f() {
+    listenrHandler() {
       if (onUpdated == null) {
         return;
       }
@@ -89,67 +89,83 @@ class GridRowCache {
         return;
       }
 
-      _rowNotifier._changeReason.whenOrNull(update: (indexs) {
-        final row = _rowDataMap[rowId];
-        if (indexs[rowId] != null && row != null) {
-          onUpdated(row);
+      notify() {
+        final row = _rowNotifier.rowDataWithId(rowId);
+        if (row != null) {
+          final cellDataMap = _dataDelegate.buildCellDataMap(row);
+          onUpdated(cellDataMap);
         }
-      });
+      }
+
+      _rowNotifier._changeReason.whenOrNull(
+        update: (indexs) {
+          if (indexs[rowId] != null) {
+            notify();
+          }
+        },
+        fieldDidChange: () => notify(),
+      );
     }
 
-    _rowNotifier.addListener(f);
-    return f;
+    _rowNotifier.addListener(listenrHandler);
+    return listenrHandler;
   }
 
   void removeRowListener(VoidCallback callback) {
     _rowNotifier.removeListener(callback);
   }
 
-  Future<Option<Row>> getRowData(String rowId) async {
-    final Row? data = _rowDataMap[rowId];
+  Option<CellDataMap> loadCellData(String rowId) {
+    final Row? data = _rowNotifier.rowDataWithId(rowId);
     if (data != null) {
-      return Future(() => Some(data));
+      return Some(_dataDelegate.buildCellDataMap(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();
-        },
+    GridEventGetRow(payload).send().then((result) {
+      result.fold(
+        (rowData) => _rowNotifier.rowData = rowData,
+        (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();
+  void updateWithBlock(List<GridBlockOrder> blocks) {
+    final rowOrders = blocks.expand((block) => block.rowOrders).toList();
+    _rowNotifier.reset(rowOrders);
+  }
+}
+
+class RowsNotifier extends ChangeNotifier {
+  List<GridRow> _rows = [];
+  HashMap<String, Row> _rowDataMap = HashMap();
+  GridRowChangeReason _changeReason = const InitialListState();
+  final GridRow Function(RowOrder) rowBuilder;
+
+  RowsNotifier({
+    required this.rowBuilder,
+  });
 
-    _rowNotifier.updateRows(newRows, const GridRowChangeReason.initial());
+  void reset(List<RowOrder> rowOrders) {
+    _rowDataMap = HashMap();
+    final rows = rowOrders.map((rowOrder) => rowBuilder(rowOrder)).toList();
+    _update(rows, const GridRowChangeReason.initial());
   }
 
-  void _deleteRows(List<RowOrder> deletedRows) {
+  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};
+    final Map<String, RowOrder> deletedRowMap = {for (var e in deletedRows) e.rowId: e};
 
-    _rowNotifier.rows.asMap().forEach((index, row) {
+    _rows.asMap().forEach((index, row) {
       if (deletedRowMap[row.rowId] == null) {
         newRows.add(row);
       } else {
@@ -157,48 +173,93 @@ class GridRowCache {
       }
     });
 
-    _rowNotifier.updateRows(newRows, GridRowChangeReason.delete(deletedIndex));
+    _update(newRows, GridRowChangeReason.delete(deletedIndex));
   }
 
-  void _insertRows(List<IndexRowOrder> createdRows) {
+  void insertRows(List<IndexRowOrder> createdRows) {
     if (createdRows.isEmpty) {
       return;
     }
 
     InsertedIndexs insertIndexs = [];
-    final List<GridRow> newRows = _rowNotifier.rows;
+    final List<GridRow> newRows = clonedRows;
     for (final createdRow in createdRows) {
-      final gridRow = GridRow.fromBlockRow(gridId, createdRow.rowOrder, _fields);
-      insertIndexs.add(
-        InsertedIndex(
-          index: createdRow.index,
-          rowId: gridRow.rowId,
-        ),
+      final insertIndex = InsertedIndex(
+        index: createdRow.index,
+        rowId: createdRow.rowOrder.rowId,
       );
-      newRows.insert(createdRow.index, gridRow);
+      insertIndexs.add(insertIndex);
+      newRows.insert(createdRow.index, (rowBuilder(createdRow.rowOrder)));
     }
-    _rowNotifier.updateRows(newRows, GridRowChangeReason.insert(insertIndexs));
+    _update(newRows, GridRowChangeReason.insert(insertIndexs));
   }
 
-  void _updateRows(List<RowOrder> updatedRows) {
+  void updateRows(List<RowOrder> updatedRows) {
     if (updatedRows.isEmpty) {
       return;
     }
 
     final UpdatedIndexs updatedIndexs = UpdatedIndexs();
-    final List<GridRow> newRows = _rowNotifier.rows;
+    final List<GridRow> newRows = clonedRows;
     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));
+        // Remove the old row data, the data will be filled if the loadRow method gets called.
         _rowDataMap.remove(rowOrder.rowId);
+
+        newRows.removeAt(index);
+        newRows.insert(index, rowBuilder(rowOrder));
         updatedIndexs[rowOrder.rowId] = UpdatedIndex(index: index, rowId: rowOrder.rowId);
       }
     }
 
-    _rowNotifier.updateRows(newRows, GridRowChangeReason.update(updatedIndexs));
+    _update(newRows, GridRowChangeReason.update(updatedIndexs));
+  }
+
+  void fieldDidChange() {
+    _update(_rows, const GridRowChangeReason.fieldDidChange());
   }
+
+  void _update(List<GridRow> rows, GridRowChangeReason reason) {
+    _rows = rows;
+    _changeReason = reason;
+
+    _changeReason.map(
+      insert: (_) => notifyListeners(),
+      delete: (_) => notifyListeners(),
+      update: (_) => notifyListeners(),
+      fieldDidChange: (_) => notifyListeners(),
+      initial: (_) {},
+    );
+  }
+
+  set rowData(Row rowData) {
+    rowData.freeze();
+
+    _rowDataMap[rowData.id] = rowData;
+    final index = _rows.indexWhere((row) => row.rowId == rowData.id);
+    if (index != -1) {
+      // update the corresponding row in _rows if they are not the same
+      if (_rows[index].data != rowData) {
+        final row = _rows.removeAt(index).copyWith(data: rowData);
+        _rows.insert(index, row);
+
+        // Calculate the update index
+        final UpdatedIndexs updatedIndexs = UpdatedIndexs();
+        updatedIndexs[row.rowId] = UpdatedIndex(index: index, rowId: row.rowId);
+        _changeReason = GridRowChangeReason.update(updatedIndexs);
+
+        //
+        notifyListeners();
+      }
+    }
+  }
+
+  Row? rowDataWithId(String rowId) {
+    return _rowDataMap[rowId];
+  }
+
+  List<GridRow> get clonedRows => [..._rows];
 }
 
 class RowService {
@@ -251,16 +312,6 @@ class RowService {
   }
 }
 
-@freezed
-class GridCellIdentifier with _$GridCellIdentifier {
-  const factory GridCellIdentifier({
-    required String gridId,
-    required String rowId,
-    required Field field,
-    Cell? cell,
-  }) = _CellData;
-}
-
 @freezed
 class GridRow with _$GridRow {
   const factory GridRow({
@@ -268,27 +319,36 @@ class GridRow with _$GridRow {
     required String rowId,
     required List<Field> fields,
     required double height,
-    required Future<Option<Row>> data,
+    Row? data,
   }) = _GridRow;
+}
 
-  factory GridRow.fromBlockRow(String gridId, RowOrder row, List<Field> fields) {
-    return GridRow(
-      gridId: gridId,
-      fields: fields,
-      rowId: row.rowId,
-      data: Future(() => none()),
-      height: row.height.toDouble(),
-    );
-  }
+@freezed
+class GridCell with _$GridCell {
+  const factory GridCell({
+    required String gridId,
+    required String rowId,
+    required Field field,
+    Cell? cell,
+  }) = _GridCell;
 }
 
 typedef InsertedIndexs = List<InsertedIndex>;
 typedef DeletedIndexs = List<DeletedIndex>;
 typedef UpdatedIndexs = LinkedHashMap<String, UpdatedIndex>;
 
+@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.fieldDidChange() = _FieldDidChange;
+  const factory GridRowChangeReason.initial() = InitialListState;
+}
+
 class InsertedIndex {
-  int index;
-  String rowId;
+  final int index;
+  final String rowId;
   InsertedIndex({
     required this.index,
     required this.rowId,
@@ -296,8 +356,8 @@ class InsertedIndex {
 }
 
 class DeletedIndex {
-  int index;
-  GridRow row;
+  final int index;
+  final GridRow row;
   DeletedIndex({
     required this.index,
     required this.row,
@@ -305,18 +365,10 @@ class DeletedIndex {
 }
 
 class UpdatedIndex {
-  int index;
-  String rowId;
+  final int index;
+  final 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;
-}

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

@@ -82,16 +82,14 @@ class CreateItem extends StatelessWidget {
 
     return FlowyHover(
       style: config,
-      builder: (context, onHover) {
-        return GestureDetector(
-          onTap: () => onSelected(pluginBuilder),
-          child: FlowyText.medium(
-            pluginBuilder.menuName,
-            color: theme.textColor,
-            fontSize: 12,
-          ).padding(horizontal: 10, vertical: 6),
-        );
-      },
+      child: GestureDetector(
+        onTap: () => onSelected(pluginBuilder),
+        child: FlowyText.medium(
+          pluginBuilder.menuName,
+          color: theme.textColor,
+          fontSize: 12,
+        ).padding(horizontal: 10, vertical: 6),
+      ),
     );
   }
 }

+ 5 - 3
frontend/app_flowy/lib/workspace/presentation/home/menu/app/menu_app.dart

@@ -95,9 +95,11 @@ class _MenuAppState extends State<MenuApp> {
   Widget _renderViewSection(AppDataNotifier notifier) {
     return MultiProvider(
       providers: [ChangeNotifierProvider.value(value: notifier)],
-      child: Consumer(builder: (context, AppDataNotifier notifier, child) {
-        return ViewSection(appData: notifier);
-      }),
+      child: Consumer(
+        builder: (context, AppDataNotifier notifier, child) {
+          return ViewSection(appData: notifier);
+        },
+      ),
     );
   }
 

+ 9 - 13
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/grid_page.dart

@@ -1,6 +1,5 @@
 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';
@@ -214,27 +213,24 @@ class _GridRowsState extends State<_GridRows> {
           key: _key,
           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(context, rowData, animation);
+            return _renderRow(context, state.rows[index], animation);
           },
         );
       },
     );
   }
 
-  Widget _renderRow(BuildContext context, GridRow rowData, Animation<double> animation) {
-    final bloc = context.read<GridBloc>();
-    final fieldCache = bloc.fieldCache;
-    final rowCache = bloc.rowCache;
-
+  Widget _renderRow(
+    BuildContext context,
+    GridRow rowData,
+    Animation<double> animation,
+  ) {
+    final rowCache = context.read<GridBloc>().rowCache;
     return SizeTransition(
       sizeFactor: animation,
       child: GridRowWidget(
-        blocBuilder: () => RowBloc(
-          rowData: rowData,
-          fieldCache: fieldCache,
-          rowCache: rowCache,
-        ),
+        rowData: rowData,
+        rowCache: rowCache,
         key: ValueKey(rowData.rowId),
       ),
     );

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

@@ -1,4 +1,5 @@
 import 'package:app_flowy/workspace/application/grid/row/row_service.dart';
+import 'package:flowy_infra_ui/style_widget/hover.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid-data-model/grid.pb.dart' show FieldType;
 import 'package:flutter/widgets.dart';
 import 'checkbox_cell.dart';
@@ -7,7 +8,7 @@ import 'number_cell.dart';
 import 'selection_cell/selection_cell.dart';
 import 'text_cell.dart';
 
-Widget buildGridCell(GridCellIdentifier cellData) {
+GridCellWidget buildGridCell(GridCell cellData, {GridCellStyle? style}) {
   final key = ValueKey(cellData.field.id + cellData.rowId);
   switch (cellData.field.fieldType) {
     case FieldType.Checkbox:
@@ -19,7 +20,7 @@ Widget buildGridCell(GridCellIdentifier cellData) {
     case FieldType.Number:
       return NumberCell(cellData: cellData, key: key);
     case FieldType.RichText:
-      return GridTextCell(cellData: cellData, key: key);
+      return GridTextCell(cellData: cellData, key: key, style: style);
     case FieldType.SingleSelect:
       return SingleSelectCell(cellData: cellData, key: key);
     default:
@@ -35,3 +36,11 @@ class BlankCell extends StatelessWidget {
     return Container();
   }
 }
+
+abstract class GridCellWidget extends HoverWidget {
+  @override
+  final ValueNotifier<bool> onFocus = ValueNotifier<bool>(false);
+  GridCellWidget({Key? key}) : super(key: key);
+}
+
+abstract class GridCellStyle {}

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

@@ -1,11 +1,12 @@
 import 'package:flowy_infra/theme.dart';
 import 'package:flutter/material.dart';
 import 'package:provider/provider.dart';
-
 import 'package:app_flowy/workspace/presentation/plugins/grid/src/layout/sizes.dart';
+import 'cell_builder.dart';
 
 class CellStateNotifier extends ChangeNotifier {
   bool _isFocus = false;
+  bool _onEnter = false;
 
   set isFocus(bool value) {
     if (_isFocus != value) {
@@ -14,38 +15,59 @@ class CellStateNotifier extends ChangeNotifier {
     }
   }
 
+  set onEnter(bool value) {
+    if (_onEnter != value) {
+      _onEnter = value;
+      notifyListeners();
+    }
+  }
+
   bool get isFocus => _isFocus;
+
+  bool get onEnter => _onEnter;
 }
 
 class CellContainer extends StatelessWidget {
-  final Widget child;
+  final GridCellWidget child;
+  final Widget? expander;
   final double width;
   const CellContainer({
     Key? key,
     required this.child,
     required this.width,
+    this.expander,
   }) : super(key: key);
 
   @override
   Widget build(BuildContext context) {
     return ChangeNotifierProvider(
       create: (_) => CellStateNotifier(),
-      child: Consumer<CellStateNotifier>(
-        builder: (context, state, _) {
+      child: Selector<CellStateNotifier, bool>(
+        selector: (context, notifier) => notifier.isFocus,
+        builder: (context, isFocus, _) {
+          Widget container = Center(child: child);
+          child.onFocus.addListener(() {
+            Provider.of<CellStateNotifier>(context, listen: false).isFocus = child.onFocus.value;
+          });
+
+          if (expander != null) {
+            container = _CellEnterRegion(child: container, expander: expander!);
+          }
+
           return Container(
             constraints: BoxConstraints(maxWidth: width),
-            decoration: _makeBoxDecoration(context, state),
+            decoration: _makeBoxDecoration(context, isFocus),
             padding: GridSize.cellContentInsets,
-            child: Center(child: child),
+            child: container,
           );
         },
       ),
     );
   }
 
-  BoxDecoration _makeBoxDecoration(BuildContext context, CellStateNotifier state) {
+  BoxDecoration _makeBoxDecoration(BuildContext context, bool isFocus) {
     final theme = context.watch<AppTheme>();
-    if (state.isFocus) {
+    if (isFocus) {
       final borderSide = BorderSide(color: theme.main1, width: 1.0);
       return BoxDecoration(border: Border.fromBorderSide(borderSide));
     } else {
@@ -55,34 +77,31 @@ class CellContainer extends StatelessWidget {
   }
 }
 
-abstract class GridCell extends StatefulWidget {
-  const GridCell({Key? key}) : super(key: key);
-
-  void setFocus(BuildContext context, bool value) {
-    Provider.of<CellStateNotifier>(context, listen: false).isFocus = value;
-  }
-}
-
-class CellFocusNode extends FocusNode {
-  VoidCallback? focusCallback;
-
-  void addCallback(BuildContext context, VoidCallback callback) {
-    if (focusCallback != null) {
-      removeListener(focusCallback!);
-    }
-    focusCallback = () {
-      Provider.of<CellStateNotifier>(context, listen: false).isFocus = hasFocus;
-      callback();
-    };
-
-    addListener(focusCallback!);
-  }
+class _CellEnterRegion extends StatelessWidget {
+  final Widget child;
+  final Widget expander;
+  const _CellEnterRegion({required this.child, required this.expander, Key? key}) : super(key: key);
 
   @override
-  void dispose() {
-    if (focusCallback != null) {
-      removeListener(focusCallback!);
-    }
-    super.dispose();
+  Widget build(BuildContext context) {
+    return Selector<CellStateNotifier, bool>(
+      selector: (context, notifier) => notifier.onEnter,
+      builder: (context, onEnter, _) {
+        List<Widget> children = [Expanded(child: child)];
+        if (onEnter) {
+          children.add(expander);
+        }
+
+        return MouseRegion(
+          cursor: SystemMouseCursors.click,
+          onEnter: (p) => Provider.of<CellStateNotifier>(context, listen: false).onEnter = true,
+          onExit: (p) => Provider.of<CellStateNotifier>(context, listen: false).onEnter = false,
+          child: Row(
+            // alignment: AlignmentDirectional.centerEnd,
+            children: children,
+          ),
+        );
+      },
+    );
   }
 }

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

@@ -4,11 +4,12 @@ import 'package:flowy_infra/image.dart';
 import 'package:flowy_infra_ui/style_widget/icon_button.dart';
 import 'package:flutter/widgets.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
+import 'cell_builder.dart';
 
-class CheckboxCell extends StatefulWidget {
-  final GridCellIdentifier cellData;
+class CheckboxCell extends GridCellWidget {
+  final GridCell cellData;
 
-  const CheckboxCell({
+  CheckboxCell({
     required this.cellData,
     Key? key,
   }) : super(key: key);

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

@@ -1,17 +1,22 @@
 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/widgets/cell/cell_container.dart';
 import 'package:flowy_infra/theme.dart';
 import 'package:flowy_infra_ui/flowy_infra_ui.dart';
 import 'package:flowy_infra_ui/style_widget/text.dart';
 import 'package:flutter/widgets.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:table_calendar/table_calendar.dart';
+import 'cell_builder.dart';
 
-class DateCell extends GridCell {
-  final GridCellIdentifier cellData;
+abstract class GridCellDelegate {
+  void onFocus(bool isFocus);
+  GridCellDelegate get delegate;
+}
+
+class DateCell extends GridCellWidget {
+  final GridCell cellData;
 
-  const DateCell({
+  DateCell({
     required this.cellData,
     Key? key,
   }) : super(key: key);
@@ -39,13 +44,13 @@ class _DateCellState extends State<DateCell> {
             child: GestureDetector(
               behavior: HitTestBehavior.opaque,
               onTap: () {
-                widget.setFocus(context, true);
+                widget.onFocus.value = true;
                 _CellCalendar.show(
                   context,
                   onSelected: (day) {
                     context.read<DateCellBloc>().add(DateCellEvent.selectDay(day));
                   },
-                  onDismissed: () => widget.setFocus(context, false),
+                  onDismissed: () => widget.onFocus.value = false,
                 );
               },
               child: MouseRegion(

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

@@ -2,14 +2,15 @@ import 'dart:async';
 
 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/widgets/cell/cell_container.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 
-class NumberCell extends GridCell {
-  final GridCellIdentifier cellData;
+import 'cell_builder.dart';
 
-  const NumberCell({
+class NumberCell extends GridCellWidget {
+  final GridCell cellData;
+
+  NumberCell({
     required this.cellData,
     Key? key,
   }) : super(key: key);
@@ -21,21 +22,23 @@ class NumberCell extends GridCell {
 class _NumberCellState extends State<NumberCell> {
   late NumberCellBloc _cellBloc;
   late TextEditingController _controller;
-  late CellFocusNode _focusNode;
+  late FocusNode _focusNode;
   Timer? _delayOperation;
 
   @override
   void initState() {
     _cellBloc = getIt<NumberCellBloc>(param1: widget.cellData)..add(const NumberCellEvent.initial());
     _controller = TextEditingController(text: _cellBloc.state.content);
-    _focusNode = CellFocusNode();
+    _focusNode = FocusNode();
+    _focusNode.addListener(() {
+      widget.onFocus.value = _focusNode.hasFocus;
+      focusChanged();
+    });
     super.initState();
   }
 
   @override
   Widget build(BuildContext context) {
-    _focusNode.addCallback(context, focusChanged);
-
     return BlocProvider.value(
       value: _cellBloc,
       child: BlocConsumer<NumberCellBloc, NumberCellState>(

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

@@ -1,16 +1,16 @@
 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/widgets/cell/cell_container.dart';
+import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/cell/cell_builder.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 
 import 'extension.dart';
 import 'selection_editor.dart';
 
-class SingleSelectCell extends GridCell {
-  final GridCellIdentifier cellData;
+class SingleSelectCell extends GridCellWidget {
+  final GridCell cellData;
 
-  const SingleSelectCell({
+  SingleSelectCell({
     required this.cellData,
     Key? key,
   }) : super(key: key);
@@ -38,16 +38,16 @@ class _SingleSelectCellState extends State<SingleSelectCell> {
           return SizedBox.expand(
             child: InkWell(
               onTap: () {
-                widget.setFocus(context, true);
+                widget.onFocus.value = true;
                 SelectOptionCellEditor.show(
                   context,
                   state.cellData,
                   state.options,
                   state.selectedOptions,
-                  () => widget.setFocus(context, false),
+                  () => widget.onFocus.value = false,
                 );
               },
-              child: Row(children: children),
+              child: ClipRRect(child: Row(children: children)),
             ),
           );
         },
@@ -63,10 +63,10 @@ class _SingleSelectCellState extends State<SingleSelectCell> {
 }
 
 //----------------------------------------------------------------
-class MultiSelectCell extends GridCell {
-  final GridCellIdentifier cellData;
+class MultiSelectCell extends GridCellWidget {
+  final GridCell cellData;
 
-  const MultiSelectCell({
+  MultiSelectCell({
     required this.cellData,
     Key? key,
   }) : super(key: key);
@@ -94,16 +94,16 @@ class _MultiSelectCellState extends State<MultiSelectCell> {
           return SizedBox.expand(
             child: InkWell(
               onTap: () {
-                widget.setFocus(context, true);
+                widget.onFocus.value = true;
                 SelectOptionCellEditor.show(
                   context,
                   state.cellData,
                   state.options,
                   state.selectedOptions,
-                  () => widget.setFocus(context, false),
+                  () => widget.onFocus.value = false,
                 );
               },
-              child: Row(children: children),
+              child: ClipRRect(child: Row(children: children)),
             ),
           );
         },

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

@@ -25,7 +25,7 @@ import 'text_field.dart';
 const double _editorPannelWidth = 300;
 
 class SelectOptionCellEditor extends StatelessWidget with FlowyOverlayDelegate {
-  final GridCellIdentifier cellData;
+  final GridCell 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,
-    GridCellIdentifier cellData,
+    GridCell cellData,
     List<SelectOption> options,
     List<SelectOption> selectedOptions,
     VoidCallback onDismissed,

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

@@ -1,16 +1,32 @@
 import 'dart:async';
-import 'package:app_flowy/startup/startup.dart';
-import 'package:app_flowy/workspace/application/grid/prelude.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
-import 'cell_container.dart';
+import 'package:app_flowy/startup/startup.dart';
+import 'package:app_flowy/workspace/application/grid/prelude.dart';
+import 'cell_builder.dart';
+
+class GridTextCellStyle extends GridCellStyle {
+  String? placeholder;
 
-class GridTextCell extends GridCell {
-  final GridCellIdentifier cellData;
-  const GridTextCell({
+  GridTextCellStyle({
+    this.placeholder,
+  });
+}
+
+class GridTextCell extends GridCellWidget {
+  final GridCell cellData;
+  late final GridTextCellStyle? cellStyle;
+  GridTextCell({
     required this.cellData,
+    GridCellStyle? style,
     Key? key,
-  }) : super(key: key);
+  }) : super(key: key) {
+    if (style != null) {
+      cellStyle = (style as GridTextCellStyle);
+    } else {
+      cellStyle = null;
+    }
+  }
 
   @override
   State<GridTextCell> createState() => _GridTextCellState();
@@ -19,21 +35,25 @@ class GridTextCell extends GridCell {
 class _GridTextCellState extends State<GridTextCell> {
   late TextCellBloc _cellBloc;
   late TextEditingController _controller;
-  late CellFocusNode _focusNode;
+  late FocusNode _focusNode;
+
   Timer? _delayOperation;
 
   @override
   void initState() {
     _cellBloc = getIt<TextCellBloc>(param1: widget.cellData);
+    _cellBloc.add(const TextCellEvent.initial());
     _controller = TextEditingController(text: _cellBloc.state.content);
-    _focusNode = CellFocusNode();
+    _focusNode = FocusNode();
+    _focusNode.addListener(() {
+      widget.onFocus.value = _focusNode.hasFocus;
+      focusChanged();
+    });
     super.initState();
   }
 
   @override
   Widget build(BuildContext context) {
-    _focusNode.addCallback(context, focusChanged);
-
     return BlocProvider.value(
       value: _cellBloc,
       child: BlocConsumer<TextCellBloc, TextCellState>(
@@ -42,6 +62,7 @@ class _GridTextCellState extends State<GridTextCell> {
             _controller.text = state.content;
           }
         },
+        buildWhen: (previous, current) => previous.content != current.content,
         builder: (context, state) {
           return TextField(
             controller: _controller,
@@ -50,9 +71,10 @@ class _GridTextCellState extends State<GridTextCell> {
             onEditingComplete: () => _focusNode.unfocus(),
             maxLines: 1,
             style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
-            decoration: const InputDecoration(
+            decoration: InputDecoration(
               contentPadding: EdgeInsets.zero,
               border: InputBorder.none,
+              hintText: widget.cellStyle?.placeholder,
               isDense: true,
             ),
           );

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

@@ -7,6 +7,7 @@ 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:flowy_sdk/protobuf/flowy-grid-data-model/grid.pb.dart' show Field;
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'field_type_extension.dart';
@@ -20,21 +21,21 @@ class GridFieldCell extends StatelessWidget {
 
   @override
   Widget build(BuildContext context) {
-    final theme = context.watch<AppTheme>();
-
     return BlocProvider(
       create: (context) => FieldCellBloc(cellContext: cellContext)..add(const FieldCellEvent.initial()),
       child: BlocBuilder<FieldCellBloc, FieldCellState>(
         builder: (context, state) {
-          final button = FlowyButton(
-            hoverColor: theme.shader6,
+          final button = FieldCellButton(
+            field: state.field,
             onTap: () => _showActionSheet(context),
-            leftIcon: svgWidget(state.field.fieldType.iconName(), color: theme.iconColor),
-            text: FlowyText.medium(state.field.name, fontSize: 12),
-            padding: GridSize.cellContentInsets,
           );
 
-          const line = Positioned(top: 0, bottom: 0, right: 0, child: _DragToExpandLine());
+          const line = Positioned(
+            top: 0,
+            bottom: 0,
+            right: 0,
+            child: _DragToExpandLine(),
+          );
 
           return _CellContainer(
             width: state.field.width.toDouble(),
@@ -125,9 +126,31 @@ class _DragToExpandLine extends StatelessWidget {
             borderRadius: BorderRadius.zero,
             contentMargin: const EdgeInsets.only(left: 5),
           ),
-          builder: (_, onHover) => const SizedBox(width: 2),
+          child: const SizedBox(width: 2),
         ),
       ),
     );
   }
 }
+
+class FieldCellButton extends StatelessWidget {
+  final VoidCallback onTap;
+  final Field field;
+  const FieldCellButton({
+    required this.field,
+    required this.onTap,
+    Key? key,
+  }) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    final theme = context.watch<AppTheme>();
+    return FlowyButton(
+      hoverColor: theme.shader6,
+      onTap: onTap,
+      leftIcon: svgWidget(field.fieldType.iconName(), color: theme.iconColor),
+      text: FlowyText.medium(field.name, fontSize: 12),
+      padding: GridSize.cellContentInsets,
+    );
+  }
+}

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

@@ -90,16 +90,6 @@ class _FieldOperationList extends StatelessWidget {
 
   @override
   Widget build(BuildContext context) {
-    final actions = FieldAction.values
-        .map(
-          (action) => FieldActionCell(
-            fieldId: fieldData.field.id,
-            action: action,
-            onTap: onDismissed,
-          ),
-        )
-        .toList();
-
     return GridView(
       // https://api.flutter.dev/flutter/widgets/AnimatedList/shrinkWrap.html
       shrinkWrap: true,
@@ -108,20 +98,44 @@ class _FieldOperationList extends StatelessWidget {
         childAspectRatio: 4.0,
         mainAxisSpacing: 8,
       ),
-      children: actions,
+      children: buildCells(),
     );
   }
+
+  List<Widget> buildCells() {
+    return FieldAction.values.map(
+      (action) {
+        bool enable = true;
+        switch (action) {
+          case FieldAction.delete:
+            enable = !fieldData.field.isPrimary;
+            break;
+          default:
+            break;
+        }
+
+        return FieldActionCell(
+          fieldId: fieldData.field.id,
+          action: action,
+          onTap: onDismissed,
+          enable: enable,
+        );
+      },
+    ).toList();
+  }
 }
 
 class FieldActionCell extends StatelessWidget {
   final String fieldId;
   final VoidCallback onTap;
   final FieldAction action;
+  final bool enable;
 
   const FieldActionCell({
     required this.fieldId,
     required this.action,
     required this.onTap,
+    required this.enable,
     Key? key,
   }) : super(key: key);
 
@@ -129,11 +143,13 @@ class FieldActionCell extends StatelessWidget {
   Widget build(BuildContext context) {
     final theme = context.watch<AppTheme>();
     return FlowyButton(
-      text: FlowyText.medium(action.title(), fontSize: 12),
+      text: FlowyText.medium(action.title(), fontSize: 12, color: enable ? null : theme.shader4),
       hoverColor: theme.hover,
       onTap: () {
-        action.run(context);
-        onTap();
+        if (enable) {
+          action.run(context);
+          onTap();
+        }
       },
       leftIcon: svgWidget(action.iconName(), color: theme.iconColor),
     );

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

@@ -2,12 +2,14 @@ import 'package:app_flowy/startup/startup.dart';
 import 'package:app_flowy/workspace/application/grid/field/field_editor_bloc.dart';
 import 'package:app_flowy/workspace/application/grid/field/field_service.dart';
 import 'package:app_flowy/workspace/application/grid/field/field_switch_bloc.dart';
+import 'package:easy_localization/easy_localization.dart';
 import 'package:flowy_infra_ui/flowy_infra_ui.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' show Field;
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:app_flowy/generated/locale_keys.g.dart';
 import 'field_name_input.dart';
 import 'field_switcher.dart';
 
@@ -70,7 +72,7 @@ class _FieldEditorWidget extends StatelessWidget {
             (field) => ListView(
               shrinkWrap: true,
               children: [
-                const FlowyText.medium("Edit property", fontSize: 12),
+                FlowyText.medium(LocaleKeys.grid_field_editProperty.tr(), fontSize: 12),
                 const VSpace(10),
                 const _FieldNameTextField(),
                 const VSpace(10),

+ 23 - 21
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/field_switcher.dart

@@ -1,7 +1,7 @@
 import 'dart:typed_data';
 
-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/date.dart';
+import 'package:app_flowy/workspace/application/grid/field/type_option/type_option_service.dart';
+import 'package:dartz/dartz.dart' show Either;
 import 'package:flowy_infra/image.dart';
 import 'package:flowy_infra/theme.dart';
 import 'package:flowy_infra_ui/flowy_infra_ui.dart';
@@ -14,11 +14,14 @@ import 'package:flowy_sdk/protobuf/flowy-grid/checkbox_type_option.pbserver.dart
 import 'package:flowy_sdk/protobuf/flowy-grid/text_type_option.pb.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
+
 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/header/field_type_list.dart';
+import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/header/type_option/date.dart';
+
 import 'field_type_extension.dart';
-import 'package:dartz/dartz.dart' show Either;
 import 'type_option/multi_select.dart';
 import 'type_option/number.dart';
 import 'type_option/single_select.dart';
@@ -58,11 +61,7 @@ class _FieldSwitcherState extends State<FieldSwitcher> {
         },
         builder: (context, state) {
           List<Widget> children = [_switchFieldTypeButton(context, state.field)];
-          final typeOptionWidget = _typeOptionWidget(
-            context: context,
-            field: state.field,
-            data: state.typeOptionData,
-          );
+          final typeOptionWidget = _typeOptionWidget(context: context, state: state);
 
           if (typeOptionWidget != null) {
             children.add(typeOptionWidget);
@@ -111,8 +110,7 @@ class _FieldSwitcherState extends State<FieldSwitcher> {
 
   Widget? _typeOptionWidget({
     required BuildContext context,
-    required Field field,
-    required TypeOptionData data,
+    required FieldSwitchState state,
   }) {
     final overlayDelegate = TypeOptionOverlayDelegate(
       showOverlay: _showOverlay,
@@ -123,9 +121,14 @@ class _FieldSwitcherState extends State<FieldSwitcher> {
       context.read<FieldSwitcherBloc>().add(FieldSwitchEvent.didUpdateTypeOptionData(data));
     });
 
+    final typeOptionContext = TypeOptionContext(
+      gridId: state.gridId,
+      field: state.field,
+      data: state.typeOptionData,
+    );
+
     final builder = _makeTypeOptionBuild(
-      field: field,
-      data: data,
+      typeOptionContext: typeOptionContext,
       overlayDelegate: overlayDelegate,
       dataDelegate: dataDelegate,
     );
@@ -165,24 +168,23 @@ abstract class TypeOptionBuilder {
 }
 
 TypeOptionBuilder _makeTypeOptionBuild({
-  required Field field,
-  required TypeOptionData data,
+  required TypeOptionContext typeOptionContext,
   required TypeOptionOverlayDelegate overlayDelegate,
   required TypeOptionDataDelegate dataDelegate,
 }) {
-  switch (field.fieldType) {
+  switch (typeOptionContext.field.fieldType) {
     case FieldType.Checkbox:
-      return CheckboxTypeOptionBuilder(data);
+      return CheckboxTypeOptionBuilder(typeOptionContext.data);
     case FieldType.DateTime:
-      return DateTypeOptionBuilder(data, overlayDelegate, dataDelegate);
+      return DateTypeOptionBuilder(typeOptionContext.data, overlayDelegate, dataDelegate);
     case FieldType.SingleSelect:
-      return SingleSelectTypeOptionBuilder(field.id, data, overlayDelegate, dataDelegate);
+      return SingleSelectTypeOptionBuilder(typeOptionContext, overlayDelegate, dataDelegate);
     case FieldType.MultiSelect:
-      return MultiSelectTypeOptionBuilder(field.id, data, overlayDelegate, dataDelegate);
+      return MultiSelectTypeOptionBuilder(typeOptionContext, overlayDelegate, dataDelegate);
     case FieldType.Number:
-      return NumberTypeOptionBuilder(data, overlayDelegate, dataDelegate);
+      return NumberTypeOptionBuilder(typeOptionContext.data, overlayDelegate, dataDelegate);
     case FieldType.RichText:
-      return RichTextTypeOptionBuilder(data);
+      return RichTextTypeOptionBuilder(typeOptionContext.data);
 
     default:
       throw UnimplementedError;

+ 6 - 11
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/type_option/multi_select.dart

@@ -1,7 +1,6 @@
-import 'package:app_flowy/startup/startup.dart';
 import 'package:app_flowy/workspace/application/grid/field/type_option/multi_select_bloc.dart';
+import 'package:app_flowy/workspace/application/grid/field/type_option/type_option_service.dart';
 import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/header/field_switcher.dart';
-import 'package:flowy_sdk/protobuf/flowy-grid/selection_type_option.pb.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 
@@ -11,13 +10,11 @@ class MultiSelectTypeOptionBuilder extends TypeOptionBuilder {
   final MultiSelectTypeOptionWidget _widget;
 
   MultiSelectTypeOptionBuilder(
-    String fieldId,
-    TypeOptionData typeOptionData,
+    TypeOptionContext typeOptionContext,
     TypeOptionOverlayDelegate overlayDelegate,
     TypeOptionDataDelegate dataDelegate,
   ) : _widget = MultiSelectTypeOptionWidget(
-          fieldId: fieldId,
-          typeOption: MultiSelectTypeOption.fromBuffer(typeOptionData),
+          typeOptionContext: typeOptionContext,
           overlayDelegate: overlayDelegate,
           dataDelegate: dataDelegate,
         );
@@ -27,13 +24,11 @@ class MultiSelectTypeOptionBuilder extends TypeOptionBuilder {
 }
 
 class MultiSelectTypeOptionWidget extends TypeOptionWidget {
-  final String fieldId;
-  final MultiSelectTypeOption typeOption;
+  final TypeOptionContext typeOptionContext;
   final TypeOptionOverlayDelegate overlayDelegate;
   final TypeOptionDataDelegate dataDelegate;
   const MultiSelectTypeOptionWidget({
-    required this.fieldId,
-    required this.typeOption,
+    required this.typeOptionContext,
     required this.overlayDelegate,
     required this.dataDelegate,
     Key? key,
@@ -42,7 +37,7 @@ class MultiSelectTypeOptionWidget extends TypeOptionWidget {
   @override
   Widget build(BuildContext context) {
     return BlocProvider(
-      create: (context) => getIt<MultiSelectTypeOptionBloc>(param1: typeOption, param2: fieldId),
+      create: (context) => MultiSelectTypeOptionBloc(typeOptionContext),
       child: BlocConsumer<MultiSelectTypeOptionBloc, MultiSelectTypeOptionState>(
         listener: (context, state) {
           dataDelegate.didUpdateTypeOptionData(state.typeOption.writeToBuffer());

+ 6 - 11
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/type_option/single_select.dart

@@ -1,7 +1,6 @@
-import 'package:app_flowy/startup/startup.dart';
 import 'package:app_flowy/workspace/application/grid/field/type_option/single_select_bloc.dart';
+import 'package:app_flowy/workspace/application/grid/field/type_option/type_option_service.dart';
 import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/header/field_switcher.dart';
-import 'package:flowy_sdk/protobuf/flowy-grid/selection_type_option.pb.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'field_option_pannel.dart';
@@ -10,13 +9,11 @@ class SingleSelectTypeOptionBuilder extends TypeOptionBuilder {
   final SingleSelectTypeOptionWidget _widget;
 
   SingleSelectTypeOptionBuilder(
-    String fieldId,
-    TypeOptionData typeOptionData,
+    TypeOptionContext typeOptionContext,
     TypeOptionOverlayDelegate overlayDelegate,
     TypeOptionDataDelegate dataDelegate,
   ) : _widget = SingleSelectTypeOptionWidget(
-          fieldId: fieldId,
-          typeOption: SingleSelectTypeOption.fromBuffer(typeOptionData),
+          typeOptionContext: typeOptionContext,
           dataDelegate: dataDelegate,
           overlayDelegate: overlayDelegate,
         );
@@ -26,13 +23,11 @@ class SingleSelectTypeOptionBuilder extends TypeOptionBuilder {
 }
 
 class SingleSelectTypeOptionWidget extends TypeOptionWidget {
-  final String fieldId;
-  final SingleSelectTypeOption typeOption;
+  final TypeOptionContext typeOptionContext;
   final TypeOptionOverlayDelegate overlayDelegate;
   final TypeOptionDataDelegate dataDelegate;
   const SingleSelectTypeOptionWidget({
-    required this.fieldId,
-    required this.typeOption,
+    required this.typeOptionContext,
     required this.dataDelegate,
     required this.overlayDelegate,
     Key? key,
@@ -41,7 +36,7 @@ class SingleSelectTypeOptionWidget extends TypeOptionWidget {
   @override
   Widget build(BuildContext context) {
     return BlocProvider(
-      create: (context) => getIt<SingleSelectTypeOptionBloc>(param1: typeOption, param2: fieldId),
+      create: (context) => SingleSelectTypeOptionBloc(typeOptionContext),
       child: BlocConsumer<SingleSelectTypeOptionBloc, SingleSelectTypeOptionState>(
         listener: (context, state) {
           dataDelegate.didUpdateTypeOptionData(state.typeOption.writeToBuffer());

+ 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 GridCellIdentifier cellData;
+  final GridCell cellData;
 
   const NumberCell({
     required this.cellData,

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

@@ -7,14 +7,18 @@ import 'package:flowy_infra_ui/style_widget/icon_button.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:provider/provider.dart';
-
 import 'row_action_sheet.dart';
+import 'package:dartz/dartz.dart' show Option;
+
+import 'row_detail.dart';
 
 class GridRowWidget extends StatefulWidget {
-  final RowBloc Function() blocBuilder;
+  final GridRow rowData;
+  final GridRowCache rowCache;
 
   const GridRowWidget({
-    required this.blocBuilder,
+    required this.rowData,
+    required this.rowCache,
     Key? key,
   }) : super(key: key);
 
@@ -24,13 +28,14 @@ class GridRowWidget extends StatefulWidget {
 
 class _GridRowWidgetState extends State<GridRowWidget> {
   late RowBloc _rowBloc;
-  late _RegionStateNotifier _rowStateNotifier;
 
   @override
   void initState() {
-    _rowBloc = widget.blocBuilder();
+    _rowBloc = RowBloc(
+      rowData: widget.rowData,
+      rowCache: widget.rowCache,
+    );
     _rowBloc.add(const RowEvent.initial());
-    _rowStateNotifier = _RegionStateNotifier();
     super.initState();
   }
 
@@ -38,29 +43,24 @@ class _GridRowWidgetState extends State<GridRowWidget> {
   Widget build(BuildContext context) {
     return BlocProvider.value(
       value: _rowBloc,
-      child: ChangeNotifierProvider.value(
-        value: _rowStateNotifier,
-        child: MouseRegion(
-          cursor: SystemMouseCursors.click,
-          onEnter: (p) => _rowStateNotifier.onEnter = true,
-          onExit: (p) => _rowStateNotifier.onEnter = false,
-          child: BlocBuilder<RowBloc, RowState>(
-            buildWhen: (p, c) => p.rowData.height != c.rowData.height,
-            builder: (context, state) {
-              return SizedBox(
-                height: 42,
-                child: Row(
-                  mainAxisSize: MainAxisSize.max,
-                  crossAxisAlignment: CrossAxisAlignment.center,
-                  children: const [
-                    _RowLeading(),
-                    _RowCells(),
-                    _RowTrailing(),
-                  ],
-                ),
-              );
-            },
-          ),
+      child: _RowEnterRegion(
+        child: BlocBuilder<RowBloc, RowState>(
+          buildWhen: (p, c) => p.rowData.height != c.rowData.height,
+          builder: (context, state) {
+            final children = [
+              const _RowLeading(),
+              _RowCells(onExpand: () => onExpandCell(context)),
+              const _RowTrailing(),
+            ];
+
+            final child = Row(
+              mainAxisSize: MainAxisSize.max,
+              crossAxisAlignment: CrossAxisAlignment.center,
+              children: children,
+            );
+
+            return SizedBox(height: 42, child: child);
+          },
         ),
       ),
     );
@@ -69,9 +69,13 @@ class _GridRowWidgetState extends State<GridRowWidget> {
   @override
   Future<void> dispose() async {
     _rowBloc.close();
-    _rowStateNotifier.dispose();
     super.dispose();
   }
+
+  void onExpandCell(BuildContext context) {
+    final page = RowDetailPage(rowData: widget.rowData, rowCache: widget.rowCache);
+    page.show(context);
+  }
 }
 
 class _RowLeading extends StatelessWidget {
@@ -143,32 +147,41 @@ class _DeleteRowButton extends StatelessWidget {
 }
 
 class _RowCells extends StatelessWidget {
-  const _RowCells({Key? key}) : super(key: key);
+  final VoidCallback onExpand;
+  const _RowCells({required this.onExpand, Key? key}) : super(key: key);
 
   @override
   Widget build(BuildContext context) {
     return BlocBuilder<RowBloc, RowState>(
       buildWhen: (previous, current) => previous.cellDataMap != current.cellDataMap,
       builder: (context, state) {
-        final List<Widget> children = state.cellDataMap.fold(() => [], _toCells);
         return Row(
           mainAxisSize: MainAxisSize.min,
           mainAxisAlignment: MainAxisAlignment.center,
-          children: children,
+          children: _makeCells(state.cellDataMap),
         );
       },
     );
   }
 
-  List<Widget> _toCells(CellDataMap dataMap) {
-    return dataMap.values.map(
-      (cellData) {
-        return CellContainer(
-          width: cellData.field.width.toDouble(),
-          child: buildGridCell(cellData),
-        );
-      },
-    ).toList();
+  List<Widget> _makeCells(Option<CellDataMap> data) {
+    return data.fold(
+      () => [],
+      (cellDataMap) => cellDataMap.values.map(
+        (cellData) {
+          Widget? expander;
+          if (cellData.field.isPrimary) {
+            expander = _CellExpander(onExpand: onExpand);
+          }
+
+          return CellContainer(
+            width: cellData.field.width.toDouble(),
+            child: buildGridCell(cellData),
+            expander: expander,
+          );
+        },
+      ).toList(),
+    );
   }
 }
 
@@ -184,3 +197,56 @@ class _RegionStateNotifier extends ChangeNotifier {
 
   bool get onEnter => _onEnter;
 }
+
+class _CellExpander extends StatelessWidget {
+  final VoidCallback onExpand;
+  const _CellExpander({required this.onExpand, Key? key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    final theme = context.watch<AppTheme>();
+    return FlowyIconButton(
+      width: 20,
+      onPressed: onExpand,
+      iconPadding: const EdgeInsets.fromLTRB(2, 2, 2, 2),
+      icon: svgWidget("grid/expander", color: theme.main1),
+    );
+  }
+}
+
+class _RowEnterRegion extends StatefulWidget {
+  final Widget child;
+  const _RowEnterRegion({required this.child, Key? key}) : super(key: key);
+
+  @override
+  State<_RowEnterRegion> createState() => _RowEnterRegionState();
+}
+
+class _RowEnterRegionState extends State<_RowEnterRegion> {
+  late _RegionStateNotifier _rowStateNotifier;
+
+  @override
+  void initState() {
+    _rowStateNotifier = _RegionStateNotifier();
+    super.initState();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return ChangeNotifierProvider.value(
+      value: _rowStateNotifier,
+      child: MouseRegion(
+        cursor: SystemMouseCursors.click,
+        onEnter: (p) => _rowStateNotifier.onEnter = true,
+        onExit: (p) => _rowStateNotifier.onEnter = false,
+        child: widget.child,
+      ),
+    );
+  }
+
+  @override
+  Future<void> dispose() async {
+    _rowStateNotifier.dispose();
+    super.dispose();
+  }
+}

+ 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 GridCellIdentifier cellData;
+  final GridCell cellData;
 
   const NumberCell({
     required this.cellData,

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

@@ -0,0 +1,157 @@
+import 'package:app_flowy/workspace/application/grid/field/field_service.dart';
+import 'package:app_flowy/workspace/application/grid/row/row_detail_bloc.dart';
+import 'package:app_flowy/workspace/application/grid/row/row_service.dart';
+import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/cell/prelude.dart';
+import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/header/field_cell.dart';
+import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/header/field_editor.dart';
+import 'package:flowy_infra/theme.dart';
+import 'package:flowy_infra_ui/flowy_infra_ui.dart';
+import 'package:flowy_infra_ui/style_widget/hover.dart';
+import 'package:flowy_infra_ui/widget/spacing.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid-data-model/grid.pb.dart' show FieldType;
+import 'package:easy_localization/easy_localization.dart';
+import 'package:app_flowy/generated/locale_keys.g.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:window_size/window_size.dart';
+
+class RowDetailPage extends StatefulWidget with FlowyOverlayDelegate {
+  final GridRow rowData;
+  final GridRowCache rowCache;
+
+  const RowDetailPage({
+    required this.rowData,
+    required this.rowCache,
+    Key? key,
+  }) : super(key: key);
+
+  @override
+  State<RowDetailPage> createState() => _RowDetailPageState();
+
+  void show(BuildContext context) async {
+    final window = await getWindowInfo();
+    final size = Size(window.frame.size.width * 0.7, window.frame.size.height * 0.7);
+    FlowyOverlay.of(context).insertWithRect(
+      widget: OverlayContainer(
+        child: this,
+        constraints: BoxConstraints.tight(size),
+      ),
+      identifier: RowDetailPage.identifier(),
+      anchorPosition: Offset(-size.width / 2.0, -size.height / 2.0),
+      anchorSize: window.frame.size,
+      anchorDirection: AnchorDirection.center,
+      style: FlowyOverlayStyle(blur: false),
+      delegate: this,
+    );
+  }
+
+  static String identifier() {
+    return (RowDetailPage).toString();
+  }
+}
+
+class _RowDetailPageState extends State<RowDetailPage> {
+  @override
+  Widget build(BuildContext context) {
+    return BlocProvider(
+      create: (context) {
+        final bloc = RowDetailBloc(rowData: widget.rowData, rowCache: widget.rowCache);
+        bloc.add(const RowDetailEvent.initial());
+        return bloc;
+      },
+      child: const Padding(
+        padding: EdgeInsets.symmetric(horizontal: 80, vertical: 40),
+        child: _PropertyList(),
+      ),
+    );
+  }
+}
+
+class _PropertyList extends StatelessWidget {
+  const _PropertyList({
+    Key? key,
+  }) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return BlocBuilder<RowDetailBloc, RowDetailState>(
+      buildWhen: (previous, current) => previous.cellDatas != current.cellDatas,
+      builder: (context, state) {
+        return ListView.separated(
+          itemCount: state.cellDatas.length,
+          itemBuilder: (BuildContext context, int index) {
+            return _RowDetailCell(cellData: state.cellDatas[index]);
+          },
+          separatorBuilder: (BuildContext context, int index) {
+            return const VSpace(2);
+          },
+        );
+      },
+    );
+  }
+}
+
+class _RowDetailCell extends StatelessWidget {
+  final GridCell cellData;
+  const _RowDetailCell({required this.cellData, Key? key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    final theme = context.watch<AppTheme>();
+    final cell = buildGridCell(
+      cellData,
+      style: _buildCellStyle(theme, cellData.field.fieldType),
+    );
+    return SizedBox(
+      height: 36,
+      child: Row(
+        crossAxisAlignment: CrossAxisAlignment.stretch,
+        mainAxisAlignment: MainAxisAlignment.center,
+        children: [
+          SizedBox(
+            width: 150,
+            child: FieldCellButton(field: cellData.field, onTap: () => _showFieldEditor(context)),
+          ),
+          const HSpace(10),
+          Expanded(
+            child: FlowyHover2(
+              child: cell,
+              contentPadding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
+            ),
+          ),
+        ],
+      ),
+    );
+  }
+
+  void _showFieldEditor(BuildContext context) {
+    FieldEditor(
+      gridId: cellData.gridId,
+      fieldContextLoader: FieldContextLoaderAdaptor(
+        gridId: cellData.gridId,
+        field: cellData.field,
+      ),
+    ).show(context);
+  }
+}
+
+GridCellStyle? _buildCellStyle(AppTheme theme, FieldType fieldType) {
+  switch (fieldType) {
+    case FieldType.Checkbox:
+      return null;
+    case FieldType.DateTime:
+      return null;
+    case FieldType.MultiSelect:
+      return null;
+    case FieldType.Number:
+      return null;
+    case FieldType.RichText:
+      return GridTextCellStyle(
+        placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
+      );
+    case FieldType.SingleSelect:
+      return null;
+    default:
+      return null;
+  }
+}

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

@@ -67,7 +67,7 @@ class GridPropertyList extends StatelessWidget with FlowyOverlayDelegate {
   }
 
   String identifier() {
-    return toString();
+    return (GridPropertyList).toString();
   }
 
   @override

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

@@ -86,29 +86,27 @@ class ActionCell<T extends ActionItem> extends StatelessWidget {
 
     return FlowyHover(
       style: HoverStyle(hoverColor: theme.hover),
-      builder: (context, onHover) {
-        return GestureDetector(
-          behavior: HitTestBehavior.opaque,
-          onTap: () => onSelected(action),
-          child: SizedBox(
-            height: itemHeight,
-            child: Row(
-              crossAxisAlignment: CrossAxisAlignment.center,
-              children: [
-                if (action.icon != null) action.icon!,
-                HSpace(ActionListSizes.itemHPadding),
-                FlowyText.medium(
-                  action.name,
-                  fontSize: 12,
-                ),
-              ],
-            ),
-          ).padding(
-            horizontal: ActionListSizes.padding,
-            vertical: ActionListSizes.padding,
+      child: GestureDetector(
+        behavior: HitTestBehavior.opaque,
+        onTap: () => onSelected(action),
+        child: SizedBox(
+          height: itemHeight,
+          child: Row(
+            crossAxisAlignment: CrossAxisAlignment.center,
+            children: [
+              if (action.icon != null) action.icon!,
+              HSpace(ActionListSizes.itemHPadding),
+              FlowyText.medium(
+                action.name,
+                fontSize: 12,
+              ),
+            ],
           ),
-        );
-      },
+        ).padding(
+          horizontal: ActionListSizes.padding,
+          vertical: ActionListSizes.padding,
+        ),
+      ),
     );
   }
 }

+ 42 - 19
frontend/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/flowy_overlay.dart

@@ -207,8 +207,14 @@ class FlowyOverlayState extends State<FlowyOverlay> {
 
       final reveredList = _overlayList.reversed.toList();
       final firstItem = reveredList.removeAt(0);
-      firstItem.delegate?.didRemove();
       _overlayList.remove(firstItem);
+      if (firstItem.delegate != null) {
+        firstItem.delegate!.didRemove();
+
+        if (firstItem.delegate!.asBarrier()) {
+          return;
+        }
+      }
 
       for (final element in reveredList) {
         if (element.delegate?.asBarrier() ?? false) {
@@ -286,27 +292,23 @@ class FlowyOverlayState extends State<FlowyOverlay> {
 
   @override
   Widget build(BuildContext context) {
-    final overlays = _overlayList.map((item) => item.widget);
-    List<Widget> children = <Widget>[widget.child];
-
-    Widget? child;
-    if (overlays.isNotEmpty) {
-      child = Container(
-        color: style.barrierColor,
-        child: GestureDetector(
-          behavior: HitTestBehavior.opaque,
-          onTap: _handleTapOnBackground,
-        ),
-      );
-
-      if (style.blur) {
-        child = BackdropFilter(
-          child: child,
-          filter: ImageFilter.blur(sigmaX: 4, sigmaY: 4),
+    final overlays = _overlayList.map((item) {
+      var widget = item.widget;
+      if (item.delegate?.asBarrier() ?? false) {
+        widget = Container(
+          color: style.barrierColor,
+          child: GestureDetector(
+            behavior: HitTestBehavior.opaque,
+            onTap: _handleTapOnBackground,
+            child: widget,
+          ),
         );
       }
-    }
+      return widget;
+    }).toList();
 
+    List<Widget> children = <Widget>[widget.child];
+    Widget? child = _renderBackground(overlays);
     if (child != null) {
       children.add(child);
     }
@@ -335,4 +337,25 @@ class FlowyOverlayState extends State<FlowyOverlay> {
   void _handleTapOnBackground() {
     removeAll();
   }
+
+  Widget? _renderBackground(List<Widget> overlays) {
+    Widget? child;
+    if (overlays.isNotEmpty) {
+      child = Container(
+        color: style.barrierColor,
+        child: GestureDetector(
+          behavior: HitTestBehavior.opaque,
+          onTap: _handleTapOnBackground,
+        ),
+      );
+
+      if (style.blur) {
+        child = BackdropFilter(
+          child: child,
+          filter: ImageFilter.blur(sigmaX: 4, sigmaY: 4),
+        );
+      }
+    }
+    return child;
+  }
 }

+ 124 - 8
frontend/app_flowy/packages/flowy_infra_ui/lib/style_widget/hover.dart

@@ -1,17 +1,22 @@
 import 'package:flutter/material.dart';
 // ignore: unused_import
 import 'package:flowy_infra/time/duration.dart';
+import 'package:flowy_infra/size.dart';
+import 'package:flowy_infra/theme.dart';
+import 'package:provider/provider.dart';
 
 typedef HoverBuilder = Widget Function(BuildContext context, bool onHover);
 
 class FlowyHover extends StatefulWidget {
   final HoverStyle style;
-  final HoverBuilder builder;
+  final HoverBuilder? builder;
+  final Widget? child;
   final bool Function()? setSelected;
 
   const FlowyHover({
     Key? key,
-    required this.builder,
+    this.builder,
+    this.child,
     required this.style,
     this.setSelected,
   }) : super(key: key);
@@ -27,25 +32,27 @@ class _FlowyHoverState extends State<FlowyHover> {
   Widget build(BuildContext context) {
     return MouseRegion(
       cursor: SystemMouseCursors.click,
+      opaque: false,
       onEnter: (p) => setState(() => _onHover = true),
       onExit: (p) => setState(() => _onHover = false),
-      child: render(),
+      child: renderWidget(),
     );
   }
 
-  Widget render() {
+  Widget renderWidget() {
     var showHover = _onHover;
     if (!showHover && widget.setSelected != null) {
       showHover = widget.setSelected!();
     }
 
+    final child = widget.child ?? widget.builder!(context, _onHover);
     if (showHover) {
       return FlowyHoverContainer(
         style: widget.style,
-        child: widget.builder(context, _onHover),
+        child: child,
       );
     } else {
-      return widget.builder(context, _onHover);
+      return child;
     }
   }
 }
@@ -67,11 +74,11 @@ class HoverStyle {
 
 class FlowyHoverContainer extends StatelessWidget {
   final HoverStyle style;
-  final Widget child;
+  final Widget? child;
 
   const FlowyHoverContainer({
     Key? key,
-    required this.child,
+    this.child,
     required this.style,
   }) : super(key: key);
 
@@ -93,3 +100,112 @@ class FlowyHoverContainer extends StatelessWidget {
     );
   }
 }
+
+//
+abstract class HoverWidget extends StatefulWidget {
+  const HoverWidget({Key? key}) : super(key: key);
+
+  ValueNotifier<bool> get onFocus;
+}
+
+class FlowyHover2 extends StatefulWidget {
+  final HoverWidget child;
+  final EdgeInsets contentPadding;
+  const FlowyHover2({
+    required this.child,
+    this.contentPadding = EdgeInsets.zero,
+    Key? key,
+  }) : super(key: key);
+
+  @override
+  State<FlowyHover2> createState() => _FlowyHover2State();
+}
+
+class _FlowyHover2State extends State<FlowyHover2> {
+  late FlowyHoverState _hoverState;
+
+  @override
+  void initState() {
+    _hoverState = FlowyHoverState();
+    widget.child.onFocus.addListener(() {
+      _hoverState.onFocus = widget.child.onFocus.value;
+    });
+    super.initState();
+  }
+
+  @override
+  void dispose() {
+    _hoverState.dispose();
+    super.dispose();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return ChangeNotifierProvider.value(
+      value: _hoverState,
+      child: MouseRegion(
+        cursor: SystemMouseCursors.click,
+        opaque: false,
+        onEnter: (p) => setState(() => _hoverState.onHover = true),
+        onExit: (p) => setState(() => _hoverState.onHover = false),
+        child: Stack(
+          fit: StackFit.loose,
+          alignment: AlignmentDirectional.center,
+          children: [
+            const _HoverBackground(),
+            Padding(
+              padding: widget.contentPadding,
+              child: widget.child,
+            ),
+          ],
+        ),
+      ),
+    );
+  }
+}
+
+class _HoverBackground extends StatelessWidget {
+  const _HoverBackground({Key? key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    final theme = context.watch<AppTheme>();
+    return Consumer<FlowyHoverState>(
+      builder: (context, state, child) {
+        if (state.onHover || state.onFocus) {
+          return FlowyHoverContainer(
+            style: HoverStyle(
+              borderRadius: Corners.s6Border,
+              hoverColor: theme.shader6,
+            ),
+          );
+        } else {
+          return const SizedBox();
+        }
+      },
+    );
+  }
+}
+
+class FlowyHoverState extends ChangeNotifier {
+  bool _onHover = false;
+  bool _onFocus = false;
+
+  set onHover(bool value) {
+    if (_onHover != value) {
+      _onHover = value;
+      notifyListeners();
+    }
+  }
+
+  bool get onHover => _onHover;
+
+  set onFocus(bool value) {
+    if (_onFocus != value) {
+      _onFocus = value;
+      notifyListeners();
+    }
+  }
+
+  bool get onFocus => _onFocus;
+}

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

@@ -172,7 +172,7 @@ class GridEventMoveItem {
 }
 
 class GridEventNewSelectOption {
-     SelectOptionName request;
+     CreateSelectOptionPayload request;
      GridEventNewSelectOption(this.request);
 
     Future<Either<SelectOption, FlowyError>> send() {

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

@@ -85,6 +85,7 @@ class Field extends $pb.GeneratedMessage {
     ..aOB(5, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'frozen')
     ..aOB(6, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'visibility')
     ..a<$core.int>(7, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'width', $pb.PbFieldType.O3)
+    ..aOB(8, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'isPrimary')
     ..hasRequiredFields = false
   ;
 
@@ -97,6 +98,7 @@ class Field extends $pb.GeneratedMessage {
     $core.bool? frozen,
     $core.bool? visibility,
     $core.int? width,
+    $core.bool? isPrimary,
   }) {
     final _result = create();
     if (id != null) {
@@ -120,6 +122,9 @@ class Field extends $pb.GeneratedMessage {
     if (width != null) {
       _result.width = width;
     }
+    if (isPrimary != null) {
+      _result.isPrimary = isPrimary;
+    }
     return _result;
   }
   factory Field.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
@@ -205,6 +210,15 @@ class Field extends $pb.GeneratedMessage {
   $core.bool hasWidth() => $_has(6);
   @$pb.TagNumber(7)
   void clearWidth() => clearField(7);
+
+  @$pb.TagNumber(8)
+  $core.bool get isPrimary => $_getBF(7);
+  @$pb.TagNumber(8)
+  set isPrimary($core.bool v) { $_setBool(7, v); }
+  @$pb.TagNumber(8)
+  $core.bool hasIsPrimary() => $_has(7);
+  @$pb.TagNumber(8)
+  void clearIsPrimary() => clearField(8);
 }
 
 class FieldOrder extends $pb.GeneratedMessage {

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

@@ -57,11 +57,12 @@ const Field$json = const {
     const {'1': 'frozen', '3': 5, '4': 1, '5': 8, '10': 'frozen'},
     const {'1': 'visibility', '3': 6, '4': 1, '5': 8, '10': 'visibility'},
     const {'1': 'width', '3': 7, '4': 1, '5': 5, '10': 'width'},
+    const {'1': 'is_primary', '3': 8, '4': 1, '5': 8, '10': 'isPrimary'},
   ],
 };
 
 /// Descriptor for `Field`. Decode as a `google.protobuf.DescriptorProto`.
-final $typed_data.Uint8List fieldDescriptor = $convert.base64Decode('CgVGaWVsZBIOCgJpZBgBIAEoCVICaWQSEgoEbmFtZRgCIAEoCVIEbmFtZRISCgRkZXNjGAMgASgJUgRkZXNjEikKCmZpZWxkX3R5cGUYBCABKA4yCi5GaWVsZFR5cGVSCWZpZWxkVHlwZRIWCgZmcm96ZW4YBSABKAhSBmZyb3plbhIeCgp2aXNpYmlsaXR5GAYgASgIUgp2aXNpYmlsaXR5EhQKBXdpZHRoGAcgASgFUgV3aWR0aA==');
+final $typed_data.Uint8List fieldDescriptor = $convert.base64Decode('CgVGaWVsZBIOCgJpZBgBIAEoCVICaWQSEgoEbmFtZRgCIAEoCVIEbmFtZRISCgRkZXNjGAMgASgJUgRkZXNjEikKCmZpZWxkX3R5cGUYBCABKA4yCi5GaWVsZFR5cGVSCWZpZWxkVHlwZRIWCgZmcm96ZW4YBSABKAhSBmZyb3plbhIeCgp2aXNpYmlsaXR5GAYgASgIUgp2aXNpYmlsaXR5EhQKBXdpZHRoGAcgASgFUgV3aWR0aBIdCgppc19wcmltYXJ5GAggASgIUglpc1ByaW1hcnk=');
 @$core.Deprecated('Use fieldOrderDescriptor instead')
 const FieldOrder$json = const {
   '1': 'FieldOrder',

+ 11 - 56
frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid/cell_entities.pb.dart

@@ -9,21 +9,23 @@ import 'dart:core' as $core;
 
 import 'package:protobuf/protobuf.dart' as $pb;
 
+import 'field_entities.pb.dart' as $0;
+
 class CreateSelectOptionPayload extends $pb.GeneratedMessage {
   static final $pb.BuilderInfo _i = $pb.BuilderInfo(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'CreateSelectOptionPayload', createEmptyInstance: create)
-    ..aOM<CellIdentifierPayload>(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'cellIdentifier', subBuilder: CellIdentifierPayload.create)
+    ..aOM<$0.FieldIdentifierPayload>(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'fieldIdentifier', subBuilder: $0.FieldIdentifierPayload.create)
     ..aOS(2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'optionName')
     ..hasRequiredFields = false
   ;
 
   CreateSelectOptionPayload._() : super();
   factory CreateSelectOptionPayload({
-    CellIdentifierPayload? cellIdentifier,
+    $0.FieldIdentifierPayload? fieldIdentifier,
     $core.String? optionName,
   }) {
     final _result = create();
-    if (cellIdentifier != null) {
-      _result.cellIdentifier = cellIdentifier;
+    if (fieldIdentifier != null) {
+      _result.fieldIdentifier = fieldIdentifier;
     }
     if (optionName != null) {
       _result.optionName = optionName;
@@ -52,15 +54,15 @@ class CreateSelectOptionPayload extends $pb.GeneratedMessage {
   static CreateSelectOptionPayload? _defaultInstance;
 
   @$pb.TagNumber(1)
-  CellIdentifierPayload get cellIdentifier => $_getN(0);
+  $0.FieldIdentifierPayload get fieldIdentifier => $_getN(0);
   @$pb.TagNumber(1)
-  set cellIdentifier(CellIdentifierPayload v) { setField(1, v); }
+  set fieldIdentifier($0.FieldIdentifierPayload v) { setField(1, v); }
   @$pb.TagNumber(1)
-  $core.bool hasCellIdentifier() => $_has(0);
+  $core.bool hasFieldIdentifier() => $_has(0);
   @$pb.TagNumber(1)
-  void clearCellIdentifier() => clearField(1);
+  void clearFieldIdentifier() => clearField(1);
   @$pb.TagNumber(1)
-  CellIdentifierPayload ensureCellIdentifier() => $_ensure(0);
+  $0.FieldIdentifierPayload ensureFieldIdentifier() => $_ensure(0);
 
   @$pb.TagNumber(2)
   $core.String get optionName => $_getSZ(1);
@@ -147,50 +149,3 @@ class CellIdentifierPayload extends $pb.GeneratedMessage {
   void clearRowId() => clearField(3);
 }
 
-class SelectOptionName extends $pb.GeneratedMessage {
-  static final $pb.BuilderInfo _i = $pb.BuilderInfo(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'SelectOptionName', createEmptyInstance: create)
-    ..aOS(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'name')
-    ..hasRequiredFields = false
-  ;
-
-  SelectOptionName._() : super();
-  factory SelectOptionName({
-    $core.String? name,
-  }) {
-    final _result = create();
-    if (name != null) {
-      _result.name = name;
-    }
-    return _result;
-  }
-  factory SelectOptionName.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
-  factory SelectOptionName.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')
-  SelectOptionName clone() => SelectOptionName()..mergeFromMessage(this);
-  @$core.Deprecated(
-  'Using this can add significant overhead to your binary. '
-  'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
-  'Will be removed in next major version')
-  SelectOptionName copyWith(void Function(SelectOptionName) updates) => super.copyWith((message) => updates(message as SelectOptionName)) as SelectOptionName; // ignore: deprecated_member_use
-  $pb.BuilderInfo get info_ => _i;
-  @$core.pragma('dart2js:noInline')
-  static SelectOptionName create() => SelectOptionName._();
-  SelectOptionName createEmptyInstance() => create();
-  static $pb.PbList<SelectOptionName> createRepeated() => $pb.PbList<SelectOptionName>();
-  @$core.pragma('dart2js:noInline')
-  static SelectOptionName getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<SelectOptionName>(create);
-  static SelectOptionName? _defaultInstance;
-
-  @$pb.TagNumber(1)
-  $core.String get name => $_getSZ(0);
-  @$pb.TagNumber(1)
-  set name($core.String v) { $_setString(0, v); }
-  @$pb.TagNumber(1)
-  $core.bool hasName() => $_has(0);
-  @$pb.TagNumber(1)
-  void clearName() => clearField(1);
-}
-

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

@@ -12,13 +12,13 @@ import 'dart:typed_data' as $typed_data;
 const CreateSelectOptionPayload$json = const {
   '1': 'CreateSelectOptionPayload',
   '2': const [
-    const {'1': 'cell_identifier', '3': 1, '4': 1, '5': 11, '6': '.CellIdentifierPayload', '10': 'cellIdentifier'},
+    const {'1': 'field_identifier', '3': 1, '4': 1, '5': 11, '6': '.FieldIdentifierPayload', '10': 'fieldIdentifier'},
     const {'1': 'option_name', '3': 2, '4': 1, '5': 9, '10': 'optionName'},
   ],
 };
 
 /// Descriptor for `CreateSelectOptionPayload`. Decode as a `google.protobuf.DescriptorProto`.
-final $typed_data.Uint8List createSelectOptionPayloadDescriptor = $convert.base64Decode('ChlDcmVhdGVTZWxlY3RPcHRpb25QYXlsb2FkEj8KD2NlbGxfaWRlbnRpZmllchgBIAEoCzIWLkNlbGxJZGVudGlmaWVyUGF5bG9hZFIOY2VsbElkZW50aWZpZXISHwoLb3B0aW9uX25hbWUYAiABKAlSCm9wdGlvbk5hbWU=');
+final $typed_data.Uint8List createSelectOptionPayloadDescriptor = $convert.base64Decode('ChlDcmVhdGVTZWxlY3RPcHRpb25QYXlsb2FkEkIKEGZpZWxkX2lkZW50aWZpZXIYASABKAsyFy5GaWVsZElkZW50aWZpZXJQYXlsb2FkUg9maWVsZElkZW50aWZpZXISHwoLb3B0aW9uX25hbWUYAiABKAlSCm9wdGlvbk5hbWU=');
 @$core.Deprecated('Use cellIdentifierPayloadDescriptor instead')
 const CellIdentifierPayload$json = const {
   '1': 'CellIdentifierPayload',
@@ -31,13 +31,3 @@ const CellIdentifierPayload$json = const {
 
 /// Descriptor for `CellIdentifierPayload`. Decode as a `google.protobuf.DescriptorProto`.
 final $typed_data.Uint8List cellIdentifierPayloadDescriptor = $convert.base64Decode('ChVDZWxsSWRlbnRpZmllclBheWxvYWQSFwoHZ3JpZF9pZBgBIAEoCVIGZ3JpZElkEhkKCGZpZWxkX2lkGAIgASgJUgdmaWVsZElkEhUKBnJvd19pZBgDIAEoCVIFcm93SWQ=');
-@$core.Deprecated('Use selectOptionNameDescriptor instead')
-const SelectOptionName$json = const {
-  '1': 'SelectOptionName',
-  '2': const [
-    const {'1': 'name', '3': 1, '4': 1, '5': 9, '10': 'name'},
-  ],
-};
-
-/// Descriptor for `SelectOptionName`. Decode as a `google.protobuf.DescriptorProto`.
-final $typed_data.Uint8List selectOptionNameDescriptor = $convert.base64Decode('ChBTZWxlY3RPcHRpb25OYW1lEhIKBG5hbWUYASABKAlSBG5hbWU=');

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

@@ -2,9 +2,7 @@
 proto_crates = [
     "src/event_map.rs",
     "src/services/field/type_options",
-    "src/services/field/field_entities.rs",
-    "src/services/cell/cell_entities.rs",
-    "src/services/row/row_entities.rs",
+    "src/services/entities",
     "src/dart_notification.rs"
 ]
 event_files = ["src/event_map.rs"]

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

@@ -1,10 +1,8 @@
 use crate::manager::GridManager;
-use crate::services::cell::cell_entities::*;
-use crate::services::field::field_entities::*;
+use crate::services::entities::*;
 use crate::services::field::type_options::*;
 use crate::services::field::{default_type_option_builder_from_type, type_option_builder_from_json_str};
 use crate::services::grid_editor::ClientGridEditor;
-use crate::services::row::row_entities::*;
 use flowy_error::{ErrorCode, FlowyError, FlowyResult};
 use flowy_grid_data_model::entities::*;
 use lib_dispatch::prelude::{data_result, AppData, Data, DataResult};
@@ -248,9 +246,20 @@ pub(crate) async fn update_cell_handler(
 }
 
 #[tracing::instrument(level = "debug", skip_all, err)]
-pub(crate) async fn new_select_option_handler(data: Data<SelectOptionName>) -> DataResult<SelectOption, FlowyError> {
-    let params = data.into_inner();
-    data_result(SelectOption::new(&params.name))
+pub(crate) async fn new_select_option_handler(
+    data: Data<CreateSelectOptionPayload>,
+    manager: AppData<Arc<GridManager>>,
+) -> DataResult<SelectOption, FlowyError> {
+    let params: CreateSelectOptionParams = data.into_inner().try_into()?;
+    let editor = manager.get_grid_editor(&params.grid_id)?;
+    match editor.get_field_meta(&params.field_id).await {
+        None => Err(ErrorCode::InvalidData.into()),
+        Some(field_meta) => {
+            let type_option = select_option_operation(&field_meta)?;
+            let select_option = type_option.create_option(&params.option_name);
+            data_result(select_option)
+        }
+    }
 }
 
 #[tracing::instrument(level = "debug", skip_all, err)]
@@ -337,20 +346,3 @@ pub(crate) async fn update_cell_select_option_handler(
     let _ = editor.update_cell(changeset).await?;
     Ok(())
 }
-
-fn select_option_operation(field_meta: &FieldMeta) -> FlowyResult<Box<dyn SelectOptionOperation>> {
-    match &field_meta.field_type {
-        FieldType::SingleSelect => {
-            let type_option = SingleSelectTypeOption::from(field_meta);
-            Ok(Box::new(type_option))
-        }
-        FieldType::MultiSelect => {
-            let type_option = MultiSelectTypeOption::from(field_meta);
-            Ok(Box::new(type_option))
-        }
-        ty => {
-            tracing::error!("Unsupported field type: {:?} for this handler", ty);
-            Err(ErrorCode::FieldInvalidOperation.into())
-        }
-    }
-}

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

@@ -69,7 +69,7 @@ pub enum GridEvent {
     #[event(input = "MoveItemPayload")]
     MoveItem = 17,
 
-    #[event(input = "SelectOptionName", output = "SelectOption")]
+    #[event(input = "CreateSelectOptionPayload", output = "SelectOption")]
     NewSelectOption = 30,
 
     #[event(input = "CellIdentifierPayload", output = "SelectOptionContext")]

+ 32 - 191
frontend/rust-lib/flowy-grid/src/protobuf/model/cell_entities.rs

@@ -26,7 +26,7 @@
 #[derive(PartialEq,Clone,Default)]
 pub struct CreateSelectOptionPayload {
     // message fields
-    pub cell_identifier: ::protobuf::SingularPtrField<CellIdentifierPayload>,
+    pub field_identifier: ::protobuf::SingularPtrField<super::field_entities::FieldIdentifierPayload>,
     pub option_name: ::std::string::String,
     // special fields
     pub unknown_fields: ::protobuf::UnknownFields,
@@ -44,37 +44,37 @@ impl CreateSelectOptionPayload {
         ::std::default::Default::default()
     }
 
-    // .CellIdentifierPayload cell_identifier = 1;
+    // .FieldIdentifierPayload field_identifier = 1;
 
 
-    pub fn get_cell_identifier(&self) -> &CellIdentifierPayload {
-        self.cell_identifier.as_ref().unwrap_or_else(|| <CellIdentifierPayload as ::protobuf::Message>::default_instance())
+    pub fn get_field_identifier(&self) -> &super::field_entities::FieldIdentifierPayload {
+        self.field_identifier.as_ref().unwrap_or_else(|| <super::field_entities::FieldIdentifierPayload as ::protobuf::Message>::default_instance())
     }
-    pub fn clear_cell_identifier(&mut self) {
-        self.cell_identifier.clear();
+    pub fn clear_field_identifier(&mut self) {
+        self.field_identifier.clear();
     }
 
-    pub fn has_cell_identifier(&self) -> bool {
-        self.cell_identifier.is_some()
+    pub fn has_field_identifier(&self) -> bool {
+        self.field_identifier.is_some()
     }
 
     // Param is passed by value, moved
-    pub fn set_cell_identifier(&mut self, v: CellIdentifierPayload) {
-        self.cell_identifier = ::protobuf::SingularPtrField::some(v);
+    pub fn set_field_identifier(&mut self, v: super::field_entities::FieldIdentifierPayload) {
+        self.field_identifier = ::protobuf::SingularPtrField::some(v);
     }
 
     // Mutable pointer to the field.
     // If field is not initialized, it is initialized with default value first.
-    pub fn mut_cell_identifier(&mut self) -> &mut CellIdentifierPayload {
-        if self.cell_identifier.is_none() {
-            self.cell_identifier.set_default();
+    pub fn mut_field_identifier(&mut self) -> &mut super::field_entities::FieldIdentifierPayload {
+        if self.field_identifier.is_none() {
+            self.field_identifier.set_default();
         }
-        self.cell_identifier.as_mut().unwrap()
+        self.field_identifier.as_mut().unwrap()
     }
 
     // Take field
-    pub fn take_cell_identifier(&mut self) -> CellIdentifierPayload {
-        self.cell_identifier.take().unwrap_or_else(|| CellIdentifierPayload::new())
+    pub fn take_field_identifier(&mut self) -> super::field_entities::FieldIdentifierPayload {
+        self.field_identifier.take().unwrap_or_else(|| super::field_entities::FieldIdentifierPayload::new())
     }
 
     // string option_name = 2;
@@ -106,7 +106,7 @@ impl CreateSelectOptionPayload {
 
 impl ::protobuf::Message for CreateSelectOptionPayload {
     fn is_initialized(&self) -> bool {
-        for v in &self.cell_identifier {
+        for v in &self.field_identifier {
             if !v.is_initialized() {
                 return false;
             }
@@ -119,7 +119,7 @@ impl ::protobuf::Message for CreateSelectOptionPayload {
             let (field_number, wire_type) = is.read_tag_unpack()?;
             match field_number {
                 1 => {
-                    ::protobuf::rt::read_singular_message_into(wire_type, is, &mut self.cell_identifier)?;
+                    ::protobuf::rt::read_singular_message_into(wire_type, is, &mut self.field_identifier)?;
                 },
                 2 => {
                     ::protobuf::rt::read_singular_proto3_string_into(wire_type, is, &mut self.option_name)?;
@@ -136,7 +136,7 @@ impl ::protobuf::Message for CreateSelectOptionPayload {
     #[allow(unused_variables)]
     fn compute_size(&self) -> u32 {
         let mut my_size = 0;
-        if let Some(ref v) = self.cell_identifier.as_ref() {
+        if let Some(ref v) = self.field_identifier.as_ref() {
             let len = v.compute_size();
             my_size += 1 + ::protobuf::rt::compute_raw_varint32_size(len) + len;
         }
@@ -149,7 +149,7 @@ impl ::protobuf::Message for CreateSelectOptionPayload {
     }
 
     fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::ProtobufResult<()> {
-        if let Some(ref v) = self.cell_identifier.as_ref() {
+        if let Some(ref v) = self.field_identifier.as_ref() {
             os.write_tag(1, ::protobuf::wire_format::WireTypeLengthDelimited)?;
             os.write_raw_varint32(v.get_cached_size())?;
             v.write_to_with_cached_sizes(os)?;
@@ -195,10 +195,10 @@ impl ::protobuf::Message for CreateSelectOptionPayload {
         static descriptor: ::protobuf::rt::LazyV2<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::LazyV2::INIT;
         descriptor.get(|| {
             let mut fields = ::std::vec::Vec::new();
-            fields.push(::protobuf::reflect::accessor::make_singular_ptr_field_accessor::<_, ::protobuf::types::ProtobufTypeMessage<CellIdentifierPayload>>(
-                "cell_identifier",
-                |m: &CreateSelectOptionPayload| { &m.cell_identifier },
-                |m: &mut CreateSelectOptionPayload| { &mut m.cell_identifier },
+            fields.push(::protobuf::reflect::accessor::make_singular_ptr_field_accessor::<_, ::protobuf::types::ProtobufTypeMessage<super::field_entities::FieldIdentifierPayload>>(
+                "field_identifier",
+                |m: &CreateSelectOptionPayload| { &m.field_identifier },
+                |m: &mut CreateSelectOptionPayload| { &mut m.field_identifier },
             ));
             fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeString>(
                 "option_name",
@@ -221,7 +221,7 @@ impl ::protobuf::Message for CreateSelectOptionPayload {
 
 impl ::protobuf::Clear for CreateSelectOptionPayload {
     fn clear(&mut self) {
-        self.cell_identifier.clear();
+        self.field_identifier.clear();
         self.option_name.clear();
         self.unknown_fields.clear();
     }
@@ -482,173 +482,14 @@ impl ::protobuf::reflect::ProtobufValue for CellIdentifierPayload {
     }
 }
 
-#[derive(PartialEq,Clone,Default)]
-pub struct SelectOptionName {
-    // message fields
-    pub name: ::std::string::String,
-    // special fields
-    pub unknown_fields: ::protobuf::UnknownFields,
-    pub cached_size: ::protobuf::CachedSize,
-}
-
-impl<'a> ::std::default::Default for &'a SelectOptionName {
-    fn default() -> &'a SelectOptionName {
-        <SelectOptionName as ::protobuf::Message>::default_instance()
-    }
-}
-
-impl SelectOptionName {
-    pub fn new() -> SelectOptionName {
-        ::std::default::Default::default()
-    }
-
-    // string name = 1;
-
-
-    pub fn get_name(&self) -> &str {
-        &self.name
-    }
-    pub fn clear_name(&mut self) {
-        self.name.clear();
-    }
-
-    // Param is passed by value, moved
-    pub fn set_name(&mut self, v: ::std::string::String) {
-        self.name = v;
-    }
-
-    // Mutable pointer to the field.
-    // If field is not initialized, it is initialized with default value first.
-    pub fn mut_name(&mut self) -> &mut ::std::string::String {
-        &mut self.name
-    }
-
-    // Take field
-    pub fn take_name(&mut self) -> ::std::string::String {
-        ::std::mem::replace(&mut self.name, ::std::string::String::new())
-    }
-}
-
-impl ::protobuf::Message for SelectOptionName {
-    fn is_initialized(&self) -> bool {
-        true
-    }
-
-    fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::ProtobufResult<()> {
-        while !is.eof()? {
-            let (field_number, wire_type) = is.read_tag_unpack()?;
-            match field_number {
-                1 => {
-                    ::protobuf::rt::read_singular_proto3_string_into(wire_type, is, &mut self.name)?;
-                },
-                _ => {
-                    ::protobuf::rt::read_unknown_or_skip_group(field_number, wire_type, is, self.mut_unknown_fields())?;
-                },
-            };
-        }
-        ::std::result::Result::Ok(())
-    }
-
-    // Compute sizes of nested messages
-    #[allow(unused_variables)]
-    fn compute_size(&self) -> u32 {
-        let mut my_size = 0;
-        if !self.name.is_empty() {
-            my_size += ::protobuf::rt::string_size(1, &self.name);
-        }
-        my_size += ::protobuf::rt::unknown_fields_size(self.get_unknown_fields());
-        self.cached_size.set(my_size);
-        my_size
-    }
-
-    fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::ProtobufResult<()> {
-        if !self.name.is_empty() {
-            os.write_string(1, &self.name)?;
-        }
-        os.write_unknown_fields(self.get_unknown_fields())?;
-        ::std::result::Result::Ok(())
-    }
-
-    fn get_cached_size(&self) -> u32 {
-        self.cached_size.get()
-    }
-
-    fn get_unknown_fields(&self) -> &::protobuf::UnknownFields {
-        &self.unknown_fields
-    }
-
-    fn mut_unknown_fields(&mut self) -> &mut ::protobuf::UnknownFields {
-        &mut self.unknown_fields
-    }
-
-    fn as_any(&self) -> &dyn (::std::any::Any) {
-        self as &dyn (::std::any::Any)
-    }
-    fn as_any_mut(&mut self) -> &mut dyn (::std::any::Any) {
-        self as &mut dyn (::std::any::Any)
-    }
-    fn into_any(self: ::std::boxed::Box<Self>) -> ::std::boxed::Box<dyn (::std::any::Any)> {
-        self
-    }
-
-    fn descriptor(&self) -> &'static ::protobuf::reflect::MessageDescriptor {
-        Self::descriptor_static()
-    }
-
-    fn new() -> SelectOptionName {
-        SelectOptionName::new()
-    }
-
-    fn descriptor_static() -> &'static ::protobuf::reflect::MessageDescriptor {
-        static descriptor: ::protobuf::rt::LazyV2<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::LazyV2::INIT;
-        descriptor.get(|| {
-            let mut fields = ::std::vec::Vec::new();
-            fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeString>(
-                "name",
-                |m: &SelectOptionName| { &m.name },
-                |m: &mut SelectOptionName| { &mut m.name },
-            ));
-            ::protobuf::reflect::MessageDescriptor::new_pb_name::<SelectOptionName>(
-                "SelectOptionName",
-                fields,
-                file_descriptor_proto()
-            )
-        })
-    }
-
-    fn default_instance() -> &'static SelectOptionName {
-        static instance: ::protobuf::rt::LazyV2<SelectOptionName> = ::protobuf::rt::LazyV2::INIT;
-        instance.get(SelectOptionName::new)
-    }
-}
-
-impl ::protobuf::Clear for SelectOptionName {
-    fn clear(&mut self) {
-        self.name.clear();
-        self.unknown_fields.clear();
-    }
-}
-
-impl ::std::fmt::Debug for SelectOptionName {
-    fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
-        ::protobuf::text_format::fmt(self, f)
-    }
-}
-
-impl ::protobuf::reflect::ProtobufValue for SelectOptionName {
-    fn as_ref(&self) -> ::protobuf::reflect::ReflectValueRef {
-        ::protobuf::reflect::ReflectValueRef::Message(self)
-    }
-}
-
 static file_descriptor_proto_data: &'static [u8] = b"\
-    \n\x13cell_entities.proto\"}\n\x19CreateSelectOptionPayload\x12?\n\x0fce\
-    ll_identifier\x18\x01\x20\x01(\x0b2\x16.CellIdentifierPayloadR\x0ecellId\
-    entifier\x12\x1f\n\x0boption_name\x18\x02\x20\x01(\tR\noptionName\"b\n\
-    \x15CellIdentifierPayload\x12\x17\n\x07grid_id\x18\x01\x20\x01(\tR\x06gr\
-    idId\x12\x19\n\x08field_id\x18\x02\x20\x01(\tR\x07fieldId\x12\x15\n\x06r\
-    ow_id\x18\x03\x20\x01(\tR\x05rowId\"&\n\x10SelectOptionName\x12\x12\n\
-    \x04name\x18\x01\x20\x01(\tR\x04nameb\x06proto3\
+    \n\x13cell_entities.proto\x1a\x14field_entities.proto\"\x80\x01\n\x19Cre\
+    ateSelectOptionPayload\x12B\n\x10field_identifier\x18\x01\x20\x01(\x0b2\
+    \x17.FieldIdentifierPayloadR\x0ffieldIdentifier\x12\x1f\n\x0boption_name\
+    \x18\x02\x20\x01(\tR\noptionName\"b\n\x15CellIdentifierPayload\x12\x17\n\
+    \x07grid_id\x18\x01\x20\x01(\tR\x06gridId\x12\x19\n\x08field_id\x18\x02\
+    \x20\x01(\tR\x07fieldId\x12\x15\n\x06row_id\x18\x03\x20\x01(\tR\x05rowId\
+    b\x06proto3\
 ";
 
 static file_descriptor_proto_lazy: ::protobuf::rt::LazyV2<::protobuf::descriptor::FileDescriptorProto> = ::protobuf::rt::LazyV2::INIT;

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

@@ -1,7 +1,8 @@
 syntax = "proto3";
+import "field_entities.proto";
 
 message CreateSelectOptionPayload {
-    CellIdentifierPayload cell_identifier = 1;
+    FieldIdentifierPayload field_identifier = 1;
     string option_name = 2;
 }
 message CellIdentifierPayload {
@@ -9,6 +10,3 @@ message CellIdentifierPayload {
     string field_id = 2;
     string row_id = 3;
 }
-message SelectOptionName {
-    string name = 1;
-}

+ 0 - 3
frontend/rust-lib/flowy-grid/src/services/cell/mod.rs

@@ -1,3 +0,0 @@
-pub(crate) mod cell_entities;
-
-pub use cell_entities::*;

+ 13 - 10
frontend/rust-lib/flowy-grid/src/services/cell/cell_entities.rs → frontend/rust-lib/flowy-grid/src/services/entities/cell_entities.rs

@@ -1,3 +1,4 @@
+use crate::services::entities::{FieldIdentifier, FieldIdentifierPayload};
 use flowy_derive::ProtoBuf;
 use flowy_error::ErrorCode;
 use flowy_grid_data_model::parser::NotEmptyStr;
@@ -5,25 +6,33 @@ use flowy_grid_data_model::parser::NotEmptyStr;
 #[derive(ProtoBuf, Default)]
 pub struct CreateSelectOptionPayload {
     #[pb(index = 1)]
-    pub cell_identifier: CellIdentifierPayload,
+    pub field_identifier: FieldIdentifierPayload,
 
     #[pb(index = 2)]
     pub option_name: String,
 }
 
 pub struct CreateSelectOptionParams {
-    pub cell_identifier: CellIdentifier,
+    pub field_identifier: FieldIdentifier,
     pub option_name: String,
 }
 
+impl std::ops::Deref for CreateSelectOptionParams {
+    type Target = FieldIdentifier;
+
+    fn deref(&self) -> &Self::Target {
+        &self.field_identifier
+    }
+}
+
 impl TryInto<CreateSelectOptionParams> for CreateSelectOptionPayload {
     type Error = ErrorCode;
 
     fn try_into(self) -> Result<CreateSelectOptionParams, Self::Error> {
         let option_name = NotEmptyStr::parse(self.option_name).map_err(|_| ErrorCode::SelectOptionNameIsEmpty)?;
-        let cell_identifier = self.cell_identifier.try_into()?;
+        let field_identifier = self.field_identifier.try_into()?;
         Ok(CreateSelectOptionParams {
-            cell_identifier,
+            field_identifier,
             option_name: option_name.0,
         })
     }
@@ -61,9 +70,3 @@ impl TryInto<CellIdentifier> for CellIdentifierPayload {
         })
     }
 }
-
-#[derive(ProtoBuf, Default)]
-pub struct SelectOptionName {
-    #[pb(index = 1)]
-    pub name: String,
-}

+ 0 - 0
frontend/rust-lib/flowy-grid/src/services/field/field_entities.rs → frontend/rust-lib/flowy-grid/src/services/entities/field_entities.rs


+ 7 - 0
frontend/rust-lib/flowy-grid/src/services/entities/mod.rs

@@ -0,0 +1,7 @@
+mod cell_entities;
+mod field_entities;
+mod row_entities;
+
+pub use cell_entities::*;
+pub use field_entities::*;
+pub use row_entities::*;

+ 31 - 0
frontend/rust-lib/flowy-grid/src/services/entities/row_entities.rs

@@ -0,0 +1,31 @@
+use flowy_derive::ProtoBuf;
+use flowy_error::ErrorCode;
+use flowy_grid_data_model::parser::NotEmptyStr;
+
+#[derive(ProtoBuf, Default)]
+pub struct RowIdentifierPayload {
+    #[pb(index = 1)]
+    pub grid_id: String,
+
+    #[pb(index = 3)]
+    pub row_id: String,
+}
+
+pub struct RowIdentifier {
+    pub grid_id: String,
+    pub row_id: String,
+}
+
+impl TryInto<RowIdentifier> for RowIdentifierPayload {
+    type Error = ErrorCode;
+
+    fn try_into(self) -> Result<RowIdentifier, Self::Error> {
+        let grid_id = NotEmptyStr::parse(self.grid_id).map_err(|_| ErrorCode::GridIdIsEmpty)?;
+        let row_id = NotEmptyStr::parse(self.row_id).map_err(|_| ErrorCode::RowIdIsEmpty)?;
+
+        Ok(RowIdentifier {
+            grid_id: grid_id.0,
+            row_id: row_id.0,
+        })
+    }
+}

+ 7 - 1
frontend/rust-lib/flowy-grid/src/services/field/field_builder.rs

@@ -13,7 +13,7 @@ pub type BoxTypeOptionBuilder = Box<dyn TypeOptionBuilder + 'static>;
 impl FieldBuilder {
     pub fn new<T: Into<BoxTypeOptionBuilder>>(type_option_builder: T) -> Self {
         let type_option_builder = type_option_builder.into();
-        let field_meta = FieldMeta::new("", "", type_option_builder.field_type());
+        let field_meta = FieldMeta::new("", "", type_option_builder.field_type(), false);
         Self {
             field_meta,
             type_option_builder,
@@ -35,6 +35,7 @@ impl FieldBuilder {
             visibility: field.visibility,
             width: field.width,
             type_options: IndexMap::default(),
+            is_primary: field.is_primary,
         };
         Self {
             field_meta,
@@ -52,6 +53,11 @@ impl FieldBuilder {
         self
     }
 
+    pub fn primary(mut self, is_primary: bool) -> Self {
+        self.field_meta.is_primary = is_primary;
+        self
+    }
+
     pub fn visibility(mut self, visibility: bool) -> Self {
         self.field_meta.visibility = visibility;
         self

+ 0 - 2
frontend/rust-lib/flowy-grid/src/services/field/mod.rs

@@ -1,7 +1,5 @@
 mod field_builder;
-pub(crate) mod field_entities;
 pub(crate) mod type_options;
 
 pub use field_builder::*;
-pub use field_entities::*;
 pub use type_options::*;

+ 58 - 2
frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option.rs

@@ -1,10 +1,10 @@
 use crate::impl_type_option;
-use crate::services::cell::{CellIdentifier, CellIdentifierPayload};
+use crate::services::entities::{CellIdentifier, CellIdentifierPayload};
 use crate::services::field::{BoxTypeOptionBuilder, TypeOptionBuilder};
 use crate::services::row::{CellDataChangeset, CellDataOperation, TypeOptionCellData};
 use bytes::Bytes;
 use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
-use flowy_error::{ErrorCode, FlowyError};
+use flowy_error::{ErrorCode, FlowyError, FlowyResult};
 use flowy_grid_data_model::entities::{
     CellChangeset, CellMeta, FieldMeta, FieldType, TypeOptionDataDeserializer, TypeOptionDataEntry,
 };
@@ -36,10 +36,35 @@ pub trait SelectOptionOperation: TypeOptionDataEntry + Send + Sync {
         }
     }
 
+    fn create_option(&self, name: &str) -> SelectOption {
+        let color = select_option_color_from_index(self.options().len());
+        SelectOption::with_color(name, color)
+    }
+
     fn option_context(&self, cell_meta: &Option<CellMeta>) -> SelectOptionContext;
+
+    fn options(&self) -> &Vec<SelectOption>;
+
     fn mut_options(&mut self) -> &mut Vec<SelectOption>;
 }
 
+pub fn select_option_operation(field_meta: &FieldMeta) -> FlowyResult<Box<dyn SelectOptionOperation>> {
+    match &field_meta.field_type {
+        FieldType::SingleSelect => {
+            let type_option = SingleSelectTypeOption::from(field_meta);
+            Ok(Box::new(type_option))
+        }
+        FieldType::MultiSelect => {
+            let type_option = MultiSelectTypeOption::from(field_meta);
+            Ok(Box::new(type_option))
+        }
+        ty => {
+            tracing::error!("Unsupported field type: {:?} for this handler", ty);
+            Err(ErrorCode::FieldInvalidOperation.into())
+        }
+    }
+}
+
 // Single select
 #[derive(Clone, Debug, Default, Serialize, Deserialize, ProtoBuf)]
 pub struct SingleSelectTypeOption {
@@ -60,6 +85,10 @@ impl SelectOptionOperation for SingleSelectTypeOption {
         }
     }
 
+    fn options(&self) -> &Vec<SelectOption> {
+        &self.options
+    }
+
     fn mut_options(&mut self) -> &mut Vec<SelectOption> {
         &mut self.options
     }
@@ -155,6 +184,10 @@ impl SelectOptionOperation for MultiSelectTypeOption {
         }
     }
 
+    fn options(&self) -> &Vec<SelectOption> {
+        &self.options
+    }
+
     fn mut_options(&mut self) -> &mut Vec<SelectOption> {
         &mut self.options
     }
@@ -265,6 +298,14 @@ impl SelectOption {
             color: SelectOptionColor::default(),
         }
     }
+
+    pub fn with_color(name: &str, color: SelectOptionColor) -> Self {
+        SelectOption {
+            id: nanoid!(4),
+            name: name.to_owned(),
+            color,
+        }
+    }
 }
 
 #[derive(Clone, Debug, Default, ProtoBuf)]
@@ -430,6 +471,21 @@ pub enum SelectOptionColor {
     Blue = 8,
 }
 
+pub fn select_option_color_from_index(index: usize) -> SelectOptionColor {
+    match index % 8 {
+        0 => SelectOptionColor::Purple,
+        1 => SelectOptionColor::Pink,
+        2 => SelectOptionColor::LightPink,
+        3 => SelectOptionColor::Orange,
+        4 => SelectOptionColor::Yellow,
+        5 => SelectOptionColor::Lime,
+        6 => SelectOptionColor::Green,
+        7 => SelectOptionColor::Aqua,
+        8 => SelectOptionColor::Blue,
+        _ => SelectOptionColor::Purple,
+    }
+}
+
 impl std::default::Default for SelectOptionColor {
     fn default() -> Self {
         SelectOptionColor::Purple

+ 28 - 25
frontend/rust-lib/flowy-grid/src/services/grid_editor.rs

@@ -1,7 +1,7 @@
 use crate::dart_notification::{send_dart_notification, GridNotification};
 use crate::manager::GridUser;
 use crate::services::block_meta_manager::GridBlockMetaEditorManager;
-use crate::services::cell::CellIdentifier;
+use crate::services::entities::CellIdentifier;
 use crate::services::field::{default_type_option_builder_from_type, type_option_builder_from_bytes, FieldBuilder};
 use crate::services::persistence::block_index::BlockIndexPersistence;
 use crate::services::row::*;
@@ -22,7 +22,7 @@ use tokio::sync::RwLock;
 pub struct ClientGridEditor {
     grid_id: String,
     user: Arc<dyn GridUser>,
-    pad: Arc<RwLock<GridMetaPad>>,
+    grid_pad: Arc<RwLock<GridMetaPad>>,
     rev_manager: Arc<RevisionManager>,
     block_meta_manager: Arc<GridBlockMetaEditorManager>,
 }
@@ -38,14 +38,14 @@ impl ClientGridEditor {
         let cloud = Arc::new(GridRevisionCloudService { token });
         let grid_pad = rev_manager.load::<GridPadBuilder>(Some(cloud)).await?;
         let rev_manager = Arc::new(rev_manager);
-        let pad = Arc::new(RwLock::new(grid_pad));
-        let blocks = pad.read().await.get_block_metas();
+        let grid_pad = Arc::new(RwLock::new(grid_pad));
+        let blocks = grid_pad.read().await.get_block_metas();
 
         let block_meta_manager = Arc::new(GridBlockMetaEditorManager::new(grid_id, &user, blocks, persistence).await?);
         Ok(Arc::new(Self {
             grid_id: grid_id.to_owned(),
             user,
-            pad,
+            grid_pad,
             rev_manager,
             block_meta_manager,
         }))
@@ -94,18 +94,18 @@ impl ClientGridEditor {
     }
 
     pub async fn create_next_field_meta(&self, field_type: &FieldType) -> FlowyResult<FieldMeta> {
-        let name = format!("Property {}", self.pad.read().await.fields().len() + 1);
+        let name = format!("Property {}", self.grid_pad.read().await.fields().len() + 1);
         let field_meta = FieldBuilder::from_field_type(field_type).name(&name).build();
         Ok(field_meta)
     }
 
     pub async fn contain_field(&self, field_id: &str) -> bool {
-        self.pad.read().await.contain_field(field_id)
+        self.grid_pad.read().await.contain_field(field_id)
     }
 
     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_meta(params.field_id.as_str()) {
+        let json_deserializer = match self.grid_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()),
         };
@@ -169,7 +169,7 @@ impl ClientGridEditor {
     }
 
     pub async fn get_field_meta(&self, field_id: &str) -> Option<FieldMeta> {
-        let field_meta = self.pad.read().await.get_field_meta(field_id)?.1.clone();
+        let field_meta = self.grid_pad.read().await.get_field_meta(field_id)?.1.clone();
         Some(field_meta)
     }
 
@@ -178,14 +178,14 @@ impl ClientGridEditor {
         T: Into<FieldOrder>,
     {
         if field_ids.is_none() {
-            let field_metas = self.pad.read().await.get_field_metas(None)?;
+            let field_metas = self.grid_pad.read().await.get_field_metas(None)?;
             return Ok(field_metas);
         }
 
         let to_field_orders = |item: Vec<T>| item.into_iter().map(|data| data.into()).collect();
         let field_orders = field_ids.map_or(vec![], to_field_orders);
         let expected_len = field_orders.len();
-        let field_metas = self.pad.read().await.get_field_metas(Some(field_orders))?;
+        let field_metas = self.grid_pad.read().await.get_field_metas(Some(field_orders))?;
         if expected_len != 0 && field_metas.len() != expected_len {
             tracing::error!(
                 "This is a bug. The len of the field_metas should equal to {}",
@@ -207,7 +207,7 @@ impl ClientGridEditor {
     }
 
     pub async fn create_row(&self, start_row_id: Option<String>) -> FlowyResult<RowOrder> {
-        let field_metas = self.pad.read().await.get_field_metas(None)?;
+        let field_metas = self.grid_pad.read().await.get_field_metas(None)?;
         let block_id = self.block_id().await?;
 
         // insert empty row below the row whose id is upper_row_id
@@ -314,7 +314,7 @@ 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_meta(&changeset.field_id) {
+        match self.grid_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))
@@ -334,7 +334,7 @@ impl ClientGridEditor {
     }
 
     pub async fn get_block_metas(&self) -> FlowyResult<Vec<GridBlockMeta>> {
-        let grid_blocks = self.pad.read().await.get_block_metas();
+        let grid_blocks = self.grid_pad.read().await.get_block_metas();
         Ok(grid_blocks)
     }
 
@@ -347,7 +347,7 @@ impl ClientGridEditor {
     }
 
     pub async fn grid_data(&self) -> FlowyResult<Grid> {
-        let pad_read_guard = self.pad.read().await;
+        let pad_read_guard = self.grid_pad.read().await;
         let field_orders = pad_read_guard.get_field_orders();
         let mut block_orders = vec![];
         for block_order in pad_read_guard.get_block_metas() {
@@ -369,7 +369,7 @@ impl ClientGridEditor {
     pub async fn grid_block_snapshots(&self, block_ids: Option<Vec<String>>) -> FlowyResult<Vec<GridBlockSnapshot>> {
         let block_ids = match block_ids {
             None => self
-                .pad
+                .grid_pad
                 .read()
                 .await
                 .get_block_metas()
@@ -396,7 +396,7 @@ impl ClientGridEditor {
         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) {
+        if let Some((index, field_meta)) = self.grid_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 {
@@ -420,14 +420,14 @@ impl ClientGridEditor {
     }
 
     pub async fn delta_bytes(&self) -> Bytes {
-        self.pad.read().await.delta_bytes()
+        self.grid_pad.read().await.delta_bytes()
     }
 
     async fn modify<F>(&self, f: F) -> FlowyResult<()>
     where
         F: for<'a> FnOnce(&'a mut GridMetaPad) -> FlowyResult<Option<GridChangeset>>,
     {
-        let mut write_guard = self.pad.write().await;
+        let mut write_guard = self.grid_pad.write().await;
         if let Some(changeset) = f(&mut *write_guard)? {
             let _ = self.apply_change(changeset).await?;
         }
@@ -455,7 +455,7 @@ impl ClientGridEditor {
     }
 
     async fn block_id(&self) -> FlowyResult<String> {
-        match self.pad.read().await.get_block_metas().last() {
+        match self.grid_pad.read().await.get_block_metas().last() {
             None => Err(FlowyError::internal().context("There is no grid block in this grid")),
             Some(grid_block) => Ok(grid_block.block_id.clone()),
         }
@@ -463,7 +463,7 @@ impl ClientGridEditor {
 
     #[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) {
+        if let Some((index, field_meta)) = self.grid_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?;
@@ -473,10 +473,13 @@ impl ClientGridEditor {
 
     #[tracing::instrument(level = "trace", skip_all, err)]
     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() {
+        if let Some((_, field_meta)) = self
+            .grid_pad
+            .read()
+            .await
+            .get_field_meta(field_id)
+            .map(|(index, field)| (index, field.clone()))
+        {
             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?;

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

@@ -2,7 +2,7 @@ mod util;
 
 pub mod block_meta_editor;
 mod block_meta_manager;
-pub mod cell;
+pub mod entities;
 pub mod field;
 pub mod grid_editor;
 pub mod persistence;

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

@@ -7,6 +7,7 @@ pub fn make_default_grid() -> BuildGridContext {
     let text_field = FieldBuilder::new(RichTextTypeOptionBuilder::default())
         .name("Name")
         .visibility(true)
+        .primary(true)
         .build();
 
     // single select

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

@@ -271,6 +271,7 @@ pub fn create_text_field(grid_id: &str) -> (InsertFieldParams, FieldMeta) {
         frozen: field_meta.frozen,
         visibility: field_meta.visibility,
         width: field_meta.width,
+        is_primary: false,
     };
 
     let params = InsertFieldParams {
@@ -303,6 +304,7 @@ pub fn create_single_select_field(grid_id: &str) -> (InsertFieldParams, FieldMet
         frozen: field_meta.frozen,
         visibility: field_meta.visibility,
         width: field_meta.width,
+        is_primary: false,
     };
 
     let params = InsertFieldParams {

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

@@ -42,6 +42,9 @@ pub struct Field {
 
     #[pb(index = 7)]
     pub width: i32,
+
+    #[pb(index = 8)]
+    pub is_primary: bool,
 }
 
 impl std::convert::From<FieldMeta> for Field {
@@ -54,6 +57,7 @@ impl std::convert::From<FieldMeta> for Field {
             frozen: field_meta.frozen,
             visibility: field_meta.visibility,
             width: field_meta.width,
+            is_primary: field_meta.is_primary,
         }
     }
 }

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

@@ -98,13 +98,21 @@ pub struct FieldMeta {
     // #[pb(index = 8)]
     /// type_options contains key/value pairs
     /// key: id of the FieldType
-    /// value: type option data string
+    /// value: type option data that can be parsed into specified TypeOptionStruct.
+    /// For example, CheckboxTypeOption, MultiSelectTypeOption etc.
     #[serde(with = "indexmap::serde_seq")]
     pub type_options: IndexMap<String, String>,
+
+    #[serde(default = "default_is_primary")]
+    pub is_primary: bool,
+}
+
+fn default_is_primary() -> bool {
+    false
 }
 
 impl FieldMeta {
-    pub fn new(name: &str, desc: &str, field_type: FieldType) -> Self {
+    pub fn new(name: &str, desc: &str, field_type: FieldType, is_primary: bool) -> Self {
         let width = field_type.default_cell_width();
         Self {
             id: gen_field_id(),
@@ -115,6 +123,7 @@ impl FieldMeta {
             visibility: true,
             width,
             type_options: Default::default(),
+            is_primary,
         }
     }
 

+ 119 - 83
shared-lib/flowy-grid-data-model/src/protobuf/model/grid.rs

@@ -290,6 +290,7 @@ pub struct Field {
     pub frozen: bool,
     pub visibility: bool,
     pub width: i32,
+    pub is_primary: bool,
     // special fields
     pub unknown_fields: ::protobuf::UnknownFields,
     pub cached_size: ::protobuf::CachedSize,
@@ -443,6 +444,21 @@ impl Field {
     pub fn set_width(&mut self, v: i32) {
         self.width = v;
     }
+
+    // bool is_primary = 8;
+
+
+    pub fn get_is_primary(&self) -> bool {
+        self.is_primary
+    }
+    pub fn clear_is_primary(&mut self) {
+        self.is_primary = false;
+    }
+
+    // Param is passed by value, moved
+    pub fn set_is_primary(&mut self, v: bool) {
+        self.is_primary = v;
+    }
 }
 
 impl ::protobuf::Message for Field {
@@ -487,6 +503,13 @@ impl ::protobuf::Message for Field {
                     let tmp = is.read_int32()?;
                     self.width = tmp;
                 },
+                8 => {
+                    if wire_type != ::protobuf::wire_format::WireTypeVarint {
+                        return ::std::result::Result::Err(::protobuf::rt::unexpected_wire_type(wire_type));
+                    }
+                    let tmp = is.read_bool()?;
+                    self.is_primary = tmp;
+                },
                 _ => {
                     ::protobuf::rt::read_unknown_or_skip_group(field_number, wire_type, is, self.mut_unknown_fields())?;
                 },
@@ -520,6 +543,9 @@ impl ::protobuf::Message for Field {
         if self.width != 0 {
             my_size += ::protobuf::rt::value_size(7, self.width, ::protobuf::wire_format::WireTypeVarint);
         }
+        if self.is_primary != false {
+            my_size += 2;
+        }
         my_size += ::protobuf::rt::unknown_fields_size(self.get_unknown_fields());
         self.cached_size.set(my_size);
         my_size
@@ -547,6 +573,9 @@ impl ::protobuf::Message for Field {
         if self.width != 0 {
             os.write_int32(7, self.width)?;
         }
+        if self.is_primary != false {
+            os.write_bool(8, self.is_primary)?;
+        }
         os.write_unknown_fields(self.get_unknown_fields())?;
         ::std::result::Result::Ok(())
     }
@@ -620,6 +649,11 @@ impl ::protobuf::Message for Field {
                 |m: &Field| { &m.width },
                 |m: &mut Field| { &mut m.width },
             ));
+            fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeBool>(
+                "is_primary",
+                |m: &Field| { &m.is_primary },
+                |m: &mut Field| { &mut m.is_primary },
+            ));
             ::protobuf::reflect::MessageDescriptor::new_pb_name::<Field>(
                 "Field",
                 fields,
@@ -643,6 +677,7 @@ impl ::protobuf::Clear for Field {
         self.frozen = false;
         self.visibility = false;
         self.width = 0;
+        self.is_primary = false;
         self.unknown_fields.clear();
     }
 }
@@ -7770,93 +7805,94 @@ static file_descriptor_proto_data: &'static [u8] = b"\
     \n\ngrid.proto\"z\n\x04Grid\x12\x0e\n\x02id\x18\x01\x20\x01(\tR\x02id\
     \x12.\n\x0cfield_orders\x18\x02\x20\x03(\x0b2\x0b.FieldOrderR\x0bfieldOr\
     ders\x122\n\x0cblock_orders\x18\x03\x20\x03(\x0b2\x0f.GridBlockOrderR\
-    \x0bblockOrders\"\xb8\x01\n\x05Field\x12\x0e\n\x02id\x18\x01\x20\x01(\tR\
+    \x0bblockOrders\"\xd7\x01\n\x05Field\x12\x0e\n\x02id\x18\x01\x20\x01(\tR\
     \x02id\x12\x12\n\x04name\x18\x02\x20\x01(\tR\x04name\x12\x12\n\x04desc\
     \x18\x03\x20\x01(\tR\x04desc\x12)\n\nfield_type\x18\x04\x20\x01(\x0e2\n.\
     FieldTypeR\tfieldType\x12\x16\n\x06frozen\x18\x05\x20\x01(\x08R\x06froze\
     n\x12\x1e\n\nvisibility\x18\x06\x20\x01(\x08R\nvisibility\x12\x14\n\x05w\
-    idth\x18\x07\x20\x01(\x05R\x05width\"'\n\nFieldOrder\x12\x19\n\x08field_\
-    id\x18\x01\x20\x01(\tR\x07fieldId\"\xc6\x01\n\x12GridFieldChangeset\x12\
-    \x17\n\x07grid_id\x18\x01\x20\x01(\tR\x06gridId\x124\n\x0finserted_field\
-    s\x18\x02\x20\x03(\x0b2\x0b.IndexFieldR\x0einsertedFields\x122\n\x0edele\
-    ted_fields\x18\x03\x20\x03(\x0b2\x0b.FieldOrderR\rdeletedFields\x12-\n\
-    \x0eupdated_fields\x18\x04\x20\x03(\x0b2\x06.FieldR\rupdatedFields\"@\n\
-    \nIndexField\x12\x1c\n\x05field\x18\x01\x20\x01(\x0b2\x06.FieldR\x05fiel\
-    d\x12\x14\n\x05index\x18\x02\x20\x01(\x05R\x05index\"\x90\x01\n\x1aGetEd\
-    itFieldContextPayload\x12\x17\n\x07grid_id\x18\x01\x20\x01(\tR\x06gridId\
-    \x12\x1b\n\x08field_id\x18\x02\x20\x01(\tH\0R\x07fieldId\x12)\n\nfield_t\
-    ype\x18\x03\x20\x01(\x0e2\n.FieldTypeR\tfieldTypeB\x11\n\x0fone_of_field\
-    _id\"q\n\x10EditFieldPayload\x12\x17\n\x07grid_id\x18\x01\x20\x01(\tR\
-    \x06gridId\x12\x19\n\x08field_id\x18\x02\x20\x01(\tR\x07fieldId\x12)\n\n\
-    field_type\x18\x03\x20\x01(\x0e2\n.FieldTypeR\tfieldType\"|\n\x10EditFie\
-    ldContext\x12\x17\n\x07grid_id\x18\x01\x20\x01(\tR\x06gridId\x12%\n\ngri\
-    d_field\x18\x02\x20\x01(\x0b2\x06.FieldR\tgridField\x12(\n\x10type_optio\
-    n_data\x18\x03\x20\x01(\x0cR\x0etypeOptionData\"-\n\rRepeatedField\x12\
-    \x1c\n\x05items\x18\x01\x20\x03(\x0b2\x06.FieldR\x05items\"7\n\x12Repeat\
-    edFieldOrder\x12!\n\x05items\x18\x01\x20\x03(\x0b2\x0b.FieldOrderR\x05it\
-    ems\"T\n\x08RowOrder\x12\x15\n\x06row_id\x18\x01\x20\x01(\tR\x05rowId\
-    \x12\x19\n\x08block_id\x18\x02\x20\x01(\tR\x07blockId\x12\x16\n\x06heigh\
-    t\x18\x03\x20\x01(\x05R\x06height\"\xb8\x01\n\x03Row\x12\x0e\n\x02id\x18\
-    \x01\x20\x01(\tR\x02id\x12@\n\x10cell_by_field_id\x18\x02\x20\x03(\x0b2\
-    \x17.Row.CellByFieldIdEntryR\rcellByFieldId\x12\x16\n\x06height\x18\x03\
-    \x20\x01(\x05R\x06height\x1aG\n\x12CellByFieldIdEntry\x12\x10\n\x03key\
-    \x18\x01\x20\x01(\tR\x03key\x12\x1b\n\x05value\x18\x02\x20\x01(\x0b2\x05\
-    .CellR\x05value:\x028\x01\")\n\x0bRepeatedRow\x12\x1a\n\x05items\x18\x01\
-    \x20\x03(\x0b2\x04.RowR\x05items\"5\n\x11RepeatedGridBlock\x12\x20\n\x05\
-    items\x18\x01\x20\x03(\x0b2\n.GridBlockR\x05items\"U\n\x0eGridBlockOrder\
-    \x12\x19\n\x08block_id\x18\x01\x20\x01(\tR\x07blockId\x12(\n\nrow_orders\
-    \x18\x02\x20\x03(\x0b2\t.RowOrderR\trowOrders\"_\n\rIndexRowOrder\x12&\n\
-    \trow_order\x18\x01\x20\x01(\x0b2\t.RowOrderR\x08rowOrder\x12\x16\n\x05i\
-    ndex\x18\x02\x20\x01(\x05H\0R\x05indexB\x0e\n\x0cone_of_index\"\xbf\x01\
-    \n\x11GridRowsChangeset\x12\x19\n\x08block_id\x18\x01\x20\x01(\tR\x07blo\
-    ckId\x123\n\rinserted_rows\x18\x02\x20\x03(\x0b2\x0e.IndexRowOrderR\x0ci\
-    nsertedRows\x12,\n\x0cdeleted_rows\x18\x03\x20\x03(\x0b2\t.RowOrderR\x0b\
-    deletedRows\x12,\n\x0cupdated_rows\x18\x04\x20\x03(\x0b2\t.RowOrderR\x0b\
-    updatedRows\"E\n\tGridBlock\x12\x0e\n\x02id\x18\x01\x20\x01(\tR\x02id\
-    \x12(\n\nrow_orders\x18\x02\x20\x03(\x0b2\t.RowOrderR\trowOrders\";\n\
-    \x04Cell\x12\x19\n\x08field_id\x18\x01\x20\x01(\tR\x07fieldId\x12\x18\n\
-    \x07content\x18\x02\x20\x01(\tR\x07content\"\x8f\x01\n\x14CellNotificati\
-    onData\x12\x17\n\x07grid_id\x18\x01\x20\x01(\tR\x06gridId\x12\x19\n\x08f\
-    ield_id\x18\x02\x20\x01(\tR\x07fieldId\x12\x15\n\x06row_id\x18\x03\x20\
-    \x01(\tR\x05rowId\x12\x1a\n\x07content\x18\x04\x20\x01(\tH\0R\x07content\
-    B\x10\n\x0eone_of_content\"+\n\x0cRepeatedCell\x12\x1b\n\x05items\x18\
-    \x01\x20\x03(\x0b2\x05.CellR\x05items\"'\n\x11CreateGridPayload\x12\x12\
-    \n\x04name\x18\x01\x20\x01(\tR\x04name\"\x1e\n\x06GridId\x12\x14\n\x05va\
-    lue\x18\x01\x20\x01(\tR\x05value\"#\n\x0bGridBlockId\x12\x14\n\x05value\
-    \x18\x01\x20\x01(\tR\x05value\"f\n\x10CreateRowPayload\x12\x17\n\x07grid\
-    _id\x18\x01\x20\x01(\tR\x06gridId\x12\"\n\x0cstart_row_id\x18\x02\x20\
-    \x01(\tH\0R\nstartRowIdB\x15\n\x13one_of_start_row_id\"\xb6\x01\n\x12Ins\
-    ertFieldPayload\x12\x17\n\x07grid_id\x18\x01\x20\x01(\tR\x06gridId\x12\
-    \x1c\n\x05field\x18\x02\x20\x01(\x0b2\x06.FieldR\x05field\x12(\n\x10type\
-    _option_data\x18\x03\x20\x01(\x0cR\x0etypeOptionData\x12&\n\x0estart_fie\
-    ld_id\x18\x04\x20\x01(\tH\0R\x0cstartFieldIdB\x17\n\x15one_of_start_fiel\
-    d_id\"d\n\x11QueryFieldPayload\x12\x17\n\x07grid_id\x18\x01\x20\x01(\tR\
-    \x06gridId\x126\n\x0cfield_orders\x18\x02\x20\x01(\x0b2\x13.RepeatedFiel\
-    dOrderR\x0bfieldOrders\"e\n\x16QueryGridBlocksPayload\x12\x17\n\x07grid_\
-    id\x18\x01\x20\x01(\tR\x06gridId\x122\n\x0cblock_orders\x18\x02\x20\x03(\
-    \x0b2\x0f.GridBlockOrderR\x0bblockOrders\"\xa8\x03\n\x15FieldChangesetPa\
-    yload\x12\x19\n\x08field_id\x18\x01\x20\x01(\tR\x07fieldId\x12\x17\n\x07\
-    grid_id\x18\x02\x20\x01(\tR\x06gridId\x12\x14\n\x04name\x18\x03\x20\x01(\
-    \tH\0R\x04name\x12\x14\n\x04desc\x18\x04\x20\x01(\tH\x01R\x04desc\x12+\n\
-    \nfield_type\x18\x05\x20\x01(\x0e2\n.FieldTypeH\x02R\tfieldType\x12\x18\
-    \n\x06frozen\x18\x06\x20\x01(\x08H\x03R\x06frozen\x12\x20\n\nvisibility\
-    \x18\x07\x20\x01(\x08H\x04R\nvisibility\x12\x16\n\x05width\x18\x08\x20\
-    \x01(\x05H\x05R\x05width\x12*\n\x10type_option_data\x18\t\x20\x01(\x0cH\
-    \x06R\x0etypeOptionDataB\r\n\x0bone_of_nameB\r\n\x0bone_of_descB\x13\n\
-    \x11one_of_field_typeB\x0f\n\rone_of_frozenB\x13\n\x11one_of_visibilityB\
-    \x0e\n\x0cone_of_widthB\x19\n\x17one_of_type_option_data\"\x9c\x01\n\x0f\
-    MoveItemPayload\x12\x17\n\x07grid_id\x18\x01\x20\x01(\tR\x06gridId\x12\
-    \x17\n\x07item_id\x18\x02\x20\x01(\tR\x06itemId\x12\x1d\n\nfrom_index\
-    \x18\x03\x20\x01(\x05R\tfromIndex\x12\x19\n\x08to_index\x18\x04\x20\x01(\
-    \x05R\x07toIndex\x12\x1d\n\x02ty\x18\x05\x20\x01(\x0e2\r.MoveItemTypeR\
-    \x02ty\"\x7f\n\rCellChangeset\x12\x17\n\x07grid_id\x18\x01\x20\x01(\tR\
-    \x06gridId\x12\x15\n\x06row_id\x18\x02\x20\x01(\tR\x05rowId\x12\x19\n\
-    \x08field_id\x18\x03\x20\x01(\tR\x07fieldId\x12\x14\n\x04data\x18\x04\
-    \x20\x01(\tH\0R\x04dataB\r\n\x0bone_of_data**\n\x0cMoveItemType\x12\r\n\
-    \tMoveField\x10\0\x12\x0b\n\x07MoveRow\x10\x01*d\n\tFieldType\x12\x0c\n\
-    \x08RichText\x10\0\x12\n\n\x06Number\x10\x01\x12\x0c\n\x08DateTime\x10\
-    \x02\x12\x10\n\x0cSingleSelect\x10\x03\x12\x0f\n\x0bMultiSelect\x10\x04\
-    \x12\x0c\n\x08Checkbox\x10\x05b\x06proto3\
+    idth\x18\x07\x20\x01(\x05R\x05width\x12\x1d\n\nis_primary\x18\x08\x20\
+    \x01(\x08R\tisPrimary\"'\n\nFieldOrder\x12\x19\n\x08field_id\x18\x01\x20\
+    \x01(\tR\x07fieldId\"\xc6\x01\n\x12GridFieldChangeset\x12\x17\n\x07grid_\
+    id\x18\x01\x20\x01(\tR\x06gridId\x124\n\x0finserted_fields\x18\x02\x20\
+    \x03(\x0b2\x0b.IndexFieldR\x0einsertedFields\x122\n\x0edeleted_fields\
+    \x18\x03\x20\x03(\x0b2\x0b.FieldOrderR\rdeletedFields\x12-\n\x0eupdated_\
+    fields\x18\x04\x20\x03(\x0b2\x06.FieldR\rupdatedFields\"@\n\nIndexField\
+    \x12\x1c\n\x05field\x18\x01\x20\x01(\x0b2\x06.FieldR\x05field\x12\x14\n\
+    \x05index\x18\x02\x20\x01(\x05R\x05index\"\x90\x01\n\x1aGetEditFieldCont\
+    extPayload\x12\x17\n\x07grid_id\x18\x01\x20\x01(\tR\x06gridId\x12\x1b\n\
+    \x08field_id\x18\x02\x20\x01(\tH\0R\x07fieldId\x12)\n\nfield_type\x18\
+    \x03\x20\x01(\x0e2\n.FieldTypeR\tfieldTypeB\x11\n\x0fone_of_field_id\"q\
+    \n\x10EditFieldPayload\x12\x17\n\x07grid_id\x18\x01\x20\x01(\tR\x06gridI\
+    d\x12\x19\n\x08field_id\x18\x02\x20\x01(\tR\x07fieldId\x12)\n\nfield_typ\
+    e\x18\x03\x20\x01(\x0e2\n.FieldTypeR\tfieldType\"|\n\x10EditFieldContext\
+    \x12\x17\n\x07grid_id\x18\x01\x20\x01(\tR\x06gridId\x12%\n\ngrid_field\
+    \x18\x02\x20\x01(\x0b2\x06.FieldR\tgridField\x12(\n\x10type_option_data\
+    \x18\x03\x20\x01(\x0cR\x0etypeOptionData\"-\n\rRepeatedField\x12\x1c\n\
+    \x05items\x18\x01\x20\x03(\x0b2\x06.FieldR\x05items\"7\n\x12RepeatedFiel\
+    dOrder\x12!\n\x05items\x18\x01\x20\x03(\x0b2\x0b.FieldOrderR\x05items\"T\
+    \n\x08RowOrder\x12\x15\n\x06row_id\x18\x01\x20\x01(\tR\x05rowId\x12\x19\
+    \n\x08block_id\x18\x02\x20\x01(\tR\x07blockId\x12\x16\n\x06height\x18\
+    \x03\x20\x01(\x05R\x06height\"\xb8\x01\n\x03Row\x12\x0e\n\x02id\x18\x01\
+    \x20\x01(\tR\x02id\x12@\n\x10cell_by_field_id\x18\x02\x20\x03(\x0b2\x17.\
+    Row.CellByFieldIdEntryR\rcellByFieldId\x12\x16\n\x06height\x18\x03\x20\
+    \x01(\x05R\x06height\x1aG\n\x12CellByFieldIdEntry\x12\x10\n\x03key\x18\
+    \x01\x20\x01(\tR\x03key\x12\x1b\n\x05value\x18\x02\x20\x01(\x0b2\x05.Cel\
+    lR\x05value:\x028\x01\")\n\x0bRepeatedRow\x12\x1a\n\x05items\x18\x01\x20\
+    \x03(\x0b2\x04.RowR\x05items\"5\n\x11RepeatedGridBlock\x12\x20\n\x05item\
+    s\x18\x01\x20\x03(\x0b2\n.GridBlockR\x05items\"U\n\x0eGridBlockOrder\x12\
+    \x19\n\x08block_id\x18\x01\x20\x01(\tR\x07blockId\x12(\n\nrow_orders\x18\
+    \x02\x20\x03(\x0b2\t.RowOrderR\trowOrders\"_\n\rIndexRowOrder\x12&\n\tro\
+    w_order\x18\x01\x20\x01(\x0b2\t.RowOrderR\x08rowOrder\x12\x16\n\x05index\
+    \x18\x02\x20\x01(\x05H\0R\x05indexB\x0e\n\x0cone_of_index\"\xbf\x01\n\
+    \x11GridRowsChangeset\x12\x19\n\x08block_id\x18\x01\x20\x01(\tR\x07block\
+    Id\x123\n\rinserted_rows\x18\x02\x20\x03(\x0b2\x0e.IndexRowOrderR\x0cins\
+    ertedRows\x12,\n\x0cdeleted_rows\x18\x03\x20\x03(\x0b2\t.RowOrderR\x0bde\
+    letedRows\x12,\n\x0cupdated_rows\x18\x04\x20\x03(\x0b2\t.RowOrderR\x0bup\
+    datedRows\"E\n\tGridBlock\x12\x0e\n\x02id\x18\x01\x20\x01(\tR\x02id\x12(\
+    \n\nrow_orders\x18\x02\x20\x03(\x0b2\t.RowOrderR\trowOrders\";\n\x04Cell\
+    \x12\x19\n\x08field_id\x18\x01\x20\x01(\tR\x07fieldId\x12\x18\n\x07conte\
+    nt\x18\x02\x20\x01(\tR\x07content\"\x8f\x01\n\x14CellNotificationData\
+    \x12\x17\n\x07grid_id\x18\x01\x20\x01(\tR\x06gridId\x12\x19\n\x08field_i\
+    d\x18\x02\x20\x01(\tR\x07fieldId\x12\x15\n\x06row_id\x18\x03\x20\x01(\tR\
+    \x05rowId\x12\x1a\n\x07content\x18\x04\x20\x01(\tH\0R\x07contentB\x10\n\
+    \x0eone_of_content\"+\n\x0cRepeatedCell\x12\x1b\n\x05items\x18\x01\x20\
+    \x03(\x0b2\x05.CellR\x05items\"'\n\x11CreateGridPayload\x12\x12\n\x04nam\
+    e\x18\x01\x20\x01(\tR\x04name\"\x1e\n\x06GridId\x12\x14\n\x05value\x18\
+    \x01\x20\x01(\tR\x05value\"#\n\x0bGridBlockId\x12\x14\n\x05value\x18\x01\
+    \x20\x01(\tR\x05value\"f\n\x10CreateRowPayload\x12\x17\n\x07grid_id\x18\
+    \x01\x20\x01(\tR\x06gridId\x12\"\n\x0cstart_row_id\x18\x02\x20\x01(\tH\0\
+    R\nstartRowIdB\x15\n\x13one_of_start_row_id\"\xb6\x01\n\x12InsertFieldPa\
+    yload\x12\x17\n\x07grid_id\x18\x01\x20\x01(\tR\x06gridId\x12\x1c\n\x05fi\
+    eld\x18\x02\x20\x01(\x0b2\x06.FieldR\x05field\x12(\n\x10type_option_data\
+    \x18\x03\x20\x01(\x0cR\x0etypeOptionData\x12&\n\x0estart_field_id\x18\
+    \x04\x20\x01(\tH\0R\x0cstartFieldIdB\x17\n\x15one_of_start_field_id\"d\n\
+    \x11QueryFieldPayload\x12\x17\n\x07grid_id\x18\x01\x20\x01(\tR\x06gridId\
+    \x126\n\x0cfield_orders\x18\x02\x20\x01(\x0b2\x13.RepeatedFieldOrderR\
+    \x0bfieldOrders\"e\n\x16QueryGridBlocksPayload\x12\x17\n\x07grid_id\x18\
+    \x01\x20\x01(\tR\x06gridId\x122\n\x0cblock_orders\x18\x02\x20\x03(\x0b2\
+    \x0f.GridBlockOrderR\x0bblockOrders\"\xa8\x03\n\x15FieldChangesetPayload\
+    \x12\x19\n\x08field_id\x18\x01\x20\x01(\tR\x07fieldId\x12\x17\n\x07grid_\
+    id\x18\x02\x20\x01(\tR\x06gridId\x12\x14\n\x04name\x18\x03\x20\x01(\tH\0\
+    R\x04name\x12\x14\n\x04desc\x18\x04\x20\x01(\tH\x01R\x04desc\x12+\n\nfie\
+    ld_type\x18\x05\x20\x01(\x0e2\n.FieldTypeH\x02R\tfieldType\x12\x18\n\x06\
+    frozen\x18\x06\x20\x01(\x08H\x03R\x06frozen\x12\x20\n\nvisibility\x18\
+    \x07\x20\x01(\x08H\x04R\nvisibility\x12\x16\n\x05width\x18\x08\x20\x01(\
+    \x05H\x05R\x05width\x12*\n\x10type_option_data\x18\t\x20\x01(\x0cH\x06R\
+    \x0etypeOptionDataB\r\n\x0bone_of_nameB\r\n\x0bone_of_descB\x13\n\x11one\
+    _of_field_typeB\x0f\n\rone_of_frozenB\x13\n\x11one_of_visibilityB\x0e\n\
+    \x0cone_of_widthB\x19\n\x17one_of_type_option_data\"\x9c\x01\n\x0fMoveIt\
+    emPayload\x12\x17\n\x07grid_id\x18\x01\x20\x01(\tR\x06gridId\x12\x17\n\
+    \x07item_id\x18\x02\x20\x01(\tR\x06itemId\x12\x1d\n\nfrom_index\x18\x03\
+    \x20\x01(\x05R\tfromIndex\x12\x19\n\x08to_index\x18\x04\x20\x01(\x05R\
+    \x07toIndex\x12\x1d\n\x02ty\x18\x05\x20\x01(\x0e2\r.MoveItemTypeR\x02ty\
+    \"\x7f\n\rCellChangeset\x12\x17\n\x07grid_id\x18\x01\x20\x01(\tR\x06grid\
+    Id\x12\x15\n\x06row_id\x18\x02\x20\x01(\tR\x05rowId\x12\x19\n\x08field_i\
+    d\x18\x03\x20\x01(\tR\x07fieldId\x12\x14\n\x04data\x18\x04\x20\x01(\tH\0\
+    R\x04dataB\r\n\x0bone_of_data**\n\x0cMoveItemType\x12\r\n\tMoveField\x10\
+    \0\x12\x0b\n\x07MoveRow\x10\x01*d\n\tFieldType\x12\x0c\n\x08RichText\x10\
+    \0\x12\n\n\x06Number\x10\x01\x12\x0c\n\x08DateTime\x10\x02\x12\x10\n\x0c\
+    SingleSelect\x10\x03\x12\x0f\n\x0bMultiSelect\x10\x04\x12\x0c\n\x08Check\
+    box\x10\x05b\x06proto3\
 ";
 
 static file_descriptor_proto_lazy: ::protobuf::rt::LazyV2<::protobuf::descriptor::FileDescriptorProto> = ::protobuf::rt::LazyV2::INIT;

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

@@ -13,6 +13,7 @@ message Field {
     bool frozen = 5;
     bool visibility = 6;
     int32 width = 7;
+    bool is_primary = 8;
 }
 message FieldOrder {
     string field_id = 1;

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

@@ -47,8 +47,8 @@ mod tests {
     fn create_default_grid_test() {
         let grid_id = "1".to_owned();
         let build_context = GridBuilder::default()
-            .add_field(FieldMeta::new("Name", "", FieldType::RichText))
-            .add_field(FieldMeta::new("Tags", "", FieldType::SingleSelect))
+            .add_field(FieldMeta::new("Name", "", FieldType::RichText, true))
+            .add_field(FieldMeta::new("Tags", "", FieldType::SingleSelect, false))
             .add_empty_row()
             .add_empty_row()
             .add_empty_row()