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

Merge remote-tracking branch 'origin/feat/appflowy_tauri_3' into feat/appflowy_tauri_3

# Conflicts:
#	frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/NavigationPanel.tsx
ascarbek 2 роки тому
батько
коміт
fb8a81eca5
100 змінених файлів з 1384 додано та 704 видалено
  1. 0 1
      frontend/.vscode/settings.json
  2. BIN
      frontend/appflowy_flutter/assets/images/Local Disk (C) - Shortcut.lnk
  3. BIN
      frontend/appflowy_flutter/assets/images/app_flowy_abstract_cover_1.jpg
  4. BIN
      frontend/appflowy_flutter/assets/images/app_flowy_abstract_cover_2.jpg
  5. 10 2
      frontend/appflowy_flutter/assets/translations/en.json
  6. 57 218
      frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_controller.dart
  7. 108 0
      frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_controller_builder.dart
  8. 0 63
      frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_field_notifier.dart
  9. 0 6
      frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_service.dart
  10. 2 2
      frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_editor_bloc.dart
  11. 4 4
      frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_type_option_edit_bloc.dart
  12. 2 2
      frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/type_option_context.dart
  13. 11 11
      frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/type_option_data_controller.dart
  14. 4 18
      frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_data_controller.dart
  15. 10 10
      frontend/appflowy_flutter/lib/plugins/database_view/board/application/board_bloc.dart
  16. 6 6
      frontend/appflowy_flutter/lib/plugins/database_view/board/application/board_data_controller.dart
  17. 1 2
      frontend/appflowy_flutter/lib/plugins/database_view/board/application/card/board_checkbox_cell_bloc.dart
  18. 1 1
      frontend/appflowy_flutter/lib/plugins/database_view/board/application/card/board_date_cell_bloc.dart
  19. 2 1
      frontend/appflowy_flutter/lib/plugins/database_view/board/application/card/board_number_cell_bloc.dart
  20. 1 1
      frontend/appflowy_flutter/lib/plugins/database_view/board/application/card/board_select_option_cell_bloc.dart
  21. 1 1
      frontend/appflowy_flutter/lib/plugins/database_view/board/application/card/board_text_cell_bloc.dart
  22. 1 2
      frontend/appflowy_flutter/lib/plugins/database_view/board/application/card/board_url_cell_bloc.dart
  23. 1 12
      frontend/appflowy_flutter/lib/plugins/database_view/board/application/card/card_data_controller.dart
  24. 1 3
      frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/board_page.dart
  25. 1 1
      frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/card/board_checkbox_cell.dart
  26. 1 1
      frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/card/board_checklist_cell.dart
  27. 1 1
      frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/card/board_date_cell.dart
  28. 1 1
      frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/card/board_number_cell.dart
  29. 1 2
      frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/card/board_select_option_cell.dart
  30. 1 1
      frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/card/board_text_cell.dart
  31. 1 1
      frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/card/board_url_cell.dart
  32. 2 2
      frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/card/card_cell_builder.dart
  33. 1 0
      frontend/appflowy_flutter/lib/plugins/database_view/grid/application/cell/checkbox_cell_bloc.dart
  34. 5 5
      frontend/appflowy_flutter/lib/plugins/database_view/grid/application/cell/checklist_cell_bloc.dart
  35. 4 5
      frontend/appflowy_flutter/lib/plugins/database_view/grid/application/cell/checklist_cell_editor_bloc.dart
  36. 1 0
      frontend/appflowy_flutter/lib/plugins/database_view/grid/application/cell/date_cal_bloc.dart
  37. 1 1
      frontend/appflowy_flutter/lib/plugins/database_view/grid/application/cell/date_cell_bloc.dart
  38. 8 17
      frontend/appflowy_flutter/lib/plugins/database_view/grid/application/cell/number_cell_bloc.dart
  39. 1 1
      frontend/appflowy_flutter/lib/plugins/database_view/grid/application/cell/select_option_cell_bloc.dart
  40. 4 4
      frontend/appflowy_flutter/lib/plugins/database_view/grid/application/cell/select_option_editor_bloc.dart
  41. 3 3
      frontend/appflowy_flutter/lib/plugins/database_view/grid/application/cell/select_option_service.dart
  42. 1 1
      frontend/appflowy_flutter/lib/plugins/database_view/grid/application/cell/text_cell_bloc.dart
  43. 1 1
      frontend/appflowy_flutter/lib/plugins/database_view/grid/application/cell/url_cell_bloc.dart
  44. 1 1
      frontend/appflowy_flutter/lib/plugins/database_view/grid/application/cell/url_cell_editor_bloc.dart
  45. 0 3
      frontend/appflowy_flutter/lib/plugins/database_view/grid/application/filter/checklist_filter_bloc.dart
  46. 7 17
      frontend/appflowy_flutter/lib/plugins/database_view/grid/application/grid_bloc.dart
  47. 8 10
      frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/grid_page.dart
  48. 4 8
      frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/cell/cell_builder.dart
  49. 1 1
      frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/cell/checkbox_cell.dart
  50. 1 1
      frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/cell/checklist_cell/checklist_cell.dart
  51. 1 1
      frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/cell/checklist_cell/checklist_cell_editor.dart
  52. 1 1
      frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/cell/date_cell/date_cell.dart
  53. 1 1
      frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/cell/date_cell/date_editor.dart
  54. 1 1
      frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/cell/number_cell.dart
  55. 1 1
      frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/cell/select_option_cell/select_option_cell.dart
  56. 1 1
      frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/cell/select_option_cell/select_option_editor.dart
  57. 1 1
      frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/cell/text_cell.dart
  58. 1 2
      frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/cell/url_cell/cell_editor.dart
  59. 1 1
      frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/cell/url_cell/url_cell.dart
  60. 2 2
      frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_type_option_editor.dart
  61. 5 5
      frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/type_option/builder.dart
  62. 1 1
      frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/toolbar/setting_button.dart
  63. 25 3
      frontend/appflowy_flutter/lib/plugins/document/document_page.dart
  64. 1 1
      frontend/appflowy_flutter/lib/plugins/document/editor_styles.dart
  65. 376 0
      frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/cover/change_cover_popover.dart
  66. 302 0
      frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/cover/cover_node_widget.dart
  67. 4 2
      frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/service/openai_client.dart
  68. 2 2
      frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/smart_edit_action.dart
  69. 1 0
      frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/smart_edit_node_widget.dart
  70. 41 34
      frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/smart_edit_toolbar_item.dart
  71. 1 0
      frontend/appflowy_flutter/lib/startup/deps_resolver.dart
  72. 16 2
      frontend/appflowy_flutter/lib/workspace/presentation/home/menu/app/header/add_button.dart
  73. 68 54
      frontend/appflowy_flutter/packages/appflowy_backend/windows/appflowy_backend_plugin.cpp
  74. 5 4
      frontend/appflowy_flutter/packages/appflowy_backend/windows/appflowy_backend_plugin_c_api.cpp
  75. 9 3
      frontend/appflowy_flutter/packages/appflowy_editor/lib/src/commands/command_extension.dart
  76. 4 1
      frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/editor_service.dart
  77. 2 0
      frontend/appflowy_flutter/packages/appflowy_editor_plugins/lib/appflowy_editor_plugins.dart
  78. 1 1
      frontend/appflowy_flutter/test/bloc_test/board_test/group_by_multi_select_field_test.dart
  79. 2 4
      frontend/appflowy_flutter/test/bloc_test/board_test/util.dart
  80. 6 6
      frontend/appflowy_flutter/test/bloc_test/grid_test/filter/create_filter_test.dart
  81. 2 2
      frontend/appflowy_flutter/test/bloc_test/grid_test/grid_bloc_test.dart
  82. 2 4
      frontend/appflowy_flutter/test/bloc_test/grid_test/util.dart
  83. 0 2
      frontend/appflowy_tauri/src/appflowy_app/App.tsx
  84. BIN
      frontend/appflowy_tauri/src/appflowy_app/assets/launch_splash.jpg
  85. 1 2
      frontend/appflowy_tauri/src/appflowy_app/components/TestApiButton/TestAPI.tsx
  86. 4 2
      frontend/appflowy_tauri/src/appflowy_app/components/TestApiButton/TestGrid.tsx
  87. 50 10
      frontend/appflowy_tauri/src/appflowy_app/components/auth/ProtectedRoutes.tsx
  88. 35 9
      frontend/appflowy_tauri/src/appflowy_app/components/auth/auth.hooks.ts
  89. 2 2
      frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/NavigationFloatingPanel.tsx
  90. 61 33
      frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/NavigationPanel.tsx
  91. 1 1
      frontend/appflowy_tauri/src/appflowy_app/components/layout/WorkspaceUser.tsx
  92. 10 7
      frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/cell_controller.ts
  93. 5 0
      frontend/appflowy_tauri/src/appflowy_app/stores/effects/user/user_bd_svc.ts
  94. 4 5
      frontend/appflowy_tauri/src/appflowy_app/stores/reducers/current-user/slice.ts
  95. 0 15
      frontend/appflowy_tauri/src/appflowy_app/views/Welcome.tsx
  96. 1 1
      frontend/rust-lib/flowy-codegen/src/protobuf_file/mod.rs
  97. 1 1
      frontend/rust-lib/flowy-core/src/lib.rs
  98. 2 2
      frontend/rust-lib/flowy-database/src/event_handler.rs
  99. 33 16
      frontend/rust-lib/flowy-database/src/manager.rs
  100. 3 0
      frontend/rust-lib/flowy-database/src/services/database/database_editor.rs

+ 0 - 1
frontend/.vscode/settings.json

@@ -24,7 +24,6 @@
     "svgviewer.showzoominout": true,
     "editor.wordWrapColumn": 80,
     "editor.minimap.maxColumn": 140,
-    "prettier.printWidth": 140,
     "editor.wordWrap": "wordWrapColumn",
     "dart.lineLength": 80,
     "typescript.validate.enable": true,

BIN
frontend/appflowy_flutter/assets/images/Local Disk (C) - Shortcut.lnk


BIN
frontend/appflowy_flutter/assets/images/app_flowy_abstract_cover_1.jpg


BIN
frontend/appflowy_flutter/assets/images/app_flowy_abstract_cover_2.jpg


+ 10 - 2
frontend/appflowy_flutter/assets/translations/en.json

@@ -353,7 +353,15 @@
       "smartEditFixSpelling": "Fix spelling",
       "smartEditSummarize": "Summarize",
       "smartEditCouldNotFetchResult": "Could not fetch result from OpenAI",
-      "smartEditCouldNotFetchKey": "Could not fetch OpenAI key"
+      "smartEditCouldNotFetchKey": "Could not fetch OpenAI key",
+      "cover": {
+        "changeCover": "Change Cover",
+        "colors": "Colors",
+        "images": "Images",
+        "abstract": "Abstract",
+        "addCover": "Add Cover",
+        "addLocalImage": "Add local image"
+      }
     }
   },
   "board": {
@@ -371,4 +379,4 @@
       "nextMonth": "Next Month"
     }
   }
-}
+}

+ 57 - 218
frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_controller.dart

@@ -1,118 +1,16 @@
-part of 'cell_service.dart';
-
-typedef TextCellController = CellController<String, String>;
-typedef CheckboxCellController = CellController<String, String>;
-typedef NumberCellController = CellController<String, String>;
-typedef SelectOptionCellController
-    = CellController<SelectOptionCellDataPB, String>;
-typedef ChecklistCellController
-    = CellController<SelectOptionCellDataPB, String>;
-typedef DateCellController = CellController<DateCellDataPB, CalendarData>;
-typedef URLCellController = CellController<URLCellDataPB, String>;
-
-abstract class CellControllerBuilderDelegate {
-  CellFieldNotifier buildFieldNotifier();
-}
-
-class CellControllerBuilder {
-  final CellIdentifier _cellId;
-  final CellCache _cellCache;
-  final CellControllerBuilderDelegate delegate;
-
-  CellControllerBuilder({
-    required this.delegate,
-    required CellIdentifier cellId,
-    required CellCache cellCache,
-  })  : _cellCache = cellCache,
-        _cellId = cellId;
-
-  CellController build() {
-    final cellFieldNotifier = delegate.buildFieldNotifier();
-    switch (_cellId.fieldType) {
-      case FieldType.Checkbox:
-        final cellDataLoader = CellDataLoader(
-          cellId: _cellId,
-          parser: StringCellDataParser(),
-        );
-        return TextCellController(
-          cellId: _cellId,
-          cellCache: _cellCache,
-          cellDataLoader: cellDataLoader,
-          fieldNotifier: cellFieldNotifier,
-          cellDataPersistence: TextCellDataPersistence(cellId: _cellId),
-        );
-      case FieldType.DateTime:
-        final cellDataLoader = CellDataLoader(
-          cellId: _cellId,
-          parser: DateCellDataParser(),
-          reloadOnFieldChanged: true,
-        );
-
-        return DateCellController(
-          cellId: _cellId,
-          cellCache: _cellCache,
-          cellDataLoader: cellDataLoader,
-          fieldNotifier: cellFieldNotifier,
-          cellDataPersistence: DateCellDataPersistence(cellId: _cellId),
-        );
-      case FieldType.Number:
-        final cellDataLoader = CellDataLoader(
-          cellId: _cellId,
-          parser: StringCellDataParser(),
-          reloadOnFieldChanged: true,
-        );
-        return NumberCellController(
-          cellId: _cellId,
-          cellCache: _cellCache,
-          cellDataLoader: cellDataLoader,
-          fieldNotifier: cellFieldNotifier,
-          cellDataPersistence: TextCellDataPersistence(cellId: _cellId),
-        );
-      case FieldType.RichText:
-        final cellDataLoader = CellDataLoader(
-          cellId: _cellId,
-          parser: StringCellDataParser(),
-        );
-        return TextCellController(
-          cellId: _cellId,
-          cellCache: _cellCache,
-          cellDataLoader: cellDataLoader,
-          fieldNotifier: cellFieldNotifier,
-          cellDataPersistence: TextCellDataPersistence(cellId: _cellId),
-        );
-      case FieldType.MultiSelect:
-      case FieldType.SingleSelect:
-      case FieldType.Checklist:
-        final cellDataLoader = CellDataLoader(
-          cellId: _cellId,
-          parser: SelectOptionCellDataParser(),
-          reloadOnFieldChanged: true,
-        );
-
-        return SelectOptionCellController(
-          cellId: _cellId,
-          cellCache: _cellCache,
-          cellDataLoader: cellDataLoader,
-          fieldNotifier: cellFieldNotifier,
-          cellDataPersistence: TextCellDataPersistence(cellId: _cellId),
-        );
-
-      case FieldType.URL:
-        final cellDataLoader = CellDataLoader(
-          cellId: _cellId,
-          parser: URLCellDataParser(),
-        );
-        return URLCellController(
-          cellId: _cellId,
-          cellCache: _cellCache,
-          cellDataLoader: cellDataLoader,
-          fieldNotifier: cellFieldNotifier,
-          cellDataPersistence: TextCellDataPersistence(cellId: _cellId),
-        );
-    }
-    throw UnimplementedError;
-  }
-}
+import 'dart:async';
+import 'package:appflowy/plugins/database_view/application/field/field_listener.dart';
+import 'package:appflowy_backend/log.dart';
+import 'package:appflowy_backend/protobuf/flowy-database/field_entities.pbenum.dart';
+import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
+import 'package:dartz/dartz.dart';
+import 'package:equatable/equatable.dart';
+import 'package:flutter/foundation.dart';
+import '../field/field_controller.dart';
+import '../field/field_service.dart';
+import '../field/type_option/type_option_context.dart';
+import 'cell_listener.dart';
+import 'cell_service.dart';
 
 /// IGridCellController is used to manipulate the cell and receive notifications.
 /// * Read/Write cell data
@@ -124,32 +22,39 @@ class CellControllerBuilder {
 // ignore: must_be_immutable
 class CellController<T, D> extends Equatable {
   final CellIdentifier cellId;
-  final CellCache _cellsCache;
+  final CellCache _cellCache;
   final CellCacheKey _cacheKey;
   final FieldBackendService _fieldBackendSvc;
-  final CellFieldNotifier _fieldNotifier;
+  final SingleFieldListener _fieldListener;
   final CellDataLoader<T> _cellDataLoader;
   final CellDataPersistence<D> _cellDataPersistence;
 
   CellListener? _cellListener;
   CellDataNotifier<T?>? _cellDataNotifier;
 
-  bool isListening = false;
-  VoidCallback? _onFieldChangedFn;
+  VoidCallback? _onCellFieldChanged;
   Timer? _loadDataOperation;
   Timer? _saveDataOperation;
-  bool _isDispose = false;
+
+  String get viewId => cellId.viewId;
+
+  String get rowId => cellId.rowId;
+
+  String get fieldId => cellId.fieldInfo.id;
+
+  FieldInfo get fieldInfo => cellId.fieldInfo;
+
+  FieldType get fieldType => cellId.fieldInfo.fieldType;
 
   CellController({
     required this.cellId,
     required CellCache cellCache,
-    required CellFieldNotifier fieldNotifier,
     required CellDataLoader<T> cellDataLoader,
     required CellDataPersistence<D> cellDataPersistence,
-  })  : _cellsCache = cellCache,
+  })  : _cellCache = cellCache,
         _cellDataLoader = cellDataLoader,
         _cellDataPersistence = cellDataPersistence,
