Browse Source

Fix/grid group (#1787)

* ci: config rust log

* chore: rename flowy-sdk to appflowy-core

* fix: create group after editing the url

* fix: start listen on new group

* chore: add tests

* refactor: mock data

* ci: update command
Nathan.fooo 2 năm trước cách đây
mục cha
commit
069519589e
73 tập tin đã thay đổi với 1570 bổ sung1901 xóa
  1. 1 1
      .github/workflows/rust_ci.yaml
  2. 1 1
      frontend/.vscode/launch.json
  3. 1 1
      frontend/.vscode/tasks.json
  4. 1 1
      frontend/Makefile.toml
  5. 43 33
      frontend/app_flowy/lib/plugins/board/application/board_bloc.dart
  6. 3 3
      frontend/app_flowy/lib/plugins/board/application/board_data_controller.dart
  7. 1 1
      frontend/app_flowy/lib/plugins/board/application/board_listener.dart
  8. 1 1
      frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/cell_controller.dart
  9. 9 4
      frontend/app_flowy/lib/plugins/grid/application/cell/url_cell_editor_bloc.dart
  10. 21 8
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/url_cell/cell_editor.dart
  11. 2 0
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/url_cell/url_cell.dart
  12. 4 4
      frontend/appflowy_tauri/src-tauri/src/init.rs
  13. 2 2
      frontend/appflowy_tauri/src-tauri/src/request.rs
  14. 5 5
      frontend/rust-lib/dart-ffi/src/lib.rs
  15. 19 19
      frontend/rust-lib/flowy-core/src/lib.rs
  16. 2 2
      frontend/rust-lib/flowy-database/src/entities/group_entities/group_changeset.rs
  17. 42 4
      frontend/rust-lib/flowy-database/src/services/cell/cell_operation.rs
  18. 1 1
      frontend/rust-lib/flowy-database/src/services/field/type_options/type_option_cell.rs
  19. 9 0
      frontend/rust-lib/flowy-database/src/services/field/type_options/url_type_option/url_type_option_entities.rs
  20. 10 8
      frontend/rust-lib/flowy-database/src/services/grid_editor.rs
  21. 43 18
      frontend/rust-lib/flowy-database/src/services/group/action.rs
  22. 50 25
      frontend/rust-lib/flowy-database/src/services/group/configuration.rs
  23. 79 40
      frontend/rust-lib/flowy-database/src/services/group/controller.rs
  24. 9 9
      frontend/rust-lib/flowy-database/src/services/group/controller_impls/checkbox_controller.rs
  25. 20 9
      frontend/rust-lib/flowy-database/src/services/group/controller_impls/default_controller.rs
  26. 7 7
      frontend/rust-lib/flowy-database/src/services/group/controller_impls/select_option_controller/multi_select_controller.rs
  27. 7 7
      frontend/rust-lib/flowy-database/src/services/group/controller_impls/select_option_controller/single_select_controller.rs
  28. 74 18
      frontend/rust-lib/flowy-database/src/services/group/controller_impls/url_controller.rs
  29. 1 1
      frontend/rust-lib/flowy-database/src/services/group/entities.rs
  30. 44 17
      frontend/rust-lib/flowy-database/src/services/view_editor/editor.rs
  31. 3 7
      frontend/rust-lib/flowy-database/src/services/view_editor/editor_manager.rs
  32. 3 3
      frontend/rust-lib/flowy-database/tests/grid/block_test/block_test.rs
  33. 10 10
      frontend/rust-lib/flowy-database/tests/grid/block_test/row_test.rs
  34. 9 9
      frontend/rust-lib/flowy-database/tests/grid/block_test/script.rs
  35. 8 8
      frontend/rust-lib/flowy-database/tests/grid/cell_test/script.rs
  36. 4 4
      frontend/rust-lib/flowy-database/tests/grid/cell_test/test.rs
  37. 193 0
      frontend/rust-lib/flowy-database/tests/grid/database_editor.rs
  38. 8 8
      frontend/rust-lib/flowy-database/tests/grid/field_test/script.rs
  39. 13 13
      frontend/rust-lib/flowy-database/tests/grid/field_test/test.rs
  40. 3 3
      frontend/rust-lib/flowy-database/tests/grid/filter_test/checkbox_filter_test.rs
  41. 3 3
      frontend/rust-lib/flowy-database/tests/grid/filter_test/checklist_filter_test.rs
  42. 6 6
      frontend/rust-lib/flowy-database/tests/grid/filter_test/date_filter_test.rs
  43. 7 7
      frontend/rust-lib/flowy-database/tests/grid/filter_test/number_filter_test.rs
  44. 8 8
      frontend/rust-lib/flowy-database/tests/grid/filter_test/script.rs
  45. 8 8
      frontend/rust-lib/flowy-database/tests/grid/filter_test/select_option_filter_test.rs
  46. 11 11
      frontend/rust-lib/flowy-database/tests/grid/filter_test/text_filter_test.rs
  47. 0 573
      frontend/rust-lib/flowy-database/tests/grid/grid_editor.rs
  48. 0 381
      frontend/rust-lib/flowy-database/tests/grid/grid_test.rs
  49. 1 0
      frontend/rust-lib/flowy-database/tests/grid/group_test/mod.rs
  50. 38 12
      frontend/rust-lib/flowy-database/tests/grid/group_test/script.rs
  51. 24 73
      frontend/rust-lib/flowy-database/tests/grid/group_test/test.rs
  52. 148 0
      frontend/rust-lib/flowy-database/tests/grid/group_test/url_group_test.rs
  53. 191 0
      frontend/rust-lib/flowy-database/tests/grid/mock_data/board_mock_data.rs
  54. 6 0
      frontend/rust-lib/flowy-database/tests/grid/mock_data/calendar_mock_data.rs
  55. 193 0
      frontend/rust-lib/flowy-database/tests/grid/mock_data/grid_mock_data.rs
  56. 19 0
      frontend/rust-lib/flowy-database/tests/grid/mock_data/mod.rs
  57. 3 1
      frontend/rust-lib/flowy-database/tests/grid/mod.rs
  58. 0 374
      frontend/rust-lib/flowy-database/tests/grid/script.rs
  59. 8 8
      frontend/rust-lib/flowy-database/tests/grid/snapshot_test/script.rs
  60. 3 3
      frontend/rust-lib/flowy-database/tests/grid/snapshot_test/test.rs
  61. 2 2
      frontend/rust-lib/flowy-database/tests/grid/sort_test/checkbox_and_text_test.rs
  62. 2 2
      frontend/rust-lib/flowy-database/tests/grid/sort_test/multi_sort_test.rs
  63. 8 8
      frontend/rust-lib/flowy-database/tests/grid/sort_test/script.rs
  64. 12 12
      frontend/rust-lib/flowy-database/tests/grid/sort_test/single_sort_test.rs
  65. 0 2
      frontend/rust-lib/flowy-error/src/ext/dispatch.rs
  66. 5 5
      frontend/rust-lib/flowy-test/src/lib.rs
  67. 1 1
      frontend/scripts/build_sdk.cmd
  68. 3 3
      frontend/scripts/build_sdk.sh
  69. 12 18
      frontend/scripts/makefile/desktop.toml
  70. 56 33
      frontend/scripts/makefile/flutter.toml
  71. 1 0
      frontend/scripts/makefile/tauri.toml
  72. 24 31
      frontend/scripts/makefile/tests.toml
  73. 1 1
      shared-lib/lib-infra/Cargo.toml

+ 1 - 1
.github/workflows/rust_ci.yaml

@@ -59,7 +59,7 @@ jobs:
       - name: Build FlowySDK
         working-directory: frontend
         run: |
-          cargo make --profile development-linux-x86_64 appflowy-sdk-dev
+          cargo make --profile development-linux-x86_64 appflowy-core-dev
 
       - name: rustfmt rust-lib
         run: cargo fmt --all -- --check

+ 1 - 1
frontend/.vscode/launch.json

@@ -23,7 +23,7 @@
             "type": "dart",
             "preLaunchTask": "AF: build_flowy_sdk",
             "env": {
-                "RUST_LOG": "info"
+                "RUST_LOG": "debug"
             },
             "cwd": "${workspaceRoot}/app_flowy"
         },

+ 1 - 1
frontend/.vscode/tasks.json

@@ -48,7 +48,7 @@
 		{
 			"label": "AF: build_flowy_sdk_for_android",
 			"type": "shell",
-			"command": "cargo make --profile development-android appflowy-sdk-dev-android",
+			"command": "cargo make --profile development-android appflowy-core-dev-android",
 			"group": "build",
 			"options": {
 				"cwd": "${workspaceFolder}"

+ 1 - 1
frontend/Makefile.toml

@@ -18,7 +18,7 @@ on_error_task = "catch"
 run_task = { name = ["restore-crate-type"] }
 
 [env]
-RUST_LOG = "info"
+RUST_LOG = "debug"
 CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
 CARGO_MAKE_CRATE_FS_NAME = "dart_ffi"
 CARGO_MAKE_CRATE_NAME = "dart-ffi"

+ 43 - 33
frontend/app_flowy/lib/plugins/board/application/board_bloc.dart

@@ -186,43 +186,20 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
     return super.close();
   }
 
-  void initializeGroups(List<GroupPB> groupsData) {
+  void initializeGroups(List<GroupPB> groups) {
     for (var controller in groupControllers.values) {
       controller.dispose();
     }
     groupControllers.clear();
     boardController.clear();
 
-    //
-    List<AppFlowyGroupData> groups = groupsData
+    boardController.addGroups(groups
         .where((group) => fieldController.getField(group.fieldId) != null)
-        .map((group) {
-      return AppFlowyGroupData(
-        id: group.groupId,
-        name: group.desc,
-        items: _buildGroupItems(group),
-        customData: GroupData(
-          group: group,
-          fieldInfo: fieldController.getField(group.fieldId)!,
-        ),
-      );
-    }).toList();
-    boardController.addGroups(groups);
-
-    for (final group in groupsData) {
-      final delegate = GroupControllerDelegateImpl(
-        controller: boardController,
-        fieldController: fieldController,
-        onNewColumnItem: (groupId, row, index) {
-          add(BoardEvent.didCreateRow(group, row, index));
-        },
-      );
-      final controller = GroupController(
-        databaseId: state.databaseId,
-        group: group,
-        delegate: delegate,
-      );
-      controller.startListening();
+        .map((group) => initializeGroupData(group))
+        .toList());
+
+    for (final group in groups) {
+      final controller = initializeGroupController(group);
       groupControllers[controller.group.groupId] = (controller);
     }
   }
@@ -245,11 +222,15 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
       },
       onDeletedGroup: (groupIds) {
         if (isClosed) return;
-        //
+        boardController.removeGroups(groupIds);
       },
-      onInsertedGroup: (insertedGroups) {
+      onInsertedGroup: (insertedGroup) {
         if (isClosed) return;
-        //
+        final group = insertedGroup.group;
+        final newGroup = initializeGroupData(group);
+        final controller = initializeGroupController(group);
+        groupControllers[controller.group.groupId] = (controller);
+        boardController.addGroup(newGroup);
       },
       onUpdatedGroup: (updatedGroups) {
         if (isClosed) return;
@@ -294,6 +275,35 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
       ),
     );
   }
+
+  GroupController initializeGroupController(GroupPB group) {
+    final delegate = GroupControllerDelegateImpl(
+      controller: boardController,
+      fieldController: fieldController,
+      onNewColumnItem: (groupId, row, index) {
+        add(BoardEvent.didCreateRow(group, row, index));
+      },
+    );
+    final controller = GroupController(
+      databaseId: state.databaseId,
+      group: group,
+      delegate: delegate,
+    );
+    controller.startListening();
+    return controller;
+  }
+
+  AppFlowyGroupData initializeGroupData(GroupPB group) {
+    return AppFlowyGroupData(
+      id: group.groupId,
+      name: group.desc,
+      items: _buildGroupItems(group),
+      customData: GroupData(
+        group: group,
+        fieldInfo: fieldController.getField(group.fieldId)!,
+      ),
+    );
+  }
 }
 
 @freezed

+ 3 - 3
frontend/app_flowy/lib/plugins/board/application/board_data_controller.dart

@@ -17,7 +17,7 @@ typedef OnGridChanged = void Function(DatabasePB);
 typedef DidLoadGroups = void Function(List<GroupPB>);
 typedef OnUpdatedGroup = void Function(List<GroupPB>);
 typedef OnDeletedGroup = void Function(List<String>);
