Quellcode durchsuchen

Feat: import and export csv (#2617)

* feat: import csv

* feat: export

* chore: implement import export csv in the backend

* chore: patch
Nathan.fooo vor 2 Jahren
Ursprung
Commit
70bb7f2ad6
30 geänderte Dateien mit 555 neuen und 150 gelöschten Zeilen
  1. 22 0
      frontend/appflowy_tauri/src-tauri/Cargo.lock
  2. 6 6
      frontend/appflowy_tauri/src-tauri/Cargo.toml
  3. 32 10
      frontend/rust-lib/Cargo.lock
  4. 5 5
      frontend/rust-lib/Cargo.toml
  5. 1 0
      frontend/rust-lib/flowy-database2/Cargo.toml
  6. 2 0
      frontend/rust-lib/flowy-database2/src/entities/mod.rs
  7. 24 0
      frontend/rust-lib/flowy-database2/src/entities/share_entities.rs
  8. 61 37
      frontend/rust-lib/flowy-database2/src/event_handler.rs
  9. 6 1
      frontend/rust-lib/flowy-database2/src/event_map.rs
  10. 26 4
      frontend/rust-lib/flowy-database2/src/manager.rs
  11. 12 31
      frontend/rust-lib/flowy-database2/src/services/cell/cell_operation.rs
  12. 17 5
      frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs
  13. 2 2
      frontend/rust-lib/flowy-database2/src/services/field/type_options/checkbox_type_option/checkbox_type_option.rs
  14. 6 3
      frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/select_type_option.rs
  15. 8 8
      frontend/rust-lib/flowy-database2/src/services/field/type_options/text_type_option/text_type_option.rs
  16. 2 2
      frontend/rust-lib/flowy-database2/src/services/field/type_options/type_option.rs
  17. 24 20
      frontend/rust-lib/flowy-database2/src/services/field/type_options/type_option_cell.rs
  18. 5 5
      frontend/rust-lib/flowy-database2/src/services/group/controller.rs
  19. 1 0
      frontend/rust-lib/flowy-database2/src/services/mod.rs
  20. 68 0
      frontend/rust-lib/flowy-database2/src/services/share/csv/export.rs
  21. 157 0
      frontend/rust-lib/flowy-database2/src/services/share/csv/import.rs
  22. 5 0
      frontend/rust-lib/flowy-database2/src/services/share/csv/mod.rs
  23. 1 0
      frontend/rust-lib/flowy-database2/src/services/share/mod.rs
  24. 21 8
      frontend/rust-lib/flowy-database2/tests/database/database_editor.rs
  25. 1 1
      frontend/rust-lib/flowy-database2/tests/database/mock_data/board_mock_data.rs
  26. 1 1
      frontend/rust-lib/flowy-database2/tests/database/mock_data/calendar_mock_data.rs
  27. 1 1
      frontend/rust-lib/flowy-database2/tests/database/mock_data/grid_mock_data.rs
  28. 1 0
      frontend/rust-lib/flowy-database2/tests/database/mod.rs
  29. 36 0
      frontend/rust-lib/flowy-database2/tests/database/share_test/export_test.rs
  30. 1 0
      frontend/rust-lib/flowy-database2/tests/database/share_test/mod.rs

+ 22 - 0
frontend/appflowy_tauri/src-tauri/Cargo.lock

@@ -1387,6 +1387,27 @@ dependencies = [
  "syn 1.0.109",
 ]
 
+[[package]]
+name = "csv"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b015497079b9a9d69c02ad25de6c0a6edef051ea6360a327d0bd05802ef64ad"
+dependencies = [
+ "csv-core",
+ "itoa 1.0.6",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "csv-core"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b2466559f260f48ad25fe6317b3c8dac77b5bdb5763ac7d9d6103530663bc90"
+dependencies = [
+ "memchr",
+]
+
 [[package]]
 name = "ctor"
 version = "0.1.26"
@@ -1829,6 +1850,7 @@ dependencies = [
  "chrono-tz 0.8.2",
  "collab",
  "collab-database",
+ "csv",
  "dashmap",
  "fancy-regex 0.10.0",
  "flowy-codegen",

+ 6 - 6
frontend/appflowy_tauri/src-tauri/Cargo.toml

@@ -34,12 +34,12 @@ default = ["custom-protocol"]
 custom-protocol = ["tauri/custom-protocol"]
 
 [patch.crates-io]
-collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6052d5" }
-collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6052d5" }
-collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6052d5" }
-collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6052d5" }
-collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6052d5" }
-appflowy-integrate = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6052d5" }
+collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d6af3a" }
+collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d6af3a" }
+collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d6af3a" }
+collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d6af3a" }
+collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d6af3a" }
+appflowy-integrate = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d6af3a" }
 
 #collab = { path = "../../AppFlowy-Collab/collab" }
 #collab-folder = { path = "../../AppFlowy-Collab/collab-folder" }

+ 32 - 10
frontend/rust-lib/Cargo.lock

@@ -85,7 +85,7 @@ checksum = "7de8ce5e0f9f8d88245311066a578d72b7af3e7088f32783804676302df237e4"
 [[package]]
 name = "appflowy-integrate"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6052d5#6052d509982f705c6d43a859c937a722a8c3358b"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d6af3a#d6af3a7662a57202e4e5b0a297c28757a18f3372"
 dependencies = [
  "anyhow",
  "collab",
@@ -886,7 +886,7 @@ dependencies = [
 [[package]]
 name = "collab"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6052d5#6052d509982f705c6d43a859c937a722a8c3358b"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d6af3a#d6af3a7662a57202e4e5b0a297c28757a18f3372"
 dependencies = [
  "anyhow",
  "bytes",
@@ -903,7 +903,7 @@ dependencies = [
 [[package]]
 name = "collab-client-ws"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6052d5#6052d509982f705c6d43a859c937a722a8c3358b"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d6af3a#d6af3a7662a57202e4e5b0a297c28757a18f3372"
 dependencies = [
  "bytes",
  "collab-sync",
@@ -921,7 +921,7 @@ dependencies = [
 [[package]]
 name = "collab-database"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6052d5#6052d509982f705c6d43a859c937a722a8c3358b"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d6af3a#d6af3a7662a57202e4e5b0a297c28757a18f3372"
 dependencies = [
  "anyhow",
  "async-trait",
@@ -946,7 +946,7 @@ dependencies = [
 [[package]]
 name = "collab-derive"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6052d5#6052d509982f705c6d43a859c937a722a8c3358b"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d6af3a#d6af3a7662a57202e4e5b0a297c28757a18f3372"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -958,7 +958,7 @@ dependencies = [
 [[package]]
 name = "collab-document"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6052d5#6052d509982f705c6d43a859c937a722a8c3358b"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d6af3a#d6af3a7662a57202e4e5b0a297c28757a18f3372"
 dependencies = [
  "anyhow",
  "collab",
@@ -975,7 +975,7 @@ dependencies = [
 [[package]]
 name = "collab-folder"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6052d5#6052d509982f705c6d43a859c937a722a8c3358b"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d6af3a#d6af3a7662a57202e4e5b0a297c28757a18f3372"
 dependencies = [
  "anyhow",
  "collab",
@@ -993,7 +993,7 @@ dependencies = [
 [[package]]
 name = "collab-persistence"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6052d5#6052d509982f705c6d43a859c937a722a8c3358b"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d6af3a#d6af3a7662a57202e4e5b0a297c28757a18f3372"
 dependencies = [
  "bincode",
  "chrono",
@@ -1013,7 +1013,7 @@ dependencies = [
 [[package]]
 name = "collab-plugins"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6052d5#6052d509982f705c6d43a859c937a722a8c3358b"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d6af3a#d6af3a7662a57202e4e5b0a297c28757a18f3372"
 dependencies = [
  "anyhow",
  "async-trait",
@@ -1043,7 +1043,7 @@ dependencies = [
 [[package]]
 name = "collab-sync"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6052d5#6052d509982f705c6d43a859c937a722a8c3358b"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d6af3a#d6af3a7662a57202e4e5b0a297c28757a18f3372"
 dependencies = [
  "bytes",
  "collab",
@@ -1212,6 +1212,27 @@ dependencies = [
  "typenum",
 ]
 
+[[package]]
+name = "csv"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b015497079b9a9d69c02ad25de6c0a6edef051ea6360a327d0bd05802ef64ad"
+dependencies = [
+ "csv-core",
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "csv-core"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b2466559f260f48ad25fe6317b3c8dac77b5bdb5763ac7d9d6103530663bc90"
+dependencies = [
+ "memchr",
+]
+
 [[package]]
 name = "cxx"
 version = "1.0.94"
@@ -1609,6 +1630,7 @@ dependencies = [
  "chrono-tz 0.8.2",
  "collab",
  "collab-database",
+ "csv",
  "dashmap",
  "fancy-regex 0.10.0",
  "flowy-codegen",

+ 5 - 5
frontend/rust-lib/Cargo.toml

@@ -33,11 +33,11 @@ opt-level = 3
 incremental = false
 
 [patch.crates-io]
-collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6052d5"  }
-collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6052d5"  }
-collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6052d5" }
-collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6052d5" }
-appflowy-integrate = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6052d5" }
+collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d6af3a"  }
+collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d6af3a"  }
+collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d6af3a" }
+collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d6af3a" }
+appflowy-integrate = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d6af3a" }
 
 #collab = { path = "../AppFlowy-Collab/collab" }
 #collab-folder = { path = "../AppFlowy-Collab/collab-folder" }

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

@@ -39,6 +39,7 @@ rayon = "1.6.1"
 nanoid = "0.4.0"
 chrono-tz = "0.8.1"
 async-trait = "0.1"
+csv = "1.1.6"
 
 strum = "0.21"
 strum_macros = "0.21"

+ 2 - 0
frontend/rust-lib/flowy-database2/src/entities/mod.rs

@@ -12,6 +12,7 @@ mod view_entities;
 
 #[macro_use]
 mod macros;
+mod share_entities;
 mod type_option_entities;
 
 pub use calendar_entities::*;
@@ -22,6 +23,7 @@ pub use filter_entities::*;
 pub use group_entities::*;
 pub use row_entities::*;
 pub use setting_entities::*;
+pub use share_entities::*;
 pub use sort_entities::*;
 pub use type_option_entities::*;
 pub use view_entities::*;

+ 24 - 0
frontend/rust-lib/flowy-database2/src/entities/share_entities.rs

@@ -0,0 +1,24 @@
+use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
+
+#[derive(Clone, Debug, ProtoBuf_Enum)]
+pub enum ImportTypePB {
+  CSV = 0,
+}
+
+impl Default for ImportTypePB {
+  fn default() -> Self {
+    Self::CSV
+  }
+}
+
+#[derive(Clone, Debug, ProtoBuf, Default)]
+pub struct DatabaseImportPB {
+  #[pb(index = 1, one_of)]
+  pub data: Option<String>,
+
+  #[pb(index = 2, one_of)]
+  pub uri: Option<String>,
+
+  #[pb(index = 3)]
+  pub import_type: ImportTypePB,
+}

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

@@ -3,7 +3,7 @@ use std::sync::Arc;
 use collab_database::rows::RowId;
 use collab_database::views::DatabaseLayout;
 
-use flowy_error::{FlowyError, FlowyResult};
+use flowy_error::{ErrorCode, FlowyError, FlowyResult};
 use lib_dispatch::prelude::{data_result_ok, AFPluginData, AFPluginState, DataResult};
 
 use crate::entities::*;
@@ -19,7 +19,7 @@ pub(crate) async fn get_database_data_handler(
   manager: AFPluginState<Arc<DatabaseManager2>>,
 ) -> DataResult<DatabasePB, FlowyError> {
   let view_id: DatabaseViewIdPB = data.into_inner();
-  let database_editor = manager.get_database(view_id.as_ref()).await?;
+  let database_editor = manager.get_database_with_view_id(view_id.as_ref()).await?;
   let data = database_editor.get_database_data(view_id.as_ref()).await;
   data_result_ok(data)
 }
@@ -30,7 +30,7 @@ pub(crate) async fn get_database_setting_handler(
   manager: AFPluginState<Arc<DatabaseManager2>>,
 ) -> DataResult<DatabaseViewSettingPB, FlowyError> {
   let view_id: DatabaseViewIdPB = data.into_inner();
-  let database_editor = manager.get_database(view_id.as_ref()).await?;
+  let database_editor = manager.get_database_with_view_id(view_id.as_ref()).await?;
   let data = database_editor
     .get_database_view_setting(view_id.as_ref())
     .await?;
@@ -43,7 +43,7 @@ pub(crate) async fn update_database_setting_handler(
   manager: AFPluginState<Arc<DatabaseManager2>>,
 ) -> Result<(), FlowyError> {
   let params: DatabaseSettingChangesetParams = data.into_inner().try_into()?;
-  let editor = manager.get_database(&params.view_id).await?;
+  let editor = manager.get_database_with_view_id(&params.view_id).await?;
 
   if let Some(insert_params) = params.insert_group {
     editor.insert_group(insert_params).await?;
@@ -76,7 +76,7 @@ pub(crate) async fn get_all_filters_handler(
   manager: AFPluginState<Arc<DatabaseManager2>>,
 ) -> DataResult<RepeatedFilterPB, FlowyError> {
   let view_id: DatabaseViewIdPB = data.into_inner();
-  let database_editor = manager.get_database(view_id.as_ref()).await?;
+  let database_editor = manager.get_database_with_view_id(view_id.as_ref()).await?;
   let filters = database_editor.get_all_filters(view_id.as_ref()).await;
   data_result_ok(filters)
 }
@@ -87,7 +87,7 @@ pub(crate) async fn get_all_sorts_handler(
   manager: AFPluginState<Arc<DatabaseManager2>>,
 ) -> DataResult<RepeatedSortPB, FlowyError> {
   let view_id: DatabaseViewIdPB = data.into_inner();
-  let database_editor = manager.get_database(view_id.as_ref()).await?;
+  let database_editor = manager.get_database_with_view_id(view_id.as_ref()).await?;
   let sorts = database_editor.get_all_sorts(view_id.as_ref()).await;
   data_result_ok(sorts)
 }
@@ -98,7 +98,7 @@ pub(crate) async fn delete_all_sorts_handler(
   manager: AFPluginState<Arc<DatabaseManager2>>,
 ) -> Result<(), FlowyError> {
   let view_id: DatabaseViewIdPB = data.into_inner();
-  let database_editor = manager.get_database(view_id.as_ref()).await?;
+  let database_editor = manager.get_database_with_view_id(view_id.as_ref()).await?;
   database_editor.delete_all_sorts(view_id.as_ref()).await;
   Ok(())
 }
@@ -109,7 +109,7 @@ pub(crate) async fn get_fields_handler(
   manager: AFPluginState<Arc<DatabaseManager2>>,
 ) -> DataResult<RepeatedFieldPB, FlowyError> {
   let params: GetFieldParams = data.into_inner().try_into()?;
-  let database_editor = manager.get_database(&params.view_id).await?;
+  let database_editor = manager.get_database_with_view_id(&params.view_id).await?;
   let fields = database_editor
     .get_fields(&params.view_id, params.field_ids)
     .into_iter()
@@ -125,7 +125,7 @@ pub(crate) async fn update_field_handler(
   manager: AFPluginState<Arc<DatabaseManager2>>,
 ) -> Result<(), FlowyError> {
   let params: FieldChangesetParams = data.into_inner().try_into()?;
-  let database_editor = manager.get_database(&params.view_id).await?;
+  let database_editor = manager.get_database_with_view_id(&params.view_id).await?;
   database_editor.update_field(params).await?;
   Ok(())
 }
@@ -136,7 +136,7 @@ pub(crate) async fn update_field_type_option_handler(
   manager: AFPluginState<Arc<DatabaseManager2>>,
 ) -> Result<(), FlowyError> {
   let params: TypeOptionChangesetParams = data.into_inner().try_into()?;
-  let database_editor = manager.get_database(&params.view_id).await?;
+  let database_editor = manager.get_database_with_view_id(&params.view_id).await?;
   if let Some(old_field) = database_editor.get_field(&params.field_id) {
     let field_type = FieldType::from(old_field.field_type);
     let type_option_data =
@@ -159,7 +159,7 @@ pub(crate) async fn delete_field_handler(
   manager: AFPluginState<Arc<DatabaseManager2>>,
 ) -> Result<(), FlowyError> {
   let params: FieldIdParams = data.into_inner().try_into()?;
-  let database_editor = manager.get_database(&params.view_id).await?;
+  let database_editor = manager.get_database_with_view_id(&params.view_id).await?;
   database_editor.delete_field(&params.field_id).await?;
   Ok(())
 }
@@ -170,7 +170,7 @@ pub(crate) async fn switch_to_field_handler(
   manager: AFPluginState<Arc<DatabaseManager2>>,
 ) -> Result<(), FlowyError> {
   let params: EditFieldParams = data.into_inner().try_into()?;
-  let database_editor = manager.get_database(&params.view_id).await?;
+  let database_editor = manager.get_database_with_view_id(&params.view_id).await?;
   let old_field = database_editor.get_field(&params.field_id);
   database_editor
     .switch_to_field_type(&params.field_id, &params.field_type)
@@ -205,7 +205,7 @@ pub(crate) async fn duplicate_field_handler(
   manager: AFPluginState<Arc<DatabaseManager2>>,
 ) -> Result<(), FlowyError> {
   let params: FieldIdParams = data.into_inner().try_into()?;
-  let database_editor = manager.get_database(&params.view_id).await?;
+  let database_editor = manager.get_database_with_view_id(&params.view_id).await?;
   database_editor
     .duplicate_field(&params.view_id, &params.field_id)
     .await?;
@@ -219,7 +219,7 @@ pub(crate) async fn get_field_type_option_data_handler(
   manager: AFPluginState<Arc<DatabaseManager2>>,
 ) -> DataResult<TypeOptionPB, FlowyError> {
   let params: TypeOptionPathParams = data.into_inner().try_into()?;
-  let database_editor = manager.get_database(&params.view_id).await?;
+  let database_editor = manager.get_database_with_view_id(&params.view_id).await?;
   if let Some((field, data)) = database_editor
     .get_field_type_option_data(&params.field_id)
     .await
@@ -242,7 +242,7 @@ pub(crate) async fn create_field_type_option_data_handler(
   manager: AFPluginState<Arc<DatabaseManager2>>,
 ) -> DataResult<TypeOptionPB, FlowyError> {
   let params: CreateFieldParams = data.into_inner().try_into()?;
-  let database_editor = manager.get_database(&params.view_id).await?;
+  let database_editor = manager.get_database_with_view_id(&params.view_id).await?;
   let (field, data) = database_editor
     .create_field_with_type_option(&params.view_id, &params.field_type, params.type_option_data)
     .await;
@@ -261,7 +261,7 @@ pub(crate) async fn move_field_handler(
   manager: AFPluginState<Arc<DatabaseManager2>>,
 ) -> Result<(), FlowyError> {
   let params: MoveFieldParams = data.into_inner().try_into()?;
-  let database_editor = manager.get_database(&params.view_id).await?;
+  let database_editor = manager.get_database_with_view_id(&params.view_id).await?;
   database_editor
     .move_field(
       &params.view_id,
@@ -279,7 +279,7 @@ pub(crate) async fn get_row_handler(
   manager: AFPluginState<Arc<DatabaseManager2>>,
 ) -> DataResult<OptionalRowPB, FlowyError> {
   let params: RowIdParams = data.into_inner().try_into()?;
-  let database_editor = manager.get_database(&params.view_id).await?;
+  let database_editor = manager.get_database_with_view_id(&params.view_id).await?;
   let row = database_editor.get_row(&params.row_id).map(RowPB::from);
   data_result_ok(OptionalRowPB { row })
 }
@@ -290,7 +290,7 @@ pub(crate) async fn delete_row_handler(
   manager: AFPluginState<Arc<DatabaseManager2>>,
 ) -> Result<(), FlowyError> {
   let params: RowIdParams = data.into_inner().try_into()?;
-  let database_editor = manager.get_database(&params.view_id).await?;
+  let database_editor = manager.get_database_with_view_id(&params.view_id).await?;
   database_editor.delete_row(&params.row_id).await;
   Ok(())
 }
@@ -301,7 +301,7 @@ pub(crate) async fn duplicate_row_handler(
   manager: AFPluginState<Arc<DatabaseManager2>>,
 ) -> Result<(), FlowyError> {
   let params: RowIdParams = data.into_inner().try_into()?;
-  let database_editor = manager.get_database(&params.view_id).await?;
+  let database_editor = manager.get_database_with_view_id(&params.view_id).await?;
   database_editor
     .duplicate_row(&params.view_id, &params.row_id)
     .await;
@@ -314,7 +314,7 @@ pub(crate) async fn move_row_handler(
   manager: AFPluginState<Arc<DatabaseManager2>>,
 ) -> Result<(), FlowyError> {
   let params: MoveRowParams = data.into_inner().try_into()?;
-  let database_editor = manager.get_database(&params.view_id).await?;
+  let database_editor = manager.get_database_with_view_id(&params.view_id).await?;
   database_editor
     .move_row(&params.view_id, params.from_row_id, params.to_row_id)
     .await;
@@ -327,7 +327,7 @@ pub(crate) async fn create_row_handler(
   manager: AFPluginState<Arc<DatabaseManager2>>,
 ) -> DataResult<RowPB, FlowyError> {
   let params: CreateRowParams = data.into_inner().try_into()?;
-  let database_editor = manager.get_database(&params.view_id).await?;
+  let database_editor = manager.get_database_with_view_id(&params.view_id).await?;
   match database_editor.create_row(params).await? {
     None => Err(FlowyError::internal().context("Create row fail")),
     Some(row) => data_result_ok(RowPB::from(row)),
@@ -340,7 +340,7 @@ pub(crate) async fn get_cell_handler(
   manager: AFPluginState<Arc<DatabaseManager2>>,
 ) -> DataResult<CellPB, FlowyError> {
   let params: CellIdParams = data.into_inner().try_into()?;
-  let database_editor = manager.get_database(&params.view_id).await?;
+  let database_editor = manager.get_database_with_view_id(&params.view_id).await?;
   let cell = database_editor
     .get_cell(&params.field_id, params.row_id)
     .await;
@@ -353,7 +353,7 @@ pub(crate) async fn update_cell_handler(
   manager: AFPluginState<Arc<DatabaseManager2>>,
 ) -> Result<(), FlowyError> {
   let params: CellChangesetPB = data.into_inner();
-  let database_editor = manager.get_database(&params.view_id).await?;
+  let database_editor = manager.get_database_with_view_id(&params.view_id).await?;
   database_editor
     .update_cell_with_changeset(
       &params.view_id,
@@ -371,7 +371,7 @@ pub(crate) async fn new_select_option_handler(
   manager: AFPluginState<Arc<DatabaseManager2>>,
 ) -> DataResult<SelectOptionPB, FlowyError> {
   let params: CreateSelectOptionParams = data.into_inner().try_into()?;
-  let database_editor = manager.get_database(&params.view_id).await?;
+  let database_editor = manager.get_database_with_view_id(&params.view_id).await?;
   let result = database_editor
     .create_select_option(&params.field_id, params.option_name)
     .await;
@@ -389,7 +389,7 @@ pub(crate) async fn insert_or_update_select_option_handler(
   manager: AFPluginState<Arc<DatabaseManager2>>,
 ) -> Result<(), FlowyError> {
   let params = data.into_inner();
-  let database_editor = manager.get_database(&params.view_id).await?;
+  let database_editor = manager.get_database_with_view_id(&params.view_id).await?;
   database_editor
     .insert_select_options(
       &params.view_id,
@@ -407,7 +407,7 @@ pub(crate) async fn delete_select_option_handler(
   manager: AFPluginState<Arc<DatabaseManager2>>,
 ) -> Result<(), FlowyError> {
   let params = data.into_inner();
-  let database_editor = manager.get_database(&params.view_id).await?;
+  let database_editor = manager.get_database_with_view_id(&params.view_id).await?;
   database_editor
     .delete_select_options(
       &params.view_id,
@@ -425,7 +425,7 @@ pub(crate) async fn get_select_option_handler(
   manager: AFPluginState<Arc<DatabaseManager2>>,
 ) -> DataResult<SelectOptionCellDataPB, FlowyError> {
   let params: CellIdParams = data.into_inner().try_into()?;
-  let database_editor = manager.get_database(&params.view_id).await?;
+  let database_editor = manager.get_database_with_view_id(&params.view_id).await?;
   let options = database_editor
     .get_select_options(params.row_id, &params.field_id)
     .await;
@@ -439,7 +439,7 @@ pub(crate) async fn update_select_option_cell_handler(
 ) -> Result<(), FlowyError> {
   let params: SelectOptionCellChangesetParams = data.into_inner().try_into()?;
   let database_editor = manager
-    .get_database(&params.cell_identifier.view_id)
+    .get_database_with_view_id(&params.cell_identifier.view_id)
     .await?;
   let changeset = SelectOptionCellChangeset {
     insert_option_ids: params.insert_option_ids,
@@ -469,7 +469,7 @@ pub(crate) async fn update_date_cell_handler(
     include_time: data.include_time,
     timezone_id: data.timezone_id,
   };
-  let database_editor = manager.get_database(&cell_id.view_id).await?;
+  let database_editor = manager.get_database_with_view_id(&cell_id.view_id).await?;
   database_editor
     .update_cell_with_changeset(
       &cell_id.view_id,
@@ -487,7 +487,7 @@ pub(crate) async fn get_groups_handler(
   manager: AFPluginState<Arc<DatabaseManager2>>,
 ) -> DataResult<RepeatedGroupPB, FlowyError> {
   let params: DatabaseViewIdPB = data.into_inner();
-  let database_editor = manager.get_database(params.as_ref()).await?;
+  let database_editor = manager.get_database_with_view_id(params.as_ref()).await?;
   let groups = database_editor.load_groups(params.as_ref()).await?;
   data_result_ok(groups)
 }
@@ -498,7 +498,7 @@ pub(crate) async fn get_group_handler(
   manager: AFPluginState<Arc<DatabaseManager2>>,
 ) -> DataResult<GroupPB, FlowyError> {
   let params: DatabaseGroupIdParams = data.into_inner().try_into()?;
-  let database_editor = manager.get_database(&params.view_id).await?;
+  let database_editor = manager.get_database_with_view_id(&params.view_id).await?;
   let group = database_editor
     .get_group(&params.view_id, &params.group_id)
     .await?;
@@ -511,7 +511,7 @@ pub(crate) async fn move_group_handler(
   manager: AFPluginState<Arc<DatabaseManager2>>,
 ) -> FlowyResult<()> {
   let params: MoveGroupParams = data.into_inner().try_into()?;
-  let database_editor = manager.get_database(&params.view_id).await?;
+  let database_editor = manager.get_database_with_view_id(&params.view_id).await?;
   database_editor
     .move_group(&params.view_id, &params.from_group_id, &params.to_group_id)
     .await?;
@@ -524,7 +524,7 @@ pub(crate) async fn move_group_row_handler(
   manager: AFPluginState<Arc<DatabaseManager2>>,
 ) -> FlowyResult<()> {
   let params: MoveGroupRowParams = data.into_inner().try_into()?;
-  let database_editor = manager.get_database(&params.view_id).await?;
+  let database_editor = manager.get_database_with_view_id(&params.view_id).await?;
   database_editor
     .move_group_row(
       &params.view_id,
@@ -550,7 +550,7 @@ pub(crate) async fn set_layout_setting_handler(
   manager: AFPluginState<Arc<DatabaseManager2>>,
 ) -> FlowyResult<()> {
   let params: LayoutSettingChangeset = data.into_inner().try_into()?;
-  let database_editor = manager.get_database(&params.view_id).await?;
+  let database_editor = manager.get_database_with_view_id(&params.view_id).await?;
   let layout_params = LayoutSettingParams {
     calendar: params.calendar,
   };
@@ -565,7 +565,7 @@ pub(crate) async fn get_layout_setting_handler(
   manager: AFPluginState<Arc<DatabaseManager2>>,
 ) -> DataResult<LayoutSettingPB, FlowyError> {
   let params: DatabaseLayoutId = data.into_inner().try_into()?;
-  let database_editor = manager.get_database(&params.view_id).await?;
+  let database_editor = manager.get_database_with_view_id(&params.view_id).await?;
   let layout_setting_pb = database_editor
     .get_layout_setting(&params.view_id, params.layout)
     .await
@@ -580,7 +580,7 @@ pub(crate) async fn get_calendar_events_handler(
   manager: AFPluginState<Arc<DatabaseManager2>>,
 ) -> DataResult<RepeatedCalendarEventPB, FlowyError> {
   let params: CalendarEventRequestParams = data.into_inner().try_into()?;
-  let database_editor = manager.get_database(&params.view_id).await?;
+  let database_editor = manager.get_database_with_view_id(&params.view_id).await?;
   let events = database_editor
     .get_all_calendar_events(&params.view_id)
     .await;
@@ -593,7 +593,7 @@ pub(crate) async fn get_calendar_event_handler(
   manager: AFPluginState<Arc<DatabaseManager2>>,
 ) -> DataResult<CalendarEventPB, FlowyError> {
   let params: RowIdParams = data.into_inner().try_into()?;
-  let database_editor = manager.get_database(&params.view_id).await?;
+  let database_editor = manager.get_database_with_view_id(&params.view_id).await?;
   let event = database_editor
     .get_calendar_event(&params.view_id, params.row_id)
     .await;
@@ -602,3 +602,27 @@ pub(crate) async fn get_calendar_event_handler(
     Some(event) => data_result_ok(event),
   }
 }
+
+#[tracing::instrument(level = "debug", skip(data, manager), err)]
+pub(crate) async fn import_data_handler(
+  data: AFPluginData<DatabaseImportPB>,
+  manager: AFPluginState<Arc<DatabaseManager2>>,
+) -> FlowyResult<()> {
+  let params = data.into_inner();
+
+  match params.import_type {
+    ImportTypePB::CSV => {
+      if let Some(data) = params.data {
+        manager.import_csv(data).await?;
+      } else if let Some(uri) = params.uri {
+        manager.import_csv_data_from_uri(uri).await?;
+      } else {
+        return Err(FlowyError::new(
+          ErrorCode::InvalidData,
+          "No data or uri provided",
+        ));
+      }
+    },
+  }
+  Ok(())
+}

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

@@ -58,7 +58,9 @@ pub fn init(database_manager: Arc<DatabaseManager2>) -> AFPlugin {
         .event(DatabaseEvent::GetCalendarEvent, get_calendar_event_handler)
         // Layout setting
         .event(DatabaseEvent::SetLayoutSetting, set_layout_setting_handler)
-        .event(DatabaseEvent::GetLayoutSetting, get_layout_setting_handler);
+        .event(DatabaseEvent::GetLayoutSetting, get_layout_setting_handler)
+        // import
+        .event(DatabaseEvent::ImportCSV, import_data_handler);
 
   plugin
 }
@@ -259,4 +261,7 @@ pub enum DatabaseEvent {
 
   #[event(input = "MoveCalendarEventPB")]
   MoveCalendarEvent = 119,
+
+  #[event(input = "DatabaseImportPB")]
+  ImportCSV = 120,
 }

+ 26 - 4
frontend/rust-lib/flowy-database2/src/manager.rs

@@ -11,11 +11,12 @@ use collab_database::views::{CreateDatabaseParams, CreateViewParams};
 use parking_lot::Mutex;
 use tokio::sync::RwLock;
 
-use flowy_error::{FlowyError, FlowyResult};
+use flowy_error::{internal_error, FlowyError, FlowyResult};
 use flowy_task::TaskDispatcher;
 
 use crate::entities::{DatabaseDescriptionPB, DatabaseLayoutPB, RepeatedDatabaseDescriptionPB};
 use crate::services::database::{DatabaseEditor, MutexDatabase};
+use crate::services::share::csv::{CSVImporter, ExportStyle};
 
 pub trait DatabaseUser2: Send + Sync {
   fn user_id(&self) -> Result<i64, FlowyError>;
@@ -77,14 +78,17 @@ impl DatabaseManager2 {
     }
   }
 
-  pub async fn get_database(&self, view_id: &str) -> FlowyResult<Arc<DatabaseEditor>> {
+  pub async fn get_database_with_view_id(&self, view_id: &str) -> FlowyResult<Arc<DatabaseEditor>> {
     let database_id = self.with_user_database(Err(FlowyError::internal()), |database| {
       database
         .get_database_id_with_view_id(view_id)
         .ok_or_else(FlowyError::record_not_found)
     })?;
+    self.get_database(&database_id).await
+  }
 
-    if let Some(editor) = self.editors.read().await.get(&database_id) {
+  pub async fn get_database(&self, database_id: &str) -> FlowyResult<Arc<DatabaseEditor>> {
+    if let Some(editor) = self.editors.read().await.get(database_id) {
       return Ok(editor.clone());
     }
 
@@ -93,7 +97,7 @@ impl DatabaseManager2 {
       Err(FlowyError::record_not_found()),
       |database| {
         database
-          .get_database(&database_id)
+          .get_database(database_id)
           .ok_or_else(FlowyError::record_not_found)
       },
     )?);
@@ -191,6 +195,24 @@ impl DatabaseManager2 {
     Ok(())
   }
 
+  pub async fn import_csv(&self, content: String) -> FlowyResult<String> {
+    let params = tokio::task::spawn_blocking(move || CSVImporter.import_csv_from_string(content))
+      .await
+      .map_err(internal_error)??;
+    let database_id = params.database_id.clone();
+    self.create_database_with_params(params).await?;
+    Ok(database_id)
+  }
+
+  pub async fn import_csv_data_from_uri(&self, _uri: String) -> FlowyResult<()> {
+    Ok(())
+  }
+
+  pub async fn export_csv(&self, view_id: &str, style: ExportStyle) -> FlowyResult<String> {
+    let database = self.get_database_with_view_id(view_id).await?;
+    database.export_csv(style).await
+  }
+
   fn with_user_database<F, Output>(&self, default_value: Output, f: F) -> Output
   where
     F: FnOnce(&InnerUserDatabase) -> Output,

+ 12 - 31
frontend/rust-lib/flowy-database2/src/services/cell/cell_operation.rs

@@ -74,7 +74,7 @@ pub fn apply_cell_changeset<C: ToCellChangeset>(
   }
 }
 
-pub fn get_type_cell_protobuf(
+pub fn get_cell_protobuf(
   cell: &Cell,
   field: &Field,
   cell_cache: Option<CellCache>,
@@ -101,25 +101,6 @@ pub fn get_type_cell_protobuf(
   }
 }
 
-pub fn get_type_cell_data<Output>(
-  cell: &Cell,
-  field: &Field,
-  cell_data_cache: Option<CellCache>,
-) -> Option<Output>
-where
-  Output: Default + 'static,
-{
-  let from_field_type = get_field_type_from_cell(cell)?;
-  let to_field_type = FieldType::from(field.field_type);
-  try_decode_cell_to_cell_data(
-    cell,
-    &from_field_type,
-    &to_field_type,
-    field,
-    cell_data_cache,
-  )
-}
-
 /// 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`
@@ -174,22 +155,22 @@ pub fn try_decode_cell_to_cell_data<T: Default + 'static>(
 ///
 /// * `cell_str`: the opaque cell string that can be decoded by corresponding structs that implement the
 /// `FromCellString` trait.
-/// * `decoded_field_type`: the field_type of the cell_str
-/// * `field_type`: use this field type's `TypeOption` to stringify this cell_str
+/// * `to_field_type`: the cell will be decoded to this field type's cell data.
+/// * `from_field_type`: the original field type of the passed-in cell data.
 /// * `field_rev`: used to get the corresponding TypeOption for the specified field type.
 ///
 /// returns: String
 pub fn stringify_cell_data(
   cell: &Cell,
-  decoded_field_type: &FieldType,
-  field_type: &FieldType,
+  to_field_type: &FieldType,
+  from_field_type: &FieldType,
   field: &Field,
 ) -> String {
   match TypeOptionCellExt::new_with_cell_data_cache(field, None)
-    .get_type_option_cell_data_handler(field_type)
+    .get_type_option_cell_data_handler(from_field_type)
   {
     None => "".to_string(),
-    Some(handler) => handler.stringify_cell_str(cell, decoded_field_type, field),
+    Some(handler) => handler.stringify_cell_str(cell, to_field_type, field),
   }
 }
 
@@ -312,17 +293,17 @@ where
 //     }
 // }
 
-pub struct CellBuilder {
+pub struct CellBuilder<'a> {
   cells: Cells,
-  field_maps: HashMap<String, Field>,
+  field_maps: HashMap<String, &'a Field>,
 }
 
-impl CellBuilder {
-  pub fn with_cells(cell_by_field_id: HashMap<String, String>, fields: Vec<Field>) -> Self {
+impl<'a> CellBuilder<'a> {
+  pub fn with_cells(cell_by_field_id: HashMap<String, String>, fields: &'a [Field]) -> Self {
     let field_maps = fields
       .into_iter()
       .map(|field| (field.id.clone(), field))
-      .collect::<HashMap<String, Field>>();
+      .collect::<HashMap<String, &Field>>();
 
     let mut cells = Cells::new();
     for (field_id, cell_str) in cell_by_field_id {

+ 17 - 5
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::{FlowyError, FlowyResult};
+use flowy_error::{internal_error, FlowyError, FlowyResult};
 use flowy_task::TaskDispatcher;
 use lib_infra::future::{to_fut, Fut};
 
@@ -23,8 +23,7 @@ use crate::entities::{
 };
 use crate::notification::{send_notification, DatabaseNotification};
 use crate::services::cell::{
-  apply_cell_changeset, get_type_cell_protobuf, AnyTypeCache, CellBuilder, CellCache,
-  ToCellChangeset,
+  apply_cell_changeset, get_cell_protobuf, AnyTypeCache, CellBuilder, CellCache, ToCellChangeset,
 };
 use crate::services::database::util::database_view_setting_pb_from_view;
 use crate::services::database_view::{DatabaseViewChanged, DatabaseViewData, DatabaseViews};
@@ -36,6 +35,7 @@ use crate::services::field::{
 };
 use crate::services::filter::Filter;
 use crate::services::group::{default_group_setting, GroupSetting, RowChangeset};
+use crate::services::share::csv::{CSVExport, ExportStyle};
 use crate::services::sort::Sort;
 
 #[derive(Clone)]
@@ -303,7 +303,7 @@ impl DatabaseEditor {
   pub async fn create_row(&self, params: CreateRowParams) -> FlowyResult<Option<Row>> {
     let fields = self.database.lock().get_fields(&params.view_id, None);
     let mut cells =
-      CellBuilder::with_cells(params.cell_data_by_field_id.unwrap_or_default(), fields).build();
+      CellBuilder::with_cells(params.cell_data_by_field_id.unwrap_or_default(), &fields).build();
     for view in self.database_views.editors().await {
       view.v_will_create_row(&mut cells, &params.group_id).await;
     }
@@ -434,7 +434,7 @@ impl DatabaseEditor {
     match (field, cell) {
       (Some(field), Some(cell)) => {
         let field_type = FieldType::from(field.field_type);
-        let cell_bytes = get_type_cell_protobuf(&cell, &field, Some(self.cell_cache.clone()));
+        let cell_bytes = get_cell_protobuf(&cell, &field, Some(self.cell_cache.clone()));
         CellPB {
           field_id: field_id.to_string(),
           row_id: row_id.into(),
@@ -808,6 +808,18 @@ impl DatabaseEditor {
       rows,
     }
   }
+
+  pub async fn export_csv(&self, style: ExportStyle) -> FlowyResult<String> {
+    let database = self.database.clone();
+    let csv = tokio::task::spawn_blocking(move || {
+      let database_guard = database.lock();
+      let csv = CSVExport.export_database(&database_guard, style)?;
+      Ok::<String, FlowyError>(csv)
+    })
+    .await
+    .map_err(internal_error)??;
+    Ok(csv)
+  }
 }
 
 pub(crate) async fn notify_did_update_cell(changesets: Vec<CellChangesetNotifyPB>) {

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

@@ -40,10 +40,10 @@ impl TypeOptionTransform for CheckboxTypeOption {
   fn transform_type_option_cell(
     &self,
     cell: &Cell,
-    _decoded_field_type: &FieldType,
+    transformed_field_type: &FieldType,
     _field: &Field,
   ) -> Option<<Self as TypeOption>::CellData> {
-    if _decoded_field_type.is_text() {
+    if transformed_field_type.is_text() {
       Some(CheckboxCellData::from(cell))
     } else {
       None

+ 6 - 3
frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/select_type_option.rs

@@ -95,11 +95,14 @@ where
   fn transform_type_option_cell(
     &self,
     cell: &Cell,
-    _decoded_field_type: &FieldType,
+    transformed_field_type: &FieldType,
     _field: &Field,
   ) -> Option<<Self as TypeOption>::CellData> {
-    match _decoded_field_type {
-      FieldType::SingleSelect | FieldType::MultiSelect | FieldType::Checklist => None,
+    match transformed_field_type {
+      FieldType::SingleSelect | FieldType::MultiSelect | FieldType::Checklist => {
+        // If the transformed field type is SingleSelect, MultiSelect or Checklist, Do nothing.
+        None
+      },
       FieldType::Checkbox => {
         let cell_content = CheckboxCellData::from(cell).to_string();
         let mut transformed_ids = Vec::new();

+ 8 - 8
frontend/rust-lib/flowy-database2/src/services/field/type_options/text_type_option/text_type_option.rs

@@ -64,19 +64,19 @@ impl TypeOptionTransform for RichTextTypeOption {
   fn transform_type_option_cell(
     &self,
     cell: &Cell,
-    _decoded_field_type: &FieldType,
+    transformed_field_type: &FieldType,
     _field: &Field,
   ) -> Option<<Self as TypeOption>::CellData> {
-    if _decoded_field_type.is_date()
-      || _decoded_field_type.is_single_select()
-      || _decoded_field_type.is_multi_select()
-      || _decoded_field_type.is_number()
-      || _decoded_field_type.is_url()
+    if transformed_field_type.is_date()
+      || transformed_field_type.is_single_select()
+      || transformed_field_type.is_multi_select()
+      || transformed_field_type.is_number()
+      || transformed_field_type.is_url()
     {
       Some(StrCellData::from(stringify_cell_data(
         cell,
-        _decoded_field_type,
-        _decoded_field_type,
+        transformed_field_type,
+        transformed_field_type,
         _field,
       )))
     } else {

+ 2 - 2
frontend/rust-lib/flowy-database2/src/services/field/type_options/type_option.rs

@@ -99,13 +99,13 @@ pub trait TypeOptionTransform: TypeOption {
   /// # Arguments
   ///
   /// * `cell_str`: the cell string of the current field type
-  /// * `decoded_field_type`: the field type of the cell data that's going to be transformed into
+  /// * `transformed_field_type`: the cell will be transformed to the is field type's cell data.
   /// current `TypeOption` field type.
   ///
   fn transform_type_option_cell(
     &self,
     _cell: &Cell,
-    _decoded_field_type: &FieldType,
+    _transformed_field_type: &FieldType,
     _field: &Field,
   ) -> Option<<Self as TypeOption>::CellData> {
     None

+ 24 - 20
frontend/rust-lib/flowy-database2/src/services/field/type_options/type_option_cell.rs

@@ -21,6 +21,7 @@ use crate::services::field::{
 
 pub const CELL_DATA: &str = "data";
 
+/// Each [FieldType] has its own [TypeOptionCellDataHandler].
 /// A helper trait that used to erase the `Self` of `TypeOption` trait to make it become a Object-safe trait
 /// Only object-safe traits can be made into trait objects.
 /// > Object-safe traits are traits with methods that follow these two rules:
@@ -46,19 +47,20 @@ pub trait TypeOptionCellDataHandler: Send + Sync + 'static {
 
   fn handle_cell_filter(&self, field_type: &FieldType, field: &Field, cell: &Cell) -> bool;
 
-  /// Decode the cell_str to corresponding cell data, and then return the display string of the
-  /// cell data.
-  fn stringify_cell_str(
-    &self,
-    cell: &Cell,
-    decoded_field_type: &FieldType,
-    field: &Field,
-  ) -> String;
+  /// Format the cell to string using the passed-in [FieldType] and [Field].
+  /// The [Cell] is generic, so we need to know the [FieldType] and [Field] to format the cell.
+  ///
+  /// For example, the field type of the [TypeOptionCellDataHandler] is [FieldType::Date], and
+  /// the if field_type is [FieldType::RichText], then the string would be something like "Mar 14, 2022".
+  ///
+  fn stringify_cell_str(&self, cell: &Cell, field_type: &FieldType, field: &Field) -> String;
 
+  /// Format the cell to [BoxCellData] using the passed-in [FieldType] and [Field].
+  /// The caller can get the cell data by calling [BoxCellData::unbox_or_none].
   fn get_cell_data(
     &self,
     cell: &Cell,
-    decoded_field_type: &FieldType,
+    field_type: &FieldType,
     field: &Field,
   ) -> FlowyResult<BoxCellData>;
 }
@@ -252,14 +254,16 @@ where
     perform_filter().unwrap_or(true)
   }
 
-  fn stringify_cell_str(
-    &self,
-    cell: &Cell,
-    decoded_field_type: &FieldType,
-    field: &Field,
-  ) -> String {
+  /// Stringify [Cell] to string
+  /// if the [TypeOptionCellDataHandler] supports transform, it will try to transform the [Cell] to
+  /// the passed-in field type [Cell].
+  /// For example, the field type of the [TypeOptionCellDataHandler] is [FieldType::MultiSelect], the field_type
+  /// is [FieldType::RichText], then the string will be transformed to a string that separated by comma with the
+  /// option's name.
+  ///
+  fn stringify_cell_str(&self, cell: &Cell, field_type: &FieldType, field: &Field) -> String {
     if self.transformable() {
-      let cell_data = self.transform_type_option_cell(cell, decoded_field_type, field);
+      let cell_data = self.transform_type_option_cell(cell, field_type, field);
       if let Some(cell_data) = cell_data {
         return self.decode_cell_data_to_str(cell_data);
       }
@@ -270,17 +274,17 @@ where
   fn get_cell_data(
     &self,
     cell: &Cell,
-    decoded_field_type: &FieldType,
+    field_type: &FieldType,
     field: &Field,
   ) -> FlowyResult<BoxCellData> {
     // tracing::debug!("get_cell_data: {:?}", std::any::type_name::<Self>());
     let cell_data = if self.transformable() {
-      match self.transform_type_option_cell(cell, decoded_field_type, field) {
-        None => self.get_decoded_cell_data(cell, decoded_field_type, field)?,
+      match self.transform_type_option_cell(cell, field_type, field) {
+        None => self.get_decoded_cell_data(cell, field_type, field)?,
         Some(cell_data) => cell_data,
       }
     } else {
-      self.get_decoded_cell_data(cell, decoded_field_type, field)?
+      self.get_decoded_cell_data(cell, field_type, field)?
     };
     Ok(BoxCellData::new(cell_data))
   }

+ 5 - 5
frontend/rust-lib/flowy-database2/src/services/group/controller.rs

@@ -10,7 +10,7 @@ use serde::Serialize;
 use flowy_error::FlowyResult;
 
 use crate::entities::{FieldType, GroupChangesetPB, GroupRowsNotificationPB, InsertedRowPB};
-use crate::services::cell::{get_type_cell_protobuf, CellProtobufBlobParser, DecodedCellData};
+use crate::services::cell::{get_cell_protobuf, CellProtobufBlobParser, DecodedCellData};
 use crate::services::group::action::{
   DidMoveGroupRowResult, DidUpdateGroupRowResult, GroupControllerActions, GroupCustomize,
 };
@@ -228,7 +228,7 @@ where
 
       if let Some(cell) = cell {
         let mut grouped_rows: Vec<GroupedRow> = vec![];
-        let cell_bytes = get_type_cell_protobuf(&cell, field, None);
+        let cell_bytes = get_cell_protobuf(&cell, field, None);
         let cell_data = cell_bytes.parser::<P>()?;
         for group in self.group_ctx.groups() {
           if self.can_group(&group.filter_content, &cell_data) {
@@ -311,7 +311,7 @@ where
       row_changesets: vec![],
     };
     if let Some(cell) = row.cells.get(&self.grouping_field_id) {
-      let cell_bytes = get_type_cell_protobuf(cell, field, None);
+      let cell_bytes = get_cell_protobuf(cell, field, None);
       let cell_data = cell_bytes.parser::<P>()?;
       if !cell_data.is_empty() {
         tracing::error!("did_delete_delete_row {:?}", cell);
@@ -349,7 +349,7 @@ where
     };
 
     if let Some(cell) = cell_rev {
-      let cell_bytes = get_type_cell_protobuf(&cell, context.field, None);
+      let cell_bytes = get_cell_protobuf(&cell, context.field, None);
       let cell_data = cell_bytes.parser::<P>()?;
       result.deleted_group = self.delete_group_when_move_row(context.row, &cell_data);
       result.row_changesets = self.move_row(&cell_data, context);
@@ -374,6 +374,6 @@ fn get_cell_data_from_row<P: CellProtobufBlobParser>(
   field: &Field,
 ) -> Option<P::Object> {
   let cell = row.and_then(|row| row.cells.get(&field.id))?;
-  let cell_bytes = get_type_cell_protobuf(cell, field, None);
+  let cell_bytes = get_cell_protobuf(cell, field, None);
   cell_bytes.parser::<P>().ok()
 }

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

@@ -5,4 +5,5 @@ pub mod field;
 pub mod filter;
 pub mod group;
 pub mod setting;
+pub mod share;
 pub mod sort;

+ 68 - 0
frontend/rust-lib/flowy-database2/src/services/share/csv/export.rs

@@ -0,0 +1,68 @@
+use crate::entities::FieldType;
+use crate::services::cell::stringify_cell_data;
+use collab_database::database::Database;
+
+use flowy_error::{FlowyError, FlowyResult};
+use std::collections::HashMap;
+
+pub enum ExportStyle {
+  /// The export data will be pure data, without any meta data.
+  /// Will lost the field type information.
+  SIMPLE,
+  /// The export data contains meta data, such as field type.
+  /// It can be used to fully restore the database.
+  META,
+}
+
+pub struct CSVExport;
+impl CSVExport {
+  pub fn export_database(&self, database: &Database, style: ExportStyle) -> FlowyResult<String> {
+    let mut wtr = csv::Writer::from_writer(vec![]);
+    let inline_view_id = database.get_inline_view_id();
+    let fields = database.get_fields(&inline_view_id, None);
+
+    // Write fields
+    let field_records = fields
+      .iter()
+      .map(|field| match &style {
+        ExportStyle::SIMPLE => field.name.clone(),
+        ExportStyle::META => serde_json::to_string(&field).unwrap(),
+      })
+      .collect::<Vec<String>>();
+    wtr
+      .write_record(&field_records)
+      .map_err(|e| FlowyError::internal().context(e))?;
+
+    // Write rows
+    let field_by_field_id = fields
+      .into_iter()
+      .map(|field| (field.id.clone(), field))
+      .collect::<HashMap<_, _>>();
+    let rows = database.get_rows_for_view(&inline_view_id);
+    for row in rows {
+      let cells = field_by_field_id
+        .iter()
+        .map(|(field_id, field)| match row.cells.get(field_id) {
+          None => "".to_string(),
+          Some(cell) => {
+            let field_type = FieldType::from(field.field_type);
+            match style {
+              ExportStyle::SIMPLE => stringify_cell_data(cell, &field_type, &field_type, field),
+              ExportStyle::META => serde_json::to_string(cell).unwrap_or_else(|_| "".to_string()),
+            }
+          },
+        })
+        .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)
+  }
+}

+ 157 - 0
frontend/rust-lib/flowy-database2/src/services/share/csv/import.rs

@@ -0,0 +1,157 @@
+use crate::entities::FieldType;
+use crate::services::cell::CellBuilder;
+use crate::services::field::default_type_option_data_for_type;
+use collab_database::database::{gen_database_id, gen_database_view_id, gen_field_id, gen_row_id};
+use collab_database::fields::Field;
+use collab_database::rows::CreateRowParams;
+use collab_database::views::{CreateDatabaseParams, DatabaseLayout};
+use flowy_error::{FlowyError, FlowyResult};
+use rayon::prelude::*;
+use std::collections::HashMap;
+use std::{fs::File, io::prelude::*};
+
+#[derive(Default)]
+pub struct CSVImporter;
+
+impl CSVImporter {
+  pub fn import_csv_from_file(&self, path: &str) -> FlowyResult<CreateDatabaseParams> {
+    let mut file = File::open(path)?;
+    let mut content = String::new();
+    file.read_to_string(&mut content)?;
+    let fields_with_rows = self.get_fields_and_rows(content)?;
+    let database_data = database_from_fields_and_rows(fields_with_rows);
+    Ok(database_data)
+  }
+
+  pub fn import_csv_from_string(&self, content: String) -> FlowyResult<CreateDatabaseParams> {
+    let fields_with_rows = self.get_fields_and_rows(content)?;
+    let database_data = database_from_fields_and_rows(fields_with_rows);
+    Ok(database_data)
+  }
+
+  fn get_fields_and_rows(&self, content: String) -> Result<FieldsRows, FlowyError> {
+    let mut fields: Vec<String> = vec![];
+    if content.is_empty() {
+      return Err(FlowyError::invalid_data().context("Import content is empty"));
+    }
+
+    let mut reader = csv::Reader::from_reader(content.as_bytes());
+    if let Ok(headers) = reader.headers() {
+      for header in headers {
+        fields.push(header.to_string());
+      }
+    } else {
+      return Err(FlowyError::invalid_data().context("Header not found"));
+    }
+
+    let rows = reader
+      .records()
+      .into_iter()
+      .flat_map(|r| r.ok())
+      .map(|record| {
+        record
+          .into_iter()
+          .map(|s| s.to_string())
+          .collect::<Vec<String>>()
+      })
+      .collect();
+
+    Ok(FieldsRows { fields, rows })
+  }
+}
+
+fn database_from_fields_and_rows(fields_and_rows: FieldsRows) -> CreateDatabaseParams {
+  let (fields, rows) = fields_and_rows.split();
+  let view_id = gen_database_view_id();
+  let database_id = gen_database_id();
+
+  let fields = fields
+    .into_iter()
+    .enumerate()
+    .map(
+      |(index, field_str)| match serde_json::from_str(&field_str) {
+        Ok(field) => field,
+        Err(_) => {
+          let field_type = FieldType::RichText;
+          let type_option_data = default_type_option_data_for_type(&field_type);
+          let is_primary = index == 0;
+          Field::new(
+            gen_field_id(),
+            field_str,
+            field_type.clone().into(),
+            is_primary,
+          )
+          .with_type_option_data(field_type, type_option_data)
+        },
+      },
+    )
+    .collect::<Vec<Field>>();
+
+  let created_rows = rows
+    .par_iter()
+    .map(|row| {
+      let mut cell_by_field_id = HashMap::new();
+      let mut params = CreateRowParams::new(gen_row_id());
+      for (index, cell) in row.iter().enumerate() {
+        if let Some(field) = fields.get(index) {
+          cell_by_field_id.insert(field.id.clone(), cell.to_string());
+        }
+      }
+      params.cells = CellBuilder::with_cells(cell_by_field_id, &fields).build();
+      params
+    })
+    .collect::<Vec<CreateRowParams>>();
+
+  CreateDatabaseParams {
+    database_id,
+    view_id,
+    name: "".to_string(),
+    layout: DatabaseLayout::Grid,
+    layout_settings: Default::default(),
+    filters: vec![],
+    groups: vec![],
+    sorts: vec![],
+    created_rows,
+    fields,
+  }
+}
+
+struct FieldsRows {
+  fields: Vec<String>,
+  rows: Vec<Vec<String>>,
+}
+impl FieldsRows {
+  fn split(self) -> (Vec<String>, Vec<Vec<String>>) {
+    (self.fields, self.rows)
+  }
+}
+
+#[cfg(test)]
+mod tests {
+  use crate::services::share::csv::CSVImporter;
+
+  #[test]
+  fn test_import_csv_from_str() {
+    let s = r#"Name,Tags,Number,Date,Checkbox,URL
+1,tag 1,1,"May 26, 2023",Yes,appflowy.io
+2,tag 2,2,"May 22, 2023",No,
+,,,,Yes,"#;
+    let importer = CSVImporter;
+    let result = importer.import_csv_from_string(s.to_string()).unwrap();
+    assert_eq!(result.created_rows.len(), 3);
+    assert_eq!(result.fields.len(), 6);
+
+    assert_eq!(result.fields[0].name, "Name");
+    assert_eq!(result.fields[1].name, "Tags");
+    assert_eq!(result.fields[2].name, "Number");
+    assert_eq!(result.fields[3].name, "Date");
+    assert_eq!(result.fields[4].name, "Checkbox");
+    assert_eq!(result.fields[5].name, "URL");
+
+    assert_eq!(result.created_rows[0].cells.len(), 6);
+    assert_eq!(result.created_rows[1].cells.len(), 6);
+    assert_eq!(result.created_rows[2].cells.len(), 6);
+
+    println!("{:?}", result);
+  }
+}

+ 5 - 0
frontend/rust-lib/flowy-database2/src/services/share/csv/mod.rs

@@ -0,0 +1,5 @@
+mod export;
+mod import;
+
+pub use export::*;
+pub use import::*;

+ 1 - 0
frontend/rust-lib/flowy-database2/src/services/share/mod.rs

@@ -0,0 +1 @@
+pub mod csv;

+ 21 - 8
frontend/rust-lib/flowy-database2/tests/database/database_editor.rs

@@ -62,7 +62,7 @@ impl DatabaseEditorTest {
 
     let editor = sdk
       .database_manager
-      .get_database(&test.child_view.id)
+      .get_database_with_view_id(&test.child_view.id)
       .await
       .unwrap();
     let fields = editor
@@ -223,21 +223,34 @@ impl DatabaseEditorTest {
     let cell_changeset = SelectOptionCellChangeset::from_insert_option_id(option_id);
     self.update_cell(&field.id, row_id, cell_changeset).await
   }
+
+  pub async fn import(&self, s: String) -> String {
+    self.sdk.database_manager.import_csv(s).await.unwrap()
+  }
+
+  pub async fn get_database(&self, database_id: &str) -> Option<Arc<DatabaseEditor>> {
+    self
+      .sdk
+      .database_manager
+      .get_database(database_id)
+      .await
+      .ok()
+  }
 }
 
-pub struct TestRowBuilder {
+pub struct TestRowBuilder<'a> {
   row_id: RowId,
-  fields: Vec<Field>,
-  cell_build: CellBuilder,
+  fields: &'a [Field],
+  cell_build: CellBuilder<'a>,
 }
 
-impl TestRowBuilder {
-  pub fn new(row_id: RowId, fields: Vec<Field>) -> Self {
-    let inner_builder = CellBuilder::with_cells(Default::default(), fields.clone());
+impl<'a> TestRowBuilder<'a> {
+  pub fn new(row_id: RowId, fields: &'a [Field]) -> Self {
+    let cell_build = CellBuilder::with_cells(Default::default(), fields);
     Self {
       row_id,
       fields,
-      cell_build: inner_builder,
+      cell_build,
     }
   }
 

+ 1 - 1
frontend/rust-lib/flowy-database2/tests/database/mock_data/board_mock_data.rs

@@ -111,7 +111,7 @@ pub fn make_test_board() -> DatabaseData {
 
   // 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 mut row_builder = TestRowBuilder::new(i.into(), fields.clone());
+    let mut row_builder = TestRowBuilder::new(i.into(), &fields);
     match i {
       0 => {
         for field_type in FieldType::iter() {

+ 1 - 1
frontend/rust-lib/flowy-database2/tests/database/mock_data/calendar_mock_data.rs

@@ -40,7 +40,7 @@ pub fn make_test_calendar() -> DatabaseData {
   let calendar_setting: LayoutSetting = CalendarLayoutSetting::new(date_field_id).into();
 
   for i in 0..5 {
-    let mut row_builder = TestRowBuilder::new(i.into(), fields.clone());
+    let mut row_builder = TestRowBuilder::new(i.into(), &fields);
     match i {
       0 => {
         for field_type in FieldType::iter() {

+ 1 - 1
frontend/rust-lib/flowy-database2/tests/database/mock_data/grid_mock_data.rs

@@ -111,7 +111,7 @@ pub fn make_test_grid() -> DatabaseData {
   }
 
   for i in 0..6 {
-    let mut row_builder = TestRowBuilder::new(i.into(), fields.clone());
+    let mut row_builder = TestRowBuilder::new(i.into(), &fields);
     match i {
       0 => {
         for field_type in FieldType::iter() {

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

@@ -7,3 +7,4 @@ mod layout_test;
 mod sort_test;
 
 mod mock_data;
+mod share_test;

+ 36 - 0
frontend/rust-lib/flowy-database2/tests/database/share_test/export_test.rs

@@ -0,0 +1,36 @@
+use crate::database::database_editor::DatabaseEditorTest;
+use flowy_database2::services::share::csv::ExportStyle;
+
+#[tokio::test]
+async fn export_and_then_import_test() {
+  let test = DatabaseEditorTest::new_grid().await;
+  let database = test.editor.clone();
+  let csv_1 = database.export_csv(ExportStyle::SIMPLE).await.unwrap();
+
+  let imported_database_id = test.import(csv_1.clone()).await;
+  let csv_2 = test
+    .get_database(&imported_database_id)
+    .await
+    .unwrap()
+    .export_csv(ExportStyle::SIMPLE)
+    .await
+    .unwrap();
+
+  let mut reader = csv::Reader::from_reader(csv_1.as_bytes());
+  let export_csv_records_1 = reader.records();
+
+  let mut reader = csv::Reader::from_reader(csv_2.as_bytes());
+  let export_csv_records_2 = reader.records();
+
+  let mut a = export_csv_records_1
+    .map(|v| v.unwrap())
+    .flat_map(|v| v.iter().map(|v| v.to_string()).collect::<Vec<_>>())
+    .collect::<Vec<String>>();
+  let mut b = export_csv_records_2
+    .map(|v| v.unwrap())
+    .flat_map(|v| v.iter().map(|v| v.to_string()).collect::<Vec<_>>())
+    .collect::<Vec<String>>();
+  a.sort();
+  b.sort();
+  assert_eq!(a, b);
+}

+ 1 - 0
frontend/rust-lib/flowy-database2/tests/database/share_test/mod.rs

@@ -0,0 +1 @@
+mod export_test;