Forráskód Böngészése

test: add databaase event test (#2728)

* test: add tests and fix modify primary field bug

* test: add more test

* fix: tauri buiuld

* chore: disable share link button
Nathan.fooo 1 éve
szülő
commit
ce8cee5637
20 módosított fájl, 623 hozzáadás és 343 törlés
  1. 4 2
      frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_data_persistence.dart
  2. 0 5
      frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_service.dart
  3. 17 1
      frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_cell_action_sheet.dart
  4. 1 0
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/bloc/checkbox_card_cell_bloc.dart
  5. 8 9
      frontend/appflowy_flutter/lib/plugins/document/presentation/share/share_button.dart
  6. 0 57
      frontend/appflowy_flutter/test/bloc_test/grid_test/filter/edit_filter_field_test.dart
  7. 20 11
      frontend/appflowy_tauri/src-tauri/Cargo.lock
  8. 1 11
      frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/field/field_bd_svc.ts
  9. 1 0
      frontend/rust-lib/Cargo.lock
  10. 0 182
      frontend/rust-lib/flowy-database/src/services/export.rs
  11. 6 14
      frontend/rust-lib/flowy-database2/src/entities/field_entities.rs
  12. 1 1
      frontend/rust-lib/flowy-database2/src/event_handler.rs
  13. 53 16
      frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs
  14. 3 1
      frontend/rust-lib/flowy-database2/src/services/field/type_options/checkbox_type_option/checkbox_type_option.rs
  15. 0 24
      frontend/rust-lib/flowy-database2/tests/database/field_test/test.rs
  16. 9 5
      frontend/rust-lib/flowy-folder2/src/event_handler.rs
  17. 1 1
      frontend/rust-lib/flowy-folder2/src/event_map.rs
  18. 1 0
      frontend/rust-lib/flowy-test/Cargo.toml
  19. 207 3
      frontend/rust-lib/flowy-test/src/lib.rs
  20. 290 0
      frontend/rust-lib/flowy-test/tests/database/test.rs

+ 4 - 2
frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_data_persistence.dart

@@ -16,8 +16,10 @@ class TextCellDataPersistence implements CellDataPersistence<String> {
 
   @override
   Future<Option<FlowyError>> save(String data) async {
-    final fut =
-        _cellBackendSvc.updateCell(cellContext: cellContext, data: data);
+    final fut = _cellBackendSvc.updateCell(
+      cellContext: cellContext,
+      data: data,
+    );
     return fut.then((result) {
       return result.fold(
         (l) => none(),

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

@@ -29,7 +29,6 @@ class FieldBackendService {
 
   Future<Either<Unit, FlowyError>> updateField({
     String? name,
-    FieldType? fieldType,
     bool? frozen,
     bool? visibility,
     double? width,
@@ -42,10 +41,6 @@ class FieldBackendService {
       payload.name = name;
     }
 
-    if (fieldType != null) {
-      payload.fieldType = fieldType;
-    }
-
     if (frozen != null) {
       payload.frozen = frozen;
     }

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

@@ -124,13 +124,29 @@ class _FieldOperationList extends StatelessWidget {
   }
 
   Widget _actionCell(FieldAction action) {
+    bool enable = true;
+
+    // If the field is primary, delete and duplicate are disabled.
+    if (fieldInfo.field.isPrimary) {
+      switch (action) {
+        case FieldAction.hide:
+          break;
+        case FieldAction.duplicate:
+          enable = false;
+          break;
+        case FieldAction.delete:
+          enable = false;
+          break;
+      }
+    }
+
     return Flexible(
       child: SizedBox(
         height: GridSize.popoverItemHeight,
         child: FieldActionCell(
           fieldInfo: fieldInfo,
           action: action,
-          enable: action != FieldAction.delete || !fieldInfo.field.isPrimary,
+          enable: enable,
         ),
       ),
     );

+ 1 - 0
frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/bloc/checkbox_card_cell_bloc.dart

@@ -72,5 +72,6 @@ class CheckboxCardCellState with _$CheckboxCardCellState {
 }
 
 bool _isSelected(String? cellData) {
+  // The backend use "Yes" and "No" to represent the checkbox cell data.
   return cellData == "Yes";
 }

+ 8 - 9
frontend/appflowy_flutter/lib/plugins/document/presentation/share/share_button.dart

@@ -4,7 +4,6 @@ import 'package:appflowy/generated/locale_keys.g.dart';
 import 'package:appflowy/startup/startup.dart';
 import 'package:appflowy/plugins/document/application/share_bloc.dart';
 import 'package:appflowy/workspace/presentation/home/toast.dart';
-import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
 import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
 import 'package:appflowy_backend/protobuf/flowy-document2/entities.pb.dart';
 import 'package:appflowy_popover/appflowy_popover.dart';
@@ -104,11 +103,11 @@ class ShareActionList extends StatelessWidget {
               showMessageToast('Exported to: $exportPath');
             }
             break;
-          case ShareAction.copyLink:
-            NavigatorAlertDialog(
-              title: LocaleKeys.shareAction_workInProgress.tr(),
-            ).show(context);
-            break;
+          // case ShareAction.copyLink:
+          //   NavigatorAlertDialog(
+          //     title: LocaleKeys.shareAction_workInProgress.tr(),
+          //   ).show(context);
+          //   break;
         }
         controller.close();
       },
@@ -118,7 +117,7 @@ class ShareActionList extends StatelessWidget {
 
 enum ShareAction {
   markdown,
-  copyLink,
+  // copyLink,
 }
 
 class ShareActionWrapper extends ActionCell {
@@ -133,8 +132,8 @@ class ShareActionWrapper extends ActionCell {
     switch (inner) {
       case ShareAction.markdown:
         return LocaleKeys.shareAction_markdown.tr();
-      case ShareAction.copyLink:
-        return LocaleKeys.shareAction_copyLink.tr();
+      // case ShareAction.copyLink:
+      //   return LocaleKeys.shareAction_copyLink.tr();
     }
   }
 }

+ 0 - 57
frontend/appflowy_flutter/test/bloc_test/grid_test/filter/edit_filter_field_test.dart

@@ -1,57 +0,0 @@
-import 'package:appflowy/plugins/database_view/application/field/field_editor_bloc.dart';
-import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart';
-import 'package:appflowy/plugins/database_view/application/filter/filter_service.dart';
-import 'package:appflowy/plugins/database_view/grid/application/filter/filter_menu_bloc.dart';
-import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
-import 'package:appflowy_backend/protobuf/flowy-database2/text_filter.pb.dart';
-import 'package:flutter_test/flutter_test.dart';
-
-import '../util.dart';
-
-void main() {
-  late AppFlowyGridTest gridTest;
-  setUpAll(() async {
-    gridTest = await AppFlowyGridTest.ensureInitialized();
-  });
-
-  test("create a text filter and then alter the filter's field)", () async {
-    final context = await gridTest.createTestGrid();
-    final service = FilterBackendService(viewId: context.gridView.id);
-    final textField = context.textFieldContext();
-
-    // Create the filter menu bloc
-    final menuBloc = GridFilterMenuBloc(
-      fieldController: context.fieldController,
-      viewId: context.gridView.id,
-    )..add(const GridFilterMenuEvent.initial());
-
-    // Insert filter for the text field
-    await service.insertTextFilter(
-      fieldId: textField.id,
-      condition: TextFilterConditionPB.TextIsEmpty,
-      content: "",
-    );
-    await gridResponseFuture();
-    assert(menuBloc.state.filters.length == 1);
-
-    // Edit the text field
-    final loader = FieldTypeOptionLoader(
-      viewId: context.gridView.id,
-      field: textField.field,
-    );
-
-    final editorBloc = FieldEditorBloc(
-      isGroupField: false,
-      loader: loader,
-      field: textField.field,
-    )..add(const FieldEditorEvent.initial());
-    await gridResponseFuture();
-
-    // Alter the field type to Number
-    editorBloc.add(const FieldEditorEvent.switchToField(FieldType.Number));
-    await gridResponseFuture();
-
-    // Check the number of filters
-    assert(menuBloc.state.filters.isEmpty);
-  });
-}

+ 20 - 11
frontend/appflowy_tauri/src-tauri/Cargo.lock

@@ -99,7 +99,7 @@ checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8"
 [[package]]
 name = "appflowy-integrate"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cbc2e0#cbc2e0acb8420dc997921bb3f56b99f9975c2aab"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a647d9#a647d922ef432510d6be0abb5f968d9a75dc7011"
 dependencies = [
  "anyhow",
  "collab",
@@ -1024,7 +1024,7 @@ dependencies = [
 [[package]]
 name = "collab"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cbc2e0#cbc2e0acb8420dc997921bb3f56b99f9975c2aab"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a647d9#a647d922ef432510d6be0abb5f968d9a75dc7011"
 dependencies = [
  "anyhow",
  "bytes",
@@ -1042,7 +1042,7 @@ dependencies = [
 [[package]]
 name = "collab-client-ws"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cbc2e0#cbc2e0acb8420dc997921bb3f56b99f9975c2aab"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a647d9#a647d922ef432510d6be0abb5f968d9a75dc7011"
 dependencies = [
  "bytes",
  "collab-sync",
@@ -1060,7 +1060,7 @@ dependencies = [
 [[package]]
 name = "collab-database"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cbc2e0#cbc2e0acb8420dc997921bb3f56b99f9975c2aab"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a647d9#a647d922ef432510d6be0abb5f968d9a75dc7011"
 dependencies = [
  "anyhow",
  "async-trait",
@@ -1086,7 +1086,7 @@ dependencies = [
 [[package]]
 name = "collab-derive"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cbc2e0#cbc2e0acb8420dc997921bb3f56b99f9975c2aab"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a647d9#a647d922ef432510d6be0abb5f968d9a75dc7011"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -1098,7 +1098,7 @@ dependencies = [
 [[package]]
 name = "collab-document"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cbc2e0#cbc2e0acb8420dc997921bb3f56b99f9975c2aab"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a647d9#a647d922ef432510d6be0abb5f968d9a75dc7011"
 dependencies = [
  "anyhow",
  "collab",
@@ -1115,7 +1115,7 @@ dependencies = [
 [[package]]
 name = "collab-folder"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cbc2e0#cbc2e0acb8420dc997921bb3f56b99f9975c2aab"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a647d9#a647d922ef432510d6be0abb5f968d9a75dc7011"
 dependencies = [
  "anyhow",
  "collab",
@@ -1134,7 +1134,7 @@ dependencies = [
 [[package]]
 name = "collab-persistence"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cbc2e0#cbc2e0acb8420dc997921bb3f56b99f9975c2aab"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a647d9#a647d922ef432510d6be0abb5f968d9a75dc7011"
 dependencies = [
  "bincode",
  "chrono",
@@ -1154,7 +1154,7 @@ dependencies = [
 [[package]]
 name = "collab-plugins"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cbc2e0#cbc2e0acb8420dc997921bb3f56b99f9975c2aab"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a647d9#a647d922ef432510d6be0abb5f968d9a75dc7011"
 dependencies = [
  "anyhow",
  "async-trait",
@@ -1173,6 +1173,7 @@ dependencies = [
  "rusoto_credential",
  "serde",
  "serde_json",
+ "similar 2.2.1",
  "thiserror",
  "tokio",
  "tokio-retry",
@@ -1184,7 +1185,7 @@ dependencies = [
 [[package]]
 name = "collab-sync"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cbc2e0#cbc2e0acb8420dc997921bb3f56b99f9975c2aab"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a647d9#a647d922ef432510d6be0abb5f968d9a75dc7011"
 dependencies = [
  "bytes",
  "collab",
@@ -1789,7 +1790,7 @@ dependencies = [
  "quote",
  "serde",
  "serde_json",
- "similar",
+ "similar 1.3.0",
  "syn 1.0.109",
  "tera",
  "toml 0.5.11",
@@ -1818,6 +1819,7 @@ version = "0.1.0"
 dependencies = [
  "appflowy-integrate",
  "bytes",
+ "diesel",
  "flowy-config",
  "flowy-database2",
  "flowy-document2",
@@ -1839,6 +1841,7 @@ dependencies = [
  "serde_repr",
  "tokio",
  "tracing",
+ "uuid",
 ]
 
 [[package]]
@@ -5055,6 +5058,12 @@ version = "1.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1ad1d488a557b235fc46dae55512ffbfc429d2482b08b4d9435ab07384ca8aec"
 
+[[package]]
+name = "similar"
+version = "2.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "420acb44afdae038210c99e69aae24109f32f15500aa708e81d46c9f29d55fcf"
+
 [[package]]
 name = "siphasher"
 version = "0.3.10"

+ 1 - 11
frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/field/field_bd_svc.ts

@@ -21,23 +21,13 @@ export abstract class TypeOptionParser<T> {
 export class FieldBackendService {
   constructor(public readonly viewId: string, public readonly fieldId: string) {}
 
-  updateField = (data: {
-    name?: string;
-    fieldType?: FieldType;
-    frozen?: boolean;
-    visibility?: boolean;
-    width?: number;
-  }) => {
+  updateField = (data: { name?: string; frozen?: boolean; visibility?: boolean; width?: number }) => {
     const payload = FieldChangesetPB.fromObject({ view_id: this.viewId, field_id: this.fieldId });
 
     if (data.name !== undefined) {
       payload.name = data.name;
     }
 
-    if (data.fieldType !== undefined) {
-      payload.field_type = data.fieldType;
-    }
-
     if (data.frozen !== undefined) {
       payload.frozen = data.frozen;
     }

+ 1 - 0
frontend/rust-lib/Cargo.lock

@@ -1864,6 +1864,7 @@ dependencies = [
  "bytes",
  "dotenv",
  "flowy-core",
+ "flowy-database2",
  "flowy-folder2",
  "flowy-net",
  "flowy-notification",

+ 0 - 182
frontend/rust-lib/flowy-database/src/services/export.rs

@@ -1,182 +0,0 @@
-use crate::entities::FieldType;
-
-use crate::services::cell::TypeCellData;
-use crate::services::database::DatabaseEditor;
-use crate::services::field::{
-  CheckboxTypeOptionPB, ChecklistTypeOptionPB, DateCellData, DateTypeOptionPB,
-  MultiSelectTypeOptionPB, NumberTypeOptionPB, RichTextTypeOptionPB, SingleSelectTypeOptionPB,
-  URLCellData,
-};
-use database_model::{FieldRevision, TypeOptionDataDeserializer};
-use flowy_error::{FlowyError, FlowyResult};
-use indexmap::IndexMap;
-use serde::Serialize;
-use serde_json::{json, Map, Value};
-use std::collections::HashMap;
-
-use std::sync::Arc;
-
-#[derive(Debug, Clone, Serialize)]
-pub struct ExportField {
-  pub id: String,
-  pub name: String,
-  pub field_type: i64,
-  pub visibility: bool,
-  pub width: i64,
-  pub type_options: HashMap<String, Value>,
-  pub is_primary: bool,
-}
-
-#[derive(Debug, Clone, Serialize)]
-struct ExportCell {
-  data: String,
-  field_type: FieldType,
-}
-
-impl From<&Arc<FieldRevision>> for ExportField {
-  fn from(field_rev: &Arc<FieldRevision>) -> Self {
-    let field_type = FieldType::from(field_rev.ty);
-    let mut type_options: HashMap<String, Value> = HashMap::new();
-
-    field_rev
-      .type_options
-      .iter()
-      .filter(|(k, _)| k == &&field_rev.ty.to_string())
-      .for_each(|(k, s)| {
-        let value = match field_type {
-          FieldType::RichText => {
-            let pb = RichTextTypeOptionPB::from_json_str(s);
-            serde_json::to_value(pb).unwrap()
-          },
-          FieldType::Number => {
-            let pb = NumberTypeOptionPB::from_json_str(s);
-            let mut map = Map::new();
-            map.insert("format".to_string(), json!(pb.format as u8));
-            map.insert("scale".to_string(), json!(pb.scale));
-            map.insert("symbol".to_string(), json!(pb.symbol));
-            map.insert("name".to_string(), json!(pb.name));
-            Value::Object(map)
-          },
-          FieldType::DateTime => {
-            let pb = DateTypeOptionPB::from_json_str(s);
-            let mut map = Map::new();
-            map.insert("date_format".to_string(), json!(pb.date_format as u8));
-            map.insert("time_format".to_string(), json!(pb.time_format as u8));
-            map.insert("field_type".to_string(), json!(FieldType::DateTime as u8));
-            Value::Object(map)
-          },
-          FieldType::SingleSelect => {
-            let pb = SingleSelectTypeOptionPB::from_json_str(s);
-            let value = serde_json::to_string(&pb).unwrap();
-            let mut map = Map::new();
-            map.insert("content".to_string(), Value::String(value));
-            Value::Object(map)
-          },
-          FieldType::MultiSelect => {
-            let pb = MultiSelectTypeOptionPB::from_json_str(s);
-            let value = serde_json::to_string(&pb).unwrap();
-            let mut map = Map::new();
-            map.insert("content".to_string(), Value::String(value));
-            Value::Object(map)
-          },
-          FieldType::Checkbox => {
-            let pb = CheckboxTypeOptionPB::from_json_str(s);
-            serde_json::to_value(pb).unwrap()
-          },
-          FieldType::URL => {
-            let pb = RichTextTypeOptionPB::from_json_str(s);
-            serde_json::to_value(pb).unwrap()
-          },
-          FieldType::Checklist => {
-            let pb = ChecklistTypeOptionPB::from_json_str(s);
-            let value = serde_json::to_string(&pb).unwrap();
-            let mut map = Map::new();
-            map.insert("content".to_string(), Value::String(value));
-            Value::Object(map)
-          },
-        };
-        type_options.insert(k.clone(), value);
-      });
-    Self {
-      id: field_rev.id.clone(),
-      name: field_rev.name.clone(),
-      field_type: field_rev.ty as i64,
-      visibility: true,
-      width: 100,
-      type_options,
-      is_primary: field_rev.is_primary,
-    }
-  }
-}
-
-pub struct CSVExport;
-impl CSVExport {
-  pub async fn export_database(
-    &self,
-    view_id: &str,
-    database_editor: &Arc<DatabaseEditor>,
-  ) -> FlowyResult<String> {
-    let mut wtr = csv::Writer::from_writer(vec![]);
-    let row_revs = database_editor.get_all_row_revs(view_id).await?;
-    let field_revs = database_editor.get_field_revs(None).await?;
-
-    // Write fields
-    let field_records = field_revs
-      .iter()
-      .map(|field| ExportField::from(field))
-      .map(|field| serde_json::to_string(&field).unwrap())
-      .collect::<Vec<String>>();
-
-    wtr
-      .write_record(&field_records)
-      .map_err(|e| FlowyError::internal().context(e))?;
-
-    // Write rows
-    let mut field_by_field_id = IndexMap::new();
-    field_revs.into_iter().for_each(|field| {
-      field_by_field_id.insert(field.id.clone(), field);
-    });
-    for row_rev in row_revs {
-      let cells = field_by_field_id
-        .iter()
-        .map(|(field_id, field)| {
-          let field_type = FieldType::from(field.ty);
-          let data = row_rev
-            .cells
-            .get(field_id)
-            .map(|cell| TypeCellData::try_from(cell))
-            .map(|data| {
-              data
-                .map(|data| match field_type {
-                  FieldType::DateTime => {
-                    match serde_json::from_str::<DateCellData>(&data.cell_str) {
-                      Ok(cell_data) => cell_data.timestamp.unwrap_or_default().to_string(),
-                      Err(_) => "".to_string(),
-                    }
-                  },
-                  FieldType::URL => match serde_json::from_str::<URLCellData>(&data.cell_str) {
-                    Ok(cell_data) => cell_data.content,
-                    Err(_) => "".to_string(),
-                  },
-                  _ => data.cell_str,
-                })
-                .unwrap_or_default()
-            })
-            .unwrap_or_else(|| "".to_string());
-          let cell = ExportCell { data, field_type };
-          serde_json::to_string(&cell).unwrap()
-        })
-        .collect::<Vec<_>>();
-
-      if let Err(e) = wtr.write_record(&cells) {
-        tracing::warn!("CSV failed to write record: {}", e);
-      }
-    }
-
-    let data = wtr
-      .into_inner()
-      .map_err(|e| FlowyError::internal().context(e))?;
-    let csv = String::from_utf8(data).map_err(|e| FlowyError::internal().context(e))?;
-    Ok(csv)
-  }
-}

+ 6 - 14
frontend/rust-lib/flowy-database2/src/entities/field_entities.rs

@@ -155,6 +155,8 @@ pub struct CreateFieldPayloadPB {
   #[pb(index = 2)]
   pub field_type: FieldType,
 
+  /// If the type_option_data is not empty, it will be used to create the field.
+  /// Otherwise, the default value will be used.
   #[pb(index = 3, one_of)]
   pub type_option_data: Option<Vec<u8>>,
 }
@@ -163,6 +165,8 @@ pub struct CreateFieldPayloadPB {
 pub struct CreateFieldParams {
   pub view_id: String,
   pub field_type: FieldType,
+  /// If the type_option_data is not empty, it will be used to create the field.
+  /// Otherwise, the default value will be used.
   pub type_option_data: Option<Vec<u8>>,
 }
 
@@ -189,9 +193,6 @@ pub struct UpdateFieldTypePayloadPB {
 
   #[pb(index = 3)]
   pub field_type: FieldType,
-
-  #[pb(index = 4)]
-  pub create_if_not_exist: bool,
 }
 
 pub struct EditFieldParams {
@@ -401,18 +402,13 @@ pub struct FieldChangesetPB {
   pub desc: Option<String>,
 
   #[pb(index = 5, one_of)]
-  pub field_type: Option<FieldType>,
-
-  #[pb(index = 6, one_of)]
   pub frozen: Option<bool>,
 
-  #[pb(index = 7, one_of)]
+  #[pb(index = 6, one_of)]
   pub visibility: Option<bool>,
 
-  #[pb(index = 8, one_of)]
+  #[pb(index = 7, one_of)]
   pub width: Option<i32>,
-  // #[pb(index = 9, one_of)]
-  // pub type_option_data: Option<Vec<u8>>,
 }
 
 impl TryInto<FieldChangesetParams> for FieldChangesetPB {
@@ -421,7 +417,6 @@ impl TryInto<FieldChangesetParams> for FieldChangesetPB {
   fn try_into(self) -> Result<FieldChangesetParams, Self::Error> {
     let view_id = NotEmptyStr::parse(self.view_id).map_err(|_| ErrorCode::DatabaseIdIsEmpty)?;
     let field_id = NotEmptyStr::parse(self.field_id).map_err(|_| ErrorCode::FieldIdIsEmpty)?;
-    let field_type = self.field_type.map(FieldType::from);
     // if let Some(type_option_data) = self.type_option_data.as_ref() {
     //     if type_option_data.is_empty() {
     //         return Err(ErrorCode::TypeOptionDataIsEmpty);
@@ -433,7 +428,6 @@ impl TryInto<FieldChangesetParams> for FieldChangesetPB {
       view_id: view_id.0,
       name: self.name,
       desc: self.desc,
-      field_type,
       frozen: self.frozen,
       visibility: self.visibility,
       width: self.width,
@@ -452,8 +446,6 @@ pub struct FieldChangesetParams {
 
   pub desc: Option<String>,
 
-  pub field_type: Option<FieldType>,
-
   pub frozen: Option<bool>,
 
   pub visibility: Option<bool>,

+ 1 - 1
frontend/rust-lib/flowy-database2/src/event_handler.rs

@@ -249,7 +249,7 @@ pub(crate) async fn get_field_type_option_data_handler(
   }
 }
 
-/// Create FieldMeta and save it. Return the FieldTypeOptionData.
+/// Create TypeOptionPB and save it. Return the FieldTypeOptionData.
 #[tracing::instrument(level = "trace", skip(data, manager), err)]
 pub(crate) async fn create_field_type_option_data_handler(
   data: AFPluginData<CreateFieldPayloadPB>,

+ 53 - 16
frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs

@@ -10,7 +10,7 @@ use collab_database::views::{DatabaseLayout, DatabaseView, LayoutSetting};
 use parking_lot::Mutex;
 use tokio::sync::{broadcast, RwLock};
 
-use flowy_error::{internal_error, FlowyError, FlowyResult};
+use flowy_error::{internal_error, ErrorCode, FlowyError, FlowyResult};
 use flowy_task::TaskDispatcher;
 use lib_infra::future::{to_fut, Fut};
 
@@ -199,32 +199,32 @@ impl DatabaseEditor {
     }
   }
 
+  /// Returns a list of fields of the view.
+  /// If `field_ids` is not provided, all the fields will be returned in the order of the field that
+  /// defined in the view. Otherwise, the fields will be returned in the order of the `field_ids`.
   pub fn get_fields(&self, view_id: &str, field_ids: Option<Vec<String>>) -> Vec<Field> {
-    self.database.lock().get_fields(view_id, field_ids)
+    let database = self.database.lock();
+    let field_ids = field_ids.unwrap_or_else(|| {
+      database
+        .fields
+        .get_all_field_orders()
+        .into_iter()
+        .map(|field| field.id)
+        .collect()
+    });
+    database.get_fields(view_id, Some(field_ids))
   }
 
   pub async fn update_field(&self, params: FieldChangesetParams) -> FlowyResult<()> {
-    let is_primary = self
-      .database
-      .lock()
-      .fields
-      .get_field(&params.field_id)
-      .map(|field| field.is_primary)
-      .unwrap_or(false);
     self
       .database
       .lock()
       .fields
-      .update_field(&params.field_id, |mut update| {
-        update = update
+      .update_field(&params.field_id, |update| {
+        update
           .set_name_if_not_none(params.name)
           .set_width_at_if_not_none(params.width.map(|value| value as i64))
           .set_visibility_if_not_none(params.visibility);
-        if is_primary {
-          tracing::warn!("Cannot update primary field type");
-        } else {
-          update.set_field_type_if_not_none(params.field_type.map(|field_type| field_type.into()));
-        }
       });
     self
       .notify_did_update_database_field(&params.field_id)
@@ -233,6 +233,21 @@ impl DatabaseEditor {
   }
 
   pub async fn delete_field(&self, field_id: &str) -> FlowyResult<()> {
+    let is_primary = self
+      .database
+      .lock()
+      .fields
+      .get_field(field_id)
+      .map(|field| field.is_primary)
+      .unwrap_or(false);
+
+    if is_primary {
+      return Err(FlowyError::new(
+        ErrorCode::Internal,
+        "Can not delete primary field",
+      ));
+    }
+
     let database_id = {
       let database = self.database.lock();
       database.delete_field(field_id);
@@ -283,6 +298,13 @@ impl DatabaseEditor {
     match field {
       None => {},
       Some(field) => {
+        if field.is_primary {
+          return Err(FlowyError::new(
+            ErrorCode::Internal,
+            "Can not update primary field's field type",
+          ));
+        }
+
         let old_field_type = FieldType::from(field.field_type);
         let old_type_option = field.get_any_type_option(old_field_type.clone());
         let new_type_option = field
@@ -312,6 +334,21 @@ impl DatabaseEditor {
   }
 
   pub async fn duplicate_field(&self, view_id: &str, field_id: &str) -> FlowyResult<()> {
+    let is_primary = self
+      .database
+      .lock()
+      .fields
+      .get_field(field_id)
+      .map(|field| field.is_primary)
+      .unwrap_or(false);
+
+    if is_primary {
+      return Err(FlowyError::new(
+        ErrorCode::Internal,
+        "Can not duplicate primary field",
+      ));
+    }
+
     let value = self
       .database
       .lock()

+ 3 - 1
frontend/rust-lib/flowy-database2/src/services/field/type_options/checkbox_type_option/checkbox_type_option.rs

@@ -90,7 +90,9 @@ impl CellDataDecoder for CheckboxTypeOption {
       return Ok(Default::default());
     }
 
-    self.parse_cell(cell)
+    let cell = self.parse_cell(cell);
+    println!("cell: {:?}", cell);
+    return cell;
   }
 
   fn stringify_cell_data(&self, cell_data: <Self as TypeOption>::CellData) -> String {

+ 0 - 24
frontend/rust-lib/flowy-database2/tests/database/field_test/test.rs

@@ -233,30 +233,6 @@ async fn grid_switch_from_checkbox_to_text_test() {
   test.run_scripts(scripts).await;
 }
 
-// Test when switching the current field from Checkbox to Text test
-// input:
-//      "Yes" -> check
-//      "" -> unchecked
-#[tokio::test]
-async fn grid_switch_from_text_to_checkbox_test() {
-  let mut test = DatabaseFieldTest::new().await;
-  let field = test.get_first_field(FieldType::RichText).clone();
-
-  let scripts = vec![
-    SwitchToField {
-      field_id: field.id.clone(),
-      new_field_type: FieldType::Checkbox,
-    },
-    AssertCellContent {
-      field_id: field.id.clone(),
-      row_index: 0,
-      from_field_type: FieldType::RichText,
-      expected_content: "".to_string(),
-    },
-  ];
-  test.run_scripts(scripts).await;
-}
-
 // Test when switching the current field from Date to Text test
 // input:
 //      1647251762 -> Mar 14,2022 (This string will be different base on current data setting)

+ 9 - 5
frontend/rust-lib/flowy-folder2/src/event_handler.rs

@@ -41,10 +41,14 @@ pub(crate) async fn open_workspace_handler(
   match params.value {
     None => Err(FlowyError::workspace_id().context("workspace id should not be empty")),
     Some(workspace_id) => {
-      let workspace = folder.open_workspace(&workspace_id).await?;
-      let views = folder.get_workspace_views(&workspace_id).await?;
-      let workspace_pb: WorkspacePB = (workspace, views).into();
-      data_result_ok(workspace_pb)
+      if workspace_id.is_empty() {
+        return Err(FlowyError::workspace_id().context("workspace id should not be empty"));
+      } else {
+        let workspace = folder.open_workspace(&workspace_id).await?;
+        let views = folder.get_workspace_views(&workspace_id).await?;
+        let workspace_pb: WorkspacePB = (workspace, views).into();
+        data_result_ok(workspace_pb)
+      }
     },
   }
 }
@@ -68,7 +72,7 @@ pub(crate) async fn read_workspaces_handler(
 }
 
 #[tracing::instrument(level = "debug", skip(folder), err)]
-pub async fn read_cur_workspace_setting_handler(
+pub async fn read_current_workspace_setting_handler(
   folder: AFPluginState<Arc<Folder2Manager>>,
 ) -> DataResult<WorkspaceSettingPB, FlowyError> {
   let workspace = folder.get_current_workspace().await?;

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

@@ -13,7 +13,7 @@ pub fn init(folder: Arc<Folder2Manager>) -> AFPlugin {
     .event(FolderEvent::CreateWorkspace, create_workspace_handler)
     .event(
       FolderEvent::GetCurrentWorkspace,
-      read_cur_workspace_setting_handler,
+      read_current_workspace_setting_handler,
     )
     .event(FolderEvent::ReadAllWorkspaces, read_workspaces_handler)
     .event(FolderEvent::OpenWorkspace, open_workspace_handler)

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

@@ -10,6 +10,7 @@ flowy-core = { path = "../flowy-core" }
 flowy-user = { path = "../flowy-user"}
 flowy-net = { path = "../flowy-net"}
 flowy-folder2 = { path = "../flowy-folder2", features = ["test_helper"] }
+flowy-database2 = { path = "../flowy-database2" }
 lib-dispatch = { path = "../lib-dispatch" }
 lib-ot = { path = "../../../shared-lib/lib-ot" }
 lib-infra = { path = "../../../shared-lib/lib-infra" }

+ 207 - 3
frontend/rust-lib/flowy-test/src/lib.rs

@@ -5,10 +5,10 @@ use nanoid::nanoid;
 use parking_lot::RwLock;
 
 use flowy_core::{AppFlowyCore, AppFlowyCoreConfig};
-use flowy_folder2::entities::{
-  CreateViewPayloadPB, RepeatedViewIdPB, ViewIdPB, ViewPB, WorkspaceSettingPB,
-};
+use flowy_database2::entities::*;
+use flowy_folder2::entities::*;
 use flowy_user::entities::{AuthTypePB, UserProfilePB};
+use flowy_user::errors::FlowyError;
 
 use crate::event_builder::EventBuilder;
 use crate::user_event::{async_sign_up, init_user_setting, SignUpContext};
@@ -113,6 +113,210 @@ impl FlowyCoreTest {
       .parse::<flowy_folder2::entities::ViewPB>()
   }
 
+  pub async fn create_grid(&self, parent_id: &str, name: String, initial_data: Vec<u8>) -> ViewPB {
+    let payload = CreateViewPayloadPB {
+      parent_view_id: parent_id.to_string(),
+      name,
+      desc: "".to_string(),
+      thumbnail: None,
+      layout: ViewLayoutPB::Grid,
+      initial_data,
+      meta: Default::default(),
+      set_as_current: true,
+    };
+    EventBuilder::new(self.clone())
+      .event(flowy_folder2::event_map::FolderEvent::CreateView)
+      .payload(payload)
+      .async_send()
+      .await
+      .parse::<flowy_folder2::entities::ViewPB>()
+  }
+
+  pub async fn get_database(&self, view_id: &str) -> DatabasePB {
+    EventBuilder::new(self.clone())
+      .event(flowy_database2::event_map::DatabaseEvent::GetDatabase)
+      .payload(DatabaseViewIdPB {
+        value: view_id.to_string(),
+      })
+      .async_send()
+      .await
+      .parse::<flowy_database2::entities::DatabasePB>()
+  }
+
+  pub async fn get_all_database_fields(&self, view_id: &str) -> RepeatedFieldPB {
+    EventBuilder::new(self.clone())
+      .event(flowy_database2::event_map::DatabaseEvent::GetFields)
+      .payload(GetFieldPayloadPB {
+        view_id: view_id.to_string(),
+        field_ids: None,
+      })
+      .async_send()
+      .await
+      .parse::<RepeatedFieldPB>()
+  }
+
+  pub async fn create_field(&self, view_id: &str, field_type: FieldType) -> FieldPB {
+    EventBuilder::new(self.clone())
+      .event(flowy_database2::event_map::DatabaseEvent::CreateTypeOption)
+      .payload(CreateFieldPayloadPB {
+        view_id: view_id.to_string(),
+        field_type,
+        type_option_data: None,
+      })
+      .async_send()
+      .await
+      .parse::<TypeOptionPB>()
+      .field
+  }
+
+  pub async fn update_field(&self, changeset: FieldChangesetPB) {
+    EventBuilder::new(self.clone())
+      .event(flowy_database2::event_map::DatabaseEvent::UpdateField)
+      .payload(changeset)
+      .async_send()
+      .await;
+  }
+
+  pub async fn delete_field(&self, view_id: &str, field_id: &str) -> Option<FlowyError> {
+    EventBuilder::new(self.clone())
+      .event(flowy_database2::event_map::DatabaseEvent::DeleteField)
+      .payload(DeleteFieldPayloadPB {
+        view_id: view_id.to_string(),
+        field_id: field_id.to_string(),
+      })
+      .async_send()
+      .await
+      .error()
+  }
+
+  pub async fn update_field_type(
+    &self,
+    view_id: &str,
+    field_id: &str,
+    field_type: FieldType,
+  ) -> Option<FlowyError> {
+    EventBuilder::new(self.clone())
+      .event(flowy_database2::event_map::DatabaseEvent::UpdateFieldType)
+      .payload(UpdateFieldTypePayloadPB {
+        view_id: view_id.to_string(),
+        field_id: field_id.to_string(),
+        field_type,
+      })
+      .async_send()
+      .await
+      .error()
+  }
+
+  pub async fn duplicate_field(&self, view_id: &str, field_id: &str) -> Option<FlowyError> {
+    EventBuilder::new(self.clone())
+      .event(flowy_database2::event_map::DatabaseEvent::DuplicateField)
+      .payload(DuplicateFieldPayloadPB {
+        view_id: view_id.to_string(),
+        field_id: field_id.to_string(),
+      })
+      .async_send()
+      .await
+      .error()
+  }
+
+  pub async fn create_row(
+    &self,
+    view_id: &str,
+    start_row_id: Option<String>,
+    data: Option<RowDataPB>,
+  ) -> RowPB {
+    EventBuilder::new(self.clone())
+      .event(flowy_database2::event_map::DatabaseEvent::CreateRow)
+      .payload(CreateRowPayloadPB {
+        view_id: view_id.to_string(),
+        start_row_id,
+        group_id: None,
+        data,
+      })
+      .async_send()
+      .await
+      .parse::<RowPB>()
+  }
+
+  pub async fn get_row(&self, view_id: &str, row_id: &str) -> RowPB {
+    EventBuilder::new(self.clone())
+      .event(flowy_database2::event_map::DatabaseEvent::GetRow)
+      .payload(RowIdPB {
+        view_id: view_id.to_string(),
+        row_id: row_id.to_string(),
+        group_id: None,
+      })
+      .async_send()
+      .await
+      .parse::<RowPB>()
+  }
+
+  pub async fn duplicate_row(&self, view_id: &str, row_id: &str) -> Option<FlowyError> {
+    EventBuilder::new(self.clone())
+      .event(flowy_database2::event_map::DatabaseEvent::DuplicateRow)
+      .payload(RowIdPB {
+        view_id: view_id.to_string(),
+        row_id: row_id.to_string(),
+        group_id: None,
+      })
+      .async_send()
+      .await
+      .error()
+  }
+
+  pub async fn update_cell(&self, changeset: CellChangesetPB) -> Option<FlowyError> {
+    EventBuilder::new(self.clone())
+      .event(flowy_database2::event_map::DatabaseEvent::UpdateCell)
+      .payload(changeset)
+      .async_send()
+      .await
+      .error()
+  }
+
+  pub async fn get_cell(&self, view_id: &str, row_id: &str, field_id: &str) -> CellPB {
+    EventBuilder::new(self.clone())
+      .event(flowy_database2::event_map::DatabaseEvent::GetCell)
+      .payload(CellIdPB {
+        view_id: view_id.to_string(),
+        row_id: row_id.to_string(),
+        field_id: field_id.to_string(),
+      })
+      .async_send()
+      .await
+      .parse::<CellPB>()
+  }
+
+  pub async fn insert_option(
+    &self,
+    view_id: &str,
+    field_id: &str,
+    row_id: &str,
+    name: &str,
+  ) -> Option<FlowyError> {
+    let option = EventBuilder::new(self.clone())
+      .event(flowy_database2::event_map::DatabaseEvent::CreateSelectOption)
+      .payload(CreateSelectOptionPayloadPB {
+        field_id: field_id.to_string(),
+        view_id: view_id.to_string(),
+        option_name: name.to_string(),
+      })
+      .async_send()
+      .await
+      .parse::<SelectOptionPB>();
+
+    EventBuilder::new(self.clone())
+      .event(flowy_database2::event_map::DatabaseEvent::InsertOrUpdateSelectOption)
+      .payload(RepeatedSelectOptionPayload {
+        view_id: view_id.to_string(),
+        field_id: field_id.to_string(),
+        row_id: row_id.to_string(),
+        items: vec![option],
+      })
+      .async_send()
+      .await
+      .error()
+  }
+
   pub async fn get_view(&self, view_id: &str) -> ViewPB {
     EventBuilder::new(self.clone())
       .event(flowy_folder2::event_map::FolderEvent::ReadView)

+ 290 - 0
frontend/rust-lib/flowy-test/tests/database/test.rs

@@ -1 +1,291 @@
+use bytes::Bytes;
+use flowy_database2::entities::{
+  CellChangesetPB, DatabaseLayoutPB, DatabaseViewIdPB, FieldType, SelectOptionCellDataPB,
+};
+use flowy_test::event_builder::EventBuilder;
+use flowy_test::FlowyCoreTest;
+use std::convert::TryFrom;
 
+#[tokio::test]
+async fn get_database_id_event_test() {
+  let test = FlowyCoreTest::new_with_user().await;
+  let current_workspace = test.get_current_workspace().await.workspace;
+  let grid_view = test
+    .create_grid(&current_workspace.id, "my grid view".to_owned(), vec![])
+    .await;
+
+  // The view id can be used to get the database id.
+  let database_id = EventBuilder::new(test.clone())
+    .event(flowy_database2::event_map::DatabaseEvent::GetDatabaseId)
+    .payload(DatabaseViewIdPB {
+      value: grid_view.id.clone(),
+    })
+    .async_send()
+    .await
+    .parse::<flowy_database2::entities::DatabaseIdPB>()
+    .value;
+
+  assert_ne!(database_id, grid_view.id);
+}
+
+#[tokio::test]
+async fn get_database_event_test() {
+  let test = FlowyCoreTest::new_with_user().await;
+  let current_workspace = test.get_current_workspace().await.workspace;
+  let grid_view = test
+    .create_grid(&current_workspace.id, "my grid view".to_owned(), vec![])
+    .await;
+
+  let database = test.get_database(&grid_view.id).await;
+  assert_eq!(database.fields.len(), 3);
+  assert_eq!(database.rows.len(), 3);
+  assert_eq!(database.layout_type, DatabaseLayoutPB::Grid);
+}
+
+#[tokio::test]
+async fn get_field_event_test() {
+  let test = FlowyCoreTest::new_with_user().await;
+  let current_workspace = test.get_current_workspace().await.workspace;
+  let grid_view = test
+    .create_grid(&current_workspace.id, "my grid view".to_owned(), vec![])
+    .await;
+
+  let fields = test.get_all_database_fields(&grid_view.id).await.items;
+  assert_eq!(fields[0].field_type, FieldType::RichText);
+  assert_eq!(fields[1].field_type, FieldType::SingleSelect);
+  assert_eq!(fields[2].field_type, FieldType::Checkbox);
+  assert_eq!(fields.len(), 3);
+}
+
+#[tokio::test]
+async fn create_field_event_test() {
+  let test = FlowyCoreTest::new_with_user().await;
+  let current_workspace = test.get_current_workspace().await.workspace;
+  let grid_view = test
+    .create_grid(&current_workspace.id, "my grid view".to_owned(), vec![])
+    .await;
+
+  test.create_field(&grid_view.id, FieldType::Checkbox).await;
+  let fields = test.get_all_database_fields(&grid_view.id).await.items;
+  assert_eq!(fields.len(), 4);
+  assert_eq!(fields[3].field_type, FieldType::Checkbox);
+}
+
+#[tokio::test]
+async fn delete_field_event_test() {
+  let test = FlowyCoreTest::new_with_user().await;
+  let current_workspace = test.get_current_workspace().await.workspace;
+  let grid_view = test
+    .create_grid(&current_workspace.id, "my grid view".to_owned(), vec![])
+    .await;
+
+  let fields = test.get_all_database_fields(&grid_view.id).await.items;
+  assert_eq!(fields[0].field_type, FieldType::RichText);
+  assert_eq!(fields[1].field_type, FieldType::SingleSelect);
+  assert_eq!(fields[2].field_type, FieldType::Checkbox);
+
+  let error = test.delete_field(&grid_view.id, &fields[1].id).await;
+  assert!(error.is_none());
+
+  let fields = test.get_all_database_fields(&grid_view.id).await.items;
+  assert_eq!(fields.len(), 2);
+}
+
+#[tokio::test]
+async fn delete_primary_field_event_test() {
+  let test = FlowyCoreTest::new_with_user().await;
+  let current_workspace = test.get_current_workspace().await.workspace;
+  let grid_view = test
+    .create_grid(&current_workspace.id, "my grid view".to_owned(), vec![])
+    .await;
+
+  let fields = test.get_all_database_fields(&grid_view.id).await.items;
+  // the primary field is not allowed to be deleted.
+  assert!(fields[0].is_primary);
+  let error = test.delete_field(&grid_view.id, &fields[0].id).await;
+  assert!(error.is_some());
+}
+
+#[tokio::test]
+async fn update_field_type_event_test() {
+  let test = FlowyCoreTest::new_with_user().await;
+  let current_workspace = test.get_current_workspace().await.workspace;
+  let grid_view = test
+    .create_grid(&current_workspace.id, "my grid view".to_owned(), vec![])
+    .await;
+
+  let fields = test.get_all_database_fields(&grid_view.id).await.items;
+  let error = test
+    .update_field_type(&grid_view.id, &fields[1].id, FieldType::Checklist)
+    .await;
+  assert!(error.is_none());
+
+  let fields = test.get_all_database_fields(&grid_view.id).await.items;
+  assert_eq!(fields[1].field_type, FieldType::Checklist);
+}
+
+#[tokio::test]
+async fn update_primary_field_type_event_test() {
+  let test = FlowyCoreTest::new_with_user().await;
+  let current_workspace = test.get_current_workspace().await.workspace;
+  let grid_view = test
+    .create_grid(&current_workspace.id, "my grid view".to_owned(), vec![])
+    .await;
+
+  let fields = test.get_all_database_fields(&grid_view.id).await.items;
+  // the primary field is not allowed to be deleted.
+  assert!(fields[0].is_primary);
+
+  // the primary field is not allowed to be updated.
+  let error = test
+    .update_field_type(&grid_view.id, &fields[0].id, FieldType::Checklist)
+    .await;
+  assert!(error.is_some());
+}
+
+#[tokio::test]
+async fn duplicate_field_event_test() {
+  let test = FlowyCoreTest::new_with_user().await;
+  let current_workspace = test.get_current_workspace().await.workspace;
+  let grid_view = test
+    .create_grid(&current_workspace.id, "my grid view".to_owned(), vec![])
+    .await;
+
+  let fields = test.get_all_database_fields(&grid_view.id).await.items;
+  // the primary field is not allowed to be updated.
+  let error = test.duplicate_field(&grid_view.id, &fields[1].id).await;
+  assert!(error.is_none());
+
+  let fields = test.get_all_database_fields(&grid_view.id).await.items;
+  assert_eq!(fields.len(), 4);
+}
+
+#[tokio::test]
+async fn duplicate_primary_field_test() {
+  let test = FlowyCoreTest::new_with_user().await;
+  let current_workspace = test.get_current_workspace().await.workspace;
+  let grid_view = test
+    .create_grid(&current_workspace.id, "my grid view".to_owned(), vec![])
+    .await;
+
+  let fields = test.get_all_database_fields(&grid_view.id).await.items;
+  // the primary field is not allowed to be duplicated.
+  let error = test.duplicate_field(&grid_view.id, &fields[0].id).await;
+  assert!(error.is_some());
+}
+
+#[tokio::test]
+async fn create_row_event_test() {
+  let test = FlowyCoreTest::new_with_user().await;
+  let current_workspace = test.get_current_workspace().await.workspace;
+  let grid_view = test
+    .create_grid(&current_workspace.id, "my grid view".to_owned(), vec![])
+    .await;
+
+  let _ = test.create_row(&grid_view.id, None, None).await;
+  let database = test.get_database(&grid_view.id).await;
+  assert_eq!(database.rows.len(), 4);
+}
+
+#[tokio::test]
+async fn duplicate_row_event_test() {
+  let test = FlowyCoreTest::new_with_user().await;
+  let current_workspace = test.get_current_workspace().await.workspace;
+  let grid_view = test
+    .create_grid(&current_workspace.id, "my grid view".to_owned(), vec![])
+    .await;
+  let database = test.get_database(&grid_view.id).await;
+  let error = test
+    .duplicate_row(&grid_view.id, &database.rows[0].id)
+    .await;
+  assert!(error.is_none());
+
+  let database = test.get_database(&grid_view.id).await;
+  assert_eq!(database.rows.len(), 4);
+}
+
+#[tokio::test]
+async fn update_text_cell_event_test() {
+  let test = FlowyCoreTest::new_with_user().await;
+  let current_workspace = test.get_current_workspace().await.workspace;
+  let grid_view = test
+    .create_grid(&current_workspace.id, "my grid view".to_owned(), vec![])
+    .await;
+  let database = test.get_database(&grid_view.id).await;
+  let fields = test.get_all_database_fields(&grid_view.id).await.items;
+
+  let row_id = database.rows[0].id.clone();
+  let field_id = fields[0].id.clone();
+  assert_eq!(fields[0].field_type, FieldType::RichText);
+
+  // Update the first cell of the first row.
+  let error = test
+    .update_cell(CellChangesetPB {
+      view_id: grid_view.id.clone(),
+      row_id: row_id.clone(),
+      field_id: field_id.clone(),
+      cell_changeset: "hello world".to_string(),
+    })
+    .await;
+  assert!(error.is_none());
+
+  let cell = test.get_cell(&grid_view.id, &row_id, &field_id).await;
+  let s = String::from_utf8(cell.data).unwrap();
+  assert_eq!(s, "hello world");
+}
+
+#[tokio::test]
+async fn update_checkbox_cell_event_test() {
+  let test = FlowyCoreTest::new_with_user().await;
+  let current_workspace = test.get_current_workspace().await.workspace;
+  let grid_view = test
+    .create_grid(&current_workspace.id, "my grid view".to_owned(), vec![])
+    .await;
+  let database = test.get_database(&grid_view.id).await;
+  let fields = test.get_all_database_fields(&grid_view.id).await.items;
+
+  let row_id = database.rows[0].id.clone();
+  let field_id = fields[2].id.clone();
+  assert_eq!(fields[2].field_type, FieldType::Checkbox);
+
+  for input in vec!["yes", "true", "1"] {
+    let error = test
+      .update_cell(CellChangesetPB {
+        view_id: grid_view.id.clone(),
+        row_id: row_id.clone(),
+        field_id: field_id.clone(),
+        cell_changeset: input.to_string(),
+      })
+      .await;
+    assert!(error.is_none());
+
+    let cell = test.get_cell(&grid_view.id, &row_id, &field_id).await;
+    let output = String::from_utf8(cell.data).unwrap();
+    assert_eq!(output, "Yes");
+  }
+}
+
+#[tokio::test]
+async fn update_single_select_cell_event_test() {
+  let test = FlowyCoreTest::new_with_user().await;
+  let current_workspace = test.get_current_workspace().await.workspace;
+  let grid_view = test
+    .create_grid(&current_workspace.id, "my grid view".to_owned(), vec![])
+    .await;
+  let database = test.get_database(&grid_view.id).await;
+  let fields = test.get_all_database_fields(&grid_view.id).await.items;
+  let row_id = database.rows[0].id.clone();
+  let field_id = fields[1].id.clone();
+  assert_eq!(fields[1].field_type, FieldType::SingleSelect);
+
+  let error = test
+    .insert_option(&grid_view.id, &field_id, &row_id, "task 1")
+    .await;
+  assert!(error.is_none());
+
+  let cell = test.get_cell(&grid_view.id, &row_id, &field_id).await;
+  let select_option_cell = SelectOptionCellDataPB::try_from(Bytes::from(cell.data)).unwrap();
+
+  assert_eq!(select_option_cell.options.len(), 1);
+  assert_eq!(select_option_cell.select_options.len(), 1);
+}