-        _fieldNotifier = fieldNotifier,
+        _fieldListener = SingleFieldListener(fieldId: cellId.fieldId),
         _fieldBackendSvc = FieldBackendService(
           viewId: cellId.viewId,
           fieldId: cellId.fieldInfo.id,
@@ -157,46 +62,12 @@ class CellController<T, D> extends Equatable {
         _cacheKey = CellCacheKey(
           rowId: cellId.rowId,
           fieldId: cellId.fieldInfo.id,
-        );
-
-  String get viewId => cellId.viewId;
-
-  String get rowId => cellId.rowId;
-
-  String get fieldId => cellId.fieldInfo.id;
-
-  FieldInfo get fieldInfo => cellId.fieldInfo;
-
-  FieldType get fieldType => cellId.fieldInfo.fieldType;
-
-  /// Listen on the cell content or field changes
-  ///
-  /// An optional [listenWhenOnCellChanged] can be implemented for more
-  ///  granular control over when [listener] is called.
-  /// [listenWhenOnCellChanged] will be invoked on each [onCellChanged]
-  /// get called.
-  /// [listenWhenOnCellChanged] takes the previous `value` and current
-  /// `value` and must return a [bool] which determines whether or not
-  ///  the [onCellChanged] function will be invoked.
-  /// [onCellChanged] is optional and if omitted, it will default to `true`.
-  ///
-  VoidCallback? startListening({
-    required void Function(T?) onCellChanged,
-    bool Function(T? oldValue, T? newValue)? listenWhenOnCellChanged,
-    VoidCallback? onCellFieldChanged,
-  }) {
-    if (isListening) {
-      Log.error("Already started. It seems like you should call clone first");
-      return null;
-    }
-    isListening = true;
-
-    _cellDataNotifier = CellDataNotifier(
-      value: _cellsCache.get(_cacheKey),
-      listenWhen: listenWhenOnCellChanged,
+        ) {
+    _cellDataNotifier = CellDataNotifier(value: _cellCache.get(_cacheKey));
+    _cellListener = CellListener(
+      rowId: cellId.rowId,
+      fieldId: cellId.fieldInfo.id,
     );
-    _cellListener =
-        CellListener(rowId: cellId.rowId, fieldId: cellId.fieldInfo.id);
 
     /// 1.Listen on user edit event and load the new cell data if needed.
     /// For example:
@@ -205,7 +76,7 @@ class CellController<T, D> extends Equatable {
     _cellListener?.start(onCellChanged: (result) {
       result.fold(
         (_) {
-          _cellsCache.remove(_cacheKey);
+          _cellCache.remove(_cacheKey);
           _loadData();
         },
         (err) => Log.error(err),
@@ -213,20 +84,25 @@ class CellController<T, D> extends Equatable {
     });
 
     /// 2.Listen on the field event and load the cell data if needed.
-    _onFieldChangedFn = () {
-      if (onCellFieldChanged != null) {
-        onCellFieldChanged();
-      }
-
-      /// reloadOnFieldChanged should be true if you need to load the data when the corresponding field is changed
-      /// For example:
-      ///   ¥12 -> $12
-      if (_cellDataLoader.reloadOnFieldChanged) {
-        _loadData();
-      }
-    };
+    _fieldListener.start(onFieldChanged: (result) {
+      result.fold((fieldPB) {
+        /// reloadOnFieldChanged should be true if you need to load the data when the corresponding field is changed
+        /// For example:
+        ///   ¥12 -> $12
+        if (_cellDataLoader.reloadOnFieldChanged) {
+          _loadData();
+        }
+        _onCellFieldChanged?.call();
+      }, (err) => Log.error(err));
+    });
+  }
 
-    _fieldNotifier.register(_cacheKey, _onFieldChangedFn!);
+  /// Listen on the cell content or field changes
+  VoidCallback? startListening({
+    required void Function(T?) onCellChanged,
+    VoidCallback? onCellFieldChanged,
+  }) {
+    _onCellFieldChanged = onCellFieldChanged;
 
     /// Notify the listener, the cell data was changed.
     onCellChangedFn() => onCellChanged(_cellDataNotifier?.value);
@@ -244,7 +120,7 @@ class CellController<T, D> extends Equatable {
   /// The cell data will be read from the Cache first, and load from disk if it does not exist.
   /// You can set [loadIfNotExist] to false (default is true) to disable loading the cell data.
   T? getCellData({bool loadIfNotExist = true}) {
-    final data = _cellsCache.get(_cacheKey);
+    final data = _cellCache.get(_cacheKey);
     if (data == null && loadIfNotExist) {
       _loadData();
     }
@@ -294,9 +170,9 @@ class CellController<T, D> extends Equatable {
     _loadDataOperation = Timer(const Duration(milliseconds: 10), () {
       _cellDataLoader.loadData().then((data) {
         if (data != null) {
-          _cellsCache.insert(_cacheKey, GridBaseCell(object: data));
+          _cellCache.insert(_cacheKey, GridBaseCell(object: data));
         } else {
-          _cellsCache.remove(_cacheKey);
+          _cellCache.remove(_cacheKey);
         }
 
         _cellDataNotifier?.value = data;
@@ -305,54 +181,17 @@ class CellController<T, D> extends Equatable {
   }
 
   Future<void> dispose() async {
-    if (_isDispose) {
-      Log.error("$this should only dispose once");
-      return;
-    }
-    _isDispose = true;
     await _cellListener?.stop();
     _loadDataOperation?.cancel();
     _saveDataOperation?.cancel();
     _cellDataNotifier?.dispose();
+    await _fieldListener.stop();
     _cellDataNotifier = null;
-
-    if (_onFieldChangedFn != null) {
-      _fieldNotifier.unregister(_cacheKey, _onFieldChangedFn!);
-      await _fieldNotifier.dispose();
-      _onFieldChangedFn = null;
-    }
   }
 
   @override
   List<Object> get props =>
-      [_cellsCache.get(_cacheKey) ?? "", cellId.rowId + cellId.fieldInfo.id];
-}
-
-class GridCellFieldNotifierImpl extends ICellFieldNotifier {
-  final FieldController _fieldController;
-  OnReceiveUpdateFields? _onChangesetFn;
-
-  GridCellFieldNotifierImpl(FieldController cache) : _fieldController = cache;
-
-  @override
-  void onCellDispose() {
-    if (_onChangesetFn != null) {
-      _fieldController.removeListener(onChangesetListener: _onChangesetFn!);
-      _onChangesetFn = null;
-    }
-  }
-
-  @override
-  void onCellFieldChanged(void Function(FieldInfo) callback) {
-    _onChangesetFn = (List<FieldInfo> filedInfos) {
-      for (final field in filedInfos) {
-        callback(field);
-      }
-    };
-    _fieldController.addListener(
-      onReceiveFields: _onChangesetFn,
-    );
-  }
+      [_cellCache.get(_cacheKey) ?? "", cellId.rowId + cellId.fieldInfo.id];
 }
 
 class CellDataNotifier<T> extends ChangeNotifier {

+ 108 - 0
frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_controller_builder.dart

@@ -0,0 +1,108 @@
+import 'package:appflowy_backend/protobuf/flowy-database/date_type_option_entities.pb.dart';
+import 'package:appflowy_backend/protobuf/flowy-database/field_entities.pbenum.dart';
+import 'package:appflowy_backend/protobuf/flowy-database/select_type_option.pb.dart';
+import 'package:appflowy_backend/protobuf/flowy-database/url_type_option_entities.pb.dart';
+
+import 'cell_controller.dart';
+import 'cell_service.dart';
+
+typedef TextCellController = CellController<String, String>;
+typedef CheckboxCellController = CellController<String, String>;
+typedef NumberCellController = CellController<String, String>;
+typedef SelectOptionCellController
+    = CellController<SelectOptionCellDataPB, String>;
+typedef ChecklistCellController
+    = CellController<SelectOptionCellDataPB, String>;
+typedef DateCellController = CellController<DateCellDataPB, CalendarData>;
+typedef URLCellController = CellController<URLCellDataPB, String>;
+
+class CellControllerBuilder {
+  final CellIdentifier _cellId;
+  final CellCache _cellCache;
+
+  CellControllerBuilder({
+    required CellIdentifier cellId,
+    required CellCache cellCache,
+  })  : _cellCache = cellCache,
+        _cellId = cellId;
+
+  CellController build() {
+    switch (_cellId.fieldType) {
+      case FieldType.Checkbox:
+        final cellDataLoader = CellDataLoader(
+          cellId: _cellId,
+          parser: StringCellDataParser(),
+        );
+        return TextCellController(
+          cellId: _cellId,
+          cellCache: _cellCache,
+          cellDataLoader: cellDataLoader,
+          cellDataPersistence: TextCellDataPersistence(cellId: _cellId),
+        );
+      case FieldType.DateTime:
+        final cellDataLoader = CellDataLoader(
+          cellId: _cellId,
+          parser: DateCellDataParser(),
+          reloadOnFieldChanged: true,
+        );
+
+        return DateCellController(
+          cellId: _cellId,
+          cellCache: _cellCache,
+          cellDataLoader: cellDataLoader,
+          cellDataPersistence: DateCellDataPersistence(cellId: _cellId),
+        );
+      case FieldType.Number:
+        final cellDataLoader = CellDataLoader(
+          cellId: _cellId,
+          parser: StringCellDataParser(),
+          reloadOnFieldChanged: true,
+        );
+        return NumberCellController(
+          cellId: _cellId,
+          cellCache: _cellCache,
+          cellDataLoader: cellDataLoader,
+          cellDataPersistence: TextCellDataPersistence(cellId: _cellId),
+        );
+      case FieldType.RichText:
+        final cellDataLoader = CellDataLoader(
+          cellId: _cellId,
+          parser: StringCellDataParser(),
+        );
+        return TextCellController(
+          cellId: _cellId,
+          cellCache: _cellCache,
+          cellDataLoader: cellDataLoader,
+          cellDataPersistence: TextCellDataPersistence(cellId: _cellId),
+        );
+      case FieldType.MultiSelect:
+      case FieldType.SingleSelect:
+      case FieldType.Checklist:
+        final cellDataLoader = CellDataLoader(
+          cellId: _cellId,
+          parser: SelectOptionCellDataParser(),
+          reloadOnFieldChanged: true,
+        );
+
+        return SelectOptionCellController(
+          cellId: _cellId,
+          cellCache: _cellCache,
+          cellDataLoader: cellDataLoader,
+          cellDataPersistence: TextCellDataPersistence(cellId: _cellId),
+        );
+
+      case FieldType.URL:
+        final cellDataLoader = CellDataLoader(
+          cellId: _cellId,
+          parser: URLCellDataParser(),
+        );
+        return URLCellController(
+          cellId: _cellId,
+          cellCache: _cellCache,
+          cellDataLoader: cellDataLoader,
+          cellDataPersistence: TextCellDataPersistence(cellId: _cellId),
+        );
+    }
+    throw UnimplementedError;
+  }
+}

+ 0 - 63
frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_field_notifier.dart

@@ -1,63 +0,0 @@
-import 'package:flutter/foundation.dart';
-import '../field/field_controller.dart';
-import 'cell_service.dart';
-
-abstract class ICellFieldNotifier {
-  void onCellFieldChanged(void Function(FieldInfo) callback);
-  void onCellDispose();
-}
-
-/// DatabasePB's cell helper wrapper that enables each cell will get notified when the corresponding field was changed.
-/// You Register an onFieldChanged callback to listen to the cell changes, and unregister if you don't want to listen.
-class CellFieldNotifier {
-  final ICellFieldNotifier notifier;
-
-  /// fieldId: {objectId: callback}
-  final Map<String, Map<String, List<VoidCallback>>> _fieldListenerByFieldId =
-      {};
-
-  CellFieldNotifier({required this.notifier}) {
-    notifier.onCellFieldChanged(
-      (field) {
-        final map = _fieldListenerByFieldId[field.id];
-        if (map != null) {
-          for (final callbacks in map.values) {
-            for (final callback in callbacks) {
-              callback();
-            }
-          }
-        }
-      },
-    );
-  }
-
-  ///
-  void register(CellCacheKey cacheKey, VoidCallback onFieldChanged) {
-    var map = _fieldListenerByFieldId[cacheKey.fieldId];
-    if (map == null) {
-      _fieldListenerByFieldId[cacheKey.fieldId] = {};
-      map = _fieldListenerByFieldId[cacheKey.fieldId];
-      map![cacheKey.rowId] = [onFieldChanged];
-    } else {
-      var objects = map[cacheKey.rowId];
-      if (objects == null) {
-        map[cacheKey.rowId] = [onFieldChanged];
-      } else {
-        objects.add(onFieldChanged);
-      }
-    }
-  }
-
-  void unregister(CellCacheKey cacheKey, VoidCallback fn) {
-    var callbacks = _fieldListenerByFieldId[cacheKey.fieldId]?[cacheKey.rowId];
-    final index = callbacks?.indexWhere((callback) => callback == fn);
-    if (index != null && index != -1) {
-      callbacks?.removeAt(index);
-    }
-  }
-
-  Future<void> dispose() async {
-    notifier.onCellDispose();
-    _fieldListenerByFieldId.clear();
-  }
-}

+ 0 - 6
frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_service.dart

@@ -1,7 +1,6 @@
 import 'dart:async';
 import 'dart:collection';
 import 'package:dartz/dartz.dart';
-import 'package:equatable/equatable.dart';
 import 'package:appflowy_backend/dispatch/dispatch.dart';
 import 'package:appflowy_backend/log.dart';
 import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
@@ -15,13 +14,8 @@ import 'package:freezed_annotation/freezed_annotation.dart';
 import 'dart:convert' show utf8;
 
 import '../field/field_controller.dart';
-import '../field/field_service.dart';
-import '../field/type_option/type_option_context.dart';
-import 'cell_field_notifier.dart';
-import 'cell_listener.dart';
 part 'cell_service.freezed.dart';
 part 'cell_data_loader.dart';
-part 'cell_controller.dart';
 part 'cell_cache.dart';
 part 'cell_data_persistence.dart';
 

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

@@ -10,7 +10,7 @@ import 'type_option/type_option_data_controller.dart';
 part 'field_editor_bloc.freezed.dart';
 
 class FieldEditorBloc extends Bloc<FieldEditorEvent, FieldEditorState> {
-  final TypeOptionDataController dataController;
+  final TypeOptionController dataController;
 
   FieldEditorBloc({
     required String viewId,
@@ -18,7 +18,7 @@ class FieldEditorBloc extends Bloc<FieldEditorEvent, FieldEditorState> {
     required bool isGroupField,
     required IFieldTypeOptionLoader loader,
   })  : dataController =
-            TypeOptionDataController(viewId: viewId, loader: loader),
+            TypeOptionController(viewId: viewId, loader: loader),
         super(FieldEditorState.initial(viewId, fieldName, isGroupField)) {
     on<FieldEditorEvent>(
       (event, emit) async {

+ 4 - 4
frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_type_option_edit_bloc.dart

@@ -8,10 +8,10 @@ part 'field_type_option_edit_bloc.freezed.dart';
 
 class FieldTypeOptionEditBloc
     extends Bloc<FieldTypeOptionEditEvent, FieldTypeOptionEditState> {
-  final TypeOptionDataController _dataController;
+  final TypeOptionController _dataController;
   void Function()? _fieldListenFn;
 
-  FieldTypeOptionEditBloc(TypeOptionDataController dataController)
+  FieldTypeOptionEditBloc(TypeOptionController dataController)
       : _dataController = dataController,
         super(FieldTypeOptionEditState.initial(dataController)) {
     on<FieldTypeOptionEditEvent>(
@@ -58,9 +58,9 @@ class FieldTypeOptionEditState with _$FieldTypeOptionEditState {
   }) = _FieldTypeOptionEditState;
 
   factory FieldTypeOptionEditState.initial(
-    TypeOptionDataController typeOptionDataController,
+    TypeOptionController typeOptionController,
   ) =>
       FieldTypeOptionEditState(
-        field: typeOptionDataController.field,
+        field: typeOptionController.field,
       );
 }

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

@@ -109,11 +109,11 @@ class ChecklistTypeOptionWidgetDataParser
 class TypeOptionContext<T extends GeneratedMessage> {
   T? _typeOptionObject;
   final TypeOptionParser<T> dataParser;
-  final TypeOptionDataController _dataController;
+  final TypeOptionController _dataController;
 
   TypeOptionContext({
     required this.dataParser,
-    required TypeOptionDataController dataController,
+    required TypeOptionController dataController,
   }) : _dataController = dataController;
 
   String get viewId => _dataController.viewId;

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

@@ -9,25 +9,25 @@ import 'package:appflowy_backend/log.dart';
 import '../field_service.dart';
 import 'type_option_context.dart';
 
-class TypeOptionDataController {
+class TypeOptionController {
   final String viewId;
+  late TypeOptionPB _typeOption;
   final IFieldTypeOptionLoader loader;
-  late TypeOptionPB _typeOptiondata;
   final PublishNotifier<FieldPB> _fieldNotifier = PublishNotifier();
 
-  /// Returns a [TypeOptionDataController] used to modify the specified
+  /// Returns a [TypeOptionController] used to modify the specified
   /// [FieldPB]'s data
   ///
   /// Should call [loadTypeOptionData] if the passed-in [FieldInfo]
   /// is null
   ///
-  TypeOptionDataController({
+  TypeOptionController({
     required this.viewId,
     required this.loader,
     FieldInfo? fieldInfo,
   }) {
     if (fieldInfo != null) {
-      _typeOptiondata = TypeOptionPB.create()
+      _typeOption = TypeOptionPB.create()
         ..viewId = viewId
         ..field_2 = fieldInfo.field;
     }
@@ -38,7 +38,7 @@ class TypeOptionDataController {
     return result.fold(
       (data) {
         data.freeze();
-        _typeOptiondata = data;
+        _typeOption = data;
         _fieldNotifier.value = data.field_2;
         return left(data);
       },
@@ -50,28 +50,28 @@ class TypeOptionDataController {
   }
 
   FieldPB get field {
-    return _typeOptiondata.field_2;
+    return _typeOption.field_2;
   }
 
   T getTypeOption<T>(TypeOptionParser<T> parser) {
-    return parser.fromBuffer(_typeOptiondata.typeOptionData);
+    return parser.fromBuffer(_typeOption.typeOptionData);
   }
 
   set fieldName(String name) {
-    _typeOptiondata = _typeOptiondata.rebuild((rebuildData) {
+    _typeOption = _typeOption.rebuild((rebuildData) {
       rebuildData.field_2 = rebuildData.field_2.rebuild((rebuildField) {
         rebuildField.name = name;
       });
     });
 
-    _fieldNotifier.value = _typeOptiondata.field_2;
+    _fieldNotifier.value = _typeOption.field_2;
 
     FieldBackendService(viewId: viewId, fieldId: field.id)
         .updateField(name: name);
   }
 
   set typeOptionData(List<int> typeOptionData) {
-    _typeOptiondata = _typeOptiondata.rebuild((rebuildData) {
+    _typeOption = _typeOption.rebuild((rebuildData) {
       if (typeOptionData.isNotEmpty) {
         rebuildData.typeOptionData = typeOptionData;
       }

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

@@ -1,24 +1,20 @@
 import 'package:flutter/material.dart';
-import '../../grid/presentation/widgets/cell/cell_builder.dart';
-import '../cell/cell_field_notifier.dart';
 import '../cell/cell_service.dart';
-import '../field/field_controller.dart';
 import 'row_cache.dart';
 
 typedef OnRowChanged = void Function(CellByFieldId, RowsChangedReason);
 
-class RowDataController extends GridCellBuilderDelegate {
+class RowDataController {
   final RowInfo rowInfo;
   final List<VoidCallback> _onRowChangedListeners = [];
-  final FieldController _fieldController;
   final RowCache _rowCache;
 
+  get cellCache => _rowCache.cellCache;
+
   RowDataController({
     required this.rowInfo,
-    required FieldController fieldController,
     required RowCache rowCache,
-  })  : _fieldController = fieldController,
-        _rowCache = rowCache;
+  }) : _rowCache = rowCache;
 
   CellByFieldId loadData() {
     return _rowCache.loadGridCells(rowInfo.rowPB.id);
@@ -36,14 +32,4 @@ class RowDataController extends GridCellBuilderDelegate {
       _rowCache.removeRowListener(fn);
     }
   }
-
-  // GridCellBuilderDelegate implementation
-  @override
-  CellFieldNotifier buildFieldNotifier() {
-    return CellFieldNotifier(
-        notifier: GridCellFieldNotifierImpl(_fieldController));
-  }
-
-  @override
-  CellCache get cellCache => _rowCache.cellCache;
 }

+ 10 - 10
frontend/appflowy_flutter/lib/plugins/database_view/board/application/board_bloc.dart

@@ -20,18 +20,18 @@ import 'group_controller.dart';
 part 'board_bloc.freezed.dart';
 
 class BoardBloc extends Bloc<BoardEvent, BoardState> {
-  final BoardDataController _gridDataController;
+  final BoardDataController _boardDataController;
   late final AppFlowyBoardController boardController;
   final GroupBackendService _groupBackendSvc;
   final LinkedHashMap<String, GroupController> groupControllers =
       LinkedHashMap();
 
-  FieldController get fieldController => _gridDataController.fieldController;
-  String get viewId => _gridDataController.viewId;
+  FieldController get fieldController => _boardDataController.fieldController;
+  String get viewId => _boardDataController.viewId;
 
   BoardBloc({required ViewPB view})
       : _groupBackendSvc = GroupBackendService(viewId: view.id),
-        _gridDataController = BoardDataController(view: view),
+        _boardDataController = BoardDataController(view: view),
         super(BoardState.initial(view.id)) {
     boardController = AppFlowyBoardController(
       onMoveGroup: (
@@ -72,7 +72,7 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
           },
           createBottomRow: (groupId) async {
             final startRowId = groupControllers[groupId]?.lastRow()?.id;
-            final result = await _gridDataController.createBoardCard(
+            final result = await _boardDataController.createBoardCard(
               groupId,
               startRowId: startRowId,
             );
@@ -82,7 +82,7 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
             );
           },
           createHeaderRow: (String groupId) async {
-            final result = await _gridDataController.createBoardCard(groupId);
+            final result = await _boardDataController.createBoardCard(groupId);
             result.fold(
               (_) {},
               (err) => Log.error(err),
@@ -178,7 +178,7 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
 
   @override
   Future<void> close() async {
-    await _gridDataController.dispose();
+    await _boardDataController.dispose();
     for (final controller in groupControllers.values) {
       controller.dispose();
     }
@@ -204,11 +204,11 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
   }
 
   RowCache? getRowCache(String blockId) {
-    return _gridDataController.rowCache;
+    return _boardDataController.rowCache;
   }
 
   void _startListening() {
-    _gridDataController.addListener(
+    _boardDataController.addListener(
       onDatabaseChanged: (grid) {
         if (!isClosed) {
           add(BoardEvent.didReceiveGridUpdate(grid));
@@ -264,7 +264,7 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
   }
 
   Future<void> _openGrid(Emitter<BoardState> emit) async {
-    final result = await _gridDataController.openGrid();
+    final result = await _boardDataController.openGrid();
     result.fold(
       (grid) => emit(
         state.copyWith(loadingState: GridLoadingState.finish(left(unit))),

+ 6 - 6
frontend/appflowy_flutter/lib/plugins/database_view/board/application/board_data_controller.dart

@@ -21,7 +21,7 @@ typedef OnResetGroups = void Function(List<GroupPB>);
 
 class BoardDataController {
   final String viewId;
-  final DatabaseBackendService _databaseFFIService;
+  final DatabaseBackendService _databaseSvc;
   final FieldController fieldController;
   final BoardListener _listener;
   late DatabaseViewCache _viewCache;
@@ -38,7 +38,7 @@ class BoardDataController {
   BoardDataController({required ViewPB view})
       : viewId = view.id,
         _listener = BoardListener(view.id),
-        _databaseFFIService = DatabaseBackendService(viewId: view.id),
+        _databaseSvc = DatabaseBackendService(viewId: view.id),
         fieldController = FieldController(viewId: view.id) {
     //
     _viewCache = DatabaseViewCache(
@@ -100,7 +100,7 @@ class BoardDataController {
   }
 
   Future<Either<Unit, FlowyError>> openGrid() async {
-    final result = await _databaseFFIService.openGrid();
+    final result = await _databaseSvc.openGrid();
     return result.fold(
       (grid) async {
         _onDatabaseChanged?.call(grid);
@@ -121,17 +121,17 @@ class BoardDataController {
 
   Future<Either<RowPB, FlowyError>> createBoardCard(String groupId,
       {String? startRowId}) {
-    return _databaseFFIService.createBoardCard(groupId, startRowId);
+    return _databaseSvc.createBoardCard(groupId, startRowId);
   }
 
   Future<void> dispose() async {
     await _viewCache.dispose();
-    await _databaseFFIService.closeView();
+    await _databaseSvc.closeView();
     await fieldController.dispose();
   }
 
   Future<void> _loadGroups() async {
-    final result = await _databaseFFIService.loadGroups();
+    final result = await _databaseSvc.loadGroups();
     return Future(
       () => result.fold(
         (groups) {

+ 1 - 2
frontend/appflowy_flutter/lib/plugins/database_view/board/application/card/board_checkbox_cell_bloc.dart

@@ -1,8 +1,7 @@
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:freezed_annotation/freezed_annotation.dart';
 import 'dart:async';
-
-import '../../../application/cell/cell_service.dart';
+import '../../../application/cell/cell_controller_builder.dart';
 
 part 'board_checkbox_cell_bloc.freezed.dart';
 

+ 1 - 1
frontend/appflowy_flutter/lib/plugins/database_view/board/application/card/board_date_cell_bloc.dart

@@ -3,7 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:freezed_annotation/freezed_annotation.dart';
 import 'dart:async';
 
-import '../../../application/cell/cell_service.dart';
+import '../../../application/cell/cell_controller_builder.dart';
 import '../../../application/field/field_controller.dart';
 part 'board_date_cell_bloc.freezed.dart';
 

+ 2 - 1
frontend/appflowy_flutter/lib/plugins/database_view/board/application/card/board_number_cell_bloc.dart

@@ -1,7 +1,8 @@
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:freezed_annotation/freezed_annotation.dart';
 import 'dart:async';
-import '../../../application/cell/cell_service.dart';
+
+import '../../../application/cell/cell_controller_builder.dart';
 
 part 'board_number_cell_bloc.freezed.dart';
 

+ 1 - 1
frontend/appflowy_flutter/lib/plugins/database_view/board/application/card/board_select_option_cell_bloc.dart

@@ -1,5 +1,5 @@
 import 'dart:async';
-import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
+import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
 import 'package:appflowy_backend/protobuf/flowy-database/select_type_option.pb.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:freezed_annotation/freezed_annotation.dart';

+ 1 - 1
frontend/appflowy_flutter/lib/plugins/database_view/board/application/card/board_text_cell_bloc.dart

@@ -1,4 +1,4 @@
-import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
+import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:freezed_annotation/freezed_annotation.dart';
 import 'dart:async';

+ 1 - 2
frontend/appflowy_flutter/lib/plugins/database_view/board/application/card/board_url_cell_bloc.dart

@@ -1,10 +1,9 @@
+import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
 import 'package:appflowy_backend/protobuf/flowy-database/url_type_option_entities.pb.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:freezed_annotation/freezed_annotation.dart';
 import 'dart:async';
 
-import '../../../application/cell/cell_service.dart';
-
 part 'board_url_cell_bloc.freezed.dart';
 
 class BoardURLCellBloc extends Bloc<BoardURLCellEvent, BoardURLCellState> {

+ 1 - 12
frontend/appflowy_flutter/lib/plugins/database_view/board/application/card/card_data_controller.dart

@@ -1,9 +1,7 @@
 import 'package:appflowy_backend/protobuf/flowy-database/row_entities.pb.dart';
 import 'package:flutter/foundation.dart';
 
-import '../../../application/cell/cell_field_notifier.dart';
 import '../../../application/cell/cell_service.dart';
-import '../../../application/field/field_controller.dart';
 import '../../../application/row/row_cache.dart';
 import '../../presentation/card/card_cell_builder.dart';
 
@@ -11,16 +9,13 @@ typedef OnCardChanged = void Function(CellByFieldId, RowsChangedReason);
 
 class CardDataController extends BoardCellBuilderDelegate {
   final RowPB rowPB;
-  final FieldController _fieldController;
   final RowCache _rowCache;
   final List<VoidCallback> _onCardChangedListeners = [];
 
   CardDataController({
     required this.rowPB,
-    required FieldController fieldController,
     required RowCache rowCache,
-  })  : _fieldController = fieldController,
-        _rowCache = rowCache;
+  }) : _rowCache = rowCache;
 
   CellByFieldId loadData() {
     return _rowCache.loadGridCells(rowPB.id);
@@ -39,12 +34,6 @@ class CardDataController extends BoardCellBuilderDelegate {
     }
   }
 
-  @override
-  CellFieldNotifier buildFieldNotifier() {
-    return CellFieldNotifier(
-        notifier: GridCellFieldNotifierImpl(_fieldController));
-  }
-
   @override
   CellCache get cellCache => _rowCache.cellCache;
 }

+ 1 - 3
frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/board_page.dart

@@ -229,7 +229,6 @@ class _BoardContentState extends State<BoardContent> {
     final fieldController = context.read<BoardBloc>().fieldController;
     final viewId = context.read<BoardBloc>().viewId;
     final cardController = CardDataController(
-      fieldController: fieldController,
       rowCache: rowCache,
       rowPB: rowPB,
     );
@@ -306,7 +305,6 @@ class _BoardContentState extends State<BoardContent> {
 
     final dataController = RowDataController(
       rowInfo: rowInfo,
-      fieldController: fieldController,
       rowCache: rowCache,
     );
 
@@ -314,7 +312,7 @@ class _BoardContentState extends State<BoardContent> {
       context: context,
       builder: (BuildContext context) {
         return RowDetailPage(
-          cellBuilder: GridCellBuilder(delegate: dataController),
+          cellBuilder: GridCellBuilder(cellCache: dataController.cellCache),
           dataController: dataController,
         );
       },

+ 1 - 1
frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/card/board_checkbox_cell.dart

@@ -1,4 +1,4 @@
-import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
+import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
 import 'package:appflowy/plugins/database_view/board/application/card/board_checkbox_cell_bloc.dart';
 import 'package:flowy_infra/image.dart';
 import 'package:flowy_infra_ui/style_widget/icon_button.dart';

+ 1 - 1
frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/card/board_checklist_cell.dart

@@ -1,7 +1,7 @@
+import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 
-import '../../../application/cell/cell_service.dart';
 import '../../../grid/application/cell/checklist_cell_bloc.dart';
 import '../../../grid/presentation/widgets/cell/checklist_cell/checklist_progress_bar.dart';
 

+ 1 - 1
frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/card/board_date_cell.dart

@@ -1,9 +1,9 @@
+import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
 import 'package:appflowy/plugins/database_view/board/application/card/board_date_cell_bloc.dart';
 import 'package:flowy_infra_ui/style_widget/text.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 
-import '../../../application/cell/cell_service.dart';
 import 'define.dart';
 
 class BoardDateCell extends StatefulWidget {

+ 1 - 1
frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/card/board_number_cell.dart

@@ -1,7 +1,7 @@
+import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
 import 'package:flowy_infra_ui/style_widget/text.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
-import '../../../application/cell/cell_service.dart';
 import '../../application/card/board_number_cell_bloc.dart';
 import 'define.dart';
 

+ 1 - 2
frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/card/board_select_option_cell.dart

@@ -1,9 +1,8 @@
+import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
 import 'package:appflowy_popover/appflowy_popover.dart';
 import 'package:flowy_infra_ui/flowy_infra_ui.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
-
-import '../../../application/cell/cell_service.dart';
 import '../../../grid/presentation/widgets/cell/select_option_cell/extension.dart';
 import '../../../grid/presentation/widgets/cell/select_option_cell/select_option_editor.dart';
 import '../../application/card/board_select_option_cell_bloc.dart';

+ 1 - 1
frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/card/board_text_cell.dart

@@ -1,10 +1,10 @@
+import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
 import 'package:appflowy/plugins/database_view/grid/presentation/widgets/cell/cell_builder.dart';
 import 'package:flowy_infra/size.dart';
 import 'package:flowy_infra_ui/style_widget/text.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:textstyle_extensions/textstyle_extensions.dart';
-import '../../../application/cell/cell_service.dart';
 import '../../application/card/board_text_cell_bloc.dart';
 import 'board_cell.dart';
 import 'define.dart';

+ 1 - 1
frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/card/board_url_cell.dart

@@ -1,9 +1,9 @@
+import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
 import 'package:flowy_infra/size.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:textstyle_extensions/textstyle_extensions.dart';
 
-import '../../../application/cell/cell_service.dart';
 import '../../application/card/board_url_cell_bloc.dart';
 import 'define.dart';
 

+ 2 - 2
frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/card/card_cell_builder.dart

@@ -1,3 +1,4 @@
+import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
 import 'package:appflowy_backend/protobuf/flowy-database/field_entities.pb.dart';
 import 'package:flutter/material.dart';
 
@@ -11,7 +12,7 @@ import 'board_select_option_cell.dart';
 import 'board_text_cell.dart';
 import 'board_url_cell.dart';
 
-abstract class BoardCellBuilderDelegate extends CellControllerBuilderDelegate {
+abstract class BoardCellBuilderDelegate {
   CellCache get cellCache;
 }
 
@@ -26,7 +27,6 @@ class BoardCellBuilder {
     EditableCellNotifier cellNotifier,
   ) {
     final cellControllerBuilder = CellControllerBuilder(
-      delegate: delegate,
       cellId: cellId,
       cellCache: delegate.cellCache,
     );

+ 1 - 0
frontend/appflowy_flutter/lib/plugins/database_view/grid/application/cell/checkbox_cell_bloc.dart

@@ -1,3 +1,4 @@
+import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
 import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:freezed_annotation/freezed_annotation.dart';

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

@@ -1,4 +1,4 @@
-import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
+import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
 import 'package:appflowy_backend/log.dart';
 import 'package:appflowy_backend/protobuf/flowy-database/select_type_option.pb.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
@@ -10,12 +10,12 @@ part 'checklist_cell_bloc.freezed.dart';
 
 class ChecklistCellBloc extends Bloc<ChecklistCellEvent, ChecklistCellState> {
   final ChecklistCellController cellController;
-  final SelectOptionFFIService _selectOptionService;
+  final SelectOptionBackendService _selectOptionSvc;
   void Function()? _onCellChangedFn;
   ChecklistCellBloc({
     required this.cellController,
-  })  : _selectOptionService =
-            SelectOptionFFIService(cellId: cellController.cellId),
+  })  : _selectOptionSvc =
+            SelectOptionBackendService(cellId: cellController.cellId),
         super(ChecklistCellState.initial(cellController)) {
     on<ChecklistCellEvent>(
       (event, emit) async {
@@ -60,7 +60,7 @@ class ChecklistCellBloc extends Bloc<ChecklistCellEvent, ChecklistCellState> {
   }
 
   void _loadOptions() {
-    _selectOptionService.getOptionContext().then((result) {
+    _selectOptionSvc.getCellData().then((result) {
       if (isClosed) return;
 
       return result.fold(

+ 4 - 5
frontend/appflowy_flutter/lib/plugins/database_view/grid/application/cell/checklist_cell_editor_bloc.dart

@@ -1,25 +1,24 @@
 import 'dart:async';
 
-import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
+import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
 import 'package:dartz/dartz.dart';
 import 'package:appflowy_backend/log.dart';
 import 'package:appflowy_backend/protobuf/flowy-database/select_type_option.pb.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:freezed_annotation/freezed_annotation.dart';
-
 import 'select_option_service.dart';
 
 part 'checklist_cell_editor_bloc.freezed.dart';
 
 class ChecklistCellEditorBloc
     extends Bloc<ChecklistCellEditorEvent, ChecklistCellEditorState> {
-  final SelectOptionFFIService _selectOptionService;
+  final SelectOptionBackendService _selectOptionService;
   final ChecklistCellController cellController;
 
   ChecklistCellEditorBloc({
     required this.cellController,
   })  : _selectOptionService =
-            SelectOptionFFIService(cellId: cellController.cellId),
+            SelectOptionBackendService(cellId: cellController.cellId),
         super(ChecklistCellEditorState.initial(cellController)) {
     on<ChecklistCellEditorEvent>(
       (event, emit) async {
@@ -87,7 +86,7 @@ class ChecklistCellEditorBloc
   }
 
   void _loadOptions() {
-    _selectOptionService.getOptionContext().then((result) {
+    _selectOptionService.getCellData().then((result) {
       if (isClosed) return;
 
       return result.fold(

+ 1 - 0
frontend/appflowy_flutter/lib/plugins/database_view/grid/application/cell/date_cal_bloc.dart

@@ -1,4 +1,5 @@
 import 'package:appflowy/generated/locale_keys.g.dart';
+import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
 import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
 import 'package:appflowy/plugins/database_view/application/field/field_service.dart';
 import 'package:easy_localization/easy_localization.dart'

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

@@ -1,4 +1,4 @@
-import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
+import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
 import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
 import 'package:appflowy_backend/protobuf/flowy-database/date_type_option_entities.pb.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';

+ 8 - 17
frontend/appflowy_flutter/lib/plugins/database_view/grid/application/cell/number_cell_bloc.dart

@@ -1,4 +1,4 @@
-import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
+import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
 import 'package:appflowy_backend/log.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:freezed_annotation/freezed_annotation.dart';
@@ -49,22 +49,13 @@ class NumberCellBloc extends Bloc<NumberCellEvent, NumberCellState> {
   }
 
   void _startListening() {
-    _onCellChangedFn =
-        cellController.startListening(onCellChanged: ((cellContent) {
-      if (!isClosed) {
-        add(NumberCellEvent.didReceiveCellUpdate(cellContent));
-      }
-    }), listenWhenOnCellChanged: (oldValue, newValue) {
-      // If the new value is not the same as the content, which means the
-      // backend formatted the content that user enter. For example:
-      //
-      // state.cellContent: "abc"
-      // oldValue: ""
-      // newValue: ""
-      // The oldValue is the same as newValue. So the [onCellChanged] won't
-      // get called. So just return true to refresh the cell content
-      return true;
-    });
+    _onCellChangedFn = cellController.startListening(
+      onCellChanged: ((cellContent) {
+        if (!isClosed) {
+          add(NumberCellEvent.didReceiveCellUpdate(cellContent));
+        }
+      }),
+    );
   }
 }
 

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

@@ -1,5 +1,5 @@
 import 'dart:async';
-import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
+import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
 import 'package:appflowy_backend/protobuf/flowy-database/select_type_option.pb.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:freezed_annotation/freezed_annotation.dart';

+ 4 - 4
frontend/appflowy_flutter/lib/plugins/database_view/grid/application/cell/select_option_editor_bloc.dart

@@ -1,5 +1,5 @@
 import 'dart:async';
-import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
+import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
 import 'package:dartz/dartz.dart';
 import 'package:appflowy_backend/log.dart';
 import 'package:appflowy_backend/protobuf/flowy-database/select_type_option.pb.dart';
@@ -11,13 +11,13 @@ part 'select_option_editor_bloc.freezed.dart';
 
 class SelectOptionCellEditorBloc
     extends Bloc<SelectOptionEditorEvent, SelectOptionEditorState> {
-  final SelectOptionFFIService _selectOptionService;
+  final SelectOptionBackendService _selectOptionService;
   final SelectOptionCellController cellController;
 
   SelectOptionCellEditorBloc({
     required this.cellController,
   })  : _selectOptionService =
-            SelectOptionFFIService(cellId: cellController.cellId),
+            SelectOptionBackendService(cellId: cellController.cellId),
         super(SelectOptionEditorState.initial(cellController)) {
     on<SelectOptionEditorEvent>(
       (event, emit) async {
@@ -159,7 +159,7 @@ class SelectOptionCellEditorBloc
   }
 
   Future<void> _loadOptions() async {
-    final result = await _selectOptionService.getOptionContext();
+    final result = await _selectOptionService.getCellData();
     if (isClosed) {
       Log.warn("Unexpected closing the bloc");
       return;

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

@@ -6,9 +6,9 @@ import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
 import 'package:appflowy_backend/protobuf/flowy-database/cell_entities.pb.dart';
 import 'package:appflowy_backend/protobuf/flowy-database/select_type_option.pb.dart';
 
-class SelectOptionFFIService {
+class SelectOptionBackendService {
   final CellIdentifier cellId;
-  SelectOptionFFIService({required this.cellId});
+  SelectOptionBackendService({required this.cellId});
 
   String get viewId => cellId.viewId;
   String get fieldId => cellId.fieldInfo.id;
@@ -60,7 +60,7 @@ class SelectOptionFFIService {
     return DatabaseEventUpdateSelectOption(payload).send();
   }
 
-  Future<Either<SelectOptionCellDataPB, FlowyError>> getOptionContext() {
+  Future<Either<SelectOptionCellDataPB, FlowyError>> getCellData() {
     final payload = CellIdPB.create()
       ..viewId = viewId
       ..fieldId = fieldId

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

@@ -1,4 +1,4 @@
-import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
+import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:freezed_annotation/freezed_annotation.dart';
 import 'dart:async';

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

@@ -1,4 +1,4 @@
-import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
+import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
 import 'package:appflowy_backend/protobuf/flowy-database/url_type_option_entities.pb.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:freezed_annotation/freezed_annotation.dart';

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

@@ -1,4 +1,4 @@
-import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
+import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
 import 'package:appflowy_backend/protobuf/flowy-database/url_type_option_entities.pb.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:freezed_annotation/freezed_annotation.dart';

+ 0 - 3
frontend/appflowy_flutter/lib/plugins/database_view/grid/application/filter/checklist_filter_bloc.dart

@@ -13,14 +13,11 @@ class ChecklistFilterEditorBloc
     extends Bloc<ChecklistFilterEditorEvent, ChecklistFilterEditorState> {
   final FilterInfo filterInfo;
   final FilterBackendService _filterBackendSvc;
-  // final SelectOptionFFIService _selectOptionService;
   final FilterListener _listener;
 
   ChecklistFilterEditorBloc({
     required this.filterInfo,
   })  : _filterBackendSvc = FilterBackendService(viewId: filterInfo.viewId),
-        // _selectOptionService =
-        //           SelectOptionFFIService(cellId: cellController.cellId)
         _listener = FilterListener(
           viewId: filterInfo.viewId,
           filterId: filterInfo.filter.id,

+ 7 - 17
frontend/appflowy_flutter/lib/plugins/database_view/grid/application/grid_bloc.dart

@@ -15,10 +15,9 @@ import 'dart:collection';
 part 'grid_bloc.freezed.dart';
 
 class GridBloc extends Bloc<GridEvent, GridState> {
-  final DatabaseController gridController;
-  void Function()? _createRowOperation;
+  final DatabaseController databaseController;
 
-  GridBloc({required ViewPB view, required this.gridController})
+  GridBloc({required ViewPB view, required this.databaseController})
       : super(GridState.initial(view.id)) {
     on<GridEvent>(
       (event, emit) async {
@@ -28,12 +27,7 @@ class GridBloc extends Bloc<GridEvent, GridState> {
             await _openGrid(emit);
           },
           createRow: () {
-            state.loadingState.when(
-              loading: () {
-                _createRowOperation = () => gridController.createRow();
-              },
-              finish: (_) => gridController.createRow(),
-            );
+            databaseController.createRow();
           },
           deleteRow: (rowInfo) async {
             final rowService = RowBackendService(
@@ -63,16 +57,16 @@ class GridBloc extends Bloc<GridEvent, GridState> {
 
   @override
   Future<void> close() async {
-    await gridController.dispose();
+    await databaseController.dispose();
     return super.close();
   }
 
   RowCache? getRowCache(String blockId, String rowId) {
-    return gridController.rowCache;
+    return databaseController.rowCache;
   }
 
   void _startListening() {
-    gridController.addListener(
+    databaseController.addListener(
       onGridChanged: (grid) {
         if (!isClosed) {
           add(GridEvent.didReceiveGridUpdate(grid));
@@ -92,13 +86,9 @@ class GridBloc extends Bloc<GridEvent, GridState> {
   }
 
   Future<void> _openGrid(Emitter<GridState> emit) async {
-    final result = await gridController.openGrid();
+    final result = await databaseController.openGrid();
     result.fold(
       (grid) {
-        if (_createRowOperation != null) {
-          _createRowOperation?.call();
-          _createRowOperation = null;
-        }
         emit(
           state.copyWith(loadingState: GridLoadingState.finish(left(unit))),
         );

+ 8 - 10
frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/grid_page.dart

@@ -35,11 +35,11 @@ class GridPage extends StatefulWidget {
     required this.view,
     this.onDeleted,
     Key? key,
-  })  : gridController = DatabaseController(view: view),
+  })  : databaseController = DatabaseController(view: view),
         super(key: key);
 
   final ViewPB view;
-  final DatabaseController gridController;
+  final DatabaseController databaseController;
   final VoidCallback? onDeleted;
 
   @override
@@ -54,19 +54,19 @@ class _GridPageState extends State<GridPage> {
         BlocProvider<GridBloc>(
           create: (context) => GridBloc(
             view: widget.view,
-            gridController: widget.gridController,
+            databaseController: widget.databaseController,
           )..add(const GridEvent.initial()),
         ),
         BlocProvider<GridFilterMenuBloc>(
           create: (context) => GridFilterMenuBloc(
             viewId: widget.view.id,
-            fieldController: widget.gridController.fieldController,
+            fieldController: widget.databaseController.fieldController,
           )..add(const GridFilterMenuEvent.initial()),
         ),
         BlocProvider<SortMenuBloc>(
           create: (context) => SortMenuBloc(
             viewId: widget.view.id,
-            fieldController: widget.gridController.fieldController,
+            fieldController: widget.databaseController.fieldController,
           )..add(const SortMenuEvent.initial()),
         ),
         BlocProvider<DatabaseSettingBloc>(
@@ -190,7 +190,7 @@ class _FlowyGridState extends State<FlowyGrid> {
 
   Widget _gridHeader(BuildContext context, String viewId) {
     final fieldController =
-        context.read<GridBloc>().gridController.fieldController;
+        context.read<GridBloc>().databaseController.fieldController;
     return GridHeaderSliverAdaptor(
       viewId: viewId,
       fieldController: fieldController,
@@ -274,10 +274,9 @@ class _GridRowsState extends State<_GridRows> {
     if (rowCache == null) return const SizedBox();
 
     final fieldController =
-        context.read<GridBloc>().gridController.fieldController;
+        context.read<GridBloc>().databaseController.fieldController;
     final dataController = RowDataController(
       rowInfo: rowInfo,
-      fieldController: fieldController,
       rowCache: rowCache,
     );
 
@@ -286,7 +285,7 @@ class _GridRowsState extends State<_GridRows> {
       child: GridRowWidget(
         rowInfo: rowInfo,
         dataController: dataController,
-        cellBuilder: GridCellBuilder(delegate: dataController),
+        cellBuilder: GridCellBuilder(cellCache: dataController.cellCache),
         openDetailPage: (context, cellBuilder) {
           _openRowDetailPage(
             context,
@@ -310,7 +309,6 @@ class _GridRowsState extends State<_GridRows> {
   ) {
     final dataController = RowDataController(
       rowInfo: rowInfo,
-      fieldController: fieldController,
       rowCache: rowCache,
     );
 

+ 4 - 8
frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/cell/cell_builder.dart

@@ -1,3 +1,4 @@
+import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
 import 'package:appflowy_backend/protobuf/flowy-database/field_entities.pb.dart';
 import 'package:flutter/services.dart';
 import 'package:flutter/widgets.dart';
@@ -13,21 +14,16 @@ import 'select_option_cell/select_option_cell.dart';
 import 'text_cell.dart';
 import 'url_cell/url_cell.dart';
 
-abstract class GridCellBuilderDelegate extends CellControllerBuilderDelegate {
-  CellCache get cellCache;
-}
-
 class GridCellBuilder {
-  final GridCellBuilderDelegate delegate;
+  final CellCache cellCache;
   GridCellBuilder({
-    required this.delegate,
+    required this.cellCache,
   });
 
   GridCellWidget build(CellIdentifier cellId, {GridCellStyle? style}) {
     final cellControllerBuilder = CellControllerBuilder(
       cellId: cellId,
-      cellCache: delegate.cellCache,
-      delegate: delegate,
+      cellCache: cellCache,
     );
 
     final key = cellId.key();

+ 1 - 1
frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/cell/checkbox_cell.dart

@@ -1,9 +1,9 @@
+import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
 import 'package:appflowy/startup/startup.dart';
 import 'package:flowy_infra/image.dart';
 import 'package:flowy_infra_ui/style_widget/icon_button.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
-import '../../../../application/cell/cell_service.dart';
 import '../../../application/cell/checkbox_cell_bloc.dart';
 import '../../layout/sizes.dart';
 import 'cell_builder.dart';

+ 1 - 1
frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/cell/checklist_cell/checklist_cell.dart

@@ -1,4 +1,4 @@
-import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
+import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
 import 'package:appflowy/plugins/database_view/grid/application/cell/checklist_cell_bloc.dart';
 import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
 import 'package:appflowy_popover/appflowy_popover.dart';

+ 1 - 1
frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/cell/checklist_cell/checklist_cell_editor.dart

@@ -1,3 +1,4 @@
+import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
 import 'package:appflowy_popover/appflowy_popover.dart';
 import 'package:flowy_infra/image.dart';
 import 'package:flowy_infra_ui/flowy_infra_ui.dart';
@@ -8,7 +9,6 @@ import 'package:flowy_infra_ui/widget/spacing.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 
-import '../../../../../application/cell/cell_service.dart';
 import '../../../../application/cell/checklist_cell_editor_bloc.dart';
 import '../../../layout/sizes.dart';
 import '../../header/type_option/select_option_editor.dart';

+ 1 - 1
frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/cell/date_cell/date_cell.dart

@@ -1,4 +1,4 @@
-import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
+import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
 import 'package:appflowy/plugins/database_view/grid/application/cell/date_cell_bloc.dart';
 import 'package:flowy_infra_ui/flowy_infra_ui.dart';
 import 'package:flutter/widgets.dart';

+ 1 - 1
frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/cell/date_cell/date_editor.dart

@@ -1,4 +1,5 @@
 import 'package:appflowy/generated/locale_keys.g.dart';
+import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
 import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart';
 import 'package:appflowy/plugins/database_view/grid/application/cell/date_cal_bloc.dart';
 import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';
@@ -20,7 +21,6 @@ import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:table_calendar/table_calendar.dart';
 import 'package:textstyle_extensions/textstyle_extensions.dart';
-import '../../../../../application/cell/cell_service.dart';
 import '../../../layout/sizes.dart';
 import '../../common/type_option_separator.dart';
 import '../../header/type_option/date.dart';

+ 1 - 1
frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/cell/number_cell.dart

@@ -1,9 +1,9 @@
 import 'dart:async';
+import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
 import 'package:appflowy/startup/startup.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 
-import '../../../../application/cell/cell_service.dart';
 import '../../../application/cell/number_cell_bloc.dart';
 import '../../layout/sizes.dart';
 import 'cell_builder.dart';

+ 1 - 1
frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/cell/select_option_cell/select_option_cell.dart

@@ -1,4 +1,4 @@
-import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
+import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
 import 'package:appflowy/startup/startup.dart';
 import 'package:appflowy_popover/appflowy_popover.dart';
 import 'package:flowy_infra_ui/flowy_infra_ui.dart';

+ 1 - 1
frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/cell/select_option_cell/select_option_editor.dart

@@ -1,4 +1,5 @@
 import 'dart:collection';
+import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
 import 'package:appflowy/plugins/database_view/grid/application/cell/select_option_editor_bloc.dart';
 import 'package:appflowy_popover/appflowy_popover.dart';
 import 'package:flowy_infra/theme_extension.dart';
@@ -15,7 +16,6 @@ import 'package:easy_localization/easy_localization.dart';
 import 'package:appflowy/generated/locale_keys.g.dart';
 import 'package:textfield_tags/textfield_tags.dart';
 
-import '../../../../../application/cell/cell_service.dart';
 import '../../../layout/sizes.dart';
 import '../../common/type_option_separator.dart';
 import '../../header/type_option/select_option_editor.dart';

+ 1 - 1
frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/cell/text_cell.dart

@@ -1,5 +1,5 @@
 import 'dart:async';
-import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
+import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
 import 'package:appflowy/plugins/database_view/grid/application/cell/text_cell_bloc.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';

+ 1 - 2
frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/cell/url_cell/cell_editor.dart

@@ -1,9 +1,8 @@
+import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
 import 'package:flutter/material.dart';
 import 'dart:async';
-
 import 'package:flutter_bloc/flutter_bloc.dart';
 
-import '../../../../../application/cell/cell_service.dart';
 import '../../../../application/cell/url_cell_editor_bloc.dart';
 
 class URLCellEditor extends StatefulWidget {

+ 1 - 1
frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/cell/url_cell/url_cell.dart

@@ -1,6 +1,6 @@
 import 'dart:async';
 import 'package:appflowy/generated/locale_keys.g.dart';
-import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
+import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
 import 'package:appflowy/workspace/presentation/home/toast.dart';
 import 'package:appflowy_popover/appflowy_popover.dart';
 import 'package:easy_localization/easy_localization.dart';

+ 2 - 2
frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_type_option_editor.dart

@@ -23,11 +23,11 @@ typedef SwitchToFieldCallback = Future<Either<TypeOptionPB, FlowyError>>
 );
 
 class FieldTypeOptionEditor extends StatelessWidget {
-  final TypeOptionDataController _dataController;
+  final TypeOptionController _dataController;
   final PopoverMutex popoverMutex;
 
   const FieldTypeOptionEditor({
-    required TypeOptionDataController dataController,
+    required TypeOptionController dataController,
     required this.popoverMutex,
     Key? key,
   })  : _dataController = dataController,

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

@@ -48,7 +48,7 @@ abstract class TypeOptionWidgetBuilder {
 
 Widget? makeTypeOptionWidget({
   required BuildContext context,
-  required TypeOptionDataController dataController,
+  required TypeOptionController dataController,
   required PopoverMutex popoverMutex,
 }) {
   final builder = makeTypeOptionWidgetBuilder(
@@ -59,7 +59,7 @@ Widget? makeTypeOptionWidget({
 }
 
 TypeOptionWidgetBuilder makeTypeOptionWidgetBuilder({
-  required TypeOptionDataController dataController,
+  required TypeOptionController dataController,
   required PopoverMutex popoverMutex,
 }) {
   final viewId = dataController.viewId;
@@ -144,7 +144,7 @@ TypeOptionContext<T> makeTypeOptionContext<T extends GeneratedMessage>({
   required FieldInfo fieldInfo,
 }) {
   final loader = FieldTypeOptionLoader(viewId: viewId, field: fieldInfo.field);
-  final dataController = TypeOptionDataController(
+  final dataController = TypeOptionController(
     viewId: viewId,
     loader: loader,
     fieldInfo: fieldInfo,
@@ -178,7 +178,7 @@ TypeOptionContext<T> makeSelectTypeOptionContext<T extends GeneratedMessage>({
     viewId: viewId,
     field: fieldPB,
   );
-  final dataController = TypeOptionDataController(
+  final dataController = TypeOptionController(
     viewId: viewId,
     loader: loader,
   );
@@ -194,7 +194,7 @@ TypeOptionContext<T>
     makeTypeOptionContextWithDataController<T extends GeneratedMessage>({
   required String viewId,
   required FieldType fieldType,
-  required TypeOptionDataController dataController,
+  required TypeOptionController dataController,
 }) {
   switch (fieldType) {
     case FieldType.Checkbox:

+ 1 - 1
frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/toolbar/setting_button.dart

@@ -35,7 +35,7 @@ class _SettingButtonState extends State<SettingButton> {
     return BlocSelector<GridBloc, GridState, GridSettingContext>(
       selector: (state) {
         final fieldController =
-            context.read<GridBloc>().gridController.fieldController;
+            context.read<GridBloc>().databaseController.fieldController;
         return GridSettingContext(
           viewId: state.viewId,
           fieldController: fieldController,

+ 25 - 3
frontend/appflowy_flutter/lib/plugins/document/document_page.dart

@@ -1,5 +1,6 @@
 import 'package:appflowy/plugins/document/presentation/plugins/board/board_menu_item.dart';
 import 'package:appflowy/plugins/document/presentation/plugins/board/board_node_widget.dart';
+import 'package:appflowy/plugins/document/presentation/plugins/cover/cover_node_widget.dart';
 import 'package:appflowy/plugins/document/presentation/plugins/grid/grid_menu_item.dart';
 import 'package:appflowy/plugins/document/presentation/plugins/grid/grid_node_widget.dart';
 import 'package:appflowy/plugins/document/presentation/plugins/openai/widgets/auto_completion_node_widget.dart';
@@ -9,6 +10,7 @@ import 'package:appflowy/plugins/document/presentation/plugins/openai/widgets/sm
 import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
 import 'package:appflowy_editor/appflowy_editor.dart';
 import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
+import 'package:dartz/dartz.dart' as dartz;
 import 'package:flowy_infra_ui/widget/error_page.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
@@ -126,9 +128,11 @@ class _AppFlowyEditorPageState extends State<_AppFlowyEditorPage> {
   @override
   Widget build(BuildContext context) {
     final theme = Theme.of(context);
+    final autoFocusParamters = _autoFocusParamters();
     final editor = AppFlowyEditor(
       editorState: editorState,
-      autoFocus: editorState.document.isEmpty,
+      autoFocus: autoFocusParamters.value1,
+      focusedSelection: autoFocusParamters.value2,
       customBuilders: {
         // Divider
         kDividerType: DividerWidgetBuilder(),
@@ -144,6 +148,8 @@ class _AppFlowyEditorPageState extends State<_AppFlowyEditorPage> {
         kCalloutType: CalloutNodeWidgetBuilder(),
         // Auto Generator,
         kAutoCompletionInputType: AutoCompletionInputBuilder(),
+        // Cover
+        kCoverType: CoverNodeWidgetBuilder(),
         // Smart Edit,
         kSmartEditType: SmartEditInputBuilder(),
       },
@@ -174,10 +180,12 @@ class _AppFlowyEditorPageState extends State<_AppFlowyEditorPage> {
         // enable open ai features if needed.
         if (openAIKey != null && openAIKey!.isNotEmpty) ...[
           autoGeneratorMenuItem,
-        ]
+        ],
       ],
       toolbarItems: [
-        smartEditItem,
+        if (openAIKey != null && openAIKey!.isNotEmpty) ...[
+          smartEditItem,
+        ]
       ],
       themeData: theme.copyWith(extensions: [
         ...theme.extensions.values,
@@ -227,4 +235,18 @@ class _AppFlowyEditorPageState extends State<_AppFlowyEditorPage> {
       await editorState.apply(transaction, withUpdateCursor: false);
     }
   }
+
+  dartz.Tuple2<bool, Selection?> _autoFocusParamters() {
+    if (editorState.document.isEmpty) {
+      return dartz.Tuple2(true, Selection.single(path: [0], startOffset: 0));
+    }
+    final texts = editorState.document.root.children.whereType<TextNode>();
+    if (texts.every((element) => element.toPlainText().isEmpty)) {
+      return dartz.Tuple2(
+        true,
+        Selection.single(path: texts.first.path, startOffset: 0),
+      );
+    }
+    return const dartz.Tuple2(false, null);
+  }
 }

+ 1 - 1
frontend/appflowy_flutter/lib/plugins/document/editor_styles.dart

@@ -9,7 +9,7 @@ EditorStyle customEditorTheme(BuildContext context) {
       ? EditorStyle.dark
       : EditorStyle.light;
   editorStyle = editorStyle.copyWith(
-    padding: const EdgeInsets.symmetric(horizontal: 100, vertical: 28),
+    padding: const EdgeInsets.symmetric(horizontal: 100, vertical: 0),
     textStyle: editorStyle.textStyle?.copyWith(
       fontFamily: 'poppins',
       fontSize: documentStyle.fontSize,

+ 376 - 0
frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/cover/change_cover_popover.dart

@@ -0,0 +1,376 @@
+import 'dart:io';
+import 'dart:ui';
+
+import 'package:appflowy/generated/locale_keys.g.dart';
+import 'package:appflowy/plugins/document/presentation/plugins/cover/cover_node_widget.dart';
+import 'package:appflowy/startup/startup.dart';
+import 'package:appflowy/util/file_picker/file_picker_service.dart';
+import 'package:appflowy/workspace/application/settings/settings_location_cubit.dart';
+import 'package:appflowy_editor/appflowy_editor.dart';
+import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:file_picker/file_picker.dart' show FileType;
+import 'package:flowy_infra/size.dart';
+import 'package:flowy_infra/theme_extension.dart';
+import 'package:flowy_infra_ui/style_widget/icon_button.dart';
+import 'package:flowy_infra_ui/style_widget/text.dart';
+import 'package:flutter/material.dart';
+import 'package:shared_preferences/shared_preferences.dart';
+
+const String kLocalImagesKey = 'local_images';
+
+List<String> get builtInAssetImages => [
+      "assets/images/app_flowy_abstract_cover_1.jpg",
+      "assets/images/app_flowy_abstract_cover_2.jpg"
+    ];
+
+class ChangeCoverPopover extends StatefulWidget {
+  final EditorState editorState;
+  final Node node;
+  final Function(
+    CoverSelectionType selectionType,
+    String selection,
+  ) onCoverChanged;
+
+  const ChangeCoverPopover({
+    super.key,
+    required this.editorState,
+    required this.onCoverChanged,
+    required this.node,
+  });
+
+  @override
+  State<ChangeCoverPopover> createState() => _ChangeCoverPopoverState();
+}
+
+class ColorOption {
+  final String colorHex;
+
+  final String name;
+  const ColorOption({
+    required this.colorHex,
+    required this.name,
+  });
+}
+
+class CoverColorPicker extends StatefulWidget {
+  final String? selectedBackgroundColorHex;
+
+  final Color pickerBackgroundColor;
+  final Color pickerItemHoverColor;
+  final void Function(String color) onSubmittedbackgroundColorHex;
+  final List<ColorOption> backgroundColorOptions;
+  const CoverColorPicker({
+    super.key,
+    this.selectedBackgroundColorHex,
+    required this.pickerBackgroundColor,
+    required this.backgroundColorOptions,
+    required this.pickerItemHoverColor,
+    required this.onSubmittedbackgroundColorHex,
+  });
+
+  @override
+  State<CoverColorPicker> createState() => _CoverColorPickerState();
+}
+
+class _ChangeCoverPopoverState extends State<ChangeCoverPopover> {
+  late Future<List<String>>? fileImages;
+
+  @override
+  void initState() {
+    super.initState();
+    fileImages = _getPreviouslyPickedImagePaths();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Padding(
+      padding: const EdgeInsets.all(15),
+      child: SingleChildScrollView(
+        child: Column(
+          crossAxisAlignment: CrossAxisAlignment.start,
+          children: [
+            FlowyText.semibold(LocaleKeys.document_plugins_cover_colors.tr()),
+            const SizedBox(height: 10),
+            _buildColorPickerList(),
+            const SizedBox(height: 10),
+            FlowyText.semibold(LocaleKeys.document_plugins_cover_images.tr()),
+            const SizedBox(height: 10),
+            _buildFileImagePicker(),
+            const SizedBox(height: 10),
+            FlowyText.semibold(LocaleKeys.document_plugins_cover_abstract.tr()),
+            const SizedBox(height: 10),
+            _buildAbstractImagePicker(),
+          ],
+        ),
+      ),
+    );
+  }
+
+  Widget _buildAbstractImagePicker() {
+    return GridView.builder(
+      shrinkWrap: true,
+      gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
+          crossAxisCount: 3,
+          childAspectRatio: 1 / 0.65,
+          crossAxisSpacing: 7,
+          mainAxisSpacing: 7),
+      itemCount: builtInAssetImages.length,
+      itemBuilder: (BuildContext ctx, index) {
+        return InkWell(
+          onTap: () {
+            widget.onCoverChanged(
+              CoverSelectionType.asset,
+              builtInAssetImages[index],
+            );
+          },
+          child: Container(
+            decoration: BoxDecoration(
+              image: DecorationImage(
+                image: AssetImage(builtInAssetImages[index]),
+                fit: BoxFit.cover,
+              ),
+              borderRadius: Corners.s8Border,
+            ),
+          ),
+        );
+      },
+    );
+  }
+
+  Widget _buildColorPickerList() {
+    return CoverColorPicker(
+      pickerBackgroundColor:
+          widget.editorState.editorStyle.selectionMenuBackgroundColor ??
+              Colors.white,
+      pickerItemHoverColor:
+          widget.editorState.editorStyle.selectionMenuItemSelectedColor ??
+              Colors.blue.withOpacity(0.3),
+      selectedBackgroundColorHex:
+          widget.node.attributes[kCoverSelectionTypeAttribute] ==
+                  CoverSelectionType.color.toString()
+              ? widget.node.attributes[kCoverSelectionAttribute]
+              : "ffffff",
+      backgroundColorOptions:
+          _generateBackgroundColorOptions(widget.editorState),
+      onSubmittedbackgroundColorHex: (color) {
+        widget.onCoverChanged(CoverSelectionType.color, color);
+        setState(() {});
+      },
+    );
+  }
+
+  Widget _buildFileImagePicker() {
+    return FutureBuilder<List<String>>(
+        future: _getPreviouslyPickedImagePaths(),
+        builder: (context, snapshot) {
+          if (snapshot.hasData) {
+            List<String> images = snapshot.data!;
+            return GridView.builder(
+              shrinkWrap: true,
+              gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
+                crossAxisCount: 3,
+                childAspectRatio: 1 / 0.65,
+                crossAxisSpacing: 7,
+                mainAxisSpacing: 7,
+              ),
+              itemCount: images.length + 1,
+              itemBuilder: (BuildContext ctx, index) {
+                if (index == 0) {
+                  return Container(
+                    decoration: BoxDecoration(
+                      color: Theme.of(context)
+                          .colorScheme
+                          .primary
+                          .withOpacity(0.15),
+                      border: Border.all(
+                        color: Theme.of(context).colorScheme.primary,
+                      ),
+                      borderRadius: Corners.s8Border,
+                    ),
+                    child: FlowyIconButton(
+                      iconPadding: EdgeInsets.zero,
+                      icon: Icon(
+                        Icons.add,
+                        color: Theme.of(context).colorScheme.primary,
+                      ),
+                      width: 20,
+                      onPressed: () {
+                        _pickImages();
+                      },
+                    ),
+                  );
+                }
+                return InkWell(
+                  onTap: () {
+                    widget.onCoverChanged(
+                      CoverSelectionType.file,
+                      images[index - 1],
+                    );
+                  },
+                  child: Container(
+                    decoration: BoxDecoration(
+                      image: DecorationImage(
+                        image: FileImage(File(images[index - 1])),
+                        fit: BoxFit.cover,
+                      ),
+                      borderRadius: Corners.s8Border,
+                    ),
+                  ),
+                );
+              },
+            );
+          } else {
+            return Container();
+          }
+        });
+  }
+
+  List<ColorOption> _generateBackgroundColorOptions(EditorState editorState) {
+    return FlowyTint.values
+        .map((t) => ColorOption(
+              colorHex: t.color(context).toHex(),
+              name: t.tintName(AppFlowyEditorLocalizations.current),
+            ))
+        .toList();
+  }
+
+  Future<List<String>> _getPreviouslyPickedImagePaths() async {
+    SharedPreferences prefs = await SharedPreferences.getInstance();
+    final imageNames = prefs.getStringList(kLocalImagesKey) ?? [];
+    final removeNames = [];
+    for (final name in imageNames) {
+      if (!File(name).existsSync()) {
+        removeNames.add(name);
+      }
+    }
+    imageNames.removeWhere((element) => removeNames.contains(element));
+    prefs.setStringList(kLocalImagesKey, imageNames);
+    return imageNames;
+  }
+
+  Future<void> _pickImages() async {
+    SharedPreferences prefs = await SharedPreferences.getInstance();
+    List<String> imageNames = prefs.getStringList(kLocalImagesKey) ?? [];
+    FilePickerResult? result = await getIt<FilePickerService>().pickFiles(
+      dialogTitle: LocaleKeys.document_plugins_cover_addLocalImage.tr(),
+      allowMultiple: false,
+      type: FileType.image,
+      allowedExtensions: ['jpg', 'png', 'jpeg'],
+    );
+    if (result != null && result.files.isNotEmpty) {
+      final path = result.files.first.path;
+      if (path != null) {
+        final directory = await _coverPath();
+        final newPath = await File(path).copy(
+          '$directory/${path.split('/').last}',
+        );
+        imageNames.add(newPath.path);
+      }
+    }
+    await prefs.setStringList(kLocalImagesKey, imageNames);
+    setState(() {});
+  }
+
+  Future<String> _coverPath() async {
+    final directory = await getIt<SettingsLocationCubit>().fetchLocation();
+    return Directory('$directory/covers')
+        .create(recursive: true)
+        .then((value) => value.path);
+  }
+}
+
+class _CoverColorPickerState extends State<CoverColorPicker> {
+  final scrollController = ScrollController();
+
+  @override
+  Widget build(BuildContext context) {
+    return Container(
+      height: 30,
+      alignment: Alignment.center,
+      child: ScrollConfiguration(
+        behavior: ScrollConfiguration.of(context).copyWith(dragDevices: {
+          PointerDeviceKind.touch,
+          PointerDeviceKind.mouse,
+        }, platform: TargetPlatform.windows),
+        child: ListView.builder(
+          controller: scrollController,
+          shrinkWrap: true,
+          itemCount: widget.backgroundColorOptions.length,
+          scrollDirection: Axis.horizontal,
+          itemBuilder: (context, index) {
+            return _buildColorItems(
+              widget.backgroundColorOptions,
+              widget.selectedBackgroundColorHex,
+            );
+          },
+        ),
+      ),
+    );
+  }
+
+  @override
+  void dispose() {
+    super.dispose();
+    scrollController.dispose();
+  }
+
+  Widget _buildColorItem(ColorOption option, bool isChecked) {
+    return InkWell(
+      customBorder: const RoundedRectangleBorder(
+        borderRadius: Corners.s6Border,
+      ),
+      hoverColor: widget.pickerItemHoverColor,
+      onTap: () {
+        widget.onSubmittedbackgroundColorHex(option.colorHex);
+      },
+      child: Padding(
+        padding: const EdgeInsets.only(right: 10.0),
+        child: SizedBox.square(
+          dimension: 25,
+          child: Container(
+            decoration: BoxDecoration(
+              color: isChecked
+                  ? Colors.transparent
+                  : Color(int.tryParse(option.colorHex) ?? 0xFFFFFFFF),
+              border: isChecked
+                  ? Border.all(
+                      color: Color(int.tryParse(option.colorHex) ?? 0xFFFFFF))
+                  : null,
+              shape: BoxShape.circle,
+            ),
+            child: isChecked
+                ? SizedBox.square(
+                    dimension: 25,
+                    child: Container(
+                      margin: const EdgeInsets.all(4),
+                      decoration: BoxDecoration(
+                        color:
+                            Color(int.tryParse(option.colorHex) ?? 0xFFFFFFFF),
+                        shape: BoxShape.circle,
+                      ),
+                    ),
+                  )
+                : null,
+          ),
+        ),
+      ),
+    );
+  }
+
+  Widget _buildColorItems(List<ColorOption> options, String? selectedColor) {
+    return Row(
+      crossAxisAlignment: CrossAxisAlignment.start,
+      mainAxisAlignment: MainAxisAlignment.start,
+      children: options
+          .map((e) => _buildColorItem(e, e.colorHex == selectedColor))
+          .toList(),
+    );
+  }
+}
+
+extension on Color {
+  String toHex() {
+    return '0x${value.toRadixString(16)}';
+  }
+}

+ 302 - 0
frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/cover/cover_node_widget.dart

@@ -0,0 +1,302 @@
+import 'dart:io';
+
+import 'package:appflowy/generated/locale_keys.g.dart';
+import 'package:appflowy/plugins/document/presentation/plugins/cover/change_cover_popover.dart';
+import 'package:appflowy_editor/appflowy_editor.dart';
+import 'package:appflowy_popover/appflowy_popover.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flowy_infra/image.dart';
+import 'package:flowy_infra/size.dart';
+import 'package:flowy_infra_ui/flowy_infra_ui.dart';
+import 'package:flowy_infra_ui/style_widget/button.dart';
+import 'package:flowy_infra_ui/style_widget/icon_button.dart';
+import 'package:flowy_infra_ui/widget/rounded_button.dart';
+import 'package:flutter/material.dart';
+
+const String kCoverType = 'cover';
+const String kCoverSelectionTypeAttribute = 'cover_selection_type';
+const String kCoverSelectionAttribute = 'cover_selection';
+
+enum CoverSelectionType {
+  initial,
+
+  color,
+  file,
+  asset;
+
+  static CoverSelectionType fromString(String? value) {
+    if (value == null) {
+      return CoverSelectionType.initial;
+    }
+    return CoverSelectionType.values.firstWhere(
+      (e) => e.toString() == value,
+      orElse: () => CoverSelectionType.initial,
+    );
+  }
+}
+
+class CoverNodeWidgetBuilder implements NodeWidgetBuilder {
+  @override
+  Widget build(NodeWidgetContext<Node> context) {
+    return _CoverImageNodeWidget(
+      key: context.node.key,
+      node: context.node,
+      editorState: context.editorState,
+    );
+  }
+
+  @override
+  NodeValidator<Node> get nodeValidator => (node) {
+        return true;
+      };
+}
+
+class _CoverImageNodeWidget extends StatefulWidget {
+  const _CoverImageNodeWidget({
+    Key? key,
+    required this.node,
+    required this.editorState,
+  }) : super(key: key);
+
+  final Node node;
+  final EditorState editorState;
+
+  @override
+  State<_CoverImageNodeWidget> createState() => _CoverImageNodeWidgetState();
+}
+
+class _CoverImageNodeWidgetState extends State<_CoverImageNodeWidget> {
+  CoverSelectionType get selectionType => CoverSelectionType.fromString(
+        widget.node.attributes[kCoverSelectionTypeAttribute],
+      );
+
+  @override
+  Widget build(BuildContext context) {
+    if (selectionType == CoverSelectionType.initial) {
+      return _AddCoverButton(
+        onTap: () {
+          _insertCover(CoverSelectionType.asset, builtInAssetImages.first);
+        },
+      );
+    } else {
+      return _CoverImage(
+        editorState: widget.editorState,
+        node: widget.node,
+        onCoverChanged: (type, value) {
+          _insertCover(type, value);
+        },
+      );
+    }
+  }
+
+  Future<void> _insertCover(CoverSelectionType type, dynamic cover) async {
+    final transaction = widget.editorState.transaction;
+    transaction.updateNode(widget.node, {
+      kCoverSelectionTypeAttribute: type.toString(),
+      kCoverSelectionAttribute: cover,
+    });
+    return widget.editorState.apply(transaction);
+  }
+}
+
+class _AddCoverButton extends StatefulWidget {
+  const _AddCoverButton({
+    required this.onTap,
+  });
+
+  final VoidCallback onTap;
+
+  @override
+  State<_AddCoverButton> createState() => _AddCoverButtonState();
+}
+
+class _AddCoverButtonState extends State<_AddCoverButton> {
+  bool isHidden = true;
+
+  @override
+  Widget build(BuildContext context) {
+    return MouseRegion(
+      onEnter: (event) {
+        setHidden(false);
+      },
+      onExit: (event) {
+        setHidden(true);
+      },
+      child: Container(
+        height: 50.0,
+        width: double.infinity,
+        padding: const EdgeInsets.only(top: 20, bottom: 5),
+        // color: Colors.red,
+        child: isHidden
+            ? const SizedBox()
+            : Row(
+                mainAxisSize: MainAxisSize.min,
+                mainAxisAlignment: MainAxisAlignment.start,
+                children: [
+                  // Add Cover Button.
+                  FlowyButton(
+                    leftIconSize: const Size.square(18),
+                    onTap: widget.onTap,
+                    useIntrinsicWidth: true,
+                    leftIcon: svgWidget(
+                      'editor/image',
+                      color: Theme.of(context).colorScheme.onSurface,
+                    ),
+                    text: FlowyText.regular(
+                      LocaleKeys.document_plugins_cover_addCover.tr(),
+                    ),
+                  )
+                  // Add Icon Button.
+                  // ...
+                ],
+              ),
+      ),
+    );
+  }
+
+  void setHidden(bool value) {
+    if (isHidden == value) return;
+    setState(() {
+      isHidden = value;
+    });
+  }
+}
+
+class _CoverImage extends StatefulWidget {
+  const _CoverImage({
+    required this.editorState,
+    required this.node,
+    required this.onCoverChanged,
+  });
+
+  final Node node;
+  final EditorState editorState;
+  final Function(
+    CoverSelectionType selectionType,
+    dynamic selection,
+  ) onCoverChanged;
+
+  @override
+  State<_CoverImage> createState() => _CoverImageState();
+}
+
+class _CoverImageState extends State<_CoverImage> {
+  final popoverController = PopoverController();
+
+  CoverSelectionType get selectionType => CoverSelectionType.fromString(
+        widget.node.attributes[kCoverSelectionTypeAttribute],
+      );
+  Color get color =>
+      Color(int.tryParse(widget.node.attributes[kCoverSelectionAttribute]) ??
+          0xFFFFFFFF);
+
+  bool isOverlayButtonsHidden = true;
+
+  @override
+  Widget build(BuildContext context) {
+    return Stack(
+      children: [
+        _buildCoverImage(context),
+        _buildCoverOverlayButtons(context),
+      ],
+    );
+  }
+
+  Widget _buildCoverOverlayButtons(BuildContext context) {
+    return Positioned(
+      bottom: 22,
+      right: 12,
+      child: Row(
+        mainAxisSize: MainAxisSize.min,
+        children: [
+          AppFlowyPopover(
+            offset: const Offset(-125, 10),
+            controller: popoverController,
+            direction: PopoverDirection.bottomWithCenterAligned,
+            constraints: BoxConstraints.loose(const Size(380, 450)),
+            margin: EdgeInsets.zero,
+            child: RoundedTextButton(
+              onPressed: () {
+                popoverController.show();
+              },
+              hoverColor: Theme.of(context).colorScheme.surface,
+              textColor: Theme.of(context).colorScheme.onSurface,
+              fillColor: Theme.of(context).colorScheme.surface.withOpacity(0.8),
+              width: 120,
+              height: 28,
+              title: LocaleKeys.document_plugins_cover_changeCover.tr(),
+            ),
+            popupBuilder: (BuildContext popoverContext) {
+              return ChangeCoverPopover(
+                node: widget.node,
+                editorState: widget.editorState,
+                onCoverChanged: widget.onCoverChanged,
+              );
+            },
+          ),
+          const SizedBox(width: 10),
+          FlowyIconButton(
+            fillColor: Theme.of(context).colorScheme.surface.withOpacity(0.8),
+            hoverColor: Theme.of(context).colorScheme.surface,
+            iconPadding: const EdgeInsets.all(5),
+            width: 28,
+            icon: svgWidget(
+              'editor/delete',
+              color: Theme.of(context).colorScheme.onSurface,
+            ),
+            onPressed: () {
+              widget.onCoverChanged(CoverSelectionType.initial, null);
+            },
+          ),
+        ],
+      ),
+    );
+  }
+
+  Widget _buildCoverImage(BuildContext context) {
+    final screenSize = MediaQuery.of(context).size;
+    const height = 200.0;
+    final Widget coverImage;
+    switch (selectionType) {
+      case CoverSelectionType.file:
+        coverImage = Image.file(
+          File(widget.node.attributes[kCoverSelectionAttribute]),
+          fit: BoxFit.cover,
+        );
+        break;
+      case CoverSelectionType.asset:
+        coverImage = Image.asset(
+          widget.node.attributes[kCoverSelectionAttribute],
+          fit: BoxFit.cover,
+        );
+        break;
+      case CoverSelectionType.color:
+        coverImage = Container(
+          decoration: BoxDecoration(
+            color: color,
+            borderRadius: Corners.s6Border,
+          ),
+          alignment: Alignment.center,
+        );
+        break;
+      case CoverSelectionType.initial:
+        coverImage = const SizedBox(); // just an empty sizebox
+        break;
+    }
+    return UnconstrainedBox(
+      child: Container(
+        padding: const EdgeInsets.only(bottom: 10),
+        height: height,
+        width: screenSize.width,
+        child: coverImage,
+      ),
+    );
+  }
+
+  void setOverlayButtonsHidden(bool value) {
+    if (isOverlayButtonsHidden == value) return;
+    setState(() {
+      isOverlayButtonsHidden = value;
+    });
+  }
+}

+ 4 - 2
frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/service/openai_client.dart

@@ -37,7 +37,7 @@ abstract class OpenAIRepository {
   Future<Either<OpenAIError, TextCompletionResponse>> getCompletions({
     required String prompt,
     String? suffix,
-    int maxTokens = 50,
+    int maxTokens = 500,
     double temperature = .3,
   });
 
@@ -72,7 +72,7 @@ class HttpOpenAIRepository implements OpenAIRepository {
   Future<Either<OpenAIError, TextCompletionResponse>> getCompletions({
     required String prompt,
     String? suffix,
-    int maxTokens = 50,
+    int maxTokens = 500,
     double temperature = 0.3,
   }) async {
     final parameters = {
@@ -102,12 +102,14 @@ class HttpOpenAIRepository implements OpenAIRepository {
     required String input,
     required String instruction,
     double temperature = 0.3,
+    int n = 1,
   }) async {
     final parameters = {
       'model': 'text-davinci-edit-001',
       'input': input,
       'instruction': instruction,
       'temperature': temperature,
+      'n': n,
     };
 
     final response = await client.post(

+ 2 - 2
frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/smart_edit_action.dart

@@ -10,9 +10,9 @@ enum SmartEditAction {
   String get toInstruction {
     switch (this) {
       case SmartEditAction.summarize:
-        return 'Summarize';
+        return 'Make it shorter';
       case SmartEditAction.fixSpelling:
-        return 'Fix the spelling mistakes';
+        return 'Fix all the spelling mistakes';
     }
   }
 }

+ 1 - 0
frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/smart_edit_node_widget.dart

@@ -254,6 +254,7 @@ class _SmartEditInputState extends State<_SmartEditInput> {
       final edits = await openAIRepository.getEdits(
         input: input,
         instruction: instruction,
+        n: input.split('\n').length,
       );
       return edits.fold((error) async {
         return dartz.Left(

+ 41 - 34
frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/smart_edit_toolbar_item.dart

@@ -42,11 +42,13 @@ class _SmartEditWidgetState extends State<_SmartEditWidget> {
           .toList(),
       buildChild: (controller) {
         return FlowyIconButton(
+          hoverColor: Colors.transparent,
           tooltipText: 'Smart Edit',
           preferBelow: false,
           icon: const Icon(
-            Icons.edit,
-            size: 14,
+            Icons.lightbulb_outline,
+            size: 13,
+            color: Colors.white,
           ),
           onPressed: () {
             controller.show();
@@ -55,39 +57,44 @@ class _SmartEditWidgetState extends State<_SmartEditWidget> {
       },
       onSelected: (action, controller) {
         controller.close();
-        final selection =
-            widget.editorState.service.selectionService.currentSelection.value;
-        if (selection == null) {
-          return;
-        }
-        final textNodes = widget
-            .editorState.service.selectionService.currentSelectedNodes
-            .whereType<TextNode>()
-            .toList(growable: false);
-        final input = widget.editorState.getTextInSelection(
-          textNodes.normalized,
-          selection.normalized,
-        );
-        final transaction = widget.editorState.transaction;
-        transaction.insertNode(
-          selection.normalized.end.path.next,
-          Node(
-            type: kSmartEditType,
-            attributes: {
-              kSmartEditInstructionType: action.inner.toInstruction,
-              kSmartEditInputType: input,
-            },
-          ),
-        );
-        widget.editorState.apply(
-          transaction,
-          options: const ApplyOptions(
-            recordUndo: false,
-            recordRedo: false,
-          ),
-          withUpdateCursor: false,
-        );
+        _insertSmartEditNode(action);
       },
     );
   }
+
+  Future<void> _insertSmartEditNode(
+      SmartEditActionWrapper actionWrapper) async {
+    final selection =
+        widget.editorState.service.selectionService.currentSelection.value;
+    if (selection == null) {
+      return;
+    }
+    final textNodes = widget
+        .editorState.service.selectionService.currentSelectedNodes
+        .whereType<TextNode>()
+        .toList(growable: false);
+    final input = widget.editorState.getTextInSelection(
+      textNodes.normalized,
+      selection.normalized,
+    );
+    final transaction = widget.editorState.transaction;
+    transaction.insertNode(
+      selection.normalized.end.path.next,
+      Node(
+        type: kSmartEditType,
+        attributes: {
+          kSmartEditInstructionType: actionWrapper.inner.toInstruction,
+          kSmartEditInputType: input,
+        },
+      ),
+    );
+    return widget.editorState.apply(
+      transaction,
+      options: const ApplyOptions(
+        recordUndo: false,
+        recordRedo: false,
+      ),
+      withUpdateCursor: false,
+    );
+  }
 }

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

@@ -1,4 +1,5 @@
 import 'package:appflowy/core/network_monitor.dart';
+import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
 import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
 import 'package:appflowy/plugins/database_view/application/field/field_action_sheet_bloc.dart';
 import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';

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

@@ -1,9 +1,10 @@
 import 'package:appflowy/plugins/document/document.dart';
+import 'package:appflowy/plugins/document/presentation/plugins/cover/cover_node_widget.dart';
 import 'package:appflowy/startup/plugin/plugin.dart';
 import 'package:appflowy/startup/startup.dart';
 import 'package:appflowy/workspace/presentation/home/menu/app/header/import/import_panel.dart';
 import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
-import 'package:appflowy_editor/appflowy_editor.dart' show Document;
+import 'package:appflowy_editor/appflowy_editor.dart' show Document, Node;
 import 'package:appflowy_popover/appflowy_popover.dart';
 import 'package:flowy_infra/image.dart';
 import 'package:flowy_infra_ui/style_widget/icon_button.dart';
@@ -60,7 +61,12 @@ class AddButton extends StatelessWidget {
       },
       onSelected: (action, controller) {
         if (action is AddButtonActionWrapper) {
-          onSelected(action.pluginBuilder, null);
+          Document? document;
+          if (action.pluginType == PluginType.editor) {
+            // initialize the document if needed.
+            document = buildInitialDocument();
+          }
+          onSelected(action.pluginBuilder, document);
         }
         if (action is ImportActionWrapper) {
           showImportPanel(context, (document) {
@@ -74,6 +80,12 @@ class AddButton extends StatelessWidget {
       },
     );
   }
+
+  Document buildInitialDocument() {
+    final document = Document.empty();
+    document.insert([0], [Node(type: kCoverType)]);
+    return document;
+  }
 }
 
 class AddButtonActionWrapper extends ActionCell {
@@ -87,6 +99,8 @@ class AddButtonActionWrapper extends ActionCell {
 
   @override
   String get name => pluginBuilder.menuName;
+
+  PluginType get pluginType => pluginBuilder.pluginType;
 }
 
 class ImportActionWrapper extends ActionCell {

+ 68 - 54
frontend/appflowy_flutter/packages/appflowy_backend/windows/appflowy_backend_plugin.cpp

@@ -1,4 +1,3 @@
-#include "include/appflowy_backend/appflowy_flutter_backend_plugin.h"
 
 // This must be included before many other Windows headers.
 #include <windows.h>
@@ -13,70 +12,85 @@
 #include <map>
 #include <memory>
 #include <sstream>
+#include "include/appflowy_backend/app_flowy_backend_plugin.h"
 
-namespace {
+namespace
+{
 
-class AppFlowyBackendPlugin : public flutter::Plugin {
- public:
-  static void RegisterWithRegistrar(flutter::PluginRegistrarWindows *registrar);
+  class AppFlowyBackendPlugin : public flutter::Plugin
+  {
+  public:
+    static void RegisterWithRegistrar(flutter::PluginRegistrarWindows *registrar);
 
-  AppFlowyBackendPlugin();
+    AppFlowyBackendPlugin();
 
-  virtual ~AppFlowyBackendPlugin();
+    virtual ~AppFlowyBackendPlugin();
 
- private:
-  // Called when a method is called on this plugin's channel from Dart.
-  void HandleMethodCall(
+  private:
+    // Called when a method is called on this plugin's channel from Dart.
+    void HandleMethodCall(
+        const flutter::MethodCall<flutter::EncodableValue> &method_call,
+        std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result);
+  };
+
+  // static
+  void AppFlowyBackendPlugin::RegisterWithRegistrar(
+      flutter::PluginRegistrarWindows *registrar)
+  {
+    auto channel =
+        std::make_unique<flutter::MethodChannel<flutter::EncodableValue>>(
+            registrar->messenger(), "appflowy_backend",
+            &flutter::StandardMethodCodec::GetInstance());
+
+    auto plugin = std::make_unique<AppFlowyBackendPlugin>();
+
+    channel->SetMethodCallHandler(
+        [plugin_pointer = plugin.get()](const auto &call, auto result)
+        {
+          plugin_pointer->HandleMethodCall(call, std::move(result));
+        });
+
+    registrar->AddPlugin(std::move(plugin));
+  }
+
+  AppFlowyBackendPlugin::AppFlowyBackendPlugin() {}
+
+  AppFlowyBackendPlugin::~AppFlowyBackendPlugin() {}
+
+  void AppFlowyBackendPlugin::HandleMethodCall(
       const flutter::MethodCall<flutter::EncodableValue> &method_call,
-      std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result);
-};
-
-// static
-void AppFlowyBackendPlugin::RegisterWithRegistrar(
-    flutter::PluginRegistrarWindows *registrar) {
-  auto channel =
-      std::make_unique<flutter::MethodChannel<flutter::EncodableValue>>(
-          registrar->messenger(), "appflowy_backend",
-          &flutter::StandardMethodCodec::GetInstance());
-
-  auto plugin = std::make_unique<AppFlowyBackendPlugin>();
-
-  channel->SetMethodCallHandler(
-      [plugin_pointer = plugin.get()](const auto &call, auto result) {
-        plugin_pointer->HandleMethodCall(call, std::move(result));
-      });
-
-  registrar->AddPlugin(std::move(plugin));
-}
-
-AppFlowyBackendPlugin::AppFlowyBackendPlugin() {}
-
-AppFlowyBackendPlugin::~AppFlowyBackendPlugin() {}
-
-void AppFlowyBackendPlugin::HandleMethodCall(
-    const flutter::MethodCall<flutter::EncodableValue> &method_call,
-    std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result) {
-  if (method_call.method_name().compare("getPlatformVersion") == 0) {
-    std::ostringstream version_stream;
-    version_stream << "Windows ";
-    if (IsWindows10OrGreater()) {
-      version_stream << "10+";
-    } else if (IsWindows8OrGreater()) {
-      version_stream << "8";
-    } else if (IsWindows7OrGreater()) {
-      version_stream << "7";
+      std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result)
+  {
+    if (method_call.method_name().compare("getPlatformVersion") == 0)
+    {
+      std::ostringstream version_stream;
+      version_stream << "Windows ";
+      if (IsWindows10OrGreater())
+      {
+        version_stream << "10+";
+      }
+      else if (IsWindows8OrGreater())
+      {
+        version_stream << "8";
+      }
+      else if (IsWindows7OrGreater())
+      {
+        version_stream << "7";
+      }
+      result->Success(flutter::EncodableValue(version_stream.str()));
+    }
+    else
+    {
+      result->NotImplemented();
     }
-    result->Success(flutter::EncodableValue(version_stream.str()));
-  } else {
-    result->NotImplemented();
   }
-}
 
-}  // namespace
+} // namespace
 
 void AppFlowyBackendPluginRegisterWithRegistrar(
-    FlutterDesktopPluginRegistrarRef registrar) {
+    FlutterDesktopPluginRegistrarRef registrar)
+{
   AppFlowyBackendPlugin::RegisterWithRegistrar(
       flutter::PluginRegistrarManager::GetInstance()
           ->GetRegistrar<flutter::PluginRegistrarWindows>(registrar));
-}
+}

+ 5 - 4
frontend/appflowy_flutter/packages/appflowy_backend/windows/appflowy_backend_plugin_c_api.cpp

@@ -5,8 +5,9 @@
 #include "appflowy_flutter_backend_plugin.h"
 
 void AppFlowyBackendPluginCApiRegisterWithRegistrar(
-    FlutterDesktopPluginRegistrarRef registrar) {
-  appflowy_backend::AppFlowyBackendPlugin::RegisterWithRegistrar(
-      flutter::PluginRegistrarManager::GetInstance()
-          ->GetRegistrar<flutter::PluginRegistrarWindows>(registrar));
+    FlutterDesktopPluginRegistrarRef registrar)
+{
+    appflowy_backend::AppFlowyBackendPlugin::RegisterWithRegistrar(
+        flutter::PluginRegistrarManager::GetInstance()
+            ->GetRegistrar<flutter::PluginRegistrarWindows>(registrar));
 }

+ 9 - 3
frontend/appflowy_flutter/packages/appflowy_editor/lib/src/commands/command_extension.dart

@@ -59,12 +59,18 @@ extension CommandExtension on EditorState {
     List<String> res = [];
     if (!selection.isCollapsed) {
       for (var i = 0; i < textNodes.length; i++) {
+        final plainText = textNodes[i].toPlainText();
         if (i == 0) {
-          res.add(textNodes[i].toPlainText().substring(selection.startIndex));
+          res.add(
+            plainText.substring(
+              selection.startIndex,
+              plainText.length,
+            ),
+          );
         } else if (i == textNodes.length - 1) {
-          res.add(textNodes[i].toPlainText().substring(0, selection.endIndex));
+          res.add(plainText.substring(0, selection.endIndex));
         } else {
-          res.add(textNodes[i].toPlainText());
+          res.add(plainText);
         }
       }
     }

+ 4 - 1
frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/editor_service.dart

@@ -32,6 +32,7 @@ class AppFlowyEditor extends StatefulWidget {
     this.toolbarItems = const [],
     this.editable = true,
     this.autoFocus = false,
+    this.focusedSelection,
     this.customActionMenuBuilder,
     ThemeData? themeData,
   }) : super(key: key) {
@@ -60,6 +61,7 @@ class AppFlowyEditor extends StatefulWidget {
 
   /// Set the value to true to focus the editor on the start of the document.
   final bool autoFocus;
+  final Selection? focusedSelection;
 
   final Positioned Function(BuildContext context, List<ActionMenuItem> items)?
       customActionMenuBuilder;
@@ -89,7 +91,8 @@ class _AppFlowyEditorState extends State<AppFlowyEditor> {
     WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
       if (widget.editable && widget.autoFocus) {
         editorState.service.selectionService.updateSelection(
-          Selection.single(path: [0], startOffset: 0),
+          widget.focusedSelection ??
+              Selection.single(path: [0], startOffset: 0),
         );
       }
     });

+ 2 - 0
frontend/appflowy_flutter/packages/appflowy_editor_plugins/lib/appflowy_editor_plugins.dart

@@ -12,3 +12,5 @@ export 'src/divider/divider_shortcut_event.dart';
 export 'src/emoji_picker/emoji_menu_item.dart';
 // Math Equation
 export 'src/math_ equation/math_equation_node_widget.dart';
+
+export 'src/extensions/theme_extension.dart';

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

@@ -1,4 +1,4 @@
-import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
+import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
 import 'package:appflowy/plugins/database_view/application/setting/group_bloc.dart';
 import 'package:appflowy/plugins/database_view/board/application/board_bloc.dart';
 import 'package:appflowy/plugins/database_view/grid/application/cell/select_option_editor_bloc.dart';

+ 2 - 4
frontend/appflowy_flutter/test/bloc_test/board_test/util.dart

@@ -1,4 +1,5 @@
-import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
+import 'package:appflowy/plugins/database_view/application/cell/cell_controller.dart';
+import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
 import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
 import 'package:appflowy/plugins/database_view/application/field/field_editor_bloc.dart';
 import 'package:appflowy/plugins/database_view/application/field/field_service.dart';
@@ -105,11 +106,9 @@ class BoardTestContext {
   ) async {
     final RowInfo rowInfo = rowInfos.last;
     final rowCache = _boardDataController.rowCache;
-    final fieldController = _boardDataController.fieldController;
 
     final rowDataController = RowDataController(
       rowInfo: rowInfo,
-      fieldController: fieldController,
       rowCache: rowCache,
     );
 
@@ -122,7 +121,6 @@ class BoardTestContext {
     return CellControllerBuilder(
       cellId: rowBloc.state.cellByFieldId[fieldId]!,
       cellCache: rowCache.cellCache,
-      delegate: rowDataController,
     );
   }
 

+ 6 - 6
frontend/appflowy_flutter/test/bloc_test/grid_test/filter/create_filter_test.dart

@@ -44,7 +44,7 @@ void main() {
     );
     await gridResponseFuture();
 
-    assert(context.fieldController.filterInfos.isEmpty);
+    expect(context.fieldController.filterInfos.length, 0);
   });
 
   test('filter rows with condition: text is empty', () async {
@@ -53,7 +53,7 @@ void main() {
     final gridController = DatabaseController(view: context.gridView);
     final gridBloc = GridBloc(
       view: context.gridView,
-      gridController: gridController,
+      databaseController: gridController,
     )..add(const GridEvent.initial());
     await gridResponseFuture();
 
@@ -64,7 +64,7 @@ void main() {
         content: "");
     await gridResponseFuture();
 
-    assert(gridBloc.state.rowInfos.length == 3);
+    expect(gridBloc.state.rowInfos.length, 3);
   });
 
   test('filter rows with condition: text is empty(After edit the row)',
@@ -74,7 +74,7 @@ void main() {
     final gridController = DatabaseController(view: context.gridView);
     final gridBloc = GridBloc(
       view: context.gridView,
-      gridController: gridController,
+      databaseController: gridController,
     )..add(const GridEvent.initial());
     await gridResponseFuture();
 
@@ -115,7 +115,7 @@ void main() {
     final gridController = DatabaseController(view: context.gridView);
     final gridBloc = GridBloc(
       view: context.gridView,
-      gridController: gridController,
+      databaseController: gridController,
     )..add(const GridEvent.initial());
 
     await gridResponseFuture();
@@ -134,7 +134,7 @@ void main() {
     final gridController = DatabaseController(view: context.gridView);
     final gridBloc = GridBloc(
       view: context.gridView,
-      gridController: gridController,
+      databaseController: gridController,
     )..add(const GridEvent.initial());
 
     await gridResponseFuture();

+ 2 - 2
frontend/appflowy_flutter/test/bloc_test/grid_test/grid_bloc_test.dart

@@ -20,7 +20,7 @@ void main() {
       "create a row",
       build: () => GridBloc(
           view: context.gridView,
-          gridController: DatabaseController(view: context.gridView))
+          databaseController: DatabaseController(view: context.gridView))
         ..add(const GridEvent.initial()),
       act: (bloc) => bloc.add(const GridEvent.createRow()),
       wait: const Duration(milliseconds: 300),
@@ -33,7 +33,7 @@ void main() {
       "delete the last row",
       build: () => GridBloc(
           view: context.gridView,
-          gridController: DatabaseController(view: context.gridView))
+          databaseController: DatabaseController(view: context.gridView))
         ..add(const GridEvent.initial()),
       act: (bloc) async {
         await gridResponseFuture();

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

@@ -1,4 +1,5 @@
-import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
+import 'package:appflowy/plugins/database_view/application/cell/cell_controller.dart';
+import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
 import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
 import 'package:appflowy/plugins/database_view/application/field/field_editor_bloc.dart';
 import 'package:appflowy/plugins/database_view/application/field/field_service.dart';
@@ -66,11 +67,9 @@ class GridTestContext {
   ) async {
     final RowInfo rowInfo = rowInfos[rowIndex];
     final rowCache = gridController.rowCache;
-    final fieldController = gridController.fieldController;
 
     final rowDataController = RowDataController(
       rowInfo: rowInfo,
-      fieldController: fieldController,
       rowCache: rowCache,
     );
 
@@ -83,7 +82,6 @@ class GridTestContext {
     return CellControllerBuilder(
       cellId: rowBloc.state.cellByFieldId[fieldId]!,
       cellCache: rowCache.cellCache,
-      delegate: rowDataController,
     );
   }
 

+ 0 - 2
frontend/appflowy_tauri/src/appflowy_app/App.tsx

@@ -1,7 +1,6 @@
 import { Routes, Route, BrowserRouter } from 'react-router-dom';
 
 import { TestColors } from './components/TestColors/TestColors';
-import { Welcome } from './views/Welcome';
 import { Provider } from 'react-redux';
 import { store } from './stores/store';
 import { DocumentPage } from './views/DocumentPage';
@@ -29,7 +28,6 @@ const App = () => {
             <Route path={'/page/document/:id'} element={<DocumentPage />} />
             <Route path={'/page/board/:id'} element={<BoardPage />} />
             <Route path={'/page/grid/:id'} element={<GridPage />} />
-            <Route path={'/'} element={<Welcome />} />
           </Route>
           <Route path={'/auth/login'} element={<LoginPage />}></Route>
           <Route path={'/auth/getStarted'} element={<GetStarted />}></Route>

BIN
frontend/appflowy_tauri/src/appflowy_app/assets/launch_splash.jpg


+ 1 - 2
frontend/appflowy_tauri/src/appflowy_app/components/TestApiButton/TestAPI.tsx

@@ -1,5 +1,4 @@
 import React from 'react';
-import TestApiButton from './TestApiButton';
 import {
   TestCreateGrid,
   TestCreateNewField,
@@ -18,7 +17,7 @@ export const TestAPI = () => {
   return (
     <React.Fragment>
       <ul className='m-6, space-y-2'>
-        <TestApiButton></TestApiButton>
+        {/*<TestApiButton></TestApiButton>*/}
         <TestCreateGrid></TestCreateGrid>
         <TestCreateRow></TestCreateRow>
         <TestDeleteRow></TestDeleteRow>

+ 4 - 2
frontend/appflowy_tauri/src/appflowy_app/components/TestApiButton/TestGrid.tsx

@@ -126,8 +126,10 @@ export const TestCreateSelectOptionInCell = () => {
         );
         await cellController.subscribeChanged({
           onCellChanged: (value) => {
-            const option: SelectOptionCellDataPB = value.unwrap();
-            console.log(option);
+            if (value.some) {
+              const option: SelectOptionCellDataPB = value.unwrap();
+              console.log(option);
+            }
           },
         });
         const backendSvc = new SelectOptionCellBackendService(cellController.cellIdentifier);

+ 50 - 10
frontend/appflowy_tauri/src/appflowy_app/components/auth/ProtectedRoutes.tsx

@@ -1,16 +1,56 @@
-import { Navigate, Outlet, useLocation } from 'react-router-dom';
+import { Outlet } from 'react-router-dom';
 import { useAuth } from './auth.hooks';
 import { Screen } from '../layout/Screen';
+import { useEffect, useState } from 'react';
+import { GetStarted } from './GetStarted/GetStarted';
+import { AppflowyLogo } from '../_shared/svg/AppflowyLogo';
+
 
 export const ProtectedRoutes = () => {
-  const location = useLocation();
-  const { currentUser } = useAuth();
-
-  return currentUser.isAuthenticated ? (
-    <Screen>
-      <Outlet />
-    </Screen>
-  ) : (
-    <Navigate to='/auth/getStarted' replace state={{ from: location }} />
+  const { currentUser, checkUser } = useAuth();
+  const [isLoading, setIsLoading] = useState(true);
+
+  useEffect(() => {
+    void checkUser().then(async (result) => {
+      await new Promise(() =>
+          setTimeout(() => {
+            setIsLoading(false);
+          }, 1200)
+      );
+
+      if (result.err) {
+        throw new Error(result.val.msg);
+      }
+
+    });
+  }, []);
+
+  if (isLoading) {
+    // It's better to make a fading effect to disappear the loading page
+    return <StartLoading />;
+  } else {
+    return <SplashScreen isAuthenticated={currentUser.isAuthenticated} />;
+  }
+};
+
+const StartLoading = () => {
+  return (
+    <div className='flex h-screen w-full flex-col items-center justify-center'>
+      <div className='h-40 w-40 justify-center'>
+        <AppflowyLogo />
+      </div>
+    </div>
   );
 };
+
+const SplashScreen = ({ isAuthenticated }: { isAuthenticated: boolean }) => {
+  if (isAuthenticated) {
+    return (
+      <Screen>
+        <Outlet />
+      </Screen>
+    );
+  } else {
+    return <GetStarted></GetStarted>;
+  }
+};

+ 35 - 9
frontend/appflowy_tauri/src/appflowy_app/components/auth/auth.hooks.ts

@@ -1,20 +1,46 @@
 import { currentUserActions } from '../../stores/reducers/current-user/slice';
 import { useAppDispatch, useAppSelector } from '../../stores/store';
 import { UserProfilePB } from '../../../services/backend/events/flowy-user';
-import { AuthBackendService } from '../../stores/effects/user/user_bd_svc';
+import { AuthBackendService, UserBackendService } from '../../stores/effects/user/user_bd_svc';
 import { FolderEventReadCurrentWorkspace } from '../../../services/backend/events/flowy-folder';
 import { WorkspaceSettingPB } from '../../../services/backend/models/flowy-folder/workspace';
+import { Log } from '../../utils/log';
 
 export const useAuth = () => {
   const dispatch = useAppDispatch();
   const currentUser = useAppSelector((state) => state.currentUser);
   const authBackendService = new AuthBackendService();
 
+  async function checkUser() {
+    const result = await UserBackendService.checkUser();
+    if (result.ok) {
+      const userProfile = result.val;
+      const workspaceSetting = await _openWorkspace().then((r) => {
+        if (r.ok) {
+          return r.val;
+        } else {
+          return undefined;
+        }
+      });
+      dispatch(
+        currentUserActions.checkUser({
+          id: userProfile.id,
+          token: userProfile.token,
+          email: userProfile.email,
+          displayName: userProfile.name,
+          isAuthenticated: true,
+          workspaceSetting: workspaceSetting,
+        })
+      );
+    }
+    return result;
+  }
+
   async function register(email: string, password: string, name: string): Promise<UserProfilePB> {
     const authResult = await authBackendService.signUp({ email, password, name });
 
     if (authResult.ok) {
-      const { id, token } = authResult.val;
+      const userProfile = authResult.val;
       // Get the workspace setting after user registered. The workspace setting
       // contains the latest visiting view and the current workspace data.
       const openWorkspaceResult = await _openWorkspace();
@@ -22,10 +48,10 @@ export const useAuth = () => {
         const workspaceSetting: WorkspaceSettingPB = openWorkspaceResult.val;
         dispatch(
           currentUserActions.updateUser({
-            id: id,
-            token: token,
-            email,
-            displayName: name,
+            id: userProfile.id,
+            token: userProfile.token,
+            email: userProfile.email,
+            displayName: userProfile.name,
             isAuthenticated: true,
             workspaceSetting: workspaceSetting,
           })
@@ -33,7 +59,7 @@ export const useAuth = () => {
       }
       return authResult.val;
     } else {
-      console.error(authResult.val.msg);
+      Log.error(authResult.val.msg);
       throw new Error(authResult.val.msg);
     }
   }
@@ -53,7 +79,7 @@ export const useAuth = () => {
       );
       return result.val;
     } else {
-      console.error(result.val.msg);
+      Log.error(result.val.msg);
       throw new Error(result.val.msg);
     }
   }
@@ -67,5 +93,5 @@ export const useAuth = () => {
     return FolderEventReadCurrentWorkspace();
   }
 
-  return { currentUser, register, login, logout };
+  return { currentUser, checkUser, register, login, logout };
 };

+ 2 - 2
frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/NavigationFloatingPanel.tsx

@@ -1,5 +1,5 @@
 import { AppLogo } from '../AppLogo';
-import { Workspace } from '../Workspace';
+import { WorkspaceUser } from '../WorkspaceUser';
 import { FolderItem } from './FolderItem';
 import { PluginsButton } from './PluginsButton';
 import { TrashButton } from './TrashButton';
@@ -52,7 +52,7 @@ export const NavigationFloatingPanel = ({
       <div className={'flex flex-col'}>
         <AppLogo iconToShow={'show'} onShowMenuClick={onFixNavigationClick}></AppLogo>
 
-        <Workspace></Workspace>
+        <WorkspaceUser></WorkspaceUser>
 
         <div className={'flex flex-col px-2'}>
           {folders.map((folder, index) => (

+ 61 - 33
frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/NavigationPanel.tsx

@@ -1,26 +1,22 @@
-import { Workspace } from '../Workspace';
+import { WorkspaceUser } from '../WorkspaceUser';
 import { AppLogo } from '../AppLogo';
 import { FolderItem } from './FolderItem';
-import { PluginsButton } from './PluginsButton';
 import { TrashButton } from './TrashButton';
 import { NewFolderButton } from './NewFolderButton';
 import { NavigationResizer } from './NavigationResizer';
 import { IFolder } from '../../../stores/reducers/folders/slice';
 import { IPage } from '../../../stores/reducers/pages/slice';
-
-const MINIMUM_WIDTH = 200;
-const ANIMATION_DURATION = 300;
+import { useNavigate } from 'react-router-dom';
+import React from 'react';
 
 export const NavigationPanel = ({
-  onHideMenuClick,
-  menuHidden,
+  onCollapseNavigationClick,
   width,
   folders,
   pages,
   onPageClick,
 }: {
-  onHideMenuClick: () => void;
-  menuHidden: boolean;
+  onCollapseNavigationClick: () => void;
   width: number;
   folders: IFolder[];
   pages: IPage[];
@@ -28,41 +24,73 @@ export const NavigationPanel = ({
 }) => {
   return (
     <>
-      <div
-        className={`absolute inset-0 flex flex-col justify-between bg-surface-1 text-sm`}
-        style={{
-          transition: `left ${ANIMATION_DURATION}ms ease-out`,
-          width: `${width}px`,
-          left: `${menuHidden ? -width : 0}px`,
-        }}
-      >
+      <div className={'flex flex-col justify-between bg-surface-1 text-sm'} style={{ width: `${width}px` }}>
         <div className={'flex flex-col'}>
-          <AppLogo iconToShow={'hide'} onHideMenuClick={onHideMenuClick}></AppLogo>
-
-          <Workspace></Workspace>
-
-          <div className={'flex flex-col overflow-auto px-2'} style={{ height: 'calc(100vh - 280px)' }}>
-            {folders.map((folder, index) => (
-              <FolderItem
-                key={index}
-                folder={folder}
-                pages={pages.filter((page) => page.folderId === folder.id)}
-                onPageClick={onPageClick}
-              ></FolderItem>
-            ))}
-          </div>
+          <AppLogo iconToShow={'hide'} onHideMenuClick={onCollapseNavigationClick}></AppLogo>
+          <WorkspaceUser></WorkspaceUser>
+          <WorkspaceApps folders={folders} pages={pages} onPageClick={onPageClick} />
         </div>
 
         <div className={'flex flex-col'}>
           <div className={'border-b border-shade-6 px-2 pb-4'}>
-            <PluginsButton></PluginsButton>
+            {/*<PluginsButton></PluginsButton>*/}
+
+            <DesignSpec></DesignSpec>
+            <TestBackendButton></TestBackendButton>
+
+            {/*Trash Button*/}
             <TrashButton></TrashButton>
           </div>
 
+          {/*New Folder Button*/}
           <NewFolderButton></NewFolderButton>
         </div>
       </div>
-      <NavigationResizer minWidth={MINIMUM_WIDTH}></NavigationResizer>
+      <NavigationResizer></NavigationResizer>
     </>
   );
 };
+
+type AppsContext = {
+  folders: IFolder[];
+  pages: IPage[];
+  onPageClick: (page: IPage) => void;
+};
+
+const WorkspaceApps: React.FC<AppsContext> = ({ folders, pages, onPageClick }) => (
+  <div className={'flex flex-col px-2'}>
+    {folders.map((folder, index) => (
+      <FolderItem
+        key={index}
+        folder={folder}
+        pages={pages.filter((page) => page.folderId === folder.id)}
+        onPageClick={onPageClick}
+      ></FolderItem>
+    ))}
+  </div>
+);
+
+export const TestBackendButton = () => {
+  const navigate = useNavigate();
+  return (
+    <button
+      onClick={() => navigate('/page/api-test')}
+      className={'flex w-full items-center rounded-lg px-4 py-2 hover:bg-surface-2'}
+    >
+      APITest
+    </button>
+  );
+};
+
+export const DesignSpec = () => {
+  const navigate = useNavigate();
+
+  return (
+    <button
+      onClick={() => navigate('page/colors')}
+      className={'flex w-full items-center rounded-lg px-4 py-2 hover:bg-surface-2'}
+    >
+      Design Specs
+    </button>
+  );
+};

+ 1 - 1
frontend/appflowy_tauri/src/appflowy_app/components/layout/Workspace.tsx → frontend/appflowy_tauri/src/appflowy_app/components/layout/WorkspaceUser.tsx

@@ -1,6 +1,6 @@
 import { useAppSelector } from '../../stores/store';
 
-export const Workspace = () => {
+export const WorkspaceUser = () => {
   const currentUser = useAppSelector((state) => state.currentUser);
 
   return (

+ 10 - 7
frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/cell_controller.ts

@@ -13,7 +13,7 @@ type Callbacks<T> = { onCellChanged: (value: Option<T>) => void; onFieldChanged?
 
 export class CellController<T, D> {
   private fieldBackendService: FieldBackendService;
-  private cellDataNotifier: CellDataNotifier<Option<T>>;
+  private cellDataNotifier: CellDataNotifier<T>;
   private cellObserver: CellObserver;
   private readonly cacheKey: CellCacheKey;
   private readonly fieldNotifier: DatabaseFieldObserver;
@@ -59,7 +59,7 @@ export class CellController<T, D> {
     this.subscribeCallbacks = callbacks;
     this.cellDataNotifier.observer.subscribe((cellData) => {
       if (cellData !== null) {
-        callbacks.onCellChanged(cellData);
+        callbacks.onCellChanged(Some(cellData));
       }
     });
   };
@@ -95,8 +95,11 @@ export class CellController<T, D> {
   private _loadCellData = () => {
     return this.cellDataLoader.loadData().then((result) => {
       if (result.ok) {
-        this.cellCache.insert(this.cacheKey, result.val);
-        this.cellDataNotifier.cellData = Some(result.val);
+        const cellData = result.val;
+        if (cellData.some) {
+          this.cellCache.insert(this.cacheKey, cellData.val);
+          this.cellDataNotifier.cellData = cellData;
+        }
       } else {
         this.cellCache.remove(this.cacheKey);
         this.cellDataNotifier.cellData = None;
@@ -110,12 +113,12 @@ export class CellController<T, D> {
   };
 }
 
-class CellDataNotifier<T> extends ChangeNotifier<T | null> {
+class CellDataNotifier<T> extends ChangeNotifier<T> {
   _cellData: Option<T>;
 
-  constructor(cellData: T) {
+  constructor(cellData: Option<T>) {
     super();
-    this._cellData = Some(cellData);
+    this._cellData = cellData;
   }
 
   set cellData(data: Option<T>) {

+ 5 - 0
frontend/appflowy_tauri/src/appflowy_app/stores/effects/user/user_bd_svc.ts

@@ -1,5 +1,6 @@
 import { nanoid } from '@reduxjs/toolkit';
 import {
+  UserEventCheckUser,
   UserEventGetUserProfile,
   UserEventSignIn,
   UserEventSignOut,
@@ -29,6 +30,10 @@ export class UserBackendService {
     return UserEventGetUserProfile();
   };
 
+  static checkUser = () => {
+    return UserEventCheckUser();
+  };
+
   updateUserProfile = (params: { name?: string; password?: string; email?: string; openAIKey?: string }) => {
     const payload = UpdateUserProfilePayloadPB.fromObject({ id: this.userId });
 

+ 4 - 5
frontend/appflowy_tauri/src/appflowy_app/stores/reducers/current-user/slice.ts

@@ -12,17 +12,16 @@ export interface ICurrentUser {
 }
 
 const initialState: ICurrentUser | null = {
-  id: nanoid(8),
-  displayName: 'Me 😃',
-  email: `${nanoid(4)}@gmail.com`,
-  token: nanoid(8),
-  isAuthenticated: true,
+  isAuthenticated: false,
 };
 
 export const currentUserSlice = createSlice({
   name: 'currentUser',
   initialState: initialState,
   reducers: {
+    checkUser: (state, action: PayloadAction<ICurrentUser>) => {
+      return action.payload;
+    },
     updateUser: (state, action: PayloadAction<ICurrentUser>) => {
       return action.payload;
     },

+ 0 - 15
frontend/appflowy_tauri/src/appflowy_app/views/Welcome.tsx

@@ -1,15 +0,0 @@
-import { Link } from 'react-router-dom';
-
-export const Welcome = () => {
-  return (
-    <div className={'p-4'}>
-      <div className={'mb-8 text-2xl'}>Welcome</div>
-      <div className={'mb-4'}>
-        <Link to={'/page/colors'}>Color Palette</Link>
-      </div>
-      <div className={'mb-4'}>
-        <Link to={'/page/api-test'}>Testing API</Link>
-      </div>
-    </div>
-  );
-};

+ 1 - 1
frontend/rust-lib/flowy-codegen/src/protobuf_file/mod.rs

@@ -132,7 +132,7 @@ fn generate_ts_protobuf_files(
     };
 
     if result.is_err() {
-      panic!("Generate dart pb file failed with: {}, {:?}", path, result)
+      panic!("Generate ts pb file failed with: {}, {:?}", path, result)
     };
   });
 

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

@@ -107,7 +107,7 @@ fn create_log_filter(level: String, with_crates: Vec<String>) -> String {
 
   #[cfg(feature = "profiling")]
   filters.push(format!("runtime={}", level));
-  filters.push(format!("tokio=trace,runtime=trace"));
+
   filters.join(",")
 }
 

+ 2 - 2
frontend/rust-lib/flowy-database/src/event_handler.rs

@@ -534,8 +534,8 @@ pub(crate) async fn get_groups_handler(
 ) -> DataResult<RepeatedGroupPB, FlowyError> {
   let params: DatabaseViewIdPB = data.into_inner();
   let editor = manager.get_database_editor(&params.value).await?;
-  let group = editor.load_groups(&params.value).await?;
-  data_result_ok(group)
+  let groups = editor.load_groups(&params.value).await?;
+  data_result_ok(groups)
 }
 
 #[tracing::instrument(level = "debug", skip(data, manager), err)]

+ 33 - 16
frontend/rust-lib/flowy-database/src/manager.rs

@@ -203,28 +203,45 @@ impl DatabaseManager {
     database_id: &str,
     view_id: &str,
   ) -> FlowyResult<Arc<DatabaseEditor>> {
-    if let Some(database_editor) = self.editors_by_database_id.read().await.get(database_id) {
-      let user_id = self.database_user.user_id()?;
-      let (view_pad, view_rev_manager) =
-        make_database_view_revision_pad(view_id, self.database_user.clone()).await?;
-
-      let view_editor = DatabaseViewEditor::from_pad(
+    let user = self.database_user.clone();
+    let create_view_editor = |database_editor: Arc<DatabaseEditor>| async move {
+      let user_id = user.user_id()?;
+      let (view_pad, view_rev_manager) = make_database_view_revision_pad(view_id, user).await?;
+      return DatabaseViewEditor::from_pad(
         &user_id,
         database_editor.database_view_data.clone(),
         database_editor.cell_data_cache.clone(),
         view_rev_manager,
         view_pad,
       )
-      .await?;
-      database_editor.open_view_editor(view_editor).await;
-      return Ok(database_editor.clone());
-    }
-    // Lock the database_editors
-    let mut editors_by_database_id = self.editors_by_database_id.write().await;
-    let db_pool = self.database_user.db_pool()?;
-    let editor = self.make_database_rev_editor(view_id, db_pool).await?;
-    editors_by_database_id.insert(database_id.to_string(), editor.clone());
-    Ok(editor)
+      .await;
+    };
+
+    let database_editor = self
+      .editors_by_database_id
+      .read()
+      .await
+      .get(database_id)
+      .cloned();
+
+    return match database_editor {
+      None => {
+        let mut editors_by_database_id = self.editors_by_database_id.write().await;
+        let db_pool = self.database_user.db_pool()?;
+        let database_editor = self.make_database_rev_editor(view_id, db_pool).await?;
+        editors_by_database_id.insert(database_id.to_string(), database_editor.clone());
+        Ok(database_editor)
+      },
+      Some(database_editor) => {
+        let is_open = database_editor.is_view_open(view_id).await;
+        if !is_open {
+          let database_view_editor = create_view_editor(database_editor.clone()).await?;
+          database_editor.open_view_editor(database_view_editor).await;
+        }
+
+        Ok(database_editor)
+      },
+    };
   }
 
   #[tracing::instrument(level = "trace", skip(self, pool), err)]

+ 3 - 0
frontend/rust-lib/flowy-database/src/services/database/database_editor.rs

@@ -131,6 +131,9 @@ impl DatabaseEditor {
     self.database_views.number_of_views().await
   }
 
+  pub async fn is_view_open(&self, view_id: &str) -> bool {
+    self.database_views.is_view_exist(view_id).await
+  }
   /// Save the type-option data to disk and send a `DatabaseNotification::DidUpdateField` notification
   /// to dart side.
   ///

Деякі файли не було показано, через те що забагато файлів було змінено