mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
Feat: import and export csv (#2617)
* feat: import csv * feat: export * chore: implement import export csv in the backend * chore: patch
This commit is contained in:
parent
2746666123
commit
70bb7f2ad6
22
frontend/appflowy_tauri/src-tauri/Cargo.lock
generated
22
frontend/appflowy_tauri/src-tauri/Cargo.lock
generated
@ -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",
|
||||
|
@ -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" }
|
||||
|
42
frontend/rust-lib/Cargo.lock
generated
42
frontend/rust-lib/Cargo.lock
generated
@ -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",
|
||||
|
@ -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" }
|
||||
|
@ -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"
|
||||
|
@ -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::*;
|
||||
|
@ -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,
|
||||
}
|
@ -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(¶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<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(¶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<Arc<DatabaseManager2>>,
|
||||
) -> 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<Arc<DatabaseManager2>>,
|
||||
) -> 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<Arc<DatabaseManager2>>,
|
||||
) -> 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<Arc<DatabaseManager2>>,
|
||||
) -> 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<Arc<DatabaseManager2>>,
|
||||
) -> 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<Arc<DatabaseManager2>>,
|
||||
) -> DataResult<TypeOptionPB, FlowyError> {
|
||||
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<Arc<DatabaseManager2>>,
|
||||
) -> DataResult<TypeOptionPB, FlowyError> {
|
||||
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<Arc<DatabaseManager2>>,
|
||||
) -> 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<Arc<DatabaseManager2>>,
|
||||
) -> DataResult<OptionalRowPB, 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?;
|
||||
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<Arc<DatabaseManager2>>,
|
||||
) -> 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<Arc<DatabaseManager2>>,
|
||||
) -> 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<Arc<DatabaseManager2>>,
|
||||
) -> 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<Arc<DatabaseManager2>>,
|
||||
) -> DataResult<RowPB, FlowyError> {
|
||||
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<Arc<DatabaseManager2>>,
|
||||
) -> DataResult<CellPB, FlowyError> {
|
||||
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<Arc<DatabaseManager2>>,
|
||||
) -> 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<Arc<DatabaseManager2>>,
|
||||
) -> DataResult<SelectOptionPB, FlowyError> {
|
||||
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<Arc<DatabaseManager2>>,
|
||||
) -> 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<Arc<DatabaseManager2>>,
|
||||
) -> 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<Arc<DatabaseManager2>>,
|
||||
) -> DataResult<SelectOptionCellDataPB, FlowyError> {
|
||||
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<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(¶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<Arc<DatabaseManager2>>,
|
||||
) -> 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<Arc<DatabaseManager2>>,
|
||||
) -> 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<Arc<DatabaseManager2>>,
|
||||
) -> 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<Arc<DatabaseManager2>>,
|
||||
) -> DataResult<LayoutSettingPB, FlowyError> {
|
||||
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<Arc<DatabaseManager2>>,
|
||||
) -> DataResult<RepeatedCalendarEventPB, FlowyError> {
|
||||
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<Arc<DatabaseManager2>>,
|
||||
) -> DataResult<CalendarEventPB, 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?;
|
||||
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<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(())
|
||||
}
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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 {
|
||||
|
@ -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(¶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<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>) {
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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))
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -5,4 +5,5 @@ pub mod field;
|
||||
pub mod filter;
|
||||
pub mod group;
|
||||
pub mod setting;
|
||||
pub mod share;
|
||||
pub mod sort;
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
mod export;
|
||||
mod import;
|
||||
|
||||
pub use export::*;
|
||||
pub use import::*;
|
@ -0,0 +1 @@
|
||||
pub mod csv;
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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() {
|
||||
|
@ -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() {
|
||||
|
@ -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() {
|
||||
|
@ -7,3 +7,4 @@ mod layout_test;
|
||||
mod sort_test;
|
||||
|
||||
mod mock_data;
|
||||
mod share_test;
|
||||
|
@ -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);
|
||||
}
|
@ -0,0 +1 @@
|
||||
mod export_test;
|
Loading…
Reference in New Issue
Block a user