diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.lock b/frontend/appflowy_tauri/src-tauri/Cargo.lock index 5222af3567..74fb14ba3a 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.lock +++ b/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", diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.toml b/frontend/appflowy_tauri/src-tauri/Cargo.toml index 27d25b2fa6..7960fe90d6 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.toml +++ b/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" } diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 50843d10f7..aba8a079da 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/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", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index afe27c26c0..1630728f17 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/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" } diff --git a/frontend/rust-lib/flowy-database2/Cargo.toml b/frontend/rust-lib/flowy-database2/Cargo.toml index 9e2395f95f..74e4409779 100644 --- a/frontend/rust-lib/flowy-database2/Cargo.toml +++ b/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" diff --git a/frontend/rust-lib/flowy-database2/src/entities/mod.rs b/frontend/rust-lib/flowy-database2/src/entities/mod.rs index 387466e7aa..0bc4b1b8d4 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/mod.rs +++ b/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::*; diff --git a/frontend/rust-lib/flowy-database2/src/entities/share_entities.rs b/frontend/rust-lib/flowy-database2/src/entities/share_entities.rs new file mode 100644 index 0000000000..0cfe2d2dfe --- /dev/null +++ b/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, + + #[pb(index = 2, one_of)] + pub uri: Option, + + #[pb(index = 3)] + pub import_type: ImportTypePB, +} diff --git a/frontend/rust-lib/flowy-database2/src/event_handler.rs b/frontend/rust-lib/flowy-database2/src/event_handler.rs index c043a35a05..64106bbed4 100644 --- a/frontend/rust-lib/flowy-database2/src/event_handler.rs +++ b/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>, ) -> DataResult { 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>, ) -> DataResult { 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>, ) -> Result<(), FlowyError> { let params: DatabaseSettingChangesetParams = data.into_inner().try_into()?; - let editor = manager.get_database(¶ms.view_id).await?; + let editor = manager.get_database_with_view_id(¶ms.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>, ) -> DataResult { 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>, ) -> DataResult { 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>, ) -> 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>, ) -> DataResult { let params: GetFieldParams = data.into_inner().try_into()?; - let database_editor = manager.get_database(¶ms.view_id).await?; + let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; let fields = database_editor .get_fields(¶ms.view_id, params.field_ids) .into_iter() @@ -125,7 +125,7 @@ pub(crate) async fn update_field_handler( manager: AFPluginState>, ) -> Result<(), FlowyError> { let params: FieldChangesetParams = data.into_inner().try_into()?; - let database_editor = manager.get_database(¶ms.view_id).await?; + let database_editor = manager.get_database_with_view_id(¶ms.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>, ) -> Result<(), FlowyError> { let params: TypeOptionChangesetParams = data.into_inner().try_into()?; - let database_editor = manager.get_database(¶ms.view_id).await?; + let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; if let Some(old_field) = database_editor.get_field(¶ms.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>, ) -> Result<(), FlowyError> { let params: FieldIdParams = data.into_inner().try_into()?; - let database_editor = manager.get_database(¶ms.view_id).await?; + let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; database_editor.delete_field(¶ms.field_id).await?; Ok(()) } @@ -170,7 +170,7 @@ pub(crate) async fn switch_to_field_handler( manager: AFPluginState>, ) -> Result<(), FlowyError> { let params: EditFieldParams = data.into_inner().try_into()?; - let database_editor = manager.get_database(¶ms.view_id).await?; + let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; let old_field = database_editor.get_field(¶ms.field_id); database_editor .switch_to_field_type(¶ms.field_id, ¶ms.field_type) @@ -205,7 +205,7 @@ pub(crate) async fn duplicate_field_handler( manager: AFPluginState>, ) -> Result<(), FlowyError> { let params: FieldIdParams = data.into_inner().try_into()?; - let database_editor = manager.get_database(¶ms.view_id).await?; + let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; database_editor .duplicate_field(¶ms.view_id, ¶ms.field_id) .await?; @@ -219,7 +219,7 @@ pub(crate) async fn get_field_type_option_data_handler( manager: AFPluginState>, ) -> DataResult { let params: TypeOptionPathParams = data.into_inner().try_into()?; - let database_editor = manager.get_database(¶ms.view_id).await?; + let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; if let Some((field, data)) = database_editor .get_field_type_option_data(¶ms.field_id) .await @@ -242,7 +242,7 @@ pub(crate) async fn create_field_type_option_data_handler( manager: AFPluginState>, ) -> DataResult { let params: CreateFieldParams = data.into_inner().try_into()?; - let database_editor = manager.get_database(¶ms.view_id).await?; + let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; let (field, data) = database_editor .create_field_with_type_option(¶ms.view_id, ¶ms.field_type, params.type_option_data) .await; @@ -261,7 +261,7 @@ pub(crate) async fn move_field_handler( manager: AFPluginState>, ) -> Result<(), FlowyError> { let params: MoveFieldParams = data.into_inner().try_into()?; - let database_editor = manager.get_database(¶ms.view_id).await?; + let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; database_editor .move_field( ¶ms.view_id, @@ -279,7 +279,7 @@ pub(crate) async fn get_row_handler( manager: AFPluginState>, ) -> DataResult { let params: RowIdParams = data.into_inner().try_into()?; - let database_editor = manager.get_database(¶ms.view_id).await?; + let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; let row = database_editor.get_row(¶ms.row_id).map(RowPB::from); data_result_ok(OptionalRowPB { row }) } @@ -290,7 +290,7 @@ pub(crate) async fn delete_row_handler( manager: AFPluginState>, ) -> Result<(), FlowyError> { let params: RowIdParams = data.into_inner().try_into()?; - let database_editor = manager.get_database(¶ms.view_id).await?; + let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; database_editor.delete_row(¶ms.row_id).await; Ok(()) } @@ -301,7 +301,7 @@ pub(crate) async fn duplicate_row_handler( manager: AFPluginState>, ) -> Result<(), FlowyError> { let params: RowIdParams = data.into_inner().try_into()?; - let database_editor = manager.get_database(¶ms.view_id).await?; + let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; database_editor .duplicate_row(¶ms.view_id, ¶ms.row_id) .await; @@ -314,7 +314,7 @@ pub(crate) async fn move_row_handler( manager: AFPluginState>, ) -> Result<(), FlowyError> { let params: MoveRowParams = data.into_inner().try_into()?; - let database_editor = manager.get_database(¶ms.view_id).await?; + let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; database_editor .move_row(¶ms.view_id, params.from_row_id, params.to_row_id) .await; @@ -327,7 +327,7 @@ pub(crate) async fn create_row_handler( manager: AFPluginState>, ) -> DataResult { let params: CreateRowParams = data.into_inner().try_into()?; - let database_editor = manager.get_database(¶ms.view_id).await?; + let database_editor = manager.get_database_with_view_id(¶ms.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>, ) -> DataResult { let params: CellIdParams = data.into_inner().try_into()?; - let database_editor = manager.get_database(¶ms.view_id).await?; + let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; let cell = database_editor .get_cell(¶ms.field_id, params.row_id) .await; @@ -353,7 +353,7 @@ pub(crate) async fn update_cell_handler( manager: AFPluginState>, ) -> Result<(), FlowyError> { let params: CellChangesetPB = data.into_inner(); - let database_editor = manager.get_database(¶ms.view_id).await?; + let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; database_editor .update_cell_with_changeset( ¶ms.view_id, @@ -371,7 +371,7 @@ pub(crate) async fn new_select_option_handler( manager: AFPluginState>, ) -> DataResult { let params: CreateSelectOptionParams = data.into_inner().try_into()?; - let database_editor = manager.get_database(¶ms.view_id).await?; + let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; let result = database_editor .create_select_option(¶ms.field_id, params.option_name) .await; @@ -389,7 +389,7 @@ pub(crate) async fn insert_or_update_select_option_handler( manager: AFPluginState>, ) -> Result<(), FlowyError> { let params = data.into_inner(); - let database_editor = manager.get_database(¶ms.view_id).await?; + let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; database_editor .insert_select_options( ¶ms.view_id, @@ -407,7 +407,7 @@ pub(crate) async fn delete_select_option_handler( manager: AFPluginState>, ) -> Result<(), FlowyError> { let params = data.into_inner(); - let database_editor = manager.get_database(¶ms.view_id).await?; + let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; database_editor .delete_select_options( ¶ms.view_id, @@ -425,7 +425,7 @@ pub(crate) async fn get_select_option_handler( manager: AFPluginState>, ) -> DataResult { let params: CellIdParams = data.into_inner().try_into()?; - let database_editor = manager.get_database(¶ms.view_id).await?; + let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; let options = database_editor .get_select_options(params.row_id, ¶ms.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(¶ms.cell_identifier.view_id) + .get_database_with_view_id(¶ms.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>, ) -> DataResult { 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>, ) -> DataResult { let params: DatabaseGroupIdParams = data.into_inner().try_into()?; - let database_editor = manager.get_database(¶ms.view_id).await?; + let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; let group = database_editor .get_group(¶ms.view_id, ¶ms.group_id) .await?; @@ -511,7 +511,7 @@ pub(crate) async fn move_group_handler( manager: AFPluginState>, ) -> FlowyResult<()> { let params: MoveGroupParams = data.into_inner().try_into()?; - let database_editor = manager.get_database(¶ms.view_id).await?; + let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; database_editor .move_group(¶ms.view_id, ¶ms.from_group_id, ¶ms.to_group_id) .await?; @@ -524,7 +524,7 @@ pub(crate) async fn move_group_row_handler( manager: AFPluginState>, ) -> FlowyResult<()> { let params: MoveGroupRowParams = data.into_inner().try_into()?; - let database_editor = manager.get_database(¶ms.view_id).await?; + let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; database_editor .move_group_row( ¶ms.view_id, @@ -550,7 +550,7 @@ pub(crate) async fn set_layout_setting_handler( manager: AFPluginState>, ) -> FlowyResult<()> { let params: LayoutSettingChangeset = data.into_inner().try_into()?; - let database_editor = manager.get_database(¶ms.view_id).await?; + let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; let layout_params = LayoutSettingParams { calendar: params.calendar, }; @@ -565,7 +565,7 @@ pub(crate) async fn get_layout_setting_handler( manager: AFPluginState>, ) -> DataResult { let params: DatabaseLayoutId = data.into_inner().try_into()?; - let database_editor = manager.get_database(¶ms.view_id).await?; + let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; let layout_setting_pb = database_editor .get_layout_setting(¶ms.view_id, params.layout) .await @@ -580,7 +580,7 @@ pub(crate) async fn get_calendar_events_handler( manager: AFPluginState>, ) -> DataResult { let params: CalendarEventRequestParams = data.into_inner().try_into()?; - let database_editor = manager.get_database(¶ms.view_id).await?; + let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; let events = database_editor .get_all_calendar_events(¶ms.view_id) .await; @@ -593,7 +593,7 @@ pub(crate) async fn get_calendar_event_handler( manager: AFPluginState>, ) -> DataResult { let params: RowIdParams = data.into_inner().try_into()?; - let database_editor = manager.get_database(¶ms.view_id).await?; + let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; let event = database_editor .get_calendar_event(¶ms.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, + manager: AFPluginState>, +) -> 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(()) +} diff --git a/frontend/rust-lib/flowy-database2/src/event_map.rs b/frontend/rust-lib/flowy-database2/src/event_map.rs index 3f0261b664..ab73c79b53 100644 --- a/frontend/rust-lib/flowy-database2/src/event_map.rs +++ b/frontend/rust-lib/flowy-database2/src/event_map.rs @@ -58,7 +58,9 @@ pub fn init(database_manager: Arc) -> 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, } diff --git a/frontend/rust-lib/flowy-database2/src/manager.rs b/frontend/rust-lib/flowy-database2/src/manager.rs index 9a2468da23..0ba9f2d5bd 100644 --- a/frontend/rust-lib/flowy-database2/src/manager.rs +++ b/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; @@ -77,14 +78,17 @@ impl DatabaseManager2 { } } - pub async fn get_database(&self, view_id: &str) -> FlowyResult> { + pub async fn get_database_with_view_id(&self, view_id: &str) -> FlowyResult> { 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> { + 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 { + 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 { + let database = self.get_database_with_view_id(view_id).await?; + database.export_csv(style).await + } + fn with_user_database(&self, default_value: Output, f: F) -> Output where F: FnOnce(&InnerUserDatabase) -> Output, diff --git a/frontend/rust-lib/flowy-database2/src/services/cell/cell_operation.rs b/frontend/rust-lib/flowy-database2/src/services/cell/cell_operation.rs index 9a4713df40..e8c6975f10 100644 --- a/frontend/rust-lib/flowy-database2/src/services/cell/cell_operation.rs +++ b/frontend/rust-lib/flowy-database2/src/services/cell/cell_operation.rs @@ -74,7 +74,7 @@ pub fn apply_cell_changeset( } } -pub fn get_type_cell_protobuf( +pub fn get_cell_protobuf( cell: &Cell, field: &Field, cell_cache: Option, @@ -101,25 +101,6 @@ pub fn get_type_cell_protobuf( } } -pub fn get_type_cell_data( - cell: &Cell, - field: &Field, - cell_data_cache: Option, -) -> Option -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( /// /// * `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, + field_maps: HashMap, } -impl CellBuilder { - pub fn with_cells(cell_by_field_id: HashMap, fields: Vec) -> Self { +impl<'a> CellBuilder<'a> { + pub fn with_cells(cell_by_field_id: HashMap, fields: &'a [Field]) -> Self { let field_maps = fields .into_iter() .map(|field| (field.id.clone(), field)) - .collect::>(); + .collect::>(); let mut cells = Cells::new(); for (field_id, cell_str) in cell_by_field_id { diff --git a/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs b/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs index 8789ae985a..219cc2b6f8 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs +++ b/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> { let fields = self.database.lock().get_fields(¶ms.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, ¶ms.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 { + 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::(csv) + }) + .await + .map_err(internal_error)??; + Ok(csv) + } } pub(crate) async fn notify_did_update_cell(changesets: Vec) { diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/checkbox_type_option/checkbox_type_option.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/checkbox_type_option/checkbox_type_option.rs index 1922da36a2..6aca5e90a1 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/checkbox_type_option/checkbox_type_option.rs +++ b/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<::CellData> { - if _decoded_field_type.is_text() { + if transformed_field_type.is_text() { Some(CheckboxCellData::from(cell)) } else { None diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/select_type_option.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/select_type_option.rs index b2f889c303..a5b6ef84ab 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/select_type_option.rs +++ b/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<::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(); diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/text_type_option/text_type_option.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/text_type_option/text_type_option.rs index d512b5f3ec..f38ba39d75 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/text_type_option/text_type_option.rs +++ b/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<::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 { diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/type_option.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/type_option.rs index b69920d740..bc5123cfc7 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/type_option.rs +++ b/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<::CellData> { None diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/type_option_cell.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/type_option_cell.rs index 6f4dd799a6..ab15bc4504 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/type_option_cell.rs +++ b/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; } @@ -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 { // tracing::debug!("get_cell_data: {:?}", std::any::type_name::()); 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)) } diff --git a/frontend/rust-lib/flowy-database2/src/services/group/controller.rs b/frontend/rust-lib/flowy-database2/src/services/group/controller.rs index bf7d5949fe..7b986cde20 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/controller.rs +++ b/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 = 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::

