Przeglądaj źródła

fix: disable edit primary field (#2695)

* refactor: field editor

* chore: disable edit type option of primary field
Nathan.fooo 2 lat temu
rodzic
commit
bec8122178
43 zmienionych plików z 362 dodań i 313 usunięć
  1. 1 1
      frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_cache.dart
  2. 18 16
      frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_controller.dart
  3. 32 25
      frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_controller_builder.dart
  4. 3 3
      frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_data_loader.dart
  5. 9 7
      frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_data_persistence.dart
  6. 12 13
      frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_service.dart
  7. 18 18
      frontend/appflowy_flutter/lib/plugins/database_view/application/cell/checklist_cell_service.dart
  8. 5 5
      frontend/appflowy_flutter/lib/plugins/database_view/application/cell/select_option_cell_service.dart
  9. 1 1
      frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_action_sheet_bloc.dart
  10. 2 3
      frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_cell_bloc.dart
  11. 15 7
      frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_editor_bloc.dart
  12. 2 2
      frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_service.dart
  13. 5 51
      frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/type_option_context.dart
  14. 12 16
      frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/type_option_data_controller.dart
  15. 12 2
      frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/type_option_service.dart
  16. 6 6
      frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_cache.dart
  17. 2 2
      frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_data_controller.dart
  18. 6 3
      frontend/appflowy_flutter/lib/plugins/database_view/grid/application/row/row_bloc.dart
  19. 2 2
      frontend/appflowy_flutter/lib/plugins/database_view/grid/application/row/row_detail_bloc.dart
  20. 1 1
      frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_cell.dart
  21. 5 5
      frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_cell_action_sheet.dart
  22. 13 10
      frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_editor.dart
  23. 34 6
      frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/grid_header.dart
  24. 3 4
      frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/type_option/builder.dart
  25. 4 1
      frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/row/row.dart
  26. 6 6
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card.dart
  27. 6 6
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card_bloc.dart
  28. 5 5
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card_cell_builder.dart
  29. 3 2
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/card_cell.dart
  30. 7 4
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cell_builder.dart
  31. 3 2
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell_bloc.dart
  32. 3 2
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell_editor_bloc.dart
  33. 3 2
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/select_option_cell/select_option_editor_bloc.dart
  34. 35 17
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_detail.dart
  35. 1 1
      frontend/appflowy_flutter/lib/startup/deps_resolver.dart
  36. 1 2
      frontend/appflowy_flutter/test/bloc_test/board_test/create_or_edit_field_test.dart
  37. 1 1
      frontend/appflowy_flutter/test/bloc_test/board_test/group_by_unsupport_field_test.dart
  38. 12 16
      frontend/appflowy_flutter/test/bloc_test/board_test/util.dart
  39. 2 4
      frontend/appflowy_flutter/test/bloc_test/grid_test/field/edit_field_test.dart
  40. 1 2
      frontend/appflowy_flutter/test/bloc_test/grid_test/filter/edit_filter_field_test.dart
  41. 27 24
      frontend/appflowy_flutter/test/bloc_test/grid_test/util.dart
  42. 22 6
      frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs
  43. 1 1
      frontend/rust-lib/flowy-database2/tests/database/block_test/row_test.rs

+ 1 - 1
frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_cache.dart

@@ -1,6 +1,6 @@
 part of 'cell_service.dart';
 
-typedef CellByFieldId = LinkedHashMap<String, CellIdentifier>;
+typedef CellContextByFieldId = LinkedHashMap<String, DatabaseCellContext>;
 
 class DatabaseCell {
   dynamic object;

+ 18 - 16
frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_controller.dart

@@ -22,7 +22,7 @@ import 'cell_service.dart';
 ///
 // ignore: must_be_immutable
 class CellController<T, D> extends Equatable {
-  final CellIdentifier cellId;
+  final DatabaseCellContext cellContext;
   final CellCache _cellCache;
   final CellCacheKey _cacheKey;
   final FieldBackendService _fieldBackendSvc;
@@ -37,37 +37,37 @@ class CellController<T, D> extends Equatable {
   Timer? _loadDataOperation;
   Timer? _saveDataOperation;
 
-  String get viewId => cellId.viewId;
+  String get viewId => cellContext.viewId;
 
-  RowId get rowId => cellId.rowId;
+  RowId get rowId => cellContext.rowId;
 
-  String get fieldId => cellId.fieldInfo.id;
+  String get fieldId => cellContext.fieldInfo.id;
 
-  FieldInfo get fieldInfo => cellId.fieldInfo;
+  FieldInfo get fieldInfo => cellContext.fieldInfo;
 
-  FieldType get fieldType => cellId.fieldInfo.fieldType;
+  FieldType get fieldType => cellContext.fieldInfo.fieldType;
 
   CellController({
-    required this.cellId,
+    required this.cellContext,
     required CellCache cellCache,
     required CellDataLoader<T> cellDataLoader,
     required CellDataPersistence<D> cellDataPersistence,
   })  : _cellCache = cellCache,
         _cellDataLoader = cellDataLoader,
         _cellDataPersistence = cellDataPersistence,
-        _fieldListener = SingleFieldListener(fieldId: cellId.fieldId),
+        _fieldListener = SingleFieldListener(fieldId: cellContext.fieldId),
         _fieldBackendSvc = FieldBackendService(
-          viewId: cellId.viewId,
-          fieldId: cellId.fieldInfo.id,
+          viewId: cellContext.viewId,
+          fieldId: cellContext.fieldInfo.id,
         ),
         _cacheKey = CellCacheKey(
-          rowId: cellId.rowId,
-          fieldId: cellId.fieldInfo.id,
+          rowId: cellContext.rowId,
+          fieldId: cellContext.fieldInfo.id,
         ) {
     _cellDataNotifier = CellDataNotifier(value: _cellCache.get(_cacheKey));
     _cellListener = CellListener(
-      rowId: cellId.rowId,
-      fieldId: cellId.fieldInfo.id,
+      rowId: cellContext.rowId,
+      fieldId: cellContext.fieldInfo.id,
     );
 
     /// 1.Listen on user edit event and load the new cell data if needed.
@@ -195,8 +195,10 @@ class CellController<T, D> extends Equatable {
   }
 
   @override
-  List<Object> get props =>
-      [_cellCache.get(_cacheKey) ?? "", cellId.rowId + cellId.fieldInfo.id];
+  List<Object> get props => [
+        _cellCache.get(_cacheKey) ?? "",
+        cellContext.rowId + cellContext.fieldInfo.id
+      ];
 }
 
 class CellDataNotifier<T> extends ChangeNotifier {

+ 32 - 25
frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_controller_builder.dart

@@ -17,104 +17,111 @@ typedef DateCellController = CellController<DateCellDataPB, DateCellData>;
 typedef URLCellController = CellController<URLCellDataPB, String>;
 
 class CellControllerBuilder {
-  final CellIdentifier _cellId;
+  final DatabaseCellContext _cellContext;
   final CellCache _cellCache;
 
   CellControllerBuilder({
-    required CellIdentifier cellId,
+    required DatabaseCellContext cellContext,
     required CellCache cellCache,
   })  : _cellCache = cellCache,
-        _cellId = cellId;
+        _cellContext = cellContext;
 
   CellController build() {
-    switch (_cellId.fieldType) {
+    switch (_cellContext.fieldType) {
       case FieldType.Checkbox:
         final cellDataLoader = CellDataLoader(
-          cellId: _cellId,
+          cellContext: _cellContext,
           parser: StringCellDataParser(),
         );
         return TextCellController(
-          cellId: _cellId,
+          cellContext: _cellContext,
           cellCache: _cellCache,
           cellDataLoader: cellDataLoader,
-          cellDataPersistence: TextCellDataPersistence(cellId: _cellId),
+          cellDataPersistence:
+              TextCellDataPersistence(cellContext: _cellContext),
         );
       case FieldType.DateTime:
       case FieldType.LastEditedTime:
       case FieldType.CreatedTime:
         final cellDataLoader = CellDataLoader(
-          cellId: _cellId,
+          cellContext: _cellContext,
           parser: DateCellDataParser(),
           reloadOnFieldChanged: true,
         );
 
         return DateCellController(
-          cellId: _cellId,
+          cellContext: _cellContext,
           cellCache: _cellCache,
           cellDataLoader: cellDataLoader,
-          cellDataPersistence: DateCellDataPersistence(cellId: _cellId),
+          cellDataPersistence:
+              DateCellDataPersistence(cellContext: _cellContext),
         );
       case FieldType.Number:
         final cellDataLoader = CellDataLoader(
-          cellId: _cellId,
+          cellContext: _cellContext,
           parser: NumberCellDataParser(),
           reloadOnFieldChanged: true,
         );
         return NumberCellController(
-          cellId: _cellId,
+          cellContext: _cellContext,
           cellCache: _cellCache,
           cellDataLoader: cellDataLoader,
-          cellDataPersistence: TextCellDataPersistence(cellId: _cellId),
+          cellDataPersistence:
+              TextCellDataPersistence(cellContext: _cellContext),
         );
       case FieldType.RichText:
         final cellDataLoader = CellDataLoader(
-          cellId: _cellId,
+          cellContext: _cellContext,
           parser: StringCellDataParser(),
         );
         return TextCellController(
-          cellId: _cellId,
+          cellContext: _cellContext,
           cellCache: _cellCache,
           cellDataLoader: cellDataLoader,
-          cellDataPersistence: TextCellDataPersistence(cellId: _cellId),
+          cellDataPersistence:
+              TextCellDataPersistence(cellContext: _cellContext),
         );
       case FieldType.MultiSelect:
       case FieldType.SingleSelect:
         final cellDataLoader = CellDataLoader(
-          cellId: _cellId,
+          cellContext: _cellContext,
           parser: SelectOptionCellDataParser(),
           reloadOnFieldChanged: true,
         );
 
         return SelectOptionCellController(
-          cellId: _cellId,
+          cellContext: _cellContext,
           cellCache: _cellCache,
           cellDataLoader: cellDataLoader,
-          cellDataPersistence: TextCellDataPersistence(cellId: _cellId),
+          cellDataPersistence:
+              TextCellDataPersistence(cellContext: _cellContext),
         );
 
       case FieldType.Checklist:
         final cellDataLoader = CellDataLoader(
-          cellId: _cellId,
+          cellContext: _cellContext,
           parser: ChecklistCellDataParser(),
           reloadOnFieldChanged: true,
         );
 
         return ChecklistCellController(
-          cellId: _cellId,
+          cellContext: _cellContext,
           cellCache: _cellCache,
           cellDataLoader: cellDataLoader,
-          cellDataPersistence: TextCellDataPersistence(cellId: _cellId),
+          cellDataPersistence:
+              TextCellDataPersistence(cellContext: _cellContext),
         );
       case FieldType.URL:
         final cellDataLoader = CellDataLoader(
-          cellId: _cellId,
+          cellContext: _cellContext,
           parser: URLCellDataParser(),
         );
         return URLCellController(
-          cellId: _cellId,
+          cellContext: _cellContext,
           cellCache: _cellCache,
           cellDataLoader: cellDataLoader,
-          cellDataPersistence: TextCellDataPersistence(cellId: _cellId),
+          cellDataPersistence:
+              TextCellDataPersistence(cellContext: _cellContext),
         );
     }
     throw UnimplementedError;

+ 3 - 3
frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_data_loader.dart

@@ -11,18 +11,18 @@ abstract class CellDataParser<T> {
 
 class CellDataLoader<T> {
   final CellBackendService service = CellBackendService();
-  final CellIdentifier cellId;
+  final DatabaseCellContext cellContext;
   final CellDataParser<T> parser;
   final bool reloadOnFieldChanged;
 
   CellDataLoader({
-    required this.cellId,
+    required this.cellContext,
     required this.parser,
     this.reloadOnFieldChanged = false,
   });
 
   Future<T?> loadData() {
-    final fut = service.getCell(cellId: cellId);
+    final fut = service.getCell(cellContext: cellContext);
     return fut.then(
       (result) => result.fold(
         (CellPB cell) {

+ 9 - 7
frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_data_persistence.dart

@@ -7,16 +7,17 @@ abstract class CellDataPersistence<D> {
 }
 
 class TextCellDataPersistence implements CellDataPersistence<String> {
-  final CellIdentifier cellId;
+  final DatabaseCellContext cellContext;
   final _cellBackendSvc = CellBackendService();
 
   TextCellDataPersistence({
-    required this.cellId,
+    required this.cellContext,
   });
 
   @override
   Future<Option<FlowyError>> save(String data) async {
-    final fut = _cellBackendSvc.updateCell(cellId: cellId, data: data);
+    final fut =
+        _cellBackendSvc.updateCell(cellContext: cellContext, data: data);
     return fut.then((result) {
       return result.fold(
         (l) => none(),
@@ -36,14 +37,15 @@ class DateCellData with _$DateCellData {
 }
 
 class DateCellDataPersistence implements CellDataPersistence<DateCellData> {
-  final CellIdentifier cellId;
+  final DatabaseCellContext cellContext;
   DateCellDataPersistence({
-    required this.cellId,
+    required this.cellContext,
   });
 
   @override
   Future<Option<FlowyError>> save(DateCellData data) {
-    var payload = DateChangesetPB.create()..cellPath = _makeCellPath(cellId);
+    var payload = DateChangesetPB.create()
+      ..cellPath = _makeCellPath(cellContext);
     if (data.dateTime != null) {
       final date = (data.dateTime!.millisecondsSinceEpoch ~/ 1000).toString();
       payload.date = date;
@@ -62,7 +64,7 @@ class DateCellDataPersistence implements CellDataPersistence<DateCellData> {
   }
 }
 
-CellIdPB _makeCellPath(CellIdentifier cellId) {
+CellIdPB _makeCellPath(DatabaseCellContext cellId) {
   return CellIdPB.create()
     ..viewId = cellId.viewId
     ..fieldId = cellId.fieldId

+ 12 - 13
frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_service.dart

@@ -25,40 +25,39 @@ class CellBackendService {
   CellBackendService();
 
   Future<Either<void, FlowyError>> updateCell({
-    required CellIdentifier cellId,
+    required DatabaseCellContext cellContext,
     required String data,
   }) {
     final payload = CellChangesetPB.create()
-      ..viewId = cellId.viewId
-      ..fieldId = cellId.fieldId
-      ..rowId = cellId.rowId
+      ..viewId = cellContext.viewId
+      ..fieldId = cellContext.fieldId
+      ..rowId = cellContext.rowId
       ..cellChangeset = data;
     return DatabaseEventUpdateCell(payload).send();
   }
 
   Future<Either<CellPB, FlowyError>> getCell({
-    required CellIdentifier cellId,
+    required DatabaseCellContext cellContext,
   }) {
     final payload = CellIdPB.create()
-      ..viewId = cellId.viewId
-      ..fieldId = cellId.fieldId
-      ..rowId = cellId.rowId;
+      ..viewId = cellContext.viewId
+      ..fieldId = cellContext.fieldId
+      ..rowId = cellContext.rowId;
     return DatabaseEventGetCell(payload).send();
   }
 }
 
-/// Id of the cell
 /// We can locate the cell by using database + rowId + field.id.
 @freezed
-class CellIdentifier with _$CellIdentifier {
-  const factory CellIdentifier({
+class DatabaseCellContext with _$DatabaseCellContext {
+  const factory DatabaseCellContext({
     required String viewId,
     required RowId rowId,
     required FieldInfo fieldInfo,
-  }) = _CellIdentifier;
+  }) = _DatabaseCellContext;
 
   // ignore: unused_element
-  const CellIdentifier._();
+  const DatabaseCellContext._();
 
   String get fieldId => fieldInfo.id;
 

+ 18 - 18
frontend/appflowy_flutter/lib/plugins/database_view/application/cell/checklist_cell_service.dart

@@ -7,17 +7,17 @@ import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
 import 'package:dartz/dartz.dart';
 
 class ChecklistCellBackendService {
-  final CellIdentifier cellId;
+  final DatabaseCellContext cellContext;
 
-  ChecklistCellBackendService({required this.cellId});
+  ChecklistCellBackendService({required this.cellContext});
 
   Future<Either<Unit, FlowyError>> create({
     required String name,
   }) {
     final payload = ChecklistCellDataChangesetPB.create()
-      ..viewId = cellId.viewId
-      ..fieldId = cellId.fieldInfo.id
-      ..rowId = cellId.rowId
+      ..viewId = cellContext.viewId
+      ..fieldId = cellContext.fieldInfo.id
+      ..rowId = cellContext.rowId
       ..insertOptions.add(name);
 
     return DatabaseEventUpdateChecklistCell(payload).send();
@@ -27,9 +27,9 @@ class ChecklistCellBackendService {
     required List<String> optionIds,
   }) {
     final payload = ChecklistCellDataChangesetPB.create()
-      ..viewId = cellId.viewId
-      ..fieldId = cellId.fieldInfo.id
-      ..rowId = cellId.rowId
+      ..viewId = cellContext.viewId
+      ..fieldId = cellContext.fieldInfo.id
+      ..rowId = cellContext.rowId
       ..deleteOptionIds.addAll(optionIds);
 
     return DatabaseEventUpdateChecklistCell(payload).send();
@@ -39,9 +39,9 @@ class ChecklistCellBackendService {
     required String optionId,
   }) {
     final payload = ChecklistCellDataChangesetPB.create()
-      ..viewId = cellId.viewId
-      ..fieldId = cellId.fieldInfo.id
-      ..rowId = cellId.rowId
+      ..viewId = cellContext.viewId
+      ..fieldId = cellContext.fieldInfo.id
+      ..rowId = cellContext.rowId
       ..selectedOptionIds.add(optionId);
 
     return DatabaseEventUpdateChecklistCell(payload).send();
@@ -51,9 +51,9 @@ class ChecklistCellBackendService {
     required SelectOptionPB option,
   }) {
     final payload = ChecklistCellDataChangesetPB.create()
-      ..viewId = cellId.viewId
-      ..fieldId = cellId.fieldInfo.id
-      ..rowId = cellId.rowId
+      ..viewId = cellContext.viewId
+      ..fieldId = cellContext.fieldInfo.id
+      ..rowId = cellContext.rowId
       ..updateOptions.add(option);
 
     return DatabaseEventUpdateChecklistCell(payload).send();
@@ -61,10 +61,10 @@ class ChecklistCellBackendService {
 
   Future<Either<ChecklistCellDataPB, FlowyError>> getCellData() {
     final payload = CellIdPB.create()
-      ..fieldId = cellId.fieldInfo.id
-      ..viewId = cellId.viewId
-      ..rowId = cellId.rowId
-      ..rowId = cellId.rowId;
+      ..fieldId = cellContext.fieldInfo.id
+      ..viewId = cellContext.viewId
+      ..rowId = cellContext.rowId
+      ..rowId = cellContext.rowId;
 
     return DatabaseEventGetChecklistCellData(payload).send();
   }

+ 5 - 5
frontend/appflowy_flutter/lib/plugins/database_view/application/cell/select_option_cell_service.dart

@@ -8,12 +8,12 @@ import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
 import 'package:appflowy_backend/protobuf/flowy-database2/cell_entities.pb.dart';
 
 class SelectOptionCellBackendService {
-  final CellIdentifier cellId;
-  SelectOptionCellBackendService({required this.cellId});
+  final DatabaseCellContext cellContext;
+  SelectOptionCellBackendService({required this.cellContext});
 
-  String get viewId => cellId.viewId;
-  String get fieldId => cellId.fieldInfo.id;
-  RowId get rowId => cellId.rowId;
+  String get viewId => cellContext.viewId;
+  String get fieldId => cellContext.fieldInfo.id;
+  RowId get rowId => cellContext.rowId;
 
   Future<Either<Unit, FlowyError>> create({
     required String name,

+ 1 - 1
frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_action_sheet_bloc.dart

@@ -10,7 +10,7 @@ class FieldActionSheetBloc
     extends Bloc<FieldActionSheetEvent, FieldActionSheetState> {
   final FieldBackendService fieldService;
 
-  FieldActionSheetBloc({required FieldCellContext fieldCellContext})
+  FieldActionSheetBloc({required FieldContext fieldCellContext})
       : fieldService = FieldBackendService(
           viewId: fieldCellContext.viewId,
           fieldId: fieldCellContext.field.id,

+ 2 - 3
frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_cell_bloc.dart

@@ -14,7 +14,7 @@ class FieldCellBloc extends Bloc<FieldCellEvent, FieldCellState> {
   final FieldBackendService _fieldBackendSvc;
 
   FieldCellBloc({
-    required FieldCellContext cellContext,
+    required FieldContext cellContext,
   })  : _fieldListener = SingleFieldListener(fieldId: cellContext.field.id),
         _fieldBackendSvc = FieldBackendService(
           viewId: cellContext.viewId,
@@ -83,8 +83,7 @@ class FieldCellState with _$FieldCellState {
     required double width,
   }) = _FieldCellState;
 
-  factory FieldCellState.initial(FieldCellContext cellContext) =>
-      FieldCellState(
+  factory FieldCellState.initial(FieldContext cellContext) => FieldCellState(
         viewId: cellContext.viewId,
         field: cellContext.field,
         width: cellContext.field.width.toDouble(),

+ 15 - 7
frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_editor_bloc.dart

@@ -12,12 +12,20 @@ class FieldEditorBloc extends Bloc<FieldEditorEvent, FieldEditorState> {
   final TypeOptionController dataController;
 
   FieldEditorBloc({
-    required String viewId,
-    required String fieldName,
     required bool isGroupField,
-    required ITypeOptionLoader loader,
-  })  : dataController = TypeOptionController(viewId: viewId, loader: loader),
-        super(FieldEditorState.initial(viewId, fieldName, isGroupField)) {
+    required FieldPB field,
+    required FieldTypeOptionLoader loader,
+  })  : dataController = TypeOptionController(
+          field: field,
+          loader: loader,
+        ),
+        super(
+          FieldEditorState.initial(
+            loader.viewId,
+            loader.field.name,
+            isGroupField,
+          ),
+        ) {
     on<FieldEditorEvent>(
       (event, emit) async {
         await event.when(
@@ -27,7 +35,7 @@ class FieldEditorBloc extends Bloc<FieldEditorEvent, FieldEditorState> {
                 add(FieldEditorEvent.didReceiveFieldChanged(field));
               }
             });
-            await dataController.loadTypeOptionData();
+            await dataController.reloadTypeOption();
             add(FieldEditorEvent.didReceiveFieldChanged(dataController.field));
           },
           updateName: (name) {
@@ -50,7 +58,7 @@ class FieldEditorBloc extends Bloc<FieldEditorEvent, FieldEditorState> {
               () => null,
               (field) {
                 final fieldService = FieldBackendService(
-                  viewId: viewId,
+                  viewId: loader.viewId,
                   fieldId: field.id,
                 );
                 fieldService.deleteField();

+ 2 - 2
frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_service.dart

@@ -107,8 +107,8 @@ class FieldBackendService {
 }
 
 @freezed
-class FieldCellContext with _$FieldCellContext {
-  const factory FieldCellContext({
+class FieldContext with _$FieldContext {
+  const factory FieldContext({
     required String viewId,
     required FieldPB field,
   }) = _FieldCellContext;

+ 5 - 51
frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/type_option_context.dart

@@ -113,7 +113,7 @@ class TypeOptionContext<T extends GeneratedMessage> {
     required TypeOptionController dataController,
   }) : _dataController = dataController;
 
-  String get viewId => _dataController.viewId;
+  String get viewId => _dataController.loader.viewId;
 
   String get fieldId => _dataController.field.id;
 
@@ -121,7 +121,7 @@ class TypeOptionContext<T extends GeneratedMessage> {
     void Function(T)? onCompleted,
     required void Function(FlowyError) onError,
   }) async {
-    await _dataController.loadTypeOptionData().then((result) {
+    await _dataController.reloadTypeOption().then((result) {
       result.fold((l) => null, (err) => onError(err));
     });
 
@@ -153,54 +153,12 @@ abstract class TypeOptionFieldDelegate {
 abstract class ITypeOptionLoader {
   String get viewId;
   String get fieldName;
-  Future<Either<TypeOptionPB, FlowyError>> initialize();
-}
-
-/// Uses when creating a new field
-class NewFieldTypeOptionLoader extends ITypeOptionLoader {
-  TypeOptionPB? fieldTypeOption;
-
-  @override
-  final String viewId;
-  NewFieldTypeOptionLoader({
-    required this.viewId,
-  });
-
-  /// Creates the field type option if the fieldTypeOption is null.
-  /// Otherwise, it loads the type option data from the backend.
-  @override
-  Future<Either<TypeOptionPB, FlowyError>> initialize() {
-    if (fieldTypeOption != null) {
-      final payload = TypeOptionPathPB.create()
-        ..viewId = viewId
-        ..fieldId = fieldTypeOption!.field_2.id
-        ..fieldType = fieldTypeOption!.field_2.fieldType;
-
-      return DatabaseEventGetTypeOption(payload).send();
-    } else {
-      final payload = CreateFieldPayloadPB.create()
-        ..viewId = viewId
-        ..fieldType = FieldType.RichText;
-
-      return DatabaseEventCreateTypeOption(payload).send().then((result) {
-        return result.fold(
-          (newFieldTypeOption) {
-            fieldTypeOption = newFieldTypeOption;
-            return left(newFieldTypeOption);
-          },
-          (err) => right(err),
-        );
-      });
-    }
-  }
 
-  @override
-  String get fieldName => fieldTypeOption?.field_2.name ?? '';
+  Future<Either<TypeOptionPB, FlowyError>> initialize();
 }
 
 /// Uses when editing a existing field
-class FieldTypeOptionLoader extends ITypeOptionLoader {
-  @override
+class FieldTypeOptionLoader {
   final String viewId;
   final FieldPB field;
 
@@ -209,8 +167,7 @@ class FieldTypeOptionLoader extends ITypeOptionLoader {
     required this.field,
   });
 
-  @override
-  Future<Either<TypeOptionPB, FlowyError>> initialize() {
+  Future<Either<TypeOptionPB, FlowyError>> load() {
     final payload = TypeOptionPathPB.create()
       ..viewId = viewId
       ..fieldId = field.id
@@ -218,7 +175,4 @@ class FieldTypeOptionLoader extends ITypeOptionLoader {
 
     return DatabaseEventGetTypeOption(payload).send();
   }
-
-  @override
-  String get fieldName => field.name;
 }

+ 12 - 16
frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/type_option_data_controller.dart

@@ -11,31 +11,27 @@ import '../field_service.dart';
 import 'type_option_context.dart';
 
 class TypeOptionController {
-  final String viewId;
   late TypeOptionPB _typeOption;
-  final ITypeOptionLoader loader;
+  final FieldTypeOptionLoader loader;
   final PublishNotifier<FieldPB> _fieldNotifier = PublishNotifier();
 
   /// Returns a [TypeOptionController] used to modify the specified
   /// [FieldPB]'s data
   ///
-  /// Should call [loadTypeOptionData] if the passed-in [FieldInfo]
+  /// Should call [reloadTypeOption] if the passed-in [FieldInfo]
   /// is null
   ///
   TypeOptionController({
-    required this.viewId,
     required this.loader,
-    FieldInfo? fieldInfo,
+    required FieldPB field,
   }) {
-    if (fieldInfo != null) {
-      _typeOption = TypeOptionPB.create()
-        ..viewId = viewId
-        ..field_2 = fieldInfo.field;
-    }
+    _typeOption = TypeOptionPB.create()
+      ..viewId = loader.viewId
+      ..field_2 = field;
   }
 
-  Future<Either<TypeOptionPB, FlowyError>> loadTypeOptionData() async {
-    final result = await loader.initialize();
+  Future<Either<TypeOptionPB, FlowyError>> reloadTypeOption() async {
+    final result = await loader.load();
     return result.fold(
       (data) {
         data.freeze();
@@ -67,7 +63,7 @@ class TypeOptionController {
 
     _fieldNotifier.value = _typeOption.field_2;
 
-    FieldBackendService(viewId: viewId, fieldId: field.id)
+    FieldBackendService(viewId: loader.viewId, fieldId: field.id)
         .updateField(name: name);
   }
 
@@ -79,7 +75,7 @@ class TypeOptionController {
     });
 
     FieldBackendService.updateFieldTypeOption(
-      viewId: viewId,
+      viewId: loader.viewId,
       fieldId: field.id,
       typeOptionData: typeOptionData,
     );
@@ -87,7 +83,7 @@ class TypeOptionController {
 
   Future<void> switchToField(FieldType newFieldType) async {
     final payload = UpdateFieldTypePayloadPB.create()
-      ..viewId = viewId
+      ..viewId = loader.viewId
       ..fieldId = field.id
       ..fieldType = newFieldType;
 
@@ -97,7 +93,7 @@ class TypeOptionController {
         // Should load the type-option data after switching to a new field.
         // After loading the type-option data, the editor widget that uses
         // the type-option data will be rebuild.
-        loadTypeOptionData();
+        reloadTypeOption();
       },
       (err) => Future(() => Log.error(err)),
     );

+ 12 - 2
frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/type_option_service.dart

@@ -1,8 +1,7 @@
-import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart';
+import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
 import 'package:dartz/dartz.dart';
 import 'package:appflowy_backend/dispatch/dispatch.dart';
 import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
-import 'package:appflowy_backend/protobuf/flowy-database2/cell_entities.pb.dart';
 
 class TypeOptionBackendService {
   final String viewId;
@@ -23,4 +22,15 @@ class TypeOptionBackendService {
 
     return DatabaseEventCreateSelectOption(payload).send();
   }
+
+  static Future<Either<TypeOptionPB, FlowyError>> createFieldTypeOption({
+    required String viewId,
+    FieldType fieldType = FieldType.RichText,
+  }) {
+    final payload = CreateFieldPayloadPB.create()
+      ..viewId = viewId
+      ..fieldType = FieldType.RichText;
+
+    return DatabaseEventCreateTypeOption(payload).send();
+  }
 }

+ 6 - 6
frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_cache.dart

@@ -185,7 +185,7 @@ class RowCache {
 
   RowUpdateCallback addListener({
     required RowId rowId,
-    void Function(CellByFieldId, RowsChangedReason)? onCellUpdated,
+    void Function(CellContextByFieldId, RowsChangedReason)? onCellUpdated,
     bool Function()? listenWhen,
   }) {
     listenerHandler() async {
@@ -197,7 +197,7 @@ class RowCache {
         if (onCellUpdated != null) {
           final rowInfo = _rowList.get(rowId);
           if (rowInfo != null) {
-            final CellByFieldId cellDataMap =
+            final CellContextByFieldId cellDataMap =
                 _makeGridCells(rowId, rowInfo.rowPB);
             onCellUpdated(cellDataMap, _rowChangeReasonNotifier.reason);
           }
@@ -220,7 +220,7 @@ class RowCache {
     _rowChangeReasonNotifier.removeListener(callback);
   }
 
-  CellByFieldId loadGridCells(RowId rowId) {
+  CellContextByFieldId loadGridCells(RowId rowId) {
     final RowPB? data = _rowList.get(rowId)?.rowPB;
     if (data == null) {
       _loadRow(rowId);
@@ -240,12 +240,12 @@ class RowCache {
     );
   }
 
-  CellByFieldId _makeGridCells(RowId rowId, RowPB? row) {
+  CellContextByFieldId _makeGridCells(RowId rowId, RowPB? row) {
     // ignore: prefer_collection_literals
-    var cellDataMap = CellByFieldId();
+    var cellDataMap = CellContextByFieldId();
     for (final field in _delegate.fields) {
       if (field.visibility) {
-        cellDataMap[field.id] = CellIdentifier(
+        cellDataMap[field.id] = DatabaseCellContext(
           rowId: rowId,
           viewId: viewId,
           fieldInfo: field,

+ 2 - 2
frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_data_controller.dart

@@ -3,7 +3,7 @@ import '../cell/cell_service.dart';
 import 'row_cache.dart';
 import 'row_service.dart';
 
-typedef OnRowChanged = void Function(CellByFieldId, RowsChangedReason);
+typedef OnRowChanged = void Function(CellContextByFieldId, RowsChangedReason);
 
 class RowController {
   final RowId rowId;
@@ -21,7 +21,7 @@ class RowController {
     this.groupId,
   }) : _rowCache = rowCache;
 
-  CellByFieldId loadData() {
+  CellContextByFieldId loadData() {
     return _rowCache.loadGridCells(rowId);
   }
 

+ 6 - 3
frontend/appflowy_flutter/lib/plugins/database_view/grid/application/row/row_bloc.dart

@@ -70,7 +70,7 @@ class RowEvent with _$RowEvent {
   const factory RowEvent.initial() = _InitialRow;
   const factory RowEvent.createRow() = _CreateRow;
   const factory RowEvent.didReceiveCells(
-    CellByFieldId cellsByFieldId,
+    CellContextByFieldId cellsByFieldId,
     RowsChangedReason reason,
   ) = _DidReceiveCells;
 }
@@ -79,12 +79,15 @@ class RowEvent with _$RowEvent {
 class RowState with _$RowState {
   const factory RowState({
     required RowInfo rowInfo,
-    required CellByFieldId cellByFieldId,
+    required CellContextByFieldId cellByFieldId,
     required UnmodifiableListView<GridCellEquatable> cells,
     RowsChangedReason? changeReason,
   }) = _RowState;
 
-  factory RowState.initial(RowInfo rowInfo, CellByFieldId cellByFieldId) =>
+  factory RowState.initial(
+    RowInfo rowInfo,
+    CellContextByFieldId cellByFieldId,
+  ) =>
       RowState(
         rowInfo: rowInfo,
         cellByFieldId: cellByFieldId,

+ 2 - 2
frontend/appflowy_flutter/lib/plugins/database_view/grid/application/row/row_detail_bloc.dart

@@ -88,14 +88,14 @@ class RowDetailEvent with _$RowDetailEvent {
   const factory RowDetailEvent.duplicateRow(String rowId, String? groupId) =
       _DuplicateRow;
   const factory RowDetailEvent.didReceiveCellDatas(
-    List<CellIdentifier> gridCells,
+    List<DatabaseCellContext> gridCells,
   ) = _DidReceiveCellDatas;
 }
 
 @freezed
 class RowDetailState with _$RowDetailState {
   const factory RowDetailState({
-    required List<CellIdentifier> gridCells,
+    required List<DatabaseCellContext> gridCells,
   }) = _RowDetailState;
 
   factory RowDetailState.initial() => RowDetailState(

+ 1 - 1
frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_cell.dart

@@ -14,7 +14,7 @@ import 'field_cell_action_sheet.dart';
 import 'field_type_extension.dart';
 
 class GridFieldCell extends StatefulWidget {
-  final FieldCellContext cellContext;
+  final FieldContext cellContext;
   const GridFieldCell({
     Key? key,
     required this.cellContext,

+ 5 - 5
frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_cell_action_sheet.dart

@@ -19,7 +19,7 @@ import '../../layout/sizes.dart';
 import 'field_editor.dart';
 
 class GridFieldCellActionSheet extends StatefulWidget {
-  final FieldCellContext cellContext;
+  final FieldContext cellContext;
   const GridFieldCellActionSheet({required this.cellContext, Key? key})
       : super(key: key);
 
@@ -69,7 +69,7 @@ class _GridFieldCellActionSheetState extends State<GridFieldCellActionSheet> {
 }
 
 class _EditFieldButton extends StatelessWidget {
-  final FieldCellContext cellContext;
+  final FieldContext cellContext;
   final void Function()? onTap;
   const _EditFieldButton({required this.cellContext, Key? key, this.onTap})
       : super(key: key);
@@ -95,7 +95,7 @@ class _EditFieldButton extends StatelessWidget {
 }
 
 class _FieldOperationList extends StatelessWidget {
-  final FieldCellContext fieldInfo;
+  final FieldContext fieldInfo;
   const _FieldOperationList(this.fieldInfo, {Key? key}) : super(key: key);
 
   @override
@@ -138,7 +138,7 @@ class _FieldOperationList extends StatelessWidget {
 }
 
 class FieldActionCell extends StatelessWidget {
-  final FieldCellContext fieldInfo;
+  final FieldContext fieldInfo;
   final FieldAction action;
   final bool enable;
 
@@ -203,7 +203,7 @@ extension _FieldActionExtension on FieldAction {
     }
   }
 
-  void run(BuildContext context, FieldCellContext fieldInfo) {
+  void run(BuildContext context, FieldContext fieldInfo) {
     switch (this) {
       case FieldAction.hide:
         context

+ 13 - 10
frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_editor.dart

@@ -19,7 +19,7 @@ class FieldEditor extends StatefulWidget {
   final bool isGroupingField;
   final Function(String)? onDeleted;
   final Function(String)? onHidden;
-  final ITypeOptionLoader typeOptionLoader;
+  final FieldTypeOptionLoader typeOptionLoader;
 
   const FieldEditor({
     required this.viewId,
@@ -53,22 +53,25 @@ class _FieldEditorState extends State<FieldEditor> {
   Widget build(BuildContext context) {
     List<Widget> children = [
       _FieldNameTextField(popoverMutex: popoverMutex),
-      const VSpace(10),
       if (widget.onDeleted != null) _addDeleteFieldButton(),
       if (widget.onHidden != null) _addHideFieldButton(),
-      _FieldTypeOptionCell(popoverMutex: popoverMutex),
+      if (!widget.typeOptionLoader.field.isPrimary)
+        _FieldTypeOptionCell(popoverMutex: popoverMutex),
     ];
     return BlocProvider(
-      create: (context) => FieldEditorBloc(
-        viewId: widget.viewId,
-        fieldName: widget.typeOptionLoader.fieldName,
-        isGroupField: widget.isGroupingField,
-        loader: widget.typeOptionLoader,
-      )..add(const FieldEditorEvent.initial()),
-      child: ListView.builder(
+      create: (context) {
+        return FieldEditorBloc(
+          isGroupField: widget.isGroupingField,
+          loader: widget.typeOptionLoader,
+          field: widget.typeOptionLoader.field,
+        )..add(const FieldEditorEvent.initial());
+      },
+      child: ListView.separated(
         shrinkWrap: true,
         itemCount: children.length,
         itemBuilder: (context, index) => children[index],
+        separatorBuilder: (context, index) =>
+            VSpace(GridSize.typeOptionSeparatorHeight),
         padding: const EdgeInsets.symmetric(vertical: 12.0),
       ),
     );

+ 34 - 6
frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/grid_header.dart

@@ -4,6 +4,7 @@ import 'package:appflowy/plugins/database_view/application/field/field_service.d
 import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart';
 import 'package:appflowy/plugins/database_view/grid/application/grid_header_bloc.dart';
 import 'package:appflowy/startup/startup.dart';
+import 'package:appflowy_backend/log.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:appflowy_popover/appflowy_popover.dart';
 import 'package:flowy_infra/theme_extension.dart';
@@ -13,6 +14,7 @@ import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:reorderables/reorderables.dart';
+import '../../../../application/field/type_option/type_option_service.dart';
 import '../../layout/sizes.dart';
 import 'field_editor.dart';
 import 'field_cell.dart';
@@ -101,8 +103,10 @@ class _GridHeaderState extends State<_GridHeader> {
         final cells = state.fields
             .where((field) => field.visibility)
             .map(
-              (field) =>
-                  FieldCellContext(viewId: widget.viewId, field: field.field),
+              (field) => FieldContext(
+                viewId: widget.viewId,
+                field: field.field,
+              ),
             )
             .map(
               (ctx) => GridFieldCell(
@@ -177,28 +181,52 @@ class _CellTrailing extends StatelessWidget {
   }
 }
 
-class CreateFieldButton extends StatelessWidget {
+class CreateFieldButton extends StatefulWidget {
   final String viewId;
   const CreateFieldButton({required this.viewId, Key? key}) : super(key: key);
 
+  @override
+  State<CreateFieldButton> createState() => _CreateFieldButtonState();
+}
+
+class _CreateFieldButtonState extends State<CreateFieldButton> {
+  final popoverController = PopoverController();
+  late TypeOptionPB typeOption;
+
   @override
   Widget build(BuildContext context) {
     return AppFlowyPopover(
+      controller: popoverController,
       direction: PopoverDirection.bottomWithRightAligned,
       asBarrier: true,
       margin: EdgeInsets.zero,
       constraints: BoxConstraints.loose(const Size(240, 600)),
+      triggerActions: PopoverTriggerFlags.none,
       child: FlowyButton(
         radius: BorderRadius.zero,
         text: FlowyText.medium(LocaleKeys.grid_field_newProperty.tr()),
         hoverColor: AFThemeExtension.of(context).greyHover,
-        onTap: () {},
+        onTap: () async {
+          final result = await TypeOptionBackendService.createFieldTypeOption(
+            viewId: widget.viewId,
+          );
+          result.fold(
+            (l) {
+              typeOption = l;
+              popoverController.show();
+            },
+            (r) => Log.error("Failed to create field type option: $r"),
+          );
+        },
         leftIcon: const FlowySvg(name: 'home/add'),
       ),
       popupBuilder: (BuildContext popover) {
         return FieldEditor(
-          viewId: viewId,
-          typeOptionLoader: NewFieldTypeOptionLoader(viewId: viewId),
+          viewId: widget.viewId,
+          typeOptionLoader: FieldTypeOptionLoader(
+            viewId: widget.viewId,
+            field: typeOption.field_2,
+          ),
         );
       },
     );

+ 3 - 4
frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/type_option/builder.dart

@@ -60,7 +60,7 @@ TypeOptionWidgetBuilder makeTypeOptionWidgetBuilder({
   required TypeOptionController dataController,
   required PopoverMutex popoverMutex,
 }) {
-  final viewId = dataController.viewId;
+  final viewId = dataController.loader.viewId;
   final fieldType = dataController.field.fieldType;
 
   switch (dataController.field.fieldType) {
@@ -146,9 +146,8 @@ TypeOptionContext<T> makeTypeOptionContext<T extends GeneratedMessage>({
 }) {
   final loader = FieldTypeOptionLoader(viewId: viewId, field: fieldInfo.field);
   final dataController = TypeOptionController(
-    viewId: viewId,
     loader: loader,
-    fieldInfo: fieldInfo,
+    field: fieldInfo.field,
   );
   return makeTypeOptionContextWithDataController(
     viewId: viewId,
@@ -180,8 +179,8 @@ TypeOptionContext<T> makeSelectTypeOptionContext<T extends GeneratedMessage>({
     field: fieldPB,
   );
   final dataController = TypeOptionController(
-    viewId: viewId,
     loader: loader,
+    field: fieldPB,
   );
   final typeOptionContext = makeTypeOptionContextWithDataController<T>(
     viewId: viewId,

+ 4 - 1
frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/row/row.dart

@@ -255,7 +255,10 @@ class RowContent extends StatelessWidget {
     );
   }
 
-  List<Widget> _makeCells(BuildContext context, CellByFieldId cellByFieldId) {
+  List<Widget> _makeCells(
+    BuildContext context,
+    CellContextByFieldId cellByFieldId,
+  ) {
     return cellByFieldId.values.map(
       (cellId) {
         final GridCellWidget child = builder.build(cellId);

+ 6 - 6
frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card.dart

@@ -194,7 +194,7 @@ class _RowCardState<T> extends State<RowCard<T>> {
 class _CardContent<CustomCardData> extends StatelessWidget {
   final CardCellBuilder<CustomCardData> cellBuilder;
   final EditableRowNotifier rowNotifier;
-  final List<CellIdentifier> cells;
+  final List<DatabaseCellContext> cells;
   final RowCardRenderHook<CustomCardData>? renderHook;
   final CustomCardData? cardData;
   final RowCardStyleConfiguration styleConfiguration;
@@ -233,28 +233,28 @@ class _CardContent<CustomCardData> extends StatelessWidget {
 
   List<Widget> _makeCells(
     BuildContext context,
-    List<CellIdentifier> cells,
+    List<DatabaseCellContext> cells,
   ) {
     final List<Widget> children = [];
     // Remove all the cell listeners.
     rowNotifier.unbind();
 
     cells.asMap().forEach(
-      (int index, CellIdentifier cell) {
+      (int index, DatabaseCellContext cellContext) {
         final isEditing = index == 0 ? rowNotifier.isEditing.value : false;
         final cellNotifier = EditableCardNotifier(isEditing: isEditing);
 
         if (index == 0) {
           // Only use the first cell to receive user's input when click the edit
           // button
-          rowNotifier.bindCell(cell, cellNotifier);
+          rowNotifier.bindCell(cellContext, cellNotifier);
         }
 
         final child = Padding(
-          key: cell.key(),
+          key: cellContext.key(),
           padding: styleConfiguration.cellPadding,
           child: cellBuilder.buildCell(
-            cellId: cell,
+            cellContext: cellContext,
             cellNotifier: cellNotifier,
             renderHook: renderHook,
             cardData: cardData,

+ 6 - 6
frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card_bloc.dart

@@ -87,11 +87,11 @@ class CardBloc extends Bloc<RowCardEvent, RowCardState> {
   }
 }
 
-List<CellIdentifier> _makeCells(
+List<DatabaseCellContext> _makeCells(
   String? groupFieldId,
-  CellByFieldId originalCellMap,
+  CellContextByFieldId originalCellMap,
 ) {
-  List<CellIdentifier> cells = [];
+  List<DatabaseCellContext> cells = [];
   for (final entry in originalCellMap.entries) {
     // Filter out the cell if it's fieldId equal to the groupFieldId
     if (groupFieldId != null) {
@@ -110,7 +110,7 @@ class RowCardEvent with _$RowCardEvent {
   const factory RowCardEvent.initial() = _InitialRow;
   const factory RowCardEvent.setIsEditing(bool isEditing) = _IsEditing;
   const factory RowCardEvent.didReceiveCells(
-    List<CellIdentifier> cells,
+    List<DatabaseCellContext> cells,
     RowsChangedReason reason,
   ) = _DidReceiveCells;
 }
@@ -119,14 +119,14 @@ class RowCardEvent with _$RowCardEvent {
 class RowCardState with _$RowCardState {
   const factory RowCardState({
     required RowPB rowPB,
-    required List<CellIdentifier> cells,
+    required List<DatabaseCellContext> cells,
     required bool isEditing,
     RowsChangedReason? changeReason,
   }) = _RowCardState;
 
   factory RowCardState.initial(
     RowPB rowPB,
-    List<CellIdentifier> cells,
+    List<DatabaseCellContext> cells,
     bool isEditing,
   ) =>
       RowCardState(

+ 5 - 5
frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card_cell_builder.dart

@@ -21,18 +21,18 @@ class CardCellBuilder<CustomCardData> {
 
   Widget buildCell({
     CustomCardData? cardData,
-    required CellIdentifier cellId,
+    required DatabaseCellContext cellContext,
     EditableCardNotifier? cellNotifier,
     RowCardRenderHook<CustomCardData>? renderHook,
   }) {
     final cellControllerBuilder = CellControllerBuilder(
-      cellId: cellId,
+      cellContext: cellContext,
       cellCache: cellCache,
     );
 
-    final key = cellId.key();
-    final style = styles?[cellId.fieldType];
-    switch (cellId.fieldType) {
+    final key = cellContext.key();
+    final style = styles?[cellContext.fieldType];
+    switch (cellContext.fieldType) {
       case FieldType.Checkbox:
         return CheckboxCardCell(
           cellControllerBuilder: cellControllerBuilder,

+ 3 - 2
frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/card_cell.dart

@@ -106,7 +106,7 @@ class EditableRowNotifier {
       : isEditing = ValueNotifier(isEditing);
 
   void bindCell(
-    CellIdentifier cellIdentifier,
+    DatabaseCellContext cellIdentifier,
     EditableCardNotifier notifier,
   ) {
     assert(
@@ -171,7 +171,8 @@ class EditableCellId {
 
   EditableCellId(this.rowId, this.fieldId);
 
-  factory EditableCellId.from(CellIdentifier cellIdentifier) => EditableCellId(
+  factory EditableCellId.from(DatabaseCellContext cellIdentifier) =>
+      EditableCellId(
         cellIdentifier.rowId,
         cellIdentifier.fieldId,
       );

+ 7 - 4
frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cell_builder.dart

@@ -20,14 +20,17 @@ class GridCellBuilder {
     required this.cellCache,
   });
 
-  GridCellWidget build(CellIdentifier cellId, {GridCellStyle? style}) {
+  GridCellWidget build(
+    DatabaseCellContext cellContext, {
+    GridCellStyle? style,
+  }) {
     final cellControllerBuilder = CellControllerBuilder(
-      cellId: cellId,
+      cellContext: cellContext,
       cellCache: cellCache,
     );
 
-    final key = cellId.key();
-    switch (cellId.fieldType) {
+    final key = cellContext.key();
+    switch (cellContext.fieldType) {
       case FieldType.Checkbox:
         return GridCheckboxCell(
           cellControllerBuilder: cellControllerBuilder,

+ 3 - 2
frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell_bloc.dart

@@ -15,8 +15,9 @@ class ChecklistCardCellBloc
   void Function()? _onCellChangedFn;
   ChecklistCardCellBloc({
     required this.cellController,
-  })  : _checklistCellSvc =
-            ChecklistCellBackendService(cellId: cellController.cellId),
+  })  : _checklistCellSvc = ChecklistCellBackendService(
+          cellContext: cellController.cellContext,
+        ),
         super(ChecklistCellState.initial(cellController)) {
     on<ChecklistCellEvent>(
       (event, emit) async {

+ 3 - 2
frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell_editor_bloc.dart

@@ -18,8 +18,9 @@ class ChecklistCellEditorBloc
 
   ChecklistCellEditorBloc({
     required this.cellController,
-  })  : _checklistCellService =
-            ChecklistCellBackendService(cellId: cellController.cellId),
+  })  : _checklistCellService = ChecklistCellBackendService(
+          cellContext: cellController.cellContext,
+        ),
         super(ChecklistCellEditorState.initial(cellController)) {
     on<ChecklistCellEditorEvent>(
       (event, emit) async {

+ 3 - 2
frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/select_option_cell/select_option_editor_bloc.dart

@@ -16,8 +16,9 @@ class SelectOptionCellEditorBloc
 
   SelectOptionCellEditorBloc({
     required this.cellController,
-  })  : _selectOptionService =
-            SelectOptionCellBackendService(cellId: cellController.cellId),
+  })  : _selectOptionService = SelectOptionCellBackendService(
+          cellContext: cellController.cellContext,
+        ),
         super(SelectOptionEditorState.initial(cellController)) {
     on<SelectOptionEditorEvent>(
       (event, emit) async {

+ 35 - 17
frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_detail.dart

@@ -1,8 +1,10 @@
 import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
 import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart';
+import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_service.dart';
 import 'package:appflowy/plugins/database_view/application/row/row_data_controller.dart';
 import 'package:appflowy/plugins/database_view/grid/application/row/row_detail_bloc.dart';
 import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
+import 'package:appflowy_backend/log.dart';
 import 'package:collection/collection.dart';
 import 'package:flowy_infra/theme_extension.dart';
 import 'package:flowy_infra/image.dart';
@@ -134,7 +136,7 @@ class _PropertyColumn extends StatelessWidget {
           crossAxisAlignment: CrossAxisAlignment.start,
           children: [
             _RowTitle(
-              cellId: state.gridCells
+              cellContext: state.gridCells
                   .firstWhereOrNull((e) => e.fieldInfo.isPrimary),
               cellBuilder: cellBuilder,
             ),
@@ -145,7 +147,7 @@ class _PropertyColumn extends StatelessWidget {
                   (cell) => Padding(
                     padding: const EdgeInsets.only(bottom: 4.0),
                     child: _PropertyCell(
-                      cellId: cell,
+                      cellContext: cell,
                       cellBuilder: cellBuilder,
                     ),
                   ),
@@ -161,14 +163,14 @@ class _PropertyColumn extends StatelessWidget {
 }
 
 class _RowTitle extends StatelessWidget {
-  final CellIdentifier? cellId;
+  final DatabaseCellContext? cellContext;
   final GridCellBuilder cellBuilder;
-  const _RowTitle({this.cellId, required this.cellBuilder, Key? key})
+  const _RowTitle({this.cellContext, required this.cellBuilder, Key? key})
       : super(key: key);
 
   @override
   Widget build(BuildContext context) {
-    if (cellId == null) {
+    if (cellContext == null) {
       return const SizedBox();
     }
     final style = GridTextCellStyle(
@@ -176,7 +178,7 @@ class _RowTitle extends StatelessWidget {
       textStyle: Theme.of(context).textTheme.titleLarge,
       autofocus: true,
     );
-    return cellBuilder.build(cellId!, style: style);
+    return cellBuilder.build(cellContext!, style: style);
   }
 }
 
@@ -194,6 +196,7 @@ class _CreatePropertyButton extends StatefulWidget {
 
 class _CreatePropertyButtonState extends State<_CreatePropertyButton> {
   late PopoverController popoverController;
+  late TypeOptionPB typeOption;
 
   @override
   void initState() {
@@ -207,6 +210,7 @@ class _CreatePropertyButtonState extends State<_CreatePropertyButton> {
       constraints: BoxConstraints.loose(const Size(240, 200)),
       controller: popoverController,
       direction: PopoverDirection.topWithLeftAligned,
+      triggerActions: PopoverTriggerFlags.none,
       margin: EdgeInsets.zero,
       child: SizedBox(
         height: 40,
@@ -216,7 +220,18 @@ class _CreatePropertyButtonState extends State<_CreatePropertyButton> {
             color: AFThemeExtension.of(context).textColor,
           ),
           hoverColor: AFThemeExtension.of(context).lightGreyHover,
-          onTap: () {},
+          onTap: () async {
+            final result = await TypeOptionBackendService.createFieldTypeOption(
+              viewId: widget.viewId,
+            );
+            result.fold(
+              (l) {
+                typeOption = l;
+                popoverController.show();
+              },
+              (r) => Log.error("Failed to create field type option: $r"),
+            );
+          },
           leftIcon: svgWidget(
             "home/add",
             color: AFThemeExtension.of(context).textColor,
@@ -226,7 +241,10 @@ class _CreatePropertyButtonState extends State<_CreatePropertyButton> {
       popupBuilder: (BuildContext popOverContext) {
         return FieldEditor(
           viewId: widget.viewId,
-          typeOptionLoader: NewFieldTypeOptionLoader(viewId: widget.viewId),
+          typeOptionLoader: FieldTypeOptionLoader(
+            viewId: widget.viewId,
+            field: typeOption.field_2,
+          ),
           onDeleted: (fieldId) {
             popoverController.close();
             NavigatorAlertDialog(
@@ -245,10 +263,10 @@ class _CreatePropertyButtonState extends State<_CreatePropertyButton> {
 }
 
 class _PropertyCell extends StatefulWidget {
-  final CellIdentifier cellId;
+  final DatabaseCellContext cellContext;
   final GridCellBuilder cellBuilder;
   const _PropertyCell({
-    required this.cellId,
+    required this.cellContext,
     required this.cellBuilder,
     Key? key,
   }) : super(key: key);
@@ -262,8 +280,8 @@ class _PropertyCellState extends State<_PropertyCell> {
 
   @override
   Widget build(BuildContext context) {
-    final style = _customCellStyle(widget.cellId.fieldType);
-    final cell = widget.cellBuilder.build(widget.cellId, style: style);
+    final style = _customCellStyle(widget.cellContext.fieldType);
+    final cell = widget.cellBuilder.build(widget.cellContext, style: style);
 
     final gesture = GestureDetector(
       behavior: HitTestBehavior.translucent,
@@ -290,7 +308,7 @@ class _PropertyCellState extends State<_PropertyCell> {
               child: SizedBox(
                 width: 150,
                 child: FieldCellButton(
-                  field: widget.cellId.fieldInfo.field,
+                  field: widget.cellContext.fieldInfo.field,
                   onTap: () => popover.show(),
                   radius: BorderRadius.circular(6),
                 ),
@@ -306,11 +324,11 @@ class _PropertyCellState extends State<_PropertyCell> {
 
   Widget buildFieldEditor() {
     return FieldEditor(
-      viewId: widget.cellId.viewId,
-      isGroupingField: widget.cellId.fieldInfo.isGroupField,
+      viewId: widget.cellContext.viewId,
+      isGroupingField: widget.cellContext.fieldInfo.isGroupField,
       typeOptionLoader: FieldTypeOptionLoader(
-        viewId: widget.cellId.viewId,
-        field: widget.cellId.fieldInfo.field,
+        viewId: widget.cellContext.viewId,
+        field: widget.cellContext.fieldInfo.field,
       ),
       onHidden: (fieldId) {
         popover.close();

+ 1 - 1
frontend/appflowy_flutter/lib/startup/deps_resolver.dart

@@ -171,7 +171,7 @@ void _resolveGridDeps(GetIt getIt) {
     ),
   );
 
-  getIt.registerFactoryParam<FieldActionSheetBloc, FieldCellContext, void>(
+  getIt.registerFactoryParam<FieldActionSheetBloc, FieldContext, void>(
     (data, _) => FieldActionSheetBloc(fieldCellContext: data),
   );
 

+ 1 - 2
frontend/appflowy_flutter/test/bloc_test/board_test/create_or_edit_field_test.dart

@@ -35,10 +35,9 @@ void main() {
     );
 
     final editorBloc = FieldEditorBloc(
-      viewId: context.gridView.id,
-      fieldName: fieldInfo.name,
       isGroupField: fieldInfo.isGroupField,
       loader: loader,
+      field: fieldInfo.field,
     )..add(const FieldEditorEvent.initial());
     await boardResponseFuture();
 

+ 1 - 1
frontend/appflowy_flutter/test/bloc_test/board_test/group_by_unsupport_field_test.dart

@@ -15,7 +15,7 @@ void main() {
     boardTest = await AppFlowyBoardTest.ensureInitialized();
     context = await boardTest.createTestBoard();
     final fieldInfo = context.singleSelectFieldContext();
-    editorBloc = context.createFieldEditor(
+    editorBloc = context.makeFieldEditor(
       fieldInfo: fieldInfo,
     )..add(const FieldEditorEvent.initial());
 

+ 12 - 16
frontend/appflowy_flutter/test/bloc_test/board_test/util.dart

@@ -76,22 +76,18 @@ class BoardTestContext {
     return _boardDataController.fieldController;
   }
 
-  FieldEditorBloc createFieldEditor({
-    FieldInfo? fieldInfo,
+  FieldEditorBloc makeFieldEditor({
+    required FieldInfo fieldInfo,
   }) {
-    ITypeOptionLoader loader;
-    if (fieldInfo == null) {
-      loader = NewFieldTypeOptionLoader(viewId: gridView.id);
-    } else {
-      loader =
-          FieldTypeOptionLoader(viewId: gridView.id, field: fieldInfo.field);
-    }
+    final loader = FieldTypeOptionLoader(
+      viewId: gridView.id,
+      field: fieldInfo.field,
+    );
 
     final editorBloc = FieldEditorBloc(
-      fieldName: fieldInfo?.name ?? '',
-      isGroupField: fieldInfo?.isGroupField ?? false,
+      isGroupField: fieldInfo.isGroupField,
       loader: loader,
-      viewId: gridView.id,
+      field: fieldInfo.field,
     );
     return editorBloc;
   }
@@ -120,13 +116,13 @@ class BoardTestContext {
     await gridResponseFuture();
 
     return CellControllerBuilder(
-      cellId: rowBloc.state.cellByFieldId[fieldId]!,
+      cellContext: rowBloc.state.cellByFieldId[fieldId]!,
       cellCache: rowCache.cellCache,
     );
   }
 
   Future<FieldEditorBloc> createField(FieldType fieldType) async {
-    final editorBloc = createFieldEditor()
+    final editorBloc = await createFieldEditor(viewId: gridView.id)
       ..add(const FieldEditorEvent.initial());
     await gridResponseFuture();
     editorBloc.add(FieldEditorEvent.switchToField(fieldType));
@@ -140,9 +136,9 @@ class BoardTestContext {
     return fieldInfo;
   }
 
-  FieldCellContext singleSelectFieldCellContext() {
+  FieldContext singleSelectFieldCellContext() {
     final field = singleSelectFieldContext().field;
-    return FieldCellContext(viewId: gridView.id, field: field);
+    return FieldContext(viewId: gridView.id, field: field);
   }
 
   FieldInfo textFieldContext() {

+ 2 - 4
frontend/appflowy_flutter/test/bloc_test/grid_test/field/edit_field_test.dart

@@ -13,10 +13,9 @@ Future<FieldEditorBloc> createEditorBloc(AppFlowyGridTest gridTest) async {
   );
 
   return FieldEditorBloc(
-    viewId: context.gridView.id,
-    fieldName: fieldInfo.name,
     isGroupField: fieldInfo.isGroupField,
     loader: loader,
+    field: fieldInfo.field,
   )..add(const FieldEditorEvent.initial());
 }
 
@@ -83,10 +82,9 @@ Future<FieldEditorBloc> makeEditorBloc(AppFlowyGridTest gridTest) async {
   );
 
   final editorBloc = FieldEditorBloc(
-    viewId: context.gridView.id,
-    fieldName: fieldInfo.name,
     isGroupField: fieldInfo.isGroupField,
     loader: loader,
+    field: fieldInfo.field,
   )..add(const FieldEditorEvent.initial());
 
   await gridResponseFuture();

+ 1 - 2
frontend/appflowy_flutter/test/bloc_test/grid_test/filter/edit_filter_field_test.dart

@@ -41,10 +41,9 @@ void main() {
     );
 
     final editorBloc = FieldEditorBloc(
-      viewId: context.gridView.id,
-      fieldName: textField.field.name,
       isGroupField: false,
       loader: loader,
+      field: textField.field,
     )..add(const FieldEditorEvent.initial());
     await gridResponseFuture();
 

+ 27 - 24
frontend/appflowy_flutter/test/bloc_test/grid_test/util.dart

@@ -4,6 +4,7 @@ import 'package:appflowy/plugins/database_view/application/field/field_controlle
 import 'package:appflowy/plugins/database_view/application/field/field_editor_bloc.dart';
 import 'package:appflowy/plugins/database_view/application/field/field_service.dart';
 import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart';
+import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_service.dart';
 import 'package:appflowy/plugins/database_view/application/row/row_cache.dart';
 import 'package:appflowy/plugins/database_view/application/row/row_data_controller.dart';
 import 'package:appflowy/plugins/database_view/application/database_controller.dart';
@@ -38,26 +39,6 @@ class GridTestContext {
     return gridController.createRow();
   }
 
-  FieldEditorBloc createFieldEditor({
-    FieldInfo? fieldInfo,
-  }) {
-    ITypeOptionLoader loader;
-    if (fieldInfo == null) {
-      loader = NewFieldTypeOptionLoader(viewId: gridView.id);
-    } else {
-      loader =
-          FieldTypeOptionLoader(viewId: gridView.id, field: fieldInfo.field);
-    }
-
-    final editorBloc = FieldEditorBloc(
-      fieldName: fieldInfo?.name ?? '',
-      isGroupField: fieldInfo?.isGroupField ?? false,
-      loader: loader,
-      viewId: gridView.id,
-    );
-    return editorBloc;
-  }
-
   Future<CellController> makeCellController(
     String fieldId,
     int rowIndex,
@@ -86,13 +67,13 @@ class GridTestContext {
     await gridResponseFuture();
 
     return CellControllerBuilder(
-      cellId: rowBloc.state.cellByFieldId[fieldId]!,
+      cellContext: rowBloc.state.cellByFieldId[fieldId]!,
       cellCache: rowCache.cellCache,
     );
   }
 
   Future<FieldEditorBloc> createField(FieldType fieldType) async {
-    final editorBloc = createFieldEditor()
+    final editorBloc = await createFieldEditor(viewId: gridView.id)
       ..add(const FieldEditorEvent.initial());
     await gridResponseFuture();
     editorBloc.add(FieldEditorEvent.switchToField(fieldType));
@@ -106,9 +87,9 @@ class GridTestContext {
     return fieldInfo;
   }
 
-  FieldCellContext singleSelectFieldCellContext() {
+  FieldContext singleSelectFieldCellContext() {
     final field = singleSelectFieldContext().field;
-    return FieldCellContext(viewId: gridView.id, field: field);
+    return FieldContext(viewId: gridView.id, field: field);
   }
 
   FieldInfo textFieldContext() {
@@ -155,6 +136,28 @@ class GridTestContext {
   }
 }
 
+Future<FieldEditorBloc> createFieldEditor({
+  required String viewId,
+}) async {
+  final result = await TypeOptionBackendService.createFieldTypeOption(
+    viewId: viewId,
+  );
+  return result.fold(
+    (data) {
+      final loader = FieldTypeOptionLoader(
+        viewId: viewId,
+        field: data.field_2,
+      );
+      return FieldEditorBloc(
+        isGroupField: FieldInfo(field: data.field_2).isGroupField,
+        loader: loader,
+        field: data.field_2,
+      );
+    },
+    (err) => throw Exception(err),
+  );
+}
+
 /// Create a empty Grid for test
 class AppFlowyGridTest {
   final AppFlowyUnitTest unitTest;

+ 22 - 6
frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs

@@ -196,16 +196,27 @@ impl DatabaseEditor {
   }
 
   pub async fn update_field(&self, params: FieldChangesetParams) -> FlowyResult<()> {
+    let is_primary = self
+      .database
+      .lock()
+      .fields
+      .get_field(&params.field_id)
+      .map(|field| field.is_primary)
+      .unwrap_or(false);
     self
       .database
       .lock()
       .fields
-      .update_field(&params.field_id, |update| {
-        update
+      .update_field(&params.field_id, |mut update| {
+        update = update
           .set_name_if_not_none(params.name)
-          .set_field_type_if_not_none(params.field_type.map(|field_type| field_type.into()))
           .set_width_at_if_not_none(params.width.map(|value| value as i64))
           .set_visibility_if_not_none(params.visibility);
+        if is_primary {
+          tracing::warn!("Cannot update primary field type");
+        } else {
+          update.set_field_type_if_not_none(params.field_type.map(|field_type| field_type.into()));
+        }
       });
     self
       .notify_did_update_database_field(&params.field_id)
@@ -238,10 +249,15 @@ impl DatabaseEditor {
       .lock()
       .fields
       .update_field(field_id, |update| {
-        update.update_type_options(|type_options_update| {
-          type_options_update.insert(&field_type.to_string(), type_option_data);
-        });
+        if old_field.is_primary {
+          tracing::warn!("Cannot update primary field type");
+        } else {
+          update.update_type_options(|type_options_update| {
+            type_options_update.insert(&field_type.to_string(), type_option_data);
+          });
+        }
       });
+
     self
       .database_views
       .did_update_field_type_option(view_id, field_id, &old_field)

+ 1 - 1
frontend/rust-lib/flowy-database2/tests/database/block_test/row_test.rs

@@ -43,7 +43,7 @@ async fn update_at_field_test() {
     .unwrap();
   let old_updated_at = DateCellData::from(&cell).timestamp.unwrap();
 
-  tokio::time::sleep(Duration::from_millis(500)).await;
+  tokio::time::sleep(Duration::from_millis(1000)).await;
   test
     .run_script(UpdateTextCell {
       row_id: row.id.clone(),