From ce8cee56379e67c1ffacee16dfe6e110d8e515ca Mon Sep 17 00:00:00 2001 From: "Nathan.fooo" <86001920+appflowy@users.noreply.github.com> Date: Wed, 7 Jun 2023 14:52:35 +0800 Subject: [PATCH] test: add databaase event test (#2728) * test: add tests and fix modify primary field bug * test: add more test * fix: tauri buiuld * chore: disable share link button --- .../cell/cell_data_persistence.dart | 6 +- .../application/field/field_service.dart | 5 - .../header/field_cell_action_sheet.dart | 18 +- .../card/bloc/checkbox_card_cell_bloc.dart | 1 + .../presentation/share/share_button.dart | 17 +- .../filter/edit_filter_field_test.dart | 57 ---- frontend/appflowy_tauri/src-tauri/Cargo.lock | 31 +- .../effects/database/field/field_bd_svc.ts | 12 +- frontend/rust-lib/Cargo.lock | 1 + .../flowy-database/src/services/export.rs | 182 ----------- .../src/entities/field_entities.rs | 20 +- .../flowy-database2/src/event_handler.rs | 2 +- .../src/services/database/database_editor.rs | 69 ++++- .../checkbox_type_option.rs | 4 +- .../tests/database/field_test/test.rs | 24 -- .../flowy-folder2/src/event_handler.rs | 14 +- .../rust-lib/flowy-folder2/src/event_map.rs | 2 +- frontend/rust-lib/flowy-test/Cargo.toml | 1 + frontend/rust-lib/flowy-test/src/lib.rs | 210 ++++++++++++- .../flowy-test/tests/database/test.rs | 290 ++++++++++++++++++ 20 files changed, 623 insertions(+), 343 deletions(-) delete mode 100644 frontend/appflowy_flutter/test/bloc_test/grid_test/filter/edit_filter_field_test.dart delete mode 100644 frontend/rust-lib/flowy-database/src/services/export.rs diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_data_persistence.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_data_persistence.dart index 8b039bd31f..d20ede7906 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_data_persistence.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_data_persistence.dart @@ -16,8 +16,10 @@ class TextCellDataPersistence implements CellDataPersistence { @override Future> save(String data) async { - final fut = - _cellBackendSvc.updateCell(cellContext: cellContext, data: data); + final fut = _cellBackendSvc.updateCell( + cellContext: cellContext, + data: data, + ); return fut.then((result) { return result.fold( (l) => none(), diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_service.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_service.dart index 83bfcf218e..4bd81c7672 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_service.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_service.dart @@ -29,7 +29,6 @@ class FieldBackendService { Future> updateField({ String? name, - FieldType? fieldType, bool? frozen, bool? visibility, double? width, @@ -42,10 +41,6 @@ class FieldBackendService { payload.name = name; } - if (fieldType != null) { - payload.fieldType = fieldType; - } - if (frozen != null) { payload.frozen = frozen; } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_cell_action_sheet.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_cell_action_sheet.dart index e54c578a5a..d7ba6dbf80 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_cell_action_sheet.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_cell_action_sheet.dart @@ -124,13 +124,29 @@ class _FieldOperationList extends StatelessWidget { } Widget _actionCell(FieldAction action) { + bool enable = true; + + // If the field is primary, delete and duplicate are disabled. + if (fieldInfo.field.isPrimary) { + switch (action) { + case FieldAction.hide: + break; + case FieldAction.duplicate: + enable = false; + break; + case FieldAction.delete: + enable = false; + break; + } + } + return Flexible( child: SizedBox( height: GridSize.popoverItemHeight, child: FieldActionCell( fieldInfo: fieldInfo, action: action, - enable: action != FieldAction.delete || !fieldInfo.field.isPrimary, + enable: enable, ), ), ); diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/bloc/checkbox_card_cell_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/bloc/checkbox_card_cell_bloc.dart index 809f57a390..3cf0ff329d 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/bloc/checkbox_card_cell_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/bloc/checkbox_card_cell_bloc.dart @@ -72,5 +72,6 @@ class CheckboxCardCellState with _$CheckboxCardCellState { } bool _isSelected(String? cellData) { + // The backend use "Yes" and "No" to represent the checkbox cell data. return cellData == "Yes"; } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/share/share_button.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/share/share_button.dart index 76c2af030e..00d2079b7d 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/share/share_button.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/share/share_button.dart @@ -4,7 +4,6 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/plugins/document/application/share_bloc.dart'; import 'package:appflowy/workspace/presentation/home/toast.dart'; -import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; import 'package:appflowy_backend/protobuf/flowy-document2/entities.pb.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; @@ -104,11 +103,11 @@ class ShareActionList extends StatelessWidget { showMessageToast('Exported to: $exportPath'); } break; - case ShareAction.copyLink: - NavigatorAlertDialog( - title: LocaleKeys.shareAction_workInProgress.tr(), - ).show(context); - break; + // case ShareAction.copyLink: + // NavigatorAlertDialog( + // title: LocaleKeys.shareAction_workInProgress.tr(), + // ).show(context); + // break; } controller.close(); }, @@ -118,7 +117,7 @@ class ShareActionList extends StatelessWidget { enum ShareAction { markdown, - copyLink, + // copyLink, } class ShareActionWrapper extends ActionCell { @@ -133,8 +132,8 @@ class ShareActionWrapper extends ActionCell { switch (inner) { case ShareAction.markdown: return LocaleKeys.shareAction_markdown.tr(); - case ShareAction.copyLink: - return LocaleKeys.shareAction_copyLink.tr(); + // case ShareAction.copyLink: + // return LocaleKeys.shareAction_copyLink.tr(); } } } diff --git a/frontend/appflowy_flutter/test/bloc_test/grid_test/filter/edit_filter_field_test.dart b/frontend/appflowy_flutter/test/bloc_test/grid_test/filter/edit_filter_field_test.dart deleted file mode 100644 index 075677f0cf..0000000000 --- a/frontend/appflowy_flutter/test/bloc_test/grid_test/filter/edit_filter_field_test.dart +++ /dev/null @@ -1,57 +0,0 @@ -import 'package:appflowy/plugins/database_view/application/field/field_editor_bloc.dart'; -import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart'; -import 'package:appflowy/plugins/database_view/application/filter/filter_service.dart'; -import 'package:appflowy/plugins/database_view/grid/application/filter/filter_menu_bloc.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/text_filter.pb.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import '../util.dart'; - -void main() { - late AppFlowyGridTest gridTest; - setUpAll(() async { - gridTest = await AppFlowyGridTest.ensureInitialized(); - }); - - test("create a text filter and then alter the filter's field)", () async { - final context = await gridTest.createTestGrid(); - final service = FilterBackendService(viewId: context.gridView.id); - final textField = context.textFieldContext(); - - // Create the filter menu bloc - final menuBloc = GridFilterMenuBloc( - fieldController: context.fieldController, - viewId: context.gridView.id, - )..add(const GridFilterMenuEvent.initial()); - - // Insert filter for the text field - await service.insertTextFilter( - fieldId: textField.id, - condition: TextFilterConditionPB.TextIsEmpty, - content: "", - ); - await gridResponseFuture(); - assert(menuBloc.state.filters.length == 1); - - // Edit the text field - final loader = FieldTypeOptionLoader( - viewId: context.gridView.id, - field: textField.field, - ); - - final editorBloc = FieldEditorBloc( - isGroupField: false, - loader: loader, - field: textField.field, - )..add(const FieldEditorEvent.initial()); - await gridResponseFuture(); - - // Alter the field type to Number - editorBloc.add(const FieldEditorEvent.switchToField(FieldType.Number)); - await gridResponseFuture(); - - // Check the number of filters - assert(menuBloc.state.filters.isEmpty); - }); -} diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.lock b/frontend/appflowy_tauri/src-tauri/Cargo.lock index ad9e7a2f14..7b78c4f5ee 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.lock +++ b/frontend/appflowy_tauri/src-tauri/Cargo.lock @@ -99,7 +99,7 @@ checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" [[package]] name = "appflowy-integrate" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cbc2e0#cbc2e0acb8420dc997921bb3f56b99f9975c2aab" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a647d9#a647d922ef432510d6be0abb5f968d9a75dc7011" dependencies = [ "anyhow", "collab", @@ -1024,7 +1024,7 @@ dependencies = [ [[package]] name = "collab" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cbc2e0#cbc2e0acb8420dc997921bb3f56b99f9975c2aab" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a647d9#a647d922ef432510d6be0abb5f968d9a75dc7011" dependencies = [ "anyhow", "bytes", @@ -1042,7 +1042,7 @@ dependencies = [ [[package]] name = "collab-client-ws" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cbc2e0#cbc2e0acb8420dc997921bb3f56b99f9975c2aab" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a647d9#a647d922ef432510d6be0abb5f968d9a75dc7011" dependencies = [ "bytes", "collab-sync", @@ -1060,7 +1060,7 @@ dependencies = [ [[package]] name = "collab-database" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cbc2e0#cbc2e0acb8420dc997921bb3f56b99f9975c2aab" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a647d9#a647d922ef432510d6be0abb5f968d9a75dc7011" dependencies = [ "anyhow", "async-trait", @@ -1086,7 +1086,7 @@ dependencies = [ [[package]] name = "collab-derive" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cbc2e0#cbc2e0acb8420dc997921bb3f56b99f9975c2aab" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a647d9#a647d922ef432510d6be0abb5f968d9a75dc7011" dependencies = [ "proc-macro2", "quote", @@ -1098,7 +1098,7 @@ dependencies = [ [[package]] name = "collab-document" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cbc2e0#cbc2e0acb8420dc997921bb3f56b99f9975c2aab" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a647d9#a647d922ef432510d6be0abb5f968d9a75dc7011" dependencies = [ "anyhow", "collab", @@ -1115,7 +1115,7 @@ dependencies = [ [[package]] name = "collab-folder" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cbc2e0#cbc2e0acb8420dc997921bb3f56b99f9975c2aab" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a647d9#a647d922ef432510d6be0abb5f968d9a75dc7011" dependencies = [ "anyhow", "collab", @@ -1134,7 +1134,7 @@ dependencies = [ [[package]] name = "collab-persistence" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cbc2e0#cbc2e0acb8420dc997921bb3f56b99f9975c2aab" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a647d9#a647d922ef432510d6be0abb5f968d9a75dc7011" dependencies = [ "bincode", "chrono", @@ -1154,7 +1154,7 @@ dependencies = [ [[package]] name = "collab-plugins" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cbc2e0#cbc2e0acb8420dc997921bb3f56b99f9975c2aab" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a647d9#a647d922ef432510d6be0abb5f968d9a75dc7011" dependencies = [ "anyhow", "async-trait", @@ -1173,6 +1173,7 @@ dependencies = [ "rusoto_credential", "serde", "serde_json", + "similar 2.2.1", "thiserror", "tokio", "tokio-retry", @@ -1184,7 +1185,7 @@ dependencies = [ [[package]] name = "collab-sync" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cbc2e0#cbc2e0acb8420dc997921bb3f56b99f9975c2aab" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a647d9#a647d922ef432510d6be0abb5f968d9a75dc7011" dependencies = [ "bytes", "collab", @@ -1789,7 +1790,7 @@ dependencies = [ "quote", "serde", "serde_json", - "similar", + "similar 1.3.0", "syn 1.0.109", "tera", "toml 0.5.11", @@ -1818,6 +1819,7 @@ version = "0.1.0" dependencies = [ "appflowy-integrate", "bytes", + "diesel", "flowy-config", "flowy-database2", "flowy-document2", @@ -1839,6 +1841,7 @@ dependencies = [ "serde_repr", "tokio", "tracing", + "uuid", ] [[package]] @@ -5055,6 +5058,12 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ad1d488a557b235fc46dae55512ffbfc429d2482b08b4d9435ab07384ca8aec" +[[package]] +name = "similar" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "420acb44afdae038210c99e69aae24109f32f15500aa708e81d46c9f29d55fcf" + [[package]] name = "siphasher" version = "0.3.10" diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/field/field_bd_svc.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/field/field_bd_svc.ts index 85ba2a599f..3580aece0b 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/field/field_bd_svc.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/field/field_bd_svc.ts @@ -21,23 +21,13 @@ export abstract class TypeOptionParser { export class FieldBackendService { constructor(public readonly viewId: string, public readonly fieldId: string) {} - updateField = (data: { - name?: string; - fieldType?: FieldType; - frozen?: boolean; - visibility?: boolean; - width?: number; - }) => { + updateField = (data: { name?: string; frozen?: boolean; visibility?: boolean; width?: number }) => { const payload = FieldChangesetPB.fromObject({ view_id: this.viewId, field_id: this.fieldId }); if (data.name !== undefined) { payload.name = data.name; } - if (data.fieldType !== undefined) { - payload.field_type = data.fieldType; - } - if (data.frozen !== undefined) { payload.frozen = data.frozen; } diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 3824215f5e..21a1555671 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -1864,6 +1864,7 @@ dependencies = [ "bytes", "dotenv", "flowy-core", + "flowy-database2", "flowy-folder2", "flowy-net", "flowy-notification", diff --git a/frontend/rust-lib/flowy-database/src/services/export.rs b/frontend/rust-lib/flowy-database/src/services/export.rs deleted file mode 100644 index 1884a7298a..0000000000 --- a/frontend/rust-lib/flowy-database/src/services/export.rs +++ /dev/null @@ -1,182 +0,0 @@ -use crate::entities::FieldType; - -use crate::services::cell::TypeCellData; -use crate::services::database::DatabaseEditor; -use crate::services::field::{ - CheckboxTypeOptionPB, ChecklistTypeOptionPB, DateCellData, DateTypeOptionPB, - MultiSelectTypeOptionPB, NumberTypeOptionPB, RichTextTypeOptionPB, SingleSelectTypeOptionPB, - URLCellData, -}; -use database_model::{FieldRevision, TypeOptionDataDeserializer}; -use flowy_error::{FlowyError, FlowyResult}; -use indexmap::IndexMap; -use serde::Serialize; -use serde_json::{json, Map, Value}; -use std::collections::HashMap; - -use std::sync::Arc; - -#[derive(Debug, Clone, Serialize)] -pub struct ExportField { - pub id: String, - pub name: String, - pub field_type: i64, - pub visibility: bool, - pub width: i64, - pub type_options: HashMap, - pub is_primary: bool, -} - -#[derive(Debug, Clone, Serialize)] -struct ExportCell { - data: String, - field_type: FieldType, -} - -impl From<&Arc> for ExportField { - fn from(field_rev: &Arc) -> Self { - let field_type = FieldType::from(field_rev.ty); - let mut type_options: HashMap = HashMap::new(); - - field_rev - .type_options - .iter() - .filter(|(k, _)| k == &&field_rev.ty.to_string()) - .for_each(|(k, s)| { - let value = match field_type { - FieldType::RichText => { - let pb = RichTextTypeOptionPB::from_json_str(s); - serde_json::to_value(pb).unwrap() - }, - FieldType::Number => { - let pb = NumberTypeOptionPB::from_json_str(s); - let mut map = Map::new(); - map.insert("format".to_string(), json!(pb.format as u8)); - map.insert("scale".to_string(), json!(pb.scale)); - map.insert("symbol".to_string(), json!(pb.symbol)); - map.insert("name".to_string(), json!(pb.name)); - Value::Object(map) - }, - FieldType::DateTime => { - let pb = DateTypeOptionPB::from_json_str(s); - let mut map = Map::new(); - map.insert("date_format".to_string(), json!(pb.date_format as u8)); - map.insert("time_format".to_string(), json!(pb.time_format as u8)); - map.insert("field_type".to_string(), json!(FieldType::DateTime as u8)); - Value::Object(map) - }, - FieldType::SingleSelect => { - let pb = SingleSelectTypeOptionPB::from_json_str(s); - let value = serde_json::to_string(&pb).unwrap(); - let mut map = Map::new(); - map.insert("content".to_string(), Value::String(value)); - Value::Object(map) - }, - FieldType::MultiSelect => { - let pb = MultiSelectTypeOptionPB::from_json_str(s); - let value = serde_json::to_string(&pb).unwrap(); - let mut map = Map::new(); - map.insert("content".to_string(), Value::String(value)); - Value::Object(map) - }, - FieldType::Checkbox => { - let pb = CheckboxTypeOptionPB::from_json_str(s); - serde_json::to_value(pb).unwrap() - }, - FieldType::URL => { - let pb = RichTextTypeOptionPB::from_json_str(s); - serde_json::to_value(pb).unwrap() - }, - FieldType::Checklist => { - let pb = ChecklistTypeOptionPB::from_json_str(s); - let value = serde_json::to_string(&pb).unwrap(); - let mut map = Map::new(); - map.insert("content".to_string(), Value::String(value)); - Value::Object(map) - }, - }; - type_options.insert(k.clone(), value); - }); - Self { - id: field_rev.id.clone(), - name: field_rev.name.clone(), - field_type: field_rev.ty as i64, - visibility: true, - width: 100, - type_options, - is_primary: field_rev.is_primary, - } - } -} - -pub struct CSVExport; -impl CSVExport { - pub async fn export_database( - &self, - view_id: &str, - database_editor: &Arc, - ) -> FlowyResult { - let mut wtr = csv::Writer::from_writer(vec![]); - let row_revs = database_editor.get_all_row_revs(view_id).await?; - let field_revs = database_editor.get_field_revs(None).await?; - - // Write fields - let field_records = field_revs - .iter() - .map(|field| ExportField::from(field)) - .map(|field| serde_json::to_string(&field).unwrap()) - .collect::>(); - - wtr - .write_record(&field_records) - .map_err(|e| FlowyError::internal().context(e))?; - - // Write rows - let mut field_by_field_id = IndexMap::new(); - field_revs.into_iter().for_each(|field| { - field_by_field_id.insert(field.id.clone(), field); - }); - for row_rev in row_revs { - let cells = field_by_field_id - .iter() - .map(|(field_id, field)| { - let field_type = FieldType::from(field.ty); - let data = row_rev - .cells - .get(field_id) - .map(|cell| TypeCellData::try_from(cell)) - .map(|data| { - data - .map(|data| match field_type { - FieldType::DateTime => { - match serde_json::from_str::(&data.cell_str) { - Ok(cell_data) => cell_data.timestamp.unwrap_or_default().to_string(), - Err(_) => "".to_string(), - } - }, - FieldType::URL => match serde_json::from_str::(&data.cell_str) { - Ok(cell_data) => cell_data.content, - Err(_) => "".to_string(), - }, - _ => data.cell_str, - }) - .unwrap_or_default() - }) - .unwrap_or_else(|| "".to_string()); - let cell = ExportCell { data, field_type }; - serde_json::to_string(&cell).unwrap() - }) - .collect::>(); - - 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/entities/field_entities.rs b/frontend/rust-lib/flowy-database2/src/entities/field_entities.rs index b5235c326a..902ef6097d 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/field_entities.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/field_entities.rs @@ -155,6 +155,8 @@ pub struct CreateFieldPayloadPB { #[pb(index = 2)] pub field_type: FieldType, + /// If the type_option_data is not empty, it will be used to create the field. + /// Otherwise, the default value will be used. #[pb(index = 3, one_of)] pub type_option_data: Option>, } @@ -163,6 +165,8 @@ pub struct CreateFieldPayloadPB { pub struct CreateFieldParams { pub view_id: String, pub field_type: FieldType, + /// If the type_option_data is not empty, it will be used to create the field. + /// Otherwise, the default value will be used. pub type_option_data: Option>, } @@ -189,9 +193,6 @@ pub struct UpdateFieldTypePayloadPB { #[pb(index = 3)] pub field_type: FieldType, - - #[pb(index = 4)] - pub create_if_not_exist: bool, } pub struct EditFieldParams { @@ -401,18 +402,13 @@ pub struct FieldChangesetPB { pub desc: Option, #[pb(index = 5, one_of)] - pub field_type: Option, - - #[pb(index = 6, one_of)] pub frozen: Option, - #[pb(index = 7, one_of)] + #[pb(index = 6, one_of)] pub visibility: Option, - #[pb(index = 8, one_of)] + #[pb(index = 7, one_of)] pub width: Option, - // #[pb(index = 9, one_of)] - // pub type_option_data: Option>, } impl TryInto for FieldChangesetPB { @@ -421,7 +417,6 @@ impl TryInto for FieldChangesetPB { fn try_into(self) -> Result { let view_id = NotEmptyStr::parse(self.view_id).map_err(|_| ErrorCode::DatabaseIdIsEmpty)?; let field_id = NotEmptyStr::parse(self.field_id).map_err(|_| ErrorCode::FieldIdIsEmpty)?; - let field_type = self.field_type.map(FieldType::from); // if let Some(type_option_data) = self.type_option_data.as_ref() { // if type_option_data.is_empty() { // return Err(ErrorCode::TypeOptionDataIsEmpty); @@ -433,7 +428,6 @@ impl TryInto for FieldChangesetPB { view_id: view_id.0, name: self.name, desc: self.desc, - field_type, frozen: self.frozen, visibility: self.visibility, width: self.width, @@ -452,8 +446,6 @@ pub struct FieldChangesetParams { pub desc: Option, - pub field_type: Option, - pub frozen: Option, pub visibility: Option, diff --git a/frontend/rust-lib/flowy-database2/src/event_handler.rs b/frontend/rust-lib/flowy-database2/src/event_handler.rs index 9e05fe860f..b3e7cfbe42 100644 --- a/frontend/rust-lib/flowy-database2/src/event_handler.rs +++ b/frontend/rust-lib/flowy-database2/src/event_handler.rs @@ -249,7 +249,7 @@ pub(crate) async fn get_field_type_option_data_handler( } } -/// Create FieldMeta and save it. Return the FieldTypeOptionData. +/// Create TypeOptionPB and save it. Return the FieldTypeOptionData. #[tracing::instrument(level = "trace", skip(data, manager), err)] pub(crate) async fn create_field_type_option_data_handler( data: AFPluginData, 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 7ebbd46520..7ecfb2ada6 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::{internal_error, FlowyError, FlowyResult}; +use flowy_error::{internal_error, ErrorCode, FlowyError, FlowyResult}; use flowy_task::TaskDispatcher; use lib_infra::future::{to_fut, Fut}; @@ -199,32 +199,32 @@ impl DatabaseEditor { } } + /// Returns a list of fields of the view. + /// If `field_ids` is not provided, all the fields will be returned in the order of the field that + /// defined in the view. Otherwise, the fields will be returned in the order of the `field_ids`. pub fn get_fields(&self, view_id: &str, field_ids: Option>) -> Vec { - self.database.lock().get_fields(view_id, field_ids) + let database = self.database.lock(); + let field_ids = field_ids.unwrap_or_else(|| { + database + .fields + .get_all_field_orders() + .into_iter() + .map(|field| field.id) + .collect() + }); + database.get_fields(view_id, Some(field_ids)) } pub async fn update_field(&self, params: FieldChangesetParams) -> FlowyResult<()> { - let is_primary = self - .database - .lock() - .fields - .get_field(¶ms.field_id) - .map(|field| field.is_primary) - .unwrap_or(false); self .database .lock() .fields - .update_field(¶ms.field_id, |mut update| { - update = update + .update_field(¶ms.field_id, |update| { + update .set_name_if_not_none(params.name) .set_width_at_if_not_none(params.width.map(|value| value as i64)) .set_visibility_if_not_none(params.visibility); - if is_primary { - tracing::warn!("Cannot update primary field type"); - } else { - update.set_field_type_if_not_none(params.field_type.map(|field_type| field_type.into())); - } }); self .notify_did_update_database_field(¶ms.field_id) @@ -233,6 +233,21 @@ impl DatabaseEditor { } pub async fn delete_field(&self, field_id: &str) -> FlowyResult<()> { + let is_primary = self + .database + .lock() + .fields + .get_field(field_id) + .map(|field| field.is_primary) + .unwrap_or(false); + + if is_primary { + return Err(FlowyError::new( + ErrorCode::Internal, + "Can not delete primary field", + )); + } + let database_id = { let database = self.database.lock(); database.delete_field(field_id); @@ -283,6 +298,13 @@ impl DatabaseEditor { match field { None => {}, Some(field) => { + if field.is_primary { + return Err(FlowyError::new( + ErrorCode::Internal, + "Can not update primary field's field type", + )); + } + let old_field_type = FieldType::from(field.field_type); let old_type_option = field.get_any_type_option(old_field_type.clone()); let new_type_option = field @@ -312,6 +334,21 @@ impl DatabaseEditor { } pub async fn duplicate_field(&self, view_id: &str, field_id: &str) -> FlowyResult<()> { + let is_primary = self + .database + .lock() + .fields + .get_field(field_id) + .map(|field| field.is_primary) + .unwrap_or(false); + + if is_primary { + return Err(FlowyError::new( + ErrorCode::Internal, + "Can not duplicate primary field", + )); + } + let value = self .database .lock() 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 3b7325f997..89b39f2036 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 @@ -90,7 +90,9 @@ impl CellDataDecoder for CheckboxTypeOption { return Ok(Default::default()); } - self.parse_cell(cell) + let cell = self.parse_cell(cell); + println!("cell: {:?}", cell); + return cell; } fn stringify_cell_data(&self, cell_data: ::CellData) -> String { diff --git a/frontend/rust-lib/flowy-database2/tests/database/field_test/test.rs b/frontend/rust-lib/flowy-database2/tests/database/field_test/test.rs index 94a8bbf437..22837bd9a5 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/field_test/test.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/field_test/test.rs @@ -233,30 +233,6 @@ async fn grid_switch_from_checkbox_to_text_test() { test.run_scripts(scripts).await; } -// Test when switching the current field from Checkbox to Text test -// input: -// "Yes" -> check -// "" -> unchecked -#[tokio::test] -async fn grid_switch_from_text_to_checkbox_test() { - let mut test = DatabaseFieldTest::new().await; - let field = test.get_first_field(FieldType::RichText).clone(); - - let scripts = vec![ - SwitchToField { - field_id: field.id.clone(), - new_field_type: FieldType::Checkbox, - }, - AssertCellContent { - field_id: field.id.clone(), - row_index: 0, - from_field_type: FieldType::RichText, - expected_content: "".to_string(), - }, - ]; - test.run_scripts(scripts).await; -} - // Test when switching the current field from Date to Text test // input: // 1647251762 -> Mar 14,2022 (This string will be different base on current data setting) diff --git a/frontend/rust-lib/flowy-folder2/src/event_handler.rs b/frontend/rust-lib/flowy-folder2/src/event_handler.rs index 3ebc9b82dd..9f9f8dace5 100644 --- a/frontend/rust-lib/flowy-folder2/src/event_handler.rs +++ b/frontend/rust-lib/flowy-folder2/src/event_handler.rs @@ -41,10 +41,14 @@ pub(crate) async fn open_workspace_handler( match params.value { None => Err(FlowyError::workspace_id().context("workspace id should not be empty")), Some(workspace_id) => { - let workspace = folder.open_workspace(&workspace_id).await?; - let views = folder.get_workspace_views(&workspace_id).await?; - let workspace_pb: WorkspacePB = (workspace, views).into(); - data_result_ok(workspace_pb) + if workspace_id.is_empty() { + return Err(FlowyError::workspace_id().context("workspace id should not be empty")); + } else { + let workspace = folder.open_workspace(&workspace_id).await?; + let views = folder.get_workspace_views(&workspace_id).await?; + let workspace_pb: WorkspacePB = (workspace, views).into(); + data_result_ok(workspace_pb) + } }, } } @@ -68,7 +72,7 @@ pub(crate) async fn read_workspaces_handler( } #[tracing::instrument(level = "debug", skip(folder), err)] -pub async fn read_cur_workspace_setting_handler( +pub async fn read_current_workspace_setting_handler( folder: AFPluginState>, ) -> DataResult { let workspace = folder.get_current_workspace().await?; diff --git a/frontend/rust-lib/flowy-folder2/src/event_map.rs b/frontend/rust-lib/flowy-folder2/src/event_map.rs index 4859ad5804..c8e384b3b7 100644 --- a/frontend/rust-lib/flowy-folder2/src/event_map.rs +++ b/frontend/rust-lib/flowy-folder2/src/event_map.rs @@ -13,7 +13,7 @@ pub fn init(folder: Arc) -> AFPlugin { .event(FolderEvent::CreateWorkspace, create_workspace_handler) .event( FolderEvent::GetCurrentWorkspace, - read_cur_workspace_setting_handler, + read_current_workspace_setting_handler, ) .event(FolderEvent::ReadAllWorkspaces, read_workspaces_handler) .event(FolderEvent::OpenWorkspace, open_workspace_handler) diff --git a/frontend/rust-lib/flowy-test/Cargo.toml b/frontend/rust-lib/flowy-test/Cargo.toml index e4fc872442..f0e94b0ba2 100644 --- a/frontend/rust-lib/flowy-test/Cargo.toml +++ b/frontend/rust-lib/flowy-test/Cargo.toml @@ -10,6 +10,7 @@ flowy-core = { path = "../flowy-core" } flowy-user = { path = "../flowy-user"} flowy-net = { path = "../flowy-net"} flowy-folder2 = { path = "../flowy-folder2", features = ["test_helper"] } +flowy-database2 = { path = "../flowy-database2" } lib-dispatch = { path = "../lib-dispatch" } lib-ot = { path = "../../../shared-lib/lib-ot" } lib-infra = { path = "../../../shared-lib/lib-infra" } diff --git a/frontend/rust-lib/flowy-test/src/lib.rs b/frontend/rust-lib/flowy-test/src/lib.rs index 1ec00bbcc7..167ee0ad78 100644 --- a/frontend/rust-lib/flowy-test/src/lib.rs +++ b/frontend/rust-lib/flowy-test/src/lib.rs @@ -5,10 +5,10 @@ use nanoid::nanoid; use parking_lot::RwLock; use flowy_core::{AppFlowyCore, AppFlowyCoreConfig}; -use flowy_folder2::entities::{ - CreateViewPayloadPB, RepeatedViewIdPB, ViewIdPB, ViewPB, WorkspaceSettingPB, -}; +use flowy_database2::entities::*; +use flowy_folder2::entities::*; use flowy_user::entities::{AuthTypePB, UserProfilePB}; +use flowy_user::errors::FlowyError; use crate::event_builder::EventBuilder; use crate::user_event::{async_sign_up, init_user_setting, SignUpContext}; @@ -113,6 +113,210 @@ impl FlowyCoreTest { .parse::() } + pub async fn create_grid(&self, parent_id: &str, name: String, initial_data: Vec) -> ViewPB { + let payload = CreateViewPayloadPB { + parent_view_id: parent_id.to_string(), + name, + desc: "".to_string(), + thumbnail: None, + layout: ViewLayoutPB::Grid, + initial_data, + meta: Default::default(), + set_as_current: true, + }; + EventBuilder::new(self.clone()) + .event(flowy_folder2::event_map::FolderEvent::CreateView) + .payload(payload) + .async_send() + .await + .parse::() + } + + pub async fn get_database(&self, view_id: &str) -> DatabasePB { + EventBuilder::new(self.clone()) + .event(flowy_database2::event_map::DatabaseEvent::GetDatabase) + .payload(DatabaseViewIdPB { + value: view_id.to_string(), + }) + .async_send() + .await + .parse::() + } + + pub async fn get_all_database_fields(&self, view_id: &str) -> RepeatedFieldPB { + EventBuilder::new(self.clone()) + .event(flowy_database2::event_map::DatabaseEvent::GetFields) + .payload(GetFieldPayloadPB { + view_id: view_id.to_string(), + field_ids: None, + }) + .async_send() + .await + .parse::() + } + + pub async fn create_field(&self, view_id: &str, field_type: FieldType) -> FieldPB { + EventBuilder::new(self.clone()) + .event(flowy_database2::event_map::DatabaseEvent::CreateTypeOption) + .payload(CreateFieldPayloadPB { + view_id: view_id.to_string(), + field_type, + type_option_data: None, + }) + .async_send() + .await + .parse::() + .field + } + + pub async fn update_field(&self, changeset: FieldChangesetPB) { + EventBuilder::new(self.clone()) + .event(flowy_database2::event_map::DatabaseEvent::UpdateField) + .payload(changeset) + .async_send() + .await; + } + + pub async fn delete_field(&self, view_id: &str, field_id: &str) -> Option { + EventBuilder::new(self.clone()) + .event(flowy_database2::event_map::DatabaseEvent::DeleteField) + .payload(DeleteFieldPayloadPB { + view_id: view_id.to_string(), + field_id: field_id.to_string(), + }) + .async_send() + .await + .error() + } + + pub async fn update_field_type( + &self, + view_id: &str, + field_id: &str, + field_type: FieldType, + ) -> Option { + EventBuilder::new(self.clone()) + .event(flowy_database2::event_map::DatabaseEvent::UpdateFieldType) + .payload(UpdateFieldTypePayloadPB { + view_id: view_id.to_string(), + field_id: field_id.to_string(), + field_type, + }) + .async_send() + .await + .error() + } + + pub async fn duplicate_field(&self, view_id: &str, field_id: &str) -> Option { + EventBuilder::new(self.clone()) + .event(flowy_database2::event_map::DatabaseEvent::DuplicateField) + .payload(DuplicateFieldPayloadPB { + view_id: view_id.to_string(), + field_id: field_id.to_string(), + }) + .async_send() + .await + .error() + } + + pub async fn create_row( + &self, + view_id: &str, + start_row_id: Option, + data: Option, + ) -> RowPB { + EventBuilder::new(self.clone()) + .event(flowy_database2::event_map::DatabaseEvent::CreateRow) + .payload(CreateRowPayloadPB { + view_id: view_id.to_string(), + start_row_id, + group_id: None, + data, + }) + .async_send() + .await + .parse::() + } + + pub async fn get_row(&self, view_id: &str, row_id: &str) -> RowPB { + EventBuilder::new(self.clone()) + .event(flowy_database2::event_map::DatabaseEvent::GetRow) + .payload(RowIdPB { + view_id: view_id.to_string(), + row_id: row_id.to_string(), + group_id: None, + }) + .async_send() + .await + .parse::() + } + + pub async fn duplicate_row(&self, view_id: &str, row_id: &str) -> Option { + EventBuilder::new(self.clone()) + .event(flowy_database2::event_map::DatabaseEvent::DuplicateRow) + .payload(RowIdPB { + view_id: view_id.to_string(), + row_id: row_id.to_string(), + group_id: None, + }) + .async_send() + .await + .error() + } + + pub async fn update_cell(&self, changeset: CellChangesetPB) -> Option { + EventBuilder::new(self.clone()) + .event(flowy_database2::event_map::DatabaseEvent::UpdateCell) + .payload(changeset) + .async_send() + .await + .error() + } + + pub async fn get_cell(&self, view_id: &str, row_id: &str, field_id: &str) -> CellPB { + EventBuilder::new(self.clone()) + .event(flowy_database2::event_map::DatabaseEvent::GetCell) + .payload(CellIdPB { + view_id: view_id.to_string(), + row_id: row_id.to_string(), + field_id: field_id.to_string(), + }) + .async_send() + .await + .parse::() + } + + pub async fn insert_option( + &self, + view_id: &str, + field_id: &str, + row_id: &str, + name: &str, + ) -> Option { + let option = EventBuilder::new(self.clone()) + .event(flowy_database2::event_map::DatabaseEvent::CreateSelectOption) + .payload(CreateSelectOptionPayloadPB { + field_id: field_id.to_string(), + view_id: view_id.to_string(), + option_name: name.to_string(), + }) + .async_send() + .await + .parse::(); + + EventBuilder::new(self.clone()) + .event(flowy_database2::event_map::DatabaseEvent::InsertOrUpdateSelectOption) + .payload(RepeatedSelectOptionPayload { + view_id: view_id.to_string(), + field_id: field_id.to_string(), + row_id: row_id.to_string(), + items: vec![option], + }) + .async_send() + .await + .error() + } + pub async fn get_view(&self, view_id: &str) -> ViewPB { EventBuilder::new(self.clone()) .event(flowy_folder2::event_map::FolderEvent::ReadView) diff --git a/frontend/rust-lib/flowy-test/tests/database/test.rs b/frontend/rust-lib/flowy-test/tests/database/test.rs index 8b13789179..f1ba92213b 100644 --- a/frontend/rust-lib/flowy-test/tests/database/test.rs +++ b/frontend/rust-lib/flowy-test/tests/database/test.rs @@ -1 +1,291 @@ +use bytes::Bytes; +use flowy_database2::entities::{ + CellChangesetPB, DatabaseLayoutPB, DatabaseViewIdPB, FieldType, SelectOptionCellDataPB, +}; +use flowy_test::event_builder::EventBuilder; +use flowy_test::FlowyCoreTest; +use std::convert::TryFrom; +#[tokio::test] +async fn get_database_id_event_test() { + let test = FlowyCoreTest::new_with_user().await; + let current_workspace = test.get_current_workspace().await.workspace; + let grid_view = test + .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) + .await; + + // The view id can be used to get the database id. + let database_id = EventBuilder::new(test.clone()) + .event(flowy_database2::event_map::DatabaseEvent::GetDatabaseId) + .payload(DatabaseViewIdPB { + value: grid_view.id.clone(), + }) + .async_send() + .await + .parse::() + .value; + + assert_ne!(database_id, grid_view.id); +} + +#[tokio::test] +async fn get_database_event_test() { + let test = FlowyCoreTest::new_with_user().await; + let current_workspace = test.get_current_workspace().await.workspace; + let grid_view = test + .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) + .await; + + let database = test.get_database(&grid_view.id).await; + assert_eq!(database.fields.len(), 3); + assert_eq!(database.rows.len(), 3); + assert_eq!(database.layout_type, DatabaseLayoutPB::Grid); +} + +#[tokio::test] +async fn get_field_event_test() { + let test = FlowyCoreTest::new_with_user().await; + let current_workspace = test.get_current_workspace().await.workspace; + let grid_view = test + .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) + .await; + + let fields = test.get_all_database_fields(&grid_view.id).await.items; + assert_eq!(fields[0].field_type, FieldType::RichText); + assert_eq!(fields[1].field_type, FieldType::SingleSelect); + assert_eq!(fields[2].field_type, FieldType::Checkbox); + assert_eq!(fields.len(), 3); +} + +#[tokio::test] +async fn create_field_event_test() { + let test = FlowyCoreTest::new_with_user().await; + let current_workspace = test.get_current_workspace().await.workspace; + let grid_view = test + .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) + .await; + + test.create_field(&grid_view.id, FieldType::Checkbox).await; + let fields = test.get_all_database_fields(&grid_view.id).await.items; + assert_eq!(fields.len(), 4); + assert_eq!(fields[3].field_type, FieldType::Checkbox); +} + +#[tokio::test] +async fn delete_field_event_test() { + let test = FlowyCoreTest::new_with_user().await; + let current_workspace = test.get_current_workspace().await.workspace; + let grid_view = test + .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) + .await; + + let fields = test.get_all_database_fields(&grid_view.id).await.items; + assert_eq!(fields[0].field_type, FieldType::RichText); + assert_eq!(fields[1].field_type, FieldType::SingleSelect); + assert_eq!(fields[2].field_type, FieldType::Checkbox); + + let error = test.delete_field(&grid_view.id, &fields[1].id).await; + assert!(error.is_none()); + + let fields = test.get_all_database_fields(&grid_view.id).await.items; + assert_eq!(fields.len(), 2); +} + +#[tokio::test] +async fn delete_primary_field_event_test() { + let test = FlowyCoreTest::new_with_user().await; + let current_workspace = test.get_current_workspace().await.workspace; + let grid_view = test + .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) + .await; + + let fields = test.get_all_database_fields(&grid_view.id).await.items; + // the primary field is not allowed to be deleted. + assert!(fields[0].is_primary); + let error = test.delete_field(&grid_view.id, &fields[0].id).await; + assert!(error.is_some()); +} + +#[tokio::test] +async fn update_field_type_event_test() { + let test = FlowyCoreTest::new_with_user().await; + let current_workspace = test.get_current_workspace().await.workspace; + let grid_view = test + .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) + .await; + + let fields = test.get_all_database_fields(&grid_view.id).await.items; + let error = test + .update_field_type(&grid_view.id, &fields[1].id, FieldType::Checklist) + .await; + assert!(error.is_none()); + + let fields = test.get_all_database_fields(&grid_view.id).await.items; + assert_eq!(fields[1].field_type, FieldType::Checklist); +} + +#[tokio::test] +async fn update_primary_field_type_event_test() { + let test = FlowyCoreTest::new_with_user().await; + let current_workspace = test.get_current_workspace().await.workspace; + let grid_view = test + .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) + .await; + + let fields = test.get_all_database_fields(&grid_view.id).await.items; + // the primary field is not allowed to be deleted. + assert!(fields[0].is_primary); + + // the primary field is not allowed to be updated. + let error = test + .update_field_type(&grid_view.id, &fields[0].id, FieldType::Checklist) + .await; + assert!(error.is_some()); +} + +#[tokio::test] +async fn duplicate_field_event_test() { + let test = FlowyCoreTest::new_with_user().await; + let current_workspace = test.get_current_workspace().await.workspace; + let grid_view = test + .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) + .await; + + let fields = test.get_all_database_fields(&grid_view.id).await.items; + // the primary field is not allowed to be updated. + let error = test.duplicate_field(&grid_view.id, &fields[1].id).await; + assert!(error.is_none()); + + let fields = test.get_all_database_fields(&grid_view.id).await.items; + assert_eq!(fields.len(), 4); +} + +#[tokio::test] +async fn duplicate_primary_field_test() { + let test = FlowyCoreTest::new_with_user().await; + let current_workspace = test.get_current_workspace().await.workspace; + let grid_view = test + .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) + .await; + + let fields = test.get_all_database_fields(&grid_view.id).await.items; + // the primary field is not allowed to be duplicated. + let error = test.duplicate_field(&grid_view.id, &fields[0].id).await; + assert!(error.is_some()); +} + +#[tokio::test] +async fn create_row_event_test() { + let test = FlowyCoreTest::new_with_user().await; + let current_workspace = test.get_current_workspace().await.workspace; + let grid_view = test + .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) + .await; + + let _ = test.create_row(&grid_view.id, None, None).await; + let database = test.get_database(&grid_view.id).await; + assert_eq!(database.rows.len(), 4); +} + +#[tokio::test] +async fn duplicate_row_event_test() { + let test = FlowyCoreTest::new_with_user().await; + let current_workspace = test.get_current_workspace().await.workspace; + let grid_view = test + .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) + .await; + let database = test.get_database(&grid_view.id).await; + let error = test + .duplicate_row(&grid_view.id, &database.rows[0].id) + .await; + assert!(error.is_none()); + + let database = test.get_database(&grid_view.id).await; + assert_eq!(database.rows.len(), 4); +} + +#[tokio::test] +async fn update_text_cell_event_test() { + let test = FlowyCoreTest::new_with_user().await; + let current_workspace = test.get_current_workspace().await.workspace; + let grid_view = test + .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) + .await; + let database = test.get_database(&grid_view.id).await; + let fields = test.get_all_database_fields(&grid_view.id).await.items; + + let row_id = database.rows[0].id.clone(); + let field_id = fields[0].id.clone(); + assert_eq!(fields[0].field_type, FieldType::RichText); + + // Update the first cell of the first row. + let error = test + .update_cell(CellChangesetPB { + view_id: grid_view.id.clone(), + row_id: row_id.clone(), + field_id: field_id.clone(), + cell_changeset: "hello world".to_string(), + }) + .await; + assert!(error.is_none()); + + let cell = test.get_cell(&grid_view.id, &row_id, &field_id).await; + let s = String::from_utf8(cell.data).unwrap(); + assert_eq!(s, "hello world"); +} + +#[tokio::test] +async fn update_checkbox_cell_event_test() { + let test = FlowyCoreTest::new_with_user().await; + let current_workspace = test.get_current_workspace().await.workspace; + let grid_view = test + .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) + .await; + let database = test.get_database(&grid_view.id).await; + let fields = test.get_all_database_fields(&grid_view.id).await.items; + + let row_id = database.rows[0].id.clone(); + let field_id = fields[2].id.clone(); + assert_eq!(fields[2].field_type, FieldType::Checkbox); + + for input in vec!["yes", "true", "1"] { + let error = test + .update_cell(CellChangesetPB { + view_id: grid_view.id.clone(), + row_id: row_id.clone(), + field_id: field_id.clone(), + cell_changeset: input.to_string(), + }) + .await; + assert!(error.is_none()); + + let cell = test.get_cell(&grid_view.id, &row_id, &field_id).await; + let output = String::from_utf8(cell.data).unwrap(); + assert_eq!(output, "Yes"); + } +} + +#[tokio::test] +async fn update_single_select_cell_event_test() { + let test = FlowyCoreTest::new_with_user().await; + let current_workspace = test.get_current_workspace().await.workspace; + let grid_view = test + .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) + .await; + let database = test.get_database(&grid_view.id).await; + let fields = test.get_all_database_fields(&grid_view.id).await.items; + let row_id = database.rows[0].id.clone(); + let field_id = fields[1].id.clone(); + assert_eq!(fields[1].field_type, FieldType::SingleSelect); + + let error = test + .insert_option(&grid_view.id, &field_id, &row_id, "task 1") + .await; + assert!(error.is_none()); + + let cell = test.get_cell(&grid_view.id, &row_id, &field_id).await; + let select_option_cell = SelectOptionCellDataPB::try_from(Bytes::from(cell.data)).unwrap(); + + assert_eq!(select_option_cell.options.len(), 1); + assert_eq!(select_option_cell.select_options.len(), 1); +}