()?; 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::

()?; 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::

()?; 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( field: &Field, ) -> Option { 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::

().ok() } diff --git a/frontend/rust-lib/flowy-database2/src/services/mod.rs b/frontend/rust-lib/flowy-database2/src/services/mod.rs index 28e4810696..5279e8202f 100644 --- a/frontend/rust-lib/flowy-database2/src/services/mod.rs +++ b/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; diff --git a/frontend/rust-lib/flowy-database2/src/services/share/csv/export.rs b/frontend/rust-lib/flowy-database2/src/services/share/csv/export.rs new file mode 100644 index 0000000000..5bc1999334 --- /dev/null +++ b/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 { + 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::>(); + 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::>(); + 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::>(); + + 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) + } +} diff --git a/frontend/rust-lib/flowy-database2/src/services/share/csv/import.rs b/frontend/rust-lib/flowy-database2/src/services/share/csv/import.rs new file mode 100644 index 0000000000..2c8e8b70a0 --- /dev/null +++ b/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 { + 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 { + 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 { + let mut fields: Vec = 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::>() + }) + .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::>(); + + 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::>(); + + 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, + rows: Vec>, +} +impl FieldsRows { + fn split(self) -> (Vec, Vec>) { + (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); + } +} diff --git a/frontend/rust-lib/flowy-database2/src/services/share/csv/mod.rs b/frontend/rust-lib/flowy-database2/src/services/share/csv/mod.rs new file mode 100644 index 0000000000..7e946037fe --- /dev/null +++ b/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::*; diff --git a/frontend/rust-lib/flowy-database2/src/services/share/mod.rs b/frontend/rust-lib/flowy-database2/src/services/share/mod.rs new file mode 100644 index 0000000000..d4996f0bb5 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/share/mod.rs @@ -0,0 +1 @@ +pub mod csv; diff --git a/frontend/rust-lib/flowy-database2/tests/database/database_editor.rs b/frontend/rust-lib/flowy-database2/tests/database/database_editor.rs index 7976c72039..46867267d4 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/database_editor.rs +++ b/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> { + self + .sdk + .database_manager + .get_database(database_id) + .await + .ok() + } } -pub struct TestRowBuilder { +pub struct TestRowBuilder<'a> { row_id: RowId, - fields: Vec, - cell_build: CellBuilder, + fields: &'a [Field], + cell_build: CellBuilder<'a>, } -impl TestRowBuilder { - pub fn new(row_id: RowId, fields: Vec) -> 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, } } diff --git a/frontend/rust-lib/flowy-database2/tests/database/mock_data/board_mock_data.rs b/frontend/rust-lib/flowy-database2/tests/database/mock_data/board_mock_data.rs index a87de81b6f..a968ee6e06 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/mock_data/board_mock_data.rs +++ b/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() { diff --git a/frontend/rust-lib/flowy-database2/tests/database/mock_data/calendar_mock_data.rs b/frontend/rust-lib/flowy-database2/tests/database/mock_data/calendar_mock_data.rs index a0d41734e8..a3e8972943 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/mock_data/calendar_mock_data.rs +++ b/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() { diff --git a/frontend/rust-lib/flowy-database2/tests/database/mock_data/grid_mock_data.rs b/frontend/rust-lib/flowy-database2/tests/database/mock_data/grid_mock_data.rs index 800e1ce550..c064d8f479 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/mock_data/grid_mock_data.rs +++ b/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() { diff --git a/frontend/rust-lib/flowy-database2/tests/database/mod.rs b/frontend/rust-lib/flowy-database2/tests/database/mod.rs index 0e00cd5769..fdb44b3dcd 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/mod.rs +++ b/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; diff --git a/frontend/rust-lib/flowy-database2/tests/database/share_test/export_test.rs b/frontend/rust-lib/flowy-database2/tests/database/share_test/export_test.rs new file mode 100644 index 0000000000..0f8c000ce9 --- /dev/null +++ b/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::>()) + .collect::>(); + let mut b = export_csv_records_2 + .map(|v| v.unwrap()) + .flat_map(|v| v.iter().map(|v| v.to_string()).collect::>()) + .collect::>(); + a.sort(); + b.sort(); + assert_eq!(a, b); +} diff --git a/frontend/rust-lib/flowy-database2/tests/database/share_test/mod.rs b/frontend/rust-lib/flowy-database2/tests/database/share_test/mod.rs new file mode 100644 index 0000000000..c39a8afb9e --- /dev/null +++ b/frontend/rust-lib/flowy-database2/tests/database/share_test/mod.rs @@ -0,0 +1 @@ +mod export_test;