-typedef OnInsertedGroup = void Function(List<InsertedGroupPB>);
+typedef OnInsertedGroup = void Function(InsertedGroupPB);
 typedef OnResetGroups = void Function(List<GroupPB>);
 
 typedef OnRowsChanged = void Function(
@@ -90,8 +90,8 @@ class BoardDataController {
               onDeletedGroup.call(changeset.deletedGroups);
             }
 
-            if (changeset.insertedGroups.isNotEmpty) {
-              onInsertedGroup.call(changeset.insertedGroups);
+            for (final insertedGroup in changeset.insertedGroups) {
+              onInsertedGroup.call(insertedGroup);
             }
           },
           (e) => _onError?.call(e),

+ 1 - 1
frontend/app_flowy/lib/plugins/board/application/board_listener.dart

@@ -46,7 +46,7 @@ class BoardListener {
       case DatabaseNotification.DidGroupByNewField:
         result.fold(
           (payload) => _groupByNewFieldNotifier?.value =
-              left(GroupViewChangesetPB.fromBuffer(payload).newGroups),
+              left(GroupViewChangesetPB.fromBuffer(payload).initialGroups),
           (error) => _groupByNewFieldNotifier?.value = right(error),
         );
         break;

+ 1 - 1
frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/cell_controller.dart

@@ -270,7 +270,7 @@ class GridCellController<T, D> extends Equatable {
   /// You can set [deduplicate] to true (default is false) to reduce the save operation.
   /// It's useful when you call this method when user editing the [TextField].
   /// The default debounce interval is 300 milliseconds.
-  void saveCellData(
+  Future<void> saveCellData(
     D data, {
     bool deduplicate = false,
     void Function(Option<FlowyError>)? onFinish,

+ 9 - 4
frontend/app_flowy/lib/plugins/grid/application/cell/url_cell_editor_bloc.dart

@@ -14,13 +14,16 @@ class URLCellEditorBloc extends Bloc<URLCellEditorEvent, URLCellEditorState> {
   }) : super(URLCellEditorState.initial(cellController)) {
     on<URLCellEditorEvent>(
       (event, emit) async {
-        event.when(
+        await event.when(
           initial: () {
             _startListening();
           },
-          updateText: (text) {
-            cellController.saveCellData(text, deduplicate: true);
-            emit(state.copyWith(content: text));
+          updateText: (text) async {
+            await cellController.saveCellData(text);
+            emit(state.copyWith(
+              content: text,
+              isFinishEditing: true,
+            ));
           },
           didReceiveCellUpdate: (cellData) {
             emit(state.copyWith(content: cellData?.content ?? ""));
@@ -63,12 +66,14 @@ class URLCellEditorEvent with _$URLCellEditorEvent {
 class URLCellEditorState with _$URLCellEditorState {
   const factory URLCellEditorState({
     required String content,
+    required bool isFinishEditing,
   }) = _URLCellEditorState;
 
   factory URLCellEditorState.initial(GridURLCellController context) {
     final cellData = context.getCellData();
     return URLCellEditorState(
       content: cellData?.content ?? "",
+      isFinishEditing: true,
     );
   }
 }

+ 21 - 8
frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/url_cell/cell_editor.dart

@@ -6,9 +6,13 @@ import 'dart:async';
 import 'package:flutter_bloc/flutter_bloc.dart';
 
 class URLCellEditor extends StatefulWidget {
+  final VoidCallback onExit;
   final GridURLCellController cellController;
-  const URLCellEditor({required this.cellController, Key? key})
-      : super(key: key);
+  const URLCellEditor({
+    required this.cellController,
+    required this.onExit,
+    Key? key,
+  }) : super(key: key);
 
   @override
   State<URLCellEditor> createState() => _URLCellEditorState();
@@ -36,12 +40,17 @@ class _URLCellEditorState extends State<URLCellEditor> {
           if (_controller.text != state.content) {
             _controller.text = state.content;
           }
+
+          if (state.isFinishEditing) {
+            widget.onExit();
+          }
         },
         child: TextField(
           autofocus: true,
           controller: _controller,
-          onChanged: (value) => focusChanged(),
-          maxLines: null,
+          onSubmitted: (value) => focusChanged(),
+          onEditingComplete: () => focusChanged(),
+          maxLines: 1,
           style: Theme.of(context).textTheme.bodyMedium,
           decoration: const InputDecoration(
             contentPadding: EdgeInsets.zero,
@@ -57,11 +66,10 @@ class _URLCellEditorState extends State<URLCellEditor> {
   @override
   Future<void> dispose() async {
     _cellBloc.close();
-
     super.dispose();
   }
 
-  Future<void> focusChanged() async {
+  void focusChanged() {
     if (mounted) {
       if (_cellBloc.isClosed == false &&
           _controller.text != _cellBloc.state.content) {
@@ -72,9 +80,13 @@ class _URLCellEditorState extends State<URLCellEditor> {
 }
 
 class URLEditorPopover extends StatelessWidget {
+  final VoidCallback onExit;
   final GridURLCellController cellController;
-  const URLEditorPopover({required this.cellController, Key? key})
-      : super(key: key);
+  const URLEditorPopover({
+    required this.cellController,
+    required this.onExit,
+    Key? key,
+  }) : super(key: key);
 
   @override
   Widget build(BuildContext context) {
@@ -84,6 +96,7 @@ class URLEditorPopover extends StatelessWidget {
         padding: const EdgeInsets.all(6),
         child: URLCellEditor(
           cellController: cellController,
+          onExit: onExit,
         ),
       ),
     );

+ 2 - 0
frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/url_cell/url_cell.dart

@@ -142,6 +142,7 @@ class _GridURLCellState extends GridCellState<GridURLCell> {
               return URLEditorPopover(
                 cellController: widget.cellControllerBuilder.build()
                     as GridURLCellController,
+                onExit: () => _popoverController.close(),
               );
             },
             onClose: () {
@@ -219,6 +220,7 @@ class _EditURLAccessoryState extends State<_EditURLAccessory>
         return URLEditorPopover(
           cellController:
               widget.cellControllerBuilder.build() as GridURLCellController,
+          onExit: () => _popoverController.close(),
         );
       },
     );

+ 4 - 4
frontend/appflowy_tauri/src-tauri/src/init.rs

@@ -1,10 +1,10 @@
-use flowy_core::{get_client_server_configuration, FlowySDK, FlowySDKConfig};
+use flowy_core::{get_client_server_configuration, AppFlowyCore, AppFlowyCoreConfig};
 
-pub fn init_flowy_core() -> FlowySDK {
+pub fn init_flowy_core() -> AppFlowyCore {
     let data_path = tauri::api::path::data_dir().unwrap();
     let path = format!("{}/AppFlowy", data_path.to_str().unwrap());
     let server_config = get_client_server_configuration().unwrap();
-    let config = FlowySDKConfig::new(&path, "AppFlowy".to_string(), server_config)
+    let config = AppFlowyCoreConfig::new(&path, "AppFlowy".to_string(), server_config)
         .log_filter("trace", vec!["appflowy_tauri".to_string()]);
-    FlowySDK::new(config)
+    AppFlowyCore::new(config)
 }

+ 2 - 2
frontend/appflowy_tauri/src-tauri/src/request.rs

@@ -1,4 +1,4 @@
-use flowy_core::FlowySDK;
+use flowy_core::AppFlowyCore;
 use lib_dispatch::prelude::{
     AFPluginDispatcher, AFPluginEventResponse, AFPluginRequest, StatusCode,
 };
@@ -39,7 +39,7 @@ pub async fn invoke_request(
     app_handler: AppHandle<Wry>,
 ) -> AFTauriResponse {
     let request: AFPluginRequest = request.into();
-    let state: State<FlowySDK> = app_handler.state();
+    let state: State<AppFlowyCore> = app_handler.state();
     let dispatcher = state.inner().dispatcher();
     let response = AFPluginDispatcher::async_send(dispatcher, request).await;
     response.into()

+ 5 - 5
frontend/rust-lib/dart-ffi/src/lib.rs

@@ -20,7 +20,7 @@ use parking_lot::RwLock;
 use std::{ffi::CStr, os::raw::c_char};
 
 lazy_static! {
-    static ref FLOWY_SDK: RwLock<Option<FlowySDK>> = RwLock::new(None);
+    static ref APPFLOWY_CORE: RwLock<Option<AppFlowyCore>> = RwLock::new(None);
 }
 
 #[no_mangle]
@@ -30,8 +30,8 @@ pub extern "C" fn init_sdk(path: *mut c_char) -> i64 {
 
     let server_config = get_client_server_configuration().unwrap();
     let log_crates = vec!["flowy-ffi".to_string()];
-    let config = FlowySDKConfig::new(path, "appflowy".to_string(), server_config).log_filter("info", log_crates);
-    *FLOWY_SDK.write() = Some(FlowySDK::new(config));
+    let config = AppFlowyCoreConfig::new(path, "appflowy".to_string(), server_config).log_filter("info", log_crates);
+    *APPFLOWY_CORE.write() = Some(AppFlowyCore::new(config));
 
     0
 }
@@ -46,7 +46,7 @@ pub extern "C" fn async_event(port: i64, input: *const u8, len: usize) {
         port
     );
 
-    let dispatcher = match FLOWY_SDK.read().as_ref() {
+    let dispatcher = match APPFLOWY_CORE.read().as_ref() {
         None => {
             log::error!("sdk not init yet.");
             return;
@@ -64,7 +64,7 @@ pub extern "C" fn sync_event(input: *const u8, len: usize) -> *const u8 {
     let request: AFPluginRequest = FFIRequest::from_u8_pointer(input, len).into();
     log::trace!("[FFI]: {} Sync Event: {:?}", &request.id, &request.event,);
 
-    let dispatcher = match FLOWY_SDK.read().as_ref() {
+    let dispatcher = match APPFLOWY_CORE.read().as_ref() {
         None => {
             log::error!("sdk not init yet.");
             return forget_rust(Vec::default());

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

@@ -34,31 +34,31 @@ use user_model::UserProfile;
 static INIT_LOG: AtomicBool = AtomicBool::new(false);
 
 #[derive(Clone)]
-pub struct FlowySDKConfig {
-    /// Different `FlowySDK` instance should have different name
+pub struct AppFlowyCoreConfig {
+    /// Different `AppFlowyCoreConfig` instance should have different name
     name: String,
     /// Panics if the `root` path is not existing
-    root: String,
+    storage_path: String,
     log_filter: String,
     server_config: ClientServerConfiguration,
     pub document: DocumentConfig,
 }
 
-impl fmt::Debug for FlowySDKConfig {
+impl fmt::Debug for AppFlowyCoreConfig {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        f.debug_struct("FlowySDKConfig")
-            .field("root", &self.root)
+        f.debug_struct("AppFlowyCoreConfig")
+            .field("storage_path", &self.storage_path)
             .field("server-config", &self.server_config)
             .field("document-config", &self.document)
             .finish()
     }
 }
 
-impl FlowySDKConfig {
+impl AppFlowyCoreConfig {
     pub fn new(root: &str, name: String, server_config: ClientServerConfiguration) -> Self {
-        FlowySDKConfig {
+        AppFlowyCoreConfig {
             name,
-            root: root.to_owned(),
+            storage_path: root.to_owned(),
             log_filter: create_log_filter("info".to_owned(), vec![]),
             server_config,
             document: DocumentConfig::default(),
@@ -106,9 +106,9 @@ fn create_log_filter(level: String, with_crates: Vec<String>) -> String {
 }
 
 #[derive(Clone)]
-pub struct FlowySDK {
+pub struct AppFlowyCore {
     #[allow(dead_code)]
-    pub config: FlowySDKConfig,
+    pub config: AppFlowyCoreConfig,
     pub user_session: Arc<UserSession>,
     pub document_manager: Arc<DocumentManager>,
     pub folder_manager: Arc<FolderManager>,
@@ -119,10 +119,10 @@ pub struct FlowySDK {
     pub task_dispatcher: Arc<RwLock<TaskDispatcher>>,
 }
 
-impl FlowySDK {
-    pub fn new(config: FlowySDKConfig) -> Self {
+impl AppFlowyCore {
+    pub fn new(config: AppFlowyCoreConfig) -> Self {
         init_log(&config);
-        init_kv(&config.root);
+        init_kv(&config.storage_path);
         tracing::debug!("🔥 {:?}", config);
         let runtime = tokio_default_runtime().unwrap();
         let task_scheduler = TaskDispatcher::new(Duration::from_secs(2));
@@ -257,22 +257,22 @@ fn init_kv(root: &str) {
     }
 }
 
-fn init_log(config: &FlowySDKConfig) {
+fn init_log(config: &AppFlowyCoreConfig) {
     if !INIT_LOG.load(Ordering::SeqCst) {
         INIT_LOG.store(true, Ordering::SeqCst);
 
-        let _ = lib_log::Builder::new("AppFlowy-Client", &config.root)
+        let _ = lib_log::Builder::new("AppFlowy-Client", &config.storage_path)
             .env_filter(&config.log_filter)
             .build();
     }
 }
 
 fn mk_user_session(
-    config: &FlowySDKConfig,
+    config: &AppFlowyCoreConfig,
     local_server: &Option<Arc<LocalServer>>,
     server_config: &ClientServerConfiguration,
 ) -> Arc<UserSession> {
-    let user_config = UserSessionConfig::new(&config.name, &config.root);
+    let user_config = UserSessionConfig::new(&config.name, &config.storage_path);
     let cloud_service = UserDepsResolver::resolve(local_server, server_config);
     Arc::new(UserSession::new(user_config, cloud_service))
 }
@@ -282,7 +282,7 @@ struct UserStatusListener {
     folder_manager: Arc<FolderManager>,
     grid_manager: Arc<DatabaseManager>,
     ws_conn: Arc<FlowyWebSocketConnect>,
-    config: FlowySDKConfig,
+    config: AppFlowyCoreConfig,
 }
 
 impl UserStatusListener {

+ 2 - 2
frontend/rust-lib/flowy-database/src/entities/group_entities/group_changeset.rs

@@ -135,7 +135,7 @@ pub struct GroupViewChangesetPB {
     pub inserted_groups: Vec<InsertedGroupPB>,
 
     #[pb(index = 3)]
-    pub new_groups: Vec<GroupPB>,
+    pub initial_groups: Vec<GroupPB>,
 
     #[pb(index = 4)]
     pub deleted_groups: Vec<String>,
@@ -146,7 +146,7 @@ pub struct GroupViewChangesetPB {
 
 impl GroupViewChangesetPB {
     pub fn is_empty(&self) -> bool {
-        self.new_groups.is_empty()
+        self.initial_groups.is_empty()
             && self.inserted_groups.is_empty()
             && self.deleted_groups.is_empty()
             && self.update_groups.is_empty()

+ 42 - 4
frontend/rust-lib/flowy-database/src/services/cell/cell_operation.rs

@@ -76,7 +76,7 @@ pub fn apply_cell_data_changeset<C: ToCellChangesetString, T: AsRef<FieldRevisio
     Ok(TypeCellData::new(cell_str, field_type).to_json())
 }
 
-pub fn decode_type_cell_data<T: TryInto<TypeCellData, Error = FlowyError> + Debug>(
+pub fn get_type_cell_protobuf<T: TryInto<TypeCellData, Error = FlowyError> + Debug>(
     data: T,
     field_rev: &FieldRevision,
     cell_data_cache: Option<AtomicCellDataCache>,
@@ -85,7 +85,13 @@ pub fn decode_type_cell_data<T: TryInto<TypeCellData, Error = FlowyError> + Debu
     match data.try_into() {
         Ok(type_cell_data) => {
             let TypeCellData { cell_str, field_type } = type_cell_data;
-            match try_decode_cell_str(cell_str, &field_type, &to_field_type, field_rev, cell_data_cache) {
+            match try_decode_cell_str_to_cell_protobuf(
+                cell_str,
+                &field_type,
+                &to_field_type,
+                field_rev,
+                cell_data_cache,
+            ) {
                 Ok(cell_bytes) => (field_type, cell_bytes),
                 Err(e) => {
                     tracing::error!("Decode cell data failed, {:?}", e);
@@ -97,12 +103,30 @@ pub fn decode_type_cell_data<T: TryInto<TypeCellData, Error = FlowyError> + Debu
             // It's okay to ignore this error, because it's okay that the current cell can't
             // display the existing cell data. For example, the UI of the text cell will be blank if
             // the type of the data of cell is Number.
-
             (to_field_type, CellProtobufBlob::default())
         }
     }
 }
 
+pub fn get_type_cell_data<CellData, Output>(
+    data: CellData,
+    field_rev: &FieldRevision,
+    cell_data_cache: Option<AtomicCellDataCache>,
+) -> Option<Output>
+where
+    CellData: TryInto<TypeCellData, Error = FlowyError> + Debug,
+    Output: Default + 'static,
+{
+    let to_field_type = field_rev.ty.into();
+    match data.try_into() {
+        Ok(type_cell_data) => {
+            let TypeCellData { cell_str, field_type } = type_cell_data;
+            try_decode_cell_str_to_cell_data(cell_str, &field_type, &to_field_type, field_rev, cell_data_cache)
+        }
+        Err(_err) => None,
+    }
+}
+
 /// Decode the opaque cell data from one field type to another using the corresponding `TypeOption`
 ///
 /// The cell data might become an empty string depends on the to_field_type's `TypeOption`   
@@ -120,7 +144,7 @@ pub fn decode_type_cell_data<T: TryInto<TypeCellData, Error = FlowyError> + Debu
 ///
 /// returns: CellBytes
 ///
-pub fn try_decode_cell_str(
+pub fn try_decode_cell_str_to_cell_protobuf(
     cell_str: String,
     from_field_type: &FieldType,
     to_field_type: &FieldType,
@@ -135,6 +159,20 @@ pub fn try_decode_cell_str(
     }
 }
 
+pub fn try_decode_cell_str_to_cell_data<T: Default + 'static>(
+    cell_str: String,
+    from_field_type: &FieldType,
+    to_field_type: &FieldType,
+    field_rev: &FieldRevision,
+    cell_data_cache: Option<AtomicCellDataCache>,
+) -> Option<T> {
+    let handler = TypeOptionCellExt::new_with_cell_data_cache(field_rev, cell_data_cache)
+        .get_type_option_cell_data_handler(to_field_type)?;
+    handler
+        .get_cell_data(cell_str, from_field_type, field_rev)
+        .ok()?
+        .unbox_or_none::<T>()
+}
 /// Returns a string that represents the current field_type's cell data.
 /// For example, The string of the Multi-Select cell will be a list of the option's name
 /// separated by a comma.

+ 1 - 1
frontend/rust-lib/flowy-database/src/services/field/type_options/type_option_cell.rs

@@ -497,7 +497,7 @@ impl BoxCellData {
         }
     }
 
-    fn unbox_or_none<T>(self) -> Option<T>
+    pub(crate) fn unbox_or_none<T>(self) -> Option<T>
     where
         T: Default + 'static,
     {

+ 9 - 0
frontend/rust-lib/flowy-database/src/services/field/type_options/url_type_option/url_type_option_entities.rs

@@ -49,6 +49,15 @@ impl URLCellData {
     }
 }
 
+impl From<URLCellDataPB> for URLCellData {
+    fn from(data: URLCellDataPB) -> Self {
+        Self {
+            url: data.url,
+            content: data.content,
+        }
+    }
+}
+
 impl AsRef<str> for URLCellData {
     fn as_ref(&self) -> &str {
         &self.url

+ 10 - 8
frontend/rust-lib/flowy-database/src/services/grid_editor.rs

@@ -4,7 +4,7 @@ use crate::manager::DatabaseUser;
 use crate::notification::{send_notification, DatabaseNotification};
 use crate::services::block_manager::DatabaseBlockManager;
 use crate::services::cell::{
-    apply_cell_data_changeset, decode_type_cell_data, stringify_cell_data, AnyTypeCache, AtomicCellDataCache,
+    apply_cell_data_changeset, get_type_cell_protobuf, stringify_cell_data, AnyTypeCache, AtomicCellDataCache,
     CellProtobufBlob, ToCellChangesetString, TypeCellData,
 };
 use crate::services::field::{
@@ -392,8 +392,9 @@ impl DatabaseRevisionEditor {
 
     pub async fn update_row(&self, changeset: RowChangeset) -> FlowyResult<()> {
         let row_id = changeset.row_id.clone();
+        let old_row = self.get_row_rev(&row_id).await?;
         self.block_manager.update_row(changeset).await?;
-        self.view_manager.did_update_cell(&row_id).await;
+        self.view_manager.did_update_row(old_row, &row_id).await;
         Ok(())
     }
 
@@ -440,7 +441,7 @@ impl DatabaseRevisionEditor {
 
     /// Returns the cell data that encoded in protobuf.
     pub async fn get_cell(&self, params: &CellPathParams) -> Option<CellPB> {
-        let (field_type, cell_bytes) = self.decode_cell_data_from(params).await?;
+        let (field_type, cell_bytes) = self.get_type_cell_protobuf(params).await?;
         Some(CellPB::new(
             &params.field_id,
             &params.row_id,
@@ -473,15 +474,15 @@ impl DatabaseRevisionEditor {
     }
 
     pub async fn get_cell_protobuf(&self, params: &CellPathParams) -> Option<CellProtobufBlob> {
-        let (_, cell_data) = self.decode_cell_data_from(params).await?;
+        let (_, cell_data) = self.get_type_cell_protobuf(params).await?;
         Some(cell_data)
     }
 
-    async fn decode_cell_data_from(&self, params: &CellPathParams) -> Option<(FieldType, CellProtobufBlob)> {
+    async fn get_type_cell_protobuf(&self, params: &CellPathParams) -> Option<(FieldType, CellProtobufBlob)> {
         let field_rev = self.get_field_rev(&params.field_id).await?;
         let (_, row_rev) = self.block_manager.get_row_rev(&params.row_id).await.ok()??;
         let cell_rev = row_rev.cells.get(&params.field_id)?.clone();
-        Some(decode_type_cell_data(
+        Some(get_type_cell_protobuf(
             cell_rev.type_cell_data,
             &field_rev,
             Some(self.cell_data_cache.clone()),
@@ -513,11 +514,12 @@ impl DatabaseRevisionEditor {
     ) -> FlowyResult<()> {
         match self.database_pad.read().await.get_field_rev(field_id) {
             None => {
-                let msg = format!("Field:{} not found", &field_id);
+                let msg = format!("Field with id:{} not found", &field_id);
                 Err(FlowyError::internal().context(msg))
             }
             Some((_, field_rev)) => {
                 tracing::trace!("Cell changeset: id:{} / value:{:?}", &field_id, cell_changeset);
+                let old_row_rev = self.get_row_rev(row_id).await?.clone();
                 let cell_rev = self.get_cell_rev(row_id, field_id).await?;
                 // Update the changeset.data property with the return value.
                 let type_cell_data =
@@ -529,7 +531,7 @@ impl DatabaseRevisionEditor {
                     type_cell_data,
                 };
                 self.block_manager.update_cell(cell_changeset).await?;
-                self.view_manager.did_update_cell(row_id).await;
+                self.view_manager.did_update_row(old_row_rev, row_id).await;
                 Ok(())
             }
         }

+ 43 - 18
frontend/rust-lib/flowy-database/src/services/group/action.rs

@@ -1,4 +1,4 @@
-use crate::entities::{GroupRowsNotificationPB, GroupViewChangesetPB};
+use crate::entities::{GroupPB, GroupRowsNotificationPB, GroupViewChangesetPB, InsertedGroupPB};
 use crate::services::cell::DecodedCellData;
 use crate::services::group::controller::MoveGroupRowContext;
 use crate::services::group::Group;
@@ -10,42 +10,53 @@ use std::sync::Arc;
 ///
 /// For example, the `CheckboxGroupController` implements this trait to provide custom behavior.
 ///
-pub trait GroupControllerCustomActions: Send + Sync {
-    type CellDataType: DecodedCellData;
-    /// Returns the a value of the cell, default value is None
+pub trait GroupCustomize: Send + Sync {
+    type CellData: DecodedCellData;
+    /// Returns the a value of the cell if the cell data is not exist.
+    /// The default value is `None`
     ///
     /// Determine which group the row is placed in based on the data of the cell. If the cell data
     /// is None. The row will be put in to the `No status` group  
     ///
-    fn default_cell_rev(&self) -> Option<CellRevision> {
+    fn placeholder_cell(&self) -> Option<CellRevision> {
         None
     }
 
     /// Returns a bool value to determine whether the group should contain this cell or not.
-    fn can_group(&self, content: &str, cell_data: &Self::CellDataType) -> bool;
+    fn can_group(&self, content: &str, cell_data: &Self::CellData) -> bool;
+
+    fn create_or_delete_group_when_cell_changed(
+        &mut self,
+        _row_rev: &RowRevision,
+        _old_cell_data: Option<&Self::CellData>,
+        _cell_data: &Self::CellData,
+    ) -> FlowyResult<(Option<InsertedGroupPB>, Option<GroupPB>)> {
+        Ok((None, None))
+    }
 
     /// Adds or removes a row if the cell data match the group filter.
     /// It gets called after editing the cell or row
     ///
-    fn add_or_remove_row_in_groups_if_match(
+    fn add_or_remove_row_when_cell_changed(
         &mut self,
         row_rev: &RowRevision,
-        cell_data: &Self::CellDataType,
+        cell_data: &Self::CellData,
     ) -> Vec<GroupRowsNotificationPB>;
 
     /// Deletes the row from the group
-    fn delete_row(&mut self, row_rev: &RowRevision, cell_data: &Self::CellDataType) -> Vec<GroupRowsNotificationPB>;
+    fn delete_row(&mut self, row_rev: &RowRevision, cell_data: &Self::CellData) -> Vec<GroupRowsNotificationPB>;
 
     /// Move row from one group to another
-    fn move_row(
-        &mut self,
-        cell_data: &Self::CellDataType,
-        context: MoveGroupRowContext,
-    ) -> Vec<GroupRowsNotificationPB>;
+    fn move_row(&mut self, cell_data: &Self::CellData, context: MoveGroupRowContext) -> Vec<GroupRowsNotificationPB>;
+
+    /// Returns None if there is no need to delete the group when corresponding row get removed
+    fn delete_group_when_move_row(&mut self, _row_rev: &RowRevision, _cell_data: &Self::CellData) -> Option<GroupPB> {
+        None
+    }
 }
 
 /// Defines the shared actions any group controller can perform.
-pub trait GroupControllerSharedActions: Send + Sync {
+pub trait GroupControllerActions: Send + Sync {
     /// The field that is used for grouping the rows
     fn field_id(&self) -> &str;
 
@@ -64,20 +75,34 @@ pub trait GroupControllerSharedActions: Send + Sync {
     /// Insert/Remove the row to the group if the corresponding cell data is changed
     fn did_update_group_row(
         &mut self,
+        old_row_rev: &Option<Arc<RowRevision>>,
         row_rev: &RowRevision,
         field_rev: &FieldRevision,
-    ) -> FlowyResult<Vec<GroupRowsNotificationPB>>;
+    ) -> FlowyResult<DidUpdateGroupRowResult>;
 
     /// Remove the row from the group if the row gets deleted
     fn did_delete_delete_row(
         &mut self,
         row_rev: &RowRevision,
         field_rev: &FieldRevision,
-    ) -> FlowyResult<Vec<GroupRowsNotificationPB>>;
+    ) -> FlowyResult<DidMoveGroupRowResult>;
 
     /// Move the row from one group to another group
-    fn move_group_row(&mut self, context: MoveGroupRowContext) -> FlowyResult<Vec<GroupRowsNotificationPB>>;
+    fn move_group_row(&mut self, context: MoveGroupRowContext) -> FlowyResult<DidMoveGroupRowResult>;
 
     /// Update the group if the corresponding field is changed
     fn did_update_group_field(&mut self, field_rev: &FieldRevision) -> FlowyResult<Option<GroupViewChangesetPB>>;
 }
+
+#[derive(Debug)]
+pub struct DidUpdateGroupRowResult {
+    pub(crate) inserted_group: Option<InsertedGroupPB>,
+    pub(crate) deleted_group: Option<GroupPB>,
+    pub(crate) row_changesets: Vec<GroupRowsNotificationPB>,
+}
+
+#[derive(Debug)]
+pub struct DidMoveGroupRowResult {
+    pub(crate) deleted_group: Option<GroupPB>,
+    pub(crate) row_changesets: Vec<GroupRowsNotificationPB>,
+}

+ 50 - 25
frontend/rust-lib/flowy-database/src/services/group/configuration.rs

@@ -1,4 +1,4 @@
-use crate::entities::{GroupPB, GroupViewChangesetPB};
+use crate::entities::{GroupPB, GroupViewChangesetPB, InsertedGroupPB};
 use crate::services::field::RowSingleCellData;
 use crate::services::group::{default_group_configuration, GeneratedGroupContext, Group};
 use flowy_error::{FlowyError, FlowyResult};
@@ -101,7 +101,7 @@ where
 
     /// Returns the no `status` group
     ///
-    /// We take the `id` of the `field` as the default group id
+    /// We take the `id` of the `field` as the no status group id
     pub(crate) fn get_no_status_group(&self) -> Option<&Group> {
         self.groups_map.get(&self.field_rev.id)
     }
@@ -140,6 +140,37 @@ where
             each(group);
         });
     }
+    #[tracing::instrument(level = "trace", skip(self), err)]
+    pub(crate) fn add_new_group(&mut self, group_rev: GroupRevision) -> FlowyResult<InsertedGroupPB> {
+        let group = Group::new(
+            group_rev.id.clone(),
+            self.field_rev.id.clone(),
+            group_rev.name.clone(),
+            group_rev.id.clone(),
+        );
+        self.groups_map.insert(group_rev.id.clone(), group);
+        let (index, group) = self.get_group(&group_rev.id).unwrap();
+        let insert_group = InsertedGroupPB {
+            group: GroupPB::from(group.clone()),
+            index: index as i32,
+        };
+        self.mut_configuration(|configuration| {
+            configuration.groups.push(group_rev);
+            true
+        })?;
+
+        Ok(insert_group)
+    }
+
+    #[tracing::instrument(level = "trace", skip(self))]
+    pub(crate) fn delete_group(&mut self, deleted_group_id: &str) -> FlowyResult<()> {
+        self.groups_map.remove(deleted_group_id);
+        self.mut_configuration(|configuration| {
+            configuration.groups.retain(|group| group.id != deleted_group_id);
+            true
+        })?;
+        Ok(())
+    }
 
     pub(crate) fn move_group(&mut self, from_id: &str, to_id: &str) -> FlowyResult<()> {
         let from_index = self.groups_map.get_index_of(from_id);
@@ -211,18 +242,12 @@ where
             old_groups.clear();
         }
 
-        // The `No status` group index is initialized to 0
-        if let Some(no_status_group) = no_status_group {
-            old_groups.insert(0, no_status_group);
-        }
-
         // The `all_group_revs` is the combination of the new groups and old groups
         let MergeGroupResult {
             mut all_group_revs,
             new_group_revs,
-            updated_group_revs: _,
             deleted_group_revs,
-        } = merge_groups(old_groups, new_groups);
+        } = merge_groups(no_status_group, old_groups, new_groups);
 
         let deleted_group_ids = deleted_group_revs
             .into_iter()
@@ -231,13 +256,10 @@ where
 
         self.mut_configuration(|configuration| {
             let mut is_changed = !deleted_group_ids.is_empty();
-
             // Remove the groups
-            if !deleted_group_ids.is_empty() {
-                configuration
-                    .groups
-                    .retain(|group| !deleted_group_ids.contains(&group.id));
-            }
+            configuration
+                .groups
+                .retain(|group| !deleted_group_ids.contains(&group.id));
 
             // Update/Insert new groups
             for group_rev in &mut all_group_revs {
@@ -276,7 +298,7 @@ where
             self.groups_map.insert(group.id.clone(), group);
         });
 
-        let new_groups = new_group_revs
+        let initial_groups = new_group_revs
             .into_iter()
             .flat_map(|group_rev| {
                 let filter_content = filter_content_map.get(&group_rev.id)?;
@@ -292,7 +314,7 @@ where
 
         let changeset = GroupViewChangesetPB {
             view_id: self.view_id.clone(),
-            new_groups,
+            initial_groups,
             deleted_groups: deleted_group_ids,
             update_groups: vec![],
             inserted_groups: vec![],
@@ -366,7 +388,11 @@ where
 
 /// Merge the new groups into old groups while keeping the order in the old groups
 ///
-fn merge_groups(old_groups: Vec<GroupRevision>, new_groups: Vec<GroupRevision>) -> MergeGroupResult {
+fn merge_groups(
+    no_status_group: Option<GroupRevision>,
+    old_groups: Vec<GroupRevision>,
+    new_groups: Vec<GroupRevision>,
+) -> MergeGroupResult {
     let mut merge_result = MergeGroupResult::new();
     // group_map is a helper map is used to filter out the new groups.
     let mut new_group_map: IndexMap<String, GroupRevision> = IndexMap::new();
@@ -378,11 +404,8 @@ fn merge_groups(old_groups: Vec<GroupRevision>, new_groups: Vec<GroupRevision>)
     for old in old_groups {
         if let Some(new) = new_group_map.remove(&old.id) {
             merge_result.all_group_revs.push(new.clone());
-            if is_group_changed(&new, &old) {
-                merge_result.updated_group_revs.push(new);
-            }
         } else {
-            merge_result.all_group_revs.push(old);
+            merge_result.deleted_group_revs.push(old);
         }
     }
 
@@ -392,6 +415,11 @@ fn merge_groups(old_groups: Vec<GroupRevision>, new_groups: Vec<GroupRevision>)
         merge_result.all_group_revs.push(group.clone());
         merge_result.new_group_revs.push(group);
     }
+
+    // The `No status` group index is initialized to 0
+    if let Some(no_status_group) = no_status_group {
+        merge_result.all_group_revs.insert(0, no_status_group);
+    }
     merge_result
 }
 
@@ -399,7 +427,6 @@ fn is_group_changed(new: &GroupRevision, old: &GroupRevision) -> bool {
     if new.name != old.name {
         return true;
     }
-
     false
 }
 
@@ -407,7 +434,6 @@ struct MergeGroupResult {
     // Contains the new groups and the updated groups
     all_group_revs: Vec<GroupRevision>,
     new_group_revs: Vec<GroupRevision>,
-    updated_group_revs: Vec<GroupRevision>,
     deleted_group_revs: Vec<GroupRevision>,
 }
 
@@ -416,7 +442,6 @@ impl MergeGroupResult {
         Self {
             all_group_revs: vec![],
             new_group_revs: vec![],
-            updated_group_revs: vec![],
             deleted_group_revs: vec![],
         }
     }

+ 79 - 40
frontend/rust-lib/flowy-database/src/services/group/controller.rs

@@ -1,11 +1,15 @@
 use crate::entities::{GroupRowsNotificationPB, GroupViewChangesetPB, InsertedRowPB, RowPB};
-use crate::services::cell::{decode_type_cell_data, CellProtobufBlobParser, DecodedCellData};
-use crate::services::group::action::{GroupControllerCustomActions, GroupControllerSharedActions};
+use crate::services::cell::{get_type_cell_protobuf, CellProtobufBlobParser, DecodedCellData};
+
+use crate::services::group::action::{
+    DidMoveGroupRowResult, DidUpdateGroupRowResult, GroupControllerActions, GroupCustomize,
+};
 use crate::services::group::configuration::GroupContext;
 use crate::services::group::entities::Group;
 use flowy_error::FlowyResult;
 use grid_model::{
-    FieldRevision, GroupConfigurationContentSerde, GroupRevision, RowChangeset, RowRevision, TypeOptionDataDeserializer,
+    CellRevision, FieldRevision, GroupConfigurationContentSerde, GroupRevision, RowChangeset, RowRevision,
+    TypeOptionDataDeserializer,
 };
 use std::marker::PhantomData;
 use std::sync::Arc;
@@ -18,7 +22,7 @@ use std::sync::Arc;
 /// If the [FieldType] doesn't implement its group controller, then the [DefaultGroupController] will
 /// be used.
 ///
-pub trait GroupController: GroupControllerSharedActions + Send + Sync {
+pub trait GroupController: GroupControllerActions + Send + Sync {
     fn will_create_row(&mut self, row_rev: &mut RowRevision, field_rev: &FieldRevision, group_id: &str);
     fn did_create_row(&mut self, row_pb: &RowPB, group_id: &str);
 }
@@ -86,12 +90,12 @@ where
 
     // https://stackoverflow.com/questions/69413164/how-to-fix-this-clippy-warning-needless-collect
     #[allow(clippy::needless_collect)]
-    fn update_default_group(
+    fn update_no_status_group(
         &mut self,
         row_rev: &RowRevision,
         other_group_changesets: &[GroupRowsNotificationPB],
     ) -> Option<GroupRowsNotificationPB> {
-        let default_group = self.group_ctx.get_mut_no_status_group()?;
+        let no_status_group = self.group_ctx.get_mut_no_status_group()?;
 
         // [other_group_inserted_row] contains all the inserted rows except the default group.
         let other_group_inserted_row = other_group_changesets
@@ -100,7 +104,7 @@ where
             .collect::<Vec<&InsertedRowPB>>();
 
         // Calculate the inserted_rows of the default_group
-        let default_group_inserted_row = other_group_changesets
+        let no_status_group_rows = other_group_changesets
             .iter()
             .flat_map(|changeset| &changeset.deleted_rows)
             .cloned()
@@ -113,10 +117,10 @@ where
             })
             .collect::<Vec<String>>();
 
-        let mut changeset = GroupRowsNotificationPB::new(default_group.id.clone());
-        if !default_group_inserted_row.is_empty() {
+        let mut changeset = GroupRowsNotificationPB::new(no_status_group.id.clone());
+        if !no_status_group_rows.is_empty() {
             changeset.inserted_rows.push(InsertedRowPB::new(row_rev.into()));
-            default_group.add_row(row_rev.into());
+            no_status_group.add_row(row_rev.into());
         }
 
         // [other_group_delete_rows] contains all the deleted rows except the default group.
@@ -138,7 +142,7 @@ where
             .collect::<Vec<&InsertedRowPB>>();
 
         let mut deleted_row_ids = vec![];
-        for row in &default_group.rows {
+        for row in &no_status_group.rows {
             if default_group_deleted_rows
                 .iter()
                 .any(|deleted_row| deleted_row.row.id == row.id)
@@ -146,20 +150,20 @@ where
                 deleted_row_ids.push(row.id.clone());
             }
         }
-        default_group.rows.retain(|row| !deleted_row_ids.contains(&row.id));
+        no_status_group.rows.retain(|row| !deleted_row_ids.contains(&row.id));
         changeset.deleted_rows.extend(deleted_row_ids);
         Some(changeset)
     }
 }
 
-impl<C, T, G, P> GroupControllerSharedActions for GenericGroupController<C, T, G, P>
+impl<C, T, G, P> GroupControllerActions for GenericGroupController<C, T, G, P>
 where
     P: CellProtobufBlobParser,
     C: GroupConfigurationContentSerde,
     T: TypeOptionDataDeserializer,
     G: GroupGenerator<Context = GroupContext<C>, TypeOptionType = T>,
 
-    Self: GroupControllerCustomActions<CellDataType = P::Object>,
+    Self: GroupCustomize<CellData = P::Object>,
 {
     fn field_id(&self) -> &str {
         &self.field_id
@@ -178,13 +182,13 @@ where
     fn fill_groups(&mut self, row_revs: &[Arc<RowRevision>], field_rev: &FieldRevision) -> FlowyResult<()> {
         for row_rev in row_revs {
             let cell_rev = match row_rev.cells.get(&self.field_id) {
-                None => self.default_cell_rev(),
+                None => self.placeholder_cell(),
                 Some(cell_rev) => Some(cell_rev.clone()),
             };
 
             if let Some(cell_rev) = cell_rev {
                 let mut grouped_rows: Vec<GroupedRow> = vec![];
-                let cell_bytes = decode_type_cell_data(cell_rev.type_cell_data, field_rev, None).1;
+                let cell_bytes = get_type_cell_protobuf(cell_rev.type_cell_data, field_rev, None).1;
                 let cell_data = cell_bytes.parser::<P>()?;
                 for group in self.group_ctx.groups() {
                     if self.can_group(&group.filter_content, &cell_data) {
@@ -206,7 +210,7 @@ where
             }
             match self.group_ctx.get_mut_no_status_group() {
                 None => {}
-                Some(default_group) => default_group.add_row(row_rev.into()),
+                Some(no_status_group) => no_status_group.add_row(row_rev.into()),
             }
         }
 
@@ -220,73 +224,99 @@ where
 
     fn did_update_group_row(
         &mut self,
+        old_row_rev: &Option<Arc<RowRevision>>,
         row_rev: &RowRevision,
         field_rev: &FieldRevision,
-    ) -> FlowyResult<Vec<GroupRowsNotificationPB>> {
-        if let Some(cell_rev) = row_rev.cells.get(&self.field_id) {
-            let cell_bytes = decode_type_cell_data(cell_rev.type_cell_data.clone(), field_rev, None).1;
-            let cell_data = cell_bytes.parser::<P>()?;
-            let mut changesets = self.add_or_remove_row_in_groups_if_match(row_rev, &cell_data);
+    ) -> FlowyResult<DidUpdateGroupRowResult> {
+        // let cell_data = row_rev.cells.get(&self.field_id).and_then(|cell_rev| {
+        //     let cell_data: Option<P> = get_type_cell_data(cell_rev, field_rev, None);
+        //     cell_data
+        // });
+        let mut result = DidUpdateGroupRowResult {
+            inserted_group: None,
+            deleted_group: None,
+            row_changesets: vec![],
+        };
+
+        if let Some(cell_data) = get_cell_data_from_row_rev::<P>(Some(row_rev), field_rev) {
+            let old_row_rev = old_row_rev.as_ref().map(|old| old.as_ref());
+            let old_cell_data = get_cell_data_from_row_rev::<P>(old_row_rev, field_rev);
+            if let Ok((insert, delete)) =
+                self.create_or_delete_group_when_cell_changed(row_rev, old_cell_data.as_ref(), &cell_data)
+            {
+                result.inserted_group = insert;
+                result.deleted_group = delete;
+            }
 
-            if let Some(default_group_changeset) = self.update_default_group(row_rev, &changesets) {
-                tracing::trace!("default_group_changeset: {}", default_group_changeset);
-                if !default_group_changeset.is_empty() {
-                    changesets.push(default_group_changeset);
+            let mut changesets = self.add_or_remove_row_when_cell_changed(row_rev, &cell_data);
+            if let Some(changeset) = self.update_no_status_group(row_rev, &changesets) {
+                if !changeset.is_empty() {
+                    changesets.push(changeset);
                 }
             }
-            Ok(changesets)
-        } else {
-            Ok(vec![])
+            result.row_changesets = changesets;
         }
+
+        Ok(result)
     }
 
     fn did_delete_delete_row(
         &mut self,
         row_rev: &RowRevision,
         field_rev: &FieldRevision,
-    ) -> FlowyResult<Vec<GroupRowsNotificationPB>> {
+    ) -> FlowyResult<DidMoveGroupRowResult> {
         // if the cell_rev is none, then the row must in the default group.
+        let mut result = DidMoveGroupRowResult {
+            deleted_group: None,
+            row_changesets: vec![],
+        };
         if let Some(cell_rev) = row_rev.cells.get(&self.field_id) {
-            let cell_bytes = decode_type_cell_data(cell_rev.type_cell_data.clone(), field_rev, None).1;
+            let cell_bytes = get_type_cell_protobuf(cell_rev.type_cell_data.clone(), field_rev, None).1;
             let cell_data = cell_bytes.parser::<P>()?;
             if !cell_data.is_empty() {
                 tracing::error!("did_delete_delete_row {:?}", cell_rev.type_cell_data);
-                return Ok(self.delete_row(row_rev, &cell_data));
+                result.row_changesets = self.delete_row(row_rev, &cell_data);
+                return Ok(result);
             }
         }
 
         match self.group_ctx.get_no_status_group() {
             None => {
                 tracing::error!("Unexpected None value. It should have the no status group");
-                Ok(vec![])
             }
             Some(no_status_group) => {
                 if !no_status_group.contains_row(&row_rev.id) {
                     tracing::error!("The row: {} should be in the no status group", row_rev.id);
                 }
-                Ok(vec![GroupRowsNotificationPB::delete(
+                result.row_changesets = vec![GroupRowsNotificationPB::delete(
                     no_status_group.id.clone(),
                     vec![row_rev.id.clone()],
-                )])
+                )];
             }
         }
+        Ok(result)
     }
 
     #[tracing::instrument(level = "trace", skip_all, err)]
-    fn move_group_row(&mut self, context: MoveGroupRowContext) -> FlowyResult<Vec<GroupRowsNotificationPB>> {
+    fn move_group_row(&mut self, context: MoveGroupRowContext) -> FlowyResult<DidMoveGroupRowResult> {
+        let mut result = DidMoveGroupRowResult {
+            deleted_group: None,
+            row_changesets: vec![],
+        };
         let cell_rev = match context.row_rev.cells.get(&self.field_id) {
             Some(cell_rev) => Some(cell_rev.clone()),
-            None => self.default_cell_rev(),
+            None => self.placeholder_cell(),
         };
 
         if let Some(cell_rev) = cell_rev {
-            let cell_bytes = decode_type_cell_data(cell_rev.type_cell_data, context.field_rev, None).1;
+            let cell_bytes = get_type_cell_protobuf(cell_rev.type_cell_data, context.field_rev, None).1;
             let cell_data = cell_bytes.parser::<P>()?;
-            Ok(self.move_row(&cell_data, context))
+            result.deleted_group = self.delete_group_when_move_row(context.row_rev, &cell_data);
+            result.row_changesets = self.move_row(&cell_data, context);
         } else {
             tracing::warn!("Unexpected moving group row, changes should not be empty");
-            Ok(vec![])
         }
+        Ok(result)
     }
 
     fn did_update_group_field(&mut self, _field_rev: &FieldRevision) -> FlowyResult<Option<GroupViewChangesetPB>> {
@@ -298,3 +328,12 @@ struct GroupedRow {
     row: RowPB,
     group_id: String,
 }
+
+fn get_cell_data_from_row_rev<P: CellProtobufBlobParser>(
+    row_rev: Option<&RowRevision>,
+    field_rev: &FieldRevision,
+) -> Option<P::Object> {
+    let cell_rev: &CellRevision = row_rev.and_then(|row_rev| row_rev.cells.get(&field_rev.id))?;
+    let cell_bytes = get_type_cell_protobuf(cell_rev.type_cell_data.clone(), field_rev, None).1;
+    cell_bytes.parser::<P>().ok()
+}

+ 9 - 9
frontend/rust-lib/flowy-database/src/services/group/controller_impls/checkbox_controller.rs

@@ -1,6 +1,6 @@
 use crate::entities::{GroupRowsNotificationPB, InsertedRowPB, RowPB};
 use crate::services::field::{CheckboxCellData, CheckboxCellDataParser, CheckboxTypeOptionPB, CHECK, UNCHECK};
-use crate::services::group::action::GroupControllerCustomActions;
+use crate::services::group::action::GroupCustomize;
 use crate::services::group::configuration::GroupContext;
 use crate::services::group::controller::{
     GenericGroupController, GroupController, GroupGenerator, MoveGroupRowContext,
@@ -19,13 +19,13 @@ pub type CheckboxGroupController = GenericGroupController<
 
 pub type CheckboxGroupContext = GroupContext<CheckboxGroupConfigurationRevision>;
 
-impl GroupControllerCustomActions for CheckboxGroupController {
-    type CellDataType = CheckboxCellData;
-    fn default_cell_rev(&self) -> Option<CellRevision> {
+impl GroupCustomize for CheckboxGroupController {
+    type CellData = CheckboxCellData;
+    fn placeholder_cell(&self) -> Option<CellRevision> {
         Some(CellRevision::new(UNCHECK.to_string()))
     }
 
-    fn can_group(&self, content: &str, cell_data: &Self::CellDataType) -> bool {
+    fn can_group(&self, content: &str, cell_data: &Self::CellData) -> bool {
         if cell_data.is_check() {
             content == CHECK
         } else {
@@ -33,10 +33,10 @@ impl GroupControllerCustomActions for CheckboxGroupController {
         }
     }
 
-    fn add_or_remove_row_in_groups_if_match(
+    fn add_or_remove_row_when_cell_changed(
         &mut self,
         row_rev: &RowRevision,
-        cell_data: &Self::CellDataType,
+        cell_data: &Self::CellData,
     ) -> Vec<GroupRowsNotificationPB> {
         let mut changesets = vec![];
         self.group_ctx.iter_mut_status_groups(|group| {
@@ -79,7 +79,7 @@ impl GroupControllerCustomActions for CheckboxGroupController {
         changesets
     }
 
-    fn delete_row(&mut self, row_rev: &RowRevision, _cell_data: &Self::CellDataType) -> Vec<GroupRowsNotificationPB> {
+    fn delete_row(&mut self, row_rev: &RowRevision, _cell_data: &Self::CellData) -> Vec<GroupRowsNotificationPB> {
         let mut changesets = vec![];
         self.group_ctx.iter_mut_groups(|group| {
             let mut changeset = GroupRowsNotificationPB::new(group.id.clone());
@@ -97,7 +97,7 @@ impl GroupControllerCustomActions for CheckboxGroupController {
 
     fn move_row(
         &mut self,
-        _cell_data: &Self::CellDataType,
+        _cell_data: &Self::CellData,
         mut context: MoveGroupRowContext,
     ) -> Vec<GroupRowsNotificationPB> {
         let mut group_changeset = vec![];

+ 20 - 9
frontend/rust-lib/flowy-database/src/services/group/controller_impls/default_controller.rs

@@ -1,5 +1,5 @@
-use crate::entities::{GroupRowsNotificationPB, GroupViewChangesetPB, RowPB};
-use crate::services::group::action::GroupControllerSharedActions;
+use crate::entities::{GroupViewChangesetPB, RowPB};
+use crate::services::group::action::{DidMoveGroupRowResult, DidUpdateGroupRowResult, GroupControllerActions};
 use crate::services::group::{Group, GroupController, MoveGroupRowContext};
 use flowy_error::FlowyResult;
 use grid_model::{FieldRevision, RowRevision};
@@ -31,7 +31,7 @@ impl DefaultGroupController {
     }
 }
 
-impl GroupControllerSharedActions for DefaultGroupController {
+impl GroupControllerActions for DefaultGroupController {
     fn field_id(&self) -> &str {
         &self.field_id
     }
@@ -57,22 +57,33 @@ impl GroupControllerSharedActions for DefaultGroupController {
 
     fn did_update_group_row(
         &mut self,
+        _old_row_rev: &Option<Arc<RowRevision>>,
         _row_rev: &RowRevision,
         _field_rev: &FieldRevision,
-    ) -> FlowyResult<Vec<GroupRowsNotificationPB>> {
-        Ok(vec![])
+    ) -> FlowyResult<DidUpdateGroupRowResult> {
+        Ok(DidUpdateGroupRowResult {
+            inserted_group: None,
+            deleted_group: None,
+            row_changesets: vec![],
+        })
     }
 
     fn did_delete_delete_row(
         &mut self,
         _row_rev: &RowRevision,
         _field_rev: &FieldRevision,
-    ) -> FlowyResult<Vec<GroupRowsNotificationPB>> {
-        Ok(vec![])
+    ) -> FlowyResult<DidMoveGroupRowResult> {
+        Ok(DidMoveGroupRowResult {
+            deleted_group: None,
+            row_changesets: vec![],
+        })
     }
 
-    fn move_group_row(&mut self, _context: MoveGroupRowContext) -> FlowyResult<Vec<GroupRowsNotificationPB>> {
-        todo!()
+    fn move_group_row(&mut self, _context: MoveGroupRowContext) -> FlowyResult<DidMoveGroupRowResult> {
+        Ok(DidMoveGroupRowResult {
+            deleted_group: None,
+            row_changesets: vec![],
+        })
     }
 
     fn did_update_group_field(&mut self, _field_rev: &FieldRevision) -> FlowyResult<Option<GroupViewChangesetPB>> {

+ 7 - 7
frontend/rust-lib/flowy-database/src/services/group/controller_impls/select_option_controller/multi_select_controller.rs

@@ -1,7 +1,7 @@
 use crate::entities::{GroupRowsNotificationPB, RowPB};
 use crate::services::cell::insert_select_option_cell;
 use crate::services::field::{MultiSelectTypeOptionPB, SelectOptionCellDataPB, SelectOptionCellDataParser};
-use crate::services::group::action::GroupControllerCustomActions;
+use crate::services::group::action::GroupCustomize;
 
 use crate::services::group::controller::{
     GenericGroupController, GroupController, GroupGenerator, MoveGroupRowContext,
@@ -19,17 +19,17 @@ pub type MultiSelectGroupController = GenericGroupController<
     SelectOptionCellDataParser,
 >;
 
-impl GroupControllerCustomActions for MultiSelectGroupController {
-    type CellDataType = SelectOptionCellDataPB;
+impl GroupCustomize for MultiSelectGroupController {
+    type CellData = SelectOptionCellDataPB;
 
     fn can_group(&self, content: &str, cell_data: &SelectOptionCellDataPB) -> bool {
         cell_data.select_options.iter().any(|option| option.id == content)
     }
 
-    fn add_or_remove_row_in_groups_if_match(
+    fn add_or_remove_row_when_cell_changed(
         &mut self,
         row_rev: &RowRevision,
-        cell_data: &Self::CellDataType,
+        cell_data: &Self::CellData,
     ) -> Vec<GroupRowsNotificationPB> {
         let mut changesets = vec![];
         self.group_ctx.iter_mut_status_groups(|group| {
@@ -40,7 +40,7 @@ impl GroupControllerCustomActions for MultiSelectGroupController {
         changesets
     }
 
-    fn delete_row(&mut self, row_rev: &RowRevision, cell_data: &Self::CellDataType) -> Vec<GroupRowsNotificationPB> {
+    fn delete_row(&mut self, row_rev: &RowRevision, cell_data: &Self::CellData) -> Vec<GroupRowsNotificationPB> {
         let mut changesets = vec![];
         self.group_ctx.iter_mut_status_groups(|group| {
             if let Some(changeset) = remove_select_option_row(group, cell_data, row_rev) {
@@ -52,7 +52,7 @@ impl GroupControllerCustomActions for MultiSelectGroupController {
 
     fn move_row(
         &mut self,
-        _cell_data: &Self::CellDataType,
+        _cell_data: &Self::CellData,
         mut context: MoveGroupRowContext,
     ) -> Vec<GroupRowsNotificationPB> {
         let mut group_changeset = vec![];

+ 7 - 7
frontend/rust-lib/flowy-database/src/services/group/controller_impls/select_option_controller/single_select_controller.rs

@@ -1,7 +1,7 @@
 use crate::entities::{GroupRowsNotificationPB, RowPB};
 use crate::services::cell::insert_select_option_cell;
 use crate::services::field::{SelectOptionCellDataPB, SelectOptionCellDataParser, SingleSelectTypeOptionPB};
-use crate::services::group::action::GroupControllerCustomActions;
+use crate::services::group::action::GroupCustomize;
 
 use crate::services::group::controller::{
     GenericGroupController, GroupController, GroupGenerator, MoveGroupRowContext,
@@ -20,16 +20,16 @@ pub type SingleSelectGroupController = GenericGroupController<
     SelectOptionCellDataParser,
 >;
 
-impl GroupControllerCustomActions for SingleSelectGroupController {
-    type CellDataType = SelectOptionCellDataPB;
+impl GroupCustomize for SingleSelectGroupController {
+    type CellData = SelectOptionCellDataPB;
     fn can_group(&self, content: &str, cell_data: &SelectOptionCellDataPB) -> bool {
         cell_data.select_options.iter().any(|option| option.id == content)
     }
 
-    fn add_or_remove_row_in_groups_if_match(
+    fn add_or_remove_row_when_cell_changed(
         &mut self,
         row_rev: &RowRevision,
-        cell_data: &Self::CellDataType,
+        cell_data: &Self::CellData,
     ) -> Vec<GroupRowsNotificationPB> {
         let mut changesets = vec![];
         self.group_ctx.iter_mut_status_groups(|group| {
@@ -40,7 +40,7 @@ impl GroupControllerCustomActions for SingleSelectGroupController {
         changesets
     }
 
-    fn delete_row(&mut self, row_rev: &RowRevision, cell_data: &Self::CellDataType) -> Vec<GroupRowsNotificationPB> {
+    fn delete_row(&mut self, row_rev: &RowRevision, cell_data: &Self::CellData) -> Vec<GroupRowsNotificationPB> {
         let mut changesets = vec![];
         self.group_ctx.iter_mut_status_groups(|group| {
             if let Some(changeset) = remove_select_option_row(group, cell_data, row_rev) {
@@ -52,7 +52,7 @@ impl GroupControllerCustomActions for SingleSelectGroupController {
 
     fn move_row(
         &mut self,
-        _cell_data: &Self::CellDataType,
+        _cell_data: &Self::CellData,
         mut context: MoveGroupRowContext,
     ) -> Vec<GroupRowsNotificationPB> {
         let mut group_changeset = vec![];

+ 74 - 18
frontend/rust-lib/flowy-database/src/services/group/controller_impls/url_controller.rs

@@ -1,12 +1,13 @@
-use crate::entities::{GroupRowsNotificationPB, InsertedRowPB, RowPB};
+use crate::entities::{GroupPB, GroupRowsNotificationPB, InsertedGroupPB, InsertedRowPB, RowPB};
 use crate::services::cell::insert_url_cell;
-use crate::services::field::{URLCellDataPB, URLCellDataParser, URLTypeOptionPB};
-use crate::services::group::action::GroupControllerCustomActions;
+use crate::services::field::{URLCellData, URLCellDataPB, URLCellDataParser, URLTypeOptionPB};
+use crate::services::group::action::GroupCustomize;
 use crate::services::group::configuration::GroupContext;
 use crate::services::group::controller::{
     GenericGroupController, GroupController, GroupGenerator, MoveGroupRowContext,
 };
 use crate::services::group::{make_no_status_group, move_group_row, GeneratedGroupConfig, GeneratedGroupContext};
+use flowy_error::FlowyResult;
 use grid_model::{CellRevision, FieldRevision, GroupRevision, RowRevision, URLGroupConfigurationRevision};
 
 pub type URLGroupController =
@@ -14,21 +15,61 @@ pub type URLGroupController =
 
 pub type URLGroupContext = GroupContext<URLGroupConfigurationRevision>;
 
-impl GroupControllerCustomActions for URLGroupController {
-    type CellDataType = URLCellDataPB;
+impl GroupCustomize for URLGroupController {
+    type CellData = URLCellDataPB;
 
-    fn default_cell_rev(&self) -> Option<CellRevision> {
+    fn placeholder_cell(&self) -> Option<CellRevision> {
         Some(CellRevision::new("".to_string()))
     }
 
-    fn can_group(&self, content: &str, cell_data: &Self::CellDataType) -> bool {
+    fn can_group(&self, content: &str, cell_data: &Self::CellData) -> bool {
         cell_data.content == content
     }
 
-    fn add_or_remove_row_in_groups_if_match(
+    fn create_or_delete_group_when_cell_changed(
         &mut self,
         row_rev: &RowRevision,
-        cell_data: &Self::CellDataType,
+        old_cell_data: Option<&Self::CellData>,
+        cell_data: &Self::CellData,
+    ) -> FlowyResult<(Option<InsertedGroupPB>, Option<GroupPB>)> {
+        // Just return if the group with this url already exists
+        let mut inserted_group = None;
+        if self.group_ctx.get_group(&cell_data.url).is_none() {
+            let cell_data: URLCellData = cell_data.clone().into();
+            let group_revision = make_group_from_url_cell(&cell_data);
+            let mut new_group = self.group_ctx.add_new_group(group_revision)?;
+            new_group.group.rows.push(RowPB::from(row_rev));
+            inserted_group = Some(new_group);
+        }
+
+        // Delete the old url group if there are no rows in that group
+        let deleted_group =
+            match old_cell_data.and_then(|old_cell_data| self.group_ctx.get_group(&old_cell_data.content)) {
+                None => None,
+                Some((_, group)) => {
+                    if group.rows.len() == 1 {
+                        Some(group.clone())
+                    } else {
+                        None
+                    }
+                }
+            };
+
+        let deleted_group = match deleted_group {
+            None => None,
+            Some(group) => {
+                self.group_ctx.delete_group(&group.id)?;
+                Some(GroupPB::from(group.clone()))
+            }
+        };
+
+        Ok((inserted_group, deleted_group))
+    }
+
+    fn add_or_remove_row_when_cell_changed(
+        &mut self,
+        row_rev: &RowRevision,
+        cell_data: &Self::CellData,
     ) -> Vec<GroupRowsNotificationPB> {
         let mut changesets = vec![];
         self.group_ctx.iter_mut_status_groups(|group| {
@@ -51,7 +92,7 @@ impl GroupControllerCustomActions for URLGroupController {
         changesets
     }
 
-    fn delete_row(&mut self, row_rev: &RowRevision, _cell_data: &Self::CellDataType) -> Vec<GroupRowsNotificationPB> {
+    fn delete_row(&mut self, row_rev: &RowRevision, _cell_data: &Self::CellData) -> Vec<GroupRowsNotificationPB> {
         let mut changesets = vec![];
         self.group_ctx.iter_mut_groups(|group| {
             let mut changeset = GroupRowsNotificationPB::new(group.id.clone());
@@ -69,7 +110,7 @@ impl GroupControllerCustomActions for URLGroupController {
 
     fn move_row(
         &mut self,
-        _cell_data: &Self::CellDataType,
+        _cell_data: &Self::CellData,
         mut context: MoveGroupRowContext,
     ) -> Vec<GroupRowsNotificationPB> {
         let mut group_changeset = vec![];
@@ -80,6 +121,19 @@ impl GroupControllerCustomActions for URLGroupController {
         });
         group_changeset
     }
+
+    fn delete_group_when_move_row(&mut self, _row_rev: &RowRevision, cell_data: &Self::CellData) -> Option<GroupPB> {
+        let mut deleted_group = None;
+        if let Some((_, group)) = self.group_ctx.get_group(&cell_data.content) {
+            if group.rows.len() == 1 {
+                deleted_group = Some(GroupPB::from(group.clone()));
+            }
+        }
+        if deleted_group.is_some() {
+            let _ = self.group_ctx.delete_group(&deleted_group.as_ref().unwrap().group_id);
+        }
+        deleted_group
+    }
 }
 
 impl GroupController for URLGroupController {
@@ -117,13 +171,9 @@ impl GroupGenerator for URLGroupGenerator {
         let group_configs = cells
             .into_iter()
             .flat_map(|value| value.into_url_field_cell_data())
-            .map(|cell| {
-                let group_id = cell.content.clone();
-                let group_name = cell.content.clone();
-                GeneratedGroupConfig {
-                    group_rev: GroupRevision::new(group_id, group_name),
-                    filter_content: cell.content,
-                }
+            .map(|cell| GeneratedGroupConfig {
+                group_rev: make_group_from_url_cell(&cell),
+                filter_content: cell.content,
             })
             .collect();
 
@@ -134,3 +184,9 @@ impl GroupGenerator for URLGroupGenerator {
         }
     }
 }
+
+fn make_group_from_url_cell(cell: &URLCellData) -> GroupRevision {
+    let group_id = cell.content.clone();
+    let group_name = cell.content.clone();
+    GroupRevision::new(group_id, group_name)
+}

+ 1 - 1
frontend/rust-lib/flowy-database/src/services/group/entities.rs

@@ -1,6 +1,6 @@
 use crate::entities::RowPB;
 
-#[derive(Clone, PartialEq, Eq)]
+#[derive(Clone, PartialEq, Debug, Eq)]
 pub struct Group {
     pub id: String,
     pub field_id: String,

+ 44 - 17
frontend/rust-lib/flowy-database/src/services/view_editor/editor.rs

@@ -240,29 +240,44 @@ impl DatabaseViewRevisionEditor {
     #[tracing::instrument(level = "trace", skip_all)]
     pub async fn did_delete_view_row(&self, row_rev: &RowRevision) {
         // Send the group notification if the current view has groups;
-        let changesets = self
+        let result = self
             .mut_group_controller(|group_controller, field_rev| {
                 group_controller.did_delete_delete_row(row_rev, &field_rev)
             })
             .await;
 
-        tracing::trace!("Delete row in view changeset: {:?}", changesets);
-        if let Some(changesets) = changesets {
-            for changeset in changesets {
+        if let Some(result) = result {
+            tracing::trace!("Delete row in view changeset: {:?}", result.row_changesets);
+            for changeset in result.row_changesets {
                 self.notify_did_update_group_rows(changeset).await;
             }
         }
     }
 
-    pub async fn did_update_view_cell(&self, row_rev: &RowRevision) {
-        let changesets = self
+    pub async fn did_update_view_row(&self, old_row_rev: Option<Arc<RowRevision>>, row_rev: &RowRevision) {
+        let result = self
             .mut_group_controller(|group_controller, field_rev| {
-                group_controller.did_update_group_row(row_rev, &field_rev)
+                Ok(group_controller.did_update_group_row(&old_row_rev, row_rev, &field_rev))
             })
             .await;
 
-        if let Some(changesets) = changesets {
-            for changeset in changesets {
+        if let Some(Ok(result)) = result {
+            let mut changeset = GroupViewChangesetPB {
+                view_id: self.view_id.clone(),
+                ..Default::default()
+            };
+            if let Some(inserted_group) = result.inserted_group {
+                tracing::trace!("Create group after editing the row: {:?}", inserted_group);
+                changeset.inserted_groups.push(inserted_group);
+            }
+            if let Some(delete_group) = result.deleted_group {
+                tracing::trace!("Delete group after editing the row: {:?}", delete_group);
+                changeset.deleted_groups.push(delete_group.group_id);
+            }
+            self.notify_did_update_view(changeset).await;
+
+            tracing::trace!("Group changesets after editing the row: {:?}", result.row_changesets);
+            for changeset in result.row_changesets {
                 self.notify_did_update_group_rows(changeset).await;
             }
         }
@@ -282,8 +297,8 @@ impl DatabaseViewRevisionEditor {
         row_changeset: &mut RowChangeset,
         to_group_id: &str,
         to_row_id: Option<String>,
-    ) -> Vec<GroupRowsNotificationPB> {
-        let changesets = self
+    ) {
+        let result = self
             .mut_group_controller(|group_controller, field_rev| {
                 let move_row_context = MoveGroupRowContext {
                     row_rev,
@@ -292,13 +307,25 @@ impl DatabaseViewRevisionEditor {
                     to_group_id,
                     to_row_id,
                 };
-
-                let changesets = group_controller.move_group_row(move_row_context)?;
-                Ok(changesets)
+                group_controller.move_group_row(move_row_context)
             })
             .await;
 
-        changesets.unwrap_or_default()
+        if let Some(result) = result {
+            let mut changeset = GroupViewChangesetPB {
+                view_id: self.view_id.clone(),
+                ..Default::default()
+            };
+            if let Some(delete_group) = result.deleted_group {
+                tracing::info!("Delete group after moving the row: {:?}", delete_group);
+                changeset.deleted_groups.push(delete_group.group_id);
+            }
+            self.notify_did_update_view(changeset).await;
+
+            for changeset in result.row_changesets {
+                self.notify_did_update_group_rows(changeset).await;
+            }
+        }
     }
     /// Only call once after grid view editor initialized
     #[tracing::instrument(level = "trace", skip(self))]
@@ -334,7 +361,7 @@ impl DatabaseViewRevisionEditor {
                     inserted_groups: vec![inserted_group],
                     deleted_groups: vec![params.from_group_id.clone()],
                     update_groups: vec![],
-                    new_groups: vec![],
+                    initial_groups: vec![],
                 };
 
                 self.notify_did_update_view(changeset).await;
@@ -610,7 +637,7 @@ impl DatabaseViewRevisionEditor {
             *self.group_controller.write().await = new_group_controller;
             let changeset = GroupViewChangesetPB {
                 view_id: self.view_id.clone(),
-                new_groups,
+                initial_groups: new_groups,
                 ..Default::default()
             };
 

+ 3 - 7
frontend/rust-lib/flowy-database/src/services/view_editor/editor_manager.rs

@@ -88,14 +88,14 @@ impl DatabaseViewManager {
     }
 
     /// Insert/Delete the group's row if the corresponding cell data was changed.  
-    pub async fn did_update_cell(&self, row_id: &str) {
+    pub async fn did_update_row(&self, old_row_rev: Option<Arc<RowRevision>>, row_id: &str) {
         match self.delegate.get_row_rev(row_id).await {
             None => {
                 tracing::warn!("Can not find the row in grid view");
             }
             Some((_, row_rev)) => {
                 for view_editor in self.view_editors.read().await.values() {
-                    view_editor.did_update_view_cell(&row_rev).await;
+                    view_editor.did_update_view_row(old_row_rev.clone(), &row_rev).await;
                 }
             }
         }
@@ -192,7 +192,7 @@ impl DatabaseViewManager {
     ) -> FlowyResult<()> {
         let mut row_changeset = RowChangeset::new(row_rev.id.clone());
         let view_editor = self.get_default_view_editor().await?;
-        let group_changesets = view_editor
+        view_editor
             .move_view_group_row(&row_rev, &mut row_changeset, &to_group_id, to_row_id.clone())
             .await;
 
@@ -200,10 +200,6 @@ impl DatabaseViewManager {
             recv_row_changeset(row_changeset).await;
         }
 
-        for group_changeset in group_changesets {
-            view_editor.notify_did_update_group_rows(group_changeset).await;
-        }
-
         Ok(())
     }
 

+ 3 - 3
frontend/rust-lib/flowy-database/tests/grid/block_test/block_test.rs

@@ -1,4 +1,4 @@
-use crate::grid::block_test::script::GridRowTest;
+use crate::grid::block_test::script::DatabaseRowTest;
 use crate::grid::block_test::script::RowScript::*;
 
 use grid_model::{GridBlockMetaRevision, GridBlockMetaRevisionChangeset};
@@ -11,7 +11,7 @@ async fn grid_create_block() {
         CreateBlock { block: block_meta_rev },
         AssertBlockCount(2),
     ];
-    GridRowTest::new().await.run_scripts(scripts).await;
+    DatabaseRowTest::new().await.run_scripts(scripts).await;
 }
 
 #[tokio::test]
@@ -37,5 +37,5 @@ async fn grid_update_block() {
             block: cloned_grid_block,
         },
     ];
-    GridRowTest::new().await.run_scripts(scripts).await;
+    DatabaseRowTest::new().await.run_scripts(scripts).await;
 }

+ 10 - 10
frontend/rust-lib/flowy-database/tests/grid/block_test/row_test.rs

@@ -1,13 +1,13 @@
 use crate::grid::block_test::script::RowScript::*;
-use crate::grid::block_test::script::{CreateRowScriptBuilder, GridRowTest};
-use crate::grid::grid_editor::{COMPLETED, FACEBOOK, GOOGLE, PAUSED, TWITTER};
+use crate::grid::block_test::script::{CreateRowScriptBuilder, DatabaseRowTest};
+use crate::grid::mock_data::{COMPLETED, FACEBOOK, GOOGLE, PAUSED, TWITTER};
 use flowy_database::entities::FieldType;
 use flowy_database::services::field::{SELECTION_IDS_SEPARATOR, UNCHECK};
 use grid_model::RowChangeset;
 
 #[tokio::test]
 async fn grid_create_row_count_test() {
-    let mut test = GridRowTest::new().await;
+    let mut test = DatabaseRowTest::new().await;
     let scripts = vec![
         AssertRowCount(6),
         CreateEmptyRow,
@@ -22,7 +22,7 @@ async fn grid_create_row_count_test() {
 
 #[tokio::test]
 async fn grid_update_row() {
-    let mut test = GridRowTest::new().await;
+    let mut test = DatabaseRowTest::new().await;
     let row_rev = test.row_builder().build();
     let changeset = RowChangeset {
         row_id: row_rev.id.clone(),
@@ -41,7 +41,7 @@ async fn grid_update_row() {
 
 #[tokio::test]
 async fn grid_delete_row() {
-    let mut test = GridRowTest::new().await;
+    let mut test = DatabaseRowTest::new().await;
     let row_1 = test.row_builder().build();
     let row_2 = test.row_builder().build();
     let row_ids = vec![row_1.id.clone(), row_2.id.clone()];
@@ -67,7 +67,7 @@ async fn grid_delete_row() {
 
 #[tokio::test]
 async fn grid_row_add_cells_test() {
-    let mut test = GridRowTest::new().await;
+    let mut test = DatabaseRowTest::new().await;
     let mut builder = CreateRowScriptBuilder::new(&test);
     builder.insert(FieldType::RichText, "hello world", "hello world");
     builder.insert(FieldType::DateTime, "1647251762", "2022/03/14");
@@ -85,7 +85,7 @@ async fn grid_row_add_cells_test() {
 
 #[tokio::test]
 async fn grid_row_insert_number_test() {
-    let mut test = GridRowTest::new().await;
+    let mut test = DatabaseRowTest::new().await;
     for (val, expected) in &[("1647251762", "2022/03/14"), ("2022/03/14", ""), ("", "")] {
         let mut builder = CreateRowScriptBuilder::new(&test);
         builder.insert(FieldType::DateTime, val, expected);
@@ -96,7 +96,7 @@ async fn grid_row_insert_number_test() {
 
 #[tokio::test]
 async fn grid_row_insert_date_test() {
-    let mut test = GridRowTest::new().await;
+    let mut test = DatabaseRowTest::new().await;
     for (val, expected) in &[
         ("18,443", "$18,443.00"),
         ("0", "$0.00"),
@@ -112,7 +112,7 @@ async fn grid_row_insert_date_test() {
 }
 #[tokio::test]
 async fn grid_row_insert_single_select_test() {
-    let mut test = GridRowTest::new().await;
+    let mut test = DatabaseRowTest::new().await;
     let mut builder = CreateRowScriptBuilder::new(&test);
     builder.insert_single_select_cell(|mut options| options.pop().unwrap(), PAUSED);
     let scripts = builder.build();
@@ -121,7 +121,7 @@ async fn grid_row_insert_single_select_test() {
 
 #[tokio::test]
 async fn grid_row_insert_multi_select_test() {
-    let mut test = GridRowTest::new().await;
+    let mut test = DatabaseRowTest::new().await;
     let mut builder = CreateRowScriptBuilder::new(&test);
     builder.insert_multi_select_cell(
         |mut options| {

+ 9 - 9
frontend/rust-lib/flowy-database/tests/grid/block_test/script.rs

@@ -1,6 +1,6 @@
 use crate::grid::block_test::script::RowScript::{AssertCell, CreateRow};
 use crate::grid::block_test::util::GridRowTestBuilder;
-use crate::grid::grid_editor::GridEditorTest;
+use crate::grid::database_editor::DatabaseEditorTest;
 use flowy_database::entities::{CellPathParams, CreateRowParams, DatabaseViewLayout, FieldType, RowPB};
 use flowy_database::services::field::*;
 use flowy_database::services::row::DatabaseBlockRow;
@@ -48,13 +48,13 @@ pub enum RowScript {
     },
 }
 
-pub struct GridRowTest {
-    inner: GridEditorTest,
+pub struct DatabaseRowTest {
+    inner: DatabaseEditorTest,
 }
 
-impl GridRowTest {
+impl DatabaseRowTest {
     pub async fn new() -> Self {
-        let editor_test = GridEditorTest::new_table().await;
+        let editor_test = DatabaseEditorTest::new_table().await;
         Self { inner: editor_test }
     }
 
@@ -282,15 +282,15 @@ fn block_from_row_pbs(row_orders: Vec<RowPB>) -> Vec<DatabaseBlockRow> {
     map.into_values().collect::<Vec<_>>()
 }
 
-impl std::ops::Deref for GridRowTest {
-    type Target = GridEditorTest;
+impl std::ops::Deref for DatabaseRowTest {
+    type Target = DatabaseEditorTest;
 
     fn deref(&self) -> &Self::Target {
         &self.inner
     }
 }
 
-impl std::ops::DerefMut for GridRowTest {
+impl std::ops::DerefMut for DatabaseRowTest {
     fn deref_mut(&mut self) -> &mut Self::Target {
         &mut self.inner
     }
@@ -303,7 +303,7 @@ pub struct CreateRowScriptBuilder<'a> {
 }
 
 impl<'a> CreateRowScriptBuilder<'a> {
-    pub fn new(test: &'a GridRowTest) -> Self {
+    pub fn new(test: &'a DatabaseRowTest) -> Self {
         Self {
             builder: test.row_builder(),
             data_by_field_type: HashMap::new(),

+ 8 - 8
frontend/rust-lib/flowy-database/tests/grid/cell_test/script.rs

@@ -1,17 +1,17 @@
-use crate::grid::grid_editor::GridEditorTest;
+use crate::grid::database_editor::DatabaseEditorTest;
 use flowy_database::entities::CellChangesetPB;
 
 pub enum CellScript {
     UpdateCell { changeset: CellChangesetPB, is_err: bool },
 }
 
-pub struct GridCellTest {
-    inner: GridEditorTest,
+pub struct DatabaseCellTest {
+    inner: DatabaseEditorTest,
 }
 
-impl GridCellTest {
+impl DatabaseCellTest {
     pub async fn new() -> Self {
-        let inner = GridEditorTest::new_table().await;
+        let inner = DatabaseEditorTest::new_table().await;
         Self { inner }
     }
 
@@ -48,15 +48,15 @@ impl GridCellTest {
     }
 }
 
-impl std::ops::Deref for GridCellTest {
-    type Target = GridEditorTest;
+impl std::ops::Deref for DatabaseCellTest {
+    type Target = DatabaseEditorTest;
 
     fn deref(&self) -> &Self::Target {
         &self.inner
     }
 }
 
-impl std::ops::DerefMut for GridCellTest {
+impl std::ops::DerefMut for DatabaseCellTest {
     fn deref_mut(&mut self) -> &mut Self::Target {
         &mut self.inner
     }

+ 4 - 4
frontend/rust-lib/flowy-database/tests/grid/cell_test/test.rs

@@ -1,5 +1,5 @@
 use crate::grid::cell_test::script::CellScript::*;
-use crate::grid::cell_test::script::GridCellTest;
+use crate::grid::cell_test::script::DatabaseCellTest;
 use crate::grid::field_test::util::make_date_cell_string;
 use flowy_database::entities::{CellChangesetPB, FieldType};
 use flowy_database::services::cell::ToCellChangesetString;
@@ -8,7 +8,7 @@ use flowy_database::services::field::{ChecklistTypeOptionPB, MultiSelectTypeOpti
 
 #[tokio::test]
 async fn grid_cell_update() {
-    let mut test = GridCellTest::new().await;
+    let mut test = DatabaseCellTest::new().await;
     let field_revs = &test.field_revs;
     let row_revs = &test.row_revs;
     let grid_blocks = &test.block_meta_revs;
@@ -60,7 +60,7 @@ async fn grid_cell_update() {
 
 #[tokio::test]
 async fn text_cell_date_test() {
-    let test = GridCellTest::new().await;
+    let test = DatabaseCellTest::new().await;
     let text_field = test.get_first_field_rev(FieldType::RichText);
     let cells = test
         .editor
@@ -84,7 +84,7 @@ async fn text_cell_date_test() {
 
 #[tokio::test]
 async fn url_cell_date_test() {
-    let test = GridCellTest::new().await;
+    let test = DatabaseCellTest::new().await;
     let url_field = test.get_first_field_rev(FieldType::URL);
     let cells = test
         .editor

+ 193 - 0
frontend/rust-lib/flowy-database/tests/grid/database_editor.rs

@@ -0,0 +1,193 @@
+use crate::grid::mock_data::*;
+use bytes::Bytes;
+use flowy_database::entities::*;
+use flowy_database::services::cell::ToCellChangesetString;
+use flowy_database::services::field::SelectOptionPB;
+use flowy_database::services::field::*;
+use flowy_database::services::grid_editor::DatabaseRevisionEditor;
+use flowy_test::helper::ViewTest;
+use flowy_test::FlowySDKTest;
+use grid_model::*;
+use std::collections::HashMap;
+use std::sync::Arc;
+use strum::EnumCount;
+
+pub struct DatabaseEditorTest {
+    pub sdk: FlowySDKTest,
+    pub view_id: String,
+    pub editor: Arc<DatabaseRevisionEditor>,
+    pub field_revs: Vec<Arc<FieldRevision>>,
+    pub block_meta_revs: Vec<Arc<GridBlockMetaRevision>>,
+    pub row_revs: Vec<Arc<RowRevision>>,
+    pub field_count: usize,
+    pub row_by_row_id: HashMap<String, RowPB>,
+}
+
+impl DatabaseEditorTest {
+    pub async fn new_table() -> Self {
+        Self::new(DatabaseViewLayout::Grid).await
+    }
+
+    pub async fn new_board() -> Self {
+        Self::new(DatabaseViewLayout::Board).await
+    }
+
+    pub async fn new(layout: DatabaseViewLayout) -> Self {
+        let sdk = FlowySDKTest::default();
+        let _ = sdk.init_user().await;
+        let test = match layout {
+            DatabaseViewLayout::Grid => {
+                let build_context = make_test_grid();
+                let view_data: Bytes = build_context.into();
+                ViewTest::new_grid_view(&sdk, view_data.to_vec()).await
+            }
+            DatabaseViewLayout::Board => {
+                let build_context = make_test_board();
+                let view_data: Bytes = build_context.into();
+                ViewTest::new_board_view(&sdk, view_data.to_vec()).await
+            }
+            DatabaseViewLayout::Calendar => {
+                let build_context = make_test_calendar();
+                let view_data: Bytes = build_context.into();
+                ViewTest::new_calendar_view(&sdk, view_data.to_vec()).await
+            }
+        };
+
+        let editor = sdk.grid_manager.open_database(&test.view.id).await.unwrap();
+        let field_revs = editor.get_field_revs(None).await.unwrap();
+        let block_meta_revs = editor.get_block_meta_revs().await.unwrap();
+        let row_pbs = editor.get_all_row_revs(&test.view.id).await.unwrap();
+        assert_eq!(block_meta_revs.len(), 1);
+
+        // It seems like you should add the field in the make_test_grid() function.
+        // Because we assert the initialize count of the fields is equal to FieldType::COUNT.
+        assert_eq!(field_revs.len(), FieldType::COUNT);
+
+        let grid_id = test.view.id;
+        Self {
+            sdk,
+            view_id: grid_id,
+            editor,
+            field_revs,
+            block_meta_revs,
+            row_revs: row_pbs,
+            field_count: FieldType::COUNT,
+            row_by_row_id: HashMap::default(),
+        }
+    }
+
+    pub async fn get_row_revs(&self) -> Vec<Arc<RowRevision>> {
+        self.editor.get_all_row_revs(&self.view_id).await.unwrap()
+    }
+
+    pub async fn grid_filters(&self) -> Vec<FilterPB> {
+        self.editor.get_all_filters().await.unwrap()
+    }
+
+    pub fn get_field_rev(&self, field_id: &str, field_type: FieldType) -> &Arc<FieldRevision> {
+        self.field_revs
+            .iter()
+            .filter(|field_rev| {
+                let t_field_type: FieldType = field_rev.ty.into();
+                field_rev.id == field_id && t_field_type == field_type
+            })
+            .collect::<Vec<_>>()
+            .pop()
+            .unwrap()
+    }
+
+    /// returns the first `FieldRevision` in the build-in test grid.
+    /// Not support duplicate `FieldType` in test grid yet.
+    pub fn get_first_field_rev(&self, field_type: FieldType) -> &Arc<FieldRevision> {
+        self.field_revs
+            .iter()
+            .filter(|field_rev| {
+                let t_field_type: FieldType = field_rev.ty.into();
+                t_field_type == field_type
+            })
+            .collect::<Vec<_>>()
+            .pop()
+            .unwrap()
+    }
+
+    pub fn get_multi_select_type_option(&self, field_id: &str) -> Vec<SelectOptionPB> {
+        let field_type = FieldType::MultiSelect;
+        let field_rev = self.get_field_rev(field_id, field_type.clone());
+        let type_option = field_rev
+            .get_type_option::<MultiSelectTypeOptionPB>(field_type.into())
+            .unwrap();
+        type_option.options
+    }
+
+    pub fn get_single_select_type_option(&self, field_id: &str) -> SingleSelectTypeOptionPB {
+        let field_type = FieldType::SingleSelect;
+        let field_rev = self.get_field_rev(field_id, field_type.clone());
+        field_rev
+            .get_type_option::<SingleSelectTypeOptionPB>(field_type.into())
+            .unwrap()
+    }
+
+    #[allow(dead_code)]
+    pub fn get_checklist_type_option(&self, field_id: &str) -> ChecklistTypeOptionPB {
+        let field_type = FieldType::Checklist;
+        let field_rev = self.get_field_rev(field_id, field_type.clone());
+        field_rev
+            .get_type_option::<ChecklistTypeOptionPB>(field_type.into())
+            .unwrap()
+    }
+
+    #[allow(dead_code)]
+    pub fn get_checkbox_type_option(&self, field_id: &str) -> CheckboxTypeOptionPB {
+        let field_type = FieldType::Checkbox;
+        let field_rev = self.get_field_rev(field_id, field_type.clone());
+        field_rev
+            .get_type_option::<CheckboxTypeOptionPB>(field_type.into())
+            .unwrap()
+    }
+
+    pub fn block_id(&self) -> &str {
+        &self.block_meta_revs.last().unwrap().block_id
+    }
+
+    pub async fn update_cell<T: ToCellChangesetString>(&mut self, field_id: &str, row_id: String, cell_changeset: T) {
+        let field_rev = self
+            .field_revs
+            .iter()
+            .find(|field_rev| field_rev.id == field_id)
+            .unwrap();
+
+        self.editor
+            .update_cell_with_changeset(&row_id, &field_rev.id, cell_changeset)
+            .await
+            .unwrap();
+    }
+
+    pub(crate) async fn update_text_cell(&mut self, row_id: String, content: &str) {
+        let field_rev = self
+            .field_revs
+            .iter()
+            .find(|field_rev| {
+                let field_type: FieldType = field_rev.ty.into();
+                field_type == FieldType::RichText
+            })
+            .unwrap()
+            .clone();
+
+        self.update_cell(&field_rev.id, row_id, content.to_string()).await;
+    }
+
+    pub(crate) async fn update_single_select_cell(&mut self, row_id: String, option_id: &str) {
+        let field_rev = self
+            .field_revs
+            .iter()
+            .find(|field_rev| {
+                let field_type: FieldType = field_rev.ty.into();
+                field_type == FieldType::SingleSelect
+            })
+            .unwrap()
+            .clone();
+
+        let cell_changeset = SelectOptionCellChangeset::from_insert_option_id(option_id);
+        self.update_cell(&field_rev.id, row_id, cell_changeset).await;
+    }
+}

+ 8 - 8
frontend/rust-lib/flowy-database/tests/grid/field_test/script.rs

@@ -1,4 +1,4 @@
-use crate::grid::grid_editor::GridEditorTest;
+use crate::grid::database_editor::DatabaseEditorTest;
 use flowy_database::entities::{CreateFieldParams, FieldChangesetParams, FieldType};
 use flowy_database::services::cell::{stringify_cell_data, TypeCellData};
 use grid_model::FieldRevision;
@@ -38,13 +38,13 @@ pub enum FieldScript {
     },
 }
 
-pub struct GridFieldTest {
-    inner: GridEditorTest,
+pub struct DatabaseFieldTest {
+    inner: DatabaseEditorTest,
 }
 
-impl GridFieldTest {
+impl DatabaseFieldTest {
     pub async fn new() -> Self {
-        let editor_test = GridEditorTest::new_table().await;
+        let editor_test = DatabaseEditorTest::new_table().await;
         Self { inner: editor_test }
     }
 
@@ -144,15 +144,15 @@ impl GridFieldTest {
     }
 }
 
-impl std::ops::Deref for GridFieldTest {
-    type Target = GridEditorTest;
+impl std::ops::Deref for DatabaseFieldTest {
+    type Target = DatabaseEditorTest;
 
     fn deref(&self) -> &Self::Target {
         &self.inner
     }
 }
 
-impl std::ops::DerefMut for GridFieldTest {
+impl std::ops::DerefMut for DatabaseFieldTest {
     fn deref_mut(&mut self) -> &mut Self::Target {
         &mut self.inner
     }

+ 13 - 13
frontend/rust-lib/flowy-database/tests/grid/field_test/test.rs

@@ -1,5 +1,5 @@
+use crate::grid::field_test::script::DatabaseFieldTest;
 use crate::grid::field_test::script::FieldScript::*;
-use crate::grid::field_test::script::GridFieldTest;
 use crate::grid::field_test::util::*;
 use bytes::Bytes;
 use flowy_database::entities::{FieldChangesetParams, FieldType};
@@ -8,7 +8,7 @@ use flowy_database::services::field::{gen_option_id, SingleSelectTypeOptionPB, C
 
 #[tokio::test]
 async fn grid_create_field() {
-    let mut test = GridFieldTest::new().await;
+    let mut test = DatabaseFieldTest::new().await;
     let (params, field_rev) = create_text_field(&test.view_id());
 
     let scripts = vec![
@@ -33,7 +33,7 @@ async fn grid_create_field() {
 
 #[tokio::test]
 async fn grid_create_duplicate_field() {
-    let mut test = GridFieldTest::new().await;
+    let mut test = DatabaseFieldTest::new().await;
     let (params, _) = create_text_field(&test.view_id());
     let field_count = test.field_count();
     let expected_field_count = field_count + 1;
@@ -46,7 +46,7 @@ async fn grid_create_duplicate_field() {
 
 #[tokio::test]
 async fn grid_update_field_with_empty_change() {
-    let mut test = GridFieldTest::new().await;
+    let mut test = DatabaseFieldTest::new().await;
     let (params, _) = create_single_select_field(&test.view_id());
     let create_field_index = test.field_count();
     let scripts = vec![CreateField { params }];
@@ -71,7 +71,7 @@ async fn grid_update_field_with_empty_change() {
 
 #[tokio::test]
 async fn grid_update_field() {
-    let mut test = GridFieldTest::new().await;
+    let mut test = DatabaseFieldTest::new().await;
     let (params, _) = create_single_select_field(&test.view_id());
     let scripts = vec![CreateField { params }];
     let create_field_index = test.field_count();
@@ -107,7 +107,7 @@ async fn grid_update_field() {
 
 #[tokio::test]
 async fn grid_delete_field() {
-    let mut test = GridFieldTest::new().await;
+    let mut test = DatabaseFieldTest::new().await;
     let original_field_count = test.field_count();
     let (params, _) = create_text_field(&test.view_id());
     let scripts = vec![CreateField { params }];
@@ -125,7 +125,7 @@ async fn grid_delete_field() {
 
 #[tokio::test]
 async fn grid_switch_from_select_option_to_checkbox_test() {
-    let mut test = GridFieldTest::new().await;
+    let mut test = DatabaseFieldTest::new().await;
     let field_rev = test.get_first_field_rev(FieldType::SingleSelect);
 
     // Update the type option data of single select option
@@ -160,7 +160,7 @@ async fn grid_switch_from_select_option_to_checkbox_test() {
 
 #[tokio::test]
 async fn grid_switch_from_checkbox_to_select_option_test() {
-    let mut test = GridFieldTest::new().await;
+    let mut test = DatabaseFieldTest::new().await;
     let field_rev = test.get_first_field_rev(FieldType::Checkbox).clone();
     let scripts = vec![
         // switch to single-select field type
@@ -203,7 +203,7 @@ async fn grid_switch_from_checkbox_to_select_option_test() {
 //      option1, option2 -> "option1.name, option2.name"
 #[tokio::test]
 async fn grid_switch_from_multi_select_to_text_test() {
-    let mut test = GridFieldTest::new().await;
+    let mut test = DatabaseFieldTest::new().await;
     let field_rev = test.get_first_field_rev(FieldType::MultiSelect).clone();
 
     let multi_select_type_option = test.get_multi_select_type_option(&field_rev.id);
@@ -235,7 +235,7 @@ async fn grid_switch_from_multi_select_to_text_test() {
 //      unchecked -> ""
 #[tokio::test]
 async fn grid_switch_from_checkbox_to_text_test() {
-    let mut test = GridFieldTest::new().await;
+    let mut test = DatabaseFieldTest::new().await;
     let field_rev = test.get_first_field_rev(FieldType::Checkbox);
 
     let scripts = vec![
@@ -265,7 +265,7 @@ async fn grid_switch_from_checkbox_to_text_test() {
 //      "" -> unchecked
 #[tokio::test]
 async fn grid_switch_from_text_to_checkbox_test() {
-    let mut test = GridFieldTest::new().await;
+    let mut test = DatabaseFieldTest::new().await;
     let field_rev = test.get_first_field_rev(FieldType::RichText).clone();
 
     let scripts = vec![
@@ -288,7 +288,7 @@ async fn grid_switch_from_text_to_checkbox_test() {
 //      1647251762 -> Mar 14,2022 (This string will be different base on current data setting)
 #[tokio::test]
 async fn grid_switch_from_date_to_text_test() {
-    let mut test = GridFieldTest::new().await;
+    let mut test = DatabaseFieldTest::new().await;
     let field_rev = test.get_first_field_rev(FieldType::DateTime).clone();
     let scripts = vec![
         SwitchToField {
@@ -316,7 +316,7 @@ async fn grid_switch_from_date_to_text_test() {
 //      $1 -> "$1"(This string will be different base on current data setting)
 #[tokio::test]
 async fn grid_switch_from_number_to_text_test() {
-    let mut test = GridFieldTest::new().await;
+    let mut test = DatabaseFieldTest::new().await;
     let field_rev = test.get_first_field_rev(FieldType::Number).clone();
 
     let scripts = vec![

+ 3 - 3
frontend/rust-lib/flowy-database/tests/grid/filter_test/checkbox_filter_test.rs

@@ -1,10 +1,10 @@
 use crate::grid::filter_test::script::FilterScript::*;
-use crate::grid::filter_test::script::{FilterRowChanged, GridFilterTest};
+use crate::grid::filter_test::script::{DatabaseFilterTest, FilterRowChanged};
 use flowy_database::entities::CheckboxFilterConditionPB;
 
 #[tokio::test]
 async fn grid_filter_checkbox_is_check_test() {
-    let mut test = GridFilterTest::new().await;
+    let mut test = DatabaseFilterTest::new().await;
     let row_count = test.row_revs.len();
     // The initial number of unchecked is 3
     // The initial number of checked is 2
@@ -20,7 +20,7 @@ async fn grid_filter_checkbox_is_check_test() {
 
 #[tokio::test]
 async fn grid_filter_checkbox_is_uncheck_test() {
-    let mut test = GridFilterTest::new().await;
+    let mut test = DatabaseFilterTest::new().await;
     let expected = 3;
     let row_count = test.row_revs.len();
     let scripts = vec![

+ 3 - 3
frontend/rust-lib/flowy-database/tests/grid/filter_test/checklist_filter_test.rs

@@ -1,10 +1,10 @@
 use crate::grid::filter_test::script::FilterScript::*;
-use crate::grid::filter_test::script::{FilterRowChanged, GridFilterTest};
+use crate::grid::filter_test::script::{DatabaseFilterTest, FilterRowChanged};
 use flowy_database::entities::ChecklistFilterConditionPB;
 
 #[tokio::test]
 async fn grid_filter_checklist_is_incomplete_test() {
-    let mut test = GridFilterTest::new().await;
+    let mut test = DatabaseFilterTest::new().await;
     let expected = 5;
     let row_count = test.row_revs.len();
     let scripts = vec![
@@ -22,7 +22,7 @@ async fn grid_filter_checklist_is_incomplete_test() {
 
 #[tokio::test]
 async fn grid_filter_checklist_is_complete_test() {
-    let mut test = GridFilterTest::new().await;
+    let mut test = DatabaseFilterTest::new().await;
     let expected = 1;
     let row_count = test.row_revs.len();
     let scripts = vec![

+ 6 - 6
frontend/rust-lib/flowy-database/tests/grid/filter_test/date_filter_test.rs

@@ -1,10 +1,10 @@
 use crate::grid::filter_test::script::FilterScript::*;
-use crate::grid::filter_test::script::{FilterRowChanged, GridFilterTest};
+use crate::grid::filter_test::script::{DatabaseFilterTest, FilterRowChanged};
 use flowy_database::entities::DateFilterConditionPB;
 
 #[tokio::test]
 async fn grid_filter_date_is_test() {
-    let mut test = GridFilterTest::new().await;
+    let mut test = DatabaseFilterTest::new().await;
     let row_count = test.row_revs.len();
     let expected = 3;
     let scripts = vec![
@@ -25,7 +25,7 @@ async fn grid_filter_date_is_test() {
 
 #[tokio::test]
 async fn grid_filter_date_after_test() {
-    let mut test = GridFilterTest::new().await;
+    let mut test = DatabaseFilterTest::new().await;
     let row_count = test.row_revs.len();
     let expected = 3;
     let scripts = vec![
@@ -46,7 +46,7 @@ async fn grid_filter_date_after_test() {
 
 #[tokio::test]
 async fn grid_filter_date_on_or_after_test() {
-    let mut test = GridFilterTest::new().await;
+    let mut test = DatabaseFilterTest::new().await;
     let row_count = test.row_revs.len();
     let expected = 3;
     let scripts = vec![
@@ -67,7 +67,7 @@ async fn grid_filter_date_on_or_after_test() {
 
 #[tokio::test]
 async fn grid_filter_date_on_or_before_test() {
-    let mut test = GridFilterTest::new().await;
+    let mut test = DatabaseFilterTest::new().await;
     let row_count = test.row_revs.len();
     let expected = 4;
     let scripts = vec![
@@ -88,7 +88,7 @@ async fn grid_filter_date_on_or_before_test() {
 
 #[tokio::test]
 async fn grid_filter_date_within_test() {
-    let mut test = GridFilterTest::new().await;
+    let mut test = DatabaseFilterTest::new().await;
     let row_count = test.row_revs.len();
     let expected = 5;
     let scripts = vec![

+ 7 - 7
frontend/rust-lib/flowy-database/tests/grid/filter_test/number_filter_test.rs

@@ -1,10 +1,10 @@
 use crate::grid::filter_test::script::FilterScript::*;
-use crate::grid::filter_test::script::{FilterRowChanged, GridFilterTest};
+use crate::grid::filter_test::script::{DatabaseFilterTest, FilterRowChanged};
 use flowy_database::entities::NumberFilterConditionPB;
 
 #[tokio::test]
 async fn grid_filter_number_is_equal_test() {
-    let mut test = GridFilterTest::new().await;
+    let mut test = DatabaseFilterTest::new().await;
     let row_count = test.row_revs.len();
     let expected = 1;
     let scripts = vec![
@@ -23,7 +23,7 @@ async fn grid_filter_number_is_equal_test() {
 
 #[tokio::test]
 async fn grid_filter_number_is_less_than_test() {
-    let mut test = GridFilterTest::new().await;
+    let mut test = DatabaseFilterTest::new().await;
     let row_count = test.row_revs.len();
     let expected = 2;
     let scripts = vec![
@@ -43,7 +43,7 @@ async fn grid_filter_number_is_less_than_test() {
 #[tokio::test]
 #[should_panic]
 async fn grid_filter_number_is_less_than_test2() {
-    let mut test = GridFilterTest::new().await;
+    let mut test = DatabaseFilterTest::new().await;
     let row_count = test.row_revs.len();
     let expected = 2;
     let scripts = vec![
@@ -62,7 +62,7 @@ async fn grid_filter_number_is_less_than_test2() {
 
 #[tokio::test]
 async fn grid_filter_number_is_less_than_or_equal_test() {
-    let mut test = GridFilterTest::new().await;
+    let mut test = DatabaseFilterTest::new().await;
     let row_count = test.row_revs.len();
     let expected = 3;
     let scripts = vec![
@@ -81,7 +81,7 @@ async fn grid_filter_number_is_less_than_or_equal_test() {
 
 #[tokio::test]
 async fn grid_filter_number_is_empty_test() {
-    let mut test = GridFilterTest::new().await;
+    let mut test = DatabaseFilterTest::new().await;
     let row_count = test.row_revs.len();
     let expected = 1;
     let scripts = vec![
@@ -100,7 +100,7 @@ async fn grid_filter_number_is_empty_test() {
 
 #[tokio::test]
 async fn grid_filter_number_is_not_empty_test() {
-    let mut test = GridFilterTest::new().await;
+    let mut test = DatabaseFilterTest::new().await;
     let row_count = test.row_revs.len();
     let expected = 5;
     let scripts = vec![

+ 8 - 8
frontend/rust-lib/flowy-database/tests/grid/filter_test/script.rs

@@ -15,7 +15,7 @@ use flowy_sqlite::schema::view_table::dsl::view_table;
 use flowy_database::services::cell::insert_select_option_cell;
 use flowy_database::services::filter::FilterType;
 use flowy_database::services::view_editor::GridViewChanged;
-use crate::grid::grid_editor::GridEditorTest;
+use crate::grid::database_editor::DatabaseEditorTest;
 
 pub struct FilterRowChanged {
     pub(crate) showing_num_of_rows: usize,
@@ -99,14 +99,14 @@ pub enum FilterScript {
     Wait { millisecond: u64 }
 }
 
-pub struct GridFilterTest {
-    inner: GridEditorTest,
+pub struct DatabaseFilterTest {
+    inner: DatabaseEditorTest,
     recv: Option<Receiver<GridViewChanged>>,
 }
 
-impl GridFilterTest {
+impl DatabaseFilterTest {
     pub async fn new() -> Self {
-        let editor_test =  GridEditorTest::new_table().await;
+        let editor_test =  DatabaseEditorTest::new_table().await;
         Self {
             inner: editor_test,
             recv: None,
@@ -298,15 +298,15 @@ impl GridFilterTest {
 }
 
 
-impl std::ops::Deref for GridFilterTest {
-    type Target = GridEditorTest;
+impl std::ops::Deref for DatabaseFilterTest {
+    type Target = DatabaseEditorTest;
 
     fn deref(&self) -> &Self::Target {
         &self.inner
     }
 }
 
-impl std::ops::DerefMut for GridFilterTest {
+impl std::ops::DerefMut for DatabaseFilterTest {
     fn deref_mut(&mut self) -> &mut Self::Target {
         &mut self.inner
     }

+ 8 - 8
frontend/rust-lib/flowy-database/tests/grid/filter_test/select_option_filter_test.rs

@@ -1,10 +1,10 @@
 use crate::grid::filter_test::script::FilterScript::*;
-use crate::grid::filter_test::script::{FilterRowChanged, GridFilterTest};
+use crate::grid::filter_test::script::{DatabaseFilterTest, FilterRowChanged};
 use flowy_database::entities::{FieldType, SelectOptionConditionPB};
 
 #[tokio::test]
 async fn grid_filter_multi_select_is_empty_test() {
-    let mut test = GridFilterTest::new().await;
+    let mut test = DatabaseFilterTest::new().await;
     let scripts = vec![
         CreateMultiSelectFilter {
             condition: SelectOptionConditionPB::OptionIsEmpty,
@@ -17,7 +17,7 @@ async fn grid_filter_multi_select_is_empty_test() {
 
 #[tokio::test]
 async fn grid_filter_multi_select_is_not_empty_test() {
-    let mut test = GridFilterTest::new().await;
+    let mut test = DatabaseFilterTest::new().await;
     let scripts = vec![
         CreateMultiSelectFilter {
             condition: SelectOptionConditionPB::OptionIsNotEmpty,
@@ -30,7 +30,7 @@ async fn grid_filter_multi_select_is_not_empty_test() {
 
 #[tokio::test]
 async fn grid_filter_multi_select_is_test() {
-    let mut test = GridFilterTest::new().await;
+    let mut test = DatabaseFilterTest::new().await;
     let field_rev = test.get_first_field_rev(FieldType::MultiSelect);
     let mut options = test.get_multi_select_type_option(&field_rev.id);
     let scripts = vec![
@@ -45,7 +45,7 @@ async fn grid_filter_multi_select_is_test() {
 
 #[tokio::test]
 async fn grid_filter_multi_select_is_test2() {
-    let mut test = GridFilterTest::new().await;
+    let mut test = DatabaseFilterTest::new().await;
     let field_rev = test.get_first_field_rev(FieldType::MultiSelect);
     let mut options = test.get_multi_select_type_option(&field_rev.id);
     let scripts = vec![
@@ -60,7 +60,7 @@ async fn grid_filter_multi_select_is_test2() {
 
 #[tokio::test]
 async fn grid_filter_single_select_is_empty_test() {
-    let mut test = GridFilterTest::new().await;
+    let mut test = DatabaseFilterTest::new().await;
     let expected = 2;
     let row_count = test.row_revs.len();
     let scripts = vec![
@@ -79,7 +79,7 @@ async fn grid_filter_single_select_is_empty_test() {
 
 #[tokio::test]
 async fn grid_filter_single_select_is_test() {
-    let mut test = GridFilterTest::new().await;
+    let mut test = DatabaseFilterTest::new().await;
     let field_rev = test.get_first_field_rev(FieldType::SingleSelect);
     let mut options = test.get_single_select_type_option(&field_rev.id).options;
     let expected = 2;
@@ -100,7 +100,7 @@ async fn grid_filter_single_select_is_test() {
 
 #[tokio::test]
 async fn grid_filter_single_select_is_test2() {
-    let mut test = GridFilterTest::new().await;
+    let mut test = DatabaseFilterTest::new().await;
     let field_rev = test.get_first_field_rev(FieldType::SingleSelect);
     let row_revs = test.get_row_revs().await;
     let mut options = test.get_single_select_type_option(&field_rev.id).options;

+ 11 - 11
frontend/rust-lib/flowy-database/tests/grid/filter_test/text_filter_test.rs

@@ -5,7 +5,7 @@ use flowy_database::services::filter::FilterType;
 
 #[tokio::test]
 async fn grid_filter_text_is_empty_test() {
-    let mut test = GridFilterTest::new().await;
+    let mut test = DatabaseFilterTest::new().await;
     let scripts = vec![
         CreateTextFilter {
             condition: TextFilterConditionPB::TextIsEmpty,
@@ -22,7 +22,7 @@ async fn grid_filter_text_is_empty_test() {
 
 #[tokio::test]
 async fn grid_filter_text_is_not_empty_test() {
-    let mut test = GridFilterTest::new().await;
+    let mut test = DatabaseFilterTest::new().await;
     // Only one row's text of the initial rows is ""
     let scripts = vec![
         CreateTextFilter {
@@ -55,7 +55,7 @@ async fn grid_filter_text_is_not_empty_test() {
 
 #[tokio::test]
 async fn grid_filter_is_text_test() {
-    let mut test = GridFilterTest::new().await;
+    let mut test = DatabaseFilterTest::new().await;
     // Only one row's text of the initial rows is "A"
     let scripts = vec![CreateTextFilter {
         condition: TextFilterConditionPB::Is,
@@ -70,7 +70,7 @@ async fn grid_filter_is_text_test() {
 
 #[tokio::test]
 async fn grid_filter_contain_text_test() {
-    let mut test = GridFilterTest::new().await;
+    let mut test = DatabaseFilterTest::new().await;
     let scripts = vec![CreateTextFilter {
         condition: TextFilterConditionPB::Contains,
         content: "A".to_string(),
@@ -84,7 +84,7 @@ async fn grid_filter_contain_text_test() {
 
 #[tokio::test]
 async fn grid_filter_contain_text_test2() {
-    let mut test = GridFilterTest::new().await;
+    let mut test = DatabaseFilterTest::new().await;
     let row_revs = test.row_revs.clone();
 
     let scripts = vec![
@@ -110,7 +110,7 @@ async fn grid_filter_contain_text_test2() {
 
 #[tokio::test]
 async fn grid_filter_does_not_contain_text_test() {
-    let mut test = GridFilterTest::new().await;
+    let mut test = DatabaseFilterTest::new().await;
     // None of the initial rows contains the text "AB"
     let scripts = vec![CreateTextFilter {
         condition: TextFilterConditionPB::DoesNotContain,
@@ -125,7 +125,7 @@ async fn grid_filter_does_not_contain_text_test() {
 
 #[tokio::test]
 async fn grid_filter_start_with_text_test() {
-    let mut test = GridFilterTest::new().await;
+    let mut test = DatabaseFilterTest::new().await;
     let scripts = vec![CreateTextFilter {
         condition: TextFilterConditionPB::StartsWith,
         content: "A".to_string(),
@@ -139,7 +139,7 @@ async fn grid_filter_start_with_text_test() {
 
 #[tokio::test]
 async fn grid_filter_ends_with_text_test() {
-    let mut test = GridFilterTest::new().await;
+    let mut test = DatabaseFilterTest::new().await;
     let scripts = vec![
         CreateTextFilter {
             condition: TextFilterConditionPB::EndsWith,
@@ -153,7 +153,7 @@ async fn grid_filter_ends_with_text_test() {
 
 #[tokio::test]
 async fn grid_update_text_filter_test() {
-    let mut test = GridFilterTest::new().await;
+    let mut test = DatabaseFilterTest::new().await;
     let scripts = vec![
         CreateTextFilter {
             condition: TextFilterConditionPB::EndsWith,
@@ -187,7 +187,7 @@ async fn grid_update_text_filter_test() {
 
 #[tokio::test]
 async fn grid_filter_delete_test() {
-    let mut test = GridFilterTest::new().await;
+    let mut test = DatabaseFilterTest::new().await;
     let field_rev = test.get_first_field_rev(FieldType::RichText).clone();
     let text_filter = TextFilterPB {
         condition: TextFilterConditionPB::TextIsEmpty,
@@ -216,7 +216,7 @@ async fn grid_filter_delete_test() {
 
 #[tokio::test]
 async fn grid_filter_update_empty_text_cell_test() {
-    let mut test = GridFilterTest::new().await;
+    let mut test = DatabaseFilterTest::new().await;
     let row_revs = test.row_revs.clone();
     let scripts = vec![
         CreateTextFilter {

+ 0 - 573
frontend/rust-lib/flowy-database/tests/grid/grid_editor.rs

@@ -1,573 +0,0 @@
-#![allow(clippy::all)]
-#![allow(dead_code)]
-#![allow(unused_imports)]
-use crate::grid::block_test::util::GridRowTestBuilder;
-use bytes::Bytes;
-use flowy_client_sync::client_database::DatabaseBuilder;
-use flowy_database::entities::*;
-use flowy_database::services::cell::ToCellChangesetString;
-use flowy_database::services::field::SelectOptionPB;
-use flowy_database::services::field::*;
-use flowy_database::services::grid_editor::{DatabaseRevisionEditor, GridRevisionSerde};
-use flowy_database::services::row::{CreateRowRevisionPayload, RowRevisionBuilder};
-use flowy_database::services::setting::GridSettingChangesetBuilder;
-use flowy_error::FlowyResult;
-use flowy_revision::REVISION_WRITE_INTERVAL_IN_MILLIS;
-use flowy_test::helper::ViewTest;
-use flowy_test::FlowySDKTest;
-use grid_model::*;
-use std::collections::HashMap;
-use std::sync::Arc;
-use std::time::Duration;
-use strum::EnumCount;
-use strum::IntoEnumIterator;
-use tokio::time::sleep;
-
-pub struct GridEditorTest {
-    pub sdk: FlowySDKTest,
-    pub view_id: String,
-    pub editor: Arc<DatabaseRevisionEditor>,
-    pub field_revs: Vec<Arc<FieldRevision>>,
-    pub block_meta_revs: Vec<Arc<GridBlockMetaRevision>>,
-    pub row_revs: Vec<Arc<RowRevision>>,
-    pub field_count: usize,
-    pub row_by_row_id: HashMap<String, RowPB>,
-}
-
-impl GridEditorTest {
-    pub async fn new_table() -> Self {
-        Self::new(DatabaseViewLayout::Grid).await
-    }
-
-    pub async fn new_board() -> Self {
-        Self::new(DatabaseViewLayout::Board).await
-    }
-
-    pub async fn new(layout: DatabaseViewLayout) -> Self {
-        let sdk = FlowySDKTest::default();
-        let _ = sdk.init_user().await;
-        let test = match layout {
-            DatabaseViewLayout::Grid => {
-                let build_context = make_test_grid();
-                let view_data: Bytes = build_context.into();
-                ViewTest::new_grid_view(&sdk, view_data.to_vec()).await
-            }
-            DatabaseViewLayout::Board => {
-                let build_context = make_test_board();
-                let view_data: Bytes = build_context.into();
-                ViewTest::new_board_view(&sdk, view_data.to_vec()).await
-            }
-            DatabaseViewLayout::Calendar => {
-                let build_context = make_test_calendar();
-                let view_data: Bytes = build_context.into();
-                ViewTest::new_calendar_view(&sdk, view_data.to_vec()).await
-            }
-        };
-
-        let editor = sdk.grid_manager.open_database(&test.view.id).await.unwrap();
-        let field_revs = editor.get_field_revs(None).await.unwrap();
-        let block_meta_revs = editor.get_block_meta_revs().await.unwrap();
-        let row_pbs = editor.get_all_row_revs(&test.view.id).await.unwrap();
-        assert_eq!(block_meta_revs.len(), 1);
-
-        // It seems like you should add the field in the make_test_grid() function.
-        // Because we assert the initialize count of the fields is equal to FieldType::COUNT.
-        assert_eq!(field_revs.len(), FieldType::COUNT);
-
-        let grid_id = test.view.id;
-        Self {
-            sdk,
-            view_id: grid_id,
-            editor,
-            field_revs,
-            block_meta_revs,
-            row_revs: row_pbs,
-            field_count: FieldType::COUNT,
-            row_by_row_id: HashMap::default(),
-        }
-    }
-
-    pub async fn get_row_revs(&self) -> Vec<Arc<RowRevision>> {
-        self.editor.get_all_row_revs(&self.view_id).await.unwrap()
-    }
-
-    pub async fn grid_filters(&self) -> Vec<FilterPB> {
-        self.editor.get_all_filters().await.unwrap()
-    }
-
-    pub fn get_field_rev(&self, field_id: &str, field_type: FieldType) -> &Arc<FieldRevision> {
-        self.field_revs
-            .iter()
-            .filter(|field_rev| {
-                let t_field_type: FieldType = field_rev.ty.into();
-                field_rev.id == field_id && t_field_type == field_type
-            })
-            .collect::<Vec<_>>()
-            .pop()
-            .unwrap()
-    }
-
-    /// returns the first `FieldRevision` in the build-in test grid.
-    /// Not support duplicate `FieldType` in test grid yet.
-    pub fn get_first_field_rev(&self, field_type: FieldType) -> &Arc<FieldRevision> {
-        self.field_revs
-            .iter()
-            .filter(|field_rev| {
-                let t_field_type: FieldType = field_rev.ty.into();
-                t_field_type == field_type
-            })
-            .collect::<Vec<_>>()
-            .pop()
-            .unwrap()
-    }
-
-    pub fn get_multi_select_type_option(&self, field_id: &str) -> Vec<SelectOptionPB> {
-        let field_type = FieldType::MultiSelect;
-        let field_rev = self.get_field_rev(field_id, field_type.clone());
-        let type_option = field_rev
-            .get_type_option::<MultiSelectTypeOptionPB>(field_type.into())
-            .unwrap();
-        type_option.options
-    }
-
-    pub fn get_single_select_type_option(&self, field_id: &str) -> SingleSelectTypeOptionPB {
-        let field_type = FieldType::SingleSelect;
-        let field_rev = self.get_field_rev(field_id, field_type.clone());
-        let type_option = field_rev
-            .get_type_option::<SingleSelectTypeOptionPB>(field_type.into())
-            .unwrap();
-        type_option
-    }
-
-    pub fn get_checklist_type_option(&self, field_id: &str) -> ChecklistTypeOptionPB {
-        let field_type = FieldType::Checklist;
-        let field_rev = self.get_field_rev(field_id, field_type.clone());
-        let type_option = field_rev
-            .get_type_option::<ChecklistTypeOptionPB>(field_type.into())
-            .unwrap();
-        type_option
-    }
-    pub fn get_checkbox_type_option(&self, field_id: &str) -> CheckboxTypeOptionPB {
-        let field_type = FieldType::Checkbox;
-        let field_rev = self.get_field_rev(field_id, field_type.clone());
-        let type_option = field_rev
-            .get_type_option::<CheckboxTypeOptionPB>(field_type.into())
-            .unwrap();
-        type_option
-    }
-
-    pub fn block_id(&self) -> &str {
-        &self.block_meta_revs.last().unwrap().block_id
-    }
-
-    pub async fn update_cell<T: ToCellChangesetString>(&mut self, field_id: &str, row_id: String, cell_changeset: T) {
-        let field_rev = self
-            .field_revs
-            .iter()
-            .find(|field_rev| field_rev.id == field_id)
-            .unwrap();
-
-        self.editor
-            .update_cell_with_changeset(&row_id, &field_rev.id, cell_changeset)
-            .await
-            .unwrap();
-    }
-
-    pub(crate) async fn update_text_cell(&mut self, row_id: String, content: &str) {
-        let field_rev = self
-            .field_revs
-            .iter()
-            .find(|field_rev| {
-                let field_type: FieldType = field_rev.ty.into();
-                field_type == FieldType::RichText
-            })
-            .unwrap()
-            .clone();
-
-        self.update_cell(&field_rev.id, row_id, content.to_string()).await;
-    }
-
-    pub(crate) async fn update_single_select_cell(&mut self, row_id: String, option_id: &str) {
-        let field_rev = self
-            .field_revs
-            .iter()
-            .find(|field_rev| {
-                let field_type: FieldType = field_rev.ty.into();
-                field_type == FieldType::SingleSelect
-            })
-            .unwrap()
-            .clone();
-
-        let cell_changeset = SelectOptionCellChangeset::from_insert_option_id(&option_id);
-        self.update_cell(&field_rev.id, row_id, cell_changeset).await;
-    }
-}
-
-pub const GOOGLE: &str = "Google";
-pub const FACEBOOK: &str = "Facebook";
-pub const TWITTER: &str = "Twitter";
-
-pub const COMPLETED: &str = "Completed";
-pub const PLANNED: &str = "Planned";
-pub const PAUSED: &str = "Paused";
-
-pub const FIRST_THING: &str = "Wake up at 6:00 am";
-pub const SECOND_THING: &str = "Get some coffee";
-pub const THIRD_THING: &str = "Start working";
-
-/// The build-in test data for grid. Currently, there are five rows in this grid, if you want to add
-/// more rows or alter the data in this grid. Some tests may fail. So you need to fix the failed tests.
-fn make_test_grid() -> BuildDatabaseContext {
-    let mut grid_builder = DatabaseBuilder::new();
-    // Iterate through the FieldType to create the corresponding Field.
-    for field_type in FieldType::iter() {
-        let field_type: FieldType = field_type;
-
-        // The
-        match field_type {
-            FieldType::RichText => {
-                let text_field = FieldBuilder::new(RichTextTypeOptionBuilder::default())
-                    .name("Name")
-                    .visibility(true)
-                    .primary(true)
-                    .build();
-                grid_builder.add_field(text_field);
-            }
-            FieldType::Number => {
-                // Number
-                let number = NumberTypeOptionBuilder::default().set_format(NumberFormat::USD);
-                let number_field = FieldBuilder::new(number).name("Price").visibility(true).build();
-                grid_builder.add_field(number_field);
-            }
-            FieldType::DateTime => {
-                // Date
-                let date = DateTypeOptionBuilder::default()
-                    .date_format(DateFormat::US)
-                    .time_format(TimeFormat::TwentyFourHour);
-                let date_field = FieldBuilder::new(date).name("Time").visibility(true).build();
-                grid_builder.add_field(date_field);
-            }
-            FieldType::SingleSelect => {
-                // Single Select
-                let single_select = SingleSelectTypeOptionBuilder::default()
-                    .add_option(SelectOptionPB::new(COMPLETED))
-                    .add_option(SelectOptionPB::new(PLANNED))
-                    .add_option(SelectOptionPB::new(PAUSED));
-                let single_select_field = FieldBuilder::new(single_select).name("Status").visibility(true).build();
-                grid_builder.add_field(single_select_field);
-            }
-            FieldType::MultiSelect => {
-                // MultiSelect
-                let multi_select = MultiSelectTypeOptionBuilder::default()
-                    .add_option(SelectOptionPB::new(GOOGLE))
-                    .add_option(SelectOptionPB::new(FACEBOOK))
-                    .add_option(SelectOptionPB::new(TWITTER));
-                let multi_select_field = FieldBuilder::new(multi_select)
-                    .name("Platform")
-                    .visibility(true)
-                    .build();
-                grid_builder.add_field(multi_select_field);
-            }
-            FieldType::Checkbox => {
-                // Checkbox
-                let checkbox = CheckboxTypeOptionBuilder::default();
-                let checkbox_field = FieldBuilder::new(checkbox).name("is urgent").visibility(true).build();
-                grid_builder.add_field(checkbox_field);
-            }
-            FieldType::URL => {
-                // URL
-                let url = URLTypeOptionBuilder::default();
-                let url_field = FieldBuilder::new(url).name("link").visibility(true).build();
-                grid_builder.add_field(url_field);
-            }
-            FieldType::Checklist => {
-                let checklist = ChecklistTypeOptionBuilder::default()
-                    .add_option(SelectOptionPB::new(FIRST_THING))
-                    .add_option(SelectOptionPB::new(SECOND_THING))
-                    .add_option(SelectOptionPB::new(THIRD_THING));
-                let checklist_field = FieldBuilder::new(checklist).name("TODO").visibility(true).build();
-                grid_builder.add_field(checklist_field);
-            }
-        }
-    }
-
-    for i in 0..6 {
-        let block_id = grid_builder.block_id().to_owned();
-        let field_revs = grid_builder.field_revs();
-        let mut row_builder = GridRowTestBuilder::new(&block_id, field_revs);
-        match i {
-            0 => {
-                for field_type in FieldType::iter() {
-                    match field_type {
-                        FieldType::RichText => row_builder.insert_text_cell("A"),
-                        FieldType::Number => row_builder.insert_number_cell("1"),
-                        FieldType::DateTime => row_builder.insert_date_cell("1647251762"),
-                        FieldType::MultiSelect => row_builder
-                            .insert_multi_select_cell(|mut options| vec![options.remove(0), options.remove(0)]),
-                        FieldType::Checklist => row_builder.insert_checklist_cell(|options| options),
-                        FieldType::Checkbox => row_builder.insert_checkbox_cell("true"),
-                        FieldType::URL => row_builder.insert_url_cell("AppFlowy website - https://www.appflowy.io"),
-                        _ => "".to_owned(),
-                    };
-                }
-            }
-            1 => {
-                for field_type in FieldType::iter() {
-                    match field_type {
-                        FieldType::RichText => row_builder.insert_text_cell(""),
-                        FieldType::Number => row_builder.insert_number_cell("2"),
-                        FieldType::DateTime => row_builder.insert_date_cell("1647251762"),
-                        FieldType::MultiSelect => row_builder
-                            .insert_multi_select_cell(|mut options| vec![options.remove(0), options.remove(1)]),
-                        FieldType::Checkbox => row_builder.insert_checkbox_cell("true"),
-                        _ => "".to_owned(),
-                    };
-                }
-            }
-            2 => {
-                for field_type in FieldType::iter() {
-                    match field_type {
-                        FieldType::RichText => row_builder.insert_text_cell("C"),
-                        FieldType::Number => row_builder.insert_number_cell("3"),
-                        FieldType::DateTime => row_builder.insert_date_cell("1647251762"),
-                        FieldType::SingleSelect => {
-                            row_builder.insert_single_select_cell(|mut options| options.remove(0))
-                        }
-                        FieldType::MultiSelect => {
-                            row_builder.insert_multi_select_cell(|mut options| vec![options.remove(1)])
-                        }
-                        FieldType::Checkbox => row_builder.insert_checkbox_cell("false"),
-                        _ => "".to_owned(),
-                    };
-                }
-            }
-            3 => {
-                for field_type in FieldType::iter() {
-                    match field_type {
-                        FieldType::RichText => row_builder.insert_text_cell("DA"),
-                        FieldType::Number => row_builder.insert_number_cell("4"),
-                        FieldType::DateTime => row_builder.insert_date_cell("1668704685"),
-                        FieldType::SingleSelect => {
-                            row_builder.insert_single_select_cell(|mut options| options.remove(0))
-                        }
-                        FieldType::Checkbox => row_builder.insert_checkbox_cell("false"),
-                        _ => "".to_owned(),
-                    };
-                }
-            }
-            4 => {
-                for field_type in FieldType::iter() {
-                    match field_type {
-                        FieldType::RichText => row_builder.insert_text_cell("AE"),
-                        FieldType::Number => row_builder.insert_number_cell(""),
-                        FieldType::DateTime => row_builder.insert_date_cell("1668359085"),
-                        FieldType::SingleSelect => {
-                            row_builder.insert_single_select_cell(|mut options| options.remove(1))
-                        }
-
-                        FieldType::Checkbox => row_builder.insert_checkbox_cell("false"),
-                        _ => "".to_owned(),
-                    };
-                }
-            }
-            5 => {
-                for field_type in FieldType::iter() {
-                    match field_type {
-                        FieldType::RichText => row_builder.insert_text_cell("AE"),
-                        FieldType::Number => row_builder.insert_number_cell("5"),
-                        FieldType::DateTime => row_builder.insert_date_cell("1671938394"),
-                        FieldType::SingleSelect => {
-                            row_builder.insert_single_select_cell(|mut options| options.remove(1))
-                        }
-                        FieldType::Checkbox => row_builder.insert_checkbox_cell("true"),
-                        _ => "".to_owned(),
-                    };
-                }
-            }
-            _ => {}
-        }
-
-        let row_rev = row_builder.build();
-        grid_builder.add_row(row_rev);
-    }
-    grid_builder.build()
-}
-
-// Kanban board unit test mock data
-fn make_test_board() -> BuildDatabaseContext {
-    let mut grid_builder = DatabaseBuilder::new();
-    // Iterate through the FieldType to create the corresponding Field.
-    for field_type in FieldType::iter() {
-        let field_type: FieldType = field_type;
-
-        // The
-        match field_type {
-            FieldType::RichText => {
-                let text_field = FieldBuilder::new(RichTextTypeOptionBuilder::default())
-                    .name("Name")
-                    .visibility(true)
-                    .primary(true)
-                    .build();
-                grid_builder.add_field(text_field);
-            }
-            FieldType::Number => {
-                // Number
-                let number = NumberTypeOptionBuilder::default().set_format(NumberFormat::USD);
-                let number_field = FieldBuilder::new(number).name("Price").visibility(true).build();
-                grid_builder.add_field(number_field);
-            }
-            FieldType::DateTime => {
-                // Date
-                let date = DateTypeOptionBuilder::default()
-                    .date_format(DateFormat::US)
-                    .time_format(TimeFormat::TwentyFourHour);
-                let date_field = FieldBuilder::new(date).name("Time").visibility(true).build();
-                grid_builder.add_field(date_field);
-            }
-            FieldType::SingleSelect => {
-                // Single Select
-                let single_select = SingleSelectTypeOptionBuilder::default()
-                    .add_option(SelectOptionPB::new(COMPLETED))
-                    .add_option(SelectOptionPB::new(PLANNED))
-                    .add_option(SelectOptionPB::new(PAUSED));
-                let single_select_field = FieldBuilder::new(single_select).name("Status").visibility(true).build();
-                grid_builder.add_field(single_select_field);
-            }
-            FieldType::MultiSelect => {
-                // MultiSelect
-                let multi_select = MultiSelectTypeOptionBuilder::default()
-                    .add_option(SelectOptionPB::new(GOOGLE))
-                    .add_option(SelectOptionPB::new(FACEBOOK))
-                    .add_option(SelectOptionPB::new(TWITTER));
-                let multi_select_field = FieldBuilder::new(multi_select)
-                    .name("Platform")
-                    .visibility(true)
-                    .build();
-                grid_builder.add_field(multi_select_field);
-            }
-            FieldType::Checkbox => {
-                // Checkbox
-                let checkbox = CheckboxTypeOptionBuilder::default();
-                let checkbox_field = FieldBuilder::new(checkbox).name("is urgent").visibility(true).build();
-                grid_builder.add_field(checkbox_field);
-            }
-            FieldType::URL => {
-                // URL
-                let url = URLTypeOptionBuilder::default();
-                let url_field = FieldBuilder::new(url).name("link").visibility(true).build();
-                grid_builder.add_field(url_field);
-            }
-            FieldType::Checklist => {
-                let checklist = ChecklistTypeOptionBuilder::default()
-                    .add_option(SelectOptionPB::new(FIRST_THING))
-                    .add_option(SelectOptionPB::new(SECOND_THING))
-                    .add_option(SelectOptionPB::new(THIRD_THING));
-                let checklist_field = FieldBuilder::new(checklist).name("TODO").visibility(true).build();
-                grid_builder.add_field(checklist_field);
-            }
-        }
-    }
-
-    // We have many assumptions base on the number of the rows, so do not change the number of the loop.
-    for i in 0..5 {
-        let block_id = grid_builder.block_id().to_owned();
-        let field_revs = grid_builder.field_revs();
-        let mut row_builder = GridRowTestBuilder::new(&block_id, field_revs);
-        match i {
-            0 => {
-                for field_type in FieldType::iter() {
-                    match field_type {
-                        FieldType::RichText => row_builder.insert_text_cell("A"),
-                        FieldType::Number => row_builder.insert_number_cell("1"),
-                        // 1647251762 => Mar 14,2022
-                        FieldType::DateTime => row_builder.insert_date_cell("1647251762"),
-                        FieldType::SingleSelect => {
-                            row_builder.insert_single_select_cell(|mut options| options.remove(0))
-                        }
-                        FieldType::MultiSelect => row_builder
-                            .insert_multi_select_cell(|mut options| vec![options.remove(0), options.remove(0)]),
-                        FieldType::Checkbox => row_builder.insert_checkbox_cell("true"),
-                        FieldType::URL => row_builder.insert_url_cell("https://appflowy.io"),
-                        _ => "".to_owned(),
-                    };
-                }
-            }
-            1 => {
-                for field_type in FieldType::iter() {
-                    match field_type {
-                        FieldType::RichText => row_builder.insert_text_cell("B"),
-                        FieldType::Number => row_builder.insert_number_cell("2"),
-                        // 1647251762 => Mar 14,2022
-                        FieldType::DateTime => row_builder.insert_date_cell("1647251762"),
-                        FieldType::SingleSelect => {
-                            row_builder.insert_single_select_cell(|mut options| options.remove(0))
-                        }
-                        FieldType::MultiSelect => row_builder
-                            .insert_multi_select_cell(|mut options| vec![options.remove(0), options.remove(0)]),
-                        FieldType::Checkbox => row_builder.insert_checkbox_cell("true"),
-                        _ => "".to_owned(),
-                    };
-                }
-            }
-            2 => {
-                for field_type in FieldType::iter() {
-                    match field_type {
-                        FieldType::RichText => row_builder.insert_text_cell("C"),
-                        FieldType::Number => row_builder.insert_number_cell("3"),
-                        // 1647251762 => Mar 14,2022
-                        FieldType::DateTime => row_builder.insert_date_cell("1647251762"),
-                        FieldType::SingleSelect => {
-                            row_builder.insert_single_select_cell(|mut options| options.remove(1))
-                        }
-                        FieldType::MultiSelect => {
-                            row_builder.insert_multi_select_cell(|mut options| vec![options.remove(0)])
-                        }
-                        FieldType::Checkbox => row_builder.insert_checkbox_cell("false"),
-                        FieldType::URL => row_builder.insert_url_cell("https://github.com/AppFlowy-IO/AppFlowy"),
-                        _ => "".to_owned(),
-                    };
-                }
-            }
-            3 => {
-                for field_type in FieldType::iter() {
-                    match field_type {
-                        FieldType::RichText => row_builder.insert_text_cell("DA"),
-                        FieldType::Number => row_builder.insert_number_cell("4"),
-                        FieldType::DateTime => row_builder.insert_date_cell("1668704685"),
-                        FieldType::SingleSelect => {
-                            row_builder.insert_single_select_cell(|mut options| options.remove(1))
-                        }
-                        FieldType::Checkbox => row_builder.insert_checkbox_cell("false"),
-                        FieldType::URL => row_builder.insert_url_cell("https://appflowy.io"),
-                        _ => "".to_owned(),
-                    };
-                }
-            }
-            4 => {
-                for field_type in FieldType::iter() {
-                    match field_type {
-                        FieldType::RichText => row_builder.insert_text_cell("AE"),
-                        FieldType::Number => row_builder.insert_number_cell(""),
-                        FieldType::DateTime => row_builder.insert_date_cell("1668359085"),
-                        FieldType::SingleSelect => {
-                            row_builder.insert_single_select_cell(|mut options| options.remove(2))
-                        }
-
-                        FieldType::Checkbox => row_builder.insert_checkbox_cell("false"),
-                        _ => "".to_owned(),
-                    };
-                }
-            }
-            _ => {}
-        }
-
-        let row_rev = row_builder.build();
-        grid_builder.add_row(row_rev);
-    }
-    grid_builder.build()
-}
-
-// Calendar unit test mock data
-fn make_test_calendar() -> BuildDatabaseContext {
-    todo!()
-}

+ 0 - 381
frontend/rust-lib/flowy-database/tests/grid/grid_test.rs

@@ -1,381 +0,0 @@
-use crate::grid::script::EditorScript::*;
-use crate::grid::script::*;
-use chrono::NaiveDateTime;
-use flowy_database::services::field::{
-    DateCellContentChangeset, DateCellData, MultiSelectTypeOptionPB, SelectOption, SelectOptionCellContentChangeset,
-    SingleSelectTypeOption, SELECTION_IDS_SEPARATOR,
-};
-use flowy_database::services::row::{decode_cell_data_from_type_option_cell_data, CreateRowMetaBuilder};
-use grid_model::entities::{
-    CellChangeset, FieldChangesetParams, FieldType, GridBlockInfoChangeset, GridBlockMetaSnapshot, RowMetaChangeset,
-    TypeOptionDataFormat,
-};
-
-#[tokio::test]
-async fn grid_create_field() {
-    let mut test = GridEditorTest::new().await;
-    let (text_field_params, text_field_meta) = create_text_field(&test.grid_id);
-    let (single_select_params, single_select_field) = create_single_select_field(&test.grid_id);
-    let scripts = vec![
-        CreateField {
-            params: text_field_params,
-        },
-        AssertFieldEqual {
-            field_index: test.field_count,
-            field_meta: text_field_meta,
-        },
-    ];
-    test.run_scripts(scripts).await;
-
-    let scripts = vec![
-        CreateField {
-            params: single_select_params,
-        },
-        AssertFieldEqual {
-            field_index: test.field_count,
-            field_meta: single_select_field,
-        },
-    ];
-    test.run_scripts(scripts).await;
-}
-
-#[tokio::test]
-async fn grid_create_duplicate_field() {
-    let mut test = GridEditorTest::new().await;
-    let (params, _) = create_text_field(&test.grid_id);
-    let field_count = test.field_count;
-    let expected_field_count = field_count + 1;
-    let scripts = vec![
-        CreateField { params: params.clone() },
-        CreateField { params },
-        AssertFieldCount(expected_field_count),
-    ];
-    test.run_scripts(scripts).await;
-}
-
-#[tokio::test]
-async fn grid_update_field_with_empty_change() {
-    let mut test = GridEditorTest::new().await;
-    let (params, field_meta) = create_single_select_field(&test.grid_id);
-    let changeset = FieldChangesetParams {
-        field_id: field_meta.id.clone(),
-        grid_id: test.grid_id.clone(),
-        ..Default::default()
-    };
-
-    let scripts = vec![
-        CreateField { params },
-        UpdateField { changeset },
-        AssertFieldEqual {
-            field_index: test.field_count,
-            field_meta,
-        },
-    ];
-    test.run_scripts(scripts).await;
-}
-
-#[tokio::test]
-async fn grid_update_field() {
-    let mut test = GridEditorTest::new().await;
-    let (single_select_params, single_select_field) = create_single_select_field(&test.grid_id);
-    let mut cloned_field = single_select_field.clone();
-
-    let mut single_select_type_option = SingleSelectTypeOption::from(&single_select_field);
-    single_select_type_option.options.push(SelectOption::new("Unknown"));
-    let changeset = FieldChangesetParams {
-        field_id: single_select_field.id.clone(),
-        grid_id: test.grid_id.clone(),
-        frozen: Some(true),
-        width: Some(1000),
-        type_option_data: Some(single_select_type_option.protobuf_bytes().to_vec()),
-        ..Default::default()
-    };
-
-    cloned_field.frozen = true;
-    cloned_field.width = 1000;
-    cloned_field.insert_type_option_entry(&single_select_type_option);
-
-    let scripts = vec![
-        CreateField {
-            params: single_select_params,
-        },
-        UpdateField { changeset },
-        AssertFieldEqual {
-            field_index: test.field_count,
-            field_meta: cloned_field,
-        },
-    ];
-    test.run_scripts(scripts).await;
-}
-
-#[tokio::test]
-async fn grid_delete_field() {
-    let mut test = GridEditorTest::new().await;
-    let expected_field_count = test.field_count;
-    let (text_params, text_field) = create_text_field(&test.grid_id);
-    let scripts = vec![
-        CreateField { params: text_params },
-        DeleteField { field_meta: text_field },
-        AssertFieldCount(expected_field_count),
-    ];
-    test.run_scripts(scripts).await;
-}
-
-#[tokio::test]
-async fn grid_create_block() {
-    let grid_block = GridBlockMetaSnapshot::new();
-    let scripts = vec![
-        AssertBlockCount(1),
-        CreateBlock { block: grid_block },
-        AssertBlockCount(2),
-    ];
-    GridEditorTest::new().await.run_scripts(scripts).await;
-}
-
-#[tokio::test]
-async fn grid_update_block() {
-    let grid_block = GridBlockMetaSnapshot::new();
-    let mut cloned_grid_block = grid_block.clone();
-    let changeset = GridBlockInfoChangeset {
-        block_id: grid_block.block_id.clone(),
-        start_row_index: Some(2),
-        row_count: Some(10),
-    };
-
-    cloned_grid_block.start_row_index = 2;
-    cloned_grid_block.row_count = 10;
-
-    let scripts = vec![
-        AssertBlockCount(1),
-        CreateBlock { block: grid_block },
-        UpdateBlock { changeset },
-        AssertBlockCount(2),
-        AssertBlockEqual {
-            block_index: 1,
-            block: cloned_grid_block,
-        },
-    ];
-    GridEditorTest::new().await.run_scripts(scripts).await;
-}
-
-#[tokio::test]
-async fn grid_create_row() {
-    let scripts = vec![AssertRowCount(3), CreateEmptyRow, CreateEmptyRow, AssertRowCount(5)];
-    GridEditorTest::new().await.run_scripts(scripts).await;
-}
-
-#[tokio::test]
-async fn grid_create_row2() {
-    let mut test = GridEditorTest::new().await;
-    let create_row_context = CreateRowMetaBuilder::new(&test.field_metas).build();
-    let scripts = vec![
-        AssertRowCount(3),
-        CreateRow {
-            context: create_row_context,
-        },
-        AssertRowCount(4),
-    ];
-    test.run_scripts(scripts).await;
-}
-
-#[tokio::test]
-async fn grid_update_row() {
-    let mut test = GridEditorTest::new().await;
-    let context = CreateRowMetaBuilder::new(&test.field_metas).build();
-    let changeset = RowMetaChangeset {
-        row_id: context.row_id.clone(),
-        height: None,
-        visibility: None,
-        cell_by_field_id: Default::default(),
-    };
-
-    let scripts = vec![
-        AssertRowCount(3),
-        CreateRow { context },
-        UpdateRow {
-            changeset: changeset.clone(),
-        },
-        AssertRow { changeset },
-        AssertRowCount(4),
-    ];
-    test.run_scripts(scripts).await;
-}
-
-#[tokio::test]
-async fn grid_delete_row() {
-    let mut test = GridEditorTest::new().await;
-    let context_1 = CreateRowMetaBuilder::new(&test.field_metas).build();
-    let context_2 = CreateRowMetaBuilder::new(&test.field_metas).build();
-    let row_ids = vec![context_1.row_id.clone(), context_2.row_id.clone()];
-    let scripts = vec![
-        AssertRowCount(3),
-        CreateRow { context: context_1 },
-        CreateRow { context: context_2 },
-        AssertBlockCount(1),
-        AssertBlock {
-            block_index: 0,
-            row_count: 5,
-            start_row_index: 0,
-        },
-        DeleteRow { row_ids },
-        AssertBlock {
-            block_index: 0,
-            row_count: 3,
-            start_row_index: 0,
-        },
-    ];
-    test.run_scripts(scripts).await;
-}
-
-#[tokio::test]
-async fn grid_row_add_cells_test() {
-    let mut test = GridEditorTest::new().await;
-    let mut builder = CreateRowMetaBuilder::new(&test.field_metas);
-    for field in &test.field_metas {
-        match field.field_type {
-            FieldType::RichText => {
-                builder.add_cell(&field.id, "hello world".to_owned()).unwrap();
-            }
-            FieldType::Number => {
-                builder.add_cell(&field.id, "18,443".to_owned()).unwrap();
-            }
-            FieldType::DateTime => {
-                builder
-                    .add_cell(&field.id, make_date_cell_string("1647251762"))
-                    .unwrap();
-            }
-            FieldType::SingleSelect => {
-                let type_option = SingleSelectTypeOption::from(field);
-                let option = type_option.options.first().unwrap();
-                builder.add_select_option_cell(&field.id, option.id.clone()).unwrap();
-            }
-            FieldType::MultiSelect => {
-                let type_option = MultiSelectTypeOptionPB::from(field);
-                let ops_ids = type_option
-                    .options
-                    .iter()
-                    .map(|option| option.id.clone())
-                    .collect::<Vec<_>>()
-                    .join(SELECTION_IDS_SEPARATOR);
-                builder.add_select_option_cell(&field.id, ops_ids).unwrap();
-            }
-            FieldType::Checkbox => {
-                builder.add_cell(&field.id, "false".to_string()).unwrap();
-            }
-            FieldType::URL => {
-                builder.add_cell(&field.id, "1".to_string()).unwrap();
-            }
-        }
-    }
-    let context = builder.build();
-    let scripts = vec![CreateRow { context }, AssertGridMetaPad];
-    test.run_scripts(scripts).await;
-}
-
-#[tokio::test]
-async fn grid_row_add_date_cell_test() {
-    let mut test = GridEditorTest::new().await;
-    let mut builder = CreateRowMetaBuilder::new(&test.field_metas);
-    let mut date_field = None;
-    let timestamp = 1647390674;
-    for field in &test.field_metas {
-        if field.field_type == FieldType::DateTime {
-            date_field = Some(field.clone());
-            NaiveDateTime::from_timestamp(123, 0);
-            // The data should not be empty
-            assert!(builder.add_cell(&field.id, "".to_string()).is_err());
-            assert!(builder.add_cell(&field.id, make_date_cell_string("123")).is_ok());
-            assert!(builder
-                .add_cell(&field.id, make_date_cell_string(&timestamp.to_string()))
-                .is_ok());
-        }
-    }
-    let context = builder.build();
-    let date_field = date_field.unwrap();
-    let cell_data = context.cell_by_field_id.get(&date_field.id).unwrap().clone();
-    assert_eq!(
-        decode_cell_data_from_type_option_cell_data(cell_data.data.clone(), &date_field, &date_field.field_type)
-            .parse::<DateCellData>()
-            .unwrap()
-            .date,
-        "2022/03/16",
-    );
-    let scripts = vec![CreateRow { context }];
-    test.run_scripts(scripts).await;
-}
-
-#[tokio::test]
-async fn grid_cell_update() {
-    let mut test = GridEditorTest::new().await;
-    let field_metas = &test.field_metas;
-    let row_metas = &test.row_metas;
-    let grid_blocks = &test.grid_blocks;
-    assert_eq!(row_metas.len(), 3);
-    assert_eq!(grid_blocks.len(), 1);
-
-    let block_id = &grid_blocks.first().unwrap().block_id;
-    let mut scripts = vec![];
-    for (index, row_meta) in row_metas.iter().enumerate() {
-        for field_meta in field_metas {
-            if index == 0 {
-                let data = match field_meta.field_type {
-                    FieldType::RichText => "".to_string(),
-                    FieldType::Number => "123".to_string(),
-                    FieldType::DateTime => make_date_cell_string("123"),
-                    FieldType::SingleSelect => {
-                        let type_option = SingleSelectTypeOption::from(field_meta);
-                        SelectOptionCellContentChangeset::from_insert(&type_option.options.first().unwrap().id).to_str()
-                    }
-                    FieldType::MultiSelect => {
-                        let type_option = MultiSelectTypeOptionPB::from(field_meta);
-                        SelectOptionCellContentChangeset::from_insert(&type_option.options.first().unwrap().id).to_str()
-                    }
-                    FieldType::Checkbox => "1".to_string(),
-                    FieldType::URL => "1".to_string(),
-                };
-
-                scripts.push(UpdateCell {
-                    changeset: CellChangeset {
-                        database_id: block_id.to_string(),
-                        row_id: row_meta.id.clone(),
-                        field_id: field_meta.id.clone(),
-                        cell_content_changeset: Some(data),
-                    },
-                    is_err: false,
-                });
-            }
-
-            if index == 1 {
-                let (data, is_err) = match field_meta.field_type {
-                    FieldType::RichText => ("1".to_string().repeat(10001), true),
-                    FieldType::Number => ("abc".to_string(), true),
-                    FieldType::DateTime => ("abc".to_string(), true),
-                    FieldType::SingleSelect => (SelectOptionCellContentChangeset::from_insert("abc").to_str(), false),
-                    FieldType::MultiSelect => (SelectOptionCellContentChangeset::from_insert("abc").to_str(), false),
-                    FieldType::Checkbox => ("2".to_string(), false),
-                    FieldType::URL => ("2".to_string(), false),
-                };
-
-                scripts.push(UpdateCell {
-                    changeset: CellChangeset {
-                        database_id: block_id.to_string(),
-                        row_id: row_meta.id.clone(),
-                        field_id: field_meta.id.clone(),
-                        cell_content_changeset: Some(data),
-                    },
-                    is_err,
-                });
-            }
-        }
-    }
-
-    test.run_scripts(scripts).await;
-}
-
-fn make_date_cell_string(s: &str) -> String {
-    serde_json::to_string(&DateCellContentChangeset {
-        date: Some(s.to_string()),
-        time: None,
-    })
-    .unwrap()
-}

+ 1 - 0
frontend/rust-lib/flowy-database/tests/grid/group_test/mod.rs

@@ -1,2 +1,3 @@
 mod script;
 mod test;
+mod url_group_test;

+ 38 - 12
frontend/rust-lib/flowy-database/tests/grid/group_test/script.rs

@@ -1,8 +1,8 @@
-use crate::grid::grid_editor::GridEditorTest;
+use crate::grid::database_editor::DatabaseEditorTest;
 use flowy_database::entities::{
     CreateRowParams, DatabaseViewLayout, FieldType, GroupPB, MoveGroupParams, MoveGroupRowParams, RowPB,
 };
-use flowy_database::services::cell::{delete_select_option_cell, insert_select_option_cell};
+use flowy_database::services::cell::{delete_select_option_cell, insert_select_option_cell, insert_url_cell};
 use flowy_database::services::field::{
     edit_single_select_type_option, SelectOptionPB, SelectTypeOptionSharedAction, SingleSelectTypeOptionPB,
 };
@@ -37,11 +37,16 @@ pub enum GroupScript {
         group_index: usize,
         row_index: usize,
     },
-    UpdateRow {
+    UpdateGroupedCell {
         from_group_index: usize,
         row_index: usize,
         to_group_index: usize,
     },
+    UpdateGroupedCellWithData {
+        from_group_index: usize,
+        row_index: usize,
+        cell_data: String,
+    },
     MoveGroup {
         from_group_index: usize,
         to_group_index: usize,
@@ -54,13 +59,13 @@ pub enum GroupScript {
     },
 }
 
-pub struct GridGroupTest {
-    inner: GridEditorTest,
+pub struct DatabaseGroupTest {
+    inner: DatabaseEditorTest,
 }
 
-impl GridGroupTest {
+impl DatabaseGroupTest {
     pub async fn new() -> Self {
-        let editor_test = GridEditorTest::new_board().await;
+        let editor_test = DatabaseEditorTest::new_board().await;
         Self { inner: editor_test }
     }
 
@@ -109,7 +114,6 @@ impl GridGroupTest {
                 assert_eq!(row.id, compare_row.id);
             }
             GroupScript::CreateRow { group_index } => {
-                //
                 let group = self.group_at_index(group_index).await;
                 let params = CreateRowParams {
                     database_id: self.editor.database_id.clone(),
@@ -123,7 +127,7 @@ impl GridGroupTest {
                 let row = self.row_at_index(group_index, row_index).await;
                 self.editor.delete_row(&row.id).await.unwrap();
             }
-            GroupScript::UpdateRow {
+            GroupScript::UpdateGroupedCell {
                 from_group_index,
                 row_index,
                 to_group_index,
@@ -154,6 +158,7 @@ impl GridGroupTest {
                         FieldType::MultiSelect => {
                             insert_select_option_cell(vec![to_group.group_id.clone()], &field_rev)
                         }
+                        FieldType::URL => insert_url_cell(to_group.group_id.clone(), &field_rev),
                         _ => {
                             panic!("Unsupported group field type");
                         }
@@ -165,6 +170,27 @@ impl GridGroupTest {
                 row_changeset.cell_by_field_id.insert(field_id, cell_rev);
                 self.editor.update_row(row_changeset).await.unwrap();
             }
+            GroupScript::UpdateGroupedCellWithData {
+                from_group_index,
+                row_index,
+                cell_data,
+            } => {
+                let from_group = self.group_at_index(from_group_index).await;
+                let field_id = from_group.field_id;
+                let field_rev = self.editor.get_field_rev(&field_id).await.unwrap();
+                let field_type: FieldType = field_rev.ty.into();
+                let cell_rev = match field_type {
+                    FieldType::URL => insert_url_cell(cell_data, &field_rev),
+                    _ => {
+                        panic!("Unsupported group field type");
+                    }
+                };
+
+                let row_id = self.row_at_index(from_group_index, row_index).await.id;
+                let mut row_changeset = RowChangeset::new(row_id);
+                row_changeset.cell_by_field_id.insert(field_id, cell_rev);
+                self.editor.update_row(row_changeset).await.unwrap();
+            }
             GroupScript::MoveGroup {
                 from_group_index,
                 to_group_index,
@@ -258,15 +284,15 @@ impl GridGroupTest {
     }
 }
 
-impl std::ops::Deref for GridGroupTest {
-    type Target = GridEditorTest;
+impl std::ops::Deref for DatabaseGroupTest {
+    type Target = DatabaseEditorTest;
 
     fn deref(&self) -> &Self::Target {
         &self.inner
     }
 }
 
-impl std::ops::DerefMut for GridGroupTest {
+impl std::ops::DerefMut for DatabaseGroupTest {
     fn deref_mut(&mut self) -> &mut Self::Target {
         &mut self.inner
     }

+ 24 - 73
frontend/rust-lib/flowy-database/tests/grid/group_test/test.rs

@@ -1,11 +1,11 @@
-use crate::grid::group_test::script::GridGroupTest;
+use crate::grid::group_test::script::DatabaseGroupTest;
 use crate::grid::group_test::script::GroupScript::*;
 
 use flowy_database::services::field::SelectOptionPB;
 
 #[tokio::test]
 async fn group_init_test() {
-    let mut test = GridGroupTest::new().await;
+    let mut test = DatabaseGroupTest::new().await;
     let scripts = vec![
         AssertGroupCount(4),
         AssertGroupRowCount {
@@ -30,7 +30,7 @@ async fn group_init_test() {
 
 #[tokio::test]
 async fn group_move_row_test() {
-    let mut test = GridGroupTest::new().await;
+    let mut test = DatabaseGroupTest::new().await;
     let group = test.group_at_index(1).await;
     let scripts = vec![
         // Move the row at 0 in group0 to group1 at 1
@@ -55,7 +55,7 @@ async fn group_move_row_test() {
 
 #[tokio::test]
 async fn group_move_row_to_other_group_test() {
-    let mut test = GridGroupTest::new().await;
+    let mut test = DatabaseGroupTest::new().await;
     let group = test.group_at_index(1).await;
     let scripts = vec![
         MoveRow {
@@ -83,7 +83,7 @@ async fn group_move_row_to_other_group_test() {
 
 #[tokio::test]
 async fn group_move_two_row_to_other_group_test() {
-    let mut test = GridGroupTest::new().await;
+    let mut test = DatabaseGroupTest::new().await;
     let group_1 = test.group_at_index(1).await;
     let scripts = vec![
         // Move row at index 0 from group 1 to group 2 at index 1
@@ -137,7 +137,7 @@ async fn group_move_two_row_to_other_group_test() {
 
 #[tokio::test]
 async fn group_move_row_to_other_group_and_reorder_from_up_to_down_test() {
-    let mut test = GridGroupTest::new().await;
+    let mut test = DatabaseGroupTest::new().await;
     let group_1 = test.group_at_index(1).await;
     let group_2 = test.group_at_index(2).await;
     let scripts = vec![
@@ -173,7 +173,7 @@ async fn group_move_row_to_other_group_and_reorder_from_up_to_down_test() {
 
 #[tokio::test]
 async fn group_move_row_to_other_group_and_reorder_from_bottom_to_up_test() {
-    let mut test = GridGroupTest::new().await;
+    let mut test = DatabaseGroupTest::new().await;
     let scripts = vec![MoveRow {
         from_group_index: 1,
         from_row_index: 0,
@@ -204,7 +204,7 @@ async fn group_move_row_to_other_group_and_reorder_from_bottom_to_up_test() {
 }
 #[tokio::test]
 async fn group_create_row_test() {
-    let mut test = GridGroupTest::new().await;
+    let mut test = DatabaseGroupTest::new().await;
     let scripts = vec![
         CreateRow { group_index: 1 },
         AssertGroupRowCount {
@@ -223,7 +223,7 @@ async fn group_create_row_test() {
 
 #[tokio::test]
 async fn group_delete_row_test() {
-    let mut test = GridGroupTest::new().await;
+    let mut test = DatabaseGroupTest::new().await;
     let scripts = vec![
         DeleteRow {
             group_index: 1,
@@ -239,7 +239,7 @@ async fn group_delete_row_test() {
 
 #[tokio::test]
 async fn group_delete_all_row_test() {
-    let mut test = GridGroupTest::new().await;
+    let mut test = DatabaseGroupTest::new().await;
     let scripts = vec![
         DeleteRow {
             group_index: 1,
@@ -259,10 +259,10 @@ async fn group_delete_all_row_test() {
 
 #[tokio::test]
 async fn group_update_row_test() {
-    let mut test = GridGroupTest::new().await;
+    let mut test = DatabaseGroupTest::new().await;
     let scripts = vec![
         // Update the row at 0 in group0 by setting the row's group field data
-        UpdateRow {
+        UpdateGroupedCell {
             from_group_index: 1,
             row_index: 0,
             to_group_index: 2,
@@ -281,10 +281,10 @@ async fn group_update_row_test() {
 
 #[tokio::test]
 async fn group_reorder_group_test() {
-    let mut test = GridGroupTest::new().await;
+    let mut test = DatabaseGroupTest::new().await;
     let scripts = vec![
         // Update the row at 0 in group0 by setting the row's group field data
-        UpdateRow {
+        UpdateGroupedCell {
             from_group_index: 1,
             row_index: 0,
             to_group_index: 2,
@@ -303,9 +303,9 @@ async fn group_reorder_group_test() {
 
 #[tokio::test]
 async fn group_move_to_default_group_test() {
-    let mut test = GridGroupTest::new().await;
+    let mut test = DatabaseGroupTest::new().await;
     let scripts = vec![
-        UpdateRow {
+        UpdateGroupedCell {
             from_group_index: 1,
             row_index: 0,
             to_group_index: 0,
@@ -324,10 +324,10 @@ async fn group_move_to_default_group_test() {
 
 #[tokio::test]
 async fn group_move_from_default_group_test() {
-    let mut test = GridGroupTest::new().await;
+    let mut test = DatabaseGroupTest::new().await;
     // Move one row from group 1 to group 0
     let scripts = vec![
-        UpdateRow {
+        UpdateGroupedCell {
             from_group_index: 1,
             row_index: 0,
             to_group_index: 0,
@@ -345,7 +345,7 @@ async fn group_move_from_default_group_test() {
 
     // Move one row from group 0 to group 1
     let scripts = vec![
-        UpdateRow {
+        UpdateGroupedCell {
             from_group_index: 0,
             row_index: 0,
             to_group_index: 1,
@@ -364,7 +364,7 @@ async fn group_move_from_default_group_test() {
 
 #[tokio::test]
 async fn group_move_group_test() {
-    let mut test = GridGroupTest::new().await;
+    let mut test = DatabaseGroupTest::new().await;
     let group_0 = test.group_at_index(0).await;
     let group_1 = test.group_at_index(1).await;
     let scripts = vec![
@@ -394,7 +394,7 @@ async fn group_move_group_test() {
 
 #[tokio::test]
 async fn group_move_group_row_after_move_group_test() {
-    let mut test = GridGroupTest::new().await;
+    let mut test = DatabaseGroupTest::new().await;
     let group_1 = test.group_at_index(1).await;
     let group_2 = test.group_at_index(2).await;
     let scripts = vec![
@@ -430,7 +430,7 @@ async fn group_move_group_row_after_move_group_test() {
 
 #[tokio::test]
 async fn group_move_group_to_default_group_pos_test() {
-    let mut test = GridGroupTest::new().await;
+    let mut test = DatabaseGroupTest::new().await;
     let group_0 = test.group_at_index(0).await;
     let group_3 = test.group_at_index(3).await;
     let scripts = vec![
@@ -452,7 +452,7 @@ async fn group_move_group_to_default_group_pos_test() {
 
 #[tokio::test]
 async fn group_insert_single_select_option_test() {
-    let mut test = GridGroupTest::new().await;
+    let mut test = DatabaseGroupTest::new().await;
     let new_option_name = "New option";
     let scripts = vec![
         AssertGroupCount(4),
@@ -468,7 +468,7 @@ async fn group_insert_single_select_option_test() {
 
 #[tokio::test]
 async fn group_group_by_other_field() {
-    let mut test = GridGroupTest::new().await;
+    let mut test = DatabaseGroupTest::new().await;
     let multi_select_field = test.get_multi_select_field().await;
     let scripts = vec![
         GroupByField {
@@ -486,52 +486,3 @@ async fn group_group_by_other_field() {
     ];
     test.run_scripts(scripts).await;
 }
-
-#[tokio::test]
-async fn group_group_by_url() {
-    let mut test = GridGroupTest::new().await;
-    let url_field = test.get_url_field().await;
-    let scripts = vec![
-        GroupByField {
-            field_id: url_field.id.clone(),
-        },
-        AssertGroupRowCount {
-            group_index: 0,
-            row_count: 2,
-        },
-        AssertGroupRowCount {
-            group_index: 1,
-            row_count: 2,
-        },
-        AssertGroupRowCount {
-            group_index: 2,
-            row_count: 1,
-        },
-        AssertGroupCount(3),
-        MoveRow {
-            from_group_index: 0,
-            from_row_index: 0,
-            to_group_index: 1,
-            to_row_index: 0,
-        },
-        MoveRow {
-            from_group_index: 1,
-            from_row_index: 0,
-            to_group_index: 2,
-            to_row_index: 0,
-        },
-        AssertGroupRowCount {
-            group_index: 0,
-            row_count: 1,
-        },
-        AssertGroupRowCount {
-            group_index: 1,
-            row_count: 2,
-        },
-        AssertGroupRowCount {
-            group_index: 2,
-            row_count: 2,
-        },
-    ];
-    test.run_scripts(scripts).await;
-}

+ 148 - 0
frontend/rust-lib/flowy-database/tests/grid/group_test/url_group_test.rs

@@ -0,0 +1,148 @@
+use crate::grid::group_test::script::DatabaseGroupTest;
+use crate::grid::group_test::script::GroupScript::*;
+
+#[tokio::test]
+async fn group_group_by_url() {
+    let mut test = DatabaseGroupTest::new().await;
+    let url_field = test.get_url_field().await;
+    let scripts = vec![
+        GroupByField {
+            field_id: url_field.id.clone(),
+        },
+        // no status group
+        AssertGroupRowCount {
+            group_index: 0,
+            row_count: 2,
+        },
+        // https://appflowy.io
+        AssertGroupRowCount {
+            group_index: 1,
+            row_count: 2,
+        },
+        // https://github.com/AppFlowy-IO/AppFlowy
+        AssertGroupRowCount {
+            group_index: 2,
+            row_count: 1,
+        },
+        AssertGroupCount(3),
+    ];
+    test.run_scripts(scripts).await;
+}
+
+#[tokio::test]
+async fn group_alter_url_to_another_group_url_test() {
+    let mut test = DatabaseGroupTest::new().await;
+    let url_field = test.get_url_field().await;
+    let scripts = vec![
+        GroupByField {
+            field_id: url_field.id.clone(),
+        },
+        // no status group
+        AssertGroupRowCount {
+            group_index: 0,
+            row_count: 2,
+        },
+        // https://appflowy.io
+        AssertGroupRowCount {
+            group_index: 1,
+            row_count: 2,
+        },
+        // https://github.com/AppFlowy-IO/AppFlowy
+        AssertGroupRowCount {
+            group_index: 2,
+            row_count: 1,
+        },
+        // When moving the last row from 2nd group to 1nd group, the 2nd group will be removed
+        UpdateGroupedCell {
+            from_group_index: 2,
+            row_index: 0,
+            to_group_index: 1,
+        },
+        AssertGroupCount(2),
+    ];
+    test.run_scripts(scripts).await;
+}
+
+#[tokio::test]
+async fn group_alter_url_to_new_url_test() {
+    let mut test = DatabaseGroupTest::new().await;
+    let url_field = test.get_url_field().await;
+    let scripts = vec![
+        GroupByField {
+            field_id: url_field.id.clone(),
+        },
+        // When moving the last row from 2nd group to 1nd group, the 2nd group will be removed
+        UpdateGroupedCellWithData {
+            from_group_index: 0,
+            row_index: 0,
+            cell_data: "https://github.com/AppFlowy-IO".to_string(),
+        },
+        // no status group
+        AssertGroupRowCount {
+            group_index: 0,
+            row_count: 1,
+        },
+        // https://appflowy.io
+        AssertGroupRowCount {
+            group_index: 1,
+            row_count: 2,
+        },
+        // https://github.com/AppFlowy-IO/AppFlowy
+        AssertGroupRowCount {
+            group_index: 2,
+            row_count: 1,
+        },
+        AssertGroupRowCount {
+            group_index: 3,
+            row_count: 1,
+        },
+        AssertGroupCount(4),
+    ];
+    test.run_scripts(scripts).await;
+}
+
+#[tokio::test]
+async fn group_move_url_group_row_test() {
+    let mut test = DatabaseGroupTest::new().await;
+    let url_field = test.get_url_field().await;
+    let scripts = vec![
+        GroupByField {
+            field_id: url_field.id.clone(),
+        },
+        // no status group
+        AssertGroupRowCount {
+            group_index: 0,
+            row_count: 2,
+        },
+        // https://appflowy.io
+        AssertGroupRowCount {
+            group_index: 1,
+            row_count: 2,
+        },
+        // https://github.com/AppFlowy-IO/AppFlowy
+        AssertGroupRowCount {
+            group_index: 2,
+            row_count: 1,
+        },
+        AssertGroupCount(3),
+        MoveRow {
+            from_group_index: 0,
+            from_row_index: 0,
+            to_group_index: 1,
+            to_row_index: 0,
+        },
+        AssertGroupRowCount {
+            group_index: 0,
+            row_count: 1,
+        },
+        AssertGroupRowCount {
+            group_index: 1,
+            row_count: 3,
+        },
+        AssertGroupRowCount {
+            group_index: 2,
+            row_count: 1,
+        },
+    ];
+    test.run_scripts(scripts).await;
+}

+ 191 - 0
frontend/rust-lib/flowy-database/tests/grid/mock_data/board_mock_data.rs

@@ -0,0 +1,191 @@
+// #![allow(clippy::all)]
+// #![allow(dead_code)]
+// #![allow(unused_imports)]
+use crate::grid::block_test::util::GridRowTestBuilder;
+use crate::grid::mock_data::{
+    COMPLETED, FACEBOOK, FIRST_THING, GOOGLE, PAUSED, PLANNED, SECOND_THING, THIRD_THING, TWITTER,
+};
+
+use flowy_client_sync::client_database::DatabaseBuilder;
+use flowy_database::entities::*;
+
+use flowy_database::services::field::SelectOptionPB;
+use flowy_database::services::field::*;
+
+use grid_model::*;
+
+use strum::IntoEnumIterator;
+
+// Kanban board unit test mock data
+pub fn make_test_board() -> BuildDatabaseContext {
+    let mut grid_builder = DatabaseBuilder::new();
+    // Iterate through the FieldType to create the corresponding Field.
+    for field_type in FieldType::iter() {
+        let field_type: FieldType = field_type;
+
+        // The
+        match field_type {
+            FieldType::RichText => {
+                let text_field = FieldBuilder::new(RichTextTypeOptionBuilder::default())
+                    .name("Name")
+                    .visibility(true)
+                    .primary(true)
+                    .build();
+                grid_builder.add_field(text_field);
+            }
+            FieldType::Number => {
+                // Number
+                let number = NumberTypeOptionBuilder::default().set_format(NumberFormat::USD);
+                let number_field = FieldBuilder::new(number).name("Price").visibility(true).build();
+                grid_builder.add_field(number_field);
+            }
+            FieldType::DateTime => {
+                // Date
+                let date = DateTypeOptionBuilder::default()
+                    .date_format(DateFormat::US)
+                    .time_format(TimeFormat::TwentyFourHour);
+                let date_field = FieldBuilder::new(date).name("Time").visibility(true).build();
+                grid_builder.add_field(date_field);
+            }
+            FieldType::SingleSelect => {
+                // Single Select
+                let single_select = SingleSelectTypeOptionBuilder::default()
+                    .add_option(SelectOptionPB::new(COMPLETED))
+                    .add_option(SelectOptionPB::new(PLANNED))
+                    .add_option(SelectOptionPB::new(PAUSED));
+                let single_select_field = FieldBuilder::new(single_select).name("Status").visibility(true).build();
+                grid_builder.add_field(single_select_field);
+            }
+            FieldType::MultiSelect => {
+                // MultiSelect
+                let multi_select = MultiSelectTypeOptionBuilder::default()
+                    .add_option(SelectOptionPB::new(GOOGLE))
+                    .add_option(SelectOptionPB::new(FACEBOOK))
+                    .add_option(SelectOptionPB::new(TWITTER));
+                let multi_select_field = FieldBuilder::new(multi_select)
+                    .name("Platform")
+                    .visibility(true)
+                    .build();
+                grid_builder.add_field(multi_select_field);
+            }
+            FieldType::Checkbox => {
+                // Checkbox
+                let checkbox = CheckboxTypeOptionBuilder::default();
+                let checkbox_field = FieldBuilder::new(checkbox).name("is urgent").visibility(true).build();
+                grid_builder.add_field(checkbox_field);
+            }
+            FieldType::URL => {
+                // URL
+                let url = URLTypeOptionBuilder::default();
+                let url_field = FieldBuilder::new(url).name("link").visibility(true).build();
+                grid_builder.add_field(url_field);
+            }
+            FieldType::Checklist => {
+                let checklist = ChecklistTypeOptionBuilder::default()
+                    .add_option(SelectOptionPB::new(FIRST_THING))
+                    .add_option(SelectOptionPB::new(SECOND_THING))
+                    .add_option(SelectOptionPB::new(THIRD_THING));
+                let checklist_field = FieldBuilder::new(checklist).name("TODO").visibility(true).build();
+                grid_builder.add_field(checklist_field);
+            }
+        }
+    }
+
+    // We have many assumptions base on the number of the rows, so do not change the number of the loop.
+    for i in 0..5 {
+        let block_id = grid_builder.block_id().to_owned();
+        let field_revs = grid_builder.field_revs();
+        let mut row_builder = GridRowTestBuilder::new(&block_id, field_revs);
+        match i {
+            0 => {
+                for field_type in FieldType::iter() {
+                    match field_type {
+                        FieldType::RichText => row_builder.insert_text_cell("A"),
+                        FieldType::Number => row_builder.insert_number_cell("1"),
+                        // 1647251762 => Mar 14,2022
+                        FieldType::DateTime => row_builder.insert_date_cell("1647251762"),
+                        FieldType::SingleSelect => {
+                            row_builder.insert_single_select_cell(|mut options| options.remove(0))
+                        }
+                        FieldType::MultiSelect => row_builder
+                            .insert_multi_select_cell(|mut options| vec![options.remove(0), options.remove(0)]),
+                        FieldType::Checkbox => row_builder.insert_checkbox_cell("true"),
+                        FieldType::URL => row_builder.insert_url_cell("https://appflowy.io"),
+                        _ => "".to_owned(),
+                    };
+                }
+            }
+            1 => {
+                for field_type in FieldType::iter() {
+                    match field_type {
+                        FieldType::RichText => row_builder.insert_text_cell("B"),
+                        FieldType::Number => row_builder.insert_number_cell("2"),
+                        // 1647251762 => Mar 14,2022
+                        FieldType::DateTime => row_builder.insert_date_cell("1647251762"),
+                        FieldType::SingleSelect => {
+                            row_builder.insert_single_select_cell(|mut options| options.remove(0))
+                        }
+                        FieldType::MultiSelect => row_builder
+                            .insert_multi_select_cell(|mut options| vec![options.remove(0), options.remove(0)]),
+                        FieldType::Checkbox => row_builder.insert_checkbox_cell("true"),
+                        _ => "".to_owned(),
+                    };
+                }
+            }
+            2 => {
+                for field_type in FieldType::iter() {
+                    match field_type {
+                        FieldType::RichText => row_builder.insert_text_cell("C"),
+                        FieldType::Number => row_builder.insert_number_cell("3"),
+                        // 1647251762 => Mar 14,2022
+                        FieldType::DateTime => row_builder.insert_date_cell("1647251762"),
+                        FieldType::SingleSelect => {
+                            row_builder.insert_single_select_cell(|mut options| options.remove(1))
+                        }
+                        FieldType::MultiSelect => {
+                            row_builder.insert_multi_select_cell(|mut options| vec![options.remove(0)])
+                        }
+                        FieldType::Checkbox => row_builder.insert_checkbox_cell("false"),
+                        FieldType::URL => row_builder.insert_url_cell("https://github.com/AppFlowy-IO/AppFlowy"),
+                        _ => "".to_owned(),
+                    };
+                }
+            }
+            3 => {
+                for field_type in FieldType::iter() {
+                    match field_type {
+                        FieldType::RichText => row_builder.insert_text_cell("DA"),
+                        FieldType::Number => row_builder.insert_number_cell("4"),
+                        FieldType::DateTime => row_builder.insert_date_cell("1668704685"),
+                        FieldType::SingleSelect => {
+                            row_builder.insert_single_select_cell(|mut options| options.remove(1))
+                        }
+                        FieldType::Checkbox => row_builder.insert_checkbox_cell("false"),
+                        FieldType::URL => row_builder.insert_url_cell("https://appflowy.io"),
+                        _ => "".to_owned(),
+                    };
+                }
+            }
+            4 => {
+                for field_type in FieldType::iter() {
+                    match field_type {
+                        FieldType::RichText => row_builder.insert_text_cell("AE"),
+                        FieldType::Number => row_builder.insert_number_cell(""),
+                        FieldType::DateTime => row_builder.insert_date_cell("1668359085"),
+                        FieldType::SingleSelect => {
+                            row_builder.insert_single_select_cell(|mut options| options.remove(2))
+                        }
+
+                        FieldType::Checkbox => row_builder.insert_checkbox_cell("false"),
+                        _ => "".to_owned(),
+                    };
+                }
+            }
+            _ => {}
+        }
+
+        let row_rev = row_builder.build();
+        grid_builder.add_row(row_rev);
+    }
+    grid_builder.build()
+}

+ 6 - 0
frontend/rust-lib/flowy-database/tests/grid/mock_data/calendar_mock_data.rs

@@ -0,0 +1,6 @@
+use grid_model::BuildDatabaseContext;
+
+// Calendar unit test mock data
+pub fn make_test_calendar() -> BuildDatabaseContext {
+    todo!()
+}

+ 193 - 0
frontend/rust-lib/flowy-database/tests/grid/mock_data/grid_mock_data.rs

@@ -0,0 +1,193 @@
+// #![allow(clippy::all)]
+// #![allow(dead_code)]
+// #![allow(unused_imports)]
+use crate::grid::block_test::util::GridRowTestBuilder;
+use crate::grid::mock_data::{
+    COMPLETED, FACEBOOK, FIRST_THING, GOOGLE, PAUSED, PLANNED, SECOND_THING, THIRD_THING, TWITTER,
+};
+
+use flowy_client_sync::client_database::DatabaseBuilder;
+use flowy_database::entities::*;
+
+use flowy_database::services::field::SelectOptionPB;
+use flowy_database::services::field::*;
+
+use grid_model::*;
+
+use strum::IntoEnumIterator;
+
+pub fn make_test_grid() -> BuildDatabaseContext {
+    let mut grid_builder = DatabaseBuilder::new();
+    // Iterate through the FieldType to create the corresponding Field.
+    for field_type in FieldType::iter() {
+        let field_type: FieldType = field_type;
+
+        // The
+        match field_type {
+            FieldType::RichText => {
+                let text_field = FieldBuilder::new(RichTextTypeOptionBuilder::default())
+                    .name("Name")
+                    .visibility(true)
+                    .primary(true)
+                    .build();
+                grid_builder.add_field(text_field);
+            }
+            FieldType::Number => {
+                // Number
+                let number = NumberTypeOptionBuilder::default().set_format(NumberFormat::USD);
+                let number_field = FieldBuilder::new(number).name("Price").visibility(true).build();
+                grid_builder.add_field(number_field);
+            }
+            FieldType::DateTime => {
+                // Date
+                let date = DateTypeOptionBuilder::default()
+                    .date_format(DateFormat::US)
+                    .time_format(TimeFormat::TwentyFourHour);
+                let date_field = FieldBuilder::new(date).name("Time").visibility(true).build();
+                grid_builder.add_field(date_field);
+            }
+            FieldType::SingleSelect => {
+                // Single Select
+                let single_select = SingleSelectTypeOptionBuilder::default()
+                    .add_option(SelectOptionPB::new(COMPLETED))
+                    .add_option(SelectOptionPB::new(PLANNED))
+                    .add_option(SelectOptionPB::new(PAUSED));
+                let single_select_field = FieldBuilder::new(single_select).name("Status").visibility(true).build();
+                grid_builder.add_field(single_select_field);
+            }
+            FieldType::MultiSelect => {
+                // MultiSelect
+                let multi_select = MultiSelectTypeOptionBuilder::default()
+                    .add_option(SelectOptionPB::new(GOOGLE))
+                    .add_option(SelectOptionPB::new(FACEBOOK))
+                    .add_option(SelectOptionPB::new(TWITTER));
+                let multi_select_field = FieldBuilder::new(multi_select)
+                    .name("Platform")
+                    .visibility(true)
+                    .build();
+                grid_builder.add_field(multi_select_field);
+            }
+            FieldType::Checkbox => {
+                // Checkbox
+                let checkbox = CheckboxTypeOptionBuilder::default();
+                let checkbox_field = FieldBuilder::new(checkbox).name("is urgent").visibility(true).build();
+                grid_builder.add_field(checkbox_field);
+            }
+            FieldType::URL => {
+                // URL
+                let url = URLTypeOptionBuilder::default();
+                let url_field = FieldBuilder::new(url).name("link").visibility(true).build();
+                grid_builder.add_field(url_field);
+            }
+            FieldType::Checklist => {
+                let checklist = ChecklistTypeOptionBuilder::default()
+                    .add_option(SelectOptionPB::new(FIRST_THING))
+                    .add_option(SelectOptionPB::new(SECOND_THING))
+                    .add_option(SelectOptionPB::new(THIRD_THING));
+                let checklist_field = FieldBuilder::new(checklist).name("TODO").visibility(true).build();
+                grid_builder.add_field(checklist_field);
+            }
+        }
+    }
+
+    for i in 0..6 {
+        let block_id = grid_builder.block_id().to_owned();
+        let field_revs = grid_builder.field_revs();
+        let mut row_builder = GridRowTestBuilder::new(&block_id, field_revs);
+        match i {
+            0 => {
+                for field_type in FieldType::iter() {
+                    match field_type {
+                        FieldType::RichText => row_builder.insert_text_cell("A"),
+                        FieldType::Number => row_builder.insert_number_cell("1"),
+                        FieldType::DateTime => row_builder.insert_date_cell("1647251762"),
+                        FieldType::MultiSelect => row_builder
+                            .insert_multi_select_cell(|mut options| vec![options.remove(0), options.remove(0)]),
+                        FieldType::Checklist => row_builder.insert_checklist_cell(|options| options),
+                        FieldType::Checkbox => row_builder.insert_checkbox_cell("true"),
+                        FieldType::URL => row_builder.insert_url_cell("AppFlowy website - https://www.appflowy.io"),
+                        _ => "".to_owned(),
+                    };
+                }
+            }
+            1 => {
+                for field_type in FieldType::iter() {
+                    match field_type {
+                        FieldType::RichText => row_builder.insert_text_cell(""),
+                        FieldType::Number => row_builder.insert_number_cell("2"),
+                        FieldType::DateTime => row_builder.insert_date_cell("1647251762"),
+                        FieldType::MultiSelect => row_builder
+                            .insert_multi_select_cell(|mut options| vec![options.remove(0), options.remove(1)]),
+                        FieldType::Checkbox => row_builder.insert_checkbox_cell("true"),
+                        _ => "".to_owned(),
+                    };
+                }
+            }
+            2 => {
+                for field_type in FieldType::iter() {
+                    match field_type {
+                        FieldType::RichText => row_builder.insert_text_cell("C"),
+                        FieldType::Number => row_builder.insert_number_cell("3"),
+                        FieldType::DateTime => row_builder.insert_date_cell("1647251762"),
+                        FieldType::SingleSelect => {
+                            row_builder.insert_single_select_cell(|mut options| options.remove(0))
+                        }
+                        FieldType::MultiSelect => {
+                            row_builder.insert_multi_select_cell(|mut options| vec![options.remove(1)])
+                        }
+                        FieldType::Checkbox => row_builder.insert_checkbox_cell("false"),
+                        _ => "".to_owned(),
+                    };
+                }
+            }
+            3 => {
+                for field_type in FieldType::iter() {
+                    match field_type {
+                        FieldType::RichText => row_builder.insert_text_cell("DA"),
+                        FieldType::Number => row_builder.insert_number_cell("4"),
+                        FieldType::DateTime => row_builder.insert_date_cell("1668704685"),
+                        FieldType::SingleSelect => {
+                            row_builder.insert_single_select_cell(|mut options| options.remove(0))
+                        }
+                        FieldType::Checkbox => row_builder.insert_checkbox_cell("false"),
+                        _ => "".to_owned(),
+                    };
+                }
+            }
+            4 => {
+                for field_type in FieldType::iter() {
+                    match field_type {
+                        FieldType::RichText => row_builder.insert_text_cell("AE"),
+                        FieldType::Number => row_builder.insert_number_cell(""),
+                        FieldType::DateTime => row_builder.insert_date_cell("1668359085"),
+                        FieldType::SingleSelect => {
+                            row_builder.insert_single_select_cell(|mut options| options.remove(1))
+                        }
+
+                        FieldType::Checkbox => row_builder.insert_checkbox_cell("false"),
+                        _ => "".to_owned(),
+                    };
+                }
+            }
+            5 => {
+                for field_type in FieldType::iter() {
+                    match field_type {
+                        FieldType::RichText => row_builder.insert_text_cell("AE"),
+                        FieldType::Number => row_builder.insert_number_cell("5"),
+                        FieldType::DateTime => row_builder.insert_date_cell("1671938394"),
+                        FieldType::SingleSelect => {
+                            row_builder.insert_single_select_cell(|mut options| options.remove(1))
+                        }
+                        FieldType::Checkbox => row_builder.insert_checkbox_cell("true"),
+                        _ => "".to_owned(),
+                    };
+                }
+            }
+            _ => {}
+        }
+
+        let row_rev = row_builder.build();
+        grid_builder.add_row(row_rev);
+    }
+    grid_builder.build()
+}

+ 19 - 0
frontend/rust-lib/flowy-database/tests/grid/mock_data/mod.rs

@@ -0,0 +1,19 @@
+mod board_mock_data;
+mod calendar_mock_data;
+mod grid_mock_data;
+
+pub use board_mock_data::*;
+pub use calendar_mock_data::*;
+pub use grid_mock_data::*;
+
+pub const GOOGLE: &str = "Google";
+pub const FACEBOOK: &str = "Facebook";
+pub const TWITTER: &str = "Twitter";
+
+pub const COMPLETED: &str = "Completed";
+pub const PLANNED: &str = "Planned";
+pub const PAUSED: &str = "Paused";
+
+pub const FIRST_THING: &str = "Wake up at 6:00 am";
+pub const SECOND_THING: &str = "Get some coffee";
+pub const THIRD_THING: &str = "Start working";

+ 3 - 1
frontend/rust-lib/flowy-database/tests/grid/mod.rs

@@ -1,8 +1,10 @@
 mod block_test;
 mod cell_test;
+mod database_editor;
 mod field_test;
 mod filter_test;
-mod grid_editor;
 mod group_test;
 mod snapshot_test;
 mod sort_test;
+
+mod mock_data;

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

@@ -1,374 +0,0 @@
-use bytes::Bytes;
-use flowy_client_sync::client_grid::GridBuilder;
-use flowy_database::services::field::*;
-use flowy_database::services::grid_meta_editor::{GridMetaEditor, GridPadBuilder};
-use flowy_database::services::row::CreateRowMetaPayload;
-use flowy_revision::REVISION_WRITE_INTERVAL_IN_MILLIS;
-use flowy_test::helper::ViewTest;
-use flowy_test::FlowySDKTest;
-use grid_model::entities::{
-    BuildGridContext, CellChangeset, Field, FieldChangesetParams, FieldMeta, FieldOrder, FieldType,
-    GridBlockInfoChangeset, GridBlockMetaSnapshot, InsertFieldParams, RowMeta, RowMetaChangeset, RowOrder,
-    TypeOptionDataFormat,
-};
-use std::collections::HashMap;
-use std::sync::Arc;
-use std::time::Duration;
-use strum::EnumCount;
-use tokio::time::sleep;
-
-pub enum EditorScript {
-    CreateField {
-        params: InsertFieldParams,
-    },
-    UpdateField {
-        changeset: FieldChangesetParams,
-    },
-    DeleteField {
-        field_meta: FieldMeta,
-    },
-    AssertFieldCount(usize),
-    AssertFieldEqual {
-        field_index: usize,
-        field_meta: FieldMeta,
-    },
-    CreateBlock {
-        block: GridBlockMetaSnapshot,
-    },
-    UpdateBlock {
-        changeset: GridBlockInfoChangeset,
-    },
-    AssertBlockCount(usize),
-    AssertBlock {
-        block_index: usize,
-        row_count: i32,
-        start_row_index: i32,
-    },
-    AssertBlockEqual {
-        block_index: usize,
-        block: GridBlockMetaSnapshot,
-    },
-    CreateEmptyRow,
-    CreateRow {
-        context: CreateRowMetaPayload,
-    },
-    UpdateRow {
-        changeset: RowMetaChangeset,
-    },
-    AssertRow {
-        changeset: RowMetaChangeset,
-    },
-    DeleteRow {
-        row_ids: Vec<String>,
-    },
-    UpdateCell {
-        changeset: CellChangeset,
-        is_err: bool,
-    },
-    AssertRowCount(usize),
-    // AssertRowEqual{ row_index: usize, row: RowMeta},
-    AssertGridMetaPad,
-}
-
-pub struct GridEditorTest {
-    pub sdk: FlowySDKTest,
-    pub grid_id: String,
-    pub editor: Arc<GridMetaEditor>,
-    pub field_metas: Vec<FieldMeta>,
-    pub grid_blocks: Vec<GridBlockMetaSnapshot>,
-    pub row_metas: Vec<Arc<RowMeta>>,
-    pub field_count: usize,
-
-    pub row_order_by_row_id: HashMap<String, RowOrder>,
-}
-
-impl GridEditorTest {
-    pub async fn new() -> Self {
-        let sdk = FlowySDKTest::default();
-        let _ = sdk.init_user().await;
-        let build_context = make_template_1_grid();
-        let view_data: Bytes = build_context.into();
-        let test = ViewTest::new_grid_view(&sdk, view_data.to_vec()).await;
-        let editor = sdk.grid_manager.open_grid(&test.view.id).await.unwrap();
-        let field_metas = editor.get_field_metas::<FieldOrder>(None).await.unwrap();
-        let grid_blocks = editor.get_block_metas().await.unwrap();
-        let row_metas = get_row_metas(&editor).await;
-
-        let grid_id = test.view.id;
-        Self {
-            sdk,
-            grid_id,
-            editor,
-            field_metas,
-            grid_blocks,
-            row_metas,
-            field_count: FieldType::COUNT,
-            row_order_by_row_id: HashMap::default(),
-        }
-    }
-
-    pub async fn run_scripts(&mut self, scripts: Vec<EditorScript>) {
-        for script in scripts {
-            self.run_script(script).await;
-        }
-    }
-
-    pub async fn run_script(&mut self, script: EditorScript) {
-        let grid_manager = self.sdk.grid_manager.clone();
-        let pool = self.sdk.user_session.db_pool().unwrap();
-        let rev_manager = self.editor.rev_manager();
-        let _cache = rev_manager.revision_cache().await;
-
-        match script {
-            EditorScript::CreateField { params } => {
-                if !self.editor.contain_field(&params.field.id).await {
-                    self.field_count += 1;
-                }
-
-                self.editor.insert_field(params).await.unwrap();
-                self.field_metas = self.editor.get_field_metas::<FieldOrder>(None).await.unwrap();
-                assert_eq!(self.field_count, self.field_metas.len());
-            }
-            EditorScript::UpdateField { changeset: change } => {
-                self.editor.update_field(change).await.unwrap();
-                self.field_metas = self.editor.get_field_metas::<FieldOrder>(None).await.unwrap();
-            }
-            EditorScript::DeleteField { field_meta } => {
-                if self.editor.contain_field(&field_meta.id).await {
-                    self.field_count -= 1;
-                }
-
-                self.editor.delete_field(&field_meta.id).await.unwrap();
-                self.field_metas = self.editor.get_field_metas::<FieldOrder>(None).await.unwrap();
-                assert_eq!(self.field_count, self.field_metas.len());
-            }
-            EditorScript::AssertFieldCount(count) => {
-                assert_eq!(
-                    self.editor.get_field_metas::<FieldOrder>(None).await.unwrap().len(),
-                    count
-                );
-            }
-            EditorScript::AssertFieldEqual {
-                field_index,
-                field_meta,
-            } => {
-                let field_metas = self.editor.get_field_metas::<FieldOrder>(None).await.unwrap();
-                assert_eq!(field_metas[field_index].clone(), field_meta);
-            }
-            EditorScript::CreateBlock { block } => {
-                self.editor.create_block(block).await.unwrap();
-                self.grid_blocks = self.editor.get_block_metas().await.unwrap();
-            }
-            EditorScript::UpdateBlock { changeset: change } => {
-                self.editor.update_block(change).await.unwrap();
-            }
-            EditorScript::AssertBlockCount(count) => {
-                assert_eq!(self.editor.get_block_metas().await.unwrap().len(), count);
-            }
-            EditorScript::AssertBlock {
-                block_index,
-                row_count,
-                start_row_index,
-            } => {
-                assert_eq!(self.grid_blocks[block_index].row_count, row_count);
-                assert_eq!(self.grid_blocks[block_index].start_row_index, start_row_index);
-            }
-            EditorScript::AssertBlockEqual { block_index, block } => {
-                let blocks = self.editor.get_block_metas().await.unwrap();
-                let compared_block = blocks[block_index].clone();
-                assert_eq!(compared_block, block);
-            }
-            EditorScript::CreateEmptyRow => {
-                let row_order = self.editor.create_row(None).await.unwrap();
-                self.row_order_by_row_id.insert(row_order.row_id.clone(), row_order);
-                self.row_metas = self.get_row_metas().await;
-                self.grid_blocks = self.editor.get_block_metas().await.unwrap();
-            }
-            EditorScript::CreateRow { context } => {
-                let row_orders = self.editor.insert_rows(vec![context]).await.unwrap();
-                for row_order in row_orders {
-                    self.row_order_by_row_id.insert(row_order.row_id.clone(), row_order);
-                }
-                self.row_metas = self.get_row_metas().await;
-                self.grid_blocks = self.editor.get_block_metas().await.unwrap();
-            }
-            EditorScript::UpdateRow { changeset: change } => self.editor.update_row(change).await.unwrap(),
-            EditorScript::DeleteRow { row_ids } => {
-                let row_orders = row_ids
-                    .into_iter()
-                    .map(|row_id| self.row_order_by_row_id.get(&row_id).unwrap().clone())
-                    .collect::<Vec<RowOrder>>();
-
-                self.editor.delete_rows(row_orders).await.unwrap();
-                self.row_metas = self.get_row_metas().await;
-                self.grid_blocks = self.editor.get_block_metas().await.unwrap();
-            }
-            EditorScript::AssertRow { changeset } => {
-                let row = self.row_metas.iter().find(|row| row.id == changeset.row_id).unwrap();
-
-                if let Some(visibility) = changeset.visibility {
-                    assert_eq!(row.visibility, visibility);
-                }
-
-                if let Some(height) = changeset.height {
-                    assert_eq!(row.height, height);
-                }
-            }
-            EditorScript::UpdateCell { changeset, is_err } => {
-                let result = self.editor.update_cell(changeset).await;
-                if is_err {
-                    assert!(result.is_err())
-                } else {
-                    let _ = result.unwrap();
-                    self.row_metas = self.get_row_metas().await;
-                }
-            }
-            EditorScript::AssertRowCount(count) => {
-                assert_eq!(self.row_metas.len(), count);
-            }
-            EditorScript::AssertGridMetaPad => {
-                sleep(Duration::from_millis(2 * REVISION_WRITE_INTERVAL_IN_MILLIS)).await;
-                let mut grid_rev_manager = grid_manager.make_grid_rev_manager(&self.grid_id, pool.clone()).unwrap();
-                let grid_pad = grid_rev_manager.load::<GridPadBuilder>(None).await.unwrap();
-                println!("{}", grid_pad.delta_str());
-            }
-        }
-    }
-
-    async fn get_row_metas(&self) -> Vec<Arc<RowMeta>> {
-        get_row_metas(&self.editor).await
-    }
-}
-
-async fn get_row_metas(editor: &Arc<GridMetaEditor>) -> Vec<Arc<RowMeta>> {
-    editor
-        .grid_block_snapshots(None)
-        .await
-        .unwrap()
-        .pop()
-        .unwrap()
-        .row_metas
-}
-
-pub fn create_text_field(grid_id: &str) -> (InsertFieldParams, FieldMeta) {
-    let field_meta = FieldBuilder::new(RichTextTypeOptionBuilder::default())
-        .name("Name")
-        .visibility(true)
-        .build();
-
-    let cloned_field_meta = field_meta.clone();
-
-    let type_option_data = field_meta
-        .get_type_option_entry::<RichTextTypeOptionPB>(&field_meta.field_type)
-        .unwrap()
-        .protobuf_bytes()
-        .to_vec();
-
-    let field = Field {
-        id: field_meta.id,
-        name: field_meta.name,
-        desc: field_meta.desc,
-        field_type: field_meta.field_type,
-        frozen: field_meta.frozen,
-        visibility: field_meta.visibility,
-        width: field_meta.width,
-        is_primary: false,
-    };
-
-    let params = InsertFieldParams {
-        grid_id: grid_id.to_owned(),
-        field,
-        type_option_data,
-        start_field_id: None,
-    };
-    (params, cloned_field_meta)
-}
-
-pub fn create_single_select_field(grid_id: &str) -> (InsertFieldParams, FieldMeta) {
-    let single_select = SingleSelectTypeOptionBuilder::default()
-        .option(SelectOption::new("Done"))
-        .option(SelectOption::new("Progress"));
-
-    let field_meta = FieldBuilder::new(single_select).name("Name").visibility(true).build();
-    let cloned_field_meta = field_meta.clone();
-    let type_option_data = field_meta
-        .get_type_option_entry::<SingleSelectTypeOption>(&field_meta.field_type)
-        .unwrap()
-        .protobuf_bytes()
-        .to_vec();
-
-    let field = Field {
-        id: field_meta.id,
-        name: field_meta.name,
-        desc: field_meta.desc,
-        field_type: field_meta.field_type,
-        frozen: field_meta.frozen,
-        visibility: field_meta.visibility,
-        width: field_meta.width,
-        is_primary: false,
-    };
-
-    let params = InsertFieldParams {
-        grid_id: grid_id.to_owned(),
-        field,
-        type_option_data,
-        start_field_id: None,
-    };
-    (params, cloned_field_meta)
-}
-
-fn make_template_1_grid() -> BuildGridContext {
-    let text_field = FieldBuilder::new(RichTextTypeOptionBuilder::default())
-        .name("Name")
-        .visibility(true)
-        .build();
-
-    // Single Select
-    let single_select = SingleSelectTypeOptionBuilder::default()
-        .option(SelectOption::new("Live"))
-        .option(SelectOption::new("Completed"))
-        .option(SelectOption::new("Planned"))
-        .option(SelectOption::new("Paused"));
-    let single_select_field = FieldBuilder::new(single_select).name("Status").visibility(true).build();
-
-    // MultiSelect
-    let multi_select = MultiSelectTypeOptionBuilder::default()
-        .option(SelectOption::new("Google"))
-        .option(SelectOption::new("Facebook"))
-        .option(SelectOption::new("Twitter"));
-    let multi_select_field = FieldBuilder::new(multi_select)
-        .name("Platform")
-        .visibility(true)
-        .build();
-
-    // Number
-    let number = NumberTypeOptionBuilder::default().set_format(NumberFormat::USD);
-    let number_field = FieldBuilder::new(number).name("Price").visibility(true).build();
-
-    // Date
-    let date = DateTypeOptionBuilder::default()
-        .date_format(DateFormat::US)
-        .time_format(TimeFormat::TwentyFourHour);
-    let date_field = FieldBuilder::new(date).name("Time").visibility(true).build();
-
-    // Checkbox
-    let checkbox = CheckboxTypeOptionBuilder::default();
-    let checkbox_field = FieldBuilder::new(checkbox).name("is done").visibility(true).build();
-
-    // URL
-    let url = URLTypeOptionBuilder::default();
-    let url_field = FieldBuilder::new(url).name("link").visibility(true).build();
-
-    GridBuilder::default()
-        .add_field(text_field)
-        .add_field(single_select_field)
-        .add_field(multi_select_field)
-        .add_field(number_field)
-        .add_field(date_field)
-        .add_field(checkbox_field)
-        .add_field(url_field)
-        .add_empty_row()
-        .add_empty_row()
-        .add_empty_row()
-        .build()
-}

+ 8 - 8
frontend/rust-lib/flowy-database/tests/grid/snapshot_test/script.rs

@@ -1,4 +1,4 @@
-use crate::grid::grid_editor::GridEditorTest;
+use crate::grid::database_editor::DatabaseEditorTest;
 
 use flowy_client_sync::client_database::{DatabaseOperations, DatabaseRevisionPad};
 use flowy_revision::{RevisionSnapshot, REVISION_WRITE_INTERVAL_IN_MILLIS};
@@ -26,15 +26,15 @@ pub enum SnapshotScript {
     },
 }
 
-pub struct GridSnapshotTest {
-    inner: GridEditorTest,
+pub struct DatabaseSnapshotTest {
+    inner: DatabaseEditorTest,
     pub current_snapshot: Option<RevisionSnapshot>,
     pub current_revision: Option<Revision>,
 }
 
-impl GridSnapshotTest {
+impl DatabaseSnapshotTest {
     pub async fn new() -> Self {
-        let editor_test = GridEditorTest::new_table().await;
+        let editor_test = DatabaseEditorTest::new_table().await;
         Self {
             inner: editor_test,
             current_snapshot: None,
@@ -88,15 +88,15 @@ impl GridSnapshotTest {
         }
     }
 }
-impl std::ops::Deref for GridSnapshotTest {
-    type Target = GridEditorTest;
+impl std::ops::Deref for DatabaseSnapshotTest {
+    type Target = DatabaseEditorTest;
 
     fn deref(&self) -> &Self::Target {
         &self.inner
     }
 }
 
-impl std::ops::DerefMut for GridSnapshotTest {
+impl std::ops::DerefMut for DatabaseSnapshotTest {
     fn deref_mut(&mut self) -> &mut Self::Target {
         &mut self.inner
     }

+ 3 - 3
frontend/rust-lib/flowy-database/tests/grid/snapshot_test/test.rs

@@ -1,9 +1,9 @@
 use crate::grid::field_test::util::create_text_field;
-use crate::grid::snapshot_test::script::{GridSnapshotTest, SnapshotScript::*};
+use crate::grid::snapshot_test::script::{DatabaseSnapshotTest, SnapshotScript::*};
 
 #[tokio::test]
 async fn snapshot_create_test() {
-    let mut test = GridSnapshotTest::new().await;
+    let mut test = DatabaseSnapshotTest::new().await;
     let (_, field_rev) = create_text_field(&test.grid_id());
     let scripts = vec![CreateField { field_rev }, WriteSnapshot];
     test.run_scripts(scripts).await;
@@ -19,7 +19,7 @@ async fn snapshot_create_test() {
 
 #[tokio::test]
 async fn snapshot_multi_version_test() {
-    let mut test = GridSnapshotTest::new().await;
+    let mut test = DatabaseSnapshotTest::new().await;
     let original_content = test.grid_pad().await.json_str().unwrap();
 
     // Create a field

+ 2 - 2
frontend/rust-lib/flowy-database/tests/grid/sort_test/checkbox_and_text_test.rs

@@ -1,10 +1,10 @@
-use crate::grid::sort_test::script::{GridSortTest, SortScript::*};
+use crate::grid::sort_test::script::{DatabaseSortTest, SortScript::*};
 use flowy_database::entities::FieldType;
 use grid_model::SortCondition;
 
 #[tokio::test]
 async fn sort_checkbox_and_then_text_by_descending_test() {
-    let mut test = GridSortTest::new().await;
+    let mut test = DatabaseSortTest::new().await;
     let checkbox_field = test.get_first_field_rev(FieldType::Checkbox);
     let text_field = test.get_first_field_rev(FieldType::RichText);
     let scripts = vec![

+ 2 - 2
frontend/rust-lib/flowy-database/tests/grid/sort_test/multi_sort_test.rs

@@ -1,11 +1,11 @@
-use crate::grid::sort_test::script::GridSortTest;
+use crate::grid::sort_test::script::DatabaseSortTest;
 use crate::grid::sort_test::script::SortScript::*;
 use flowy_database::entities::FieldType;
 use grid_model::SortCondition;
 
 #[tokio::test]
 async fn sort_text_with_checkbox_by_ascending_test() {
-    let mut test = GridSortTest::new().await;
+    let mut test = DatabaseSortTest::new().await;
     let text_field = test.get_first_field_rev(FieldType::RichText).clone();
     let checkbox_field = test.get_first_field_rev(FieldType::Checkbox).clone();
     let scripts = vec![

+ 8 - 8
frontend/rust-lib/flowy-database/tests/grid/sort_test/script.rs

@@ -1,4 +1,4 @@
-use crate::grid::grid_editor::GridEditorTest;
+use crate::grid::database_editor::DatabaseEditorTest;
 use async_stream::stream;
 use flowy_database::entities::{AlterSortParams, CellPathParams, DeleteSortParams};
 use flowy_database::services::sort::SortType;
@@ -36,15 +36,15 @@ pub enum SortScript {
     },
 }
 
-pub struct GridSortTest {
-    inner: GridEditorTest,
+pub struct DatabaseSortTest {
+    inner: DatabaseEditorTest,
     pub current_sort_rev: Option<SortRevision>,
     recv: Option<Receiver<GridViewChanged>>,
 }
 
-impl GridSortTest {
+impl DatabaseSortTest {
     pub async fn new() -> Self {
-        let editor_test = GridEditorTest::new_table().await;
+        let editor_test = DatabaseEditorTest::new_table().await;
         Self {
             inner: editor_test,
             current_sort_rev: None,
@@ -154,15 +154,15 @@ async fn assert_sort_changed(
         .await;
 }
 
-impl std::ops::Deref for GridSortTest {
-    type Target = GridEditorTest;
+impl std::ops::Deref for DatabaseSortTest {
+    type Target = DatabaseEditorTest;
 
     fn deref(&self) -> &Self::Target {
         &self.inner
     }
 }
 
-impl std::ops::DerefMut for GridSortTest {
+impl std::ops::DerefMut for DatabaseSortTest {
     fn deref_mut(&mut self) -> &mut Self::Target {
         &mut self.inner
     }

+ 12 - 12
frontend/rust-lib/flowy-database/tests/grid/sort_test/single_sort_test.rs

@@ -1,10 +1,10 @@
-use crate::grid::sort_test::script::{GridSortTest, SortScript::*};
+use crate::grid::sort_test::script::{DatabaseSortTest, SortScript::*};
 use flowy_database::entities::FieldType;
 use grid_model::SortCondition;
 
 #[tokio::test]
 async fn sort_text_by_ascending_test() {
-    let mut test = GridSortTest::new().await;
+    let mut test = DatabaseSortTest::new().await;
     let text_field = test.get_first_field_rev(FieldType::RichText);
     let scripts = vec![
         AssertCellContentOrder {
@@ -25,7 +25,7 @@ async fn sort_text_by_ascending_test() {
 
 #[tokio::test]
 async fn sort_change_notification_by_update_text_test() {
-    let mut test = GridSortTest::new().await;
+    let mut test = DatabaseSortTest::new().await;
     let text_field = test.get_first_field_rev(FieldType::RichText).clone();
     let scripts = vec![
         InsertSort {
@@ -57,7 +57,7 @@ async fn sort_change_notification_by_update_text_test() {
 
 #[tokio::test]
 async fn sort_text_by_ascending_and_delete_sort_test() {
-    let mut test = GridSortTest::new().await;
+    let mut test = DatabaseSortTest::new().await;
     let text_field = test.get_first_field_rev(FieldType::RichText).clone();
     let scripts = vec![InsertSort {
         field_rev: text_field.clone(),
@@ -80,7 +80,7 @@ async fn sort_text_by_ascending_and_delete_sort_test() {
 
 #[tokio::test]
 async fn sort_text_by_descending_test() {
-    let mut test = GridSortTest::new().await;
+    let mut test = DatabaseSortTest::new().await;
     let text_field = test.get_first_field_rev(FieldType::RichText);
     let scripts = vec![
         AssertCellContentOrder {
@@ -101,7 +101,7 @@ async fn sort_text_by_descending_test() {
 
 #[tokio::test]
 async fn sort_checkbox_by_ascending_test() {
-    let mut test = GridSortTest::new().await;
+    let mut test = DatabaseSortTest::new().await;
     let checkbox_field = test.get_first_field_rev(FieldType::Checkbox);
     let scripts = vec![
         AssertCellContentOrder {
@@ -118,7 +118,7 @@ async fn sort_checkbox_by_ascending_test() {
 
 #[tokio::test]
 async fn sort_checkbox_by_descending_test() {
-    let mut test = GridSortTest::new().await;
+    let mut test = DatabaseSortTest::new().await;
     let checkbox_field = test.get_first_field_rev(FieldType::Checkbox);
     let scripts = vec![
         AssertCellContentOrder {
@@ -139,7 +139,7 @@ async fn sort_checkbox_by_descending_test() {
 
 #[tokio::test]
 async fn sort_date_by_ascending_test() {
-    let mut test = GridSortTest::new().await;
+    let mut test = DatabaseSortTest::new().await;
     let date_field = test.get_first_field_rev(FieldType::DateTime);
     let scripts = vec![
         AssertCellContentOrder {
@@ -160,7 +160,7 @@ async fn sort_date_by_ascending_test() {
 
 #[tokio::test]
 async fn sort_date_by_descending_test() {
-    let mut test = GridSortTest::new().await;
+    let mut test = DatabaseSortTest::new().await;
     let date_field = test.get_first_field_rev(FieldType::DateTime);
     let scripts = vec![
         AssertCellContentOrder {
@@ -195,7 +195,7 @@ async fn sort_date_by_descending_test() {
 
 #[tokio::test]
 async fn sort_number_by_descending_test() {
-    let mut test = GridSortTest::new().await;
+    let mut test = DatabaseSortTest::new().await;
     let number_field = test.get_first_field_rev(FieldType::Number);
     let scripts = vec![
         AssertCellContentOrder {
@@ -216,7 +216,7 @@ async fn sort_number_by_descending_test() {
 
 #[tokio::test]
 async fn sort_single_select_by_descending_test() {
-    let mut test = GridSortTest::new().await;
+    let mut test = DatabaseSortTest::new().await;
     let single_select = test.get_first_field_rev(FieldType::SingleSelect);
     let scripts = vec![
         AssertCellContentOrder {
@@ -237,7 +237,7 @@ async fn sort_single_select_by_descending_test() {
 
 #[tokio::test]
 async fn sort_multi_select_by_ascending_test() {
-    let mut test = GridSortTest::new().await;
+    let mut test = DatabaseSortTest::new().await;
     let multi_select = test.get_first_field_rev(FieldType::MultiSelect);
     let scripts = vec![
         AssertCellContentOrder {

+ 0 - 2
frontend/rust-lib/flowy-error/src/ext/dispatch.rs

@@ -5,8 +5,6 @@ use std::convert::TryInto;
 impl lib_dispatch::Error for FlowyError {
     fn as_response(&self) -> AFPluginEventResponse {
         let bytes: Bytes = self.clone().try_into().unwrap();
-
-        println!("Serialize FlowyError: {:?} to event response", self);
         ResponseBuilder::Err().data(bytes).build()
     }
 }

+ 5 - 5
frontend/rust-lib/flowy-test/src/lib.rs

@@ -3,7 +3,7 @@ pub mod helper;
 
 use crate::helper::*;
 
-use flowy_core::{FlowySDK, FlowySDKConfig};
+use flowy_core::{AppFlowyCore, AppFlowyCoreConfig};
 use flowy_document::entities::DocumentVersionPB;
 use flowy_net::get_client_server_configuration;
 use flowy_user::entities::UserProfilePB;
@@ -16,11 +16,11 @@ pub mod prelude {
 
 #[derive(Clone)]
 pub struct FlowySDKTest {
-    pub inner: FlowySDK,
+    pub inner: AppFlowyCore,
 }
 
 impl std::ops::Deref for FlowySDKTest {
-    type Target = FlowySDK;
+    type Target = AppFlowyCore;
 
     fn deref(&self) -> &Self::Target {
         &self.inner
@@ -36,10 +36,10 @@ impl std::default::Default for FlowySDKTest {
 impl FlowySDKTest {
     pub fn new(document_version: DocumentVersionPB) -> Self {
         let server_config = get_client_server_configuration().unwrap();
-        let config = FlowySDKConfig::new(&root_dir(), nanoid!(6), server_config)
+        let config = AppFlowyCoreConfig::new(&root_dir(), nanoid!(6), server_config)
             .with_document_version(document_version)
             .log_filter("info", vec![]);
-        let sdk = std::thread::spawn(|| FlowySDK::new(config)).join().unwrap();
+        let sdk = std::thread::spawn(|| AppFlowyCore::new(config)).join().unwrap();
         std::mem::forget(sdk.dispatcher());
         Self { inner: sdk }
     }

+ 1 - 1
frontend/scripts/build_sdk.cmd

@@ -1,3 +1,3 @@
 echo "Start building rust sdk"
 rustup show
-cargo make --profile development-windows-x86 appflowy-sdk-dev
+cargo make --profile development-windows-x86 appflowy-core-dev

+ 3 - 3
frontend/scripts/build_sdk.sh

@@ -17,15 +17,15 @@ rustup show
 
 case "$FLOWY_DEV_ENV" in
 Linux) 
- cargo make --profile "development-linux-$(uname -m)" appflowy-sdk-dev
+ cargo make --profile "development-linux-$(uname -m)" appflowy-core-dev
  ;;
 
 macOS)
- cargo make --profile "development-mac-$(uname -m)" appflowy-sdk-dev
+ cargo make --profile "development-mac-$(uname -m)" appflowy-core-dev
  ;;
 
 Windows) 
- cargo make --profile development-windows appflowy-sdk-dev
+ cargo make --profile development-windows appflowy-core-dev
  ;;
 
 *)

+ 12 - 18
frontend/scripts/makefile/desktop.toml

@@ -1,8 +1,3 @@
-# cargo make --profile production task
-
-# Run the task with profile, e.g.
-# cargo make --profile development-mac appflowy-sdk-dev
-# cargo make --profile production-windows-x86 appflowy-sdk-dev
 
 [tasks.env_check]
 dependencies = ["echo_env", "install_flutter_protobuf"]
@@ -15,12 +10,12 @@ condition = { env_set = [
   "stable",
 ] }
 
-[tasks.appflowy-sdk-dev]
-mac_alias = "appflowy-sdk-dev-macos"
-windows_alias = "appflowy-sdk-dev-windows"
-linux_alias = "appflowy-sdk-dev-linux"
+[tasks.appflowy-core-dev]
+mac_alias = "appflowy-core-dev-macos"
+windows_alias = "appflowy-core-dev-windows"
+linux_alias = "appflowy-core-dev-linux"
 
-[tasks.appflowy-sdk-dev-android]
+[tasks.appflowy-core-dev-android]
 category = "Build"
 dependencies = ["env_check"]
 run_task = { name = [
@@ -29,7 +24,7 @@ run_task = { name = [
   "restore-crate-type",
 ] }
 
-[tasks.appflowy-sdk-dev-macos]
+[tasks.appflowy-core-dev-macos]
 category = "Build"
 dependencies = ["env_check"]
 run_task = { name = [
@@ -39,7 +34,7 @@ run_task = { name = [
   "restore-crate-type",
 ] }
 
-[tasks.appflowy-sdk-dev-windows]
+[tasks.appflowy-core-dev-windows]
 category = "Build"
 dependencies = ["env_check"]
 run_task = { name = [
@@ -49,7 +44,7 @@ run_task = { name = [
   "restore-crate-type",
 ] }
 
-[tasks.appflowy-sdk-dev-linux]
+[tasks.appflowy-core-dev-linux]
 category = "Build"
 dependencies = ["env_check"]
 run_task = { name = [
@@ -59,7 +54,6 @@ run_task = { name = [
   "restore-crate-type",
 ] }
 
-
 #
 [tasks.sdk-build]
 private = true
@@ -112,7 +106,7 @@ script = [
 script_runner = "@duckscript"
 
 #
-[tasks.appflowy-sdk-release]
+[tasks.appflowy-core-release]
 description = "Build flowy sdk in release mode"
 category = "Build"
 dependencies = ["env_check"]
@@ -144,7 +138,7 @@ linux_alias = "post-desktop-linux"
 private = true
 script = [
   """
-    echo "🚀 🚀 🚀  Flowy-SDK(macOS) build success"
+    echo "🚀 🚀 🚀  AppFlowy-Core build success"
     dart_ffi_dir= set ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/app_flowy/packages/appflowy_backend/${TARGET_OS}
     lib = set lib${LIB_NAME}.${LIB_EXT}
 
@@ -161,7 +155,7 @@ script_runner = "@duckscript"
 private = true
 script = [
   """
-    echo "🚀 🚀 🚀  Flowy-SDK(windows) build success"
+    echo "🚀 🚀 🚀  AppFlowy-Core build success"
     dart_ffi_dir= set ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/app_flowy/windows/flutter/dart_ffi
     lib = set ${LIB_NAME}.${LIB_EXT}
 
@@ -180,7 +174,7 @@ script_runner = "@duckscript"
 private = true
 script = [
   """
-    echo "🚀 🚀 🚀  Flowy-SDK(linux) build success"
+    echo "🚀 🚀 🚀  AppFlowy-Core build success"
     dart_ffi_dir= set ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/app_flowy/linux/flutter/dart_ffi
     lib = set lib${LIB_NAME}.${LIB_EXT}
 

+ 56 - 33
frontend/scripts/makefile/flutter.toml

@@ -4,17 +4,33 @@ windows_alias = "appflowy-windows"
 linux_alias = "appflowy-linux"
 
 [tasks.appflowy-macos]
-dependencies = ["appflowy-sdk-release"]
-run_task = { name = ["code_generation", "set-app-version", "flutter-build", "copy-to-product"] }
+dependencies = ["appflowy-core-release"]
+run_task = { name = [
+  "code_generation",
+  "set-app-version",
+  "flutter-build",
+  "copy-to-product",
+] }
 script_runner = "@shell"
 
 [tasks.appflowy-windows]
-dependencies = ["appflowy-sdk-release"]
-run_task = { name = ["code_generation", "set-app-version", "flutter-build", "copy-to-product"] }
+dependencies = ["appflowy-core-release"]
+run_task = { name = [
+  "code_generation",
+  "set-app-version",
+  "flutter-build",
+  "copy-to-product",
+] }
 
 [tasks.appflowy-linux]
-dependencies = ["appflowy-sdk-release"]
-run_task = { name = ["code_generation", "set-app-version", "flutter-build", "copy-to-product", "create-release-archive"] }
+dependencies = ["appflowy-core-release"]
+run_task = { name = [
+  "code_generation",
+  "set-app-version",
+  "flutter-build",
+  "copy-to-product",
+  "create-release-archive",
+] }
 script_runner = "@shell"
 
 [tasks.appflowy-dev]
@@ -23,17 +39,32 @@ windows_alias = "appflowy-windows-dev"
 linux_alias = "appflowy-linux-dev"
 
 [tasks.appflowy-macos-dev]
-dependencies = ["appflowy-sdk-dev"]
-run_task = { name = ["code_generation", "set-app-version", "flutter-build", "copy-to-product"] }
+dependencies = ["appflowy-core-dev"]
+run_task = { name = [
+  "code_generation",
+  "set-app-version",
+  "flutter-build",
+  "copy-to-product",
+] }
 script_runner = "@shell"
 
 [tasks.appflowy-windows-dev]
-dependencies = ["appflowy-sdk-dev"]
-run_task = { name = ["code_generation", "set-app-version", "flutter-build", "copy-to-product"] }
+dependencies = ["appflowy-core-dev"]
+run_task = { name = [
+  "code_generation",
+  "set-app-version",
+  "flutter-build",
+  "copy-to-product",
+] }
 
 [tasks.appflowy-linux-dev]
-dependencies = ["appflowy-sdk-dev"]
-run_task = { name = ["code_generation", "set-app-version", "flutter-build", "copy-to-product"] }
+dependencies = ["appflowy-core-dev"]
+run_task = { name = [
+  "code_generation",
+  "set-app-version",
+  "flutter-build",
+  "copy-to-product",
+] }
 script_runner = "@shell"
 
 [tasks.copy-to-product]
@@ -96,15 +127,13 @@ script = [
 script_runner = "@duckscript"
 
 [tasks.set-app-version]
-script = [
-  """
+script = ["""
   if is_empty ${APP_VERSION}
     APP_VERSION = set ${CURRENT_APP_VERSION}
     set_env APP_VERSION ${CURRENT_APP_VERSION}
   end
   echo APP_VERSION: ${APP_VERSION}
-  """,
-]
+  """]
 script_runner = "@duckscript"
 
 # The following tasks will create an archive that will be used on the GitHub Releases section
@@ -118,7 +147,7 @@ linux_alias = "create-release-archive-linux"
 [tasks.create-release-archive-linux]
 script = [
   "cd ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/app_flowy/product/${APP_VERSION}/${TARGET_OS}/Release",
-  "tar -czf ${PRODUCT_NAME}-${TARGET_OS}-x86.tar.gz *"
+  "tar -czf ${PRODUCT_NAME}-${TARGET_OS}-x86.tar.gz *",
 ]
 
 [tasks.create-release-archive-windows]
@@ -136,36 +165,30 @@ script = [
 ]
 
 [tasks.flutter-build]
-script = [
-  """
+script = ["""
   cd app_flowy/
   flutter clean
   flutter pub get
   flutter build ${TARGET_OS} --${BUILD_FLAG} --build-name=${APP_VERSION}
-  """,
-]
+  """]
 script_runner = "@shell"
 
 [tasks.flutter-build.linux]
-script = [
-  """
+script = ["""
   cd app_flowy/
   flutter clean
   flutter pub get
   flutter build ${TARGET_OS} --${BUILD_FLAG}
-  """,
-]
+  """]
 script_runner = "@shell"
 
 [tasks.flutter-build.windows]
-script = [
-  """
+script = ["""
   cd app_flowy
   exec cmd.exe /c flutter clean
   exec cmd.exe /c flutter pub get
   exec cmd.exe /c flutter build ${TARGET_OS} --${BUILD_FLAG}
-  """,
-]
+  """]
 script_runner = "@duckscript"
 
 [tasks.code_generation]
@@ -177,7 +200,7 @@ script = [
   flutter packages pub get
   flutter packages pub run easy_localization:generate -S assets/translations/ -f keys -o locale_keys.g.dart -S assets/translations -s en.json
   flutter packages pub run build_runner build --delete-conflicting-outputs
-  """
+  """,
 ]
 
 [tasks.code_generation.windows]
@@ -189,7 +212,7 @@ script = [
   exec cmd.exe /c flutter packages pub get
   exec cmd.exe /c flutter packages pub run easy_localization:generate -S assets/translations/ -f keys -o locale_keys.g.dart -S assets/translations -s en.json
   exec cmd.exe /c flutter packages pub run build_runner build --delete-conflicting-outputs
-  """
+  """,
 ]
 
 [tasks.dry_code_generation]
@@ -199,7 +222,7 @@ script = [
   cd app_flowy
   flutter packages pub run easy_localization:generate -S assets/translations/ -f keys -o locale_keys.g.dart -S assets/translations -s en.json
   flutter packages pub run build_runner build --delete-conflicting-outputs
-  """
+  """,
 ]
 
 [tasks.dry_code_generation.windows]
@@ -209,5 +232,5 @@ script = [
   cd ./app_flowy/
   exec cmd.exe /c flutter packages pub run easy_localization:generate -S assets/translations/ -f keys -o locale_keys.g.dart -S assets/translations -s en.json
   exec cmd.exe /c flutter packages pub run build_runner build --delete-conflicting-outputs
-  """
+  """,
 ]

+ 1 - 0
frontend/scripts/makefile/tauri.toml

@@ -7,6 +7,7 @@ script = ["""
 script_runner = "@shell"
 
 [tasks.tauri_dev]
+env = { RUST_LOG = "debug" }
 script = ["""
     cd appflowy_tauri
     npm run tauri dev

+ 24 - 31
frontend/scripts/makefile/tests.toml

@@ -18,6 +18,7 @@ cargo make --profile test-linux dart_unit_test_inner
 script_runner = "@shell"
 
 [tasks.dart_unit_test_inner]
+env = { RUST_LOG = "info" }
 dependencies = ["build-test-lib"]
 description = "Run flutter unit tests"
 script = '''
@@ -29,6 +30,7 @@ flutter test --dart-define=RUST_LOG=${TEST_RUST_LOG} --concurrency=1
 run_task = { name = ["rust_lib_unit_test", "shared_lib_unit_test"] }
 
 [tasks.rust_lib_unit_test]
+env = { RUST_LOG = "info" }
 description = "Run rust-lib unit tests"
 script = '''
 cd rust-lib
@@ -36,6 +38,7 @@ cargo test --no-default-features --features="sync, rev-sqlite"
 '''
 
 [tasks.shared_lib_unit_test]
+env = { RUST_LOG = "info" }
 description = "Run shared-lib unit test"
 script = '''
 cd ../shared-lib
@@ -62,8 +65,7 @@ fi
 [tasks.clean_profraw_files]
 description = "Cleans profraw files that are created by `cargo test`"
 script_runner = "@duckscript"
-script = [
-  """
+script = ["""
   rust_lib_profs = glob_array ./rust-lib/**/*.profraw
   for prof in ${rust_lib_profs}
     full_path = canonicalize ${prof}
@@ -76,14 +78,12 @@ script = [
     rm ${full_path}
   end
 
-  """
-]
+  """]
 
 [tasks.run_rustlib_coverage_tests]
 description = "Run tests with coverage instrumentation"
 script_runner = "@shell"
-script = [
-  """
+script = ["""
   echo --- Running coverage tests ---
   cd rust-lib/
 
@@ -91,14 +91,12 @@ script = [
   RUSTFLAGS='-C instrument-coverage' \
   LLVM_PROFILE_FILE='prof-%p-%m.profraw' \
   cargo test --no-default-features --features="sync,rev-sqlite"
-  """
-]
+  """]
 
 [tasks.run_sharedlib_coverage_tests]
 description = "Run tests with coverage instrumentation"
 script_runner = "@shell"
-script = [
-  """
+script = ["""
   echo --- Running coverage tests ---
   cd ../shared-lib
 
@@ -107,8 +105,7 @@ script = [
   LLVM_PROFILE_FILE='prof-%p-%m.profraw' \
   cargo test --no-default-features
 
-  """
-]
+  """]
 
 [tasks.get_rustlib_grcov_report]
 description = "Get `grcov` HTML report for test coverage for rust-lib"
@@ -128,7 +125,7 @@ script = [
   --output-path target/coverage-html
 
   echo "--- Done! Generated HTML report under 'target/coverage-html' for rustlib."
-  """
+  """,
 ]
 
 [tasks.get_sharedlib_grcov_report]
@@ -149,21 +146,20 @@ script = [
   --output-path target/coverage-html
 
   echo "--- Done! Generated HTML report under 'target/coverage-html' for sharedlib."
-  """
+  """,
 ]
 
 [tasks.get_grcov_report]
 description = "Get `grcov` HTML report for test coverage"
 run_task = { name = [
   "get_rustlib_grcov_report",
-  "get_sharedlib_grcov_report"
+  "get_sharedlib_grcov_report",
 ], parallel = true }
 
 [tasks.get_sharedlib_lcov_report]
 description = "Generates `lcov` report for `shared-lib`"
 script_runner = "@shell"
-script = [
-  """
+script = ["""
   echo Getting 'lcov' results for 'shared-lib'
 
   cd ../shared-lib
@@ -178,14 +174,12 @@ script = [
   --output-path target/coverage.lcov
 
   echo "--- Done! Generated 'target/coverage.lcov' sharedlib."
-  """
-]
+  """]
 
 [tasks.get_rustlib_lcov_report]
 description = "Generates `lcov` report for `rust-lib`"
 script_runner = "@shell"
-script = [
-  """
+script = ["""
   echo Getting 'lcov' results for 'rust-lib'
 
   cd rust-lib/
@@ -200,23 +194,22 @@ script = [
   --output-path target/coverage.lcov
 
   echo "--- Done! Generated 'target/coverage.lcov' for rustlib."
-  """
-]
+  """]
 
 [tasks.get_lcov_report]
 description = "Get `lcov` reports for test coverage"
 run_task = { name = [
   "get_sharedlib_lcov_report",
-  "get_rustlib_lcov_report"
+  "get_rustlib_lcov_report",
 ], parallel = true }
 
 [tasks.rust_unit_test_with_coverage]
 description = "Run rust unit test with code coverage"
 run_task = { name = [
-    "check_grcov",
-    'appflowy-flutter-deps-tools',
-    "run_rustlib_coverage_tests",
-    "run_sharedlib_coverage_tests",
-    "get_lcov_report",
-    "clean_profraw_files"
-  ]}
+  "check_grcov",
+  'appflowy-flutter-deps-tools',
+  "run_rustlib_coverage_tests",
+  "run_sharedlib_coverage_tests",
+  "get_lcov_report",
+  "clean_profraw_files",
+] }

+ 1 - 1
shared-lib/lib-infra/Cargo.toml

@@ -10,7 +10,7 @@ chrono = "0.4.19"
 bytes = { version = "1.0" }
 pin-project = "1.0.12"
 futures-core = { version = "0.3" }
-tokio = { version = "1.0", features = ["time", "rt"] }
+tokio = { version = "1", features = ["time", "rt"] }
 rand = "0.8.5"
 async-trait = "0.1.59"
 md5 = "0.7.0"